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 ? ( +

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

    + ) : ( + + )} +
    + ); +} 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({