feat(frontend): 编辑器内联工具箱——就地调生成器(含金手指),结果一键插入正文

Phase 1(写作工作台重构):编辑器「工具箱」按钮打开内联面板,不离开写作界面即可调用
全部生成器(含金手指——后端 golden-finger 早已齐全,此处首次露出)。复用 GeneratorRunner,
为其加可选 onInsertText:文本类产物(开篇/简介/续写/扩写等)点「插入正文」直接追加到章末,
可入库类仍走既有 ingest。useToolboxTools 客户端拉描述符(已单测)。润色/续写/工具箱三卡互斥。
This commit is contained in:
Yaojia Wang
2026-07-07 19:48:13 +02:00
parent 9195cf36f3
commit 04f4785292
5 changed files with 245 additions and 4 deletions

View File

@@ -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<EditorSelection | null>(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 ? (
<InlineToolbox
projectId={project.id}
onInsertText={(chunk) => {
appendToDraft(chunk);
setToolboxOpen(false);
}}
onClose={() => setToolboxOpen(false)}
/>
) : null}
<div className="flex-1 overflow-auto px-6 py-8">
<Editor
value={text}
@@ -244,6 +269,7 @@ export function Workbench({
canRefine={canRefine}
onRefineSelection={openRefine}
onContinue={openContinue}
onToolbox={openToolbox}
/>
</section>
@@ -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 (
<div className="sticky bottom-0 z-10 border-t border-line bg-panel/95 px-4 py-2 backdrop-blur sm:px-6 sm:py-3">
@@ -485,6 +514,10 @@ function Toolbar({
<Wand2 className="h-4 w-4" aria-hidden="true" />
</Button>
<Button onClick={onToolbox} variant="secondary" size="sm">
<Sparkles className="h-4 w-4" aria-hidden="true" />
</Button>
</>
) : null}
{streaming ? (