"use client"; import { useCallback, useMemo, useState } from "react"; import { useToast } from "@/components/Toast"; import { ConflictAdjudication } from "@/components/generation/ConflictAdjudication"; import type { ToolDescriptorView, ToolInputFieldView } from "@/lib/api/types"; import { buildGenerateRequest, initialFieldValues, missingRequiredFields, previewRows, type FieldValues, type PreviewItem, } from "@/lib/toolbox/toolbox"; import { ingestTable } from "@/lib/toolbox/ingest"; import { useGenerator } from "@/lib/toolbox/useGenerator"; interface GeneratorRunnerProps { projectId: string; tool: ToolDescriptorView; onClose: () => void; } // 声明驱动的通用生成器(核心):按 descriptor.input_fields 渲染表单 → 调通用 generate → // 按 output_kind 渲染结构化预览 → 可入库者勾选 + 入库(复用 ConflictAdjudication 过 409)。 // 不硬编码任何工具的表单/预览/入库形状——全部由声明 + lib/toolbox 纯逻辑驱动。 export function GeneratorRunner({ projectId, tool, onClose, }: GeneratorRunnerProps) { const gen = useGenerator(); const toast = useToast(); const [values, setValues] = useState(() => initialFieldValues(tool.input_fields), ); // 入库勾选:预览行下标集合(与 gen.rawPreview 行对齐)。 const [selected, setSelected] = useState>(new Set()); const setField = useCallback((name: string, value: string): void => { setValues((prev) => ({ ...prev, [name]: value })); }, []); const onGenerate = useCallback(async (): Promise => { const missing = missingRequiredFields(tool.input_fields, values); if (missing.length > 0) { toast(`请填写:${missing.join("、")}`, "error"); return; } setSelected(new Set()); await gen.generate(projectId, tool.key, buildGenerateRequest(values)); }, [gen, projectId, tool, values, toast]); const rows = useMemo( () => previewRows(gen.rawPreview), [gen.rawPreview], ); const table = gen.outputKind ? ingestTable(gen.outputKind) : null; const canIngest = tool.ingestable && table !== null && rows.length > 0; const toggle = useCallback((idx: number): void => { setSelected((prev) => { const next = new Set(prev); if (next.has(idx)) { next.delete(idx); } else { next.add(idx); } return next; }); }, []); const selectedRows = useMemo( () => rows.filter((_, i) => selected.has(i)), [rows, selected], ); const runIngest = useCallback( async (acknowledge: boolean): Promise => { if (!gen.outputKind) return; const chapterNo = Number(values["chapter_no"]?.trim() || "") || null; await gen.ingest(projectId, tool.key, { outputKind: gen.outputKind, rows: selectedRows, chapterNo, acknowledgeConflicts: acknowledge, }); }, [gen, projectId, tool.key, selectedRows, values], ); const generating = gen.genStatus === "generating"; const ingesting = gen.ingestStatus === "ingesting"; return (

{tool.title}

{tool.subtitle}

{ e.preventDefault(); void onGenerate(); }} > {(tool.input_fields ?? []).map((field) => ( setField(field.name, v)} /> ))} {gen.genStatus === "preview" && gen.preview ? (

预览({gen.preview.items.length}) {canIngest ? "(勾选后可入库)" : null}

    {gen.preview.items.map((item, i) => ( ))}
{canIngest && gen.ingestStatus !== "conflict" ? ( ) : null} {gen.ingestStatus === "conflict" && gen.conflicts ? ( void runIngest(true)} onCancel={() => gen.reset()} /> ) : null} {gen.ingestStatus === "done" ? (

已入库 {gen.created.length} 项。

) : null}
) : null}
); } interface FormFieldProps { field: ToolInputFieldView; value: string; onChange: (value: string) => void; } // 声明式控件映射:type → text / textarea / number(select 暂同 text,后端未给 options)。 function FormField({ field, value, onChange }: FormFieldProps) { const labelText = field.required ? `${field.label} *` : field.label; const common = "mt-1 w-full rounded border border-line bg-bg px-3 py-2 text-sm text-ink"; return (