SvelteKit-based WebUI (#14839)
This commit is contained in:
committed by
GitHub
parent
8f8f2274ee
commit
a7a98e0fff
1394
tools/server/webui/src/lib/stores/chat.svelte.ts
Normal file
1394
tools/server/webui/src/lib/stores/chat.svelte.ts
Normal file
File diff suppressed because it is too large
Load Diff
349
tools/server/webui/src/lib/stores/database.ts
Normal file
349
tools/server/webui/src/lib/stores/database.ts
Normal file
@@ -0,0 +1,349 @@
|
||||
import Dexie, { type EntityTable } from 'dexie';
|
||||
import { filterByLeafNodeId, findDescendantMessages } from '$lib/utils/branching';
|
||||
|
||||
class LlamacppDatabase extends Dexie {
|
||||
conversations!: EntityTable<DatabaseConversation, string>;
|
||||
messages!: EntityTable<DatabaseMessage, string>;
|
||||
|
||||
constructor() {
|
||||
super('LlamacppWebui');
|
||||
|
||||
this.version(1).stores({
|
||||
conversations: 'id, lastModified, currNode, name',
|
||||
messages: 'id, convId, type, role, timestamp, parent, children'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const db = new LlamacppDatabase();
|
||||
|
||||
/**
|
||||
* DatabaseStore - Persistent data layer for conversation and message management
|
||||
*
|
||||
* This service provides a comprehensive data access layer built on IndexedDB using Dexie.
|
||||
* It handles all persistent storage operations for conversations, messages, and application settings
|
||||
* with support for complex conversation branching and message threading.
|
||||
*
|
||||
* **Architecture & Relationships:**
|
||||
* - **DatabaseStore** (this class): Stateless data persistence layer
|
||||
* - Manages IndexedDB operations through Dexie ORM
|
||||
* - Handles conversation and message CRUD operations
|
||||
* - Supports complex branching with parent-child relationships
|
||||
* - Provides transaction safety for multi-table operations
|
||||
*
|
||||
* - **ChatStore**: Primary consumer for conversation state management
|
||||
* - Uses DatabaseStore for all persistence operations
|
||||
* - Coordinates UI state with database state
|
||||
* - Handles conversation lifecycle and message branching
|
||||
*
|
||||
* **Key Features:**
|
||||
* - **Conversation Management**: Create, read, update, delete conversations
|
||||
* - **Message Branching**: Support for tree-like conversation structures
|
||||
* - **Transaction Safety**: Atomic operations for data consistency
|
||||
* - **Path Resolution**: Navigate conversation branches and find leaf nodes
|
||||
* - **Cascading Deletion**: Remove entire conversation branches
|
||||
*
|
||||
* **Database Schema:**
|
||||
* - `conversations`: Conversation metadata with current node tracking
|
||||
* - `messages`: Individual messages with parent-child relationships
|
||||
*
|
||||
* **Branching Model:**
|
||||
* Messages form a tree structure where each message can have multiple children,
|
||||
* enabling conversation branching and alternative response paths. The conversation's
|
||||
* `currNode` tracks the currently active branch endpoint.
|
||||
*/
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
export class DatabaseStore {
|
||||
/**
|
||||
* Adds a new message to the database.
|
||||
*
|
||||
* @param message - Message to add (without id)
|
||||
* @returns The created message
|
||||
*/
|
||||
static async addMessage(message: Omit<DatabaseMessage, 'id'>): Promise<DatabaseMessage> {
|
||||
const newMessage: DatabaseMessage = {
|
||||
...message,
|
||||
id: uuid()
|
||||
};
|
||||
|
||||
await db.messages.add(newMessage);
|
||||
return newMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new conversation.
|
||||
*
|
||||
* @param name - Name of the conversation
|
||||
* @returns The created conversation
|
||||
*/
|
||||
static async createConversation(name: string): Promise<DatabaseConversation> {
|
||||
const conversation: DatabaseConversation = {
|
||||
id: uuid(),
|
||||
name,
|
||||
lastModified: Date.now(),
|
||||
currNode: ''
|
||||
};
|
||||
|
||||
await db.conversations.add(conversation);
|
||||
return conversation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new message branch by adding a message and updating parent/child relationships.
|
||||
* Also updates the conversation's currNode to point to the new message.
|
||||
*
|
||||
* @param message - Message to add (without id)
|
||||
* @param parentId - Parent message ID to attach to
|
||||
* @returns The created message
|
||||
*/
|
||||
static async createMessageBranch(
|
||||
message: Omit<DatabaseMessage, 'id'>,
|
||||
parentId: string | null
|
||||
): Promise<DatabaseMessage> {
|
||||
return await db.transaction('rw', [db.conversations, db.messages], async () => {
|
||||
// Handle null parent (root message case)
|
||||
if (parentId !== null) {
|
||||
const parentMessage = await db.messages.get(parentId);
|
||||
if (!parentMessage) {
|
||||
throw new Error(`Parent message ${parentId} not found`);
|
||||
}
|
||||
}
|
||||
|
||||
const newMessage: DatabaseMessage = {
|
||||
...message,
|
||||
id: uuid(),
|
||||
parent: parentId,
|
||||
children: []
|
||||
};
|
||||
|
||||
await db.messages.add(newMessage);
|
||||
|
||||
// Update parent's children array if parent exists
|
||||
if (parentId !== null) {
|
||||
const parentMessage = await db.messages.get(parentId);
|
||||
if (parentMessage) {
|
||||
await db.messages.update(parentId, {
|
||||
children: [...parentMessage.children, newMessage.id]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await this.updateConversation(message.convId, {
|
||||
currNode: newMessage.id
|
||||
});
|
||||
|
||||
return newMessage;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a root message for a new conversation.
|
||||
* Root messages are not displayed but serve as the tree root for branching.
|
||||
*
|
||||
* @param convId - Conversation ID
|
||||
* @returns The created root message
|
||||
*/
|
||||
static async createRootMessage(convId: string): Promise<string> {
|
||||
const rootMessage: DatabaseMessage = {
|
||||
id: uuid(),
|
||||
convId,
|
||||
type: 'root',
|
||||
timestamp: Date.now(),
|
||||
role: 'system',
|
||||
content: '',
|
||||
parent: null,
|
||||
thinking: '',
|
||||
children: []
|
||||
};
|
||||
|
||||
await db.messages.add(rootMessage);
|
||||
return rootMessage.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a conversation and all its messages.
|
||||
*
|
||||
* @param id - Conversation ID
|
||||
*/
|
||||
static async deleteConversation(id: string): Promise<void> {
|
||||
await db.transaction('rw', [db.conversations, db.messages], async () => {
|
||||
await db.conversations.delete(id);
|
||||
await db.messages.where('convId').equals(id).delete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a message and removes it from its parent's children array.
|
||||
*
|
||||
* @param messageId - ID of the message to delete
|
||||
*/
|
||||
static async deleteMessage(messageId: string): Promise<void> {
|
||||
await db.transaction('rw', db.messages, async () => {
|
||||
const message = await db.messages.get(messageId);
|
||||
if (!message) return;
|
||||
|
||||
// Remove this message from its parent's children array
|
||||
if (message.parent) {
|
||||
const parent = await db.messages.get(message.parent);
|
||||
if (parent) {
|
||||
parent.children = parent.children.filter((childId: string) => childId !== messageId);
|
||||
await db.messages.put(parent);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the message
|
||||
await db.messages.delete(messageId);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a message and all its descendant messages (cascading deletion).
|
||||
* This removes the entire branch starting from the specified message.
|
||||
*
|
||||
* @param conversationId - ID of the conversation containing the message
|
||||
* @param messageId - ID of the root message to delete (along with all descendants)
|
||||
* @returns Array of all deleted message IDs
|
||||
*/
|
||||
static async deleteMessageCascading(
|
||||
conversationId: string,
|
||||
messageId: string
|
||||
): Promise<string[]> {
|
||||
return await db.transaction('rw', db.messages, async () => {
|
||||
// Get all messages in the conversation to find descendants
|
||||
const allMessages = await db.messages.where('convId').equals(conversationId).toArray();
|
||||
|
||||
// Find all descendant messages
|
||||
const descendants = findDescendantMessages(allMessages, messageId);
|
||||
const allToDelete = [messageId, ...descendants];
|
||||
|
||||
// Get the message to delete for parent cleanup
|
||||
const message = await db.messages.get(messageId);
|
||||
if (message && message.parent) {
|
||||
const parent = await db.messages.get(message.parent);
|
||||
if (parent) {
|
||||
parent.children = parent.children.filter((childId: string) => childId !== messageId);
|
||||
await db.messages.put(parent);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete all messages in the branch
|
||||
await db.messages.bulkDelete(allToDelete);
|
||||
|
||||
return allToDelete;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all conversations, sorted by last modified time (newest first).
|
||||
*
|
||||
* @returns Array of conversations
|
||||
*/
|
||||
static async getAllConversations(): Promise<DatabaseConversation[]> {
|
||||
return await db.conversations.orderBy('lastModified').reverse().toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a conversation by ID.
|
||||
*
|
||||
* @param id - Conversation ID
|
||||
* @returns The conversation if found, otherwise undefined
|
||||
*/
|
||||
static async getConversation(id: string): Promise<DatabaseConversation | undefined> {
|
||||
return await db.conversations.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all leaf nodes (messages with no children) in a conversation.
|
||||
* Useful for finding all possible conversation endpoints.
|
||||
*
|
||||
* @param convId - Conversation ID
|
||||
* @returns Array of leaf node message IDs
|
||||
*/
|
||||
static async getConversationLeafNodes(convId: string): Promise<string[]> {
|
||||
const allMessages = await this.getConversationMessages(convId);
|
||||
return allMessages.filter((msg) => msg.children.length === 0).map((msg) => msg.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all messages in a conversation, sorted by timestamp (oldest first).
|
||||
*
|
||||
* @param convId - Conversation ID
|
||||
* @returns Array of messages in the conversation
|
||||
*/
|
||||
static async getConversationMessages(convId: string): Promise<DatabaseMessage[]> {
|
||||
return await db.messages.where('convId').equals(convId).sortBy('timestamp');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the conversation path from root to the current leaf node.
|
||||
* Uses the conversation's currNode to determine the active branch.
|
||||
*
|
||||
* @param convId - Conversation ID
|
||||
* @returns Array of messages in the current conversation path
|
||||
*/
|
||||
static async getConversationPath(convId: string): Promise<DatabaseMessage[]> {
|
||||
const conversation = await this.getConversation(convId);
|
||||
|
||||
if (!conversation) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const allMessages = await this.getConversationMessages(convId);
|
||||
|
||||
if (allMessages.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// If no currNode is set, use the latest message as leaf
|
||||
const leafNodeId =
|
||||
conversation.currNode ||
|
||||
allMessages.reduce((latest, msg) => (msg.timestamp > latest.timestamp ? msg : latest)).id;
|
||||
|
||||
return filterByLeafNodeId(allMessages, leafNodeId, false) as DatabaseMessage[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a conversation.
|
||||
*
|
||||
* @param id - Conversation ID
|
||||
* @param updates - Partial updates to apply
|
||||
* @returns Promise that resolves when the conversation is updated
|
||||
*/
|
||||
static async updateConversation(
|
||||
id: string,
|
||||
updates: Partial<Omit<DatabaseConversation, 'id'>>
|
||||
): Promise<void> {
|
||||
await db.conversations.update(id, {
|
||||
...updates,
|
||||
lastModified: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the conversation's current node (active branch).
|
||||
* This determines which conversation path is currently being viewed.
|
||||
*
|
||||
* @param convId - Conversation ID
|
||||
* @param nodeId - Message ID to set as current node
|
||||
*/
|
||||
static async updateCurrentNode(convId: string, nodeId: string): Promise<void> {
|
||||
await this.updateConversation(convId, {
|
||||
currNode: nodeId
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a message.
|
||||
*
|
||||
* @param id - Message ID
|
||||
* @param updates - Partial updates to apply
|
||||
* @returns Promise that resolves when the message is updated
|
||||
*/
|
||||
static async updateMessage(
|
||||
id: string,
|
||||
updates: Partial<Omit<DatabaseMessage, 'id'>>
|
||||
): Promise<void> {
|
||||
await db.messages.update(id, updates);
|
||||
}
|
||||
}
|
||||
184
tools/server/webui/src/lib/stores/server.svelte.ts
Normal file
184
tools/server/webui/src/lib/stores/server.svelte.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import { ChatService } from '$lib/services/chat';
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
|
||||
/**
|
||||
* ServerStore - Server state management and capability detection
|
||||
*
|
||||
* This store manages communication with the llama.cpp server to retrieve and maintain
|
||||
* server properties, model information, and capability detection. It provides reactive
|
||||
* state for server connectivity, model capabilities, and endpoint availability.
|
||||
*
|
||||
* **Architecture & Relationships:**
|
||||
* - **ServerStore** (this class): Server state and capability management
|
||||
* - Fetches and caches server properties from `/props` endpoint
|
||||
* - Detects model capabilities (vision, audio support)
|
||||
* - Tests endpoint availability (slots endpoint)
|
||||
* - Provides reactive server state for UI components
|
||||
*
|
||||
* - **ChatService**: Uses server properties for request validation
|
||||
* - **SlotsService**: Depends on slots endpoint availability detection
|
||||
* - **UI Components**: Subscribe to server state for capability-based rendering
|
||||
*
|
||||
* **Key Features:**
|
||||
* - **Server Properties**: Model path, context size, build information
|
||||
* - **Capability Detection**: Vision and audio modality support
|
||||
* - **Endpoint Testing**: Slots endpoint availability checking
|
||||
* - **Error Handling**: User-friendly error messages for connection issues
|
||||
* - **Reactive State**: Svelte 5 runes for automatic UI updates
|
||||
* - **State Management**: Loading states and error recovery
|
||||
*
|
||||
* **Server Capabilities Detected:**
|
||||
* - Model name extraction from file path
|
||||
* - Vision support (multimodal image processing)
|
||||
* - Audio support (speech processing)
|
||||
* - Slots endpoint availability (for processing state monitoring)
|
||||
* - Context window size and token limits
|
||||
*/
|
||||
class ServerStore {
|
||||
private _serverProps = $state<ApiLlamaCppServerProps | null>(null);
|
||||
private _loading = $state(false);
|
||||
private _error = $state<string | null>(null);
|
||||
private _slotsEndpointAvailable = $state<boolean | null>(null);
|
||||
|
||||
get serverProps(): ApiLlamaCppServerProps | null {
|
||||
return this._serverProps;
|
||||
}
|
||||
|
||||
get loading(): boolean {
|
||||
return this._loading;
|
||||
}
|
||||
|
||||
get error(): string | null {
|
||||
return this._error;
|
||||
}
|
||||
|
||||
get modelName(): string | null {
|
||||
if (!this._serverProps?.model_path) return null;
|
||||
return this._serverProps.model_path.split(/(\\|\/)/).pop() || null;
|
||||
}
|
||||
|
||||
get supportedModalities(): string[] {
|
||||
const modalities: string[] = [];
|
||||
if (this._serverProps?.modalities?.audio) {
|
||||
modalities.push('audio');
|
||||
}
|
||||
if (this._serverProps?.modalities?.vision) {
|
||||
modalities.push('vision');
|
||||
}
|
||||
return modalities;
|
||||
}
|
||||
|
||||
get supportsVision(): boolean {
|
||||
return this._serverProps?.modalities?.vision ?? false;
|
||||
}
|
||||
|
||||
get supportsAudio(): boolean {
|
||||
return this._serverProps?.modalities?.audio ?? false;
|
||||
}
|
||||
|
||||
get slotsEndpointAvailable(): boolean | null {
|
||||
return this._slotsEndpointAvailable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if slots endpoint is available based on server properties and endpoint support
|
||||
*/
|
||||
private async checkSlotsEndpointAvailability(): Promise<void> {
|
||||
if (!this._serverProps) {
|
||||
this._slotsEndpointAvailable = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._serverProps.total_slots <= 0) {
|
||||
this._slotsEndpointAvailable = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const currentConfig = config();
|
||||
const apiKey = currentConfig.apiKey?.toString().trim();
|
||||
|
||||
const response = await fetch('/slots', {
|
||||
headers: {
|
||||
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {})
|
||||
}
|
||||
});
|
||||
|
||||
if (response.status === 501) {
|
||||
console.info('Slots endpoint not implemented - server started without --slots flag');
|
||||
this._slotsEndpointAvailable = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this._slotsEndpointAvailable = true;
|
||||
} catch (error) {
|
||||
console.warn('Unable to test slots endpoint availability:', error);
|
||||
this._slotsEndpointAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches server properties from the server
|
||||
*/
|
||||
async fetchServerProps(): Promise<void> {
|
||||
this._loading = true;
|
||||
this._error = null;
|
||||
|
||||
try {
|
||||
console.log('Fetching server properties...');
|
||||
const props = await ChatService.getServerProps();
|
||||
this._serverProps = props;
|
||||
console.log('Server properties loaded:', props);
|
||||
|
||||
// Check slots endpoint availability after server props are loaded
|
||||
await this.checkSlotsEndpointAvailability();
|
||||
} catch (error) {
|
||||
let errorMessage = 'Failed to connect to server';
|
||||
|
||||
if (error instanceof Error) {
|
||||
// Handle specific error types with user-friendly messages
|
||||
if (error.name === 'TypeError' && error.message.includes('fetch')) {
|
||||
errorMessage = 'Server is not running or unreachable';
|
||||
} else if (error.message.includes('ECONNREFUSED')) {
|
||||
errorMessage = 'Connection refused - server may be offline';
|
||||
} else if (error.message.includes('ENOTFOUND')) {
|
||||
errorMessage = 'Server not found - check server address';
|
||||
} else if (error.message.includes('ETIMEDOUT')) {
|
||||
errorMessage = 'Connection timeout - server may be overloaded';
|
||||
} else if (error.message.includes('500')) {
|
||||
errorMessage = 'Server error - check server logs';
|
||||
} else if (error.message.includes('404')) {
|
||||
errorMessage = 'Server endpoint not found';
|
||||
} else if (error.message.includes('403') || error.message.includes('401')) {
|
||||
errorMessage = 'Access denied';
|
||||
}
|
||||
}
|
||||
|
||||
this._error = errorMessage;
|
||||
console.error('Error fetching server properties:', error);
|
||||
} finally {
|
||||
this._loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the server state
|
||||
*/
|
||||
clear(): void {
|
||||
this._serverProps = null;
|
||||
this._error = null;
|
||||
this._loading = false;
|
||||
this._slotsEndpointAvailable = null;
|
||||
}
|
||||
}
|
||||
|
||||
export const serverStore = new ServerStore();
|
||||
|
||||
export const serverProps = () => serverStore.serverProps;
|
||||
export const serverLoading = () => serverStore.loading;
|
||||
export const serverError = () => serverStore.error;
|
||||
export const modelName = () => serverStore.modelName;
|
||||
export const supportedModalities = () => serverStore.supportedModalities;
|
||||
export const supportsVision = () => serverStore.supportsVision;
|
||||
export const supportsAudio = () => serverStore.supportsAudio;
|
||||
export const slotsEndpointAvailable = () => serverStore.slotsEndpointAvailable;
|
||||
206
tools/server/webui/src/lib/stores/settings.svelte.ts
Normal file
206
tools/server/webui/src/lib/stores/settings.svelte.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* SettingsStore - Application configuration and theme management
|
||||
*
|
||||
* This store manages all application settings including AI model parameters, UI preferences,
|
||||
* and theme configuration. It provides persistent storage through localStorage with reactive
|
||||
* state management using Svelte 5 runes.
|
||||
*
|
||||
* **Architecture & Relationships:**
|
||||
* - **SettingsStore** (this class): Configuration state management
|
||||
* - Manages AI model parameters (temperature, max tokens, etc.)
|
||||
* - Handles theme switching and persistence
|
||||
* - Provides localStorage synchronization
|
||||
* - Offers reactive configuration access
|
||||
*
|
||||
* - **ChatService**: Reads model parameters for API requests
|
||||
* - **UI Components**: Subscribe to theme and configuration changes
|
||||
*
|
||||
* **Key Features:**
|
||||
* - **Model Parameters**: Temperature, max tokens, top-p, top-k, repeat penalty
|
||||
* - **Theme Management**: Auto, light, dark theme switching
|
||||
* - **Persistence**: Automatic localStorage synchronization
|
||||
* - **Reactive State**: Svelte 5 runes for automatic UI updates
|
||||
* - **Default Handling**: Graceful fallback to defaults for missing settings
|
||||
* - **Batch Updates**: Efficient multi-setting updates
|
||||
* - **Reset Functionality**: Restore defaults for individual or all settings
|
||||
*
|
||||
* **Configuration Categories:**
|
||||
* - Generation parameters (temperature, tokens, sampling)
|
||||
* - UI preferences (theme, display options)
|
||||
* - System settings (model selection, prompts)
|
||||
* - Advanced options (seed, penalties, context handling)
|
||||
*/
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import { SETTING_CONFIG_DEFAULT } from '$lib/constants/settings-config';
|
||||
|
||||
class SettingsStore {
|
||||
config = $state<SettingsConfigType>({ ...SETTING_CONFIG_DEFAULT });
|
||||
theme = $state<string>('auto');
|
||||
isInitialized = $state(false);
|
||||
|
||||
constructor() {
|
||||
if (browser) {
|
||||
this.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the settings store by loading from localStorage
|
||||
*/
|
||||
initialize() {
|
||||
try {
|
||||
this.loadConfig();
|
||||
this.loadTheme();
|
||||
this.isInitialized = true;
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize settings store:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load configuration from localStorage
|
||||
* Returns default values for missing keys to prevent breaking changes
|
||||
*/
|
||||
private loadConfig() {
|
||||
if (!browser) return;
|
||||
|
||||
try {
|
||||
const savedVal = JSON.parse(localStorage.getItem('config') || '{}');
|
||||
// Merge with defaults to prevent breaking changes
|
||||
this.config = {
|
||||
...SETTING_CONFIG_DEFAULT,
|
||||
...savedVal
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse config from localStorage, using defaults:', error);
|
||||
this.config = { ...SETTING_CONFIG_DEFAULT };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load theme from localStorage
|
||||
*/
|
||||
private loadTheme() {
|
||||
if (!browser) return;
|
||||
|
||||
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]) {
|
||||
this.config[key] = value;
|
||||
this.saveConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update multiple configuration settings at once
|
||||
* @param updates - Object containing the configuration updates
|
||||
*/
|
||||
updateMultipleConfig(updates: Partial<SettingsConfigType>) {
|
||||
Object.assign(this.config, updates);
|
||||
this.saveConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the current configuration to localStorage
|
||||
*/
|
||||
private saveConfig() {
|
||||
if (!browser) return;
|
||||
|
||||
try {
|
||||
localStorage.setItem('config', JSON.stringify(this.config));
|
||||
} catch (error) {
|
||||
console.error('Failed to save config to localStorage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the theme setting
|
||||
* @param newTheme - The new theme value
|
||||
*/
|
||||
updateTheme(newTheme: string) {
|
||||
this.theme = newTheme;
|
||||
this.saveTheme();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the current theme to localStorage
|
||||
*/
|
||||
private saveTheme() {
|
||||
if (!browser) return;
|
||||
|
||||
try {
|
||||
if (this.theme === 'auto') {
|
||||
localStorage.removeItem('theme');
|
||||
} else {
|
||||
localStorage.setItem('theme', this.theme);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save theme to localStorage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset configuration to defaults
|
||||
*/
|
||||
resetConfig() {
|
||||
this.config = { ...SETTING_CONFIG_DEFAULT };
|
||||
this.saveConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset theme to auto
|
||||
*/
|
||||
resetTheme() {
|
||||
this.theme = 'auto';
|
||||
this.saveTheme();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all settings to defaults
|
||||
*/
|
||||
resetAll() {
|
||||
this.resetConfig();
|
||||
this.resetTheme();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific configuration value
|
||||
* @param key - The configuration key to get
|
||||
* @returns The configuration value
|
||||
*/
|
||||
getConfig<K extends keyof SettingsConfigType>(key: K): SettingsConfigType[K] {
|
||||
return this.config[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entire configuration object
|
||||
* @returns The complete configuration object
|
||||
*/
|
||||
getAllConfig(): SettingsConfigType {
|
||||
return { ...this.config };
|
||||
}
|
||||
}
|
||||
|
||||
// Create and export the settings store instance
|
||||
export const settingsStore = new SettingsStore();
|
||||
|
||||
// Export reactive getters for easy access in components
|
||||
export const config = () => settingsStore.config;
|
||||
export const theme = () => settingsStore.theme;
|
||||
export const isInitialized = () => settingsStore.isInitialized;
|
||||
|
||||
// Export bound methods for easy access
|
||||
export const updateConfig = settingsStore.updateConfig.bind(settingsStore);
|
||||
export const updateMultipleConfig = settingsStore.updateMultipleConfig.bind(settingsStore);
|
||||
export const updateTheme = settingsStore.updateTheme.bind(settingsStore);
|
||||
export const resetConfig = settingsStore.resetConfig.bind(settingsStore);
|
||||
export const resetTheme = settingsStore.resetTheme.bind(settingsStore);
|
||||
export const resetAll = settingsStore.resetAll.bind(settingsStore);
|
||||
export const getConfig = settingsStore.getConfig.bind(settingsStore);
|
||||
export const getAllConfig = settingsStore.getAllConfig.bind(settingsStore);
|
||||
Reference in New Issue
Block a user