import type { ChatMessage } from "../types/messages"; import type { ChatMessagePart } from "../types/messageParts"; import { isAssistantMessage } from "../utils/messageParts"; import type { ConversationSnapshot, SerializableChatMessage } from "./types"; const FILE_PLACEHOLDER = "File uploaded"; function isJsonSafe(value: unknown): boolean { try { JSON.stringify(value); return true; } catch { return false; } } function cloneJsonSafe(value: T): T | null { if (!isJsonSafe(value)) { return null; } return JSON.parse(JSON.stringify(value)) as T; } function sanitizeText(text: string): string { return text.replace(/\s+/g, " ").trim(); } function serializePart(part: ChatMessagePart): ChatMessagePart | null { const cloned = cloneJsonSafe(part); if (!cloned) { return null; } if (cloned.type === "text" && cloned.status === "streaming") { return { ...cloned, status: "done" }; } return cloned; } export function getConversationTitle(messages: ChatMessage[]): string | null { const firstUserMessage = messages.find( (message) => message.sender === "user", ); if (!firstUserMessage) { return null; } if ("text" in firstUserMessage) { const title = sanitizeText(firstUserMessage.text); return title || FILE_PLACEHOLDER; } return FILE_PLACEHOLDER; } export function serializeMessages( messages: ChatMessage[], ): SerializableChatMessage[] { const serialized: SerializableChatMessage[] = []; for (const message of messages) { if (message.sender === "user") { const text = "text" in message ? sanitizeText(message.text) : ""; serialized.push({ id: message.id, sender: "user", text: text || FILE_PLACEHOLDER, }); continue; } if (isAssistantMessage(message)) { const parts = message.parts .map(serializePart) .filter((part): part is ChatMessagePart => part !== null); serialized.push({ id: message.id, sender: "bot", type: "assistant", parts, transparent: message.transparent, }); continue; } if ("text" in message) { serialized.push({ id: message.id, sender: "bot", text: message.text, transparent: message.transparent, }); continue; } const cloned = cloneJsonSafe(message); if (cloned) { serialized.push(cloned); } } return serialized; } export function createConversationSnapshot(params: { sessionId: string; messages: ChatMessage[]; existingSnapshot?: ConversationSnapshot | null; now?: number; }): ConversationSnapshot | null { const title = getConversationTitle(params.messages); if (!title) { return null; } const now = params.now ?? Date.now(); return { schemaVersion: 1, sessionId: params.sessionId, title, messages: serializeMessages(params.messages), createdAt: params.existingSnapshot?.createdAt ?? now, updatedAt: now, }; } export function restoreMessages( messages: SerializableChatMessage[], ): ChatMessage[] { return messages.map((message) => { if (isAssistantMessage(message)) { const restored = { ...message, parts: message.parts.map((part) => part.type === "text" ? ({ ...part, status: "done" } as const) : part, ), transientParts: undefined, }; return restored as ChatMessage; } return { ...message }; }); }