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
144 lines
6.7 KiB
TypeScript
144 lines
6.7 KiB
TypeScript
"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 };
|
||
}
|