refactor(home): 校正面 15 个状态从 Home 下沉
Home() useState 66->52, useRef 41->39. Shell-facing fields merge into one object; subtree state lives in useRectificationSurface. Chat props 20->7. Growth contract rebaselined. Zero behavior/copy change.
This commit is contained in:
+31
-51
@@ -21,13 +21,11 @@ import {
|
||||
entrySummaryFromResponse,
|
||||
rectificationEntryLabels,
|
||||
resolveRectificationEntryAction,
|
||||
type RectificationEntrySummary,
|
||||
} from "@/lib/rectification-entry";
|
||||
import { ConversationalBirthTimeRectification, type PersistedRectificationTurn } from "@/components/conversational-birth-time-rectification";
|
||||
import { ConversationalBirthTimeRectification } from "@/components/conversational-birth-time-rectification";
|
||||
import {
|
||||
declaredBirthTime,
|
||||
RECTIFICATION_OPENING_LABEL,
|
||||
type RectificationCaseSnapshotPayload,
|
||||
} from "@/lib/rectification-surface-state";
|
||||
import {
|
||||
toggleChatMessageFeedback,
|
||||
@@ -72,7 +70,11 @@ import { useBirthTimeGuidedJourney } from "@/hooks/use-birth-time-guided-journey
|
||||
import { useConsultationRun } from "@/hooks/use-consultation-run";
|
||||
import { useConversationScrollAnchor } from "@/hooks/use-conversation-scroll-anchor";
|
||||
import { useProfileOnboarding } from "@/hooks/use-profile-onboarding";
|
||||
import { useRectificationSurface } from "@/hooks/use-rectification-surface";
|
||||
import {
|
||||
createRectificationShellSetters,
|
||||
EMPTY_RECTIFICATION_SHELL,
|
||||
useRectificationSurface,
|
||||
} from "@/hooks/use-rectification-surface";
|
||||
import { useSessionManagement } from "@/hooks/use-session-management";
|
||||
import { sortSessions } from "@/lib/session-groups";
|
||||
import { showChatNotice as setComposerNotice } from "@/lib/chat-notice";
|
||||
@@ -282,23 +284,26 @@ export default function Home() {
|
||||
const [birthTimeConsultationConsent, setBirthTimeConsultationConsent] = useState<BirthTimeConsultationConsentState>(
|
||||
createBirthTimeConsultationConsentState,
|
||||
);
|
||||
const [rectificationSessionId, setRectificationSessionId] = useState<string | null>(null);
|
||||
const [rectificationCaseId, setRectificationCaseId] = useState<string | null>(null);
|
||||
const [rectificationHeaderSlot, setRectificationHeaderSlot] = useState<HTMLElement | null>(null);
|
||||
const [rectificationPendingQuestion, setRectificationPendingQuestion] = useState<string | null>(null);
|
||||
const [rectificationLoading, setRectificationLoading] = useState(false);
|
||||
const [rectificationMutationPending, setRectificationMutationPending] = useState(false);
|
||||
const [rectificationError, setRectificationError] = useState("");
|
||||
const [rectificationErrorSessionId, setRectificationErrorSessionId] = useState<string | null>(null);
|
||||
const [rectificationReadonly, setRectificationReadonly] = useState(false);
|
||||
const [rectificationShouldStartOpening, setRectificationShouldStartOpening] = useState(false);
|
||||
const [rectificationTurns, setRectificationTurns] = useState<PersistedRectificationTurn[]>([]);
|
||||
const [rectificationSnapshot, setRectificationSnapshot] = useState<RectificationCaseSnapshotPayload | null>(null);
|
||||
const [rectificationOpeningSessionId, setRectificationOpeningSessionId] = useState<string | null>(null);
|
||||
const [rectificationEntrySummary, setRectificationEntrySummary] = useState<RectificationEntrySummary | null>(null);
|
||||
const [rectification, setRectification] = useState(EMPTY_RECTIFICATION_SHELL);
|
||||
const {
|
||||
sessionId: rectificationSessionId,
|
||||
caseId: rectificationCaseId,
|
||||
openingSessionId: rectificationOpeningSessionId,
|
||||
loading: rectificationLoading,
|
||||
mutationPending: rectificationMutationPending,
|
||||
entrySummary: rectificationEntrySummary,
|
||||
entrySummarySettled: rectificationEntrySummarySettled,
|
||||
error: rectificationError,
|
||||
errorSessionId: rectificationErrorSessionId,
|
||||
} = rectification;
|
||||
const {
|
||||
setRectificationError,
|
||||
setRectificationErrorSessionId,
|
||||
setRectificationEntrySummary,
|
||||
setRectificationEntrySummarySettled,
|
||||
} = createRectificationShellSetters(setRectification);
|
||||
const [hydrated, setHydrated] = useState(false);
|
||||
const [bootstrapPhase, setBootstrapPhase] = useState<BootstrapPhase>("account");
|
||||
const [rectificationEntrySummarySettled, setRectificationEntrySummarySettled] = useState(false);
|
||||
const prepareStartedAt = useRef<number | null>(null);
|
||||
const [guidedJourneyPreview, setGuidedJourneyPreview] = useState(false);
|
||||
const [profileSaving, setProfileSaving] = useState(false);
|
||||
@@ -347,8 +352,6 @@ export default function Home() {
|
||||
onFollowUp() {},
|
||||
});
|
||||
const accountRefreshGuard = useRef(createLatestAccountRequestGuard());
|
||||
const resumeRectificationSession = useRef<(session: ChatSession) => void>(() => undefined);
|
||||
const rectificationOpenInFlight = useRef(false);
|
||||
const sessionDetailInFlight = useRef(new Set<string>());
|
||||
const sessionsRef = useRef(sessions);
|
||||
const rectificationSessionOpenerRef = useRef(async (_exactSessionId: string) => {});
|
||||
@@ -465,21 +468,15 @@ export default function Home() {
|
||||
});
|
||||
|
||||
const {
|
||||
refreshRectificationCase,
|
||||
setRectificationHeaderSlot,
|
||||
rectificationPanel,
|
||||
resumeRectificationSession,
|
||||
openRectificationFromHomepage,
|
||||
startNewRectification,
|
||||
handleRectificationProfileIncomplete,
|
||||
handleRectificationMessagesChange,
|
||||
} = useRectificationSurface({
|
||||
account, activeChartId, activeSessionIdRef, chartLibrary, creatingSession, modelCatalog,
|
||||
persistSession, profile, rectificationLoading, rectificationMutationPending,
|
||||
rectificationOpenInFlight, rectificationSessionId, resumeRectificationSession,
|
||||
persistSession, profile, rectification, setRectification,
|
||||
sessionSelectionSource, sessions, setActiveSessionId, setDraft, setDraftEntrypoint,
|
||||
setDraftTheme, setOnboardingStep, setProfileNotice, setRectificationCaseId,
|
||||
setRectificationEntrySummary, setRectificationError, setRectificationErrorSessionId, setRectificationLoading,
|
||||
setRectificationPendingQuestion, setRectificationReadonly, setRectificationSessionId,
|
||||
setRectificationShouldStartOpening, setRectificationSnapshot, setRectificationOpeningSessionId,
|
||||
setRectificationTurns, setSessions, uiPreview,
|
||||
setDraftTheme, setOnboardingStep, setProfileNotice, setSessions, uiPreview,
|
||||
updateSession, openAccountDialog, refreshAccount, rectificationSessionOpenerRef,
|
||||
});
|
||||
|
||||
@@ -540,6 +537,7 @@ export default function Home() {
|
||||
rectificationLoading,
|
||||
rectificationMutationPending,
|
||||
rectificationSessionId,
|
||||
resumeRectificationSession,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -1855,31 +1853,13 @@ export default function Home() {
|
||||
{rectificationSurfaceOpen && rectificationCaseId && (
|
||||
<ConversationalBirthTimeRectification
|
||||
key={`${rectificationSessionId}-${rectificationCaseId}`}
|
||||
caseId={rectificationCaseId}
|
||||
sessionId={rectificationSessionId ?? ""}
|
||||
readonly={rectificationReadonly}
|
||||
shouldStartOpening={rectificationShouldStartOpening}
|
||||
initialTurns={rectificationTurns}
|
||||
initialSnapshot={rectificationSnapshot}
|
||||
declaredTime={rectificationDeclaredTime}
|
||||
models={modelCatalog?.models ?? []}
|
||||
selectedModelId={activeSession?.modelId ?? ""}
|
||||
onSelectModel={(modelId) => void selectSessionModel(modelId)}
|
||||
onMessagesChange={handleRectificationMessagesChange}
|
||||
onOpeningConsumed={() => setRectificationShouldStartOpening(false)}
|
||||
onCompleted={() => {
|
||||
void refreshAccount();
|
||||
if (rectificationCaseId && rectificationSessionId) {
|
||||
void refreshRectificationCase(rectificationCaseId, rectificationSessionId);
|
||||
}
|
||||
}}
|
||||
onPendingChange={setRectificationMutationPending}
|
||||
onProfileIncomplete={handleRectificationProfileIncomplete}
|
||||
onOpenBilling={(options) => openAccountDialog("billing", { source: options?.source ?? "rectification" })}
|
||||
onSaved={() => void refreshAccount()}
|
||||
pendingConsultationQuestion={rectificationPendingQuestion}
|
||||
onRestart={() => void startNewRectification()}
|
||||
headerSlot={rectificationHeaderSlot}
|
||||
panel={rectificationPanel}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ export type PersistedRectificationTurn = Readonly<{
|
||||
}> | null;
|
||||
}>;
|
||||
|
||||
export type ConversationalBirthTimeRectificationProps = Readonly<{
|
||||
export type RectificationChatPanel = Readonly<{
|
||||
caseId: string;
|
||||
sessionId: string;
|
||||
readonly: boolean;
|
||||
@@ -39,23 +39,44 @@ export type ConversationalBirthTimeRectificationProps = Readonly<{
|
||||
initialTurns: readonly PersistedRectificationTurn[];
|
||||
/** The Case snapshot read together with the turns before the surface mounted; null when hydration failed. */
|
||||
initialSnapshot: RectificationCaseSnapshotPayload | null;
|
||||
pendingConsultationQuestion?: string | null;
|
||||
headerSlot: HTMLElement | null;
|
||||
onMessagesChange?: (messages: ChatMessage[]) => void;
|
||||
onCompleted?: () => void;
|
||||
onPendingChange?: (pending: boolean) => void;
|
||||
onProfileIncomplete?: () => void;
|
||||
onOpeningConsumed?: () => void;
|
||||
onRestart?: () => void;
|
||||
}>;
|
||||
|
||||
export type ConversationalBirthTimeRectificationProps = Readonly<{
|
||||
/** The declared birth minute from the profile, shown on the board before any candidate exists. */
|
||||
declaredTime: string | null;
|
||||
models: readonly PublicLanguageModel[];
|
||||
selectedModelId: string;
|
||||
onSelectModel: (modelId: string) => void;
|
||||
onMessagesChange?: (messages: ChatMessage[]) => void;
|
||||
onCompleted?: () => void;
|
||||
onPendingChange?: (pending: boolean) => void;
|
||||
onProfileIncomplete?: () => void;
|
||||
onOpenBilling?: (options?: { source?: string }) => void;
|
||||
onSaved?: (time: string, status: "accepted" | "confirmed") => void;
|
||||
onOpeningConsumed?: () => void;
|
||||
pendingConsultationQuestion?: string | null;
|
||||
onRestart?: () => void;
|
||||
headerSlot: HTMLElement | null;
|
||||
panel: RectificationChatPanel;
|
||||
}>;
|
||||
|
||||
export function ConversationalBirthTimeRectification(props: ConversationalBirthTimeRectificationProps) {
|
||||
export function ConversationalBirthTimeRectification({
|
||||
declaredTime,
|
||||
models,
|
||||
selectedModelId,
|
||||
onSelectModel,
|
||||
onOpenBilling,
|
||||
onSaved,
|
||||
panel,
|
||||
}: ConversationalBirthTimeRectificationProps) {
|
||||
const props = {
|
||||
declaredTime,
|
||||
models,
|
||||
selectedModelId,
|
||||
onSelectModel,
|
||||
onOpenBilling,
|
||||
onSaved,
|
||||
...panel,
|
||||
};
|
||||
return <RectificationAgenticChat {...props} />;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import type { Dispatch, MutableRefObject, SetStateAction } from "react";
|
||||
import { useEffect, useRef, useState, type Dispatch, type MutableRefObject, type SetStateAction } from "react";
|
||||
|
||||
import { resolveSessionTitle } from "@/lib/agent-reply";
|
||||
import { rectificationOpenIdentity } from "@/lib/rectification-session-open";
|
||||
@@ -27,7 +27,10 @@ import {
|
||||
openResponseFromPayload,
|
||||
type RectificationEntrySummary,
|
||||
} from "@/lib/rectification-entry";
|
||||
import type { PersistedRectificationTurn } from "@/components/conversational-birth-time-rectification";
|
||||
import type {
|
||||
PersistedRectificationTurn,
|
||||
RectificationChatPanel,
|
||||
} from "@/components/conversational-birth-time-rectification";
|
||||
import type { PublicLanguageModelCatalog } from "@/lib/public-models";
|
||||
import {
|
||||
hydrateRectificationCase,
|
||||
@@ -38,6 +41,60 @@ import {
|
||||
type RectificationCaseSnapshotPayload,
|
||||
} from "@/lib/rectification-surface-state";
|
||||
|
||||
export type RectificationShellState = {
|
||||
sessionId: string | null;
|
||||
caseId: string | null;
|
||||
openingSessionId: string | null;
|
||||
loading: boolean;
|
||||
mutationPending: boolean;
|
||||
entrySummary: RectificationEntrySummary | null;
|
||||
entrySummarySettled: boolean;
|
||||
error: string;
|
||||
errorSessionId: string | null;
|
||||
};
|
||||
|
||||
export const EMPTY_RECTIFICATION_SHELL: RectificationShellState = {
|
||||
sessionId: null,
|
||||
caseId: null,
|
||||
openingSessionId: null,
|
||||
loading: false,
|
||||
mutationPending: false,
|
||||
entrySummary: null,
|
||||
entrySummarySettled: false,
|
||||
error: "",
|
||||
errorSessionId: null,
|
||||
};
|
||||
|
||||
function fieldSetter<K extends keyof RectificationShellState>(
|
||||
setRectification: Dispatch<SetStateAction<RectificationShellState>>,
|
||||
key: K,
|
||||
): Dispatch<SetStateAction<RectificationShellState[K]>> {
|
||||
return (value) => {
|
||||
setRectification((current) => ({
|
||||
...current,
|
||||
[key]: typeof value === "function"
|
||||
? (value as (prev: RectificationShellState[K]) => RectificationShellState[K])(current[key])
|
||||
: value,
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
export function createRectificationShellSetters(
|
||||
setRectification: Dispatch<SetStateAction<RectificationShellState>>,
|
||||
) {
|
||||
return {
|
||||
setRectificationSessionId: fieldSetter(setRectification, "sessionId"),
|
||||
setRectificationCaseId: fieldSetter(setRectification, "caseId"),
|
||||
setRectificationOpeningSessionId: fieldSetter(setRectification, "openingSessionId"),
|
||||
setRectificationLoading: fieldSetter(setRectification, "loading"),
|
||||
setRectificationMutationPending: fieldSetter(setRectification, "mutationPending"),
|
||||
setRectificationEntrySummary: fieldSetter(setRectification, "entrySummary"),
|
||||
setRectificationEntrySummarySettled: fieldSetter(setRectification, "entrySummarySettled"),
|
||||
setRectificationError: fieldSetter(setRectification, "error"),
|
||||
setRectificationErrorSessionId: fieldSetter(setRectification, "errorSessionId"),
|
||||
};
|
||||
}
|
||||
|
||||
export type RectificationSurfaceParams = {
|
||||
account: Account | null;
|
||||
activeChartId: string;
|
||||
@@ -47,11 +104,8 @@ export type RectificationSurfaceParams = {
|
||||
modelCatalog: PublicLanguageModelCatalog | null;
|
||||
persistSession: (session: ChatSession, mode?: "create" | "update") => Promise<void>;
|
||||
profile: Profile;
|
||||
rectificationLoading: boolean;
|
||||
rectificationMutationPending: boolean;
|
||||
rectificationOpenInFlight: MutableRefObject<boolean>;
|
||||
rectificationSessionId: string | null;
|
||||
resumeRectificationSession: MutableRefObject<(session: ChatSession) => void>;
|
||||
rectification: RectificationShellState;
|
||||
setRectification: Dispatch<SetStateAction<RectificationShellState>>;
|
||||
sessionSelectionSource: MutableRefObject<"user" | "history">;
|
||||
sessions: ChatSession[];
|
||||
setActiveSessionId: Dispatch<SetStateAction<string>>;
|
||||
@@ -60,18 +114,6 @@ export type RectificationSurfaceParams = {
|
||||
setDraftTheme: (theme: Theme | null) => void;
|
||||
setOnboardingStep: Dispatch<SetStateAction<OnboardingStep>>;
|
||||
setProfileNotice: Dispatch<SetStateAction<string>>;
|
||||
setRectificationCaseId: Dispatch<SetStateAction<string | null>>;
|
||||
setRectificationEntrySummary: Dispatch<SetStateAction<RectificationEntrySummary | null>>;
|
||||
setRectificationError: Dispatch<SetStateAction<string>>;
|
||||
setRectificationErrorSessionId: Dispatch<SetStateAction<string | null>>;
|
||||
setRectificationLoading: Dispatch<SetStateAction<boolean>>;
|
||||
setRectificationPendingQuestion: Dispatch<SetStateAction<string | null>>;
|
||||
setRectificationReadonly: Dispatch<SetStateAction<boolean>>;
|
||||
setRectificationSessionId: Dispatch<SetStateAction<string | null>>;
|
||||
setRectificationShouldStartOpening: Dispatch<SetStateAction<boolean>>;
|
||||
setRectificationSnapshot: Dispatch<SetStateAction<RectificationCaseSnapshotPayload | null>>;
|
||||
setRectificationOpeningSessionId: Dispatch<SetStateAction<string | null>>;
|
||||
setRectificationTurns: Dispatch<SetStateAction<PersistedRectificationTurn[]>>;
|
||||
setSessions: Dispatch<SetStateAction<ChatSession[]>>;
|
||||
uiPreview: MutableRefObject<boolean>;
|
||||
updateSession: (sessionId: string, change: (session: ChatSession) => ChatSession) => void;
|
||||
@@ -90,11 +132,8 @@ export function useRectificationSurface(params: RectificationSurfaceParams) {
|
||||
modelCatalog,
|
||||
persistSession,
|
||||
profile,
|
||||
rectificationLoading,
|
||||
rectificationMutationPending,
|
||||
rectificationOpenInFlight,
|
||||
rectificationSessionId,
|
||||
resumeRectificationSession,
|
||||
rectification,
|
||||
setRectification,
|
||||
sessionSelectionSource,
|
||||
sessions,
|
||||
setActiveSessionId,
|
||||
@@ -103,18 +142,6 @@ export function useRectificationSurface(params: RectificationSurfaceParams) {
|
||||
setDraftTheme,
|
||||
setOnboardingStep,
|
||||
setProfileNotice,
|
||||
setRectificationCaseId,
|
||||
setRectificationEntrySummary,
|
||||
setRectificationError,
|
||||
setRectificationErrorSessionId,
|
||||
setRectificationLoading,
|
||||
setRectificationPendingQuestion,
|
||||
setRectificationReadonly,
|
||||
setRectificationSessionId,
|
||||
setRectificationShouldStartOpening,
|
||||
setRectificationSnapshot,
|
||||
setRectificationOpeningSessionId,
|
||||
setRectificationTurns,
|
||||
setSessions,
|
||||
uiPreview,
|
||||
updateSession,
|
||||
@@ -123,6 +150,29 @@ export function useRectificationSurface(params: RectificationSurfaceParams) {
|
||||
rectificationSessionOpenerRef,
|
||||
} = params;
|
||||
|
||||
const rectificationSessionId = rectification.sessionId;
|
||||
const rectificationCaseId = rectification.caseId;
|
||||
const rectificationLoading = rectification.loading;
|
||||
const rectificationMutationPending = rectification.mutationPending;
|
||||
|
||||
const setRectificationSessionId = fieldSetter(setRectification, "sessionId");
|
||||
const setRectificationCaseId = fieldSetter(setRectification, "caseId");
|
||||
const setRectificationOpeningSessionId = fieldSetter(setRectification, "openingSessionId");
|
||||
const setRectificationLoading = fieldSetter(setRectification, "loading");
|
||||
const setRectificationMutationPending = fieldSetter(setRectification, "mutationPending");
|
||||
const setRectificationEntrySummary = fieldSetter(setRectification, "entrySummary");
|
||||
const setRectificationError = fieldSetter(setRectification, "error");
|
||||
const setRectificationErrorSessionId = fieldSetter(setRectification, "errorSessionId");
|
||||
|
||||
const [rectificationHeaderSlot, setRectificationHeaderSlot] = useState<HTMLElement | null>(null);
|
||||
const [rectificationPendingQuestion, setRectificationPendingQuestion] = useState<string | null>(null);
|
||||
const [rectificationReadonly, setRectificationReadonly] = useState(false);
|
||||
const [rectificationShouldStartOpening, setRectificationShouldStartOpening] = useState(false);
|
||||
const [rectificationTurns, setRectificationTurns] = useState<PersistedRectificationTurn[]>([]);
|
||||
const [rectificationSnapshot, setRectificationSnapshot] = useState<RectificationCaseSnapshotPayload | null>(null);
|
||||
const rectificationOpenInFlight = useRef(false);
|
||||
const resumeRectificationSession = useRef<(session: ChatSession) => void>(() => undefined);
|
||||
|
||||
async function refreshRectificationEntrySummary() {
|
||||
if (!account) return;
|
||||
try {
|
||||
@@ -289,12 +339,6 @@ export function useRectificationSurface(params: RectificationSurfaceParams) {
|
||||
await openRectificationCase("new", null, null);
|
||||
}
|
||||
|
||||
resumeRectificationSession.current = (session) => {
|
||||
sessionSelectionSource.current = "history";
|
||||
void openRectificationSession(session.id);
|
||||
};
|
||||
rectificationSessionOpenerRef.current = openRectificationSession;
|
||||
|
||||
function handleRectificationProfileIncomplete() {
|
||||
setRectificationError("profile_incomplete");
|
||||
setRectificationSessionId(null);
|
||||
@@ -320,7 +364,41 @@ export function useRectificationSurface(params: RectificationSurfaceParams) {
|
||||
}));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
resumeRectificationSession.current = (session) => {
|
||||
sessionSelectionSource.current = "history";
|
||||
void openRectificationSession(session.id);
|
||||
};
|
||||
rectificationSessionOpenerRef.current = openRectificationSession;
|
||||
});
|
||||
|
||||
const rectificationPanel: RectificationChatPanel = {
|
||||
caseId: rectificationCaseId ?? "",
|
||||
sessionId: rectificationSessionId ?? "",
|
||||
readonly: rectificationReadonly,
|
||||
shouldStartOpening: rectificationShouldStartOpening,
|
||||
initialTurns: rectificationTurns,
|
||||
initialSnapshot: rectificationSnapshot,
|
||||
pendingConsultationQuestion: rectificationPendingQuestion,
|
||||
headerSlot: rectificationHeaderSlot,
|
||||
onMessagesChange: handleRectificationMessagesChange,
|
||||
onCompleted: () => {
|
||||
void refreshAccount();
|
||||
if (rectificationCaseId && rectificationSessionId) {
|
||||
void refreshRectificationCase(rectificationCaseId, rectificationSessionId);
|
||||
}
|
||||
},
|
||||
onPendingChange: setRectificationMutationPending,
|
||||
onProfileIncomplete: handleRectificationProfileIncomplete,
|
||||
onOpeningConsumed: () => setRectificationShouldStartOpening(false),
|
||||
onRestart: () => void startNewRectification(),
|
||||
};
|
||||
|
||||
return {
|
||||
rectificationHeaderSlot,
|
||||
setRectificationHeaderSlot,
|
||||
rectificationPanel,
|
||||
resumeRectificationSession,
|
||||
refreshRectificationEntrySummary,
|
||||
refreshRectificationCase,
|
||||
openRectificationCase,
|
||||
|
||||
@@ -173,7 +173,10 @@ test("homepage birth-time card opens the V9 Agentic surface via the server case
|
||||
assert.match(source, /openRectificationFromHomepage/);
|
||||
assert.match(source, /<ConversationalBirthTimeRectification/);
|
||||
assert.match(component, /<RectificationAgenticChat \{\.\.\.props\} \/>/);
|
||||
assert.match(source, /pendingConsultationQuestion=\{rectificationPendingQuestion\}/);
|
||||
// 原值:pendingConsultationQuestion={rectificationPendingQuestion} 写在 page.tsx JSX
|
||||
// 新值:pendingConsultationQuestion: rectificationPendingQuestion 写在 hook 的 panel
|
||||
// 原因:校正面子树状态收到 panel,Home 不再逐个传
|
||||
assert.match(source, /pendingConsultationQuestion: rectificationPendingQuestion/);
|
||||
assert.doesNotMatch(source, /chooseSuggestedQuestion\([\s\S]{0,180}"birth_time_rectification"/);
|
||||
assert.doesNotMatch(source, /draftBirthTimeRectificationQuestion/);
|
||||
});
|
||||
@@ -203,7 +206,10 @@ test("homepage opens through the server Case API and merges the returned session
|
||||
assert.match(handler, /rectificationOpenInFlight\.current = true;[\s\S]*?finally \{[\s\S]*?rectificationOpenInFlight\.current = false;/);
|
||||
assert.doesNotMatch(handler, /onNarrativeDelta/);
|
||||
assert.match(source, /const rectificationSurfaceOpen = activeRectificationSession\s*&& activeSession\.id === rectificationSessionId/);
|
||||
assert.match(source, /rectificationSurfaceOpen && rectificationCaseId && \([\s\S]*?<ConversationalBirthTimeRectification[\s\S]*?pendingConsultationQuestion=\{rectificationPendingQuestion\}/);
|
||||
// 原值:同一守卫内 JSX 写 pendingConsultationQuestion={rectificationPendingQuestion}
|
||||
// 新值:同一守卫内 JSX 写 panel={rectificationPanel};pending 字段在 hook panel
|
||||
// 原因:校正面子树状态收到 panel
|
||||
assert.match(source, /rectificationSurfaceOpen && rectificationCaseId && \([\s\S]*?<ConversationalBirthTimeRectification[\s\S]*?panel=\{rectificationPanel\}/);
|
||||
});
|
||||
|
||||
test("the page never creates the session shell locally; the server owns session creation", () => {
|
||||
@@ -278,7 +284,10 @@ test("rectify-first handoffs stay as Agent context", () => {
|
||||
const source = homeSurface;
|
||||
const chat = readFileSync(new URL("../src/components/rectification-agentic-chat.tsx", import.meta.url), "utf8");
|
||||
|
||||
assert.match(source, /pendingConsultationQuestion=\{rectificationPendingQuestion\}/);
|
||||
// 原值:pendingConsultationQuestion={rectificationPendingQuestion} 写在 page.tsx JSX
|
||||
// 新值:pendingConsultationQuestion: rectificationPendingQuestion 写在 hook 的 panel
|
||||
// 原因:校正面子树状态收到 panel,Home 不再逐个传
|
||||
assert.match(source, /pendingConsultationQuestion: rectificationPendingQuestion/);
|
||||
assert.match(chat, /pendingConsultationQuestion\?\.trim\(\)/);
|
||||
// 旧 横幅承诺按钮会带回原问题 → 新 按钮已删,改成结束后新建对话再问 → 保留 pendingConsultationQuestion 传递链
|
||||
assert.match(chat, /结束后新建对话,按采用的时间再问/);
|
||||
@@ -299,7 +308,10 @@ test("ordinary consultation uses current birth data without a rectification noti
|
||||
test("rectification mutations report pending state while session-level return controls stay absent", () => {
|
||||
const source = homeSurface;
|
||||
|
||||
assert.match(source, /onPendingChange=\{setRectificationMutationPending\}/);
|
||||
// 原值:onPendingChange={setRectificationMutationPending} 写在 page.tsx JSX
|
||||
// 新值:onPendingChange: setRectificationMutationPending 写在 hook 的 panel
|
||||
// 原因:mutationPending 仍由外壳对象持有,但 setter 经 panel 交给子树
|
||||
assert.match(source, /onPendingChange: setRectificationMutationPending/);
|
||||
assert.match(source, /disabled=\{productEntrypointsDisabled \|\| rectificationLoading \|\| rectificationMutationPending\}/);
|
||||
assert.doesNotMatch(source, /重试恢复/);
|
||||
assert.doesNotMatch(source, /返回并恢复原问题|返回首页/);
|
||||
|
||||
@@ -4,15 +4,16 @@ import test from "node:test";
|
||||
|
||||
const pageSource = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
|
||||
|
||||
// Measured 2026-09-16 on origin/staging @ 51a65d92.
|
||||
// Line count: (pageSource.match(/\n/g) ?? []).length, same as `wc -l` = 1951.
|
||||
// Measured 2026-09-16 after home-state-lowering on origin/staging @ 3b17c1b2.
|
||||
// Line count: (pageSource.match(/\n/g) ?? []).length, same as `wc -l` = 1931.
|
||||
// Hook counts use \buseState[<(] / \buseRef[<(] so `useState<Type>(` is not missed.
|
||||
// Former line-count assertion lived in tests/chart-view-route.test.ts
|
||||
// (`page.tsx does not grow to host the chart page`).
|
||||
const PAGE_LINE_COUNT_BASELINE = 1951;
|
||||
// Former caps were useState 66 / useRef 41 / lines 1951 on freeze-metric @ 51a65d92.
|
||||
// This round moved 14 rectification useStates out of Home (15 → 1 shell object)
|
||||
// and 2 rectification refs into useRectificationSurface.
|
||||
const PAGE_LINE_COUNT_BASELINE = 1931;
|
||||
const PAGE_LINE_COUNT_CAP = PAGE_LINE_COUNT_BASELINE + 150;
|
||||
const HOME_USE_STATE_CAP = 66;
|
||||
const HOME_USE_REF_CAP = 41;
|
||||
const HOME_USE_STATE_CAP = 52;
|
||||
const HOME_USE_REF_CAP = 39;
|
||||
|
||||
const USE_STATE_RE = /\buseState[<(]/g;
|
||||
const USE_REF_RE = /\buseRef[<(]/g;
|
||||
|
||||
@@ -124,9 +124,15 @@ test("opening is server-owned: shouldStartOpening drives the first turn, never c
|
||||
// 新:增加 skip_probe(核对卡「这题跳过」)
|
||||
// 原因:决策 4,跳过只关本题
|
||||
assert.match(route, /action: z\.enum\(\["opening", "message", "read_only", "answer_choice", "stop_and_review", "skip_probe"\]\)/);
|
||||
assert.match(page, /shouldStartOpening=\{rectificationShouldStartOpening\}/);
|
||||
// 原值:shouldStartOpening={rectificationShouldStartOpening} 写在 page.tsx JSX
|
||||
// 新值:shouldStartOpening: rectificationShouldStartOpening 写在 hook 的 panel
|
||||
// 原因:开场开关只服务子树,随 panel 下沉
|
||||
assert.match(page, /shouldStartOpening: rectificationShouldStartOpening/);
|
||||
assert.match(page, /setRectificationShouldStartOpening\(opened\.shouldStartOpening\)/);
|
||||
assert.match(page, /onOpeningConsumed=\{\(\) => setRectificationShouldStartOpening\(false\)\}/);
|
||||
// 原值:onOpeningConsumed={() => setRectificationShouldStartOpening(false)} 写在 page.tsx JSX
|
||||
// 新值:onOpeningConsumed: () => setRectificationShouldStartOpening(false) 写在 hook 的 panel
|
||||
// 原因:同上
|
||||
assert.match(page, /onOpeningConsumed: \(\) => setRectificationShouldStartOpening\(false\)/);
|
||||
});
|
||||
|
||||
test("incomplete profiles stay in the shared onboarding flow before any open request", () => {
|
||||
@@ -221,9 +227,12 @@ test("persisted turns survive remounts; duplicate openings are suppressed by the
|
||||
// Was: the "ready"/"loading" key suffix (see above). Persisted turns now survive
|
||||
// because nothing remounts: the key is the session/Case binding only (BUG-505).
|
||||
assert.match(page, /key=\{`\$\{rectificationSessionId\}-\$\{rectificationCaseId\}`\}/);
|
||||
assert.match(page, /initialTurns=\{rectificationTurns\}/);
|
||||
assert.match(page, /onMessagesChange=\{handleRectificationMessagesChange\}/);
|
||||
assert.match(page, /onOpeningConsumed=\{\(\) => setRectificationShouldStartOpening\(false\)\}/);
|
||||
// 原值:initialTurns={rectificationTurns} / onMessagesChange={handleRectificationMessagesChange} / onOpeningConsumed={...} 写在 page.tsx JSX
|
||||
// 新值:同名字段写在 hook 的 panel
|
||||
// 原因:子树自有状态与回调收到 panel,Home 只传外壳还要读的绑定
|
||||
assert.match(page, /initialTurns: rectificationTurns/);
|
||||
assert.match(page, /onMessagesChange: handleRectificationMessagesChange/);
|
||||
assert.match(page, /onOpeningConsumed: \(\) => setRectificationShouldStartOpening\(false\)/);
|
||||
});
|
||||
|
||||
test("the agent route verifies the exact Case/Session binding before any turn", () => {
|
||||
@@ -579,7 +588,10 @@ test("compact board overlays chat as a bottom sheet above the composer", () => {
|
||||
assert.match(page, /className="chat-header-rectification"/);
|
||||
assert.match(page, /data-rectification-header-slot=""/);
|
||||
assert.match(page, /ref=\{setRectificationHeaderSlot\}/);
|
||||
assert.match(page, /headerSlot=\{rectificationHeaderSlot\}/);
|
||||
// 原值:headerSlot={rectificationHeaderSlot} 写在 page.tsx JSX
|
||||
// 新值:headerSlot: rectificationHeaderSlot 写在 hook 的 panel;page 仍 ref={setRectificationHeaderSlot}
|
||||
// 原因:header 节点在外壳,值只服务子树
|
||||
assert.match(page, /headerSlot: rectificationHeaderSlot/);
|
||||
assert.match(styles, /\.chat-header-rectification \.rectification-board-peek \{[\s\S]*width: auto/);
|
||||
assert.doesNotMatch(chat, /rectification-workspace__board-trigger/);
|
||||
assert.doesNotMatch(chat.slice(chat.indexOf("className=\"composer-wrap\""), chat.indexOf("<form className=\"composer\"")), /RectificationBoardPeek/);
|
||||
|
||||
@@ -46,7 +46,11 @@ test("a history-list open failure shows under the clicked row, not on the starte
|
||||
// 原值:rectificationError 只由 starter-home 画;selectSession 不切会话,表现为点了没反应
|
||||
// 新值:session 失败绑到被点的那一行;首页起始卡不重复同一条
|
||||
// 决策 3:错误就地显示
|
||||
assert.match(pageSource, /const \[rectificationErrorSessionId, setRectificationErrorSessionId\]/);
|
||||
// 原值:const [rectificationErrorSessionId, setRectificationErrorSessionId] = useState(...)
|
||||
// 新值:外壳 9 个校正字段合成一个 rectification 对象;errorSessionId 仍叫 rectificationErrorSessionId,setter 仍叫 setRectificationErrorSessionId
|
||||
// 原因:外壳也要读的校正状态合并成一个对象,散装 useState 下降
|
||||
assert.match(pageSource, /errorSessionId: rectificationErrorSessionId,/);
|
||||
assert.match(pageSource, /setRectificationErrorSessionId,/);
|
||||
assert.match(
|
||||
pageSource,
|
||||
/rectificationError=\{rectificationErrorSessionId \? "" : rectificationError\}/,
|
||||
|
||||
@@ -35,7 +35,10 @@ test("the rectification surface is revealed once: Case hydration precedes the sw
|
||||
// Panel: one key per binding, snapshot and turns as initial state, later turns as a prop update.
|
||||
assert.match(page, /key=\{`\$\{rectificationSessionId\}-\$\{rectificationCaseId\}`\}/);
|
||||
assert.doesNotMatch(page, /"ready" : "loading"/);
|
||||
assert.match(page, /initialSnapshot=\{rectificationSnapshot\}/);
|
||||
// 原值:initialSnapshot={rectificationSnapshot} 写在 page.tsx JSX
|
||||
// 新值:initialSnapshot: rectificationSnapshot 写在 hook 的 panel
|
||||
// 原因:snapshot 只服务子树,随 panel 下沉
|
||||
assert.match(page, /initialSnapshot: rectificationSnapshot/);
|
||||
assert.match(page, /declaredTime=\{rectificationDeclaredTime\}/);
|
||||
assert.match(wrapper, /initialSnapshot: RectificationCaseSnapshotPayload \| null;/);
|
||||
assert.match(chat, /useState\(initialSnapshot !== null\)/);
|
||||
|
||||
Reference in New Issue
Block a user