import { filterByLeafNodeId, findLeafNode } from '$lib/utils';
import type { McpServerOverride } from '$lib/types/database';
import { MessageRole } from '$lib/enums';
+import {
+ ISO_DATE_TIME_SEPARATOR,
+ ISO_DATE_TIME_SEPARATOR_REPLACEMENT,
+ ISO_TIMESTAMP_SLICE_LENGTH,
+ EXPORT_CONV_ID_TRIM_LENGTH,
+ EXPORT_CONV_NONALNUM_REPLACEMENT,
+ EXPORT_CONV_NAME_SUFFIX_MAX_LENGTH,
+ ISO_TIME_SEPARATOR,
+ ISO_TIME_SEPARATOR_REPLACEMENT,
+ NON_ALPHANUMERIC_REGEX,
+ MULTIPLE_UNDERSCORE_REGEX
+} from '$lib/constants';
class ConversationsStore {
/**
*/
/**
- * Downloads a conversation as JSON file.
- * @param convId - The conversation ID to download
+ * Generates a sanitized filename for a conversation export
+ * @param conversation - The conversation metadata
+ * @param msgs - Optional array of messages belonging to the conversation
+ * @returns The generated filename string
*/
- async downloadConversation(convId: string): Promise<void> {
- let conversation: DatabaseConversation | null;
- let messages: DatabaseMessage[];
+ generateConversationFilename(
+ conversation: { id?: string; name?: string },
+ msgs?: DatabaseMessage[]
+ ): string {
+ const conversationName = (conversation.name ?? '').trim().toLowerCase();
- if (this.activeConversation?.id === convId) {
- conversation = this.activeConversation;
- messages = this.activeMessages;
- } else {
- conversation = await DatabaseService.getConversation(convId);
- if (!conversation) return;
- messages = await DatabaseService.getConversationMessages(convId);
- }
+ const sanitizedName = conversationName
+ .replace(NON_ALPHANUMERIC_REGEX, EXPORT_CONV_NONALNUM_REPLACEMENT)
+ .replace(MULTIPLE_UNDERSCORE_REGEX, '_')
+ .substring(0, EXPORT_CONV_NAME_SUFFIX_MAX_LENGTH);
+
+ // If we have messages, use the timestamp of the newest message
+ const referenceDate = msgs?.length
+ ? new Date(Math.max(...msgs.map((m) => m.timestamp)))
+ : new Date();
- this.triggerDownload({ conv: conversation, messages });
+ const iso = referenceDate.toISOString().slice(0, ISO_TIMESTAMP_SLICE_LENGTH);
+ const formattedDate = iso
+ .replace(ISO_DATE_TIME_SEPARATOR, ISO_DATE_TIME_SEPARATOR_REPLACEMENT)
+ .replaceAll(ISO_TIME_SEPARATOR, ISO_TIME_SEPARATOR_REPLACEMENT);
+ const trimmedConvId = conversation.id?.slice(0, EXPORT_CONV_ID_TRIM_LENGTH) ?? '';
+ return `${formattedDate}_conv_${trimmedConvId}_${sanitizedName}.json`;
}
/**
- * Exports all conversations with their messages as a JSON file
- * @returns The list of exported conversations
+ * Triggers a browser download of the provided exported conversation data
+ * @param data - The exported conversation payload (either a single conversation or array of them)
+ * @param filename - Filename; if omitted, a deterministic name is generated
*/
- async exportAllConversations(): Promise<DatabaseConversation[]> {
- const allConversations = await DatabaseService.getAllConversations();
+ downloadConversationFile(data: ExportedConversations, filename?: string): void {
+ // Choose the first conversation or message
+ const conversation =
+ 'conv' in data ? data.conv : Array.isArray(data) ? data[0]?.conv : undefined;
+ const msgs =
+ 'messages' in data ? data.messages : Array.isArray(data) ? data[0]?.messages : undefined;
- if (allConversations.length === 0) {
- throw new Error('No conversations to export');
+ if (!conversation) {
+ console.error('Invalid data: missing conversation');
+ return;
}
- const allData = await Promise.all(
- allConversations.map(async (conv) => {
- const messages = await DatabaseService.getConversationMessages(conv.id);
- return { conv, messages };
- })
- );
+ const downloadFilename = filename ?? this.generateConversationFilename(conversation, msgs);
- const blob = new Blob([JSON.stringify(allData, null, 2)], { type: 'application/json' });
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
- a.download = `all_conversations_${new Date().toISOString().split('T')[0]}.json`;
+ a.download = downloadFilename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
+ }
+
+ /**
+ * Downloads a conversation as JSON file.
+ * @param convId - The conversation ID to download
+ */
+ async downloadConversation(convId: string): Promise<void> {
+ let conversation: DatabaseConversation | null;
+ let messages: DatabaseMessage[];
- toast.success(`All conversations (${allConversations.length}) prepared for download`);
+ if (this.activeConversation?.id === convId) {
+ conversation = this.activeConversation;
+ messages = this.activeMessages;
+ } else {
+ conversation = await DatabaseService.getConversation(convId);
+ if (!conversation) return;
+ messages = await DatabaseService.getConversationMessages(convId);
+ }
- return allConversations;
+ this.downloadConversationFile({ conv: conversation, messages });
}
/**
await this.loadConversations();
return result;
}
-
- /**
- * Triggers file download in browser
- */
- private triggerDownload(data: ExportedConversations, filename?: string): void {
- const conversation =
- 'conv' in data ? data.conv : Array.isArray(data) ? data[0]?.conv : undefined;
-
- if (!conversation) {
- console.error('Invalid data: missing conversation');
- return;
- }
-
- const conversationName = conversation.name?.trim() || '';
- const truncatedSuffix = conversationName
- .toLowerCase()
- .replace(/[^a-z0-9]/gi, '_')
- .replace(/_+/g, '_')
- .substring(0, 20);
- const downloadFilename = filename || `conversation_${conversation.id}_${truncatedSuffix}.json`;
-
- const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = downloadFilename;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
- }
}
export const conversationsStore = new ConversationsStore();