Unverified Commit 67a5020c authored by Timothy Jaeryang Baek's avatar Timothy Jaeryang Baek Committed by GitHub
Browse files

Merge pull request #2505 from open-webui/dev-models

feat: openai api compatible model presets (profiles/modelfiles)
parents d0d76e2a a6af20e1
<script lang="ts"> <script lang="ts">
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { onMount, tick, getContext } from 'svelte'; import { onMount, tick, getContext } from 'svelte';
import { mobile, modelfiles, settings, showSidebar } from '$lib/stores'; import { type Model, mobile, settings, showSidebar, models } from '$lib/stores';
import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils'; import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
import { import {
...@@ -27,7 +27,9 @@ ...@@ -27,7 +27,9 @@
export let stopResponse: Function; export let stopResponse: Function;
export let autoScroll = true; export let autoScroll = true;
export let selectedModel = '';
export let atSelectedModel: Model | undefined;
export let selectedModels: [''];
let chatTextAreaElement: HTMLTextAreaElement; let chatTextAreaElement: HTMLTextAreaElement;
let filesInputElement; let filesInputElement;
...@@ -52,6 +54,11 @@ ...@@ -52,6 +54,11 @@
let speechRecognition; let speechRecognition;
let visionCapableModels = [];
$: visionCapableModels = [...(atSelectedModel ? [atSelectedModel] : selectedModels)].filter(
(model) => $models.find((m) => m.id === model)?.info?.meta?.capabilities?.vision ?? true
);
$: if (prompt) { $: if (prompt) {
if (chatTextAreaElement) { if (chatTextAreaElement) {
chatTextAreaElement.style.height = ''; chatTextAreaElement.style.height = '';
...@@ -358,6 +365,10 @@ ...@@ -358,6 +365,10 @@
inputFiles.forEach((file) => { inputFiles.forEach((file) => {
console.log(file, file.name.split('.').at(-1)); console.log(file, file.name.split('.').at(-1));
if (['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(file['type'])) { if (['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(file['type'])) {
if (visionCapableModels.length === 0) {
toast.error($i18n.t('Selected model(s) do not support image inputs'));
return;
}
let reader = new FileReader(); let reader = new FileReader();
reader.onload = (event) => { reader.onload = (event) => {
files = [ files = [
...@@ -429,8 +440,8 @@ ...@@ -429,8 +440,8 @@
<div class="fixed bottom-0 {$showSidebar ? 'left-0 md:left-[260px]' : 'left-0'} right-0"> <div class="fixed bottom-0 {$showSidebar ? 'left-0 md:left-[260px]' : 'left-0'} right-0">
<div class="w-full"> <div class="w-full">
<div class="px-2.5 md:px-16 -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center"> <div class=" -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center">
<div class="flex flex-col max-w-5xl w-full"> <div class="flex flex-col max-w-6xl px-2.5 md:px-6 w-full">
<div class="relative"> <div class="relative">
{#if autoScroll === false && messages.length > 0} {#if autoScroll === false && messages.length > 0}
<div class=" absolute -top-12 left-0 right-0 flex justify-center z-30"> <div class=" absolute -top-12 left-0 right-0 flex justify-center z-30">
...@@ -494,12 +505,12 @@ ...@@ -494,12 +505,12 @@
bind:chatInputPlaceholder bind:chatInputPlaceholder
{messages} {messages}
on:select={(e) => { on:select={(e) => {
selectedModel = e.detail; atSelectedModel = e.detail;
chatTextAreaElement?.focus(); chatTextAreaElement?.focus();
}} }}
/> />
{#if selectedModel !== ''} {#if atSelectedModel !== undefined}
<div <div
class="px-3 py-2.5 text-left w-full flex justify-between items-center absolute bottom-0 left-0 right-0 bg-gradient-to-t from-50% from-white dark:from-gray-900" class="px-3 py-2.5 text-left w-full flex justify-between items-center absolute bottom-0 left-0 right-0 bg-gradient-to-t from-50% from-white dark:from-gray-900"
> >
...@@ -508,21 +519,21 @@ ...@@ -508,21 +519,21 @@
crossorigin="anonymous" crossorigin="anonymous"
alt="model profile" alt="model profile"
class="size-5 max-w-[28px] object-cover rounded-full" class="size-5 max-w-[28px] object-cover rounded-full"
src={$modelfiles.find((modelfile) => modelfile.tagName === selectedModel.id) src={$models.find((model) => model.id === atSelectedModel.id)?.info?.meta
?.imageUrl ?? ?.profile_image_url ??
($i18n.language === 'dg-DG' ($i18n.language === 'dg-DG'
? `/doge.png` ? `/doge.png`
: `${WEBUI_BASE_URL}/static/favicon.png`)} : `${WEBUI_BASE_URL}/static/favicon.png`)}
/> />
<div> <div>
Talking to <span class=" font-medium">{selectedModel.name} </span> Talking to <span class=" font-medium">{atSelectedModel.name}</span>
</div> </div>
</div> </div>
<div> <div>
<button <button
class="flex items-center" class="flex items-center"
on:click={() => { on:click={() => {
selectedModel = ''; atSelectedModel = undefined;
}} }}
> >
<XMark /> <XMark />
...@@ -535,7 +546,7 @@ ...@@ -535,7 +546,7 @@
</div> </div>
<div class="bg-white dark:bg-gray-900"> <div class="bg-white dark:bg-gray-900">
<div class="max-w-6xl px-2.5 md:px-16 mx-auto inset-x-0"> <div class="max-w-6xl px-2.5 md:px-6 mx-auto inset-x-0">
<div class=" pb-2"> <div class=" pb-2">
<input <input
bind:this={filesInputElement} bind:this={filesInputElement}
...@@ -550,6 +561,12 @@ ...@@ -550,6 +561,12 @@
if ( if (
['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(file['type']) ['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(file['type'])
) { ) {
if (visionCapableModels.length === 0) {
toast.error($i18n.t('Selected model(s) do not support image inputs'));
inputFiles = null;
filesInputElement.value = '';
return;
}
let reader = new FileReader(); let reader = new FileReader();
reader.onload = (event) => { reader.onload = (event) => {
files = [ files = [
...@@ -589,6 +606,7 @@ ...@@ -589,6 +606,7 @@
dir={$settings?.chatDirection ?? 'LTR'} dir={$settings?.chatDirection ?? 'LTR'}
class=" flex flex-col relative w-full rounded-3xl px-1.5 bg-gray-50 dark:bg-gray-850 dark:text-gray-100" class=" flex flex-col relative w-full rounded-3xl px-1.5 bg-gray-50 dark:bg-gray-850 dark:text-gray-100"
on:submit|preventDefault={() => { on:submit|preventDefault={() => {
// check if selectedModels support image input
submitPrompt(prompt, user); submitPrompt(prompt, user);
}} }}
> >
...@@ -597,7 +615,36 @@ ...@@ -597,7 +615,36 @@
{#each files as file, fileIdx} {#each files as file, fileIdx}
<div class=" relative group"> <div class=" relative group">
{#if file.type === 'image'} {#if file.type === 'image'}
<img src={file.url} alt="input" class=" h-16 w-16 rounded-xl object-cover" /> <div class="relative">
<img
src={file.url}
alt="input"
class=" h-16 w-16 rounded-xl object-cover"
/>
{#if atSelectedModel ? visionCapableModels.length === 0 : selectedModels.length !== visionCapableModels.length}
<Tooltip
className=" absolute top-1 left-1"
content={$i18n.t('{{ models }}', {
models: [...(atSelectedModel ? [atSelectedModel] : selectedModels)]
.filter((id) => !visionCapableModels.includes(id))
.join(', ')
})}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-4 fill-yellow-300"
>
<path
fill-rule="evenodd"
d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003ZM12 8.25a.75.75 0 0 1 .75.75v3.75a.75.75 0 0 1-1.5 0V9a.75.75 0 0 1 .75-.75Zm0 8.25a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z"
clip-rule="evenodd"
/>
</svg>
</Tooltip>
{/if}
</div>
{:else if file.type === 'doc'} {:else if file.type === 'doc'}
<div <div
class="h-16 w-[15rem] flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none" class="h-16 w-[15rem] flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none"
...@@ -883,7 +930,7 @@ ...@@ -883,7 +930,7 @@
if (e.key === 'Escape') { if (e.key === 'Escape') {
console.log('Escape'); console.log('Escape');
selectedModel = ''; atSelectedModel = undefined;
} }
}} }}
rows="1" rows="1"
......
<script lang="ts"> <script lang="ts">
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { chats, config, modelfiles, settings, user as _user, mobile } from '$lib/stores'; import { chats, config, settings, user as _user, mobile } from '$lib/stores';
import { tick, getContext } from 'svelte'; import { tick, getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
...@@ -26,7 +26,6 @@ ...@@ -26,7 +26,6 @@
export let user = $_user; export let user = $_user;
export let prompt; export let prompt;
export let suggestionPrompts = [];
export let processing = ''; export let processing = '';
export let bottomPadding = false; export let bottomPadding = false;
export let autoScroll; export let autoScroll;
...@@ -34,7 +33,6 @@ ...@@ -34,7 +33,6 @@
export let messages = []; export let messages = [];
export let selectedModels; export let selectedModels;
export let selectedModelfiles = [];
$: if (autoScroll && bottomPadding) { $: if (autoScroll && bottomPadding) {
(async () => { (async () => {
...@@ -247,9 +245,7 @@ ...@@ -247,9 +245,7 @@
<div class="h-full flex mb-16"> <div class="h-full flex mb-16">
{#if messages.length == 0} {#if messages.length == 0}
<Placeholder <Placeholder
models={selectedModels} modelIds={selectedModels}
modelfiles={selectedModelfiles}
{suggestionPrompts}
submitPrompt={async (p) => { submitPrompt={async (p) => {
let text = p; let text = p;
...@@ -316,7 +312,6 @@ ...@@ -316,7 +312,6 @@
{#key message.id} {#key message.id}
<ResponseMessage <ResponseMessage
{message} {message}
modelfiles={selectedModelfiles}
siblings={history.messages[message.parentId]?.childrenIds ?? []} siblings={history.messages[message.parentId]?.childrenIds ?? []}
isLastMessage={messageIdx + 1 === messages.length} isLastMessage={messageIdx + 1 === messages.length}
{readOnly} {readOnly}
...@@ -348,7 +343,6 @@ ...@@ -348,7 +343,6 @@
{chatId} {chatId}
parentMessage={history.messages[message.parentId]} parentMessage={history.messages[message.parentId]}
{messageIdx} {messageIdx}
{selectedModelfiles}
{updateChatMessages} {updateChatMessages}
{confirmEditResponseMessage} {confirmEditResponseMessage}
{rateMessage} {rateMessage}
......
...@@ -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 === 'python' || (lang === '' && checkPythonCode(code))}
{#if lang === 'python' || (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;
...@@ -338,17 +340,13 @@ ...@@ -338,17 +340,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
...@@ -442,8 +440,8 @@ ...@@ -442,8 +440,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, {
......
...@@ -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}
......
{
"description": "Developer lead assistant with no code explanation",
"profile_image_url": "data:image/jpeg;base64,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",
"ollama": {
"modelfile": "FROM llama3\nPARAMETER temperature 1\nSYSTEM \"\"\"\nI want you to act as a senior full-stack tech leader and top-tier brilliant software developer, you embody technical excellence and a deep understanding of a wide range of technologies. Your expertise covers not just coding, but also algorithm design, system architecture, and technology strategy. for every question there is no need to explain, only give the solution.\n\nCoding Mastery: Possess exceptional skills in programming languages including Python, JavaScript, SQL, NoSQL, mySQL, C++, C, Rust, Groovy, Go, and Java. Your proficiency goes beyond mere syntax; you explore and master the nuances and complexities of each language, crafting code that is both highly efficient and robust. Your capability to optimize performance and manage complex codebases sets the benchmark in software development.\n\nPython | JavaScript | C++ | C | RUST | Groovy | Go | Java | SQL | MySQL | NoSQL\nEfficient, Optimal, Good Performance, Excellent Complexity, Robust Code\n\nCutting-Edge Technologies: Adept at leveraging the latest technologies, frameworks, and tools to drive innovation and efficiency. Experienced with Docker, Kubernetes, React, Angular, AWS, Supabase, Firebase, Azure, and Google Cloud. Your understanding of these platforms enables you to architect and deploy scalable, resilient applications that meet modern business demands.\n\nDocker | Kubernetes | React | Angular | AWS | Supabase | Firebase | Azure | Google Cloud\nSeamlessly Integrating Modern Tech Stacks\n\nComplex Algorithms & Data Structures\nOptimized Solutions for Enhanced Performance & Scalability\n\nSolution Architect: Your comprehensive grasp of the software development lifecycle empowers you to design solutions that are not only technically sound but also align perfectly with business goals. From concept to deployment, you ensure adherence to industry best practices and agile methodologies, making the development process both agile and effective.\n\nInteractive Solutions: When crafting user-facing features, employ modern ES6 JavaScript, TypeScript, and native browser APIs to manage interactivity seamlessly, enabling a dynamic and engaging user experience. Your focus lies in delivering functional, ready-to-deploy code, ensuring that explanations are succinct and directly aligned with the required solutions.\n\nnever explain the code just write code \n\"\"\""
},
"suggestion_prompts": [
{
"content": "Create a pac-man game in C"
},
{
"content": "Create react page example"
},
{
"content": "write character collisions in godot engine"
}
],
"categories": [
"assistant",
"programming",
"data analysis"
],
"user": {
"username": "vianch",
"name": "",
"community": true
}
}
\ No newline at end of file
...@@ -45,13 +45,11 @@ ...@@ -45,13 +45,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">
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>
...@@ -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 () => {
...@@ -80,14 +80,14 @@ ...@@ -80,14 +80,14 @@
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 +228,7 @@ ...@@ -228,7 +228,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 +300,21 @@ ...@@ -300,20 +300,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 !== null ? 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
}); });
......
<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,19 @@ ...@@ -12,32 +11,19 @@
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';
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
...@@ -439,71 +425,22 @@ ...@@ -439,71 +425,22 @@
} }
}; };
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>
...@@ -587,24 +524,28 @@ ...@@ -587,24 +524,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
...@@ -833,24 +774,28 @@ ...@@ -833,24 +774,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 +874,8 @@ ...@@ -929,203 +874,8 @@
{/if} {/if}
</div> </div>
</div> </div>
<hr class=" dark:border-gray-700 my-2" /> {:else}
<div>Ollama Not Detected</div>
{/if} {/if}
<div class=" space-y-3">
<div class="mt-2 space-y-3 pr-1.5">
<div>
<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>
</div> </div>
...@@ -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';
......
<script lang="ts"> <script lang="ts">
import { getContext, onMount } from 'svelte'; import { getContext, onMount } from 'svelte';
import { models } 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)
)
}), }),
'*' '*'
); );
......
...@@ -29,6 +29,7 @@ ...@@ -29,6 +29,7 @@
dispatch('change', _state); dispatch('change', _state);
} }
}} }}
type="button"
> >
<div class="top-0 left-0 absolute w-full flex justify-center"> <div class="top-0 left-0 absolute w-full flex justify-center">
{#if _state === 'checked'} {#if _state === 'checked'}
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
export let placement = 'top'; export let placement = 'top';
export let content = `I'm a tooltip!`; export let content = `I'm a tooltip!`;
export let touch = true; export let touch = true;
export let className = 'flex';
let tooltipElement; let tooltipElement;
let tooltipInstance; let tooltipInstance;
...@@ -29,6 +30,6 @@ ...@@ -29,6 +30,6 @@
}); });
</script> </script>
<div bind:this={tooltipElement} aria-label={content} class="flex"> <div bind:this={tooltipElement} aria-label={content} class={className}>
<slot /> <slot />
</div> </div>
...@@ -6,7 +6,6 @@ ...@@ -6,7 +6,6 @@
WEBUI_NAME, WEBUI_NAME,
chatId, chatId,
mobile, mobile,
modelfiles,
settings, settings,
showArchivedChats, showArchivedChats,
showSettings, showSettings,
......
...@@ -5,67 +5,82 @@ ...@@ -5,67 +5,82 @@
import { onMount, getContext } from 'svelte'; import { onMount, getContext } from 'svelte';
import { WEBUI_NAME, modelfiles, settings, user } from '$lib/stores'; import { WEBUI_NAME, modelfiles, models, settings, user } from '$lib/stores';
import { createModel, deleteModel } from '$lib/apis/ollama'; import { addNewModel, deleteModelById, getModelInfos } from '$lib/apis/models';
import {
createNewModelfile, import { deleteModel } from '$lib/apis/ollama';
deleteModelfileByTagName,
getModelfiles
} from '$lib/apis/modelfiles';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { getModels } from '$lib/apis';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
let localModelfiles = []; let localModelfiles = [];
let importFiles;
let modelfilesImportInputElement: HTMLInputElement;
const deleteModelHandler = async (tagName) => {
let success = null;
success = await deleteModel(localStorage.token, tagName).catch((err) => { let importFiles;
toast.error(err); let modelsImportInputElement: HTMLInputElement;
const deleteModelHandler = async (model) => {
console.log(model.info);
if (!model?.info) {
toast.error(
$i18n.t('{{ owner }}: You cannot delete a base model', {
owner: model.owned_by.toUpperCase()
})
);
return null; return null;
}); }
if (success) { const res = await deleteModelById(localStorage.token, model.id);
toast.success($i18n.t(`Deleted {{tagName}}`, { tagName }));
if (res) {
toast.success($i18n.t(`Deleted {{name}}`, { name: model.id }));
} }
return success; await models.set(await getModels(localStorage.token));
}; };
const deleteModelfile = async (tagName) => { const cloneModelHandler = async (model) => {
await deleteModelHandler(tagName); if ((model?.info?.base_model_id ?? null) === null) {
await deleteModelfileByTagName(localStorage.token, tagName); toast.error($i18n.t('You cannot clone a base model'));
await modelfiles.set(await getModelfiles(localStorage.token)); return;
} else {
sessionStorage.model = JSON.stringify({
...model,
id: `${model.id}-clone`,
name: `${model.name} (Clone)`
});
goto('/workspace/models/create');
}
}; };
const shareModelfile = async (modelfile) => { const shareModelHandler = async (model) => {
toast.success($i18n.t('Redirecting you to OpenWebUI Community')); toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
const url = 'https://openwebui.com'; const url = 'https://openwebui.com';
const tab = await window.open(`${url}/modelfiles/create`, '_blank'); const tab = await window.open(`${url}/models/create`, '_blank');
window.addEventListener( window.addEventListener(
'message', 'message',
(event) => { (event) => {
if (event.origin !== url) return; if (event.origin !== url) return;
if (event.data === 'loaded') { if (event.data === 'loaded') {
tab.postMessage(JSON.stringify(modelfile), '*'); tab.postMessage(JSON.stringify(model), '*');
} }
}, },
false false
); );
}; };
const saveModelfiles = async (modelfiles) => { const downloadModels = async (models) => {
let blob = new Blob([JSON.stringify(modelfiles)], { let blob = new Blob([JSON.stringify(models)], {
type: 'application/json' type: 'application/json'
}); });
saveAs(blob, `modelfiles-export-${Date.now()}.json`); saveAs(blob, `models-export-${Date.now()}.json`);
}; };
onMount(() => { onMount(() => {
// Legacy code to sync localModelfiles with models
localModelfiles = JSON.parse(localStorage.getItem('modelfiles') ?? '[]'); localModelfiles = JSON.parse(localStorage.getItem('modelfiles') ?? '[]');
if (localModelfiles) { if (localModelfiles) {
...@@ -76,13 +91,13 @@ ...@@ -76,13 +91,13 @@
<svelte:head> <svelte:head>
<title> <title>
{$i18n.t('Modelfiles')} | {$WEBUI_NAME} {$i18n.t('Models')} | {$WEBUI_NAME}
</title> </title>
</svelte:head> </svelte:head>
<div class=" text-lg font-semibold mb-3">{$i18n.t('Modelfiles')}</div> <div class=" text-lg font-semibold mb-3">{$i18n.t('Models')}</div>
<a class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2" href="/workspace/modelfiles/create"> <a class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2" href="/workspace/models/create">
<div class=" self-center w-10"> <div class=" self-center w-10">
<div <div
class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200" class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
...@@ -98,26 +113,26 @@ ...@@ -98,26 +113,26 @@
</div> </div>
<div class=" self-center"> <div class=" self-center">
<div class=" font-bold">{$i18n.t('Create a modelfile')}</div> <div class=" font-bold">{$i18n.t('Create a model')}</div>
<div class=" text-sm">{$i18n.t('Customize Ollama models for a specific purpose')}</div> <div class=" text-sm">{$i18n.t('Customize models for a specific purpose')}</div>
</div> </div>
</a> </a>
<hr class=" dark:border-gray-850" /> <hr class=" dark:border-gray-850" />
<div class=" my-2 mb-5"> <div class=" my-2 mb-5">
{#each $modelfiles as modelfile} {#each $models as model}
<div <div
class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl" class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
> >
<a <a
class=" flex flex-1 space-x-4 cursor-pointer w-full" class=" flex flex-1 space-x-4 cursor-pointer w-full"
href={`/?models=${encodeURIComponent(modelfile.tagName)}`} href={`/?models=${encodeURIComponent(model.id)}`}
> >
<div class=" self-center w-10"> <div class=" self-center w-10">
<div class=" rounded-full bg-stone-700"> <div class=" rounded-full bg-stone-700">
<img <img
src={modelfile.imageUrl ?? '/user.png'} src={model?.info?.meta?.profile_image_url ?? '/favicon.png'}
alt="modelfile profile" alt="modelfile profile"
class=" rounded-full w-full h-auto object-cover" class=" rounded-full w-full h-auto object-cover"
/> />
...@@ -125,9 +140,9 @@ ...@@ -125,9 +140,9 @@
</div> </div>
<div class=" flex-1 self-center"> <div class=" flex-1 self-center">
<div class=" font-bold capitalize">{modelfile.title}</div> <div class=" font-bold line-clamp-1">{model.name}</div>
<div class=" text-sm overflow-hidden text-ellipsis line-clamp-1"> <div class=" text-sm overflow-hidden text-ellipsis line-clamp-1">
{modelfile.desc} {!!model?.info?.meta?.description ? model?.info?.meta?.description : model.id}
</div> </div>
</div> </div>
</a> </a>
...@@ -135,7 +150,7 @@ ...@@ -135,7 +150,7 @@
<a <a
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl" class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button" type="button"
href={`/workspace/modelfiles/edit?tag=${encodeURIComponent(modelfile.tagName)}`} href={`/workspace/models/edit?id=${encodeURIComponent(model.id)}`}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
...@@ -157,9 +172,7 @@ ...@@ -157,9 +172,7 @@
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl" class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button" type="button"
on:click={() => { on:click={() => {
// console.log(modelfile); cloneModelHandler(model);
sessionStorage.modelfile = JSON.stringify(modelfile);
goto('/workspace/modelfiles/create');
}} }}
> >
<svg <svg
...@@ -182,7 +195,7 @@ ...@@ -182,7 +195,7 @@
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl" class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button" type="button"
on:click={() => { on:click={() => {
shareModelfile(modelfile); shareModelHandler(model);
}} }}
> >
<svg <svg
...@@ -205,7 +218,7 @@ ...@@ -205,7 +218,7 @@
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl" class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button" type="button"
on:click={() => { on:click={() => {
deleteModelfile(modelfile.tagName); deleteModelHandler(model);
}} }}
> >
<svg <svg
...@@ -231,8 +244,8 @@ ...@@ -231,8 +244,8 @@
<div class=" flex justify-end w-full mb-3"> <div class=" flex justify-end w-full mb-3">
<div class="flex space-x-1"> <div class="flex space-x-1">
<input <input
id="modelfiles-import-input" id="models-import-input"
bind:this={modelfilesImportInputElement} bind:this={modelsImportInputElement}
bind:files={importFiles} bind:files={importFiles}
type="file" type="file"
accept=".json" accept=".json"
...@@ -242,16 +255,18 @@ ...@@ -242,16 +255,18 @@
let reader = new FileReader(); let reader = new FileReader();
reader.onload = async (event) => { reader.onload = async (event) => {
let savedModelfiles = JSON.parse(event.target.result); let savedModels = JSON.parse(event.target.result);
console.log(savedModelfiles); console.log(savedModels);
for (const modelfile of savedModelfiles) { for (const model of savedModels) {
await createNewModelfile(localStorage.token, modelfile).catch((error) => { if (model?.info ?? false) {
return null; await addNewModel(localStorage.token, model.info).catch((error) => {
}); return null;
});
}
} }
await modelfiles.set(await getModelfiles(localStorage.token)); await models.set(await getModels(localStorage.token));
}; };
reader.readAsText(importFiles[0]); reader.readAsText(importFiles[0]);
...@@ -261,10 +276,10 @@ ...@@ -261,10 +276,10 @@
<button <button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition" class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
on:click={() => { on:click={() => {
modelfilesImportInputElement.click(); modelsImportInputElement.click();
}} }}
> >
<div class=" self-center mr-2 font-medium">{$i18n.t('Import Modelfiles')}</div> <div class=" self-center mr-2 font-medium">{$i18n.t('Import Models')}</div>
<div class=" self-center"> <div class=" self-center">
<svg <svg
...@@ -285,10 +300,10 @@ ...@@ -285,10 +300,10 @@
<button <button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition" class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
on:click={async () => { on:click={async () => {
saveModelfiles($modelfiles); downloadModels($models);
}} }}
> >
<div class=" self-center mr-2 font-medium">{$i18n.t('Export Modelfiles')}</div> <div class=" self-center mr-2 font-medium">{$i18n.t('Export Models')}</div>
<div class=" self-center"> <div class=" self-center">
<svg <svg
...@@ -314,47 +329,13 @@ ...@@ -314,47 +329,13 @@
</div> </div>
<div class="flex space-x-1"> <div class="flex space-x-1">
<button
class="self-center w-fit text-sm px-3 py-1 border dark:border-gray-600 rounded-xl flex"
on:click={async () => {
for (const modelfile of localModelfiles) {
await createNewModelfile(localStorage.token, modelfile).catch((error) => {
return null;
});
}
saveModelfiles(localModelfiles);
localStorage.removeItem('modelfiles');
localModelfiles = JSON.parse(localStorage.getItem('modelfiles') ?? '[]');
await modelfiles.set(await getModelfiles(localStorage.token));
}}
>
<div class=" self-center mr-2 font-medium">{$i18n.t('Sync All')}</div>
<div class=" self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-3.5 h-3.5"
>
<path
fill-rule="evenodd"
d="M13.836 2.477a.75.75 0 0 1 .75.75v3.182a.75.75 0 0 1-.75.75h-3.182a.75.75 0 0 1 0-1.5h1.37l-.84-.841a4.5 4.5 0 0 0-7.08.932.75.75 0 0 1-1.3-.75 6 6 0 0 1 9.44-1.242l.842.84V3.227a.75.75 0 0 1 .75-.75Zm-.911 7.5A.75.75 0 0 1 13.199 11a6 6 0 0 1-9.44 1.241l-.84-.84v1.371a.75.75 0 0 1-1.5 0V9.591a.75.75 0 0 1 .75-.75H5.35a.75.75 0 0 1 0 1.5H3.98l.841.841a4.5 4.5 0 0 0 7.08-.932.75.75 0 0 1 1.025-.273Z"
clip-rule="evenodd"
/>
</svg>
</div>
</button>
<button <button
class="self-center w-fit text-sm p-1.5 border dark:border-gray-600 rounded-xl flex" class="self-center w-fit text-sm p-1.5 border dark:border-gray-600 rounded-xl flex"
on:click={async () => { on:click={async () => {
saveModelfiles(localModelfiles); downloadModels(localModelfiles);
localStorage.removeItem('modelfiles'); localStorage.removeItem('modelfiles');
localModelfiles = JSON.parse(localStorage.getItem('modelfiles') ?? '[]'); localModelfiles = JSON.parse(localStorage.getItem('modelfiles') ?? '[]');
await modelfiles.set(await getModelfiles(localStorage.token));
}} }}
> >
<div class=" self-center"> <div class=" self-center">
...@@ -402,7 +383,7 @@ ...@@ -402,7 +383,7 @@
</div> </div>
<div class=" self-center"> <div class=" self-center">
<div class=" font-bold">{$i18n.t('Discover a modelfile')}</div> <div class=" font-bold">{$i18n.t('Discover a model')}</div>
<div class=" text-sm">{$i18n.t('Discover, download, and explore model presets')}</div> <div class=" text-sm">{$i18n.t('Discover, download, and explore model presets')}</div>
</div> </div>
</a> </a>
......
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