@@ -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