Unverified Commit 5166e92f authored by arkohut's avatar arkohut Committed by GitHub
Browse files

Merge branch 'dev' into support-py-for-run-code

parents b443d61c b6b71c08
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
import hljs from 'highlight.js'; import hljs from 'highlight.js';
import 'highlight.js/styles/github-dark.min.css'; import 'highlight.js/styles/github-dark.min.css';
import { loadPyodide } from 'pyodide'; import { loadPyodide } from 'pyodide';
import { tick } from 'svelte'; import { onMount, tick } from 'svelte';
import PyodideWorker from '$lib/workers/pyodide.worker?worker'; import PyodideWorker from '$lib/workers/pyodide.worker?worker';
export let id = ''; export let id = '';
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
export let lang = ''; export let lang = '';
export let code = ''; export let code = '';
let highlightedCode = null;
let executing = false; let executing = false;
let stdout = null; let stdout = null;
...@@ -202,60 +203,60 @@ __builtins__.input = input`); ...@@ -202,60 +203,60 @@ __builtins__.input = input`);
}; };
}; };
$: highlightedCode = code ? hljs.highlightAuto(code, hljs.getLanguage(lang)?.aliases).value : ''; $: if (code) {
highlightedCode = hljs.highlightAuto(code, hljs.getLanguage(lang)?.aliases).value || code;
}
</script> </script>
{#if code} <div class="mb-4" dir="ltr">
<div class="mb-4" dir="ltr"> <div
<div class="flex justify-between bg-[#202123] text-white text-xs px-4 pt-1 pb-0.5 rounded-t-lg overflow-x-auto"
class="flex justify-between bg-[#202123] text-white text-xs px-4 pt-1 pb-0.5 rounded-t-lg overflow-x-auto" >
> <div class="p-1">{@html lang}</div>
<div class="p-1">{@html lang}</div>
<div class="flex items-center">
<div class="flex items-center"> {#if lang.toLowerCase() === 'python' || lang.toLowerCase() === 'py' || (lang === '' && checkPythonCode(code))}
{#if lang.toLowerCase() === 'python' || lang.toLowerCase() === 'py' || (lang === '' && checkPythonCode(code))} {#if executing}
{#if executing} <div class="copy-code-button bg-none border-none p-1 cursor-not-allowed">Running</div>
<div class="copy-code-button bg-none border-none p-1 cursor-not-allowed">Running</div> {:else}
{:else} <button
<button class="copy-code-button bg-none border-none p-1"
class="copy-code-button bg-none border-none p-1" on:click={() => {
on:click={() => { executePython(code);
executePython(code); }}>Run</button
}}>Run</button >
>
{/if}
{/if} {/if}
<button class="copy-code-button bg-none border-none p-1" on:click={copyCode} {/if}
>{copied ? 'Copied' : 'Copy Code'}</button <button class="copy-code-button bg-none border-none p-1" on:click={copyCode}
> >{copied ? 'Copied' : 'Copy Code'}</button
</div> >
</div> </div>
<pre
class=" hljs p-4 px-5 overflow-x-auto"
style="border-top-left-radius: 0px; border-top-right-radius: 0px; {(executing ||
stdout ||
stderr ||
result) &&
'border-bottom-left-radius: 0px; border-bottom-right-radius: 0px;'}"><code
class="language-{lang} rounded-t-none whitespace-pre">{@html highlightedCode || code}</code
></pre>
<div
id="plt-canvas-{id}"
class="bg-[#202123] text-white max-w-full overflow-x-auto scrollbar-hidden"
/>
{#if executing}
<div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
<div class="text-sm">Running...</div>
</div>
{:else if stdout || stderr || result}
<div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
<div class="text-sm">{stdout || stderr || result}</div>
</div>
{/if}
</div> </div>
{/if}
<pre
class=" hljs p-4 px-5 overflow-x-auto"
style="border-top-left-radius: 0px; border-top-right-radius: 0px; {(executing ||
stdout ||
stderr ||
result) &&
'border-bottom-left-radius: 0px; border-bottom-right-radius: 0px;'}"><code
class="language-{lang} rounded-t-none whitespace-pre">{@html highlightedCode || code}</code
></pre>
<div
id="plt-canvas-{id}"
class="bg-[#202123] text-white max-w-full overflow-x-auto scrollbar-hidden"
/>
{#if executing}
<div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
<div class="text-sm">Running...</div>
</div>
{:else if stdout || stderr || result}
<div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
<div class="text-sm">{stdout || stderr || result}</div>
</div>
{/if}
</div>
...@@ -13,8 +13,6 @@ ...@@ -13,8 +13,6 @@
export let parentMessage; export let parentMessage;
export let selectedModelfiles;
export let updateChatMessages: Function; export let updateChatMessages: Function;
export let confirmEditResponseMessage: Function; export let confirmEditResponseMessage: Function;
export let rateMessage: Function; export let rateMessage: Function;
...@@ -130,7 +128,6 @@ ...@@ -130,7 +128,6 @@
> >
<ResponseMessage <ResponseMessage
message={groupedMessages[model].messages[groupedMessagesIdx[model]]} message={groupedMessages[model].messages[groupedMessagesIdx[model]]}
modelfiles={selectedModelfiles}
siblings={groupedMessages[model].messages.map((m) => m.id)} siblings={groupedMessages[model].messages.map((m) => m.id)}
isLastMessage={true} isLastMessage={true}
{updateChatMessages} {updateChatMessages}
......
<script lang="ts"> <script lang="ts">
import { WEBUI_BASE_URL } from '$lib/constants'; import { WEBUI_BASE_URL } from '$lib/constants';
import { user } from '$lib/stores'; import { config, user, models as _models } from '$lib/stores';
import { onMount, getContext } from 'svelte'; import { onMount, getContext } from 'svelte';
import { blur, fade } from 'svelte/transition'; import { blur, fade } from 'svelte/transition';
...@@ -9,23 +9,20 @@ ...@@ -9,23 +9,20 @@
const i18n = getContext('i18n'); const i18n = getContext('i18n');
export let modelIds = [];
export let models = []; export let models = [];
export let modelfiles = [];
export let submitPrompt; export let submitPrompt;
export let suggestionPrompts;
let mounted = false; let mounted = false;
let modelfile = null;
let selectedModelIdx = 0; let selectedModelIdx = 0;
$: modelfile = $: if (modelIds.length > 0) {
models[selectedModelIdx] in modelfiles ? modelfiles[models[selectedModelIdx]] : null;
$: if (models.length > 0) {
selectedModelIdx = models.length - 1; selectedModelIdx = models.length - 1;
} }
$: models = modelIds.map((id) => $_models.find((m) => m.id === id));
onMount(() => { onMount(() => {
mounted = true; mounted = true;
}); });
...@@ -41,25 +38,14 @@ ...@@ -41,25 +38,14 @@
selectedModelIdx = modelIdx; selectedModelIdx = modelIdx;
}} }}
> >
{#if model in modelfiles} <img
<img crossorigin="anonymous"
crossorigin="anonymous" src={model?.info?.meta?.profile_image_url ??
src={modelfiles[model]?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`} ($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)}
alt="modelfile" class=" size-[2.7rem] rounded-full border-[1px] border-gray-200 dark:border-none"
class=" size-[2.7rem] rounded-full border-[1px] border-gray-200 dark:border-none" alt="logo"
draggable="false" draggable="false"
/> />
{:else}
<img
crossorigin="anonymous"
src={$i18n.language === 'dg-DG'
? `/doge.png`
: `${WEBUI_BASE_URL}/static/favicon.png`}
class=" size-[2.7rem] rounded-full border-[1px] border-gray-200 dark:border-none"
alt="logo"
draggable="false"
/>
{/if}
</button> </button>
{/each} {/each}
</div> </div>
...@@ -70,23 +56,32 @@ ...@@ -70,23 +56,32 @@
> >
<div> <div>
<div class=" capitalize line-clamp-1" in:fade={{ duration: 200 }}> <div class=" capitalize line-clamp-1" in:fade={{ duration: 200 }}>
{#if modelfile} {#if models[selectedModelIdx]?.info}
{modelfile.title} {models[selectedModelIdx]?.info?.name}
{:else} {:else}
{$i18n.t('Hello, {{name}}', { name: $user.name })} {$i18n.t('Hello, {{name}}', { name: $user.name })}
{/if} {/if}
</div> </div>
<div in:fade={{ duration: 200, delay: 200 }}> <div in:fade={{ duration: 200, delay: 200 }}>
{#if modelfile} {#if models[selectedModelIdx]?.info}
<div class="mt-0.5 text-base font-normal text-gray-500 dark:text-gray-400"> <div class="mt-0.5 text-base font-normal text-gray-500 dark:text-gray-400 line-clamp-3">
{modelfile.desc} {models[selectedModelIdx]?.info?.meta?.description}
</div> </div>
{#if modelfile.user} {#if models[selectedModelIdx]?.info?.meta?.user}
<div class="mt-0.5 text-sm font-normal text-gray-400 dark:text-gray-500"> <div class="mt-0.5 text-sm font-normal text-gray-400 dark:text-gray-500">
By <a href="https://openwebui.com/m/{modelfile.user.username}" By
>{modelfile.user.name ? modelfile.user.name : `@${modelfile.user.username}`}</a {#if models[selectedModelIdx]?.info?.meta?.user.community}
> <a
href="https://openwebui.com/m/{models[selectedModelIdx]?.info?.meta?.user
.username}"
>{models[selectedModelIdx]?.info?.meta?.user.name
? models[selectedModelIdx]?.info?.meta?.user.name
: `@${models[selectedModelIdx]?.info?.meta?.user.username}`}</a
>
{:else}
{models[selectedModelIdx]?.info?.meta?.user.name}
{/if}
</div> </div>
{/if} {/if}
{:else} {:else}
...@@ -99,7 +94,11 @@ ...@@ -99,7 +94,11 @@
</div> </div>
<div class=" w-full" in:fade={{ duration: 200, delay: 300 }}> <div class=" w-full" in:fade={{ duration: 200, delay: 300 }}>
<Suggestions {suggestionPrompts} {submitPrompt} /> <Suggestions
suggestionPrompts={models[selectedModelIdx]?.info?.meta?.suggestion_prompts ??
$config.default_prompt_suggestions}
{submitPrompt}
/>
</div> </div>
</div> </div>
{/key} {/key}
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
import { config, settings } from '$lib/stores'; import { config, models, settings } from '$lib/stores';
import { synthesizeOpenAISpeech } from '$lib/apis/audio'; import { synthesizeOpenAISpeech } from '$lib/apis/audio';
import { imageGenerations } from '$lib/apis/images'; import { imageGenerations } from '$lib/apis/images';
import { import {
...@@ -34,7 +34,6 @@ ...@@ -34,7 +34,6 @@
import RateComment from './RateComment.svelte'; import RateComment from './RateComment.svelte';
import CitationsModal from '$lib/components/chat/Messages/CitationsModal.svelte'; import CitationsModal from '$lib/components/chat/Messages/CitationsModal.svelte';
export let modelfiles = [];
export let message; export let message;
export let siblings; export let siblings;
...@@ -52,6 +51,9 @@ ...@@ -52,6 +51,9 @@
export let continueGeneration: Function; export let continueGeneration: Function;
export let regenerateResponse: Function; export let regenerateResponse: Function;
let model = null;
$: model = $models.find((m) => m.id === message.model);
let edit = false; let edit = false;
let editedContent = ''; let editedContent = '';
let editTextAreaElement: HTMLTextAreaElement; let editTextAreaElement: HTMLTextAreaElement;
...@@ -78,6 +80,13 @@ ...@@ -78,6 +80,13 @@
return `<code>${code.replaceAll('&amp;', '&')}</code>`; return `<code>${code.replaceAll('&amp;', '&')}</code>`;
}; };
// Open all links in a new tab/window (from https://github.com/markedjs/marked/issues/655#issuecomment-383226346)
const origLinkRenderer = renderer.link;
renderer.link = (href, title, text) => {
const html = origLinkRenderer.call(renderer, href, title, text);
return html.replace(/^<a /, '<a target="_blank" rel="nofollow" ');
};
const { extensions, ...defaults } = marked.getDefaults() as marked.MarkedOptions & { const { extensions, ...defaults } = marked.getDefaults() as marked.MarkedOptions & {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
extensions: any; extensions: any;
...@@ -338,17 +347,13 @@ ...@@ -338,17 +347,13 @@
dir={$settings.chatDirection} dir={$settings.chatDirection}
> >
<ProfileImage <ProfileImage
src={modelfiles[message.model]?.imageUrl ?? src={model?.info?.meta?.profile_image_url ??
($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)} ($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)}
/> />
<div class="w-full overflow-hidden pl-1"> <div class="w-full overflow-hidden pl-1">
<Name> <Name>
{#if message.model in modelfiles} {model?.name ?? message.model}
{modelfiles[message.model]?.title}
{:else}
{message.model ? ` ${message.model}` : ''}
{/if}
{#if message.timestamp} {#if message.timestamp}
<span <span
...@@ -391,7 +396,7 @@ ...@@ -391,7 +396,7 @@
<div class=" mt-2 mb-1 flex justify-end space-x-1.5 text-sm font-medium"> <div class=" mt-2 mb-1 flex justify-end space-x-1.5 text-sm font-medium">
<button <button
id="close-edit-message-button" id="close-edit-message-button"
class=" px-4 py-2 bg-gray-900 hover:bg-gray-850 text-gray-100 transition rounded-3xl" class="px-4 py-2 bg-white hover:bg-gray-100 text-gray-800 transition rounded-3xl"
on:click={() => { on:click={() => {
cancelEditMessage(); cancelEditMessage();
}} }}
...@@ -401,7 +406,7 @@ ...@@ -401,7 +406,7 @@
<button <button
id="save-edit-message-button" id="save-edit-message-button"
class="px-4 py-2 bg-white hover:bg-gray-100 text-gray-800 transition rounded-3xl" class=" px-4 py-2 bg-gray-900 hover:bg-gray-850 text-gray-100 transition rounded-3xl"
on:click={() => { on:click={() => {
editMessageConfirmHandler(); editMessageConfirmHandler();
}} }}
...@@ -442,8 +447,8 @@ ...@@ -442,8 +447,8 @@
{#if token.type === 'code'} {#if token.type === 'code'}
<CodeBlock <CodeBlock
id={`${message.id}-${tokenIdx}`} id={`${message.id}-${tokenIdx}`}
lang={token.lang} lang={token?.lang ?? ''}
code={revertSanitizedResponseContent(token.text)} code={revertSanitizedResponseContent(token?.text ?? '')}
/> />
{:else} {:else}
{@html marked.parse(token.raw, { {@html marked.parse(token.raw, {
...@@ -688,7 +693,7 @@ ...@@ -688,7 +693,7 @@
</button> </button>
</Tooltip> </Tooltip>
{#if $config.images && !readOnly} {#if $config?.features.enable_image_generation && !readOnly}
<Tooltip content="Generate Image" placement="bottom"> <Tooltip content="Generate Image" placement="bottom">
<button <button
class="{isLastMessage class="{isLastMessage
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
import { tick, createEventDispatcher, getContext } from 'svelte'; import { tick, createEventDispatcher, getContext } from 'svelte';
import Name from './Name.svelte'; import Name from './Name.svelte';
import ProfileImage from './ProfileImage.svelte'; import ProfileImage from './ProfileImage.svelte';
import { modelfiles, settings } from '$lib/stores'; import { models, settings } from '$lib/stores';
import Tooltip from '$lib/components/common/Tooltip.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte';
import { user as _user } from '$lib/stores'; import { user as _user } from '$lib/stores';
...@@ -60,8 +60,7 @@ ...@@ -60,8 +60,7 @@
{#if !($settings?.chatBubble ?? true)} {#if !($settings?.chatBubble ?? true)}
<ProfileImage <ProfileImage
src={message.user src={message.user
? $modelfiles.find((modelfile) => modelfile.tagName === message.user)?.imageUrl ?? ? $models.find((m) => m.id === message.user)?.info?.meta?.profile_image_url ?? '/user.png'
'/user.png'
: user?.profile_image_url ?? '/user.png'} : user?.profile_image_url ?? '/user.png'}
/> />
{/if} {/if}
...@@ -70,12 +69,8 @@ ...@@ -70,12 +69,8 @@
<div> <div>
<Name> <Name>
{#if message.user} {#if message.user}
{#if $modelfiles.map((modelfile) => modelfile.tagName).includes(message.user)} {$i18n.t('You')}
{$modelfiles.find((modelfile) => modelfile.tagName === message.user)?.title} <span class=" text-gray-500 text-sm font-medium">{message?.user ?? ''}</span>
{:else}
{$i18n.t('You')}
<span class=" text-gray-500 text-sm font-medium">{message?.user ?? ''}</span>
{/if}
{:else if $settings.showUsername || $_user.name !== user.name} {:else if $settings.showUsername || $_user.name !== user.name}
{user.name} {user.name}
{:else} {:else}
...@@ -201,7 +196,7 @@ ...@@ -201,7 +196,7 @@
<div class=" mt-2 mb-1 flex justify-end space-x-1.5 text-sm font-medium"> <div class=" mt-2 mb-1 flex justify-end space-x-1.5 text-sm font-medium">
<button <button
id="close-edit-message-button" id="close-edit-message-button"
class=" px-4 py-2 bg-gray-900 hover:bg-gray-850 text-gray-100 transition rounded-3xl" class="px-4 py-2 bg-white hover:bg-gray-100 text-gray-800 transition rounded-3xl"
on:click={() => { on:click={() => {
cancelEditMessage(); cancelEditMessage();
}} }}
...@@ -211,7 +206,7 @@ ...@@ -211,7 +206,7 @@
<button <button
id="save-edit-message-button" id="save-edit-message-button"
class="px-4 py-2 bg-white hover:bg-gray-100 text-gray-800 transition rounded-3xl" class=" px-4 py-2 bg-gray-900 hover:bg-gray-850 text-gray-100 transition rounded-3xl"
on:click={() => { on:click={() => {
editMessageConfirmHandler(); editMessageConfirmHandler();
}} }}
......
<script lang="ts"> <script lang="ts">
import { Collapsible } from 'bits-ui';
import { setDefaultModels } from '$lib/apis/configs';
import { models, showSettings, settings, user, mobile } from '$lib/stores'; import { models, showSettings, settings, user, mobile } from '$lib/stores';
import { onMount, tick, getContext } from 'svelte'; import { onMount, tick, getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import Selector from './ModelSelector/Selector.svelte'; import Selector from './ModelSelector/Selector.svelte';
import Tooltip from '../common/Tooltip.svelte'; import Tooltip from '../common/Tooltip.svelte';
import { setDefaultModels } from '$lib/apis/configs';
import { updateUserSettings } from '$lib/apis/users';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
export let selectedModels = ['']; export let selectedModels = [''];
...@@ -22,12 +22,8 @@ ...@@ -22,12 +22,8 @@
return; return;
} }
settings.set({ ...$settings, models: selectedModels }); settings.set({ ...$settings, models: selectedModels });
localStorage.setItem('settings', JSON.stringify($settings)); await updateUserSettings(localStorage.token, { ui: $settings });
if ($user.role === 'admin') {
console.log('setting default models globally');
await setDefaultModels(localStorage.token, selectedModels.join(','));
}
toast.success($i18n.t('Default model updated')); toast.success($i18n.t('Default model updated'));
}; };
...@@ -45,13 +41,11 @@ ...@@ -45,13 +41,11 @@
<div class="mr-1 max-w-full"> <div class="mr-1 max-w-full">
<Selector <Selector
placeholder={$i18n.t('Select a model')} placeholder={$i18n.t('Select a model')}
items={$models items={$models.map((model) => ({
.filter((model) => model.name !== 'hr') value: model.id,
.map((model) => ({ label: model.name,
value: model.id, model: model
label: model.name, }))}
info: model
}))}
bind:value={selectedModel} bind:value={selectedModel}
/> />
</div> </div>
......
...@@ -12,7 +12,9 @@ ...@@ -12,7 +12,9 @@
import { user, MODEL_DOWNLOAD_POOL, models, mobile } from '$lib/stores'; import { user, MODEL_DOWNLOAD_POOL, models, mobile } from '$lib/stores';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { capitalizeFirstLetter, getModels, splitStream } from '$lib/utils'; import { capitalizeFirstLetter, sanitizeResponseContent, splitStream } from '$lib/utils';
import { getModels } from '$lib/apis';
import Tooltip from '$lib/components/common/Tooltip.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
...@@ -23,7 +25,12 @@ ...@@ -23,7 +25,12 @@
export let searchEnabled = true; export let searchEnabled = true;
export let searchPlaceholder = $i18n.t('Search a model'); export let searchPlaceholder = $i18n.t('Search a model');
export let items = [{ value: 'mango', label: 'Mango' }]; export let items: {
label: string;
value: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
} = [];
export let className = 'w-[30rem]'; export let className = 'w-[30rem]';
...@@ -239,19 +246,37 @@ ...@@ -239,19 +246,37 @@
}} }}
> >
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="line-clamp-1"> <div class="flex items-center">
{item.label} <div class="line-clamp-1">
{item.label}
<span class=" text-xs font-medium text-gray-600 dark:text-gray-400" </div>
>{item.info?.details?.parameter_size ?? ''}</span {#if item.model.owned_by === 'ollama' && (item.model.ollama?.details?.parameter_size ?? '') !== ''}
> <div class="flex ml-1 items-center">
<Tooltip
content={`${
item.model.ollama?.details?.quantization_level
? item.model.ollama?.details?.quantization_level + ' '
: ''
}${
item.model.ollama?.size
? `(${(item.model.ollama?.size / 1024 ** 3).toFixed(1)}GB)`
: ''
}`}
className="self-end"
>
<span class=" text-xs font-medium text-gray-600 dark:text-gray-400"
>{item.model.ollama?.details?.parameter_size ?? ''}</span
>
</Tooltip>
</div>
{/if}
</div> </div>
<!-- {JSON.stringify(item.info)} --> <!-- {JSON.stringify(item.info)} -->
{#if item.info.external} {#if item.model.owned_by === 'openai'}
<Tooltip content={item.info?.source ?? 'External'}> <Tooltip content={`${'External'}`}>
<div class=" mr-2"> <div class="">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16" viewBox="0 0 16 16"
...@@ -271,15 +296,15 @@ ...@@ -271,15 +296,15 @@
</svg> </svg>
</div> </div>
</Tooltip> </Tooltip>
{:else} {/if}
{#if item.model?.info?.meta?.description}
<Tooltip <Tooltip
content={`${ content={`${sanitizeResponseContent(
item.info?.details?.quantization_level item.model?.info?.meta?.description
? item.info?.details?.quantization_level + ' ' ).replaceAll('\n', '<br>')}`}
: ''
}${item.info.size ? `(${(item.info.size / 1024 ** 3).toFixed(1)}GB)` : ''}`}
> >
<div class=" mr-2"> <div class="">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
......
<script lang="ts"> <script lang="ts">
import { getVersionUpdates } from '$lib/apis'; import { getVersionUpdates } from '$lib/apis';
import { getOllamaVersion } from '$lib/apis/ollama'; import { getOllamaVersion } from '$lib/apis/ollama';
import { WEBUI_VERSION } from '$lib/constants'; import { WEBUI_BUILD_HASH, WEBUI_VERSION } from '$lib/constants';
import { WEBUI_NAME, config, showChangelog } from '$lib/stores'; import { WEBUI_NAME, config, showChangelog } from '$lib/stores';
import { compareVersion } from '$lib/utils'; import { compareVersion } from '$lib/utils';
import { onMount, getContext } from 'svelte'; import { onMount, getContext } from 'svelte';
...@@ -54,7 +54,7 @@ ...@@ -54,7 +54,7 @@
<div class="flex w-full justify-between items-center"> <div class="flex w-full justify-between items-center">
<div class="flex flex-col text-xs text-gray-700 dark:text-gray-200"> <div class="flex flex-col text-xs text-gray-700 dark:text-gray-200">
<div class="flex gap-1"> <div class="flex gap-1">
<Tooltip content={WEBUI_VERSION === '0.1.117' ? "🪖 We're just getting started." : ''}> <Tooltip content={WEBUI_BUILD_HASH}>
v{WEBUI_VERSION} v{WEBUI_VERSION}
</Tooltip> </Tooltip>
......
<script lang="ts">
import { createEventDispatcher, onMount, getContext } from 'svelte';
import AdvancedParams from './Advanced/AdvancedParams.svelte';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
export let saveSettings: Function;
// Advanced
let requestFormat = '';
let keepAlive = null;
let options = {
// Advanced
seed: 0,
temperature: '',
repeat_penalty: '',
repeat_last_n: '',
mirostat: '',
mirostat_eta: '',
mirostat_tau: '',
top_k: '',
top_p: '',
stop: '',
tfs_z: '',
num_ctx: '',
num_predict: ''
};
const toggleRequestFormat = async () => {
if (requestFormat === '') {
requestFormat = 'json';
} else {
requestFormat = '';
}
saveSettings({ requestFormat: requestFormat !== '' ? requestFormat : undefined });
};
onMount(() => {
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
requestFormat = settings.requestFormat ?? '';
keepAlive = settings.keepAlive ?? null;
options.seed = settings.seed ?? 0;
options.temperature = settings.temperature ?? '';
options.repeat_penalty = settings.repeat_penalty ?? '';
options.top_k = settings.top_k ?? '';
options.top_p = settings.top_p ?? '';
options.num_ctx = settings.num_ctx ?? '';
options = { ...options, ...settings.options };
options.stop = (settings?.options?.stop ?? []).join(',');
});
</script>
<div class="flex flex-col h-full justify-between text-sm">
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
<div class=" text-sm font-medium">{$i18n.t('Parameters')}</div>
<AdvancedParams bind:options />
<hr class=" dark:border-gray-700" />
<div class=" py-1 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Keep Alive')}</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
type="button"
on:click={() => {
keepAlive = keepAlive === null ? '5m' : null;
}}
>
{#if keepAlive === null}
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
{/if}
</button>
</div>
{#if keepAlive !== null}
<div class="flex mt-1 space-x-2">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
type="text"
placeholder={$i18n.t("e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.")}
bind:value={keepAlive}
/>
</div>
{/if}
</div>
<div>
<div class=" py-1 flex w-full justify-between">
<div class=" self-center text-sm font-medium">{$i18n.t('Request Mode')}</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
on:click={() => {
toggleRequestFormat();
}}
>
{#if requestFormat === ''}
<span class="ml-2 self-center"> {$i18n.t('Default')} </span>
{:else if requestFormat === 'json'}
<!-- <svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4 self-center"
>
<path
d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z"
/>
</svg> -->
<span class="ml-2 self-center">{$i18n.t('JSON')}</span>
{/if}
</button>
</div>
</div>
</div>
<div class="flex justify-end pt-3 text-sm font-medium">
<button
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
on:click={() => {
saveSettings({
options: {
seed: (options.seed !== 0 ? options.seed : undefined) ?? undefined,
stop: options.stop !== '' ? options.stop.split(',').filter((e) => e) : undefined,
temperature: options.temperature !== '' ? options.temperature : undefined,
repeat_penalty: options.repeat_penalty !== '' ? options.repeat_penalty : undefined,
repeat_last_n: options.repeat_last_n !== '' ? options.repeat_last_n : undefined,
mirostat: options.mirostat !== '' ? options.mirostat : undefined,
mirostat_eta: options.mirostat_eta !== '' ? options.mirostat_eta : undefined,
mirostat_tau: options.mirostat_tau !== '' ? options.mirostat_tau : undefined,
top_k: options.top_k !== '' ? options.top_k : undefined,
top_p: options.top_p !== '' ? options.top_p : undefined,
tfs_z: options.tfs_z !== '' ? options.tfs_z : undefined,
num_ctx: options.num_ctx !== '' ? options.num_ctx : undefined,
num_predict: options.num_predict !== '' ? options.num_predict : undefined
},
keepAlive: keepAlive ? (isNaN(keepAlive) ? keepAlive : parseInt(keepAlive)) : undefined
});
dispatch('save');
}}
>
{$i18n.t('Save')}
</button>
</div>
</div>
<script lang="ts"> <script lang="ts">
import { getContext } from 'svelte'; import { getContext, createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
const i18n = getContext('i18n'); const i18n = getContext('i18n');
export let options = { export let params = {
// Advanced // Advanced
seed: 0, seed: 0,
stop: '', stop: null,
temperature: '', temperature: '',
repeat_penalty: '', frequency_penalty: '',
repeat_last_n: '', repeat_last_n: '',
mirostat: '', mirostat: '',
mirostat_eta: '', mirostat_eta: '',
...@@ -17,40 +19,86 @@ ...@@ -17,40 +19,86 @@
top_p: '', top_p: '',
tfs_z: '', tfs_z: '',
num_ctx: '', num_ctx: '',
num_predict: '' max_tokens: '',
template: null
}; };
let customFieldName = '';
let customFieldValue = '';
$: if (params) {
dispatch('change', params);
}
</script> </script>
<div class=" space-y-3 text-xs"> <div class=" space-y-1 text-xs">
<div> <div class=" py-0.5 w-full justify-between">
<div class=" py-0.5 flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Seed')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Seed')}</div>
<div class=" flex-1 self-center">
<input <button
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="p-1 px-3 text-xs flex rounded transition"
type="number" type="button"
placeholder="Enter Seed" on:click={() => {
bind:value={options.seed} params.seed = (params?.seed ?? null) === null ? 0 : null;
autocomplete="off" }}
min="0" >
/> {#if (params?.seed ?? null) === null}
</div> <span class="ml-2 self-center"> {$i18n.t('Default')} </span>
{:else}
<span class="ml-2 self-center"> {$i18n.t('Custom')} </span>
{/if}
</button>
</div> </div>
</div>
<div> {#if (params?.seed ?? null) !== null}
<div class=" py-0.5 flex w-full justify-between"> <div class="flex mt-0.5 space-x-2">
<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Stop Sequence')}</div> <div class=" flex-1">
<div class=" flex-1 self-center"> <input
<input class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" type="number"
type="text" placeholder="Enter Seed"
placeholder={$i18n.t('Enter stop sequence')} bind:value={params.seed}
bind:value={options.stop} autocomplete="off"
autocomplete="off" min="0"
/> />
</div>
</div> </div>
{/if}
</div>
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Stop Sequence')}</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
type="button"
on:click={() => {
params.stop = (params?.stop ?? null) === null ? '' : null;
}}
>
{#if (params?.stop ?? null) === null}
<span class="ml-2 self-center"> {$i18n.t('Default')} </span>
{:else}
<span class="ml-2 self-center"> {$i18n.t('Custom')} </span>
{/if}
</button>
</div> </div>
{#if (params?.stop ?? null) !== null}
<div class="flex mt-0.5 space-x-2">
<div class=" flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
type="text"
placeholder={$i18n.t('Enter stop sequence')}
bind:value={params.stop}
autocomplete="off"
/>
</div>
</div>
{/if}
</div> </div>
<div class=" py-0.5 w-full justify-between"> <div class=" py-0.5 w-full justify-between">
...@@ -61,10 +109,10 @@ ...@@ -61,10 +109,10 @@
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
type="button" type="button"
on:click={() => { on:click={() => {
options.temperature = options.temperature === '' ? 0.8 : ''; params.temperature = (params?.temperature ?? '') === '' ? 0.8 : '';
}} }}
> >
{#if options.temperature === ''} {#if (params?.temperature ?? '') === ''}
<span class="ml-2 self-center"> {$i18n.t('Default')} </span> <span class="ml-2 self-center"> {$i18n.t('Default')} </span>
{:else} {:else}
<span class="ml-2 self-center"> {$i18n.t('Custom')} </span> <span class="ml-2 self-center"> {$i18n.t('Custom')} </span>
...@@ -72,7 +120,7 @@ ...@@ -72,7 +120,7 @@
</button> </button>
</div> </div>
{#if options.temperature !== ''} {#if (params?.temperature ?? '') !== ''}
<div class="flex mt-0.5 space-x-2"> <div class="flex mt-0.5 space-x-2">
<div class=" flex-1"> <div class=" flex-1">
<input <input
...@@ -81,13 +129,13 @@ ...@@ -81,13 +129,13 @@
min="0" min="0"
max="1" max="1"
step="0.05" step="0.05"
bind:value={options.temperature} bind:value={params.temperature}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/> />
</div> </div>
<div> <div>
<input <input
bind:value={options.temperature} bind:value={params.temperature}
type="number" type="number"
class=" bg-transparent text-center w-14" class=" bg-transparent text-center w-14"
min="0" min="0"
...@@ -107,18 +155,18 @@ ...@@ -107,18 +155,18 @@
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
type="button" type="button"
on:click={() => { on:click={() => {
options.mirostat = options.mirostat === '' ? 0 : ''; params.mirostat = (params?.mirostat ?? '') === '' ? 0 : '';
}} }}
> >
{#if options.mirostat === ''} {#if (params?.mirostat ?? '') === ''}
<span class="ml-2 self-center">{$i18n.t('Default')}</span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else} {:else}
<span class="ml-2 self-center">{$i18n.t('Default')}</span> <span class="ml-2 self-center">{$i18n.t('Custom')}</span>
{/if} {/if}
</button> </button>
</div> </div>
{#if options.mirostat !== ''} {#if (params?.mirostat ?? '') !== ''}
<div class="flex mt-0.5 space-x-2"> <div class="flex mt-0.5 space-x-2">
<div class=" flex-1"> <div class=" flex-1">
<input <input
...@@ -127,13 +175,13 @@ ...@@ -127,13 +175,13 @@
min="0" min="0"
max="2" max="2"
step="1" step="1"
bind:value={options.mirostat} bind:value={params.mirostat}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/> />
</div> </div>
<div> <div>
<input <input
bind:value={options.mirostat} bind:value={params.mirostat}
type="number" type="number"
class=" bg-transparent text-center w-14" class=" bg-transparent text-center w-14"
min="0" min="0"
...@@ -153,18 +201,18 @@ ...@@ -153,18 +201,18 @@
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
type="button" type="button"
on:click={() => { on:click={() => {
options.mirostat_eta = options.mirostat_eta === '' ? 0.1 : ''; params.mirostat_eta = (params?.mirostat_eta ?? '') === '' ? 0.1 : '';
}} }}
> >
{#if options.mirostat_eta === ''} {#if (params?.mirostat_eta ?? '') === ''}
<span class="ml-2 self-center">{$i18n.t('Default')}</span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else} {:else}
<span class="ml-2 self-center">{$i18n.t('Default')}</span> <span class="ml-2 self-center">{$i18n.t('Custom')}</span>
{/if} {/if}
</button> </button>
</div> </div>
{#if options.mirostat_eta !== ''} {#if (params?.mirostat_eta ?? '') !== ''}
<div class="flex mt-0.5 space-x-2"> <div class="flex mt-0.5 space-x-2">
<div class=" flex-1"> <div class=" flex-1">
<input <input
...@@ -173,13 +221,13 @@ ...@@ -173,13 +221,13 @@
min="0" min="0"
max="1" max="1"
step="0.05" step="0.05"
bind:value={options.mirostat_eta} bind:value={params.mirostat_eta}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/> />
</div> </div>
<div> <div>
<input <input
bind:value={options.mirostat_eta} bind:value={params.mirostat_eta}
type="number" type="number"
class=" bg-transparent text-center w-14" class=" bg-transparent text-center w-14"
min="0" min="0"
...@@ -199,10 +247,10 @@ ...@@ -199,10 +247,10 @@
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
type="button" type="button"
on:click={() => { on:click={() => {
options.mirostat_tau = options.mirostat_tau === '' ? 5.0 : ''; params.mirostat_tau = (params?.mirostat_tau ?? '') === '' ? 5.0 : '';
}} }}
> >
{#if options.mirostat_tau === ''} {#if (params?.mirostat_tau ?? '') === ''}
<span class="ml-2 self-center">{$i18n.t('Default')}</span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else} {:else}
<span class="ml-2 self-center">{$i18n.t('Custom')}</span> <span class="ml-2 self-center">{$i18n.t('Custom')}</span>
...@@ -210,7 +258,7 @@ ...@@ -210,7 +258,7 @@
</button> </button>
</div> </div>
{#if options.mirostat_tau !== ''} {#if (params?.mirostat_tau ?? '') !== ''}
<div class="flex mt-0.5 space-x-2"> <div class="flex mt-0.5 space-x-2">
<div class=" flex-1"> <div class=" flex-1">
<input <input
...@@ -219,13 +267,13 @@ ...@@ -219,13 +267,13 @@
min="0" min="0"
max="10" max="10"
step="0.5" step="0.5"
bind:value={options.mirostat_tau} bind:value={params.mirostat_tau}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/> />
</div> </div>
<div> <div>
<input <input
bind:value={options.mirostat_tau} bind:value={params.mirostat_tau}
type="number" type="number"
class=" bg-transparent text-center w-14" class=" bg-transparent text-center w-14"
min="0" min="0"
...@@ -245,18 +293,18 @@ ...@@ -245,18 +293,18 @@
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
type="button" type="button"
on:click={() => { on:click={() => {
options.top_k = options.top_k === '' ? 40 : ''; params.top_k = (params?.top_k ?? '') === '' ? 40 : '';
}} }}
> >
{#if options.top_k === ''} {#if (params?.top_k ?? '') === ''}
<span class="ml-2 self-center">{$i18n.t('Default')}</span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else} {:else}
<span class="ml-2 self-center">{$i18n.t('Default')}</span> <span class="ml-2 self-center">{$i18n.t('Custom')}</span>
{/if} {/if}
</button> </button>
</div> </div>
{#if options.top_k !== ''} {#if (params?.top_k ?? '') !== ''}
<div class="flex mt-0.5 space-x-2"> <div class="flex mt-0.5 space-x-2">
<div class=" flex-1"> <div class=" flex-1">
<input <input
...@@ -265,13 +313,13 @@ ...@@ -265,13 +313,13 @@
min="0" min="0"
max="100" max="100"
step="0.5" step="0.5"
bind:value={options.top_k} bind:value={params.top_k}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/> />
</div> </div>
<div> <div>
<input <input
bind:value={options.top_k} bind:value={params.top_k}
type="number" type="number"
class=" bg-transparent text-center w-14" class=" bg-transparent text-center w-14"
min="0" min="0"
...@@ -291,18 +339,18 @@ ...@@ -291,18 +339,18 @@
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
type="button" type="button"
on:click={() => { on:click={() => {
options.top_p = options.top_p === '' ? 0.9 : ''; params.top_p = (params?.top_p ?? '') === '' ? 0.9 : '';
}} }}
> >
{#if options.top_p === ''} {#if (params?.top_p ?? '') === ''}
<span class="ml-2 self-center">{$i18n.t('Default')}</span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else} {:else}
<span class="ml-2 self-center">{$i18n.t('Default')}</span> <span class="ml-2 self-center">{$i18n.t('Custom')}</span>
{/if} {/if}
</button> </button>
</div> </div>
{#if options.top_p !== ''} {#if (params?.top_p ?? '') !== ''}
<div class="flex mt-0.5 space-x-2"> <div class="flex mt-0.5 space-x-2">
<div class=" flex-1"> <div class=" flex-1">
<input <input
...@@ -311,13 +359,13 @@ ...@@ -311,13 +359,13 @@
min="0" min="0"
max="1" max="1"
step="0.05" step="0.05"
bind:value={options.top_p} bind:value={params.top_p}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/> />
</div> </div>
<div> <div>
<input <input
bind:value={options.top_p} bind:value={params.top_p}
type="number" type="number"
class=" bg-transparent text-center w-14" class=" bg-transparent text-center w-14"
min="0" min="0"
...@@ -331,24 +379,24 @@ ...@@ -331,24 +379,24 @@
<div class=" py-0.5 w-full justify-between"> <div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Repeat Penalty')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Frequencey Penalty')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
type="button" type="button"
on:click={() => { on:click={() => {
options.repeat_penalty = options.repeat_penalty === '' ? 1.1 : ''; params.frequency_penalty = (params?.frequency_penalty ?? '') === '' ? 1.1 : '';
}} }}
> >
{#if options.repeat_penalty === ''} {#if (params?.frequency_penalty ?? '') === ''}
<span class="ml-2 self-center">{$i18n.t('Default')}</span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else} {:else}
<span class="ml-2 self-center">{$i18n.t('Default')}</span> <span class="ml-2 self-center">{$i18n.t('Custom')}</span>
{/if} {/if}
</button> </button>
</div> </div>
{#if options.repeat_penalty !== ''} {#if (params?.frequency_penalty ?? '') !== ''}
<div class="flex mt-0.5 space-x-2"> <div class="flex mt-0.5 space-x-2">
<div class=" flex-1"> <div class=" flex-1">
<input <input
...@@ -357,13 +405,13 @@ ...@@ -357,13 +405,13 @@
min="0" min="0"
max="2" max="2"
step="0.05" step="0.05"
bind:value={options.repeat_penalty} bind:value={params.frequency_penalty}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/> />
</div> </div>
<div> <div>
<input <input
bind:value={options.repeat_penalty} bind:value={params.frequency_penalty}
type="number" type="number"
class=" bg-transparent text-center w-14" class=" bg-transparent text-center w-14"
min="0" min="0"
...@@ -383,18 +431,18 @@ ...@@ -383,18 +431,18 @@
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
type="button" type="button"
on:click={() => { on:click={() => {
options.repeat_last_n = options.repeat_last_n === '' ? 64 : ''; params.repeat_last_n = (params?.repeat_last_n ?? '') === '' ? 64 : '';
}} }}
> >
{#if options.repeat_last_n === ''} {#if (params?.repeat_last_n ?? '') === ''}
<span class="ml-2 self-center">{$i18n.t('Default')}</span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else} {:else}
<span class="ml-2 self-center">{$i18n.t('Default')}</span> <span class="ml-2 self-center">{$i18n.t('Custom')}</span>
{/if} {/if}
</button> </button>
</div> </div>
{#if options.repeat_last_n !== ''} {#if (params?.repeat_last_n ?? '') !== ''}
<div class="flex mt-0.5 space-x-2"> <div class="flex mt-0.5 space-x-2">
<div class=" flex-1"> <div class=" flex-1">
<input <input
...@@ -403,13 +451,13 @@ ...@@ -403,13 +451,13 @@
min="-1" min="-1"
max="128" max="128"
step="1" step="1"
bind:value={options.repeat_last_n} bind:value={params.repeat_last_n}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/> />
</div> </div>
<div> <div>
<input <input
bind:value={options.repeat_last_n} bind:value={params.repeat_last_n}
type="number" type="number"
class=" bg-transparent text-center w-14" class=" bg-transparent text-center w-14"
min="-1" min="-1"
...@@ -429,18 +477,18 @@ ...@@ -429,18 +477,18 @@
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
type="button" type="button"
on:click={() => { on:click={() => {
options.tfs_z = options.tfs_z === '' ? 1 : ''; params.tfs_z = (params?.tfs_z ?? '') === '' ? 1 : '';
}} }}
> >
{#if options.tfs_z === ''} {#if (params?.tfs_z ?? '') === ''}
<span class="ml-2 self-center">{$i18n.t('Default')}</span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else} {:else}
<span class="ml-2 self-center">{$i18n.t('Default')}</span> <span class="ml-2 self-center">{$i18n.t('Custom')}</span>
{/if} {/if}
</button> </button>
</div> </div>
{#if options.tfs_z !== ''} {#if (params?.tfs_z ?? '') !== ''}
<div class="flex mt-0.5 space-x-2"> <div class="flex mt-0.5 space-x-2">
<div class=" flex-1"> <div class=" flex-1">
<input <input
...@@ -449,13 +497,13 @@ ...@@ -449,13 +497,13 @@
min="0" min="0"
max="2" max="2"
step="0.05" step="0.05"
bind:value={options.tfs_z} bind:value={params.tfs_z}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/> />
</div> </div>
<div> <div>
<input <input
bind:value={options.tfs_z} bind:value={params.tfs_z}
type="number" type="number"
class=" bg-transparent text-center w-14" class=" bg-transparent text-center w-14"
min="0" min="0"
...@@ -475,18 +523,18 @@ ...@@ -475,18 +523,18 @@
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
type="button" type="button"
on:click={() => { on:click={() => {
options.num_ctx = options.num_ctx === '' ? 2048 : ''; params.num_ctx = (params?.num_ctx ?? '') === '' ? 2048 : '';
}} }}
> >
{#if options.num_ctx === ''} {#if (params?.num_ctx ?? '') === ''}
<span class="ml-2 self-center">{$i18n.t('Default')}</span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else} {:else}
<span class="ml-2 self-center">{$i18n.t('Default')}</span> <span class="ml-2 self-center">{$i18n.t('Custom')}</span>
{/if} {/if}
</button> </button>
</div> </div>
{#if options.num_ctx !== ''} {#if (params?.num_ctx ?? '') !== ''}
<div class="flex mt-0.5 space-x-2"> <div class="flex mt-0.5 space-x-2">
<div class=" flex-1"> <div class=" flex-1">
<input <input
...@@ -495,13 +543,13 @@ ...@@ -495,13 +543,13 @@
min="-1" min="-1"
max="10240000" max="10240000"
step="1" step="1"
bind:value={options.num_ctx} bind:value={params.num_ctx}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/> />
</div> </div>
<div class=""> <div class="">
<input <input
bind:value={options.num_ctx} bind:value={params.num_ctx}
type="number" type="number"
class=" bg-transparent text-center w-14" class=" bg-transparent text-center w-14"
min="-1" min="-1"
...@@ -513,24 +561,24 @@ ...@@ -513,24 +561,24 @@
</div> </div>
<div class=" py-0.5 w-full justify-between"> <div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Max Tokens')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Max Tokens (num_predict)')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
type="button" type="button"
on:click={() => { on:click={() => {
options.num_predict = options.num_predict === '' ? 128 : ''; params.max_tokens = (params?.max_tokens ?? '') === '' ? 128 : '';
}} }}
> >
{#if options.num_predict === ''} {#if (params?.max_tokens ?? '') === ''}
<span class="ml-2 self-center">{$i18n.t('Default')}</span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else} {:else}
<span class="ml-2 self-center">{$i18n.t('Default')}</span> <span class="ml-2 self-center">{$i18n.t('Custom')}</span>
{/if} {/if}
</button> </button>
</div> </div>
{#if options.num_predict !== ''} {#if (params?.max_tokens ?? '') !== ''}
<div class="flex mt-0.5 space-x-2"> <div class="flex mt-0.5 space-x-2">
<div class=" flex-1"> <div class=" flex-1">
<input <input
...@@ -539,13 +587,13 @@ ...@@ -539,13 +587,13 @@
min="-2" min="-2"
max="16000" max="16000"
step="1" step="1"
bind:value={options.num_predict} bind:value={params.max_tokens}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/> />
</div> </div>
<div class=""> <div class="">
<input <input
bind:value={options.num_predict} bind:value={params.max_tokens}
type="number" type="number"
class=" bg-transparent text-center w-14" class=" bg-transparent text-center w-14"
min="-2" min="-2"
...@@ -556,4 +604,36 @@ ...@@ -556,4 +604,36 @@
</div> </div>
{/if} {/if}
</div> </div>
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Template')}</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
type="button"
on:click={() => {
params.template = (params?.template ?? null) === null ? '' : null;
}}
>
{#if (params?.template ?? null) === null}
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
{/if}
</button>
</div>
{#if (params?.template ?? null) !== null}
<div class="flex mt-0.5 space-x-2">
<div class=" flex-1">
<textarea
class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg -mb-1"
placeholder="Write your model template content here"
rows="4"
bind:value={params.template}
/>
</div>
</div>
{/if}
</div>
</div> </div>
<script lang="ts"> <script lang="ts">
import { getAudioConfig, updateAudioConfig } from '$lib/apis/audio'; import { getAudioConfig, updateAudioConfig } from '$lib/apis/audio';
import { user } from '$lib/stores'; import { user, settings } from '$lib/stores';
import { createEventDispatcher, onMount, getContext } from 'svelte'; import { createEventDispatcher, onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
...@@ -99,16 +99,14 @@ ...@@ -99,16 +99,14 @@
}; };
onMount(async () => { onMount(async () => {
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); conversationMode = $settings.conversationMode ?? false;
speechAutoSend = $settings.speechAutoSend ?? false;
conversationMode = settings.conversationMode ?? false; responseAutoPlayback = $settings.responseAutoPlayback ?? false;
speechAutoSend = settings.speechAutoSend ?? false;
responseAutoPlayback = settings.responseAutoPlayback ?? false; STTEngine = $settings?.audio?.STTEngine ?? '';
TTSEngine = $settings?.audio?.TTSEngine ?? '';
STTEngine = settings?.audio?.STTEngine ?? ''; speaker = $settings?.audio?.speaker ?? '';
TTSEngine = settings?.audio?.TTSEngine ?? ''; model = $settings?.audio?.model ?? '';
speaker = settings?.audio?.speaker ?? '';
model = settings?.audio?.model ?? '';
if (TTSEngine === 'openai') { if (TTSEngine === 'openai') {
getOpenAIVoices(); getOpenAIVoices();
......
...@@ -2,9 +2,10 @@ ...@@ -2,9 +2,10 @@
import fileSaver from 'file-saver'; import fileSaver from 'file-saver';
const { saveAs } = fileSaver; const { saveAs } = fileSaver;
import { chats, user, config } from '$lib/stores'; import { chats, user, settings } from '$lib/stores';
import { import {
archiveAllChats,
createNewChat, createNewChat,
deleteAllChats, deleteAllChats,
getAllChats, getAllChats,
...@@ -22,7 +23,10 @@ ...@@ -22,7 +23,10 @@
// Chats // Chats
let saveChatHistory = true; let saveChatHistory = true;
let importFiles; let importFiles;
let showArchiveConfirm = false;
let showDeleteConfirm = false; let showDeleteConfirm = false;
let chatImportInputElement: HTMLInputElement; let chatImportInputElement: HTMLInputElement;
$: if (importFiles) { $: if (importFiles) {
...@@ -68,14 +72,15 @@ ...@@ -68,14 +72,15 @@
saveAs(blob, `chat-export-${Date.now()}.json`); saveAs(blob, `chat-export-${Date.now()}.json`);
}; };
const exportAllUserChats = async () => { const archiveAllChatsHandler = async () => {
let blob = new Blob([JSON.stringify(await getAllUserChats(localStorage.token))], { await goto('/');
type: 'application/json' await archiveAllChats(localStorage.token).catch((error) => {
toast.error(error);
}); });
saveAs(blob, `all-chats-export-${Date.now()}.json`); await chats.set(await getChatList(localStorage.token));
}; };
const deleteChats = async () => { const deleteAllChatsHandler = async () => {
await goto('/'); await goto('/');
await deleteAllChats(localStorage.token).catch((error) => { await deleteAllChats(localStorage.token).catch((error) => {
toast.error(error); toast.error(error);
...@@ -94,9 +99,7 @@ ...@@ -94,9 +99,7 @@
}; };
onMount(async () => { onMount(async () => {
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); saveChatHistory = $settings.saveChatHistory ?? true;
saveChatHistory = settings.saveChatHistory ?? true;
}); });
</script> </script>
...@@ -217,118 +220,177 @@ ...@@ -217,118 +220,177 @@
<hr class=" dark:border-gray-700" /> <hr class=" dark:border-gray-700" />
{#if showDeleteConfirm} <div class="flex flex-col">
<div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition"> {#if showArchiveConfirm}
<div class="flex items-center space-x-3"> <div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition">
<svg <div class="flex items-center space-x-3">
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" />
<path
fill-rule="evenodd"
d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM5.72 7.47a.75.75 0 0 1 1.06 0L8 8.69l1.22-1.22a.75.75 0 1 1 1.06 1.06L9.06 9.75l1.22 1.22a.75.75 0 1 1-1.06 1.06L8 10.81l-1.22 1.22a.75.75 0 0 1-1.06-1.06l1.22-1.22-1.22-1.22a.75.75 0 0 1 0-1.06Z"
clip-rule="evenodd"
/>
</svg>
<span>{$i18n.t('Are you sure?')}</span>
</div>
<div class="flex space-x-1.5 items-center">
<button
class="hover:text-white transition"
on:click={() => {
deleteChats();
showDeleteConfirm = false;
}}
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20" viewBox="0 0 16 16"
fill="currentColor" fill="currentColor"
class="w-4 h-4" class="w-4 h-4"
> >
<path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" />
<path <path
fill-rule="evenodd" fill-rule="evenodd"
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM5.72 7.47a.75.75 0 0 1 1.06 0L8 8.69l1.22-1.22a.75.75 0 1 1 1.06 1.06L9.06 9.75l1.22 1.22a.75.75 0 1 1-1.06 1.06L8 10.81l-1.22 1.22a.75.75 0 0 1-1.06-1.06l1.22-1.22-1.22-1.22a.75.75 0 0 1 0-1.06Z"
clip-rule="evenodd" clip-rule="evenodd"
/> />
</svg> </svg>
</button> <span>{$i18n.t('Are you sure?')}</span>
<button </div>
class="hover:text-white transition"
on:click={() => { <div class="flex space-x-1.5 items-center">
showDeleteConfirm = false; <button
}} class="hover:text-white transition"
> on:click={() => {
archiveAllChatsHandler();
showArchiveConfirm = false;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
clip-rule="evenodd"
/>
</svg>
</button>
<button
class="hover:text-white transition"
on:click={() => {
showArchiveConfirm = false;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
</div>
{:else}
<button
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
on:click={() => {
showArchiveConfirm = true;
}}
>
<div class=" self-center mr-3">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20" viewBox="0 0 24 24"
fill="currentColor" fill="currentColor"
class="w-4 h-4" class="size-4"
> >
<path <path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" d="M3.375 3C2.339 3 1.5 3.84 1.5 4.875v.75c0 1.036.84 1.875 1.875 1.875h17.25c1.035 0 1.875-.84 1.875-1.875v-.75C22.5 3.839 21.66 3 20.625 3H3.375Z"
/>
<path
fill-rule="evenodd"
d="m3.087 9 .54 9.176A3 3 0 0 0 6.62 21h10.757a3 3 0 0 0 2.995-2.824L20.913 9H3.087Zm6.163 3.75A.75.75 0 0 1 10 12h4a.75.75 0 0 1 0 1.5h-4a.75.75 0 0 1-.75-.75Z"
clip-rule="evenodd"
/> />
</svg> </svg>
</button> </div>
</div> <div class=" self-center text-sm font-medium">{$i18n.t('Archive All Chats')}</div>
</div> </button>
{:else} {/if}
<button
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
on:click={() => {
showDeleteConfirm = true;
}}
>
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm7 7a.75.75 0 0 1-.75.75h-4.5a.75.75 0 0 1 0-1.5h4.5A.75.75 0 0 1 11 9Z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center text-sm font-medium">{$i18n.t('Delete Chats')}</div>
</button>
{/if}
{#if $user?.role === 'admin' && ($config?.admin_export_enabled ?? true)} {#if showDeleteConfirm}
<hr class=" dark:border-gray-700" /> <div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition">
<div class="flex items-center space-x-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" />
<path
fill-rule="evenodd"
d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM5.72 7.47a.75.75 0 0 1 1.06 0L8 8.69l1.22-1.22a.75.75 0 1 1 1.06 1.06L9.06 9.75l1.22 1.22a.75.75 0 1 1-1.06 1.06L8 10.81l-1.22 1.22a.75.75 0 0 1-1.06-1.06l1.22-1.22-1.22-1.22a.75.75 0 0 1 0-1.06Z"
clip-rule="evenodd"
/>
</svg>
<span>{$i18n.t('Are you sure?')}</span>
</div>
<button <div class="flex space-x-1.5 items-center">
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition" <button
on:click={() => { class="hover:text-white transition"
exportAllUserChats(); on:click={() => {
}} deleteAllChatsHandler();
> showDeleteConfirm = false;
<div class=" self-center mr-3"> }}
<svg >
xmlns="http://www.w3.org/2000/svg" <svg
viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
fill="currentColor" viewBox="0 0 20 20"
class="w-4 h-4" fill="currentColor"
> class="w-4 h-4"
<path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" /> >
<path <path
fill-rule="evenodd" fill-rule="evenodd"
d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM8.75 7.75a.75.75 0 0 0-1.5 0v2.69L6.03 9.22a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l2.5-2.5a.75.75 0 1 0-1.06-1.06l-1.22 1.22V7.75Z" d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
clip-rule="evenodd" clip-rule="evenodd"
/> />
</svg> </svg>
</div> </button>
<div class=" self-center text-sm font-medium"> <button
{$i18n.t('Export All Chats (All Users)')} class="hover:text-white transition"
on:click={() => {
showDeleteConfirm = false;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
</div> </div>
</button> {:else}
{/if} <button
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
on:click={() => {
showDeleteConfirm = true;
}}
>
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm7 7a.75.75 0 0 1-.75.75h-4.5a.75.75 0 0 1 0-1.5h4.5A.75.75 0 0 1 11 9Z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center text-sm font-medium">{$i18n.t('Delete All Chats')}</div>
</button>
{/if}
</div>
</div> </div>
</div> </div>
...@@ -3,7 +3,13 @@ ...@@ -3,7 +3,13 @@
import { createEventDispatcher, onMount, getContext } from 'svelte'; import { createEventDispatcher, onMount, getContext } from 'svelte';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
import { getOllamaUrls, getOllamaVersion, updateOllamaUrls } from '$lib/apis/ollama'; import {
getOllamaConfig,
getOllamaUrls,
getOllamaVersion,
updateOllamaConfig,
updateOllamaUrls
} from '$lib/apis/ollama';
import { import {
getOpenAIConfig, getOpenAIConfig,
getOpenAIKeys, getOpenAIKeys,
...@@ -14,6 +20,7 @@ ...@@ -14,6 +20,7 @@
} from '$lib/apis/openai'; } from '$lib/apis/openai';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import Switch from '$lib/components/common/Switch.svelte'; import Switch from '$lib/components/common/Switch.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
...@@ -25,7 +32,8 @@ ...@@ -25,7 +32,8 @@
let OPENAI_API_KEYS = ['']; let OPENAI_API_KEYS = [''];
let OPENAI_API_BASE_URLS = ['']; let OPENAI_API_BASE_URLS = [''];
let ENABLE_OPENAI_API = false; let ENABLE_OPENAI_API = null;
let ENABLE_OLLAMA_API = null;
const updateOpenAIHandler = async () => { const updateOpenAIHandler = async () => {
OPENAI_API_BASE_URLS = await updateOpenAIUrls(localStorage.token, OPENAI_API_BASE_URLS); OPENAI_API_BASE_URLS = await updateOpenAIUrls(localStorage.token, OPENAI_API_BASE_URLS);
...@@ -50,13 +58,23 @@ ...@@ -50,13 +58,23 @@
onMount(async () => { onMount(async () => {
if ($user.role === 'admin') { if ($user.role === 'admin') {
OLLAMA_BASE_URLS = await getOllamaUrls(localStorage.token); await Promise.all([
(async () => {
OLLAMA_BASE_URLS = await getOllamaUrls(localStorage.token);
})(),
(async () => {
OPENAI_API_BASE_URLS = await getOpenAIUrls(localStorage.token);
})(),
(async () => {
OPENAI_API_KEYS = await getOpenAIKeys(localStorage.token);
})()
]);
const config = await getOpenAIConfig(localStorage.token); const ollamaConfig = await getOllamaConfig(localStorage.token);
ENABLE_OPENAI_API = config.ENABLE_OPENAI_API; const openaiConfig = await getOpenAIConfig(localStorage.token);
OPENAI_API_BASE_URLS = await getOpenAIUrls(localStorage.token); ENABLE_OPENAI_API = openaiConfig.ENABLE_OPENAI_API;
OPENAI_API_KEYS = await getOpenAIKeys(localStorage.token); ENABLE_OLLAMA_API = ollamaConfig.ENABLE_OLLAMA_API;
} }
}); });
</script> </script>
...@@ -68,189 +86,212 @@ ...@@ -68,189 +86,212 @@
dispatch('save'); dispatch('save');
}} }}
> >
<div class=" pr-1.5 overflow-y-scroll max-h-[25rem] space-y-3"> <div class="space-y-3 pr-1.5 overflow-y-scroll h-[24rem] max-h-[25rem]">
<div class=" space-y-3"> {#if ENABLE_OPENAI_API !== null && ENABLE_OLLAMA_API !== null}
<div class="mt-2 space-y-2 pr-1.5"> <div class=" space-y-3">
<div class="mt-2 space-y-2 pr-1.5">
<div class="flex justify-between items-center text-sm">
<div class=" font-medium">{$i18n.t('OpenAI API')}</div>
<div class="mt-1">
<Switch
bind:state={ENABLE_OPENAI_API}
on:change={async () => {
updateOpenAIConfig(localStorage.token, ENABLE_OPENAI_API);
}}
/>
</div>
</div>
{#if ENABLE_OPENAI_API}
<div class="flex flex-col gap-1">
{#each OPENAI_API_BASE_URLS as url, idx}
<div class="flex w-full gap-2">
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Base URL')}
bind:value={url}
autocomplete="off"
/>
</div>
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Key')}
bind:value={OPENAI_API_KEYS[idx]}
autocomplete="off"
/>
</div>
<div class="self-center flex items-center">
{#if idx === 0}
<button
class="px-1"
on:click={() => {
OPENAI_API_BASE_URLS = [...OPENAI_API_BASE_URLS, ''];
OPENAI_API_KEYS = [...OPENAI_API_KEYS, ''];
}}
type="button"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
/>
</svg>
</button>
{:else}
<button
class="px-1"
on:click={() => {
OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.filter(
(url, urlIdx) => idx !== urlIdx
);
OPENAI_API_KEYS = OPENAI_API_KEYS.filter((key, keyIdx) => idx !== keyIdx);
}}
type="button"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" />
</svg>
</button>
{/if}
</div>
</div>
<div class=" mb-1 text-xs text-gray-400 dark:text-gray-500">
{$i18n.t('WebUI will make requests to')}
<span class=" text-gray-200">'{url}/models'</span>
</div>
{/each}
</div>
{/if}
</div>
</div>
<hr class=" dark:border-gray-700" />
<div class="pr-1.5 space-y-2">
<div class="flex justify-between items-center text-sm"> <div class="flex justify-between items-center text-sm">
<div class=" font-medium">{$i18n.t('OpenAI API')}</div> <div class=" font-medium">{$i18n.t('Ollama API')}</div>
<div class="mt-1"> <div class="mt-1">
<Switch <Switch
bind:state={ENABLE_OPENAI_API} bind:state={ENABLE_OLLAMA_API}
on:change={async () => { on:change={async () => {
updateOpenAIConfig(localStorage.token, ENABLE_OPENAI_API); updateOllamaConfig(localStorage.token, ENABLE_OLLAMA_API);
}} }}
/> />
</div> </div>
</div> </div>
{#if ENABLE_OLLAMA_API}
{#if ENABLE_OPENAI_API} <div class="flex w-full gap-1.5">
<div class="flex flex-col gap-1"> <div class="flex-1 flex flex-col gap-2">
{#each OPENAI_API_BASE_URLS as url, idx} {#each OLLAMA_BASE_URLS as url, idx}
<div class="flex w-full gap-2"> <div class="flex gap-1.5">
<div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Base URL')} placeholder={$i18n.t('Enter URL (e.g. http://localhost:11434)')}
bind:value={url} bind:value={url}
autocomplete="off"
/> />
</div>
<div class="flex-1"> <div class="self-center flex items-center">
<input {#if idx === 0}
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" <button
placeholder={$i18n.t('API Key')} class="px-1"
bind:value={OPENAI_API_KEYS[idx]} on:click={() => {
autocomplete="off" OLLAMA_BASE_URLS = [...OLLAMA_BASE_URLS, ''];
/> }}
</div> type="button"
<div class="self-center flex items-center">
{#if idx === 0}
<button
class="px-1"
on:click={() => {
OPENAI_API_BASE_URLS = [...OPENAI_API_BASE_URLS, ''];
OPENAI_API_KEYS = [...OPENAI_API_KEYS, ''];
}}
type="button"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
> >
<path <svg
d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z" xmlns="http://www.w3.org/2000/svg"
/> viewBox="0 0 16 16"
</svg> fill="currentColor"
</button> class="w-4 h-4"
{:else} >
<button <path
class="px-1" d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
on:click={() => { />
OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.filter( </svg>
(url, urlIdx) => idx !== urlIdx </button>
); {:else}
OPENAI_API_KEYS = OPENAI_API_KEYS.filter((key, keyIdx) => idx !== keyIdx); <button
}} class="px-1"
type="button" on:click={() => {
> OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter(
<svg (url, urlIdx) => idx !== urlIdx
xmlns="http://www.w3.org/2000/svg" );
viewBox="0 0 16 16" }}
fill="currentColor" type="button"
class="w-4 h-4"
> >
<path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" /> <svg
</svg> xmlns="http://www.w3.org/2000/svg"
</button> viewBox="0 0 16 16"
{/if} fill="currentColor"
class="w-4 h-4"
>
<path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" />
</svg>
</button>
{/if}
</div>
</div> </div>
</div> {/each}
<div class=" mb-1 text-xs text-gray-400 dark:text-gray-500"> </div>
{$i18n.t('WebUI will make requests to')}
<span class=" text-gray-200">'{url}/models'</span>
</div>
{/each}
</div>
{/if}
</div>
</div>
<hr class=" dark:border-gray-700" />
<div>
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Ollama Base URL')}</div>
<div class="flex w-full gap-1.5">
<div class="flex-1 flex flex-col gap-2">
{#each OLLAMA_BASE_URLS as url, idx}
<div class="flex gap-1.5">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Enter URL (e.g. http://localhost:11434)')}
bind:value={url}
/>
<div class="self-center flex items-center"> <div class="flex">
{#if idx === 0} <button
<button class="self-center p-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition"
class="px-1" on:click={() => {
on:click={() => { updateOllamaUrlsHandler();
OLLAMA_BASE_URLS = [...OLLAMA_BASE_URLS, '']; }}
}} type="button"
type="button" >
> <svg
<svg xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
viewBox="0 0 16 16" fill="currentColor"
fill="currentColor" class="w-4 h-4"
class="w-4 h-4" >
> <path
<path fill-rule="evenodd"
d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z" d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
/> clip-rule="evenodd"
</svg> />
</button> </svg>
{:else} </button>
<button
class="px-1"
on:click={() => {
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url, urlIdx) => idx !== urlIdx);
}}
type="button"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" />
</svg>
</button>
{/if}
</div>
</div> </div>
{/each} </div>
</div>
<div class=""> <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
<button {$i18n.t('Trouble accessing Ollama?')}
class="p-2.5 bg-gray-200 hover:bg-gray-300 dark:bg-gray-850 dark:hover:bg-gray-800 rounded-lg transition" <a
on:click={() => { class=" text-gray-300 font-medium underline"
updateOllamaUrlsHandler(); href="https://github.com/open-webui/open-webui#troubleshooting"
}} target="_blank"
type="button"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
> >
<path {$i18n.t('Click here for help.')}
fill-rule="evenodd" </a>
d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z" </div>
clip-rule="evenodd" {/if}
/>
</svg>
</button>
</div>
</div> </div>
{:else}
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500"> <div class="flex h-full justify-center">
{$i18n.t('Trouble accessing Ollama?')} <div class="my-auto">
<a <Spinner className="size-6" />
class=" text-gray-300 font-medium underline" </div>
href="https://github.com/open-webui/open-webui#troubleshooting"
target="_blank"
>
{$i18n.t('Click here for help.')}
</a>
</div> </div>
</div> {/if}
</div> </div>
<div class="flex justify-end pt-3 text-sm font-medium"> <div class="flex justify-end pt-3 text-sm font-medium">
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
import { getLanguages } from '$lib/i18n'; import { getLanguages } from '$lib/i18n';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
import { models, user, theme } from '$lib/stores'; import { models, settings, theme } from '$lib/stores';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
...@@ -41,21 +41,21 @@ ...@@ -41,21 +41,21 @@
let requestFormat = ''; let requestFormat = '';
let keepAlive = null; let keepAlive = null;
let options = { let params = {
// Advanced // Advanced
seed: 0, seed: 0,
temperature: '', temperature: '',
repeat_penalty: '', frequency_penalty: '',
repeat_last_n: '', repeat_last_n: '',
mirostat: '', mirostat: '',
mirostat_eta: '', mirostat_eta: '',
mirostat_tau: '', mirostat_tau: '',
top_k: '', top_k: '',
top_p: '', top_p: '',
stop: '', stop: null,
tfs_z: '', tfs_z: '',
num_ctx: '', num_ctx: '',
num_predict: '' max_tokens: ''
}; };
const toggleRequestFormat = async () => { const toggleRequestFormat = async () => {
...@@ -71,23 +71,22 @@ ...@@ -71,23 +71,22 @@
onMount(async () => { onMount(async () => {
selectedTheme = localStorage.theme ?? 'system'; selectedTheme = localStorage.theme ?? 'system';
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
languages = await getLanguages(); languages = await getLanguages();
notificationEnabled = settings.notificationEnabled ?? false; notificationEnabled = $settings.notificationEnabled ?? false;
system = settings.system ?? ''; system = $settings.system ?? '';
requestFormat = settings.requestFormat ?? ''; requestFormat = $settings.requestFormat ?? '';
keepAlive = settings.keepAlive ?? null; keepAlive = $settings.keepAlive ?? null;
options.seed = settings.seed ?? 0; params.seed = $settings.seed ?? 0;
options.temperature = settings.temperature ?? ''; params.temperature = $settings.temperature ?? '';
options.repeat_penalty = settings.repeat_penalty ?? ''; params.frequency_penalty = $settings.frequency_penalty ?? '';
options.top_k = settings.top_k ?? ''; params.top_k = $settings.top_k ?? '';
options.top_p = settings.top_p ?? ''; params.top_p = $settings.top_p ?? '';
options.num_ctx = settings.num_ctx ?? ''; params.num_ctx = $settings.num_ctx ?? '';
options = { ...options, ...settings.options }; params = { ...params, ...$settings.params };
options.stop = (settings?.options?.stop ?? []).join(','); params.stop = $settings?.params?.stop ? ($settings?.params?.stop ?? []).join(',') : null;
}); });
const applyTheme = (_theme: string) => { const applyTheme = (_theme: string) => {
...@@ -228,7 +227,7 @@ ...@@ -228,7 +227,7 @@
</div> </div>
{#if showAdvanced} {#if showAdvanced}
<AdvancedParams bind:options /> <AdvancedParams bind:params />
<hr class=" dark:border-gray-700" /> <hr class=" dark:border-gray-700" />
<div class=" py-1 w-full justify-between"> <div class=" py-1 w-full justify-between">
...@@ -300,20 +299,21 @@ ...@@ -300,20 +299,21 @@
on:click={() => { on:click={() => {
saveSettings({ saveSettings({
system: system !== '' ? system : undefined, system: system !== '' ? system : undefined,
options: { params: {
seed: (options.seed !== 0 ? options.seed : undefined) ?? undefined, seed: (params.seed !== 0 ? params.seed : undefined) ?? undefined,
stop: options.stop !== '' ? options.stop.split(',').filter((e) => e) : undefined, stop: params.stop ? params.stop.split(',').filter((e) => e) : undefined,
temperature: options.temperature !== '' ? options.temperature : undefined, temperature: params.temperature !== '' ? params.temperature : undefined,
repeat_penalty: options.repeat_penalty !== '' ? options.repeat_penalty : undefined, frequency_penalty:
repeat_last_n: options.repeat_last_n !== '' ? options.repeat_last_n : undefined, params.frequency_penalty !== '' ? params.frequency_penalty : undefined,
mirostat: options.mirostat !== '' ? options.mirostat : undefined, repeat_last_n: params.repeat_last_n !== '' ? params.repeat_last_n : undefined,
mirostat_eta: options.mirostat_eta !== '' ? options.mirostat_eta : undefined, mirostat: params.mirostat !== '' ? params.mirostat : undefined,
mirostat_tau: options.mirostat_tau !== '' ? options.mirostat_tau : undefined, mirostat_eta: params.mirostat_eta !== '' ? params.mirostat_eta : undefined,
top_k: options.top_k !== '' ? options.top_k : undefined, mirostat_tau: params.mirostat_tau !== '' ? params.mirostat_tau : undefined,
top_p: options.top_p !== '' ? options.top_p : undefined, top_k: params.top_k !== '' ? params.top_k : undefined,
tfs_z: options.tfs_z !== '' ? options.tfs_z : undefined, top_p: params.top_p !== '' ? params.top_p : undefined,
num_ctx: options.num_ctx !== '' ? options.num_ctx : undefined, tfs_z: params.tfs_z !== '' ? params.tfs_z : undefined,
num_predict: options.num_predict !== '' ? options.num_predict : undefined num_ctx: params.num_ctx !== '' ? params.num_ctx : undefined,
max_tokens: params.max_tokens !== '' ? params.max_tokens : undefined
}, },
keepAlive: keepAlive ? (isNaN(keepAlive) ? keepAlive : parseInt(keepAlive)) : undefined keepAlive: keepAlive ? (isNaN(keepAlive) ? keepAlive : parseInt(keepAlive)) : undefined
}); });
......
...@@ -104,23 +104,18 @@ ...@@ -104,23 +104,18 @@
promptSuggestions = $config?.default_prompt_suggestions; promptSuggestions = $config?.default_prompt_suggestions;
} }
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); titleAutoGenerate = $settings?.title?.auto ?? true;
titleAutoGenerateModel = $settings?.title?.model ?? '';
titleAutoGenerate = settings?.title?.auto ?? true; titleAutoGenerateModelExternal = $settings?.title?.modelExternal ?? '';
titleAutoGenerateModel = settings?.title?.model ?? '';
titleAutoGenerateModelExternal = settings?.title?.modelExternal ?? '';
titleGenerationPrompt = titleGenerationPrompt =
settings?.title?.prompt ?? $settings?.title?.prompt ??
$i18n.t( `Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title': {{prompt}}`;
"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':" responseAutoCopy = $settings.responseAutoCopy ?? false;
) + ' {{prompt}}'; showUsername = $settings.showUsername ?? false;
chatBubble = $settings.chatBubble ?? true;
responseAutoCopy = settings.responseAutoCopy ?? false; fullScreenMode = $settings.fullScreenMode ?? false;
showUsername = settings.showUsername ?? false; splitLargeChunks = $settings.splitLargeChunks ?? false;
chatBubble = settings.chatBubble ?? true; chatDirection = $settings.chatDirection ?? 'LTR';
fullScreenMode = settings.fullScreenMode ?? false;
splitLargeChunks = settings.splitLargeChunks ?? false;
chatDirection = settings.chatDirection ?? 'LTR';
}); });
</script> </script>
......
<script lang="ts"> <script lang="ts">
import queue from 'async/queue';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { import {
...@@ -12,32 +11,20 @@ ...@@ -12,32 +11,20 @@
cancelOllamaRequest, cancelOllamaRequest,
uploadModel uploadModel
} from '$lib/apis/ollama'; } from '$lib/apis/ollama';
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants'; import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
import { WEBUI_NAME, models, MODEL_DOWNLOAD_POOL, user } from '$lib/stores'; import { WEBUI_NAME, models, MODEL_DOWNLOAD_POOL, user, config } from '$lib/stores';
import { splitStream } from '$lib/utils'; import { splitStream } from '$lib/utils';
import { onMount, getContext } from 'svelte'; import { onMount, getContext } from 'svelte';
import { addLiteLLMModel, deleteLiteLLMModel, getLiteLLMModelInfo } from '$lib/apis/litellm';
import Tooltip from '$lib/components/common/Tooltip.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
export let getModels: Function; export let getModels: Function;
let showLiteLLM = false;
let showLiteLLMParams = false;
let modelUploadInputElement: HTMLInputElement; let modelUploadInputElement: HTMLInputElement;
let liteLLMModelInfo = [];
let liteLLMModel = '';
let liteLLMModelName = '';
let liteLLMAPIBase = '';
let liteLLMAPIKey = '';
let liteLLMRPM = '';
let liteLLMMaxTokens = '';
let deleteLiteLLMModelName = '';
$: liteLLMModelName = liteLLMModel;
// Models // Models
...@@ -48,7 +35,8 @@ ...@@ -48,7 +35,8 @@
let updateProgress = null; let updateProgress = null;
let showExperimentalOllama = false; let showExperimentalOllama = false;
let ollamaVersion = '';
let ollamaVersion = null;
const MAX_PARALLEL_DOWNLOADS = 3; const MAX_PARALLEL_DOWNLOADS = 3;
let modelTransferring = false; let modelTransferring = false;
...@@ -70,8 +58,11 @@ ...@@ -70,8 +58,11 @@
const updateModelsHandler = async () => { const updateModelsHandler = async () => {
for (const model of $models.filter( for (const model of $models.filter(
(m) => (m) =>
m.size != null && !(m?.preset ?? false) &&
(selectedOllamaUrlIdx === null ? true : (m?.urls ?? []).includes(selectedOllamaUrlIdx)) m.owned_by === 'ollama' &&
(selectedOllamaUrlIdx === null
? true
: (m?.ollama?.urls ?? []).includes(selectedOllamaUrlIdx))
)) { )) {
console.log(model); console.log(model);
...@@ -439,77 +430,28 @@ ...@@ -439,77 +430,28 @@
} }
}; };
const addLiteLLMModelHandler = async () => {
if (!liteLLMModelInfo.find((info) => info.model_name === liteLLMModelName)) {
const res = await addLiteLLMModel(localStorage.token, {
name: liteLLMModelName,
model: liteLLMModel,
api_base: liteLLMAPIBase,
api_key: liteLLMAPIKey,
rpm: liteLLMRPM,
max_tokens: liteLLMMaxTokens
}).catch((error) => {
toast.error(error);
return null;
});
if (res) {
if (res.message) {
toast.success(res.message);
}
}
} else {
toast.error($i18n.t(`Model {{modelName}} already exists.`, { modelName: liteLLMModelName }));
}
liteLLMModelName = '';
liteLLMModel = '';
liteLLMAPIBase = '';
liteLLMAPIKey = '';
liteLLMRPM = '';
liteLLMMaxTokens = '';
liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
models.set(await getModels());
};
const deleteLiteLLMModelHandler = async () => {
const res = await deleteLiteLLMModel(localStorage.token, deleteLiteLLMModelName).catch(
(error) => {
toast.error(error);
return null;
}
);
if (res) {
if (res.message) {
toast.success(res.message);
}
}
deleteLiteLLMModelName = '';
liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
models.set(await getModels());
};
onMount(async () => { onMount(async () => {
OLLAMA_URLS = await getOllamaUrls(localStorage.token).catch((error) => { await Promise.all([
toast.error(error); (async () => {
return []; OLLAMA_URLS = await getOllamaUrls(localStorage.token).catch((error) => {
}); toast.error(error);
return [];
if (OLLAMA_URLS.length > 0) { });
selectedOllamaUrlIdx = 0;
}
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false); if (OLLAMA_URLS.length > 0) {
liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token); selectedOllamaUrlIdx = 0;
}
})(),
(async () => {
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
})()
]);
}); });
</script> </script>
<div class="flex flex-col h-full justify-between text-sm"> <div class="flex flex-col h-full justify-between text-sm">
<div class=" space-y-3 pr-1.5 overflow-y-scroll h-[24rem]"> <div class=" space-y-3 pr-1.5 overflow-y-scroll h-[24rem]">
{#if ollamaVersion} {#if ollamaVersion !== null}
<div class="space-y-2 pr-1.5"> <div class="space-y-2 pr-1.5">
<div class="text-sm font-medium">{$i18n.t('Manage Ollama Models')}</div> <div class="text-sm font-medium">{$i18n.t('Manage Ollama Models')}</div>
...@@ -587,24 +529,28 @@ ...@@ -587,24 +529,28 @@
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="currentColor" fill="currentColor"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
><style> >
<style>
.spinner_ajPY { .spinner_ajPY {
transform-origin: center; transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear; animation: spinner_AtaB 0.75s infinite linear;
} }
@keyframes spinner_AtaB { @keyframes spinner_AtaB {
100% { 100% {
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
</style><path </style>
<path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25" opacity=".25"
/><path />
<path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY" class="spinner_ajPY"
/></svg />
> </svg>
</div> </div>
{:else} {:else}
<svg <svg
...@@ -703,9 +649,12 @@ ...@@ -703,9 +649,12 @@
{#if !deleteModelTag} {#if !deleteModelTag}
<option value="" disabled selected>{$i18n.t('Select a model')}</option> <option value="" disabled selected>{$i18n.t('Select a model')}</option>
{/if} {/if}
{#each $models.filter((m) => m.size != null && (selectedOllamaUrlIdx === null ? true : (m?.urls ?? []).includes(selectedOllamaUrlIdx))) as model} {#each $models.filter((m) => !(m?.preset ?? false) && m.owned_by === 'ollama' && (selectedOllamaUrlIdx === null ? true : (m?.ollama?.urls ?? []).includes(selectedOllamaUrlIdx))) as model}
<option value={model.name} class="bg-gray-100 dark:bg-gray-700" <option value={model.name} class="bg-gray-100 dark:bg-gray-700"
>{model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'}</option >{model.name +
' (' +
(model.ollama.size / 1024 ** 3).toFixed(1) +
' GB)'}</option
> >
{/each} {/each}
</select> </select>
...@@ -833,24 +782,28 @@ ...@@ -833,24 +782,28 @@
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="currentColor" fill="currentColor"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
><style> >
<style>
.spinner_ajPY { .spinner_ajPY {
transform-origin: center; transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear; animation: spinner_AtaB 0.75s infinite linear;
} }
@keyframes spinner_AtaB { @keyframes spinner_AtaB {
100% { 100% {
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
</style><path </style>
<path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25" opacity=".25"
/><path />
<path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY" class="spinner_ajPY"
/></svg />
> </svg>
</div> </div>
{:else} {:else}
<svg <svg
...@@ -929,203 +882,14 @@ ...@@ -929,203 +882,14 @@
{/if} {/if}
</div> </div>
</div> </div>
<hr class=" dark:border-gray-700 my-2" /> {:else if ollamaVersion === false}
{/if} <div>Ollama Not Detected</div>
{:else}
<div class=" space-y-3"> <div class="flex h-full justify-center">
<div class="mt-2 space-y-3 pr-1.5"> <div class="my-auto">
<div> <Spinner className="size-6" />
<div class="mb-2">
<div class="flex justify-between items-center text-xs">
<div class=" text-sm font-medium">{$i18n.t('Manage LiteLLM Models')}</div>
<button
class=" text-xs font-medium text-gray-500"
type="button"
on:click={() => {
showLiteLLM = !showLiteLLM;
}}>{showLiteLLM ? $i18n.t('Hide') : $i18n.t('Show')}</button
>
</div>
</div>
{#if showLiteLLM}
<div>
<div class="flex justify-between items-center text-xs">
<div class=" text-sm font-medium">{$i18n.t('Add a model')}</div>
<button
class=" text-xs font-medium text-gray-500"
type="button"
on:click={() => {
showLiteLLMParams = !showLiteLLMParams;
}}
>{showLiteLLMParams
? $i18n.t('Hide Additional Params')
: $i18n.t('Show Additional Params')}</button
>
</div>
</div>
<div class="my-2 space-y-2">
<div class="flex w-full mb-1.5">
<div class="flex-1 mr-2">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Enter LiteLLM Model (litellm_params.model)')}
bind:value={liteLLMModel}
autocomplete="off"
/>
</div>
<button
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
on:click={() => {
addLiteLLMModelHandler();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
/>
</svg>
</button>
</div>
{#if showLiteLLMParams}
<div>
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('Model Name')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder="Enter Model Name (model_name)"
bind:value={liteLLMModelName}
autocomplete="off"
/>
</div>
</div>
</div>
<div>
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('API Base URL')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t(
'Enter LiteLLM API Base URL (litellm_params.api_base)'
)}
bind:value={liteLLMAPIBase}
autocomplete="off"
/>
</div>
</div>
</div>
<div>
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('API Key')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Enter LiteLLM API Key (litellm_params.api_key)')}
bind:value={liteLLMAPIKey}
autocomplete="off"
/>
</div>
</div>
</div>
<div>
<div class="mb-1.5 text-sm font-medium">{$i18n.t('API RPM')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Enter LiteLLM API RPM (litellm_params.rpm)')}
bind:value={liteLLMRPM}
autocomplete="off"
/>
</div>
</div>
</div>
<div>
<div class="mb-1.5 text-sm font-medium">{$i18n.t('Max Tokens')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Enter Max Tokens (litellm_params.max_tokens)')}
bind:value={liteLLMMaxTokens}
type="number"
min="1"
autocomplete="off"
/>
</div>
</div>
</div>
{/if}
</div>
<div class="mb-2 text-xs text-gray-400 dark:text-gray-500">
{$i18n.t('Not sure what to add?')}
<a
class=" text-gray-300 font-medium underline"
href="https://litellm.vercel.app/docs/proxy/configs#quick-start"
target="_blank"
>
{$i18n.t('Click here for help.')}
</a>
</div>
<div>
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Delete a model')}</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
<select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={deleteLiteLLMModelName}
placeholder={$i18n.t('Select a model')}
>
{#if !deleteLiteLLMModelName}
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
{/if}
{#each liteLLMModelInfo as model}
<option value={model.model_name} class="bg-gray-100 dark:bg-gray-700"
>{model.model_name}</option
>
{/each}
</select>
</div>
<button
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
on:click={() => {
deleteLiteLLMModelHandler();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M5 3.25V4H2.75a.75.75 0 0 0 0 1.5h.3l.815 8.15A1.5 1.5 0 0 0 5.357 15h5.285a1.5 1.5 0 0 0 1.493-1.35l.815-8.15h.3a.75.75 0 0 0 0-1.5H11v-.75A2.25 2.25 0 0 0 8.75 1h-1.5A2.25 2.25 0 0 0 5 3.25Zm2.25-.75a.75.75 0 0 0-.75.75V4h3v-.75a.75.75 0 0 0-.75-.75h-1.5ZM6.05 6a.75.75 0 0 1 .787.713l.275 5.5a.75.75 0 0 1-1.498.075l-.275-5.5A.75.75 0 0 1 6.05 6Zm3.9 0a.75.75 0 0 1 .712.787l-.275 5.5a.75.75 0 0 1-1.498-.075l.275-5.5a.75.75 0 0 1 .786-.711Z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>
</div>
{/if}
</div> </div>
</div> </div>
</div> {/if}
</div> </div>
</div> </div>
...@@ -19,8 +19,7 @@ ...@@ -19,8 +19,7 @@
let enableMemory = false; let enableMemory = false;
onMount(async () => { onMount(async () => {
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); enableMemory = $settings?.memory ?? false;
enableMemory = settings?.memory ?? false;
}); });
</script> </script>
......
...@@ -54,7 +54,7 @@ ...@@ -54,7 +54,7 @@
class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6 h-[28rem] max-h-screen outline outline-1 rounded-xl outline-gray-100 dark:outline-gray-800 mb-4 mt-1" class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6 h-[28rem] max-h-screen outline outline-1 rounded-xl outline-gray-100 dark:outline-gray-800 mb-4 mt-1"
> >
{#if memories.length > 0} {#if memories.length > 0}
<div class="text-left text-sm w-full mb-4 max-h-[22rem] overflow-y-scroll"> <div class="text-left text-sm w-full mb-4 overflow-y-scroll">
<div class="relative overflow-x-auto"> <div class="relative overflow-x-auto">
<table class="w-full text-sm text-left text-gray-600 dark:text-gray-400 table-auto"> <table class="w-full text-sm text-left text-gray-600 dark:text-gray-400 table-auto">
<thead <thead
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { models, settings, user } from '$lib/stores'; import { models, settings, user } from '$lib/stores';
import { getModels as _getModels } from '$lib/utils'; import { getModels as _getModels } from '$lib/apis';
import Modal from '../common/Modal.svelte'; import Modal from '../common/Modal.svelte';
import Account from './Settings/Account.svelte'; import Account from './Settings/Account.svelte';
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
import Images from './Settings/Images.svelte'; import Images from './Settings/Images.svelte';
import User from '../icons/User.svelte'; import User from '../icons/User.svelte';
import Personalization from './Settings/Personalization.svelte'; import Personalization from './Settings/Personalization.svelte';
import { updateUserSettings } from '$lib/apis/users';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
...@@ -26,7 +27,7 @@ ...@@ -26,7 +27,7 @@
console.log(updated); console.log(updated);
await settings.set({ ...$settings, ...updated }); await settings.set({ ...$settings, ...updated });
await models.set(await getModels()); await models.set(await getModels());
localStorage.setItem('settings', JSON.stringify($settings)); await updateUserSettings(localStorage.token, { ui: $settings });
}; };
const getModels = async () => { const getModels = async () => {
......
<script lang="ts"> <script lang="ts">
import { getContext, onMount } from 'svelte'; import { getContext, onMount } from 'svelte';
import { models, config } from '$lib/stores';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { deleteSharedChatById, getChatById, shareChatById } from '$lib/apis/chats'; import { deleteSharedChatById, getChatById, shareChatById } from '$lib/apis/chats';
import { modelfiles } from '$lib/stores';
import { copyToClipboard } from '$lib/utils'; import { copyToClipboard } from '$lib/utils';
import Modal from '../common/Modal.svelte'; import Modal from '../common/Modal.svelte';
...@@ -43,9 +43,7 @@ ...@@ -43,9 +43,7 @@
tab.postMessage( tab.postMessage(
JSON.stringify({ JSON.stringify({
chat: _chat, chat: _chat,
modelfiles: $modelfiles.filter((modelfile) => models: $models.filter((m) => _chat.models.includes(m.id))
_chat.models.includes(modelfile.tagName)
)
}), }),
'*' '*'
); );
...@@ -136,16 +134,18 @@ ...@@ -136,16 +134,18 @@
<div class="flex justify-end"> <div class="flex justify-end">
<div class="flex flex-col items-end space-x-1 mt-1.5"> <div class="flex flex-col items-end space-x-1 mt-1.5">
<div class="flex gap-1"> <div class="flex gap-1">
<button {#if $config?.features.enable_community_sharing}
class=" self-center px-3.5 py-2 rounded-xl text-sm font-medium bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white" <button
type="button" class=" self-center px-3.5 py-2 rounded-xl text-sm font-medium bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white"
on:click={() => { type="button"
shareChat(); on:click={() => {
show = false; shareChat();
}} show = false;
> }}
{$i18n.t('Share to OpenWebUI Community')} >
</button> {$i18n.t('Share to OpenWebUI Community')}
</button>
{/if}
<button <button
class=" self-center flex items-center gap-1 px-3.5 py-2 rounded-xl text-sm font-medium bg-emerald-600 hover:bg-emerald-500 text-white" class=" self-center flex items-center gap-1 px-3.5 py-2 rounded-xl text-sm font-medium bg-emerald-600 hover:bg-emerald-500 text-white"
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment