refactor(home): stabilize shell setters and lower the synastry cluster

5.1: memoize createRectificationShellSetters so the four shell setters keep a
stable identity across renders, then list the two the entry-summary effect uses
in its dependency array. Lint warnings 120 -> 119 with no new warning.

5.2: move the four synastry useStates and draftSynastryQuestionFromChart into
the new useSynastry hook. Home keeps only two stable callbacks the chart-library
effect calls; the chart-library dialog receives one spread synastryPanel object.

Zero behavior change, zero copy change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
This commit is contained in:
Jesse_Chen
2026-09-16 02:02:22 +00:00
co-authored by Claude Opus 5
parent 6df40c322c
commit 99425fe61f
3 changed files with 159 additions and 98 deletions
+15 -98
View File
@@ -5,7 +5,7 @@ import Link from "next/link";
import dynamic from "next/dynamic";
import { useRouter } from "next/navigation";
import { Sparkles } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import type { FormEvent, KeyboardEvent } from "react";
import { AccountDialogOverlay, type AccountOverlayModel } from "@/components/account-dialog-overlay";
import { ProfilePanel } from "@/components/profile-panel";
@@ -75,6 +75,7 @@ import {
EMPTY_RECTIFICATION_SHELL,
useRectificationSurface,
} from "@/hooks/use-rectification-surface";
import { useSynastry } from "@/hooks/use-synastry";
import { useSessionManagement } from "@/hooks/use-session-management";
import { sortSessions } from "@/lib/session-groups";
import { showChatNotice as setComposerNotice } from "@/lib/chat-notice";
@@ -170,13 +171,10 @@ import {
type RequestError,
type StoredPendingConsultation,
type StreamingReply,
type SynastryRelationshipType,
type SynastryReportCard,
type Theme,
} from "@/lib/home-types";
import {
birthQuestion,
buildSynastryQuestion,
chartSnapshotForSession,
completedOnboardingMessage,
completedOnboardingTranscript,
@@ -216,7 +214,6 @@ import {
readStoredDailyStarlanguage,
readStoredPendingConsultation,
redirectToLogin,
saveCloudSynastryReport,
waitForUndoWindow,
writeStoredDailyStarlanguage,
} from "@/lib/home-cloud-sync";
@@ -251,10 +248,6 @@ export default function Home() {
const [activeAccountDialog, setActiveAccountDialog] = useState<AccountDialog | null>(null);
const [chartLibrary, setChartLibrary] = useState<ChartLibraryRecord[]>([]);
const [activeChartId, setActiveChartId] = useState("self");
const [synastryRelationshipType, setSynastryRelationshipType] = useState<SynastryRelationshipType>("romance");
const [synastryPendingId, setSynastryPendingId] = useState<string | null>(null);
const [synastryReportCard, setSynastryReportCard] = useState<SynastryReportCard | null>(null);
const [synastryHistory, setSynastryHistory] = useState<SynastryReportCard[]>([]);
const [billingPane, setBillingPane] = useState<OpenAccountDialogOptions>({});
const [dailyStarlanguage, setDailyStarlanguage] = useState<DailyStarlanguageState>({ kind: "pending" });
const [profileNotice, setProfileNotice] = useState("");
@@ -301,7 +294,7 @@ export default function Home() {
setRectificationErrorSessionId,
setRectificationEntrySummary,
setRectificationEntrySummarySettled,
} = createRectificationShellSetters(setRectification);
} = useMemo(() => createRectificationShellSetters(setRectification), [setRectification]);
const [hydrated, setHydrated] = useState(false);
const [bootstrapPhase, setBootstrapPhase] = useState<BootstrapPhase>("account");
const prepareStartedAt = useRef<number | null>(null);
@@ -467,6 +460,10 @@ export default function Home() {
guidedBirthTimeReadyRef, editDeclaredBirthTimeDetailsRef,
});
const { synastryPanel, clearSynastryHistory, applyCloudSynastryHistory } = useSynastry({
accountId, profile, chooseSuggestedQuestion, closeAccountDialog,
});
const {
setRectificationHeaderSlot,
rectificationPanel,
@@ -554,7 +551,7 @@ export default function Home() {
setRectificationEntrySummarySettled(true);
}
})();
}, [accountId, bootstrapPhase]);
}, [accountId, bootstrapPhase, setRectificationEntrySummary, setRectificationEntrySummarySettled]);
useEffect(() => {
if (!hydrated || !accountId) return;
@@ -568,7 +565,7 @@ export default function Home() {
chartLibraryLoadedAccount.current = "";
queueMicrotask(() => {
setChartLibrary([]);
setSynastryHistory([]);
clearSynastryHistory();
setActiveChartId("self");
});
return;
@@ -578,7 +575,7 @@ export default function Home() {
chartLibraryLoadedAccount.current = accountId;
discardLegacyCloudMirrorKeys(accountId);
setChartLibrary(chartLibraryOnCloudFailure(profileForLibrary, upsertSelfChart));
setSynastryHistory([]);
clearSynastryHistory();
void fetchCloudChartLibrary()
.then((cloudLibrary) => {
setChartLibrary(chartLibraryFromCloudOthers(cloudLibrary, profileForLibrary, upsertSelfChart));
@@ -599,7 +596,7 @@ export default function Home() {
});
void fetchCloudSynastryHistory()
.then((cloudHistory) => {
setSynastryHistory([...cloudHistory].sort((a, b) => b.createdAt - a.createdAt).slice(0, 10));
applyCloudSynastryHistory(cloudHistory);
})
.catch(() => {
setComposerNotice("合盘历史暂时无法读取,请重试。", {
@@ -607,7 +604,7 @@ export default function Home() {
onClick: () => {
void fetchCloudSynastryHistory()
.then((cloudHistory) => {
setSynastryHistory([...cloudHistory].sort((a, b) => b.createdAt - a.createdAt).slice(0, 10));
applyCloudSynastryHistory(cloudHistory);
})
.catch(() => {
setComposerNotice("合盘历史暂时无法读取,请重试。");
@@ -617,7 +614,7 @@ export default function Home() {
});
}
setChartLibrary((current) => upsertSelfChart(current, profileForLibrary));
}, [account, accountId, activeChartId, profile]);
}, [account, accountId, activeChartId, applyCloudSynastryHistory, clearSynastryHistory, profile]);
useEffect(() => {
if (!hydrated || !accountId || chartLibrary.length === 0) return;
@@ -1408,84 +1405,6 @@ export default function Home() {
);
}
async function draftSynastryQuestionFromChart(record: ChartLibraryRecord, relationshipType: SynastryRelationshipType) {
if (record.role !== "other") return;
if (synastryPendingId) return;
const baseQuestion = buildSynastryQuestion(profile, record.profile, relationshipType);
setSynastryPendingId(record.id);
setComposerNotice("正在计算基础合盘证据,请稍候。");
try {
const response = await fetch("/api/synastry", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ partnerChartProfileId: record.id, relationshipType }),
});
const payload = await response.json().catch(() => null) as { status?: string; claimStatus?: string; blockedLayers?: string[]; evidenceLayers?: string[]; synastry?: { total_score?: number; max_score?: number; assessment?: string }; relationshipReport?: { headline?: string; scoreBand?: string; strengths?: string[]; risks?: string[]; nextEvidence?: string[] } } | null;
if (response.ok && payload?.status === "ok") {
const score = payload.synastry?.total_score;
const max = payload.synastry?.max_score;
const assessment = payload.synastry?.assessment;
const layers = (payload.evidenceLayers || []).join(" / ") || "Ashtakoot / Moon / D9";
const evidenceSummary = relationshipType === "business"
? `已完成基础商业合作证据筛查:${layers};声明状态:${payload.claimStatus || "partial"};未用层:${(payload.blockedLayers || []).join(" / ") || "A10 / 双方 Dasha-Narayana / 功能吉凶"}。请勿将其表述为合作保证或精确时点。`
: `已计算基础合盘证据:${layers}Ashtakoot ${score ?? "?"}/${max ?? "?"},初步评级:${assessment || "待解释"}。请基于这个证据包继续分析。`;
const reportCard: SynastryReportCard = {
id: `${record.id}-${timestamp()}`,
partnerName: record.profile.name || "对方",
partnerChartId: record.id,
score,
maxScore: max,
assessment,
headline: payload.relationshipReport?.headline,
scoreBand: payload.relationshipReport?.scoreBand,
strengths: payload.relationshipReport?.strengths,
risks: payload.relationshipReport?.risks,
nextEvidence: payload.relationshipReport?.nextEvidence,
createdAt: timestamp(),
};
let savedReportCard = reportCard;
let historyPersisted = !accountId;
if (accountId) {
try {
savedReportCard = await saveCloudSynastryReport(reportCard);
historyPersisted = true;
} catch {
historyPersisted = false;
}
}
setSynastryReportCard(savedReportCard);
if (accountId && historyPersisted) {
setSynastryHistory((current) => (
[savedReportCard, ...current.filter((item) => item.id !== savedReportCard.id)].slice(0, 10)
));
}
chooseSuggestedQuestion([
baseQuestion,
"",
evidenceSummary,
payload.relationshipReport?.headline ? `结构化摘要:${payload.relationshipReport.headline}` : "",
].join("\n"), relationshipType === "business" ? "career" : "marriage");
if (accountId && !historyPersisted) {
setComposerNotice("未能存入历史");
}
} else {
chooseSuggestedQuestion(baseQuestion, relationshipType === "business" ? "career" : "marriage");
setComposerNotice(response.status === 404
? "请先把对方星盘保存到云端,再用于合盘。"
: response.status === 429
? "合盘请求过于频繁,请稍后再试。"
: payload?.status === "blocked" ? "合盘计算暂时不可用,已先生成问题草稿。" : "已生成合盘问题草稿。");
}
} catch {
chooseSuggestedQuestion(baseQuestion, relationshipType === "business" ? "career" : "marriage");
setComposerNotice("合盘计算暂时不可用,已先生成问题草稿。");
} finally {
setSynastryPendingId(null);
}
closeAccountDialog();
}
async function copyAssistantMessage(renderKey: string, text: string) {
try {
await navigator.clipboard.writeText(text);
@@ -1609,10 +1528,8 @@ export default function Home() {
account={account} accountId={accountId} chartLibrary={chartLibrary} setChartLibrary={setChartLibrary}
profile={profile} profileDraft={profileDraft} setProfileDraft={setProfileDraft} setProfile={setProfile}
profileSaving={profileSaving} profileNotice={profileNotice} setProfileNotice={setProfileNotice} setAccountError={setAccountError}
synastryRelationshipType={synastryRelationshipType} setSynastryRelationshipType={setSynastryRelationshipType}
synastryPendingId={synastryPendingId} synastryReportCard={synastryReportCard} setSynastryReportCard={setSynastryReportCard}
synastryHistory={synastryHistory} activeChartId={activeChartId} setActiveChartId={setActiveChartId}
saveProfile={saveProfile} draftSynastryQuestionFromChart={draftSynastryQuestionFromChart}
{...synastryPanel} activeChartId={activeChartId} setActiveChartId={setActiveChartId}
saveProfile={saveProfile}
/>
</>
);
+143
View File
@@ -0,0 +1,143 @@
"use client";
import { useCallback, useState, type Dispatch, type SetStateAction } from "react";
import { showChatNotice as setComposerNotice } from "@/lib/chat-notice";
import { saveCloudSynastryReport } from "@/lib/home-cloud-sync";
import { buildSynastryQuestion } from "@/lib/home-profile";
import {
timestamp,
type ChartLibraryRecord,
type Profile,
type SynastryRelationshipType,
type SynastryReportCard,
type Theme,
} from "@/lib/home-types";
export type SynastryParams = {
accountId: string | undefined;
profile: Profile;
chooseSuggestedQuestion: (question: string, theme: Theme) => void;
closeAccountDialog: () => void;
};
export type SynastryPanel = {
synastryRelationshipType: SynastryRelationshipType;
setSynastryRelationshipType: Dispatch<SetStateAction<SynastryRelationshipType>>;
synastryPendingId: string | null;
synastryReportCard: SynastryReportCard | null;
setSynastryReportCard: Dispatch<SetStateAction<SynastryReportCard | null>>;
synastryHistory: SynastryReportCard[];
draftSynastryQuestionFromChart: (record: ChartLibraryRecord, relationshipType: SynastryRelationshipType) => Promise<void>;
};
/**
* Owns the four synastry states that used to sit in Home(). Home keeps only the
* two stable history hooks the chart-library effect calls; everything else the
* chart-library dialog needs travels as one `synastryPanel` object.
*/
export function useSynastry(params: SynastryParams) {
const { accountId, profile, chooseSuggestedQuestion, closeAccountDialog } = params;
const [synastryRelationshipType, setSynastryRelationshipType] = useState<SynastryRelationshipType>("romance");
const [synastryPendingId, setSynastryPendingId] = useState<string | null>(null);
const [synastryReportCard, setSynastryReportCard] = useState<SynastryReportCard | null>(null);
const [synastryHistory, setSynastryHistory] = useState<SynastryReportCard[]>([]);
// Stable across renders so Home's chart-library effect can list them in its
// dependency array without gaining a re-run.
const clearSynastryHistory = useCallback(() => {
setSynastryHistory([]);
}, []);
const applyCloudSynastryHistory = useCallback((cloudHistory: SynastryReportCard[]) => {
setSynastryHistory([...cloudHistory].sort((a, b) => b.createdAt - a.createdAt).slice(0, 10));
}, []);
async function draftSynastryQuestionFromChart(record: ChartLibraryRecord, relationshipType: SynastryRelationshipType) {
if (record.role !== "other") return;
if (synastryPendingId) return;
const baseQuestion = buildSynastryQuestion(profile, record.profile, relationshipType);
setSynastryPendingId(record.id);
setComposerNotice("正在计算基础合盘证据,请稍候。");
try {
const response = await fetch("/api/synastry", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ partnerChartProfileId: record.id, relationshipType }),
});
const payload = await response.json().catch(() => null) as { status?: string; claimStatus?: string; blockedLayers?: string[]; evidenceLayers?: string[]; synastry?: { total_score?: number; max_score?: number; assessment?: string }; relationshipReport?: { headline?: string; scoreBand?: string; strengths?: string[]; risks?: string[]; nextEvidence?: string[] } } | null;
if (response.ok && payload?.status === "ok") {
const score = payload.synastry?.total_score;
const max = payload.synastry?.max_score;
const assessment = payload.synastry?.assessment;
const layers = (payload.evidenceLayers || []).join(" / ") || "Ashtakoot / Moon / D9";
const evidenceSummary = relationshipType === "business"
? `已完成基础商业合作证据筛查:${layers};声明状态:${payload.claimStatus || "partial"};未用层:${(payload.blockedLayers || []).join(" / ") || "A10 / 双方 Dasha-Narayana / 功能吉凶"}。请勿将其表述为合作保证或精确时点。`
: `已计算基础合盘证据:${layers}Ashtakoot ${score ?? "?"}/${max ?? "?"},初步评级:${assessment || "待解释"}。请基于这个证据包继续分析。`;
const reportCard: SynastryReportCard = {
id: `${record.id}-${timestamp()}`,
partnerName: record.profile.name || "对方",
partnerChartId: record.id,
score,
maxScore: max,
assessment,
headline: payload.relationshipReport?.headline,
scoreBand: payload.relationshipReport?.scoreBand,
strengths: payload.relationshipReport?.strengths,
risks: payload.relationshipReport?.risks,
nextEvidence: payload.relationshipReport?.nextEvidence,
createdAt: timestamp(),
};
let savedReportCard = reportCard;
let historyPersisted = !accountId;
if (accountId) {
try {
savedReportCard = await saveCloudSynastryReport(reportCard);
historyPersisted = true;
} catch {
historyPersisted = false;
}
}
setSynastryReportCard(savedReportCard);
if (accountId && historyPersisted) {
setSynastryHistory((current) => (
[savedReportCard, ...current.filter((item) => item.id !== savedReportCard.id)].slice(0, 10)
));
}
chooseSuggestedQuestion([
baseQuestion,
"",
evidenceSummary,
payload.relationshipReport?.headline ? `结构化摘要:${payload.relationshipReport.headline}` : "",
].join("\n"), relationshipType === "business" ? "career" : "marriage");
if (accountId && !historyPersisted) {
setComposerNotice("未能存入历史");
}
} else {
chooseSuggestedQuestion(baseQuestion, relationshipType === "business" ? "career" : "marriage");
setComposerNotice(response.status === 404
? "请先把对方星盘保存到云端,再用于合盘。"
: response.status === 429
? "合盘请求过于频繁,请稍后再试。"
: payload?.status === "blocked" ? "合盘计算暂时不可用,已先生成问题草稿。" : "已生成合盘问题草稿。");
}
} catch {
chooseSuggestedQuestion(baseQuestion, relationshipType === "business" ? "career" : "marriage");
setComposerNotice("合盘计算暂时不可用,已先生成问题草稿。");
} finally {
setSynastryPendingId(null);
}
closeAccountDialog();
}
const synastryPanel: SynastryPanel = {
synastryRelationshipType,
setSynastryRelationshipType,
synastryPendingId,
synastryReportCard,
setSynastryReportCard,
synastryHistory,
draftSynastryQuestionFromChart,
};
return { synastryPanel, clearSynastryHistory, applyCloudSynastryHistory };
}