Files
memind/src/utils/sessions.ts
T
2026-06-29 22:20:04 +08:00

183 lines
5.8 KiB
TypeScript

import type { Session, SessionSummary } from '../types';
export const DEFAULT_CHAT_TITLE = 'New Chat';
function isGeneratedSessionName(name: string): boolean {
return /^20\d{6}(?:[_-]\d+)?$/.test(name.trim());
}
function isDefaultSessionName(name: string): boolean {
const trimmed = name.trim();
return !trimmed || trimmed === DEFAULT_CHAT_TITLE || trimmed === '新对话' || isGeneratedSessionName(trimmed);
}
export function shouldShowNewChatTitle(session: SessionSummary): boolean {
if (session.recipe) return false;
return !session.user_set_name && session.message_count === 0 && isDefaultSessionName(session.name);
}
export function sortAndTrim<T extends SessionSummary>(sessions: T[]): T[] {
return [...sessions]
.sort((a, b) => {
const aTime = new Date(a.updated_at ?? a.created_at ?? 0).getTime();
const bTime = new Date(b.updated_at ?? b.created_at ?? 0).getTime();
return bTime - aTime;
});
}
export function hasMeaningfulSessionTitle(session: SessionSummary): boolean {
if (session.user_set_name) return Boolean(session.name.trim());
if (session.recipe?.title?.trim()) return true;
return !isDefaultSessionName(session.name);
}
export function toSessionSummary(session: Session | SessionSummary): SessionSummary {
return {
id: session.id,
name: session.name,
message_count: session.message_count,
created_at: session.created_at,
updated_at: session.updated_at,
user_set_name: session.user_set_name,
recipe: session.recipe ?? null,
};
}
export function mergeSessionRecord(
existing: SessionSummary | undefined,
incoming: SessionSummary,
): SessionSummary {
if (!existing) return incoming;
const keepExistingTitle =
hasMeaningfulSessionTitle(existing) && !hasMeaningfulSessionTitle(incoming);
if (!keepExistingTitle) return { ...existing, ...incoming, id: incoming.id };
return {
...existing,
...incoming,
id: incoming.id,
name: existing.name,
user_set_name: existing.user_set_name,
recipe: existing.recipe ?? incoming.recipe,
};
}
export function mergeSessionLists(prev: SessionSummary[], incoming: SessionSummary[]): SessionSummary[] {
const previousById = new Map(prev.map((item) => [item.id, item]));
return sortAndTrim(incoming.map((item) => mergeSessionRecord(previousById.get(item.id), item)));
}
export function appendSessionLists(prev: SessionSummary[], incoming: SessionSummary[]): SessionSummary[] {
const previousById = new Map(prev.map((item) => [item.id, item]));
const mergedIncoming = incoming.map((item) => mergeSessionRecord(previousById.get(item.id), item));
const incomingIds = new Set(mergedIncoming.map((item) => item.id));
return sortAndTrim([
...prev.filter((item) => !incomingIds.has(item.id)),
...mergedIncoming,
]);
}
export function prependUnique(prev: SessionSummary[], session: SessionSummary): SessionSummary[] {
return sortAndTrim([
mergeSessionRecord(prev.find((s) => s.id === session.id), session),
...prev.filter((s) => s.id !== session.id),
]);
}
export function touchSession(
sessions: SessionSummary[],
sessionId: string,
messageDelta = 0,
): SessionSummary[] {
const now = new Date().toISOString();
return sortAndTrim(
sessions.map((s) =>
s.id === sessionId
? {
...s,
updated_at: now,
message_count: Math.max(0, s.message_count + messageDelta),
}
: s,
),
);
}
export function getSessionDisplayName(session: SessionSummary): string {
if (session.user_set_name) return session.name;
if (session.recipe?.title) return session.recipe.title;
if (shouldShowNewChatTitle(session)) return DEFAULT_CHAT_TITLE;
return session.name;
}
function sameLocalDay(a: Date, b: Date): boolean {
return (
a.getFullYear() === b.getFullYear() &&
a.getMonth() === b.getMonth() &&
a.getDate() === b.getDate()
);
}
export function getSessionDateKey(session: SessionSummary): string {
const iso = session.updated_at ?? session.created_at;
if (!iso) return '';
const date = new Date(iso);
if (Number.isNaN(date.getTime())) return '';
return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
}
export function formatSessionDateLabel(session: SessionSummary): string {
const iso = session.updated_at ?? session.created_at;
if (!iso) return '';
const date = new Date(iso);
if (Number.isNaN(date.getTime())) return '';
const now = new Date();
if (sameLocalDay(date, now)) return '今天';
const yesterday = new Date(now);
yesterday.setDate(yesterday.getDate() - 1);
if (sameLocalDay(date, yesterday)) return '昨天';
if (date.getFullYear() === now.getFullYear()) {
return `${date.getMonth() + 1}${date.getDate()}`;
}
return `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}`;
}
export type SessionDateGroup = {
dateKey: string;
label: string;
sessions: SessionSummary[];
};
export function groupSessionsByDate(sessions: SessionSummary[]): SessionDateGroup[] {
const groups: SessionDateGroup[] = [];
for (const session of sessions) {
const dateKey = getSessionDateKey(session);
const label = formatSessionDateLabel(session) || '未知日期';
const last = groups[groups.length - 1];
if (last && last.dateKey === dateKey) {
last.sessions.push(session);
} else {
groups.push({ dateKey, label, sessions: [session] });
}
}
return groups;
}
const DEFAULT_SESSION_NAMES = new Set([DEFAULT_CHAT_TITLE, '新对话']);
export function getSessionListLabel(session: SessionSummary): string {
const displayName = getSessionDisplayName(session).trim();
if (displayName && !DEFAULT_SESSION_NAMES.has(displayName) && !isGeneratedSessionName(displayName)) {
return displayName;
}
if (session.message_count > 0) {
return `对话 · ${session.message_count} 条消息`;
}
return `会话 ${session.id.slice(0, 8)}`;
}