// PURPOSE: Export bundle flow (spec §3.4, §8). Request an export of the whole // estate or one corpus; the app polls getExport and feeds jobs back in // (the timer lives in the app, per the polling contract). Download on // ready, error verbatim on failure. Props in, callbacks out. // =========================================================================== import { useState, type FC, type ReactNode } from "react"; import { Archive, CheckCircle2, Download, Loader2, XCircle } from "lucide-react"; import type { Collection, ExportJob, ExportScope } from "./types"; import { Spinner, cn, formatBytes, formatRelative } from "./_internal"; // ---- ExportJobRow --------------------------------------------------------- export const ExportJobRow: FC<{ job: ExportJob; onDownload?: (job: ExportJob) => void; className?: string }> = ({ job, onDownload, className, }) => { const running = job.status === "pending" || job.status === "running"; return (

{job.scope === "all" ? "Whole knowledge base" : job.scope}

{running ? "Building…" : job.status === "ready" ? `${job.object_count ?? 0} items · ${formatBytes(job.byte_size)} · ${formatRelative(job.updated_at)}` : job.error || "Failed"}

{job.status === "ready" && onDownload && ( )} {running && }
); }; const StatusIcon: FC<{ status: ExportJob["status"] }> = ({ status }) => { if (status === "ready") return ; if (status === "failed") return ; return ; }; // ---- ExportPanel ---------------------------------------------------------- export interface ExportPanelProps { /** Corpuses the caller may export (for the scope picker). */ collections: Collection[]; jobs: ExportJob[]; busy?: boolean; onRequest: (scope: ExportScope) => void; onDownload?: (job: ExportJob) => void; intro?: ReactNode; className?: string; } export const ExportPanel: FC = ({ collections, jobs, busy, onRequest, onDownload, intro, className }) => { const [scope, setScope] = useState("all"); return (

Export a bundle

{intro ?? ( <> A self-contained zip — your originals, extracted text, catalog cards, and claims, in open formats. Citations resolve inside the bundle, so it stays readable with no service in the loop. It's yours to keep. )}

{jobs.length > 0 && (

Exports

{jobs.map((j) => ( ))}
)}
); };