|
|
|
@@ -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,
|
|
|
|
|
};
|
|
|
|
|
}
|