enh: playground export
This commit is contained in:
@@ -1,5 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import fileSaver from 'file-saver';
|
||||||
|
const { saveAs } = fileSaver;
|
||||||
|
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
import { DropdownMenu } from 'bits-ui';
|
||||||
|
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { onMount, tick, getContext } from 'svelte';
|
import { onMount, tick, getContext } from 'svelte';
|
||||||
@@ -11,11 +15,13 @@
|
|||||||
WEBUI_BASE_URL
|
WEBUI_BASE_URL
|
||||||
} from '$lib/constants';
|
} from '$lib/constants';
|
||||||
import { WEBUI_NAME, config, user, models, settings } from '$lib/stores';
|
import { WEBUI_NAME, config, user, models, settings } from '$lib/stores';
|
||||||
|
import { flyAndScale } from '$lib/utils/transitions';
|
||||||
|
|
||||||
import { chatCompletion } from '$lib/apis/openai';
|
import { chatCompletion } from '$lib/apis/openai';
|
||||||
|
|
||||||
import { splitStream } from '$lib/utils';
|
import { splitStream } from '$lib/utils';
|
||||||
import Collapsible from '../common/Collapsible.svelte';
|
import Collapsible from '../common/Collapsible.svelte';
|
||||||
|
import Dropdown from '../common/Dropdown.svelte';
|
||||||
|
|
||||||
import Messages from '$lib/components/playground/Chat/Messages.svelte';
|
import Messages from '$lib/components/playground/Chat/Messages.svelte';
|
||||||
import ChevronUp from '../icons/ChevronUp.svelte';
|
import ChevronUp from '../icons/ChevronUp.svelte';
|
||||||
@@ -24,6 +30,8 @@
|
|||||||
import Cog6 from '../icons/Cog6.svelte';
|
import Cog6 from '../icons/Cog6.svelte';
|
||||||
import Sidebar from '../common/Sidebar.svelte';
|
import Sidebar from '../common/Sidebar.svelte';
|
||||||
import ArrowRight from '../icons/ArrowRight.svelte';
|
import ArrowRight from '../icons/ArrowRight.svelte';
|
||||||
|
import Download from '../icons/Download.svelte';
|
||||||
|
import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
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 () => {
|
onMount(async () => {
|
||||||
if ($user?.role !== 'admin') {
|
if ($user?.role !== 'admin') {
|
||||||
await goto('/');
|
await goto('/');
|
||||||
@@ -212,7 +308,7 @@
|
|||||||
<div class=" flex flex-col justify-between w-full overflow-y-auto h-full">
|
<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="mx-auto w-full md:px-0 h-full relative">
|
||||||
<div class=" flex flex-col h-full px-3.5">
|
<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
|
<Collapsible
|
||||||
className="w-full flex-1"
|
className="w-full flex-1"
|
||||||
bind:open={showSystem}
|
bind:open={showSystem}
|
||||||
@@ -256,6 +352,58 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Collapsible>
|
</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>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|||||||
Reference in New Issue
Block a user