- RefinePanel/ChapterRewritePanel:结束后 append [author(意见/答复), ai(产出)];
clarify buffer-until-concluded——澄清 Q&A(原意见→反问→答复)缓冲,随最终产出批
一起落库同 thread;纯放弃不记。refine ai 行 meta 存 version_no+原选段(每版重锚)。
- ContinuePanel:ai-only(续写无作者输入),每候选一条 ai 气泡 meta{candidate_index}。
- GeneratorRunner:成功后 append [author(字段摘要, meta{tool_key,input_fields}),
ai(渲染预览, meta{tool_key,output_kind})];空产物不记。lib/toolbox/aiSummary.ts 纯助手+单测。
- InlineToolbox→GeneratorRunner 透传 append+本章号;ToolboxPage 用 silent 直投项目级(chapter_no=null)。
- Workbench:挂 useAiConversation 单实例 + AiConversationDrawer + 底栏「AI 对话」入口(flag 灰度),
三个 DRAFT-only 再接受回调(替换整章/插入正文/回填选段),成功关抽屉+toast、漂移不盲替换。
99 lines
3.9 KiB
TypeScript
99 lines
3.9 KiB
TypeScript
"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";
|
||
import type { AiTurnInput } from "@/lib/workbench/useAiConversation";
|
||
|
||
interface InlineToolboxProps {
|
||
projectId: string;
|
||
// 当前章号:内联工具箱生成留痕挂本章(chapter_no=current)。
|
||
chapterNo: number;
|
||
// 把生成结果插入正文(追加到章末)。
|
||
onInsertText: (text: string) => void;
|
||
// 留痕:生成成功后 append 一批(透传给 GeneratorRunner)。
|
||
appendMessages: (turn: AiTurnInput) => void;
|
||
// 当前正文(编辑器 text)。透传给 GeneratorRunner,供 book-title 等据正文反推 brief。
|
||
manuscriptText?: string;
|
||
onClose: () => void;
|
||
}
|
||
|
||
// 编辑器内联工具箱(不离开写作界面):列出生成器(含金手指),就地生成结构化预览,
|
||
// 文本类产物点「插入正文」直接填入写作处,可入库类走既有 ingest。legacy 生成器(世界观/
|
||
// 人设/大纲)仍走各自更丰富的独立页,不塞进内联面板。
|
||
export function InlineToolbox({
|
||
projectId,
|
||
chapterNo,
|
||
onInsertText,
|
||
appendMessages,
|
||
manuscriptText,
|
||
onClose,
|
||
}: InlineToolboxProps) {
|
||
const { status, tools } = useToolboxTools();
|
||
const [active, setActive] = useState<ToolDescriptorView | null>(null);
|
||
const openable = tools.filter((tool) => !isLegacyTool(tool));
|
||
|
||
return (
|
||
<div className="max-h-[70vh] overflow-auto border-b border-line bg-panel px-4 py-3 sm:px-6">
|
||
<div className="flex items-center justify-between gap-3">
|
||
<SectionHeader
|
||
title="工具箱"
|
||
description="就地生成,点「插入正文」直接填入写作处;可入库内容经一致性预检。"
|
||
/>
|
||
<Button onClick={onClose} variant="ghost" size="icon" aria-label="关闭工具箱">
|
||
<X className="h-4 w-4" aria-hidden="true" />
|
||
</Button>
|
||
</div>
|
||
|
||
{active ? (
|
||
<div className="mt-3">
|
||
<GeneratorRunner
|
||
projectId={projectId}
|
||
tool={active}
|
||
manuscriptText={manuscriptText}
|
||
appendMessages={appendMessages}
|
||
logChapterNo={chapterNo}
|
||
onClose={() => setActive(null)}
|
||
onInsertText={(text) => {
|
||
if (text.trim().length > 0) onInsertText(text);
|
||
}}
|
||
/>
|
||
</div>
|
||
) : status === "loading" ? (
|
||
<div className="mt-3">
|
||
<ThinkingIndicator label="加载工具箱" className="text-xs text-cinnabar" />
|
||
</div>
|
||
) : status === "error" ? (
|
||
<StatusNote variant="danger" className="mt-3 text-xs" title="工具箱加载失败">
|
||
<p>请检查后端连接后重开。</p>
|
||
</StatusNote>
|
||
) : openable.length === 0 ? (
|
||
<p className="mt-3 text-xs text-ink-soft">暂无可内联调用的生成器。</p>
|
||
) : (
|
||
<ul className="mt-3 grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||
{openable.map((tool) => (
|
||
<li key={tool.key}>
|
||
<button
|
||
type="button"
|
||
onClick={() => setActive(tool)}
|
||
className="w-full rounded border border-line bg-bg p-3 text-left transition-colors hover:border-cinnabar"
|
||
>
|
||
<p className="text-sm text-ink">{tool.title}</p>
|
||
<p className="mt-0.5 text-xs text-ink-soft">{tool.subtitle}</p>
|
||
</button>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|