This commit is contained in:
Timothy Jaeryang Baek committed 2026-07-15 00:14:01 -04:00
1 parent 254888cf15
commit 63ada24706
31 files changed
+2560 -2754

No files matched your search

+85 -83
View File
@@ -393,94 +393,96 @@
</nav> </nav>
<div class="flex-1 min-h-0 p-4 lg:px-5 flex flex-col"> <div class="flex-1 min-h-0 p-4 lg:px-5 flex flex-col">
<h2 class="text-sm font-medium text-gray-900 dark:text-white mb-4"> <div class="w-full h-full min-h-0 flex flex-col">
{$i18n.t(selectedTabTitle)} <h2 class="text-sm font-medium text-gray-900 dark:text-white mb-4">
</h2> {$i18n.t(selectedTabTitle)}
</h2>
<div class="flex-1 min-h-0 overflow-hidden"> <div class="flex-1 min-h-0 overflow-hidden">
{#if selectedTab === 'general'} {#if selectedTab === 'general'}
<General <General
saveHandler={async () => { saveHandler={async () => {
toast.success($i18n.t('Settings saved successfully!')); toast.success($i18n.t('Settings saved successfully!'));
await tick(); await tick();
await config.set(await getBackendConfig()); await config.set(await getBackendConfig());
}} }}
/> />
{:else if selectedTab === 'authentication'} {:else if selectedTab === 'authentication'}
<Authentication /> <Authentication />
{:else if selectedTab === 'connections'} {:else if selectedTab === 'connections'}
<Connections <Connections
on:save={() => { on:save={() => {
toast.success($i18n.t('Settings saved successfully!')); toast.success($i18n.t('Settings saved successfully!'));
}} }}
/> />
{:else if selectedTab === 'models'} {:else if selectedTab === 'models'}
<Models /> <Models />
{:else if selectedTab === 'subagents'} {:else if selectedTab === 'subagents'}
<Subagents /> <Subagents />
{:else if selectedTab === 'evaluations'} {:else if selectedTab === 'evaluations'}
<Evaluations /> <Evaluations />
{:else if selectedTab === 'integrations'} {:else if selectedTab === 'integrations'}
<Integrations /> <Integrations />
{:else if selectedTab === 'documents'} {:else if selectedTab === 'documents'}
<Documents <Documents
on:save={async () => { on:save={async () => {
toast.success($i18n.t('Settings saved successfully!')); toast.success($i18n.t('Settings saved successfully!'));
await tick(); await tick();
await config.set(await getBackendConfig()); await config.set(await getBackendConfig());
}} }}
/> />
{:else if selectedTab === 'web'} {:else if selectedTab === 'web'}
<WebSearch <WebSearch
saveHandler={async () => { saveHandler={async () => {
toast.success($i18n.t('Settings saved successfully!')); toast.success($i18n.t('Settings saved successfully!'));
await tick(); await tick();
await config.set(await getBackendConfig()); await config.set(await getBackendConfig());
}} }}
/> />
{:else if selectedTab === 'code-execution'} {:else if selectedTab === 'code-execution'}
<CodeExecution <CodeExecution
saveHandler={async () => { saveHandler={async () => {
toast.success($i18n.t('Settings saved successfully!')); toast.success($i18n.t('Settings saved successfully!'));
await tick(); await tick();
await config.set(await getBackendConfig()); await config.set(await getBackendConfig());
}} }}
/> />
{:else if selectedTab === 'interface'} {:else if selectedTab === 'interface'}
<Interface <Interface
on:save={() => { on:save={() => {
toast.success($i18n.t('Settings saved successfully!')); toast.success($i18n.t('Settings saved successfully!'));
}} }}
/> />
{:else if selectedTab === 'audio'} {:else if selectedTab === 'audio'}
<Audio <Audio
saveHandler={() => { saveHandler={() => {
toast.success($i18n.t('Settings saved successfully!')); toast.success($i18n.t('Settings saved successfully!'));
}} }}
/> />
{:else if selectedTab === 'images'} {:else if selectedTab === 'images'}
<Images <Images
on:save={() => { on:save={() => {
toast.success($i18n.t('Settings saved successfully!')); toast.success($i18n.t('Settings saved successfully!'));
}} }}
/> />
{:else if selectedTab === 'db'} {:else if selectedTab === 'db'}
<Database <Database
saveHandler={() => { saveHandler={() => {
toast.success($i18n.t('Settings saved successfully!')); toast.success($i18n.t('Settings saved successfully!'));
}} }}
/> />
{:else if selectedTab === 'pipelines'} {:else if selectedTab === 'pipelines'}
<Pipelines <Pipelines
saveHandler={() => { saveHandler={() => {
toast.success($i18n.t('Settings saved successfully!')); toast.success($i18n.t('Settings saved successfully!'));
}} }}
/> />
{/if} {/if}
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -0,0 +1,24 @@
<script lang="ts">
export let label = '';
export let description = '';
export let forId = '';
export let className = '';
</script>
<div class={className}>
{#if label}
<label class="text-xs text-gray-600 dark:text-gray-400" for={forId || undefined}>
{label}
</label>
{/if}
<div class={label ? 'mt-1' : ''}>
<slot />
</div>
{#if description}
<p class="mt-0.5 text-[0.6875rem] text-gray-400 dark:text-gray-600">
{description}
</p>
{/if}
</div>
@@ -0,0 +1,22 @@
<script lang="ts">
export let label = '';
export let description = '';
export let className = '';
export let labelClassName = '';
</script>
<div class="flex items-center justify-between gap-4 {className}">
<div class="min-w-0 text-xs text-gray-600 dark:text-gray-400 {labelClassName}">
<slot name="label">{label}</slot>
</div>
<div class="shrink-0">
<slot />
</div>
</div>
{#if description}
<p class="-mt-1 text-[0.6875rem] text-gray-400 dark:text-gray-600">
{description}
</p>
{/if}
@@ -0,0 +1,17 @@
<script lang="ts">
export let title = '';
export let first = false;
export let className = '';
</script>
<section class="{first ? '' : 'mt-5'} {className}">
{#if title}
<h3 class="mb-2 text-xs text-gray-400 dark:text-gray-600">
{title}
</h3>
{/if}
<div class="flex flex-col gap-2.5">
<slot />
</div>
</section>
+111 -77
View File
@@ -227,28 +227,28 @@
</script> </script>
<form <form
class="flex flex-col h-full justify-between space-y-3 text-sm" class="flex h-full flex-col justify-between text-sm"
on:submit|preventDefault={async () => { on:submit|preventDefault={async () => {
await updateConfigHandler(); await updateConfigHandler();
dispatch('save'); dispatch('save');
}} }}
> >
<div class=" space-y-3 flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5 -mr-1.5"> <div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5">
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
<div> <div>
<div class=" mt-0.5 mb-2.5 text-xs text-gray-400 dark:text-gray-600"> <div class=" mb-2 text-xs text-gray-400 dark:text-gray-600">
{$i18n.t('Speech-to-Text')} {$i18n.t('Speech-to-Text')}
</div> </div>
<hr class=" border-gray-100/20 dark:border-white/[0.025] my-2" />
{#if STT_ENGINE !== 'web'} {#if STT_ENGINE !== 'web'}
<div class="mb-2"> <div class="mb-2">
<div class=" mb-1.5 text-xs font-normal">{$i18n.t('Supported MIME Types')}</div> <div class=" mb-1.5 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Supported MIME Types')}
</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
bind:value={STT_SUPPORTED_CONTENT_TYPES} bind:value={STT_SUPPORTED_CONTENT_TYPES}
placeholder={$i18n.t( placeholder={$i18n.t(
'e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults)' 'e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults)'
@@ -260,10 +260,12 @@
{/if} {/if}
<div class="mb-2 py-0.5 flex w-full justify-between"> <div class="mb-2 py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-normal">{$i18n.t('Speech-to-Text Engine')}</div> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Speech-to-Text Engine')}
</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<select <select
class="cursor-pointer w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right" class="cursor-pointer w-fit pr-8 rounded-lg px-2 p-1 text-xs bg-transparent outline-hidden text-right"
bind:value={STT_ENGINE} bind:value={STT_ENGINE}
placeholder={$i18n.t('Select an engine')} placeholder={$i18n.t('Select an engine')}
> >
@@ -291,13 +293,13 @@
</div> </div>
</div> </div>
<hr class="border-gray-100/20 dark:border-white/[0.025] my-2" />
<div class="mb-2 py-0.5 flex w-full justify-between"> <div class="mb-2 py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-normal">{$i18n.t('Request Format')}</div> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Request Format')}
</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<select <select
class="cursor-pointer w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right" class="cursor-pointer w-fit pr-8 rounded-lg px-2 p-1 text-xs bg-transparent outline-hidden text-right"
bind:value={STT_OPENAI_API_REQUEST_FORMAT} bind:value={STT_OPENAI_API_REQUEST_FORMAT}
> >
<option value="multipart">{$i18n.t('Multipart Upload')}</option> <option value="multipart">{$i18n.t('Multipart Upload')}</option>
@@ -306,15 +308,15 @@
</div> </div>
</div> </div>
<hr class="border-gray-100/20 dark:border-white/[0.025] my-2" />
<div> <div>
<div class=" mb-1.5 text-xs font-normal">{$i18n.t('STT Model')}</div> <div class=" mb-1.5 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('STT Model')}
</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
list="model-list" list="model-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
bind:value={STT_MODEL} bind:value={STT_MODEL}
placeholder={$i18n.t('Select a model')} placeholder={$i18n.t('Select a model')}
/> />
@@ -332,14 +334,14 @@
</div> </div>
</div> </div>
<hr class="border-gray-100/20 dark:border-white/[0.025] my-2" />
<div> <div>
<div class=" mb-1.5 text-xs font-normal">{$i18n.t('STT Model')}</div> <div class=" mb-1.5 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('STT Model')}
</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
bind:value={STT_MODEL} bind:value={STT_MODEL}
placeholder={$i18n.t('Select a model (optional)')} placeholder={$i18n.t('Select a model (optional)')}
/> />
@@ -366,14 +368,14 @@
/> />
</div> </div>
<hr class="border-gray-100/20 dark:border-white/[0.025] my-2" />
<div> <div>
<div class=" mb-1.5 text-xs font-normal">{$i18n.t('Azure Region')}</div> <div class=" mb-1.5 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Azure Region')}
</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
bind:value={STT_AZURE_REGION} bind:value={STT_AZURE_REGION}
placeholder={$i18n.t('e.g., westus (leave blank for eastus)')} placeholder={$i18n.t('e.g., westus (leave blank for eastus)')}
/> />
@@ -382,11 +384,13 @@
</div> </div>
<div> <div>
<div class=" mb-1.5 text-xs font-normal">{$i18n.t('Language Locales')}</div> <div class=" mb-1.5 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Language Locales')}
</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
bind:value={STT_AZURE_LOCALES} bind:value={STT_AZURE_LOCALES}
placeholder={$i18n.t('e.g., en-US,ja-JP (leave blank for auto-detect)')} placeholder={$i18n.t('e.g., en-US,ja-JP (leave blank for auto-detect)')}
/> />
@@ -395,11 +399,13 @@
</div> </div>
<div> <div>
<div class=" mb-1.5 text-xs font-normal">{$i18n.t('Endpoint URL')}</div> <div class=" mb-1.5 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Endpoint URL')}
</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
bind:value={STT_AZURE_BASE_URL} bind:value={STT_AZURE_BASE_URL}
placeholder={$i18n.t('(leave blank for to use commercial endpoint)')} placeholder={$i18n.t('(leave blank for to use commercial endpoint)')}
/> />
@@ -408,11 +414,13 @@
</div> </div>
<div> <div>
<div class=" mb-1.5 text-xs font-normal">{$i18n.t('Max Speakers')}</div> <div class=" mb-1.5 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Max Speakers')}
</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
bind:value={STT_AZURE_MAX_SPEAKERS} bind:value={STT_AZURE_MAX_SPEAKERS}
placeholder={$i18n.t('e.g., 3, 4, 5 (leave blank for default)')} placeholder={$i18n.t('e.g., 3, 4, 5 (leave blank for default)')}
/> />
@@ -434,14 +442,14 @@
</div> </div>
</div> </div>
<hr class="border-gray-100/20 dark:border-white/[0.025] my-2" />
<div> <div>
<div class=" mb-1.5 text-xs font-normal">{$i18n.t('STT Model')}</div> <div class=" mb-1.5 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('STT Model')}
</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
bind:value={STT_MODEL} bind:value={STT_MODEL}
placeholder="voxtral-mini-latest" placeholder="voxtral-mini-latest"
/> />
@@ -459,11 +467,11 @@
</div> </div>
</div> </div>
<hr class="border-gray-100/20 dark:border-white/[0.025] my-2" />
<div> <div>
<div class="flex items-center justify-between mb-2"> <div class="flex items-center justify-between mb-2">
<div class="text-xs font-normal">{$i18n.t('Use Chat Completions API')}</div> <div class="text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Use Chat Completions API')}
</div>
<label class="relative inline-flex items-center cursor-pointer"> <label class="relative inline-flex items-center cursor-pointer">
<input <input
type="checkbox" type="checkbox"
@@ -483,19 +491,21 @@
</div> </div>
{:else if STT_ENGINE === ''} {:else if STT_ENGINE === ''}
<div> <div>
<div class=" mb-1.5 text-xs font-normal">{$i18n.t('STT Model')}</div> <div class=" mb-1.5 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('STT Model')}
</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
placeholder={$i18n.t('Set whisper model')} placeholder={$i18n.t('Set whisper model')}
bind:value={STT_WHISPER_MODEL} bind:value={STT_WHISPER_MODEL}
/> />
</div> </div>
<button <button
class="px-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition" class="px-2.5 hover:bg-black/5 text-gray-800 dark:hover:bg-white/5 dark:text-gray-100 rounded-lg transition"
on:click={() => { on:click={() => {
sttModelUpdateHandler(); sttModelUpdateHandler();
}} }}
@@ -541,17 +551,17 @@
</div> </div>
<div> <div>
<div class=" mt-0.5 mb-2.5 text-xs text-gray-400 dark:text-gray-600"> <div class=" mb-2 text-xs text-gray-400 dark:text-gray-600">
{$i18n.t('Text-to-Speech')} {$i18n.t('Text-to-Speech')}
</div> </div>
<hr class=" border-gray-100/20 dark:border-white/[0.025] my-2" />
<div class="mb-2 py-0.5 flex w-full justify-between"> <div class="mb-2 py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-normal">{$i18n.t('Text-to-Speech Engine')}</div> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Text-to-Speech Engine')}
</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<select <select
class="w-fit pr-8 cursor-pointer rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right" class="w-fit pr-8 cursor-pointer rounded-lg px-2 p-1 text-xs bg-transparent outline-hidden text-right"
bind:value={TTS_ENGINE} bind:value={TTS_ENGINE}
placeholder={$i18n.t('Select a mode')} placeholder={$i18n.t('Select a mode')}
on:change={async (e) => { on:change={async (e) => {
@@ -606,14 +616,14 @@
<SensitiveInput placeholder={$i18n.t('API Key')} bind:value={TTS_API_KEY} required /> <SensitiveInput placeholder={$i18n.t('API Key')} bind:value={TTS_API_KEY} required />
</div> </div>
<hr class="border-gray-100/20 dark:border-white/[0.025] my-2" />
<div> <div>
<div class=" mb-1.5 text-xs font-normal">{$i18n.t('Azure Region')}</div> <div class=" mb-1.5 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Azure Region')}
</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
bind:value={TTS_AZURE_SPEECH_REGION} bind:value={TTS_AZURE_SPEECH_REGION}
placeholder={$i18n.t('e.g., westus (leave blank for eastus)')} placeholder={$i18n.t('e.g., westus (leave blank for eastus)')}
/> />
@@ -622,11 +632,13 @@
</div> </div>
<div> <div>
<div class=" mb-1.5 text-xs font-normal">{$i18n.t('Endpoint URL')}</div> <div class=" mb-1.5 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Endpoint URL')}
</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
bind:value={TTS_AZURE_SPEECH_BASE_URL} bind:value={TTS_AZURE_SPEECH_BASE_URL}
placeholder={$i18n.t('(leave blank for to use commercial endpoint)')} placeholder={$i18n.t('(leave blank for to use commercial endpoint)')}
/> />
@@ -652,11 +664,13 @@
<div class="mb-2"> <div class="mb-2">
{#if TTS_ENGINE === ''} {#if TTS_ENGINE === ''}
<div> <div>
<div class=" mb-1.5 text-xs font-normal">{$i18n.t('TTS Voice')}</div> <div class=" mb-1.5 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('TTS Voice')}
</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<select <select
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
bind:value={TTS_VOICE} bind:value={TTS_VOICE}
> >
<option value="" selected={TTS_VOICE !== ''}>{$i18n.t('Default')}</option> <option value="" selected={TTS_VOICE !== ''}>{$i18n.t('Default')}</option>
@@ -673,12 +687,14 @@
</div> </div>
{:else if TTS_ENGINE === 'transformers'} {:else if TTS_ENGINE === 'transformers'}
<div> <div>
<div class=" mb-1.5 text-xs font-normal">{$i18n.t('TTS Model')}</div> <div class=" mb-1.5 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('TTS Model')}
</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
list="model-list" list="model-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
bind:value={TTS_MODEL} bind:value={TTS_MODEL}
placeholder={$i18n.t('CMU ARCTIC speaker embedding name')} placeholder={$i18n.t('CMU ARCTIC speaker embedding name')}
/> />
@@ -715,25 +731,29 @@
{:else if TTS_ENGINE === 'openai'} {:else if TTS_ENGINE === 'openai'}
<div class=" flex gap-2"> <div class=" flex gap-2">
<div class="w-full"> <div class="w-full">
<div class=" mb-1.5 text-xs font-normal">{$i18n.t('TTS Voice')}</div> <div class=" mb-1.5 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('TTS Voice')}
</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<TTSVoiceInput <TTSVoiceInput
bind:value={TTS_VOICE} bind:value={TTS_VOICE}
voices={providerVoices} voices={providerVoices}
placeholder={$i18n.t('Select a voice')} placeholder={$i18n.t('Select a voice')}
className="w-full rounded-lg py-2 px-4 bg-gray-50 dark:text-gray-300 dark:bg-gray-850" className="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
/> />
</div> </div>
</div> </div>
</div> </div>
<div class="w-full"> <div class="w-full">
<div class=" mb-1.5 text-xs font-normal">{$i18n.t('TTS Model')}</div> <div class=" mb-1.5 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('TTS Model')}
</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
list="tts-model-list" list="tts-model-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
bind:value={TTS_MODEL} bind:value={TTS_MODEL}
placeholder={$i18n.t('Select a model')} placeholder={$i18n.t('Select a model')}
/> />
@@ -750,11 +770,13 @@
<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500"> <div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
<div class="w-full"> <div class="w-full">
<div class=" mb-1.5 text-xs font-normal">{$i18n.t('Additional Parameters')}</div> <div class=" mb-1.5 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Additional Parameters')}
</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<Textarea <Textarea
className="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" className="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
bind:value={TTS_OPENAI_PARAMS} bind:value={TTS_OPENAI_PARAMS}
placeholder={$i18n.t('Enter additional parameters in JSON format')} placeholder={$i18n.t('Enter additional parameters in JSON format')}
minSize={100} minSize={100}
@@ -766,25 +788,29 @@
{:else if TTS_ENGINE === 'elevenlabs'} {:else if TTS_ENGINE === 'elevenlabs'}
<div class=" flex gap-2"> <div class=" flex gap-2">
<div class="w-full"> <div class="w-full">
<div class=" mb-1.5 text-xs font-normal">{$i18n.t('TTS Voice')}</div> <div class=" mb-1.5 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('TTS Voice')}
</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<TTSVoiceInput <TTSVoiceInput
bind:value={TTS_VOICE} bind:value={TTS_VOICE}
voices={providerVoices} voices={providerVoices}
placeholder={$i18n.t('Select a voice')} placeholder={$i18n.t('Select a voice')}
className="w-full rounded-lg py-2 px-4 bg-gray-50 dark:text-gray-300 dark:bg-gray-850" className="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
/> />
</div> </div>
</div> </div>
</div> </div>
<div class="w-full"> <div class="w-full">
<div class=" mb-1.5 text-xs font-normal">{$i18n.t('TTS Model')}</div> <div class=" mb-1.5 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('TTS Model')}
</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
list="tts-model-list" list="tts-model-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
bind:value={TTS_MODEL} bind:value={TTS_MODEL}
placeholder={$i18n.t('Select a model')} placeholder={$i18n.t('Select a model')}
/> />
@@ -801,20 +827,22 @@
{:else if TTS_ENGINE === 'azure'} {:else if TTS_ENGINE === 'azure'}
<div class=" flex gap-2"> <div class=" flex gap-2">
<div class="w-full"> <div class="w-full">
<div class=" mb-1.5 text-xs font-normal">{$i18n.t('TTS Voice')}</div> <div class=" mb-1.5 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('TTS Voice')}
</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<TTSVoiceInput <TTSVoiceInput
bind:value={TTS_VOICE} bind:value={TTS_VOICE}
voices={providerVoices} voices={providerVoices}
placeholder={$i18n.t('Select a voice')} placeholder={$i18n.t('Select a voice')}
className="w-full rounded-lg py-2 px-4 bg-gray-50 dark:text-gray-300 dark:bg-gray-850" className="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
/> />
</div> </div>
</div> </div>
</div> </div>
<div class="w-full"> <div class="w-full">
<div class=" mb-1.5 text-xs font-normal"> <div class=" mb-1.5 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Output format')} {$i18n.t('Output format')}
<a <a
href="https://learn.microsoft.com/en-us/azure/ai-services/speech-service/rest-text-to-speech?tabs=streaming#audio-outputs" href="https://learn.microsoft.com/en-us/azure/ai-services/speech-service/rest-text-to-speech?tabs=streaming#audio-outputs"
@@ -827,7 +855,7 @@
<div class="flex-1"> <div class="flex-1">
<input <input
list="tts-model-list" list="tts-model-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
bind:value={TTS_AZURE_SPEECH_OUTPUT_FORMAT} bind:value={TTS_AZURE_SPEECH_OUTPUT_FORMAT}
placeholder={$i18n.t('Select an output format')} placeholder={$i18n.t('Select an output format')}
/> />
@@ -838,25 +866,29 @@
{:else if TTS_ENGINE === 'mistral'} {:else if TTS_ENGINE === 'mistral'}
<div class=" flex gap-2"> <div class=" flex gap-2">
<div class="w-full"> <div class="w-full">
<div class=" mb-1.5 text-xs font-normal">{$i18n.t('TTS Voice')}</div> <div class=" mb-1.5 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('TTS Voice')}
</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<TTSVoiceInput <TTSVoiceInput
bind:value={TTS_VOICE} bind:value={TTS_VOICE}
voices={providerVoices} voices={providerVoices}
placeholder={$i18n.t('Select a voice')} placeholder={$i18n.t('Select a voice')}
className="w-full rounded-lg py-2 px-4 bg-gray-50 dark:text-gray-300 dark:bg-gray-850" className="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
/> />
</div> </div>
</div> </div>
</div> </div>
<div class="w-full"> <div class="w-full">
<div class=" mb-1.5 text-xs font-normal">{$i18n.t('TTS Model')}</div> <div class=" mb-1.5 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('TTS Model')}
</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
list="tts-model-list" list="tts-model-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
bind:value={TTS_MODEL} bind:value={TTS_MODEL}
placeholder={$i18n.t('Select a model')} placeholder={$i18n.t('Select a model')}
/> />
@@ -874,10 +906,12 @@
</div> </div>
<div class="pt-0.5 flex w-full justify-between"> <div class="pt-0.5 flex w-full justify-between">
<div class="self-center text-xs font-normal">{$i18n.t('Response splitting')}</div> <div class="self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Response splitting')}
</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<select <select
class="w-fit pr-8 cursor-pointer rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right" class="w-fit pr-8 cursor-pointer rounded-lg px-2 p-1 text-xs bg-transparent outline-hidden text-right"
aria-label={$i18n.t('Select how to split message text for TTS requests')} aria-label={$i18n.t('Select how to split message text for TTS requests')}
bind:value={TTS_SPLIT_ON} bind:value={TTS_SPLIT_ON}
> >
@@ -897,7 +931,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="flex justify-end text-sm font-normal"> <div class="flex justify-end pt-6 text-sm font-normal">
<button <button
class="px-3.5 py-1.5 text-sm font-normal bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full" class="px-3.5 py-1.5 text-sm font-normal bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
type="submit" type="submit"
File diff suppressed because it is too large. Load diff
@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { toast } from 'svelte-sonner';
import { onMount, getContext } from 'svelte'; import { onMount, getContext } from 'svelte';
import { getCodeExecutionConfig, setCodeExecutionConfig } from '$lib/apis/configs'; import { getCodeExecutionConfig, setCodeExecutionConfig } from '$lib/apis/configs';
@@ -8,14 +7,25 @@
import Tooltip from '$lib/components/common/Tooltip.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte';
import Textarea from '$lib/components/common/Textarea.svelte'; import Textarea from '$lib/components/common/Textarea.svelte';
import Switch from '$lib/components/common/Switch.svelte'; import Switch from '$lib/components/common/Switch.svelte';
import AdminSettingField from './AdminSettingField.svelte';
import AdminSettingRow from './AdminSettingRow.svelte';
import AdminSettingSection from './AdminSettingSection.svelte';
const i18n = getContext('i18n'); const i18n: any = getContext('i18n');
export let saveHandler: Function; export let saveHandler: Function;
let config = null; let config: any = null;
let engines = ['pyodide', 'jupyter']; let engines = ['pyodide', 'jupyter'];
const inputClass =
'w-full h-7 rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 text-xs text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500';
const textareaClass =
'w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500';
const inlineInputClass =
'w-fit h-7 rounded-lg border border-transparent bg-transparent px-2 text-xs text-gray-600 outline-hidden transition-colors hover:bg-black/5 focus:border-blue-400 dark:text-gray-400 dark:hover:bg-white/5 dark:focus:border-blue-500';
const selectClass =
'cursor-pointer bg-transparent text-xs text-gray-600 outline-hidden dark:text-gray-400';
const submitHandler = async () => { const submitHandler = async () => {
const res = await setCodeExecutionConfig(localStorage.token, config); const res = await setCodeExecutionConfig(localStorage.token, config);
@@ -31,304 +41,217 @@
</script> </script>
<form <form
class="flex flex-col h-full justify-between space-y-3 text-sm" class="flex h-full flex-col justify-between text-sm"
on:submit|preventDefault={async () => { on:submit|preventDefault={async () => {
await submitHandler(); await submitHandler();
saveHandler(); saveHandler();
}} }}
> >
<div class=" space-y-3 flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5 -mr-1.5"> <div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5">
{#if config} {#if config}
<div> <AdminSettingSection first>
<div class="mb-3.5"> <AdminSettingRow label={$i18n.t('Enable Code Execution')}>
<div class=" mt-0.5 mb-2.5 text-xs text-gray-400 dark:text-gray-600"> <Switch bind:state={config.ENABLE_CODE_EXECUTION} />
{$i18n.t('Code Execution')} </AdminSettingRow>
</div>
<hr class=" border-gray-100/20 dark:border-white/[0.025] my-2" /> {#if config.ENABLE_CODE_EXECUTION}
<AdminSettingRow
label={$i18n.t('Code Execution Engine')}
description={config.CODE_EXECUTION_ENGINE === 'jupyter'
? $i18n.t(
'Warning: Jupyter execution enables arbitrary code execution, posing severe security risks—proceed with extreme caution.'
)
: ''}
>
<select
class={selectClass}
bind:value={config.CODE_EXECUTION_ENGINE}
placeholder={$i18n.t('Select a engine')}
required
>
<option disabled selected value="">{$i18n.t('Select a engine')}</option>
{#each engines as engine}
<option value={engine}>{engine}{engine === 'jupyter' ? ' (Legacy)' : ''}</option>
{/each}
</select>
</AdminSettingRow>
<div class="mb-2.5"> {#if config.CODE_EXECUTION_ENGINE === 'jupyter'}
<div class=" flex w-full justify-between"> <AdminSettingField label={$i18n.t('Jupyter URL')}>
<div class=" self-center text-xs font-normal"> <input
{$i18n.t('Enable Code Execution')} class={inputClass}
</div> type="text"
placeholder={$i18n.t('Enter Jupyter URL')}
bind:value={config.CODE_EXECUTION_JUPYTER_URL}
autocomplete="off"
/>
</AdminSettingField>
<Switch bind:state={config.ENABLE_CODE_EXECUTION} /> <AdminSettingRow label={$i18n.t('Jupyter Auth')}>
</div> <select
</div> class={selectClass}
bind:value={config.CODE_EXECUTION_JUPYTER_AUTH}
placeholder={$i18n.t('Select an auth method')}
>
<option selected value="">{$i18n.t('None')}</option>
<option value="token">{$i18n.t('Token')}</option>
<option value="password">{$i18n.t('Password')}</option>
</select>
</AdminSettingRow>
{#if config.ENABLE_CODE_EXECUTION} {#if config.CODE_EXECUTION_JUPYTER_AUTH}
<div class="mb-2.5"> <AdminSettingField
<div class="flex w-full justify-between"> label={config.CODE_EXECUTION_JUPYTER_AUTH === 'password'
<div class=" self-center text-xs font-normal"> ? $i18n.t('Jupyter Password')
{$i18n.t('Code Execution Engine')} : $i18n.t('Jupyter Token')}
</div> >
<div class="flex items-center relative"> {#if config.CODE_EXECUTION_JUPYTER_AUTH === 'password'}
<select <SensitiveInput
class="w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right" type="text"
bind:value={config.CODE_EXECUTION_ENGINE} placeholder={$i18n.t('Enter Jupyter Password')}
placeholder={$i18n.t('Select a engine')} bind:value={config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD}
required autocomplete="off"
> outerClassName="flex flex-1"
<option disabled selected value="">{$i18n.t('Select a engine')}</option> inputClassName={inputClass}
{#each engines as engine}
<option value={engine}
>{engine}{engine === 'jupyter' ? ' (Legacy)' : ''}</option
>
{/each}
</select>
</div>
</div>
{#if config.CODE_EXECUTION_ENGINE === 'jupyter'}
<div class="text-gray-500 text-xs">
{$i18n.t(
'Warning: Jupyter execution enables arbitrary code execution, posing severe security risks—proceed with extreme caution.'
)}
</div>
{/if}
</div>
{#if config.CODE_EXECUTION_ENGINE === 'jupyter'}
<div class="mb-2.5 flex flex-col gap-1.5 w-full">
<div class="text-xs font-normal">
{$i18n.t('Jupyter URL')}
</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full text-sm py-0.5 placeholder:text-gray-300 dark:placeholder:text-gray-700 bg-transparent outline-hidden"
type="text"
placeholder={$i18n.t('Enter Jupyter URL')}
bind:value={config.CODE_EXECUTION_JUPYTER_URL}
autocomplete="off"
/>
</div>
</div>
</div>
<div class="mb-2.5 flex flex-col gap-1.5 w-full">
<div class=" flex gap-2 w-full items-center justify-between">
<div class="text-xs font-normal">
{$i18n.t('Jupyter Auth')}
</div>
<div>
<select
class="w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-left"
bind:value={config.CODE_EXECUTION_JUPYTER_AUTH}
placeholder={$i18n.t('Select an auth method')}
>
<option selected value="">{$i18n.t('None')}</option>
<option value="token">{$i18n.t('Token')}</option>
<option value="password">{$i18n.t('Password')}</option>
</select>
</div>
</div>
{#if config.CODE_EXECUTION_JUPYTER_AUTH}
<div class="flex w-full gap-2">
<div class="flex-1">
{#if config.CODE_EXECUTION_JUPYTER_AUTH === 'password'}
<SensitiveInput
type="text"
placeholder={$i18n.t('Enter Jupyter Password')}
bind:value={config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD}
autocomplete="off"
/>
{:else}
<SensitiveInput
type="text"
placeholder={$i18n.t('Enter Jupyter Token')}
bind:value={config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN}
autocomplete="off"
/>
{/if}
</div>
</div>
{/if}
</div>
<div class="flex gap-2 w-full items-center justify-between">
<div class="text-xs font-normal">
{$i18n.t('Code Execution Timeout')}
</div>
<div class="">
<Tooltip content={$i18n.t('Enter timeout in seconds')}>
<input
class="w-fit rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
type="number"
bind:value={config.CODE_EXECUTION_JUPYTER_TIMEOUT}
placeholder={$i18n.t('e.g. 60')}
autocomplete="off"
/>
</Tooltip>
</div>
</div>
{/if}
{/if}
</div>
<div class="mb-3.5">
<div class=" mt-0.5 mb-2.5 text-xs text-gray-400 dark:text-gray-600">
{$i18n.t('Code Interpreter')}
</div>
<hr class=" border-gray-100/20 dark:border-white/[0.025] my-2" />
<div class="mb-2.5">
<div class=" flex w-full justify-between">
<div class=" self-center text-xs font-normal">
{$i18n.t('Enable Code Interpreter')}
</div>
<Switch bind:state={config.ENABLE_CODE_INTERPRETER} />
</div>
</div>
{#if config.ENABLE_CODE_INTERPRETER}
<div class="mb-2.5">
<div class=" flex w-full justify-between">
<div class=" self-center text-xs font-normal">
{$i18n.t('Code Interpreter Engine')}
</div>
<div class="flex items-center relative">
<select
class="w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
bind:value={config.CODE_INTERPRETER_ENGINE}
placeholder={$i18n.t('Select a engine')}
required
>
<option disabled selected value="">{$i18n.t('Select a engine')}</option>
{#each engines as engine}
<option value={engine}
>{engine}{engine === 'jupyter' ? ' (Legacy)' : ''}</option
>
{/each}
</select>
</div>
</div>
{#if config.CODE_INTERPRETER_ENGINE === 'jupyter'}
<div class="text-gray-500 text-xs">
{$i18n.t(
'Warning: Jupyter execution enables arbitrary code execution, posing severe security risks—proceed with extreme caution.'
)}
</div>
{/if}
</div>
{#if config.CODE_INTERPRETER_ENGINE === 'jupyter'}
<div class="mb-2.5 flex flex-col gap-1.5 w-full">
<div class="text-xs font-normal">
{$i18n.t('Jupyter URL')}
</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full text-sm py-0.5 placeholder:text-gray-300 dark:placeholder:text-gray-700 bg-transparent outline-hidden"
type="text"
placeholder={$i18n.t('Enter Jupyter URL')}
bind:value={config.CODE_INTERPRETER_JUPYTER_URL}
autocomplete="off"
/>
</div>
</div>
</div>
<div class="mb-2.5 flex flex-col gap-1.5 w-full">
<div class="flex gap-2 w-full items-center justify-between">
<div class="text-xs font-normal">
{$i18n.t('Jupyter Auth')}
</div>
<div>
<select
class="w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-left"
bind:value={config.CODE_INTERPRETER_JUPYTER_AUTH}
placeholder={$i18n.t('Select an auth method')}
>
<option selected value="">{$i18n.t('None')}</option>
<option value="token">{$i18n.t('Token')}</option>
<option value="password">{$i18n.t('Password')}</option>
</select>
</div>
</div>
{#if config.CODE_INTERPRETER_JUPYTER_AUTH}
<div class="flex w-full gap-2">
<div class="flex-1">
{#if config.CODE_INTERPRETER_JUPYTER_AUTH === 'password'}
<SensitiveInput
type="text"
placeholder={$i18n.t('Enter Jupyter Password')}
bind:value={config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD}
autocomplete="off"
/>
{:else}
<SensitiveInput
type="text"
placeholder={$i18n.t('Enter Jupyter Token')}
bind:value={config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN}
autocomplete="off"
/>
{/if}
</div>
</div>
{/if}
</div>
<div class="flex gap-2 w-full items-center justify-between">
<div class="text-xs font-normal">
{$i18n.t('Code Execution Timeout')}
</div>
<div class="">
<Tooltip content={$i18n.t('Enter timeout in seconds')}>
<input
class="w-fit rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
type="number"
bind:value={config.CODE_INTERPRETER_JUPYTER_TIMEOUT}
placeholder={$i18n.t('e.g. 60')}
autocomplete="off"
/>
</Tooltip>
</div>
</div>
{/if}
<hr class="border-gray-100/20 dark:border-white/[0.025] my-2" />
<div>
<div class="py-0.5 w-full">
<div class=" mb-2.5 text-xs font-normal">
{$i18n.t('Code Interpreter Prompt Template')}
</div>
<Tooltip
content={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt'
)}
placement="top-start"
>
<Textarea
bind:value={config.CODE_INTERPRETER_PROMPT_TEMPLATE}
placeholder={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt'
)}
/> />
</Tooltip> {:else}
</div> <SensitiveInput
</div> type="text"
placeholder={$i18n.t('Enter Jupyter Token')}
bind:value={config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN}
autocomplete="off"
outerClassName="flex flex-1"
inputClassName={inputClass}
/>
{/if}
</AdminSettingField>
{/if}
<AdminSettingRow label={$i18n.t('Code Execution Timeout')}>
<Tooltip content={$i18n.t('Enter timeout in seconds')}>
<input
class={inlineInputClass}
type="number"
bind:value={config.CODE_EXECUTION_JUPYTER_TIMEOUT}
placeholder={$i18n.t('e.g. 60')}
autocomplete="off"
/>
</Tooltip>
</AdminSettingRow>
{/if} {/if}
</div> {/if}
</div> </AdminSettingSection>
<AdminSettingSection title={$i18n.t('Code Interpreter')}>
<AdminSettingRow label={$i18n.t('Enable Code Interpreter')}>
<Switch bind:state={config.ENABLE_CODE_INTERPRETER} />
</AdminSettingRow>
{#if config.ENABLE_CODE_INTERPRETER}
<AdminSettingRow
label={$i18n.t('Code Interpreter Engine')}
description={config.CODE_INTERPRETER_ENGINE === 'jupyter'
? $i18n.t(
'Warning: Jupyter execution enables arbitrary code execution, posing severe security risks—proceed with extreme caution.'
)
: ''}
>
<select
class={selectClass}
bind:value={config.CODE_INTERPRETER_ENGINE}
placeholder={$i18n.t('Select a engine')}
required
>
<option disabled selected value="">{$i18n.t('Select a engine')}</option>
{#each engines as engine}
<option value={engine}>{engine}{engine === 'jupyter' ? ' (Legacy)' : ''}</option>
{/each}
</select>
</AdminSettingRow>
{#if config.CODE_INTERPRETER_ENGINE === 'jupyter'}
<AdminSettingField label={$i18n.t('Jupyter URL')}>
<input
class={inputClass}
type="text"
placeholder={$i18n.t('Enter Jupyter URL')}
bind:value={config.CODE_INTERPRETER_JUPYTER_URL}
autocomplete="off"
/>
</AdminSettingField>
<AdminSettingRow label={$i18n.t('Jupyter Auth')}>
<select
class={selectClass}
bind:value={config.CODE_INTERPRETER_JUPYTER_AUTH}
placeholder={$i18n.t('Select an auth method')}
>
<option selected value="">{$i18n.t('None')}</option>
<option value="token">{$i18n.t('Token')}</option>
<option value="password">{$i18n.t('Password')}</option>
</select>
</AdminSettingRow>
{#if config.CODE_INTERPRETER_JUPYTER_AUTH}
<AdminSettingField
label={config.CODE_INTERPRETER_JUPYTER_AUTH === 'password'
? $i18n.t('Jupyter Password')
: $i18n.t('Jupyter Token')}
>
{#if config.CODE_INTERPRETER_JUPYTER_AUTH === 'password'}
<SensitiveInput
type="text"
placeholder={$i18n.t('Enter Jupyter Password')}
bind:value={config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD}
autocomplete="off"
outerClassName="flex flex-1"
inputClassName={inputClass}
/>
{:else}
<SensitiveInput
type="text"
placeholder={$i18n.t('Enter Jupyter Token')}
bind:value={config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN}
autocomplete="off"
outerClassName="flex flex-1"
inputClassName={inputClass}
/>
{/if}
</AdminSettingField>
{/if}
<AdminSettingRow label={$i18n.t('Code Execution Timeout')}>
<Tooltip content={$i18n.t('Enter timeout in seconds')}>
<input
class={inlineInputClass}
type="number"
bind:value={config.CODE_INTERPRETER_JUPYTER_TIMEOUT}
placeholder={$i18n.t('e.g. 60')}
autocomplete="off"
/>
</Tooltip>
</AdminSettingRow>
{/if}
<AdminSettingField label={$i18n.t('Code Interpreter Prompt Template')}>
<Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
placement="top-start"
>
<Textarea
className={textareaClass}
bind:value={config.CODE_INTERPRETER_PROMPT_TEMPLATE}
placeholder={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt'
)}
/>
</Tooltip>
</AdminSettingField>
{/if}
</AdminSettingSection>
{/if} {/if}
</div> </div>
<div class="flex justify-end pt-3 text-sm font-normal"> <div class="flex justify-end pt-6 text-sm font-normal">
<button <button
class="px-3.5 py-1.5 text-sm font-normal bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full" class="px-3.5 py-1.5 text-sm font-normal bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
type="submit" type="submit"
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { createEventDispatcher, onMount, getContext, tick } from 'svelte'; import { createEventDispatcher, onMount, getContext } from 'svelte';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@@ -19,13 +19,15 @@
import OpenAIConnection from './Connections/OpenAIConnection.svelte'; import OpenAIConnection from './Connections/OpenAIConnection.svelte';
import AddConnectionModal from '$lib/components/AddConnectionModal.svelte'; import AddConnectionModal from '$lib/components/AddConnectionModal.svelte';
import OllamaConnection from './Connections/OllamaConnection.svelte'; import OllamaConnection from './Connections/OllamaConnection.svelte';
import AdminSettingRow from './AdminSettingRow.svelte';
import AdminSettingSection from './AdminSettingSection.svelte';
const i18n = getContext('i18n'); const i18n: any = getContext('i18n');
const getModels = async () => { const getModels = async () => {
const models = await _getModels( const models = await _getModels(
localStorage.token, localStorage.token,
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null), $config?.features?.enable_direct_connections ? ($settings?.directConnections ?? null) : null,
false, false,
true true
); );
@@ -33,19 +35,19 @@
}; };
// External // External
let OLLAMA_BASE_URLS = ['']; let OLLAMA_BASE_URLS: string[] = [''];
let OLLAMA_API_CONFIGS = {}; let OLLAMA_API_CONFIGS: any = {};
let OPENAI_API_KEYS = ['']; let OPENAI_API_KEYS: string[] = [''];
let OPENAI_API_BASE_URLS = ['']; let OPENAI_API_BASE_URLS: string[] = [''];
let OPENAI_API_CONFIGS = {}; let OPENAI_API_CONFIGS: any = {};
let ENABLE_OPENAI_API: null | boolean = null; let ENABLE_OPENAI_API: null | boolean = null;
let ENABLE_OLLAMA_API: null | boolean = null; let ENABLE_OLLAMA_API: null | boolean = null;
let connectionsConfig = null; let connectionsConfig: any = null;
let pipelineUrls = {}; let pipelineUrls: Record<string, boolean> = {};
let showAddOpenAIConnectionModal = false; let showAddOpenAIConnectionModal = false;
let showAddOllamaConnectionModal = false; let showAddOllamaConnectionModal = false;
@@ -118,7 +120,7 @@
} }
}; };
const addOpenAIConnectionHandler = async (connection) => { const addOpenAIConnectionHandler = async (connection: any) => {
OPENAI_API_BASE_URLS = [...OPENAI_API_BASE_URLS, connection.url]; OPENAI_API_BASE_URLS = [...OPENAI_API_BASE_URLS, connection.url];
OPENAI_API_KEYS = [...OPENAI_API_KEYS, connection.key]; OPENAI_API_KEYS = [...OPENAI_API_KEYS, connection.key];
OPENAI_API_CONFIGS[OPENAI_API_BASE_URLS.length - 1] = connection.config; OPENAI_API_CONFIGS[OPENAI_API_BASE_URLS.length - 1] = connection.config;
@@ -126,7 +128,7 @@
await updateOpenAIHandler(); await updateOpenAIHandler();
}; };
const addOllamaConnectionHandler = async (connection) => { const addOllamaConnectionHandler = async (connection: any) => {
OLLAMA_BASE_URLS = [...OLLAMA_BASE_URLS, connection.url]; OLLAMA_BASE_URLS = [...OLLAMA_BASE_URLS, connection.url];
OLLAMA_API_CONFIGS[OLLAMA_BASE_URLS.length - 1] = { OLLAMA_API_CONFIGS[OLLAMA_BASE_URLS.length - 1] = {
...connection.config, ...connection.config,
@@ -138,8 +140,8 @@
onMount(async () => { onMount(async () => {
if ($user?.role === 'admin') { if ($user?.role === 'admin') {
let ollamaConfig = {}; let ollamaConfig: any = {};
let openaiConfig = {}; let openaiConfig: any = {};
await Promise.all([ await Promise.all([
(async () => { (async () => {
@@ -215,202 +217,162 @@
onSubmit={addOllamaConnectionHandler} onSubmit={addOllamaConnectionHandler}
/> />
<form class="flex flex-col h-full justify-between text-sm" on:submit|preventDefault={submitHandler}> <form class="flex h-full flex-col justify-between text-sm" on:submit|preventDefault={submitHandler}>
<div class=" flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5 -mr-1.5"> <div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5">
{#if ENABLE_OPENAI_API !== null && ENABLE_OLLAMA_API !== null && connectionsConfig !== null} {#if ENABLE_OPENAI_API !== null && ENABLE_OLLAMA_API !== null && connectionsConfig !== null}
<div class="mb-3.5"> <AdminSettingSection first>
<div class=" mt-0.5 mb-2.5 text-xs text-gray-400 dark:text-gray-600"> <AdminSettingRow label={$i18n.t('OpenAI API')}>
{$i18n.t('General')} <Switch
</div> bind:state={ENABLE_OPENAI_API}
on:change={async () => {
updateOpenAIHandler();
}}
/>
</AdminSettingRow>
<hr class=" border-gray-100/20 dark:border-white/[0.025] my-2" /> {#if ENABLE_OPENAI_API}
<div>
<div class="my-2"> <div class="mb-2 flex items-center justify-between gap-4">
<div class="mt-2 space-y-2"> <div class="text-xs text-gray-600 dark:text-gray-400">
<div class="flex justify-between items-center text-sm"> {$i18n.t('Manage OpenAI API Connections')}
<div class=" font-normal">{$i18n.t('OpenAI API')}</div>
<div class="flex items-center">
<div class="">
<Switch
bind:state={ENABLE_OPENAI_API}
on:change={async () => {
updateOpenAIHandler();
}}
/>
</div>
</div>
</div>
{#if ENABLE_OPENAI_API}
<div class="">
<div class="flex justify-between items-center">
<div class="font-normal text-xs">{$i18n.t('Manage OpenAI API Connections')}</div>
<Tooltip content={$i18n.t(`Add Connection`)}>
<button
class="px-1"
on:click={() => {
showAddOpenAIConnectionModal = true;
}}
type="button"
>
<Plus />
</button>
</Tooltip>
</div>
<div class="flex flex-col gap-1.5 mt-1.5">
{#each OPENAI_API_BASE_URLS as url, idx}
<OpenAIConnection
bind:url={OPENAI_API_BASE_URLS[idx]}
bind:key={OPENAI_API_KEYS[idx]}
bind:config={OPENAI_API_CONFIGS[idx]}
pipeline={pipelineUrls[url] ? true : false}
onSubmit={() => {
updateOpenAIHandler();
}}
onDelete={() => {
OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.filter(
(url, urlIdx) => idx !== urlIdx
);
OPENAI_API_KEYS = OPENAI_API_KEYS.filter((key, keyIdx) => idx !== keyIdx);
let newConfig = {};
OPENAI_API_BASE_URLS.forEach((url, newIdx) => {
newConfig[newIdx] =
OPENAI_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
});
OPENAI_API_CONFIGS = newConfig;
updateOpenAIHandler();
}}
/>
{/each}
</div>
</div>
{/if}
</div>
</div>
<div class=" my-2">
<div class="flex justify-between items-center text-sm mb-2">
<div class=" font-normal">{$i18n.t('Ollama API')}</div>
<div class="mt-1">
<Switch
bind:state={ENABLE_OLLAMA_API}
on:change={async () => {
updateOllamaHandler();
}}
/>
</div>
</div>
{#if ENABLE_OLLAMA_API}
<div class="">
<div class="flex justify-between items-center">
<div class="font-normal text-xs">{$i18n.t('Manage Ollama API Connections')}</div>
<Tooltip content={$i18n.t(`Add Connection`)}>
<button
class="px-1"
on:click={() => {
showAddOllamaConnectionModal = true;
}}
type="button"
>
<Plus />
</button>
</Tooltip>
</div> </div>
<div class="flex w-full gap-1.5"> <Tooltip content={$i18n.t(`Add Connection`)}>
<div class="flex-1 flex flex-col gap-1.5 mt-1.5"> <button
{#each OLLAMA_BASE_URLS as url, idx} class="flex size-6 items-center justify-center rounded-lg text-gray-400 transition-colors hover:bg-black/5 hover:text-gray-700 dark:text-gray-600 dark:hover:bg-white/5 dark:hover:text-gray-300"
<OllamaConnection on:click={() => {
bind:url={OLLAMA_BASE_URLS[idx]} showAddOpenAIConnectionModal = true;
bind:config={OLLAMA_API_CONFIGS[idx]} }}
{idx} type="button"
onSubmit={() => {
updateOllamaHandler();
}}
onDelete={() => {
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url, urlIdx) => idx !== urlIdx);
let newConfig = {};
OLLAMA_BASE_URLS.forEach((url, newIdx) => {
newConfig[newIdx] =
OLLAMA_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
});
OLLAMA_API_CONFIGS = newConfig;
}}
/>
{/each}
</div>
</div>
<div class="mt-1 text-xs text-gray-400 dark:text-gray-500">
{$i18n.t('Trouble accessing Ollama?')}
<a
class=" text-gray-300 font-normal underline"
href="https://github.com/open-webui/open-webui#troubleshooting"
target="_blank"
> >
{$i18n.t('Click here for help.')} <Plus />
</a> </button>
</div> </Tooltip>
</div> </div>
{/if}
</div>
<div class="my-2"> <div class="flex flex-col gap-1.5">
<div class="flex justify-between items-center text-sm"> {#each OPENAI_API_BASE_URLS as url, idx}
<div class=" font-normal">{$i18n.t('Direct Connections')}</div> <OpenAIConnection
bind:url={OPENAI_API_BASE_URLS[idx]}
bind:key={OPENAI_API_KEYS[idx]}
bind:config={OPENAI_API_CONFIGS[idx]}
pipeline={pipelineUrls[url] ? true : false}
onSubmit={() => {
updateOpenAIHandler();
}}
onDelete={() => {
OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.filter(
(url, urlIdx) => idx !== urlIdx
);
OPENAI_API_KEYS = OPENAI_API_KEYS.filter((key, keyIdx) => idx !== keyIdx);
<div class="flex items-center"> let newConfig: any = {};
<div class=""> OPENAI_API_BASE_URLS.forEach((url, newIdx) => {
<Switch newConfig[newIdx] = OPENAI_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
bind:state={connectionsConfig.ENABLE_DIRECT_CONNECTIONS} });
on:change={async () => { OPENAI_API_CONFIGS = newConfig;
updateConnectionsHandler(); updateOpenAIHandler();
}} }}
/> />
</div> {/each}
</div> </div>
</div> </div>
{/if}
<div class="mt-1 text-xs text-gray-400 dark:text-gray-500"> <AdminSettingRow label={$i18n.t('Ollama API')}>
{$i18n.t( <Switch
'Direct Connections allow users to connect to their own OpenAI compatible API endpoints.' bind:state={ENABLE_OLLAMA_API}
)} on:change={async () => {
</div> updateOllamaHandler();
</div> }}
/>
</AdminSettingRow>
<hr class=" border-gray-100/20 dark:border-white/[0.025] my-2" /> {#if ENABLE_OLLAMA_API}
<div>
<div class="mb-2 flex items-center justify-between gap-4">
<div class="text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Manage Ollama API Connections')}
</div>
<div class="my-2"> <Tooltip content={$i18n.t(`Add Connection`)}>
<div class="flex justify-between items-center text-sm"> <button
<div class=" text-xs font-normal">{$i18n.t('Cache Base Model List')}</div> class="flex size-6 items-center justify-center rounded-lg text-gray-400 transition-colors hover:bg-black/5 hover:text-gray-700 dark:text-gray-600 dark:hover:bg-white/5 dark:hover:text-gray-300"
on:click={() => {
showAddOllamaConnectionModal = true;
}}
type="button"
>
<Plus />
</button>
</Tooltip>
</div>
<div class="flex items-center"> <div class="flex flex-col gap-1.5">
<div class=""> {#each OLLAMA_BASE_URLS as url, idx}
<Switch <OllamaConnection
bind:state={connectionsConfig.ENABLE_BASE_MODELS_CACHE} bind:url={OLLAMA_BASE_URLS[idx]}
on:change={async () => { bind:config={OLLAMA_API_CONFIGS[idx]}
updateConnectionsHandler(); {idx}
onSubmit={() => {
updateOllamaHandler();
}}
onDelete={() => {
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url, urlIdx) => idx !== urlIdx);
let newConfig: any = {};
OLLAMA_BASE_URLS.forEach((url, newIdx) => {
newConfig[newIdx] = OLLAMA_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
});
OLLAMA_API_CONFIGS = newConfig;
}} }}
/> />
</div> {/each}
</div>
<div class="mt-1 text-[0.6875rem] text-gray-400 dark:text-gray-600">
{$i18n.t('Trouble accessing Ollama?')}
<a
class="font-normal underline hover:text-gray-700 dark:hover:text-gray-300"
href="https://github.com/open-webui/open-webui#troubleshooting"
target="_blank"
>
{$i18n.t('Click here for help.')}
</a>
</div> </div>
</div> </div>
{/if}
</AdminSettingSection>
<div class="mt-1 text-xs text-gray-400 dark:text-gray-500"> <AdminSettingSection title={$i18n.t('User Connections')}>
{$i18n.t( <AdminSettingRow
'Base Model List Cache speeds up access by fetching base models only at startup or on settings save—faster, but may not show recent base model changes.' label={$i18n.t('Direct Connections')}
)} description={$i18n.t(
</div> 'Direct Connections allow users to connect to their own OpenAI compatible API endpoints.'
</div> )}
</div> >
<Switch
bind:state={connectionsConfig.ENABLE_DIRECT_CONNECTIONS}
on:change={async () => {
updateConnectionsHandler();
}}
/>
</AdminSettingRow>
<AdminSettingRow
label={$i18n.t('Cache Base Model List')}
description={$i18n.t(
'Base Model List Cache speeds up access by fetching base models only at startup or on settings save—faster, but may not show recent base model changes.'
)}
>
<Switch
bind:state={connectionsConfig.ENABLE_BASE_MODELS_CACHE}
on:change={async () => {
updateConnectionsHandler();
}}
/>
</AdminSettingRow>
</AdminSettingSection>
{:else} {:else}
<div class="flex h-full justify-center"> <div class="flex h-full justify-center">
<div class="my-auto"> <div class="my-auto">
@@ -420,7 +382,7 @@
{/if} {/if}
</div> </div>
<div class="flex justify-end pt-3 text-sm font-normal"> <div class="flex justify-end pt-6 text-sm font-normal">
<button <button
class="px-3.5 py-1.5 text-sm font-normal bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full" class="px-3.5 py-1.5 text-sm font-normal bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
type="submit" type="submit"
@@ -3,17 +3,20 @@
const { saveAs } = fileSaver; const { saveAs } = fileSaver;
import { downloadDatabase } from '$lib/apis/utils'; import { downloadDatabase } from '$lib/apis/utils';
import { onMount, getContext } from 'svelte'; import { getContext } from 'svelte';
import { config, user } from '$lib/stores'; import { config } from '$lib/stores';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { getAllUserChats } from '$lib/apis/chats'; import { getAllUserChats } from '$lib/apis/chats';
import { getAllUsers } from '$lib/apis/users'; import { getAllUsers } from '$lib/apis/users';
import { exportConfig, importConfig } from '$lib/apis/configs'; import { exportConfig, importConfig } from '$lib/apis/configs';
const i18n = getContext('i18n'); const i18n: any = getContext('i18n');
export let saveHandler: Function; export let saveHandler: Function;
const actionButtonClass =
'rounded-md px-1.5 py-0.5 text-xs text-gray-500 transition-colors hover:bg-black/5 hover:text-gray-900 dark:text-gray-500 dark:hover:bg-white/5 dark:hover:text-white';
const exportAllUserChats = async () => { const exportAllUserChats = async () => {
let blob = new Blob([JSON.stringify(await getAllUserChats(localStorage.token))], { let blob = new Blob([JSON.stringify(await getAllUserChats(localStorage.token))], {
type: 'application/json' type: 'application/json'
@@ -43,14 +46,10 @@
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
saveAs(blob, 'users.csv'); saveAs(blob, 'users.csv');
}; };
onMount(async () => {
// permissions = await getUserPermissions(localStorage.token);
});
</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 flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5 -mr-1.5"> <div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5">
<input <input
id="config-json-input" id="config-json-input"
hidden hidden
@@ -77,29 +76,24 @@
}} }}
/> />
<div> <div class="flex w-full flex-col gap-2.5">
<div class="mb-1 text-sm font-normal">{$i18n.t('Config')}</div> <div class="flex items-center gap-2">
<div class="w-24 shrink-0 text-xs text-gray-600 dark:text-gray-400">
<div> {$i18n.t('Config')}
<div class="py-0.5 flex w-full justify-between"> </div>
<div class="self-center text-xs">{$i18n.t('Import Config')}</div> <div class="flex flex-wrap items-center gap-1">
<button <button
class="p-1 px-3 text-xs flex rounded-sm transition" class={actionButtonClass}
on:click={() => { on:click={() => {
document.getElementById('config-json-input').click(); document.getElementById('config-json-input')?.click();
}} }}
type="button" type="button"
> >
<span class="self-center">{$i18n.t('Import')}</span> {$i18n.t('Import')}
</button> </button>
</div>
</div>
<div>
<div class="py-0.5 flex w-full justify-between">
<div class="self-center text-xs">{$i18n.t('Export Config')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded-sm transition" class={actionButtonClass}
on:click={async () => { on:click={async () => {
const config = await exportConfig(localStorage.token); const config = await exportConfig(localStorage.token);
const blob = new Blob([JSON.stringify(config)], { const blob = new Blob([JSON.stringify(config)], {
@@ -109,63 +103,47 @@
}} }}
type="button" type="button"
> >
<span class="self-center">{$i18n.t('Export')}</span> {$i18n.t('Export')}
</button> </button>
</div> </div>
</div> </div>
{#if $config?.features.enable_admin_export ?? true}
<div class="flex items-center gap-2">
<div class="w-24 shrink-0 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Database')}
</div>
<button
class={actionButtonClass}
on:click={() => {
downloadDatabase(localStorage.token).catch((error) => {
toast.error(`${error}`);
});
}}
type="button"
>
{$i18n.t('Download')}
</button>
</div>
<div class="flex items-center gap-2">
<div class="w-24 shrink-0 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('All chats')}
</div>
<button class={actionButtonClass} on:click={exportAllUserChats} type="button">
{$i18n.t('Export')}
</button>
</div>
<div class="flex items-center gap-2">
<div class="w-24 shrink-0 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Users')}
</div>
<button class={actionButtonClass} on:click={exportUsers} type="button">
{$i18n.t('Export')}
</button>
</div>
{/if}
</div> </div>
{#if $config?.features.enable_admin_export ?? true}
<div>
<div class="mb-1 text-sm font-normal">{$i18n.t('Database')}</div>
<div>
<div class="py-0.5 flex w-full justify-between">
<div class="self-center text-xs">{$i18n.t('Download Database')}</div>
<button
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
downloadDatabase(localStorage.token).catch((error) => {
toast.error(`${error}`);
});
}}
type="button"
>
<span class="self-center">{$i18n.t('Download')}</span>
</button>
</div>
</div>
<div>
<div class="py-0.5 flex w-full justify-between">
<div class="self-center text-xs">{$i18n.t('Export All Chats (All Users)')}</div>
<button
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
exportAllUserChats();
}}
type="button"
>
<span class="self-center">{$i18n.t('Export')}</span>
</button>
</div>
</div>
<div>
<div class="py-0.5 flex w-full justify-between">
<div class="self-center text-xs">{$i18n.t('Export Users')}</div>
<button
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
exportUsers();
}}
type="button"
>
<span class="self-center">{$i18n.t('Export')}</span>
</button>
</div>
</div>
</div>
{/if}
</div> </div>
</div> </div>
+146 -122
View File
@@ -66,6 +66,8 @@
}; };
let RAGConfig = null; let RAGConfig = null;
const textareaClass =
'w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500';
const embeddingModelUpdateHandler = async () => { const embeddingModelUpdateHandler = async () => {
if (RAG_EMBEDDING_ENGINE === '' && RAG_EMBEDDING_MODEL.split('/').length - 1 > 1) { if (RAG_EMBEDDING_ENGINE === '' && RAG_EMBEDDING_MODEL.split('/').length - 1 > 1) {
@@ -367,29 +369,23 @@
/> />
<form <form
class="flex flex-col h-full justify-between space-y-3 text-sm" class="flex h-full flex-col justify-between text-sm"
on:submit|preventDefault={() => { on:submit|preventDefault={() => {
submitHandler(); submitHandler();
}} }}
> >
{#if RAGConfig} {#if RAGConfig}
<div class=" space-y-2.5 flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5 -mr-1.5"> <div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5">
<div class=""> <div class="">
<div class="mb-3"> <div class="mb-5">
<div class=" mt-0.5 mb-2.5 text-xs text-gray-400 dark:text-gray-600">
{$i18n.t('General')}
</div>
<hr class=" border-gray-100/20 dark:border-white/[0.025] my-2" />
<div class="mb-2.5 flex flex-col w-full justify-between"> <div class="mb-2.5 flex flex-col w-full justify-between">
<div class="flex w-full justify-between mb-1"> <div class="flex w-full justify-between mb-1">
<div class="self-center text-xs font-normal"> <div class="self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Content Extraction Engine')} {$i18n.t('Content Extraction Engine')}
</div> </div>
<div class=""> <div class="">
<select <select
class="w-fit pr-8 rounded-sm px-2 text-xs bg-transparent outline-hidden text-right" class="w-fit pr-8 rounded-lg px-2 text-xs bg-transparent outline-hidden text-right"
bind:value={RAGConfig.CONTENT_EXTRACTION_ENGINE} bind:value={RAGConfig.CONTENT_EXTRACTION_ENGINE}
> >
<option value="">{$i18n.t('Default')}</option> <option value="">{$i18n.t('Default')}</option>
@@ -408,7 +404,7 @@
{#if RAGConfig.CONTENT_EXTRACTION_ENGINE === ''} {#if RAGConfig.CONTENT_EXTRACTION_ENGINE === ''}
<div class="flex w-full mt-1"> <div class="flex w-full mt-1">
<div class="flex-1 flex justify-between"> <div class="flex-1 flex justify-between">
<div class=" self-center text-xs font-normal"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('PDF Extract Images (OCR)')} {$i18n.t('PDF Extract Images (OCR)')}
</div> </div>
<div class="flex items-center relative"> <div class="flex items-center relative">
@@ -419,7 +415,7 @@
<div class="flex w-full mt-2"> <div class="flex w-full mt-2">
<div class="flex-1 flex justify-between"> <div class="flex-1 flex justify-between">
<div class=" self-center text-xs font-normal"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
<Tooltip <Tooltip
content={$i18n.t( content={$i18n.t(
'Page mode creates one document per page. Single mode combines all pages into one document for better chunking across page boundaries.' 'Page mode creates one document per page. Single mode combines all pages into one document for better chunking across page boundaries.'
@@ -431,7 +427,7 @@
</div> </div>
<div class=""> <div class="">
<select <select
class="w-fit pr-8 rounded-sm px-2 text-xs bg-transparent outline-hidden text-right" class="w-fit pr-8 rounded-lg px-2 text-xs bg-transparent outline-hidden text-right"
bind:value={RAGConfig.PDF_LOADER_MODE} bind:value={RAGConfig.PDF_LOADER_MODE}
> >
<option value="page">{$i18n.t('Page')}</option> <option value="page">{$i18n.t('Page')}</option>
@@ -450,7 +446,7 @@
className="w-full" className="w-full"
> >
<input <input
class="flex-1 w-full text-sm bg-transparent outline-hidden" class="flex-1 w-full text-xs bg-transparent outline-hidden"
placeholder={$i18n.t('Enter Datalab Marker API Base URL')} placeholder={$i18n.t('Enter Datalab Marker API Base URL')}
bind:value={RAGConfig.DATALAB_MARKER_API_BASE_URL} bind:value={RAGConfig.DATALAB_MARKER_API_BASE_URL}
/> />
@@ -466,7 +462,7 @@
<div class="flex flex-col gap-2 mt-2"> <div class="flex flex-col gap-2 mt-2">
<div class=" flex flex-col w-full justify-between"> <div class=" flex flex-col w-full justify-between">
<div class=" mb-1 text-xs font-normal"> <div class=" mb-1 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Additional Config')} {$i18n.t('Additional Config')}
</div> </div>
<div class="flex w-full items-center relative"> <div class="flex w-full items-center relative">
@@ -478,6 +474,7 @@
className="w-full" className="w-full"
> >
<Textarea <Textarea
className={textareaClass}
bind:value={RAGConfig.DATALAB_MARKER_ADDITIONAL_CONFIG} bind:value={RAGConfig.DATALAB_MARKER_ADDITIONAL_CONFIG}
placeholder={$i18n.t('Enter JSON config (e.g., {"disable_links": true})')} placeholder={$i18n.t('Enter JSON config (e.g., {"disable_links": true})')}
/> />
@@ -487,7 +484,7 @@
</div> </div>
<div class="flex justify-between w-full mt-2"> <div class="flex justify-between w-full mt-2">
<div class="self-center text-xs font-normal"> <div class="self-center text-xs text-gray-600 dark:text-gray-400">
<Tooltip <Tooltip
content={$i18n.t( content={$i18n.t(
'Significantly improves accuracy by using an LLM to enhance tables, forms, inline math, and layout detection. Will increase latency. Defaults to False.' 'Significantly improves accuracy by using an LLM to enhance tables, forms, inline math, and layout detection. Will increase latency. Defaults to False.'
@@ -502,7 +499,7 @@
</div> </div>
</div> </div>
<div class="flex justify-between w-full mt-2"> <div class="flex justify-between w-full mt-2">
<div class="self-center text-xs font-normal"> <div class="self-center text-xs text-gray-600 dark:text-gray-400">
<Tooltip <Tooltip
content={$i18n.t('Skip the cache and re-run the inference. Defaults to False.')} content={$i18n.t('Skip the cache and re-run the inference. Defaults to False.')}
placement="top-start" placement="top-start"
@@ -515,7 +512,7 @@
</div> </div>
</div> </div>
<div class="flex justify-between w-full mt-2"> <div class="flex justify-between w-full mt-2">
<div class="self-center text-xs font-normal"> <div class="self-center text-xs text-gray-600 dark:text-gray-400">
<Tooltip <Tooltip
content={$i18n.t( content={$i18n.t(
'Force OCR on all pages of the PDF. This can lead to worse results if you have good text in your PDFs. Defaults to False.' 'Force OCR on all pages of the PDF. This can lead to worse results if you have good text in your PDFs. Defaults to False.'
@@ -530,7 +527,7 @@
</div> </div>
</div> </div>
<div class="flex justify-between w-full mt-2"> <div class="flex justify-between w-full mt-2">
<div class="self-center text-xs font-normal"> <div class="self-center text-xs text-gray-600 dark:text-gray-400">
<Tooltip <Tooltip
content={$i18n.t( content={$i18n.t(
'Whether to paginate the output. Each page will be separated by a horizontal rule and page number. Defaults to False.' 'Whether to paginate the output. Each page will be separated by a horizontal rule and page number. Defaults to False.'
@@ -545,7 +542,7 @@
</div> </div>
</div> </div>
<div class="flex justify-between w-full mt-2"> <div class="flex justify-between w-full mt-2">
<div class="self-center text-xs font-normal"> <div class="self-center text-xs text-gray-600 dark:text-gray-400">
<Tooltip <Tooltip
content={$i18n.t( content={$i18n.t(
'Strip existing OCR text from the PDF and re-run OCR. Ignored if Force OCR is enabled. Defaults to False.' 'Strip existing OCR text from the PDF and re-run OCR. Ignored if Force OCR is enabled. Defaults to False.'
@@ -560,7 +557,7 @@
</div> </div>
</div> </div>
<div class="flex justify-between w-full mt-2"> <div class="flex justify-between w-full mt-2">
<div class="self-center text-xs font-normal"> <div class="self-center text-xs text-gray-600 dark:text-gray-400">
<Tooltip <Tooltip
content={$i18n.t( content={$i18n.t(
'Disable image extraction from the PDF. If Use LLM is enabled, images will be automatically captioned. Defaults to False.' 'Disable image extraction from the PDF. If Use LLM is enabled, images will be automatically captioned. Defaults to False.'
@@ -575,7 +572,7 @@
</div> </div>
</div> </div>
<div class="flex justify-between w-full mt-2"> <div class="flex justify-between w-full mt-2">
<div class="self-center text-xs font-normal"> <div class="self-center text-xs text-gray-600 dark:text-gray-400">
<Tooltip <Tooltip
content={$i18n.t( content={$i18n.t(
'Format the lines in the output. Defaults to False. If set to True, the lines will be formatted to detect inline math and styles.' 'Format the lines in the output. Defaults to False. If set to True, the lines will be formatted to detect inline math and styles.'
@@ -590,7 +587,7 @@
</div> </div>
</div> </div>
<div class="flex justify-between w-full mt-2"> <div class="flex justify-between w-full mt-2">
<div class="self-center text-xs font-normal"> <div class="self-center text-xs text-gray-600 dark:text-gray-400">
<Tooltip <Tooltip
content={$i18n.t( content={$i18n.t(
"The output format for the text. Can be 'json', 'markdown', or 'html'. Defaults to 'markdown'." "The output format for the text. Can be 'json', 'markdown', or 'html'. Defaults to 'markdown'."
@@ -602,7 +599,7 @@
</div> </div>
<div class=""> <div class="">
<select <select
class="w-fit pr-8 rounded-sm px-2 text-xs bg-transparent outline-hidden text-right" class="w-fit pr-8 rounded-lg px-2 text-xs bg-transparent outline-hidden text-right"
bind:value={RAGConfig.DATALAB_MARKER_OUTPUT_FORMAT} bind:value={RAGConfig.DATALAB_MARKER_OUTPUT_FORMAT}
> >
<option value="markdown">{$i18n.t('Markdown')}</option> <option value="markdown">{$i18n.t('Markdown')}</option>
@@ -615,7 +612,7 @@
<div class="my-0.5 flex flex-col gap-2 pr-2"> <div class="my-0.5 flex flex-col gap-2 pr-2">
<div class="flex gap-2"> <div class="flex gap-2">
<input <input
class="flex-1 w-full text-sm bg-transparent outline-hidden" class="flex-1 w-full text-xs bg-transparent outline-hidden"
placeholder={$i18n.t('Enter External Document Loader URL')} placeholder={$i18n.t('Enter External Document Loader URL')}
bind:value={RAGConfig.EXTERNAL_DOCUMENT_LOADER_URL} bind:value={RAGConfig.EXTERNAL_DOCUMENT_LOADER_URL}
/> />
@@ -633,7 +630,7 @@
)} )}
> >
<Textarea <Textarea
className="w-full text-sm outline-hidden" className={textareaClass}
bind:value={RAGConfig.EXTERNAL_DOCUMENT_LOADER_HEADERS} bind:value={RAGConfig.EXTERNAL_DOCUMENT_LOADER_HEADERS}
placeholder={$i18n.t('Enter additional headers in JSON format')} placeholder={$i18n.t('Enter additional headers in JSON format')}
required={false} required={false}
@@ -689,7 +686,7 @@
<div class="flex w-full mt-1"> <div class="flex w-full mt-1">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="flex-1 w-full text-sm bg-transparent outline-hidden" class="flex-1 w-full text-xs bg-transparent outline-hidden"
placeholder={$i18n.t('Enter Tika Server URL')} placeholder={$i18n.t('Enter Tika Server URL')}
bind:value={RAGConfig.TIKA_SERVER_URL} bind:value={RAGConfig.TIKA_SERVER_URL}
/> />
@@ -698,7 +695,7 @@
{:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'docling'} {:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'docling'}
<div class="my-0.5 flex gap-2 pr-2"> <div class="my-0.5 flex gap-2 pr-2">
<input <input
class="flex-1 w-full text-sm bg-transparent outline-hidden" class="flex-1 w-full text-xs bg-transparent outline-hidden"
placeholder={$i18n.t('Enter Docling Server URL')} placeholder={$i18n.t('Enter Docling Server URL')}
bind:value={RAGConfig.DOCLING_SERVER_URL} bind:value={RAGConfig.DOCLING_SERVER_URL}
/> />
@@ -711,11 +708,12 @@
<div class="flex flex-col gap-2 mt-2"> <div class="flex flex-col gap-2 mt-2">
<div class=" flex flex-col w-full justify-between"> <div class=" flex flex-col w-full justify-between">
<div class=" mb-1 text-xs font-normal"> <div class=" mb-1 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Parameters')} {$i18n.t('Parameters')}
</div> </div>
<div class="flex w-full items-center relative"> <div class="flex w-full items-center relative">
<Textarea <Textarea
className={textareaClass}
bind:value={RAGConfig.DOCLING_PARAMS} bind:value={RAGConfig.DOCLING_PARAMS}
placeholder={$i18n.t('Enter additional parameters in JSON format')} placeholder={$i18n.t('Enter additional parameters in JSON format')}
minSize={100} minSize={100}
@@ -726,7 +724,7 @@
{:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'document_intelligence'} {:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'document_intelligence'}
<div class="my-0.5 flex gap-2 pr-2"> <div class="my-0.5 flex gap-2 pr-2">
<input <input
class="flex-1 w-full text-sm bg-transparent outline-hidden" class="flex-1 w-full text-xs bg-transparent outline-hidden"
placeholder={$i18n.t('Enter Document Intelligence Endpoint')} placeholder={$i18n.t('Enter Document Intelligence Endpoint')}
bind:value={RAGConfig.DOCUMENT_INTELLIGENCE_ENDPOINT} bind:value={RAGConfig.DOCUMENT_INTELLIGENCE_ENDPOINT}
/> />
@@ -737,13 +735,13 @@
/> />
</div> </div>
<div class="my-0.5 flex flex-col w-full"> <div class="my-0.5 flex flex-col w-full">
<div class=" mb-1 text-xs font-normal"> <div class=" mb-1 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Document Intelligence Model')} {$i18n.t('Document Intelligence Model')}
</div> </div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="flex-1 w-full text-sm bg-transparent outline-hidden" class="flex-1 w-full text-xs bg-transparent outline-hidden"
placeholder={$i18n.t('Enter Document Intelligence Model')} placeholder={$i18n.t('Enter Document Intelligence Model')}
bind:value={RAGConfig.DOCUMENT_INTELLIGENCE_MODEL} bind:value={RAGConfig.DOCUMENT_INTELLIGENCE_MODEL}
/> />
@@ -753,7 +751,7 @@
{:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'mistral_ocr'} {:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'mistral_ocr'}
<div class="my-0.5 flex gap-2 pr-2"> <div class="my-0.5 flex gap-2 pr-2">
<input <input
class="flex-1 w-full text-sm bg-transparent outline-hidden" class="flex-1 w-full text-xs bg-transparent outline-hidden"
placeholder={$i18n.t('Enter Mistral API Base URL')} placeholder={$i18n.t('Enter Mistral API Base URL')}
bind:value={RAGConfig.MISTRAL_OCR_API_BASE_URL} bind:value={RAGConfig.MISTRAL_OCR_API_BASE_URL}
/> />
@@ -763,7 +761,7 @@
/> />
</div> </div>
<div class="flex justify-between w-full mt-2 pr-2"> <div class="flex justify-between w-full mt-2 pr-2">
<div class="self-center text-xs font-normal"> <div class="self-center text-xs text-gray-600 dark:text-gray-400">
<Tooltip <Tooltip
content={$i18n.t( content={$i18n.t(
'Send the PDF as a base64 data URL instead of uploading it first.' 'Send the PDF as a base64 data URL instead of uploading it first.'
@@ -780,7 +778,7 @@
{:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'paddleocr_vl'} {:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'paddleocr_vl'}
<div class="my-0.5 flex gap-2 pr-2"> <div class="my-0.5 flex gap-2 pr-2">
<input <input
class="flex-1 w-full text-sm bg-transparent outline-hidden" class="flex-1 w-full text-xs bg-transparent outline-hidden"
placeholder={$i18n.t('Enter PaddleOCR-vl API Base URL')} placeholder={$i18n.t('Enter PaddleOCR-vl API Base URL')}
bind:value={RAGConfig.PADDLEOCR_VL_BASE_URL} bind:value={RAGConfig.PADDLEOCR_VL_BASE_URL}
/> />
@@ -794,11 +792,11 @@
<!-- API Mode Selection --> <!-- API Mode Selection -->
<div class="flex w-full mt-2"> <div class="flex w-full mt-2">
<div class="flex-1 flex justify-between"> <div class="flex-1 flex justify-between">
<div class="self-center text-xs font-normal"> <div class="self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('API Mode')} {$i18n.t('API Mode')}
</div> </div>
<select <select
class="w-fit pr-8 rounded-sm px-2 text-xs bg-transparent outline-hidden" class="w-fit pr-8 rounded-lg px-2 text-xs bg-transparent outline-hidden"
bind:value={RAGConfig.MINERU_API_MODE} bind:value={RAGConfig.MINERU_API_MODE}
on:change={() => { on:change={() => {
// Auto-update URL when switching modes if it's empty or matches the opposite mode's default // Auto-update URL when switching modes if it's empty or matches the opposite mode's default
@@ -825,7 +823,7 @@
<!-- API URL --> <!-- API URL -->
<div class="flex w-full mt-2"> <div class="flex w-full mt-2">
<input <input
class="flex-1 w-full text-sm bg-transparent outline-hidden" class="flex-1 w-full text-xs bg-transparent outline-hidden"
placeholder={RAGConfig.MINERU_API_MODE === 'cloud' placeholder={RAGConfig.MINERU_API_MODE === 'cloud'
? $i18n.t('https://mineru.net/api/v4') ? $i18n.t('https://mineru.net/api/v4')
: $i18n.t('http://localhost:8000')} : $i18n.t('http://localhost:8000')}
@@ -842,11 +840,11 @@
<div class="flex w-full mt-2"> <div class="flex w-full mt-2">
<div class="flex-1 flex justify-between"> <div class="flex-1 flex justify-between">
<div class="self-center text-xs font-normal"> <div class="self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('API Timeout')} {$i18n.t('API Timeout')}
</div> </div>
<input <input
class="w-16 text-sm bg-transparent outline-hidden text-right" class="w-16 text-xs bg-transparent outline-hidden text-right"
type="number" type="number"
min="1" min="1"
bind:value={RAGConfig.MINERU_API_TIMEOUT} bind:value={RAGConfig.MINERU_API_TIMEOUT}
@@ -857,7 +855,7 @@
<!-- Parameters --> <!-- Parameters -->
<div class="flex flex-col justify-between w-full mt-2"> <div class="flex flex-col justify-between w-full mt-2">
<div class="text-xs font-normal"> <div class="text-xs text-gray-600 dark:text-gray-400">
<Tooltip <Tooltip
content={$i18n.t( content={$i18n.t(
'Advanced parameters for MinerU parsing (enable_ocr, enable_formula, enable_table, language, model_version, page_ranges)' 'Advanced parameters for MinerU parsing (enable_ocr, enable_formula, enable_table, language, model_version, page_ranges)'
@@ -869,6 +867,7 @@
</div> </div>
<div class="mt-1.5"> <div class="mt-1.5">
<Textarea <Textarea
className={textareaClass}
bind:value={RAGConfig.MINERU_PARAMS} bind:value={RAGConfig.MINERU_PARAMS}
placeholder={`{\n "enable_ocr": false,\n "enable_formula": true,\n "enable_table": true,\n "language": "en",\n "model_version": "pipeline",\n "page_ranges": ""\n}`} placeholder={`{\n "enable_ocr": false,\n "enable_formula": true,\n "enable_table": true,\n "language": "en",\n "model_version": "pipeline",\n "page_ranges": ""\n}`}
minSize={100} minSize={100}
@@ -878,7 +877,7 @@
<!-- File Extensions --> <!-- File Extensions -->
<div class="flex flex-col justify-between w-full mt-2"> <div class="flex flex-col justify-between w-full mt-2">
<div class="text-xs font-normal mb-1"> <div class="text-xs text-gray-600 dark:text-gray-400 mb-1">
<Tooltip <Tooltip
content={$i18n.t( content={$i18n.t(
'Comma-separated list of file extensions MinerU will handle (e.g. pdf, docx, pptx, xlsx)' 'Comma-separated list of file extensions MinerU will handle (e.g. pdf, docx, pptx, xlsx)'
@@ -889,7 +888,7 @@
</Tooltip> </Tooltip>
</div> </div>
<input <input
class="flex-1 w-full text-sm bg-transparent outline-hidden" class="flex-1 w-full text-xs bg-transparent outline-hidden"
placeholder={$i18n.t('pdf, docx, pptx, xlsx')} placeholder={$i18n.t('pdf, docx, pptx, xlsx')}
bind:value={RAGConfig.MINERU_FILE_EXTENSIONS} bind:value={RAGConfig.MINERU_FILE_EXTENSIONS}
/> />
@@ -898,7 +897,7 @@
</div> </div>
<div class=" mb-2.5 flex w-full justify-between"> <div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-normal"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
<Tooltip content={$i18n.t('Full Context Mode')} placement="top-start"> <Tooltip content={$i18n.t('Full Context Mode')} placement="top-start">
{$i18n.t('Bypass Embedding and Retrieval')} {$i18n.t('Bypass Embedding and Retrieval')}
</Tooltip> </Tooltip>
@@ -920,10 +919,12 @@
{#if !RAGConfig.BYPASS_EMBEDDING_AND_RETRIEVAL} {#if !RAGConfig.BYPASS_EMBEDDING_AND_RETRIEVAL}
<div class=" mb-2.5 flex w-full justify-between"> <div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-normal">{$i18n.t('Text Splitter')}</div> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Text Splitter')}
</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<select <select
class="w-fit pr-8 rounded-sm px-2 text-xs bg-transparent outline-hidden text-right" class="w-fit pr-8 rounded-lg px-2 text-xs bg-transparent outline-hidden text-right"
bind:value={RAGConfig.TEXT_SPLITTER} bind:value={RAGConfig.TEXT_SPLITTER}
> >
<option value="">{$i18n.t('Default')} ({$i18n.t('Character')})</option> <option value="">{$i18n.t('Default')} ({$i18n.t('Character')})</option>
@@ -937,12 +938,14 @@
{#if RAGConfig.TEXT_SPLITTER === 'token_transformers'} {#if RAGConfig.TEXT_SPLITTER === 'token_transformers'}
<div class="mb-2.5 flex flex-col w-full justify-between"> <div class="mb-2.5 flex flex-col w-full justify-between">
<div class="self-center text-xs font-normal min-w-fit mb-1 w-full"> <div
class="self-center text-xs text-gray-600 dark:text-gray-400 min-w-fit mb-1 w-full"
>
{$i18n.t('Tokenizer Model')} {$i18n.t('Tokenizer Model')}
</div> </div>
<div class="self-center w-full"> <div class="self-center w-full">
<input <input
class="w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
placeholder={$i18n.t('Enter Tokenizer Model')} placeholder={$i18n.t('Enter Tokenizer Model')}
bind:value={RAGConfig.RAG_TOKENIZER_MODEL} bind:value={RAGConfig.RAG_TOKENIZER_MODEL}
autocomplete="off" autocomplete="off"
@@ -953,7 +956,7 @@
{/if} {/if}
<div class=" mb-2.5 flex w-full justify-between"> <div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-normal"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
<Tooltip <Tooltip
placement="top-start" placement="top-start"
content={$i18n.t( content={$i18n.t(
@@ -971,12 +974,12 @@
<div class=" mb-2.5 flex w-full justify-between"> <div class=" mb-2.5 flex w-full justify-between">
<div class=" flex gap-1.5 w-full"> <div class=" flex gap-1.5 w-full">
<div class=" w-full justify-between"> <div class=" w-full justify-between">
<div class="self-center text-xs font-normal min-w-fit mb-1"> <div class="self-center text-xs text-gray-600 dark:text-gray-400 min-w-fit mb-1">
{$i18n.t('Chunk Size')} {$i18n.t('Chunk Size')}
</div> </div>
<div class="self-center"> <div class="self-center">
<input <input
class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class=" w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
type="number" type="number"
placeholder={$i18n.t('Enter Chunk Size')} placeholder={$i18n.t('Enter Chunk Size')}
bind:value={RAGConfig.CHUNK_SIZE} bind:value={RAGConfig.CHUNK_SIZE}
@@ -987,13 +990,13 @@
</div> </div>
<div class="w-full"> <div class="w-full">
<div class=" self-center text-xs font-normal min-w-fit mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 min-w-fit mb-1">
{$i18n.t('Chunk Overlap')} {$i18n.t('Chunk Overlap')}
</div> </div>
<div class="self-center"> <div class="self-center">
<input <input
class="w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
type="number" type="number"
placeholder={$i18n.t('Enter Chunk Overlap')} placeholder={$i18n.t('Enter Chunk Overlap')}
bind:value={RAGConfig.CHUNK_OVERLAP} bind:value={RAGConfig.CHUNK_OVERLAP}
@@ -1009,7 +1012,9 @@
<div class=" mb-2.5 flex w-full justify-between"> <div class=" mb-2.5 flex w-full justify-between">
<div class=" flex gap-1.5 w-full"> <div class=" flex gap-1.5 w-full">
<div class="w-full"> <div class="w-full">
<div class="self-center text-xs font-normal min-w-fit mb-1"> <div
class="self-center text-xs text-gray-600 dark:text-gray-400 min-w-fit mb-1"
>
<Tooltip <Tooltip
placement="top-start" placement="top-start"
content={$i18n.t( content={$i18n.t(
@@ -1021,7 +1026,7 @@
</div> </div>
<div class="self-center"> <div class="self-center">
<input <input
class="w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
type="number" type="number"
placeholder={$i18n.t('Enter Chunk Min Size Target')} placeholder={$i18n.t('Enter Chunk Min Size Target')}
bind:value={RAGConfig.CHUNK_MIN_SIZE_TARGET} bind:value={RAGConfig.CHUNK_MIN_SIZE_TARGET}
@@ -1037,21 +1042,19 @@
</div> </div>
{#if !RAGConfig.BYPASS_EMBEDDING_AND_RETRIEVAL} {#if !RAGConfig.BYPASS_EMBEDDING_AND_RETRIEVAL}
<div class="mb-3"> <div class="mb-5">
<div class=" mt-0.5 mb-2.5 text-xs text-gray-400 dark:text-gray-600"> <div class=" mb-2 text-xs text-gray-400 dark:text-gray-600">
{$i18n.t('Embedding')} {$i18n.t('Embedding')}
</div> </div>
<hr class=" border-gray-100/20 dark:border-white/[0.025] my-2" />
<div class=" mb-2.5 flex flex-col w-full justify-between"> <div class=" mb-2.5 flex flex-col 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-normal"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Embedding Model Engine')} {$i18n.t('Embedding Model Engine')}
</div> </div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<select <select
class="w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right" class="w-fit pr-8 rounded-lg px-2 p-1 text-xs bg-transparent outline-hidden text-right"
bind:value={RAG_EMBEDDING_ENGINE} bind:value={RAG_EMBEDDING_ENGINE}
placeholder={$i18n.t('Select an embedding model engine')} placeholder={$i18n.t('Select an embedding model engine')}
on:change={(e) => { on:change={(e) => {
@@ -1077,7 +1080,7 @@
{#if RAG_EMBEDDING_ENGINE === 'openai'} {#if RAG_EMBEDDING_ENGINE === 'openai'}
<div class="my-0.5 flex gap-2 pr-2"> <div class="my-0.5 flex gap-2 pr-2">
<input <input
class="flex-1 w-full text-sm bg-transparent outline-hidden" class="flex-1 w-full text-xs bg-transparent outline-hidden"
placeholder={$i18n.t('API Base URL')} placeholder={$i18n.t('API Base URL')}
bind:value={OpenAIUrl} bind:value={OpenAIUrl}
required required
@@ -1092,7 +1095,7 @@
{:else if RAG_EMBEDDING_ENGINE === 'ollama'} {:else if RAG_EMBEDDING_ENGINE === 'ollama'}
<div class="my-0.5 flex gap-2 pr-2"> <div class="my-0.5 flex gap-2 pr-2">
<input <input
class="flex-1 w-full text-sm bg-transparent outline-hidden" class="flex-1 w-full text-xs bg-transparent outline-hidden"
placeholder={$i18n.t('API Base URL')} placeholder={$i18n.t('API Base URL')}
bind:value={OllamaUrl} bind:value={OllamaUrl}
required required
@@ -1108,7 +1111,7 @@
<div class="my-0.5 flex flex-col gap-2 pr-2 w-full"> <div class="my-0.5 flex flex-col gap-2 pr-2 w-full">
<div class="flex gap-2"> <div class="flex gap-2">
<input <input
class="flex-1 w-full text-sm bg-transparent outline-hidden" class="flex-1 w-full text-xs bg-transparent outline-hidden"
placeholder={$i18n.t('API Base URL')} placeholder={$i18n.t('API Base URL')}
bind:value={AzureOpenAIUrl} bind:value={AzureOpenAIUrl}
required required
@@ -1117,7 +1120,7 @@
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<input <input
class="flex-1 w-full text-sm bg-transparent outline-hidden" class="flex-1 w-full text-xs bg-transparent outline-hidden"
placeholder={$i18n.t('Version')} placeholder={$i18n.t('Version')}
bind:value={AzureOpenAIVersion} bind:value={AzureOpenAIVersion}
required required
@@ -1128,14 +1131,16 @@
</div> </div>
<div class=" mb-2.5 flex flex-col w-full"> <div class=" mb-2.5 flex flex-col w-full">
<div class=" mb-1 text-xs font-normal">{$i18n.t('Embedding Model')}</div> <div class=" mb-1 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Embedding Model')}
</div>
<div class=""> <div class="">
{#if RAG_EMBEDDING_ENGINE === 'ollama'} {#if RAG_EMBEDDING_ENGINE === 'ollama'}
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="flex-1 w-full text-sm bg-transparent outline-hidden" class="flex-1 w-full text-xs bg-transparent outline-hidden"
bind:value={RAG_EMBEDDING_MODEL} bind:value={RAG_EMBEDDING_MODEL}
placeholder={$i18n.t('Set embedding model')} placeholder={$i18n.t('Set embedding model')}
required required
@@ -1146,7 +1151,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="flex-1 w-full text-sm bg-transparent outline-hidden" class="flex-1 w-full text-xs bg-transparent outline-hidden"
placeholder={$i18n.t('Set embedding model (e.g. {{model}})', { placeholder={$i18n.t('Set embedding model (e.g. {{model}})', {
model: RAG_EMBEDDING_MODEL.slice(-40) model: RAG_EMBEDDING_MODEL.slice(-40)
})} })}
@@ -1195,7 +1200,7 @@
</div> </div>
<div class=" mb-2.5 flex w-full justify-between"> <div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-normal"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Embedding Batch Size')} {$i18n.t('Embedding Batch Size')}
</div> </div>
@@ -1213,7 +1218,7 @@
{#if RAG_EMBEDDING_ENGINE === 'ollama' || RAG_EMBEDDING_ENGINE === 'openai' || RAG_EMBEDDING_ENGINE === 'azure_openai'} {#if RAG_EMBEDDING_ENGINE === 'ollama' || RAG_EMBEDDING_ENGINE === 'openai' || RAG_EMBEDDING_ENGINE === 'azure_openai'}
<div class=" mb-2.5 flex w-full justify-between"> <div class=" mb-2.5 flex w-full justify-between">
<div class="self-center text-xs font-normal"> <div class="self-center text-xs text-gray-600 dark:text-gray-400">
<Tooltip <Tooltip
content={$i18n.t( content={$i18n.t(
'Runs embedding tasks concurrently to speed up processing. Turn off if rate limits become an issue.' 'Runs embedding tasks concurrently to speed up processing. Turn off if rate limits become an issue.'
@@ -1229,7 +1234,7 @@
</div> </div>
<div class=" mb-2.5 flex w-full justify-between"> <div class=" mb-2.5 flex w-full justify-between">
<div class="self-center text-xs font-normal"> <div class="self-center text-xs text-gray-600 dark:text-gray-400">
<Tooltip <Tooltip
content={$i18n.t( content={$i18n.t(
'Limits the number of concurrent embedding requests. Set to 0 for unlimited.' 'Limits the number of concurrent embedding requests. Set to 0 for unlimited.'
@@ -1253,16 +1258,16 @@
</div> </div>
{/if} {/if}
<div class="mb-3"> <div class="mb-5">
<div class=" mt-0.5 mb-2.5 text-xs text-gray-400 dark:text-gray-600"> <div class=" mb-2 text-xs text-gray-400 dark:text-gray-600">
{$i18n.t('Retrieval')} {$i18n.t('Retrieval')}
</div> </div>
<hr class=" border-gray-100/20 dark:border-white/[0.025] my-2" />
{#if !RAGConfig.BYPASS_EMBEDDING_AND_RETRIEVAL} {#if !RAGConfig.BYPASS_EMBEDDING_AND_RETRIEVAL}
<div class=" mb-2.5 flex w-full justify-between"> <div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-normal">{$i18n.t('Full Context Mode')}</div> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Full Context Mode')}
</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<Tooltip <Tooltip
content={RAGConfig.RAG_FULL_CONTEXT content={RAGConfig.RAG_FULL_CONTEXT
@@ -1280,7 +1285,9 @@
{#if !RAGConfig.RAG_FULL_CONTEXT} {#if !RAGConfig.RAG_FULL_CONTEXT}
<div class=" mb-2.5 flex w-full justify-between"> <div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-normal">{$i18n.t('Hybrid Search')}</div> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Hybrid Search')}
</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<Switch bind:state={RAGConfig.ENABLE_RAG_HYBRID_SEARCH} /> <Switch bind:state={RAGConfig.ENABLE_RAG_HYBRID_SEARCH} />
</div> </div>
@@ -1288,7 +1295,7 @@
{#if RAGConfig.ENABLE_RAG_HYBRID_SEARCH === true} {#if RAGConfig.ENABLE_RAG_HYBRID_SEARCH === true}
<div class="mb-2.5 flex w-full justify-between"> <div class="mb-2.5 flex w-full justify-between">
<div class="self-center text-xs font-normal"> <div class="self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Enrich Hybrid Search Text')} {$i18n.t('Enrich Hybrid Search Text')}
</div> </div>
<div class="flex items-center relative"> <div class="flex items-center relative">
@@ -1304,12 +1311,12 @@
<div class=" mb-2.5 flex flex-col w-full justify-between"> <div class=" mb-2.5 flex flex-col 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-normal"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Reranking Engine')} {$i18n.t('Reranking Engine')}
</div> </div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<select <select
class="w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right" class="w-fit pr-8 rounded-lg px-2 p-1 text-xs bg-transparent outline-hidden text-right"
bind:value={RAGConfig.RAG_RERANKING_ENGINE} bind:value={RAGConfig.RAG_RERANKING_ENGINE}
placeholder={$i18n.t('Select a reranking model engine')} placeholder={$i18n.t('Select a reranking model engine')}
on:change={(e) => { on:change={(e) => {
@@ -1329,7 +1336,7 @@
{#if RAGConfig.RAG_RERANKING_ENGINE === 'external'} {#if RAGConfig.RAG_RERANKING_ENGINE === 'external'}
<div class="my-0.5 flex gap-2 pr-2"> <div class="my-0.5 flex gap-2 pr-2">
<input <input
class="flex-1 w-full text-sm bg-transparent outline-hidden" class="flex-1 w-full text-xs bg-transparent outline-hidden"
placeholder={$i18n.t('API Base URL')} placeholder={$i18n.t('API Base URL')}
bind:value={RAGConfig.RAG_EXTERNAL_RERANKER_URL} bind:value={RAGConfig.RAG_EXTERNAL_RERANKER_URL}
required required
@@ -1345,13 +1352,15 @@
</div> </div>
<div class=" mb-2.5 flex flex-col w-full"> <div class=" mb-2.5 flex flex-col w-full">
<div class=" mb-1 text-xs font-normal">{$i18n.t('Reranking Model')}</div> <div class=" mb-1 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Reranking Model')}
</div>
<div class=""> <div class="">
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="flex-1 w-full text-sm bg-transparent outline-hidden" class="flex-1 w-full text-xs bg-transparent outline-hidden"
placeholder={$i18n.t('Set reranking model (e.g. {{model}})', { placeholder={$i18n.t('Set reranking model (e.g. {{model}})', {
model: 'BAAI/bge-reranker-v2-m3' model: 'BAAI/bge-reranker-v2-m3'
})} })}
@@ -1364,7 +1373,7 @@
{/if} {/if}
<div class=" mb-2.5 flex w-full justify-between"> <div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-normal"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Reranking Batch Size')} {$i18n.t('Reranking Batch Size')}
</div> </div>
@@ -1381,10 +1390,12 @@
</div> </div>
<div class=" mb-2.5 flex w-full justify-between"> <div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-normal">{$i18n.t('Top K')}</div> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Top K')}
</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<input <input
class="flex-1 w-full text-sm bg-transparent outline-hidden" class="flex-1 w-full text-xs bg-transparent outline-hidden"
type="number" type="number"
placeholder={$i18n.t('Enter Top K')} placeholder={$i18n.t('Enter Top K')}
bind:value={RAGConfig.TOP_K} bind:value={RAGConfig.TOP_K}
@@ -1396,10 +1407,12 @@
{#if RAGConfig.ENABLE_RAG_HYBRID_SEARCH === true} {#if RAGConfig.ENABLE_RAG_HYBRID_SEARCH === true}
<div class="mb-2.5 flex w-full justify-between"> <div class="mb-2.5 flex w-full justify-between">
<div class="self-center text-xs font-normal">{$i18n.t('Top K Reranker')}</div> <div class="self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Top K Reranker')}
</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<input <input
class="flex-1 w-full text-sm bg-transparent outline-hidden" class="flex-1 w-full text-xs bg-transparent outline-hidden"
type="number" type="number"
placeholder={$i18n.t('Enter Top K Reranker')} placeholder={$i18n.t('Enter Top K Reranker')}
bind:value={RAGConfig.TOP_K_RERANKER} bind:value={RAGConfig.TOP_K_RERANKER}
@@ -1413,12 +1426,12 @@
{#if RAGConfig.ENABLE_RAG_HYBRID_SEARCH === true} {#if RAGConfig.ENABLE_RAG_HYBRID_SEARCH === true}
<div class=" mb-2.5 flex flex-col w-full justify-between"> <div class=" mb-2.5 flex flex-col 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-normal"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Relevance Threshold')} {$i18n.t('Relevance Threshold')}
</div> </div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<input <input
class="flex-1 w-full text-sm bg-transparent outline-hidden" class="flex-1 w-full text-xs bg-transparent outline-hidden"
type="number" type="number"
step="0.01" step="0.01"
placeholder={$i18n.t('Enter Score')} placeholder={$i18n.t('Enter Score')}
@@ -1449,11 +1462,11 @@
className="inline-tooltip" className="inline-tooltip"
> >
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" self-center text-xs font-normal"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('BM25 Weight')} {$i18n.t('BM25 Weight')}
</div> </div>
<button <button
class="p-1 px-3 text-xs flex rounded-sm transition shrink-0 outline-hidden" class="p-1 px-3 text-xs flex rounded-lg transition shrink-0 outline-hidden"
type="button" type="button"
on:click={() => { on:click={() => {
RAGConfig.HYBRID_BM25_WEIGHT = RAGConfig.HYBRID_BM25_WEIGHT =
@@ -1511,7 +1524,9 @@
{/if} {/if}
<div class=" mb-2.5 flex flex-col w-full justify-between"> <div class=" mb-2.5 flex flex-col w-full justify-between">
<div class=" mb-1 text-xs font-normal">{$i18n.t('RAG Template')}</div> <div class=" mb-1 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('RAG Template')}
</div>
<div class="flex w-full items-center relative"> <div class="flex w-full items-center relative">
<Tooltip <Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')} content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
@@ -1519,6 +1534,7 @@
className="w-full" className="w-full"
> >
<Textarea <Textarea
className={textareaClass}
bind:value={RAGConfig.RAG_TEMPLATE} bind:value={RAGConfig.RAG_TEMPLATE}
placeholder={$i18n.t( placeholder={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt' 'Leave empty to use the default prompt, or enter a custom prompt'
@@ -1537,15 +1553,15 @@
</div> </div>
</div> </div>
<div class="mb-3"> <div class="mb-5">
<div class=" mt-0.5 mb-2.5 text-xs text-gray-400 dark:text-gray-600"> <div class=" mb-2 text-xs text-gray-400 dark:text-gray-600">
{$i18n.t('Files')} {$i18n.t('Files')}
</div> </div>
<hr class=" border-gray-100/20 dark:border-white/[0.025] my-2" />
<div class=" mb-2.5 flex w-full justify-between"> <div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-normal">{$i18n.t('Allowed File Extensions')}</div> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Allowed File Extensions')}
</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<Tooltip <Tooltip
content={$i18n.t( content={$i18n.t(
@@ -1554,7 +1570,7 @@
placement="top-start" placement="top-start"
> >
<input <input
class="flex-1 w-full text-sm bg-transparent outline-hidden" class="flex-1 w-full text-xs bg-transparent outline-hidden"
type="text" type="text"
placeholder={$i18n.t('e.g. pdf, docx, txt')} placeholder={$i18n.t('e.g. pdf, docx, txt')}
bind:value={RAGConfig.ALLOWED_FILE_EXTENSIONS} bind:value={RAGConfig.ALLOWED_FILE_EXTENSIONS}
@@ -1565,7 +1581,9 @@
</div> </div>
<div class=" mb-2.5 flex w-full justify-between"> <div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-normal">{$i18n.t('Max Upload Size')}</div> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Max Upload Size')}
</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<Tooltip <Tooltip
content={$i18n.t( content={$i18n.t(
@@ -1574,7 +1592,7 @@
placement="top-start" placement="top-start"
> >
<input <input
class="flex-1 w-full text-sm bg-transparent outline-hidden" class="flex-1 w-full text-xs bg-transparent outline-hidden"
type="number" type="number"
placeholder={$i18n.t('Leave empty for unlimited')} placeholder={$i18n.t('Leave empty for unlimited')}
bind:value={RAGConfig.FILE_MAX_SIZE} bind:value={RAGConfig.FILE_MAX_SIZE}
@@ -1586,7 +1604,9 @@
</div> </div>
<div class=" mb-2.5 flex w-full justify-between"> <div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-normal">{$i18n.t('Max Upload Count')}</div> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Max Upload Count')}
</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<Tooltip <Tooltip
content={$i18n.t( content={$i18n.t(
@@ -1595,7 +1615,7 @@
placement="top-start" placement="top-start"
> >
<input <input
class="flex-1 w-full text-sm bg-transparent outline-hidden" class="flex-1 w-full text-xs bg-transparent outline-hidden"
type="number" type="number"
placeholder={$i18n.t('Leave empty for unlimited')} placeholder={$i18n.t('Leave empty for unlimited')}
bind:value={RAGConfig.FILE_MAX_COUNT} bind:value={RAGConfig.FILE_MAX_COUNT}
@@ -1607,7 +1627,9 @@
</div> </div>
<div class=" mb-2.5 flex w-full justify-between"> <div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-normal">{$i18n.t('Image Compression Width')}</div> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Image Compression Width')}
</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<Tooltip <Tooltip
content={$i18n.t( content={$i18n.t(
@@ -1616,7 +1638,7 @@
placement="top-start" placement="top-start"
> >
<input <input
class="flex-1 w-full text-sm bg-transparent outline-hidden" class="flex-1 w-full text-xs bg-transparent outline-hidden"
type="number" type="number"
placeholder={$i18n.t('Leave empty for no compression')} placeholder={$i18n.t('Leave empty for no compression')}
bind:value={RAGConfig.FILE_IMAGE_COMPRESSION_WIDTH} bind:value={RAGConfig.FILE_IMAGE_COMPRESSION_WIDTH}
@@ -1628,7 +1650,7 @@
</div> </div>
<div class=" mb-2.5 flex w-full justify-between"> <div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-normal"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Image Compression Height')} {$i18n.t('Image Compression Height')}
</div> </div>
<div class="flex items-center relative"> <div class="flex items-center relative">
@@ -1639,7 +1661,7 @@
placement="top-start" placement="top-start"
> >
<input <input
class="flex-1 w-full text-sm bg-transparent outline-hidden" class="flex-1 w-full text-xs bg-transparent outline-hidden"
type="number" type="number"
placeholder={$i18n.t('Leave empty for no compression')} placeholder={$i18n.t('Leave empty for no compression')}
bind:value={RAGConfig.FILE_IMAGE_COMPRESSION_HEIGHT} bind:value={RAGConfig.FILE_IMAGE_COMPRESSION_HEIGHT}
@@ -1651,37 +1673,39 @@
</div> </div>
</div> </div>
<div class="mb-3"> <div class="mb-5">
<div class=" mt-0.5 mb-2.5 text-xs text-gray-400 dark:text-gray-600"> <div class=" mb-2 text-xs text-gray-400 dark:text-gray-600">
{$i18n.t('Integration')} {$i18n.t('Integration')}
</div> </div>
<hr class=" border-gray-100/20 dark:border-white/[0.025] my-2" />
<div class=" mb-2.5 flex w-full justify-between"> <div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-normal">{$i18n.t('Google Drive')}</div> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Google Drive')}
</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<Switch bind:state={RAGConfig.ENABLE_GOOGLE_DRIVE_INTEGRATION} /> <Switch bind:state={RAGConfig.ENABLE_GOOGLE_DRIVE_INTEGRATION} />
</div> </div>
</div> </div>
<div class=" mb-2.5 flex w-full justify-between"> <div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-normal">{$i18n.t('OneDrive')}</div> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('OneDrive')}
</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<Switch bind:state={RAGConfig.ENABLE_ONEDRIVE_INTEGRATION} /> <Switch bind:state={RAGConfig.ENABLE_ONEDRIVE_INTEGRATION} />
</div> </div>
</div> </div>
</div> </div>
<div class="mb-3"> <div class="mb-5">
<div class=" mt-0.5 mb-2.5 text-xs text-gray-400 dark:text-gray-600"> <div class=" mb-2 text-xs text-gray-400 dark:text-gray-600">
{$i18n.t('Danger Zone')} {$i18n.t('Danger Zone')}
</div> </div>
<hr class=" border-gray-100/20 dark:border-white/[0.025] my-2" />
<div class=" mb-2.5 flex w-full justify-between"> <div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-normal">{$i18n.t('Reset Upload Directory')}</div> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Reset Upload Directory')}
</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<button <button
class="text-xs" class="text-xs"
@@ -1696,7 +1720,7 @@
</div> </div>
<div class=" mb-2.5 flex w-full justify-between"> <div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-normal"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Reset Vector Storage/Knowledge')} {$i18n.t('Reset Vector Storage/Knowledge')}
</div> </div>
<div class="flex items-center relative"> <div class="flex items-center relative">
@@ -1712,7 +1736,7 @@
</div> </div>
</div> </div>
<div class=" mb-2.5 flex w-full justify-between"> <div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-normal"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Reindex Knowledge Base Vectors')} {$i18n.t('Reindex Knowledge Base Vectors')}
</div> </div>
<div class="flex items-center relative"> <div class="flex items-center relative">
@@ -1730,7 +1754,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="flex justify-end pt-3 text-sm font-normal"> <div class="flex justify-end pt-6 text-sm font-normal">
<button <button
class="px-3.5 py-1.5 text-sm font-normal bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full" class="px-3.5 py-1.5 text-sm font-normal bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
type="submit" type="submit"
@@ -13,8 +13,10 @@
import Plus from '$lib/components/icons/Plus.svelte'; import Plus from '$lib/components/icons/Plus.svelte';
import Model from './Evaluations/Model.svelte'; import Model from './Evaluations/Model.svelte';
import ArenaModelModal from './Evaluations/ArenaModelModal.svelte'; import ArenaModelModal from './Evaluations/ArenaModelModal.svelte';
import AdminSettingRow from './AdminSettingRow.svelte';
import AdminSettingSection from './AdminSettingSection.svelte';
const i18n = getContext('i18n'); const i18n: any = getContext('i18n');
let evaluationConfig = null; let evaluationConfig = null;
let showAddModel = false; let showAddModel = false;
@@ -30,7 +32,9 @@
models.set( models.set(
await getModels( await getModels(
localStorage.token, localStorage.token,
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null) $config?.features?.enable_direct_connections
? ($settings?.directConnections ?? null)
: null
) )
); );
} }
@@ -44,7 +48,7 @@
models.set( models.set(
await getModels( await getModels(
localStorage.token, localStorage.token,
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null) $config?.features?.enable_direct_connections ? ($settings?.directConnections ?? null) : null
) )
); );
}; };
@@ -57,7 +61,7 @@
models.set( models.set(
await getModels( await getModels(
localStorage.token, localStorage.token,
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null) $config?.features?.enable_direct_connections ? ($settings?.directConnections ?? null) : null
) )
); );
}; };
@@ -100,75 +104,60 @@
dispatch('save'); dispatch('save');
}} }}
> >
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5 -mr-1.5"> <div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5">
{#if evaluationConfig !== null} {#if evaluationConfig !== null}
<div class=""> <AdminSettingSection first>
<div class="mb-3"> <AdminSettingRow
<div class=" mt-0.5 mb-2.5 text-xs text-gray-400 dark:text-gray-600"> label={$i18n.t('Arena Models')}
{$i18n.t('General')} description={$i18n.t('Message rating should be enabled to use this feature')}
</div> >
<Tooltip content={$i18n.t(`Message rating should be enabled to use this feature`)}>
<Switch bind:state={evaluationConfig.ENABLE_EVALUATION_ARENA_MODELS} />
</Tooltip>
</AdminSettingRow>
</AdminSettingSection>
<hr class=" border-gray-100/20 dark:border-white/[0.025] my-2" /> {#if evaluationConfig.ENABLE_EVALUATION_ARENA_MODELS}
<AdminSettingSection title={$i18n.t('Models')}>
<div class="mb-2 flex items-center justify-between">
<div class="text-xs text-gray-600 dark:text-gray-400">{$i18n.t('Arena Models')}</div>
<div class="mb-2.5 flex w-full justify-between"> <Tooltip content={$i18n.t('Add Arena Model')}>
<div class=" text-xs font-normal">{$i18n.t('Arena Models')}</div> <button
class="flex size-6 items-center justify-center rounded-lg text-gray-400 transition-colors hover:bg-black/5 hover:text-gray-900 dark:text-gray-600 dark:hover:bg-white/5 dark:hover:text-white"
<Tooltip content={$i18n.t(`Message rating should be enabled to use this feature`)}> type="button"
<Switch bind:state={evaluationConfig.ENABLE_EVALUATION_ARENA_MODELS} /> on:click={() => {
showAddModel = true;
}}
>
<Plus />
</button>
</Tooltip> </Tooltip>
</div> </div>
</div>
{#if evaluationConfig.ENABLE_EVALUATION_ARENA_MODELS} <div class="flex flex-col gap-2">
<div class="mb-3"> {#if (evaluationConfig?.EVALUATION_ARENA_MODELS ?? []).length > 0}
<div {#each evaluationConfig.EVALUATION_ARENA_MODELS as model, index}
class=" mt-0.5 mb-2.5 text-xs text-gray-400 dark:text-gray-600 flex justify-between items-center" <Model
> {model}
<div> on:edit={(e) => {
{$i18n.t('Manage')} editModelHandler(e.detail, index);
}}
on:delete={(e) => {
deleteModelHandler(index);
}}
/>
{/each}
{:else}
<div class="text-center text-[0.6875rem] text-gray-400 dark:text-gray-600">
{$i18n.t(
`Using the default arena model with all models. Click the plus button to add custom models.`
)}
</div> </div>
{/if}
<div>
<Tooltip content={$i18n.t('Add Arena Model')}>
<button
class="p-1"
type="button"
on:click={() => {
showAddModel = true;
}}
>
<Plus />
</button>
</Tooltip>
</div>
</div>
<hr class=" border-gray-100/20 dark:border-white/[0.025] my-2" />
<div class="flex flex-col gap-2">
{#if (evaluationConfig?.EVALUATION_ARENA_MODELS ?? []).length > 0}
{#each evaluationConfig.EVALUATION_ARENA_MODELS as model, index}
<Model
{model}
on:edit={(e) => {
editModelHandler(e.detail, index);
}}
on:delete={(e) => {
deleteModelHandler(index);
}}
/>
{/each}
{:else}
<div class=" text-center text-xs text-gray-500">
{$i18n.t(
`Using the default arena model with all models. Click the plus button to add custom models.`
)}
</div>
{/if}
</div>
</div> </div>
{/if} </AdminSettingSection>
</div> {/if}
{:else} {:else}
<div class="flex h-full justify-center"> <div class="flex h-full justify-center">
<div class="my-auto"> <div class="my-auto">
@@ -178,7 +167,7 @@
{/if} {/if}
</div> </div>
<div class="flex justify-end pt-3 text-sm font-normal"> <div class="flex justify-end pt-6 text-sm font-normal">
<button <button
class="px-3.5 py-1.5 text-sm font-normal bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full" class="px-3.5 py-1.5 text-sm font-normal bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
type="submit" type="submit"
@@ -412,7 +412,7 @@
> >
<input <input
id="event-webhook-name" id="event-webhook-name"
class={`w-full text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`} class={`w-full text-xs bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
type="text" type="text"
placeholder={$i18n.t('Identity audit')} placeholder={$i18n.t('Identity audit')}
autocomplete="off" autocomplete="off"
@@ -431,7 +431,7 @@
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<input <input
id="event-webhook-url" id="event-webhook-url"
class={`w-full text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`} class={`w-full text-xs bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
type="url" type="url"
placeholder="https://example.com/events" placeholder="https://example.com/events"
autocomplete="off" autocomplete="off"
@@ -482,7 +482,7 @@
<div class="flex flex-wrap gap-1 mb-2"> <div class="flex flex-wrap gap-1 mb-2">
{#each targetGroupIds as id} {#each targetGroupIds as id}
<div <div
class="flex items-center gap-1 rounded-full bg-gray-100 dark:bg-gray-850 px-2 py-1 text-xs" class="flex items-center gap-1 rounded-full bg-gray-100 dark:bg-white/[0.03] px-2 py-1 text-xs"
> >
<span class="truncate max-w-36">{selectedGroups[id]?.name ?? id}</span> <span class="truncate max-w-36">{selectedGroups[id]?.name ?? id}</span>
<span class="text-gray-500">{$i18n.t('group')}</span> <span class="text-gray-500">{$i18n.t('group')}</span>
@@ -498,7 +498,7 @@
{#each targetUserIds as id} {#each targetUserIds as id}
<div <div
class="flex items-center gap-1 rounded-full bg-gray-100 dark:bg-gray-850 px-2 py-1 text-xs" class="flex items-center gap-1 rounded-full bg-gray-100 dark:bg-white/[0.03] px-2 py-1 text-xs"
> >
<span class="truncate max-w-36">{selectedUsers[id]?.name ?? id}</span> <span class="truncate max-w-36">{selectedUsers[id]?.name ?? id}</span>
<span class="text-gray-500">{$i18n.t('user')}</span> <span class="text-gray-500">{$i18n.t('user')}</span>
@@ -516,7 +516,7 @@
<div class="relative"> <div class="relative">
<input <input
class={`w-full text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`} class={`w-full text-xs bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
type="text" type="text"
placeholder={$i18n.t('Search users or groups')} placeholder={$i18n.t('Search users or groups')}
autocomplete="off" autocomplete="off"
@@ -585,7 +585,7 @@
<div class="flex flex-wrap gap-1"> <div class="flex flex-wrap gap-1">
{#each form.events as event} {#each form.events as event}
<div <div
class="flex items-center gap-1 rounded-full bg-gray-100 dark:bg-gray-850 px-2 py-1 text-xs" class="flex items-center gap-1 rounded-full bg-gray-100 dark:bg-white/[0.03] px-2 py-1 text-xs"
> >
<span class="font-mono break-all">{event}</span> <span class="font-mono break-all">{event}</span>
<button <button
@@ -602,7 +602,7 @@
<div class="flex gap-2"> <div class="flex gap-2">
<input <input
class={`w-full flex-1 text-sm bg-transparent font-mono ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`} class={`w-full flex-1 text-xs bg-transparent font-mono ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
type="text" type="text"
placeholder={$i18n.t('Search or add pattern')} placeholder={$i18n.t('Search or add pattern')}
autocomplete="off" autocomplete="off"
@@ -689,7 +689,7 @@
<div class="mb-3"> <div class="mb-3">
<div class="mt-0.5 mb-2.5 text-xs text-gray-400 dark:text-gray-600">{$i18n.t('Events')}</div> <div class="mt-0.5 mb-2.5 text-xs text-gray-400 dark:text-gray-600">{$i18n.t('Events')}</div>
<hr class="border-gray-100/20 dark:border-white/[0.025] my-2" /> <hr class="border-gray-100/10 dark:border-white/[0.015] my-2" />
<div class="mb-2.5 flex flex-col w-full justify-between"> <div class="mb-2.5 flex flex-col w-full justify-between">
<div class="flex justify-between items-center mb-1"> <div class="flex justify-between items-center mb-1">
@@ -788,18 +788,16 @@
</div> </div>
</Modal> </Modal>
<div class="mb-2.5 flex flex-col w-full justify-between text-sm"> <div class="flex w-full flex-col justify-between text-xs">
<div class="flex justify-between items-center mb-1"> <div class="mb-2 flex items-center justify-between">
<div class="flex items-center gap-2"> <div class="text-gray-600 dark:text-gray-400">{$i18n.t('Sources')}</div>
<div class="font-normal">{$i18n.t('External Knowledge Sources')}</div>
<span
class="text-[0.65rem] font-normal uppercase px-1.5 py-0.5 rounded-full bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400"
>{$i18n.t('Experimental')}</span
>
</div>
<Tooltip content={$i18n.t('Add Connection')}> <Tooltip content={$i18n.t('Add Connection')}>
<button class="px-1" on:click={openCreateSource} type="button"> <button
class="flex size-6 items-center justify-center rounded-lg text-gray-400 transition-colors hover:bg-black/5 hover:text-gray-900 dark:text-gray-600 dark:hover:bg-white/5 dark:hover:text-white"
on:click={openCreateSource}
type="button"
>
<Plus /> <Plus />
</button> </button>
</Tooltip> </Tooltip>
@@ -821,7 +819,7 @@
<DatabaseSettings className="size-4" strokeWidth="1.5" /> <DatabaseSettings className="size-4" strokeWidth="1.5" />
</Tooltip> </Tooltip>
<div class="outline-hidden w-full bg-transparent text-sm min-w-0 line-clamp-1"> <div class="outline-hidden w-full bg-transparent text-xs min-w-0 line-clamp-1">
<span>{item.name}</span> <span>{item.name}</span>
{' '} {' '}
<span class="text-gray-500"> <span class="text-gray-500">
@@ -868,16 +866,14 @@
<Spinner /> <Spinner />
</div> </div>
{:else if items.length === 0} {:else if items.length === 0}
<div class="text-xs text-gray-400 dark:text-gray-500"> <div class="text-[0.6875rem] text-gray-400 dark:text-gray-600">
{$i18n.t('No external knowledge sources configured.')} {$i18n.t('No external knowledge sources configured.')}
</div> </div>
{/if} {/if}
<div class="my-1.5"> {#if items.length === 0}
<div class="text-xs text-gray-500"> <div class="text-[0.6875rem] text-gray-400 dark:text-gray-600">
{$i18n.t( {$i18n.t('Test must pass before a source is created.')}
'Create one read-only Knowledge source per external collection. Test must pass before the source is created.'
)}
</div> </div>
</div> {/if}
</div> </div>
+230 -339
View File
@@ -16,20 +16,27 @@
import Textarea from '$lib/components/common/Textarea.svelte'; import Textarea from '$lib/components/common/Textarea.svelte';
import Banners from './Interface/Banners.svelte'; import Banners from './Interface/Banners.svelte';
import Events from './Events.svelte'; import Events from './Events.svelte';
import AdminSettingField from './AdminSettingField.svelte';
import AdminSettingRow from './AdminSettingRow.svelte';
import AdminSettingSection from './AdminSettingSection.svelte';
const i18n = getContext('i18n'); const i18n: any = getContext('i18n');
export let saveHandler: Function; export let saveHandler: Function;
let updateAvailable = false; let updateAvailable: boolean | null = false;
let version = { let version = {
current: WEBUI_VERSION, current: WEBUI_VERSION,
latest: WEBUI_VERSION latest: WEBUI_VERSION
}; };
let adminConfig = null; let adminConfig: any = null;
let banners: Banner[] = []; let banners: Banner[] = [];
const inputClass =
'w-full h-7 rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 text-xs text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500';
const textareaClass =
'w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500';
const checkForVersionUpdates = async () => { const checkForVersionUpdates = async () => {
updateAvailable = null; updateAvailable = null;
@@ -72,378 +79,262 @@
</script> </script>
<form <form
class="flex flex-col h-full justify-between space-y-3 text-sm" class="flex h-full flex-col justify-between text-sm"
on:submit|preventDefault={async () => { on:submit|preventDefault={async () => {
updateHandler(); updateHandler();
}} }}
> >
<div class="space-y-3 flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5 -mr-1.5"> <div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5">
{#if adminConfig !== null} {#if adminConfig !== null}
<div class=""> <AdminSettingSection first>
<div class="mb-3.5"> <div class="flex items-start justify-between gap-4">
<div class=" mt-0.5 mb-2.5 text-xs text-gray-400 dark:text-gray-600"> <div class="min-w-0 text-xs">
{$i18n.t('General')} <div class="text-gray-600 dark:text-gray-400">{$i18n.t('Version')}</div>
</div> <div class="mt-1 flex flex-wrap gap-x-1 text-gray-700 dark:text-gray-200">
<Tooltip content={WEBUI_BUILD_HASH}>v{WEBUI_VERSION}</Tooltip>
<hr class=" border-gray-100/20 dark:border-white/[0.025] my-2" />
<div class="mb-2.5">
<div class=" mb-1 text-xs font-normal flex space-x-2 items-center">
<div>
{$i18n.t('Version')}
</div>
</div>
<div class="flex w-full justify-between items-center">
<div class="flex flex-col text-xs text-gray-700 dark:text-gray-200">
<div class="flex gap-1">
<Tooltip content={WEBUI_BUILD_HASH}>
v{WEBUI_VERSION}
</Tooltip>
{#if $config?.features?.enable_version_update_check}
<a
href="https://github.com/open-webui/open-webui/releases/tag/v{version.latest}"
target="_blank"
>
{updateAvailable === null
? $i18n.t('Checking for updates...')
: updateAvailable
? `(v${version.latest} ${$i18n.t('available!')})`
: $i18n.t('(latest)')}
</a>
{/if}
</div>
<button
class=" underline flex items-center space-x-1 text-xs text-gray-500 dark:text-gray-500"
type="button"
on:click={() => {
showChangelog.set(true);
}}
>
<div>{$i18n.t("See what's new")}</div>
</button>
</div>
{#if $config?.features?.enable_version_update_check} {#if $config?.features?.enable_version_update_check}
<button <a
class=" text-xs px-3 py-1.5 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-lg font-normal" href="https://github.com/open-webui/open-webui/releases/tag/v{version.latest}"
type="button" target="_blank"
on:click={() => { class="text-gray-500 hover:text-gray-700 dark:text-gray-500 dark:hover:text-gray-300"
checkForVersionUpdates();
}}
> >
{$i18n.t('Check for updates')} {updateAvailable === null
</button> ? $i18n.t('Checking for updates...')
: updateAvailable
? `(v${version.latest} ${$i18n.t('available!')})`
: $i18n.t('(latest)')}
</a>
{/if} {/if}
</div> </div>
<button
class="mt-0.5 text-xs text-gray-400 transition-colors hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-300"
type="button"
on:click={() => {
showChangelog.set(true);
}}
>
{$i18n.t("See what's new")}
</button>
</div> </div>
<div class="mb-2.5"> {#if $config?.features?.enable_version_update_check}
<div class="flex w-full justify-between items-center"> <button
<div class="text-xs pr-2"> class="shrink-0 text-xs text-gray-500 transition-colors hover:text-gray-900 dark:text-gray-500 dark:hover:text-white"
<div class=""> type="button"
{$i18n.t('Help')} on:click={() => {
</div> checkForVersionUpdates();
<div class=" text-xs text-gray-500"> }}
{$i18n.t('Discover how to use Open WebUI and seek support from the community.')} >
</div> {$i18n.t('Check for updates')}
</div> </button>
{/if}
</div>
<a <div class="text-xs">
class="flex-shrink-0 text-xs font-normal underline" <div class="flex items-start justify-between gap-4">
href="https://docs.openwebui.com/" <div class="min-w-0">
target="_blank" <div class="text-gray-600 dark:text-gray-400">{$i18n.t('Help')}</div>
> <div class="mt-0.5 text-gray-400 dark:text-gray-600">
{$i18n.t('Documentation')} {$i18n.t('Discover how to use Open WebUI and seek support from the community.')}
</a>
</div>
<div class="mt-1">
<div class="flex space-x-1">
<a href="https://discord.gg/5rJgQTnV4s" target="_blank">
<img
alt="Discord"
src="https://img.shields.io/badge/Discord-Open_WebUI-blue?logo=discord&logoColor=white"
/>
</a>
<a href="https://twitter.com/OpenWebUI" target="_blank">
<img
alt="X (formerly Twitter) Follow"
src="https://img.shields.io/twitter/follow/OpenWebUI"
/>
</a>
<a href="https://github.com/open-webui/open-webui" target="_blank">
<img
alt="Github Repo"
src="https://img.shields.io/github/stars/open-webui/open-webui?style=social&label=Star us on Github"
/>
</a>
</div> </div>
</div> </div>
<a
class="shrink-0 text-gray-500 transition-colors hover:text-gray-900 dark:text-gray-500 dark:hover:text-white"
href="https://docs.openwebui.com/"
target="_blank"
>
{$i18n.t('Documentation')}
</a>
</div> </div>
<div class="mb-2.5"> <div class="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-gray-400 dark:text-gray-600">
<div class="flex w-full justify-between items-center"> <a
<div class="text-xs pr-2"> class="hover:text-gray-700 dark:hover:text-gray-300"
<div class=""> href="https://discord.gg/5rJgQTnV4s"
{$i18n.t('License')} target="_blank">Discord</a
</div> >
<a
{#if $config?.license_metadata} class="hover:text-gray-700 dark:hover:text-gray-300"
<a href="https://twitter.com/OpenWebUI"
href="https://docs.openwebui.com/enterprise" target="_blank">X</a
target="_blank" >
class="text-gray-500 mt-0.5" <a
> class="hover:text-gray-700 dark:hover:text-gray-300"
<span class=" capitalize text-black dark:text-white" href="https://github.com/open-webui/open-webui"
>{$config?.license_metadata?.type} target="_blank">GitHub</a
license</span >
>
registered to
<span class=" capitalize text-black dark:text-white"
>{$config?.license_metadata?.organization_name}</span
>
for
<span class=" font-normal text-black dark:text-white"
>{$config?.license_metadata?.seats ?? 'Unlimited'} users.</span
>
</a>
{#if $config?.license_metadata?.html}
<div class="mt-0.5">
{@html DOMPurify.sanitize($config?.license_metadata?.html)}
</div>
{/if}
{:else}
<a
class=" text-xs hover:underline"
href="https://docs.openwebui.com/enterprise"
target="_blank"
>
<span class="text-gray-500">
{$i18n.t(
'Upgrade to a licensed plan for enhanced capabilities, including custom theming and branding, and dedicated support.'
)}
</span>
</a>
{/if}
</div>
<!-- <button
class="flex-shrink-0 text-xs px-3 py-1.5 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-lg font-normal"
>
{$i18n.t('Activate')}
</button> -->
</div>
</div> </div>
</div> </div>
<div class="mb-3"> <div class="text-xs">
<div class=" mt-0.5 mb-2.5 text-xs text-gray-400 dark:text-gray-600"> <div class="text-gray-600 dark:text-gray-400">{$i18n.t('License')}</div>
{$i18n.t('Features')}
</div>
<hr class=" border-gray-100/20 dark:border-white/[0.025] my-2" /> {#if $config?.license_metadata}
<a
<div class="mb-2.5 flex w-full items-center justify-between pr-2"> href="https://docs.openwebui.com/enterprise"
<div class=" self-center text-xs font-normal"> target="_blank"
{$i18n.t('Enable Community Sharing')} class="mt-0.5 block text-gray-500"
</div> >
<span class="capitalize text-black dark:text-white"
<Switch bind:state={adminConfig.ENABLE_COMMUNITY_SHARING} /> >{$config?.license_metadata?.type} license</span
</div> >
registered to
<div class="mb-2.5 flex w-full items-center justify-between pr-2"> <span class="capitalize text-black dark:text-white"
<div class=" self-center text-xs font-normal">{$i18n.t('Enable Message Rating')}</div> >{$config?.license_metadata?.organization_name}</span
>
<Switch bind:state={adminConfig.ENABLE_MESSAGE_RATING} /> for
</div> <span class="text-black dark:text-white"
>{$config?.license_metadata?.seats ?? 'Unlimited'} users.</span
<div class="mb-2.5 flex w-full items-center justify-between pr-2"> >
<div class=" self-center text-xs font-normal"> </a>
{$i18n.t('Folders')} {#if $config?.license_metadata?.html}
</div> <div class="mt-0.5 text-gray-500">
{@html DOMPurify.sanitize($config?.license_metadata?.html)}
<Switch bind:state={adminConfig.ENABLE_FOLDERS} />
</div>
{#if adminConfig.ENABLE_FOLDERS}
<div class="mb-2.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-normal">
{$i18n.t('Folder Max File Count')}
</div>
</div> </div>
{/if}
<div class="flex mt-2 space-x-2"> {:else}
<input <a
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="mt-0.5 block text-gray-400 transition-colors hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-300"
type="number" href="https://docs.openwebui.com/enterprise"
min="0" target="_blank"
placeholder={$i18n.t('Leave empty for unlimited')} >
bind:value={adminConfig.FOLDER_MAX_FILE_COUNT}
/>
</div>
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
{$i18n.t('Maximum number of files allowed per folder.')}
</div>
</div>
{/if}
<div class="mb-2.5 flex w-full items-center justify-between pr-2">
<div class=" self-center text-xs font-normal">
{$i18n.t('Memories')}
</div>
<Switch bind:state={adminConfig.ENABLE_MEMORIES} />
</div>
{#if adminConfig.ENABLE_MEMORIES}
<div class="mb-2.5 flex w-full items-center justify-between pr-2 pl-4">
<div class=" self-center text-xs font-normal text-gray-500 dark:text-gray-400">
{$i18n.t('Memory System Context')}
</div>
<Switch bind:state={adminConfig.ENABLE_MEMORY_SYSTEM_CONTEXT} />
</div>
{/if}
<div class="mb-2.5 flex w-full items-center justify-between pr-2">
<div class=" self-center text-xs font-normal">
{$i18n.t('Notes')}
</div>
<Switch bind:state={adminConfig.ENABLE_NOTES} />
</div>
<div class="mb-2.5 flex w-full items-center justify-between pr-2">
<div class=" self-center text-xs font-normal">
{$i18n.t('Channels')}
</div>
<Switch bind:state={adminConfig.ENABLE_CHANNELS} />
</div>
<div class="mb-2.5 flex w-full items-center justify-between pr-2">
<div class=" self-center text-xs font-normal">
{$i18n.t('Calendar')}
</div>
<Switch bind:state={adminConfig.ENABLE_CALENDAR} />
</div>
<div class="mb-2.5 flex w-full items-center justify-between pr-2">
<div class=" self-center text-xs font-normal">
{$i18n.t('Automations')}
</div>
<Switch bind:state={adminConfig.ENABLE_AUTOMATIONS} />
</div>
<div class="mb-2.5 flex w-full items-center justify-between pr-2">
<div class=" self-center text-xs font-normal">
{$i18n.t('User Webhooks')}
</div>
<Switch bind:state={adminConfig.ENABLE_USER_WEBHOOKS} />
</div>
<div class="mb-2.5 flex w-full items-center justify-between pr-2">
<div class=" self-center text-xs font-normal">
{$i18n.t('User Status')}
</div>
<Switch bind:state={adminConfig.ENABLE_USER_STATUS} />
</div>
<div class="mb-2.5">
<div class=" self-center text-xs font-normal mb-2">
{$i18n.t('Response Watermark')}
</div>
<Textarea
placeholder={$i18n.t('Enter a watermark for the response. Leave empty for none.')}
bind:value={adminConfig.RESPONSE_WATERMARK}
/>
</div>
<div class="mb-2.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-normal">{$i18n.t('WebUI URL')}</div>
</div>
<div class="flex mt-2 space-x-2">
<input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
type="text"
placeholder={`e.g.) "http://localhost:3000"`}
bind:value={adminConfig.WEBUI_URL}
/>
</div>
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
{$i18n.t( {$i18n.t(
'Enter the public URL of your WebUI. This URL will be used to generate links in the notifications.' 'Upgrade to a licensed plan for enhanced capabilities, including custom theming and branding, and dedicated support.'
)} )}
</div> </a>
</div> {/if}
</div> </div>
</AdminSettingSection>
<Events /> <AdminSettingSection title={$i18n.t('Features')}>
<AdminSettingRow label={$i18n.t('Enable Community Sharing')}>
<Switch bind:state={adminConfig.ENABLE_COMMUNITY_SHARING} />
</AdminSettingRow>
<AdminSettingRow label={$i18n.t('Enable Message Rating')}>
<Switch bind:state={adminConfig.ENABLE_MESSAGE_RATING} />
</AdminSettingRow>
<AdminSettingRow label={$i18n.t('Folders')}>
<Switch bind:state={adminConfig.ENABLE_FOLDERS} />
</AdminSettingRow>
<div class="mb-3.5"> {#if adminConfig.ENABLE_FOLDERS}
<div class=" mt-0.5 mb-2.5 text-xs text-gray-400 dark:text-gray-600">{$i18n.t('UI')}</div> <AdminSettingField
label={$i18n.t('Folder Max File Count')}
description={$i18n.t('Maximum number of files allowed per folder.')}
>
<input
class={inputClass}
type="number"
min="0"
placeholder={$i18n.t('Leave empty for unlimited')}
bind:value={adminConfig.FOLDER_MAX_FILE_COUNT}
/>
</AdminSettingField>
{/if}
<hr class=" border-gray-100/20 dark:border-white/[0.025] my-2" /> <AdminSettingRow label={$i18n.t('Memories')}>
<Switch bind:state={adminConfig.ENABLE_MEMORIES} />
</AdminSettingRow>
{#if adminConfig.ENABLE_MEMORIES}
<AdminSettingRow
label={$i18n.t('Memory System Context')}
labelClassName="pl-4 text-gray-500 dark:text-gray-500"
>
<Switch bind:state={adminConfig.ENABLE_MEMORY_SYSTEM_CONTEXT} />
</AdminSettingRow>
{/if}
<AdminSettingRow label={$i18n.t('Notes')}>
<Switch bind:state={adminConfig.ENABLE_NOTES} />
</AdminSettingRow>
<AdminSettingRow label={$i18n.t('Channels')}>
<Switch bind:state={adminConfig.ENABLE_CHANNELS} />
</AdminSettingRow>
<AdminSettingRow label={$i18n.t('Calendar')}>
<Switch bind:state={adminConfig.ENABLE_CALENDAR} />
</AdminSettingRow>
<AdminSettingRow label={$i18n.t('Automations')}>
<Switch bind:state={adminConfig.ENABLE_AUTOMATIONS} />
</AdminSettingRow>
<AdminSettingRow label={$i18n.t('User Webhooks')}>
<Switch bind:state={adminConfig.ENABLE_USER_WEBHOOKS} />
</AdminSettingRow>
<AdminSettingRow label={$i18n.t('User Status')}>
<Switch bind:state={adminConfig.ENABLE_USER_STATUS} />
</AdminSettingRow>
<div class="mb-2.5"> <AdminSettingField label={$i18n.t('Response Watermark')}>
<div class="flex w-full justify-between"> <Textarea
<div class=" self-center text-xs"> className={textareaClass}
{$i18n.t('Banners')} placeholder={$i18n.t('Enter a watermark for the response. Leave empty for none.')}
</div> bind:value={adminConfig.RESPONSE_WATERMARK}
/>
</AdminSettingField>
<button <AdminSettingField
class="p-1 px-3 text-xs flex rounded-sm transition" label={$i18n.t('WebUI URL')}
type="button" description={$i18n.t(
on:click={() => { 'Enter the public URL of your WebUI. This URL will be used to generate links in the notifications.'
if (banners.length === 0 || banners.at(-1).content !== '') { )}
banners = [ >
...banners, <input
{ class={inputClass}
id: uuidv4(), type="text"
type: '', placeholder={`e.g.) "http://localhost:3000"`}
title: '', bind:value={adminConfig.WEBUI_URL}
content: '', />
dismissible: true, </AdminSettingField>
timestamp: Math.floor(Date.now() / 1000) </AdminSettingSection>
}
]; <Events />
}
}} <AdminSettingSection title={$i18n.t('UI')}>
<div>
<div class="mb-2 flex w-full items-center justify-between">
<div class="text-xs text-gray-600 dark:text-gray-400">{$i18n.t('Banners')}</div>
<button
class="flex size-6 items-center justify-center rounded-lg text-gray-400 transition-colors hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-300"
type="button"
aria-label={$i18n.t('Add banner')}
on:click={() => {
if (banners.length === 0 || banners[banners.length - 1]?.content !== '') {
banners = [
...banners,
{
id: uuidv4(),
type: '',
title: '',
content: '',
dismissible: true,
timestamp: Math.floor(Date.now() / 1000)
}
];
}
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="size-4"
> >
<svg <path
xmlns="http://www.w3.org/2000/svg" d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
viewBox="0 0 20 20" />
fill="currentColor" </svg>
class="w-4 h-4" </button>
>
<path
d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
/>
</svg>
</button>
</div>
<Banners bind:banners />
</div> </div>
<Banners bind:banners />
</div> </div>
</div> </AdminSettingSection>
{/if} {/if}
</div> </div>
<div class="flex justify-end pt-3 text-sm font-normal"> <div class="flex justify-end pt-6 text-sm font-normal">
<button <button
class="px-3.5 py-1.5 text-sm font-normal bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full" class="px-3.5 py-1.5 text-sm font-normal bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
type="submit" type="submit"
+28 -38
View File
@@ -284,21 +284,15 @@
</script> </script>
<form <form
class="flex flex-col h-full justify-between space-y-3 text-sm" class="flex h-full flex-col justify-between text-sm"
on:submit|preventDefault={async () => { on:submit|preventDefault={async () => {
saveHandler(); saveHandler();
}} }}
> >
<div class=" space-y-3 overflow-y-scroll scrollbar-hidden pr-2"> <div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5">
{#if config} {#if config}
<div> <div>
<div class="mb-3"> <div class="mb-5">
<div class=" mt-0.5 mb-2.5 text-xs text-gray-400 dark:text-gray-600">
{$i18n.t('General')}
</div>
<hr class=" border-gray-100/20 dark:border-white/[0.025] my-2" />
<div class="mb-2.5"> <div class="mb-2.5">
<div class="flex w-full justify-between items-center"> <div class="flex w-full justify-between items-center">
<div class="text-xs pr-2"> <div class="text-xs pr-2">
@@ -312,13 +306,11 @@
</div> </div>
</div> </div>
<div class="mb-3"> <div class="mb-5">
<div class=" mt-0.5 mb-2.5 text-xs text-gray-400 dark:text-gray-600"> <div class=" mb-2 text-xs text-gray-400 dark:text-gray-600">
{$i18n.t('Create Image')} {$i18n.t('Create Image')}
</div> </div>
<hr class=" border-gray-100/20 dark:border-white/[0.025] my-2" />
{#if config.ENABLE_IMAGE_GENERATION} {#if config.ENABLE_IMAGE_GENERATION}
<div class="mb-2.5"> <div class="mb-2.5">
<div class="flex w-full justify-between items-center"> <div class="flex w-full justify-between items-center">
@@ -331,7 +323,7 @@
<Tooltip content={$i18n.t('Enter Model ID')} placement="top-start"> <Tooltip content={$i18n.t('Enter Model ID')} placement="top-start">
<input <input
list="model-list" list="model-list"
class=" text-right text-sm bg-transparent outline-hidden max-w-full w-52" class=" text-right text-xs bg-transparent outline-hidden max-w-full w-52"
bind:value={config.IMAGE_GENERATION_MODEL} bind:value={config.IMAGE_GENERATION_MODEL}
placeholder={$i18n.t('Select a model')} placeholder={$i18n.t('Select a model')}
required required
@@ -356,7 +348,7 @@
<Tooltip content={$i18n.t('Enter Image Size (e.g. 512x512)')} placement="top-start"> <Tooltip content={$i18n.t('Enter Image Size (e.g. 512x512)')} placement="top-start">
<input <input
class=" text-right text-sm bg-transparent outline-hidden max-w-full w-52" class=" text-right text-xs bg-transparent outline-hidden max-w-full w-52"
placeholder={$i18n.t('Enter Image Size (e.g. 512x512)')} placeholder={$i18n.t('Enter Image Size (e.g. 512x512)')}
bind:value={config.IMAGE_SIZE} bind:value={config.IMAGE_SIZE}
/> />
@@ -378,7 +370,7 @@
placement="top-start" placement="top-start"
> >
<input <input
class=" text-right text-sm bg-transparent outline-hidden" class=" text-right text-xs bg-transparent outline-hidden"
placeholder={$i18n.t('Enter Number of Steps (e.g. 50)')} placeholder={$i18n.t('Enter Number of Steps (e.g. 50)')}
bind:value={config.IMAGE_STEPS} bind:value={config.IMAGE_STEPS}
required required
@@ -410,7 +402,7 @@
</div> </div>
<select <select
class="w-fit pr-8 cursor-pointer rounded-sm px-2 text-xs bg-transparent outline-hidden text-right" class="w-fit pr-8 cursor-pointer rounded-lg px-2 text-xs bg-transparent outline-hidden text-right"
bind:value={config.IMAGE_GENERATION_ENGINE} bind:value={config.IMAGE_GENERATION_ENGINE}
placeholder={$i18n.t('Select Engine')} placeholder={$i18n.t('Select Engine')}
> >
@@ -434,7 +426,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full text-sm bg-transparent outline-hidden text-right" class="w-full text-xs bg-transparent outline-hidden text-right"
placeholder={$i18n.t('API Base URL')} placeholder={$i18n.t('API Base URL')}
bind:value={config.IMAGES_OPENAI_API_BASE_URL} bind:value={config.IMAGES_OPENAI_API_BASE_URL}
/> />
@@ -475,7 +467,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full text-sm bg-transparent outline-hidden text-right" class="w-full text-xs bg-transparent outline-hidden text-right"
placeholder={$i18n.t('API Version')} placeholder={$i18n.t('API Version')}
bind:value={config.IMAGES_OPENAI_API_VERSION} bind:value={config.IMAGES_OPENAI_API_VERSION}
/> />
@@ -495,7 +487,7 @@
<div class="mt-1.5 flex w-full"> <div class="mt-1.5 flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<Textarea <Textarea
className="rounded-lg w-full py-2 px-3 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" className="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500"
bind:value={config.IMAGES_OPENAI_API_PARAMS} bind:value={config.IMAGES_OPENAI_API_PARAMS}
placeholder={$i18n.t('Enter additional parameters in JSON format')} placeholder={$i18n.t('Enter additional parameters in JSON format')}
minSize={100} minSize={100}
@@ -515,7 +507,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="w-full text-sm bg-transparent outline-hidden text-right" class="w-full text-xs bg-transparent outline-hidden text-right"
placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')} placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
bind:value={config.AUTOMATIC1111_BASE_URL} bind:value={config.AUTOMATIC1111_BASE_URL}
/> />
@@ -609,7 +601,7 @@
<div class="mt-1.5 flex w-full"> <div class="mt-1.5 flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<Textarea <Textarea
className="rounded-lg w-full py-2 px-3 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" className="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500"
bind:value={config.AUTOMATIC1111_PARAMS} bind:value={config.AUTOMATIC1111_PARAMS}
placeholder={$i18n.t('Enter additional parameters in JSON format')} placeholder={$i18n.t('Enter additional parameters in JSON format')}
minSize={100} minSize={100}
@@ -629,7 +621,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="w-full text-sm bg-transparent outline-hidden text-right" class="w-full text-xs bg-transparent outline-hidden text-right"
placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')} placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
bind:value={config.COMFYUI_BASE_URL} bind:value={config.COMFYUI_BASE_URL}
/> />
@@ -759,7 +751,7 @@
/> />
<!-- {#if config.COMFYUI_WORKFLOW} <!-- {#if config.COMFYUI_WORKFLOW}
<Textarea <Textarea
class="w-full rounded-lg my-1 py-2 px-3 text-xs bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden disabled:text-gray-600 resize-none" className="my-1 w-full resize-none rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 disabled:text-gray-600 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500"
rows="10" rows="10"
bind:value={config.COMFYUI_WORKFLOW} bind:value={config.COMFYUI_WORKFLOW}
required required
@@ -836,7 +828,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full text-sm bg-transparent outline-hidden text-right" class="w-full text-xs bg-transparent outline-hidden text-right"
placeholder={$i18n.t('API Base URL')} placeholder={$i18n.t('API Base URL')}
bind:value={config.IMAGES_GEMINI_API_BASE_URL} bind:value={config.IMAGES_GEMINI_API_BASE_URL}
/> />
@@ -875,7 +867,7 @@
</div> </div>
<select <select
class="w-fit pr-8 cursor-pointer rounded-sm px-2 text-xs bg-transparent outline-hidden text-right" class="w-fit pr-8 cursor-pointer rounded-lg px-2 text-xs bg-transparent outline-hidden text-right"
bind:value={config.IMAGES_GEMINI_ENDPOINT_METHOD} bind:value={config.IMAGES_GEMINI_ENDPOINT_METHOD}
placeholder={$i18n.t('Select Method')} placeholder={$i18n.t('Select Method')}
> >
@@ -887,13 +879,11 @@
{/if} {/if}
</div> </div>
<div class="mb-3"> <div class="mb-5">
<div class=" mt-0.5 mb-2.5 text-xs text-gray-400 dark:text-gray-600"> <div class=" mb-2 text-xs text-gray-400 dark:text-gray-600">
{$i18n.t('Edit Image')} {$i18n.t('Edit Image')}
</div> </div>
<hr class=" border-gray-100/20 dark:border-white/[0.025] my-2" />
<div class="mb-2.5"> <div class="mb-2.5">
<div class="flex w-full justify-between items-center"> <div class="flex w-full justify-between items-center">
<div class="text-xs pr-2"> <div class="text-xs pr-2">
@@ -918,7 +908,7 @@
<Tooltip content={$i18n.t('Enter Model ID')} placement="top-start"> <Tooltip content={$i18n.t('Enter Model ID')} placement="top-start">
<input <input
list="model-list" list="model-list"
class="text-right text-sm bg-transparent outline-hidden max-w-full w-52" class="text-right text-xs bg-transparent outline-hidden max-w-full w-52"
bind:value={config.IMAGE_EDIT_MODEL} bind:value={config.IMAGE_EDIT_MODEL}
placeholder={$i18n.t('Select a model')} placeholder={$i18n.t('Select a model')}
/> />
@@ -942,7 +932,7 @@
<Tooltip content={$i18n.t('Enter Image Size (e.g. 512x512)')} placement="top-start"> <Tooltip content={$i18n.t('Enter Image Size (e.g. 512x512)')} placement="top-start">
<input <input
class="text-right text-sm bg-transparent outline-hidden max-w-full w-52" class="text-right text-xs bg-transparent outline-hidden max-w-full w-52"
placeholder={$i18n.t('Enter Image Size (e.g. 512x512)')} placeholder={$i18n.t('Enter Image Size (e.g. 512x512)')}
bind:value={config.IMAGE_EDIT_SIZE} bind:value={config.IMAGE_EDIT_SIZE}
/> />
@@ -960,7 +950,7 @@
</div> </div>
<select <select
class="w-fit pr-8 cursor-pointer rounded-sm px-2 text-xs bg-transparent outline-hidden text-right" class="w-fit pr-8 cursor-pointer rounded-lg px-2 text-xs bg-transparent outline-hidden text-right"
bind:value={config.IMAGE_EDIT_ENGINE} bind:value={config.IMAGE_EDIT_ENGINE}
placeholder={$i18n.t('Select Engine')} placeholder={$i18n.t('Select Engine')}
> >
@@ -983,7 +973,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full text-sm bg-transparent outline-hidden text-right" class="w-full text-xs bg-transparent outline-hidden text-right"
placeholder={$i18n.t('API Base URL')} placeholder={$i18n.t('API Base URL')}
bind:value={config.IMAGES_EDIT_OPENAI_API_BASE_URL} bind:value={config.IMAGES_EDIT_OPENAI_API_BASE_URL}
/> />
@@ -1024,7 +1014,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full text-sm bg-transparent outline-hidden text-right" class="w-full text-xs bg-transparent outline-hidden text-right"
placeholder={$i18n.t('API Version')} placeholder={$i18n.t('API Version')}
bind:value={config.IMAGES_EDIT_OPENAI_API_VERSION} bind:value={config.IMAGES_EDIT_OPENAI_API_VERSION}
/> />
@@ -1044,7 +1034,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="w-full text-sm bg-transparent outline-hidden text-right" class="w-full text-xs bg-transparent outline-hidden text-right"
placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')} placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
bind:value={config.IMAGES_EDIT_COMFYUI_BASE_URL} bind:value={config.IMAGES_EDIT_COMFYUI_BASE_URL}
/> />
@@ -1243,7 +1233,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full text-sm bg-transparent outline-hidden text-right" class="w-full text-xs bg-transparent outline-hidden text-right"
placeholder={$i18n.t('API Base URL')} placeholder={$i18n.t('API Base URL')}
bind:value={config.IMAGES_EDIT_GEMINI_API_BASE_URL} bind:value={config.IMAGES_EDIT_GEMINI_API_BASE_URL}
/> />
@@ -1278,7 +1268,7 @@
{/if} {/if}
</div> </div>
<div class="flex justify-end pt-3 text-sm font-normal"> <div class="flex justify-end pt-6 text-sm font-normal">
<button <button
class="px-3.5 py-1.5 text-sm font-normal bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full flex items-center gap-2 whitespace-nowrap {loading class="px-3.5 py-1.5 text-sm font-normal bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full flex items-center gap-2 whitespace-nowrap {loading
? ' cursor-not-allowed' ? ' cursor-not-allowed'
@@ -24,6 +24,7 @@
import AddToolServerModal from '$lib/components/AddToolServerModal.svelte'; import AddToolServerModal from '$lib/components/AddToolServerModal.svelte';
import AddTerminalServerModal from '$lib/components/AddTerminalServerModal.svelte'; import AddTerminalServerModal from '$lib/components/AddTerminalServerModal.svelte';
import ExternalKnowledge from './ExternalKnowledge.svelte'; import ExternalKnowledge from './ExternalKnowledge.svelte';
import AdminSettingSection from './AdminSettingSection.svelte';
import { import {
getToolServerConnections, getToolServerConnections,
@@ -155,171 +156,159 @@
/> />
<form <form
class="flex flex-col h-full justify-between text-sm" class="flex h-full flex-col justify-between text-sm"
on:submit|preventDefault={() => { on:submit|preventDefault={() => {
updateHandler(); updateHandler();
}} }}
> >
<div class=" flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5 -mr-1.5"> <div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5">
{#if servers !== null} {#if servers !== null}
<div class=""> <AdminSettingSection title={$i18n.t('Tools')} first>
<div class="mb-3"> <div>
<div class=" mt-0.5 mb-2.5 text-xs text-gray-400 dark:text-gray-600"> <div class="mb-2 flex items-center justify-between">
{$i18n.t('Tools')} <div class="text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('External Tool Servers')}
</div>
<Tooltip content={$i18n.t(`Add Connection`)}>
<button
class="flex size-6 items-center justify-center rounded-lg text-gray-400 transition-colors hover:bg-black/5 hover:text-gray-900 dark:text-gray-600 dark:hover:bg-white/5 dark:hover:text-white"
on:click={() => {
showConnectionModal = true;
}}
type="button"
>
<Plus />
</button>
</Tooltip>
</div> </div>
<hr class=" border-gray-100/20 dark:border-white/[0.025] my-2" /> <div class="flex flex-col gap-1">
{#each servers ?? [] as server, idx}
<div class="mb-2.5 flex flex-col w-full justify-between"> <Connection
<div class="flex justify-between items-center mb-0.5"> bind:connection={server}
<div class="font-normal">{$i18n.t('External Tool Servers')}</div> onSubmit={() => {
updateHandler();
<Tooltip content={$i18n.t(`Add Connection`)}> }}
<button onDelete={() => {
class="px-1" servers = (servers ?? []).filter((_, i) => i !== idx);
on:click={() => { updateHandler();
showConnectionModal = true; }}
}} />
type="button" {/each}
>
<Plus />
</button>
</Tooltip>
</div>
<div class="flex flex-col gap-1">
{#each servers ?? [] as server, idx}
<Connection
bind:connection={server}
onSubmit={() => {
updateHandler();
}}
onDelete={() => {
servers = (servers ?? []).filter((_, i) => i !== idx);
updateHandler();
}}
/>
{/each}
</div>
{#if (servers ?? []).length === 0}
<div class="text-xs text-gray-400 dark:text-gray-500">
{$i18n.t('No tool server connections configured.')}
</div>
{/if}
<div class="my-1.5">
<div class="text-xs text-gray-500">
{$i18n.t('Connect to your own OpenAPI compatible external tool servers.')}
</div>
</div>
</div> </div>
<div class="mt-4 mb-2.5 flex flex-col w-full"> {#if (servers ?? []).length === 0}
<div class="flex justify-between items-center mb-1"> <div class="text-[0.6875rem] text-gray-400 dark:text-gray-600">
<div class="font-normal">{$i18n.t('Open Terminal')}</div> {$i18n.t('No tool server connections configured.')}
<Tooltip content={$i18n.t('Add Connection')}>
<button
class="px-1"
on:click={() => {
editTerminalIdx = null;
showAddTerminalModal = true;
}}
type="button"
>
<Plus />
</button>
</Tooltip>
</div> </div>
{/if}
<div class="mt-1 text-[0.6875rem] text-gray-400 dark:text-gray-600">
{$i18n.t('Connect to your own OpenAPI compatible external tool servers.')}
</div>
</div>
</AdminSettingSection>
<AdminSettingSection title={$i18n.t('Terminal')}>
<div>
<div class="mb-2 flex items-center justify-between">
<div class="text-xs text-gray-600 dark:text-gray-400">{$i18n.t('Open Terminal')}</div>
<Tooltip content={$i18n.t('Add Connection')}>
<button
class="flex size-6 items-center justify-center rounded-lg text-gray-400 transition-colors hover:bg-black/5 hover:text-gray-900 dark:text-gray-600 dark:hover:bg-white/5 dark:hover:text-white"
on:click={() => {
editTerminalIdx = null;
showAddTerminalModal = true;
}}
type="button"
>
<Plus />
</button>
</Tooltip>
</div>
<div class="flex flex-col gap-1.5">
{#each terminalConnections as connection, idx}
<div class="flex w-full gap-2 items-center">
<Tooltip className="w-full relative" content={''} placement="top-start">
<div class="flex w-full">
<div
class="flex-1 relative flex gap-1.5 items-center {connection?.enabled ===
false
? 'opacity-50'
: ''}"
>
<Tooltip content={$i18n.t('Terminal')}>
<Cloud className="size-4" strokeWidth="1.5" />
</Tooltip>
<div class="flex flex-col gap-1.5">
{#each terminalConnections as connection, idx}
<div class="flex w-full gap-2 items-center">
<Tooltip className="w-full relative" content={''} placement="top-start">
<div class="flex w-full">
<div <div
class="flex-1 relative flex gap-1.5 items-center {connection?.enabled === class="outline-hidden w-full bg-transparent text-xs text-gray-700 dark:text-gray-300"
false
? 'opacity-50'
: ''}"
> >
<Tooltip content={$i18n.t('Terminal')}> {connection.name || connection.url || $i18n.t('New Terminal')}
<Cloud className="size-4" strokeWidth="1.5" />
</Tooltip>
<div class="outline-hidden w-full bg-transparent text-sm">
{connection.name || connection.url || $i18n.t('New Terminal')}
</div>
</div> </div>
</div> </div>
</div>
</Tooltip>
<div class="flex gap-1 items-center">
<Tooltip content={$i18n.t('Configure')}>
<button
class="self-center p-1 bg-transparent hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition"
on:click={() => {
editTerminalIdx = idx;
showAddTerminalModal = true;
}}
type="button"
>
<Cog6 />
</button>
</Tooltip> </Tooltip>
<div class="flex gap-1 items-center"> <Tooltip
<Tooltip content={$i18n.t('Configure')}> content={connection?.enabled !== false
<button ? $i18n.t('Enabled')
class="self-center p-1 bg-transparent hover:bg-gray-100 dark:hover:bg-gray-850 rounded-lg transition" : $i18n.t('Disabled')}
on:click={() => { >
editTerminalIdx = idx; <Switch
showAddTerminalModal = true; state={connection?.enabled !== false}
}} on:change={() => {
type="button" terminalConnections = terminalConnections.map((c, i) =>
> i === idx ? { ...c, enabled: !(c?.enabled !== false) } : c
<Cog6 /> );
</button> saveTerminalServers();
</Tooltip> }}
/>
<Tooltip </Tooltip>
content={connection?.enabled !== false
? $i18n.t('Enabled')
: $i18n.t('Disabled')}
>
<Switch
state={connection?.enabled !== false}
on:change={() => {
terminalConnections = terminalConnections.map((c, i) =>
i === idx ? { ...c, enabled: !(c?.enabled !== false) } : c
);
saveTerminalServers();
}}
/>
</Tooltip>
</div>
</div> </div>
{/each}
</div>
{#if terminalConnections.length === 0}
<div class="text-xs text-gray-400 dark:text-gray-500">
{$i18n.t('No terminal connections configured.')}
</div> </div>
{/if} {/each}
<div class="mt-1.5">
<div class="text-xs text-gray-500">
{$i18n.t(
'Connect to Open Terminal instances. All users will have access to file browsing and terminal tools through these servers.'
)}
</div>
<div class="text-xs text-gray-600 dark:text-gray-300 mt-1">
<a
class="underline"
href="https://github.com/open-webui/open-terminal"
target="_blank">{$i18n.t('Learn more about Open Terminal')} ↗</a
>
</div>
</div>
</div> </div>
<div class="mt-8 mb-2.5 text-xs text-gray-400 dark:text-gray-600"> {#if terminalConnections.length === 0}
{$i18n.t('Knowledge')} <div class="text-[0.6875rem] text-gray-400 dark:text-gray-600">
{$i18n.t('No terminal connections configured.')}
</div>
{/if}
<div class="mt-1 text-[0.6875rem] text-gray-400 dark:text-gray-600">
{$i18n.t(
'Connect to Open Terminal instances. All users will have access to file browsing and terminal tools through these servers.'
)}
</div> </div>
<a
<hr class=" border-gray-100/20 dark:border-white/[0.025] my-2" /> class="mt-0.5 block text-[0.6875rem] text-gray-500 underline hover:text-gray-700 dark:text-gray-500 dark:hover:text-gray-300"
href="https://github.com/open-webui/open-terminal"
<ExternalKnowledge /> target="_blank">{$i18n.t('Learn more about Open Terminal')} ↗</a
>
</div> </div>
</div> </AdminSettingSection>
<AdminSettingSection title={$i18n.t('Knowledge')}>
<ExternalKnowledge />
</AdminSettingSection>
{:else} {:else}
<div class="flex h-full justify-center"> <div class="flex h-full justify-center">
<div class="my-auto"> <div class="my-auto">
@@ -329,7 +318,7 @@
{/if} {/if}
</div> </div>
<div class="flex justify-end pt-3 text-sm font-normal"> <div class="flex justify-end pt-6 text-sm font-normal">
<button <button
class="px-3.5 py-1.5 text-sm font-normal bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full" class="px-3.5 py-1.5 text-sm font-normal bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
type="submit" type="submit"
@@ -14,7 +14,7 @@
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const i18n = getContext('i18n'); const i18n: any = getContext('i18n');
let taskConfig = { let taskConfig = {
TASK_MODEL: '', TASK_MODEL: '',
@@ -55,6 +55,8 @@
let baseModels = null; let baseModels = null;
let models = null; let models = null;
const textareaClass =
'w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500';
const init = async () => { const init = async () => {
try { try {
@@ -100,20 +102,18 @@
{#if models !== null && taskConfig && chatConfig} {#if models !== null && taskConfig && chatConfig}
<form <form
class="flex flex-col h-full justify-between space-y-3 text-sm" class="flex h-full flex-col justify-between text-sm"
on:submit|preventDefault={() => { on:submit|preventDefault={() => {
updateInterfaceHandler(); updateInterfaceHandler();
dispatch('save'); dispatch('save');
}} }}
> >
<div class=" flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5 -mr-1.5"> <div class=" flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5">
<div class="mb-3.5"> <div class="mb-5">
<div class=" mt-0.5 mb-2.5 text-xs text-gray-400 dark:text-gray-600"> <div class=" mb-2 text-xs text-gray-400 dark:text-gray-600">
{$i18n.t('Tasks')} {$i18n.t('Tasks')}
</div> </div>
<hr class=" border-gray-100/20 dark:border-white/[0.025] my-2" />
<div class=" mb-2 font-normal flex items-center"> <div class=" mb-2 font-normal flex items-center">
<div class=" text-xs mr-1">{$i18n.t('Task Model')}</div> <div class=" text-xs mr-1">{$i18n.t('Task Model')}</div>
<Tooltip <Tooltip
@@ -142,7 +142,7 @@
<div class="flex-1"> <div class="flex-1">
<div class=" text-xs mb-1">{$i18n.t('Local Task Model')}</div> <div class=" text-xs mb-1">{$i18n.t('Local Task Model')}</div>
<select <select
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
bind:value={taskConfig.TASK_MODEL} bind:value={taskConfig.TASK_MODEL}
placeholder={$i18n.t('Select a model')} placeholder={$i18n.t('Select a model')}
on:change={() => { on:change={() => {
@@ -185,7 +185,7 @@
<div class="flex-1"> <div class="flex-1">
<div class=" text-xs mb-1">{$i18n.t('External Task Model')}</div> <div class=" text-xs mb-1">{$i18n.t('External Task Model')}</div>
<select <select
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
bind:value={taskConfig.TASK_MODEL_EXTERNAL} bind:value={taskConfig.TASK_MODEL_EXTERNAL}
placeholder={$i18n.t('Select a model')} placeholder={$i18n.t('Select a model')}
on:change={() => { on:change={() => {
@@ -226,10 +226,8 @@
</div> </div>
</div> </div>
<hr class=" border-gray-100/20 dark:border-white/[0.025] my-3" />
<div class="mb-2.5 flex w-full items-center justify-between"> <div class="mb-2.5 flex w-full items-center justify-between">
<div class=" self-center text-xs font-normal"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Context Compaction')} {$i18n.t('Context Compaction')}
</div> </div>
@@ -238,7 +236,9 @@
{#if chatConfig.ENABLE_CONTEXT_COMPACTION} {#if chatConfig.ENABLE_CONTEXT_COMPACTION}
<div class="mb-2.5"> <div class="mb-2.5">
<div class=" mb-1 text-xs font-normal">{$i18n.t('Token Threshold')}</div> <div class=" mb-1 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Token Threshold')}
</div>
<Tooltip <Tooltip
content={$i18n.t( content={$i18n.t(
@@ -250,14 +250,14 @@
type="number" type="number"
min="1" min="1"
step="1" step="1"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
bind:value={chatConfig.CONTEXT_COMPACTION_TOKEN_THRESHOLD} bind:value={chatConfig.CONTEXT_COMPACTION_TOKEN_THRESHOLD}
/> />
</Tooltip> </Tooltip>
</div> </div>
<div class="mb-2.5"> <div class="mb-2.5">
<div class=" mb-1 text-xs font-normal">{$i18n.t('Token Cap')}</div> <div class=" mb-1 text-xs text-gray-600 dark:text-gray-400">{$i18n.t('Token Cap')}</div>
<Tooltip <Tooltip
content={$i18n.t( content={$i18n.t(
@@ -269,20 +269,23 @@
type="number" type="number"
min="1" min="1"
step="1" step="1"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
bind:value={chatConfig.CONTEXT_COMPACTION_TOKEN_CAP} bind:value={chatConfig.CONTEXT_COMPACTION_TOKEN_CAP}
/> />
</Tooltip> </Tooltip>
</div> </div>
<div class="mb-2.5"> <div class="mb-2.5">
<div class=" mb-1 text-xs font-normal">{$i18n.t('Context Compaction Prompt')}</div> <div class=" mb-1 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Context Compaction Prompt')}
</div>
<Tooltip <Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')} content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
placement="top-start" placement="top-start"
> >
<Textarea <Textarea
className={textareaClass}
bind:value={chatConfig.CONTEXT_COMPACTION_PROMPT_TEMPLATE} bind:value={chatConfig.CONTEXT_COMPACTION_PROMPT_TEMPLATE}
placeholder={$i18n.t( placeholder={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt' 'Leave empty to use the default prompt, or enter a custom prompt'
@@ -300,10 +303,8 @@
</div> </div>
{/if} {/if}
<hr class=" border-gray-100/20 dark:border-white/[0.025] my-3" />
<div class="mb-2.5 flex w-full items-center justify-between"> <div class="mb-2.5 flex w-full items-center justify-between">
<div class=" self-center text-xs font-normal"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Title Generation')} {$i18n.t('Title Generation')}
</div> </div>
@@ -312,13 +313,16 @@
{#if taskConfig.ENABLE_TITLE_GENERATION} {#if taskConfig.ENABLE_TITLE_GENERATION}
<div class="mb-2.5"> <div class="mb-2.5">
<div class=" mb-1 text-xs font-normal">{$i18n.t('Title Generation Prompt')}</div> <div class=" mb-1 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Title Generation Prompt')}
</div>
<Tooltip <Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')} content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
placement="top-start" placement="top-start"
> >
<Textarea <Textarea
className={textareaClass}
bind:value={taskConfig.TITLE_GENERATION_PROMPT_TEMPLATE} bind:value={taskConfig.TITLE_GENERATION_PROMPT_TEMPLATE}
placeholder={$i18n.t( placeholder={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt' 'Leave empty to use the default prompt, or enter a custom prompt'
@@ -329,7 +333,7 @@
{/if} {/if}
<div class="mb-2.5 flex w-full items-center justify-between"> <div class="mb-2.5 flex w-full items-center justify-between">
<div class=" self-center text-xs font-normal"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Voice Mode Prompt')} {$i18n.t('Voice Mode Prompt')}
</div> </div>
@@ -338,13 +342,16 @@
{#if taskConfig.ENABLE_VOICE_MODE_PROMPT} {#if taskConfig.ENABLE_VOICE_MODE_PROMPT}
<div class="mb-2.5"> <div class="mb-2.5">
<div class=" mb-1 text-xs font-normal">{$i18n.t('Prompt Template')}</div> <div class=" mb-1 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Prompt Template')}
</div>
<Tooltip <Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')} content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
placement="top-start" placement="top-start"
> >
<Textarea <Textarea
className={textareaClass}
bind:value={taskConfig.VOICE_MODE_PROMPT_TEMPLATE} bind:value={taskConfig.VOICE_MODE_PROMPT_TEMPLATE}
placeholder={$i18n.t( placeholder={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt' 'Leave empty to use the default prompt, or enter a custom prompt'
@@ -355,7 +362,7 @@
{/if} {/if}
<div class="mb-2.5 flex w-full items-center justify-between"> <div class="mb-2.5 flex w-full items-center justify-between">
<div class=" self-center text-xs font-normal"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Follow Up Generation')} {$i18n.t('Follow Up Generation')}
</div> </div>
@@ -364,13 +371,16 @@
{#if taskConfig.ENABLE_FOLLOW_UP_GENERATION} {#if taskConfig.ENABLE_FOLLOW_UP_GENERATION}
<div class="mb-2.5"> <div class="mb-2.5">
<div class=" mb-1 text-xs font-normal">{$i18n.t('Follow Up Generation Prompt')}</div> <div class=" mb-1 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Follow Up Generation Prompt')}
</div>
<Tooltip <Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')} content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
placement="top-start" placement="top-start"
> >
<Textarea <Textarea
className={textareaClass}
bind:value={taskConfig.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE} bind:value={taskConfig.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE}
placeholder={$i18n.t( placeholder={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt' 'Leave empty to use the default prompt, or enter a custom prompt'
@@ -381,7 +391,7 @@
{/if} {/if}
<div class="mb-2.5 flex w-full items-center justify-between"> <div class="mb-2.5 flex w-full items-center justify-between">
<div class=" self-center text-xs font-normal"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Tags Generation')} {$i18n.t('Tags Generation')}
</div> </div>
@@ -390,13 +400,16 @@
{#if taskConfig.ENABLE_TAGS_GENERATION} {#if taskConfig.ENABLE_TAGS_GENERATION}
<div class="mb-2.5"> <div class="mb-2.5">
<div class=" mb-1 text-xs font-normal">{$i18n.t('Tags Generation Prompt')}</div> <div class=" mb-1 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Tags Generation Prompt')}
</div>
<Tooltip <Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')} content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
placement="top-start" placement="top-start"
> >
<Textarea <Textarea
className={textareaClass}
bind:value={taskConfig.TAGS_GENERATION_PROMPT_TEMPLATE} bind:value={taskConfig.TAGS_GENERATION_PROMPT_TEMPLATE}
placeholder={$i18n.t( placeholder={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt' 'Leave empty to use the default prompt, or enter a custom prompt'
@@ -407,7 +420,7 @@
{/if} {/if}
<div class="mb-2.5 flex w-full items-center justify-between"> <div class="mb-2.5 flex w-full items-center justify-between">
<div class=" self-center text-xs font-normal"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Retrieval Query Generation')} {$i18n.t('Retrieval Query Generation')}
</div> </div>
@@ -415,7 +428,7 @@
</div> </div>
<div class="mb-2.5 flex w-full items-center justify-between"> <div class="mb-2.5 flex w-full items-center justify-between">
<div class=" self-center text-xs font-normal"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Web Search Query Generation')} {$i18n.t('Web Search Query Generation')}
</div> </div>
@@ -423,13 +436,16 @@
</div> </div>
<div class="mb-2.5"> <div class="mb-2.5">
<div class=" mb-1 text-xs font-normal">{$i18n.t('Query Generation Prompt')}</div> <div class=" mb-1 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Query Generation Prompt')}
</div>
<Tooltip <Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')} content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
placement="top-start" placement="top-start"
> >
<Textarea <Textarea
className={textareaClass}
bind:value={taskConfig.QUERY_GENERATION_PROMPT_TEMPLATE} bind:value={taskConfig.QUERY_GENERATION_PROMPT_TEMPLATE}
placeholder={$i18n.t( placeholder={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt' 'Leave empty to use the default prompt, or enter a custom prompt'
@@ -439,7 +455,7 @@
</div> </div>
<div class="mb-2.5 flex w-full items-center justify-between"> <div class="mb-2.5 flex w-full items-center justify-between">
<div class=" self-center text-xs font-normal"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Autocomplete Generation')} {$i18n.t('Autocomplete Generation')}
</div> </div>
@@ -450,7 +466,7 @@
{#if taskConfig.ENABLE_AUTOCOMPLETE_GENERATION} {#if taskConfig.ENABLE_AUTOCOMPLETE_GENERATION}
<div class="mb-2.5"> <div class="mb-2.5">
<div class=" mb-1 text-xs font-normal"> <div class=" mb-1 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Autocomplete Generation Input Max Length')} {$i18n.t('Autocomplete Generation Input Max Length')}
</div> </div>
@@ -466,13 +482,16 @@
</Tooltip> </Tooltip>
</div> </div>
<div class="mb-2.5"> <div class="mb-2.5">
<div class=" mb-1 text-xs font-normal">{$i18n.t('Autocomplete Generation Prompt')}</div> <div class=" mb-1 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Autocomplete Generation Prompt')}
</div>
<Tooltip <Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')} content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
placement="top-start" placement="top-start"
> >
<Textarea <Textarea
className={textareaClass}
bind:value={taskConfig.AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE} bind:value={taskConfig.AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE}
placeholder={$i18n.t( placeholder={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt' 'Leave empty to use the default prompt, or enter a custom prompt'
@@ -483,13 +502,16 @@
{/if} {/if}
<div class="mb-2.5"> <div class="mb-2.5">
<div class=" mb-1 text-xs font-normal">{$i18n.t('Image Prompt Generation Prompt')}</div> <div class=" mb-1 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Image Prompt Generation Prompt')}
</div>
<Tooltip <Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')} content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
placement="top-start" placement="top-start"
> >
<Textarea <Textarea
className={textareaClass}
bind:value={taskConfig.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE} bind:value={taskConfig.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE}
placeholder={$i18n.t( placeholder={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt' 'Leave empty to use the default prompt, or enter a custom prompt'
@@ -499,13 +521,16 @@
</div> </div>
<div class="mb-2.5"> <div class="mb-2.5">
<div class=" mb-1 text-xs font-normal">{$i18n.t('Tools Function Calling Prompt')}</div> <div class=" mb-1 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Tools Function Calling Prompt')}
</div>
<Tooltip <Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')} content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
placement="top-start" placement="top-start"
> >
<Textarea <Textarea
className={textareaClass}
bind:value={taskConfig.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE} bind:value={taskConfig.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE}
placeholder={$i18n.t( placeholder={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt' 'Leave empty to use the default prompt, or enter a custom prompt'
@@ -516,7 +541,7 @@
</div> </div>
</div> </div>
<div class="flex justify-end text-sm font-normal"> <div class="flex justify-end pt-6 text-sm font-normal">
<button <button
class="px-3.5 py-1.5 text-sm font-normal bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full" class="px-3.5 py-1.5 text-sm font-normal bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
type="submit" type="submit"
+18 -22
View File
@@ -382,14 +382,10 @@
{#if selectedModelId === null} {#if selectedModelId === null}
<div class="flex flex-col gap-1 mt-1.5 mb-2"> <div class="flex flex-col gap-1 mt-1.5 mb-2">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<div class="flex items-center md:self-center text-xl font-normal px-0.5 gap-2 shrink-0"> <div
<div> class="flex items-center md:self-center text-xs text-gray-400 dark:text-gray-600 px-0.5 gap-1.5 shrink-0"
{$i18n.t('Models')} >
</div> <div>{filteredModels.length}</div>
<div class="text-lg font-normal text-gray-500 dark:text-gray-500">
{filteredModels.length}
</div>
</div> </div>
<div class="flex w-full justify-end gap-1.5"> <div class="flex w-full justify-end gap-1.5">
@@ -430,7 +426,7 @@
/> />
<button <button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-200 transition" class="flex text-xs items-center space-x-1 px-2.5 py-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/5 dark:text-gray-200 transition"
disabled={modelsImportInProgress} disabled={modelsImportInProgress}
on:click={() => { on:click={() => {
modelsImportInputElement.click(); modelsImportInputElement.click();
@@ -445,7 +441,7 @@
</button> </button>
<button <button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-200 transition" class="flex text-xs items-center space-x-1 px-2.5 py-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/5 dark:text-gray-200 transition"
on:click={async () => { on:click={async () => {
downloadModels(models); downloadModels(models);
}} }}
@@ -457,7 +453,7 @@
{/if} {/if}
<button <button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-200 transition" class="flex text-xs items-center space-x-1 px-2.5 py-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/5 dark:text-gray-200 transition"
type="button" type="button"
on:click={() => { on:click={() => {
showManageModal = true; showManageModal = true;
@@ -483,10 +479,8 @@
</div> </div>
</div> </div>
<div <div class="py-1">
class="py-2 bg-white dark:bg-gray-900 rounded-3xl border border-gray-100/20 dark:border-white/[0.025]" <div class="flex flex-1 items-center w-full space-x-2 py-0.5 pb-2">
>
<div class="px-3.5 flex flex-1 items-center w-full space-x-2 py-0.5 pb-2">
<div class="flex flex-1 items-center"> <div class="flex flex-1 items-center">
<div class=" self-center ml-1 mr-3"> <div class=" self-center ml-1 mr-3">
<Search className="size-3.5" /> <Search className="size-3.5" />
@@ -499,7 +493,7 @@
{#if searchValue} {#if searchValue}
<div class="self-center pl-1.5 translate-y-[0.5px] rounded-l-xl bg-transparent"> <div class="self-center pl-1.5 translate-y-[0.5px] rounded-l-xl bg-transparent">
<button <button
class="p-0.5 rounded-full hover:bg-gray-100 dark:hover:bg-gray-900 transition" class="p-0.5 rounded-sm hover:bg-black/5 dark:hover:bg-white/5 transition"
on:click={() => { on:click={() => {
searchValue = ''; searchValue = '';
}} }}
@@ -511,7 +505,7 @@
</div> </div>
</div> </div>
<div class="px-3 flex w-full items-center bg-transparent overflow-x-auto scrollbar-none"> <div class="flex w-full items-center bg-transparent overflow-x-auto scrollbar-none">
<div <div
class="flex gap-0.5 w-fit text-center text-sm rounded-full bg-transparent whitespace-nowrap" class="flex gap-0.5 w-fit text-center text-sm rounded-full bg-transparent whitespace-nowrap"
> >
@@ -533,7 +527,7 @@
<Dropdown> <Dropdown>
<Tooltip content={$i18n.t('Actions')}> <Tooltip content={$i18n.t('Actions')}>
<button <button
class="p-1 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition" class="p-1 rounded-lg hover:bg-black/5 dark:hover:bg-white/5 transition"
type="button" type="button"
> >
<EllipsisHorizontal className="size-3.5" /> <EllipsisHorizontal className="size-3.5" />
@@ -564,7 +558,7 @@
<div class="flex items-center">{$i18n.t('Disable All')}</div> <div class="flex items-center">{$i18n.t('Disable All')}</div>
</button> </button>
<hr class="border-gray-100/40 dark:border-white/[0.03] my-1" /> <hr class="border-gray-100/20 dark:border-white/[0.02] my-1" />
<button <button
class="select-none flex w-full gap-2 items-center h-[1.6875rem] px-2 text-[13px] font-normal cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 rounded-xl" class="select-none flex w-full gap-2 items-center h-[1.6875rem] px-2 text-[13px] font-normal cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 rounded-xl"
@@ -592,7 +586,7 @@
</Dropdown> </Dropdown>
</div> </div>
<div class="px-3 my-2" id="model-list"> <div class="my-2" id="model-list">
{#if filteredModels.length > 0} {#if filteredModels.length > 0}
{#each filteredModels.slice((currentPage - 1) * perPage, currentPage * perPage) as model, modelIdx (`${model.id}-${modelIdx}`)} {#each filteredModels.slice((currentPage - 1) * perPage, currentPage * perPage) as model, modelIdx (`${model.id}-${modelIdx}`)}
<div <div
@@ -766,8 +760,10 @@
{:else} {:else}
<div class=" w-full h-full flex flex-col justify-center items-center my-16 mb-24"> <div class=" w-full h-full flex flex-col justify-center items-center my-16 mb-24">
<div class="max-w-md text-center"> <div class="max-w-md text-center">
<div class=" text-3xl mb-3">😕</div> <div class="mb-2 text-xl">😕</div>
<div class=" text-lg font-normal mb-1">{$i18n.t('No models found')}</div> <div class="mb-1 text-sm text-gray-700 dark:text-gray-300">
{$i18n.t('No models found')}
</div>
<div class=" text-gray-500 text-center text-xs"> <div class=" text-gray-500 text-center text-xs">
{$i18n.t('Try adjusting your search or filter to find what you are looking for.')} {$i18n.t('Try adjusting your search or filter to find what you are looking for.')}
</div> </div>
+175 -309
View File
@@ -1,6 +1,4 @@
<script lang="ts"> <script lang="ts">
import { v4 as uuidv4 } from 'uuid';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { config, models, settings } from '$lib/stores'; import { config, models, settings } from '$lib/stores';
import { getContext, onMount, tick } from 'svelte'; import { getContext, onMount, tick } from 'svelte';
@@ -20,6 +18,8 @@
import Spinner from '$lib/components/common/Spinner.svelte'; import Spinner from '$lib/components/common/Spinner.svelte';
import Switch from '$lib/components/common/Switch.svelte'; import Switch from '$lib/components/common/Switch.svelte';
import AdminSettingField from './AdminSettingField.svelte';
import AdminSettingRow from './AdminSettingRow.svelte';
const i18n: Writable<i18nType> = getContext('i18n'); const i18n: Writable<i18nType> = getContext('i18n');
@@ -28,18 +28,27 @@
let downloading = false; let downloading = false;
let uploading = false; let uploading = false;
let pipelineFiles; let pipelineFiles: any = null;
let PIPELINES_LIST = null; let PIPELINES_LIST: any[] | null = null;
let selectedPipelinesUrlIdx = ''; let selectedPipelinesUrlIdx = '';
let pipelines = null; let pipelines: any[] | null = null;
let valves = null; let valves: any = null;
let valves_spec = null; let valves_spec: any = null;
let selectedPipelineIdx = null; let selectedPipelineIdx: number | null = null;
let pipelineDownloadUrl = ''; let pipelineDownloadUrl = '';
const inputClass =
'w-full h-7 rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 text-xs text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500';
const selectClass =
'w-full h-7 rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500';
const actionButtonClass =
'rounded-lg px-2 py-1 text-xs text-gray-500 transition-colors hover:bg-black/5 hover:text-gray-900 disabled:opacity-50 dark:text-gray-500 dark:hover:bg-white/5 dark:hover:text-white';
const iconButtonClass =
'flex size-7 items-center justify-center rounded-lg text-gray-400 transition-colors hover:bg-black/5 hover:text-gray-900 disabled:opacity-50 dark:text-gray-600 dark:hover:bg-white/5 dark:hover:text-white';
const mutedMessageClass = 'text-xs text-gray-400 dark:text-gray-600';
const updateHandler = async () => { const updateHandler = async () => {
const pipeline = pipelines[selectedPipelineIdx]; const pipeline = pipelines[selectedPipelineIdx];
@@ -66,7 +75,9 @@
models.set( models.set(
await getModels( await getModels(
localStorage.token, localStorage.token,
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null) $config?.features?.enable_direct_connections
? ($settings?.directConnections ?? null)
: null
) )
); );
saveHandler(); saveHandler();
@@ -133,7 +144,9 @@
models.set( models.set(
await getModels( await getModels(
localStorage.token, localStorage.token,
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null) $config?.features?.enable_direct_connections
? ($settings?.directConnections ?? null)
: null
) )
); );
} }
@@ -163,7 +176,9 @@
models.set( models.set(
await getModels( await getModels(
localStorage.token, localStorage.token,
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null) $config?.features?.enable_direct_connections
? ($settings?.directConnections ?? null)
: null
) )
); );
} }
@@ -197,7 +212,9 @@
models.set( models.set(
await getModels( await getModels(
localStorage.token, localStorage.token,
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null) $config?.features?.enable_direct_connections
? ($settings?.directConnections ?? null)
: null
) )
); );
} }
@@ -216,346 +233,195 @@
</script> </script>
<form <form
class="flex flex-col h-full justify-between space-y-3 text-sm" class="flex h-full flex-col justify-between text-sm"
on:submit|preventDefault={async () => { on:submit|preventDefault={async () => {
updateHandler(); updateHandler();
}} }}
> >
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5 -mr-1.5"> <div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5">
{#if PIPELINES_LIST !== null} {#if PIPELINES_LIST !== null}
<div class="flex w-full justify-between mb-2">
<div class=" self-center text-sm font-normal">
{$i18n.t('Manage Pipelines')}
</div>
</div>
{#if PIPELINES_LIST.length > 0} {#if PIPELINES_LIST.length > 0}
<div class="space-y-1"> <input
<div class="flex gap-2"> id="pipelines-upload-input"
<div class="flex-1"> bind:files={pipelineFiles}
<select type="file"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" accept=".py"
bind:value={selectedPipelinesUrlIdx} hidden
placeholder={$i18n.t('Select a pipeline url')} />
on:change={async () => {
await tick(); <div class="flex flex-col gap-2.5">
await setPipelines(); <AdminSettingField label={$i18n.t('Pipeline URL')}>
}} <select
class={selectClass}
bind:value={selectedPipelinesUrlIdx}
placeholder={$i18n.t('Select a pipeline url')}
on:change={async () => {
await tick();
await setPipelines();
}}
>
<option value="" selected disabled class="bg-gray-100 dark:bg-gray-700"
>{$i18n.t('Select a pipeline url')}</option
> >
<option value="" selected disabled class="bg-gray-100 dark:bg-gray-700"
>{$i18n.t('Select a pipeline url')}</option {#each PIPELINES_LIST as pipelines}
<option value={pipelines.idx.toString()} class="bg-gray-100 dark:bg-gray-700"
>{pipelines.url}</option
> >
{/each}
</select>
</AdminSettingField>
{#each PIPELINES_LIST as pipelines, idx} <AdminSettingField label={$i18n.t('Upload Pipeline')}>
<option value={pipelines.idx.toString()} class="bg-gray-100 dark:bg-gray-700" <div class="flex gap-2">
>{pipelines.url}</option
>
{/each}
</select>
</div>
</div>
</div>
<div class=" my-2">
<div class=" mb-2 text-sm font-normal">
{$i18n.t('Upload Pipeline')}
</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
<input
id="pipelines-upload-input"
bind:files={pipelineFiles}
type="file"
accept=".py"
hidden
/>
<button <button
class="w-full text-sm font-normal py-2 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-850 dark:hover:bg-gray-850 text-center rounded-xl" class="h-7 flex-1 rounded-lg border border-dashed border-gray-100/50 bg-transparent px-2 text-left text-xs text-gray-500 transition-colors hover:bg-black/5 hover:text-gray-900 dark:border-white/[0.04] dark:text-gray-500 dark:hover:bg-white/5 dark:hover:text-white"
type="button" type="button"
on:click={() => { on:click={() => {
document.getElementById('pipelines-upload-input')?.click(); document.getElementById('pipelines-upload-input')?.click();
}} }}
> >
{#if pipelineFiles} {#if pipelineFiles}
{pipelineFiles.length > 0 ? `${pipelineFiles.length}` : ''} pipeline(s) selected. {pipelineFiles.length > 0 ? `${pipelineFiles.length}` : ''} pipeline(s) selected
{:else} {:else}
{$i18n.t('Click here to select a py file.')} {$i18n.t('Select a .py file')}
{/if} {/if}
</button> </button>
<button
class={actionButtonClass}
on:click={() => {
uploadPipelineHandler();
}}
disabled={uploading}
type="button"
>
{uploading ? $i18n.t('Uploading') : $i18n.t('Upload')}
</button>
</div> </div>
<button </AdminSettingField>
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={() => {
uploadPipelineHandler();
}}
disabled={uploading}
type="button"
>
{#if uploading}
<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 { <AdminSettingField
100% { label={$i18n.t('GitHub URL')}
transform: rotate(360deg); description={`${$i18n.t('Pipelines are a plugin system with arbitrary code execution.')} ${$i18n.t("Don't fetch random pipelines from sources you don't trust.")}`}
} >
} <div class="flex gap-2">
</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
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="size-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>
</div>
</div>
<div class=" my-2">
<div class=" mb-2 text-sm font-normal">
{$i18n.t('Install from Github URL')}
</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class={inputClass}
placeholder={$i18n.t('Enter Github Raw URL')} placeholder={$i18n.t('Enter GitHub Raw URL')}
bind:value={pipelineDownloadUrl} bind:value={pipelineDownloadUrl}
/> />
<button
class={actionButtonClass}
on:click={() => {
addPipelineHandler();
}}
disabled={downloading}
type="button"
>
{downloading ? $i18n.t('Installing') : $i18n.t('Install')}
</button>
</div> </div>
<button </AdminSettingField>
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={() => {
addPipelineHandler();
}}
disabled={downloading}
type="button"
>
{#if downloading}
<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 { {#if pipelines !== null}
100% { {#if pipelines.length > 0}
transform: rotate(360deg); <AdminSettingField label={$i18n.t('Pipeline')}>
}
}
</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
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<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"
/>
<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>
</div>
<div class="mt-2 text-xs text-gray-500">
<span class=" font-normal dark:text-gray-200">{$i18n.t('Warning:')}</span>
{$i18n.t('Pipelines are a plugin system with arbitrary code execution —')}
<span class=" font-normal dark:text-gray-400"
>{$i18n.t("don't fetch random pipelines from sources you don't trust.")}</span
>
</div>
</div>
<hr class="border-gray-100/20 dark:border-white/[0.025] my-3 w-full" />
{#if pipelines !== null}
{#if pipelines.length > 0}
<div class="flex w-full justify-between mb-2">
<div class=" self-center text-sm font-normal">
{$i18n.t('Pipelines Valves')}
</div>
</div>
<div class="space-y-1">
{#if pipelines.length > 0}
<div class="flex gap-2"> <div class="flex gap-2">
<div class="flex-1"> <select
<select class={selectClass}
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" bind:value={selectedPipelineIdx}
bind:value={selectedPipelineIdx} placeholder={$i18n.t('Select a pipeline')}
placeholder={$i18n.t('Select a pipeline')} on:change={async () => {
on:change={async () => { await tick();
await tick(); await getValves(selectedPipelineIdx ?? 0);
await getValves(selectedPipelineIdx); }}
}} >
> {#each pipelines as pipeline, idx}
{#each pipelines as pipeline, idx} <option value={idx} class="bg-gray-100 dark:bg-gray-700">
<option value={idx} class="bg-gray-100 dark:bg-gray-700" {pipeline.name} ({pipeline.type ?? 'pipe'})
>{pipeline.name} ({pipeline.type ?? 'pipe'})</option </option>
> {/each}
{/each} </select>
</select>
</div>
<button <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" class={actionButtonClass}
on:click={() => { on:click={() => {
deletePipelineHandler(); deletePipelineHandler();
}} }}
type="button" type="button"
> >
<svg {$i18n.t('Delete')}
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> </button>
</div> </div>
{/if} </AdminSettingField>
<div class="space-y-1"> {#if pipelines[selectedPipelineIdx ?? 0]?.valves}
{#if pipelines[selectedPipelineIdx].valves} {#if valves}
{#if valves} {#each Object.keys(valves_spec.properties) as property}
{#each Object.keys(valves_spec.properties) as property, idx} <div>
<div class=" py-0.5 w-full justify-between"> <AdminSettingRow label={valves_spec.properties[property].title}>
<div class="flex w-full justify-between"> <button
<div class=" self-center text-xs font-normal"> class={actionButtonClass}
{valves_spec.properties[property].title} type="button"
</div> on:click={() => {
valves[property] = (valves[property] ?? null) === null ? '' : null;
}}
>
{(valves[property] ?? null) === null
? $i18n.t('None')
: $i18n.t('Custom')}
</button>
</AdminSettingRow>
<button {#if (valves[property] ?? null) !== null}
class="p-1 px-3 text-xs flex rounded-sm transition" <div class="mt-1">
type="button" {#if valves_spec.properties[property]?.enum ?? null}
on:click={() => { <select class={selectClass} bind:value={valves[property]}>
valves[property] = (valves[property] ?? null) === null ? '' : null; {#each valves_spec.properties[property].enum as option}
}} <option value={option} selected={option === valves[property]}>
> {option}
{#if (valves[property] ?? null) === null} </option>
<span class="ml-2 self-center"> {$i18n.t('None')} </span> {/each}
{:else} </select>
<span class="ml-2 self-center"> {$i18n.t('Custom')} </span> {:else if (valves_spec.properties[property]?.type ?? null) === 'boolean'}
{/if} <AdminSettingRow
</button> label={valves[property] ? $i18n.t('Enabled') : $i18n.t('Disabled')}
labelClassName="text-gray-400 dark:text-gray-600"
>
<Switch bind:state={valves[property]} />
</AdminSettingRow>
{:else}
<input
class={inputClass}
type="text"
placeholder={valves_spec.properties[property].title}
bind:value={valves[property]}
autocomplete="off"
required
/>
{/if}
</div> </div>
{/if}
{#if (valves[property] ?? null) !== null} </div>
<!-- {valves[property]} --> {/each}
<div class="flex mt-0.5 mb-1.5 space-x-2">
<div class=" flex-1">
{#if valves_spec.properties[property]?.enum ?? null}
<select
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={valves[property]}
>
{#each valves_spec.properties[property].enum as option}
<option value={option} selected={option === valves[property]}>
{option}
</option>
{/each}
</select>
{:else if (valves_spec.properties[property]?.type ?? null) === 'boolean'}
<div class="flex justify-between items-center">
<div class="text-xs text-gray-500">
{valves[property] ? $i18n.t('Enabled') : $i18n.t('Disabled')}
</div>
<div class=" pr-2">
<Switch bind:state={valves[property]} />
</div>
</div>
{:else}
<input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
type="text"
placeholder={valves_spec.properties[property].title}
bind:value={valves[property]}
autocomplete="off"
required
/>
{/if}
</div>
</div>
{/if}
</div>
{/each}
{:else}
<Spinner className="size-5" />
{/if}
{:else} {:else}
<div>{$i18n.t('No valves')}</div> <Spinner className="size-5" />
{/if} {/if}
</div> {:else}
</div> <div class={mutedMessageClass}>{$i18n.t('No valves')}</div>
{:else if pipelines.length === 0} {/if}
<div>{$i18n.t('Pipelines Not Detected')}</div> {:else}
{/if} <div class={mutedMessageClass}>{$i18n.t('Pipelines Not Detected')}</div>
{:else} {/if}
<div class="flex justify-center"> {:else}
<div class="my-auto"> <div class="flex justify-center py-4">
<Spinner className="size-4" /> <Spinner className="size-4" />
</div> </div>
</div> {/if}
{/if} </div>
{:else} {:else}
<div>{$i18n.t('Pipelines Not Detected')}</div> <div class={mutedMessageClass}>{$i18n.t('Pipelines Not Detected')}</div>
{/if} {/if}
{:else} {:else}
<div class="flex justify-center h-full"> <div class="flex justify-center h-full">
@@ -567,7 +433,7 @@
</div> </div>
{#if PIPELINES_LIST !== null && PIPELINES_LIST.length > 0} {#if PIPELINES_LIST !== null && PIPELINES_LIST.length > 0}
<div class="flex justify-end pt-3 text-sm font-normal"> <div class="flex justify-end pt-6 text-sm font-normal">
<button <button
class="px-3.5 py-1.5 text-sm font-normal bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full" class="px-3.5 py-1.5 text-sm font-normal bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
type="submit" type="submit"
@@ -57,9 +57,7 @@
</script> </script>
<form class="flex h-full flex-col justify-between text-sm" on:submit|preventDefault={save}> <form class="flex h-full flex-col justify-between text-sm" on:submit|preventDefault={save}>
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5 -mr-1.5"> <div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5">
<div class="mt-0.5 mb-4 text-xs text-gray-400 dark:text-gray-600">{$i18n.t('Sub-agents')}</div>
{#if loading} {#if loading}
<div class="flex justify-center py-8"><Spinner className="size-6" /></div> <div class="flex justify-center py-8"><Spinner className="size-6" /></div>
{:else} {:else}
@@ -87,7 +85,7 @@
type="number" type="number"
bind:value={maxConcurrent} bind:value={maxConcurrent}
min="-1" min="-1"
class="h-7 w-16 rounded-lg border border-gray-200 bg-gray-100 px-2 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/10 dark:bg-white/5 dark:text-gray-300 dark:focus:border-blue-500" class="h-7 w-16 rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
/> />
<span class="text-[0.6875rem] text-gray-400 dark:text-gray-600"> <span class="text-[0.6875rem] text-gray-400 dark:text-gray-600">
{$i18n.t('simultaneous sub-agents')} {$i18n.t('simultaneous sub-agents')}
@@ -120,7 +118,7 @@
type="number" type="number"
bind:value={maxAsync} bind:value={maxAsync}
min="-1" min="-1"
class="h-7 w-16 rounded-lg border border-gray-200 bg-gray-100 px-2 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/10 dark:bg-white/5 dark:text-gray-300 dark:focus:border-blue-500" class="h-7 w-16 rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
/> />
<span class="text-[0.6875rem] text-gray-400 dark:text-gray-600"> <span class="text-[0.6875rem] text-gray-400 dark:text-gray-600">
{$i18n.t('background sub-agents')} {$i18n.t('background sub-agents')}
@@ -140,7 +138,7 @@
bind:value={maxIterations} bind:value={maxIterations}
min="1" min="1"
max="100" max="100"
class="h-7 w-16 rounded-lg border border-gray-200 bg-gray-100 px-2 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/10 dark:bg-white/5 dark:text-gray-300 dark:focus:border-blue-500" class="h-7 w-16 rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
/> />
<span class="text-[0.6875rem] text-gray-400 dark:text-gray-600"> <span class="text-[0.6875rem] text-gray-400 dark:text-gray-600">
{$i18n.t('tool loops per sub-agent')} {$i18n.t('tool loops per sub-agent')}
@@ -160,7 +158,7 @@
min="1000" min="1000"
max="100000" max="100000"
step="1000" step="1000"
class="h-7 w-20 rounded-lg border border-gray-200 bg-gray-100 px-2 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/10 dark:bg-white/5 dark:text-gray-300 dark:focus:border-blue-500" class="h-7 w-20 rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
/> />
<span class="text-[0.6875rem] text-gray-400 dark:text-gray-600">chars</span> <span class="text-[0.6875rem] text-gray-400 dark:text-gray-600">chars</span>
</div> </div>
@@ -175,7 +173,7 @@
bind:value={systemPrompt} bind:value={systemPrompt}
rows="4" rows="4"
placeholder={$i18n.t('You are a sub-agent...')} placeholder={$i18n.t('You are a sub-agent...')}
class="mt-1 w-full resize-y rounded-lg border border-gray-200 bg-gray-100 px-2 py-1.5 font-mono text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/10 dark:bg-white/5 dark:text-gray-300 dark:focus:border-blue-500" class="mt-1 w-full resize-y rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 font-mono text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
></textarea> ></textarea>
<p class="mt-0.5 text-[0.6875rem] text-gray-400 dark:text-gray-600"> <p class="mt-0.5 text-[0.6875rem] text-gray-400 dark:text-gray-600">
{$i18n.t('Leave empty for the built-in default.')} {$i18n.t('Leave empty for the built-in default.')}
+130 -131
View File
@@ -47,6 +47,8 @@
let webLoaderEngines = ['playwright', 'firecrawl', 'tavily', 'microsoft_web_iq', 'external']; let webLoaderEngines = ['playwright', 'firecrawl', 'tavily', 'microsoft_web_iq', 'external'];
let webConfig = null; let webConfig = null;
const textareaClass =
'w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500';
const submitHandler = async () => { const submitHandler = async () => {
// Convert domain filter string to array before sending // Convert domain filter string to array before sending
@@ -144,24 +146,18 @@
</script> </script>
<form <form
class="flex flex-col h-full justify-between space-y-3 text-sm" class="flex h-full flex-col justify-between text-sm"
on:submit|preventDefault={async () => { on:submit|preventDefault={async () => {
await submitHandler(); await submitHandler();
saveHandler(); saveHandler();
}} }}
> >
<div class=" space-y-3 flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5 -mr-1.5"> <div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5">
{#if webConfig} {#if webConfig}
<div class=""> <div class="">
<div class="mb-3"> <div class="mb-5">
<div class=" mt-0.5 mb-2.5 text-xs text-gray-400 dark:text-gray-600">
{$i18n.t('General')}
</div>
<hr class=" border-gray-100/20 dark:border-white/[0.025] my-2" />
<div class=" mb-2.5 flex w-full justify-between"> <div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-normal"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Web Search')} {$i18n.t('Web Search')}
</div> </div>
<div class="flex items-center relative"> <div class="flex items-center relative">
@@ -170,7 +166,7 @@
</div> </div>
<div class="mb-2.5 flex w-full justify-between"> <div class="mb-2.5 flex w-full justify-between">
<div class="self-center text-xs font-normal"> <div class="self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Web Search Confirmation')} {$i18n.t('Web Search Confirmation')}
</div> </div>
<div class="flex items-center relative"> <div class="flex items-center relative">
@@ -182,10 +178,11 @@
{#if webConfig.ENABLE_WEB_SEARCH_CONFIRMATION} {#if webConfig.ENABLE_WEB_SEARCH_CONFIRMATION}
<div class="mb-2.5"> <div class="mb-2.5">
<div class="self-center text-xs font-normal mb-2"> <div class="self-center text-xs text-gray-600 dark:text-gray-400 mb-2">
{$i18n.t('Web Search Confirmation Content')} {$i18n.t('Web Search Confirmation Content')}
</div> </div>
<Textarea <Textarea
className={textareaClass}
placeholder={$i18n.t( placeholder={$i18n.t(
'Your query will be sent to the configured web search provider.' 'Your query will be sent to the configured web search provider.'
)} )}
@@ -195,12 +192,12 @@
{/if} {/if}
<div class=" mb-2.5 flex w-full justify-between"> <div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-normal"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Web Search Engine')} {$i18n.t('Web Search Engine')}
</div> </div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<select <select
class="w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right" class="w-fit pr-8 rounded-lg px-2 p-1 text-xs bg-transparent outline-hidden text-right"
bind:value={webConfig.WEB_SEARCH_ENGINE} bind:value={webConfig.WEB_SEARCH_ENGINE}
placeholder={$i18n.t('Select a engine')} placeholder={$i18n.t('Select a engine')}
required required
@@ -223,7 +220,7 @@
{#if webConfig.WEB_SEARCH_ENGINE === 'ollama_cloud'} {#if webConfig.WEB_SEARCH_ENGINE === 'ollama_cloud'}
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div> <div>
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Ollama Cloud API Key')} {$i18n.t('Ollama Cloud API Key')}
</div> </div>
@@ -240,14 +237,14 @@
{:else if webConfig.WEB_SEARCH_ENGINE === 'perplexity_search'} {:else if webConfig.WEB_SEARCH_ENGINE === 'perplexity_search'}
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div> <div>
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Perplexity Search API URL')} {$i18n.t('Perplexity Search API URL')}
</div> </div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
type="text" type="text"
placeholder={$i18n.t('Enter Perplexity Search API URL')} placeholder={$i18n.t('Enter Perplexity Search API URL')}
bind:value={webConfig.PERPLEXITY_SEARCH_API_URL} bind:value={webConfig.PERPLEXITY_SEARCH_API_URL}
@@ -260,7 +257,7 @@
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div> <div>
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Perplexity API Key')} {$i18n.t('Perplexity API Key')}
</div> </div>
@@ -277,14 +274,14 @@
{:else if webConfig.WEB_SEARCH_ENGINE === 'searxng'} {:else if webConfig.WEB_SEARCH_ENGINE === 'searxng'}
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div> <div>
<div class=" self-left text-xs font-normal mb-1"> <div class=" self-left text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Searxng Query URL')} {$i18n.t('Searxng Query URL')}
</div> </div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
type="text" type="text"
placeholder={$i18n.t('Enter Searxng Query URL')} placeholder={$i18n.t('Enter Searxng Query URL')}
bind:value={webConfig.SEARXNG_QUERY_URL} bind:value={webConfig.SEARXNG_QUERY_URL}
@@ -295,14 +292,14 @@
</div> </div>
</div> </div>
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div class=" self-left text-xs font-normal mb-1"> <div class=" self-left text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Searxng search language (all, en, es, de, fr, etc.)')} {$i18n.t('Searxng search language (all, en, es, de, fr, etc.)')}
</div> </div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
type="text" type="text"
placeholder={$i18n.t('Enter Searxng search language')} placeholder={$i18n.t('Enter Searxng search language')}
bind:value={webConfig.SEARXNG_LANGUAGE} bind:value={webConfig.SEARXNG_LANGUAGE}
@@ -316,14 +313,14 @@
{:else if webConfig.WEB_SEARCH_ENGINE === 'yacy'} {:else if webConfig.WEB_SEARCH_ENGINE === 'yacy'}
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div> <div>
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Yacy Instance URL')} {$i18n.t('Yacy Instance URL')}
</div> </div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
type="text" type="text"
placeholder={$i18n.t('Enter Yacy URL (e.g. http://yacy.example.com:8090)')} placeholder={$i18n.t('Enter Yacy URL (e.g. http://yacy.example.com:8090)')}
bind:value={webConfig.YACY_QUERY_URL} bind:value={webConfig.YACY_QUERY_URL}
@@ -336,12 +333,12 @@
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div class="flex gap-2"> <div class="flex gap-2">
<div class="w-full"> <div class="w-full">
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Yacy Username')} {$i18n.t('Yacy Username')}
</div> </div>
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
placeholder={$i18n.t('Enter Yacy Username')} placeholder={$i18n.t('Enter Yacy Username')}
bind:value={webConfig.YACY_USERNAME} bind:value={webConfig.YACY_USERNAME}
required required
@@ -349,7 +346,7 @@
</div> </div>
<div class="w-full"> <div class="w-full">
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Yacy Password')} {$i18n.t('Yacy Password')}
</div> </div>
@@ -363,7 +360,7 @@
{:else if webConfig.WEB_SEARCH_ENGINE === 'google_pse'} {:else if webConfig.WEB_SEARCH_ENGINE === 'google_pse'}
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div> <div>
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Google PSE API Key')} {$i18n.t('Google PSE API Key')}
</div> </div>
@@ -373,14 +370,14 @@
/> />
</div> </div>
<div class="mt-1.5"> <div class="mt-1.5">
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Google PSE Engine Id')} {$i18n.t('Google PSE Engine Id')}
</div> </div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
type="text" type="text"
placeholder={$i18n.t('Enter Google PSE Engine Id')} placeholder={$i18n.t('Enter Google PSE Engine Id')}
bind:value={webConfig.GOOGLE_PSE_ENGINE_ID} bind:value={webConfig.GOOGLE_PSE_ENGINE_ID}
@@ -393,7 +390,7 @@
{:else if webConfig.WEB_SEARCH_ENGINE === 'brave'} {:else if webConfig.WEB_SEARCH_ENGINE === 'brave'}
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div> <div>
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Brave Search API Key')} {$i18n.t('Brave Search API Key')}
</div> </div>
@@ -406,7 +403,7 @@
{:else if webConfig.WEB_SEARCH_ENGINE === 'brave_llm_context'} {:else if webConfig.WEB_SEARCH_ENGINE === 'brave_llm_context'}
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div> <div>
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Brave Search API Key')} {$i18n.t('Brave Search API Key')}
</div> </div>
@@ -416,14 +413,14 @@
/> />
</div> </div>
<div class="mt-1.5"> <div class="mt-1.5">
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Context Tokens')} {$i18n.t('Context Tokens')}
</div> </div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
type="number" type="number"
min="1024" min="1024"
max="32768" max="32768"
@@ -439,7 +436,7 @@
{:else if webConfig.WEB_SEARCH_ENGINE === 'kagi'} {:else if webConfig.WEB_SEARCH_ENGINE === 'kagi'}
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div> <div>
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Kagi Search API Key')} {$i18n.t('Kagi Search API Key')}
</div> </div>
@@ -452,7 +449,7 @@
{:else if webConfig.WEB_SEARCH_ENGINE === 'mojeek'} {:else if webConfig.WEB_SEARCH_ENGINE === 'mojeek'}
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div> <div>
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Mojeek Search API Key')} {$i18n.t('Mojeek Search API Key')}
</div> </div>
@@ -465,7 +462,7 @@
{:else if webConfig.WEB_SEARCH_ENGINE === 'bocha'} {:else if webConfig.WEB_SEARCH_ENGINE === 'bocha'}
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div> <div>
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Bocha Search API Key')} {$i18n.t('Bocha Search API Key')}
</div> </div>
@@ -478,7 +475,7 @@
{:else if webConfig.WEB_SEARCH_ENGINE === 'serpstack'} {:else if webConfig.WEB_SEARCH_ENGINE === 'serpstack'}
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div> <div>
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Serpstack API Key')} {$i18n.t('Serpstack API Key')}
</div> </div>
@@ -491,7 +488,7 @@
{:else if webConfig.WEB_SEARCH_ENGINE === 'serper'} {:else if webConfig.WEB_SEARCH_ENGINE === 'serper'}
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div> <div>
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Serper API Key')} {$i18n.t('Serper API Key')}
</div> </div>
@@ -504,7 +501,7 @@
{:else if webConfig.WEB_SEARCH_ENGINE === 'serphouse'} {:else if webConfig.WEB_SEARCH_ENGINE === 'serphouse'}
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div> <div>
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('SERPHouse API Key')} {$i18n.t('SERPHouse API Key')}
</div> </div>
@@ -514,14 +511,14 @@
/> />
</div> </div>
<div class="mt-1.5"> <div class="mt-1.5">
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('SERPHouse Domain')} {$i18n.t('SERPHouse Domain')}
</div> </div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
type="text" type="text"
placeholder="google.com" placeholder="google.com"
bind:value={webConfig.SERPHOUSE_DOMAIN} bind:value={webConfig.SERPHOUSE_DOMAIN}
@@ -534,7 +531,7 @@
{:else if webConfig.WEB_SEARCH_ENGINE === 'serply'} {:else if webConfig.WEB_SEARCH_ENGINE === 'serply'}
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div> <div>
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Serply API Key')} {$i18n.t('Serply API Key')}
</div> </div>
@@ -547,7 +544,7 @@
{:else if webConfig.WEB_SEARCH_ENGINE === 'tavily'} {:else if webConfig.WEB_SEARCH_ENGINE === 'tavily'}
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div> <div>
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Tavily API Key')} {$i18n.t('Tavily API Key')}
</div> </div>
@@ -560,7 +557,7 @@
{:else if webConfig.WEB_SEARCH_ENGINE === 'searchapi'} {:else if webConfig.WEB_SEARCH_ENGINE === 'searchapi'}
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div> <div>
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('SearchApi API Key')} {$i18n.t('SearchApi API Key')}
</div> </div>
@@ -570,14 +567,14 @@
/> />
</div> </div>
<div class="mt-1.5"> <div class="mt-1.5">
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('SearchApi Engine')} {$i18n.t('SearchApi Engine')}
</div> </div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
type="text" type="text"
placeholder={$i18n.t('Enter SearchApi Engine')} placeholder={$i18n.t('Enter SearchApi Engine')}
bind:value={webConfig.SEARCHAPI_ENGINE} bind:value={webConfig.SEARCHAPI_ENGINE}
@@ -590,7 +587,7 @@
{:else if webConfig.WEB_SEARCH_ENGINE === 'serpapi'} {:else if webConfig.WEB_SEARCH_ENGINE === 'serpapi'}
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div> <div>
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('SerpApi API Key')} {$i18n.t('SerpApi API Key')}
</div> </div>
@@ -600,14 +597,14 @@
/> />
</div> </div>
<div class="mt-1.5"> <div class="mt-1.5">
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('SerpApi Engine')} {$i18n.t('SerpApi Engine')}
</div> </div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
type="text" type="text"
placeholder={$i18n.t('Enter SerpApi Engine')} placeholder={$i18n.t('Enter SerpApi Engine')}
bind:value={webConfig.SERPAPI_ENGINE} bind:value={webConfig.SERPAPI_ENGINE}
@@ -620,14 +617,14 @@
{:else if webConfig.WEB_SEARCH_ENGINE === 'jina'} {:else if webConfig.WEB_SEARCH_ENGINE === 'jina'}
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div> <div>
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Jina API Base URL')} {$i18n.t('Jina API Base URL')}
</div> </div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
type="text" type="text"
placeholder={$i18n.t('Enter Jina API Base URL')} placeholder={$i18n.t('Enter Jina API Base URL')}
bind:value={webConfig.JINA_API_BASE_URL} bind:value={webConfig.JINA_API_BASE_URL}
@@ -638,7 +635,7 @@
</div> </div>
<div class="mt-2"> <div class="mt-2">
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Jina API Key')} {$i18n.t('Jina API Key')}
</div> </div>
@@ -651,14 +648,14 @@
{:else if webConfig.WEB_SEARCH_ENGINE === 'bing'} {:else if webConfig.WEB_SEARCH_ENGINE === 'bing'}
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div> <div>
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Bing Search V7 Endpoint')} {$i18n.t('Bing Search V7 Endpoint')}
</div> </div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
type="text" type="text"
placeholder={$i18n.t('Enter Bing Search V7 Endpoint')} placeholder={$i18n.t('Enter Bing Search V7 Endpoint')}
bind:value={webConfig.BING_SEARCH_V7_ENDPOINT} bind:value={webConfig.BING_SEARCH_V7_ENDPOINT}
@@ -669,7 +666,7 @@
</div> </div>
<div class="mt-2"> <div class="mt-2">
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Bing Search V7 Subscription Key')} {$i18n.t('Bing Search V7 Subscription Key')}
</div> </div>
@@ -682,7 +679,7 @@
{:else if webConfig.WEB_SEARCH_ENGINE === 'exa'} {:else if webConfig.WEB_SEARCH_ENGINE === 'exa'}
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div> <div>
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Exa API Key')} {$i18n.t('Exa API Key')}
</div> </div>
@@ -695,7 +692,7 @@
{:else if webConfig.WEB_SEARCH_ENGINE === 'perplexity'} {:else if webConfig.WEB_SEARCH_ENGINE === 'perplexity'}
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div> <div>
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Perplexity API Key')} {$i18n.t('Perplexity API Key')}
</div> </div>
@@ -708,12 +705,12 @@
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div> <div>
<div class="self-center text-xs font-normal mb-1"> <div class="self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Perplexity Model')} {$i18n.t('Perplexity Model')}
</div> </div>
<input <input
list="perplexity-model-list" list="perplexity-model-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
bind:value={webConfig.PERPLEXITY_MODEL} bind:value={webConfig.PERPLEXITY_MODEL}
/> />
@@ -729,11 +726,11 @@
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div> <div>
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Perplexity Search Context Usage')} {$i18n.t('Perplexity Search Context Usage')}
</div> </div>
<select <select
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
bind:value={webConfig.PERPLEXITY_SEARCH_CONTEXT_USAGE} bind:value={webConfig.PERPLEXITY_SEARCH_CONTEXT_USAGE}
> >
<option value="low">{$i18n.t('Low')}</option> <option value="low">{$i18n.t('Low')}</option>
@@ -745,14 +742,14 @@
{:else if webConfig.WEB_SEARCH_ENGINE === 'microsoft_web_iq'} {:else if webConfig.WEB_SEARCH_ENGINE === 'microsoft_web_iq'}
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div> <div>
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Microsoft Web IQ API Base URL')} {$i18n.t('Microsoft Web IQ API Base URL')}
</div> </div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
type="text" type="text"
placeholder={$i18n.t('Enter Microsoft Web IQ API Base URL')} placeholder={$i18n.t('Enter Microsoft Web IQ API Base URL')}
bind:value={webConfig.MICROSOFT_WEB_IQ_API_BASE_URL} bind:value={webConfig.MICROSOFT_WEB_IQ_API_BASE_URL}
@@ -763,7 +760,7 @@
</div> </div>
<div class="mt-2"> <div class="mt-2">
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Microsoft Web IQ API Key')} {$i18n.t('Microsoft Web IQ API Key')}
</div> </div>
@@ -774,12 +771,12 @@
</div> </div>
<div class="mt-2"> <div class="mt-2">
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Language')} {$i18n.t('Language')}
</div> </div>
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
type="text" type="text"
placeholder={$i18n.t('Enter language')} placeholder={$i18n.t('Enter language')}
bind:value={webConfig.MICROSOFT_WEB_IQ_LANGUAGE} bind:value={webConfig.MICROSOFT_WEB_IQ_LANGUAGE}
@@ -790,7 +787,7 @@
{:else if webConfig.WEB_SEARCH_ENGINE === 'sougou'} {:else if webConfig.WEB_SEARCH_ENGINE === 'sougou'}
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div> <div>
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Sougou Search API sID')} {$i18n.t('Sougou Search API sID')}
</div> </div>
@@ -802,7 +799,7 @@
</div> </div>
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div> <div>
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Sougou Search API SK')} {$i18n.t('Sougou Search API SK')}
</div> </div>
@@ -815,14 +812,14 @@
{:else if webConfig.WEB_SEARCH_ENGINE === 'firecrawl'} {:else if webConfig.WEB_SEARCH_ENGINE === 'firecrawl'}
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div> <div>
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Firecrawl API Base URL')} {$i18n.t('Firecrawl API Base URL')}
</div> </div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
type="text" type="text"
placeholder={$i18n.t('Enter Firecrawl API Base URL')} placeholder={$i18n.t('Enter Firecrawl API Base URL')}
bind:value={webConfig.FIRECRAWL_API_BASE_URL} bind:value={webConfig.FIRECRAWL_API_BASE_URL}
@@ -833,7 +830,7 @@
</div> </div>
<div class="mt-2"> <div class="mt-2">
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Firecrawl API Key')} {$i18n.t('Firecrawl API Key')}
</div> </div>
@@ -844,14 +841,14 @@
</div> </div>
<div class="mt-2"> <div class="mt-2">
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Firecrawl Timeout (s)')} {$i18n.t('Firecrawl Timeout (s)')}
</div> </div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
type="number" type="number"
placeholder={$i18n.t('Enter Firecrawl Timeout')} placeholder={$i18n.t('Enter Firecrawl Timeout')}
bind:value={webConfig.FIRECRAWL_TIMEOUT} bind:value={webConfig.FIRECRAWL_TIMEOUT}
@@ -864,14 +861,14 @@
{:else if webConfig.WEB_SEARCH_ENGINE === 'external'} {:else if webConfig.WEB_SEARCH_ENGINE === 'external'}
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div> <div>
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('External Web Search URL')} {$i18n.t('External Web Search URL')}
</div> </div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
type="text" type="text"
placeholder={$i18n.t('Enter External Web Search URL')} placeholder={$i18n.t('Enter External Web Search URL')}
bind:value={webConfig.EXTERNAL_WEB_SEARCH_URL} bind:value={webConfig.EXTERNAL_WEB_SEARCH_URL}
@@ -882,7 +879,7 @@
</div> </div>
<div class="mt-2"> <div class="mt-2">
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('External Web Search API Key')} {$i18n.t('External Web Search API Key')}
</div> </div>
@@ -895,14 +892,14 @@
{:else if webConfig.WEB_SEARCH_ENGINE === 'yandex'} {:else if webConfig.WEB_SEARCH_ENGINE === 'yandex'}
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div> <div>
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Yandex Web Search URL')} {$i18n.t('Yandex Web Search URL')}
</div> </div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
type="text" type="text"
placeholder={$i18n.t('Enter Yandex Web Search URL')} placeholder={$i18n.t('Enter Yandex Web Search URL')}
bind:value={webConfig.YANDEX_WEB_SEARCH_URL} bind:value={webConfig.YANDEX_WEB_SEARCH_URL}
@@ -913,7 +910,7 @@
</div> </div>
<div class="mt-2"> <div class="mt-2">
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Yandex Web Search API Key')} {$i18n.t('Yandex Web Search API Key')}
</div> </div>
@@ -924,7 +921,9 @@
</div> </div>
<div class="mb-2.5"> <div class="mb-2.5">
<div class=" mb-1 text-xs font-normal">{$i18n.t('Yandex Web Search config')}</div> <div class=" mb-1 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Yandex Web Search config')}
</div>
<Tooltip <Tooltip
content={$i18n.t( content={$i18n.t(
@@ -933,6 +932,7 @@
placement="top-start" placement="top-start"
> >
<Textarea <Textarea
className={textareaClass}
bind:value={webConfig.YANDEX_WEB_SEARCH_CONFIG} bind:value={webConfig.YANDEX_WEB_SEARCH_CONFIG}
placeholder={$i18n.t( placeholder={$i18n.t(
'Leave empty to use the default config, or enter a valid json (see https://yandex.cloud/en/docs/search-api/api-ref/WebSearch/search#yandex.cloud.searchapi.v2.WebSearchRequest)' 'Leave empty to use the default config, or enter a valid json (see https://yandex.cloud/en/docs/search-api/api-ref/WebSearch/search#yandex.cloud.searchapi.v2.WebSearchRequest)'
@@ -944,7 +944,7 @@
{:else if webConfig.WEB_SEARCH_ENGINE === 'youcom'} {:else if webConfig.WEB_SEARCH_ENGINE === 'youcom'}
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div> <div>
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('You.com API Key')} {$i18n.t('You.com API Key')}
</div> </div>
@@ -957,7 +957,7 @@
{:else if webConfig.WEB_SEARCH_ENGINE === 'linkup'} {:else if webConfig.WEB_SEARCH_ENGINE === 'linkup'}
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div> <div>
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Linkup API Key')} {$i18n.t('Linkup API Key')}
</div> </div>
@@ -968,11 +968,12 @@
</div> </div>
<div class="mt-2"> <div class="mt-2">
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Parameters')} {$i18n.t('Parameters')}
</div> </div>
<Textarea <Textarea
className={textareaClass}
bind:value={webConfig.LINKUP_SEARCH_PARAMS} bind:value={webConfig.LINKUP_SEARCH_PARAMS}
placeholder={`{\n "depth": "standard",\n "outputType": "sourcedAnswer"\n}`} placeholder={`{\n "depth": "standard",\n "outputType": "sourcedAnswer"\n}`}
/> />
@@ -983,14 +984,14 @@
{#if webConfig.WEB_SEARCH_ENGINE === 'duckduckgo'} {#if webConfig.WEB_SEARCH_ENGINE === 'duckduckgo'}
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div> <div>
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('DDGS Backend')} {$i18n.t('DDGS Backend')}
</div> </div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<select <select
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
bind:value={webConfig.DDGS_BACKEND} bind:value={webConfig.DDGS_BACKEND}
> >
<option value="auto">{$i18n.t('Auto (Random)')}</option> <option value="auto">{$i18n.t('Auto (Random)')}</option>
@@ -1015,12 +1016,12 @@
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div class="flex gap-2"> <div class="flex gap-2">
<div class="w-full"> <div class="w-full">
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Search Result Count')} {$i18n.t('Search Result Count')}
</div> </div>
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
placeholder={$i18n.t('Search Result Count')} placeholder={$i18n.t('Search Result Count')}
bind:value={webConfig.WEB_SEARCH_RESULT_COUNT} bind:value={webConfig.WEB_SEARCH_RESULT_COUNT}
required required
@@ -1028,7 +1029,7 @@
</div> </div>
<div class="w-full"> <div class="w-full">
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
<Tooltip <Tooltip
content={$i18n.t( content={$i18n.t(
'Limit concurrent search queries. 0 = unlimited (default). Set to 1 for sequential execution (recommended for APIs with strict rate limits like Brave free tier).' 'Limit concurrent search queries. 0 = unlimited (default). Set to 1 for sequential execution (recommended for APIs with strict rate limits like Brave free tier).'
@@ -1040,7 +1041,7 @@
</div> </div>
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
placeholder={$i18n.t('Concurrent Requests')} placeholder={$i18n.t('Concurrent Requests')}
bind:value={webConfig.WEB_SEARCH_CONCURRENT_REQUESTS} bind:value={webConfig.WEB_SEARCH_CONCURRENT_REQUESTS}
type="number" type="number"
@@ -1051,7 +1052,7 @@
</div> </div>
<div class="mb-2.5 w-full"> <div class="mb-2.5 w-full">
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
<Tooltip <Tooltip
content={$i18n.t( content={$i18n.t(
'Maximum characters to return from fetched URLs. Leave empty for no limit.' 'Maximum characters to return from fetched URLs. Leave empty for no limit.'
@@ -1063,7 +1064,7 @@
</div> </div>
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
placeholder={$i18n.t('No limit')} placeholder={$i18n.t('No limit')}
bind:value={webConfig.WEB_FETCH_MAX_CONTENT_LENGTH} bind:value={webConfig.WEB_FETCH_MAX_CONTENT_LENGTH}
type="number" type="number"
@@ -1072,12 +1073,12 @@
</div> </div>
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div class=" text-xs font-normal mb-1"> <div class=" text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Domain Filter List')} {$i18n.t('Domain Filter List')}
</div> </div>
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
placeholder={$i18n.t( placeholder={$i18n.t(
'Enter domains separated by commas (e.g., example.com,site.org,!excludedsite.com)' 'Enter domains separated by commas (e.g., example.com,site.org,!excludedsite.com)'
)} )}
@@ -1087,7 +1088,7 @@
{/if} {/if}
<div class=" mb-2.5 flex w-full justify-between"> <div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-normal"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
<Tooltip content={$i18n.t('Full Context Mode')} placement="top-start"> <Tooltip content={$i18n.t('Full Context Mode')} placement="top-start">
{$i18n.t('Bypass Embedding and Retrieval')} {$i18n.t('Bypass Embedding and Retrieval')}
</Tooltip> </Tooltip>
@@ -1108,7 +1109,7 @@
</div> </div>
<div class=" mb-2.5 flex w-full justify-between"> <div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-normal"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
<Tooltip content={$i18n.t('Bypass Web Loader')} placement="top-start"> <Tooltip content={$i18n.t('Bypass Web Loader')} placement="top-start">
{$i18n.t('Bypass Web Loader')} {$i18n.t('Bypass Web Loader')}
</Tooltip> </Tooltip>
@@ -1121,7 +1122,7 @@
</div> </div>
<div class=" mb-2.5 flex w-full justify-between"> <div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-normal"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Trust Proxy Environment')} {$i18n.t('Trust Proxy Environment')}
</div> </div>
<div class="flex items-center relative"> <div class="flex items-center relative">
@@ -1138,20 +1139,18 @@
</div> </div>
</div> </div>
<div class="mb-3"> <div class="mb-5">
<div class=" mt-0.5 mb-2.5 text-xs text-gray-400 dark:text-gray-600"> <div class=" mb-2 text-xs text-gray-400 dark:text-gray-600">
{$i18n.t('Loader')} {$i18n.t('Loader')}
</div> </div>
<hr class=" border-gray-100/20 dark:border-white/[0.025] my-2" />
<div class=" mb-2.5 flex w-full justify-between"> <div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-normal"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Web Loader Engine')} {$i18n.t('Web Loader Engine')}
</div> </div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<select <select
class="w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right" class="w-fit pr-8 rounded-lg px-2 p-1 text-xs bg-transparent outline-hidden text-right"
bind:value={webConfig.WEB_LOADER_ENGINE} bind:value={webConfig.WEB_LOADER_ENGINE}
placeholder={$i18n.t('Select a engine')} placeholder={$i18n.t('Select a engine')}
> >
@@ -1165,12 +1164,12 @@
{#if webConfig.WEB_LOADER_ENGINE === '' || webConfig.WEB_LOADER_ENGINE === 'safe_web'} {#if webConfig.WEB_LOADER_ENGINE === '' || webConfig.WEB_LOADER_ENGINE === 'safe_web'}
<div class=" mb-2.5 flex w-full justify-between"> <div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-normal"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Timeout')} {$i18n.t('Timeout')}
</div> </div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<input <input
class="flex-1 w-full text-sm bg-transparent outline-hidden" class="flex-1 w-full text-xs bg-transparent outline-hidden"
placeholder={$i18n.t('Timeout')} placeholder={$i18n.t('Timeout')}
bind:value={webConfig.WEB_LOADER_TIMEOUT} bind:value={webConfig.WEB_LOADER_TIMEOUT}
/> />
@@ -1178,7 +1177,7 @@
</div> </div>
<div class=" mb-2.5 flex w-full justify-between"> <div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-normal"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Verify SSL Certificate')} {$i18n.t('Verify SSL Certificate')}
</div> </div>
<div class="flex items-center relative"> <div class="flex items-center relative">
@@ -1188,14 +1187,14 @@
{:else if webConfig.WEB_LOADER_ENGINE === 'playwright'} {:else if webConfig.WEB_LOADER_ENGINE === 'playwright'}
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div> <div>
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Playwright WebSocket URL')} {$i18n.t('Playwright WebSocket URL')}
</div> </div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
type="text" type="text"
placeholder={$i18n.t('Enter Playwright WebSocket URL')} placeholder={$i18n.t('Enter Playwright WebSocket URL')}
bind:value={webConfig.PLAYWRIGHT_WS_URL} bind:value={webConfig.PLAYWRIGHT_WS_URL}
@@ -1206,14 +1205,14 @@
</div> </div>
<div class="mt-2"> <div class="mt-2">
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Playwright Timeout (ms)')} {$i18n.t('Playwright Timeout (ms)')}
</div> </div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
placeholder={$i18n.t('Enter Playwright Timeout')} placeholder={$i18n.t('Enter Playwright Timeout')}
bind:value={webConfig.PLAYWRIGHT_TIMEOUT} bind:value={webConfig.PLAYWRIGHT_TIMEOUT}
autocomplete="off" autocomplete="off"
@@ -1225,14 +1224,14 @@
{:else if webConfig.WEB_LOADER_ENGINE === 'firecrawl' && webConfig.WEB_SEARCH_ENGINE !== 'firecrawl'} {:else if webConfig.WEB_LOADER_ENGINE === 'firecrawl' && webConfig.WEB_SEARCH_ENGINE !== 'firecrawl'}
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div> <div>
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Firecrawl API Base URL')} {$i18n.t('Firecrawl API Base URL')}
</div> </div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
type="text" type="text"
placeholder={$i18n.t('Enter Firecrawl API Base URL')} placeholder={$i18n.t('Enter Firecrawl API Base URL')}
bind:value={webConfig.FIRECRAWL_API_BASE_URL} bind:value={webConfig.FIRECRAWL_API_BASE_URL}
@@ -1243,7 +1242,7 @@
</div> </div>
<div class="mt-2"> <div class="mt-2">
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Firecrawl API Key')} {$i18n.t('Firecrawl API Key')}
</div> </div>
@@ -1256,14 +1255,14 @@
{:else if webConfig.WEB_LOADER_ENGINE === 'tavily'} {:else if webConfig.WEB_LOADER_ENGINE === 'tavily'}
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div> <div>
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Tavily Extract Depth')} {$i18n.t('Tavily Extract Depth')}
</div> </div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
type="text" type="text"
placeholder={$i18n.t('Enter Tavily Extract Depth')} placeholder={$i18n.t('Enter Tavily Extract Depth')}
bind:value={webConfig.TAVILY_EXTRACT_DEPTH} bind:value={webConfig.TAVILY_EXTRACT_DEPTH}
@@ -1275,7 +1274,7 @@
{#if webConfig.WEB_SEARCH_ENGINE !== 'tavily'} {#if webConfig.WEB_SEARCH_ENGINE !== 'tavily'}
<div class="mt-2"> <div class="mt-2">
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Tavily API Key')} {$i18n.t('Tavily API Key')}
</div> </div>
@@ -1290,14 +1289,14 @@
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
{#if webConfig.WEB_SEARCH_ENGINE !== 'microsoft_web_iq'} {#if webConfig.WEB_SEARCH_ENGINE !== 'microsoft_web_iq'}
<div> <div>
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Microsoft Web IQ API Base URL')} {$i18n.t('Microsoft Web IQ API Base URL')}
</div> </div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
type="text" type="text"
placeholder={$i18n.t('Enter Microsoft Web IQ API Base URL')} placeholder={$i18n.t('Enter Microsoft Web IQ API Base URL')}
bind:value={webConfig.MICROSOFT_WEB_IQ_API_BASE_URL} bind:value={webConfig.MICROSOFT_WEB_IQ_API_BASE_URL}
@@ -1308,7 +1307,7 @@
</div> </div>
<div class="mt-2"> <div class="mt-2">
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Microsoft Web IQ API Key')} {$i18n.t('Microsoft Web IQ API Key')}
</div> </div>
@@ -1319,12 +1318,12 @@
</div> </div>
<div class="mt-2"> <div class="mt-2">
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Language')} {$i18n.t('Language')}
</div> </div>
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
type="text" type="text"
placeholder={$i18n.t('Enter language')} placeholder={$i18n.t('Enter language')}
bind:value={webConfig.MICROSOFT_WEB_IQ_LANGUAGE} bind:value={webConfig.MICROSOFT_WEB_IQ_LANGUAGE}
@@ -1336,14 +1335,14 @@
{:else if webConfig.WEB_LOADER_ENGINE === 'external'} {:else if webConfig.WEB_LOADER_ENGINE === 'external'}
<div class="mb-2.5 flex w-full flex-col"> <div class="mb-2.5 flex w-full flex-col">
<div> <div>
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('External Web Loader URL')} {$i18n.t('External Web Loader URL')}
</div> </div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
type="text" type="text"
placeholder={$i18n.t('Enter External Web Loader URL')} placeholder={$i18n.t('Enter External Web Loader URL')}
bind:value={webConfig.EXTERNAL_WEB_LOADER_URL} bind:value={webConfig.EXTERNAL_WEB_LOADER_URL}
@@ -1354,7 +1353,7 @@
</div> </div>
<div class="mt-2"> <div class="mt-2">
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('External Web Loader API Key')} {$i18n.t('External Web Loader API Key')}
</div> </div>
@@ -1367,12 +1366,12 @@
{/if} {/if}
<div class="mb-2.5 w-full"> <div class="mb-2.5 w-full">
<div class=" self-center text-xs font-normal mb-1"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400 mb-1">
{$i18n.t('Concurrent Requests')} {$i18n.t('Concurrent Requests')}
</div> </div>
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
placeholder={$i18n.t('Concurrent Requests')} placeholder={$i18n.t('Concurrent Requests')}
bind:value={webConfig.WEB_LOADER_CONCURRENT_REQUESTS} bind:value={webConfig.WEB_LOADER_CONCURRENT_REQUESTS}
required required
@@ -1380,12 +1379,12 @@
</div> </div>
<div class=" mb-2.5 flex w-full justify-between"> <div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-normal"> <div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Youtube Language')} {$i18n.t('Youtube Language')}
</div> </div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<input <input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden" class="flex-1 w-full rounded-lg text-xs bg-transparent outline-hidden"
type="text" type="text"
placeholder={$i18n.t('Enter language codes')} placeholder={$i18n.t('Enter language codes')}
bind:value={webConfig.YOUTUBE_LOADER_LANGUAGE} bind:value={webConfig.YOUTUBE_LOADER_LANGUAGE}
@@ -1395,12 +1394,12 @@
</div> </div>
<div class=" mb-2.5 flex flex-col w-full justify-between"> <div class=" mb-2.5 flex flex-col w-full justify-between">
<div class=" mb-1 text-xs font-normal"> <div class=" mb-1 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Youtube Proxy URL')} {$i18n.t('Youtube Proxy URL')}
</div> </div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<input <input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden" class="flex-1 w-full rounded-lg text-xs bg-transparent outline-hidden"
type="text" type="text"
placeholder={$i18n.t('Enter proxy URL (e.g. https://user:password@host:port)')} placeholder={$i18n.t('Enter proxy URL (e.g. https://user:password@host:port)')}
bind:value={webConfig.YOUTUBE_LOADER_PROXY_URL} bind:value={webConfig.YOUTUBE_LOADER_PROXY_URL}
@@ -1412,7 +1411,7 @@
</div> </div>
{/if} {/if}
</div> </div>
<div class="flex justify-end pt-3 text-sm font-normal"> <div class="flex justify-end pt-6 text-sm font-normal">
<button <button
class="px-3.5 py-1.5 text-sm font-normal bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full" class="px-3.5 py-1.5 text-sm font-normal bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
type="submit" type="submit"
+53
View File
@@ -70,6 +70,7 @@
import { import {
archiveChatById, archiveChatById,
cloneChatById,
compactChatById, compactChatById,
createNewChat, createNewChat,
deleteChatById, deleteChatById,
@@ -2265,6 +2266,53 @@
document.getElementById('chat-input')?.focus(); document.getElementById('chat-input')?.focus();
}; };
const handleForkChat = async () => {
if (!$chatId || !history?.currentId) {
toast.message($i18n.t('No chat to fork'));
return;
}
if (!($user?.role === 'admin' || ($user?.permissions?.chat?.import ?? true))) {
toast.error($i18n.t('Access prohibited'));
return;
}
const currentMessage = history.messages?.[history.currentId];
if (
generating ||
taskIds?.length ||
(currentMessage?.role === 'assistant' && !currentMessage.done)
) {
toast.warning($i18n.t('Wait for the current response to finish before forking.'));
return;
}
const toastId = toast.loading($i18n.t('Forking chat...'));
try {
const result = await cloneChatById(
localStorage.token,
$chatId,
$i18n.t('Clone of {{TITLE}}', {
TITLE: $chatTitle || 'Chat'
})
);
if (result?.id) {
await goto(`/c/${result.id}`);
await refreshChatList(localStorage.token, { refreshPinned: true });
toast.success($i18n.t('Chat forked'), { id: toastId });
} else {
toast.error($i18n.t('Failed to fork chat'), { id: toastId });
}
} catch (error) {
toast.error(`${error}`, { id: toastId });
} finally {
messageInput?.setText('');
prompt = '';
document.getElementById('chat-input')?.focus();
}
};
const submitHandler = async (userPrompt, { _raw = false } = {}) => { const submitHandler = async (userPrompt, { _raw = false } = {}) => {
console.log('submitHandler', userPrompt, $chatId); console.log('submitHandler', userPrompt, $chatId);
@@ -2284,6 +2332,10 @@
handleStatusCommand(); handleStatusCommand();
return; return;
} }
if (String(userPrompt).trim() === '/fork') {
await handleForkChat();
return;
}
if (pendingOAuthTools.length > 0) { if (pendingOAuthTools.length > 0) {
toast.warning($i18n.t('Please connect all required integrations before sending a message')); toast.warning($i18n.t('Please connect all required integrations before sending a message'));
@@ -3451,6 +3503,7 @@
{contextUsage} {contextUsage}
compactHandler={handleManualCompact} compactHandler={handleManualCompact}
statusHandler={handleStatusCommand} statusHandler={handleStatusCommand}
forkHandler={handleForkChat}
toolServers={$toolServers} toolServers={$toolServers}
{generating} {generating}
{stopResponse} {stopResponse}
@@ -116,6 +116,7 @@
export let stopResponse: Function; export let stopResponse: Function;
export let compactHandler: Function = () => {}; export let compactHandler: Function = () => {};
export let statusHandler: Function = () => {}; export let statusHandler: Function = () => {};
export let forkHandler: Function = () => {};
export let chatId = ''; export let chatId = '';
export let contextUsage = null; export let contextUsage = null;
@@ -1086,9 +1087,12 @@
canCompact: () => !!history?.currentId, canCompact: () => !!history?.currentId,
compactDisabled: () => isActive, compactDisabled: () => isActive,
canStatus: () => !!history?.currentId, canStatus: () => !!history?.currentId,
canFork: () => !!history?.currentId,
forkDisabled: () => isActive,
contextUsage: () => contextUsage, contextUsage: () => contextUsage,
onCompact: compactHandler, onCompact: compactHandler,
onStatus: statusHandler, onStatus: statusHandler,
onFork: forkHandler,
onSelect: (e) => { onSelect: (e) => {
const { type, data } = e; const { type, data } = e;
@@ -14,16 +14,21 @@
export let onUpload: (e: any) => void = () => {}; export let onUpload: (e: any) => void = () => {};
export let onCompact: () => void = () => {}; export let onCompact: () => void = () => {};
export let onStatus: () => void = () => {}; export let onStatus: () => void = () => {};
export let onFork: () => void = () => {};
export let insertTextHandler: (text: string) => void = () => {}; export let insertTextHandler: (text: string) => void = () => {};
export let canCompact: boolean | (() => boolean) = false; export let canCompact: boolean | (() => boolean) = false;
export let compactDisabled: boolean | (() => boolean) = false; export let compactDisabled: boolean | (() => boolean) = false;
export let canStatus: boolean | (() => boolean) = false; export let canStatus: boolean | (() => boolean) = false;
export let canFork: boolean | (() => boolean) = false;
export let forkDisabled: boolean | (() => boolean) = false;
export let contextUsage = null; export let contextUsage = null;
$: compactAvailable = typeof canCompact === 'function' ? canCompact() : canCompact; $: compactAvailable = typeof canCompact === 'function' ? canCompact() : canCompact;
$: isCompactDisabled = $: isCompactDisabled =
typeof compactDisabled === 'function' ? compactDisabled() : compactDisabled; typeof compactDisabled === 'function' ? compactDisabled() : compactDisabled;
$: statusAvailable = typeof canStatus === 'function' ? canStatus() : canStatus; $: statusAvailable = typeof canStatus === 'function' ? canStatus() : canStatus;
$: forkAvailable = typeof canFork === 'function' ? canFork() : canFork;
$: isForkDisabled = typeof forkDisabled === 'function' ? forkDisabled() : forkDisabled;
$: resolvedContextUsage = typeof contextUsage === 'function' ? contextUsage() : contextUsage; $: resolvedContextUsage = typeof contextUsage === 'function' ? contextUsage() : contextUsage;
$: contextPercent = Math.max(0, Math.round(resolvedContextUsage?.percent ?? 0)); $: contextPercent = Math.max(0, Math.round(resolvedContextUsage?.percent ?? 0));
@@ -77,6 +82,8 @@
canCompact={compactAvailable} canCompact={compactAvailable}
compactDisabled={isCompactDisabled} compactDisabled={isCompactDisabled}
canStatus={statusAvailable} canStatus={statusAvailable}
canFork={forkAvailable}
forkDisabled={isForkDisabled}
{contextPercent} {contextPercent}
onSelect={(e) => { onSelect={(e) => {
const { type, data } = e; const { type, data } = e;
@@ -89,6 +96,9 @@
} else if (type === 'command' && data.id === 'status') { } else if (type === 'command' && data.id === 'status') {
insertTextHandler(''); insertTextHandler('');
onStatus(); onStatus();
} else if (type === 'command' && data.id === 'fork') {
insertTextHandler('');
onFork();
} else if (type === 'skill') { } else if (type === 'skill') {
command({ command({
id: `${data.id}|${data.name}`, id: `${data.id}|${data.name}`,
@@ -12,6 +12,8 @@
export let canCompact = false; export let canCompact = false;
export let compactDisabled = false; export let compactDisabled = false;
export let canStatus = false; export let canStatus = false;
export let canFork = false;
export let forkDisabled = false;
export let contextPercent = 0; export let contextPercent = 0;
let selectedIdx = 0; let selectedIdx = 0;
@@ -28,6 +30,9 @@
...(canCompact && 'compact'.startsWith(query.toLowerCase()) ...(canCompact && 'compact'.startsWith(query.toLowerCase())
? [{ type: 'command', data: { id: 'compact' } }] ? [{ type: 'command', data: { id: 'compact' } }]
: []), : []),
...(canFork && 'fork'.startsWith(query.toLowerCase())
? [{ type: 'command', data: { id: 'fork' } }]
: []),
...(canStatus && 'status'.startsWith(query.toLowerCase()) ...(canStatus && 'status'.startsWith(query.toLowerCase())
? [{ type: 'command', data: { id: 'status' } }] ? [{ type: 'command', data: { id: 'status' } }]
: []) : [])
@@ -86,6 +91,9 @@
if (item?.type === 'command' && item.data?.id === 'compact' && compactDisabled) { if (item?.type === 'command' && item.data?.id === 'compact' && compactDisabled) {
return; return;
} }
if (item?.type === 'command' && item.data?.id === 'fork' && forkDisabled) {
return;
}
if (item) { if (item) {
onSelect(item); onSelect(item);
} }
@@ -167,6 +175,50 @@
</span> </span>
</button> </button>
</Tooltip> </Tooltip>
{:else if item.data.id === 'fork'}
<Tooltip content="Clone this chat into a new copy." placement="top">
<button
type="button"
aria-label="Fork: clone this chat into a new copy."
class="slash-command-row flex items-center gap-2 w-full h-6 px-2 rounded-xl text-xs text-left transition-colors duration-75
{commandIdx === selectedIdx ? 'app-interactive-active' : ''} disabled:opacity-50"
disabled={forkDisabled}
on:mousedown={(e) => e.preventDefault()}
on:click={() => {
if (!forkDisabled) {
onSelect(item);
}
}}
on:mouseenter={() => {
selectedIdx = commandIdx;
}}
on:focus={() => {}}
data-selected={commandIdx === selectedIdx}
>
<span class="app-icon-muted flex items-center justify-center w-4 shrink-0">
<svg
class="size-3.5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M4 12H9" />
<path d="M9 12C12.5 12 12.5 7 16 7H20" />
<path d="M17 4L20 7L17 10" />
<path d="M9 12C12.5 12 12.5 17 16 17H20" />
<path d="M17 14L20 17L17 20" />
</svg>
</span>
<span class="flex-1 min-w-0 flex items-baseline gap-1.5 overflow-hidden">
<span class="truncate">Fork</span>
<span class="app-muted text-[0.625rem] truncate shrink-0">Clone chat</span>
</span>
</button>
</Tooltip>
{:else if item.data.id === 'status'} {:else if item.data.id === 'status'}
<Tooltip content="Check what is running in this chat." placement="top"> <Tooltip content="Check what is running in this chat." placement="top">
<button <button
+100 -107
View File
@@ -124,119 +124,108 @@
}); });
</script> </script>
<div id="tab-account" class="flex flex-col h-full justify-between text-sm"> <div id="tab-account" class="flex h-full flex-col text-sm">
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5 -mr-1.5"> <div class="flex-1 min-h-0 w-full overflow-y-auto scrollbar-hover pr-1.5 -mr-1.5 py-1">
<div class="space-y-1"> <h2 class="mb-4 text-sm font-medium text-gray-900 dark:text-white">{$i18n.t('Account')}</h2>
<div>
<div class="text-xs text-gray-400 dark:text-gray-600 mb-2">
{$i18n.t('Your Account')}
</div>
<div class="text-xs text-gray-500 mt-0.5">
{$i18n.t('Manage your account information.')}
</div>
</div>
<div class="space-y-3">
<!-- <div class=" text-sm font-normal">{$i18n.t('Account')}</div> --> <!-- <div class=" text-sm font-normal">{$i18n.t('Account')}</div> -->
<div class="flex space-x-5 my-4"> <UserProfileImage
<UserProfileImage bind:profileImageUrl user={$user} /> bind:profileImageUrl
user={$user}
variant="account"
displayName={$user?.name}
/>
<div class="flex flex-1 flex-col"> <div class="flex flex-col w-full">
<div class=" flex-1"> <div class="mb-1 text-xs text-gray-400 dark:text-gray-600">{$i18n.t('Name')}</div>
<div class="flex flex-col w-full">
<div class=" mb-1 text-xs font-normal">{$i18n.t('Name')}</div>
<div class="flex-1"> <input
<input class="w-full bg-transparent py-1 text-[0.8125rem] text-gray-700 outline-hidden placeholder:text-gray-300 dark:text-gray-300 dark:placeholder:text-gray-700"
class="w-full text-sm dark:text-gray-300 bg-transparent outline-hidden" type="text"
type="text" bind:value={name}
bind:value={name} aria-label={$i18n.t('Name')}
aria-label={$i18n.t('Name')} required
required placeholder={$i18n.t('Enter your name')}
placeholder={$i18n.t('Enter your name')} />
/> </div>
</div>
</div>
<div class="flex flex-col w-full mt-2"> <div class="mt-2 flex flex-col w-full">
<div class=" mb-1 text-xs font-normal">{$i18n.t('Bio')}</div> <div class="mb-1 text-xs text-gray-400 dark:text-gray-600">{$i18n.t('Bio')}</div>
<div class="flex-1"> <Textarea
<Textarea className="w-full bg-transparent py-1 text-[0.8125rem] text-gray-700 outline-hidden placeholder:text-gray-300 dark:text-gray-300 dark:placeholder:text-gray-700"
className="w-full text-sm dark:text-gray-300 bg-transparent outline-hidden" minSize={60}
minSize={60} bind:value={bio}
bind:value={bio} ariaLabel={$i18n.t('Bio')}
ariaLabel={$i18n.t('Bio')} placeholder={$i18n.t('Share your background and interests')}
placeholder={$i18n.t('Share your background and interests')} />
/> </div>
</div>
</div>
<div class="flex flex-col w-full mt-2"> <div class="mt-2 flex flex-col w-full">
<div class=" mb-1 text-xs font-normal">{$i18n.t('Gender')}</div> <div class="mb-1 text-xs text-gray-400 dark:text-gray-600">
{$i18n.t('Gender')}
<div class="flex-1">
<select
class="w-full text-sm dark:text-gray-300 bg-transparent outline-hidden"
bind:value={_gender}
aria-label={$i18n.t('Gender')}
on:change={(e) => {
console.log(_gender);
if (_gender === 'custom') {
// Handle custom gender input
gender = '';
} else {
gender = _gender;
}
}}
>
<option value="" selected>{$i18n.t('Prefer not to say')}</option>
<option value="male">{$i18n.t('Male')}</option>
<option value="female">{$i18n.t('Female')}</option>
<option value="custom">{$i18n.t('Custom')}</option>
</select>
</div>
{#if _gender === 'custom'}
<input
class="w-full text-sm dark:text-gray-300 bg-transparent outline-hidden mt-1"
type="text"
required
aria-label={$i18n.t('Custom Gender')}
placeholder={$i18n.t('Enter your gender')}
bind:value={gender}
/>
{/if}
</div>
<div class="flex flex-col w-full mt-2">
<div class=" mb-1 text-xs font-normal">{$i18n.t('Birth Date')}</div>
<div class="flex-1">
<input
class="w-full text-sm dark:text-gray-300 dark:placeholder:text-gray-300 bg-transparent outline-hidden"
type="date"
aria-label={$i18n.t('Birth Date')}
bind:value={dateOfBirth}
required
/>
</div>
</div>
</div>
</div> </div>
<select
class="w-full bg-transparent py-1 text-[0.8125rem] text-gray-700 outline-hidden dark:text-gray-300"
bind:value={_gender}
aria-label={$i18n.t('Gender')}
on:change={(e) => {
console.log(_gender);
if (_gender === 'custom') {
// Handle custom gender input
gender = '';
} else {
gender = _gender;
}
}}
>
<option value="" selected>{$i18n.t('Prefer not to say')}</option>
<option value="male">{$i18n.t('Male')}</option>
<option value="female">{$i18n.t('Female')}</option>
<option value="custom">{$i18n.t('Custom')}</option>
</select>
{#if _gender === 'custom'}
<input
class="mt-1 w-full bg-transparent py-1 text-[0.8125rem] text-gray-700 outline-hidden placeholder:text-gray-300 dark:text-gray-300 dark:placeholder:text-gray-700"
type="text"
required
aria-label={$i18n.t('Custom Gender')}
placeholder={$i18n.t('Enter your gender')}
bind:value={gender}
/>
{/if}
</div>
<div class="mt-2 flex flex-col w-full">
<div class="mb-1 text-xs text-gray-400 dark:text-gray-600">
{$i18n.t('Birth Date')}
</div>
<input
class="w-full bg-transparent py-1 text-[0.8125rem] text-gray-700 outline-hidden dark:text-gray-300 dark:placeholder:text-gray-700"
type="date"
aria-label={$i18n.t('Birth Date')}
bind:value={dateOfBirth}
required
/>
</div> </div>
</div> </div>
{#if $config?.features?.enable_user_webhooks && ($user?.role === 'admin' || ($user?.permissions?.features?.webhooks ?? false))} {#if $config?.features?.enable_user_webhooks && ($user?.role === 'admin' || ($user?.permissions?.features?.webhooks ?? false))}
<div class="mt-2"> <div class="mt-3">
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<div class=" mb-1 text-xs font-normal">{$i18n.t('Notification Webhook')}</div> <div class="mb-1 text-xs text-gray-400 dark:text-gray-600">
{$i18n.t('Notification Webhook')}
</div>
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full text-sm outline-hidden" class="w-full bg-transparent py-1 text-[0.8125rem] text-gray-700 outline-hidden placeholder:text-gray-300 dark:text-gray-300 dark:placeholder:text-gray-700"
type="url" type="url"
placeholder={$i18n.t('Enter your webhook URL')} placeholder={$i18n.t('Enter your webhook URL')}
aria-label={$i18n.t('Notification Webhook')} aria-label={$i18n.t('Notification Webhook')}
@@ -248,7 +237,7 @@
</div> </div>
{/if} {/if}
<hr class="border-gray-50 dark:border-gray-850/30 my-4" /> <hr class="my-4 border-gray-100/40 dark:border-white/[0.03]" />
{#if $config?.features.enable_login_form && $config?.features.enable_password_change_form} {#if $config?.features.enable_login_form && $config?.features.enable_password_change_form}
<div class="mt-2"> <div class="mt-2">
@@ -257,10 +246,10 @@
{/if} {/if}
{#if ($config?.features?.enable_api_keys ?? true) && ($user?.role === 'admin' || ($user?.permissions?.features?.api_keys ?? false))} {#if ($config?.features?.enable_api_keys ?? true) && ($user?.role === 'admin' || ($user?.permissions?.features?.api_keys ?? false))}
<div class="flex justify-between items-center text-sm mt-2"> <div class="mt-3 flex items-center justify-between text-sm">
<div class=" font-normal">{$i18n.t('API keys')}</div> <div class="text-xs text-gray-400 dark:text-gray-600">{$i18n.t('API keys')}</div>
<button <button
class=" text-xs font-normal text-gray-500" class="text-xs font-normal text-gray-500 transition-colors hover:text-gray-700 dark:hover:text-gray-300"
type="button" type="button"
on:click={() => { on:click={() => {
showAPIKeys = !showAPIKeys; showAPIKeys = !showAPIKeys;
@@ -273,14 +262,16 @@
{#if $user?.role === 'admin'} {#if $user?.role === 'admin'}
<div class="justify-between w-full mt-2"> <div class="justify-between w-full mt-2">
<div class="flex justify-between w-full"> <div class="flex justify-between w-full">
<div class="self-center text-xs font-normal mb-1">{$i18n.t('JWT Token')}</div> <div class="mb-1 self-center text-xs text-gray-400 dark:text-gray-600">
{$i18n.t('JWT Token')}
</div>
</div> </div>
<div class="flex"> <div class="flex">
<SensitiveInput value={localStorage.token} readOnly={true} /> <SensitiveInput value={localStorage.token} readOnly={true} />
<button <button
class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-850 transition rounded-lg" class="ml-1.5 rounded-sm px-1.5 py-1 text-gray-500 transition hover:bg-gray-100 hover:text-gray-700 dark:hover:bg-gray-850 dark:hover:text-gray-300"
aria-label={$i18n.t('Copy Token')} aria-label={$i18n.t('Copy Token')}
on:click={() => { on:click={() => {
copyToClipboard(localStorage.token); copyToClipboard(localStorage.token);
@@ -331,7 +322,9 @@
<div class="justify-between w-full mt-2"> <div class="justify-between w-full mt-2">
{#if $user?.role === 'admin'} {#if $user?.role === 'admin'}
<div class="flex justify-between w-full"> <div class="flex justify-between w-full">
<div class="self-center text-xs font-normal mb-1">{$i18n.t('API Key')}</div> <div class="mb-1 self-center text-xs text-gray-400 dark:text-gray-600">
{$i18n.t('API Key')}
</div>
</div> </div>
{/if} {/if}
<div class="flex"> <div class="flex">
@@ -339,7 +332,7 @@
<SensitiveInput value={APIKey} readOnly={true} /> <SensitiveInput value={APIKey} readOnly={true} />
<button <button
class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-850 transition rounded-lg" class="ml-1.5 rounded-sm px-1.5 py-1 text-gray-500 transition hover:bg-gray-100 hover:text-gray-700 dark:hover:bg-gray-850 dark:hover:text-gray-300"
aria-label={$i18n.t('Copy API Key')} aria-label={$i18n.t('Copy API Key')}
on:click={() => { on:click={() => {
copyToClipboard(APIKey); copyToClipboard(APIKey);
@@ -385,7 +378,7 @@
<Tooltip content={$i18n.t('Create new key')}> <Tooltip content={$i18n.t('Create new key')}>
<button <button
class=" px-1.5 py-1 dark:hover:bg-gray-850transition rounded-lg" class="rounded-sm px-1.5 py-1 text-gray-500 transition hover:bg-gray-100 hover:text-gray-700 dark:hover:bg-gray-850 dark:hover:text-gray-300"
aria-label={$i18n.t('Create new key')} aria-label={$i18n.t('Create new key')}
on:click={() => { on:click={() => {
createAPIKeyHandler(); createAPIKeyHandler();
@@ -409,7 +402,7 @@
</Tooltip> </Tooltip>
{:else} {:else}
<button <button
class="flex gap-1.5 items-center font-normal px-3.5 py-1.5 rounded-lg bg-gray-100/70 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-850 transition" class="flex items-center gap-1.5 text-[0.8125rem] font-normal text-gray-600 transition-colors hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
on:click={() => { on:click={() => {
createAPIKeyHandler(); createAPIKeyHandler();
}} }}
@@ -427,7 +420,7 @@
{/if} {/if}
</div> </div>
<div class="shrink-0 flex justify-end pt-3 text-sm font-normal"> <div class="shrink-0 flex w-full justify-end pt-3 text-sm font-normal">
<button <button
class="px-3.5 py-1.5 text-sm font-normal bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full" class="px-3.5 py-1.5 text-sm font-normal bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
on:click={async () => { on:click={async () => {
@@ -44,9 +44,9 @@
}} }}
> >
<div class="flex justify-between items-center text-sm"> <div class="flex justify-between items-center text-sm">
<div class=" font-normal">{$i18n.t('Change Password')}</div> <div class="text-xs text-gray-400 dark:text-gray-600">{$i18n.t('Change Password')}</div>
<button <button
class=" text-xs font-normal text-gray-500" class="text-xs font-normal text-gray-500 transition-colors hover:text-gray-700 dark:hover:text-gray-300"
type="button" type="button"
on:click={() => { on:click={() => {
show = !show; show = !show;
@@ -13,6 +13,8 @@
export let user = null; export let user = null;
export let imageClassName = 'size-14 md:size-18'; export let imageClassName = 'size-14 md:size-18';
export let variant = 'default';
export let displayName = '';
let profileImageInputElement; let profileImageInputElement;
</script> </script>
@@ -79,10 +81,11 @@
}} }}
/> />
<div class="flex flex-col self-start group"> {#if variant === 'account'}
<div class="self-center flex"> <div class="mb-6 flex items-center gap-4">
<button <button
class="relative rounded-full dark:bg-gray-700" class="group relative flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden rounded-full bg-gray-100
ring-1 ring-gray-200 transition-all duration-200 hover:ring-2 hover:ring-gray-300 dark:bg-white/8 dark:ring-white/10 dark:hover:ring-white/20"
type="button" type="button"
on:click={() => { on:click={() => {
profileImageInputElement.click(); profileImageInputElement.click();
@@ -91,61 +94,154 @@
<img <img
src={profileImageUrl !== '' ? profileImageUrl : generateInitialsImage(user?.name)} src={profileImageUrl !== '' ? profileImageUrl : generateInitialsImage(user?.name)}
alt="profile" alt="profile"
class=" rounded-full {imageClassName} object-cover" class="h-full w-full object-cover"
/> />
<div class="absolute bottom-0 right-0 opacity-0 group-hover:opacity-100 transition"> <div
<div class="p-1 rounded-full bg-white text-black border-gray-100 shadow"> class="absolute inset-0 flex items-center justify-center bg-black/0 transition-colors duration-200 group-hover:bg-black/40"
<svg >
xmlns="http://www.w3.org/2000/svg" <svg
viewBox="0 0 20 20" class="h-4 w-4 text-white opacity-0 drop-shadow-sm transition-opacity duration-200 group-hover:opacity-100"
fill="currentColor" fill="none"
class="size-3" viewBox="0 0 24 24"
> stroke="currentColor"
<path stroke-width="1.5"
d="m2.695 14.762-1.262 3.155a.5.5 0 0 0 .65.65l3.155-1.262a4 4 0 0 0 1.343-.886L17.5 5.501a2.121 2.121 0 0 0-3-3L3.58 13.419a4 4 0 0 0-.885 1.343Z" >
/> <path
</svg> stroke-linecap="round"
</div> stroke-linejoin="round"
d="M6.827 6.175A2.31 2.31 0 0 1 5.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 0 0-1.134-.175 2.31 2.31 0 0 1-1.64-1.055l-.822-1.316a2.192 2.192 0 0 0-1.736-1.039 48.774 48.774 0 0 0-5.232 0 2.192 2.192 0 0 0-1.736 1.039l-.821 1.316Z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.5 12.75a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0Z"
/>
</svg>
</div> </div>
</button> </button>
</div>
<div class="flex flex-col w-full justify-center mt-2">
<button
class=" text-xs text-center text-gray-500 rounded-lg py-0.5 opacity-0 group-hover:opacity-100 transition-all"
type="button"
on:click={async () => {
profileImageUrl = `${WEBUI_BASE_URL}/user.png`;
}}>{$i18n.t('Remove')}</button
>
<button <div class="flex flex-col gap-1">
class=" text-xs text-center text-gray-800 dark:text-gray-400 rounded-lg py-0.5 opacity-0 group-hover:opacity-100 transition-all" <span class="text-sm font-medium text-gray-900 dark:text-white"
type="button" >{displayName || user?.name}</span
on:click={async () => { >
if (canvasPixelTest()) { <div class="flex flex-wrap items-center gap-2">
profileImageUrl = generateInitialsImage(user?.name); <button
} else { class="text-[0.6875rem] text-gray-400 transition-colors duration-100 hover:text-gray-600 dark:hover:text-gray-300"
toast.info( type="button"
$i18n.t( on:click={() => {
'Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.' profileImageInputElement.click();
), }}>{$i18n.t('Upload Photo')}</button
{ >
duration: 1000 * 10 <span class="text-[0.6875rem] text-gray-300 dark:text-gray-700">·</span>
<button
class="text-[0.6875rem] text-gray-400 transition-colors duration-100 hover:text-gray-600 dark:hover:text-gray-300"
type="button"
on:click={async () => {
profileImageUrl = `${WEBUI_BASE_URL}/user.png`;
}}>{$i18n.t('Remove')}</button
>
<span class="text-[0.6875rem] text-gray-300 dark:text-gray-700">·</span>
<button
class="text-[0.6875rem] text-gray-400 transition-colors duration-100 hover:text-gray-600 dark:hover:text-gray-300"
type="button"
on:click={async () => {
if (canvasPixelTest()) {
profileImageUrl = generateInitialsImage(user?.name);
} else {
toast.info(
$i18n.t(
'Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.'
),
{
duration: 1000 * 10
}
);
} }
); }}>{$i18n.t('Initials')}</button
} >
}}>{$i18n.t('Initials')}</button <span class="text-[0.6875rem] text-gray-300 dark:text-gray-700">·</span>
> <button
class="text-[0.6875rem] text-gray-400 transition-colors duration-100 hover:text-gray-600 dark:hover:text-gray-300"
type="button"
on:click={async () => {
const url = await getGravatarUrl(localStorage.token, user?.email);
<button profileImageUrl = url;
class=" text-xs text-center text-gray-800 dark:text-gray-400 rounded-lg py-0.5 opacity-0 group-hover:opacity-100 transition-all" }}>{$i18n.t('Gravatar')}</button
type="button" >
on:click={async () => { </div>
const url = await getGravatarUrl(localStorage.token, user?.email); </div>
profileImageUrl = url;
}}>{$i18n.t('Gravatar')}</button
>
</div> </div>
</div> {:else}
<div class="flex flex-col self-start group">
<div class="self-center flex">
<button
class="relative rounded-full dark:bg-gray-700"
type="button"
on:click={() => {
profileImageInputElement.click();
}}
>
<img
src={profileImageUrl !== '' ? profileImageUrl : generateInitialsImage(user?.name)}
alt="profile"
class=" rounded-full {imageClassName} object-cover"
/>
<div class="absolute bottom-0 right-0 opacity-0 group-hover:opacity-100 transition">
<div class="p-1 rounded-full bg-white text-black border-gray-100 shadow">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="size-3"
>
<path
d="m2.695 14.762-1.262 3.155a.5.5 0 0 0 .65.65l3.155-1.262a4 4 0 0 0 1.343-.886L17.5 5.501a2.121 2.121 0 0 0-3-3L3.58 13.419a4 4 0 0 0-.885 1.343Z"
/>
</svg>
</div>
</div>
</button>
</div>
<div class="flex flex-col w-full justify-center mt-2">
<button
class=" text-xs text-center text-gray-500 rounded-lg py-0.5 opacity-0 group-hover:opacity-100 transition-all"
type="button"
on:click={async () => {
profileImageUrl = `${WEBUI_BASE_URL}/user.png`;
}}>{$i18n.t('Remove')}</button
>
<button
class=" text-xs text-center text-gray-800 dark:text-gray-400 rounded-lg py-0.5 opacity-0 group-hover:opacity-100 transition-all"
type="button"
on:click={async () => {
if (canvasPixelTest()) {
profileImageUrl = generateInitialsImage(user?.name);
} else {
toast.info(
$i18n.t(
'Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.'
),
{
duration: 1000 * 10
}
);
}
}}>{$i18n.t('Initials')}</button
>
<button
class=" text-xs text-center text-gray-800 dark:text-gray-400 rounded-lg py-0.5 opacity-0 group-hover:opacity-100 transition-all"
type="button"
on:click={async () => {
const url = await getGravatarUrl(localStorage.token, user?.email);
profileImageUrl = url;
}}>{$i18n.t('Gravatar')}</button
>
</div>
</div>
{/if}
+7 -4
View File
@@ -592,7 +592,8 @@
<Modal <Modal
size="full" size="full"
className="w-full max-w-3xl lg:max-w-4xl xl:max-w-5xl mx-4 md:mx-0 flex flex-col md:flex-row max-h-[85vh] lg:max-h-[90vh] md:h-[35rem] lg:h-[42rem] xl:h-[46rem] bg-white dark:bg-gray-900 rounded-4xl" containerClassName="p-4 sm:p-6 lg:p-8"
className="w-full max-w-[88rem] h-[min(50rem,calc(100dvh-4rem))] max-h-[calc(100dvh-4rem)] flex flex-col md:flex-row bg-white dark:bg-gray-900 rounded-4xl"
bind:show bind:show
> >
<nav <nav
@@ -806,9 +807,11 @@
</nav> </nav>
<div class="flex-1 min-h-0 p-4 md:px-5 flex flex-col"> <div class="flex-1 min-h-0 p-4 md:px-5 flex flex-col">
<h2 class="text-sm font-medium text-gray-900 dark:text-white mb-4"> {#if selectedTab !== 'account'}
{$i18n.t(selectedTabTitle)} <h2 class="text-sm font-medium text-gray-900 dark:text-white mb-4">
</h2> {$i18n.t(selectedTabTitle)}
</h2>
{/if}
<div class="flex-1 min-h-0 overflow-hidden"> <div class="flex-1 min-h-0 overflow-hidden">
{#if selectedTab === 'general'} {#if selectedTab === 'general'}
+1 -1
View File
@@ -1569,7 +1569,7 @@
> >
<button <button
type="button" type="button"
class=" flex items-center rounded-xl py-2 px-1 w-full hover:bg-gray-100/50 dark:hover:bg-gray-900/50 transition" class=" flex items-center rounded-xl py-1.5 px-1.5 w-full hover:bg-gray-100/50 dark:hover:bg-gray-900/50 transition"
aria-label={$i18n.t('User menu')} aria-label={$i18n.t('User menu')}
> >
<div class=" self-center mr-3 relative flex-shrink-0"> <div class=" self-center mr-3 relative flex-shrink-0">
+78 -21
View File
@@ -118,9 +118,48 @@
let heartbeatInterval = null; let heartbeatInterval = null;
let disconnectToastTimer = null; let disconnectToastTimer = null;
let disconnectWarningShown = false; let disconnectWarningShown = false;
let pageIsVisible = true;
let pageWasHidden = false;
let lastVisibleAt = Date.now();
let disconnectReason = null;
const BREAKPOINT = 768; const BREAKPOINT = 768;
const DISCONNECT_TOAST_DELAY_MS = 2000; const DISCONNECT_TOAST_DELAY_MS = 2000;
const RECENT_RESUME_GRACE_MS = 8000;
const RESUME_DISCONNECT_REASONS = new Set(['ping timeout', 'transport close', 'transport error']);
const clearDisconnectToastTimer = () => {
if (disconnectToastTimer) {
clearTimeout(disconnectToastTimer);
disconnectToastTimer = null;
}
};
const recentlyResumed = () =>
pageWasHidden && Date.now() - lastVisibleAt < RECENT_RESUME_GRACE_MS;
const isLikelyResumeDisconnect = (reason) => {
return (!pageIsVisible || recentlyResumed()) && RESUME_DISCONNECT_REASONS.has(reason);
};
const scheduleDisconnectToast = () => {
clearDisconnectToastTimer();
const resumeDelay = isLikelyResumeDisconnect(disconnectReason)
? Math.max(RECENT_RESUME_GRACE_MS - (Date.now() - lastVisibleAt), 0)
: 0;
disconnectToastTimer = setTimeout(() => {
disconnectToastTimer = null;
if ($socket?.connected || !pageIsVisible || isLikelyResumeDisconnect(disconnectReason)) {
return;
}
disconnectWarningShown = true;
toast.warning($i18n.t('Connection lost. Reconnecting...'));
}, resumeDelay + DISCONNECT_TOAST_DELAY_MS);
};
const setupSocket = async (enableWebsocket) => { const setupSocket = async (enableWebsocket) => {
const _socket = io(`${WEBUI_BASE_URL}` || undefined, { const _socket = io(`${WEBUI_BASE_URL}` || undefined, {
@@ -144,19 +183,17 @@
console.log('connected', _socket.id); console.log('connected', _socket.id);
// Cancel any pending disconnect toast if we reconnected quickly // Cancel any pending disconnect toast if we reconnected quickly
if (disconnectToastTimer) { clearDisconnectToastTimer();
clearTimeout(disconnectToastTimer);
disconnectToastTimer = null;
}
if (hasConnectedOnce) { if (hasConnectedOnce) {
socketConnected.set(true); socketConnected.set(true);
// Only show "Reconnected" if the user actually saw the disconnect warning // Only show "Reconnected" if the user actually saw the disconnect warning
if (disconnectWarningShown) { if (disconnectWarningShown) {
toast.success($i18n.t('Reconnected')); toast.success($i18n.t('Reconnected'));
disconnectWarningShown = false;
} }
} }
disconnectWarningShown = false;
disconnectReason = null;
hasConnectedOnce = true; hasConnectedOnce = true;
const res = await getVersion(localStorage.token); const res = await getVersion(localStorage.token);
@@ -213,18 +250,15 @@
_socket.on('disconnect', (reason, details) => { _socket.on('disconnect', (reason, details) => {
console.log(`Socket ${_socket.id} disconnected due to ${reason}`); console.log(`Socket ${_socket.id} disconnected due to ${reason}`);
socketConnected.set(false); socketConnected.set(false);
disconnectReason = reason;
// Delay showing the disconnect toast so brief interruptions
// (e.g. mobile tab backgrounding) don't flash a nuisance warning
if (disconnectToastTimer) {
clearTimeout(disconnectToastTimer);
}
disconnectWarningShown = false; disconnectWarningShown = false;
disconnectToastTimer = setTimeout(() => {
disconnectToastTimer = null; // Delay visible warnings while mobile browsers resume suspended tabs.
disconnectWarningShown = true; if (isLikelyResumeDisconnect(reason)) {
toast.warning($i18n.t('Connection lost. Reconnecting...')); clearDisconnectToastTimer();
}, DISCONNECT_TOAST_DELAY_MS); } else {
scheduleDisconnectToast();
}
if (heartbeatInterval) { if (heartbeatInterval) {
clearInterval(heartbeatInterval); clearInterval(heartbeatInterval);
@@ -1044,18 +1078,39 @@
}; };
// Set yourself as the last active tab when this tab is focused // Set yourself as the last active tab when this tab is focused
const handlePageHidden = () => {
pageIsVisible = false;
pageWasHidden = true;
clearDisconnectToastTimer();
};
const handlePageVisible = () => {
pageIsVisible = true;
lastVisibleAt = Date.now();
isLastActiveTab.set(true); // This tab is now the active tab
bc.postMessage('active'); // Notify other tabs that this tab is active
// Check token expiry when the tab becomes active
checkTokenExpiry();
if ($socket && !$socket.connected) {
scheduleDisconnectToast();
}
};
const handleVisibilityChange = () => { const handleVisibilityChange = () => {
if (document.visibilityState === 'visible') { if (document.visibilityState === 'visible') {
isLastActiveTab.set(true); // This tab is now the active tab handlePageVisible();
bc.postMessage('active'); // Notify other tabs that this tab is active } else {
handlePageHidden();
// Check token expiry when the tab becomes active
checkTokenExpiry();
} }
}; };
// Add event listener for visibility state changes // Add event listener for visibility state changes
document.addEventListener('visibilitychange', handleVisibilityChange); document.addEventListener('visibilitychange', handleVisibilityChange);
window.addEventListener('pagehide', handlePageHidden);
window.addEventListener('pageshow', handlePageVisible);
// Call visibility change handler initially to set state on load // Call visibility change handler initially to set state on load
handleVisibilityChange(); handleVisibilityChange();
@@ -1228,6 +1283,8 @@
document.removeEventListener('touchmove', touchmoveHandler); document.removeEventListener('touchmove', touchmoveHandler);
document.removeEventListener('touchend', touchendHandler); document.removeEventListener('touchend', touchendHandler);
document.removeEventListener('visibilitychange', handleVisibilityChange); document.removeEventListener('visibilitychange', handleVisibilityChange);
window.removeEventListener('pagehide', handlePageHidden);
window.removeEventListener('pageshow', handlePageVisible);
}; };
}); });