feat(frontend): 润色再沟通接入 AI 反问澄清——选项芯片 + 折答案回炉(WFW-9 M1)

RefinePanel「再沟通」升级为对话式:意见含糊/极短(门控 <10 字,或点「帮我理清方向」)时先调
clarify 预检端点让 AI 反问,渲染 ChoiceChips 选项芯片(+自由输入兜底);作者选/答后
foldClarifications 把答案折进 instruction,再走既有 refine 回炉(零迁移)。清晰意见直接回炉、
不交往返税;预检失败=放行(useClarify 视 error/异常为 needClarification=false,不阻塞润色)。
新增:lib/workbench/clarify.ts(VM 类型+foldClarifications+needsClarifyGate 纯逻辑) +
useClarify(SSE 外调映射 snake→VM) + ChoiceChips.tsx;gen:api 重生成客户端。
门禁绿:tsc/lint/vitest 654(+clarify/useClarify 18 例)/build/coverage 95.36%。
This commit is contained in:
Yaojia Wang
2026-07-08 08:47:28 +02:00
parent 1652ad9d20
commit e30c933c76
7 changed files with 714 additions and 10 deletions

View File

@@ -0,0 +1,90 @@
import { describe, expect, it } from "vitest";
import {
foldClarifications,
needsClarifyGate,
type ClarifyAnswer,
} from "./clarify";
describe("foldClarifications", () => {
it("原意见在前,每条澄清另起一行并保序", () => {
const answers: ClarifyAnswer[] = [
{ question: "这段谁在说话?", answer: "主角内心独白" },
{ question: "结尾要留钩子吗?", answer: "留一个悬念" },
];
const result = foldClarifications("收紧节奏", answers);
expect(result).toBe(
"收紧节奏\n已澄清这段谁在说话 → 主角内心独白\n已澄清结尾要留钩子吗 → 留一个悬念",
);
});
it("无回答时原样返回去空白后的 instruction", () => {
expect(foldClarifications(" 收紧节奏 ", [])).toBe("收紧节奏");
});
it("instruction 为空时只输出澄清行", () => {
const answers: ClarifyAnswer[] = [
{ question: "谁在说话?", answer: "配角旁白" },
];
expect(foldClarifications(" ", answers)).toBe("已澄清:谁在说话? → 配角旁白");
});
it("跳过空/纯空白回答的条目", () => {
const answers: ClarifyAnswer[] = [
{ question: "Q1", answer: " " },
{ question: "Q2", answer: "有效回答" },
];
expect(foldClarifications("原意见", answers)).toBe(
"原意见\n已澄清Q2 → 有效回答",
);
});
it("问题为空(自由输入)时省略问题只留答案", () => {
const answers: ClarifyAnswer[] = [{ question: " ", answer: "就按我说的改" }];
expect(foldClarifications("原意见", answers)).toBe(
"原意见\n已澄清就按我说的改",
);
});
it("去除问题与答案两端空白", () => {
const answers: ClarifyAnswer[] = [
{ question: " 谁在说话? ", answer: " 主角 " },
];
expect(foldClarifications("x", answers)).toBe("x\n已澄清谁在说话 → 主角");
});
it("不可变:不修改入参数组", () => {
const answers: ClarifyAnswer[] = [{ question: "Q", answer: "A" }];
const snapshot = JSON.parse(JSON.stringify(answers));
foldClarifications("原意见", answers);
expect(answers).toEqual(snapshot);
});
it("全部为空回答且 instruction 为空时返回空串", () => {
expect(foldClarifications("", [{ question: "Q", answer: "" }])).toBe("");
});
});
describe("needsClarifyGate", () => {
it("去空白后长度小于 minChars 时需要预检", () => {
expect(needsClarifyGate("改一下", 8)).toBe(true);
});
it("恰好等于 minChars 时不需预检(边界)", () => {
expect(needsClarifyGate("12345678", 8)).toBe(false);
});
it("超过 minChars 时不需预检", () => {
expect(needsClarifyGate("这是一段足够清晰具体的润色意见", 8)).toBe(false);
});
it("纯空白按空计(长度 0 < minChars需预检", () => {
expect(needsClarifyGate(" ", 8)).toBe(true);
});
it("先去两端空白再计长度", () => {
expect(needsClarifyGate(" 改 ", 2)).toBe(true);
});
});