fix(rectification): satisfy React compiler lint for collect stem attach
Move the current-question ref off render and persist the collect stem from snapshot/send callbacks so ESLint no longer fails the staging gate. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+2
-2
@@ -7514,8 +7514,8 @@
|
||||
- 用户现象:开场气泡只有打招呼,采集题干仍出现在点赞/复制/重生成图标下面。staging 已是 BUG-487 的 SHA。
|
||||
- 触发条件:新 Case opening,`current_question.kind=collect_spoken`,GET 已有 prompt。
|
||||
- 根因:BUG-487 把题干接到 `assistant_message` 并允许槽位兜底。直播正文仍可能只有打招呼(replace 未进客户端 raw、或 GET 后才有 current_question),于是 `showCollectSpokenPrompt` 把同一句画在图标下。用户要的是气泡正文,不是槽。
|
||||
- 修复:不再渲染采集题槽。最新助手气泡按题干精确身份 `composeCollectSpokenAssistantText`;定稿与 snapshot 把组合文本写进该条消息状态,复制走组合文本。选择卡仍走问题槽。模型仍不自己提问,避免气泡里两句相近问法。不 bump Skill,不放宽确认门。
|
||||
- 验证:`rectification-spoken-collect` 锁气泡拼接、禁止 `rectification-question-slot__prompt`;既有 collect-prompt / v9-agent / regenerate / voice 锁保持。staging 门禁 run 2304 因 `showActivity={bubbleMessage.state…}` 对不上 `rectification-agentic-entry` 源码锁失败;`showActivity` 改回 `displayedMessage.state`(气泡仍走 `bubbleMessage`),该文件与 spoken-collect 本地 52/52。
|
||||
- 修复:不再渲染采集题槽。最新助手气泡按题干精确身份 `composeCollectSpokenAssistantText`;定稿后的 Case snapshot、发送下一条、以及挂载后的 snapshot 回调把组合文本写进该条消息状态,复制走组合文本。选择卡仍走问题槽。模型仍不自己提问,避免气泡里两句相近问法。不 bump Skill,不放宽确认门。
|
||||
- 验证:`rectification-spoken-collect` 锁气泡拼接、禁止 `rectification-question-slot__prompt`;既有 collect-prompt / v9-agent / regenerate / voice 锁保持。staging 门禁 run 2304 因 `showActivity={bubbleMessage.state…}` 对不上 `rectification-agentic-entry` 源码锁失败;`showActivity` 改回 `displayedMessage.state`(气泡仍走 `bubbleMessage`)。run 2305 前端 2509/2509 后 ESLint 在 `currentQuestionRef.current = currentQuestion`(render)和 snapshot `setMessages` effect 上报 `react-hooks/refs` / `react-hooks/set-state-in-effect`;ref 改到 `useLayoutEffect`,题干写入改到 snapshot/send 回调。
|
||||
- 防复发:口述采集题干不得作为动作图标下的兄弟节点。不得用正文字符串判断「有没有问过」。不得同时让模型提问又拼接同一句服务端题干。
|
||||
- 相关记录:BUG-471、BUG-485、BUG-487
|
||||
- 复发自:BUG-487(服务端拼接后槽位兜底仍可见)
|
||||
|
||||
@@ -120,6 +120,25 @@ function currentQuestionFromSnapshot(value: unknown): CurrentQuestionModel | nul
|
||||
};
|
||||
}
|
||||
|
||||
function collectSpokenPromptFromQuestion(question: CurrentQuestionModel | null): string | null {
|
||||
return question?.kind === "collect_spoken" ? question.prompt : null;
|
||||
}
|
||||
|
||||
function attachCollectSpokenStem(current: RenderMessage[], prompt: string): RenderMessage[] {
|
||||
const last = [...current].reverse().find((message) => (
|
||||
message.role === "assistant"
|
||||
&& message.state === "settled"
|
||||
&& !message.failed
|
||||
&& Boolean(message.text)
|
||||
));
|
||||
if (!last) return current;
|
||||
const combined = composeCollectSpokenAssistantText(last.text, prompt);
|
||||
if (combined === last.text) return current;
|
||||
return current.map((item) => (
|
||||
item.renderKey === last.renderKey ? { ...item, text: combined } : item
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
function RectificationCandidateCards({
|
||||
result,
|
||||
@@ -324,7 +343,6 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
const runAbort = useRef<AbortController | null>(null);
|
||||
const choiceActionIds = useRef(new Map<string, string>());
|
||||
const currentQuestionRef = useRef(currentQuestion);
|
||||
currentQuestionRef.current = currentQuestion;
|
||||
const [compactBoard, setCompactBoard] = useState(false);
|
||||
const [boardOpen, setBoardOpen] = useState(false);
|
||||
const [boardDiff, setBoardDiff] = useState(() => diffRectificationBoard(null, null));
|
||||
@@ -335,6 +353,10 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
// new cards land the viewport on the bottom only while the reader is there.
|
||||
const conversationAnchor = useConversationScrollAnchor(conversation, true, caseId);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
currentQuestionRef.current = currentQuestion;
|
||||
}, [currentQuestion]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const query = window.matchMedia(`(max-width: ${RECTIFICATION_BOARD_SPLIT_MIN_PX - 1}px)`);
|
||||
const update = () => {
|
||||
@@ -399,16 +421,19 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadCaseSnapshot = useCallback(async () => {
|
||||
const loadCaseSnapshot = useCallback(async (): Promise<CurrentQuestionModel | null | undefined> => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/rectification/cases/${encodeURIComponent(caseId)}?sessionId=${encodeURIComponent(sessionId)}`,
|
||||
{ cache: "no-store" },
|
||||
);
|
||||
if (!response.ok) return;
|
||||
applyCaseSnapshot(await response.json().catch(() => null));
|
||||
if (!response.ok) return undefined;
|
||||
const payload = await response.json().catch(() => null);
|
||||
applyCaseSnapshot(payload);
|
||||
return currentQuestionFromSnapshot(payload?.current_question);
|
||||
} catch {
|
||||
// Snapshot refresh is best-effort; the durable Case remains on the server.
|
||||
return undefined;
|
||||
}
|
||||
}, [applyCaseSnapshot, caseId, sessionId]);
|
||||
|
||||
@@ -420,7 +445,10 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
)
|
||||
.then((response) => (response.ok ? response.json() : null))
|
||||
.then((payload) => {
|
||||
if (!controller.signal.aborted) applyCaseSnapshot(payload);
|
||||
if (controller.signal.aborted) return;
|
||||
applyCaseSnapshot(payload);
|
||||
const prompt = collectSpokenPromptFromQuestion(currentQuestionFromSnapshot(payload?.current_question));
|
||||
if (prompt) setMessages((current) => attachCollectSpokenStem(current, prompt));
|
||||
})
|
||||
.catch(() => {
|
||||
// Snapshot refresh is best-effort; the durable Case remains on the server.
|
||||
@@ -428,25 +456,6 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
return () => controller.abort();
|
||||
}, [applyCaseSnapshot, caseId, sessionId]);
|
||||
|
||||
useEffect(() => {
|
||||
const prompt = currentQuestion?.kind === "collect_spoken" ? currentQuestion.prompt : null;
|
||||
if (!prompt || busy || regeneratingMessageKey !== null) return;
|
||||
setMessages((current) => {
|
||||
const last = [...current].reverse().find((message) => (
|
||||
message.role === "assistant"
|
||||
&& message.state === "settled"
|
||||
&& !message.failed
|
||||
&& Boolean(message.text)
|
||||
));
|
||||
if (!last) return current;
|
||||
const combined = composeCollectSpokenAssistantText(last.text, prompt);
|
||||
if (combined === last.text) return current;
|
||||
return current.map((item) => (
|
||||
item.renderKey === last.renderKey ? { ...item, text: combined } : item
|
||||
));
|
||||
});
|
||||
}, [busy, currentQuestion, regeneratingMessageKey]);
|
||||
|
||||
const send = useCallback(async (action: "opening" | "message" | "read_only", messageText: string) => {
|
||||
const trimmed = action === "message" ? messageText.trim() : "";
|
||||
if ((action === "message" && !trimmed) || busy || readonly) return;
|
||||
@@ -459,8 +468,11 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
const userRenderKey = `v9-user-${turnKey}`;
|
||||
const assistantRenderKey = `v9-assistant-${turnKey}`;
|
||||
|
||||
const pendingCollectPrompt = collectSpokenPromptFromQuestion(currentQuestionRef.current);
|
||||
setMessages((current) => [
|
||||
...current,
|
||||
...(action === "message" && pendingCollectPrompt
|
||||
? attachCollectSpokenStem(current, pendingCollectPrompt)
|
||||
: current),
|
||||
...(action === "message"
|
||||
? [{ role: "user", text: trimmed, renderKey: userRenderKey, state: "settled" } satisfies RenderMessage]
|
||||
: []),
|
||||
@@ -667,9 +679,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
|
||||
const parsed = completed && !streamFailed ? parseAgentReply(raw) : { text: "", title: undefined };
|
||||
const succeeded = completed && !streamFailed && Boolean(parsed.text);
|
||||
const collectPrompt = currentQuestionRef.current?.kind === "collect_spoken"
|
||||
? currentQuestionRef.current.prompt
|
||||
: null;
|
||||
const collectPrompt = collectSpokenPromptFromQuestion(currentQuestionRef.current);
|
||||
const settledText = succeeded && collectPrompt
|
||||
? composeCollectSpokenAssistantText(parsed.text, collectPrompt)
|
||||
: parsed.text;
|
||||
@@ -708,12 +718,20 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
setError((current) => current || userFacingRunFailure("run_failed"));
|
||||
}
|
||||
if (succeeded) {
|
||||
const snapshotPrompt = collectSpokenPromptFromQuestion(await loadCaseSnapshot() ?? null);
|
||||
const attachedText = snapshotPrompt
|
||||
? composeCollectSpokenAssistantText(parsed.text, snapshotPrompt)
|
||||
: settledText;
|
||||
if (attachedText !== settledText) {
|
||||
setMessages((current) => current.map((message) => (
|
||||
message.renderKey === assistantRenderKey ? { ...message, text: attachedText } : message
|
||||
)));
|
||||
}
|
||||
onMessagesChange?.([
|
||||
...(action === "message" ? [{ role: "user" as const, text: trimmed }] : []),
|
||||
{ role: "assistant", text: settledText },
|
||||
{ role: "assistant", text: attachedText },
|
||||
]);
|
||||
onCompleted?.();
|
||||
await loadCaseSnapshot();
|
||||
}
|
||||
} catch (caught) {
|
||||
frames.settle();
|
||||
@@ -1010,8 +1028,12 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
if (!response.ok || payload?.ok !== true || typeof payload.assistantMessage !== "string") {
|
||||
throw new Error(payload?.message || payload?.error || "暂时无法重新生成回答");
|
||||
}
|
||||
const collectPrompt = collectSpokenPromptFromQuestion(currentQuestion);
|
||||
const nextText = collectPrompt
|
||||
? composeCollectSpokenAssistantText(payload.assistantMessage, collectPrompt)
|
||||
: payload.assistantMessage;
|
||||
setMessages((current) => current.map((item) => item.renderKey === message.renderKey
|
||||
? { ...item, text: payload.assistantMessage, state: "settled" }
|
||||
? { ...item, text: nextText, state: "settled" }
|
||||
: item));
|
||||
} catch (caught) {
|
||||
setMessages((current) => current.map((item) => item.renderKey === message.renderKey
|
||||
|
||||
@@ -114,6 +114,9 @@ test("collect_spoken stem is joined into the latest assistant bubble, never a sl
|
||||
assert.match(chat, /composeCollectSpokenAssistantText\(displayedMessage\.text, collectSpokenPrompt\)/);
|
||||
assert.match(chat, /composeCollectSpokenAssistantText\(last\.text, prompt\)/);
|
||||
assert.match(chat, /composeCollectSpokenAssistantText\(parsed\.text, collectPrompt\)/);
|
||||
assert.match(chat, /function attachCollectSpokenStem/);
|
||||
assert.match(chat, /useLayoutEffect\(\(\) => \{\s*currentQuestionRef\.current = currentQuestion;/);
|
||||
assert.doesNotMatch(chat, /\[busy, currentQuestion, regeneratingMessageKey\]/);
|
||||
assert.doesNotMatch(chat, /showCollectSpokenPrompt/);
|
||||
assert.doesNotMatch(chat, /rectification-question-slot__prompt/);
|
||||
assert.doesNotMatch(chat, /collectSpokenPromptId/);
|
||||
|
||||
Reference in New Issue
Block a user