diff --git a/tools/ui/src/lib/components/app/settings/SettingsChat/SettingsChat.svelte b/tools/ui/src/lib/components/app/settings/SettingsChat/SettingsChat.svelte index d017fe20469..69a120b7cb6 100644 --- a/tools/ui/src/lib/components/app/settings/SettingsChat/SettingsChat.svelte +++ b/tools/ui/src/lib/components/app/settings/SettingsChat/SettingsChat.svelte @@ -75,9 +75,13 @@ } function handleSave() { - if (localConfig.custom && typeof localConfig.custom === 'string' && localConfig.custom.trim()) { + if ( + localConfig.customJson && + typeof localConfig.customJson === 'string' && + localConfig.customJson.trim() + ) { try { - JSON.parse(localConfig.custom); + JSON.parse(localConfig.customJson); } catch (error) { alert('Invalid JSON in custom parameters. Please check the format and try again.'); console.error(error); diff --git a/tools/ui/src/lib/constants/settings-keys.ts b/tools/ui/src/lib/constants/settings-keys.ts index 53243992fa2..7cdd2db7c4d 100644 --- a/tools/ui/src/lib/constants/settings-keys.ts +++ b/tools/ui/src/lib/constants/settings-keys.ts @@ -66,5 +66,6 @@ export const SETTINGS_KEYS = { EXCLUDE_REASONING_FROM_CONTEXT: 'excludeReasoningFromContext', SHOW_RAW_OUTPUT_SWITCH: 'showRawOutputSwitch', // PY_INTERPRETER_ENABLED: 'pyInterpreterEnabled', - CUSTOM: 'custom' + CUSTOM_JSON: 'customJson', + CUSTOM_CSS: 'customCss' } as const; diff --git a/tools/ui/src/lib/constants/settings-registry.ts b/tools/ui/src/lib/constants/settings-registry.ts index efef18fdeb6..20ac33c85f8 100644 --- a/tools/ui/src/lib/constants/settings-registry.ts +++ b/tools/ui/src/lib/constants/settings-registry.ts @@ -659,12 +659,24 @@ const SETTINGS_REGISTRY: Record = { } }, { - key: SETTINGS_KEYS.CUSTOM, + key: SETTINGS_KEYS.CUSTOM_JSON, label: 'Custom JSON', help: 'Custom JSON parameters to send to the API. Must be valid JSON format.', defaultValue: '', type: SettingsFieldType.TEXTAREA, section: SETTINGS_SECTION_SLUGS.DEVELOPER + }, + { + key: SETTINGS_KEYS.CUSTOM_CSS, + label: 'Custom CSS', + help: 'CSS injected into the page at runtime. Set it here, or ship it server side via the --ui-config customCss field.', + defaultValue: '', + type: SettingsFieldType.TEXTAREA, + section: SETTINGS_SECTION_SLUGS.DEVELOPER, + sync: { + serverKey: SETTINGS_KEYS.CUSTOM_CSS, + paramType: SyncableParameterType.STRING + } } ] }, diff --git a/tools/ui/src/lib/services/migration.service.ts b/tools/ui/src/lib/services/migration.service.ts index 35d47070acf..20dfa9b19fa 100644 --- a/tools/ui/src/lib/services/migration.service.ts +++ b/tools/ui/src/lib/services/migration.service.ts @@ -470,11 +470,36 @@ const themeMigration: Migration = { // Migration Registry & Runner +const CUSTOM_JSON_MIGRATION_ID = 'custom-json-key-v1'; + +const customJsonKeyMigration: Migration = { + id: CUSTOM_JSON_MIGRATION_ID, + description: 'Copy legacy custom config key to customJson (non-destructive)', + + async run(): Promise { + const configRaw = localStorage.getItem(CONFIG_LOCALSTORAGE_KEY); + if (configRaw === null) return; + + const config = JSON.parse(configRaw); + + if (!('custom' in config)) return; + if (SETTINGS_KEYS.CUSTOM_JSON in config) return; + + config[SETTINGS_KEYS.CUSTOM_JSON] = config.custom; + localStorage.setItem(CONFIG_LOCALSTORAGE_KEY, JSON.stringify(config)); + + // Non-destructive: keep the legacy custom key for downgrade compatibility + if (import.meta.env.DEV && import.meta.env.VITE_DEBUG) + console.log(`[Migration] Custom JSON: copied custom to customJson (preserved old key)`); + } +}; + const migrations: Migration[] = [ localStorageMigration, idxdbMigration, legacyMessageMigration, - themeMigration + themeMigration, + customJsonKeyMigration ]; export const MigrationService = { diff --git a/tools/ui/src/lib/stores/chat.svelte.ts b/tools/ui/src/lib/stores/chat.svelte.ts index 5b264482602..f2f13f25dc1 100644 --- a/tools/ui/src/lib/stores/chat.svelte.ts +++ b/tools/ui/src/lib/stores/chat.svelte.ts @@ -1869,7 +1869,7 @@ class ChatStore { apiOptions.backend_sampling = currentConfig.backend_sampling; - if (currentConfig.custom) apiOptions.custom = currentConfig.custom; + if (currentConfig.customJson) apiOptions.custom = currentConfig.customJson; return apiOptions; } diff --git a/tools/ui/src/lib/stores/tools.svelte.ts b/tools/ui/src/lib/stores/tools.svelte.ts index 5404a7a46fe..3ac44aedf70 100644 --- a/tools/ui/src/lib/stores/tools.svelte.ts +++ b/tools/ui/src/lib/stores/tools.svelte.ts @@ -57,7 +57,7 @@ class ToolsStore { } get customTools(): OpenAIToolDefinition[] { - const raw = config().custom; + const raw = config().customJson; if (!raw || typeof raw !== 'string') return []; try { diff --git a/tools/ui/src/lib/types/settings.d.ts b/tools/ui/src/lib/types/settings.d.ts index 65096db3449..03818091a1d 100644 --- a/tools/ui/src/lib/types/settings.d.ts +++ b/tools/ui/src/lib/types/settings.d.ts @@ -90,8 +90,8 @@ export interface SettingsChatServiceOptions { // Sampler configuration samplers?: string | string[]; backend_sampling?: boolean; - // Custom parameters - custom?: string; + // Custom JSON parameters + customJson?: string; timings_per_token?: boolean; // Continuation control (vLLM compat), opt in to the explicit continue final message flag continueFinalMessage?: boolean; diff --git a/tools/ui/src/routes/+layout.svelte b/tools/ui/src/routes/+layout.svelte index 2f1f5249722..be474109ad9 100644 --- a/tools/ui/src/routes/+layout.svelte +++ b/tools/ui/src/routes/+layout.svelte @@ -169,6 +169,14 @@ } }); + // Inject custom CSS at runtime through an action on the head style node + // textContent keeps the value as text, never parsed as HTML + function customCss(node: HTMLStyleElement) { + $effect(() => { + node.textContent = (config().customCss as string | undefined) ?? ''; + }); + } + // Fetch router models when in router mode (for status and modalities) // Wait for models to be loaded first, run only once let routerModelsFetched = false; @@ -227,6 +235,12 @@ }); + + {#if config().customCss} + + {/if} + +