Skip to content

Commit a00837e

Browse files
committed
webui : import and export (for all conversations)
1 parent a4805dc commit a00837e

File tree

3 files changed

+154
-1
lines changed

3 files changed

+154
-1
lines changed

tools/server/webui/src/lib/components/app/chat/ChatSidebar/ChatSidebarActions.svelte

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
<script lang="ts">
2-
import { Search, SquarePen, X } from '@lucide/svelte';
2+
import { Search, SquarePen, X, Download, Upload } from '@lucide/svelte';
33
import { KeyboardShortcutInfo } from '$lib/components/app';
44
import { Button } from '$lib/components/ui/button';
55
import { Input } from '$lib/components/ui/input';
6+
import { exportAllConversations, importAllConversations } from '$lib/stores/chat.svelte';
67
78
interface Props {
89
handleMobileSidebarItemClick: () => void;
@@ -77,5 +78,37 @@
7778

7879
<KeyboardShortcutInfo keys={['cmd', 'k']} />
7980
</Button>
81+
82+
<!-- Export All Conversations -->
83+
<Button
84+
class="w-full justify-start text-sm"
85+
onclick={() => {
86+
exportAllConversations();
87+
}}
88+
variant="ghost"
89+
>
90+
<div class="flex items-center gap-2">
91+
<Download class="h-4 w-4" />
92+
Export all
93+
</div>
94+
</Button>
95+
96+
<!-- Import Conversations -->
97+
<Button
98+
class="w-full justify-start text-sm"
99+
onclick={() => {
100+
importAllConversations().catch(err => {
101+
console.error('Import failed:', err);
102+
// Optional: show toast or dialog
103+
});
104+
}}
105+
variant="ghost"
106+
>
107+
108+
<div class="flex items-center gap-2">
109+
<Upload class="h-4 w-4" />
110+
Import all
111+
</div>
112+
</Button>
80113
{/if}
81114
</div>

tools/server/webui/src/lib/stores/chat.svelte.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { filterByLeafNodeId, findLeafNode, findDescendantMessages } from '$lib/u
66
import { browser } from '$app/environment';
77
import { goto } from '$app/navigation';
88
import { extractPartialThinking } from '$lib/utils/thinking';
9+
import { toast } from 'svelte-sonner';
910

1011
/**
1112
* ChatStore - Central state management for chat conversations and AI interactions
@@ -1004,6 +1005,87 @@ class ChatStore {
10041005
URL.revokeObjectURL(url);
10051006
}
10061007

1008+
/**
1009+
* Exports all conversations with their messages as a JSON file
1010+
*/
1011+
async exportAllConversations(): Promise<void> {
1012+
try {
1013+
const allConversations = await DatabaseStore.getAllConversations();
1014+
if (allConversations.length === 0) {
1015+
throw new Error('No conversations to export');
1016+
}
1017+
1018+
const allData = await Promise.all(
1019+
allConversations.map(async (conv) => {
1020+
const messages = await DatabaseStore.getConversationMessages(conv.id);
1021+
return { conv, messages };
1022+
})
1023+
);
1024+
1025+
const blob = new Blob([JSON.stringify(allData, null, 2)], {
1026+
type: 'application/json'
1027+
});
1028+
const url = URL.createObjectURL(blob);
1029+
const a = document.createElement('a');
1030+
a.href = url;
1031+
a.download = `all_conversations_${new Date().toISOString().split('T')[0]}.json`;
1032+
document.body.appendChild(a);
1033+
a.click();
1034+
document.body.removeChild(a);
1035+
URL.revokeObjectURL(url);
1036+
1037+
toast.success(`All conversations (${allConversations.length}) prepared for download`);
1038+
} catch (err) {
1039+
console.error('Failed to export conversations:', err);
1040+
throw err;
1041+
}
1042+
}
1043+
1044+
/**
1045+
* Imports conversations from a JSON file
1046+
* Uses DatabaseStore for safe, encapsulated data access
1047+
*/
1048+
async importAllConversations(): Promise<void> {
1049+
return new Promise((resolve, reject) => {
1050+
const input = document.createElement('input');
1051+
input.type = 'file';
1052+
input.accept = '.json';
1053+
1054+
input.onchange = async (e) => {
1055+
const file = (e.target as HTMLInputElement)?.files?.[0];
1056+
if (!file) {
1057+
reject(new Error('No file selected'));
1058+
return;
1059+
}
1060+
1061+
try {
1062+
const text = await file.text();
1063+
const importedData: { conv: DatabaseConversation; messages: DatabaseMessage[] }[] =
1064+
JSON.parse(text);
1065+
1066+
if (!Array.isArray(importedData)) {
1067+
throw new Error('Invalid file format: expected array of conversations');
1068+
}
1069+
1070+
const result = await DatabaseStore.importConversations(importedData);
1071+
1072+
// Refresh UI
1073+
await this.loadConversations();
1074+
1075+
toast.success(`Imported ${result.imported} conversation(s), skipped ${result.skipped}`);
1076+
1077+
resolve(undefined);
1078+
} catch (err: unknown) {
1079+
const message = err instanceof Error ? err.message : 'Unknown error';
1080+
console.error('Failed to import conversations:', err);
1081+
reject(new Error(`Import failed: ${message}`));
1082+
}
1083+
};
1084+
1085+
input.click();
1086+
});
1087+
}
1088+
10071089
/**
10081090
* Deletes a conversation and all its messages
10091091
* @param convId - The conversation ID to delete
@@ -1481,6 +1563,8 @@ export const maxContextError = () => chatStore.maxContextError;
14811563

14821564
export const createConversation = chatStore.createConversation.bind(chatStore);
14831565
export const downloadConversation = chatStore.downloadConversation.bind(chatStore);
1566+
export const exportAllConversations = chatStore.exportAllConversations.bind(chatStore);
1567+
export const importAllConversations = chatStore.importAllConversations.bind(chatStore);
14841568
export const deleteConversation = chatStore.deleteConversation.bind(chatStore);
14851569
export const sendMessage = chatStore.sendMessage.bind(chatStore);
14861570
export const gracefulStop = chatStore.gracefulStop.bind(chatStore);

tools/server/webui/src/lib/stores/database.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,4 +346,40 @@ export class DatabaseStore {
346346
): Promise<void> {
347347
await db.messages.update(id, updates);
348348
}
349+
350+
/**
351+
* Imports multiple conversations and their messages.
352+
* Skips conversations that already exist.
353+
*
354+
* @param data - Array of { conv, messages } objects
355+
*/
356+
static async importConversations(
357+
data: { conv: DatabaseConversation; messages: DatabaseMessage[] }[]
358+
): Promise<{ imported: number; skipped: number }> {
359+
let importedCount = 0;
360+
let skippedCount = 0;
361+
362+
return await db.transaction('rw', [db.conversations, db.messages], async () => {
363+
for (const item of data) {
364+
const { conv, messages } = item;
365+
366+
const existing = await db.conversations.get(conv.id);
367+
if (existing) {
368+
console.warn(`Conversation "${conv.name}" already exists, skipping...`);
369+
skippedCount++;
370+
continue;
371+
}
372+
373+
await db.conversations.add(conv);
374+
for (const msg of messages) {
375+
await db.messages.put(msg);
376+
}
377+
378+
importedCount++;
379+
}
380+
381+
return { imported: importedCount, skipped: skippedCount };
382+
});
383+
}
384+
349385
}

0 commit comments

Comments
 (0)