"use client"; import type { Dispatch, MutableRefObject, SetStateAction } from "react"; import { resolveSessionTitle } from "@/lib/agent-reply"; import { showChatNotice as setComposerNotice } from "@/lib/chat-notice"; import { writeSessionUrl } from "@/lib/chat-session-url"; import type { ConsultationEntrypoint } from "@/lib/consultation-entrypoint"; import { chartSnapshotForSession, missingProfileStep } from "@/lib/home-profile"; import { timestamp, type Account, type AccountDialog, type ChartLibraryRecord, type ChatSession, type Message, type OnboardingStep, type Profile, type Theme, } from "@/lib/home-types"; import { entrySummaryFromResponse, isTerminalRectificationStatus, openRectificationRequestBody, openResponseFromPayload, type RectificationEntrySummary, } from "@/lib/rectification-entry"; import type { PersistedRectificationTurn } from "@/components/conversational-birth-time-rectification"; import type { PublicLanguageModelCatalog } from "@/lib/public-models"; export type RectificationSurfaceParams = { account: Account | null; activeChartId: string; activeSessionIdRef: MutableRefObject; chartLibrary: ChartLibraryRecord[]; creatingSession: boolean; modelCatalog: PublicLanguageModelCatalog | null; persistSession: (session: ChatSession, mode?: "create" | "update") => Promise; profile: Profile; rectificationLoading: boolean; rectificationMutationPending: boolean; rectificationOpenInFlight: MutableRefObject; rectificationSessionId: string | null; resumeRectificationSession: MutableRefObject<(session: ChatSession) => void>; sessionSelectionSource: MutableRefObject<"user" | "history">; sessions: ChatSession[]; setActiveSessionId: Dispatch>; setDraft: (value: string) => void; setDraftEntrypoint: (entrypoint: ConsultationEntrypoint | null) => void; setDraftTheme: (theme: Theme | null) => void; setOnboardingStep: Dispatch>; setProfileNotice: Dispatch>; setRectificationCaseId: Dispatch>; setRectificationEntrySummary: Dispatch>; setRectificationError: Dispatch>; setRectificationLoading: Dispatch>; setRectificationPendingQuestion: Dispatch>; setRectificationReadonly: Dispatch>; setRectificationSessionId: Dispatch>; setRectificationShouldStartOpening: Dispatch>; setRectificationTurns: Dispatch>; setSessions: Dispatch>; uiPreview: MutableRefObject; updateSession: (sessionId: string, change: (session: ChatSession) => ChatSession) => void; openAccountDialog: (dialog: AccountDialog, returnTarget?: HTMLButtonElement | null) => void; refreshAccount: () => Promise; }; export function useRectificationSurface(params: RectificationSurfaceParams) { const { account, activeChartId, activeSessionIdRef, chartLibrary, creatingSession, modelCatalog, persistSession, profile, rectificationLoading, rectificationMutationPending, rectificationOpenInFlight, rectificationSessionId, resumeRectificationSession, sessionSelectionSource, sessions, setActiveSessionId, setDraft, setDraftEntrypoint, setDraftTheme, setOnboardingStep, setProfileNotice, setRectificationCaseId, setRectificationEntrySummary, setRectificationError, setRectificationLoading, setRectificationPendingQuestion, setRectificationReadonly, setRectificationSessionId, setRectificationShouldStartOpening, setRectificationTurns, setSessions, uiPreview, updateSession, openAccountDialog, refreshAccount, } = params; async function refreshRectificationEntrySummary() { if (!account) return; try { const response = await fetch("/api/rectification/cases/entry-summary", { cache: "no-store" }); if (!response.ok) return; const payload = await response.json().catch(() => null); setRectificationEntrySummary(entrySummaryFromResponse(payload)); } catch { // The CTA falls back to the server-agnostic default labels. } } async function refreshRectificationCase(caseId: string, sessionId: string) { try { const response = await fetch( `/api/rectification/cases/${encodeURIComponent(caseId)}?sessionId=${encodeURIComponent(sessionId)}`, { cache: "no-store" }, ); if (!response.ok) return; const payload = await response.json().catch(() => null); const turns = Array.isArray(payload?.turns) ? payload.turns : []; setRectificationTurns(turns.map((turn: { id?: unknown; role?: unknown; text?: unknown; status?: unknown; receipt?: unknown }) => ({ id: String(turn?.id ?? ""), role: turn?.role === "user" ? "user" as const : "assistant" as const, text: typeof turn?.text === "string" ? turn.text : null, status: String(turn?.status ?? "completed"), receipt: turn?.receipt && typeof turn.receipt === "object" ? { status: String((turn.receipt as { status?: unknown }).status ?? ""), phases: Array.isArray((turn.receipt as { phases?: unknown }).phases) ? (turn.receipt as { phases: unknown[] }).phases.map(String) : [], tools: Array.isArray((turn.receipt as { tools?: unknown }).tools) ? (turn.receipt as { tools: unknown[] }).tools.map(String) : [], methods: Array.isArray((turn.receipt as { methods?: unknown }).methods) ? (turn.receipt as { methods: unknown[] }).methods.map(String) : [], skill_name: typeof (turn.receipt as { skill_name?: unknown }).skill_name === "string" ? (turn.receipt as { skill_name: string }).skill_name : undefined, skill_version: typeof (turn.receipt as { skill_version?: unknown }).skill_version === "string" ? (turn.receipt as { skill_version: string }).skill_version : undefined, } : null, }))); } catch { // History refresh is best-effort; the stream restores live turns. } } async function openRectificationCase( intent: "homepage" | "session" | "new", exactSessionId: string | null, pendingConsultationQuestion: string | null, ) { if (!account || !modelCatalog || creatingSession || rectificationLoading || rectificationOpenInFlight.current || rectificationMutationPending) { sessionSelectionSource.current = "user"; return null; } const missingStep = missingProfileStep(profile); if (missingStep) { sessionSelectionSource.current = "user"; setRectificationSessionId(null); setRectificationCaseId(null); setRectificationPendingQuestion(null); setOnboardingStep(missingStep); setComposerNotice("请先完成出生资料,再开始生时校正。"); return null; } rectificationOpenInFlight.current = true; setRectificationLoading(true); setRectificationError(""); try { const response = await fetch("/api/rectification/cases/open", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(openRectificationRequestBody(intent, exactSessionId)), }); const payload = await response.json().catch(() => null); if (!response.ok) { const code = payload?.code; if (code === "profile_incomplete") { handleRectificationProfileIncomplete(); } else if (code === "invalid_open_request") { setRectificationError(payload?.error || "生时校正打开请求不正确,请刷新后重试。"); } else if (code === "active_case_conflict") { setRectificationError(payload?.error || "仍有未完成的校正,请先回到当前校正。"); } else if (code === "case_session_not_found") { setRectificationError("该校正会话不存在或已结束。"); } else { setRectificationError(payload?.error || "暂时无法打开生时校正。"); } return null; } const opened = openResponseFromPayload(payload); if (!opened) { setRectificationError("生时校正服务响应不正确,请稍后重试。"); return null; } // Merge the server-created session into the local list. The browser // never generates a Case id; it only mirrors the returned binding. const existing = sessions.find((session) => session.id === opened.sessionId); const merged: ChatSession = { id: opened.sessionId, title: resolveSessionTitle("生时校正", undefined, { entrypoint: "birth_time_rectification", existingTitles: sessions.map((session) => session.title), }), theme: "general", modelId: modelCatalog.defaultModelId ?? "", messages: [], updatedAt: timestamp(), sessionType: "birth_time_rectification", rectificationCaseId: opened.caseId, pinned: existing?.pinned ?? false, archivedAt: existing?.archivedAt ?? null, messagesHydrated: true, ...chartSnapshotForSession(activeChartId, chartLibrary, profile), }; setSessions((current) => [merged, ...current.filter((session) => session.id !== merged.id)]); void persistSession(merged).catch(() => {}); setRectificationPendingQuestion(pendingConsultationQuestion); setDraft(""); setDraftTheme(null); setDraftEntrypoint(null); setRectificationSessionId(opened.sessionId); setRectificationCaseId(opened.caseId); setRectificationShouldStartOpening(opened.shouldStartOpening); setRectificationReadonly( opened.disposition === "readonly" || isTerminalRectificationStatus(opened.status), ); setRectificationTurns([]); activeSessionIdRef.current = opened.sessionId; setActiveSessionId(opened.sessionId); if (!uiPreview.current && sessionSelectionSource.current === "user") { writeSessionUrl(opened.sessionId, "push"); } void refreshRectificationCase(opened.caseId, opened.sessionId); void refreshRectificationEntrySummary(); return opened; } catch { setRectificationError("生时校正会话暂时无法打开,请稍后重试。"); return null; } finally { sessionSelectionSource.current = "user"; rectificationOpenInFlight.current = false; setRectificationLoading(false); } } async function openRectificationFromHomepage(pendingConsultationQuestion: string | null = null) { await openRectificationCase("homepage", null, pendingConsultationQuestion); } async function openRectificationSession(exactSessionId: string) { await openRectificationCase("session", exactSessionId, null); } async function startNewRectification() { await openRectificationCase("new", null, null); } resumeRectificationSession.current = (session) => { sessionSelectionSource.current = "history"; void openRectificationSession(session.id); }; function handleRectificationProfileIncomplete() { setRectificationError("profile_incomplete"); setRectificationSessionId(null); setRectificationCaseId(null); setRectificationPendingQuestion(null); const missingStep = missingProfileStep(profile); if (missingStep) { setOnboardingStep(missingStep); setComposerNotice("请先完成出生资料,再开始生时校正。"); return; } openAccountDialog("profile"); setProfileNotice("服务端未能读取完整出生资料,请重新确认并保存。"); void refreshAccount(); } function handleRectificationMessagesChange(messages: Message[]) { if (!rectificationSessionId) return; updateSession(rectificationSessionId, (session) => ({ ...session, messages, updatedAt: timestamp(), })); } return { refreshRectificationEntrySummary, refreshRectificationCase, openRectificationCase, openRectificationFromHomepage, openRectificationSession, startNewRectification, handleRectificationProfileIncomplete, handleRectificationMessagesChange, }; }