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;