229 lines
9.3 KiB
TypeScript
229 lines
9.3 KiB
TypeScript
"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>
|
||
);
|
||
}
|