Skip to content

Commit 7c4f2b8

Browse files
committed
Import/Export of all conversations in WebUI (ggml-org#13352)
1 parent 245abaa commit 7c4f2b8

File tree

2 files changed

+121
-1
lines changed

2 files changed

+121
-1
lines changed

tools/server/webui/src/components/Sidebar.tsx

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { useEffect, useMemo, useState } from 'react';
22
import { classNames } from '../utils/misc';
3-
import { Conversation } from '../utils/types';
3+
import { Conversation, Message } from '../utils/types';
44
import StorageUtils from '../utils/storage';
55
import { useNavigate, useParams } from 'react-router';
66
import {
77
ArrowDownTrayIcon,
8+
CloudArrowUpIcon,
89
EllipsisVerticalIcon,
910
PencilIcon,
1011
PencilSquareIcon,
@@ -15,6 +16,7 @@ import { BtnWithTooltips } from '../utils/common';
1516
import { useAppContext } from '../utils/app.context';
1617
import toast from 'react-hot-toast';
1718
import { useModals } from './ModalProvider';
19+
import { db } from '../utils/storage'; // am Anfang der Datei
1820

1921
export default function Sidebar() {
2022
const params = useParams();
@@ -46,6 +48,102 @@ export default function Sidebar() {
4648
[conversations]
4749
);
4850

51+
// Export all conversations
52+
const onExportAll = async () => {
53+
if (conversations.length === 0) {
54+
toast.error('No conversations to export');
55+
return;
56+
}
57+
58+
try {
59+
const allData = await Promise.all(
60+
conversations.map(async (conv) => {
61+
const messages = await StorageUtils.getMessages(conv.id);
62+
return { conv, messages };
63+
})
64+
);
65+
66+
const blob = new Blob([JSON.stringify(allData, null, 2)], {
67+
type: 'application/json',
68+
});
69+
const url = URL.createObjectURL(blob);
70+
const a = document.createElement('a');
71+
a.href = url;
72+
a.download = `all_conversations_${new Date().toISOString().split('T')[0]}.json`;
73+
document.body.appendChild(a);
74+
a.click();
75+
document.body.removeChild(a);
76+
URL.revokeObjectURL(url);
77+
toast.success('All conversations exported');
78+
} catch (err) {
79+
console.error(err);
80+
toast.error('Failed to export conversations');
81+
}
82+
};
83+
84+
// Upload and import all conversations
85+
const onUploadAll = async () => {
86+
const input = document.createElement('input');
87+
input.type = 'file';
88+
input.accept = '.json';
89+
90+
input.onchange = async (e) => {
91+
const file = (e.target as HTMLInputElement)?.files?.[0];
92+
if (!file) return;
93+
94+
try {
95+
const text = await file.text();
96+
const importedData: { conv: Conversation; messages: Message[] }[] =
97+
JSON.parse(text);
98+
99+
if (!Array.isArray(importedData)) {
100+
toast.error('Invalid file format');
101+
return;
102+
}
103+
104+
await db.transaction('rw', db.conversations, db.messages, async () => {
105+
for (const item of importedData) {
106+
const { conv, messages } = item;
107+
108+
// Check if conversation already exists
109+
const existing = await db.conversations.get(conv.id);
110+
if (existing) {
111+
// Optional: skip, overwrite, or rename?
112+
toast(`Conversation "${conv.name}" already exists, skipping...`, {
113+
icon: '⚠️',
114+
});
115+
continue;
116+
}
117+
118+
// Save conversation
119+
await db.conversations.add(conv);
120+
121+
// Save messages
122+
for (const msg of messages) {
123+
await db.messages.put(msg);
124+
}
125+
}
126+
});
127+
128+
toast.success(
129+
`Successfully imported ${importedData.length} conversation(s)`
130+
);
131+
// Refresh the list
132+
setConversations(await StorageUtils.getAllConversations());
133+
} catch (err: unknown) {
134+
if (err instanceof Error) {
135+
console.error(err);
136+
toast.error('Failed to import: ' + err.message);
137+
} else {
138+
console.error(err);
139+
toast.error('Failed to import: unknown error');
140+
}
141+
}
142+
};
143+
144+
input.click();
145+
};
146+
49147
return (
50148
<>
51149
<input
@@ -106,6 +204,26 @@ export default function Sidebar() {
106204
New conversation
107205
</button>
108206

207+
{/* Export All Button */}
208+
<button
209+
className="btn btn-ghost justify-start px-2"
210+
onClick={onExportAll}
211+
aria-label="Export all conversations"
212+
>
213+
<ArrowDownTrayIcon className="w-5 h-5" />
214+
Export all
215+
</button>
216+
217+
{/* Upload All Button */}
218+
<button
219+
className="btn btn-ghost justify-start px-2"
220+
onClick={onUploadAll}
221+
aria-label="Upload all conversations"
222+
>
223+
<CloudArrowUpIcon className="w-5 h-5" />
224+
Upload all
225+
</button>
226+
109227
{/* list of conversations */}
110228
{groupedConv.map((group, i) => (
111229
<div key={i} role="group">

tools/server/webui/src/utils/storage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,3 +292,5 @@ async function migrationLStoIDB() {
292292
localStorage.setItem('migratedToIDB', '1');
293293
});
294294
}
295+
296+
export { db }; // Füge diese Zeile hinzu

0 commit comments

Comments
 (0)