import { useEffect, useRef, useState } from "react"; import { artifactsApi, type GenerateBody } from "../api/endpoints"; import type { Artifact, ChatSummary } from "../api/types"; import { SourceFocusFields, sourceSummary, type SourceSelection } from "./SourceFocusFields"; import { ArtifactFeedback } from "./ArtifactFeedback"; import { ShareDialog } from "./ShareDialog"; import { errorMessage, Modal } from "./ui"; /** The looks: black and blue, and white with orbits. The AI composes the page itself in the look chosen * (backend/app/infographics/design.py); the thumbnails are rendered from a sample (backend/scripts/render_theme_thumbnails.py). */ export const INFOGRAPHIC_THEMES = [ { id: "midnight", name: "Midnight", blurb: "Black and blue, white type" }, { id: "paper", name: "Orbit", blurb: "White, black type, blue orbits and stars" }, ] as const; /** The kind of layout to lean towards; "auto" lets the AI choose from the content. */ export const INFOGRAPHIC_STYLES = [ { id: "auto", name: "Auto", blurb: "Let the AI choose" }, { id: "flowchart", name: "Flow chart", blurb: "Steps joined by arrows" }, { id: "timeline", name: "Timeline", blurb: "What happens, in order" }, { id: "compare", name: "Comparison", blurb: "Two sides side by side" }, { id: "hierarchy", name: "Hierarchy", blurb: "A whole and its parts" }, { id: "overview", name: "Overview", blurb: "Key ideas and numbers" }, ] as const; export type InfographicRequest = Omit; export type Layout = "landscape" | "portrait"; type Which = Layout | "both"; /** Downloading an infographic: the server draws each page as a PNG (a few seconds), which is then saved. Lives in the card, * so the viewer can close at once and the card shows how it is going. */ function useInfographicDownload(artifact: Artifact) { const [busy, setBusy] = useState(null); const [done, setDone] = useState(false); const [error, setError] = useState(null); const name = artifact.title.replace(/[^\w\- ]+/g, "").trim().slice(0, 80) || "infographic"; const save = async (layout: Layout) => { const blob = await artifactsApi.image(artifact.id, layout); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `${name} (${layout}).png`; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(url), 10_000); }; const download = async (which: Which) => { if (busy) return; setBusy(which); setDone(false); setError(null); try { if (which === "both") { await save("landscape"); await save("portrait"); } else await save(which); setDone(true); setTimeout(() => setDone(false), 4000); } catch (e) { setError(errorMessage(e)); } finally { setBusy(null); } }; return { busy, done, error, download }; } /** The Download button with its menu: landscape, portrait or both. */ function DownloadMenu({ busy, onPick }: { busy: Which | null; onPick: (which: Which) => void }) { const [open, setOpen] = useState(false); const pick = (which: Which) => { setOpen(false); onPick(which); }; return (
{open && (
)}
); } const PORTRAIT = { w: 1600, h: 2260 }; const LANDSCAPE = { w: 2260, h: 1600 }; /** The infographic's page (HTML from our own template, every word escaped) in a sandboxed frame, scaled to the width it * is given. No scripts run in it. `crop` shows only the top of the page, for previews. */ export function InfographicFrame({ html, title, crop = false, layout = "portrait" }: { html: string; title: string; crop?: boolean; layout?: Layout }) { const { w: PAGE_W, h: PAGE_H } = layout === "landscape" ? LANDSCAPE : PORTRAIT; const box = useRef(null); const [scale, setScale] = useState(0.25); useEffect(() => { const el = box.current; if (!el) return; const fit = () => setScale(el.clientWidth / PAGE_W || 0.25); fit(); if (typeof ResizeObserver === "undefined") return; const ro = new ResizeObserver(fit); ro.observe(el); return () => ro.disconnect(); }, []); return (