import { useEffect, useRef, useState } from "react"; import { supabase } from "@/integrations/supabase/client"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { useToast } from "@/hooks/use-toast"; import { Loader2, Send, Sparkles, BookOpen, AlertCircle } from "lucide-react"; import MarkdownView from "@/components/MarkdownView"; import { embedChunks, streamChat, type Citation } from "@/lib/pipeline"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; type ChatMessage = { id: string; role: "user" | "assistant"; content: string; citations?: Citation[]; pending?: boolean; }; export default function ChatPanel({ documentId, noteReady, }: { documentId: string; noteReady: boolean }) { const { toast } = useToast(); const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); const [sending, setSending] = useState(false); const [chunkCount, setChunkCount] = useState(null); const [indexing, setIndexing] = useState(false); const scrollRef = useRef(null); const autoIndexedRef = useRef(null); // Load history + chunk count on mount. useEffect(() => { let mounted = true; (async () => { const [{ data: msgs }, { count }] = await Promise.all([ supabase .from("chat_messages") .select("id,role,content,created_at") .eq("document_id", documentId) .order("created_at"), supabase .from("document_chunks") .select("id", { count: "exact", head: true }) .eq("document_id", documentId), ]); if (!mounted) return; setMessages( (msgs ?? []).map((m) => ({ id: m.id, role: m.role as "user" | "assistant", content: m.content, })), ); setChunkCount(count ?? 0); })(); return () => { mounted = false; }; }, [documentId]); // Auto-index once notes are ready. useEffect(() => { if (!noteReady) return; if (chunkCount === null) return; if (chunkCount > 0) return; if (autoIndexedRef.current === documentId) return; if (indexing) return; autoIndexedRef.current = documentId; void runIndex(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [noteReady, chunkCount, documentId]); // Autoscroll on new content. useEffect(() => { scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight, behavior: "smooth" }); }, [messages]); const runIndex = async () => { setIndexing(true); try { const r = await embedChunks(documentId); setChunkCount(r.chunks ?? 0); if (!r.cached) toast({ title: "Document indexed", description: `${r.chunks} passages ready for chat.` }); } catch (e: unknown) { const msg = e instanceof Error ? e.message : String(e); toast({ title: "Indexing failed", description: msg, variant: "destructive" }); } finally { setIndexing(false); } }; const send = async () => { const text = input.trim(); if (!text || sending) return; setInput(""); const userMsg: ChatMessage = { id: crypto.randomUUID(), role: "user", content: text }; const assistantId = crypto.randomUUID(); const assistantMsg: ChatMessage = { id: assistantId, role: "assistant", content: "", pending: true }; setMessages((prev) => [...prev, userMsg, assistantMsg]); setSending(true); try { await streamChat({ documentId, message: text, onCitations: (cites) => { setMessages((prev) => prev.map((m) => (m.id === assistantId ? { ...m, citations: cites } : m)), ); }, onDelta: (chunk) => { setMessages((prev) => prev.map((m) => m.id === assistantId ? { ...m, content: m.content + chunk, pending: false } : m, ), ); }, }); } catch (e: unknown) { const msg = e instanceof Error ? e.message : String(e); setMessages((prev) => prev.map((m) => m.id === assistantId ? { ...m, content: `_Error: ${msg}_`, pending: false } : m, ), ); toast({ title: "Chat failed", description: msg, variant: "destructive" }); } finally { setSending(false); } }; if (!noteReady) { return (

Chat unavailable

Generate notes first — chat needs the document to be processed.

); } if (chunkCount === null) { return (
); } if (chunkCount === 0) { return (

Index this document for chat

We'll split it into searchable passages so the assistant can cite the source.

); } return (
{messages.length === 0 && (
Ask anything about this document — answers will cite the source passages.
)} {messages.map((m) => ( ))}