"use client"; import { useCallback, useMemo, useState } from "react"; import { ArrowLeft, Database, Sparkles } from "lucide-react"; import { useToast } from "@/components/Toast"; import { ConflictAdjudication } from "@/components/generation/ConflictAdjudication"; import { Button } from "@/components/ui/Button"; import { Badge } from "@/components/ui/Badge"; import { Field } from "@/components/ui/Field"; import { StatusNote } from "@/components/ui/StatusNote"; import { TextArea } from "@/components/ui/TextArea"; import { TextInput } from "@/components/ui/TextInput"; import { ThinkingIndicator } from "@/components/ThinkingIndicator"; import type { ToolDescriptorView, ToolInputFieldView } from "@/lib/api/types"; import { buildGenerateRequest, initialFieldValues, missingRequiredFields, previewRows, templateFillTarget, type FieldValues, type PreviewItem, } from "@/lib/toolbox/toolbox"; import { ingestTable, isSingleObjectIngest } from "@/lib/toolbox/ingest"; import { useGenerator } from "@/lib/toolbox/useGenerator"; import { TemplateFiller } from "./TemplateFiller"; 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 })); }, []); // 「从模板填入」的目标字段(brief / text;无则不渲染)。匹配按 name,展示用中文 label。 const fillTarget = useMemo( () => templateFillTarget(tool.input_fields), [tool.input_fields], ); const fillLabel = useMemo( () => (tool.input_fields ?? []).find((f) => f.name === fillTarget)?.label ?? fillTarget, [tool.input_fields, fillTarget], ); 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; // 单对象产物(如拆书):整个 preview 即一条入库项,无逐行勾选;行式产物按勾选入库。 const singleObject = gen.outputKind ? isSingleObjectIngest(gen.outputKind) : false; const hasPreview = (gen.preview?.items.length ?? 0) > 0; const canIngest = tool.ingestable && table !== null && (singleObject ? hasPreview : 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; // 单对象产物(拆书):整个 rawPreview 作唯一入库行;行式产物用勾选行。 const ingestRows = singleObject && gen.rawPreview ? [gen.rawPreview] : selectedRows; await gen.ingest(projectId, tool.key, { outputKind: gen.outputKind, rows: ingestRows, chapterNo, acknowledgeConflicts: acknowledge, }); }, [gen, projectId, tool.key, selectedRows, values, singleObject], ); const generating = gen.genStatus === "generating"; const ingesting = gen.ingestStatus === "ingesting"; return (

{tool.title}

{tool.ingestable ? ( ) : null}

{tool.subtitle}

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

预览({gen.preview.items.length}) {canIngest ? singleObject ? "(可入库)" : "(勾选后可入库)" : 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" ? (

内容已写入设定库,可在对应页面查看与续编。

) : 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) { return ( {field.type === "textarea" ? (