Files
chat-frontend/frontend/components/Layout.tsx

229 lines
9.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useEffect } from "react";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
interface Chat {
id: string;
title: string;
model_alias: string;
updated_at: string;
}
interface User {
login: string;
}
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "http://127.0.0.1:18000";
// Global modal state since Sidebar and Topbar both might need logic, but let's keep it simple
export default function Layout({ children, chats: initialChats, user }: { children: React.ReactNode, chats: Chat[], user: User }) {
const [chats, setChats] = useState<Chat[]>(initialChats);
const [models, setModels] = useState<{alias: string, name: string}[]>([]);
const pathname = usePathname();
const router = useRouter();
const [isModalOpen, setIsModalOpen] = useState(false);
const [newChatTitle, setNewChatTitle] = useState("");
const [newChatModel, setNewChatModel] = useState("");
useEffect(() => {
fetch(`${API_BASE_URL}/api/models`)
.then(res => res.json())
.then(data => {
setModels(data);
if (data.length > 0) setNewChatModel(data[0].alias);
})
.catch(console.error);
}, []);
const handleLogout = async () => {
await fetch(`${API_BASE_URL}/api/auth/logout`, { method: "POST" });
window.location.href = "/";
};
const createChat = async (e: React.FormEvent) => {
e.preventDefault();
if (!newChatTitle.trim() || !newChatModel) return;
try {
const res = await fetch(`${API_BASE_URL}/api/chats`, {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ title: newChatTitle, model_alias: newChatModel }),
});
if (res.ok) {
const chat = await res.json();
setChats([chat, ...chats]);
setIsModalOpen(false);
setNewChatTitle("");
router.push(`/chat/${chat.id}`);
}
} catch (err) {
console.error("Failed to create chat", err);
}
};
return (
<div style={{ display: 'flex', height: '100vh', flexDirection: 'column', backgroundColor: 'var(--bg-color)' }}>
{/* Top Bar */}
<header style={{
height: 56,
backgroundColor: 'var(--panel-bg)',
borderBottom: '1px solid var(--border-color)',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '0 24px',
flexShrink: 0
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, fontSize: 18, fontWeight: 600, color: 'var(--primary)' }}>
<Link href="/" style={{ color: 'inherit', textDecoration: 'none' }}>AI Chat MVP</Link>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<div style={{ fontSize: 14, color: 'var(--text-main)', display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{ width: 28, height: 28, borderRadius: '50%', backgroundColor: '#e2e8f0', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 12, fontWeight: 600, color: 'var(--text-main)' }}>
{user.login.charAt(0).toUpperCase()}
</div>
{user.login}
</div>
<button onClick={handleLogout} style={{ background: 'none', border: 'none', color: 'var(--text-muted)', fontSize: 14, display: 'flex', alignItems: 'center', gap: 4 }}>
Выход
</button>
</div>
</header>
{/* Main Container */}
<div style={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
{/* Sidebar */}
<aside style={{
width: 'var(--sidebar-w)',
backgroundColor: 'var(--panel-bg)',
borderRight: '1px solid var(--border-color)',
display: 'flex',
flexDirection: 'column',
zIndex: 10
}}>
<div style={{ padding: '16px 16px 8px 16px' }}>
<button
onClick={() => setIsModalOpen(true)}
style={{
width: '100%',
padding: '10px 16px',
backgroundColor: 'var(--bg-color)',
border: '1px solid var(--border-color)',
borderRadius: '8px',
color: 'var(--text-main)',
fontSize: 14,
fontWeight: 500,
display: 'flex',
alignItems: 'center',
gap: 8,
transition: 'background 0.2s',
justifyContent: 'center'
}}
onMouseOver={e => e.currentTarget.style.backgroundColor = '#e8eaed'}
onMouseOut={e => e.currentTarget.style.backgroundColor = 'var(--bg-color)'}
>
<span style={{ fontSize: 18, color: 'var(--primary)', fontWeight: 300 }}>+</span> Новый чат
</button>
</div>
<div style={{ padding: '16px', fontSize: 13, fontWeight: 600, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
Мои чаты
</div>
<div style={{ flex: 1, overflowY: 'auto' }}>
{chats.map(chat => (
<Link key={chat.id} href={`/chat/${chat.id}`} style={{
display: 'block',
padding: '12px 16px',
fontSize: 14,
textDecoration: 'none',
color: 'var(--text-main)',
backgroundColor: pathname === `/chat/${chat.id}` ? 'var(--bg-color)' : 'transparent',
borderLeft: pathname === `/chat/${chat.id}` ? '3px solid var(--primary)' : '3px solid transparent',
}}
onMouseOver={e => { if (pathname !== `/chat/${chat.id}`) e.currentTarget.style.backgroundColor = 'rgba(0,0,0,0.02)' }}
onMouseOut={e => { if (pathname !== `/chat/${chat.id}`) e.currentTarget.style.backgroundColor = 'transparent' }}
>
<div style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', fontWeight: pathname === `/chat/${chat.id}` ? 500 : 400 }}>
{chat.title}
</div>
<div style={{ fontSize: 12, color: 'var(--text-muted)', marginTop: 4 }}>
{chat.model_alias}
</div>
</Link>
))}
</div>
</aside>
{/* Main Content Area */}
<main style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden', position: 'relative' }}>
{children}
</main>
</div>
{/* New Chat Modal */}
{isModalOpen && (
<div style={{
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
backgroundColor: 'rgba(0,0,0,0.4)', zIndex: 100,
display: 'flex', alignItems: 'center', justifyContent: 'center'
}}>
<div style={{
backgroundColor: '#fff', borderRadius: 16, width: '100%', maxWidth: 480,
boxShadow: '0 10px 40px rgba(0,0,0,0.1)', padding: 32
}}>
<h2 style={{ fontSize: 20, margin: '0 0 24px 0', fontWeight: 600 }}>Новый чат</h2>
<form onSubmit={createChat} style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
<div>
<label style={{ display: 'block', fontSize: 14, marginBottom: 8, color: 'var(--text-main)', fontWeight: 500 }}>Название чата</label>
<input
type="text"
value={newChatTitle}
onChange={e => setNewChatTitle(e.target.value)}
placeholder="Введите название..."
required
style={{ width: '100%', padding: '10px 14px', borderRadius: 8, border: '1px solid var(--border-color)', fontSize: 14, outline: 'none' }}
/>
</div>
<div>
<label style={{ display: 'block', fontSize: 14, marginBottom: 8, color: 'var(--text-main)', fontWeight: 500 }}>Модель</label>
<select
value={newChatModel}
onChange={e => setNewChatModel(e.target.value)}
style={{ width: '100%', padding: '10px 14px', borderRadius: 8, border: '1px solid var(--border-color)', fontSize: 14, outline: 'none', backgroundColor: '#fff' }}
>
{models.map(m => (
<option key={m.alias} value={m.alias}>{m.name}</option>
))}
</select>
</div>
<div style={{ display: 'flex', gap: 12, marginTop: 12, justifyContent: 'flex-end' }}>
<button
type="button"
onClick={() => setIsModalOpen(false)}
style={{ padding: '10px 20px', backgroundColor: 'transparent', border: '1px solid var(--border-color)', borderRadius: 8, fontSize: 14, fontWeight: 500, color: 'var(--text-main)' }}
>
Отмена
</button>
<button
type="submit"
style={{ padding: '10px 20px', backgroundColor: 'var(--primary)', border: 'none', borderRadius: 8, fontSize: 14, fontWeight: 500, color: '#fff' }}
>
Создать чат
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}