"use client"; import { useEffect, useRef, useState } from "react"; import { ArrowUp, FileText, ListChecks, Quote, ScanSearch, Sparkles, SquareStack, StopCircle, } from "lucide-react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import { streamChat } from "@/lib/api"; import type { Citation, DocumentDetail } from "@/lib/types"; interface Msg { role: "user" | "assistant"; content: string; citations?: Citation[]; } const SUGGESTIONS = [ { icon: SquareStack, text: "Summarize this document" }, { icon: ScanSearch, text: "What type of document is this?" }, { icon: ListChecks, text: "Extract all the key fields" }, { icon: Sparkles, text: "Are there any missing fields or anomalies?" }, ]; export default function ChatPanel({ documentId, detail, provider, onCitationClick, }: { documentId: string | null; detail: DocumentDetail | null; provider: string | null; onCitationClick: (c: Citation) => void; }) { const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); const [busy, setBusy] = useState(false); const abortRef = useRef(null); const scrollRef = useRef(null); const taRef = useRef(null); useEffect(() => setMessages([]), [documentId]); useEffect(() => { scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight, behavior: "smooth", }); }, [messages, busy]); // autosize composer useEffect(() => { const ta = taRef.current; if (!ta) return; ta.style.height = "auto"; ta.style.height = Math.min(ta.scrollHeight, 140) + "px"; }, [input]); const send = async (text: string) => { const q = text.trim(); if (!q || busy) return; const history = [...messages, { role: "user" as const, content: q }]; setMessages([...history, { role: "assistant", content: "" }]); setInput(""); setBusy(true); abortRef.current = new AbortController(); await streamChat( { document_id: documentId, provider, messages: history.map((m) => ({ role: m.role, content: m.content })), }, { onToken: (t) => setMessages((prev) => { const next = [...prev]; const last = next[next.length - 1]; next[next.length - 1] = { ...last, content: last.content + t }; return next; }), onCitations: (c) => setMessages((prev) => { const next = [...prev]; next[next.length - 1] = { ...next[next.length - 1], citations: c }; return next; }), onDone: () => setBusy(false), onError: (msg) => setMessages((prev) => { const next = [...prev]; const last = next[next.length - 1]; next[next.length - 1] = { role: "assistant", content: (last.content || "") + `\n\n_⚠️ ${msg}_`, }; return next; }), }, abortRef.current.signal, ); setBusy(false); }; const stop = () => { abortRef.current?.abort(); setBusy(false); }; const ready = detail?.status === "ready"; const lastIsStreaming = busy; return (
{/* messages */}
{messages.length === 0 && (

{documentId ? "Ask about this document" : "Upload a document to begin"}

{documentId ? "I can summarize, classify, extract structured data, and answer questions — each grounded with clickable citations." : "Drop a PDF, Word file, or image in the left rail. I'll parse it, then you can chat about it."}

{documentId && (
{SUGGESTIONS.map((s) => { const Icon = s.icon; return ( ); })}
)} {documentId && !ready && (

Processing… you can chat once it's ready.

)}
)} {messages.map((m, i) => { const streaming = lastIsStreaming && i === messages.length - 1; return (
{m.role === "user" ? (
{m.content}
) : (
{!m.content && streaming ? ( ) : (
{m.content}
)}
{m.citations && m.citations.length > 0 && (
Sources: {m.citations.map((c, ci) => ( ))}
)}
)}
); })}
{/* composer */}