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、漂移不盲替换。
This commit is contained in:
Yaojia Wang
2026-07-09 17:27:14 +02:00
parent 59f2f60a7e
commit 33a9b7656c
10 changed files with 379 additions and 28 deletions

View File

@@ -1,6 +1,6 @@
"use client";
import { useCallback, useMemo, useState } from "react";
import { useCallback, useMemo, useRef, useState } from "react";
import { ArrowLeft, Database, Sparkles } from "lucide-react";
import { useToast } from "@/components/Toast";
@@ -23,8 +23,10 @@ import {
type PreviewItem,
} from "@/lib/toolbox/toolbox";
import { ingestTable, isSingleObjectIngest } from "@/lib/toolbox/ingest";
import { fieldSummary, renderPreviewText } from "@/lib/toolbox/aiSummary";
import { buildManuscriptExcerpt } from "@/lib/workbench/manuscriptExcerpt";
import { useGenerator } from "@/lib/toolbox/useGenerator";
import type { AiTurnInput } from "@/lib/workbench/useAiConversation";
import { TemplateFiller } from "./TemplateFiller";
interface GeneratorRunnerProps {
@@ -36,6 +38,10 @@ interface GeneratorRunnerProps {
// 可选:作者【已写正文】。传入且当前工具有 brief 字段时,表单里出现「用当前正文」按钮,
// 点击把正文摘录填进 brief让内容感知型生成器如书名据正文反推。工具箱页不传 → 不显示。
manuscriptText?: string;
// 可选留痕:生成成功后 append 一批 [author(字段摘要), ai(预览文本)]。
// 内联工具箱传 conversation.appendMessages + 当前章号;工具箱页传 silent 直投 + chapterNo=null。
appendMessages?: (turn: AiTurnInput) => void;
logChapterNo?: number | null;
}
// 声明驱动的通用生成器(核心):按 descriptor.input_fields 渲染表单 → 调通用 generate →
@@ -47,6 +53,8 @@ export function GeneratorRunner({
onClose,
onInsertText,
manuscriptText,
appendMessages,
logChapterNo,
}: GeneratorRunnerProps) {
const gen = useGenerator();
const toast = useToast();
@@ -55,6 +63,8 @@ export function GeneratorRunner({
);
// 入库勾选:预览行下标集合(与 gen.rawPreview 行对齐)。
const [selected, setSelected] = useState<Set<number>>(new Set());
// 本 runner 一次会话 = 一个 thread多次生成归此组。
const threadId = useRef(crypto.randomUUID()).current;
const setField = useCallback((name: string, value: string): void => {
setValues((prev) => ({ ...prev, [name]: value }));
@@ -99,8 +109,36 @@ export function GeneratorRunner({
return;
}
setSelected(new Set());
await gen.generate(projectId, tool.key, buildGenerateRequest(values));
}, [gen, projectId, tool, values, toast]);
const artifact = await gen.generate(
projectId,
tool.key,
buildGenerateRequest(values),
);
// 留痕成功且有可读预览时author(字段摘要) + ai(渲染预览);空产物/错误路径不记。
if (artifact && appendMessages) {
const aiText = renderPreviewText(artifact.preview);
if (aiText.length > 0) {
appendMessages({
threadId,
kind: "generator",
toolKey: tool.key,
chapterNo: logChapterNo ?? null,
messages: [
{
role: "author",
content: fieldSummary(tool.input_fields ?? [], values) || tool.title,
meta: { tool_key: tool.key, input_fields: values },
},
{
role: "ai",
content: aiText,
meta: { tool_key: tool.key, output_kind: artifact.outputKind },
},
],
});
}
}
}, [gen, projectId, tool, values, toast, appendMessages, logChapterNo, threadId]);
const rows = useMemo(
() => previewRows(gen.rawPreview),

View File

@@ -9,6 +9,7 @@ import { EmptyState } from "@/components/ui/EmptyState";
import { PageHeader } from "@/components/ui/PageHeader";
import type { ProjectResponse, ToolDescriptorView } from "@/lib/api/types";
import { isLegacyTool, resolveLegacyRoute } from "@/lib/toolbox/toolbox";
import { useAiConversation } from "@/lib/workbench/useAiConversation";
import { ToolCard } from "./ToolCard";
import { GeneratorRunner } from "./GeneratorRunner";
@@ -27,6 +28,8 @@ export function ToolboxPage({
initialOpenKey,
}: ToolboxPageProps) {
const router = useRouter();
// 工具箱页无抽屉silent 直投——项目级(chapter_no=null)留痕,不 GET、失败不 toast。
const conversation = useAiConversation(project.id, 0, { silent: true });
const findOpenable = useCallback(
(key: string | undefined): ToolDescriptorView | null => {
@@ -76,6 +79,8 @@ export function ToolboxPage({
<GeneratorRunner
projectId={project.id}
tool={active}
appendMessages={conversation.appendMessages}
logChapterNo={null}
onClose={() => setActive(null)}
/>
) : sorted.length === 0 ? (