feat: release chat and mindspace UI updates

This commit is contained in:
john
2026-06-27 21:57:21 +08:00
parent c924b2bb77
commit 8264e71f9e
24 changed files with 2042 additions and 209 deletions
+20
View File
@@ -113,6 +113,26 @@ export function normalizeConversationMessages(messages: Message[]): Message[] {
);
}
/**
* Merge an authoritative server conversation snapshot into the currently
* displayed messages without ever erasing what the user can already see.
*
* `UpdateConversation` snapshots arrive on every SSE (re)connect — which on
* mobile happens constantly via the page visibility cycle. A mid-turn snapshot
* can be shorter than the live view (e.g. assistant messages not yet flagged
* userVisible), so a blind replace would blank an active conversation even
* though the backend session is alive and persisted. We therefore treat the
* snapshot as authoritative for content/order but keep any locally-displayed
* messages the snapshot hasn't caught up to yet (optimistic / in-flight tail).
* The authoritative full reload on `Finish` corrects any drift afterwards.
*/
export function mergeConversationSnapshot(current: Message[], incoming: Message[]): Message[] {
if (incoming.length === 0) return current;
const incomingIds = new Set(incoming.map((message) => message.id).filter(Boolean));
const localOnly = current.filter((message) => message.id && !incomingIds.has(message.id));
return localOnly.length ? [...incoming, ...localOnly] : incoming;
}
export function getDisplayText(message: Message): string {
if ('displayText' in message.metadata) {
return message.metadata.displayText ?? '';
+46 -5
View File
@@ -3,9 +3,18 @@ import type { Session } 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: Session): boolean {
if (session.recipe) return false;
return !session.user_set_name && session.message_count === 0;
return !session.user_set_name && session.message_count === 0 && isDefaultSessionName(session.name);
}
export function sortAndTrim(sessions: Session[]): Session[] {
@@ -18,9 +27,39 @@ export function sortAndTrim(sessions: Session[]): Session[] {
.slice(0, appConfig.sessionMaxCount);
}
export function hasMeaningfulSessionTitle(session: Session): boolean {
if (session.user_set_name) return Boolean(session.name.trim());
if (session.recipe?.title?.trim()) return true;
return !isDefaultSessionName(session.name);
}
export function mergeSessionRecord(existing: Session | undefined, incoming: Session): Session {
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: Session[], incoming: Session[]): Session[] {
const previousById = new Map(prev.map((item) => [item.id, item]));
return sortAndTrim(incoming.map((item) => mergeSessionRecord(previousById.get(item.id), item)));
}
export function prependUnique(prev: Session[], session: Session): Session[] {
if (prev.some((s) => s.id === session.id)) return sortAndTrim(prev);
return sortAndTrim([session, ...prev.filter((s) => s.id !== session.id)]);
return sortAndTrim([
mergeSessionRecord(prev.find((s) => s.id === session.id), session),
...prev.filter((s) => s.id !== session.id),
]);
}
export function touchSession(sessions: Session[], sessionId: string, messageDelta = 0): Session[] {
@@ -102,9 +141,11 @@ export function groupSessionsByDate(sessions: Session[]): SessionDateGroup[] {
return groups;
}
const DEFAULT_SESSION_NAMES = new Set([DEFAULT_CHAT_TITLE, '新对话']);
export function getSessionListLabel(session: Session): string {
const displayName = getSessionDisplayName(session);
if (displayName !== DEFAULT_CHAT_TITLE && displayName !== '新对话') {
const displayName = getSessionDisplayName(session).trim();
if (displayName && !DEFAULT_SESSION_NAMES.has(displayName) && !isGeneratedSessionName(displayName)) {
return displayName;
}
if (session.message_count > 0) {