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 { suggestTemplates } from "../hooks/reportSuggestions"; 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. // The server reads one kind of source for them: the posts when there are any, else the chats (as for the focus cards). const suggestFor = sel?.ready && !sel.loading ? (sel.request.document_ids?.length ? { ...sel.request, chat_ids: undefined } : { ...sel.request, document_ids: undefined, notebook_id: undefined }) : null; const requestKey = suggestFor ? JSON.stringify(suggestFor) : ""; const templateIdRef = useRef(templateId); templateIdRef.current = templateId; const haveSuggestions = useRef(false); haveSuggestions.current = suggested !== null; useEffect(() => { if (!requestKey || !suggestFor) return; let live = true; // Ticking or unticking a post changes the key. The templates already on screen stay put (no blank loader) while the new ones are // fetched, a moment after the last tick so a run of ticks makes one request; a suggested template the reader has picked is not // swapped from under them. Only the very first fetch shows the loader. const t = setTimeout(() => { suggestTemplates(suggestFor) .then((templates) => live && setSuggested((prev) => (prev && templateIdRef.current.startsWith("suggested-") ? prev : templates))) .catch(() => live && setSuggested((prev) => prev ?? [])); }, haveSuggestions.current ? 600 : 0); return () => { live = false; clearTimeout(t); }; // 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