feat: release chat and mindspace UI updates
This commit is contained in:
@@ -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
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user