fix(chat): choose the chart person before the first message

This commit is contained in:
jesse-ux
2026-09-22 12:57:04 +08:00
parent 6d06fca0d8
commit 2a3013f224
22 changed files with 973 additions and 27 deletions
@@ -0,0 +1,41 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import type { ChatProfileCatalogStatus } from "@/lib/chat-profile-picker-model";
/**
* Load state for the ordinary-chat person list.
* The page effect still owns the chart-library fetch; this hook only records
* whether that read is in flight, ready, or failed. A failure stays failed
* until an explicit retry, so a self-only fallback cannot look like deletion.
*/
export function useChatProfileCatalog(accountId: string | undefined) {
const accountRef = useRef(accountId);
useEffect(() => {
accountRef.current = accountId;
}, [accountId]);
const [trackedAccount, setTrackedAccount] = useState(accountId);
const [status, setStatus] = useState<ChatProfileCatalogStatus>(accountId ? "loading" : "idle");
if (trackedAccount !== accountId) {
setTrackedAccount(accountId);
setStatus(accountId ? "loading" : "idle");
}
const markReady = useCallback((forAccountId: string) => {
if (accountRef.current !== forAccountId) return;
setStatus("ready");
}, []);
const markFailed = useCallback((forAccountId: string) => {
if (accountRef.current !== forAccountId) return;
setStatus("failed");
}, []);
const beginReload = useCallback(() => {
if (!accountRef.current) return;
setStatus("loading");
}, []);
return { status, markReady, markFailed, beginReload };
}
@@ -117,6 +117,7 @@ export type ConsultationRunParams = {
pendingConsultation: MutableRefObject<PendingConsultation | null>;
pendingSessionId: string | null;
profile: Profile;
subjectDeleted: boolean;
sessions: ChatSession[];
setAccount: Dispatch<SetStateAction<Account | null>>;
setActiveSessionId: Dispatch<SetStateAction<string>>;
@@ -175,6 +176,7 @@ export function useConsultationRun(params: ConsultationRunParams) {
pendingConsultation,
pendingSessionId,
profile,
subjectDeleted,
sessions,
setAccount,
setActiveSessionId,
@@ -513,6 +515,10 @@ export function useConsultationRun(params: ConsultationRunParams) {
: null);
const resumeRequestId = options.resumeRequestId;
const resuming = Boolean(resumeRequestId);
if (!resuming && subjectDeleted) {
setComposerNotice("这份资料已经删除,这段对话只能看。");
return false;
}
const liveSession = targetSessionId
? sessions.find((session) => session.id === targetSessionId)
: activeSession;
+98 -12
View File
@@ -19,7 +19,6 @@ import {
type BirthTimeConsultationConsentState,
} from "@/lib/birth-time-consultation-consent";
import {
activeChartStorageKey,
createSession,
fetchSessionDetail,
fetchSessions,
@@ -34,6 +33,12 @@ import {
isUnsavedEmptyConsultation,
replaceUnsavedEmptyConsultations,
} from "@/lib/session-list-filter";
import {
chatSessionSubjectLocked,
emptySessionBindingWrite,
NEW_CHAT_SUBJECT_ID,
} from "@/lib/chat-profile-picker-model";
import { isConsultationSubjectProfileComplete } from "@/lib/consultation-subject-resolver";
import { chartSnapshotForSession } from "@/lib/home-profile";
import { beginSessionPageLoad, mergeSessionPage } from "@/lib/session-groups";
import type { ConsultationEntrypoint } from "@/lib/consultation-entrypoint";
@@ -91,8 +96,6 @@ export type SessionManagementParams = {
export function useSessionManagement(params: SessionManagementParams) {
const {
account,
accountId,
activeChartId,
activeSession,
activeSessionId,
activeSessionIdRef,
@@ -111,7 +114,6 @@ export function useSessionManagement(params: SessionManagementParams) {
sessionSelectionSource,
sessions,
sessionsRef,
setActiveChartId,
setActiveSessionId,
setBirthTimeConsultationConsent,
setDraft,
@@ -297,7 +299,7 @@ export function useSessionManagement(params: SessionManagementParams) {
if (!account || !modelCatalog || creatingSession) return null;
const nextSession = {
...createSession(modelCatalog.defaultModelId),
...chartSnapshotForSession(activeChartId, chartLibrary, profile),
...chartSnapshotForSession(NEW_CHAT_SUBJECT_ID, chartLibrary, profile),
};
for (const session of sessions) {
if (isUnsavedEmptyConsultation(session)) pendingCreateById.current.delete(session.id);
@@ -330,13 +332,8 @@ export function useSessionManagement(params: SessionManagementParams) {
setComposerNotice("");
setRectificationError("");
setRectificationErrorSessionId(null);
if (nextSession?.chartProfileId) {
const boundChart = chartLibrary.find((record) => record.id === nextSession.chartProfileId);
if (boundChart && boundChart.id !== activeChartId && accountId) {
setActiveChartId(boundChart.id);
localStorage.setItem(activeChartStorageKey(accountId), boundChart.id);
}
}
// Opening a stored session keeps that session's binding. It does not
// follow or rewrite the account-level active chart.
if (nextSession?.sessionType === "birth_time_rectification") {
setRectificationError("");
if (deferredRectificationSwitch) {
@@ -490,6 +487,93 @@ export function useSessionManagement(params: SessionManagementParams) {
});
}
}
function selectableChartRecord(chartProfileId: string) {
const record = chartProfileId === "self"
? chartLibrary.find((item) => item.id === "self" || item.role === "self")
: chartLibrary.find((item) => item.id === chartProfileId && item.role === "other");
if (!record || !isConsultationSubjectProfileComplete(record.profile)) return null;
return record;
}
async function bindEmptyChatSubject(chartProfileId: string): Promise<boolean> {
const session = sessionsRef.current.find((item) => item.id === activeSession?.id) ?? activeSession;
if (!session || chatSessionSubjectLocked(session)) return false;
const record = selectableChartRecord(chartProfileId);
if (!record) return false;
const requestedId = record.role === "self" ? "self" : record.id;
if (session.chartProfileId === requestedId && session.chartProfileRole === record.role) return true;
if (cloudCreatedIds.current.has(session.id)) {
const previousRole = session.chartProfileRole === "self" || session.chartProfileRole === "other"
? session.chartProfileRole
: null;
try {
await writeChatSession(session.id, emptySessionBindingWrite(requestedId, record.role), "update");
const detailed = await fetchSessionDetail(session.id, modelCatalog);
if (detailed?.chartProfileId !== requestedId || detailed.chartProfileRole !== record.role) {
throw new Error("人物没有绑上");
}
updateSession(session.id, (current) => {
if (chatSessionSubjectLocked(current)) return current;
return {
...current,
chartProfileId: detailed.chartProfileId,
chartProfileName: detailed.chartProfileName,
chartProfileRole: detailed.chartProfileRole,
};
});
return true;
} catch (caught) {
if (previousRole) {
try {
await writeChatSession(
session.id,
emptySessionBindingWrite(session.chartProfileId ?? "self", previousRole),
"update",
);
} catch {
setComposerNotice("人物没有绑上");
return false;
}
}
setComposerNotice(caught instanceof Error && caught.message !== "人物没有绑上"
? caught.message
: "人物没有绑上");
return false;
}
}
const snapshot = chartSnapshotForSession(requestedId, chartLibrary, profile);
updateSession(session.id, (current) => {
if (chatSessionSubjectLocked(current)) return current;
return { ...current, ...snapshot };
});
return true;
}
async function openChatBoundToProfile(chartProfileId: string): Promise<boolean> {
if (!account || !modelCatalog || creatingSession) return false;
if (pendingSessionId || cancellationPending) return false;
const record = selectableChartRecord(chartProfileId);
if (!record) return false;
const requestedId = record.role === "self" ? "self" : record.id;
const nextSession = {
...createSession(modelCatalog.defaultModelId),
...chartSnapshotForSession(requestedId, chartLibrary, profile),
};
for (const existing of sessions) {
if (isUnsavedEmptyConsultation(existing)) pendingCreateById.current.delete(existing.id);
}
pendingCreateById.current.set(nextSession.id, {});
setSessions((current) => replaceUnsavedEmptyConsultations(current, nextSession));
setActiveSessionId(nextSession.id);
setDraft("");
setDraftTheme(null);
setDraftEntrypoint(null);
setComposerNotice("");
setRequestError(null);
return true;
}
return {
visibleSessions,
sessionsCursor,
@@ -508,6 +592,8 @@ export function useSessionManagement(params: SessionManagementParams) {
togglePinnedSession,
shareSession,
startNewChat,
bindEmptyChatSubject,
openChatBoundToProfile,
selectSession,
selectSessionModel,
loadMoreSessions,