@@ -6,6 +6,8 @@ import { filterByLeafNodeId, findLeafNode, findDescendantMessages } from '$lib/u
66import { browser } from '$app/environment' ;
77import { goto } from '$app/navigation' ;
88import { extractPartialThinking } from '$lib/utils/thinking' ;
9+ import { toast } from 'svelte-sonner' ;
10+ import type { ExportedConversations } from '$lib/types/database' ;
911
1012/**
1113 * ChatStore - Central state management for chat conversations and AI interactions
@@ -951,6 +953,166 @@ class ChatStore {
951953 }
952954 }
953955
956+ /**
957+ * Downloads a conversation as JSON file
958+ * @param convId - The conversation ID to download
959+ */
960+ async downloadConversation ( convId : string ) : Promise < void > {
961+ if ( ! this . activeConversation || this . activeConversation . id !== convId ) {
962+ // Load the conversation if not currently active
963+ const conversation = await DatabaseStore . getConversation ( convId ) ;
964+ if ( ! conversation ) return ;
965+
966+ const messages = await DatabaseStore . getConversationMessages ( convId ) ;
967+ const conversationData = {
968+ conv : conversation ,
969+ messages
970+ } ;
971+
972+ this . triggerDownload ( conversationData ) ;
973+ } else {
974+ // Use current active conversation data
975+ const conversationData : ExportedConversations = {
976+ conv : this . activeConversation ! ,
977+ messages : this . activeMessages
978+ } ;
979+
980+ this . triggerDownload ( conversationData ) ;
981+ }
982+ }
983+
984+ /**
985+ * Triggers file download in browser
986+ * @param data - Data to download (expected: { conv: DatabaseConversation, messages: DatabaseMessage[] })
987+ * @param filename - Optional filename
988+ */
989+ private triggerDownload ( data : ExportedConversations , filename ?: string ) : void {
990+ const conversation =
991+ 'conv' in data ? data . conv : Array . isArray ( data ) ? data [ 0 ] ?. conv : undefined ;
992+ if ( ! conversation ) {
993+ console . error ( 'Invalid data: missing conversation' ) ;
994+ return ;
995+ }
996+ const conversationName = conversation . name ? conversation . name . trim ( ) : '' ;
997+ const convId = conversation . id || 'unknown' ;
998+ const truncatedSuffix = conversationName
999+ . toLowerCase ( )
1000+ . replace ( / [ ^ a - z 0 - 9 ] / gi, '_' )
1001+ . replace ( / _ + / g, '_' )
1002+ . substring ( 0 , 20 ) ;
1003+ const downloadFilename = filename || `conversation_${ convId } _${ truncatedSuffix } .json` ;
1004+
1005+ const conversationJson = JSON . stringify ( data , null , 2 ) ;
1006+ const blob = new Blob ( [ conversationJson ] , {
1007+ type : 'application/json'
1008+ } ) ;
1009+ const url = URL . createObjectURL ( blob ) ;
1010+ const a = document . createElement ( 'a' ) ;
1011+ a . href = url ;
1012+ a . download = downloadFilename ;
1013+ document . body . appendChild ( a ) ;
1014+ a . click ( ) ;
1015+ document . body . removeChild ( a ) ;
1016+ URL . revokeObjectURL ( url ) ;
1017+ }
1018+
1019+ /**
1020+ * Exports all conversations with their messages as a JSON file
1021+ */
1022+ async exportAllConversations ( ) : Promise < void > {
1023+ try {
1024+ const allConversations = await DatabaseStore . getAllConversations ( ) ;
1025+ if ( allConversations . length === 0 ) {
1026+ throw new Error ( 'No conversations to export' ) ;
1027+ }
1028+
1029+ const allData : ExportedConversations = await Promise . all (
1030+ allConversations . map ( async ( conv ) => {
1031+ const messages = await DatabaseStore . getConversationMessages ( conv . id ) ;
1032+ return { conv, messages } ;
1033+ } )
1034+ ) ;
1035+
1036+ const blob = new Blob ( [ JSON . stringify ( allData , null , 2 ) ] , {
1037+ type : 'application/json'
1038+ } ) ;
1039+ const url = URL . createObjectURL ( blob ) ;
1040+ const a = document . createElement ( 'a' ) ;
1041+ a . href = url ;
1042+ a . download = `all_conversations_${ new Date ( ) . toISOString ( ) . split ( 'T' ) [ 0 ] } .json` ;
1043+ document . body . appendChild ( a ) ;
1044+ a . click ( ) ;
1045+ document . body . removeChild ( a ) ;
1046+ URL . revokeObjectURL ( url ) ;
1047+
1048+ toast . success ( `All conversations (${ allConversations . length } ) prepared for download` ) ;
1049+ } catch ( err ) {
1050+ console . error ( 'Failed to export conversations:' , err ) ;
1051+ throw err ;
1052+ }
1053+ }
1054+
1055+ /**
1056+ * Imports conversations from a JSON file.
1057+ * Supports both single conversation (object) and multiple conversations (array).
1058+ * Uses DatabaseStore for safe, encapsulated data access
1059+ */
1060+ async importConversations ( ) : Promise < void > {
1061+ return new Promise ( ( resolve , reject ) => {
1062+ const input = document . createElement ( 'input' ) ;
1063+ input . type = 'file' ;
1064+ input . accept = '.json' ;
1065+
1066+ input . onchange = async ( e ) => {
1067+ const file = ( e . target as HTMLInputElement ) ?. files ?. [ 0 ] ;
1068+ if ( ! file ) {
1069+ reject ( new Error ( 'No file selected' ) ) ;
1070+ return ;
1071+ }
1072+
1073+ try {
1074+ const text = await file . text ( ) ;
1075+ const parsedData = JSON . parse ( text ) ;
1076+ let importedData : ExportedConversations ;
1077+
1078+ if ( Array . isArray ( parsedData ) ) {
1079+ importedData = parsedData ;
1080+ } else if (
1081+ parsedData &&
1082+ typeof parsedData === 'object' &&
1083+ 'conv' in parsedData &&
1084+ 'messages' in parsedData
1085+ ) {
1086+ // Single conversation object
1087+ importedData = [ parsedData ] ;
1088+ } else {
1089+ throw new Error (
1090+ 'Invalid file format: expected array of conversations or single conversation object'
1091+ ) ;
1092+ }
1093+
1094+ const result = await DatabaseStore . importConversations ( importedData ) ;
1095+
1096+ // Refresh UI
1097+ await this . loadConversations ( ) ;
1098+
1099+ toast . success ( `Imported ${ result . imported } conversation(s), skipped ${ result . skipped } ` ) ;
1100+
1101+ resolve ( undefined ) ;
1102+ } catch ( err : unknown ) {
1103+ const message = err instanceof Error ? err . message : 'Unknown error' ;
1104+ console . error ( 'Failed to import conversations:' , err ) ;
1105+ toast . error ( 'Import failed' , {
1106+ description : message
1107+ } ) ;
1108+ reject ( new Error ( `Import failed: ${ message } ` ) ) ;
1109+ }
1110+ } ;
1111+
1112+ input . click ( ) ;
1113+ } ) ;
1114+ }
1115+
9541116 /**
9551117 * Deletes a conversation and all its messages
9561118 * @param convId - The conversation ID to delete
@@ -1427,6 +1589,9 @@ export const isInitialized = () => chatStore.isInitialized;
14271589export const maxContextError = ( ) => chatStore . maxContextError ;
14281590
14291591export const createConversation = chatStore . createConversation . bind ( chatStore ) ;
1592+ export const downloadConversation = chatStore . downloadConversation . bind ( chatStore ) ;
1593+ export const exportAllConversations = chatStore . exportAllConversations . bind ( chatStore ) ;
1594+ export const importConversations = chatStore . importConversations . bind ( chatStore ) ;
14301595export const deleteConversation = chatStore . deleteConversation . bind ( chatStore ) ;
14311596export const sendMessage = chatStore . sendMessage . bind ( chatStore ) ;
14321597export const gracefulStop = chatStore . gracefulStop . bind ( chatStore ) ;
0 commit comments