Spaces:
Sleeping
Sleeping
<script lang="ts"> | |
import { goto } from '$app/navigation'; | |
import { onMount, tick, getContext } from 'svelte'; | |
import { toast } from 'svelte-sonner'; | |
import { OLLAMA_API_BASE_URL, OPENAI_API_BASE_URL, WEBUI_API_BASE_URL } from '$lib/constants'; | |
import { WEBUI_NAME, config, user, models, settings } from '$lib/stores'; | |
import { generateChatCompletion } from '$lib/apis/ollama'; | |
import { generateOpenAIChatCompletion } from '$lib/apis/openai'; | |
import { splitStream } from '$lib/utils'; | |
import ChatCompletion from '$lib/components/playground/ChatCompletion.svelte'; | |
import Selector from '$lib/components/chat/ModelSelector/Selector.svelte'; | |
const i18n = getContext('i18n'); | |
let mode = 'chat'; | |
let loaded = false; | |
let text = ''; | |
let selectedModelId = ''; | |
let loading = false; | |
let stopResponseFlag = false; | |
let messagesContainerElement: HTMLDivElement; | |
let textCompletionAreaElement: HTMLTextAreaElement; | |
let system = ''; | |
let messages = [ | |
{ | |
role: 'user', | |
content: '' | |
} | |
]; | |
const scrollToBottom = () => { | |
const element = mode === 'chat' ? messagesContainerElement : textCompletionAreaElement; | |
if (element) { | |
element.scrollTop = element?.scrollHeight; | |
} | |
}; | |
const stopResponse = () => { | |
stopResponseFlag = true; | |
console.log('stopResponse'); | |
}; | |
const textCompletionHandler = async () => { | |
const model = $models.find((model) => model.id === selectedModelId); | |
const [res, controller] = await generateOpenAIChatCompletion( | |
localStorage.token, | |
{ | |
model: model.id, | |
stream: true, | |
messages: [ | |
{ | |
role: 'assistant', | |
content: text | |
} | |
] | |
}, | |
model?.owned_by === 'openai' ? `${OPENAI_API_BASE_URL}` : `${OLLAMA_API_BASE_URL}/v1` | |
); | |
if (res && res.ok) { | |
const reader = res.body | |
.pipeThrough(new TextDecoderStream()) | |
.pipeThrough(splitStream('\n')) | |
.getReader(); | |
while (true) { | |
const { value, done } = await reader.read(); | |
if (done || stopResponseFlag) { | |
if (stopResponseFlag) { | |
controller.abort('User: Stop Response'); | |
} | |
break; | |
} | |
try { | |
let lines = value.split('\n'); | |
for (const line of lines) { | |
if (line !== '') { | |
if (line === 'data: [DONE]') { | |
// responseMessage.done = true; | |
console.log('done'); | |
} else { | |
let data = JSON.parse(line.replace(/^data: /, '')); | |
console.log(data); | |
text += data.choices[0].delta.content ?? ''; | |
} | |
} | |
} | |
} catch (error) { | |
console.log(error); | |
} | |
scrollToBottom(); | |
} | |
} | |
}; | |
const chatCompletionHandler = async () => { | |
const model = $models.find((model) => model.id === selectedModelId); | |
const [res, controller] = await generateOpenAIChatCompletion( | |
localStorage.token, | |
{ | |
model: model.id, | |
stream: true, | |
messages: [ | |
system | |
? { | |
role: 'system', | |
content: system | |
} | |
: undefined, | |
...messages | |
].filter((message) => message) | |
}, | |
model?.owned_by === 'openai' ? `${OPENAI_API_BASE_URL}` : `${OLLAMA_API_BASE_URL}/v1` | |
); | |
let responseMessage; | |
if (messages.at(-1)?.role === 'assistant') { | |
responseMessage = messages.at(-1); | |
} else { | |
responseMessage = { | |
role: 'assistant', | |
content: '' | |
}; | |
messages.push(responseMessage); | |
messages = messages; | |
} | |
await tick(); | |
const textareaElement = document.getElementById(`assistant-${messages.length - 1}-textarea`); | |
if (res && res.ok) { | |
const reader = res.body | |
.pipeThrough(new TextDecoderStream()) | |
.pipeThrough(splitStream('\n')) | |
.getReader(); | |
while (true) { | |
const { value, done } = await reader.read(); | |
if (done || stopResponseFlag) { | |
if (stopResponseFlag) { | |
controller.abort('User: Stop Response'); | |
} | |
break; | |
} | |
try { | |
let lines = value.split('\n'); | |
for (const line of lines) { | |
if (line !== '') { | |
console.log(line); | |
if (line === 'data: [DONE]') { | |
// responseMessage.done = true; | |
messages = messages; | |
} else { | |
let data = JSON.parse(line.replace(/^data: /, '')); | |
console.log(data); | |
if (responseMessage.content == '' && data.choices[0].delta.content == '\n') { | |
continue; | |
} else { | |
textareaElement.style.height = textareaElement.scrollHeight + 'px'; | |
responseMessage.content += data.choices[0].delta.content ?? ''; | |
messages = messages; | |
textareaElement.style.height = textareaElement.scrollHeight + 'px'; | |
await tick(); | |
} | |
} | |
} | |
} | |
} catch (error) { | |
console.log(error); | |
} | |
scrollToBottom(); | |
} | |
} | |
}; | |
const submitHandler = async () => { | |
if (selectedModelId) { | |
loading = true; | |
if (mode === 'complete') { | |
await textCompletionHandler(); | |
} else if (mode === 'chat') { | |
await chatCompletionHandler(); | |
} | |
loading = false; | |
stopResponseFlag = false; | |
} | |
}; | |
onMount(async () => { | |
if ($user?.role !== 'admin') { | |
await goto('/'); | |
} | |
if ($settings?.models) { | |
selectedModelId = $settings?.models[0]; | |
} else if ($config?.default_models) { | |
selectedModelId = $config?.default_models.split(',')[0]; | |
} else { | |
selectedModelId = ''; | |
} | |
loaded = true; | |
}); | |
</script> | |
<svelte:head> | |
<title> | |
{$i18n.t('Playground')} | {$WEBUI_NAME} | |
</title> | |
</svelte:head> | |
<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"> | |
<div class=" flex flex-col h-full"> | |
<div class="flex flex-col justify-between mb-2.5 gap-1"> | |
<div class="flex justify-between items-center gap-2"> | |
<div class=" text-lg font-semibold self-center flex"> | |
{$i18n.t('Playground')} | |
<span class=" text-xs text-gray-500 self-center ml-1">{$i18n.t('(Beta)')}</span> | |
</div> | |
<div> | |
<button | |
class=" flex items-center gap-0.5 text-xs px-2.5 py-0.5 rounded-lg {mode === 'chat' && | |
'text-sky-600 dark:text-sky-200 bg-sky-200/30'} {mode === 'complete' && | |
'text-green-600 dark:text-green-200 bg-green-200/30'} " | |
on:click={() => { | |
if (mode === 'complete') { | |
mode = 'chat'; | |
} else { | |
mode = 'complete'; | |
} | |
}} | |
> | |
{#if mode === 'complete'} | |
{$i18n.t('Text Completion')} | |
{:else if mode === 'chat'} | |
{$i18n.t('Chat')} | |
{/if} | |
<div> | |
<svg | |
xmlns="http://www.w3.org/2000/svg" | |
viewBox="0 0 16 16" | |
fill="currentColor" | |
class="w-3 h-3" | |
> | |
<path | |
fill-rule="evenodd" | |
d="M5.22 10.22a.75.75 0 0 1 1.06 0L8 11.94l1.72-1.72a.75.75 0 1 1 1.06 1.06l-2.25 2.25a.75.75 0 0 1-1.06 0l-2.25-2.25a.75.75 0 0 1 0-1.06ZM10.78 5.78a.75.75 0 0 1-1.06 0L8 4.06 6.28 5.78a.75.75 0 0 1-1.06-1.06l2.25-2.25a.75.75 0 0 1 1.06 0l2.25 2.25a.75.75 0 0 1 0 1.06Z" | |
clip-rule="evenodd" | |
/> | |
</svg> | |
</div> | |
</button> | |
</div> | |
</div> | |
<div class="flex flex-col gap-1 w-full"> | |
<div class="flex w-full"> | |
<div class="overflow-hidden w-full"> | |
<div class="max-w-full"> | |
<Selector | |
placeholder={$i18n.t('Select a model')} | |
items={$models.map((model) => ({ | |
value: model.id, | |
label: model.name, | |
model: model | |
}))} | |
bind:value={selectedModelId} | |
/> | |
</div> | |
</div> | |
</div> | |
<!-- <button | |
class=" self-center dark:hover:text-gray-300" | |
id="open-settings-button" | |
on:click={async () => {}} | |
> | |
<svg | |
xmlns="http://www.w3.org/2000/svg" | |
fill="none" | |
viewBox="0 0 24 24" | |
stroke-width="1.5" | |
stroke="currentColor" | |
class="w-4 h-4" | |
> | |
<path | |
stroke-linecap="round" | |
stroke-linejoin="round" | |
d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z" | |
/> | |
<path | |
stroke-linecap="round" | |
stroke-linejoin="round" | |
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" | |
/> | |
</svg> | |
</button> --> | |
</div> | |
</div> | |
{#if mode === 'chat'} | |
<div class="p-1"> | |
<div class="p-3 outline outline-1 outline-gray-200 dark:outline-gray-800 rounded-lg"> | |
<div class=" text-sm font-medium">{$i18n.t('System')}</div> | |
<textarea | |
id="system-textarea" | |
class="w-full h-full bg-transparent resize-none outline-none text-sm" | |
bind:value={system} | |
placeholder={$i18n.t("You're a helpful assistant.")} | |
rows="4" | |
/> | |
</div> | |
</div> | |
{/if} | |
<div | |
class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0" | |
id="messages-container" | |
bind:this={messagesContainerElement} | |
> | |
<div class=" h-full w-full flex flex-col"> | |
<div class="flex-1 p-1"> | |
{#if mode === 'complete'} | |
<textarea | |
id="text-completion-textarea" | |
bind:this={textCompletionAreaElement} | |
class="w-full h-full p-3 bg-transparent outline outline-1 outline-gray-200 dark:outline-gray-800 resize-none rounded-lg text-sm" | |
bind:value={text} | |
placeholder={$i18n.t("You're a helpful assistant.")} | |
/> | |
{:else if mode === 'chat'} | |
<ChatCompletion bind:messages /> | |
{/if} | |
</div> | |
</div> | |
</div> | |
<div class="pb-3"> | |
{#if !loading} | |
<button | |
class="px-3 py-1.5 text-sm font-medium bg-emerald-600 hover:bg-emerald-700 text-gray-50 transition rounded-lg" | |
on:click={() => { | |
submitHandler(); | |
}} | |
> | |
{$i18n.t('Submit')} | |
</button> | |
{:else} | |
<button | |
class="px-3 py-1.5 text-sm font-medium bg-gray-100 hover:bg-gray-200 text-gray-900 transition rounded-lg" | |
on:click={() => { | |
stopResponse(); | |
}} | |
> | |
{$i18n.t('Cancel')} | |
</button> | |
{/if} | |
</div> | |
</div> | |
</div> | |
</div> | |
<style> | |
.scrollbar-hidden::-webkit-scrollbar { | |
display: none; /* for Chrome, Safari and Opera */ | |
} | |
.scrollbar-hidden { | |
-ms-overflow-style: none; /* IE and Edge */ | |
scrollbar-width: none; /* Firefox */ | |
} | |
</style> | |