Commit ae376ec8 authored by Jun Siang Cheah's avatar Jun Siang Cheah
Browse files

Merge remote-tracking branch 'upstream/dev' into feat/oauth

parents af4f8aa5 1bb7fc7c
...@@ -196,7 +196,7 @@ ...@@ -196,7 +196,7 @@
<div class=" mt-2 mb-1 flex justify-end space-x-1.5 text-sm font-medium"> <div class=" mt-2 mb-1 flex justify-end space-x-1.5 text-sm font-medium">
<button <button
id="close-edit-message-button" id="close-edit-message-button"
class="px-4 py-2 bg-white hover:bg-gray-100 text-gray-800 transition rounded-3xl" class="px-4 py-2 bg-white dark:bg-gray-900 hover:bg-gray-100 text-gray-800 dark:text-gray-100 transition rounded-3xl"
on:click={() => { on:click={() => {
cancelEditMessage(); cancelEditMessage();
}} }}
...@@ -206,7 +206,7 @@ ...@@ -206,7 +206,7 @@
<button <button
id="save-edit-message-button" id="save-edit-message-button"
class=" px-4 py-2 bg-gray-900 hover:bg-gray-850 text-gray-100 transition rounded-3xl" class=" px-4 py-2 bg-gray-900 dark:bg-white hover:bg-gray-850 text-gray-100 dark:text-gray-800 transition rounded-3xl"
on:click={() => { on:click={() => {
editMessageConfirmHandler(); editMessageConfirmHandler();
}} }}
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
import Check from '$lib/components/icons/Check.svelte'; import Check from '$lib/components/icons/Check.svelte';
import Search from '$lib/components/icons/Search.svelte'; import Search from '$lib/components/icons/Search.svelte';
import { cancelOllamaRequest, deleteModel, getOllamaVersion, pullModel } from '$lib/apis/ollama'; import { deleteModel, getOllamaVersion, pullModel } from '$lib/apis/ollama';
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';
...@@ -42,9 +42,16 @@ ...@@ -42,9 +42,16 @@
let searchValue = ''; let searchValue = '';
let ollamaVersion = null; let ollamaVersion = null;
$: filteredItems = searchValue $: filteredItems = items.filter(
? items.filter((item) => item.value.toLowerCase().includes(searchValue.toLowerCase())) (item) =>
: items; (searchValue
? item.value.toLowerCase().includes(searchValue.toLowerCase()) ||
item.label.toLowerCase().includes(searchValue.toLowerCase()) ||
(item.model?.info?.meta?.tags ?? []).some((tag) =>
tag.name.toLowerCase().includes(searchValue.toLowerCase())
)
: true) && !(item.model?.info?.meta?.hidden ?? false)
);
const pullModelHandler = async () => { const pullModelHandler = async () => {
const sanitizedModelTag = searchValue.trim().replace(/^ollama\s+(run|pull)\s+/, ''); const sanitizedModelTag = searchValue.trim().replace(/^ollama\s+(run|pull)\s+/, '');
...@@ -65,10 +72,12 @@ ...@@ -65,10 +72,12 @@
return; return;
} }
const res = await pullModel(localStorage.token, sanitizedModelTag, '0').catch((error) => { const [res, controller] = await pullModel(localStorage.token, sanitizedModelTag, '0').catch(
toast.error(error); (error) => {
return null; toast.error(error);
}); return null;
}
);
if (res) { if (res) {
const reader = res.body const reader = res.body
...@@ -76,6 +85,16 @@ ...@@ -76,6 +85,16 @@
.pipeThrough(splitStream('\n')) .pipeThrough(splitStream('\n'))
.getReader(); .getReader();
MODEL_DOWNLOAD_POOL.set({
...$MODEL_DOWNLOAD_POOL,
[sanitizedModelTag]: {
...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
abortController: controller,
reader,
done: false
}
});
while (true) { while (true) {
try { try {
const { value, done } = await reader.read(); const { value, done } = await reader.read();
...@@ -94,19 +113,6 @@ ...@@ -94,19 +113,6 @@
throw data.detail; throw data.detail;
} }
if (data.id) {
MODEL_DOWNLOAD_POOL.set({
...$MODEL_DOWNLOAD_POOL,
[sanitizedModelTag]: {
...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
requestId: data.id,
reader,
done: false
}
});
console.log(data);
}
if (data.status) { if (data.status) {
if (data.digest) { if (data.digest) {
let downloadProgress = 0; let downloadProgress = 0;
...@@ -174,11 +180,12 @@ ...@@ -174,11 +180,12 @@
}); });
const cancelModelPullHandler = async (model: string) => { const cancelModelPullHandler = async (model: string) => {
const { reader, requestId } = $MODEL_DOWNLOAD_POOL[model]; const { reader, abortController } = $MODEL_DOWNLOAD_POOL[model];
if (abortController) {
abortController.abort();
}
if (reader) { if (reader) {
await reader.cancel(); await reader.cancel();
await cancelOllamaRequest(localStorage.token, requestId);
delete $MODEL_DOWNLOAD_POOL[model]; delete $MODEL_DOWNLOAD_POOL[model];
MODEL_DOWNLOAD_POOL.set({ MODEL_DOWNLOAD_POOL.set({
...$MODEL_DOWNLOAD_POOL ...$MODEL_DOWNLOAD_POOL
...@@ -245,87 +252,113 @@ ...@@ -245,87 +252,113 @@
show = false; show = false;
}} }}
> >
<div class="flex items-center gap-2"> <div class="flex flex-col">
<div class="flex items-center"> {#if $mobile && (item?.model?.info?.meta?.tags ?? []).length > 0}
<div class="line-clamp-1"> <div class="flex gap-0.5 self-start h-full mb-0.5 -translate-x-1">
{item.label} {#each item.model?.info?.meta.tags as tag}
</div> <div
{#if item.model.owned_by === 'ollama' && (item.model.ollama?.details?.parameter_size ?? '') !== ''} class=" text-xs font-black px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
<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" {tag.name}
>{item.model.ollama?.details?.parameter_size ?? ''}</span </div>
{/each}
</div>
{/if}
<div class="flex items-center gap-2">
<div class="flex items-center">
<div class="line-clamp-1">
{item.label}
</div>
{#if item.model.owned_by === 'ollama' && (item.model.ollama?.details?.parameter_size ?? '') !== ''}
<div class="flex ml-1 items-center translate-y-[0.5px]">
<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"
> >
</Tooltip> <span
class=" text-xs font-medium text-gray-600 dark:text-gray-400 line-clamp-1"
>{item.model.ollama?.details?.parameter_size ?? ''}</span
>
</Tooltip>
</div>
{/if}
</div>
{#if !$mobile && (item?.model?.info?.meta?.tags ?? []).length > 0}
<div class="flex gap-0.5 self-center items-center h-full translate-y-[0.5px]">
{#each item.model?.info?.meta.tags as tag}
<div
class=" text-xs font-black px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
>
{tag.name}
</div>
{/each}
</div> </div>
{/if} {/if}
</div>
<!-- {JSON.stringify(item.info)} --> <!-- {JSON.stringify(item.info)} -->
{#if item.model.owned_by === 'openai'} {#if item.model.owned_by === 'openai'}
<Tooltip content={`${'External'}`}> <Tooltip content={`${'External'}`}>
<div class=""> <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"
fill="currentColor" fill="currentColor"
class="size-3" class="size-3"
> >
<path <path
fill-rule="evenodd" fill-rule="evenodd"
d="M8.914 6.025a.75.75 0 0 1 1.06 0 3.5 3.5 0 0 1 0 4.95l-2 2a3.5 3.5 0 0 1-5.396-4.402.75.75 0 0 1 1.251.827 2 2 0 0 0 3.085 2.514l2-2a2 2 0 0 0 0-2.828.75.75 0 0 1 0-1.06Z" d="M8.914 6.025a.75.75 0 0 1 1.06 0 3.5 3.5 0 0 1 0 4.95l-2 2a3.5 3.5 0 0 1-5.396-4.402.75.75 0 0 1 1.251.827 2 2 0 0 0 3.085 2.514l2-2a2 2 0 0 0 0-2.828.75.75 0 0 1 0-1.06Z"
clip-rule="evenodd" clip-rule="evenodd"
/> />
<path <path
fill-rule="evenodd" fill-rule="evenodd"
d="M7.086 9.975a.75.75 0 0 1-1.06 0 3.5 3.5 0 0 1 0-4.95l2-2a3.5 3.5 0 0 1 5.396 4.402.75.75 0 0 1-1.251-.827 2 2 0 0 0-3.085-2.514l-2 2a2 2 0 0 0 0 2.828.75.75 0 0 1 0 1.06Z" d="M7.086 9.975a.75.75 0 0 1-1.06 0 3.5 3.5 0 0 1 0-4.95l2-2a3.5 3.5 0 0 1 5.396 4.402.75.75 0 0 1-1.251-.827 2 2 0 0 0-3.085-2.514l-2 2a2 2 0 0 0 0 2.828.75.75 0 0 1 0 1.06Z"
clip-rule="evenodd" clip-rule="evenodd"
/> />
</svg> </svg>
</div> </div>
</Tooltip> </Tooltip>
{/if} {/if}
{#if item.model?.info?.meta?.description} {#if item.model?.info?.meta?.description}
<Tooltip <Tooltip
content={`${sanitizeResponseContent( content={`${sanitizeResponseContent(
item.model?.info?.meta?.description item.model?.info?.meta?.description
).replaceAll('\n', '<br>')}`} ).replaceAll('\n', '<br>')}`}
> >
<div class=""> <div class="">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
class="w-4 h-4" class="w-4 h-4"
> >
<path <path
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
/> />
</svg> </svg>
</div> </div>
</Tooltip> </Tooltip>
{/if} {/if}
</div>
</div> </div>
{#if value === item.value} {#if value === item.value}
<div class="ml-auto"> <div class="ml-auto pl-2">
<Check /> <Check />
</div> </div>
{/if} {/if}
......
...@@ -20,6 +20,9 @@ ...@@ -20,6 +20,9 @@
tfs_z: '', tfs_z: '',
num_ctx: '', num_ctx: '',
max_tokens: '', max_tokens: '',
use_mmap: null,
use_mlock: null,
num_thread: null,
template: null template: null
}; };
...@@ -379,7 +382,7 @@ ...@@ -379,7 +382,7 @@
<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('Frequencey Penalty')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Frequency Penalty')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
...@@ -559,6 +562,7 @@ ...@@ -559,6 +562,7 @@
</div> </div>
{/if} {/if}
</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 (num_predict)')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Max Tokens (num_predict)')}</div>
...@@ -604,6 +608,93 @@ ...@@ -604,6 +608,93 @@
</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('use_mmap (Ollama)')}</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
type="button"
on:click={() => {
params.use_mmap = (params?.use_mmap ?? null) === null ? true : null;
}}
>
{#if (params?.use_mmap ?? null) === null}
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{/if}
</button>
</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('use_mlock (Ollama)')}</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
type="button"
on:click={() => {
params.use_mlock = (params?.use_mlock ?? null) === null ? true : null;
}}
>
{#if (params?.use_mlock ?? null) === null}
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{/if}
</button>
</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('num_thread (Ollama)')}</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
type="button"
on:click={() => {
params.num_thread = (params?.num_thread ?? null) === null ? 2 : null;
}}
>
{#if (params?.num_thread ?? 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?.num_thread ?? null) !== null}
<div class="flex mt-0.5 space-x-2">
<div class=" flex-1">
<input
id="steps-range"
type="range"
min="1"
max="256"
step="1"
bind:value={params.num_thread}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
</div>
<div class="">
<input
bind:value={params.num_thread}
type="number"
class=" bg-transparent text-center w-14"
min="1"
max="256"
step="1"
/>
</div>
</div>
{/if}
</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('Template')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Template')}</div>
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
import { user, settings } from '$lib/stores'; import { user, settings } from '$lib/stores';
import { createEventDispatcher, onMount, getContext } from 'svelte'; import { createEventDispatcher, onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import Switch from '$lib/components/common/Switch.svelte';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const i18n = getContext('i18n'); const i18n = getContext('i18n');
...@@ -13,6 +14,7 @@ ...@@ -13,6 +14,7 @@
let OpenAIUrl = ''; let OpenAIUrl = '';
let OpenAIKey = ''; let OpenAIKey = '';
let OpenAISpeaker = '';
let STTEngines = ['', 'openai']; let STTEngines = ['', 'openai'];
let STTEngine = ''; let STTEngine = '';
...@@ -20,6 +22,7 @@ ...@@ -20,6 +22,7 @@
let conversationMode = false; let conversationMode = false;
let speechAutoSend = false; let speechAutoSend = false;
let responseAutoPlayback = false; let responseAutoPlayback = false;
let nonLocalVoices = false;
let TTSEngines = ['', 'openai']; let TTSEngines = ['', 'openai'];
let TTSEngine = ''; let TTSEngine = '';
...@@ -86,14 +89,14 @@ ...@@ -86,14 +89,14 @@
url: OpenAIUrl, url: OpenAIUrl,
key: OpenAIKey, key: OpenAIKey,
model: model, model: model,
speaker: speaker speaker: OpenAISpeaker
}); });
if (res) { if (res) {
OpenAIUrl = res.OPENAI_API_BASE_URL; OpenAIUrl = res.OPENAI_API_BASE_URL;
OpenAIKey = res.OPENAI_API_KEY; OpenAIKey = res.OPENAI_API_KEY;
model = res.OPENAI_API_MODEL; model = res.OPENAI_API_MODEL;
speaker = res.OPENAI_API_VOICE; OpenAISpeaker = res.OPENAI_API_VOICE;
} }
} }
}; };
...@@ -105,6 +108,7 @@ ...@@ -105,6 +108,7 @@
STTEngine = $settings?.audio?.STTEngine ?? ''; STTEngine = $settings?.audio?.STTEngine ?? '';
TTSEngine = $settings?.audio?.TTSEngine ?? ''; TTSEngine = $settings?.audio?.TTSEngine ?? '';
nonLocalVoices = $settings.audio?.nonLocalVoices ?? false;
speaker = $settings?.audio?.speaker ?? ''; speaker = $settings?.audio?.speaker ?? '';
model = $settings?.audio?.model ?? ''; model = $settings?.audio?.model ?? '';
...@@ -122,7 +126,10 @@ ...@@ -122,7 +126,10 @@
OpenAIUrl = res.OPENAI_API_BASE_URL; OpenAIUrl = res.OPENAI_API_BASE_URL;
OpenAIKey = res.OPENAI_API_KEY; OpenAIKey = res.OPENAI_API_KEY;
model = res.OPENAI_API_MODEL; model = res.OPENAI_API_MODEL;
speaker = res.OPENAI_API_VOICE; OpenAISpeaker = res.OPENAI_API_VOICE;
if (TTSEngine === 'openai') {
speaker = OpenAISpeaker;
}
} }
} }
}); });
...@@ -138,8 +145,14 @@ ...@@ -138,8 +145,14 @@
audio: { audio: {
STTEngine: STTEngine !== '' ? STTEngine : undefined, STTEngine: STTEngine !== '' ? STTEngine : undefined,
TTSEngine: TTSEngine !== '' ? TTSEngine : undefined, TTSEngine: TTSEngine !== '' ? TTSEngine : undefined,
speaker: speaker !== '' ? speaker : undefined, speaker:
model: model !== '' ? model : undefined (TTSEngine === 'openai' ? OpenAISpeaker : speaker) !== ''
? TTSEngine === 'openai'
? OpenAISpeaker
: speaker
: undefined,
model: model !== '' ? model : undefined,
nonLocalVoices: nonLocalVoices
} }
}); });
dispatch('save'); dispatch('save');
...@@ -227,7 +240,7 @@ ...@@ -227,7 +240,7 @@
on:change={(e) => { on:change={(e) => {
if (e.target.value === 'openai') { if (e.target.value === 'openai') {
getOpenAIVoices(); getOpenAIVoices();
speaker = 'alloy'; OpenAISpeaker = 'alloy';
model = 'tts-1'; model = 'tts-1';
} else { } else {
getWebAPIVoices(); getWebAPIVoices();
...@@ -290,16 +303,27 @@ ...@@ -290,16 +303,27 @@
<select <select
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"
bind:value={speaker} bind:value={speaker}
placeholder="Select a voice"
> >
<option value="" selected>{$i18n.t('Default')}</option> <option value="" selected={speaker !== ''}>{$i18n.t('Default')}</option>
{#each voices.filter((v) => v.localService === true) as voice} {#each voices.filter((v) => nonLocalVoices || v.localService === true) as voice}
<option value={voice.name} class="bg-gray-100 dark:bg-gray-700">{voice.name}</option <option
value={voice.name}
class="bg-gray-100 dark:bg-gray-700"
selected={speaker === voice.name}>{voice.name}</option
> >
{/each} {/each}
</select> </select>
</div> </div>
</div> </div>
<div class="flex items-center justify-between my-1.5">
<div class="text-xs">
{$i18n.t('Allow non-local voices')}
</div>
<div class="mt-1">
<Switch bind:state={nonLocalVoices} />
</div>
</div>
</div> </div>
{:else if TTSEngine === 'openai'} {:else if TTSEngine === 'openai'}
<div> <div>
...@@ -309,7 +333,7 @@ ...@@ -309,7 +333,7 @@
<input <input
list="voice-list" list="voice-list"
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"
bind:value={speaker} bind:value={OpenAISpeaker}
placeholder="Select a voice" placeholder="Select a voice"
/> />
......
<script lang="ts"> <script lang="ts">
import { models, user } from '$lib/stores'; import { models, user } from '$lib/stores';
import { createEventDispatcher, onMount, getContext } from 'svelte'; import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
import { import {
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
import { import {
getOpenAIConfig, getOpenAIConfig,
getOpenAIKeys, getOpenAIKeys,
getOpenAIModels,
getOpenAIUrls, getOpenAIUrls,
updateOpenAIConfig, updateOpenAIConfig,
updateOpenAIKeys, updateOpenAIKeys,
...@@ -21,6 +22,7 @@ ...@@ -21,6 +22,7 @@
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import Switch from '$lib/components/common/Switch.svelte'; import Switch from '$lib/components/common/Switch.svelte';
import Spinner from '$lib/components/common/Spinner.svelte'; import Spinner from '$lib/components/common/Spinner.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
...@@ -32,27 +34,88 @@ ...@@ -32,27 +34,88 @@
let OPENAI_API_KEYS = ['']; let OPENAI_API_KEYS = [''];
let OPENAI_API_BASE_URLS = ['']; let OPENAI_API_BASE_URLS = [''];
let pipelineUrls = {};
let ENABLE_OPENAI_API = null; let ENABLE_OPENAI_API = null;
let ENABLE_OLLAMA_API = null; let ENABLE_OLLAMA_API = null;
const updateOpenAIHandler = async () => { const verifyOpenAIHandler = async (idx) => {
OPENAI_API_BASE_URLS = await updateOpenAIUrls(localStorage.token, OPENAI_API_BASE_URLS); OPENAI_API_BASE_URLS = await updateOpenAIUrls(localStorage.token, OPENAI_API_BASE_URLS);
OPENAI_API_KEYS = await updateOpenAIKeys(localStorage.token, OPENAI_API_KEYS); 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()); await models.set(await getModels());
}; };
const updateOllamaUrlsHandler = async () => { const verifyOllamaHandler = async (idx) => {
OLLAMA_BASE_URLS = await updateOllamaUrls(localStorage.token, OLLAMA_BASE_URLS); OLLAMA_BASE_URLS = await updateOllamaUrls(localStorage.token, OLLAMA_BASE_URLS);
const ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => { const res = await getOllamaVersion(localStorage.token, idx).catch((error) => {
toast.error(error); toast.error(error);
return null; return null;
}); });
if (ollamaVersion) { if (res) {
toast.success($i18n.t('Server connection verified')); toast.success($i18n.t('Server connection verified'));
await models.set(await getModels()); }
await models.set(await getModels());
};
const updateOpenAIHandler = async () => {
// 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('');
}
}
}
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 !== '');
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) => {
toast.error(error);
return null;
});
if (ollamaVersion) {
toast.success($i18n.t('Server connection verified'));
await models.set(await getModels());
}
} }
}; };
...@@ -70,6 +133,13 @@ ...@@ -70,6 +133,13 @@
})() })()
]); ]);
OPENAI_API_BASE_URLS.forEach(async (url, idx) => {
const res = await getOpenAIModels(localStorage.token, idx);
if (res.pipelines) {
pipelineUrls[url] = true;
}
});
const ollamaConfig = await getOllamaConfig(localStorage.token); const ollamaConfig = await getOllamaConfig(localStorage.token);
const openaiConfig = await getOpenAIConfig(localStorage.token); const openaiConfig = await getOpenAIConfig(localStorage.token);
...@@ -83,6 +153,8 @@ ...@@ -83,6 +153,8 @@
class="flex flex-col h-full justify-between text-sm" class="flex flex-col h-full justify-between text-sm"
on:submit|preventDefault={() => { on:submit|preventDefault={() => {
updateOpenAIHandler(); updateOpenAIHandler();
updateOllamaUrlsHandler();
dispatch('save'); dispatch('save');
}} }}
> >
...@@ -107,13 +179,38 @@ ...@@ -107,13 +179,38 @@
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
{#each OPENAI_API_BASE_URLS as url, idx} {#each OPENAI_API_BASE_URLS as url, idx}
<div class="flex w-full gap-2"> <div class="flex w-full gap-2">
<div class="flex-1"> <div class="flex-1 relative">
<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 {pipelineUrls[url]
? 'pr-8'
: ''} text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Base URL')} placeholder={$i18n.t('API Base URL')}
bind:value={url} bind:value={url}
autocomplete="off" autocomplete="off"
/> />
{#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>
<div class="flex-1"> <div class="flex-1">
...@@ -167,6 +264,31 @@ ...@@ -167,6 +264,31 @@
</button> </button>
{/if} {/if}
</div> </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>
<div class=" mb-1 text-xs text-gray-400 dark:text-gray-500"> <div class=" mb-1 text-xs text-gray-400 dark:text-gray-500">
{$i18n.t('WebUI will make requests to')} {$i18n.t('WebUI will make requests to')}
...@@ -189,6 +311,10 @@ ...@@ -189,6 +311,10 @@
bind:state={ENABLE_OLLAMA_API} bind:state={ENABLE_OLLAMA_API}
on:change={async () => { on:change={async () => {
updateOllamaConfig(localStorage.token, ENABLE_OLLAMA_API); updateOllamaConfig(localStorage.token, ENABLE_OLLAMA_API);
if (OLLAMA_BASE_URLS.length === 0) {
OLLAMA_BASE_URLS = [''];
}
}} }}
/> />
</div> </div>
...@@ -245,32 +371,34 @@ ...@@ -245,32 +371,34 @@
</button> </button>
{/if} {/if}
</div> </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> </div>
{/each} {/each}
</div> </div>
<div class="flex">
<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={() => {
updateOllamaUrlsHandler();
}}
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>
</div>
</div> </div>
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500"> <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import { config, models, settings, user } from '$lib/stores'; import { config, models, settings, user } from '$lib/stores';
import { createEventDispatcher, onMount, getContext } from 'svelte'; import { createEventDispatcher, onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import Tooltip from '$lib/components/common/Tooltip.svelte';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const i18n = getContext('i18n'); const i18n = getContext('i18n');
...@@ -15,11 +16,12 @@ ...@@ -15,11 +16,12 @@
let responseAutoCopy = false; let responseAutoCopy = false;
let titleAutoGenerateModel = ''; let titleAutoGenerateModel = '';
let titleAutoGenerateModelExternal = ''; let titleAutoGenerateModelExternal = '';
let fullScreenMode = false; let widescreenMode = false;
let titleGenerationPrompt = ''; let titleGenerationPrompt = '';
let splitLargeChunks = false; let splitLargeChunks = false;
// Interface // Interface
let defaultModelId = '';
let promptSuggestions = []; let promptSuggestions = [];
let showUsername = false; let showUsername = false;
let chatBubble = true; let chatBubble = true;
...@@ -30,9 +32,9 @@ ...@@ -30,9 +32,9 @@
saveSettings({ splitLargeChunks: splitLargeChunks }); saveSettings({ splitLargeChunks: splitLargeChunks });
}; };
const toggleFullScreenMode = async () => { const togglewidescreenMode = async () => {
fullScreenMode = !fullScreenMode; widescreenMode = !widescreenMode;
saveSettings({ fullScreenMode: fullScreenMode }); saveSettings({ widescreenMode: widescreenMode });
}; };
const toggleChatBubble = async () => { const toggleChatBubble = async () => {
...@@ -95,7 +97,8 @@ ...@@ -95,7 +97,8 @@
modelExternal: modelExternal:
titleAutoGenerateModelExternal !== '' ? titleAutoGenerateModelExternal : undefined, titleAutoGenerateModelExternal !== '' ? titleAutoGenerateModelExternal : undefined,
prompt: titleGenerationPrompt ? titleGenerationPrompt : undefined prompt: titleGenerationPrompt ? titleGenerationPrompt : undefined
} },
models: [defaultModelId]
}); });
}; };
...@@ -113,9 +116,11 @@ ...@@ -113,9 +116,11 @@
responseAutoCopy = $settings.responseAutoCopy ?? false; responseAutoCopy = $settings.responseAutoCopy ?? false;
showUsername = $settings.showUsername ?? false; showUsername = $settings.showUsername ?? false;
chatBubble = $settings.chatBubble ?? true; chatBubble = $settings.chatBubble ?? true;
fullScreenMode = $settings.fullScreenMode ?? false; widescreenMode = $settings.widescreenMode ?? false;
splitLargeChunks = $settings.splitLargeChunks ?? false; splitLargeChunks = $settings.splitLargeChunks ?? false;
chatDirection = $settings.chatDirection ?? 'LTR'; chatDirection = $settings.chatDirection ?? 'LTR';
defaultModelId = ($settings?.models ?? ['']).at(0);
}); });
</script> </script>
...@@ -194,16 +199,16 @@ ...@@ -194,16 +199,16 @@
<div> <div>
<div class=" py-0.5 flex w-full justify-between"> <div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Full Screen Mode')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Widescreen Mode')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
on:click={() => { on:click={() => {
toggleFullScreenMode(); togglewidescreenMode();
}} }}
type="button" type="button"
> >
{#if fullScreenMode === true} {#if widescreenMode === true}
<span class="ml-2 self-center">{$i18n.t('On')}</span> <span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else} {:else}
<span class="ml-2 self-center">{$i18n.t('Off')}</span> <span class="ml-2 self-center">{$i18n.t('Off')}</span>
...@@ -277,10 +282,55 @@ ...@@ -277,10 +282,55 @@
</div> </div>
</div> </div>
<hr class=" dark:border-gray-700" /> <hr class=" dark:border-gray-850" />
<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 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>
<hr class=" dark:border-gray-850" />
<div> <div>
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Title Auto-Generation Model')}</div> <div class=" mb-2.5 text-sm font-medium flex">
<div class=" mr-1">{$i18n.t('Set Task Model')}</div>
<Tooltip
content={$i18n.t(
'A task model is used when performing tasks such as generating titles for chats and web search queries'
)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
/>
</svg>
</Tooltip>
</div>
<div class="flex w-full gap-2 pr-2"> <div class="flex w-full gap-2 pr-2">
<div class="flex-1"> <div class="flex-1">
<div class=" text-xs mb-1">Local Models</div> <div class=" text-xs mb-1">Local Models</div>
...@@ -290,12 +340,10 @@ ...@@ -290,12 +340,10 @@
placeholder={$i18n.t('Select a model')} placeholder={$i18n.t('Select a model')}
> >
<option value="" selected>{$i18n.t('Current Model')}</option> <option value="" selected>{$i18n.t('Current Model')}</option>
{#each $models as model} {#each $models.filter((m) => m.owned_by === 'ollama') as model}
{#if model.size != null} <option value={model.id} class="bg-gray-100 dark:bg-gray-700">
<option value={model.name} class="bg-gray-100 dark:bg-gray-700"> {model.name}
{model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'} </option>
</option>
{/if}
{/each} {/each}
</select> </select>
</div> </div>
...@@ -309,11 +357,9 @@ ...@@ -309,11 +357,9 @@
> >
<option value="" selected>{$i18n.t('Current Model')}</option> <option value="" selected>{$i18n.t('Current Model')}</option>
{#each $models as model} {#each $models as model}
{#if model.name !== 'hr'} <option value={model.id} class="bg-gray-100 dark:bg-gray-700">
<option value={model.name} class="bg-gray-100 dark:bg-gray-700"> {model.name}
{model.name} </option>
</option>
{/if}
{/each} {/each}
</select> </select>
</div> </div>
......
...@@ -8,8 +8,8 @@ ...@@ -8,8 +8,8 @@
getOllamaUrls, getOllamaUrls,
getOllamaVersion, getOllamaVersion,
pullModel, pullModel,
cancelOllamaRequest, uploadModel,
uploadModel getOllamaConfig
} 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';
...@@ -28,6 +28,8 @@ ...@@ -28,6 +28,8 @@
// Models // Models
let ollamaEnabled = null;
let OLLAMA_URLS = []; let OLLAMA_URLS = [];
let selectedOllamaUrlIdx: string | null = null; let selectedOllamaUrlIdx: string | null = null;
...@@ -41,6 +43,13 @@ ...@@ -41,6 +43,13 @@
let modelTransferring = false; let modelTransferring = false;
let modelTag = ''; let modelTag = '';
let createModelLoading = false;
let createModelTag = '';
let createModelContent = '';
let createModelDigest = '';
let createModelPullProgress = null;
let digest = ''; let digest = '';
let pullProgress = null; let pullProgress = null;
...@@ -67,12 +76,14 @@ ...@@ -67,12 +76,14 @@
console.log(model); console.log(model);
updateModelId = model.id; updateModelId = model.id;
const res = await pullModel(localStorage.token, model.id, selectedOllamaUrlIdx).catch( const [res, controller] = await pullModel(
(error) => { localStorage.token,
toast.error(error); model.id,
return null; selectedOllamaUrlIdx
} ).catch((error) => {
); toast.error(error);
return null;
});
if (res) { if (res) {
const reader = res.body const reader = res.body
...@@ -141,10 +152,12 @@ ...@@ -141,10 +152,12 @@
return; return;
} }
const res = await pullModel(localStorage.token, sanitizedModelTag, '0').catch((error) => { const [res, controller] = await pullModel(localStorage.token, sanitizedModelTag, '0').catch(
toast.error(error); (error) => {
return null; toast.error(error);
}); return null;
}
);
if (res) { if (res) {
const reader = res.body const reader = res.body
...@@ -152,6 +165,16 @@ ...@@ -152,6 +165,16 @@
.pipeThrough(splitStream('\n')) .pipeThrough(splitStream('\n'))
.getReader(); .getReader();
MODEL_DOWNLOAD_POOL.set({
...$MODEL_DOWNLOAD_POOL,
[sanitizedModelTag]: {
...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
abortController: controller,
reader,
done: false
}
});
while (true) { while (true) {
try { try {
const { value, done } = await reader.read(); const { value, done } = await reader.read();
...@@ -170,19 +193,6 @@ ...@@ -170,19 +193,6 @@
throw data.detail; throw data.detail;
} }
if (data.id) {
MODEL_DOWNLOAD_POOL.set({
...$MODEL_DOWNLOAD_POOL,
[sanitizedModelTag]: {
...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
requestId: data.id,
reader,
done: false
}
});
console.log(data);
}
if (data.status) { if (data.status) {
if (data.digest) { if (data.digest) {
let downloadProgress = 0; let downloadProgress = 0;
...@@ -416,11 +426,12 @@ ...@@ -416,11 +426,12 @@
}; };
const cancelModelPullHandler = async (model: string) => { const cancelModelPullHandler = async (model: string) => {
const { reader, requestId } = $MODEL_DOWNLOAD_POOL[model]; const { reader, abortController } = $MODEL_DOWNLOAD_POOL[model];
if (abortController) {
abortController.abort();
}
if (reader) { if (reader) {
await reader.cancel(); await reader.cancel();
await cancelOllamaRequest(localStorage.token, requestId);
delete $MODEL_DOWNLOAD_POOL[model]; delete $MODEL_DOWNLOAD_POOL[model];
MODEL_DOWNLOAD_POOL.set({ MODEL_DOWNLOAD_POOL.set({
...$MODEL_DOWNLOAD_POOL ...$MODEL_DOWNLOAD_POOL
...@@ -430,54 +441,216 @@ ...@@ -430,54 +441,216 @@
} }
}; };
onMount(async () => { const createModelHandler = async () => {
await Promise.all([ createModelLoading = true;
(async () => { const res = await createModel(
OLLAMA_URLS = await getOllamaUrls(localStorage.token).catch((error) => { localStorage.token,
toast.error(error); createModelTag,
return []; createModelContent,
}); selectedOllamaUrlIdx
).catch((error) => {
toast.error(error);
return null;
});
if (res && res.ok) {
const reader = res.body
.pipeThrough(new TextDecoderStream())
.pipeThrough(splitStream('\n'))
.getReader();
while (true) {
const { value, done } = await reader.read();
if (done) break;
try {
let lines = value.split('\n');
for (const line of lines) {
if (line !== '') {
console.log(line);
let data = JSON.parse(line);
console.log(data);
if (OLLAMA_URLS.length > 0) { if (data.error) {
selectedOllamaUrlIdx = 0; throw data.error;
}
if (data.detail) {
throw data.detail;
}
if (data.status) {
if (
!data.digest &&
!data.status.includes('writing') &&
!data.status.includes('sha256')
) {
toast.success(data.status);
} else {
if (data.digest) {
createModelDigest = data.digest;
if (data.completed) {
createModelPullProgress =
Math.round((data.completed / data.total) * 1000) / 10;
} else {
createModelPullProgress = 100;
}
}
}
}
}
}
} catch (error) {
console.log(error);
toast.error(error);
} }
})(), }
(async () => { }
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
})() models.set(await getModels());
]);
createModelLoading = false;
createModelTag = '';
createModelContent = '';
createModelDigest = '';
createModelPullProgress = null;
};
onMount(async () => {
const ollamaConfig = await getOllamaConfig(localStorage.token);
if (ollamaConfig.ENABLE_OLLAMA_API) {
ollamaEnabled = true;
await Promise.all([
(async () => {
OLLAMA_URLS = await getOllamaUrls(localStorage.token).catch((error) => {
toast.error(error);
return [];
});
if (OLLAMA_URLS.length > 0) {
selectedOllamaUrlIdx = 0;
}
})(),
(async () => {
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
})()
]);
} else {
ollamaEnabled = false;
toast.error('Ollama API is disabled');
}
}); });
</script> </script>
<div class="flex flex-col h-full justify-between text-sm"> <div class="flex flex-col h-full justify-between text-sm">
<div class=" space-y-3 pr-1.5 overflow-y-scroll h-[24rem]"> <div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[27rem]">
{#if ollamaVersion !== null} {#if ollamaEnabled}
<div class="space-y-2 pr-1.5"> {#if ollamaVersion !== null}
<div class="text-sm font-medium">{$i18n.t('Manage Ollama Models')}</div> <div class="space-y-2 pr-1.5">
<div class="text-sm font-medium">{$i18n.t('Manage Ollama Models')}</div>
{#if OLLAMA_URLS.length > 0}
<div class="flex gap-2"> {#if OLLAMA_URLS.length > 0}
<div class="flex-1 pb-1"> <div class="flex gap-2">
<select <div class="flex-1 pb-1">
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" <select
bind:value={selectedOllamaUrlIdx} class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Select an Ollama instance')} bind:value={selectedOllamaUrlIdx}
> placeholder={$i18n.t('Select an Ollama instance')}
{#each OLLAMA_URLS as url, idx} >
<option value={idx} class="bg-gray-100 dark:bg-gray-700">{url}</option> {#each OLLAMA_URLS as url, idx}
{/each} <option value={idx} class="bg-gray-100 dark:bg-gray-700">{url}</option>
</select> {/each}
</select>
</div>
<div>
<div class="flex w-full justify-end">
<Tooltip content="Update All Models" placement="top">
<button
class="p-2.5 flex gap-2 items-center 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={() => {
updateModelsHandler();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M7 1a.75.75 0 0 1 .75.75V6h-1.5V1.75A.75.75 0 0 1 7 1ZM6.25 6v2.94L5.03 7.72a.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.06L7.75 8.94V6H10a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2.25Z"
/>
<path
d="M4.268 14A2 2 0 0 0 6 15h6a2 2 0 0 0 2-2v-3a2 2 0 0 0-1-1.732V11a3 3 0 0 1-3 3H4.268Z"
/>
</svg>
</button>
</Tooltip>
</div>
</div>
</div> </div>
{#if updateModelId}
Updating "{updateModelId}" {updateProgress ? `(${updateProgress}%)` : ''}
{/if}
{/if}
<div class="space-y-2">
<div> <div>
<div class="flex w-full justify-end"> <div class=" mb-2 text-sm font-medium">{$i18n.t('Pull a model from Ollama.com')}</div>
<Tooltip content="Update All Models" placement="top"> <div class="flex w-full">
<button <div class="flex-1 mr-2">
class="p-2.5 flex gap-2 items-center 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" <input
on:click={() => { class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
updateModelsHandler(); placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
}} modelTag: 'mistral:7b'
> })}
bind:value={modelTag}
/>
</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={() => {
pullModelHandler();
}}
disabled={modelTransferring}
>
{#if modelTransferring}
<div class="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>
{:else}
<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"
...@@ -485,74 +658,111 @@ ...@@ -485,74 +658,111 @@
class="w-4 h-4" class="w-4 h-4"
> >
<path <path
d="M7 1a.75.75 0 0 1 .75.75V6h-1.5V1.75A.75.75 0 0 1 7 1ZM6.25 6v2.94L5.03 7.72a.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.06L7.75 8.94V6H10a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2.25Z" d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z"
/> />
<path <path
d="M4.268 14A2 2 0 0 0 6 15h6a2 2 0 0 0 2-2v-3a2 2 0 0 0-1-1.732V11a3 3 0 0 1-3 3H4.268Z" d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
/> />
</svg> </svg>
</button> {/if}
</Tooltip> </button>
</div> </div>
</div>
</div>
{#if updateModelId} <div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
Updating "{updateModelId}" {updateProgress ? `(${updateProgress}%)` : ''} {$i18n.t('To access the available model names for downloading,')}
{/if} <a
{/if} class=" text-gray-500 dark:text-gray-300 font-medium underline"
href="https://ollama.com/library"
<div class="space-y-2"> target="_blank">{$i18n.t('click here.')}</a
<div> >
<div class=" mb-2 text-sm font-medium">{$i18n.t('Pull a model from Ollama.com')}</div>
<div class="flex w-full">
<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 model tag (e.g. {{modelTag}})', {
modelTag: 'mistral:7b'
})}
bind:value={modelTag}
/>
</div> </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" {#if Object.keys($MODEL_DOWNLOAD_POOL).length > 0}
on:click={() => { {#each Object.keys($MODEL_DOWNLOAD_POOL) as model}
pullModelHandler(); {#if 'pullProgress' in $MODEL_DOWNLOAD_POOL[model]}
}} <div class="flex flex-col">
disabled={modelTransferring} <div class="font-medium mb-1">{model}</div>
> <div class="">
{#if modelTransferring} <div class="flex flex-row justify-between space-x-4 pr-2">
<div class="self-center"> <div class=" flex-1">
<svg <div
class=" w-4 h-4" class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
viewBox="0 0 24 24" style="width: {Math.max(
fill="currentColor" 15,
xmlns="http://www.w3.org/2000/svg" $MODEL_DOWNLOAD_POOL[model].pullProgress ?? 0
> )}%"
<style> >
.spinner_ajPY { {$MODEL_DOWNLOAD_POOL[model].pullProgress ?? 0}%
transform-origin: center; </div>
animation: spinner_AtaB 0.75s infinite linear; </div>
}
<Tooltip content={$i18n.t('Cancel')}>
@keyframes spinner_AtaB { <button
100% { class="text-gray-800 dark:text-gray-100"
transform: rotate(360deg); on:click={() => {
} cancelModelPullHandler(model);
} }}
</style> >
<path <svg
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" class="w-4 h-4 text-gray-800 dark:text-white"
opacity=".25" aria-hidden="true"
/> xmlns="http://www.w3.org/2000/svg"
<path width="24"
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" height="24"
class="spinner_ajPY" fill="currentColor"
/> viewBox="0 0 24 24"
</svg> >
</div> <path
{:else} stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18 17.94 6M18 18 6.06 6"
/>
</svg>
</button>
</Tooltip>
</div>
{#if 'digest' in $MODEL_DOWNLOAD_POOL[model]}
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
{$MODEL_DOWNLOAD_POOL[model].digest}
</div>
{/if}
</div>
</div>
{/if}
{/each}
{/if}
</div>
<div>
<div class=" mb-2 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={deleteModelTag}
placeholder={$i18n.t('Select a model')}
>
{#if !deleteModelTag}
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
{/if}
{#each $models.filter((m) => !(m?.preset ?? false) && m.owned_by === 'ollama' && (selectedOllamaUrlIdx === null ? true : (m?.ollama?.urls ?? []).includes(selectedOllamaUrlIdx))) as model}
<option value={model.name} class="bg-gray-100 dark:bg-gray-700"
>{model.name +
' (' +
(model.ollama.size / 1024 ** 3).toFixed(1) +
' GB)'}</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={() => {
deleteModelHandler();
}}
>
<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"
...@@ -560,330 +770,300 @@ ...@@ -560,330 +770,300 @@
class="w-4 h-4" class="w-4 h-4"
> >
<path <path
d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z" 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"
<path clip-rule="evenodd"
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
/> />
</svg> </svg>
{/if} </button>
</button> </div>
</div> </div>
<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500"> <div>
{$i18n.t('To access the available model names for downloading,')} <div class=" mb-2 text-sm font-medium">{$i18n.t('Create a model')}</div>
<a <div class="flex w-full">
class=" text-gray-500 dark:text-gray-300 font-medium underline" <div class="flex-1 mr-2 flex flex-col gap-2">
href="https://ollama.com/library" <input
target="_blank">{$i18n.t('click here.')}</a 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 model tag (e.g. {{modelTag}})', {
</div> modelTag: 'my-modelfile'
})}
bind:value={createModelTag}
disabled={createModelLoading}
/>
{#if Object.keys($MODEL_DOWNLOAD_POOL).length > 0} <textarea
{#each Object.keys($MODEL_DOWNLOAD_POOL) as model} bind:value={createModelContent}
{#if 'pullProgress' in $MODEL_DOWNLOAD_POOL[model]} class="w-full rounded-lg py-2 px-4 text-sm bg-gray-100 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none scrollbar-hidden"
<div class="flex flex-col"> rows="6"
<div class="font-medium mb-1">{model}</div> placeholder={`TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSISTANT:"`}
<div class=""> disabled={createModelLoading}
<div class="flex flex-row justify-between space-x-4 pr-2"> />
<div class=" flex-1"> </div>
<div
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
style="width: {Math.max(
15,
$MODEL_DOWNLOAD_POOL[model].pullProgress ?? 0
)}%"
>
{$MODEL_DOWNLOAD_POOL[model].pullProgress ?? 0}%
</div>
</div>
<Tooltip content={$i18n.t('Cancel')}> <div class="flex self-start">
<button <button
class="text-gray-800 dark:text-gray-100" class="px-2.5 py-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 disabled:cursor-not-allowed"
on:click={() => { on:click={() => {
cancelModelPullHandler(model); createModelHandler();
}} }}
> disabled={createModelLoading}
<svg >
class="w-4 h-4 text-gray-800 dark:text-white" <svg
aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"
width="24" fill="currentColor"
height="24" class="size-4"
fill="currentColor" >
viewBox="0 0 24 24" <path
> d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z"
<path />
stroke="currentColor" <path
stroke-linecap="round" d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
stroke-linejoin="round" />
stroke-width="2" </svg>
d="M6 18 17.94 6M18 18 6.06 6" </button>
/> </div>
</svg> </div>
</button>
</Tooltip> {#if createModelDigest !== ''}
</div> <div class="flex flex-col mt-1">
{#if 'digest' in $MODEL_DOWNLOAD_POOL[model]} <div class="font-medium mb-1">{createModelTag}</div>
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;"> <div class="">
{$MODEL_DOWNLOAD_POOL[model].digest} <div class="flex flex-row justify-between space-x-4 pr-2">
<div class=" flex-1">
<div
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
style="width: {Math.max(15, createModelPullProgress ?? 0)}%"
>
{createModelPullProgress ?? 0}%
</div> </div>
{/if} </div>
</div> </div>
{#if createModelDigest}
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
{createModelDigest}
</div>
{/if}
</div> </div>
{/if} </div>
{/each} {/if}
{/if} </div>
</div>
<div> <div class="pt-1">
<div class=" mb-2 text-sm font-medium">{$i18n.t('Delete a model')}</div> <div class="flex justify-between items-center text-xs">
<div class="flex w-full"> <div class=" text-sm font-medium">{$i18n.t('Experimental')}</div>
<div class="flex-1 mr-2"> <button
<select class=" text-xs font-medium text-gray-500"
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" type="button"
bind:value={deleteModelTag} on:click={() => {
placeholder={$i18n.t('Select a model')} showExperimentalOllama = !showExperimentalOllama;
}}>{showExperimentalOllama ? $i18n.t('Hide') : $i18n.t('Show')}</button
> >
{#if !deleteModelTag}
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
{/if}
{#each $models.filter((m) => !(m?.preset ?? false) && m.owned_by === 'ollama' && (selectedOllamaUrlIdx === null ? true : (m?.ollama?.urls ?? []).includes(selectedOllamaUrlIdx))) as model}
<option value={model.name} class="bg-gray-100 dark:bg-gray-700"
>{model.name +
' (' +
(model.ollama.size / 1024 ** 3).toFixed(1) +
' GB)'}</option
>
{/each}
</select>
</div> </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={() => {
deleteModelHandler();
}}
>
<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>
</div>
<div class="pt-1"> {#if showExperimentalOllama}
<div class="flex justify-between items-center text-xs"> <form
<div class=" text-sm font-medium">{$i18n.t('Experimental')}</div> on:submit|preventDefault={() => {
<button uploadModelHandler();
class=" text-xs font-medium text-gray-500" }}
type="button"
on:click={() => {
showExperimentalOllama = !showExperimentalOllama;
}}>{showExperimentalOllama ? $i18n.t('Hide') : $i18n.t('Show')}</button
> >
</div> <div class=" mb-2 flex w-full justify-between">
</div> <div class=" text-sm font-medium">{$i18n.t('Upload a GGUF model')}</div>
{#if showExperimentalOllama} <button
<form class="p-1 px-3 text-xs flex rounded transition"
on:submit|preventDefault={() => { on:click={() => {
uploadModelHandler(); if (modelUploadMode === 'file') {
}} modelUploadMode = 'url';
> } else {
<div class=" mb-2 flex w-full justify-between"> modelUploadMode = 'file';
<div class=" text-sm font-medium">{$i18n.t('Upload a GGUF model')}</div> }
}}
type="button"
>
{#if modelUploadMode === 'file'}
<span class="ml-2 self-center">{$i18n.t('File Mode')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('URL Mode')}</span>
{/if}
</button>
</div>
<button <div class="flex w-full mb-1.5">
class="p-1 px-3 text-xs flex rounded transition" <div class="flex flex-col w-full">
on:click={() => { {#if modelUploadMode === 'file'}
if (modelUploadMode === 'file') { <div
modelUploadMode = 'url'; class="flex-1 {modelInputFile && modelInputFile.length > 0 ? 'mr-2' : ''}"
} else { >
modelUploadMode = 'file'; <input
} id="model-upload-input"
}} bind:this={modelUploadInputElement}
type="button" type="file"
> bind:files={modelInputFile}
{#if modelUploadMode === 'file'} on:change={() => {
<span class="ml-2 self-center">{$i18n.t('File Mode')}</span> console.log(modelInputFile);
{:else} }}
<span class="ml-2 self-center">{$i18n.t('URL Mode')}</span> accept=".gguf,.safetensors"
{/if} required
</button> hidden
</div> />
<div class="flex w-full mb-1.5"> <button
<div class="flex flex-col w-full"> type="button"
{#if modelUploadMode === 'file'} class="w-full rounded-lg text-left py-2 px-4 bg-white dark:text-gray-300 dark:bg-gray-850"
<div class="flex-1 {modelInputFile && modelInputFile.length > 0 ? 'mr-2' : ''}"> on:click={() => {
<input modelUploadInputElement.click();
id="model-upload-input" }}
bind:this={modelUploadInputElement} >
type="file" {#if modelInputFile && modelInputFile.length > 0}
bind:files={modelInputFile} {modelInputFile[0].name}
on:change={() => { {:else}
console.log(modelInputFile); {$i18n.t('Click here to select')}
}} {/if}
accept=".gguf,.safetensors" </button>
required </div>
hidden {:else}
/> <div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
<input
class="w-full rounded-lg text-left py-2 px-4 bg-white dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !==
''
? 'mr-2'
: ''}"
type="url"
required
bind:value={modelFileUrl}
placeholder={$i18n.t('Type Hugging Face Resolve (Download) URL')}
/>
</div>
{/if}
</div>
<button {#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
type="button" <button
class="w-full rounded-lg text-left py-2 px-4 bg-white dark:text-gray-300 dark:bg-gray-850" 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 disabled:cursor-not-allowed transition"
on:click={() => { type="submit"
modelUploadInputElement.click(); disabled={modelTransferring}
}} >
> {#if modelTransferring}
{#if modelInputFile && modelInputFile.length > 0} <div class="self-center">
{modelInputFile[0].name} <svg
{:else} class=" w-4 h-4"
{$i18n.t('Click here to select')} viewBox="0 0 24 24"
{/if} fill="currentColor"
</button> xmlns="http://www.w3.org/2000/svg"
</div> >
{:else} <style>
<div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}"> .spinner_ajPY {
<input transform-origin: center;
class="w-full rounded-lg text-left py-2 px-4 bg-white dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !== animation: spinner_AtaB 0.75s infinite linear;
'' }
? 'mr-2'
: ''}"
type="url"
required
bind:value={modelFileUrl}
placeholder={$i18n.t('Type Hugging Face Resolve (Download) URL')}
/>
</div>
{/if}
</div>
{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')} @keyframes spinner_AtaB {
<button 100% {
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 disabled:cursor-not-allowed transition" transform: rotate(360deg);
type="submit" }
disabled={modelTransferring} }
> </style>
{#if modelTransferring} <path
<div class="self-center"> 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>
{:else}
<svg <svg
class=" w-4 h-4"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
> >
<style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
}
}
</style>
<path <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="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z"
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="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
class="spinner_ajPY"
/> />
</svg> </svg>
</div> {/if}
{:else} </button>
<svg {/if}
xmlns="http://www.w3.org/2000/svg" </div>
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z"
/>
<path
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
/>
</svg>
{/if}
</button>
{/if}
</div>
{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')} {#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
<div>
<div> <div>
<div class=" my-2.5 text-sm font-medium">{$i18n.t('Modelfile Content')}</div> <div>
<textarea <div class=" my-2.5 text-sm font-medium">{$i18n.t('Modelfile Content')}</div>
bind:value={modelFileContent} <textarea
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-100 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none" bind:value={modelFileContent}
rows="6" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-100 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none"
/> rows="6"
/>
</div>
</div> </div>
{/if}
<div class=" mt-1 text-xs text-gray-400 dark:text-gray-500">
{$i18n.t('To access the GGUF models available for downloading,')}
<a
class=" text-gray-500 dark:text-gray-300 font-medium underline"
href="https://huggingface.co/models?search=gguf"
target="_blank">{$i18n.t('click here.')}</a
>
</div> </div>
{/if}
<div class=" mt-1 text-xs text-gray-400 dark:text-gray-500">
{$i18n.t('To access the GGUF models available for downloading,')}
<a
class=" text-gray-500 dark:text-gray-300 font-medium underline"
href="https://huggingface.co/models?search=gguf"
target="_blank">{$i18n.t('click here.')}</a
>
</div>
{#if uploadMessage} {#if uploadMessage}
<div class="mt-2"> <div class="mt-2">
<div class=" mb-2 text-xs">{$i18n.t('Upload Progress')}</div> <div class=" mb-2 text-xs">{$i18n.t('Upload Progress')}</div>
<div class="w-full rounded-full dark:bg-gray-800"> <div class="w-full rounded-full dark:bg-gray-800">
<div <div
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full" class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
style="width: 100%" style="width: 100%"
> >
{uploadMessage} {uploadMessage}
</div>
</div> </div>
</div> <div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;"> {modelFileDigest}
{modelFileDigest}
</div>
</div>
{:else if uploadProgress !== null}
<div class="mt-2">
<div class=" mb-2 text-xs">{$i18n.t('Upload Progress')}</div>
<div class="w-full rounded-full dark:bg-gray-800">
<div
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
style="width: {Math.max(15, uploadProgress ?? 0)}%"
>
{uploadProgress ?? 0}%
</div> </div>
</div> </div>
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;"> {:else if uploadProgress !== null}
{modelFileDigest} <div class="mt-2">
<div class=" mb-2 text-xs">{$i18n.t('Upload Progress')}</div>
<div class="w-full rounded-full dark:bg-gray-800">
<div
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
style="width: {Math.max(15, uploadProgress ?? 0)}%"
>
{uploadProgress ?? 0}%
</div>
</div>
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
{modelFileDigest}
</div>
</div> </div>
</div> {/if}
{/if} </form>
</form> {/if}
{/if} </div>
</div> </div>
</div> {:else if ollamaVersion === false}
{:else if ollamaVersion === false} <div>Ollama Not Detected</div>
<div>Ollama Not Detected</div> {:else}
<div class="flex h-full justify-center">
<div class="my-auto">
<Spinner className="size-6" />
</div>
</div>
{/if}
{:else if ollamaEnabled === false}
<div>Ollama API is disabled</div>
{:else} {:else}
<div class="flex h-full justify-center"> <div class="flex h-full justify-center">
<div class="my-auto"> <div class="my-auto">
......
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
{#if !dismissed} {#if !dismissed}
{#if mounted} {#if mounted}
<div <div
class=" top-0 left-0 right-0 p-2 mx-4 px-3 flex justify-center items-center relative rounded-xl border border-gray-100 dark:border-gray-850 text-gray-800 dark:text-gary-100 bg-white dark:bg-gray-900 backdrop-blur-xl z-40" class=" top-0 left-0 right-0 p-2 mx-4 px-3 flex justify-center items-center relative rounded-xl border border-gray-50 dark:border-gray-850 text-gray-800 dark:text-gary-100 bg-white dark:bg-gray-900 backdrop-blur-xl z-30"
transition:fade={{ delay: 100, duration: 300 }} transition:fade={{ delay: 100, duration: 300 }}
> >
<div class=" flex flex-col md:flex-row md:items-center flex-1 text-sm w-fit gap-1.5"> <div class=" flex flex-col md:flex-row md:items-center flex-1 text-sm w-fit gap-1.5">
......
...@@ -10,9 +10,11 @@ ...@@ -10,9 +10,11 @@
<DropdownMenu.Root <DropdownMenu.Root
bind:open={show} bind:open={show}
closeFocus={false}
onOpenChange={(state) => { onOpenChange={(state) => {
dispatch('change', state); dispatch('change', state);
}} }}
typeahead={false}
> >
<DropdownMenu.Trigger> <DropdownMenu.Trigger>
<slot /> <slot />
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
export let addTag: Function; export let addTag: Function;
</script> </script>
<div class="flex flex-row flex-wrap gap-0.5 line-clamp-1"> <div class="flex flex-row flex-wrap gap-1 line-clamp-1">
<TagList <TagList
{tags} {tags}
on:delete={(e) => { on:delete={(e) => {
......
...@@ -22,26 +22,12 @@ ...@@ -22,26 +22,12 @@
}; };
</script> </script>
<div class="flex space-x-1 pl-1.5"> <div class="flex {showTagInput ? 'flex-row-reverse' : ''}">
{#if showTagInput} {#if showTagInput}
<div class="flex items-center"> <div class="flex items-center">
<button type="button" on:click={addTagHandler}>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-3 h-3"
>
<path
fill-rule="evenodd"
d="M12.416 3.376a.75.75 0 0 1 .208 1.04l-5 7.5a.75.75 0 0 1-1.154.114l-3-3a.75.75 0 0 1 1.06-1.06l2.353 2.353 4.493-6.74a.75.75 0 0 1 1.04-.207Z"
clip-rule="evenodd"
/>
</svg>
</button>
<input <input
bind:value={tagName} bind:value={tagName}
class=" pl-2 cursor-pointer self-center text-xs h-fit bg-transparent outline-none line-clamp-1 w-[5.5rem]" class=" px-2 cursor-pointer self-center text-xs h-fit bg-transparent outline-none line-clamp-1 w-[5.5rem]"
placeholder={$i18n.t('Add a tag')} placeholder={$i18n.t('Add a tag')}
list="tagOptions" list="tagOptions"
on:keydown={(event) => { on:keydown={(event) => {
...@@ -55,11 +41,27 @@ ...@@ -55,11 +41,27 @@
<option value={tag.name} /> <option value={tag.name} />
{/each} {/each}
</datalist> </datalist>
<button type="button" on:click={addTagHandler}>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
stroke-width="2"
class="w-3 h-3"
>
<path
fill-rule="evenodd"
d="M12.416 3.376a.75.75 0 0 1 .208 1.04l-5 7.5a.75.75 0 0 1-1.154.114l-3-3a.75.75 0 0 1 1.06-1.06l2.353 2.353 4.493-6.74a.75.75 0 0 1 1.04-.207Z"
clip-rule="evenodd"
/>
</svg>
</button>
</div> </div>
{/if} {/if}
<button <button
class=" cursor-pointer self-center p-0.5 space-x-1 flex h-fit items-center dark:hover:bg-gray-700 rounded-full transition border dark:border-gray-600 border-dashed" class=" cursor-pointer self-center p-0.5 flex h-fit items-center dark:hover:bg-gray-700 rounded-full transition border dark:border-gray-600 border-dashed"
type="button" type="button"
on:click={() => { on:click={() => {
showTagInput = !showTagInput; showTagInput = !showTagInput;
...@@ -80,6 +82,6 @@ ...@@ -80,6 +82,6 @@
</button> </button>
{#if label && !showTagInput} {#if label && !showTagInput}
<span class="text-xs pl-1.5 self-center">{label}</span> <span class="text-xs pl-2 self-center">{label}</span>
{/if} {/if}
</div> </div>
...@@ -7,22 +7,23 @@ ...@@ -7,22 +7,23 @@
{#each tags as tag} {#each tags as tag}
<div <div
class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition border dark:border-gray-800 dark:text-white" class="px-2 py-[0.5px] gap-0.5 flex justify-between h-fit items-center rounded-full transition border dark:border-gray-800 dark:text-white"
> >
<div class=" text-[0.7rem] font-medium self-center line-clamp-1"> <div class=" text-[0.7rem] font-medium self-center line-clamp-1">
{tag.name} {tag.name}
</div> </div>
<button <button
class=" m-auto self-center cursor-pointer" class="h-full flex self-center cursor-pointer"
on:click={() => { on:click={() => {
dispatch('delete', tag.name); dispatch('delete', tag.name);
}} }}
type="button"
> >
<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"
fill="currentColor" fill="currentColor"
class="w-3 h-3" class="size-3 m-auto self-center translate-y-[0.3px] translate-x-[3px]"
> >
<path <path
d="M5.28 4.22a.75.75 0 0 0-1.06 1.06L6.94 8l-2.72 2.72a.75.75 0 1 0 1.06 1.06L8 9.06l2.72 2.72a.75.75 0 1 0 1.06-1.06L9.06 8l2.72-2.72a.75.75 0 0 0-1.06-1.06L8 6.94 5.28 4.22Z" d="M5.28 4.22a.75.75 0 0 0-1.06 1.06L6.94 8l-2.72 2.72a.75.75 0 1 0 1.06 1.06L8 9.06l2.72 2.72a.75.75 0 1 0 1.06-1.06L9.06 8l2.72-2.72a.75.75 0 0 0-1.06-1.06L8 6.94 5.28 4.22Z"
......
...@@ -21,6 +21,10 @@ ...@@ -21,6 +21,10 @@
touch: touch touch: touch
}); });
} }
} else if (tooltipInstance && content === '') {
if (tooltipInstance) {
tooltipInstance.destroy();
}
} }
onDestroy(() => { onDestroy(() => {
......
...@@ -8,7 +8,8 @@ ...@@ -8,7 +8,8 @@
getEmbeddingConfig, getEmbeddingConfig,
updateEmbeddingConfig, updateEmbeddingConfig,
getRerankingConfig, getRerankingConfig,
updateRerankingConfig updateRerankingConfig,
resetUploadDir
} from '$lib/apis/rag'; } from '$lib/apis/rag';
import { documents, models } from '$lib/stores'; import { documents, models } from '$lib/stores';
...@@ -24,6 +25,7 @@ ...@@ -24,6 +25,7 @@
let updateRerankingModelLoading = false; let updateRerankingModelLoading = false;
let showResetConfirm = false; let showResetConfirm = false;
let showResetUploadDirConfirm = false;
let embeddingEngine = ''; let embeddingEngine = '';
let embeddingModel = ''; let embeddingModel = '';
...@@ -31,6 +33,7 @@ ...@@ -31,6 +33,7 @@
let OpenAIKey = ''; let OpenAIKey = '';
let OpenAIUrl = ''; let OpenAIUrl = '';
let OpenAIBatchSize = 1;
let querySettings = { let querySettings = {
template: '', template: '',
...@@ -92,7 +95,8 @@ ...@@ -92,7 +95,8 @@
? { ? {
openai_config: { openai_config: {
key: OpenAIKey, key: OpenAIKey,
url: OpenAIUrl url: OpenAIUrl,
batch_size: OpenAIBatchSize
} }
} }
: {}) : {})
...@@ -159,6 +163,7 @@ ...@@ -159,6 +163,7 @@
OpenAIKey = embeddingConfig.openai_config.key; OpenAIKey = embeddingConfig.openai_config.key;
OpenAIUrl = embeddingConfig.openai_config.url; OpenAIUrl = embeddingConfig.openai_config.url;
OpenAIBatchSize = embeddingConfig.openai_config.batch_size ?? 1;
} }
}; };
...@@ -282,6 +287,30 @@ ...@@ -282,6 +287,30 @@
required required
/> />
</div> </div>
<div class="flex mt-0.5 space-x-2">
<div class=" self-center text-xs font-medium">{$i18n.t('Embedding Batch Size')}</div>
<div class=" flex-1">
<input
id="steps-range"
type="range"
min="1"
max="2048"
step="1"
bind:value={OpenAIBatchSize}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
</div>
<div class="">
<input
bind:value={OpenAIBatchSize}
type="number"
class=" bg-transparent text-center w-14"
min="-2"
max="16000"
step="1"
/>
</div>
</div>
{/if} {/if}
<div class=" flex w-full justify-between"> <div class=" flex w-full justify-between">
...@@ -469,99 +498,203 @@ ...@@ -469,99 +498,203 @@
{/if} {/if}
</div> </div>
<hr class=" dark:border-gray-700" /> <hr class=" dark:border-gray-850" />
{#if showResetConfirm} <div>
<div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition"> {#if showResetUploadDirConfirm}
<div class="flex items-center space-x-3"> <div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition">
<svg <div class="flex items-center space-x-3">
xmlns="http://www.w3.org/2000/svg" <svg
viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
fill="currentColor" viewBox="0 0 24 24"
class="w-4 h-4" fill="currentColor"
> class="size-4"
<path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" /> >
<path <path
fill-rule="evenodd" fill-rule="evenodd"
d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM5.72 7.47a.75.75 0 0 1 1.06 0L8 8.69l1.22-1.22a.75.75 0 1 1 1.06 1.06L9.06 9.75l1.22 1.22a.75.75 0 1 1-1.06 1.06L8 10.81l-1.22 1.22a.75.75 0 0 1-1.06-1.06l1.22-1.22-1.22-1.22a.75.75 0 0 1 0-1.06Z" d="M5.625 1.5H9a3.75 3.75 0 0 1 3.75 3.75v1.875c0 1.036.84 1.875 1.875 1.875H16.5a3.75 3.75 0 0 1 3.75 3.75v7.875c0 1.035-.84 1.875-1.875 1.875H5.625a1.875 1.875 0 0 1-1.875-1.875V3.375c0-1.036.84-1.875 1.875-1.875ZM9.75 14.25a.75.75 0 0 0 0 1.5H15a.75.75 0 0 0 0-1.5H9.75Z"
clip-rule="evenodd" clip-rule="evenodd"
/> />
</svg> <path
<span>{$i18n.t('Are you sure?')}</span> d="M14.25 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 16.5 7.5h-1.875a.375.375 0 0 1-.375-.375V5.25Z"
</div> />
</svg>
<span>{$i18n.t('Are you sure?')}</span>
</div>
<div class="flex space-x-1.5 items-center"> <div class="flex space-x-1.5 items-center">
<button <button
class="hover:text-white transition" class="hover:text-white transition"
on:click={() => { on:click={() => {
const res = resetVectorDB(localStorage.token).catch((error) => { const res = resetUploadDir(localStorage.token).catch((error) => {
toast.error(error); toast.error(error);
return null; return null;
}); });
if (res) {
toast.success($i18n.t('Success'));
}
showResetConfirm = false; if (res) {
}} toast.success($i18n.t('Success'));
> }
showResetUploadDirConfirm = false;
}}
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="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
clip-rule="evenodd"
/>
</svg>
</button>
<button
class="hover:text-white transition"
type="button"
on:click={() => {
showResetUploadDirConfirm = false;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
</div>
{:else}
<button
class=" flex rounded-xl py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
on:click={() => {
showResetUploadDirConfirm = true;
}}
type="button"
>
<div class=" self-center mr-3">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20" viewBox="0 0 24 24"
fill="currentColor" fill="currentColor"
class="w-4 h-4" class="size-4"
> >
<path <path
fill-rule="evenodd" fill-rule="evenodd"
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" d="M5.625 1.5H9a3.75 3.75 0 0 1 3.75 3.75v1.875c0 1.036.84 1.875 1.875 1.875H16.5a3.75 3.75 0 0 1 3.75 3.75v7.875c0 1.035-.84 1.875-1.875 1.875H5.625a1.875 1.875 0 0 1-1.875-1.875V3.375c0-1.036.84-1.875 1.875-1.875ZM9.75 14.25a.75.75 0 0 0 0 1.5H15a.75.75 0 0 0 0-1.5H9.75Z"
clip-rule="evenodd" clip-rule="evenodd"
/> />
<path
d="M14.25 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 16.5 7.5h-1.875a.375.375 0 0 1-.375-.375V5.25Z"
/>
</svg> </svg>
</button> </div>
<button <div class=" self-center text-sm font-medium">{$i18n.t('Reset Upload Directory')}</div>
class="hover:text-white transition" </button>
on:click={() => { {/if}
showResetConfirm = false;
}} {#if showResetConfirm}
> <div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition">
<div class="flex items-center space-x-3">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20" viewBox="0 0 16 16"
fill="currentColor" fill="currentColor"
class="w-4 h-4" class="w-4 h-4"
> >
<path <path
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" fill-rule="evenodd"
d="M3.5 2A1.5 1.5 0 0 0 2 3.5v9A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 12.5 4H9.621a1.5 1.5 0 0 1-1.06-.44L7.439 2.44A1.5 1.5 0 0 0 6.38 2H3.5Zm6.75 7.75a.75.75 0 0 0 0-1.5h-4.5a.75.75 0 0 0 0 1.5h4.5Z"
clip-rule="evenodd"
/> />
</svg> </svg>
</button> <span>{$i18n.t('Are you sure?')}</span>
</div> </div>
</div>
{:else} <div class="flex space-x-1.5 items-center">
<button <button
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition" class="hover:text-white transition"
on:click={() => { on:click={() => {
showResetConfirm = true; const res = resetVectorDB(localStorage.token).catch((error) => {
}} toast.error(error);
> return null;
<div class=" self-center mr-3"> });
<svg
xmlns="http://www.w3.org/2000/svg" if (res) {
viewBox="0 0 16 16" toast.success($i18n.t('Success'));
fill="currentColor" }
class="w-4 h-4"
> showResetConfirm = false;
<path }}
fill-rule="evenodd" type="button"
d="M3.5 2A1.5 1.5 0 0 0 2 3.5v9A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 12.5 4H9.621a1.5 1.5 0 0 1-1.06-.44L7.439 2.44A1.5 1.5 0 0 0 6.38 2H3.5Zm6.75 7.75a.75.75 0 0 0 0-1.5h-4.5a.75.75 0 0 0 0 1.5h4.5Z" >
clip-rule="evenodd" <svg
/> xmlns="http://www.w3.org/2000/svg"
</svg> viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
clip-rule="evenodd"
/>
</svg>
</button>
<button
class="hover:text-white transition"
on:click={() => {
showResetConfirm = false;
}}
type="button"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
</div> </div>
<div class=" self-center text-sm font-medium">{$i18n.t('Reset Vector Storage')}</div> {:else}
</button> <button
{/if} class=" flex rounded-xl py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
on:click={() => {
showResetConfirm = true;
}}
type="button"
>
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M3.5 2A1.5 1.5 0 0 0 2 3.5v9A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 12.5 4H9.621a1.5 1.5 0 0 1-1.06-.44L7.439 2.44A1.5 1.5 0 0 0 6.38 2H3.5Zm6.75 7.75a.75.75 0 0 0 0-1.5h-4.5a.75.75 0 0 0 0 1.5h4.5Z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center text-sm font-medium">{$i18n.t('Reset Vector Storage')}</div>
</button>
{/if}
</div>
</div> </div>
<div class="flex justify-end pt-3 text-sm font-medium"> <div class="flex justify-end pt-3 text-sm font-medium">
<button <button
......
<script lang="ts"> <script lang="ts">
import { getRAGConfig, updateRAGConfig } from '$lib/apis/rag'; import { getRAGConfig, updateRAGConfig } from '$lib/apis/rag';
import Switch from '$lib/components/common/Switch.svelte';
import { documents, models } from '$lib/stores'; import { documents, models } from '$lib/stores';
import { onMount, getContext } from 'svelte'; import { onMount, getContext } from 'svelte';
...@@ -9,14 +10,15 @@ ...@@ -9,14 +10,15 @@
export let saveHandler: Function; export let saveHandler: Function;
let webLoaderSSLVerification = true; let webConfig = null;
let webSearchEngines = ['searxng', 'google_pse', 'brave', 'serpstack', 'serper'];
let youtubeLanguage = 'en'; let youtubeLanguage = 'en';
let youtubeTranslation = null; let youtubeTranslation = null;
const submitHandler = async () => { const submitHandler = async () => {
const res = await updateRAGConfig(localStorage.token, { const res = await updateRAGConfig(localStorage.token, {
web_loader_ssl_verification: webLoaderSSLVerification, web: webConfig,
youtube: { youtube: {
language: youtubeLanguage.split(',').map((lang) => lang.trim()), language: youtubeLanguage.split(',').map((lang) => lang.trim()),
translation: youtubeTranslation translation: youtubeTranslation
...@@ -28,7 +30,8 @@ ...@@ -28,7 +30,8 @@
const res = await getRAGConfig(localStorage.token); const res = await getRAGConfig(localStorage.token);
if (res) { if (res) {
webLoaderSSLVerification = res.web_loader_ssl_verification; webConfig = res.web;
youtubeLanguage = res.youtube.language.join(','); youtubeLanguage = res.youtube.language.join(',');
youtubeTranslation = res.youtube.translation; youtubeTranslation = res.youtube.translation;
} }
...@@ -37,59 +40,239 @@ ...@@ -37,59 +40,239 @@
<form <form
class="flex flex-col h-full justify-between space-y-3 text-sm" class="flex flex-col h-full justify-between space-y-3 text-sm"
on:submit|preventDefault={() => { on:submit|preventDefault={async () => {
submitHandler(); await submitHandler();
saveHandler(); saveHandler();
}} }}
> >
<div class=" space-y-3 pr-1.5 overflow-y-scroll h-full max-h-[22rem]"> <div class=" space-y-3 pr-1.5 overflow-y-scroll h-full max-h-[22rem]">
<div> {#if webConfig}
<div class=" mb-1 text-sm font-medium">
{$i18n.t('Web Loader Settings')}
</div>
<div> <div>
<div class=" mb-1 text-sm font-medium">
{$i18n.t('Web Search')}
</div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('Enable Web Search')}
</div>
<Switch bind:state={webConfig.search.enabled} />
</div>
</div>
<div class=" py-0.5 flex w-full justify-between"> <div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium"> <div class=" self-center text-xs font-medium">{$i18n.t('Web Search Engine')}</div>
{$i18n.t('Bypass SSL verification for Websites')} <div class="flex items-center relative">
<select
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
bind:value={webConfig.search.engine}
placeholder="Select a engine"
required
>
<option disabled selected value="">Select a engine</option>
{#each webSearchEngines as engine}
<option value={engine}>{engine}</option>
{/each}
</select>
</div> </div>
</div>
{#if webConfig.search.engine !== ''}
<div class="mt-1.5">
{#if webConfig.search.engine === 'searxng'}
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Searxng Query 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"
type="text"
placeholder={$i18n.t('Enter Searxng Query URL')}
bind:value={webConfig.search.searxng_query_url}
autocomplete="off"
/>
</div>
</div>
</div>
{:else if webConfig.search.engine === 'google_pse'}
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Google PSE 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"
type="text"
placeholder={$i18n.t('Enter Google PSE API Key')}
bind:value={webConfig.search.google_pse_api_key}
autocomplete="off"
/>
</div>
</div>
</div>
<div class="mt-1.5">
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Google PSE Engine Id')}
</div>
<button <div class="flex w-full">
class="p-1 px-3 text-xs flex rounded transition" <div class="flex-1">
on:click={() => { <input
webLoaderSSLVerification = !webLoaderSSLVerification; class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
submitHandler(); type="text"
}} placeholder={$i18n.t('Enter Google PSE Engine Id')}
type="button" bind:value={webConfig.search.google_pse_engine_id}
> autocomplete="off"
{#if webLoaderSSLVerification === true} />
<span class="ml-2 self-center">{$i18n.t('On')}</span> </div>
{:else} </div>
<span class="ml-2 self-center">{$i18n.t('Off')}</span> </div>
{:else if webConfig.search.engine === 'brave'}
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Brave Search 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"
type="text"
placeholder={$i18n.t('Enter Brave Search API Key')}
bind:value={webConfig.search.brave_search_api_key}
autocomplete="off"
/>
</div>
</div>
</div>
{:else if webConfig.search.engine === 'serpstack'}
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Serpstack 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"
type="text"
placeholder={$i18n.t('Enter Serpstack API Key')}
bind:value={webConfig.search.serpstack_api_key}
autocomplete="off"
/>
</div>
</div>
</div>
{:else if webConfig.search.engine === 'serper'}
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Serper 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"
type="text"
placeholder={$i18n.t('Enter Serper API Key')}
bind:value={webConfig.search.serper_api_key}
autocomplete="off"
/>
</div>
</div>
</div>
{/if} {/if}
</button> </div>
</div> {/if}
</div>
{#if webConfig.search.enabled}
<div class="mt-2 flex gap-2 mb-1">
<div class="w-full">
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Search Result Count')}
</div>
<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('Search Result Count')}
bind:value={webConfig.search.result_count}
required
/>
</div>
<div class=" mt-2 mb-1 text-sm font-medium"> <div class="w-full">
{$i18n.t('Youtube Loader Settings')} <div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Concurrent Requests')}
</div>
<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('Concurrent Requests')}
bind:value={webConfig.search.concurrent_requests}
required
/>
</div>
</div>
{/if}
</div> </div>
<hr class=" dark:border-gray-850 my-2" />
<div> <div>
<div class=" py-0.5 flex w-full justify-between"> <div class=" mb-1 text-sm font-medium">
<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Language')}</div> {$i18n.t('Web Loader Settings')}
<div class=" flex-1 self-center"> </div>
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" <div>
type="text" <div class=" py-0.5 flex w-full justify-between">
placeholder={$i18n.t('Enter language codes')} <div class=" self-center text-xs font-medium">
bind:value={youtubeLanguage} {$i18n.t('Bypass SSL verification for Websites')}
autocomplete="off" </div>
/>
<button
class="p-1 px-3 text-xs flex rounded transition"
on:click={() => {
webConfig.ssl_verification = !webConfig.ssl_verification;
submitHandler();
}}
type="button"
>
{#if webConfig.ssl_verification === true}
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
{/if}
</button>
</div>
</div>
<div class=" mt-2 mb-1 text-sm font-medium">
{$i18n.t('Youtube Loader Settings')}
</div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Language')}</div>
<div class=" flex-1 self-center">
<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 language codes')}
bind:value={youtubeLanguage}
autocomplete="off"
/>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> {/if}
</div> </div>
<div class="flex justify-end pt-3 text-sm font-medium"> <div class="flex justify-end pt-3 text-sm font-medium">
<button <button
......
<script> <script>
import { getContext } from 'svelte'; import { getContext, tick } from 'svelte';
import Modal from '../common/Modal.svelte'; import Modal from '../common/Modal.svelte';
import General from './Settings/General.svelte'; import General from './Settings/General.svelte';
import ChunkParams from './Settings/ChunkParams.svelte'; import ChunkParams from './Settings/ChunkParams.svelte';
import QueryParams from './Settings/QueryParams.svelte'; import QueryParams from './Settings/QueryParams.svelte';
import WebParams from './Settings/WebParams.svelte'; import WebParams from './Settings/WebParams.svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { config } from '$lib/stores';
import { getBackendConfig } from '$lib/apis';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
...@@ -171,8 +173,11 @@ ...@@ -171,8 +173,11 @@
/> />
{:else if selectedTab === 'web'} {:else if selectedTab === 'web'}
<WebParams <WebParams
saveHandler={() => { saveHandler={async () => {
toast.success($i18n.t('Settings saved successfully!')); toast.success($i18n.t('Settings saved successfully!'));
await tick();
await config.set(await getBackendConfig());
}} }}
/> />
{/if} {/if}
......
<script lang="ts">
export let className = 'size-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3"
/>
</svg>
<script lang="ts">
export let className = 'w-4 h-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
>
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 15.75 7.5-7.5 7.5 7.5" />
</svg>
<script lang="ts">
export let className = 'size-4';
</script>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class={className}>
<path
fill-rule="evenodd"
d="M5.625 1.5H9a3.75 3.75 0 0 1 3.75 3.75v1.875c0 1.036.84 1.875 1.875 1.875H16.5a3.75 3.75 0 0 1 3.75 3.75v7.875c0 1.035-.84 1.875-1.875 1.875H5.625a1.875 1.875 0 0 1-1.875-1.875V3.375c0-1.036.84-1.875 1.875-1.875Zm6.905 9.97a.75.75 0 0 0-1.06 0l-3 3a.75.75 0 1 0 1.06 1.06l1.72-1.72V18a.75.75 0 0 0 1.5 0v-4.19l1.72 1.72a.75.75 0 1 0 1.06-1.06l-3-3Z"
clip-rule="evenodd"
/>
<path
d="M14.25 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 16.5 7.5h-1.875a.375.375 0 0 1-.375-.375V5.25Z"
/>
</svg>
<script lang="ts">
export let className = 'w-4 h-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75"
/>
</svg>
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