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(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)}`; }