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,141 @@
import { isConsultationSubjectProfileComplete } from "@/lib/consultation-subject-resolver";
import { chartRelationshipLabel, sessionChartLabel } from "@/lib/home-profile";
import type { ChartLibraryRecord, ChatSession } from "@/lib/home-types";
export type ChatProfileCatalogStatus = "idle" | "loading" | "ready" | "failed";
/** New ordinary chats show self. They do not inherit the account's active chart. */
export const NEW_CHAT_SUBJECT_ID = "self" as const;
export type ChatProfileChoice =
| { readonly action: "bind"; readonly chartProfileId: string }
| { readonly action: "open-new"; readonly chartProfileId: string }
| { readonly action: "ignore" };
export type ChatProfilePickerRow = {
readonly id: string;
readonly role: "self" | "other";
readonly name: string;
readonly detail: string;
readonly complete: boolean;
readonly selected: boolean;
};
type SubjectSession = Pick<ChatSession, "chartProfileId" | "chartProfileName" | "chartProfileRole" | "sessionType" | "messagesHydrated" | "messages">;
export function chatSessionSubjectLocked(session: {
sessionType: string;
messagesHydrated: boolean;
messages: readonly unknown[];
}): boolean {
if (session.sessionType !== "consultation") return true;
if (!session.messagesHydrated) return true;
return session.messages.length > 0;
}
export function chatProfileSelectedId(session: Pick<ChatSession, "chartProfileId" | "chartProfileRole">): string {
if (session.chartProfileRole === "other" || (session.chartProfileId && session.chartProfileId !== "self")) {
return session.chartProfileId ?? "";
}
return "self";
}
export function chatProfileDisplayName(input: {
session: Pick<ChatSession, "chartProfileId" | "chartProfileName" | "chartProfileRole">;
status: ChatProfileCatalogStatus;
library: readonly ChartLibraryRecord[];
}): string {
if (input.status !== "ready") {
const snapshot = input.session.chartProfileName?.trim();
if (snapshot) return snapshot;
if (!input.session.chartProfileId || input.session.chartProfileId === "self" || input.session.chartProfileRole !== "other") {
return "我";
}
return "未命名资料";
}
return sessionChartLabel(input.session, input.library);
}
export function chatSubjectSendBlocked(input: {
session: Pick<ChatSession, "sessionType" | "chartProfileId" | "chartProfileRole"> | undefined;
status: ChatProfileCatalogStatus;
library: readonly ChartLibraryRecord[];
}): boolean {
if (!input.session || input.session.sessionType !== "consultation") return false;
if (input.status !== "ready") return false;
const id = input.session.chartProfileId;
if (!id || id === "self" || input.session.chartProfileRole === "self") return false;
return !input.library.some((record) => record.id === id && record.role === "other");
}
function rowName(record: ChartLibraryRecord): string {
return record.profile.name.trim() || (record.role === "self" ? "我" : "未命名资料");
}
export function chatProfilePickerRows(input: {
status: ChatProfileCatalogStatus;
library: readonly ChartLibraryRecord[];
selectedId: string;
}): ChatProfilePickerRow[] {
if (input.status !== "ready") return [];
const self = input.library.find((record) => record.role === "self" || record.id === "self");
const others = input.library.filter((record) => record.role === "other" && record.id !== "self");
const rows: ChatProfilePickerRow[] = [];
if (self) rows.push(toRow(self, input.selectedId));
else {
rows.push({
id: "self",
role: "self",
name: "我",
detail: "这份资料还不完整",
complete: false,
selected: input.selectedId === "self",
});
}
for (const record of others) rows.push(toRow(record, input.selectedId));
return rows;
}
function toRow(record: ChartLibraryRecord, selectedId: string): ChatProfilePickerRow {
const complete = isConsultationSubjectProfileComplete(record.profile);
const id = record.role === "self" ? "self" : record.id;
return {
id,
role: record.role,
name: rowName(record),
detail: complete
? (record.role === "self" ? "本人" : chartRelationshipLabel(record.relationship))
: "这份资料还不完整",
complete,
selected: id === selectedId,
};
}
export function chatProfileChoice(input: {
session: Pick<SubjectSession, "sessionType" | "messagesHydrated" | "messages" | "chartProfileId">;
chartProfileId: string;
requestInFlight: boolean;
libraryReady: boolean;
selectable: boolean;
}): ChatProfileChoice {
if (!input.libraryReady || !input.selectable || !input.chartProfileId) return { action: "ignore" };
if (input.session.sessionType !== "consultation") return { action: "ignore" };
if (chatSessionSubjectLocked(input.session)) {
if (input.chartProfileId === (input.session.chartProfileId || "self")) return { action: "ignore" };
if (input.requestInFlight) return { action: "ignore" };
return { action: "open-new", chartProfileId: input.chartProfileId };
}
if (input.chartProfileId === input.session.chartProfileId) return { action: "ignore" };
return { action: "bind", chartProfileId: input.chartProfileId };
}
/** Empty-session PATCH body. The server fills the name; the client does not send one. */
export function emptySessionBindingWrite(chartProfileId: string, role: "self" | "other"): {
chart_profile_id: string;
chart_profile_role: "self" | "other";
} {
return {
chart_profile_id: role === "self" ? "self" : chartProfileId,
chart_profile_role: role,
};
}
@@ -149,6 +149,12 @@ function otherProfileComplete(row: Record<string, unknown>): boolean {
return Boolean(name && birthDate && source && status && latitude && longitude && label && (timezoneOffset || timezoneId));
}
/** Same completeness gate the resolver uses before it will bind a person. */
export function isConsultationSubjectProfileComplete(profile: unknown): boolean {
const row = chartProfileToConsultationRow(profile);
return Boolean(row && otherProfileComplete(row));
}
export function consultationSubjectBindingFromSession(row: {
chart_profile_id?: unknown;
chart_profile_name?: unknown;
+13 -4
View File
@@ -97,11 +97,20 @@ export function chartSnapshotForSession(
return { chartProfileId: chartId || null, chartProfileName: "未命名资料", chartProfileRole: chartId ? "other" : null };
}
export function sessionChartLabel(session: ChatSession, library: readonly ChartLibraryRecord[]) {
export function sessionChartLabel(
session: Pick<ChatSession, "chartProfileId" | "chartProfileName" | "chartProfileRole">,
library: readonly ChartLibraryRecord[],
) {
if (!session.chartProfileId) return "未关联资料";
const current = session.chartProfileId === "self" || library.some((record) => record.id === session.chartProfileId);
const name = session.chartProfileName?.trim() || (session.chartProfileRole === "self" ? "我" : "未命名资料");
return current ? name : `资料已删除 · ${name}`;
const record = session.chartProfileId === "self"
? library.find((item) => item.id === "self" || item.role === "self")
: library.find((item) => item.id === session.chartProfileId && item.role === "other");
if (record) {
return record.profile.name.trim() || (record.role === "self" ? "我" : "未命名资料");
}
if (session.chartProfileId === "self") return session.chartProfileName?.trim() || "我";
const name = session.chartProfileName?.trim() || "未命名资料";
return `资料已删除 · ${name}`;
}
export function upsertSelfChart(library: ChartLibraryRecord[], profile: Profile) {
@@ -61,8 +61,10 @@ export function ordinaryComposerPlaceholder(input: {
onboardingName: boolean;
presetMessageFinished: boolean;
creditsEmpty: boolean;
subjectDeleted?: boolean;
}): string {
if (input.locked) return RECTIFICATION_OPENING_COMPOSER_PLACEHOLDER;
if (input.subjectDeleted) return "这份资料已经删除,这段对话只能看";
if (!input.hasAccount) return "正在读取账户…";
if (!input.profileComplete) {
if (!input.onboardingName) return "请先完成上方资料";