Files
writer-work-flow/apps/web/components/workbench/InlineToolbox.tsx
Yaojia Wang 33a9b7656c feat(frontend): 五类交互接入 AI 对话留痕 + Workbench 抽屉入口(AC-3)
- 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、漂移不盲替换。
2026-07-09 17:27:14 +02:00

99 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
);
}