fix(web): stop rectification effects from setting state on mount

eslint-config-next failed staging lint because the case snapshot helper and choice-card reset called setState from useEffect. Load the snapshot in the fetch callback and remount the card by question id instead.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-21 18:15:18 +08:00
co-authored by Cursor
parent 85c6669a70
commit a4ab229e84
4 changed files with 62 additions and 25 deletions
@@ -306,6 +306,27 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
// Candidate snapshot comes from the persisted Candidate Snapshot API, never
// from parsing agent text or hidden sentinels.
const applyCaseSnapshot = useCallback((payload: {
latest_result?: unknown;
choice_card?: unknown;
case?: { accepted_time?: unknown; confirmed_time?: unknown };
} | null) => {
if (!payload) return;
const nextCandidate = parseRectificationCandidateResult(payload.latest_result);
const nextChoice = parseRectificationChoiceCard(payload.choice_card);
const acceptedTime = typeof payload.case?.accepted_time === "string" ? payload.case.accepted_time : null;
const confirmedTime = typeof payload.case?.confirmed_time === "string" ? payload.case.confirmed_time : null;
setCandidateResult(nextCandidate);
setChoiceCard(nextChoice);
if (confirmedTime) {
setSavedTime(confirmedTime);
setSavedStatus("confirmed");
} else if (acceptedTime) {
setSavedTime(acceptedTime);
setSavedStatus("accepted");
}
}, []);
const loadCaseSnapshot = useCallback(async () => {
try {
const response = await fetch(
@@ -313,32 +334,27 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
{ cache: "no-store" },
);
if (!response.ok) return;
const payload = await response.json().catch(() => null);
const nextCandidate = parseRectificationCandidateResult(payload?.latest_result);
const nextChoice = parseRectificationChoiceCard(payload?.choice_card);
const acceptedTime = typeof payload?.case?.accepted_time === "string" ? payload.case.accepted_time : null;
const confirmedTime = typeof payload?.case?.confirmed_time === "string" ? payload.case.confirmed_time : null;
setCandidateResult(nextCandidate);
setChoiceCard(nextChoice);
if (confirmedTime) {
setSavedTime(confirmedTime);
setSavedStatus("confirmed");
} else if (acceptedTime) {
setSavedTime(acceptedTime);
setSavedStatus("accepted");
}
applyCaseSnapshot(await response.json().catch(() => null));
} catch {
// Snapshot refresh is best-effort; the durable Case remains on the server.
}
}, [caseId, sessionId]);
}, [applyCaseSnapshot, caseId, sessionId]);
useEffect(() => {
let active = true;
void loadCaseSnapshot().then(() => {
if (!active) return;
});
return () => { active = false; };
}, [loadCaseSnapshot]);
const controller = new AbortController();
void fetch(
`/api/rectification/cases/${encodeURIComponent(caseId)}?sessionId=${encodeURIComponent(sessionId)}`,
{ cache: "no-store", signal: controller.signal },
)
.then((response) => (response.ok ? response.json() : null))
.then((payload) => {
if (!controller.signal.aborted) applyCaseSnapshot(payload);
})
.catch(() => {
// Snapshot refresh is best-effort; the durable Case remains on the server.
});
return () => controller.abort();
}, [applyCaseSnapshot, caseId, sessionId]);
const send = useCallback(async (action: "opening" | "message", messageText: string) => {
const trimmed = action === "message" ? messageText.trim() : "";
@@ -839,6 +855,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
)}
{showChoiceCards && message.renderKey === choiceCardMessageKey && choiceCard && (
<RectificationChoiceCard
key={choiceCard.question_id}
card={choiceCard}
pending={busy}
disabled={readonly}
@@ -20,10 +20,6 @@ export function RectificationChoiceCard(props: RectificationChoiceCardProps) {
const primary = props.card.options.filter((option) => option.role === "primary");
const secondary = props.card.options.filter((option) => option.role === "secondary");
useEffect(() => {
setSelectedKey("");
}, [props.card.question_id]);
useEffect(() => {
if (!props.pending) firstChoiceRef.current?.focus();
}, [props.card.question_id, props.pending]);