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
+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 };
}