"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import { Database, Sparkles } from "lucide-react"; import { Button } from "@/components/ui/Button"; import { Field } from "@/components/ui/Field"; import { SectionHeader } from "@/components/ui/SectionHeader"; import { StatusNote } from "@/components/ui/StatusNote"; import { TextInput } from "@/components/ui/TextInput"; import type { CharacterCardView } from "@/lib/api/types"; import { MAX_CHARACTER_COUNT, MIN_CHARACTER_COUNT, updateCard, } from "@/lib/generation/cards"; import { useCharacterGen } from "@/lib/generation/useCharacterGen"; import { CharacterCardItem } from "./CharacterCardItem"; import { ConflictAdjudication } from "./ConflictAdjudication"; interface CharacterGeneratorProps { projectId: string; // 入库成功后回调(如刷新 Codex 本地列表)。 onIngested?: (created: string[], cards: CharacterCardView[]) => void; } // 角色生成器(UX §6.6):一句话需求 + 数量 + 定位 → 预览卡 → 选/改 → 入库(处理 409 裁决)。 export function CharacterGenerator({ projectId, onIngested, }: CharacterGeneratorProps) { const gen = useCharacterGen(); const [brief, setBrief] = useState(""); const [count, setCount] = useState(MIN_CHARACTER_COUNT); const [role, setRole] = useState(""); // 编辑态卡片(预览返回后拷贝进本地,允许就地改)。 const [drafts, setDrafts] = useState([]); const [selected, setSelected] = useState>(new Set()); // 预览返回 → 同步进本地编辑态 + 默认全选。 const syncDrafts = useCallback((cards: CharacterCardView[]) => { setDrafts(cards); setSelected(new Set(cards.map((_, i) => i))); }, []); const onGenerate = useCallback(async () => { await gen.generate({ projectId, brief, count, role }); }, [gen, projectId, brief, count, role]); // gen.cards 变化(新预览)→ 重新同步本地草稿。 const previewKey = gen.cards.map((c) => c.name).join("|"); useEffect(() => { if (gen.genStatus === "preview") syncDrafts(gen.cards); // eslint-disable-next-line react-hooks/exhaustive-deps }, [previewKey, gen.genStatus]); const toggle = (i: number): void => { setSelected((prev) => { const next = new Set(prev); if (next.has(i)) next.delete(i); else next.add(i); return next; }); }; const selectedCards = useMemo( () => drafts.filter((_, i) => selected.has(i)), [drafts, selected], ); const doIngest = useCallback( async (acknowledge: boolean) => { const ok = await gen.ingest(projectId, selectedCards, acknowledge); if (ok) { onIngested?.(gen.created, selectedCards); setDrafts([]); setSelected(new Set()); } }, [gen, projectId, selectedCards, onIngested], ); const generating = gen.genStatus === "generating"; const ingesting = gen.ingestStatus === "ingesting"; return (
setBrief(e.target.value)} placeholder="如:一个亦正亦邪的剑修,背负灭门血仇" />
setCount(Number(e.target.value))} /> setRole(e.target.value)} placeholder="主角 / CP / 对手 / 导师 / 工具人" />
{gen.genStatus === "preview" && drafts.length > 0 ? (

已生成 {drafts.length} 张,选 {selectedCards.length} 张入库(可就地编辑名/弧光)。

{drafts.map((card, i) => ( toggle(i)} onEdit={(patch) => setDrafts((prev) => prev.map((c, j) => (j === i ? updateCard(c, patch) : c)), ) } /> ))}
{gen.ingestStatus === "conflict" && gen.conflicts ? ( void doIngest(true)} onCancel={() => gen.reset()} /> ) : ( )}
) : null} {gen.ingestStatus === "done" && gen.created.length > 0 ? ( 已入库:{gen.created.join("、")} ) : null}
); }