Spaces:
Running
Running
<script lang="ts"> | |
import { toast } from 'svelte-sonner'; | |
import { goto } from '$app/navigation'; | |
import { onMount, tick, getContext } from 'svelte'; | |
import { | |
OLLAMA_API_BASE_URL, | |
OPENAI_API_BASE_URL, | |
WEBUI_API_BASE_URL, | |
WEBUI_BASE_URL | |
} from '$lib/constants'; | |
import { WEBUI_NAME, config, user, models, settings } from '$lib/stores'; | |
import { generateOpenAIChatCompletion } from '$lib/apis/openai'; | |
import { splitStream } from '$lib/utils'; | |
import Collapsible from '../common/Collapsible.svelte'; | |
import Messages from '$lib/components/playground/Chat/Messages.svelte'; | |
import ChevronUp from '../icons/ChevronUp.svelte'; | |
import ChevronDown from '../icons/ChevronDown.svelte'; | |
import Pencil from '../icons/Pencil.svelte'; | |
import Cog6 from '../icons/Cog6.svelte'; | |
import Sidebar from '../common/Sidebar.svelte'; | |
import ArrowRight from '../icons/ArrowRight.svelte'; | |
const i18n = getContext('i18n'); | |
let loaded = false; | |
let selectedModelId = ''; | |
let loading = false; | |
let stopResponseFlag = false; | |
let messagesContainerElement: HTMLDivElement; | |
let showSystem = false; | |
let showSettings = false; | |
let system = ''; | |
let role = 'user'; | |
let message = ''; | |
let messages = []; | |
const scrollToBottom = () => { | |
const element = messagesContainerElement; | |
if (element) { | |
element.scrollTop = element?.scrollHeight; | |
} | |
}; | |
const stopResponse = () => { | |
stopResponseFlag = true; | |
console.log('stopResponse'); | |
}; | |
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) | |
}, | |
`${WEBUI_BASE_URL}/api` | |
); | |
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 addHandler = async () => { | |
if (message) { | |
messages.push({ | |
role: role, | |
content: message | |
}); | |
messages = messages; | |
message = ''; | |
await tick(); | |
scrollToBottom(); | |
} | |
}; | |
const submitHandler = async () => { | |
if (selectedModelId) { | |
await addHandler(); | |
loading = true; | |
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> | |
<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"> | |
<Sidebar bind:show={showSettings} className=" bg-white dark:bg-gray-900" width="300px"> | |
<div class="flex flex-col px-5 py-3 text-sm"> | |
<div class="flex justify-between items-center mb-2"> | |
<div class=" font-medium text-base">Settings</div> | |
<div class=" translate-x-1.5"> | |
<button | |
class="p-1.5 bg-transparent hover:bg-white/5 transition rounded-lg" | |
on:click={() => { | |
showSettings = !showSettings; | |
}} | |
> | |
<ArrowRight className="size-3" strokeWidth="2.5" /> | |
</button> | |
</div> | |
</div> | |
<div class="mt-1"> | |
<div> | |
<div class=" text-xs font-medium mb-1">Model</div> | |
<div class="w-full"> | |
<select | |
class="w-full bg-transparent border border-gray-50 dark:border-gray-850 rounded-lg py-1 px-2 -mx-0.5 text-sm outline-none" | |
bind:value={selectedModelId} | |
> | |
{#each $models as model} | |
<option value={model.id} class="bg-gray-50 dark:bg-gray-700">{model.name}</option> | |
{/each} | |
</select> | |
</div> | |
</div> | |
</div> | |
</div> | |
</Sidebar> | |
<div class=" flex flex-col h-full px-3.5"> | |
<div class="flex w-full items-start gap-1.5"> | |
<Collapsible | |
className="w-full flex-1" | |
bind:open={showSystem} | |
buttonClassName="w-full rounded-lg text-sm border border-gray-50 dark:border-gray-850 w-full py-1 px-1.5" | |
grow={true} | |
> | |
<div class="flex gap-2 justify-between items-center"> | |
<div class=" flex-shrink-0 font-medium ml-1.5"> | |
{$i18n.t('System Instructions')} | |
</div> | |
{#if !showSystem} | |
<div class=" flex-1 text-gray-500 line-clamp-1"> | |
{system} | |
</div> | |
{/if} | |
<div class="flex-shrink-0"> | |
<button class="p-1.5 bg-transparent hover:bg-white/5 transition rounded-lg"> | |
{#if showSystem} | |
<ChevronUp className="size-3.5" /> | |
{:else} | |
<Pencil className="size-3.5" /> | |
{/if} | |
</button> | |
</div> | |
</div> | |
<div slot="content"> | |
<div class="pt-1 px-1.5"> | |
<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> | |
</Collapsible> | |
<div class="translate-y-1"> | |
<button | |
class="p-1.5 bg-transparent hover:bg-white/5 transition rounded-lg" | |
on:click={() => { | |
showSettings = !showSettings; | |
}} | |
> | |
<Cog6 /> | |
</button> | |
</div> | |
</div> | |
<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"> | |
<Messages bind:messages /> | |
</div> | |
</div> | |
</div> | |
<div class="pb-3"> | |
<div class="text-xs font-medium text-gray-500 px-2 py-1"> | |
{selectedModelId} | |
</div> | |
<div class="border border-gray-50 dark:border-gray-850 w-full px-3 py-2.5 rounded-xl"> | |
<div class="py-0.5"> | |
<!-- $i18n.t('a user') --> | |
<!-- $i18n.t('an assistant') --> | |
<textarea | |
bind:value={message} | |
class=" w-full h-full bg-transparent resize-none outline-none text-sm" | |
placeholder={$i18n.t(`Enter {{role}} message here`, { | |
role: role === 'user' ? $i18n.t('a user') : $i18n.t('an assistant') | |
})} | |
on:input={(e) => { | |
e.target.style.height = ''; | |
e.target.style.height = Math.min(e.target.scrollHeight, 150) + 'px'; | |
}} | |
on:focus={(e) => { | |
e.target.style.height = ''; | |
e.target.style.height = Math.min(e.target.scrollHeight, 150) + 'px'; | |
}} | |
rows="2" | |
/> | |
</div> | |
<div class="flex justify-between"> | |
<div> | |
<button | |
class="px-3.5 py-1.5 text-sm font-medium bg-gray-50 hover:bg-gray-100 text-gray-900 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-200 transition rounded-lg" | |
on:click={() => { | |
role = role === 'user' ? 'assistant' : 'user'; | |
}} | |
> | |
{#if role === 'user'} | |
{$i18n.t('User')} | |
{:else} | |
{$i18n.t('Assistant')} | |
{/if} | |
</button> | |
</div> | |
<div> | |
{#if !loading} | |
<button | |
disabled={message === ''} | |
class="px-3.5 py-1.5 text-sm font-medium disabled:bg-gray-50 dark:disabled:hover:bg-gray-850 disabled:cursor-not-allowed bg-gray-50 hover:bg-gray-100 text-gray-900 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-200 transition rounded-lg" | |
on:click={() => { | |
addHandler(); | |
role = role === 'user' ? 'assistant' : 'user'; | |
}} | |
> | |
{$i18n.t('Add')} | |
</button> | |
<button | |
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-lg" | |
on:click={() => { | |
submitHandler(); | |
}} | |
> | |
{$i18n.t('Run')} | |
</button> | |
{:else} | |
<button | |
class="px-3 py-1.5 text-sm font-medium bg-gray-300 text-black transition rounded-lg" | |
on:click={() => { | |
stopResponse(); | |
}} | |
> | |
{$i18n.t('Cancel')} | |
</button> | |
{/if} | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |