enh: playground export
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
<script lang="ts">
|
||||
import fileSaver from 'file-saver';
|
||||
const { saveAs } = fileSaver;
|
||||
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { DropdownMenu } from 'bits-ui';
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount, tick, getContext } from 'svelte';
|
||||
@@ -11,11 +15,13 @@
|
||||
WEBUI_BASE_URL
|
||||
} from '$lib/constants';
|
||||
import { WEBUI_NAME, config, user, models, settings } from '$lib/stores';
|
||||
import { flyAndScale } from '$lib/utils/transitions';
|
||||
|
||||
import { chatCompletion } from '$lib/apis/openai';
|
||||
|
||||
import { splitStream } from '$lib/utils';
|
||||
import Collapsible from '../common/Collapsible.svelte';
|
||||
import Dropdown from '../common/Dropdown.svelte';
|
||||
|
||||
import Messages from '$lib/components/playground/Chat/Messages.svelte';
|
||||
import ChevronUp from '../icons/ChevronUp.svelte';
|
||||
@@ -24,6 +30,8 @@
|
||||
import Cog6 from '../icons/Cog6.svelte';
|
||||
import Sidebar from '../common/Sidebar.svelte';
|
||||
import ArrowRight from '../icons/ArrowRight.svelte';
|
||||
import Download from '../icons/Download.svelte';
|
||||
import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -193,6 +201,94 @@
|
||||
}
|
||||
};
|
||||
|
||||
const exportToJson = () => {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
// Convert flat messages array to history map format
|
||||
const messagesMap: Record<string, any> = {};
|
||||
let currentId: string | null = null;
|
||||
let parentId: string | null = null;
|
||||
|
||||
// Add system message if present
|
||||
if (system) {
|
||||
const systemId = crypto.randomUUID();
|
||||
messagesMap[systemId] = {
|
||||
id: systemId,
|
||||
parentId: null,
|
||||
childrenIds: [],
|
||||
role: 'system',
|
||||
content: system,
|
||||
timestamp: now
|
||||
};
|
||||
parentId = systemId;
|
||||
}
|
||||
|
||||
// Add conversation messages
|
||||
for (const msg of messages) {
|
||||
const msgId = crypto.randomUUID();
|
||||
|
||||
// Link parent to child
|
||||
if (parentId && messagesMap[parentId]) {
|
||||
messagesMap[parentId].childrenIds.push(msgId);
|
||||
}
|
||||
|
||||
messagesMap[msgId] = {
|
||||
id: msgId,
|
||||
parentId: parentId,
|
||||
childrenIds: [],
|
||||
role: msg.role,
|
||||
content: msg.content,
|
||||
timestamp: now,
|
||||
...(msg.role === 'assistant' && selectedModelId ? { model: selectedModelId } : {})
|
||||
};
|
||||
|
||||
currentId = msgId;
|
||||
parentId = msgId;
|
||||
}
|
||||
|
||||
const exportData = {
|
||||
chat: {
|
||||
title: 'Playground Chat',
|
||||
models: [selectedModelId],
|
||||
params: system ? { system } : {},
|
||||
history: {
|
||||
messages: messagesMap,
|
||||
currentId
|
||||
}
|
||||
},
|
||||
meta: {},
|
||||
pinned: false,
|
||||
created_at: now,
|
||||
updated_at: now
|
||||
};
|
||||
|
||||
const blob = new Blob([JSON.stringify([exportData], null, 2)], {
|
||||
type: 'application/json'
|
||||
});
|
||||
saveAs(blob, `playground-chat-${Date.now()}.json`);
|
||||
toast.success($i18n.t('Chat exported successfully'));
|
||||
};
|
||||
|
||||
const downloadTxt = () => {
|
||||
let chatText = '';
|
||||
|
||||
// Add system message if present
|
||||
if (system) {
|
||||
chatText += `### SYSTEM\n${system}\n\n`;
|
||||
}
|
||||
|
||||
// Add conversation messages
|
||||
for (const msg of messages) {
|
||||
chatText += `### ${msg.role.toUpperCase()}\n${msg.content}\n\n`;
|
||||
}
|
||||
|
||||
const blob = new Blob([chatText.trim()], {
|
||||
type: 'text/plain'
|
||||
});
|
||||
saveAs(blob, `playground-chat-${Date.now()}.txt`);
|
||||
toast.success($i18n.t('Chat exported successfully'));
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
if ($user?.role !== 'admin') {
|
||||
await goto('/');
|
||||
@@ -212,7 +308,7 @@
|
||||
<div class=" flex flex-col justify-between w-full overflow-y-auto h-full">
|
||||
<div class="mx-auto w-full md:px-0 h-full relative">
|
||||
<div class=" flex flex-col h-full px-3.5">
|
||||
<div class="flex w-full items-start gap-1.5">
|
||||
<div class="flex w-full items-center gap-1.5">
|
||||
<Collapsible
|
||||
className="w-full flex-1"
|
||||
bind:open={showSystem}
|
||||
@@ -256,6 +352,58 @@
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible>
|
||||
|
||||
<Dropdown>
|
||||
<button
|
||||
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"
|
||||
aria-label={$i18n.t('More options')}
|
||||
>
|
||||
<EllipsisHorizontal className="size-4" />
|
||||
</button>
|
||||
|
||||
<div slot="content">
|
||||
<DropdownMenu.Content
|
||||
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"
|
||||
sideOffset={8}
|
||||
side="bottom"
|
||||
align="end"
|
||||
transition={flyAndScale}
|
||||
>
|
||||
<DropdownMenu.Sub>
|
||||
<DropdownMenu.SubTrigger
|
||||
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"
|
||||
>
|
||||
<Download strokeWidth="1.5" />
|
||||
<div class="flex items-center">{$i18n.t('Download')}</div>
|
||||
</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.SubContent
|
||||
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"
|
||||
transition={flyAndScale}
|
||||
sideOffset={8}
|
||||
>
|
||||
<DropdownMenu.Item
|
||||
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"
|
||||
disabled={messages.length === 0}
|
||||
on:click={() => {
|
||||
exportToJson();
|
||||
}}
|
||||
>
|
||||
<div class="flex items-center line-clamp-1">{$i18n.t('Export chat (.json)')}</div>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
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"
|
||||
disabled={messages.length === 0}
|
||||
on:click={() => {
|
||||
downloadTxt();
|
||||
}}
|
||||
>
|
||||
<div class="flex items-center line-clamp-1">{$i18n.t('Plain text (.txt)')}</div>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.SubContent>
|
||||
</DropdownMenu.Sub>
|
||||
</DropdownMenu.Content>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user