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

@@ -0,0 +1,51 @@
import { describe, expect, it } from "vitest";
import type { ToolInputFieldView } from "@/lib/api/types";
import type { PreviewModel } from "./toolbox";
import { fieldSummary, renderPreviewText } from "./aiSummary";
const fields: ToolInputFieldView[] = [
{ name: "brief", label: "脑洞", type: "textarea", required: true, help: null },
{ name: "count", label: "数量", type: "number", required: false, help: null },
];
describe("fieldSummary", () => {
it("拼出「labelvalue」摘要跳过空值", () => {
const summary = fieldSummary(fields, { brief: "废柴逆袭", count: "" });
expect(summary).toBe("脑洞:废柴逆袭");
});
it("多字段用 · 连接", () => {
const summary = fieldSummary(fields, { brief: "扮猪吃虎", count: "3" });
expect(summary).toBe("脑洞:扮猪吃虎 · 数量3");
});
it("字段为空 / 无值 → 空串", () => {
expect(fieldSummary([], {})).toBe("");
expect(fieldSummary(fields, {})).toBe("");
});
});
describe("renderPreviewText", () => {
it("把预览项渲染成人读文本heading + 字段 + 正文)", () => {
const preview: PreviewModel = {
kind: "IdeaListResult",
items: [
{
heading: "废柴逆袭",
fields: [{ label: "钩子", value: "扮猪吃虎" }],
body: null,
},
{ heading: null, fields: [], body: "一段开篇正文。" },
],
};
const text = renderPreviewText(preview);
expect(text).toContain("废柴逆袭");
expect(text).toContain("钩子:扮猪吃虎");
expect(text).toContain("一段开篇正文。");
});
it("空预览 → 空串", () => {
expect(renderPreviewText({ kind: "x", items: [] })).toBe("");
});
});