import { useState, type ReactNode } from "react"; import { Link } from "react-router-dom"; import { artifactsApi, videosApi } from "../api/endpoints"; import type { Artifact, Citation, Segment } from "../api/types"; import { useJob } from "../hooks/useJob"; import { Markdown } from "./Markdown"; import { MindMapExplorer } from "./MindMap"; import { FlashcardsBody } from "./Flashcards"; import { InfographicBody } from "./Infographic"; import { QuizBody } from "./Quiz"; import { StudyOverlay } from "./StudyOverlay"; import { ReportBody } from "./ReportBody"; import { ConfirmButton, errorMessage, formatDate, formatDuration, JobProgress, StatusPill } from "./ui"; export function CitationList({ citations, onCite, collapsible = false, }: { citations: Citation[]; onCite?: (c: Citation) => void; collapsible?: boolean; }) { if (!citations.length) return null; // The same source is listed once: its title, then one clickable number for each place it was cited. const groups: { key: string; title: string; cites: Citation[] }[] = []; for (const c of [...citations].sort((x, y) => x.marker - y.marker)) { const key = c.document_id || c.path || c.title; const group = groups.find((g) => g.key === key); if (group) group.cites.push(c); else groups.push({ key, title: c.title, cites: [c] }); } const lines = (c: Citation) => `lines ${c.line_start}${c.line_end > c.line_start ? ` to ${c.line_end}` : ""}`; const list = ( ); if (!collapsible) return list; return (
Citations {citations.length} {list}
); } export function Transcript({ segments, onSeek }: { segments: Segment[]; onSeek: (t: number) => void }) { return (
    {segments.map((s, i) => (
  1. {s.speaker === "host_b" ? "B" : "A"} {s.text} {s.sources.length > 0 && [{s.sources.map((r) => r.title ?? r.path).join("; ")}]}
  2. ))}
); } /** Cards whose buttons (Open, Share, Delete...) sit together on one line, all the same size. */ const INLINE_ACTIONS = ["summary", "report", "quiz", "flashcards", "mind_map", "launch_kit", "infographic"]; /** A summary in its card is a title and a way in: it opens full screen (like the Mind Constellation) and can be minimised. */ function SummaryBody({ artifact, onCite }: { artifact: Artifact; onCite?: (c: Citation) => void }) { const [open, setOpen] = useState(false); const c = artifact.content; const themes = (c.themes as string[] | undefined) ?? []; return (
{themes.length > 0 &&

{themes.length} themes · {(c.citations ?? []).length} citations

} {open && ( setOpen(false)}> {themes.length > 0 && (
{themes.map((t) => {t})}
)}
)}
); } /** The player, image or text of a finished artifact (nothing while it is still being made). */ export function Body({ artifact, onCite }: { artifact: Artifact; onCite?: (c: Citation) => void }) { const [showTranscript, setShowTranscript] = useState(false); const [audioEl, setAudioEl] = useState(null); const [exploring, setExploring] = useState(false); const c = artifact.content; if (artifact.status !== "ready") return null; switch (artifact.type) { case "summary": return ; case "audio_overview": return (
{artifact.url &&
); case "video": if (artifact.provider === "blog2video") { return (
{artifact.url &&
); } return artifact.url ? (
) : null; case "quote_card": return artifact.url ? (
{c.quote
) : null; case "carousel": return (
{artifact.slide_urls.map((u, i) => ( {`Slide ))}
); case "mind_map": return (

{c.node_count ?? 0} stars across {c.post_count ?? 0} posts.

{exploring && setExploring(false)} />}
); case "quiz": return ; case "flashcards": return ; case "report": return ; case "infographic": return ; case "launch_kit": return (

{c.claims?.length ?? 0} claims, {c.hooks?.length ?? 0} hooks, posts for 4 platforms, SEO pack and carousel.

Open Launch Kit
); default: return null; } } /** Any generated artifact: live progress while it runs, then the right player or preview. */ export function ArtifactCard({ artifact: initial, onCite, onRemoved, onChanged, actions, }: { artifact: Artifact; onCite?: (c: Citation) => void; onRemoved?: (id: string) => void; onChanged?: (a: Artifact) => void; actions?: (a: Artifact) => ReactNode; }) { const [artifact, setArtifact] = useState(initial); const [error, setError] = useState(null); const job = useJob(artifact.job, () => { artifactsApi.get(artifact.id).then((a) => { setArtifact(a); onChanged?.(a); }); }); const running = job && (job.status === "queued" || job.status === "running"); const failedMessage = artifact.status === "failed" ? (artifact.content.error as string | undefined) ?? job?.error : null; return (

{artifact.type_label}

{artifact.title}

{formatDate(artifact.created_at, true)} {artifact.content.duration_s ? ` · ${formatDuration(artifact.content.duration_s)}` : ""}

{running && job && } {failedMessage &&

{failedMessage}

} {error &&

{error}

}
); }