diff --git a/backend/open_webui/routers/configs.py b/backend/open_webui/routers/configs.py index c286152abd..fd2ad14602 100644 --- a/backend/open_webui/routers/configs.py +++ b/backend/open_webui/routers/configs.py @@ -676,9 +676,19 @@ class InterfaceDefaultsForm(BaseModel): @field_validator("chatDirection") @classmethod def validate_chat_direction(cls, value): - if value is not None and value not in ("auto", "ltr", "rtl"): - raise ValueError("chatDirection must be 'auto', 'ltr', or 'rtl'") - return value + # The user-side store is typed 'LTR' | 'RTL' | 'auto' and the + # Interface settings toggle cycles uppercase values, so accept any + # casing and normalize to the shape the frontend expects. Rejecting + # the uppercase values outright (as the validator did before) made + # the modal fail with a 422 on every LTR/RTL save. + if value is None: + return value + normalized = value.lower() + if normalized == "auto": + return "auto" + if normalized in ("ltr", "rtl"): + return normalized.upper() + raise ValueError("chatDirection must be 'auto', 'LTR', or 'RTL'") @field_validator("textScale") @classmethod diff --git a/src/lib/apis/configs/index.ts b/src/lib/apis/configs/index.ts index 7cbdad0431..df16b033d2 100644 --- a/src/lib/apis/configs/index.ts +++ b/src/lib/apis/configs/index.ts @@ -664,7 +664,12 @@ export const getInterfaceDefaults = async (token: string) => { }) .catch((err) => { console.error(err); - error = err.detail; + // Fall back through detail / message / stringification so a + // network TypeError or non-JSON response still produces a truthy + // error — otherwise the function silently returned null and the + // caller couldn't tell a failed fetch from an empty-defaults + // response. + error = err?.detail ?? err?.message ?? (typeof err === 'string' ? err : 'Request failed'); return null; }); @@ -692,7 +697,7 @@ export const setInterfaceDefaults = async (token: string, defaults: object) => { }) .catch((err) => { console.error(err); - error = err.detail; + error = err?.detail ?? err?.message ?? (typeof err === 'string' ? err : 'Request failed'); return null; }); diff --git a/src/lib/apis/users/index.ts b/src/lib/apis/users/index.ts index b599f0b7fc..23e8eca5df 100644 --- a/src/lib/apis/users/index.ts +++ b/src/lib/apis/users/index.ts @@ -567,7 +567,11 @@ export const resetAllUsersInterfaceSettings = async (token: string) => { }) .catch((err) => { console.error(err); - error = err.detail; + // Fall back through detail / message / stringification so a + // network TypeError or non-JSON response still produces a truthy + // error — otherwise the function silently returned null and the + // caller could mistake a failed reset for a silent success. + error = err?.detail ?? err?.message ?? (typeof err === 'string' ? err : 'Request failed'); return null; }); diff --git a/src/lib/components/admin/Settings/Interface.svelte b/src/lib/components/admin/Settings/Interface.svelte index 204415910c..8bec5c8606 100644 --- a/src/lib/components/admin/Settings/Interface.svelte +++ b/src/lib/components/admin/Settings/Interface.svelte @@ -114,6 +114,42 @@ }} >
+
+
+ {$i18n.t('Interface Defaults')} +
+ +
+ +
+ {$i18n.t( + "Configure default interface settings that apply to all users who haven't customized their own." + )} +
+ +
+ + + +
+
+
{$i18n.t('Tasks')}
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 480f01f2c4..165465eafa 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -929,14 +929,15 @@ }) ]); - // Admin defaults are base, user settings override. - const effectiveSettings = deepMerge(adminDefaults ?? {}, userSettings?.ui ?? {}); - - if (userSettings) { - settings.set(effectiveSettings); - } else { - settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}')); - } + // Admin defaults are the base layer; whatever the user has + // actually customized overrides them. For brand-new users + // getUserSettings() returns null — the whole point of admin + // defaults is that THOSE users get them, so always merge + // against adminDefaults instead of skipping straight to + // localStorage when userSettings is falsy. + const localStorageSettings = JSON.parse(localStorage.getItem('settings') ?? '{}'); + const userUI = userSettings?.ui ?? localStorageSettings ?? {}; + settings.set(deepMerge(adminDefaults ?? {}, userUI)); setTextScale($settings?.textScale ?? 1); // Set up the token expiry check