写作工作台重构剩余 3 项(多 agent 并行构建各自新文件,主线统一集成): - WFW-5 内容感知书名(#6):buildManuscriptExcerpt 取正文首尾摘录(硬上界控 token); GeneratorRunner 加 manuscriptText,有 brief 字段时出现「用当前正文」按钮把摘录填入 brief,让 book-title 等据正文反推。纯前端零后端;HITL——只填表单,生成仍需作者触发。 - WFW-6 上下文速查抽屉(#7):ContextDrawer 视口无关右侧 slide-over,设定库/大纲完整 只读速查 + 伏笔/规则/文风摘要跳链;底栏「速查」按钮触发。加法式、CONTEXT_DRAWER_ENABLED 一键回滚、旧侧栏/全宽页路由全保留。 - WFW-7 genre 采集 + 无大纲降级:项目无题材时首次写章前弹 GenrePicker 采集(临时并入 本次 directive 不落库);chapters 为空时编辑器显式提示「尚无大纲,将按设定自由生成」。 各项 TDD:manuscriptExcerpt/useGenreGate/contextDrawer/useContextDrawer 共 32 新单测。 门禁全绿:vitest 629 / tsc / lint / build / coverage 95.43%。
56 lines
2.0 KiB
TypeScript
56 lines
2.0 KiB
TypeScript
"use client";
|
||
|
||
import { useCallback, useMemo, useState } from "react";
|
||
|
||
// 极轻 genre 采集门(WFW-7,纯前端零契约)。首次写章前,若项目无 genre,
|
||
// 让作者点一个题材,仅【临时并入本次写作指令】——MVP 不落库(无项目更新端点),
|
||
// 避免无题材 slop。纯状态,无副作用;组件只负责渲染 chips + 触发 onWrite。
|
||
|
||
// 有 effectiveGenre 时的题材指令前缀(并进 composeDirective)。空题材 → ""。
|
||
export function toGenreDirective(genre: string | null): string {
|
||
const trimmed = genre?.trim() ?? "";
|
||
return trimmed.length > 0 ? `本章题材:${trimmed}` : "";
|
||
}
|
||
|
||
export interface GenreGate {
|
||
// 项目缺 genre(需先采集)。为空串/纯空白亦视为缺失。
|
||
needsGenre: boolean;
|
||
// 作者本次临时点选的题材(未选 = null)。
|
||
chosenGenre: string | null;
|
||
setChosenGenre: (genre: string) => void;
|
||
// 生效题材:项目 genre 优先,其次作者点选,都无 → null。
|
||
effectiveGenre: string | null;
|
||
// 并入本次 directive 的题材串(形如「本章题材:玄幻」);无生效题材 → ""。
|
||
genreDirective: string;
|
||
}
|
||
|
||
export function useGenreGate(projectGenre: string | null): GenreGate {
|
||
const [chosenGenre, setChosenGenreState] = useState<string | null>(null);
|
||
|
||
const setChosenGenre = useCallback((genre: string): void => {
|
||
const trimmed = genre.trim();
|
||
setChosenGenreState(trimmed.length > 0 ? trimmed : null);
|
||
}, []);
|
||
|
||
const needsGenre = (projectGenre?.trim() ?? "").length === 0;
|
||
|
||
const effectiveGenre = useMemo<string | null>(() => {
|
||
const fromProject = projectGenre?.trim() ?? "";
|
||
if (fromProject.length > 0) return fromProject;
|
||
return chosenGenre;
|
||
}, [projectGenre, chosenGenre]);
|
||
|
||
const genreDirective = useMemo(
|
||
() => toGenreDirective(effectiveGenre),
|
||
[effectiveGenre],
|
||
);
|
||
|
||
return {
|
||
needsGenre,
|
||
chosenGenre,
|
||
setChosenGenre,
|
||
effectiveGenre,
|
||
genreDirective,
|
||
};
|
||
}
|