This commit is contained in:
Timothy Jaeryang Baek 2026-10-21 11:17:43 +09:00
parent 0de8d03c62
commit b93b812468
16 changed files with 409 additions and 262 deletions

View file

@ -78,7 +78,9 @@
</div>
{:else}<div class="flex flex-col gap-1.5">
{#each apps as app (app.id)}
<div class="flex w-full items-center gap-3 text-xs text-gray-600 dark:text-gray-400">
<div
class="group/setting-row flex w-full items-center gap-3 text-xs text-gray-600 dark:text-gray-400"
>
<div
class="flex min-w-0 flex-1 items-center gap-1.5 {app.is_active
? ''
@ -91,9 +93,11 @@
className="size-4 shrink-0 rounded!"
/>
{:else}<Grid className="size-4 shrink-0" strokeWidth="1.5" />{/if}
<span class="truncate"
<label
for={`app-${app.id}-control`}
class="truncate text-gray-600 dark:text-gray-400 transition-colors duration-150 group-hover/setting-row:text-gray-900 group-focus-within/setting-row:text-gray-900 dark:group-hover/setting-row:text-white dark:group-focus-within/setting-row:text-white group-has-[button[id$=-control]:enabled]/setting-row:cursor-pointer group-has-[button[id$=-control]:disabled]/setting-row:cursor-not-allowed"
>{app.name}
<span class="text-gray-400 dark:text-gray-600">{app.slug}</span></span
<span class="text-gray-400 dark:text-gray-600">{app.slug}</span></label
>
</div>
{#if app.is_active}
@ -122,6 +126,7 @@
: $i18n.t('Uninstalled')}
>
{#key `${app.is_active}-${show}-${busy}`}<Switch
id={`app-${app.id}-control`}
state={app.is_active}
disabled={busy === app.id}
ariaLabel={`${$i18n.t('Enable')} ${app.name}`}

View file

@ -45,7 +45,7 @@
<ManageOllamaModal bind:show={showManageModal} urlIdx={idx} />
<div class="flex gap-1.5">
<div class="group/setting-row flex gap-1.5">
<Tooltip
className="w-full relative"
content={$i18n.t(`WebUI will make requests to "{{url}}/api/chat"`, {
@ -60,7 +60,7 @@
{/if}
<input
class="w-full text-sm bg-transparent outline-hidden"
class="text-gray-600 dark:text-gray-400 transition-colors duration-150 group-hover/setting-row:text-gray-900 group-focus-within/setting-row:text-gray-900 dark:group-hover/setting-row:text-white dark:group-focus-within/setting-row:text-white w-full text-sm bg-transparent outline-hidden"
placeholder={$i18n.t('Enter URL (e.g. http://localhost:11434)')}
bind:value={url}
readonly={true}

View file

@ -42,7 +42,7 @@
}}
/>
<div class="flex w-full gap-2 items-center">
<div class="group/setting-row flex w-full gap-2 items-center">
<Tooltip
className="w-full relative"
content={$i18n.t(`WebUI will make requests to "{{url}}/chat/completions"`, {
@ -58,7 +58,9 @@
<div class="flex w-full gap-2">
<div class="flex-1 relative">
<input
class=" outline-hidden w-full bg-transparent {pipeline ? 'pr-8' : ''}"
class="text-gray-600 dark:text-gray-400 transition-colors duration-150 group-hover/setting-row:text-gray-900 group-focus-within/setting-row:text-gray-900 dark:group-hover/setting-row:text-white dark:group-focus-within/setting-row:text-white outline-hidden w-full bg-transparent {pipeline
? 'pr-8'
: ''}"
placeholder={$i18n.t('API Base URL')}
bind:value={url}
autocomplete="off"

View file

@ -717,14 +717,17 @@
<div class="flex flex-col gap-1">
{#each webhooks as webhook}
<div class="flex w-full gap-2 items-center">
<div class="group/setting-row flex w-full gap-2 items-center">
<div
class="flex-1 min-w-0 flex gap-1.5 items-center {webhook.enabled ? '' : 'opacity-50'}"
>
<div class="outline-hidden w-full bg-transparent text-sm truncate">
<span class="font-normal">
<label
for={`webhook-${webhook.id}-control`}
class="font-normal text-gray-600 dark:text-gray-400 transition-colors duration-150 group-hover/setting-row:text-gray-900 group-focus-within/setting-row:text-gray-900 dark:group-hover/setting-row:text-white dark:group-focus-within/setting-row:text-white group-has-[button[id$=-control]:enabled]/setting-row:cursor-pointer group-has-[button[id$=-control]:disabled]/setting-row:cursor-not-allowed"
>
{webhook.id === 'default' ? $i18n.t('Default webhook') : webhook.name}
</span>
</label>
<span class="text-xs text-gray-500">
- {urlHost(webhook.url)} - {eventSummary(webhook)} - {targetSummary(webhook)}
</span>
@ -744,6 +747,7 @@
<Tooltip content={webhook.enabled ? $i18n.t('Enabled') : $i18n.t('Disabled')}>
<Switch
id={`webhook-${webhook.id}-control`}
state={webhook.enabled}
on:change={(event) => {
toggleWebhook(webhook, event.detail);

View file

@ -815,7 +815,7 @@
<div class="flex flex-col gap-1.5">
{#each items as item}
{@const connection = connectionForItem(item)}
<div class="flex w-full gap-2 items-center">
<div class="group/setting-row flex w-full gap-2 items-center">
<Tooltip className="w-full relative" content={''} placement="top-start">
<div class="flex w-full">
<div
@ -829,7 +829,11 @@
</Tooltip>
<div class="outline-hidden w-full bg-transparent text-xs min-w-0 line-clamp-1">
<span>{item.name}</span>
<label
for={`knowledge-${item.id}-control`}
class="text-gray-600 dark:text-gray-400 transition-colors duration-150 group-hover/setting-row:text-gray-900 group-focus-within/setting-row:text-gray-900 dark:group-hover/setting-row:text-white dark:group-focus-within/setting-row:text-white group-has-[button[id$=-control]:enabled]/setting-row:cursor-pointer group-has-[button[id$=-control]:disabled]/setting-row:cursor-not-allowed"
>{item.name}</label
>
{' '}
<span class="text-gray-500">
{item?.meta?.external?.provider ?? connection?.provider}
@ -859,6 +863,7 @@
content={connection?.enabled !== false ? $i18n.t('Enabled') : $i18n.t('Disabled')}
>
<Switch
id={`knowledge-${item.id}-control`}
state={connection?.enabled !== false}
on:change={() => {
toggleSource(item);

View file

@ -260,7 +260,7 @@
<div class="flex flex-col gap-1.5">
{#each terminalConnections as connection, idx}
<div class="flex w-full gap-2 items-center">
<div class="group/setting-row flex w-full gap-2 items-center">
<Tooltip className="w-full relative" content={''} placement="top-start">
<div class="flex w-full">
<div
@ -275,11 +275,12 @@
<Cloud className="size-4" strokeWidth="1.5" />
</Tooltip>
<div
class="outline-hidden w-full bg-transparent text-xs text-gray-700 dark:text-gray-300"
<label
for={`terminal-${idx}-control`}
class="outline-hidden w-full bg-transparent text-xs text-gray-600 dark:text-gray-400 transition-colors duration-150 group-hover/setting-row:text-gray-900 group-focus-within/setting-row:text-gray-900 dark:group-hover/setting-row:text-white dark:group-focus-within/setting-row:text-white group-has-[button[id$=-control]:enabled]/setting-row:cursor-pointer group-has-[button[id$=-control]:disabled]/setting-row:cursor-not-allowed"
>
{connection.name || connection.url || $i18n.t('New Terminal')}
</div>
</label>
</div>
</div>
</Tooltip>
@ -304,6 +305,7 @@
: $i18n.t('Disabled')}
>
<Switch
id={`terminal-${idx}-control`}
state={connection?.enabled !== false}
on:change={() => {
terminalConnections = terminalConnections.map((c, i) =>

View file

@ -1028,7 +1028,7 @@
{#if filteredModels.length > 0}
{#each filteredModels as model, modelIdx (`${model.id}-${modelIdx}`)}
<div
class="flex cursor-pointer transition w-full px-2 py-1 rounded-xl hover:bg-gray-50/70 dark:hover:bg-gray-850/50 {model
class="group/setting-row flex cursor-pointer transition w-full px-2 py-1 rounded-xl hover:bg-gray-50/70 dark:hover:bg-gray-850/50 {model
?.meta?.hidden
? 'opacity-50 dark:opacity-50'
: ''}"
@ -1090,7 +1090,10 @@
<div
class="flex min-w-0 items-center gap-1.5 text-[0.8125rem] font-normal leading-4"
>
<span class="min-w-0 truncate">{model.name}</span>
<span
class="min-w-0 truncate text-gray-600 dark:text-gray-400 transition-colors duration-150 group-hover/setting-row:text-gray-900 group-focus-within/setting-row:text-gray-900 dark:group-hover/setting-row:text-white dark:group-focus-within/setting-row:text-white"
>{model.name}</span
>
<span
class="shrink-0 text-[0.6875rem] font-normal leading-4 {modelAccessClass(

View file

@ -1,4 +1,5 @@
<script lang="ts">
import AdminSettingRow from './AdminSettingRow.svelte';
import { getContext, onMount } from 'svelte';
import { toast } from 'svelte-sonner';
@ -66,12 +67,12 @@
<div class="flex justify-center py-8"><Spinner className="size-6" /></div>
{:else}
<div class="flex flex-col gap-2.5">
<label class="flex cursor-pointer items-center justify-between">
<span class="text-xs text-gray-600 dark:text-gray-400">
<AdminSettingRow let:labelId>
<span slot="label">
{$i18n.t('settings.admin.subagents.enableSubAgents.label')}
</span>
<Switch bind:state={enabled} />
</label>
<Switch ariaLabelledbyId={labelId} bind:state={enabled} />
</AdminSettingRow>
<p class="-mt-1 text-[0.6875rem] text-gray-400 dark:text-gray-600">
{$i18n.t(
'Allow the AI to delegate tasks to sub-agents. Each sub-agent creates a real chat with full tool access. Uses additional LLM calls.'
@ -98,12 +99,12 @@
</div>
<div>
<label class="flex cursor-pointer items-center justify-between">
<span class="text-xs text-gray-600 dark:text-gray-400">
<AdminSettingRow let:labelId>
<span slot="label">
{$i18n.t('settings.admin.subagents.enableBackgroundSubAgents.label')}
</span>
<Switch bind:state={backgroundEnabled} />
</label>
<Switch ariaLabelledbyId={labelId} bind:state={backgroundEnabled} />
</AdminSettingRow>
<p class="mt-1 text-[0.6875rem] text-gray-400 dark:text-gray-600">
{$i18n.t('settings.admin.subagents.enableBackgroundSubAgents.description')}
</p>

View file

@ -1,4 +1,5 @@
<script lang="ts">
import UserSettingRow from '../UserSettingRow.svelte';
import { onMount, getContext } from 'svelte';
import { mfaRequest } from '$lib/apis/auths/mfa';
import type { MfaManagementFlow } from '$lib/components/auth/MfaManagement.svelte';
@ -53,20 +54,21 @@
<div class="space-y-2.5 text-xs">
{#if status}
<div class="flex items-center justify-between gap-2.5">
<span class="text-gray-600 dark:text-gray-400"
<UserSettingRow let:labelId>
<span slot="label"
>{status.enabled
? $i18n.t('Authenticator configured')
: $i18n.t('Authenticator not configured')}</span
>
{#if status.required && status.enabled}<button
id={`${labelId}-control`}
type="button"
class={actionButtonClass}
on:click={() => {
show = !show;
}}>{show ? $i18n.t('Hide') : $i18n.t('Manage')}</button
>{/if}
</div>
</UserSettingRow>
<p class="text-[0.6875rem] text-gray-400 dark:text-gray-600">
{status.enabled
? $i18n.t('{{COUNT}} recovery codes remaining', { COUNT: status.recovery_codes_remaining })

View file

@ -1,4 +1,5 @@
<script lang="ts">
import UserSettingRow from '../UserSettingRow.svelte';
import { getContext } from 'svelte';
import { toast } from 'svelte-sonner';
import { getLogoutRedirectUrl, updateUserPassword, userSignOut } from '$lib/apis/auths';
@ -54,18 +55,19 @@
updatePasswordHandler();
}}
>
<div class="flex items-center justify-between gap-2.5">
<div class="text-xs text-gray-600 dark:text-gray-400">
<UserSettingRow let:labelId>
<span slot="label">
{$i18n.t('settings.personal.account.changePassword.label')}
</div>
</span>
<button
id={`${labelId}-control`}
class={actionButtonClass}
type="button"
on:click={() => {
show = !show;
}}>{show ? $i18n.t('Hide') : $i18n.t('Show')}</button
>
</div>
</UserSettingRow>
<p class="mt-0.5 text-[0.6875rem] text-gray-400 dark:text-gray-600">
{$i18n.t('settings.personal.account.changePassword.description')}
</p>

View file

@ -40,7 +40,7 @@
}}
/>
<div class="flex w-full items-center gap-3">
<div class="group/setting-row flex w-full items-center gap-3">
<Tooltip
className="w-full relative"
content={$i18n.t(`WebUI will make requests to "{{url}}/chat/completions"`, {
@ -56,7 +56,7 @@
<div class="flex w-full gap-2">
<div class="flex-1 relative">
<input
class={`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 ${pipeline ? 'pr-8' : ''}`}
class={`text-gray-600 dark:text-gray-400 transition-colors duration-150 group-hover/setting-row:text-gray-900 group-focus-within/setting-row:text-gray-900 dark:group-hover/setting-row:text-white dark:group-focus-within/setting-row:text-white h-7 w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 text-xs outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:placeholder:text-gray-700 dark:focus:border-blue-500 ${pipeline ? 'pr-8' : ''}`}
placeholder={$i18n.t('API Base URL')}
bind:value={url}
autocomplete="off"

View file

@ -1,4 +1,6 @@
<script lang="ts">
import { v4 as uuidv4 } from 'uuid';
const settingId = `connection-${uuidv4()}`;
import { getContext } from 'svelte';
import type { Writable } from 'svelte/store';
import type { i18n as i18nType } from 'i18next';
@ -50,7 +52,7 @@
}}
/>
<div class="flex w-full gap-2 items-center">
<div class="group/setting-row flex w-full gap-2 items-center">
<Tooltip className="w-full relative" content={''} placement="top-start">
<div class="flex w-full">
<div
@ -60,9 +62,12 @@
<Cloud className="size-4" strokeWidth="1.5" />
</Tooltip>
<div class="outline-hidden w-full bg-transparent text-xs text-gray-700 dark:text-gray-300">
<label
for={`${settingId}-control`}
class="outline-hidden w-full bg-transparent text-xs text-gray-600 dark:text-gray-400 transition-colors duration-150 group-hover/setting-row:text-gray-900 group-focus-within/setting-row:text-gray-900 dark:group-hover/setting-row:text-white dark:group-focus-within/setting-row:text-white group-has-[button[id$=-control]:enabled]/setting-row:cursor-pointer group-has-[button[id$=-control]:disabled]/setting-row:cursor-not-allowed"
>
{connection.name || connection.url || $i18n.t('New Terminal')}
</div>
</label>
</div>
</div>
</Tooltip>
@ -82,6 +87,7 @@
<Tooltip content={connection?.enabled ? $i18n.t('Enabled') : $i18n.t('Disabled')}>
<Switch
id={`${settingId}-control`}
state={connection?.enabled}
on:change={() => (connection?.enabled ? onDisable() : onEnable())}
/>

View file

@ -1,4 +1,5 @@
<script lang="ts">
import UserSettingRow from './UserSettingRow.svelte';
import { canUseNotificationTargets } from '$lib/utils/settings-access';
import { getContext, onMount } from 'svelte';
import type { Writable } from 'svelte/store';
@ -197,32 +198,34 @@
</h2>
<div class="flex flex-col gap-2.5">
<label class="flex cursor-pointer items-center justify-between">
<span class="text-xs text-gray-600 dark:text-gray-400">
<UserSettingRow let:labelId>
<span slot="label">
{$i18n.t('settings.personal.notifications.browserNotifications.label')}
</span>
<Switch
ariaLabelledbyId={labelId}
bind:state={notificationEnabled}
ariaLabel={$i18n.t('settings.personal.notifications.browserNotifications.label')}
on:change={toggleNotifications}
/>
</label>
</UserSettingRow>
<p class="-mt-1 text-[0.6875rem] text-gray-400 dark:text-gray-600">
{$i18n.t('Allow browser notifications for completed responses.')}
</p>
<label class="flex cursor-pointer items-center justify-between">
<span class="text-xs text-gray-600 dark:text-gray-400">
<UserSettingRow let:labelId>
<span slot="label">
{$i18n.t('settings.personal.notifications.notificationSound.label')}
</span>
<Switch
ariaLabelledbyId={labelId}
bind:state={notificationSound}
ariaLabel={$i18n.t('settings.personal.notifications.notificationSound.label')}
on:change={() => {
saveSettings({ notificationSound });
}}
/>
</label>
</UserSettingRow>
{#if canUseWebhooks}
<div class="mt-3 flex items-center justify-between">
@ -253,12 +256,15 @@
.filter((event) => target.events.includes(event.event))
.map((event) => $i18n.t(event.label))
.join(', ')}
<div class="flex items-center gap-3 px-1 py-1.5">
<div class="group/setting-row flex items-center gap-3 px-1 py-1.5">
<div class="min-w-0 flex-1">
<div class="flex min-w-0 items-center gap-2">
<span class="truncate text-[0.71875rem] text-gray-700 dark:text-gray-300">
<label
for={`notification-${target.id}-control`}
class="truncate text-[0.71875rem] text-gray-600 dark:text-gray-400 transition-colors duration-150 group-hover/setting-row:text-gray-900 group-focus-within/setting-row:text-gray-900 dark:group-hover/setting-row:text-white dark:group-focus-within/setting-row:text-white group-has-[button[id$=-control]:enabled]/setting-row:cursor-pointer group-has-[button[id$=-control]:disabled]/setting-row:cursor-not-allowed"
>
{target.id}
</span>
</label>
<span class="shrink-0 text-[0.625rem] text-gray-400 dark:text-gray-600">
{$i18n.t('Webhook')}
</span>
@ -326,6 +332,7 @@
<div class="flex w-9 shrink-0 justify-end">
<Switch
id={`notification-${target.id}-control`}
state={target.enabled}
ariaLabel={$i18n.t('Enabled')}
on:change={(event) => patchTarget(target, { enabled: event.detail })}

View file

@ -1,4 +1,5 @@
<script lang="ts">
import UserSettingRow from './UserSettingRow.svelte';
import { getContext, onMount } from 'svelte';
import type { Writable } from 'svelte/store';
import { updateUserSettings } from '$lib/apis/users';
@ -119,17 +120,14 @@
</div>
<div class="mb-3">
<div class="flex items-center justify-between gap-2.5">
<div
id="enable-keyboard-shortcuts-label"
class="min-w-0 text-xs text-gray-600 dark:text-gray-400"
>
<UserSettingRow let:labelId>
<span slot="label">
{$i18n.t('settings.personal.shortcuts.enableKeyboardShortcuts.label')}
</div>
</span>
<div class="flex shrink-0 items-center justify-end gap-1.5">
<Switch
ariaLabelledbyId="enable-keyboard-shortcuts-label"
ariaLabelledbyId={labelId}
tooltip={true}
bind:state={enableKeyboardShortcuts}
on:change={() => {
@ -137,7 +135,7 @@
}}
/>
</div>
</div>
</UserSettingRow>
<p class="mt-1.5 text-[0.6875rem] text-gray-400 dark:text-gray-600">
{$i18n.t('settings.personal.shortcuts.enableKeyboardShortcuts.description')}
</p>

View file

@ -1,4 +1,6 @@
<script lang="ts">
import { v4 as uuidv4 } from 'uuid';
const settingId = `connection-${uuidv4()}`;
import { getContext, tick } from 'svelte';
const i18n = getContext('i18n');
@ -33,7 +35,9 @@
}}
/>
<div class="flex w-full items-center gap-3 text-xs text-gray-600 dark:text-gray-400">
<div
class="group/setting-row flex w-full items-center gap-3 text-xs text-gray-600 dark:text-gray-400"
>
<Tooltip className="w-full relative" content={''} placement="top-start">
<div class="flex w-full">
<div
@ -52,14 +56,20 @@
</Tooltip>
{#if connection?.info?.name}
<div class="w-full bg-transparent capitalize outline-hidden">
<label
for={`${settingId}-control`}
class="w-full bg-transparent capitalize outline-hidden text-gray-600 dark:text-gray-400 transition-colors duration-150 group-hover/setting-row:text-gray-900 group-focus-within/setting-row:text-gray-900 dark:group-hover/setting-row:text-white dark:group-focus-within/setting-row:text-white group-has-[button[id$=-control]:enabled]/setting-row:cursor-pointer group-has-[button[id$=-control]:disabled]/setting-row:cursor-not-allowed"
>
{connection?.info?.name ?? connection?.url}
<span class="text-gray-500">{connection?.info?.id ?? ''}</span>
</div>
</label>
{:else}
<div>
<label
for={`${settingId}-control`}
class="text-gray-600 dark:text-gray-400 transition-colors duration-150 group-hover/setting-row:text-gray-900 group-focus-within/setting-row:text-gray-900 dark:group-hover/setting-row:text-white dark:group-focus-within/setting-row:text-white group-has-[button[id$=-control]:enabled]/setting-row:cursor-pointer group-has-[button[id$=-control]:disabled]/setting-row:cursor-not-allowed"
>
{connection?.url}
</div>
</label>
{/if}
</div>
</div>
@ -82,6 +92,7 @@
content={(connection?.config?.enable ?? true) ? $i18n.t('Enabled') : $i18n.t('Disabled')}
>
<Switch
id={`${settingId}-control`}
state={connection?.config?.enable ?? true}
on:change={() => {
if (!connection.config) connection.config = {};