Skip to content

Commit 8e2b0b6

Browse files
committed
enh: playground export
1 parent ce50d9b commit 8e2b0b6

1 file changed

Lines changed: 149 additions & 1 deletion

File tree

src/lib/components/playground/Chat.svelte

Lines changed: 149 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
<script lang="ts">
2+
import fileSaver from 'file-saver';
3+
const { saveAs } = fileSaver;
4+
25
import { toast } from 'svelte-sonner';
6+
import { DropdownMenu } from 'bits-ui';
37
48
import { goto } from '$app/navigation';
59
import { onMount, tick, getContext } from 'svelte';
@@ -11,11 +15,13 @@
1115
WEBUI_BASE_URL
1216
} from '$lib/constants';
1317
import { WEBUI_NAME, config, user, models, settings } from '$lib/stores';
18+
import { flyAndScale } from '$lib/utils/transitions';
1419
1520
import { chatCompletion } from '$lib/apis/openai';
1621
1722
import { splitStream } from '$lib/utils';
1823
import Collapsible from '../common/Collapsible.svelte';
24+
import Dropdown from '../common/Dropdown.svelte';
1925
2026
import Messages from '$lib/components/playground/Chat/Messages.svelte';
2127
import ChevronUp from '../icons/ChevronUp.svelte';
@@ -24,6 +30,8 @@
2430
import Cog6 from '../icons/Cog6.svelte';
2531
import Sidebar from '../common/Sidebar.svelte';
2632
import ArrowRight from '../icons/ArrowRight.svelte';
33+
import Download from '../icons/Download.svelte';
34+
import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
2735
2836
const i18n = getContext('i18n');
2937
@@ -193,6 +201,94 @@
193201
}
194202
};
195203
204+
const exportToJson = () => {
205+
const now = Math.floor(Date.now() / 1000);
206+
207+
// Convert flat messages array to history map format
208+
const messagesMap: Record<string, any> = {};
209+
let currentId: string | null = null;
210+
let parentId: string | null = null;
211+
212+
// Add system message if present
213+
if (system) {
214+
const systemId = crypto.randomUUID();
215+
messagesMap[systemId] = {
216+
id: systemId,
217+
parentId: null,
218+
childrenIds: [],
219+
role: 'system',
220+
content: system,
221+
timestamp: now
222+
};
223+
parentId = systemId;
224+
}
225+
226+
// Add conversation messages
227+
for (const msg of messages) {
228+
const msgId = crypto.randomUUID();
229+
230+
// Link parent to child
231+
if (parentId && messagesMap[parentId]) {
232+
messagesMap[parentId].childrenIds.push(msgId);
233+
}
234+
235+
messagesMap[msgId] = {
236+
id: msgId,
237+
parentId: parentId,
238+
childrenIds: [],
239+
role: msg.role,
240+
content: msg.content,
241+
timestamp: now,
242+
...(msg.role === 'assistant' && selectedModelId ? { model: selectedModelId } : {})
243+
};
244+
245+
currentId = msgId;
246+
parentId = msgId;
247+
}
248+
249+
const exportData = {
250+
chat: {
251+
title: 'Playground Chat',
252+
models: [selectedModelId],
253+
params: system ? { system } : {},
254+
history: {
255+
messages: messagesMap,
256+
currentId
257+
}
258+
},
259+
meta: {},
260+
pinned: false,
261+
created_at: now,
262+
updated_at: now
263+
};
264+
265+
const blob = new Blob([JSON.stringify([exportData], null, 2)], {
266+
type: 'application/json'
267+
});
268+
saveAs(blob, `playground-chat-${Date.now()}.json`);
269+
toast.success($i18n.t('Chat exported successfully'));
270+
};
271+
272+
const downloadTxt = () => {
273+
let chatText = '';
274+
275+
// Add system message if present
276+
if (system) {
277+
chatText += `### SYSTEM\n${system}\n\n`;
278+
}
279+
280+
// Add conversation messages
281+
for (const msg of messages) {
282+
chatText += `### ${msg.role.toUpperCase()}\n${msg.content}\n\n`;
283+
}
284+
285+
const blob = new Blob([chatText.trim()], {
286+
type: 'text/plain'
287+
});
288+
saveAs(blob, `playground-chat-${Date.now()}.txt`);
289+
toast.success($i18n.t('Chat exported successfully'));
290+
};
291+
196292
onMount(async () => {
197293
if ($user?.role !== 'admin') {
198294
await goto('/');
@@ -212,7 +308,7 @@
212308
<div class=" flex flex-col justify-between w-full overflow-y-auto h-full">
213309
<div class="mx-auto w-full md:px-0 h-full relative">
214310
<div class=" flex flex-col h-full px-3.5">
215-
<div class="flex w-full items-start gap-1.5">
311+
<div class="flex w-full items-center gap-1.5">
216312
<Collapsible
217313
className="w-full flex-1"
218314
bind:open={showSystem}
@@ -256,6 +352,58 @@
256352
</div>
257353
</div>
258354
</Collapsible>
355+
356+
<Dropdown>
357+
<button
358+
class="p-1.5 text-sm font-medium bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500 dark:text-gray-400 transition rounded-lg"
359+
aria-label={$i18n.t('More options')}
360+
>
361+
<EllipsisHorizontal className="size-4" />
362+
</button>
363+
364+
<div slot="content">
365+
<DropdownMenu.Content
366+
class="w-full max-w-[200px] rounded-2xl px-1 py-1 border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
367+
sideOffset={8}
368+
side="bottom"
369+
align="end"
370+
transition={flyAndScale}
371+
>
372+
<DropdownMenu.Sub>
373+
<DropdownMenu.SubTrigger
374+
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl select-none w-full"
375+
>
376+
<Download strokeWidth="1.5" />
377+
<div class="flex items-center">{$i18n.t('Download')}</div>
378+
</DropdownMenu.SubTrigger>
379+
<DropdownMenu.SubContent
380+
class="w-full rounded-2xl p-1 z-50 bg-white dark:bg-gray-850 dark:text-white border border-gray-100 dark:border-gray-800 shadow-lg max-h-52 overflow-y-auto scrollbar-hidden"
381+
transition={flyAndScale}
382+
sideOffset={8}
383+
>
384+
<DropdownMenu.Item
385+
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl select-none w-full"
386+
disabled={messages.length === 0}
387+
on:click={() => {
388+
exportToJson();
389+
}}
390+
>
391+
<div class="flex items-center line-clamp-1">{$i18n.t('Export chat (.json)')}</div>
392+
</DropdownMenu.Item>
393+
<DropdownMenu.Item
394+
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl select-none w-full"
395+
disabled={messages.length === 0}
396+
on:click={() => {
397+
downloadTxt();
398+
}}
399+
>
400+
<div class="flex items-center line-clamp-1">{$i18n.t('Plain text (.txt)')}</div>
401+
</DropdownMenu.Item>
402+
</DropdownMenu.SubContent>
403+
</DropdownMenu.Sub>
404+
</DropdownMenu.Content>
405+
</div>
406+
</Dropdown>
259407
</div>
260408

261409
<div

0 commit comments

Comments
 (0)