Files
Jyotisha/frontend/src/lib/rectification-chat-question-repair.ts
T
Jesse_ChenandClaude Opus 5.5 3a66c39f48 refactor(rectification): split the chat component into hooks, parameter functions and blocks
Move-only (TASK-rectification-code-split-20260926 T1/T4). No behavior, API,
copy, DB, Skill or scoring change.

- rectification-agentic-chat.tsx: 2043 -> 737 lines; component body
  1625 -> 630; useState 35 -> 12, useRef 16 -> 9, effects 9 -> 5,
  useCallback 12 -> 6.
- Snapshot sync -> hooks/use-rectification-case-snapshot.ts; board layout
  and live label clock -> two small hooks.
- send / submitStructuredChoice / acceptCandidate bodies ->
  lib/rectification-chat-{turn,choice,accept}-run.ts parameter functions
  (bodies verbatim, deps destructured to the same names); copy/regenerate,
  question repair, pure transcript and snapshot helpers and the per-render
  view derivation -> lib/rectification-chat-*.ts (no React hooks).
- Question-gap blocks and the read-only range line ->
  components/rectification-question-gap-notices.tsx.
- Dependency arrays kept exactly as before (lint warnings +3, listed in
  PROGRESS); no dep added to appease the linter.
- Tests: whole-source contracts read tests/rectification-chat-surface.ts
  (container + split files, like home-surface.ts); slices of moved code now
  call the extracted functions or render the extracted block, each with a
  原值/新值/原因 comment. New tests/rectification-growth-contract.test.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
2026-09-26 19:23:52 +08:00

100 lines
3.7 KiB
TypeScript

/**
* 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<SetStateAction<RenderMessage[]>>;
}>;
export async function refetchRectificationQuestion(deps: RectificationQuestionRefetchDeps): Promise<void> {
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<SetStateAction<boolean>>;
setQuestionRepairAttempts: Dispatch<SetStateAction<number>>;
applyCaseSnapshot: (payload: RectificationCaseSnapshotPayload | null) => void;
loadCaseSnapshot: RectificationSnapshotLoader;
setMessages: Dispatch<SetStateAction<RenderMessage[]>>;
}>;
export async function repairRectificationQuestion(deps: RectificationQuestionRepairDeps): Promise<void> {
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);
}
}