}
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);
EXCLUDE_REASONING_FROM_CONTEXT: 'excludeReasoningFromContext',
SHOW_RAW_OUTPUT_SWITCH: 'showRawOutputSwitch',
// PY_INTERPRETER_ENABLED: 'pyInterpreterEnabled',
- CUSTOM: 'custom'
+ CUSTOM_JSON: 'customJson',
+ CUSTOM_CSS: 'customCss'
} as const;
}
},
{
- 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
+ }
}
]
},
// 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 = {
apiOptions.backend_sampling = currentConfig.backend_sampling;
- if (currentConfig.custom) apiOptions.custom = currentConfig.custom;
+ if (currentConfig.customJson) apiOptions.custom = currentConfig.customJson;
return apiOptions;
}
}
get customTools(): OpenAIToolDefinition[] {
- const raw = config().custom;
+ const raw = config().customJson;
if (!raw || typeof raw !== 'string') return [];
try {
// 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;
}
});
+ // 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 />