Files
Jyotisha/frontend/src/lib/chat-profile-picker-model.ts
T
jesse-ux d08ffdc0a6
Independent Staging Quality Gate / validate (push) Failing after 6m46s
Independent Staging Quality Gate / publish (push) Skipped
fix: keep western labels near their planets and chats on the current person
Cap planet captions at 30 degrees with a third ring, fall back only within the current person's sessions, and load self when the people catalog fails.
2026-09-25 23:02:17 +08:00

157 lines
5.9 KiB
TypeScript

import { readCurrentSubjectId } from "@/lib/current-subject";
import { isConsultationSubjectProfileComplete } from "@/lib/consultation-subject-resolver";
import { sessionChartLabel } from "@/lib/home-profile";
import type { ChartLibraryRecord, ChatSession } from "@/lib/home-types";
export type ChatProfileCatalogStatus = "idle" | "loading" | "ready" | "failed" | "degraded";
/** New ordinary chats follow the current person, not a fixed self id. */
export function NEW_CHAT_SUBJECT_ID(): string {
return readCurrentSubjectId();
}
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 sessionMatchesSubject(
session: Pick<ChatSession, "chartProfileId" | "chartProfileRole">,
subjectId: string,
): boolean {
const id = session.chartProfileId?.trim() || null;
if (!subjectId || subjectId === "self") {
return id == null || id === "self" || session.chartProfileRole === "self";
}
return id === subjectId;
}
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" && input.status !== "degraded") 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",
});
}
if (input.status === "degraded") return rows;
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" ? "本人" : (record.profile.date || "其他人"))
: "这份资料还不完整",
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,
};
}