Files
writer-work-flow/apps/web/lib/workbench/directive.ts
Yaojia Wang 44b0b8a7f0 feat(frontend): AI写作三槽——文风 / 剧情需求 / 自定义要求,写章前按需组合
Phase 2(写作工作台重构):把写章指令升级为三槽——文风预设 + 新增剧情需求预设
(打脸反转/章末钩子/高潮/转折/扮猪吃虎,源自 plan §4.2 分类法,tianyayu6/oh-story MIT)
+ 自定义自由文本。三者经 composeDirective 合并为单条 directive→volatile(MVP 纯前端、
零契约、不动 write_craft.md 字节,金标准零回归)。DirectivePanel 拆成文风/剧情两组 chips。
2026-07-07 19:51:19 +02:00

56 lines
2.7 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.

// 本章 AI写作三槽纯逻辑UX §3写作工作台重构 Phase 2
// 三槽 = 文风预设 + 剧情需求预设 + 自定义要求(自由文本)→ 单条 directive 串,
// 直通后端 volatileMVP 纯前端、零契约、不动 write_craft.md 字节 → 金标准零回归)。
// 组件只渲染 chips/textarea预设清单与组装规则集中在此确定性、不可变。
export interface Preset {
id: string;
label: string;
text: string;
}
// 文风预设(书级文风倾向;点选引导本章表达)。来源:现有 + style_extract 维度离散化。
export const STYLE_PRESETS: readonly Preset[] = [
{ id: "less-ai", label: "减少AI味", text: "减少 AI 腔,去掉套话与排比堆砌" },
{ id: "colloquial", label: "口语化", text: "用更口语、贴近人物的表达" },
{ id: "fast-pace", label: "快节奏", text: "加快节奏,少铺垫多冲突推进" },
];
// 剧情需求预设章级桥段诉求作者「卡文给方向」。来源plan §4.2 分类法
// tianyayu6/write-web-novels + oh-story-claudecode均 MIT
export const PLOT_PRESETS: readonly Preset[] = [
{ id: "plot-faceslap", label: "打脸反转", text: "本章安排一次打脸/反转,先抑后扬给足爽感" },
{ id: "plot-hook-end", label: "章末钩子", text: "章末留一个强钩子/悬念,驱动追读" },
{ id: "plot-climax", label: "高潮爆发", text: "本章为高潮,冲突集中爆发、情绪拉满" },
{ id: "plot-turn", label: "关键转折", text: "本章制造关键转折,改变人物处境或目标" },
{ id: "plot-reveal", label: "扮猪吃虎", text: "先藏拙后亮实力,制造反差与压制感" },
];
// 全部预设(文风在前、剧情在后)——组装按此声明序,保证输出稳定可测。
const ALL_PRESETS: readonly Preset[] = [...STYLE_PRESETS, ...PLOT_PRESETS];
const PRESET_BY_ID: ReadonlyMap<string, Preset> = new Map(
ALL_PRESETS.map((preset) => [preset.id, preset]),
);
const SEPARATOR = "";
// 组合选中预设文案(按 ALL_PRESETS 原序:文风→剧情)+ 自定义自由文本为一条指令。空 → ""。
export function composeDirective(
freeText: string,
presetIds: readonly string[],
): string {
const selected = new Set(presetIds);
const presetTexts = ALL_PRESETS.filter((preset) =>
selected.has(preset.id),
).map((preset) => preset.text);
const trimmedFree = freeText.trim();
const parts = trimmedFree ? [...presetTexts, trimmedFree] : presetTexts;
return parts.join(SEPARATOR);
}
// 仅暴露给测试/组件的辅助id 是否为已知预设。
export function isKnownPreset(id: string): boolean {
return PRESET_BY_ID.has(id);
}