import { useEffect, useState, type ReactNode } from "react"; import { Link } from "react-router-dom"; import { artifactsApi, videosApi } from "../api/endpoints"; import type { Artifact, Citation } from "../api/types"; import { useJob } from "../hooks/useJob"; import { Body } from "./ArtifactCard"; import { SlideDeckViewer } from "./SlideDeck"; import { TrashIcon } from "./icons/Icons"; import { errorMessage, formatDate, formatDuration, JobProgress } from "./ui"; function statusOf(a: Artifact, running: boolean): { label: string; tone: "done" | "busy" | "failed" } { if (a.status === "failed") return { label: "Failed", tone: "failed" }; if (running || a.status !== "ready") return { label: "Generating", tone: "busy" }; return { label: "Ready", tone: "done" }; } /** One generated item as a full-width row (like the Videos list): its type, title, status and date. A video opens its * editor, a finished slide deck its viewer (to read, edit, present or download it); anything else expands in place * to its player or image. */ export function ArtifactRow({ 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 [openRow, setOpenRow] = useState(false); const [armed, setArmed] = useState(false); 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 st = statusOf(artifact, running); const isEditorVideo = artifact.provider === "blog2video"; const isDeck = artifact.type === "slide_deck" && artifact.status === "ready"; const [viewing, setViewing] = useState(false); useEffect(() => { if (!armed) return; const t = setTimeout(() => setArmed(false), 4000); return () => clearTimeout(t); }, [armed]); async function remove() { if (!armed) return setArmed(true); setArmed(false); try { // A blog2video video is also removed from blog2video. await (isEditorVideo ? videosApi.remove(artifact.id) : artifactsApi.remove(artifact.id)); onRemoved?.(artifact.id); } catch (e) { setError(errorMessage(e)); } } async function retry() { setError(null); try { setArtifact(await artifactsApi.retry(artifact.id)); } catch (e) { setError(errorMessage(e)); } } const main = ( <> {artifact.type_label} {/* A deck's title starts "Slide deck: ", which its chip already says */} {artifact.type === "slide_deck" ? artifact.title.replace(/^Slide deck: /, "") : artifact.title} {artifact.created_at && {formatDate(artifact.created_at, true)}} {artifact.content.duration_s ? {formatDuration(artifact.content.duration_s)} : null} {artifact.content.aspect_ratio && {artifact.content.aspect_ratio === "portrait" ? "Portrait" : "Landscape"}} ); return (
  • {isEditorVideo ? ( {main} ) : isDeck ? ( ) : ( )} {isDeck && actions?.(artifact)} {artifact.status === "failed" && ( )} {onRemoved && ( )}
    {running && job && } {error &&

    {error}

    } {viewing && ( setViewing(false)} onSaved={(a) => { setArtifact(a); onChanged?.(a); }} onDelete={async () => { await artifactsApi.remove(artifact.id); setViewing(false); onRemoved?.(artifact.id); }} /> )} {openRow && !isEditorVideo && !isDeck && (
    {artifact.status === "failed" && artifact.content.error &&

    {artifact.content.error}

    }
    {actions?.(artifact)} {artifact.download_url && Download}
    )}
  • ); }