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:
@@ -11,6 +11,10 @@ import { TextArea } from "@/components/ui/TextArea";
|
||||
import { useRefine } from "@/lib/workbench/useRefine";
|
||||
import { useClarify } from "@/lib/workbench/useClarify";
|
||||
import { foldClarifications, needsClarifyGate } from "@/lib/workbench/clarify";
|
||||
import type {
|
||||
AiTurnInput,
|
||||
AiTurnMessage,
|
||||
} from "@/lib/workbench/useAiConversation";
|
||||
import { ChoiceChips } from "./ChoiceChips";
|
||||
|
||||
// 门控阈值:再沟通意见 trim 后短于此字数才自动预检反问(清晰意见不交往返税)。
|
||||
@@ -23,6 +27,8 @@ interface RefinePanelProps {
|
||||
original: string;
|
||||
// 接受回填:把选段替换为该产出(调用方按内容重锚落回草稿)。
|
||||
onAccept: (refined: string) => void;
|
||||
// 留痕:把本轮往复(含缓冲的 clarify Q&A)作为一批 append(fail-soft,不阻塞回炉)。
|
||||
appendMessages: (turn: AiTurnInput) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
@@ -33,6 +39,7 @@ export function RefinePanel({
|
||||
chapterNo,
|
||||
original,
|
||||
onAccept,
|
||||
appendMessages,
|
||||
onClose,
|
||||
}: RefinePanelProps) {
|
||||
const { status, versions, latest, refine, recommunicate } = useRefine();
|
||||
@@ -40,12 +47,36 @@ export function RefinePanel({
|
||||
const [instruction, setInstruction] = useState("");
|
||||
const [pendingInstruction, setPendingInstruction] = useState<string | null>(null);
|
||||
const ranRef = useRef(false);
|
||||
// 本面板一次会话 = 一个 thread;所有润色/再沟通/澄清往复归此组。版本号 repo 无关,本地累加。
|
||||
const threadId = useRef(crypto.randomUUID()).current;
|
||||
const versionNoRef = useRef(0);
|
||||
|
||||
// 打开即对选段回炉一次(仅一次,避免重复请求)。
|
||||
// 一版 ai 产出气泡:content=产出,meta 存版本号 + 原选段(每版都存,供任意版 re-accept 重锚)。
|
||||
const aiRow = (refined: string): AiTurnMessage => {
|
||||
versionNoRef.current += 1;
|
||||
return {
|
||||
role: "ai",
|
||||
content: refined,
|
||||
meta: { version_no: versionNoRef.current, segment: original },
|
||||
};
|
||||
};
|
||||
|
||||
// 打开即对选段回炉一次(仅一次,避免重复请求);成功后留痕 [author(原选段), ai(润色)]。
|
||||
useEffect(() => {
|
||||
if (ranRef.current) return;
|
||||
ranRef.current = true;
|
||||
void refine(projectId, chapterNo, original);
|
||||
void (async () => {
|
||||
const v = await refine(projectId, chapterNo, original);
|
||||
if (v) {
|
||||
appendMessages({
|
||||
threadId,
|
||||
kind: "refine",
|
||||
chapterNo,
|
||||
messages: [{ role: "author", content: original }, aiRow(v.refined)],
|
||||
});
|
||||
}
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [projectId, chapterNo, original, refine]);
|
||||
|
||||
const busy = status === "refining";
|
||||
@@ -53,12 +84,23 @@ export function RefinePanel({
|
||||
const firstQuestion = clarify.decision?.questions[0] ?? null;
|
||||
const asking = clarify.status === "asking" && firstQuestion !== null;
|
||||
|
||||
// 折入澄清答案(或原意见)后回炉,并清理澄清态。
|
||||
const runRecommunicate = (finalInstruction: string): void => {
|
||||
// 折入澄清答案(或原意见)后回炉,结束后留痕;clarifyTurns 非空时把缓冲的 Q&A 一并落库。
|
||||
const runRecommunicate = (
|
||||
finalInstruction: string,
|
||||
clarifyTurns: AiTurnMessage[] | null,
|
||||
): void => {
|
||||
clarify.reset();
|
||||
setPendingInstruction(null);
|
||||
setInstruction("");
|
||||
void recommunicate(projectId, chapterNo, finalInstruction);
|
||||
void (async () => {
|
||||
const v = await recommunicate(projectId, chapterNo, finalInstruction);
|
||||
if (!v) return;
|
||||
// clarify 分支:作者答复 IS 本轮 author 输入,不再另加 author(意见) 气泡;否则记 author(意见)。
|
||||
const messages = clarifyTurns
|
||||
? [...clarifyTurns, aiRow(v.refined)]
|
||||
: [{ role: "author" as const, content: finalInstruction }, aiRow(v.refined)];
|
||||
appendMessages({ threadId, kind: "refine", chapterNo, messages });
|
||||
})();
|
||||
};
|
||||
|
||||
// 「再改一版」:意见含糊/极短(或 force)先预检反问;否则直接回炉。
|
||||
@@ -73,14 +115,33 @@ export function RefinePanel({
|
||||
return;
|
||||
}
|
||||
}
|
||||
runRecommunicate(trimmed);
|
||||
runRecommunicate(trimmed, 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 = pendingInstruction ?? instruction.trim();
|
||||
runRecommunicate(foldClarifications(base, [{ question, answer }]));
|
||||
// 缓冲三条:原含糊意见(author) → AI 反问(ai, 选项进 meta) → 作者答复(author),随本轮产出一起落库。
|
||||
const clarifyTurns: AiTurnMessage[] = [
|
||||
{ role: "author", content: base },
|
||||
{
|
||||
role: "ai",
|
||||
content: question,
|
||||
meta: {
|
||||
parent_kind: "refine",
|
||||
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 },
|
||||
];
|
||||
runRecommunicate(foldClarifications(base, [{ question, answer }]), clarifyTurns);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user