import { useEffect, useRef, useState } from "react"; import { Send, Loader2, MessageSquareText } from "lucide-react"; import { Button } from "@/components/ui/button"; import MessageBubble from "@/components/MessageBubble"; import { streamChat } from "@/lib/stream"; import type { ChatMessage, Chunk } from "@/types"; export default function ChatWindow() { const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); const [isPending, setIsPending] = useState(false); const bottomRef = useRef(null); const abortRef = useRef(null); useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages, isPending]); // Cancel any in-flight stream on unmount useEffect(() => { return () => abortRef.current?.abort(); }, []); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); const question = input.trim(); if (!question || isPending) return; // Cancel previous stream if still running abortRef.current?.abort(); const controller = new AbortController(); abortRef.current = controller; setInput(""); setMessages((prev) => [ ...prev, { role: "user", content: question }, { role: "assistant", content: "", sources: [] }, ]); setIsPending(true); let sources: Chunk[] = []; await streamChat( question, { onSources: (s) => { sources = s; }, onToken: (token) => { setMessages((prev) => { const next = [...prev]; const last = next[next.length - 1]; if (last?.role === "assistant") next[next.length - 1] = { ...last, content: last.content + token }; return next; }); }, onUsage: (tokens, cached) => { setMessages((prev) => { const next = [...prev]; const last = next[next.length - 1]; if (last?.role === "assistant") next[next.length - 1] = { ...last, tokens, cached }; return next; }); }, onDone: () => { setMessages((prev) => { const next = [...prev]; const last = next[next.length - 1]; if (last?.role === "assistant") next[next.length - 1] = { ...last, sources }; return next; }); setIsPending(false); }, onError: (message) => { setMessages((prev) => { const next = [...prev]; const last = next[next.length - 1]; if (last?.role === "assistant") next[next.length - 1] = { ...last, content: "", error: message }; return next; }); setIsPending(false); }, }, controller.signal, ); setIsPending(false); } return (
{messages.length === 0 && (

Ask anything.

Upload a PDF from the sidebar, then ask questions about its contents.

)} {messages.map((msg, i) => ( ))} {isPending && messages[messages.length - 1]?.content === "" && !messages[messages.length - 1]?.error && (
)}
setInput(e.target.value)} placeholder="Ask a question about your documents…" disabled={isPending} className="flex-1 rounded-lg border border-border bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-accent/50 disabled:opacity-50" />
); }