"use client"; import { useCallback, useEffect, useState } from "react"; import { ConflictCard } from "@/components/review/ConflictCard"; import { useToast } from "@/components/Toast"; import { api } from "@/lib/api/client"; import { buildResumeRequest } from "@/lib/chain/chain"; import { friendlyFromApiError } from "@/lib/errors/messages"; import type { ReviewConflict } from "@/lib/review/sse"; import { allResolved, emptyDecisions, setNote, setVerdict, unresolvedIndices, type DecisionDraft, type Verdict, } from "@/lib/review/decisions"; import { latestReview, normalizeConflicts } from "@/lib/review/history"; interface ChainAdjudicationProps { projectId: string; jobId: string; // interrupt 命中的章号(读其最近审稿冲突供裁决)。 chapterNo: number; // 续跑发起后回调(父层重启轮询)。 onResumed: () => void; } type LoadState = "loading" | "ready" | "error"; // 链裁决面板(净新但复用满):读 awaiting 章最近审稿 → 渲 ConflictCard(每冲突一张,复用审稿页 // 组件 + decisions.ts 纯逻辑)→ 全决后 POST resume(buildResumeRequest 复用 accept 裁决组装)。 // 链无在手草稿正文 → 无段内定位(snippet=null/locatable=false),裁决纯靠 suggestion + 补丁对。 export function ChainAdjudication({ projectId, jobId, chapterNo, onResumed, }: ChainAdjudicationProps) { const toast = useToast(); const [loadState, setLoadState] = useState("loading"); const [conflicts, setConflicts] = useState([]); const [drafts, setDrafts] = useState([]); const [resuming, setResuming] = useState(false); useEffect(() => { let active = true; setLoadState("loading"); void (async () => { try { const { data, error } = await api.GET( "/projects/{project_id}/chapters/{chapter_no}/reviews", { params: { path: { project_id: projectId, chapter_no: chapterNo }, }, }, ); if (!active) return; if (error || !data) { setLoadState("error"); return; } const next = normalizeConflicts(latestReview(data.reviews)); setConflicts(next); setDrafts(emptyDecisions(next.length)); setLoadState("ready"); } catch { if (active) setLoadState("error"); } })(); return () => { active = false; }; }, [projectId, chapterNo]); const onVerdict = useCallback((index: number, verdict: Verdict): void => { setDrafts((prev) => setVerdict(prev, index, verdict)); }, []); const onNote = useCallback((index: number, note: string): void => { setDrafts((prev) => setNote(prev, index, note)); }, []); const onResume = useCallback(async (): Promise => { if (!allResolved(drafts)) { toast("仍有未裁决的冲突,请先逐条裁决。", "error"); return; } setResuming(true); try { const { error } = await api.POST( "/projects/{project_id}/chains/runs/{job_id}/resume", { params: { path: { project_id: projectId, job_id: jobId } }, body: buildResumeRequest(drafts), }, ); if (error) { toast(friendlyFromApiError(error).text, "error"); setResuming(false); return; } onResumed(); } catch { toast("续跑请求异常,请检查网络。", "error"); setResuming(false); } }, [drafts, projectId, jobId, onResumed, toast]); if (loadState === "loading") { return

加载第 {chapterNo} 章冲突…

; } if (loadState === "error") { return (

加载第 {chapterNo} 章审稿冲突失败,请刷新重试。

); } const pending = unresolvedIndices(drafts); return (

第 {chapterNo} 章冲突裁决({conflicts.length})

{conflicts.length === 0 ? (

该章无未决冲突,可直接续跑。

) : (
    {conflicts.map((conflict, i) => { const draft = drafts[i] ?? { verdict: null, note: "" }; return ( {}} /> ); })}
)} {pending.length > 0 ? (

还有 {pending.length} 条冲突未裁决。

) : null}
); }