You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

137 lines
5.0 KiB

'use client';
import { streamChat } from '@/lib/api';
import { useState, useRef, useEffect, useCallback } from 'react';
import { Send, Loader2, Bot, User, BookOpen } from 'lucide-react';
interface Message {
role: 'user' | 'assistant';
content: string;
sources?: { title: string; path: string; score: number }[];
}
export default function ChatPage() {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState('');
const [streaming, setStreaming] = useState(false);
const cancelRef = useRef<(() => void) | null>(null);
const bottomRef = useRef<HTMLDivElement>(null);
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
const sendMessage = useCallback(async () => {
const text = input.trim();
if (!text || streaming) return;
setInput('');
const userMsg: Message = { role: 'user', content: text };
setMessages((prev) => [...prev, userMsg]);
setStreaming(true);
const assistantMsg: Message = { role: 'assistant', content: '', sources: [] };
setMessages((prev) => [...prev, assistantMsg]);
cancelRef.current = streamChat(
text,
5,
(token) => {
setMessages((prev) => {
const next = [...prev];
const last = next[next.length - 1];
next[next.length - 1] = { ...last, content: last.content + token };
return next;
});
},
(sources) => {
setMessages((prev) => {
const next = [...prev];
next[next.length - 1] = { ...next[next.length - 1], sources };
return next;
});
},
() => setStreaming(false),
);
}, [input, streaming]);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
};
return (
<div className="max-w-3xl mx-auto flex flex-col h-[calc(100vh-5rem)]">
<h1 className="text-2xl font-bold text-slate-900 mb-4">AI Chat</h1>
<div className="flex-1 overflow-y-auto space-y-4 pr-1 mb-4">
{messages.length === 0 && (
<div className="text-center text-slate-400 py-16">
<Bot size={48} className="mx-auto mb-3 opacity-30" />
<p>Ask anything about your knowledge base</p>
</div>
)}
{messages.map((msg, i) => (
<div key={i} className={`flex gap-3 ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
{msg.role === 'assistant' && (
<div className="w-8 h-8 rounded-full bg-brain-600 flex items-center justify-center shrink-0 mt-1">
<Bot size={16} className="text-white" />
</div>
)}
<div className={`max-w-[80%] ${msg.role === 'user' ? 'order-1' : ''}`}>
<div className={`rounded-2xl px-4 py-3 text-sm leading-relaxed whitespace-pre-wrap
${msg.role === 'user'
? 'bg-brain-600 text-white rounded-tr-none'
: 'bg-white border border-slate-200 text-slate-800 rounded-tl-none'
}`}>
{msg.content}
{msg.role === 'assistant' && streaming && i === messages.length - 1 && (
<span className="inline-block w-1.5 h-4 bg-brain-500 animate-pulse ml-0.5 rounded" />
)}
</div>
{msg.sources && msg.sources.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{msg.sources.map((src, si) => (
<span key={si} className="text-xs bg-slate-100 text-slate-600 px-2 py-0.5 rounded-full flex items-center gap-1">
<BookOpen size={10} />
{src.title}
</span>
))}
</div>
)}
</div>
{msg.role === 'user' && (
<div className="w-8 h-8 rounded-full bg-slate-200 flex items-center justify-center shrink-0 mt-1">
<User size={16} className="text-slate-600" />
</div>
)}
</div>
))}
<div ref={bottomRef} />
</div>
<div className="flex gap-2">
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
disabled={streaming}
placeholder="Ask your second brain... (Enter to send, Shift+Enter for newline)"
rows={2}
className="flex-1 px-4 py-2.5 border border-slate-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-brain-500 resize-none text-sm disabled:opacity-60"
/>
<button
onClick={sendMessage}
disabled={streaming || !input.trim()}
className="px-4 bg-brain-600 text-white rounded-xl hover:bg-brain-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center"
>
{streaming ? <Loader2 size={20} className="animate-spin" /> : <Send size={20} />}
</button>
</div>
</div>
);
}

Powered by TurnKey Linux.