Skip to content

Commit a4805dc

Browse files
committed
webui : added download action (ggml-org#13552)
1 parent 128d522 commit a4805dc

File tree

2 files changed

+65
-1
lines changed

2 files changed

+65
-1
lines changed

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script lang="ts">
2-
import { Trash2, Pencil, MoreHorizontal } from '@lucide/svelte';
2+
import { Trash2, Pencil, MoreHorizontal, Download } from '@lucide/svelte';
33
import { ActionDropdown } from '$lib/components/app';
4+
import { downloadConversation } from '$lib/stores/chat.svelte';
45
import { onMount } from 'svelte';
56
67
interface Props {
@@ -101,6 +102,15 @@
101102
onclick: handleEdit,
102103
shortcut: ['shift', 'cmd', 'e']
103104
},
105+
{
106+
icon: Download,
107+
label: 'Download',
108+
onclick: (e) => {
109+
e.stopPropagation();
110+
downloadConversation(conversation.id);
111+
},
112+
shortcut: ['shift', 'cmd', 's']
113+
},
104114
{
105115
icon: Trash2,
106116
label: 'Delete',

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

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -951,6 +951,59 @@ class ChatStore {
951951
}
952952
}
953953

954+
/**
955+
* Downloads a conversation as JSON file
956+
* @param convId - The conversation ID to download
957+
*/
958+
async downloadConversation(convId: string): Promise<void> {
959+
if (!this.activeConversation || this.activeConversation.id !== convId) {
960+
// Load the conversation if not currently active
961+
const conversation = await DatabaseStore.getConversation(convId);
962+
if (!conversation) return;
963+
964+
const messages = await DatabaseStore.getConversationMessages(convId);
965+
const conversationData = {
966+
...conversation,
967+
messages
968+
};
969+
970+
this.triggerDownload(conversationData);
971+
} else {
972+
// Use current active conversation data
973+
const conversationData = {
974+
...this.activeConversation,
975+
messages: this.activeMessages
976+
};
977+
978+
this.triggerDownload(conversationData);
979+
}
980+
}
981+
982+
/**
983+
* Triggers file download in browser
984+
* @param data - Data to download
985+
* @param filename - Optional filename
986+
*/
987+
private triggerDownload(data: any, filename?: string): void {
988+
const conversationName = data.name || '';
989+
const truncatedSuffix = conversationName.toLowerCase()
990+
.replace(/[^a-z0-9]/gi, '_').replace(/_+/g, '_').substring(0, 20);
991+
const downloadFilename = filename || `conversation_${data.id}_${truncatedSuffix}.json`;
992+
993+
const conversationJson = JSON.stringify(data, null, 2);
994+
const blob = new Blob([conversationJson], {
995+
type: 'application/json',
996+
});
997+
const url = URL.createObjectURL(blob);
998+
const a = document.createElement('a');
999+
a.href = url;
1000+
a.download = downloadFilename;
1001+
document.body.appendChild(a);
1002+
a.click();
1003+
document.body.removeChild(a);
1004+
URL.revokeObjectURL(url);
1005+
}
1006+
9541007
/**
9551008
* Deletes a conversation and all its messages
9561009
* @param convId - The conversation ID to delete
@@ -1427,6 +1480,7 @@ export const isInitialized = () => chatStore.isInitialized;
14271480
export const maxContextError = () => chatStore.maxContextError;
14281481

14291482
export const createConversation = chatStore.createConversation.bind(chatStore);
1483+
export const downloadConversation = chatStore.downloadConversation.bind(chatStore);
14301484
export const deleteConversation = chatStore.deleteConversation.bind(chatStore);
14311485
export const sendMessage = chatStore.sendMessage.bind(chatStore);
14321486
export const gracefulStop = chatStore.gracefulStop.bind(chatStore);

0 commit comments

Comments
 (0)