import { useCallback, useEffect, useState } from "react"; import { Link, useNavigate, useParams } from "react-router-dom"; import { artifactsApi, notebooksApi, reportsApi } from "../api/endpoints"; import type { Artifact, Citation, ReportSuggestion } from "../api/types"; import { ArtifactFeedback } from "../components/ArtifactFeedback"; import { ReportAddDialog } from "../components/ReportAddDialog"; import { ShareDialog } from "../components/ShareDialog"; import { Reader } from "../components/Reader"; import { reportMarkdown, ReportView } from "../components/ReportView"; import { ConfirmButton, errorMessage, JobProgress, Loading } from "../components/ui"; import { useJob } from "../hooks/useJob"; /** One report, full page: /app/reports/:id. */ export default function ReportPage() { const { id = "" } = useParams(); const navigate = useNavigate(); const [artifact, setArtifact] = useState(null); const [error, setError] = useState(null); const [adding, setAdding] = useState<{ suggestion: ReportSuggestion; existing: Artifact[]; loading: boolean } | null>(null); const [sharing, setSharing] = useState(false); const [addFailed, setAddFailed] = useState(null); const [reading, setReading] = useState<{ id: string; start: number; end: number } | null>(null); const [removingId, setRemovingId] = useState(null); const load = useCallback(() => artifactsApi.get(id).then(setArtifact, () => setError("Report not found.")), [id]); useEffect(() => { void load(); }, [load]); const [addingId, setAddingId] = useState(null); // the suggestion being built (its card shows a loader) const job = useJob(artifact?.job, () => { void load(); setAddingId(null); }); // If the live updates drop, the loader still ends once the job's own state is final. Only the job that adds a visual counts: the // report's own, long finished job is what the page still holds in the moment after Add is clicked. useEffect(() => { if (addingId && job && job.kind === "report_block" && job.id === artifact?.job?.id && (job.status === "done" || job.status === "failed")) { setAddingId(null); void load(); } }, [job, addingId, artifact?.job?.id, load]); // An infographic added to the report is drawn after the report page shows it: look again until it is ready. const drawing = (artifact?.content.blocks ?? []).some((b: { type: string; status?: string }) => b.type === "infographic" && ["pending", "generating", "rendering"].includes(b.status ?? "")); useEffect(() => { if (!drawing) return; const t = setTimeout(() => void load(), 2500); return () => clearTimeout(t); }, [drawing, artifact, load]); if (error) return

{error}

; if (!artifact) return ; const running = job && (job.status === "queued" || job.status === "running"); const adding_ = !!addingId || !!(running && job?.kind === "report_block"); // a visual is being added: one at a time // Which suggestion's card shows the loader: the one just clicked, or, after a reload or a dropped update, the one the running job is adding. const buildingId = addingId ?? (running && job?.kind === "report_block" ? (job.params.suggestion_id as string | undefined) ?? null : null); const blockFailed = job?.kind === "report_block" && job.status === "failed" ? job.error : null; const back = artifact.notebook_id ? `/app/notebooks/${artifact.notebook_id}` : "/app/archive"; const download = () => { const url = URL.createObjectURL(new Blob([reportMarkdown(artifact.content)], { type: "text/markdown" })); const a = document.createElement("a"); a.href = url; a.download = `${artifact.title.slice(0, 60)}.md`; a.click(); URL.revokeObjectURL(url); }; const startAdd = async (s: ReportSuggestion) => { setAddFailed(null); setAdding({ suggestion: s, existing: [], loading: true }); // The ones already made from this notebook (or, for a report with no notebook, anywhere), to offer as an alternative // to making a new one. const all = artifact.notebook_id ? await notebooksApi.artifacts(artifact.notebook_id).catch(() => [] as Artifact[]) : await artifactsApi.list({ type: s.kind, status: "ready", limit: 50 }).then((p) => p.items).catch(() => [] as Artifact[]); setAdding((cur) => cur && cur.suggestion.id === s.id ? { ...cur, loading: false, existing: all.filter((a) => a.type === s.kind && a.status === "ready") } : cur); }; // Add closes the dialog at once and the suggestion's own card shows the loader, down in the report where the visual will appear. const add = async (existingId: string | null, theme?: string) => { if (!adding) return; const { suggestion } = adding; setAddFailed(null); setAddingId(suggestion.id); setAdding(null); try { setArtifact(await reportsApi.addBlock(artifact.id, { kind: suggestion.kind, after_block_id: suggestion.after_block_id, suggestion_id: suggestion.id, brief: suggestion.brief, existing_artifact_id: existingId ?? undefined, theme, })); } catch (e) { setAddingId(null); setAddFailed(errorMessage(e)); } }; const remove = async (blockId: string) => { setRemovingId(blockId); try { setArtifact(await reportsApi.removeBlock(artifact.id, blockId)); } catch (e) { setError(errorMessage(e)); } finally { setRemovingId(null); } }; const cite = (c: Citation) => c.document_id && setReading({ id: c.document_id, start: c.line_start, end: c.line_end }); return (
Back

{artifact.content.format === "interactive" ? "Interactive report" : "Report"}

{artifact.title}

{artifact.status === "ready" && (
{ try { await artifactsApi.remove(artifact.id); navigate(back); } catch (e) { setError(errorMessage(e)); } }}>Delete
)}
{running && job && job.kind !== "report_block" && } {artifact.status === "failed" &&

{(artifact.content.error as string | undefined) ?? job?.error ?? "The report could not be made."}

} {(addFailed ?? blockFailed) &&

{addFailed ?? blockFailed}

} {artifact.status === "ready" && ( )} {sharing && setSharing(false)} />} {adding && setAdding(null)} onAdd={add} />} {reading && setReading(null)} />}
); }