Files
writer-work-flow/apps/web/components/chain/ChainStarter.tsx
Yaojia Wang 355a2d11cd feat(ui): Checkbox/Radio 原子件 + 3 处原生控件采用(P2-5)
- checkboxClass/radioClass(accent-cinnabar + 统一焦点环)+ 同名测试
- Checkbox/Radio 原子件:无 label 只渲染受控原生 input 便于就地替换
- 采用:ChainStarter 链类型单选、GeneratorRunner 入库勾选、CharacterCardItem 角色勾选
2026-07-11 14:04:01 +02:00

113 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState } from "react";
import { Play } from "lucide-react";
import { Button } from "@/components/ui/Button";
import { Field } from "@/components/ui/Field";
import { Radio } from "@/components/ui/Radio";
import { SectionHeader } from "@/components/ui/SectionHeader";
import { TextInput } from "@/components/ui/TextInput";
import { CHAIN_KINDS, type ChainKind } from "@/lib/chain/chain";
interface ChainStarterProps {
// 发起一条链(链类型 + 起始章 + 章数);父层负责 POST run + 轮询。
onStart: (chainKey: ChainKind, startChapterNo: number, count: number) => void;
// 链正在运行/等待裁决时禁用(避免重复发起)。
disabled: boolean;
}
const DEFAULT_START = 1;
const DEFAULT_COUNT = 3;
const MAX_COUNT = 50;
const DEFAULT_CHAIN: ChainKind = "draft_volume";
// 链发起表单(净新):选起始章号 + 连续写几章 → 调 onStart。
// count 1..50(对齐后端 ChainRunRequest Field 约束;前端先拦一道,越界后端 422 兜底)。
export function ChainStarter({ onStart, disabled }: ChainStarterProps) {
const [chainKey, setChainKey] = useState<ChainKind>(DEFAULT_CHAIN);
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 (
<form
className="flex flex-col gap-4 rounded border border-line bg-panel p-5"
aria-label="发起多章链"
onSubmit={(e) => {
e.preventDefault();
if (valid && !disabled) onStart(chainKey, startNo, countNo);
}}
>
<SectionHeader
title="连续写多章"
description="从指定章起循环「写章 → 四审 → 验收」;遇未决冲突会暂停等你裁决再续跑。"
/>
<fieldset className="flex flex-col gap-2">
<legend className="text-sm text-ink-soft"></legend>
<div className="flex flex-wrap gap-4">
{CHAIN_KINDS.map((kind) => (
<label
key={kind.key}
className="flex items-start gap-2 text-sm text-ink"
>
<Radio
name="chain-kind"
value={kind.key}
checked={chainKey === kind.key}
onChange={() => setChainKey(kind.key)}
className="mt-1"
aria-label={kind.label}
/>
<span>
<span className="block">{kind.label}</span>
<span className="block text-xs text-ink-soft">
{kind.description}
</span>
</span>
</label>
))}
</div>
</fieldset>
<div className="flex flex-wrap gap-4">
<Field label="起始章号" className="w-32">
<TextInput
type="number"
min={1}
value={start}
onChange={(e) => setStart(e.target.value)}
aria-label="起始章号"
/>
</Field>
<Field label={`连续章数1..${MAX_COUNT}`} className="w-40">
<TextInput
type="number"
min={1}
max={MAX_COUNT}
value={count}
onChange={(e) => setCount(e.target.value)}
aria-label="连续章数"
/>
</Field>
</div>
<Button
type="submit"
disabled={disabled || !valid}
variant="primary"
className="self-start"
>
<Play className="h-4 w-4" aria-hidden="true" />
{disabled ? "运行中…" : "发起多章链"}
</Button>
</form>
);
}