feat(frontend): 写作工作台补齐三项——内容感知书名 / 上下文速查抽屉 / genre采集
写作工作台重构剩余 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%。
This commit is contained in:
@@ -23,6 +23,7 @@ import {
|
||||
type PreviewItem,
|
||||
} from "@/lib/toolbox/toolbox";
|
||||
import { ingestTable, isSingleObjectIngest } from "@/lib/toolbox/ingest";
|
||||
import { buildManuscriptExcerpt } from "@/lib/workbench/manuscriptExcerpt";
|
||||
import { useGenerator } from "@/lib/toolbox/useGenerator";
|
||||
import { TemplateFiller } from "./TemplateFiller";
|
||||
|
||||
@@ -32,6 +33,9 @@ interface GeneratorRunnerProps {
|
||||
onClose: () => void;
|
||||
// 可选:把某条预览文本插入正文(编辑器内联工具箱场景)。工具箱页不传 → 不显示「插入正文」。
|
||||
onInsertText?: (text: string) => void;
|
||||
// 可选:作者【已写正文】。传入且当前工具有 brief 字段时,表单里出现「用当前正文」按钮,
|
||||
// 点击把正文摘录填进 brief,让内容感知型生成器(如书名)据正文反推。工具箱页不传 → 不显示。
|
||||
manuscriptText?: string;
|
||||
}
|
||||
|
||||
// 声明驱动的通用生成器(核心):按 descriptor.input_fields 渲染表单 → 调通用 generate →
|
||||
@@ -42,6 +46,7 @@ export function GeneratorRunner({
|
||||
tool,
|
||||
onClose,
|
||||
onInsertText,
|
||||
manuscriptText,
|
||||
}: GeneratorRunnerProps) {
|
||||
const gen = useGenerator();
|
||||
const toast = useToast();
|
||||
@@ -67,6 +72,26 @@ export function GeneratorRunner({
|
||||
[tool.input_fields, fillTarget],
|
||||
);
|
||||
|
||||
// 当前工具是否有名为 brief 的输入字段——只有它才承接「据正文反推」。
|
||||
const hasBriefField = useMemo(
|
||||
() => (tool.input_fields ?? []).some((f) => f.name === "brief"),
|
||||
[tool.input_fields],
|
||||
);
|
||||
// 有 brief 字段且拿到非空正文时,才提供「用当前正文」。
|
||||
const canUseManuscript =
|
||||
hasBriefField && (manuscriptText?.trim().length ?? 0) > 0;
|
||||
|
||||
const onUseManuscript = useCallback((): void => {
|
||||
if (!manuscriptText) return;
|
||||
const excerpt = buildManuscriptExcerpt(manuscriptText);
|
||||
if (excerpt.length === 0) {
|
||||
toast("当前还没有正文可供反推。", "error");
|
||||
return;
|
||||
}
|
||||
setField("brief", excerpt);
|
||||
toast("已按当前正文填入,可继续编辑后生成。", "success");
|
||||
}, [manuscriptText, setField, toast]);
|
||||
|
||||
const onGenerate = useCallback(async (): Promise<void> => {
|
||||
const missing = missingRequiredFields(tool.input_fields, values);
|
||||
if (missing.length > 0) {
|
||||
@@ -166,6 +191,22 @@ export function GeneratorRunner({
|
||||
targetLabel={fillLabel}
|
||||
onFill={setField}
|
||||
/>
|
||||
{canUseManuscript ? (
|
||||
<div className="flex items-center justify-between gap-2 rounded border border-line bg-bg/50 p-2">
|
||||
<span className="text-xs text-ink-soft">
|
||||
据当前正文反推(自动摘录首尾,控制长度)
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onUseManuscript}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
<Sparkles className="h-4 w-4" aria-hidden="true" />
|
||||
用当前正文
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
{(tool.input_fields ?? []).map((field) => (
|
||||
<FormField
|
||||
key={field.name}
|
||||
|
||||
Reference in New Issue
Block a user