From 04f4785292bc9c0cce8a40215fbb24d66a9db195 Mon Sep 17 00:00:00 2001 From: Yaojia Wang Date: Tue, 7 Jul 2026 19:48:13 +0200 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20=E7=BC=96=E8=BE=91=E5=99=A8?= =?UTF-8?q?=E5=86=85=E8=81=94=E5=B7=A5=E5=85=B7=E7=AE=B1=E2=80=94=E2=80=94?= =?UTF-8?q?=E5=B0=B1=E5=9C=B0=E8=B0=83=E7=94=9F=E6=88=90=E5=99=A8(?= =?UTF-8?q?=E5=90=AB=E9=87=91=E6=89=8B=E6=8C=87)=EF=BC=8C=E7=BB=93?= =?UTF-8?q?=E6=9E=9C=E4=B8=80=E9=94=AE=E6=8F=92=E5=85=A5=E6=AD=A3=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1(写作工作台重构):编辑器「工具箱」按钮打开内联面板,不离开写作界面即可调用 全部生成器(含金手指——后端 golden-finger 早已齐全,此处首次露出)。复用 GeneratorRunner, 为其加可选 onInsertText:文本类产物(开篇/简介/续写/扩写等)点「插入正文」直接追加到章末, 可入库类仍走既有 ingest。useToolboxTools 客户端拉描述符(已单测)。润色/续写/工具箱三卡互斥。 --- .../components/toolbox/GeneratorRunner.tsx | 23 +++++ .../components/workbench/InlineToolbox.tsx | 85 +++++++++++++++++++ apps/web/components/workbench/Workbench.tsx | 41 ++++++++- apps/web/lib/toolbox/useToolboxTools.test.ts | 52 ++++++++++++ apps/web/lib/toolbox/useToolboxTools.ts | 48 +++++++++++ 5 files changed, 245 insertions(+), 4 deletions(-) create mode 100644 apps/web/components/workbench/InlineToolbox.tsx create mode 100644 apps/web/lib/toolbox/useToolboxTools.test.ts create mode 100644 apps/web/lib/toolbox/useToolboxTools.ts diff --git a/apps/web/components/toolbox/GeneratorRunner.tsx b/apps/web/components/toolbox/GeneratorRunner.tsx index 6480c6e..6526fc5 100644 --- a/apps/web/components/toolbox/GeneratorRunner.tsx +++ b/apps/web/components/toolbox/GeneratorRunner.tsx @@ -30,6 +30,8 @@ interface GeneratorRunnerProps { projectId: string; tool: ToolDescriptorView; onClose: () => void; + // 可选:把某条预览文本插入正文(编辑器内联工具箱场景)。工具箱页不传 → 不显示「插入正文」。 + onInsertText?: (text: string) => void; } // 声明驱动的通用生成器(核心):按 descriptor.input_fields 渲染表单 → 调通用 generate → @@ -39,6 +41,7 @@ export function GeneratorRunner({ projectId, tool, onClose, + onInsertText, }: GeneratorRunnerProps) { const gen = useGenerator(); const toast = useToast(); @@ -202,6 +205,11 @@ export function GeneratorRunner({ selectable={canIngest && !singleObject} checked={selected.has(i)} onToggle={toggle} + onInsert={ + onInsertText + ? () => onInsertText(item.body ?? item.heading ?? "") + : undefined + } /> ))} @@ -276,6 +284,8 @@ interface PreviewRowProps { selectable: boolean; checked: boolean; onToggle: (index: number) => void; + // 可选:把本条插入正文(编辑器内联工具箱)。有可插入文本时才渲染按钮。 + onInsert?: () => void; } // 统一预览行渲染(heading + 次要字段 + 正文段),由 mapPreview 归一,跨工具复用。 @@ -285,7 +295,9 @@ function PreviewRow({ selectable, checked, onToggle, + onInsert, }: PreviewRowProps) { + const canInsert = onInsert && (item.body || item.heading); return (
  • {selectable ? ( @@ -310,6 +322,17 @@ function PreviewRow({ {item.body ? (

    {item.body}

    ) : null} + {canInsert ? ( + + ) : null}
  • ); diff --git a/apps/web/components/workbench/InlineToolbox.tsx b/apps/web/components/workbench/InlineToolbox.tsx new file mode 100644 index 0000000..6665d4d --- /dev/null +++ b/apps/web/components/workbench/InlineToolbox.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { useState } from "react"; +import { X } from "lucide-react"; + +import { ThinkingIndicator } from "@/components/ThinkingIndicator"; +import { Button } from "@/components/ui/Button"; +import { SectionHeader } from "@/components/ui/SectionHeader"; +import { StatusNote } from "@/components/ui/StatusNote"; +import { GeneratorRunner } from "@/components/toolbox/GeneratorRunner"; +import type { ToolDescriptorView } from "@/lib/api/types"; +import { isLegacyTool } from "@/lib/toolbox/toolbox"; +import { useToolboxTools } from "@/lib/toolbox/useToolboxTools"; + +interface InlineToolboxProps { + projectId: string; + // 把生成结果插入正文(追加到章末)。 + onInsertText: (text: string) => void; + onClose: () => void; +} + +// 编辑器内联工具箱(不离开写作界面):列出生成器(含金手指),就地生成结构化预览, +// 文本类产物点「插入正文」直接填入写作处,可入库类走既有 ingest。legacy 生成器(世界观/ +// 人设/大纲)仍走各自更丰富的独立页,不塞进内联面板。 +export function InlineToolbox({ + projectId, + onInsertText, + onClose, +}: InlineToolboxProps) { + const { status, tools } = useToolboxTools(); + const [active, setActive] = useState(null); + const openable = tools.filter((tool) => !isLegacyTool(tool)); + + return ( +
    +
    + + +
    + + {active ? ( +
    + setActive(null)} + onInsertText={(text) => { + if (text.trim().length > 0) onInsertText(text); + }} + /> +
    + ) : status === "loading" ? ( +
    + +
    + ) : status === "error" ? ( + +

    请检查后端连接后重开。

    +
    + ) : openable.length === 0 ? ( +

    暂无可内联调用的生成器。

    + ) : ( +
      + {openable.map((tool) => ( +
    • + +
    • + ))} +
    + )} +
    + ); +} diff --git a/apps/web/components/workbench/Workbench.tsx b/apps/web/components/workbench/Workbench.tsx index 01dbe0b..8a348ca 100644 --- a/apps/web/components/workbench/Workbench.tsx +++ b/apps/web/components/workbench/Workbench.tsx @@ -11,6 +11,7 @@ import { PanelLeft, PanelRight, PenLine, + Sparkles, Square, Wand2, WrapText, @@ -40,6 +41,7 @@ import { ChapterAssistant, AssistantContent } from "./ChapterAssistant"; import { Editor, type EditorSelection } from "./Editor"; import { RefinePanel } from "./RefinePanel"; import { ContinuePanel } from "./ContinuePanel"; +import { InlineToolbox } from "./InlineToolbox"; interface WorkbenchProps { project: ProjectResponse; @@ -74,6 +76,7 @@ export function Workbench({ const [selection, setSelection] = useState(null); const [refineOpen, setRefineOpen] = useState(false); const [continueOpen, setContinueOpen] = useState(false); + const [toolboxOpen, setToolboxOpen] = useState(false); const toast = useToast(); const autosave = useAutosave(project.id, chapterNo, initialText); const stream = useDraftStream(); @@ -134,9 +137,10 @@ export function Workbench({ setSelection(null); }; - // 打开润色/续写互斥,避免两张结果卡同时占位。 + // 三张 AI 结果卡(润色/续写/工具箱)互斥,避免同时占位。 const openRefine = (): void => { setContinueOpen(false); + setToolboxOpen(false); setRefineOpen(true); }; @@ -144,15 +148,26 @@ export function Workbench({ const openContinue = (): void => { autosave.flush(); setRefineOpen(false); + setToolboxOpen(false); setContinueOpen(true); }; - // 插入续写候选:追加到章末(续写语义),落回草稿。 - const onInsertContinuation = (candidate: string): void => { + const openToolbox = (): void => { + setRefineOpen(false); + setContinueOpen(false); + setToolboxOpen(true); + }; + + // 追加一段文本到章末并落草稿(续写候选 / 工具箱产物共用)。 + const appendToDraft = (chunk: string): void => { const base = text.trimEnd(); - const next = base.length > 0 ? `${base}\n\n${candidate}` : candidate; + const next = base.length > 0 ? `${base}\n\n${chunk}` : chunk; setText(next); autosave.onChange(next); + }; + + const onInsertContinuation = (candidate: string): void => { + appendToDraft(candidate); setContinueOpen(false); }; @@ -222,6 +237,16 @@ export function Workbench({ onClose={() => setContinueOpen(false)} /> ) : null} + {toolboxOpen ? ( + { + appendToDraft(chunk); + setToolboxOpen(false); + }} + onClose={() => setToolboxOpen(false)} + /> + ) : null}
    @@ -436,6 +462,8 @@ interface ToolbarProps { onRefineSelection: () => void; // 续写:读本章前文续写候选。 onContinue: () => void; + // 工具箱:编辑器内联调用生成器,结果回填正文。 + onToolbox: () => void; } function Toolbar({ @@ -452,6 +480,7 @@ function Toolbar({ canRefine, onRefineSelection, onContinue, + onToolbox, }: ToolbarProps) { return (
    @@ -485,6 +514,10 @@ function Toolbar({