import { ChatSettingsFooter, ChatSettingsFields } from '$lib/components/app';
import * as Dialog from '$lib/components/ui/dialog';
import { ScrollArea } from '$lib/components/ui/scroll-area';
- import { SETTING_CONFIG_DEFAULT } from '$lib/constants/settings-config';
- import { config, updateMultipleConfig, resetConfig } from '$lib/stores/settings.svelte';
+ import { config, updateMultipleConfig } from '$lib/stores/settings.svelte';
import { setMode } from 'mode-watcher';
import type { Component } from 'svelte';
}
function handleReset() {
- resetConfig();
+ localConfig = { ...config() };
- localConfig = { ...SETTING_CONFIG_DEFAULT };
-
- setMode(SETTING_CONFIG_DEFAULT.theme as 'light' | 'dark' | 'system');
- originalTheme = SETTING_CONFIG_DEFAULT.theme as string;
+ setMode(localConfig.theme as 'light' | 'dark' | 'system');
+ originalTheme = localConfig.theme as string;
}
function handleSave() {
- // Validate custom JSON if provided
if (localConfig.custom && typeof localConfig.custom === 'string' && localConfig.custom.trim()) {
try {
JSON.parse(localConfig.custom);
<script lang="ts">
+ import { RotateCcw } from '@lucide/svelte';
import { Checkbox } from '$lib/components/ui/checkbox';
import { Input } from '$lib/components/ui/input';
import Label from '$lib/components/ui/label/label.svelte';
import { Textarea } from '$lib/components/ui/textarea';
import { SETTING_CONFIG_DEFAULT, SETTING_CONFIG_INFO } from '$lib/constants/settings-config';
import { supportsVision } from '$lib/stores/server.svelte';
+ import { getParameterInfo, resetParameterToServerDefault } from '$lib/stores/settings.svelte';
+ import { ParameterSyncService } from '$lib/services/parameter-sync';
+ import ParameterSourceIndicator from './ParameterSourceIndicator.svelte';
import type { Component } from 'svelte';
interface Props {
}
let { fields, localConfig, onConfigChange, onThemeChange }: Props = $props();
+
+ // Helper function to get parameter source info for syncable parameters
+ function getParameterSourceInfo(key: string) {
+ if (!ParameterSyncService.canSyncParameter(key)) {
+ return null;
+ }
+
+ return getParameterInfo(key);
+ }
</script>
{#each fields as field (field.key)}
<div class="space-y-2">
{#if field.type === 'input'}
- <Label for={field.key} class="block text-sm font-medium">
- {field.label}
- </Label>
+ {@const paramInfo = getParameterSourceInfo(field.key)}
+ {@const currentValue = String(localConfig[field.key] ?? '')}
+ {@const propsDefault = paramInfo?.serverDefault}
+ {@const isCustomRealTime = (() => {
+ if (!paramInfo || propsDefault === undefined) return false;
- <Input
- id={field.key}
- value={String(localConfig[field.key] ?? '')}
- onchange={(e) => onConfigChange(field.key, e.currentTarget.value)}
- placeholder={`Default: ${SETTING_CONFIG_DEFAULT[field.key] ?? 'none'}`}
- class="w-full md:max-w-md"
- />
+ // Apply same rounding logic for real-time comparison
+ const inputValue = currentValue;
+ const numericInput = parseFloat(inputValue);
+ const normalizedInput = !isNaN(numericInput)
+ ? Math.round(numericInput * 1000000) / 1000000
+ : inputValue;
+ const normalizedDefault =
+ typeof propsDefault === 'number'
+ ? Math.round(propsDefault * 1000000) / 1000000
+ : propsDefault;
+
+ return normalizedInput !== normalizedDefault;
+ })()}
+
+ <div class="flex items-center gap-2">
+ <Label for={field.key} class="text-sm font-medium">
+ {field.label}
+ </Label>
+ {#if isCustomRealTime}
+ <ParameterSourceIndicator />
+ {/if}
+ </div>
+
+ <div class="relative w-full md:max-w-md">
+ <Input
+ id={field.key}
+ value={currentValue}
+ oninput={(e) => {
+ // Update local config immediately for real-time badge feedback
+ onConfigChange(field.key, e.currentTarget.value);
+ }}
+ placeholder={`Default: ${SETTING_CONFIG_DEFAULT[field.key] ?? 'none'}`}
+ class="w-full {isCustomRealTime ? 'pr-8' : ''}"
+ />
+ {#if isCustomRealTime}
+ <button
+ type="button"
+ onclick={() => {
+ resetParameterToServerDefault(field.key);
+ // Trigger UI update by calling onConfigChange with the default value
+ const defaultValue = propsDefault ?? SETTING_CONFIG_DEFAULT[field.key];
+ onConfigChange(field.key, String(defaultValue));
+ }}
+ class="absolute top-1/2 right-2 inline-flex h-5 w-5 -translate-y-1/2 items-center justify-center rounded transition-colors hover:bg-muted"
+ aria-label="Reset to default"
+ title="Reset to default"
+ >
+ <RotateCcw class="h-3 w-3" />
+ </button>
+ {/if}
+ </div>
{#if field.help || SETTING_CONFIG_INFO[field.key]}
<p class="mt-1 text-xs text-muted-foreground">
{field.help || SETTING_CONFIG_INFO[field.key]}
(opt: { value: string; label: string; icon?: Component }) =>
opt.value === localConfig[field.key]
)}
+ {@const paramInfo = getParameterSourceInfo(field.key)}
+ {@const currentValue = localConfig[field.key]}
+ {@const propsDefault = paramInfo?.serverDefault}
+ {@const isCustomRealTime = (() => {
+ if (!paramInfo || propsDefault === undefined) return false;
- <Label for={field.key} class="block text-sm font-medium">
- {field.label}
- </Label>
+ // For select fields, do direct comparison (no rounding needed)
+ return currentValue !== propsDefault;
+ })()}
+
+ <div class="flex items-center gap-2">
+ <Label for={field.key} class="text-sm font-medium">
+ {field.label}
+ </Label>
+ {#if isCustomRealTime}
+ <ParameterSourceIndicator />
+ {/if}
+ </div>
<Select.Root
type="single"
- value={localConfig[field.key]}
+ value={currentValue}
onValueChange={(value) => {
if (field.key === 'theme' && value && onThemeChange) {
onThemeChange(value);
}
}}
>
- <Select.Trigger class="w-full md:w-auto md:max-w-md">
- <div class="flex items-center gap-2">
- {#if selectedOption?.icon}
- {@const IconComponent = selectedOption.icon}
- <IconComponent class="h-4 w-4" />
- {/if}
-
- {selectedOption?.label || `Select ${field.label.toLowerCase()}`}
- </div>
- </Select.Trigger>
+ <div class="relative w-full md:w-auto md:max-w-md">
+ <Select.Trigger class="w-full">
+ <div class="flex items-center gap-2">
+ {#if selectedOption?.icon}
+ {@const IconComponent = selectedOption.icon}
+ <IconComponent class="h-4 w-4" />
+ {/if}
+
+ {selectedOption?.label || `Select ${field.label.toLowerCase()}`}
+ </div>
+ </Select.Trigger>
+ {#if isCustomRealTime}
+ <button
+ type="button"
+ onclick={() => {
+ resetParameterToServerDefault(field.key);
+ // Trigger UI update by calling onConfigChange with the default value
+ const defaultValue = propsDefault ?? SETTING_CONFIG_DEFAULT[field.key];
+ onConfigChange(field.key, String(defaultValue));
+ }}
+ class="absolute top-1/2 right-8 inline-flex h-5 w-5 -translate-y-1/2 items-center justify-center rounded transition-colors hover:bg-muted"
+ aria-label="Reset to default"
+ title="Reset to default"
+ >
+ <RotateCcw class="h-3 w-3" />
+ </button>
+ {/if}
+ </div>
<Select.Content>
{#if field.options}
{#each field.options as option (option.value)}
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import * as AlertDialog from '$lib/components/ui/alert-dialog';
+ import { forceSyncWithServerDefaults } from '$lib/stores/settings.svelte';
+ import { RotateCcw } from '@lucide/svelte';
interface Props {
onReset?: () => void;
}
function handleConfirmReset() {
+ forceSyncWithServerDefaults();
onReset?.();
+
showResetDialog = false;
}
</script>
<div class="flex justify-between border-t border-border/30 p-6">
- <Button variant="outline" onclick={handleResetClick}>Reset to default</Button>
+ <div class="flex gap-2">
+ <Button variant="outline" onclick={handleResetClick}>
+ <RotateCcw class="h-3 w-3" />
+
+ Reset to default
+ </Button>
+ </div>
<Button onclick={handleSave}>Save settings</Button>
</div>
<AlertDialog.Header>
<AlertDialog.Title>Reset Settings to Default</AlertDialog.Title>
<AlertDialog.Description>
- Are you sure you want to reset all settings to their default values? This action cannot be
- undone and will permanently remove all your custom configurations.
+ Are you sure you want to reset all settings to their default values? This will reset all
+ parameters to the values provided by the server's /props endpoint and remove all your custom
+ configurations.
</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Footer>
--- /dev/null
+<script lang="ts">
+ import { Wrench } from '@lucide/svelte';
+ import { Badge } from '$lib/components/ui/badge';
+
+ interface Props {
+ class?: string;
+ }
+
+ let { class: className = '' }: Props = $props();
+</script>
+
+<Badge
+ variant="secondary"
+ class="h-5 bg-orange-100 px-1.5 py-0.5 text-xs text-orange-800 dark:bg-orange-900 dark:text-orange-200 {className}"
+>
+ <Wrench class="mr-1 h-3 w-3" />
+ Custom
+</Badge>
export { default as ChatSettingsDialog } from './chat/ChatSettings/ChatSettingsDialog.svelte';
export { default as ChatSettingsFooter } from './chat/ChatSettings/ChatSettingsFooter.svelte';
export { default as ChatSettingsFields } from './chat/ChatSettings/ChatSettingsFields.svelte';
+export { default as ParameterSourceIndicator } from './chat/ChatSettings/ParameterSourceIndicator.svelte';
export { default as ChatSidebar } from './chat/ChatSidebar/ChatSidebar.svelte';
export { default as ChatSidebarConversationItem } from './chat/ChatSidebar/ChatSidebarConversationItem.svelte';
--- /dev/null
+export const PRECISION_MULTIPLIER = 1000000;
+export const PRECISION_DECIMAL_PLACES = 6;
--- /dev/null
+import { describe, it, expect } from 'vitest';
+import { ParameterSyncService } from './parameter-sync';
+import type { ApiLlamaCppServerProps } from '$lib/types/api';
+
+describe('ParameterSyncService', () => {
+ describe('roundFloatingPoint', () => {
+ it('should fix JavaScript floating-point precision issues', () => {
+ // Test the specific values from the screenshot
+ const mockServerParams = {
+ top_p: 0.949999988079071,
+ min_p: 0.009999999776482582,
+ temperature: 0.800000011920929,
+ top_k: 40,
+ samplers: ['top_k', 'typ_p', 'top_p', 'min_p', 'temperature']
+ };
+
+ const result = ParameterSyncService.extractServerDefaults({
+ ...mockServerParams,
+ // Add other required fields to match the API type
+ n_predict: 512,
+ seed: -1,
+ dynatemp_range: 0.0,
+ dynatemp_exponent: 1.0,
+ xtc_probability: 0.0,
+ xtc_threshold: 0.1,
+ typ_p: 1.0,
+ repeat_last_n: 64,
+ repeat_penalty: 1.0,
+ presence_penalty: 0.0,
+ frequency_penalty: 0.0,
+ dry_multiplier: 0.0,
+ dry_base: 1.75,
+ dry_allowed_length: 2,
+ dry_penalty_last_n: -1,
+ mirostat: 0,
+ mirostat_tau: 5.0,
+ mirostat_eta: 0.1,
+ stop: [],
+ max_tokens: -1,
+ n_keep: 0,
+ n_discard: 0,
+ ignore_eos: false,
+ stream: true,
+ logit_bias: [],
+ n_probs: 0,
+ min_keep: 0,
+ grammar: '',
+ grammar_lazy: false,
+ grammar_triggers: [],
+ preserved_tokens: [],
+ chat_format: '',
+ reasoning_format: '',
+ reasoning_in_content: false,
+ thinking_forced_open: false,
+ 'speculative.n_max': 0,
+ 'speculative.n_min': 0,
+ 'speculative.p_min': 0.0,
+ timings_per_token: false,
+ post_sampling_probs: false,
+ lora: [],
+ top_n_sigma: 0.0,
+ dry_sequence_breakers: []
+ } as ApiLlamaCppServerProps['default_generation_settings']['params']);
+
+ // Check that the problematic floating-point values are rounded correctly
+ expect(result.top_p).toBe(0.95);
+ expect(result.min_p).toBe(0.01);
+ expect(result.temperature).toBe(0.8);
+ expect(result.top_k).toBe(40); // Integer should remain unchanged
+ expect(result.samplers).toBe('top_k;typ_p;top_p;min_p;temperature');
+ });
+
+ it('should preserve non-numeric values', () => {
+ const mockServerParams = {
+ samplers: ['top_k', 'temperature'],
+ max_tokens: -1,
+ temperature: 0.7
+ };
+
+ const result = ParameterSyncService.extractServerDefaults({
+ ...mockServerParams,
+ // Minimal required fields
+ n_predict: 512,
+ seed: -1,
+ dynatemp_range: 0.0,
+ dynatemp_exponent: 1.0,
+ top_k: 40,
+ top_p: 0.95,
+ min_p: 0.05,
+ xtc_probability: 0.0,
+ xtc_threshold: 0.1,
+ typ_p: 1.0,
+ repeat_last_n: 64,
+ repeat_penalty: 1.0,
+ presence_penalty: 0.0,
+ frequency_penalty: 0.0,
+ dry_multiplier: 0.0,
+ dry_base: 1.75,
+ dry_allowed_length: 2,
+ dry_penalty_last_n: -1,
+ mirostat: 0,
+ mirostat_tau: 5.0,
+ mirostat_eta: 0.1,
+ stop: [],
+ n_keep: 0,
+ n_discard: 0,
+ ignore_eos: false,
+ stream: true,
+ logit_bias: [],
+ n_probs: 0,
+ min_keep: 0,
+ grammar: '',
+ grammar_lazy: false,
+ grammar_triggers: [],
+ preserved_tokens: [],
+ chat_format: '',
+ reasoning_format: '',
+ reasoning_in_content: false,
+ thinking_forced_open: false,
+ 'speculative.n_max': 0,
+ 'speculative.n_min': 0,
+ 'speculative.p_min': 0.0,
+ timings_per_token: false,
+ post_sampling_probs: false,
+ lora: [],
+ top_n_sigma: 0.0,
+ dry_sequence_breakers: []
+ } as ApiLlamaCppServerProps['default_generation_settings']['params']);
+
+ expect(result.samplers).toBe('top_k;temperature');
+ expect(result.max_tokens).toBe(-1);
+ expect(result.temperature).toBe(0.7);
+ });
+ });
+});
--- /dev/null
+/**
+ * ParameterSyncService - Handles synchronization between server defaults and user settings
+ *
+ * This service manages the complex logic of merging server-provided default parameters
+ * with user-configured overrides, ensuring the UI reflects the actual server state
+ * while preserving user customizations.
+ *
+ * **Key Responsibilities:**
+ * - Extract syncable parameters from server props
+ * - Merge server defaults with user overrides
+ * - Track parameter sources (server, user, default)
+ * - Provide sync utilities for settings store integration
+ */
+
+import type { ApiLlamaCppServerProps } from '$lib/types/api';
+import { normalizeFloatingPoint } from '$lib/utils/precision';
+
+export type ParameterSource = 'default' | 'custom';
+export type ParameterValue = string | number | boolean;
+export type ParameterRecord = Record<string, ParameterValue>;
+
+export interface ParameterInfo {
+ value: string | number | boolean;
+ source: ParameterSource;
+ serverDefault?: string | number | boolean;
+ userOverride?: string | number | boolean;
+}
+
+export interface SyncableParameter {
+ key: string;
+ serverKey: string;
+ type: 'number' | 'string' | 'boolean';
+ canSync: boolean;
+}
+
+/**
+ * Mapping of webui setting keys to server parameter keys
+ * Only parameters that should be synced from server are included
+ */
+export const SYNCABLE_PARAMETERS: SyncableParameter[] = [
+ { key: 'temperature', serverKey: 'temperature', type: 'number', canSync: true },
+ { key: 'top_k', serverKey: 'top_k', type: 'number', canSync: true },
+ { key: 'top_p', serverKey: 'top_p', type: 'number', canSync: true },
+ { key: 'min_p', serverKey: 'min_p', type: 'number', canSync: true },
+ { key: 'dynatemp_range', serverKey: 'dynatemp_range', type: 'number', canSync: true },
+ { key: 'dynatemp_exponent', serverKey: 'dynatemp_exponent', type: 'number', canSync: true },
+ { key: 'xtc_probability', serverKey: 'xtc_probability', type: 'number', canSync: true },
+ { key: 'xtc_threshold', serverKey: 'xtc_threshold', type: 'number', canSync: true },
+ { key: 'typ_p', serverKey: 'typ_p', type: 'number', canSync: true },
+ { key: 'repeat_last_n', serverKey: 'repeat_last_n', type: 'number', canSync: true },
+ { key: 'repeat_penalty', serverKey: 'repeat_penalty', type: 'number', canSync: true },
+ { key: 'presence_penalty', serverKey: 'presence_penalty', type: 'number', canSync: true },
+ { key: 'frequency_penalty', serverKey: 'frequency_penalty', type: 'number', canSync: true },
+ { key: 'dry_multiplier', serverKey: 'dry_multiplier', type: 'number', canSync: true },
+ { key: 'dry_base', serverKey: 'dry_base', type: 'number', canSync: true },
+ { key: 'dry_allowed_length', serverKey: 'dry_allowed_length', type: 'number', canSync: true },
+ { key: 'dry_penalty_last_n', serverKey: 'dry_penalty_last_n', type: 'number', canSync: true },
+ { key: 'max_tokens', serverKey: 'max_tokens', type: 'number', canSync: true },
+ { key: 'samplers', serverKey: 'samplers', type: 'string', canSync: true }
+];
+
+export class ParameterSyncService {
+ /**
+ * Round floating-point numbers to avoid JavaScript precision issues
+ */
+ private static roundFloatingPoint(value: ParameterValue): ParameterValue {
+ return normalizeFloatingPoint(value) as ParameterValue;
+ }
+
+ /**
+ * Extract server default parameters that can be synced
+ */
+ static extractServerDefaults(
+ serverParams: ApiLlamaCppServerProps['default_generation_settings']['params'] | null
+ ): ParameterRecord {
+ if (!serverParams) return {};
+
+ const extracted: ParameterRecord = {};
+
+ for (const param of SYNCABLE_PARAMETERS) {
+ if (param.canSync && param.serverKey in serverParams) {
+ const value = (serverParams as unknown as Record<string, ParameterValue>)[param.serverKey];
+ if (value !== undefined) {
+ // Apply precision rounding to avoid JavaScript floating-point issues
+ extracted[param.key] = this.roundFloatingPoint(value);
+ }
+ }
+ }
+
+ // Handle samplers array conversion to string
+ if (serverParams.samplers && Array.isArray(serverParams.samplers)) {
+ extracted.samplers = serverParams.samplers.join(';');
+ }
+
+ return extracted;
+ }
+
+ /**
+ * Merge server defaults with current user settings
+ * Returns updated settings that respect user overrides while using server defaults
+ */
+ static mergeWithServerDefaults(
+ currentSettings: ParameterRecord,
+ serverDefaults: ParameterRecord,
+ userOverrides: Set<string> = new Set()
+ ): ParameterRecord {
+ const merged = { ...currentSettings };
+
+ for (const [key, serverValue] of Object.entries(serverDefaults)) {
+ // Only update if user hasn't explicitly overridden this parameter
+ if (!userOverrides.has(key)) {
+ merged[key] = this.roundFloatingPoint(serverValue);
+ }
+ }
+
+ return merged;
+ }
+
+ /**
+ * Get parameter information including source and values
+ */
+ static getParameterInfo(
+ key: string,
+ currentValue: ParameterValue,
+ propsDefaults: ParameterRecord,
+ userOverrides: Set<string>
+ ): ParameterInfo {
+ const hasPropsDefault = propsDefaults[key] !== undefined;
+ const isUserOverride = userOverrides.has(key);
+
+ // Simple logic: either using default (from props) or custom (user override)
+ const source: ParameterSource = isUserOverride ? 'custom' : 'default';
+
+ return {
+ value: currentValue,
+ source,
+ serverDefault: hasPropsDefault ? propsDefaults[key] : undefined, // Keep same field name for compatibility
+ userOverride: isUserOverride ? currentValue : undefined
+ };
+ }
+
+ /**
+ * Check if a parameter can be synced from server
+ */
+ static canSyncParameter(key: string): boolean {
+ return SYNCABLE_PARAMETERS.some((param) => param.key === key && param.canSync);
+ }
+
+ /**
+ * Get all syncable parameter keys
+ */
+ static getSyncableParameterKeys(): string[] {
+ return SYNCABLE_PARAMETERS.filter((param) => param.canSync).map((param) => param.key);
+ }
+
+ /**
+ * Validate server parameter value
+ */
+ static validateServerParameter(key: string, value: ParameterValue): boolean {
+ const param = SYNCABLE_PARAMETERS.find((p) => p.key === key);
+ if (!param) return false;
+
+ switch (param.type) {
+ case 'number':
+ return typeof value === 'number' && !isNaN(value);
+ case 'string':
+ return typeof value === 'string';
+ case 'boolean':
+ return typeof value === 'boolean';
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Create a diff between current settings and server defaults
+ */
+ static createParameterDiff(
+ currentSettings: ParameterRecord,
+ serverDefaults: ParameterRecord
+ ): Record<string, { current: ParameterValue; server: ParameterValue; differs: boolean }> {
+ const diff: Record<
+ string,
+ { current: ParameterValue; server: ParameterValue; differs: boolean }
+ > = {};
+
+ for (const key of this.getSyncableParameterKeys()) {
+ const currentValue = currentSettings[key];
+ const serverValue = serverDefaults[key];
+
+ if (serverValue !== undefined) {
+ diff[key] = {
+ current: currentValue,
+ server: serverValue,
+ differs: currentValue !== serverValue
+ };
+ }
+ }
+
+ return diff;
+ }
+}
return this._slotsEndpointAvailable;
}
+ get serverDefaultParams():
+ | ApiLlamaCppServerProps['default_generation_settings']['params']
+ | null {
+ return this._serverProps?.default_generation_settings?.params || null;
+ }
+
/**
* Check if slots endpoint is available based on server properties and endpoint support
*/
export const supportsVision = () => serverStore.supportsVision;
export const supportsAudio = () => serverStore.supportsAudio;
export const slotsEndpointAvailable = () => serverStore.slotsEndpointAvailable;
+export const serverDefaultParams = () => serverStore.serverDefaultParams;
import { browser } from '$app/environment';
import { SETTING_CONFIG_DEFAULT } from '$lib/constants/settings-config';
+import { normalizeFloatingPoint } from '$lib/utils/precision';
+import { ParameterSyncService } from '$lib/services/parameter-sync';
+import { serverStore } from '$lib/stores/server.svelte';
+import { setConfigValue, getConfigValue, configToParameterRecord } from '$lib/utils/config-helpers';
class SettingsStore {
config = $state<SettingsConfigType>({ ...SETTING_CONFIG_DEFAULT });
theme = $state<string>('auto');
isInitialized = $state(false);
+ userOverrides = $state<Set<string>>(new Set());
+
+ /**
+ * Helper method to get server defaults with null safety
+ * Centralizes the pattern of getting and extracting server defaults
+ */
+ private getServerDefaults(): Record<string, string | number | boolean> {
+ const serverParams = serverStore.serverDefaultParams;
+ return serverParams ? ParameterSyncService.extractServerDefaults(serverParams) : {};
+ }
constructor() {
if (browser) {
try {
const savedVal = JSON.parse(localStorage.getItem('config') || '{}');
+
// Merge with defaults to prevent breaking changes
this.config = {
...SETTING_CONFIG_DEFAULT,
...savedVal
};
+
+ // Load user overrides
+ const savedOverrides = JSON.parse(localStorage.getItem('userOverrides') || '[]');
+ this.userOverrides = new Set(savedOverrides);
} catch (error) {
console.warn('Failed to parse config from localStorage, using defaults:', error);
this.config = { ...SETTING_CONFIG_DEFAULT };
+ this.userOverrides = new Set();
}
}
this.theme = localStorage.getItem('theme') || 'auto';
}
-
/**
* Update a specific configuration setting
* @param key - The configuration key to update
* @param value - The new value for the configuration key
*/
- updateConfig<K extends keyof SettingsConfigType>(key: K, value: SettingsConfigType[K]) {
+ updateConfig<K extends keyof SettingsConfigType>(key: K, value: SettingsConfigType[K]): void {
this.config[key] = value;
+
+ if (ParameterSyncService.canSyncParameter(key as string)) {
+ const propsDefaults = this.getServerDefaults();
+ const propsDefault = propsDefaults[key as string];
+
+ if (propsDefault !== undefined) {
+ const normalizedValue = normalizeFloatingPoint(value);
+ const normalizedDefault = normalizeFloatingPoint(propsDefault);
+
+ if (normalizedValue === normalizedDefault) {
+ this.userOverrides.delete(key as string);
+ } else {
+ this.userOverrides.add(key as string);
+ }
+ }
+ }
+
this.saveConfig();
}
*/
updateMultipleConfig(updates: Partial<SettingsConfigType>) {
Object.assign(this.config, updates);
+
+ const propsDefaults = this.getServerDefaults();
+
+ for (const [key, value] of Object.entries(updates)) {
+ if (ParameterSyncService.canSyncParameter(key)) {
+ const propsDefault = propsDefaults[key];
+
+ if (propsDefault !== undefined) {
+ const normalizedValue = normalizeFloatingPoint(value);
+ const normalizedDefault = normalizeFloatingPoint(propsDefault);
+
+ if (normalizedValue === normalizedDefault) {
+ this.userOverrides.delete(key);
+ } else {
+ this.userOverrides.add(key);
+ }
+ }
+ }
+ }
+
this.saveConfig();
}
try {
localStorage.setItem('config', JSON.stringify(this.config));
+
+ localStorage.setItem('userOverrides', JSON.stringify(Array.from(this.userOverrides)));
} catch (error) {
console.error('Failed to save config to localStorage:', error);
}
getAllConfig(): SettingsConfigType {
return { ...this.config };
}
+
+ /**
+ * Initialize settings with props defaults when server properties are first loaded
+ * This sets up the default values from /props endpoint
+ */
+ syncWithServerDefaults(): void {
+ const serverParams = serverStore.serverDefaultParams;
+ if (!serverParams) {
+ console.warn('No server parameters available for initialization');
+
+ return;
+ }
+
+ const propsDefaults = this.getServerDefaults();
+
+ for (const [key, propsValue] of Object.entries(propsDefaults)) {
+ const currentValue = getConfigValue(this.config, key);
+
+ const normalizedCurrent = normalizeFloatingPoint(currentValue);
+ const normalizedDefault = normalizeFloatingPoint(propsValue);
+
+ if (normalizedCurrent === normalizedDefault) {
+ this.userOverrides.delete(key);
+ setConfigValue(this.config, key, propsValue);
+ } else if (!this.userOverrides.has(key)) {
+ setConfigValue(this.config, key, propsValue);
+ }
+ }
+
+ this.saveConfig();
+ console.log('Settings initialized with props defaults:', propsDefaults);
+ console.log('Current user overrides after sync:', Array.from(this.userOverrides));
+ }
+
+ /**
+ * Clear all user overrides (for debugging)
+ */
+ clearAllUserOverrides(): void {
+ this.userOverrides.clear();
+ this.saveConfig();
+ console.log('Cleared all user overrides');
+ }
+
+ /**
+ * Reset all parameters to their default values (from props)
+ * This is used by the "Reset to Default" functionality
+ * Prioritizes server defaults from /props, falls back to webui defaults
+ */
+ forceSyncWithServerDefaults(): void {
+ const propsDefaults = this.getServerDefaults();
+ const syncableKeys = ParameterSyncService.getSyncableParameterKeys();
+
+ for (const key of syncableKeys) {
+ if (propsDefaults[key] !== undefined) {
+ const normalizedValue = normalizeFloatingPoint(propsDefaults[key]);
+
+ setConfigValue(this.config, key, normalizedValue);
+ } else {
+ if (key in SETTING_CONFIG_DEFAULT) {
+ const defaultValue = getConfigValue(SETTING_CONFIG_DEFAULT, key);
+
+ setConfigValue(this.config, key, defaultValue);
+ }
+ }
+
+ this.userOverrides.delete(key);
+ }
+
+ this.saveConfig();
+ }
+
+ /**
+ * Get parameter information including source for a specific parameter
+ */
+ getParameterInfo(key: string) {
+ const propsDefaults = this.getServerDefaults();
+ const currentValue = getConfigValue(this.config, key);
+
+ return ParameterSyncService.getParameterInfo(
+ key,
+ currentValue ?? '',
+ propsDefaults,
+ this.userOverrides
+ );
+ }
+
+ /**
+ * Reset a parameter to server default (or webui default if no server default)
+ */
+ resetParameterToServerDefault(key: string): void {
+ const serverDefaults = this.getServerDefaults();
+
+ if (serverDefaults[key] !== undefined) {
+ const value = normalizeFloatingPoint(serverDefaults[key]);
+
+ this.config[key as keyof SettingsConfigType] =
+ value as SettingsConfigType[keyof SettingsConfigType];
+ } else {
+ if (key in SETTING_CONFIG_DEFAULT) {
+ const defaultValue = getConfigValue(SETTING_CONFIG_DEFAULT, key);
+
+ setConfigValue(this.config, key, defaultValue);
+ }
+ }
+
+ this.userOverrides.delete(key);
+ this.saveConfig();
+ }
+
+ /**
+ * Get diff between current settings and server defaults
+ */
+ getParameterDiff() {
+ const serverDefaults = this.getServerDefaults();
+ if (Object.keys(serverDefaults).length === 0) return {};
+
+ const configAsRecord = configToParameterRecord(
+ this.config,
+ ParameterSyncService.getSyncableParameterKeys()
+ );
+
+ return ParameterSyncService.createParameterDiff(configAsRecord, serverDefaults);
+ }
}
// Create and export the settings store instance
export const resetAll = settingsStore.resetAll.bind(settingsStore);
export const getConfig = settingsStore.getConfig.bind(settingsStore);
export const getAllConfig = settingsStore.getAllConfig.bind(settingsStore);
+export const syncWithServerDefaults = settingsStore.syncWithServerDefaults.bind(settingsStore);
+export const forceSyncWithServerDefaults =
+ settingsStore.forceSyncWithServerDefaults.bind(settingsStore);
+export const getParameterInfo = settingsStore.getParameterInfo.bind(settingsStore);
+export const resetParameterToServerDefault =
+ settingsStore.resetParameterToServerDefault.bind(settingsStore);
+export const getParameterDiff = settingsStore.getParameterDiff.bind(settingsStore);
+export const clearAllUserOverrides = settingsStore.clearAllUserOverrides.bind(settingsStore);
--- /dev/null
+/**
+ * Type-safe configuration helpers
+ *
+ * Provides utilities for safely accessing and modifying configuration objects
+ * with dynamic keys while maintaining TypeScript type safety.
+ */
+
+import type { SettingsConfigType } from '$lib/types/settings';
+
+/**
+ * Type-safe helper to access config properties dynamically
+ * Provides better type safety than direct casting to Record
+ */
+export function setConfigValue<T extends SettingsConfigType>(
+ config: T,
+ key: string,
+ value: unknown
+): void {
+ if (key in config) {
+ (config as Record<string, unknown>)[key] = value;
+ }
+}
+
+/**
+ * Type-safe helper to get config values dynamically
+ */
+export function getConfigValue<T extends SettingsConfigType>(
+ config: T,
+ key: string
+): string | number | boolean | undefined {
+ const value = (config as Record<string, unknown>)[key];
+ return value as string | number | boolean | undefined;
+}
+
+/**
+ * Convert a SettingsConfigType to a ParameterRecord for specific keys
+ * Useful for parameter synchronization operations
+ */
+export function configToParameterRecord<T extends SettingsConfigType>(
+ config: T,
+ keys: string[]
+): Record<string, string | number | boolean> {
+ const record: Record<string, string | number | boolean> = {};
+
+ for (const key of keys) {
+ const value = getConfigValue(config, key);
+ if (value !== undefined) {
+ record[key] = value;
+ }
+ }
+
+ return record;
+}
--- /dev/null
+/**
+ * Floating-point precision utilities
+ *
+ * Provides functions to normalize floating-point numbers for consistent comparison
+ * and display, addressing JavaScript's floating-point precision issues.
+ */
+
+import { PRECISION_MULTIPLIER } from '$lib/constants/precision';
+
+/**
+ * Normalize floating-point numbers for consistent comparison
+ * Addresses JavaScript floating-point precision issues (e.g., 0.949999988079071 → 0.95)
+ */
+export function normalizeFloatingPoint(value: unknown): unknown {
+ return typeof value === 'number'
+ ? Math.round(value * PRECISION_MULTIPLIER) / PRECISION_MULTIPLIER
+ : value;
+}
+
+/**
+ * Type-safe version that only accepts numbers
+ */
+export function normalizeNumber(value: number): number {
+ return Math.round(value * PRECISION_MULTIPLIER) / PRECISION_MULTIPLIER;
+}
} from '$lib/stores/chat.svelte';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import { serverStore } from '$lib/stores/server.svelte';
- import { config } from '$lib/stores/settings.svelte';
+ import { config, settingsStore } from '$lib/stores/settings.svelte';
import { ModeWatcher } from 'mode-watcher';
import { Toaster } from 'svelte-sonner';
import { goto } from '$app/navigation';
serverStore.fetchServerProps();
});
+ // Sync settings when server props are loaded
+ $effect(() => {
+ const serverProps = serverStore.serverProps;
+
+ if (serverProps?.default_generation_settings?.params) {
+ settingsStore.syncWithServerDefaults();
+ }
+ });
+
// Monitor API key changes and redirect to error page if removed or changed when required
$effect(() => {
const apiKey = config().apiKey;