feat(web): Scope B B1 — 多章工作流链前端 UI(发起/轮询/裁决续跑)

链页 app/projects/[id]/chains(RSC fetchProject)+ ChainPage/ChainStarter/
ChainProgress/ChainAdjudication;lib/chain 纯函数(result 收窄/相位映射/resume 组装)。
复用 useJobPoll 轮询、ConflictCard + decisions.ts 裁决、normalizeConflicts/latestReview、
friendlyError;nav 加 chains 入口。awaiting(interrupt)→ 读该章冲突渲 ConflictCard →
POST .../chains/runs/{job_id}/resume 续跑。gen:api 纳入 chains run/resume。
守 #5(链暂停=等裁决,token/正文不入 job result)。
This commit is contained in:
Yaojia Wang
2026-06-23 19:21:00 +02:00
parent 416caed56b
commit a2dcd89881
11 changed files with 874 additions and 4 deletions

View File

@@ -0,0 +1,63 @@
"use client";
import type { ChainPhase, ChainResultView } from "@/lib/chain/chain";
interface ChainProgressProps {
phase: ChainPhase;
progress: number;
result: ChainResultView;
}
const PHASE_LABEL: Record<ChainPhase, string> = {
running: "运行中",
awaiting: "等待裁决",
completed: "已完成",
failed: "已失败",
};
// 链进度视图:相位徽标 + 进度条 + 已写章号清单token/正文绝不展示result 只含元数据)。
export function ChainProgress({ phase, progress, result }: ChainProgressProps) {
return (
<section
className="flex flex-col gap-3 rounded border border-line bg-panel p-5"
aria-label="链进度"
>
<div className="flex items-center justify-between gap-3">
<h2 className="font-serif text-lg text-ink"></h2>
<span
className="rounded bg-cinnabar/10 px-2 py-0.5 text-xs text-cinnabar"
aria-live="polite"
>
{PHASE_LABEL[phase]}
</span>
</div>
<div
className="h-2 w-full overflow-hidden rounded bg-bg"
role="progressbar"
aria-valuenow={progress}
aria-valuemin={0}
aria-valuemax={100}
>
<div
className="h-full bg-cinnabar transition-all"
style={{ width: `${progress}%` }}
/>
</div>
<p className="text-sm text-ink-soft">
{result.written.length}
{result.written.length > 0 ? (
<span className="ml-1 font-mono text-ink">
{result.written.join("、")}
</span>
) : null}
</p>
{phase === "awaiting" && result.awaitingChapter !== null ? (
<p className="text-sm text-conflict">
{result.awaitingChapter}
</p>
) : null}
</section>
);
}