123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367 |
- <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 Selector from '$lib/components/chat/ModelSelector/Selector.svelte';
- import Collapsible from '../common/Collapsible.svelte';
- import Messages from './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 = '';
- role = role === 'user' ? 'assistant' : 'user';
- await tick();
- scrollToBottom();
- }
- };
- const submitHandler = async () => {
- if (selectedModelId) {
- 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=" dark:bg-gray-900" width="250px">
- <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>
- <button class="p-1.5 bg-transparent hover:bg-white/5 transition rounded-lg">
- <ArrowRight strokeWidth="2.5" />
- </button>
- </div>
- </div>
- <div>
- <div>
- <div class=" text-xs font-medium mb-1">Model</div>
- <div class="w-full">
- <Selector
- triggerClassName="text-sm"
- placeholder={$i18n.t('Select a model')}
- items={$models.map((model) => ({
- value: model.id,
- label: model.name,
- model: model
- }))}
- bind:value={selectedModelId}
- />
- </div>
- </div>
- </div>
- </div>
- </Sidebar>
- <div class=" flex flex-col h-full px-4 py-1">
- <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="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();
- }}
- >
- {$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>
|