/** * Question recovery of the rectification chat: re-read the Case while the * next question is `preparing`, and the one-button repair when it is * `unavailable`. Moved out of RectificationAgenticChat unchanged * (TASK-rectification-code-split-20260926). Parameter functions; the timer * itself stays in the component (useVisibilityAwarePoll). No React hooks here. */ import { startTransition, type Dispatch, type SetStateAction } from "react"; import { RECTIFICATION_QUESTION_REPAIR_LIMIT, type RectificationCaseSnapshotPayload, } from "@/lib/rectification-surface-state"; import { parseRectificationChoiceCard } from "@/lib/rectification-agentic/v9/choice-card"; import { applySnapshotTurnsToMessages } from "@/lib/rectification-snapshot-messages"; import { messagesFromTurns, type PersistedTurn } from "@/lib/rectification-chat-messages"; import { currentQuestionFromSnapshot } from "@/lib/rectification-chat-snapshot"; import type { RectificationSnapshotLoader } from "@/lib/rectification-chat-turn-run"; import type { RenderMessage } from "@/components/rectification-message-entry"; export type RectificationQuestionRefetchDeps = Readonly<{ loadCaseSnapshot: RectificationSnapshotLoader; setMessages: Dispatch>; }>; export async function refetchRectificationQuestion(deps: RectificationQuestionRefetchDeps): Promise { const { loadCaseSnapshot, setMessages } = deps; await loadCaseSnapshot((turns) => { setMessages((current) => applySnapshotTurnsToMessages( current, turns, (incoming) => messagesFromTurns(incoming as readonly PersistedTurn[]), )); }); } export type RectificationQuestionRepairDeps = Readonly<{ caseId: string; sessionId: string; questionRepairing: boolean; questionRepairAttempts: number; setQuestionRepairing: Dispatch>; setQuestionRepairAttempts: Dispatch>; applyCaseSnapshot: (payload: RectificationCaseSnapshotPayload | null) => void; loadCaseSnapshot: RectificationSnapshotLoader; setMessages: Dispatch>; }>; export async function repairRectificationQuestion(deps: RectificationQuestionRepairDeps): Promise { const { caseId, sessionId, questionRepairing, questionRepairAttempts, setQuestionRepairing, setQuestionRepairAttempts, applyCaseSnapshot, loadCaseSnapshot, setMessages, } = deps; if (questionRepairing || questionRepairAttempts >= RECTIFICATION_QUESTION_REPAIR_LIMIT) return; setQuestionRepairing(true); try { const response = await fetch( `/api/rectification/cases/${encodeURIComponent(caseId)}/repair-exit`, { method: "POST", headers: { "content-type": "application/json" }, cache: "no-store", body: JSON.stringify({ sessionId }), }, ); const payload = await response.json().catch(() => null); if (response.ok && payload) { startTransition(() => { applyCaseSnapshot(payload); }); const recovered = Boolean( currentQuestionFromSnapshot(payload.current_question) || parseRectificationChoiceCard(payload.choice_card) || payload.case?.accepted_time || payload.case?.confirmed_time ); if (!recovered) setQuestionRepairAttempts((current) => current + 1); } else { await loadCaseSnapshot((turns) => { setMessages((current) => applySnapshotTurnsToMessages( current, turns, (incoming) => messagesFromTurns(incoming as readonly PersistedTurn[]), )); }); setQuestionRepairAttempts((current) => current + 1); } } catch { setQuestionRepairAttempts((current) => current + 1); } finally { setQuestionRepairing(false); } }