import { useCallback, useEffect, useRef, useState, type KeyboardEvent, } from "react"; import { MessageSquareText, Send, Trash2, Brain, Eye, CheckCircle, ChevronDown, ChevronRight, Loader2, X, AlertTriangle, } from "lucide-react"; import Markdown from "react-markdown"; import { cn } from "@/lib/utils"; import { useConversation, type ChatMessage } from "@/hooks/use-conversation"; import { useChat } from "@/hooks/use-chat"; import { useSettings } from "@/providers/settings-provider"; import { useProgressStore } from "@/hooks/use-progress-store"; import { AutoTextarea } from "@/components/ui/textarea"; // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- const MODES = [ { value: "graph-rag" as const, label: "Graph RAG" }, { value: "document-rag" as const, label: "Doc RAG" }, { value: "agent" as const, label: "Agent" }, ]; // --------------------------------------------------------------------------- // Agent phase section (collapsible) // --------------------------------------------------------------------------- function AgentPhaseBlock({ phase, icon, label, content, isActive, }: { phase: string; icon: React.ReactNode; label: string; content: string; isActive: boolean; }) { const [expanded, setExpanded] = useState(false); if (!content && !isActive) return null; const phaseColors: Record = { think: "border-amber-500/30 bg-amber-500/5", observe: "border-sky-500/30 bg-sky-500/5", answer: "border-emerald-500/30 bg-emerald-500/5", }; const badgeColors: Record = { think: "bg-amber-500/20 text-amber-400", observe: "bg-sky-500/20 text-sky-400", answer: "bg-emerald-500/20 text-emerald-400", }; return (
{expanded && content && (

{content}

)}
); } // --------------------------------------------------------------------------- // Single message bubble // --------------------------------------------------------------------------- function MessageBubble({ msg }: { msg: ChatMessage }) { const isUser = msg.role === "user"; const hasAgentPhases = msg.agentPhases != null; const isError = !isUser && msg.content.startsWith("Error:"); return (
{/* Agent phase blocks (only for agent messages) */} {hasAgentPhases && msg.agentPhases && (
} label="Thinking" content={msg.agentPhases.think} isActive={msg.activePhase === "think"} /> } label="Observing" content={msg.agentPhases.observe} isActive={msg.activePhase === "observe"} /> {msg.agentPhases.answer && (
Answer
)}
)} {/* Main content (markdown for assistant, plain for user) */} {isUser ? (

{msg.content}

) : isError ? (

{msg.content}

) : (
{msg.content || (msg.isStreaming ? "" : "(empty)")}
)} {/* Streaming indicator */} {msg.isStreaming && ( )} {/* Token metadata */} {msg.metadata && (
{msg.metadata.model && {msg.metadata.model}} {msg.metadata.inTokens != null && ( in: {msg.metadata.inTokens} )} {msg.metadata.outTokens != null && ( out: {msg.metadata.outTokens} )}
)}
); } // --------------------------------------------------------------------------- // Chat page // --------------------------------------------------------------------------- export default function ChatPage() { const messages = useConversation((s) => s.messages); const input = useConversation((s) => s.input); const chatMode = useConversation((s) => s.chatMode); const setInput = useConversation((s) => s.setInput); const setChatMode = useConversation((s) => s.setChatMode); const clearMessages = useConversation((s) => s.clearMessages); const { submitMessage, cancelRequest } = useChat(); const collection = useSettings((s) => s.settings.collection); const isLoading = useProgressStore((s) => s.isLoading); const scrollRef = useRef(null); // Elapsed time counter while loading const [elapsed, setElapsed] = useState(0); useEffect(() => { if (!isLoading) { setElapsed(0); return; } const interval = setInterval(() => setElapsed((e) => e + 1), 1000); return () => clearInterval(interval); }, [isLoading]); // Auto-scroll to bottom when messages change useEffect(() => { scrollRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); const handleSubmit = useCallback(() => { if (input.trim()) { submitMessage({ input }); } }, [input, submitMessage]); const handleKeyDown = useCallback( (e: KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSubmit(); } }, [handleSubmit], ); return (
{/* Header */}

Chat

{collection}
{/* Mode selector */}
{MODES.map((mode) => ( ))}
{/* Messages */}
{messages.length === 0 && (

Send a message to start a conversation.

Mode: {chatMode}

)} {messages.map((msg) => ( ))}
{/* Loading indicator */} {isLoading && (
Processing... {elapsed}s
)} {/* Input area */}
setInput(e.target.value)} onKeyDown={handleKeyDown} placeholder="Type your message... (Enter to send, Shift+Enter for new line)" aria-label="Chat message" maxRows={6} />
); }