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,97 @@
"use client";
import { useCallback, useState } from "react";
import { api } from "@/lib/api/client";
import type { ClarifyDecisionVM } from "./clarify";
// 澄清预检结论为「放行」(直接去 refine不反问——后端 error/前端异常时的确定性回退。
const PROCEED: ClarifyDecisionVM = { needClarification: false, questions: [] };
export type ClarifyStatus = "idle" | "checking" | "asking" | "proceed" | "error";
export interface UseClarify {
status: ClarifyStatus;
// status=asking 时的待答问题(含选项);其它时为 null。
decision: ClarifyDecisionVM | null;
// 预检:调 refine/clarify 端点,返回 VM 供调用方同步分支。失败=放行needClarification=false不阻塞润色。
check: (
projectId: string,
chapterNo: number,
segment: string,
instruction: string,
) => Promise<ClarifyDecisionVM>;
reset: () => void;
}
// 后端 snake_case ClarifyDecision → 前端 camelCase VM不信任外部数据全部显式取值 + 默认)。
function toVM(data: {
need_clarification?: boolean;
questions?: {
question?: string;
options?: { label?: string; value?: string }[];
allow_free_text?: boolean;
}[];
verification?: string | null;
}): ClarifyDecisionVM {
return {
needClarification: Boolean(data.need_clarification),
questions: (data.questions ?? []).map((q) => ({
question: q.question ?? "",
options: (q.options ?? []).map((o) => ({
label: o.label ?? "",
value: o.value ?? "",
})),
allowFreeText: Boolean(q.allow_free_text),
})),
verification: data.verification ?? undefined,
};
}
// AI 反问澄清预检WFW-9 M1路线A 两阶段之「问题」阶段)。
// 「再沟通」意见含糊/极短时先调独立非流式端点让 AI 反问needClarification=false 或失败=放行去既有 refine。
export function useClarify(): UseClarify {
const [status, setStatus] = useState<ClarifyStatus>("idle");
const [decision, setDecision] = useState<ClarifyDecisionVM | null>(null);
const check = useCallback<UseClarify["check"]>(
async (projectId, chapterNo, segment, instruction) => {
setStatus("checking");
try {
const { data, error } = await api.POST(
"/projects/{project_id}/chapters/{chapter_no}/refine/clarify",
{
params: { path: { project_id: projectId, chapter_no: chapterNo } },
body: { segment, instruction },
},
);
if (error || !data) {
setStatus("proceed");
setDecision(null);
return PROCEED;
}
const vm = toVM(data);
if (vm.needClarification && vm.questions.length > 0) {
setStatus("asking");
setDecision(vm);
} else {
setStatus("proceed");
setDecision(null);
}
return vm;
} catch {
setStatus("proceed");
setDecision(null);
return PROCEED;
}
},
[],
);
const reset = useCallback((): void => {
setStatus("idle");
setDecision(null);
}, []);
return { status, decision, check, reset };
}