]> git.djapps.eu Git - pkg/ggml/sources/llama.cpp/commitdiff
webui: add custom CSS injection via config (#23904)
authorPascal <redacted>
Sat, 30 May 2026 21:49:31 +0000 (23:49 +0200)
committerGitHub <redacted>
Sat, 30 May 2026 21:49:31 +0000 (23:49 +0200)
* webui: add custom CSS injection via config

register a customCSS setting in the Developer section under Custom JSON,
syncable so it rides the existing ui-config pass through. inject the value
into a single style element in the head, reactive on the setting. lets an
operator theme a prebuilt binary through --ui-config without rebuilding,
and lets a user set it from the settings panel.

* ui: address review from @niutech and @allozaur, rename custom JSON key and CSS field

* ui: address review from @allozaur, move custom CSS injection to a style tag in svelte:head

* ui: inject custom CSS through a svelte action instead of a bound element

move the textContent write into a use: action on the head style node.
the action is the idiomatic way to touch a node, so the no-dom-manipulating
lint rule is satisfied without a disable. value stays text through
textContent, never parsed as HTML.

* Update tools/ui/src/lib/constants/settings-keys.ts

Co-authored-by: Aleksander Grygier <redacted>
* ui: address review from @allozaur, rename custom config key to customJson with migration

rename the custom config key to customJson across the type, the chat
request builder, the settings save check and the custom tools reader,
keeping the custom API param name unchanged. add a non destructive
migration that copies the legacy custom key to customJson at startup.
only render the head style tag when custom CSS is set.

---------

Co-authored-by: Aleksander Grygier <redacted>
tools/ui/src/lib/components/app/settings/SettingsChat/SettingsChat.svelte
tools/ui/src/lib/constants/settings-keys.ts
tools/ui/src/lib/constants/settings-registry.ts
tools/ui/src/lib/services/migration.service.ts
tools/ui/src/lib/stores/chat.svelte.ts
tools/ui/src/lib/stores/tools.svelte.ts
tools/ui/src/lib/types/settings.d.ts
tools/ui/src/routes/+layout.svelte

index d017fe204694e8fe84ee20043b3cba7f27d46d52..69a120b7cb680b14445fbe6e48dac2171675da26 100644 (file)
        }
 
        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);
index 53243992fa2f3c5469957f83ba83344b39dc1c5b..7cdd2db7c4d78df547fef3f61fe6ad4ac2ad1233 100644 (file)
@@ -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;
index efef18fdeb685753e7f9ab0ed047d0d00fc21cf9..20ac33c85f803c1f2f652c1db0cd25eb2cbc14f7 100644 (file)
@@ -659,12 +659,24 @@ const SETTINGS_REGISTRY: Record<string, SettingsSectionEntry> = {
                                }
                        },
                        {
-                               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
+                               }
                        }
                ]
        },
index 35d47070acfb4b515625119bfd9ffc75f11dfa5d..20dfa9b19fa70356d4a4af09894c4f0b12438387 100644 (file)
@@ -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<void> {
+               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 = {
index 5b264482602301f1e7a1fa33e574560c9bc67240..f2f13f25dc1c14cf26cdf6193bc4baecc7743e1a 100644 (file)
@@ -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;
        }
index 5404a7a46fecad5c23db70c452bfc219c551e92f..3ac44aedf709f3415c04d0a08986f7485c8f3a90 100644 (file)
@@ -57,7 +57,7 @@ class ToolsStore {
        }
 
        get customTools(): OpenAIToolDefinition[] {
-               const raw = config().custom;
+               const raw = config().customJson;
                if (!raw || typeof raw !== 'string') return [];
 
                try {
index 65096db344949d52c9b5c32ad2fc09a535a33cce..03818091a1d7ab545dc0bb9a91315a8cf5276887 100644 (file)
@@ -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;
index 2f1f5249722fdf1868eee88b35c16f2d8df1a1ed..be474109ad92475317d2d4e395aaa1c54cc3656a 100644 (file)
                }
        });
 
+       // 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;
        });
 </script>
 
+<svelte:head>
+       {#if config().customCss}
+               <style use:customCss></style>
+       {/if}
+</svelte:head>
+
 <Tooltip.Provider delayDuration={TOOLTIP_DELAY_DURATION}>
        <ModeWatcher />
        <Toaster richColors />