1/**
2 * settingsStore - Application configuration and theme management
3 *
4 * This store manages all application settings including AI model parameters, UI preferences,
5 * and theme configuration. It provides persistent storage through localStorage with reactive
6 * state management using Svelte 5 runes.
7 *
8 * **Architecture & Relationships:**
9 * - **settingsStore** (this class): Configuration state management
10 * - Manages AI model parameters (temperature, max tokens, etc.)
11 * - Handles theme switching and persistence
12 * - Provides localStorage synchronization
13 * - Offers reactive configuration access
14 *
15 * - **ChatService**: Reads model parameters for API requests
16 * - **UI Components**: Subscribe to theme and configuration changes
17 *
18 * **Key Features:**
19 * - **Model Parameters**: Temperature, max tokens, top-p, top-k, repeat penalty
20 * - **Theme Management**: Auto, light, dark theme switching
21 * - **Persistence**: Automatic localStorage synchronization
22 * - **Reactive State**: Svelte 5 runes for automatic UI updates
23 * - **Default Handling**: Graceful fallback to defaults for missing settings
24 * - **Batch Updates**: Efficient multi-setting updates
25 * - **Reset Functionality**: Restore defaults for individual or all settings
26 *
27 * **Configuration Categories:**
28 * - Generation parameters (temperature, tokens, sampling)
29 * - UI preferences (theme, display options)
30 * - System settings (model selection, prompts)
31 * - Advanced options (seed, penalties, context handling)
32 */
33
34import { browser } from '$app/environment';
35import { SETTING_CONFIG_DEFAULT } from '$lib/constants/settings-config';
36import { ParameterSyncService } from '$lib/services/parameter-sync';
37import { serverStore } from '$lib/stores/server.svelte';
38import {
39 configToParameterRecord,
40 normalizeFloatingPoint,
41 getConfigValue,
42 setConfigValue
43} from '$lib/utils';
44import {
45 CONFIG_LOCALSTORAGE_KEY,
46 USER_OVERRIDES_LOCALSTORAGE_KEY
47} from '$lib/constants/localstorage-keys';
48
49class SettingsStore {
50 // ─────────────────────────────────────────────────────────────────────────────
51 // State
52 // ─────────────────────────────────────────────────────────────────────────────
53
54 config = $state<SettingsConfigType>({ ...SETTING_CONFIG_DEFAULT });
55 theme = $state<string>('auto');
56 isInitialized = $state(false);
57 userOverrides = $state<Set<string>>(new Set());
58
59 // ─────────────────────────────────────────────────────────────────────────────
60 // Utilities (private helpers)
61 // ─────────────────────────────────────────────────────────────────────────────
62
63 /**
64 * Helper method to get server defaults with null safety
65 * Centralizes the pattern of getting and extracting server defaults
66 */
67 private getServerDefaults(): Record<string, string | number | boolean> {
68 const serverParams = serverStore.defaultParams;
69 const webuiSettings = serverStore.webuiSettings;
70 return ParameterSyncService.extractServerDefaults(serverParams, webuiSettings);
71 }
72
73 constructor() {
74 if (browser) {
75 this.initialize();
76 }
77 }
78
79 // ─────────────────────────────────────────────────────────────────────────────
80 // Lifecycle
81 // ─────────────────────────────────────────────────────────────────────────────
82
83 /**
84 * Initialize the settings store by loading from localStorage
85 */
86 initialize() {
87 try {
88 this.loadConfig();
89 this.loadTheme();
90 this.isInitialized = true;
91 } catch (error) {
92 console.error('Failed to initialize settings store:', error);
93 }
94 }
95
96 /**
97 * Load configuration from localStorage
98 * Returns default values for missing keys to prevent breaking changes
99 */
100 private loadConfig() {
101 if (!browser) return;
102
103 try {
104 const storedConfigRaw = localStorage.getItem(CONFIG_LOCALSTORAGE_KEY);
105 const savedVal = JSON.parse(storedConfigRaw || '{}');
106
107 // Merge with defaults to prevent breaking changes
108 this.config = {
109 ...SETTING_CONFIG_DEFAULT,
110 ...savedVal
111 };
112
113 // Load user overrides
114 const savedOverrides = JSON.parse(
115 localStorage.getItem(USER_OVERRIDES_LOCALSTORAGE_KEY) || '[]'
116 );
117 this.userOverrides = new Set(savedOverrides);
118 } catch (error) {
119 console.warn('Failed to parse config from localStorage, using defaults:', error);
120 this.config = { ...SETTING_CONFIG_DEFAULT };
121 this.userOverrides = new Set();
122 }
123 }
124
125 /**
126 * Load theme from localStorage
127 */
128 private loadTheme() {
129 if (!browser) return;
130
131 this.theme = localStorage.getItem('theme') || 'auto';
132 }
133 // ─────────────────────────────────────────────────────────────────────────────
134 // Config Updates
135 // ─────────────────────────────────────────────────────────────────────────────
136
137 /**
138 * Update a specific configuration setting
139 * @param key - The configuration key to update
140 * @param value - The new value for the configuration key
141 */
142 updateConfig<K extends keyof SettingsConfigType>(key: K, value: SettingsConfigType[K]): void {
143 this.config[key] = value;
144
145 if (ParameterSyncService.canSyncParameter(key as string)) {
146 const propsDefaults = this.getServerDefaults();
147 const propsDefault = propsDefaults[key as string];
148
149 if (propsDefault !== undefined) {
150 const normalizedValue = normalizeFloatingPoint(value);
151 const normalizedDefault = normalizeFloatingPoint(propsDefault);
152
153 if (normalizedValue === normalizedDefault) {
154 this.userOverrides.delete(key as string);
155 } else {
156 this.userOverrides.add(key as string);
157 }
158 }
159 }
160
161 this.saveConfig();
162 }
163
164 /**
165 * Update multiple configuration settings at once
166 * @param updates - Object containing the configuration updates
167 */
168 updateMultipleConfig(updates: Partial<SettingsConfigType>) {
169 Object.assign(this.config, updates);
170
171 const propsDefaults = this.getServerDefaults();
172
173 for (const [key, value] of Object.entries(updates)) {
174 if (ParameterSyncService.canSyncParameter(key)) {
175 const propsDefault = propsDefaults[key];
176
177 if (propsDefault !== undefined) {
178 const normalizedValue = normalizeFloatingPoint(value);
179 const normalizedDefault = normalizeFloatingPoint(propsDefault);
180
181 if (normalizedValue === normalizedDefault) {
182 this.userOverrides.delete(key);
183 } else {
184 this.userOverrides.add(key);
185 }
186 }
187 }
188 }
189
190 this.saveConfig();
191 }
192
193 /**
194 * Save the current configuration to localStorage
195 */
196 private saveConfig() {
197 if (!browser) return;
198
199 try {
200 localStorage.setItem(CONFIG_LOCALSTORAGE_KEY, JSON.stringify(this.config));
201
202 localStorage.setItem(
203 USER_OVERRIDES_LOCALSTORAGE_KEY,
204 JSON.stringify(Array.from(this.userOverrides))
205 );
206 } catch (error) {
207 console.error('Failed to save config to localStorage:', error);
208 }
209 }
210
211 /**
212 * Update the theme setting
213 * @param newTheme - The new theme value
214 */
215 updateTheme(newTheme: string) {
216 this.theme = newTheme;
217 this.saveTheme();
218 }
219
220 /**
221 * Save the current theme to localStorage
222 */
223 private saveTheme() {
224 if (!browser) return;
225
226 try {
227 if (this.theme === 'auto') {
228 localStorage.removeItem('theme');
229 } else {
230 localStorage.setItem('theme', this.theme);
231 }
232 } catch (error) {
233 console.error('Failed to save theme to localStorage:', error);
234 }
235 }
236
237 // ─────────────────────────────────────────────────────────────────────────────
238 // Reset
239 // ─────────────────────────────────────────────────────────────────────────────
240
241 /**
242 * Reset configuration to defaults
243 */
244 resetConfig() {
245 this.config = { ...SETTING_CONFIG_DEFAULT };
246 this.saveConfig();
247 }
248
249 /**
250 * Reset theme to auto
251 */
252 resetTheme() {
253 this.theme = 'auto';
254 this.saveTheme();
255 }
256
257 /**
258 * Reset all settings to defaults
259 */
260 resetAll() {
261 this.resetConfig();
262 this.resetTheme();
263 }
264
265 /**
266 * Reset a parameter to server default (or webui default if no server default)
267 */
268 resetParameterToServerDefault(key: string): void {
269 const serverDefaults = this.getServerDefaults();
270
271 if (serverDefaults[key] !== undefined) {
272 const value = normalizeFloatingPoint(serverDefaults[key]);
273
274 this.config[key as keyof SettingsConfigType] =
275 value as SettingsConfigType[keyof SettingsConfigType];
276 } else {
277 if (key in SETTING_CONFIG_DEFAULT) {
278 const defaultValue = getConfigValue(SETTING_CONFIG_DEFAULT, key);
279
280 setConfigValue(this.config, key, defaultValue);
281 }
282 }
283
284 this.userOverrides.delete(key);
285 this.saveConfig();
286 }
287
288 // ─────────────────────────────────────────────────────────────────────────────
289 // Server Sync
290 // ─────────────────────────────────────────────────────────────────────────────
291
292 /**
293 * Initialize settings with props defaults when server properties are first loaded
294 * This sets up the default values from /props endpoint
295 */
296 syncWithServerDefaults(): void {
297 const propsDefaults = this.getServerDefaults();
298
299 if (Object.keys(propsDefaults).length === 0) {
300 console.warn('No server defaults available for initialization');
301
302 return;
303 }
304
305 for (const [key, propsValue] of Object.entries(propsDefaults)) {
306 const currentValue = getConfigValue(this.config, key);
307
308 const normalizedCurrent = normalizeFloatingPoint(currentValue);
309 const normalizedDefault = normalizeFloatingPoint(propsValue);
310
311 if (normalizedCurrent === normalizedDefault) {
312 this.userOverrides.delete(key);
313 setConfigValue(this.config, key, propsValue);
314 } else if (!this.userOverrides.has(key)) {
315 setConfigValue(this.config, key, propsValue);
316 }
317 }
318
319 this.saveConfig();
320 console.log('Settings initialized with props defaults:', propsDefaults);
321 console.log('Current user overrides after sync:', Array.from(this.userOverrides));
322 }
323
324 /**
325 * Reset all parameters to their default values (from props)
326 * This is used by the "Reset to Default" functionality
327 * Prioritizes server defaults from /props, falls back to webui defaults
328 */
329 forceSyncWithServerDefaults(): void {
330 const propsDefaults = this.getServerDefaults();
331 const syncableKeys = ParameterSyncService.getSyncableParameterKeys();
332
333 for (const key of syncableKeys) {
334 if (propsDefaults[key] !== undefined) {
335 const normalizedValue = normalizeFloatingPoint(propsDefaults[key]);
336
337 setConfigValue(this.config, key, normalizedValue);
338 } else {
339 if (key in SETTING_CONFIG_DEFAULT) {
340 const defaultValue = getConfigValue(SETTING_CONFIG_DEFAULT, key);
341
342 setConfigValue(this.config, key, defaultValue);
343 }
344 }
345
346 this.userOverrides.delete(key);
347 }
348
349 this.saveConfig();
350 }
351
352 // ─────────────────────────────────────────────────────────────────────────────
353 // Utilities
354 // ─────────────────────────────────────────────────────────────────────────────
355
356 /**
357 * Get a specific configuration value
358 * @param key - The configuration key to get
359 * @returns The configuration value
360 */
361 getConfig<K extends keyof SettingsConfigType>(key: K): SettingsConfigType[K] {
362 return this.config[key];
363 }
364
365 /**
366 * Get the entire configuration object
367 * @returns The complete configuration object
368 */
369 getAllConfig(): SettingsConfigType {
370 return { ...this.config };
371 }
372
373 canSyncParameter(key: string): boolean {
374 return ParameterSyncService.canSyncParameter(key);
375 }
376
377 /**
378 * Get parameter information including source for a specific parameter
379 */
380 getParameterInfo(key: string) {
381 const propsDefaults = this.getServerDefaults();
382 const currentValue = getConfigValue(this.config, key);
383
384 return ParameterSyncService.getParameterInfo(
385 key,
386 currentValue ?? '',
387 propsDefaults,
388 this.userOverrides
389 );
390 }
391
392 /**
393 * Get diff between current settings and server defaults
394 */
395 getParameterDiff() {
396 const serverDefaults = this.getServerDefaults();
397 if (Object.keys(serverDefaults).length === 0) return {};
398
399 const configAsRecord = configToParameterRecord(
400 this.config,
401 ParameterSyncService.getSyncableParameterKeys()
402 );
403
404 return ParameterSyncService.createParameterDiff(configAsRecord, serverDefaults);
405 }
406
407 /**
408 * Clear all user overrides (for debugging)
409 */
410 clearAllUserOverrides(): void {
411 this.userOverrides.clear();
412 this.saveConfig();
413 console.log('Cleared all user overrides');
414 }
415}
416
417export const settingsStore = new SettingsStore();
418
419export const config = () => settingsStore.config;
420export const theme = () => settingsStore.theme;
421export const isInitialized = () => settingsStore.isInitialized;