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 { useState } from "react";
import { useRef, useState } from "react";
import { Check, MessagesSquare, RotateCcw, Square, X } from "lucide-react";
import { ThinkingIndicator } from "@/components/ThinkingIndicator";
@@ -12,6 +12,10 @@ import { friendlyError } from "@/lib/errors/messages";
import { foldClarifications, needsClarifyGate } from "@/lib/workbench/clarify";
import { useChapterRewrite } from "@/lib/workbench/useChapterRewrite";
import { useRewriteClarify } from "@/lib/workbench/useRewriteClarify";
import type {
AiTurnInput,
AiTurnMessage,
} from "@/lib/workbench/useAiConversation";
import { ChoiceChips } from "./ChoiceChips";
// 门控阈值:整章意见 trim 后短于此字数才自动预检反问(清晰意见不交往返税)。
@@ -24,6 +28,8 @@ interface ChapterRewritePanelProps {
currentDraft: string;
// 接受某一版 → 替换整章正文HITL作者显式点选才落草稿
onAccept: (text: string) => void;
// 留痕:每版重写记 [author(折后意见), ai(版本正文)],含缓冲的 clarify Q&A同 thread 迭代。
appendMessages: (turn: AiTurnInput) => void;
onClose: () => void;
}
@@ -34,12 +40,16 @@ export function ChapterRewritePanel({
chapterNo,
currentDraft,
onAccept,
appendMessages,
onClose,
}: ChapterRewritePanelProps) {
const rewrite = useChapterRewrite();
const clarify = useRewriteClarify();
const [feedback, setFeedback] = useState("");
const [pendingFeedback, setPendingFeedback] = useState<string | null>(null);
// 本面板一次会话 = 一个 thread多版迭代 + 澄清往复归此组,版本号本地累加。
const threadId = useRef(crypto.randomUUID()).current;
const versionNoRef = useRef(0);
const hasVersions = rewrite.versions.length > 0;
const clarifying = clarify.status === "checking";
@@ -51,12 +61,34 @@ export function ChapterRewritePanel({
const priorDraft = rewrite.latest?.text ?? currentDraft;
const err = rewrite.state.phase === "error" ? rewrite.state.error : null;
// 折入澄清答案(或原意见)后流式重写,并清理澄清态
const runSend = (fb: string): void => {
// 折入澄清答案(或原意见)后流式重写,结束后留痕clarifyTurns 非空时把缓冲 Q&A 一并落库
const runSend = (fb: string, clarifyTurns: AiTurnMessage[] | null): void => {
clarify.reset();
setPendingFeedback(null);
setFeedback("");
void rewrite.send(projectId, chapterNo, fb, priorDraft);
void (async () => {
const v = await rewrite.send(projectId, chapterNo, fb, priorDraft);
if (!v) return; // 停止/失败/空版:不留痕。
versionNoRef.current += 1;
const versionNo = versionNoRef.current;
const aiRow: AiTurnMessage = {
role: "ai",
content: v.text,
meta: { version_no: versionNo },
};
// clarify 分支:作者答复 IS 本轮 author 输入;否则记 author(折后意见, meta{version_no})。
const messages = clarifyTurns
? [...clarifyTurns, aiRow]
: [
{
role: "author" as const,
content: fb,
meta: { version_no: versionNo },
},
aiRow,
];
appendMessages({ threadId, kind: "rewrite", chapterNo, messages });
})();
};
// 「重写整章 / 再改一版」:意见含糊/极短(或 force先预检反问否则直接流式重写。
@@ -70,14 +102,32 @@ export function ChapterRewritePanel({
return;
}
}
runSend(fb);
runSend(fb, null);
};
// 作者答复澄清(点选 value 或自由输入)→ 折进意见 → 流式重写。
// 作者答复澄清(点选 value 或自由输入)→ 缓冲 Q&A原意见→反问→答复+ 折进意见 → 流式重写。
const onClarifyAnswer = (answer: string): void => {
const question = clarify.decision?.questions[0]?.question ?? "";
const q = clarify.decision?.questions[0];
const question = q?.question ?? "";
const base = pendingFeedback ?? feedback.trim();
runSend(foldClarifications(base, [{ question, answer }]));
const clarifyTurns: AiTurnMessage[] = [
{ role: "author", content: base },
{
role: "ai",
content: question,
meta: {
parent_kind: "rewrite",
options: (q?.options ?? []).map((o) => ({
label: o.label,
value: o.value,
})),
allow_free_text: q?.allowFreeText ?? false,
verification: clarify.decision?.verification ?? "",
},
},
{ role: "author", content: answer },
];
runSend(foldClarifications(base, [{ question, answer }]), clarifyTurns);
};
return (