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
100 lines
3.7 KiB
TypeScript
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);
|
|
}
|
|
}
|