refactor(home): 校正面 15 个状态从 Home 下沉
Independent Staging Quality Gate / validate (push) Failing after 6m43s
Independent Staging Quality Gate / publish (push) Skipped

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:
jesse-ux
2026-09-16 08:33:48 +08:00
parent 3b17c1b242
commit f8e607c29a
10 changed files with 448 additions and 122 deletions
+31 -51
View File
@@ -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} />;
}
+120 -42
View File
@@ -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,