"use client"; import { useEffect, useRef, useState } from "react"; import { FileText, RefreshCw, X } from "lucide-react"; import { errorMessage, getArtifactDetail } from "@/lib/api"; import type { ArtifactDetailProjection } from "@/lib/types"; import { TabList } from "@/components/ui/TabList"; import { ScriptTable } from "@/components/artifact/ScriptTable"; import { ScriptElementTable } from "@/components/artifact/ScriptElementTable"; import { StatusBadge } from "@/components/ui/Badges"; type ViewerTab = "script" | "elements"; interface Props { buildId: string; snapshotRef: string; onClose: () => void; } export function ScriptArtifactViewer({ buildId, snapshotRef, onClose }: Props) { const dialog = useRef(null); const [detail, setDetail] = useState(); const [loading, setLoading] = useState(true); const [error, setError] = useState(); const [tab, setTab] = useState("script"); const [selectedElement, setSelectedElement] = useState(); const [reloadKey, setReloadKey] = useState(0); useEffect(() => { const node = dialog.current; if (node && !node.open && typeof node.showModal === "function") node.showModal(); const cancel = (event: Event) => { event.preventDefault(); onClose(); }; node?.addEventListener("cancel", cancel); const previousOverflow = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { node?.removeEventListener("cancel", cancel); document.body.style.overflow = previousOverflow; }; }, [onClose]); useEffect(() => { const controller = new AbortController(); setLoading(true); setError(undefined); void getArtifactDetail(buildId, snapshotRef, controller.signal) .then(setDetail) .catch((reason) => { if (!controller.signal.aborted) setError(errorMessage(reason)); }) .finally(() => { if (!controller.signal.aborted) setLoading(false); }); return () => controller.abort(); }, [buildId, snapshotRef, reloadKey]); const table = detail?.scriptTable; const context = detail?.artifactContext; const candidate = context?.type === "candidate" || snapshotRef.startsWith("artifact:branch:"); const roundOutput = context?.type === "round" || snapshotRef.startsWith("artifact:round:"); const title = candidate ? context?.roundIndex ? `第 ${context.roundIndex} 轮 · 方案 ${context.branchId ?? "-"}` : `候选方案 ${context?.branchId ?? snapshotRef.split(":").at(-1) ?? "-"}` : roundOutput ? `第 ${context?.roundIndex ?? snapshotRef.split(":").at(-1) ?? "-"} 轮 · 本轮产出` : `Run #${buildId} · 当前保存版本`; const versionNotice = candidate ? context ? candidateVersionNotice(context) : "正在读取候选版本信息" : roundOutput ? context?.note || "正在确认本轮产出版本" : "当前保存的最终主脚本"; const canRefresh = candidate && ["open", "parked"].includes(String(context?.branchStatus || "").toLowerCase()); const openElement = (id: number | string) => { setSelectedElement(id); setTab("elements"); }; return event.stopPropagation()}>
{candidate ? "完整候选脚本" : roundOutput ? "本轮产出脚本表" : "当前最终主脚本"}{candidate && context?.branchStatus ? : null}

{title}

{table ?

{table.counts.paragraphs} 个段落 · {table.counts.elements} 个元素 · {table.counts.links} 条关联

: null}
{canRefresh ? : null}

{versionNotice}

{loading ?

{candidate ? "正在读取候选脚本…" : "正在读取当前主脚本…"}

: null} {!loading && error ?
{candidate ? "完整候选脚本暂时无法读取" : "完整主脚本暂时无法读取"}

{error}

: null} {!loading && !error && table && tab === "script" ? table.paragraphs.length ? :
{candidate ? "当前候选版本还没有段落" : "当前主脚本还没有段落"}

可以切换到“脚本元素”查看已经保存的元素。

: null} {!loading && !error && table && tab === "elements" ? table.elements.length ? :
{candidate ? "当前候选版本还没有元素" : "当前主脚本还没有元素"}

创作脚本段落仍可在另一个页签查看。

: null}
; } export function candidateVersionNotice(context: ArtifactDetailProjection["artifactContext"]) { const status = String(context.branchStatus || "").toLowerCase(); if (context.exactness === "historical-snapshot") { if (status === "discarded") return "未采用前冻结保存的候选版本"; if (status === "merged") return "采用前冻结保存的候选版本"; return "处置前冻结保存的候选版本"; } if (status === "parked") return "根据当前主脚本和暂存改动组合;不是暂存当时的冻结历史"; if (status === "open") return "显示截至本次读取已写入的内容;方案仍可能继续更新"; return context.note || "根据当前主脚本和分支改动组合的候选版本"; }