This commit is contained in:
Timothy Jaeryang Baek 2026-07-15 18:52:12 -04:00
parent 6abee5cc3c
commit b3255a3656
10 changed files with 3529 additions and 4394 deletions

File diff suppressed because it is too large Load diff

View file

@ -4,7 +4,6 @@
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Textarea from '$lib/components/common/Textarea.svelte';
import Switch from '$lib/components/common/Switch.svelte';
import AdminSettingField from './AdminSettingField.svelte';
@ -23,8 +22,6 @@
'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 submitHandler = async () => {
const res = await setCodeExecutionConfig(localStorage.token, config);
@ -52,8 +49,11 @@
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5">
{#if config}
<AdminSettingSection first>
<AdminSettingRow label={$i18n.t('Enable Code Execution')}>
<AdminSettingSection title={$i18n.t('Code Execution')} first>
<AdminSettingRow
label={$i18n.t('Enable Code Execution')}
description={$i18n.t('Allow models to run generated code and return execution results.')}
>
<Switch bind:state={config.ENABLE_CODE_EXECUTION} />
</AdminSettingRow>
@ -64,7 +64,7 @@
? $i18n.t(
'Warning: Jupyter execution enables arbitrary code execution, posing severe security risks—proceed with extreme caution.'
)
: ''}
: $i18n.t('Choose the runtime used for generated code blocks.')}
>
<SettingsSelect
bind:value={config.CODE_EXECUTION_ENGINE}
@ -79,7 +79,10 @@
</AdminSettingRow>
{#if config.CODE_EXECUTION_ENGINE === 'jupyter'}
<AdminSettingField label={$i18n.t('Jupyter URL')}>
<AdminSettingField
label={$i18n.t('Jupyter URL')}
description={$i18n.t('Connect code execution to a Jupyter server endpoint.')}
>
<input
class={inputClass}
type="text"
@ -89,7 +92,10 @@
/>
</AdminSettingField>
<AdminSettingRow label={$i18n.t('Jupyter Auth')}>
<AdminSettingRow
label={$i18n.t('Jupyter Auth')}
description={$i18n.t('Select how Open WebUI authenticates with the Jupyter server.')}
>
<SettingsSelect
bind:value={config.CODE_EXECUTION_JUPYTER_AUTH}
placeholder={$i18n.t('Select an auth method')}
@ -105,6 +111,7 @@
label={config.CODE_EXECUTION_JUPYTER_AUTH === 'password'
? $i18n.t('Jupyter Password')
: $i18n.t('Jupyter Token')}
description={$i18n.t('Credentials used to authenticate with the Jupyter server.')}
>
{#if config.CODE_EXECUTION_JUPYTER_AUTH === 'password'}
<SensitiveInput
@ -126,23 +133,27 @@
</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>
<AdminSettingField
label={$i18n.t('Code Execution Timeout')}
description={$i18n.t('Maximum runtime in seconds before execution is stopped.')}
>
<input
class={inputClass}
type="number"
bind:value={config.CODE_EXECUTION_JUPYTER_TIMEOUT}
placeholder={$i18n.t('e.g. 60')}
autocomplete="off"
/>
</AdminSettingField>
{/if}
{/if}
</AdminSettingSection>
<AdminSettingSection title={$i18n.t('Code Interpreter')}>
<AdminSettingRow label={$i18n.t('Enable Code Interpreter')}>
<AdminSettingRow
label={$i18n.t('Enable Code Interpreter')}
description={$i18n.t('Allow models to use the code interpreter tool during chats.')}
>
<Switch bind:state={config.ENABLE_CODE_INTERPRETER} />
</AdminSettingRow>
@ -153,7 +164,7 @@
? $i18n.t(
'Warning: Jupyter execution enables arbitrary code execution, posing severe security risks—proceed with extreme caution.'
)
: ''}
: $i18n.t('Choose the runtime used by the code interpreter tool.')}
>
<SettingsSelect
bind:value={config.CODE_INTERPRETER_ENGINE}
@ -168,7 +179,10 @@
</AdminSettingRow>
{#if config.CODE_INTERPRETER_ENGINE === 'jupyter'}
<AdminSettingField label={$i18n.t('Jupyter URL')}>
<AdminSettingField
label={$i18n.t('Jupyter URL')}
description={$i18n.t('Connect code interpreter to a Jupyter server endpoint.')}
>
<input
class={inputClass}
type="text"
@ -178,7 +192,10 @@
/>
</AdminSettingField>
<AdminSettingRow label={$i18n.t('Jupyter Auth')}>
<AdminSettingRow
label={$i18n.t('Jupyter Auth')}
description={$i18n.t('Select how Open WebUI authenticates with the Jupyter server.')}
>
<SettingsSelect
bind:value={config.CODE_INTERPRETER_JUPYTER_AUTH}
placeholder={$i18n.t('Select an auth method')}
@ -194,6 +211,7 @@
label={config.CODE_INTERPRETER_JUPYTER_AUTH === 'password'
? $i18n.t('Jupyter Password')
: $i18n.t('Jupyter Token')}
description={$i18n.t('Credentials used to authenticate with the Jupyter server.')}
>
{#if config.CODE_INTERPRETER_JUPYTER_AUTH === 'password'}
<SensitiveInput
@ -215,32 +233,33 @@
</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>
<AdminSettingField
label={$i18n.t('Code Execution Timeout')}
description={$i18n.t('Maximum runtime in seconds before execution is stopped.')}
>
<input
class={inputClass}
type="number"
bind:value={config.CODE_INTERPRETER_JUPYTER_TIMEOUT}
placeholder={$i18n.t('e.g. 60')}
autocomplete="off"
/>
</AdminSettingField>
{/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
label={$i18n.t('Code Interpreter Prompt Template')}
description={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt.'
)}
>
<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'
)}
/>
</AdminSettingField>
{/if}
</AdminSettingSection>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,17 +1,18 @@
<script lang="ts">
import { getModels, getTaskConfig, updateTaskConfig } from '$lib/apis';
import { getChatConfig, updateChatConfig } from '$lib/apis/chats';
import { config, settings } from '$lib/stores';
import { createEventDispatcher, onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
import { getBaseModels } from '$lib/apis/models';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Switch from '$lib/components/common/Switch.svelte';
import Textarea from '$lib/components/common/Textarea.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
import SettingsSelect from '$lib/components/common/SettingsSelect.svelte';
import AdminSettingField from './AdminSettingField.svelte';
import AdminSettingRow from './AdminSettingRow.svelte';
import AdminSettingSection from './AdminSettingSection.svelte';
const dispatch = createEventDispatcher();
@ -52,10 +53,13 @@
]);
};
let workspaceModels = null;
let baseModels = null;
let workspaceModels: any[] = [];
let baseModels: any[] = [];
let models = null;
let models: any[] | null = null;
$: modelOptions = models ?? [];
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';
@ -69,8 +73,8 @@
workspaceModels = await getBaseModels(localStorage.token);
baseModels = await getModels(localStorage.token, null, false);
models = baseModels.map((m) => {
const workspaceModel = workspaceModels.find((wm) => wm.id === m.id);
models = baseModels.map((m: any) => {
const workspaceModel = workspaceModels.find((wm: any) => wm.id === m.id);
if (workspaceModel) {
return {
@ -90,8 +94,9 @@
console.debug('models', models);
} catch (err) {
const error = err as { detail?: string; message?: string };
console.error('Failed to initialize Interface settings:', err);
toast.error(err?.detail ?? err?.message ?? $i18n.t('Failed to load Interface settings'));
toast.error(error?.detail ?? error?.message ?? $i18n.t('Failed to load Interface settings'));
models = [];
}
};
@ -113,191 +118,163 @@
{$i18n.t('Interface')}
</h2>
<div class=" flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5">
<div class="mb-5">
<div class=" mb-2 text-xs text-gray-400 dark:text-gray-600">
{$i18n.t('Tasks')}
</div>
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5">
<AdminSettingSection title={$i18n.t('Tasks')} first>
<div>
<div class="mb-2">
<div class="text-xs text-gray-600 dark:text-gray-400">{$i18n.t('Task Model')}</div>
<div class="mt-1.5 text-[0.6875rem] text-gray-400 dark:text-gray-600">
{$i18n.t(
'Choose fallback models for background tasks. Current Model follows the active chat model.'
)}
</div>
</div>
<div class=" mb-2 font-normal flex items-center">
<div class=" text-xs mr-1">{$i18n.t('Task Model')}</div>
<Tooltip
content={$i18n.t(
'A task model is used when performing tasks such as generating titles for chats and web search queries'
)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-3.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
/>
</svg>
</Tooltip>
</div>
<div class=" mb-2.5 flex w-full gap-2">
<div class="flex-1">
<div class=" text-xs mb-1">{$i18n.t('Local Task Model')}</div>
<SettingsSelect
bind:value={taskConfig.TASK_MODEL}
className="w-full"
placeholder={$i18n.t('Select a model')}
on:change={() => {
if (taskConfig.TASK_MODEL) {
const model = models.find((m) => m.id === taskConfig.TASK_MODEL);
if (model) {
if (
model?.access_grants &&
!model.access_grants.some(
(g) =>
g.principal_type === 'user' &&
g.principal_id === '*' &&
g.permission === 'read'
)
) {
toast.error(
$i18n.t(
'This model is not publicly available. Please select another model.'
<div class="grid w-full grid-cols-1 gap-2.5 sm:grid-cols-2">
<AdminSettingField label={$i18n.t('Local Task Model')}>
<SettingsSelect
bind:value={taskConfig.TASK_MODEL}
className="w-full"
placeholder={$i18n.t('Select a model')}
on:change={() => {
if (taskConfig.TASK_MODEL) {
const model = modelOptions.find((m: any) => m.id === taskConfig.TASK_MODEL);
if (model) {
if (
model?.access_grants &&
!model.access_grants.some(
(g: any) =>
g.principal_type === 'user' &&
g.principal_id === '*' &&
g.permission === 'read'
)
);
) {
toast.error(
$i18n.t(
'This model is not publicly available. Please select another model.'
)
);
}
taskConfig.TASK_MODEL = model.id;
} else {
taskConfig.TASK_MODEL = '';
}
taskConfig.TASK_MODEL = model.id;
} else {
taskConfig.TASK_MODEL = '';
}
}
}}
>
<option value="" selected>{$i18n.t('Current Model')}</option>
{#each models as model}
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">
{model.name}
{model?.connection_type === 'local' ? `(${$i18n.t('Local')})` : ''}
</option>
{/each}
</SettingsSelect>
</div>
}}
>
<option value="" selected>{$i18n.t('Current Model')}</option>
{#each modelOptions as model}
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">
{model.name}
{model?.connection_type === 'local' ? `(${$i18n.t('Local')})` : ''}
</option>
{/each}
</SettingsSelect>
</AdminSettingField>
<div class="flex-1">
<div class=" text-xs mb-1">{$i18n.t('External Task Model')}</div>
<SettingsSelect
bind:value={taskConfig.TASK_MODEL_EXTERNAL}
className="w-full"
placeholder={$i18n.t('Select a model')}
on:change={() => {
if (taskConfig.TASK_MODEL_EXTERNAL) {
const model = models.find((m) => m.id === taskConfig.TASK_MODEL_EXTERNAL);
if (model) {
if (
model?.access_grants &&
!model.access_grants.some(
(g) =>
g.principal_type === 'user' &&
g.principal_id === '*' &&
g.permission === 'read'
)
) {
toast.error(
$i18n.t(
'This model is not publicly available. Please select another model.'
<AdminSettingField label={$i18n.t('External Task Model')}>
<SettingsSelect
bind:value={taskConfig.TASK_MODEL_EXTERNAL}
className="w-full"
placeholder={$i18n.t('Select a model')}
on:change={() => {
if (taskConfig.TASK_MODEL_EXTERNAL) {
const model = modelOptions.find(
(m: any) => m.id === taskConfig.TASK_MODEL_EXTERNAL
);
if (model) {
if (
model?.access_grants &&
!model.access_grants.some(
(g: any) =>
g.principal_type === 'user' &&
g.principal_id === '*' &&
g.permission === 'read'
)
);
}
) {
toast.error(
$i18n.t(
'This model is not publicly available. Please select another model.'
)
);
}
taskConfig.TASK_MODEL_EXTERNAL = model.id;
} else {
taskConfig.TASK_MODEL_EXTERNAL = '';
taskConfig.TASK_MODEL_EXTERNAL = model.id;
} else {
taskConfig.TASK_MODEL_EXTERNAL = '';
}
}
}
}}
>
<option value="" selected>{$i18n.t('Current Model')}</option>
{#each models as model}
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">
{model.name}
{model?.connection_type === 'local' ? `(${$i18n.t('Local')})` : ''}
</option>
{/each}
</SettingsSelect>
}}
>
<option value="" selected>{$i18n.t('Current Model')}</option>
{#each modelOptions as model}
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">
{model.name}
{model?.connection_type === 'local' ? `(${$i18n.t('Local')})` : ''}
</option>
{/each}
</SettingsSelect>
</AdminSettingField>
</div>
</div>
</AdminSettingSection>
<div class="mb-2.5 flex w-full items-center justify-between">
<div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Context Compaction')}
</div>
<AdminSettingSection title={$i18n.t('Chat')}>
<AdminSettingRow
label={$i18n.t('Context Compaction')}
description={$i18n.t(
'Summarize older chat history when the conversation context grows large.'
)}
>
<Switch bind:state={chatConfig.ENABLE_CONTEXT_COMPACTION} />
</div>
</AdminSettingRow>
{#if chatConfig.ENABLE_CONTEXT_COMPACTION}
<div class="mb-2.5">
<div class=" mb-1 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Token Threshold')}
</div>
<AdminSettingField
label={$i18n.t('Token Threshold')}
description={$i18n.t(
'Older messages are summarized when estimated context exceeds this token limit.'
)}
>
<input
type="number"
min="1"
step="1"
class={inputClass}
bind:value={chatConfig.CONTEXT_COMPACTION_TOKEN_THRESHOLD}
/>
</AdminSettingField>
<Tooltip
content={$i18n.t(
'Older messages are summarized when estimated context exceeds this token limit.'
<AdminSettingField
label={$i18n.t('Token Cap')}
description={$i18n.t(
'Model-specific context compaction thresholds cannot exceed this token limit.'
)}
>
<input
type="number"
min="1"
step="1"
class={inputClass}
bind:value={chatConfig.CONTEXT_COMPACTION_TOKEN_CAP}
/>
</AdminSettingField>
<AdminSettingField
label={$i18n.t('Context Compaction Prompt')}
description={$i18n.t(
'Controls how older messages are rewritten into a running summary.'
)}
>
<Textarea
className={textareaClass}
bind:value={chatConfig.CONTEXT_COMPACTION_PROMPT_TEMPLATE}
placeholder={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt'
)}
placement="top-start"
>
<input
type="number"
min="1"
step="1"
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}
/>
</Tooltip>
</div>
<div class="mb-2.5">
<div class=" mb-1 text-xs text-gray-600 dark:text-gray-400">{$i18n.t('Token Cap')}</div>
<Tooltip
content={$i18n.t(
'Model-specific context compaction thresholds cannot exceed this token limit.'
)}
placement="top-start"
>
<input
type="number"
min="1"
step="1"
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}
/>
</Tooltip>
</div>
<div class="mb-2.5">
<div class=" mb-1 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Context Compaction Prompt')}
</div>
<Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
placement="top-start"
>
<Textarea
className={textareaClass}
bind:value={chatConfig.CONTEXT_COMPACTION_PROMPT_TEMPLATE}
placeholder={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt'
)}
/>
</Tooltip>
<div class="mt-1 text-xs text-gray-500 dark:text-gray-400">
/>
<div class="mt-1 text-[0.6875rem] text-gray-400 dark:text-gray-600">
{$i18n.t('Available variables')}:
<code>{'{{PREVIOUS_SUMMARY}}'}</code>,
<code>{'{{COMPACTED_MESSAGES}}'}</code>,
@ -305,245 +282,182 @@
<code>{'{{MESSAGES}}'}</code>,
<code>{'{{CURRENT_DATE}}'}</code>
</div>
</div>
</AdminSettingField>
{/if}
</AdminSettingSection>
<div class="mb-2.5 flex w-full items-center justify-between">
<div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Title Generation')}
</div>
<AdminSettingSection title={$i18n.t('Generation')}>
<AdminSettingRow
label={$i18n.t('Title Generation')}
description={$i18n.t('Allow automatic names for new chats.')}
>
<Switch bind:state={taskConfig.ENABLE_TITLE_GENERATION} />
</div>
</AdminSettingRow>
{#if taskConfig.ENABLE_TITLE_GENERATION}
<div class="mb-2.5">
<div class=" mb-1 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Title Generation Prompt')}
</div>
<Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
placement="top-start"
>
<Textarea
className={textareaClass}
bind:value={taskConfig.TITLE_GENERATION_PROMPT_TEMPLATE}
placeholder={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt'
)}
/>
</Tooltip>
</div>
<AdminSettingField
label={$i18n.t('Title Generation Prompt')}
description={$i18n.t('Shapes the short label generated for each chat.')}
>
<Textarea
className={textareaClass}
bind:value={taskConfig.TITLE_GENERATION_PROMPT_TEMPLATE}
placeholder={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt'
)}
/>
</AdminSettingField>
{/if}
<div class="mb-2.5 flex w-full items-center justify-between">
<div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Voice Mode Prompt')}
</div>
<AdminSettingRow
label={$i18n.t('Voice Mode Prompt')}
description={$i18n.t('Apply voice-specific instructions while voice mode is active.')}
>
<Switch bind:state={taskConfig.ENABLE_VOICE_MODE_PROMPT} />
</div>
</AdminSettingRow>
{#if taskConfig.ENABLE_VOICE_MODE_PROMPT}
<div class="mb-2.5">
<div class=" mb-1 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Prompt Template')}
</div>
<Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
placement="top-start"
>
<Textarea
className={textareaClass}
bind:value={taskConfig.VOICE_MODE_PROMPT_TEMPLATE}
placeholder={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt'
)}
/>
</Tooltip>
</div>
<AdminSettingField
label={$i18n.t('Prompt Template')}
description={$i18n.t('Defines the behavior used for spoken conversations.')}
>
<Textarea
className={textareaClass}
bind:value={taskConfig.VOICE_MODE_PROMPT_TEMPLATE}
placeholder={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt'
)}
/>
</AdminSettingField>
{/if}
<div class="mb-2.5 flex w-full items-center justify-between">
<div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Follow Up Generation')}
</div>
<AdminSettingRow
label={$i18n.t('Follow Up Generation')}
description={$i18n.t('Show suggested next questions after assistant responses.')}
>
<Switch bind:state={taskConfig.ENABLE_FOLLOW_UP_GENERATION} />
</div>
</AdminSettingRow>
{#if taskConfig.ENABLE_FOLLOW_UP_GENERATION}
<div class="mb-2.5">
<div class=" mb-1 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Follow Up Generation Prompt')}
</div>
<Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
placement="top-start"
>
<Textarea
className={textareaClass}
bind:value={taskConfig.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE}
placeholder={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt'
)}
/>
</Tooltip>
</div>
<AdminSettingField
label={$i18n.t('Follow Up Generation Prompt')}
description={$i18n.t('Guides the suggestions shown after an assistant response.')}
>
<Textarea
className={textareaClass}
bind:value={taskConfig.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE}
placeholder={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt'
)}
/>
</AdminSettingField>
{/if}
<div class="mb-2.5 flex w-full items-center justify-between">
<div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Tags Generation')}
</div>
<AdminSettingRow
label={$i18n.t('Tags Generation')}
description={$i18n.t('Create chat tags from conversation content.')}
>
<Switch bind:state={taskConfig.ENABLE_TAGS_GENERATION} />
</div>
</AdminSettingRow>
{#if taskConfig.ENABLE_TAGS_GENERATION}
<div class="mb-2.5">
<div class=" mb-1 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Tags Generation Prompt')}
</div>
<Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
placement="top-start"
>
<Textarea
className={textareaClass}
bind:value={taskConfig.TAGS_GENERATION_PROMPT_TEMPLATE}
placeholder={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt'
)}
/>
</Tooltip>
</div>
{/if}
<div class="mb-2.5 flex w-full items-center justify-between">
<div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Retrieval Query Generation')}
</div>
<Switch bind:state={taskConfig.ENABLE_RETRIEVAL_QUERY_GENERATION} />
</div>
<div class="mb-2.5 flex w-full items-center justify-between">
<div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Web Search Query Generation')}
</div>
<Switch bind:state={taskConfig.ENABLE_SEARCH_QUERY_GENERATION} />
</div>
<div class="mb-2.5">
<div class=" mb-1 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Query Generation Prompt')}
</div>
<Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
placement="top-start"
<AdminSettingField
label={$i18n.t('Tags Generation Prompt')}
description={$i18n.t('Controls how chat tags are inferred.')}
>
<Textarea
className={textareaClass}
bind:value={taskConfig.QUERY_GENERATION_PROMPT_TEMPLATE}
bind:value={taskConfig.TAGS_GENERATION_PROMPT_TEMPLATE}
placeholder={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt'
)}
/>
</Tooltip>
</div>
</AdminSettingField>
{/if}
<div class="mb-2.5 flex w-full items-center justify-between">
<div class=" self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Autocomplete Generation')}
</div>
<AdminSettingRow
label={$i18n.t('Retrieval Query Generation')}
description={$i18n.t('Rewrite user requests for knowledge retrieval.')}
>
<Switch bind:state={taskConfig.ENABLE_RETRIEVAL_QUERY_GENERATION} />
</AdminSettingRow>
<Tooltip content={$i18n.t('Enable autocomplete generation for chat messages')}>
<Switch bind:state={taskConfig.ENABLE_AUTOCOMPLETE_GENERATION} />
</Tooltip>
</div>
<AdminSettingRow
label={$i18n.t('Web Search Query Generation')}
description={$i18n.t('Rewrite user requests into web-search queries.')}
>
<Switch bind:state={taskConfig.ENABLE_SEARCH_QUERY_GENERATION} />
</AdminSettingRow>
<AdminSettingField
label={$i18n.t('Query Generation Prompt')}
description={$i18n.t('Shared prompt for retrieval and web-search query rewriting.')}
>
<Textarea
className={textareaClass}
bind:value={taskConfig.QUERY_GENERATION_PROMPT_TEMPLATE}
placeholder={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
/>
</AdminSettingField>
<AdminSettingRow
label={$i18n.t('Autocomplete Generation')}
description={$i18n.t('Suggest completions while users type chat messages.')}
>
<Switch bind:state={taskConfig.ENABLE_AUTOCOMPLETE_GENERATION} />
</AdminSettingRow>
{#if taskConfig.ENABLE_AUTOCOMPLETE_GENERATION}
<div class="mb-2.5">
<div class=" mb-1 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Autocomplete Generation Input Max Length')}
</div>
<AdminSettingField
label={$i18n.t('Autocomplete Generation Input Max Length')}
description={$i18n.t('Limit how much draft text is sent for suggestion generation.')}
>
<input
type="number"
min="-1"
step="1"
class={inputClass}
bind:value={taskConfig.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH}
placeholder={$i18n.t('-1 for no limit, or a positive integer for a specific limit')}
/>
</AdminSettingField>
<Tooltip
content={$i18n.t('Character limit for autocomplete generation input')}
placement="top-start"
>
<input
class="w-full outline-hidden bg-transparent"
bind:value={taskConfig.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH}
placeholder={$i18n.t('-1 for no limit, or a positive integer for a specific limit')}
/>
</Tooltip>
</div>
<div class="mb-2.5">
<div class=" mb-1 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Autocomplete Generation Prompt')}
</div>
<Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
placement="top-start"
>
<Textarea
className={textareaClass}
bind:value={taskConfig.AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE}
placeholder={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt'
)}
/>
</Tooltip>
</div>
<AdminSettingField
label={$i18n.t('Autocomplete Generation Prompt')}
description={$i18n.t('Guides inline completions while users type a message.')}
>
<Textarea
className={textareaClass}
bind:value={taskConfig.AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE}
placeholder={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt'
)}
/>
</AdminSettingField>
{/if}
<div class="mb-2.5">
<div class=" mb-1 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Image Prompt Generation Prompt')}
</div>
<AdminSettingField
label={$i18n.t('Image Prompt Generation Prompt')}
description={$i18n.t('Rewrites user intent into an image-generation prompt.')}
>
<Textarea
className={textareaClass}
bind:value={taskConfig.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE}
placeholder={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
/>
</AdminSettingField>
<Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
placement="top-start"
>
<Textarea
className={textareaClass}
bind:value={taskConfig.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE}
placeholder={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt'
)}
/>
</Tooltip>
</div>
<div class="mb-2.5">
<div class=" mb-1 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Tools Function Calling Prompt')}
</div>
<Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
placement="top-start"
>
<Textarea
className={textareaClass}
bind:value={taskConfig.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE}
placeholder={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt'
)}
/>
</Tooltip>
</div>
</div>
<AdminSettingField
label={$i18n.t('Tools Function Calling Prompt')}
description={$i18n.t('Guides how the assistant formats tool and function calls.')}
>
<Textarea
className={textareaClass}
bind:value={taskConfig.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE}
placeholder={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
/>
</AdminSettingField>
</AdminSettingSection>
</div>
<div class="flex justify-end pt-6 text-sm font-normal">
@ -556,7 +470,7 @@
</div>
</form>
{:else}
<div class=" h-full w-full flex justify-center items-center">
<div class="flex h-full w-full items-center justify-center">
<Spinner className="size-5" />
</div>
{/if}

View file

@ -20,6 +20,7 @@
import Switch from '$lib/components/common/Switch.svelte';
import AdminSettingField from './AdminSettingField.svelte';
import AdminSettingRow from './AdminSettingRow.svelte';
import AdminSettingSection from './AdminSettingSection.svelte';
import SettingsSelect from '$lib/components/common/SettingsSelect.svelte';
const i18n: Writable<i18nType> = getContext('i18n');
@ -38,24 +39,27 @@
let valves: any = null;
let valves_spec: any = null;
let selectedPipelineIdx: number | null = null;
let selectedPipelineIdx = 0;
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 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';
'shrink-0 text-xs text-gray-500 transition-colors hover:text-gray-900 disabled:opacity-50 dark:text-gray-500 dark:hover:text-white';
const mutedMessageClass = 'text-xs text-gray-400 dark:text-gray-600';
const updateHandler = async () => {
if (!pipelines) {
toast.error($i18n.t('No valves to update'));
return;
}
const pipeline = pipelines[selectedPipelineIdx];
if (pipeline && (pipeline?.valves ?? false)) {
for (const property in valves_spec.properties) {
if (valves_spec.properties[property]?.type === 'array') {
valves[property] = (valves[property] ?? '').split(',').map((v) => v.trim());
valves[property] = (valves[property] ?? '').split(',').map((v: string) => v.trim());
}
}
@ -86,7 +90,11 @@
}
};
const getValves = async (idx) => {
const getValves = async (idx: number) => {
if (!pipelines?.[idx]) {
return;
}
valves = null;
valves_spec = null;
@ -113,11 +121,12 @@
valves = null;
valves_spec = null;
if (PIPELINES_LIST.length > 0) {
if ((PIPELINES_LIST ?? []).length > 0) {
console.debug(selectedPipelinesUrlIdx);
pipelines = await getPipelines(localStorage.token, selectedPipelinesUrlIdx);
const loadedPipelines = await getPipelines(localStorage.token, selectedPipelinesUrlIdx);
pipelines = loadedPipelines;
if (pipelines.length > 0) {
if (loadedPipelines.length > 0) {
selectedPipelineIdx = 0;
await getValves(selectedPipelineIdx);
}
@ -186,19 +195,26 @@
}
pipelineFiles = null;
const pipelineUploadInputElement = document.getElementById('pipelines-upload-input');
const pipelineUploadInputElement = document.getElementById(
'pipelines-upload-input'
) as HTMLInputElement | null;
if (pipelineUploadInputElement) {
pipelineUploadInputElement.value = null;
pipelineUploadInputElement.value = '';
}
uploading = false;
};
const deletePipelineHandler = async () => {
const pipeline = pipelines?.[selectedPipelineIdx];
if (!pipeline) {
return;
}
const res = await deletePipeline(
localStorage.token,
pipelines[selectedPipelineIdx].id,
pipeline.id,
selectedPipelinesUrlIdx
).catch((error) => {
toast.error(`${error}`);
@ -223,8 +239,9 @@
PIPELINES_LIST = await getPipelinesList(localStorage.token);
console.log(PIPELINES_LIST);
if (PIPELINES_LIST.length > 0) {
selectedPipelinesUrlIdx = PIPELINES_LIST[0]['idx'].toString();
const pipelinesList = PIPELINES_LIST ?? [];
if (pipelinesList.length > 0) {
selectedPipelinesUrlIdx = pipelinesList[0]['idx'].toString();
}
await setPipelines();
@ -250,8 +267,11 @@
hidden
/>
<div class="flex flex-col gap-2.5">
<AdminSettingField label={$i18n.t('Pipeline URL')}>
<AdminSettingSection title={$i18n.t('Source')} first>
<AdminSettingField
label={$i18n.t('Pipeline URL')}
description={$i18n.t('Select the Pipelines server to manage.')}
>
<SettingsSelect
bind:value={selectedPipelinesUrlIdx}
className="w-full"
@ -273,7 +293,10 @@
</SettingsSelect>
</AdminSettingField>
<AdminSettingField label={$i18n.t('Upload Pipeline')}>
<AdminSettingField
label={$i18n.t('Upload Pipeline')}
description={$i18n.t('Upload a local Python pipeline file to the selected server.')}
>
<div class="flex gap-2">
<button
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"
@ -324,10 +347,15 @@
</button>
</div>
</AdminSettingField>
</AdminSettingSection>
{#if pipelines !== null}
{#if pipelines.length > 0}
<AdminSettingField label={$i18n.t('Pipeline')}>
{#if pipelines !== null}
{#if pipelines.length > 0}
<AdminSettingSection title={$i18n.t('Pipelines')}>
<AdminSettingField
label={$i18n.t('Pipeline')}
description={$i18n.t('Select an installed pipeline to configure or remove.')}
>
<div class="flex gap-2">
<SettingsSelect
bind:value={selectedPipelineIdx}
@ -356,12 +384,17 @@
</button>
</div>
</AdminSettingField>
</AdminSettingSection>
<AdminSettingSection title={$i18n.t('Valves')}>
{#if pipelines[selectedPipelineIdx ?? 0]?.valves}
{#if valves}
{#if valves && valves_spec}
{#each Object.keys(valves_spec.properties) as property}
<div>
<AdminSettingRow label={valves_spec.properties[property].title}>
<AdminSettingRow
label={valves_spec.properties[property].title}
description={valves_spec.properties[property]?.description ?? ''}
>
<button
class={actionButtonClass}
type="button"
@ -407,22 +440,28 @@
</div>
{/each}
{:else}
<Spinner className="size-5" />
<div class="flex justify-center py-2">
<Spinner className="size-4" />
</div>
{/if}
{:else}
<div class={mutedMessageClass}>{$i18n.t('No valves')}</div>
{/if}
{:else}
<div class={mutedMessageClass}>{$i18n.t('Pipelines Not Detected')}</div>
{/if}
</AdminSettingSection>
{:else}
<div class="flex justify-center py-4">
<Spinner className="size-4" />
</div>
<AdminSettingSection title={$i18n.t('Pipelines')}>
<div class={mutedMessageClass}>{$i18n.t('Pipelines Not Detected')}</div>
</AdminSettingSection>
{/if}
</div>
{:else}
<div class="flex justify-center py-4">
<Spinner className="size-4" />
</div>
{/if}
{:else}
<div class={mutedMessageClass}>{$i18n.t('Pipelines Not Detected')}</div>
<AdminSettingSection title={$i18n.t('Source')} first>
<div class={mutedMessageClass}>{$i18n.t('Pipelines Not Detected')}</div>
</AdminSettingSection>
{/if}
{:else}
<div class="flex justify-center h-full">

File diff suppressed because it is too large Load diff

View file

@ -154,7 +154,7 @@
<div class="text-xs text-gray-400 dark:text-gray-500">
{$i18n.t('Created by')}
<a class="text-gray-500 dark:text-gray-400" href="https://github.com/tjbck" target="_blank"
>Timothy J. Baek</a
>Tim J. Baek</a
>
</div>
</UserSettingSection>

View file

@ -41,8 +41,6 @@
let playbackRate = 1;
const inputClass =
'h-7 w-full 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 actionButtonClass =
'text-xs text-gray-500 transition-colors hover:text-gray-900 dark:text-gray-500 dark:hover:text-white';
const getVoices = async () => {
if (TTSEngine === 'browser-kokoro') {
@ -80,13 +78,13 @@
}
};
const toggleResponseAutoPlayback = async () => {
responseAutoPlayback = !responseAutoPlayback;
const setResponseAutoPlayback = async (enabled: boolean) => {
responseAutoPlayback = enabled;
saveSettings({ responseAutoPlayback: responseAutoPlayback });
};
const toggleSpeechAutoSend = async () => {
speechAutoSend = !speechAutoSend;
const setSpeechAutoSend = async (enabled: boolean) => {
speechAutoSend = enabled;
saveSettings({ speechAutoSend: speechAutoSend });
};
@ -231,17 +229,13 @@
'Send transcribed voice input immediately after speech recognition finishes.'
)}
>
<button
class={actionButtonClass}
on:click={() => {
toggleSpeechAutoSend();
<Switch
state={speechAutoSend}
ariaLabel={$i18n.t('Instant Auto-Send After Voice Transcription')}
on:change={(event) => {
setSpeechAutoSend(event.detail);
}}
type="button"
role="switch"
aria-checked={speechAutoSend}
>
{speechAutoSend === true ? $i18n.t('On') : $i18n.t('Off')}
</button>
/>
</UserSettingRow>
</UserSettingSection>
@ -283,17 +277,13 @@
label={$i18n.t('Auto-Playback Response')}
description={$i18n.t('Play assistant responses aloud automatically.')}
>
<button
class={actionButtonClass}
on:click={() => {
toggleResponseAutoPlayback();
<Switch
state={responseAutoPlayback}
ariaLabel={$i18n.t('Auto-Playback Response')}
on:change={(event) => {
setResponseAutoPlayback(event.detail);
}}
type="button"
role="switch"
aria-checked={responseAutoPlayback}
>
{responseAutoPlayback === true ? $i18n.t('On') : $i18n.t('Off')}
</button>
/>
</UserSettingRow>
<UserSettingRow

View file

@ -14,6 +14,7 @@
import UserSettingRow from './UserSettingRow.svelte';
import UserSettingSection from './UserSettingSection.svelte';
import SettingsSelect from '$lib/components/common/SettingsSelect.svelte';
import Switch from '$lib/components/common/Switch.svelte';
export let saveSettings: Function;
export let getModels: Function;
@ -33,13 +34,14 @@
const actionButtonClass =
'text-xs text-gray-500 transition-colors hover:text-gray-900 dark:text-gray-500 dark:hover:text-white';
const toggleNotification = async () => {
const permission = await Notification.requestPermission();
const setNotificationEnabled = async (enabled: boolean) => {
const permission = enabled ? await Notification.requestPermission() : 'granted';
if (permission === 'granted') {
notificationEnabled = !notificationEnabled;
notificationEnabled = enabled;
saveSettings({ notificationEnabled: notificationEnabled });
} else {
notificationEnabled = false;
toast.error(
$i18n.t(
'Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.'
@ -273,17 +275,13 @@
label={$i18n.t('Notifications')}
description={$i18n.t('Allow browser notifications for completed responses.')}
>
<button
class={actionButtonClass}
on:click={() => {
toggleNotification();
<Switch
state={notificationEnabled}
ariaLabel={$i18n.t('Notifications')}
on:change={(event) => {
setNotificationEnabled(event.detail);
}}
type="button"
role="switch"
aria-checked={notificationEnabled}
>
{notificationEnabled === true ? $i18n.t('On') : $i18n.t('Off')}
</button>
/>
</UserSettingRow>
</UserSettingSection>