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