import { useCallback, useEffect, useRef, useState, type FormEvent } from "react"; import { Link, useParams } from "react-router-dom"; import { artifactsApi, notebooksApi, type GenerateBody } from "../api/endpoints"; import { streamSSE } from "../api/stream"; import type { Artifact, ChatSummary, Citation, Notebook } from "../api/types"; import { ArtifactCard, CitationList } from "../components/ArtifactCard"; import { Markdown } from "../components/Markdown"; import { DocPicker } from "../components/DocPicker"; import { TelescopeIcon } from "../components/icons/Icons"; import { Reader } from "../components/Reader"; import { ConfirmButton, errorMessage, formatDate, Loading, Modal } from "../components/ui"; type Turn = { role: "user" | "assistant"; text: string; citations?: Citation[]; status?: string; steps?: string[] }; function StudioPanel({ notebookId, disabled }: { notebookId: string; disabled: boolean }) { const [artifacts, setArtifacts] = useState(null); const [format, setFormat] = useState<"deep_dive" | "brief" | "debate">("deep_dive"); const [minutes, setMinutes] = useState(6); const [style, setStyle] = useState<"short" | "explainer">("short"); const [error, setError] = useState(null); const [busy, setBusy] = useState(false); const [reading, setReading] = useState<{ id: string; start: number; end: number } | null>(null); const load = useCallback(() => notebooksApi.artifacts(notebookId).then(setArtifacts), [notebookId]); useEffect(() => { load(); }, [load]); const make = async (body: Omit) => { setBusy(true); setError(null); try { const a = await artifactsApi.generate({ ...body, notebook_id: notebookId }); setArtifacts((list) => [a, ...(list ?? [])]); } catch (e) { setError(errorMessage(e)); } finally { setBusy(false); } }; const cite = (c: Citation) => c.document_id && setReading({ id: c.document_id, start: c.line_start, end: c.line_end }); return ( ); } export default function NotebookView() { const { id = "" } = useParams(); const [nb, setNb] = useState(null); const [loadError, setLoadError] = useState(null); const [turns, setTurns] = useState([]); const [q, setQ] = useState(""); const [chatId, setChatId] = useState(null); const [chats, setChats] = useState([]); const [busy, setBusy] = useState(false); const [adding, setAdding] = useState(false); const [toAdd, setToAdd] = useState([]); const [addError, setAddError] = useState(null); const [reading, setReading] = useState<{ id: string; start?: number; end?: number } | null>(null); const [editingTitle, setEditingTitle] = useState(false); const [title, setTitle] = useState(""); const bottom = useRef(null); const load = useCallback( () => notebooksApi.get(id).then( (n) => { setNb(n); setTitle(n.title); }, () => setLoadError("Notebook not found."), ), [id], ); const loadChats = useCallback(() => notebooksApi.chats(id).then(setChats), [id]); useEffect(() => { load(); loadChats(); setTurns([]); setChatId(null); }, [load, loadChats]); useEffect(() => { bottom.current?.scrollIntoView({ block: "end" }); }, [turns]); const openChat = async (cid: string) => { setChatId(cid); const msgs = await notebooksApi.messages(cid); setTurns(msgs.map((m) => ({ role: m.role, text: m.text, citations: m.citations }))); }; const ask = async (e: FormEvent) => { e.preventDefault(); const question = q.trim(); if (!question || busy) return; setQ(""); setBusy(true); setTurns((t) => [...t, { role: "user", text: question }, { role: "assistant", text: "", status: "Scanning" }]); const patchLast = (p: Partial) => setTurns((t) => [...t.slice(0, -1), { ...t[t.length - 1], ...p }]); try { await streamSSE( `/api/notebooks/${id}/chat`, (event, data) => { const d = data as Record; if (event === "status") { if (d.chat_id) setChatId(d.chat_id as string); patchLast({ status: d.message as string }); } else if (event === "step") { setTurns((t) => { const last = t[t.length - 1]; return [...t.slice(0, -1), { ...last, status: d.message as string, steps: [...(last.steps ?? []), d.message as string] }]; }); } else if (event === "error") { patchLast({ text: d.message as string, status: undefined }); } else if (event === "answer") { patchLast({ text: d.text as string, citations: d.citations as Citation[], status: undefined }); } }, { method: "POST", body: JSON.stringify({ question, chat_id: chatId }) }, ); } catch { patchLast({ text: "Lost signal. Try again.", status: undefined }); } finally { setBusy(false); loadChats(); } }; if (loadError) return

{loadError}

; if (!nb) return ; const cite = (c: Citation) => c.document_id ? setReading({ id: c.document_id, start: c.line_start, end: c.line_end }) : window.open(c.url, "_blank"); return (
{chatId && ( { await notebooksApi.removeChat(chatId); setChatId(null); setTurns([]); loadChats(); }} > Delete chat )}
{turns.length === 0 && (

Ask anything about these posts. Answers cite the passage they came from.

)} {turns.map((t, i) => (
{t.steps && t.steps.length > 0 && (
{t.status ? `${t.status}...` : `${t.steps.length} steps through the archive`}
    {t.steps.map((s, j) => (
  1. {s}
  2. ))}
)} {t.status && !t.steps?.length &&

{t.status}...

} {t.text && (t.role === "assistant" ? ( ) : (

{t.text}

))} {t.citations && }
))}
setQ(e.target.value)} placeholder={nb.documents.length ? "What have I written about pricing?" : "Add posts to start asking"} aria-label="Ask your notebook" disabled={!nb.documents.length} />
{adding && ( setAdding(false)} wide> d.id)} /> {addError &&

{addError}

}
)} {reading && setReading(null)} />}
); }