"use client"; import { useEffect, useState } from "react"; import { adjudicateLine, correctLineAttribution, getCanonScene, listCanonScenes, listCanonWorks, listCharacters, listContradictions, reattemptQuarantined, type CanonWork, type CharacterSummary, type SceneContradiction, type SceneLine, } from "../../lib/api"; interface LineRow extends SceneLine { sceneId: string; } function characterLabel(characters: CharacterSummary[], characterId: string | null): string { if (!characterId) return "(無)"; const found = characters.find((c) => c.id === characterId); return found?.aliases[0]?.formalName ?? characterId; } // M-11 待裁決畫面:列出隔離區台詞與待人工確認的矛盾,提供逐句裁決/歸屬更正/指紋精煉後重試。 export function CanonReviewView() { const [works, setWorks] = useState([]); const [workId, setWorkId] = useState(""); const [characters, setCharacters] = useState([]); const [quarantined, setQuarantined] = useState([]); const [confirmed, setConfirmed] = useState([]); const [contradictions, setContradictions] = useState([]); const [selection, setSelection] = useState>({}); const [pending, setPending] = useState(false); const [error, setError] = useState(null); useEffect(() => { void listCanonWorks() .then(setWorks) .catch(() => setError("無法連線到 api 服務,作品清單暫時無法顯示")); }, []); async function refreshWork(targetWorkId: string) { if (!targetWorkId) return; setError(null); try { const [allCharacters, scenes, contradictionList] = await Promise.all([ listCharacters(), listCanonScenes(targetWorkId), listContradictions(targetWorkId), ]); setCharacters(allCharacters.filter((c) => c.workId === targetWorkId)); setContradictions(contradictionList); const details = await Promise.all(scenes.map((scene) => getCanonScene(scene.id))); const nextQuarantined: LineRow[] = []; const nextConfirmed: LineRow[] = []; for (const scene of details) { for (const line of scene.lines) { const row = { ...line, sceneId: scene.id }; if (line.status === "QUARANTINED") { nextQuarantined.push(row); } else if (line.status === "CONFIRMED") { nextConfirmed.push(row); } } } setQuarantined(nextQuarantined); setConfirmed(nextConfirmed); } catch { setError("待裁決資料查詢失敗,請確認 api 服務是否已啟動"); } } useEffect(() => { void refreshWork(workId); // eslint-disable-next-line react-hooks/exhaustive-deps }, [workId]); async function handleAdjudicate(lineId: string) { const speakerCharacterId = selection[lineId]; if (!speakerCharacterId || pending) return; setPending(true); setError(null); try { await adjudicateLine(lineId, speakerCharacterId); await refreshWork(workId); } catch { setError("裁決失敗,請確認 api 服務是否已啟動"); } finally { setPending(false); } } async function handleCorrect(lineId: string) { const newSpeakerCharacterId = selection[lineId]; if (!newSpeakerCharacterId || pending) return; setPending(true); setError(null); try { await correctLineAttribution(lineId, newSpeakerCharacterId); await refreshWork(workId); } catch { setError("歸屬更正失敗,請確認 api 服務是否已啟動"); } finally { setPending(false); } } async function handleReattempt(sceneId: string) { if (pending) return; setPending(true); setError(null); try { const result = await reattemptQuarantined(sceneId); setError(null); await refreshWork(workId); // eslint-disable-next-line no-console console.info(`指紋精煉後重試釋放了 ${result.released} 句`); } catch { setError("重試失敗,請確認 api 服務是否已啟動"); } finally { setPending(false); } } const sceneIdsWithQuarantine = Array.from(new Set(quarantined.map((line) => line.sceneId))); return (
{error && (

{error}

)} {workId && ( <>

隔離區台詞(QUARANTINED)

{sceneIdsWithQuarantine.map((sceneId) => ( ))}
{quarantined.length === 0 && (

目前沒有隔離中的台詞。

)} {quarantined.map((line) => (

{line.rawText}

信心分數:{(line.confidence * 100).toFixed(0)}%.證據來源: {line.hasExplicitMarker ? `明示標記→${characterLabel(characters, line.markerCharacterId)}` : "推論(無明示標記)"}

))}

已歸屬台詞(可更正)

{confirmed.length === 0 && (

目前沒有已歸屬的台詞。

)} {confirmed.map((line) => (

{line.rawText}

目前歸屬:{characterLabel(characters, line.speakerCharacterId)}

))}

待人工確認的矛盾(PENDING_HUMAN_REVIEW)

{contradictions.filter((c) => c.status === "PENDING_HUMAN_REVIEW").length === 0 && (

目前沒有待確認的矛盾。

)} {contradictions .filter((c) => c.status === "PENDING_HUMAN_REVIEW") .map((contradiction) => (

{contradiction.eventLabel}.{contradiction.factKey}

{contradiction.description}

))}
)}
); }