"use client"; import { useState, useRef, useEffect } from "react"; import type { DocInfo } from "@/app/dashboard/page"; import { api } from "@/lib/api"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import MessageBubble from "./MessageBubble"; import SourceCard from "./SourceCard"; import { Send, Loader2, Trash2, MessageSquare } from "lucide-react"; export interface SourceChunk { text: string; filename: string; page: number; score: number; confidence: number; } export interface ChatMsg { id: string; role: "user" | "assistant"; content: string; sources: SourceChunk[]; isStreaming?: boolean; } interface Props { activeDoc: DocInfo | null; onCitationClick: (page: number) => void; } export default function ChatPanel({ activeDoc, onCitationClick }: Props) { const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); const [streaming, setStreaming] = useState(false); const textareaRef = useRef(null); const bottomRef = useRef(null); const prevDocId = useRef(null); useEffect(() => { const textarea = textareaRef.current; if (!textarea) return; textarea.style.height = "auto"; const computedMaxHeight = Number.parseFloat( window.getComputedStyle(textarea).maxHeight ); const maxHeight = Number.isFinite(computedMaxHeight) ? computedMaxHeight : textarea.scrollHeight; const nextHeight = Math.min(textarea.scrollHeight, maxHeight); textarea.style.height = `${nextHeight}px`; textarea.style.overflowY = textarea.scrollHeight > maxHeight ? "auto" : "hidden"; }, [input]); // Auto-scroll to bottom whenever messages change useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); // Load history on doc change useEffect(() => { if (!activeDoc) { prevDocId.current = null; setMessages([]); return; } if (activeDoc.id === prevDocId.current) return; const documentId = activeDoc.id; prevDocId.current = documentId; setMessages([]); let cancelled = false; api .get<{ messages: Array<{ id: string; role: string; content: string; sources?: SourceChunk[] }> }>( `/api/v1/chat/history/${documentId}` ) .then((data) => { if (cancelled || prevDocId.current !== documentId) return; setMessages( data.messages.map((m) => ({ id: m.id, role: m.role as "user" | "assistant", content: m.content, sources: m.sources || [], })) ); }) .catch(() => { if (cancelled || prevDocId.current !== documentId) return; setMessages([]); }); return () => { cancelled = true; }; }, [activeDoc]); const handleSend = async () => { if (!input.trim() || streaming) return; const question = input.trim(); setInput(""); // Add user message const userMsg: ChatMsg = { id: `user-${Date.now()}`, role: "user", content: question, sources: [], }; setMessages((prev) => [...prev, userMsg]); // Add placeholder assistant message const assistantId = `assistant-${Date.now()}`; const assistantMsg: ChatMsg = { id: assistantId, role: "assistant", content: "", sources: [], isStreaming: true, }; setMessages((prev) => [...prev, assistantMsg]); setStreaming(true); try { const stream = api.streamPost("/api/v1/chat/ask/stream", { question, document_id: activeDoc?.id || null, }); for await (const event of stream) { if (event.type === "token") { setMessages((prev) => prev.map((m) => m.id === assistantId ? { ...m, content: m.content + (event.data as string) } : m ) ); } else if (event.type === "sources") { setMessages((prev) => prev.map((m) => m.id === assistantId ? { ...m, sources: event.data as SourceChunk[] } : m ) ); } else if (event.type === "error") { setMessages((prev) => prev.map((m) => m.id === assistantId ? { ...m, content: `Error: ${event.data}`, isStreaming: false } : m ) ); } else if (event.type === "done") { setMessages((prev) => prev.map((m) => m.id === assistantId ? { ...m, isStreaming: false } : m ) ); } } } catch (err) { setMessages((prev) => prev.map((m) => m.id === assistantId ? { ...m, content: `Failed to get response: ${err instanceof Error ? err.message : "Unknown error"}`, isStreaming: false, } : m ) ); } finally { setStreaming(false); } }; const handleClear = async () => { if (!activeDoc || !confirm("Clear all chat history for this document?")) return; try { await api.delete(`/api/v1/chat/history/${activeDoc.id}`); setMessages([]); } catch { // silently fail } }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSend(); } }; return (
{/* ── Chat Messages ──────────────────────────── */}
{messages.length === 0 ? (

{activeDoc ? "Ask about your document" : "Select a document"}

{activeDoc ? `"${activeDoc.original_name}" is ready. Ask any question and get cited answers.` : "Upload and select a document from the sidebar to start chatting."}

) : (
{messages.map((msg) => (
{msg.role === "assistant" && msg.sources.length > 0 && (
)}
))}
)}
{/* ── Input Area ─────────────────────────────── */}