fix(chat): make Home pass react-hooks lint after the split
Independent Staging Quality Gate / validate (push) Successful in 17m39s
Independent Staging Quality Gate / publish (push) Successful in 7m11s

eslint-plugin-react-hooks now analyzes the smaller Home, so render-time ref writes and sync effect setState were failing the staging gate.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-02 07:01:36 +08:00
parent 124d3990b2
commit 058e5db9b5
7 changed files with 106 additions and 75 deletions
+61 -63
View File
@@ -235,16 +235,6 @@ const BirthTimeRectification = dynamic(
},
);
const rectificationSessionOpener = {
current: async (_exactSessionId: string) => {},
};
const guidedBirthTimeReady = {
current: (_result: JourneyClientResponse) => {},
};
const editDeclaredBirthTimeDetailsHolder = {
current: () => {},
};
export default function Home() {
const router = useRouter();
const [profile, setProfile] = useState<Profile>(emptyProfile);
@@ -300,6 +290,7 @@ export default function Home() {
const [rectificationTurns, setRectificationTurns] = useState<PersistedRectificationTurn[]>([]);
const [rectificationEntrySummary, setRectificationEntrySummary] = useState<RectificationEntrySummary | null>(null);
const [hydrated, setHydrated] = useState(false);
const [guidedJourneyPreview, setGuidedJourneyPreview] = useState(false);
const [profileSaving, setProfileSaving] = useState(false);
const [creatingSession, setCreatingSession] = useState(false);
const [sessionDetailLoadingId, setSessionDetailLoadingId] = useState<string | null>(null);
@@ -341,8 +332,6 @@ export default function Home() {
const sessionSelectionSource = useRef<"user" | "history">("user");
const applySessionPopStateRef = useRef<(search: string) => void>(() => undefined);
const chartLibraryLoadedAccount = useRef("");
const accountOverlayRef = useRef<AccountOverlayModel | null>(null);
const accountOverlayEpochRef = useRef(0);
const chatActionsRef = useRef<ChatTranscriptActions>({
onFeedback() {},
onCopy() {},
@@ -355,16 +344,18 @@ export default function Home() {
const rectificationOpenInFlight = useRef(false);
const sessionDetailInFlight = useRef(new Set<string>());
const sessionsRef = useRef(sessions);
sessionsRef.current = sessions;
const rectificationSessionOpenerRef = useRef(async (_exactSessionId: string) => {});
const guidedBirthTimeReadyRef = useRef((_result: JourneyClientResponse) => {});
const editDeclaredBirthTimeDetailsRef = useRef(() => {});
const uiPreview = useRef(false);
const uiPreviewMode = useRef<string | null>(null);
const birthTimeRevisionPending = useRef(false);
const birthTimeGuided = useBirthTimeGuidedJourney({
journey: birthTimeJourney,
preview: process.env.NODE_ENV === "development" && uiPreview.current,
preview: guidedJourneyPreview,
onJourney: setBirthTimeJourney,
onReady: (result) => guidedBirthTimeReady.current(result),
onEditBirthTimeDetails: () => editDeclaredBirthTimeDetailsHolder.current(),
onReady: (result) => guidedBirthTimeReadyRef.current(result),
onEditBirthTimeDetails: () => editDeclaredBirthTimeDetailsRef.current(),
});
const activeSession = sessions.find((session) => session.id === activeSessionId) ?? sessions[0];
@@ -442,7 +433,7 @@ export default function Home() {
setActiveChartId, setActiveSessionId, setBirthTimeConsultationConsent, setCreatingSession,
setDraft, setDraftEntrypoint, setDraftTheme, setRectificationError, setRequestError,
setSessionDetailLoadingId, setSessionFullPrompt, setSessions, uiPreview, visibleSessions,
openRectificationSession: (exactSessionId) => rectificationSessionOpener.current(exactSessionId),
openRectificationSession: (exactSessionId) => rectificationSessionOpenerRef.current(exactSessionId),
});
const {
@@ -453,9 +444,7 @@ export default function Home() {
saveProfile,
saveOnboardingName,
saveOnboardingBirth,
editDeclaredBirthTimeDetails,
saveOnboardingPlace,
completeGuidedBirthTime,
retryBirthTimeAssessment,
signOut,
} = useProfileOnboarding({
@@ -466,15 +455,13 @@ export default function Home() {
setBirthTimeConsultationConsent, setBirthTimeError, setBirthTimeJourney, setDraft,
setEditingSelfChart, setOnboardingJustCompleted, setOnboardingStep, setPresetMessageLength,
setProfile, setProfileDraft, setProfileNotice, setProfileSaving, setRectificationError,
setSigningOut, setStartGreeting, signingOut, uiPreview,
setSigningOut, setStartGreeting, signingOut, uiPreview,
guidedBirthTimeReadyRef, editDeclaredBirthTimeDetailsRef,
});
guidedBirthTimeReady.current = completeGuidedBirthTime;
editDeclaredBirthTimeDetailsHolder.current = editDeclaredBirthTimeDetails;
const {
refreshRectificationCase,
openRectificationFromHomepage,
openRectificationSession,
startNewRectification,
handleRectificationProfileIncomplete,
handleRectificationMessagesChange,
@@ -487,9 +474,8 @@ export default function Home() {
setRectificationEntrySummary, setRectificationError, setRectificationLoading,
setRectificationPendingQuestion, setRectificationReadonly, setRectificationSessionId,
setRectificationShouldStartOpening, setRectificationTurns, setSessions, uiPreview,
updateSession, openAccountDialog, refreshAccount,
updateSession, openAccountDialog, refreshAccount, rectificationSessionOpenerRef,
});
rectificationSessionOpener.current = openRectificationSession;
useEffect(() => {
activeSessionIdRef.current = activeSessionId;
@@ -546,16 +532,19 @@ export default function Home() {
useEffect(() => {
if (!hydrated || !accountId) return;
setActiveChartId(localStorage.getItem(activeChartStorageKey(accountId)) || "self");
const storedChartId = localStorage.getItem(activeChartStorageKey(accountId)) || "self";
queueMicrotask(() => setActiveChartId(storedChartId));
}, [accountId, hydrated]);
useEffect(() => {
const branch = chartLibrarySessionBranch(accountId, chartLibraryLoadedAccount.current);
if (branch === "clear" || !accountId) {
setChartLibrary([]);
setSynastryHistory([]);
setActiveChartId("self");
chartLibraryLoadedAccount.current = "";
queueMicrotask(() => {
setChartLibrary([]);
setSynastryHistory([]);
setActiveChartId("self");
});
return;
}
const profileForLibrary: Profile = activeChartId === "self" ? profile : account ? readProfile(account.profile) : profile;
@@ -610,11 +599,16 @@ export default function Home() {
|| chartLibrary.find((item) => item.role === "self");
if (!record) return;
if (record.id !== activeChartId) {
setActiveChartId(record.id);
localStorage.setItem(activeChartStorageKey(accountId), record.id);
}
setProfile((current) => preserveShallowEqual(current, record.profile));
if (record.role === "self") setProfileDraft((current) => preserveShallowEqual(current, record.profile));
const nextChartId = record.id !== activeChartId ? record.id : null;
const nextProfile = record.profile;
const syncSelfDraft = record.role === "self";
queueMicrotask(() => {
if (nextChartId) setActiveChartId(nextChartId);
setProfile((current) => preserveShallowEqual(current, nextProfile));
if (syncSelfDraft) setProfileDraft((current) => preserveShallowEqual(current, nextProfile));
});
}, [accountId, activeChartId, chartLibrary, hydrated]);
const profileComplete = isProfileComplete(profile);
@@ -768,6 +762,7 @@ export default function Home() {
if (previewMode) {
uiPreview.current = true;
uiPreviewMode.current = previewMode;
setGuidedJourneyPreview(true);
if (previewMode === "error") {
setAccountError("连接云端服务超时。请检查网络后重试,或返回登录页重新建立会话。");
setHydrated(true);
@@ -1198,11 +1193,14 @@ export default function Home() {
const stored = readStoredDailyStarlanguage(accountId);
const controller = new AbortController();
let retryTimer: ReturnType<typeof setTimeout> | undefined;
if (stored && stored.day === today && stored.fingerprint === fingerprint) {
setDailyStarlanguage({ kind: "ready", card: stored.card });
} else {
setDailyStarlanguage({ kind: "pending" });
}
const cachedCard = stored && stored.day === today && stored.fingerprint === fingerprint
? stored.card
: null;
queueMicrotask(() => {
if (controller.signal.aborted) return;
if (cachedCard) setDailyStarlanguage({ kind: "ready", card: cachedCard });
else setDailyStarlanguage({ kind: "pending" });
});
const attempt = (remainingRetries: number) => {
void fetchDailyStarlanguage(controller.signal)
.then((next) => {
@@ -1393,7 +1391,7 @@ export default function Home() {
? `已完成基础商业合作证据筛查:${layers};声明状态:${payload.claimStatus || "partial"};未用层:${(payload.blockedLayers || []).join(" / ") || "A10 / 双方 Dasha-Narayana / 功能吉凶"}。请勿将其表述为合作保证或精确时点。`
: `已计算基础合盘证据:${layers}Ashtakoot ${score ?? "?"}/${max ?? "?"},初步评级:${assessment || "待解释"}。请基于这个证据包继续分析。`;
const reportCard: SynastryReportCard = {
id: `${record.id}-${Date.now()}`,
id: `${record.id}-${timestamp()}`,
partnerName: record.profile.name || "对方",
score,
maxScore: max,
@@ -1403,7 +1401,7 @@ export default function Home() {
strengths: payload.relationshipReport?.strengths,
risks: payload.relationshipReport?.risks,
nextEvidence: payload.relationshipReport?.nextEvidence,
createdAt: Date.now(),
createdAt: timestamp(),
};
let savedReportCard = reportCard;
let historyPersisted = !accountId;
@@ -1460,6 +1458,28 @@ export default function Home() {
}
}
useEffect(() => {
chatActionsRef.current = {
onFeedback(feedbackKey, requested) {
setMessageFeedback((current) => {
const next = { ...current };
const value = toggleChatMessageFeedback(current[feedbackKey], requested);
if (value) next[feedbackKey] = value;
else delete next[feedbackKey];
return next;
});
},
onCopy(feedbackKey, text) {
void copyAssistantMessage(feedbackKey, text);
},
onRegenerate(renderKey) {
regenerateLatestAnswer(renderKey);
},
onFollowUp(question) {
void send(question, activeSession?.theme);
},
};
});
function submit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
@@ -1529,28 +1549,7 @@ export default function Home() {
}));
const modalOpen = activeAccountDialog !== null || onboardingPaywallOpen;
chatActionsRef.current = {
onFeedback(feedbackKey, requested) {
setMessageFeedback((current) => {
const next = { ...current };
const value = toggleChatMessageFeedback(current[feedbackKey], requested);
if (value) next[feedbackKey] = value;
else delete next[feedbackKey];
return next;
});
},
onCopy(feedbackKey, text) {
void copyAssistantMessage(feedbackKey, text);
},
onRegenerate(renderKey) {
regenerateLatestAnswer(renderKey);
},
onFollowUp(question) {
void send(question, activeSession?.theme);
},
};
if (activeAccountDialog !== null) accountOverlayEpochRef.current += 1;
accountOverlayRef.current = {
const accountOverlayModel: AccountOverlayModel | null = activeAccountDialog === null ? null : {
title: activeAccountDialog ? accountDialogTitles[activeAccountDialog] : "",
dialogClass: activeAccountDialog ? accountDialogClasses[activeAccountDialog] : "",
signingOut,
@@ -1981,8 +1980,7 @@ export default function Home() {
<AccountDialogOverlay
open={activeAccountDialog !== null}
dialog={activeAccountDialog}
openEpoch={activeAccountDialog !== null ? accountOverlayEpochRef.current : 0}
modelRef={accountOverlayRef}
model={accountOverlayModel}
/>
{onboardingPaywallOpen && account && (
<OnboardingRedeemPaywall
@@ -33,18 +33,13 @@ const settingsItems: ReadonlyArray<{
export const AccountDialogOverlay = memo(function AccountDialogOverlay({
open,
dialog,
openEpoch,
modelRef,
model,
}: Readonly<{
open: boolean;
dialog: "profile" | "chart-library" | "general" | "logout" | null;
openEpoch: number;
modelRef: MutableRefObject<AccountOverlayModel | null>;
model: AccountOverlayModel | null;
}>) {
if (!open || dialog === null) return null;
const model = modelRef.current;
if (!model) return null;
void openEpoch;
if (!open || dialog === null || model === null) return null;
const isSettingsDialog = dialog !== "logout";
const renderSettingsContent = () => {
@@ -87,6 +87,8 @@ export type ProfileOnboardingParams = {
setStartGreeting: Dispatch<SetStateAction<string>>;
signingOut: boolean;
uiPreview: MutableRefObject<boolean>;
guidedBirthTimeReadyRef: MutableRefObject<(result: JourneyClientResponse) => void>;
editDeclaredBirthTimeDetailsRef: MutableRefObject<() => void>;
};
export function useProfileOnboarding(params: ProfileOnboardingParams) {
@@ -127,6 +129,8 @@ export function useProfileOnboarding(params: ProfileOnboardingParams) {
setStartGreeting,
signingOut,
uiPreview,
guidedBirthTimeReadyRef,
editDeclaredBirthTimeDetailsRef,
} = params;
async function refreshAccount() {
@@ -405,6 +409,9 @@ export function useProfileOnboarding(params: ProfileOnboardingParams) {
}
}
guidedBirthTimeReadyRef.current = completeGuidedBirthTime;
editDeclaredBirthTimeDetailsRef.current = editDeclaredBirthTimeDetails;
return {
refreshAccount,
openAccountDialog,
@@ -64,6 +64,7 @@ export type RectificationSurfaceParams = {
updateSession: (sessionId: string, change: (session: ChatSession) => ChatSession) => void;
openAccountDialog: (dialog: AccountDialog, returnTarget?: HTMLButtonElement | null) => void;
refreshAccount: () => Promise<void>;
rectificationSessionOpenerRef: MutableRefObject<(exactSessionId: string) => Promise<void>>;
};
export function useRectificationSurface(params: RectificationSurfaceParams) {
@@ -103,6 +104,7 @@ export function useRectificationSurface(params: RectificationSurfaceParams) {
updateSession,
openAccountDialog,
refreshAccount,
rectificationSessionOpenerRef,
} = params;
async function refreshRectificationEntrySummary() {
@@ -267,6 +269,7 @@ export function useRectificationSurface(params: RectificationSurfaceParams) {
sessionSelectionSource.current = "history";
void openRectificationSession(session.id);
};
rectificationSessionOpenerRef.current = openRectificationSession;
function handleRectificationProfileIncomplete() {
setRectificationError("profile_incomplete");
@@ -117,6 +117,7 @@ export function useSessionManagement(params: SessionManagementParams) {
visibleSessions,
openRectificationSession,
} = params;
sessionsRef.current = sessions;
function updateSession(sessionId: string, change: (session: ChatSession) => ChatSession) {
setSessions((current) => current.map((session) => (session.id === sessionId ? change(session) : session)));