'use client'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { PanelLeftOpen } from 'lucide-react'; import MessageBubble from './MessageBubble'; import EmptyState from './EmptyState'; import ChatInput from './ChatInput'; import { useChatStore } from '@/store/chatStore'; import { useSSE } from '@/hooks/useSSE'; import { useAuth } from '@/hooks/useAuth'; import { authHeaders } from '@/lib/auth-client'; import { parseSlashCommand } from '@/lib/slashCommands'; import type { Message } from '@/types/chat'; export default function ChatWindow() { const { user } = useAuth(); const { conversations, currentConversationId, model, temperature, topK, sidebarOpen, toggleSidebar, createConversation, newConversation, appendMessage, updateMessage, setTemperature, setTopK, } = useChatStore(); const [draft, setDraft] = useState(''); const [streamingMsgId, setStreamingMsgId] = useState(null); const streamingBufferRef = useRef(''); const scrollRef = useRef(null); const active = useMemo( () => conversations.find((c) => c.id === currentConversationId) ?? null, [conversations, currentConversationId], ); const messages: Message[] = active?.messages ?? []; const isEmpty = messages.length === 0; const scrollToBottom = useCallback(() => { const el = scrollRef.current; if (el) el.scrollTop = el.scrollHeight; }, []); useEffect(() => { scrollToBottom(); }, [messages.length, streamingMsgId, scrollToBottom]); const { start, stop, isStreaming } = useSSE('/api/chat/stream', { onToken: (token) => { streamingBufferRef.current += token; if (streamingMsgId && currentConversationId) { updateMessage(currentConversationId, streamingMsgId, streamingBufferRef.current); } }, onDone: () => { setStreamingMsgId(null); streamingBufferRef.current = ''; }, onError: (err) => { console.error('[chat] stream error:', err); if (streamingMsgId && currentConversationId) { updateMessage( currentConversationId, streamingMsgId, `Error: ${err.message}. Using mock responses requires only the frontend; cloud streaming requires CHAT_API_URL.`, ); } setStreamingMsgId(null); streamingBufferRef.current = ''; }, }); const ensureConversation = useCallback(async () => { if (currentConversationId) return currentConversationId; // Try creating via the API first const apiId = await createConversation(); if (apiId) return apiId; // Fallback to local-only conversation (mock mode) return newConversation(); }, [currentConversationId, createConversation, newConversation]); const handleSend = useCallback( async (rawInput?: string) => { const text = (rawInput ?? draft).trim(); if (!text || isStreaming) return; const convId = await ensureConversation(); const slash = parseSlashCommand(text, { temperature, topK }); if (slash.handled) { setDraft(''); if (slash.setTemperature !== undefined) setTemperature(slash.setTemperature); if (slash.setTopK !== undefined) setTopK(slash.setTopK); if (slash.clear) { const apiId = await createConversation(); if (!apiId) newConversation(); return; } if (slash.consoleMessage) { appendMessage(convId, { role: 'console', content: slash.consoleMessage }); } return; } setDraft(''); appendMessage(convId, { role: 'user', content: text }); const assistantId = appendMessage(convId, { role: 'assistant', content: '' }); setStreamingMsgId(assistantId); streamingBufferRef.current = ''; const history = [ ...(useChatStore.getState().conversations.find((c) => c.id === convId)?.messages ?? []), ] .filter((m) => m.role === 'user' || m.role === 'assistant') .slice(0, -1) .map((m) => ({ role: m.role, content: m.content })); await start({ messages: history, model, temperature, topK, conversationId: convId, auth: authHeaders(), }); }, [ draft, isStreaming, ensureConversation, temperature, topK, appendMessage, model, setTemperature, setTopK, createConversation, newConversation, start, ], ); return (
{!sidebarOpen && ( )}

Chat Completions

{model}
{user?.name ? `Hi, ${user.name.split(' ')[0]}` : ''}
{isEmpty ? ( handleSend(p)} /> ) : (
{messages.map((m) => ( ))}
)}
handleSend()} onStop={stop} isStreaming={isStreaming} />
); }