This commit is contained in:
Timothy Jaeryang Baek
2026-08-10 19:16:21 -06:00
parent ec03e88144
commit 72a909fd2f
6 changed files with 129 additions and 32 deletions
+6 -1
View File
@@ -469,9 +469,14 @@ async def get_default_user_permissions_defaults(user=Depends(get_admin_user)):
@router.get('/user/settings', response_model=UserSettings | None)
async def get_user_settings_by_session_user(
user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)
raw: bool = False,
user=Depends(get_verified_user),
db: AsyncSession = Depends(get_async_session),
):
# user already fetched by get_verified_user — no need to refetch
if raw:
return user.settings
default_interface_settings = await Config.get('ui.default_interface_settings')
if not isinstance(default_interface_settings, dict) or not default_interface_settings:
return user.settings
+2 -2
View File
@@ -271,9 +271,9 @@ export const getAllUsers = async (token: string) => {
return res;
};
export const getUserSettings = async (token: string) => {
export const getUserSettings = async (token: string, raw = false) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/users/user/settings`, {
const res = await fetch(`${WEBUI_API_BASE_URL}/users/user/settings${raw ? '?raw=true' : ''}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
@@ -6,6 +6,7 @@
const i18n: any = getContext('i18n');
export let saveSettings: Function;
export let personalSettingsValue: Record<string, any> = {};
let interfaceSettings: any;
</script>
@@ -21,7 +22,7 @@
<h2 class="text-sm font-medium text-gray-900 dark:text-white mb-4">{$i18n.t('Interface')}</h2>
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5">
<InterfaceSettings bind:this={interfaceSettings} {saveSettings} />
<InterfaceSettings bind:this={interfaceSettings} {saveSettings} {personalSettingsValue} />
</div>
<div class="shrink-0 flex justify-end text-sm font-normal">
+22 -2
View File
@@ -5,7 +5,7 @@
import { toast } from 'svelte-sonner';
import { config, models, settings, user } from '$lib/stores';
import type { SettingsModalRequest } from '$lib/stores';
import { updateUserSettings } from '$lib/apis/users';
import { getUserSettings, updateUserSettings } from '$lib/apis/users';
import { getBackendConfig, getModels as _getModels } from '$lib/apis';
import Modal from '../common/Modal.svelte';
@@ -61,6 +61,18 @@
let modalShow = false;
let lastShow: boolean | string | SettingsModalRequest = false;
let tabState: Record<string, unknown> | null = null;
let personalUiSettings: Record<string, any> = {};
const loadPersonalUiSettings = async () => {
const userSettings = await getUserSettings(localStorage.token, true).catch((error) => {
console.error(error);
return null;
});
personalUiSettings =
userSettings?.ui && typeof userSettings.ui === 'object' && !Array.isArray(userSettings.ui)
? userSettings.ui
: {};
};
$: if (show !== lastShow) {
lastShow = show;
@@ -70,13 +82,18 @@
show = true;
lastShow = true;
modalShow = true;
loadPersonalUiSettings();
} else if (typeof show === 'string') {
selectedTab = show;
show = true;
lastShow = true;
modalShow = true;
loadPersonalUiSettings();
} else {
modalShow = show;
if (show) {
loadPersonalUiSettings();
}
if (!show) {
selectedTab = 'general';
tabState = null;
@@ -818,7 +835,9 @@
console.log(updated);
await settings.set({ ...$settings, ...updated });
await models.set(await getModels());
await updateUserSettings(localStorage.token, { ui: $settings });
const saved = await updateUserSettings(localStorage.token, { ui: $settings });
personalUiSettings =
saved?.ui && typeof saved.ui === 'object' && !Array.isArray(saved.ui) ? saved.ui : {};
};
const getModels = async () => {
@@ -1169,6 +1188,7 @@
{:else if selectedTab === 'interface'}
<Interface
{saveSettings}
personalSettingsValue={personalUiSettings}
on:save={() => {
toast.success($i18n.t('Settings saved successfully!'));
}}
@@ -16,6 +16,7 @@
export let saveSettings: Function;
export let settingsValue: Record<string, any> | null = null;
export let personalSettingsValue: Record<string, any> = {};
$: currentSettings = (settingsValue ?? $settings ?? {}) as Record<string, any>;
$: externalSettings = settingsValue !== null;
@@ -118,10 +119,29 @@
const actionButtonClass =
'text-xs text-gray-500 transition-colors hover:text-gray-900 dark:text-gray-500 dark:hover:text-white';
const matchesDefaultSetting = (key: string, value: unknown) =>
!externalSettings &&
Object.prototype.hasOwnProperty.call(defaultSettings, key) &&
JSON.stringify(defaultSettings[key]) === JSON.stringify(value);
const hasSettingPath = (source: Record<string, any>, path: string) => {
let current: any = source;
for (const part of path.split('.')) {
if (!current || typeof current !== 'object' || !Object.prototype.hasOwnProperty.call(current, part)) {
return false;
}
current = current[part];
}
return true;
};
const settingPathByState: Record<string, string> = {
titleAutoGenerate: 'title.auto'
};
const isDefaultSetting = (stateKey: string) => {
const path = settingPathByState[stateKey] ?? stateKey;
return (
!externalSettings &&
hasSettingPath(defaultSettings, path) &&
!hasSettingPath(personalSettingsValue, path)
);
};
const toggleLandingPageMode = async () => {
landingPageMode = landingPageMode === '' ? 'chat' : '';
@@ -315,7 +335,7 @@
lastSettingsValue = settingsValue;
init();
}
$: textScaleUsesDefault = textScale === null || matchesDefaultSetting('textScale', textScale);
$: textScaleUsesDefault = textScale === null || isDefaultSetting('textScale');
onMount(async () => {
init();
@@ -463,6 +483,7 @@
ariaLabelledbyId="high-contrast-mode-label"
tooltip={true}
bind:state={highContrastMode}
inherited={isDefaultSetting('highContrastMode')}
on:change={() => {
saveSettings({ highContrastMode });
}}
@@ -485,6 +506,7 @@
ariaLabelledbyId="use-chat-title-as-tab-title-label"
tooltip={true}
bind:state={showChatTitleInTab}
inherited={isDefaultSetting('showChatTitleInTab')}
on:change={() => {
saveSettings({ showChatTitleInTab });
}}
@@ -505,6 +527,7 @@
ariaLabelledbyId="allow-user-location-label"
tooltip={true}
bind:state={userLocation}
inherited={isDefaultSetting('userLocation')}
on:change={() => {
toggleUserLocation();
}}
@@ -527,6 +550,7 @@
ariaLabelledbyId="haptic-feedback-label"
tooltip={true}
bind:state={hapticFeedback}
inherited={isDefaultSetting('hapticFeedback')}
on:change={() => {
saveSettings({ hapticFeedback });
}}
@@ -549,6 +573,7 @@
ariaLabelledbyId="copy-formatted-label"
tooltip={true}
bind:state={copyFormatted}
inherited={isDefaultSetting('copyFormatted')}
on:change={() => {
saveSettings({ copyFormatted });
}}
@@ -572,6 +597,7 @@
ariaLabelledbyId="toast-notifications-label"
tooltip={true}
bind:state={showUpdateToast}
inherited={isDefaultSetting('showUpdateToast')}
on:change={() => {
saveSettings({ showUpdateToast });
}}
@@ -594,6 +620,7 @@
ariaLabelledbyId="whats-new-label"
tooltip={true}
bind:state={showChangelog}
inherited={isDefaultSetting('showChangelog')}
on:change={() => {
saveSettings({ showChangelog });
}}
@@ -619,6 +646,7 @@
ariaLabelledbyId="enable-message-queue-label"
tooltip={true}
bind:state={enableMessageQueue}
inherited={isDefaultSetting('enableMessageQueue')}
on:change={() => {
saveSettings({ enableMessageQueue });
}}
@@ -720,6 +748,7 @@
tooltip={true}
ariaLabelledbyId="chat-bubble-ui-label"
bind:state={chatBubble}
inherited={isDefaultSetting('chatBubble')}
on:change={() => {
saveSettings({ chatBubble });
}}
@@ -743,6 +772,7 @@
ariaLabelledbyId="chat-bubble-username-label"
tooltip={true}
bind:state={showUsername}
inherited={isDefaultSetting('showUsername')}
on:change={() => {
saveSettings({ showUsername });
}}
@@ -766,6 +796,7 @@
ariaLabelledbyId="widescreen-mode-label"
tooltip={true}
bind:state={widescreenMode}
inherited={isDefaultSetting('widescreenMode')}
on:change={() => {
saveSettings({ widescreenMode });
}}
@@ -789,6 +820,7 @@
ariaLabelledbyId="temp-chat-default-label"
tooltip={true}
bind:state={temporaryChatByDefault}
inherited={isDefaultSetting('temporaryChatByDefault')}
on:change={() => {
saveSettings({ temporaryChatByDefault });
}}
@@ -812,6 +844,7 @@
ariaLabelledbyId="fade-streaming-label"
tooltip={true}
bind:state={chatFadeStreamingText}
inherited={isDefaultSetting('chatFadeStreamingText')}
on:change={() => {
saveSettings({ chatFadeStreamingText });
}}
@@ -834,6 +867,7 @@
ariaLabelledbyId="render-markdown-user-label"
tooltip={true}
bind:state={renderMarkdownInUserMessages}
inherited={isDefaultSetting('renderMarkdownInUserMessages')}
on:change={() => {
saveSettings({ renderMarkdownInUserMessages });
}}
@@ -856,6 +890,7 @@
ariaLabelledbyId="render-markdown-assistant-label"
tooltip={true}
bind:state={renderMarkdownInAssistantMessages}
inherited={isDefaultSetting('renderMarkdownInAssistantMessages')}
on:change={() => {
saveSettings({ renderMarkdownInAssistantMessages });
}}
@@ -878,6 +913,7 @@
ariaLabelledbyId="auto-generation-label"
tooltip={true}
bind:state={titleAutoGenerate}
inherited={isDefaultSetting('titleAutoGenerate')}
on:change={() => {
toggleTitleAutoGenerate();
}}
@@ -900,6 +936,7 @@
ariaLabelledbyId="follow-up-auto-generation-label"
tooltip={true}
bind:state={autoFollowUps}
inherited={isDefaultSetting('autoFollowUps')}
on:change={() => {
saveSettings({ autoFollowUps });
}}
@@ -922,6 +959,7 @@
ariaLabelledbyId="chat-tags-label"
tooltip={true}
bind:state={autoTags}
inherited={isDefaultSetting('autoTags')}
on:change={() => {
saveSettings({ autoTags });
}}
@@ -944,6 +982,7 @@
ariaLabelledbyId="auto-copy-label"
tooltip={true}
bind:state={responseAutoCopy}
inherited={isDefaultSetting('responseAutoCopy')}
on:change={() => {
toggleResponseAutoCopy();
}}
@@ -966,6 +1005,7 @@
ariaLabelledbyId="response-auto-scroll-label"
tooltip={true}
bind:state={scrollOnResponseGeneration}
inherited={isDefaultSetting('scrollOnResponseGeneration')}
on:change={() => {
saveSettings({ scrollOnResponseGeneration });
}}
@@ -988,6 +1028,7 @@
ariaLabelledbyId="insert-suggestion-prompt-label"
tooltip={true}
bind:state={insertSuggestionPrompt}
inherited={isDefaultSetting('insertSuggestionPrompt')}
on:change={() => {
saveSettings({ insertSuggestionPrompt });
}}
@@ -1010,6 +1051,7 @@
ariaLabelledbyId="keep-follow-up-prompts-label"
tooltip={true}
bind:state={keepFollowUpPrompts}
inherited={isDefaultSetting('keepFollowUpPrompts')}
on:change={() => {
saveSettings({ keepFollowUpPrompts });
}}
@@ -1032,6 +1074,7 @@
ariaLabelledbyId="insert-follow-up-prompt-label"
tooltip={true}
bind:state={insertFollowUpPrompt}
inherited={isDefaultSetting('insertFollowUpPrompt')}
on:change={() => {
saveSettings({ insertFollowUpPrompt });
}}
@@ -1054,6 +1097,7 @@
ariaLabelledbyId="regenerate-menu-label"
tooltip={true}
bind:state={regenerateMenu}
inherited={isDefaultSetting('regenerateMenu')}
on:change={() => {
saveSettings({ regenerateMenu });
}}
@@ -1076,6 +1120,7 @@
ariaLabelledbyId="always-collapse-label"
tooltip={true}
bind:state={collapseCodeBlocks}
inherited={isDefaultSetting('collapseCodeBlocks')}
on:change={() => {
saveSettings({ collapseCodeBlocks });
}}
@@ -1098,6 +1143,7 @@
ariaLabelledbyId="always-expand-label"
tooltip={true}
bind:state={expandDetails}
inherited={isDefaultSetting('expandDetails')}
on:change={() => {
saveSettings({ expandDetails });
}}
@@ -1120,6 +1166,7 @@
ariaLabelledbyId="render-markdown-in-previews-label"
tooltip={true}
bind:state={renderMarkdownInPreviews}
inherited={isDefaultSetting('renderMarkdownInPreviews')}
on:change={() => {
saveSettings({ renderMarkdownInPreviews });
}}
@@ -1142,6 +1189,7 @@
ariaLabelledbyId="keep-followup-prompts-label"
tooltip={true}
bind:state={displayMultiModelResponsesInTabs}
inherited={isDefaultSetting('displayMultiModelResponsesInTabs')}
on:change={() => {
saveSettings({ displayMultiModelResponsesInTabs });
}}
@@ -1164,6 +1212,7 @@
ariaLabelledbyId="scroll-on-branch-change-label"
tooltip={true}
bind:state={scrollOnBranchChange}
inherited={isDefaultSetting('scrollOnBranchChange')}
on:change={() => {
saveSettings({ scrollOnBranchChange });
}}
@@ -1186,6 +1235,7 @@
ariaLabelledbyId="show-files-on-terminal-select-label"
tooltip={true}
bind:state={showFilesOnTerminalSelect}
inherited={isDefaultSetting('showFilesOnTerminalSelect')}
on:change={() => {
saveSettings({ showFilesOnTerminalSelect });
}}
@@ -1208,6 +1258,7 @@
ariaLabelledbyId="stylized-pdf-export-label"
tooltip={true}
bind:state={stylizedPdfExport}
inherited={isDefaultSetting('stylizedPdfExport')}
on:change={() => {
saveSettings({ stylizedPdfExport });
}}
@@ -1243,6 +1294,7 @@
ariaLabelledbyId="floating-action-buttons-label"
tooltip={true}
bind:state={showFloatingActionButtons}
inherited={isDefaultSetting('showFloatingActionButtons')}
on:change={() => {
saveSettings({ showFloatingActionButtons });
}}
@@ -1317,6 +1369,7 @@
tooltip={true}
ariaLabelledbyId="rich-input-label"
bind:state={richTextInput}
inherited={isDefaultSetting('richTextInput')}
on:change={() => {
saveSettings({ richTextInput });
}}
@@ -1340,6 +1393,7 @@
ariaLabelledbyId="prompt-autocompletion-label"
tooltip={true}
bind:state={promptAutocomplete}
inherited={isDefaultSetting('promptAutocomplete')}
on:change={() => {
saveSettings({ promptAutocomplete });
}}
@@ -1364,6 +1418,7 @@
ariaLabelledbyId="show-formatting-toolbar-label"
tooltip={true}
bind:state={showFormattingToolbar}
inherited={isDefaultSetting('showFormattingToolbar')}
on:change={() => {
saveSettings({ showFormattingToolbar });
}}
@@ -1386,6 +1441,7 @@
ariaLabelledbyId="insert-prompt-as-rich-text-label"
tooltip={true}
bind:state={insertPromptAsRichText}
inherited={isDefaultSetting('insertPromptAsRichText')}
on:change={() => {
saveSettings({ insertPromptAsRichText });
}}
@@ -1409,6 +1465,7 @@
tooltip={true}
ariaLabelledbyId="paste-large-label"
bind:state={largeTextAsFile}
inherited={isDefaultSetting('largeTextAsFile')}
on:change={() => {
saveSettings({ largeTextAsFile });
}}
@@ -1433,6 +1490,7 @@
ariaLabelledbyId="detect-artifacts-label"
tooltip={true}
bind:state={detectArtifacts}
inherited={isDefaultSetting('detectArtifacts')}
on:change={() => {
saveSettings({ detectArtifacts });
}}
@@ -1455,6 +1513,7 @@
ariaLabelledbyId="iframe-sandbox-allow-same-origin-label"
tooltip={true}
bind:state={iframeSandboxAllowSameOrigin}
inherited={isDefaultSetting('iframeSandboxAllowSameOrigin')}
on:change={() => {
saveSettings({ iframeSandboxAllowSameOrigin });
}}
@@ -1477,6 +1536,7 @@
ariaLabelledbyId="iframe-sandbox-allow-forms-label"
tooltip={true}
bind:state={iframeSandboxAllowForms}
inherited={isDefaultSetting('iframeSandboxAllowForms')}
on:change={() => {
saveSettings({ iframeSandboxAllowForms });
}}
@@ -1501,6 +1561,7 @@
ariaLabelledbyId="allow-voice-interruption-in-call-label"
tooltip={true}
bind:state={voiceInterruption}
inherited={isDefaultSetting('voiceInterruption')}
on:change={() => {
saveSettings({ voiceInterruption });
}}
@@ -1523,6 +1584,7 @@
ariaLabelledbyId="display-emoji-label"
tooltip={true}
bind:state={showEmojiInCall}
inherited={isDefaultSetting('showEmojiInCall')}
on:change={() => {
saveSettings({ showEmojiInCall });
}}
@@ -1587,6 +1649,7 @@
ariaLabelledbyId="image-compression-label"
tooltip={true}
bind:state={imageCompression}
inherited={isDefaultSetting('imageCompression')}
on:change={() => {
saveSettings({ imageCompression });
}}
@@ -1610,6 +1673,7 @@
ariaLabelledbyId="image-compression-in-channels-label"
tooltip={true}
bind:state={imageCompressionInChannels}
inherited={isDefaultSetting('imageCompressionInChannels')}
on:change={() => {
saveSettings({ imageCompressionInChannels });
}}
+28 -21
View File
@@ -10,8 +10,9 @@
export let ariaLabelledbyId = '';
export let ariaLabel = '';
export let tooltip = false;
export let inherited = false;
const i18n = getContext('i18n');
const i18n: any = getContext('i18n');
const dispatch = createEventDispatcher();
</script>
@@ -25,24 +26,30 @@
: ''}
placement="top"
>
<Switch.Root
bind:checked={state}
{id}
aria-labelledby={ariaLabelledbyId || undefined}
aria-label={ariaLabel || undefined}
class="relative h-4 min-h-4 w-7 shrink-0 cursor-pointer rounded-full mx-[0.0625rem] transition-colors duration-150 {($settings?.highContrastMode ??
false)
? 'focus:outline focus:outline-2 focus:outline-gray-800 focus:dark:outline-gray-200'
: 'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-400 dark:focus-visible:outline-gray-500'} {state
? 'bg-gray-900 dark:bg-white'
: 'bg-gray-300 dark:bg-gray-700'}"
onCheckedChange={async () => {
await tick();
dispatch('change', state);
}}
>
<Switch.Thumb
class="pointer-events-none absolute top-[0.125rem] block h-3 w-3 shrink-0 rounded-full transition-all duration-150 data-[state=checked]:left-[0.875rem] data-[state=checked]:bg-white data-[state=checked]:dark:bg-black data-[state=unchecked]:left-[0.125rem] data-[state=unchecked]:bg-white data-[state=unchecked]:dark:bg-gray-500"
/>
</Switch.Root>
<div class="flex items-center gap-1.5">
{#if inherited}
<span class="text-[0.6875rem] text-gray-400 dark:text-gray-600">{$i18n.t('Default')}</span>
{/if}
<Switch.Root
bind:checked={state}
{id}
aria-labelledby={ariaLabelledbyId || undefined}
aria-label={ariaLabel || undefined}
class="relative h-4 min-h-4 w-7 shrink-0 cursor-pointer rounded-full mx-[0.0625rem] transition-colors duration-150 {($settings?.highContrastMode ??
false)
? 'focus:outline focus:outline-2 focus:outline-gray-800 focus:dark:outline-gray-200'
: 'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-400 dark:focus-visible:outline-gray-500'} {state
? 'bg-gray-900 dark:bg-white'
: 'bg-gray-300 dark:bg-gray-700'}"
onCheckedChange={async () => {
await tick();
dispatch('change', state);
}}
>
<Switch.Thumb
class="pointer-events-none absolute top-[0.125rem] block h-3 w-3 shrink-0 rounded-full transition-all duration-150 data-[state=checked]:left-[0.875rem] data-[state=checked]:bg-white data-[state=checked]:dark:bg-black data-[state=unchecked]:left-[0.125rem] data-[state=unchecked]:bg-white data-[state=unchecked]:dark:bg-gray-500"
/>
</Switch.Root>
</div>
</Tooltip>