import { useEffect, useMemo, useRef, useState } from "react"; import { reportsApi, type GenerateBody } from "../api/endpoints"; import type { ChatSummary, ReportTemplate } from "../api/types"; import { CheckIcon, SparkleIcon } from "./icons/Icons"; import { SourceFocusFields, type SourceSelection } from "./SourceFocusFields"; import { Modal } from "./ui"; export type ReportRequest = Omit; type Format = "document" | "interactive"; const fill = (prompt: string, about: string) => prompt.replaceAll("{about}", about || "the selected sources"); /** Create report: step 1 picks the format (and, for a document, a template); step 2 shows the instructions the writer * will get, already filled in from the topic and the sources, for the user to edit. */ export function ReportDialog({ notebookId, notebookTitle, chats, currentChatId, busy, error, onClose, onCreate }: { notebookId: string; notebookTitle?: string; chats: ChatSummary[]; currentChatId: string | null; busy: boolean; error: string | null; onClose: () => void; onCreate: (body: ReportRequest) => void; }) { const [step, setStep] = useState<1 | 2>(1); const [format, setFormat] = useState("document"); const [catalog, setCatalog] = useState<{ templates: ReportTemplate[]; interactive: ReportTemplate } | null>(null); const [templateId, setTemplateId] = useState("briefing"); const [sel, setSel] = useState(null); const [text, setText] = useState(""); const [suggested, setSuggested] = useState(null); // null: still on their way const dirty = useRef(false); // the user has edited the instructions: they are never replaced by a refill useEffect(() => { reportsApi.templates().then(setCatalog).catch(() => undefined); }, []); const template = format === "interactive" ? catalog?.interactive : catalog?.templates.find((t) => t.id === templateId) ?? suggested?.find((t) => t.id === templateId); const about = ""; // no topic box: the instructions text says what the report is about, so the template says "the selected sources" const prefilled = useMemo(() => (template ? fill(template.prompt, about) : ""), [template, about]); // The suggested templates belong to the chosen sources (and topic): asked for once the choice is complete. const requestKey = sel?.ready && !sel.loading ? JSON.stringify(sel.request) : ""; useEffect(() => { if (!requestKey || !sel) return; let live = true; setSuggested(null); reportsApi.suggest(sel.request) .then((r) => live && setSuggested(r.templates)) .catch(() => live && setSuggested([])); return () => { live = false; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [requestKey]); // A suggested template that the new suggestions no longer include falls back to the first fixed one. useEffect(() => { if (suggested && templateId.startsWith("suggested-") && !suggested.some((t) => t.id === templateId)) choose("document", "briefing"); // eslint-disable-next-line react-hooks/exhaustive-deps }, [suggested]); // Whatever the user has not typed follows the template, the topic and the sources. useEffect(() => { if (!dirty.current) setText(prefilled); }, [prefilled]); const choose = (f: Format, id = templateId) => { dirty.current = false; setFormat(f); setTemplateId(id); }; const ready = !!sel?.ready && !sel.loading && !!template; const needsText = !text.trim(); const generate = () => sel && template && onCreate({ ...sel.request, report_format: format, template_id: template.id, instructions: text.trim(), }); const editor = (
Describe the report you want to create