Files
writer-work-flow/apps/web/lib/wizard/wizard.ts
Yaojia Wang b523b4fd21 feat: M1 — 立项→写章草稿(SSE)→自动保存;连一家 provider
- 薄自建 LLM 网关:OpenAI 兼容适配器(DeepSeek) + instructor 结构化输出 + usage_ledger 记账 + 档位路由
- 记忆服务 assemble:确定性选择(显式+主角+近况) + 渲染卡 + 缓存断点(中性文本)
- LangGraph 写章节点 + Postgres checkpointer + SSE 归一(token/done/error)
- API:立项 + 写章 draft(SSE) + PUT 自动保存 + 提供商凭据(Fernet 加密/测试连接)
- 前端:AppShell + 作品库 + 5 步立项向导 + 写作工作台(流式打字机+自动保存) + 设置页
- M1 E2E:真实 DB + mock 网关零 token 走通闭环
2026-06-18 11:38:28 +02:00

63 lines
1.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.

import type { ProjectCreateRequest } from "@/lib/api/types";
// 立项向导 5 步UX §6.2)。表单状态用字符串/数组,提交时归一为 ProjectCreateRequest。
export interface WizardForm {
title: string;
genre: string;
logline: string;
sellingPoints: string[];
structure: string;
premise: string;
theme: string;
}
export const WIZARD_STEPS = 5;
export const emptyWizardForm: WizardForm = {
title: "",
genre: "",
logline: "",
sellingPoints: [],
structure: "",
premise: "",
theme: "",
};
export const GENRES = ["玄幻", "仙侠", "都市", "科幻", "历史", "悬疑"];
export const STRUCTURES = ["三幕", "故事圈", "雪花"];
export const SELLING_POINT_PRESETS = ["逆袭", "打脸", "系统流", "双男主", "群像"];
// 每一步可否前进:仅第 1 步(书名)必填,其余可空着继续(草稿式立项)。
export function canAdvance(step: number, form: WizardForm): boolean {
if (step === 1) return form.title.trim().length > 0;
return true;
}
// 整个向导可否提交:书名必填。
export function canSubmit(form: WizardForm): boolean {
return form.title.trim().length > 0;
}
export function clampStep(step: number): number {
if (step < 1) return 1;
if (step > WIZARD_STEPS) return WIZARD_STEPS;
return step;
}
// 归一为后端请求体snake_case空值落 null/省略)。
export function toCreateRequest(form: WizardForm): ProjectCreateRequest {
const trim = (s: string): string | null => {
const v = s.trim();
return v.length > 0 ? v : null;
};
return {
title: form.title.trim(),
genre: trim(form.genre),
logline: trim(form.logline),
premise: trim(form.premise),
theme: trim(form.theme),
selling_points: form.sellingPoints,
structure: trim(form.structure),
};
}