Files
writer-work-flow/apps/web/lib/generation/useCharacterGen.ts
Yaojia Wang c6651b74b9 fix(web): stale-closure + focus trap + 去边界强转 + a11y/性能打磨 + 类型化客户端
P1-6 useStyleLearn/useKimiOauth 修 stale-closure 依赖。
P1-7 CommandPalette/Drawer 加 focus trap(新 lib/a11y/focusTrap)。
P1-8 SSE 解析与 server.ts 去 as 强转改类型守卫/运行时校验。
P2 删冗余强转、开 noUncheckedIndexedAccess、Toast 清理+稳定 key、列表 key 稳定化、
  rel=noopener、useMemo 大文本、ConflictCard role=group、useInjection 加锁、
  client.test 真断言。
codegen 重生成 schema.d.ts + 消费 DimensionEntry/ReviewConflictView 强类型。
2026-06-21 19:32:49 +02:00

144 lines
4.4 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 { useCallback, useState } from "react";
import { api } from "@/lib/api/client";
import { useToast } from "@/components/Toast";
import type { CharacterCardView } from "@/lib/api/types";
import {
buildCharacterGenerateRequest,
buildCharacterIngestRequest,
extractIngestConflicts,
generationErrorMessage,
type IngestConflicts,
} from "./cards";
export type CharacterGenStatus = "idle" | "generating" | "preview" | "error";
export type IngestStatus = "idle" | "ingesting" | "conflict" | "done" | "error";
export interface CharacterGenInput {
projectId: string;
brief: string;
count: number;
role?: string | null;
}
export interface UseCharacterGen {
genStatus: CharacterGenStatus;
cards: CharacterCardView[];
ingestStatus: IngestStatus;
// 409 冲突详情(待作者裁决);裁决后带 acknowledge 重发。
conflicts: IngestConflicts | null;
created: string[];
generate: (input: CharacterGenInput) => Promise<void>;
// 入库选中卡acknowledge 用于过 409已查看冲突后重发
ingest: (
projectId: string,
cards: readonly CharacterCardView[],
acknowledge?: boolean,
) => Promise<boolean>;
reset: () => void;
}
// 角色生成器UX §6.6):生成预览 → 作者选/改 → 入库(处理 409 裁决流)。
export function useCharacterGen(): UseCharacterGen {
const [genStatus, setGenStatus] = useState<CharacterGenStatus>("idle");
const [cards, setCards] = useState<CharacterCardView[]>([]);
const [ingestStatus, setIngestStatus] = useState<IngestStatus>("idle");
const [conflicts, setConflicts] = useState<IngestConflicts | null>(null);
const [created, setCreated] = useState<string[]>([]);
const toast = useToast();
const generate = useCallback<UseCharacterGen["generate"]>(
async ({ projectId, brief, count, role }) => {
setGenStatus("generating");
setCards([]);
setConflicts(null);
setIngestStatus("idle");
try {
const { data, error } = await api.POST(
"/projects/{project_id}/characters/generate",
{
params: { path: { project_id: projectId } },
body: buildCharacterGenerateRequest(brief, count, role),
},
);
if (error || !data) {
setGenStatus("error");
toast(generationErrorMessage(error), "error");
return;
}
setCards(data.cards ?? []);
setGenStatus("preview");
} catch {
setGenStatus("error");
toast("生成请求异常,请检查网络。", "error");
}
},
[toast],
);
const ingest = useCallback<UseCharacterGen["ingest"]>(
async (projectId, ingestCards, acknowledge = false) => {
if (ingestCards.length === 0) {
toast("请至少选择一张角色卡。", "error");
return false;
}
setIngestStatus("ingesting");
try {
const { data, error } = await api.POST(
"/projects/{project_id}/characters",
{
params: { path: { project_id: projectId } },
body: buildCharacterIngestRequest(ingestCards, acknowledge),
},
);
if (error || !data) {
const conflict = extractIngestConflicts(error);
if (conflict) {
// 409 CONFLICT_UNRESOLVED展示冲突让作者裁决再带 acknowledge 重发。
setConflicts(conflict);
setIngestStatus("conflict");
return false;
}
setIngestStatus("error");
toast(generationErrorMessage(error), "error");
return false;
}
setCreated(data.created ?? []);
setConflicts(null);
setIngestStatus("done");
toast(`已入库 ${data.created?.length ?? 0} 个角色`, "success");
if (data.rejected_tables && data.rejected_tables.length > 0) {
toast(`越权写表被丢弃:${data.rejected_tables.join("、")}`, "info");
}
return true;
} catch {
setIngestStatus("error");
toast("入库请求异常,请检查网络。", "error");
return false;
}
},
[toast],
);
const reset = useCallback((): void => {
setGenStatus("idle");
setCards([]);
setIngestStatus("idle");
setConflicts(null);
setCreated([]);
}, []);
return {
genStatus,
cards,
ingestStatus,
conflicts,
created,
generate,
ingest,
reset,
};
}