/* ============================================ WORKSPACE — Chat (RAG) + Document Viewer with bbox overlay ============================================ */ function WorkspaceScreen({ onNavigate }) { const [input, setInput] = useState(""); const [sending, setSending] = useState(false); const [messages, setMessages] = useState([]); const [citations, setCitations] = useState([]); const [activeIndex, setActiveIndex] = useState(null); const [error, setError] = useState(null); const scrollRef = useRef(null); useEffect(() => { if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight; }, [messages]); // Auto-select citation [1] whenever a new set arrives. useEffect(() => { if (citations.length > 0) setActiveIndex(1); else setActiveIndex(null); }, [citations]); const send = async () => { const prompt = input.trim(); if (!prompt || sending) return; setError(null); setInput(""); setSending(true); setMessages((m) => [...m, { role: "user", text: prompt }, { role: "assistant", text: "" }]); setCitations([]); try { const res = await window.API.chat(prompt); const reader = res.body.getReader(); const decoder = new TextDecoder(); let buf = ""; while (true) { const { done, value } = await reader.read(); if (done) break; buf += decoder.decode(value, { stream: true }); const events = buf.split("\n\n"); buf = events.pop(); for (const evt of events) { if (evt.startsWith("event: done")) continue; if (!evt.startsWith("data: ")) continue; const dataStr = evt.slice(6); if (dataStr === "[DONE]") continue; let payload; try { payload = JSON.parse(dataStr); } catch (_) { continue; } if (payload.citations) { setCitations(payload.citations); } else if (payload.token) { setMessages((m) => { const next = [...m]; const last = next[next.length - 1]; next[next.length - 1] = { ...last, text: last.text + payload.token }; return next; }); } else if (payload.error) { setError(payload.error); } } } } catch (err) { setError(err.message || "Chat failed."); } finally { setSending(false); } }; const reset = () => { setMessages([]); setCitations([]); setActiveIndex(null); setError(null); }; const activeCitation = citations.find((c) => c.index === activeIndex) || null; return (
{/* ===== LEFT: Chat panel ===== */}

Ask the indexed manuals

{messages.length === 0 ? "No messages yet" : `${messages.length} message${messages.length === 1 ? "" : "s"} · ${citations.length} sources in scope`}

{messages.length === 0 && (
Ask a question about your ingested manuals.
Citations appear as clickable pills — click one to see the source page.
)} {messages.map((msg, i) => ( {msg.role === "assistant" ? ( <> {!msg.text && sending && i === messages.length - 1 && } ) : msg.text} ))} {error && (
{error}
)}
{/* Composer */}