Merge branch 'open-webui:main' into main

This commit is contained in:
Igor Brai
2024-11-21 15:25:41 +02:00
committed by GitHub
264 changed files with 29674 additions and 15257 deletions
+44 -2
View File
@@ -181,7 +181,7 @@
<div>
<div class="mt-1 flex gap-2 mb-1">
<input
class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
class="flex-1 w-full bg-transparent outline-none"
placeholder={$i18n.t('API Base URL')}
bind:value={STT_OPENAI_API_BASE_URL}
required
@@ -322,6 +322,7 @@
}}
>
<option value="">{$i18n.t('Web API')}</option>
<option value="transformers">{$i18n.t('Transformers')} ({$i18n.t('Local')})</option>
<option value="openai">{$i18n.t('OpenAI')}</option>
<option value="elevenlabs">{$i18n.t('ElevenLabs')}</option>
<option value="azure">{$i18n.t('Azure AI Speech')}</option>
@@ -333,7 +334,7 @@
<div>
<div class="mt-1 flex gap-2 mb-1">
<input
class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
class="flex-1 w-full bg-transparent outline-none"
placeholder={$i18n.t('API Base URL')}
bind:value={TTS_OPENAI_API_BASE_URL}
required
@@ -396,6 +397,47 @@
</div>
</div>
</div>
{:else if TTS_ENGINE === 'transformers'}
<div>
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('TTS Model')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
list="model-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={TTS_MODEL}
placeholder="CMU ARCTIC speaker embedding name"
/>
<datalist id="model-list">
<option value="tts-1" />
</datalist>
</div>
</div>
<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
{$i18n.t(`Open WebUI uses SpeechT5 and CMU Arctic speaker embeddings.`)}
To learn more about SpeechT5,
<a
class=" hover:underline dark:text-gray-200 text-gray-800"
href="https://github.com/microsoft/SpeechT5"
target="_blank"
>
{$i18n.t(`click here`, {
name: 'SpeechT5'
})}.
</a>
To see the available CMU Arctic speaker embeddings,
<a
class=" hover:underline dark:text-gray-200 text-gray-800"
href="https://huggingface.co/datasets/Matthijs/cmu-arctic-xvectors"
target="_blank"
>
{$i18n.t(`click here`)}.
</a>
</div>
</div>
{:else if TTS_ENGINE === 'openai'}
<div class=" flex gap-2">
<div class="w-full">
@@ -1,31 +1,23 @@
<script lang="ts">
import { models, user } from '$lib/stores';
import { toast } from 'svelte-sonner';
import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
const dispatch = createEventDispatcher();
import {
getOllamaConfig,
getOllamaUrls,
getOllamaVersion,
updateOllamaConfig,
updateOllamaUrls
} from '$lib/apis/ollama';
import {
getOpenAIConfig,
getOpenAIKeys,
getOpenAIModels,
getOpenAIUrls,
updateOpenAIConfig,
updateOpenAIKeys,
updateOpenAIUrls
} from '$lib/apis/openai';
import { toast } from 'svelte-sonner';
import { getOllamaConfig, updateOllamaConfig } from '$lib/apis/ollama';
import { getOpenAIConfig, updateOpenAIConfig, getOpenAIModels } from '$lib/apis/openai';
import { getModels as _getModels } from '$lib/apis';
import { models, user } from '$lib/stores';
import Switch from '$lib/components/common/Switch.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import { getModels as _getModels } from '$lib/apis';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
import Plus from '$lib/components/icons/Plus.svelte';
import OpenAIConnection from './Connections/OpenAIConnection.svelte';
import AddConnectionModal from './Connections/AddConnectionModal.svelte';
import OllamaConnection from './Connections/OllamaConnection.svelte';
const i18n = getContext('i18n');
@@ -36,395 +28,302 @@
// External
let OLLAMA_BASE_URLS = [''];
let OLLAMA_API_CONFIGS = {};
let OPENAI_API_KEYS = [''];
let OPENAI_API_BASE_URLS = [''];
let OPENAI_API_CONFIGS = {};
let ENABLE_OPENAI_API: null | boolean = null;
let ENABLE_OLLAMA_API: null | boolean = null;
let pipelineUrls = {};
let ENABLE_OPENAI_API = null;
let ENABLE_OLLAMA_API = null;
const verifyOpenAIHandler = async (idx) => {
OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.map((url) => url.replace(/\/$/, ''));
OPENAI_API_BASE_URLS = await updateOpenAIUrls(localStorage.token, OPENAI_API_BASE_URLS);
OPENAI_API_KEYS = await updateOpenAIKeys(localStorage.token, OPENAI_API_KEYS);
const res = await getOpenAIModels(localStorage.token, idx).catch((error) => {
toast.error(error);
return null;
});
if (res) {
toast.success($i18n.t('Server connection verified'));
if (res.pipelines) {
pipelineUrls[OPENAI_API_BASE_URLS[idx]] = true;
}
}
await models.set(await getModels());
};
const verifyOllamaHandler = async (idx) => {
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url) => url !== '').map((url) =>
url.replace(/\/$/, '')
);
OLLAMA_BASE_URLS = await updateOllamaUrls(localStorage.token, OLLAMA_BASE_URLS);
const res = await getOllamaVersion(localStorage.token, idx).catch((error) => {
toast.error(error);
return null;
});
if (res) {
toast.success($i18n.t('Server connection verified'));
}
await models.set(await getModels());
};
let showAddOpenAIConnectionModal = false;
let showAddOllamaConnectionModal = false;
const updateOpenAIHandler = async () => {
OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.map((url) => url.replace(/\/$/, ''));
if (ENABLE_OPENAI_API !== null) {
OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.filter(
(url, urlIdx) => OPENAI_API_BASE_URLS.indexOf(url) === urlIdx && url !== ''
).map((url) => url.replace(/\/$/, ''));
// Check if API KEYS length is same than API URLS length
if (OPENAI_API_KEYS.length !== OPENAI_API_BASE_URLS.length) {
// if there are more keys than urls, remove the extra keys
if (OPENAI_API_KEYS.length > OPENAI_API_BASE_URLS.length) {
OPENAI_API_KEYS = OPENAI_API_KEYS.slice(0, OPENAI_API_BASE_URLS.length);
}
// Check if API KEYS length is same than API URLS length
if (OPENAI_API_KEYS.length !== OPENAI_API_BASE_URLS.length) {
// if there are more keys than urls, remove the extra keys
if (OPENAI_API_KEYS.length > OPENAI_API_BASE_URLS.length) {
OPENAI_API_KEYS = OPENAI_API_KEYS.slice(0, OPENAI_API_BASE_URLS.length);
}
// if there are more urls than keys, add empty keys
if (OPENAI_API_KEYS.length < OPENAI_API_BASE_URLS.length) {
const diff = OPENAI_API_BASE_URLS.length - OPENAI_API_KEYS.length;
for (let i = 0; i < diff; i++) {
OPENAI_API_KEYS.push('');
// if there are more urls than keys, add empty keys
if (OPENAI_API_KEYS.length < OPENAI_API_BASE_URLS.length) {
const diff = OPENAI_API_BASE_URLS.length - OPENAI_API_KEYS.length;
for (let i = 0; i < diff; i++) {
OPENAI_API_KEYS.push('');
}
}
}
}
OPENAI_API_BASE_URLS = await updateOpenAIUrls(localStorage.token, OPENAI_API_BASE_URLS);
OPENAI_API_KEYS = await updateOpenAIKeys(localStorage.token, OPENAI_API_KEYS);
await models.set(await getModels());
};
const updateOllamaUrlsHandler = async () => {
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url) => url !== '').map((url) =>
url.replace(/\/$/, '')
);
console.log(OLLAMA_BASE_URLS);
if (OLLAMA_BASE_URLS.length === 0) {
ENABLE_OLLAMA_API = false;
await updateOllamaConfig(localStorage.token, ENABLE_OLLAMA_API);
toast.info($i18n.t('Ollama API disabled'));
} else {
OLLAMA_BASE_URLS = await updateOllamaUrls(localStorage.token, OLLAMA_BASE_URLS);
const ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => {
const res = await updateOpenAIConfig(localStorage.token, {
ENABLE_OPENAI_API: ENABLE_OPENAI_API,
OPENAI_API_BASE_URLS: OPENAI_API_BASE_URLS,
OPENAI_API_KEYS: OPENAI_API_KEYS,
OPENAI_API_CONFIGS: OPENAI_API_CONFIGS
}).catch((error) => {
toast.error(error);
return null;
});
if (ollamaVersion) {
toast.success($i18n.t('Server connection verified'));
if (res) {
toast.success($i18n.t('OpenAI API settings updated'));
await models.set(await getModels());
}
}
};
const updateOllamaHandler = async () => {
if (ENABLE_OLLAMA_API !== null) {
// Remove duplicate URLs
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter(
(url, urlIdx) => OLLAMA_BASE_URLS.indexOf(url) === urlIdx && url !== ''
).map((url) => url.replace(/\/$/, ''));
console.log(OLLAMA_BASE_URLS);
if (OLLAMA_BASE_URLS.length === 0) {
ENABLE_OLLAMA_API = false;
toast.info($i18n.t('Ollama API disabled'));
}
const res = await updateOllamaConfig(localStorage.token, {
ENABLE_OLLAMA_API: ENABLE_OLLAMA_API,
OLLAMA_BASE_URLS: OLLAMA_BASE_URLS,
OLLAMA_API_CONFIGS: OLLAMA_API_CONFIGS
}).catch((error) => {
toast.error(error);
});
if (res) {
toast.success($i18n.t('Ollama API settings updated'));
await models.set(await getModels());
}
}
};
const addOpenAIConnectionHandler = async (connection) => {
OPENAI_API_BASE_URLS = [...OPENAI_API_BASE_URLS, connection.url];
OPENAI_API_KEYS = [...OPENAI_API_KEYS, connection.key];
OPENAI_API_CONFIGS[connection.url] = connection.config;
await updateOpenAIHandler();
};
const addOllamaConnectionHandler = async (connection) => {
OLLAMA_BASE_URLS = [...OLLAMA_BASE_URLS, connection.url];
OLLAMA_API_CONFIGS[connection.url] = connection.config;
await updateOllamaHandler();
};
onMount(async () => {
if ($user.role === 'admin') {
let ollamaConfig = {};
let openaiConfig = {};
await Promise.all([
(async () => {
OLLAMA_BASE_URLS = await getOllamaUrls(localStorage.token);
ollamaConfig = await getOllamaConfig(localStorage.token);
})(),
(async () => {
OPENAI_API_BASE_URLS = await getOpenAIUrls(localStorage.token);
})(),
(async () => {
OPENAI_API_KEYS = await getOpenAIKeys(localStorage.token);
openaiConfig = await getOpenAIConfig(localStorage.token);
})()
]);
const ollamaConfig = await getOllamaConfig(localStorage.token);
const openaiConfig = await getOpenAIConfig(localStorage.token);
ENABLE_OPENAI_API = openaiConfig.ENABLE_OPENAI_API;
ENABLE_OLLAMA_API = ollamaConfig.ENABLE_OLLAMA_API;
OPENAI_API_BASE_URLS = openaiConfig.OPENAI_API_BASE_URLS;
OPENAI_API_KEYS = openaiConfig.OPENAI_API_KEYS;
OPENAI_API_CONFIGS = openaiConfig.OPENAI_API_CONFIGS;
OLLAMA_BASE_URLS = ollamaConfig.OLLAMA_BASE_URLS;
OLLAMA_API_CONFIGS = ollamaConfig.OLLAMA_API_CONFIGS;
if (ENABLE_OPENAI_API) {
for (const url of OPENAI_API_BASE_URLS) {
if (!OPENAI_API_CONFIGS[url]) {
OPENAI_API_CONFIGS[url] = {};
}
}
OPENAI_API_BASE_URLS.forEach(async (url, idx) => {
OPENAI_API_CONFIGS[url] = OPENAI_API_CONFIGS[url] || {};
if (!(OPENAI_API_CONFIGS[url]?.enable ?? true)) {
return;
}
const res = await getOpenAIModels(localStorage.token, idx);
if (res.pipelines) {
pipelineUrls[url] = true;
}
});
}
if (ENABLE_OLLAMA_API) {
for (const url of OLLAMA_BASE_URLS) {
if (!OLLAMA_API_CONFIGS[url]) {
OLLAMA_API_CONFIGS[url] = {};
}
}
}
}
});
</script>
<AddConnectionModal
bind:show={showAddOpenAIConnectionModal}
onSubmit={addOpenAIConnectionHandler}
/>
<AddConnectionModal
ollama
bind:show={showAddOllamaConnectionModal}
onSubmit={addOllamaConnectionHandler}
/>
<form
class="flex flex-col h-full justify-between text-sm"
on:submit|preventDefault={() => {
updateOpenAIHandler();
updateOllamaUrlsHandler();
updateOllamaHandler();
dispatch('save');
}}
>
<div class="space-y-3 overflow-y-scroll scrollbar-hidden h-full">
<div class=" overflow-y-scroll scrollbar-hidden h-full">
{#if ENABLE_OPENAI_API !== null && ENABLE_OLLAMA_API !== null}
<div class=" space-y-3">
<div class="my-2">
<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 class="flex items-center">
<div class="">
<Switch
bind:state={ENABLE_OPENAI_API}
on:change={async () => {
updateOpenAIHandler();
}}
/>
</div>
</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 relative">
<input
class="w-full rounded-lg py-2 px-4 {pipelineUrls[url]
? 'pr-8'
: ''} text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Base URL')}
bind:value={url}
autocomplete="off"
/>
<hr class=" border-gray-50 dark:border-gray-850" />
{#if pipelineUrls[url]}
<div class=" absolute top-2.5 right-2.5">
<Tooltip content="Pipelines">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-4"
>
<path
d="M11.644 1.59a.75.75 0 0 1 .712 0l9.75 5.25a.75.75 0 0 1 0 1.32l-9.75 5.25a.75.75 0 0 1-.712 0l-9.75-5.25a.75.75 0 0 1 0-1.32l9.75-5.25Z"
/>
<path
d="m3.265 10.602 7.668 4.129a2.25 2.25 0 0 0 2.134 0l7.668-4.13 1.37.739a.75.75 0 0 1 0 1.32l-9.75 5.25a.75.75 0 0 1-.71 0l-9.75-5.25a.75.75 0 0 1 0-1.32l1.37-.738Z"
/>
<path
d="m10.933 19.231-7.668-4.13-1.37.739a.75.75 0 0 0 0 1.32l9.75 5.25c.221.12.489.12.71 0l9.75-5.25a.75.75 0 0 0 0-1.32l-1.37-.738-7.668 4.13a2.25 2.25 0 0 1-2.134-.001Z"
/>
</svg>
</Tooltip>
</div>
{/if}
</div>
<div class="">
<div class="flex justify-between items-center">
<div class="font-medium">{$i18n.t('Manage OpenAI API Connections')}</div>
<SensitiveInput
placeholder={$i18n.t('API Key')}
bind:value={OPENAI_API_KEYS[idx]}
<Tooltip content={$i18n.t(`Add Connection`)}>
<button
class="px-1"
on:click={() => {
showAddOpenAIConnectionModal = true;
}}
type="button"
>
<Plus />
</button>
</Tooltip>
</div>
<div class="flex flex-col gap-1.5 mt-1.5">
{#each OPENAI_API_BASE_URLS as url, idx}
<OpenAIConnection
pipeline={pipelineUrls[url] ? true : false}
bind:url
bind:key={OPENAI_API_KEYS[idx]}
bind:config={OPENAI_API_CONFIGS[url]}
onSubmit={() => {
updateOpenAIHandler();
}}
onDelete={() => {
OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.filter(
(url, urlIdx) => idx !== urlIdx
);
OPENAI_API_KEYS = OPENAI_API_KEYS.filter((key, keyIdx) => idx !== keyIdx);
}}
/>
<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 class="flex">
<Tooltip content="Verify connection" className="self-start mt-0.5">
<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"
on:click={() => {
verifyOpenAIHandler(idx);
}}
type="button"
>
<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="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>
</Tooltip>
</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}
{/each}
</div>
</div>
{/if}
</div>
</div>
<hr class=" dark:border-gray-850" />
<hr class=" border-gray-50 dark:border-gray-850" />
<div class="pr-1.5 space-y-2">
<div class="flex justify-between items-center text-sm">
<div class="pr-1.5 my-2">
<div class="flex justify-between items-center text-sm mb-2">
<div class=" font-medium">{$i18n.t('Ollama API')}</div>
<div class="mt-1">
<Switch
bind:state={ENABLE_OLLAMA_API}
on:change={async () => {
updateOllamaConfig(localStorage.token, ENABLE_OLLAMA_API);
if (OLLAMA_BASE_URLS.length === 0) {
OLLAMA_BASE_URLS = [''];
}
updateOllamaHandler();
}}
/>
</div>
</div>
{#if ENABLE_OLLAMA_API}
<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 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Enter URL (e.g. http://localhost:11434)')}
bind:value={url}
/>
<hr class=" border-gray-50 dark:border-gray-850 my-2" />
<div class="self-center flex items-center">
{#if idx === 0}
<button
class="px-1"
on:click={() => {
OLLAMA_BASE_URLS = [...OLLAMA_BASE_URLS, ''];
}}
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={() => {
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 class="">
<div class="flex justify-between items-center">
<div class="font-medium">{$i18n.t('Manage Ollama API Connections')}</div>
<div class="flex">
<Tooltip content="Verify connection" className="self-start mt-0.5">
<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"
on:click={() => {
verifyOllamaHandler(idx);
}}
type="button"
>
<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="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>
</Tooltip>
</div>
</div>
{/each}
<Tooltip content={$i18n.t(`Add Connection`)}>
<button
class="px-1"
on:click={() => {
showAddOllamaConnectionModal = true;
}}
type="button"
>
<Plus />
</button>
</Tooltip>
</div>
</div>
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
{$i18n.t('Trouble accessing Ollama?')}
<a
class=" text-gray-300 font-medium underline"
href="https://github.com/open-webui/open-webui#troubleshooting"
target="_blank"
>
{$i18n.t('Click here for help.')}
</a>
<div class="flex w-full gap-1.5">
<div class="flex-1 flex flex-col gap-1.5 mt-1.5">
{#each OLLAMA_BASE_URLS as url, idx}
<OllamaConnection
bind:url
bind:config={OLLAMA_API_CONFIGS[url]}
{idx}
onSubmit={() => {
updateOllamaHandler();
}}
onDelete={() => {
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url, urlIdx) => idx !== urlIdx);
}}
/>
{/each}
</div>
</div>
<div class="mt-1 text-xs text-gray-400 dark:text-gray-500">
{$i18n.t('Trouble accessing Ollama?')}
<a
class=" text-gray-300 font-medium underline"
href="https://github.com/open-webui/open-webui#troubleshooting"
target="_blank"
>
{$i18n.t('Click here for help.')}
</a>
</div>
</div>
{/if}
</div>
@@ -0,0 +1,365 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { getContext, onMount } from 'svelte';
const i18n = getContext('i18n');
import { models } from '$lib/stores';
import { verifyOpenAIConnection } from '$lib/apis/openai';
import { verifyOllamaConnection } from '$lib/apis/ollama';
import Modal from '$lib/components/common/Modal.svelte';
import Plus from '$lib/components/icons/Plus.svelte';
import Minus from '$lib/components/icons/Minus.svelte';
import PencilSolid from '$lib/components/icons/PencilSolid.svelte';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Switch from '$lib/components/common/Switch.svelte';
export let onSubmit: Function = () => {};
export let onDelete: Function = () => {};
export let show = false;
export let edit = false;
export let ollama = false;
export let connection = null;
let url = '';
let key = '';
let prefixId = '';
let enable = true;
let modelId = '';
let modelIds = [];
let loading = false;
const verifyOllamaHandler = async () => {
const res = await verifyOllamaConnection(localStorage.token, url, key).catch((error) => {
toast.error(error);
});
if (res) {
toast.success($i18n.t('Server connection verified'));
}
};
const verifyOpenAIHandler = async () => {
const res = await verifyOpenAIConnection(localStorage.token, url, key).catch((error) => {
toast.error(error);
});
if (res) {
toast.success($i18n.t('Server connection verified'));
}
};
const verifyHandler = () => {
if (ollama) {
verifyOllamaHandler();
} else {
verifyOpenAIHandler();
}
};
const addModelHandler = () => {
if (modelId) {
modelIds = [...modelIds, modelId];
modelId = '';
}
};
const submitHandler = async () => {
loading = true;
if (!ollama && (!url || !key)) {
loading = false;
toast.error('URL and Key are required');
return;
}
const connection = {
url,
key,
config: {
enable: enable,
prefix_id: prefixId,
model_ids: modelIds
}
};
await onSubmit(connection);
loading = false;
show = false;
url = '';
key = '';
prefixId = '';
modelIds = [];
};
const init = () => {
if (connection) {
url = connection.url;
key = connection.key;
enable = connection.config?.enable ?? true;
prefixId = connection.config?.prefix_id ?? '';
modelIds = connection.config?.model_ids ?? [];
}
};
$: if (show) {
init();
}
onMount(() => {
init();
});
</script>
<Modal size="sm" bind:show>
<div>
<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 pb-2">
<div class=" text-lg font-medium self-center font-primary">
{#if edit}
{$i18n.t('Edit Connection')}
{:else}
{$i18n.t('Add Connection')}
{/if}
</div>
<button
class="self-center"
on:click={() => {
show = false;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<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 class="flex flex-col md:flex-row w-full px-4 pb-4 md:space-x-4 dark:text-gray-200">
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
<form
class="flex flex-col w-full"
on:submit={(e) => {
e.preventDefault();
submitHandler();
}}
>
<div class="px-1">
<div class="flex gap-2">
<div class="flex flex-col w-full">
<div class=" mb-0.5 text-xs text-gray-500">{$i18n.t('URL')}</div>
<div class="flex-1">
<input
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
type="text"
bind:value={url}
placeholder={$i18n.t('API Base URL')}
autocomplete="off"
required
/>
</div>
</div>
<Tooltip content="Verify Connection" className="self-end -mb-1">
<button
class="self-center p-1 bg-transparent hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition"
on:click={() => {
verifyHandler();
}}
type="button"
>
<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="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>
</Tooltip>
<div class="flex flex-col flex-shrink-0 self-end">
<Tooltip content={enable ? $i18n.t('Enabled') : $i18n.t('Disabled')}>
<Switch bind:state={enable} />
</Tooltip>
</div>
</div>
<div class="flex gap-2 mt-2">
<div class="flex flex-col w-full">
<div class=" mb-0.5 text-xs text-gray-500">{$i18n.t('Key')}</div>
<div class="flex-1">
<SensitiveInput
className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
bind:value={key}
placeholder={$i18n.t('API Key')}
required={!ollama}
/>
</div>
</div>
<div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Prefix ID')}</div>
<div class="flex-1">
<Tooltip
content={$i18n.t(
'Prefix ID is used to avoid conflicts with other connections by adding a prefix to the model IDs - leave empty to disable'
)}
>
<input
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
type="text"
bind:value={prefixId}
placeholder={$i18n.t('Prefix ID')}
autocomplete="off"
/>
</Tooltip>
</div>
</div>
</div>
<hr class=" border-gray-100 dark:border-gray-700/10 my-2.5 w-full" />
<div class="flex flex-col w-full">
<div class="mb-1 flex justify-between">
<div class="text-xs text-gray-500">{$i18n.t('Model IDs')}</div>
</div>
{#if modelIds.length > 0}
<div class="flex flex-col">
{#each modelIds as modelId, modelIdx}
<div class=" flex gap-2 w-full justify-between items-center">
<div class=" text-sm flex-1 py-1 rounded-lg">
{modelId}
</div>
<div class="flex-shrink-0">
<button
type="button"
on:click={() => {
modelIds = modelIds.filter((_, idx) => idx !== modelIdx);
}}
>
<Minus strokeWidth="2" className="size-3.5" />
</button>
</div>
</div>
{/each}
</div>
{:else}
<div class="text-gray-500 text-xs text-center py-2 px-10">
{#if ollama}
{$i18n.t('Leave empty to include all models from "{{URL}}/api/tags" endpoint', {
URL: url
})}
{:else}
{$i18n.t('Leave empty to include all models from "{{URL}}/models" endpoint', {
URL: url
})}
{/if}
</div>
{/if}
</div>
<hr class=" border-gray-100 dark:border-gray-700/10 my-2.5 w-full" />
<div class="flex items-center">
<input
class="w-full py-1 text-sm rounded-lg bg-transparent {modelId
? ''
: 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
bind:value={modelId}
placeholder={$i18n.t('Add a model ID')}
/>
<div>
<button
type="button"
on:click={() => {
addModelHandler();
}}
>
<Plus className="size-3.5" strokeWidth="2" />
</button>
</div>
</div>
</div>
<div class="flex justify-end pt-3 text-sm font-medium gap-1.5">
{#if edit}
<button
class="px-3.5 py-1.5 text-sm font-medium dark:bg-black dark:hover:bg-gray-900 dark:text-white bg-white text-black hover:bg-gray-100 transition rounded-full flex flex-row space-x-1 items-center"
type="button"
on:click={() => {
onDelete();
show = false;
}}
>
{$i18n.t('Delete')}
</button>
{/if}
<button
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full flex flex-row space-x-1 items-center {loading
? ' cursor-not-allowed'
: ''}"
type="submit"
disabled={loading}
>
{$i18n.t('Save')}
{#if loading}
<div class="ml-2 self-center">
<svg
class=" w-4 h-4"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
}
}
</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"
opacity=".25"
/><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"
class="spinner_ajPY"
/></svg
>
</div>
{/if}
</button>
</div>
</form>
</div>
</div>
</div>
</Modal>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,89 @@
<script lang="ts">
import { getContext, tick } from 'svelte';
const i18n = getContext('i18n');
import Tooltip from '$lib/components/common/Tooltip.svelte';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
import AddConnectionModal from './AddConnectionModal.svelte';
import Cog6 from '$lib/components/icons/Cog6.svelte';
import Wrench from '$lib/components/icons/Wrench.svelte';
import ManageOllamaModal from './ManageOllamaModal.svelte';
export let onDelete = () => {};
export let onSubmit = () => {};
export let url = '';
export let idx = 0;
export let config = {};
let showManageModal = false;
let showConfigModal = false;
</script>
<AddConnectionModal
ollama
edit
bind:show={showConfigModal}
connection={{
url,
key: config?.key ?? '',
config: config
}}
{onDelete}
onSubmit={(connection) => {
url = connection.url;
config = { ...connection.config, key: connection.key };
onSubmit(connection);
}}
/>
<ManageOllamaModal bind:show={showManageModal} urlIdx={idx} />
<div class="flex gap-1.5">
<Tooltip
className="w-full relative"
content={$i18n.t(`WebUI will make requests to "{{url}}/api/chat"`, {
url
})}
placement="top-start"
>
{#if !(config?.enable ?? true)}
<div
class="absolute top-0 bottom-0 left-0 right-0 opacity-60 bg-white dark:bg-gray-900 z-10"
></div>
{/if}
<input
class="w-full text-sm bg-transparent outline-none"
placeholder={$i18n.t('Enter URL (e.g. http://localhost:11434)')}
bind:value={url}
/>
</Tooltip>
<div class="flex gap-1">
<Tooltip content={$i18n.t('Manage')} className="self-start">
<button
class="self-center p-1 bg-transparent hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition"
on:click={() => {
showManageModal = true;
}}
type="button"
>
<Wrench />
</button>
</Tooltip>
<Tooltip content={$i18n.t('Configure')} className="self-start">
<button
class="self-center p-1 bg-transparent hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition"
on:click={() => {
showConfigModal = true;
}}
type="button"
>
<Cog6 />
</button>
</Tooltip>
</div>
</div>
@@ -0,0 +1,107 @@
<script lang="ts">
import { getContext, tick } from 'svelte';
const i18n = getContext('i18n');
import Tooltip from '$lib/components/common/Tooltip.svelte';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
import Cog6 from '$lib/components/icons/Cog6.svelte';
import AddConnectionModal from './AddConnectionModal.svelte';
import { connect } from 'socket.io-client';
export let onDelete = () => {};
export let onSubmit = () => {};
export let pipeline = false;
export let url = '';
export let key = '';
export let config = {};
let showConfigModal = false;
</script>
<AddConnectionModal
edit
bind:show={showConfigModal}
connection={{
url,
key,
config
}}
{onDelete}
onSubmit={(connection) => {
url = connection.url;
key = connection.key;
config = connection.config;
onSubmit(connection);
}}
/>
<div class="flex w-full gap-2 items-center">
<Tooltip
className="w-full relative"
content={$i18n.t(`WebUI will make requests to "{{url}}/chat/completions"`, {
url
})}
placement="top-start"
>
{#if !(config?.enable ?? true)}
<div
class="absolute top-0 bottom-0 left-0 right-0 opacity-60 bg-white dark:bg-gray-900 z-10"
></div>
{/if}
<div class="flex w-full">
<div class="flex-1 relative">
<input
class=" outline-none w-full bg-transparent {pipeline ? 'pr-8' : ''}"
placeholder={$i18n.t('API Base URL')}
bind:value={url}
autocomplete="off"
/>
{#if pipeline}
<div class=" absolute top-2.5 right-2.5">
<Tooltip content="Pipelines">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-4"
>
<path
d="M11.644 1.59a.75.75 0 0 1 .712 0l9.75 5.25a.75.75 0 0 1 0 1.32l-9.75 5.25a.75.75 0 0 1-.712 0l-9.75-5.25a.75.75 0 0 1 0-1.32l9.75-5.25Z"
/>
<path
d="m3.265 10.602 7.668 4.129a2.25 2.25 0 0 0 2.134 0l7.668-4.13 1.37.739a.75.75 0 0 1 0 1.32l-9.75 5.25a.75.75 0 0 1-.71 0l-9.75-5.25a.75.75 0 0 1 0-1.32l1.37-.738Z"
/>
<path
d="m10.933 19.231-7.668-4.13-1.37.739a.75.75 0 0 0 0 1.32l9.75 5.25c.221.12.489.12.71 0l9.75-5.25a.75.75 0 0 0 0-1.32l-1.37-.738-7.668 4.13a2.25 2.25 0 0 1-2.134-.001Z"
/>
</svg>
</Tooltip>
</div>
{/if}
</div>
<SensitiveInput
inputClassName=" outline-none bg-transparent w-full"
placeholder={$i18n.t('API Key')}
bind:value={key}
/>
</div>
</Tooltip>
<div class="flex gap-1">
<Tooltip content={$i18n.t('Configure')} className="self-start">
<button
class="self-center p-1 bg-transparent hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition"
on:click={() => {
showConfigModal = true;
}}
type="button"
>
<Cog6 />
</button>
</Tooltip>
</div>
</div>
@@ -181,37 +181,6 @@
</div>
</button>
{/if}
<hr class=" dark:border-gray-850 my-1" />
<button
type="button"
class=" flex rounded-md py-2 px-3 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
on:click={() => {
downloadLiteLLMConfig(localStorage.token).catch((error) => {
toast.error(error);
});
}}
>
<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 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-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"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center text-sm font-medium">
{$i18n.t('Export LiteLLM config.yaml')}
</div>
</button>
</div>
</div>
@@ -19,7 +19,7 @@
} from '$lib/apis/retrieval';
import { knowledge, models } from '$lib/stores';
import { getKnowledgeItems } from '$lib/apis/knowledge';
import { getKnowledgeBases } from '$lib/apis/knowledge';
import { uploadDir, deleteAllFiles, deleteFileById } from '$lib/apis/files';
import ResetUploadDirConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
@@ -56,8 +56,11 @@
let chunkOverlap = 0;
let pdfExtractImages = true;
let OpenAIKey = '';
let OpenAIUrl = '';
let OpenAIKey = '';
let OllamaUrl = '';
let OllamaKey = '';
let querySettings = {
template: '',
@@ -104,19 +107,15 @@
const res = await updateEmbeddingConfig(localStorage.token, {
embedding_engine: embeddingEngine,
embedding_model: embeddingModel,
...(embeddingEngine === 'openai' || embeddingEngine === 'ollama'
? {
embedding_batch_size: embeddingBatchSize
}
: {}),
...(embeddingEngine === 'openai'
? {
openai_config: {
key: OpenAIKey,
url: OpenAIUrl
}
}
: {})
embedding_batch_size: embeddingBatchSize,
ollama_config: {
key: OllamaKey,
url: OllamaUrl
},
openai_config: {
key: OpenAIKey,
url: OpenAIUrl
}
}).catch(async (error) => {
toast.error(error);
await setEmbeddingConfig();
@@ -206,6 +205,9 @@
OpenAIKey = embeddingConfig.openai_config.key;
OpenAIUrl = embeddingConfig.openai_config.url;
OllamaKey = embeddingConfig.ollama_config.key;
OllamaUrl = embeddingConfig.ollama_config.url;
}
};
@@ -310,9 +312,9 @@
</div>
{#if embeddingEngine === 'openai'}
<div class="my-0.5 flex gap-2">
<div class="my-0.5 flex gap-2 pr-2">
<input
class="flex-1 w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-none"
placeholder={$i18n.t('API Base URL')}
bind:value={OpenAIUrl}
required
@@ -320,7 +322,23 @@
<SensitiveInput placeholder={$i18n.t('API Key')} bind:value={OpenAIKey} />
</div>
{:else if embeddingEngine === 'ollama'}
<div class="my-0.5 flex gap-2 pr-2">
<input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-none"
placeholder={$i18n.t('API Base URL')}
bind:value={OllamaUrl}
required
/>
<SensitiveInput
placeholder={$i18n.t('API Key')}
bind:value={OllamaKey}
required={false}
/>
</div>
{/if}
{#if embeddingEngine === 'ollama' || embeddingEngine === 'openai'}
<div class="flex mt-0.5 space-x-2">
<div class=" self-center text-xs font-medium">{$i18n.t('Embedding Batch Size')}</div>
@@ -376,19 +394,12 @@
{#if embeddingEngine === 'ollama'}
<div class="flex w-full">
<div class="flex-1 mr-2">
<select
<input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={embeddingModel}
placeholder={$i18n.t('Select a model')}
placeholder={$i18n.t('Set embedding model')}
required
>
{#if !embeddingModel}
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
{/if}
{#each $models.filter((m) => m.id && m.ollama && !(m?.preset ?? false)) as model}
<option value={model.id} class="bg-gray-50 dark:bg-gray-700">{model.name}</option>
{/each}
</select>
/>
</div>
</div>
{:else}
@@ -11,7 +11,7 @@
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Plus from '$lib/components/icons/Plus.svelte';
import Model from './Evaluations/Model.svelte';
import ModelModal from './Evaluations/ModelModal.svelte';
import ArenaModelModal from './Evaluations/ArenaModelModal.svelte';
import { getConfig, updateConfig } from '$lib/apis/evaluations';
const i18n = getContext('i18n');
@@ -65,7 +65,7 @@
});
</script>
<ModelModal
<ArenaModelModal
bind:show={showAddModel}
on:submit={async (e) => {
addModelHandler(e.detail);
@@ -9,6 +9,7 @@
import Minus from '$lib/components/icons/Minus.svelte';
import PencilSolid from '$lib/components/icons/PencilSolid.svelte';
import { toast } from 'svelte-sonner';
import AccessControl from '$lib/components/workspace/common/AccessControl.svelte';
export let show = false;
export let edit = false;
@@ -39,6 +40,8 @@
let modelIds = [];
let filterMode = 'include';
let accessControl = {};
let imageInputElement;
let loading = false;
@@ -74,7 +77,8 @@
profile_image_url: profileImageUrl,
description: description || null,
model_ids: modelIds.length > 0 ? modelIds : null,
filter_mode: modelIds.length > 0 ? (filterMode ? filterMode : null) : null
filter_mode: modelIds.length > 0 ? (filterMode ? filterMode : null) : null,
access_control: accessControl
}
};
@@ -98,6 +102,7 @@
description = model.meta.description;
modelIds = model.meta.model_ids || [];
filterMode = model.meta?.filter_mode ?? 'include';
accessControl = 'access_control' in model.meta ? model.meta.access_control : {};
}
};
@@ -283,6 +288,14 @@
<hr class=" border-gray-100 dark:border-gray-700/10 my-2.5 w-full" />
<div class="my-2 -mx-2">
<div class="px-3 py-2 bg-gray-50 dark:bg-gray-950 rounded-lg">
<AccessControl bind:accessControl />
</div>
</div>
<hr class=" border-gray-100 dark:border-gray-700/10 my-2.5 w-full" />
<div class="flex flex-col w-full">
<div class="mb-1 flex justify-between">
<div class="text-xs text-gray-500">{$i18n.t('Models')}</div>
@@ -4,13 +4,13 @@
const i18n = getContext('i18n');
import Cog6 from '$lib/components/icons/Cog6.svelte';
import ModelModal from './ModelModal.svelte';
import ArenaModelModal from './ArenaModelModal.svelte';
export let model;
let showModel = false;
</script>
<ModelModal
<ArenaModelModal
bind:show={showModel}
edit={true}
{model}
@@ -1,7 +1,16 @@
<script lang="ts">
import { getBackendConfig, getWebhookUrl, updateWebhookUrl } from '$lib/apis';
import { getAdminConfig, updateAdminConfig } from '$lib/apis/auths';
import {
getAdminConfig,
getLdapConfig,
getLdapServer,
updateAdminConfig,
updateLdapConfig,
updateLdapServer
} from '$lib/apis/auths';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
import Switch from '$lib/components/common/Switch.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import { config } from '$lib/stores';
import { onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
@@ -13,9 +22,37 @@
let adminConfig = null;
let webhookUrl = '';
// LDAP
let ENABLE_LDAP = false;
let LDAP_SERVER = {
label: '',
host: '',
port: '',
attribute_for_username: 'uid',
app_dn: '',
app_dn_password: '',
search_base: '',
search_filters: '',
use_tls: false,
certificate_path: '',
ciphers: ''
};
const updateLdapServerHandler = async () => {
if (!ENABLE_LDAP) return;
const res = await updateLdapServer(localStorage.token, LDAP_SERVER).catch((error) => {
toast.error(error);
return null;
});
if (res) {
toast.success($i18n.t('LDAP server updated'));
}
};
const updateHandler = async () => {
webhookUrl = await updateWebhookUrl(localStorage.token, webhookUrl);
const res = await updateAdminConfig(localStorage.token, adminConfig);
await updateLdapServerHandler();
if (res) {
saveHandler();
@@ -32,8 +69,14 @@
(async () => {
webhookUrl = await getWebhookUrl(localStorage.token);
})(),
(async () => {
LDAP_SERVER = await getLdapServer(localStorage.token);
})()
]);
const ldapConfig = await getLdapConfig(localStorage.token);
ENABLE_LDAP = ldapConfig.ENABLE_LDAP;
});
</script>
@@ -69,7 +112,13 @@
</div>
</div>
<hr class=" dark:border-gray-850 my-2" />
<div class=" flex w-full justify-between pr-2">
<div class=" self-center text-xs font-medium">{$i18n.t('Enable API Key Auth')}</div>
<Switch bind:state={adminConfig.ENABLE_API_KEY} />
</div>
<hr class=" border-gray-50 dark:border-gray-850 my-2" />
<div class="my-3 flex w-full items-center justify-between pr-2">
<div class=" self-center text-xs font-medium">
@@ -91,7 +140,7 @@
<Switch bind:state={adminConfig.ENABLE_MESSAGE_RATING} />
</div>
<hr class=" dark:border-gray-850 my-2" />
<hr class=" border-gray-50 dark:border-gray-850 my-2" />
<div class=" w-full justify-between">
<div class="flex w-full justify-between">
@@ -115,7 +164,7 @@
</div>
</div>
<hr class=" dark:border-gray-850 my-2" />
<hr class=" border-gray-50 dark:border-gray-850 my-2" />
<div class=" w-full justify-between">
<div class="flex w-full justify-between">
@@ -133,6 +182,196 @@
</div>
</div>
{/if}
<hr class=" border-gray-50 dark:border-gray-850" />
<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('LDAP')}</div>
<div class="mt-1">
<Switch
bind:state={ENABLE_LDAP}
on:change={async () => {
updateLdapConfig(localStorage.token, ENABLE_LDAP);
}}
/>
</div>
</div>
{#if ENABLE_LDAP}
<div class="flex flex-col gap-1">
<div class="flex w-full gap-2">
<div class="w-full">
<div class=" self-center text-xs font-medium min-w-fit mb-1">
{$i18n.t('Label')}
</div>
<input
class="w-full bg-transparent outline-none py-0.5"
required
placeholder={$i18n.t('Enter server label')}
bind:value={LDAP_SERVER.label}
/>
</div>
<div class="w-full"></div>
</div>
<div class="flex w-full gap-2">
<div class="w-full">
<div class=" self-center text-xs font-medium min-w-fit mb-1">
{$i18n.t('Host')}
</div>
<input
class="w-full bg-transparent outline-none py-0.5"
required
placeholder={$i18n.t('Enter server host')}
bind:value={LDAP_SERVER.host}
/>
</div>
<div class="w-full">
<div class=" self-center text-xs font-medium min-w-fit mb-1">
{$i18n.t('Port')}
</div>
<Tooltip
placement="top-start"
content={$i18n.t('Default to 389 or 636 if TLS is enabled')}
className="w-full"
>
<input
class="w-full bg-transparent outline-none py-0.5"
type="number"
placeholder={$i18n.t('Enter server port')}
bind:value={LDAP_SERVER.port}
/>
</Tooltip>
</div>
</div>
<div class="flex w-full gap-2">
<div class="w-full">
<div class=" self-center text-xs font-medium min-w-fit mb-1">
{$i18n.t('Application DN')}
</div>
<Tooltip
content={$i18n.t('The Application Account DN you bind with for search')}
placement="top-start"
>
<input
class="w-full bg-transparent outline-none py-0.5"
required
placeholder={$i18n.t('Enter Application DN')}
bind:value={LDAP_SERVER.app_dn}
/>
</Tooltip>
</div>
<div class="w-full">
<div class=" self-center text-xs font-medium min-w-fit mb-1">
{$i18n.t('Application DN Password')}
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Application DN Password')}
bind:value={LDAP_SERVER.app_dn_password}
/>
</div>
</div>
<div class="flex w-full gap-2">
<div class="w-full">
<div class=" self-center text-xs font-medium min-w-fit mb-1">
{$i18n.t('Attribute for Username')}
</div>
<Tooltip
content={$i18n.t(
'The LDAP attribute that maps to the username that users use to sign in.'
)}
placement="top-start"
>
<input
class="w-full bg-transparent outline-none py-0.5"
required
placeholder={$i18n.t('Example: sAMAccountName or uid or userPrincipalName')}
bind:value={LDAP_SERVER.attribute_for_username}
/>
</Tooltip>
</div>
</div>
<div class="flex w-full gap-2">
<div class="w-full">
<div class=" self-center text-xs font-medium min-w-fit mb-1">
{$i18n.t('Search Base')}
</div>
<Tooltip content={$i18n.t('The base to search for users')} placement="top-start">
<input
class="w-full bg-transparent outline-none py-0.5"
required
placeholder={$i18n.t('Example: ou=users,dc=foo,dc=example')}
bind:value={LDAP_SERVER.search_base}
/>
</Tooltip>
</div>
</div>
<div class="flex w-full gap-2">
<div class="w-full">
<div class=" self-center text-xs font-medium min-w-fit mb-1">
{$i18n.t('Search Filters')}
</div>
<input
class="w-full bg-transparent outline-none py-0.5"
placeholder={$i18n.t('Example: (&(objectClass=inetOrgPerson)(uid=%s))')}
bind:value={LDAP_SERVER.search_filters}
/>
</div>
</div>
<div class="text-xs text-gray-400 dark:text-gray-500">
<a
class=" text-gray-300 font-medium underline"
href="https://ldap.com/ldap-filters/"
target="_blank"
>
{$i18n.t('Click here for filter guides.')}
</a>
</div>
<div>
<div class="flex justify-between items-center text-sm">
<div class=" font-medium">{$i18n.t('TLS')}</div>
<div class="mt-1">
<Switch bind:state={LDAP_SERVER.use_tls} />
</div>
</div>
{#if LDAP_SERVER.use_tls}
<div class="flex w-full gap-2">
<div class="w-full">
<div class=" self-center text-xs font-medium min-w-fit mb-1 mt-1">
{$i18n.t('Certificate Path')}
</div>
<input
class="w-full bg-transparent outline-none py-0.5"
required
placeholder={$i18n.t('Enter certificate path')}
bind:value={LDAP_SERVER.certificate_path}
/>
</div>
</div>
<div class="flex w-full gap-2">
<div class="w-full">
<div class=" self-center text-xs font-medium min-w-fit mb-1">
{$i18n.t('Ciphers')}
</div>
<Tooltip content={$i18n.t('Default to ALL')} placement="top-start">
<input
class="w-full bg-transparent outline-none py-0.5"
placeholder={$i18n.t('Example: ALL')}
bind:value={LDAP_SERVER.ciphers}
/>
</Tooltip>
</div>
<div class="w-full"></div>
</div>
{/if}
</div>
</div>
{/if}
</div>
</div>
</div>
<div class="flex justify-end pt-3 text-sm font-medium">
@@ -566,7 +566,7 @@
<div class="flex gap-2 mb-1">
<input
class="flex-1 w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
class="flex-1 w-full text-sm bg-transparent outline-none"
placeholder={$i18n.t('API Base URL')}
bind:value={config.openai.OPENAI_API_BASE_URL}
required
@@ -25,8 +25,10 @@
TASK_MODEL_EXTERNAL: '',
TITLE_GENERATION_PROMPT_TEMPLATE: '',
TAGS_GENERATION_PROMPT_TEMPLATE: '',
ENABLE_SEARCH_QUERY: true,
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE: ''
ENABLE_TAGS_GENERATION: true,
ENABLE_SEARCH_QUERY_GENERATION: true,
ENABLE_RETRIEVAL_QUERY_GENERATION: true,
QUERY_GENERATION_PROMPT_TEMPLATE: ''
};
let promptSuggestions = [];
@@ -133,40 +135,26 @@
</Tooltip>
</div>
<div class="mt-3">
<div class=" mb-2.5 text-xs font-medium">{$i18n.t('Tags Generation Prompt')}</div>
<Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
placement="top-start"
>
<Textarea
bind:value={taskConfig.TAGS_GENERATION_PROMPT_TEMPLATE}
placeholder={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
/>
</Tooltip>
</div>
<hr class=" dark:border-gray-850 my-3" />
<div class="my-3 flex w-full items-center justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('Enable Web Search Query Generation')}
{$i18n.t('Enable Tags Generation')}
</div>
<Switch bind:state={taskConfig.ENABLE_SEARCH_QUERY} />
<Switch bind:state={taskConfig.ENABLE_TAGS_GENERATION} />
</div>
{#if taskConfig.ENABLE_SEARCH_QUERY}
<div class="">
<div class=" mb-2.5 text-xs font-medium">{$i18n.t('Search Query Generation Prompt')}</div>
{#if taskConfig.ENABLE_TAGS_GENERATION}
<div class="mt-3">
<div class=" mb-2.5 text-xs font-medium">{$i18n.t('Tags Generation Prompt')}</div>
<Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
placement="top-start"
>
<Textarea
bind:value={taskConfig.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE}
bind:value={taskConfig.TAGS_GENERATION_PROMPT_TEMPLATE}
placeholder={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt'
)}
@@ -174,6 +162,38 @@
</Tooltip>
</div>
{/if}
<hr class=" dark:border-gray-850 my-3" />
<div class="my-3 flex w-full items-center justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('Enable Retrieval Query Generation')}
</div>
<Switch bind:state={taskConfig.ENABLE_RETRIEVAL_QUERY_GENERATION} />
</div>
<div class="my-3 flex w-full items-center justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('Enable Web Search Query Generation')}
</div>
<Switch bind:state={taskConfig.ENABLE_SEARCH_QUERY_GENERATION} />
</div>
<div class="">
<div class=" mb-2.5 text-xs font-medium">{$i18n.t('Query Generation Prompt')}</div>
<Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
placement="top-start"
>
<Textarea
bind:value={taskConfig.QUERY_GENERATION_PROMPT_TEMPLATE}
placeholder={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
/>
</Tooltip>
</div>
</div>
<hr class=" dark:border-gray-850 my-3" />
File diff suppressed because it is too large Load Diff
@@ -1,214 +0,0 @@
<script lang="ts">
import { getBackendConfig, getModelFilterConfig, updateModelFilterConfig } from '$lib/apis';
import { getSignUpEnabledStatus, toggleSignUpEnabledStatus } from '$lib/apis/auths';
import { getUserPermissions, updateUserPermissions } from '$lib/apis/users';
import { onMount, getContext } from 'svelte';
import { models, config } from '$lib/stores';
import Switch from '$lib/components/common/Switch.svelte';
import { setDefaultModels } from '$lib/apis/configs';
const i18n = getContext('i18n');
export let saveHandler: Function;
let defaultModelId = '';
let whitelistEnabled = false;
let whitelistModels = [''];
let permissions = {
chat: {
deletion: true,
edit: true,
temporary: true
}
};
let chatDeletion = true;
let chatEdit = true;
let chatTemporary = true;
onMount(async () => {
permissions = await getUserPermissions(localStorage.token);
chatDeletion = permissions?.chat?.deletion ?? true;
chatEdit = permissions?.chat?.editing ?? true;
chatTemporary = permissions?.chat?.temporary ?? true;
const res = await getModelFilterConfig(localStorage.token);
if (res) {
whitelistEnabled = res.enabled;
whitelistModels = res.models.length > 0 ? res.models : [''];
}
defaultModelId = $config.default_models ? $config?.default_models.split(',')[0] : '';
});
</script>
<form
class="flex flex-col h-full justify-between space-y-3 text-sm"
on:submit|preventDefault={async () => {
// console.log('submit');
await setDefaultModels(localStorage.token, defaultModelId);
await updateUserPermissions(localStorage.token, {
chat: {
deletion: chatDeletion,
editing: chatEdit,
temporary: chatTemporary
}
});
await updateModelFilterConfig(localStorage.token, whitelistEnabled, whitelistModels);
saveHandler();
await config.set(await getBackendConfig());
}}
>
<div class=" space-y-3 overflow-y-scroll max-h-full">
<div>
<div class=" mb-2 text-sm font-medium">{$i18n.t('User Permissions')}</div>
<div class=" flex w-full justify-between my-2 pr-2">
<div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Deletion')}</div>
<Switch bind:state={chatDeletion} />
</div>
<div class=" flex w-full justify-between my-2 pr-2">
<div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Editing')}</div>
<Switch bind:state={chatEdit} />
</div>
<div class=" flex w-full justify-between my-2 pr-2">
<div class=" self-center text-xs font-medium">{$i18n.t('Allow Temporary Chat')}</div>
<Switch bind:state={chatTemporary} />
</div>
</div>
<hr class=" dark:border-gray-850 my-2" />
<div class="mt-2 space-y-3">
<div>
<div class="mb-2">
<div class="flex justify-between items-center text-xs">
<div class=" text-sm font-medium">{$i18n.t('Manage Models')}</div>
</div>
</div>
<div class=" space-y-1 mb-3">
<div class="mb-2">
<div class="flex justify-between items-center text-xs">
<div class=" text-xs font-medium">{$i18n.t('Default Model')}</div>
</div>
</div>
<div class="flex-1 mr-2">
<select
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={defaultModelId}
placeholder="Select a model"
>
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
{#each $models.filter((model) => model.id) as model}
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option>
{/each}
</select>
</div>
</div>
<div class=" space-y-1">
<div class="mb-2">
<div class="flex justify-between items-center text-xs my-3 pr-2">
<div class=" text-xs font-medium">{$i18n.t('Model Whitelisting')}</div>
<Switch bind:state={whitelistEnabled} />
</div>
</div>
{#if whitelistEnabled}
<div>
<div class=" space-y-1.5">
{#each whitelistModels as modelId, modelIdx}
<div class="flex w-full">
<div class="flex-1 mr-2">
<select
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={modelId}
placeholder="Select a model"
>
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
{#each $models.filter((model) => model.id) as model}
<option value={model.id} class="bg-gray-100 dark:bg-gray-700"
>{model.name}</option
>
{/each}
</select>
</div>
{#if modelIdx === 0}
<button
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-900 dark:text-white rounded-lg transition"
type="button"
on:click={() => {
if (whitelistModels.at(-1) !== '') {
whitelistModels = [...whitelistModels, ''];
}
}}
>
<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-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-900 dark:text-white rounded-lg transition"
type="button"
on:click={() => {
whitelistModels.splice(modelIdx, 1);
whitelistModels = whitelistModels;
}}
>
<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>
{/each}
</div>
<div class="flex justify-end items-center text-xs mt-1.5 text-right">
<div class=" text-xs font-medium">
{whitelistModels.length}
{$i18n.t('Model(s) Whitelisted')}
</div>
</div>
</div>
{/if}
</div>
</div>
</div>
</div>
<div class="flex justify-end pt-3 text-sm font-medium">
<button
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
type="submit"
>
{$i18n.t('Save')}
</button>
</div>
</form>
@@ -23,7 +23,8 @@
'searchapi',
'duckduckgo',
'tavily',
'jina'
'jina',
'bing'
];
let youtubeLanguage = 'en';
@@ -234,6 +235,46 @@
bind:value={webConfig.search.tavily_api_key}
/>
</div>
{:else if webConfig.search.engine === 'jina'}
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Jina API Key')}
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Jina API Key')}
bind:value={webConfig.search.jina_api_key}
/>
</div>
{:else if webConfig.search.engine === 'bing'}
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Bing Search V7 Endpoint')}
</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
type="text"
placeholder={$i18n.t('Enter Bing Search V7 Endpoint')}
bind:value={webConfig.search.bing_search_v7_endpoint}
autocomplete="off"
/>
</div>
</div>
</div>
<div class="mt-2">
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Bing Search V7 Subscription Key')}
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Bing Search V7 Subscription Key')}
bind:value={webConfig.search.bing_search_v7_subscription_key}
/>
</div>
{/if}
</div>
{/if}
@@ -285,12 +326,12 @@
<button
class="p-1 px-3 text-xs flex rounded transition"
on:click={() => {
webConfig.ssl_verification = !webConfig.ssl_verification;
webConfig.web_loader_ssl_verification = !webConfig.web_loader_ssl_verification;
submitHandler();
}}
type="button"
>
{#if webConfig.ssl_verification === true}
{#if webConfig.web_loader_ssl_verification === false}
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('Off')}</span>