@@ -6,6 +6,8 @@ import { filterByLeafNodeId, findLeafNode, findDescendantMessages } from '$lib/u
6
6
import { browser } from '$app/environment' ;
7
7
import { goto } from '$app/navigation' ;
8
8
import { extractPartialThinking } from '$lib/utils/thinking' ;
9
+ import { toast } from 'svelte-sonner' ;
10
+ import type { ExportedConversations } from '$lib/types/database' ;
9
11
10
12
/**
11
13
* ChatStore - Central state management for chat conversations and AI interactions
@@ -951,6 +953,166 @@ class ChatStore {
951
953
}
952
954
}
953
955
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
+
954
1116
/**
955
1117
* Deletes a conversation and all its messages
956
1118
* @param convId - The conversation ID to delete
@@ -1427,6 +1589,9 @@ export const isInitialized = () => chatStore.isInitialized;
1427
1589
export const maxContextError = ( ) => chatStore . maxContextError ;
1428
1590
1429
1591
export 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 ) ;
1430
1595
export const deleteConversation = chatStore . deleteConversation . bind ( chatStore ) ;
1431
1596
export const sendMessage = chatStore . sendMessage . bind ( chatStore ) ;
1432
1597
export const gracefulStop = chatStore . gracefulStop . bind ( chatStore ) ;
0 commit comments