+
+
链进度
+
+ {PHASE_LABEL[phase]}
+
+
+
+
+
+
+ 已完成 {result.written.length} 章
+ {result.written.length > 0 ? (
+
+ ({result.written.join("、")})
+
+ ) : null}
+
+ {phase === "awaiting" && result.awaitingChapter !== null ? (
+
+ 第 {result.awaitingChapter} 章存在未决冲突,请在下方逐条裁决后续跑。
+
+ ) : null}
+
+ );
+}
diff --git a/apps/web/components/chain/ChainStarter.tsx b/apps/web/components/chain/ChainStarter.tsx
new file mode 100644
index 0000000..5ae691d
--- /dev/null
+++ b/apps/web/components/chain/ChainStarter.tsx
@@ -0,0 +1,80 @@
+"use client";
+
+import { useState } from "react";
+
+interface ChainStarterProps {
+ // 发起一条链(起始章 + 章数);父层负责 POST run + 轮询。
+ onStart: (startChapterNo: number, count: number) => void;
+ // 链正在运行/等待裁决时禁用(避免重复发起)。
+ disabled: boolean;
+}
+
+const DEFAULT_START = 1;
+const DEFAULT_COUNT = 3;
+const MAX_COUNT = 50;
+
+// 链发起表单(净新):选起始章号 + 连续写几章 → 调 onStart。
+// count 1..50(对齐后端 ChainRunRequest Field 约束;前端先拦一道,越界后端 422 兜底)。
+export function ChainStarter({ onStart, disabled }: ChainStarterProps) {
+ const [start, setStart] = useState(String(DEFAULT_START));
+ const [count, setCount] = useState(String(DEFAULT_COUNT));
+
+ const startNo = Number.parseInt(start, 10);
+ const countNo = Number.parseInt(count, 10);
+ const valid =
+ Number.isInteger(startNo) &&
+ startNo >= 1 &&
+ Number.isInteger(countNo) &&
+ countNo >= 1 &&
+ countNo <= MAX_COUNT;
+
+ return (
+