/** * Settings Dialog * Provides a UI for configuring SageUtils settings * Currently focuses on LLM provider configuration (REST providers, OpenAI, and Native) */ import { api } from '../../../../scripts/api.js'; import { createDialog } from '../components/dialogManager.js'; import { handleError } from '../shared/errorHandler.js'; import { notifications } from '../shared/notifications.js'; import { app } from '../../../../scripts/app.js'; import { createCheckbox, createRadioGroup, createInput } from '../components/formElements.js'; import { javascriptTimer, uiTimer, printTimingReport } from '../shared/performanceTimer.js'; import { TAB_VISIBILITY_CONFIG, hasTabVisibilityUpdates, normalizeTabVisibilitySettings } from '../shared/sidebarVisibility.js'; /** * Mapping between backend setting keys and ComfyUI setting IDs */ const SETTING_KEY_TO_ID_MAP = { 'enable_lmstudio_rest': 'SageUtils.LLM Providers.enable_lmstudio_rest', 'enable_ollama_rest': 'SageUtils.LLM Providers.enable_ollama_rest', 'enable_openai': 'SageUtils.LLM Providers.enable_openai', 'openai_api_key': 'SageUtils.OpenAI.openai_api_key', 'openai_use_custom_url': 'SageUtils.OpenAI.openai_use_custom_url', 'openai_base_url': 'SageUtils.OpenAI.openai_base_url', 'ollama_custom_url': 'SageUtils.Local Custom Ollama URL.ollama_custom_url', 'ollama_use_custom_url': 'SageUtils.Local Custom Ollama URL.ollama_use_custom_url', 'ollama_api_key': 'SageUtils.Ollama.ollama_api_key', 'lmstudio_custom_url': 'SageUtils.Local Custom LM Studio URL.lmstudio_custom_url', 'lmstudio_use_custom_url': 'SageUtils.Local Custom LM Studio URL.lmstudio_use_custom_url', 'lmstudio_api_token': 'SageUtils.LM Studio.lmstudio_api_token' }; /** * Show the settings dialog */ export async function showSettingsDialog() { const content = document.createElement('div'); content.style.cssText = ` min-width: 600px; max-width: 700px; min-height: 400px; `; const dialog = createDialog({ title: 'SageUtils Settings', content: content, width: '650px', height: 'auto', showFooter: true, closeOnOverlayClick: false }); // Create loading indicator const loadingSection = document.createElement('div'); loadingSection.style.cssText = ` text-align: center; padding: 40px; color: #888; `; loadingSection.innerHTML = `
Loading settings...
Please wait
`; content.appendChild(loadingSection); // Show the dialog dialog.show(); try { // Load current settings const response = await api.fetchApi('/sage_utils/settings'); if (!response.ok) { throw new Error(`Failed to load settings: ${response.statusText}`); } const result = await response.json(); console.log('Loaded settings from API:', result); if (!result.success) { throw new Error(result.error || 'Failed to load settings'); } const settings = result.settings; console.log('Settings object:', settings); // Clear loading and build settings UI content.innerHTML = ''; buildSettingsUI(content, settings, dialog); } catch (error) { content.innerHTML = `
Error loading settings
${error.message}
`; handleError(error, 'Failed to load settings'); } } /** * Build the settings UI * @param {HTMLElement} container - Container element * @param {Object} settings - Current settings data * @param {Object} dialog - Dialog instance */ function buildSettingsUI(container, settings, dialog) { // Create main settings container const settingsContainer = document.createElement('div'); settingsContainer.style.cssText = ` display: flex; flex-direction: column; gap: 20px; `; // LLM Integration Section const llmSection = createLLMSection(settings); settingsContainer.appendChild(llmSection); // Tab Visibility Section const tabVisibilitySection = createTabVisibilitySection(settings); settingsContainer.appendChild(tabVisibilitySection); // Performance & Telemetry Section (frontend-only) const perfSection = createPerformanceSection(); settingsContainer.appendChild(perfSection); container.appendChild(settingsContainer); // Add footer buttons dialog.addFooterButton('Save', async () => { await saveSettings(dialog, settingsContainer, settings); }, 'primary'); dialog.addFooterButton('Reset to Defaults', async () => { if (confirm('Are you sure you want to reset all settings to their default values?')) { await resetSettings(dialog); } }); dialog.addFooterButton('Cancel', () => { dialog.close(); }); } /** * Create LLM Integration settings section * @param {Object} settings - Current settings * @returns {HTMLElement} Section element */ function createLLMSection(settings) { const section = document.createElement('div'); section.style.cssText = ` padding: 15px; background: #1e1e1e; border-radius: 6px; border: 1px solid #444; `; const title = document.createElement('h3'); title.textContent = 'LLM Integration'; title.style.cssText = ` margin: 0 0 15px 0; color: #4CAF50; font-size: 16px; font-weight: 600; `; section.appendChild(title); // Default provider selection const defaultProviderGroup = createDefaultProviderSelector(settings); section.appendChild(defaultProviderGroup); // Separator after default provider const topSeparator = document.createElement('div'); topSeparator.style.cssText = ` height: 1px; background: #444; margin: 15px 0; `; section.appendChild(topSeparator); // Ollama settings const ollamaGroup = createProviderGroup('Ollama', settings, 'ollama_rest', 'ollama'); section.appendChild(ollamaGroup); // Separator const separator = document.createElement('div'); separator.style.cssText = ` height: 1px; background: #444; margin: 15px 0; `; section.appendChild(separator); // OpenAI settings const openaiGroup = createOpenAIProviderGroup(settings); section.appendChild(openaiGroup); // Separator const openaiSeparator = document.createElement('div'); openaiSeparator.style.cssText = ` height: 1px; background: #444; margin: 15px 0; `; section.appendChild(openaiSeparator); // LM Studio settings const lmstudioGroup = createProviderGroup('LM Studio', settings, 'lmstudio_rest', 'lmstudio'); section.appendChild(lmstudioGroup); return section; } /** * Create default LLM provider selector * @param {Object} settings - Current settings * @returns {HTMLElement} Default provider selector group */ function createDefaultProviderSelector(settings) { const group = document.createElement('div'); group.style.cssText = 'margin-bottom: 15px;'; const label = document.createElement('label'); label.textContent = 'Default LLM Provider:'; label.style.cssText = ` display: block; color: #ccc; font-size: 14px; margin-bottom: 8px; font-weight: 500; `; group.appendChild(label); const description = document.createElement('div'); description.textContent = 'Select which provider to use by default in the LLM tab and provider-switching LLM v3 nodes'; description.style.cssText = ` color: #888; font-size: 12px; margin-bottom: 10px; font-style: italic; `; group.appendChild(description); // Get current setting const defaultProviderSetting = settings['default_llm_provider']; const currentValue = defaultProviderSetting ? defaultProviderSetting.current_value : 'lmstudio_rest'; // Create radio button group const radioItems = [ { value: 'lmstudio_rest', label: 'LM Studio' }, { value: 'ollama_rest', label: 'Ollama' }, { value: 'openai', label: 'OpenAI' }, { value: 'native', label: 'Native (CLIP)' } ]; const { container: radioContainer } = createRadioGroup('default_llm_provider', radioItems, { selectedValue: currentValue, layout: 'horizontal', style: { display: 'flex', gap: '20px', marginLeft: '10px' } }); // Add data attributes to radios for settings management const radios = radioContainer.querySelectorAll('input[type="radio"]'); radios.forEach(radio => { radio.dataset.settingKey = 'default_llm_provider'; }); group.appendChild(radioContainer); return group; } /** * Create Tab Visibility settings section * @param {Object} settings - Current settings * @returns {HTMLElement} Section element */ function createTabVisibilitySection(settings) { const section = document.createElement('div'); section.style.cssText = ` padding: 15px; background: #1e1e1e; border-radius: 6px; border: 1px solid #444; `; const title = document.createElement('h3'); title.textContent = 'Sidebar Tab Visibility'; title.style.cssText = ` margin: 0 0 15px 0; color: #4CAF50; font-size: 16px; font-weight: 600; `; section.appendChild(title); const description = document.createElement('p'); description.textContent = 'Control which tabs are visible in the sidebar. Changes take effect immediately.'; description.style.cssText = ` margin: 0 0 15px 0; color: #888; font-size: 13px; font-style: italic; `; section.appendChild(description); // Create a grid for tab checkboxes const tabsGrid = document.createElement('div'); tabsGrid.style.cssText = ` display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; `; const visibilitySettings = normalizeTabVisibilitySettings(settings); // Create checkbox for each tab TAB_VISIBILITY_CONFIG.forEach((tab) => { const { container, checkbox } = createCheckbox(tab.label, { checked: visibilitySettings[tab.settingKey], id: `setting-${tab.settingKey}` }); // Add setting key to checkbox for saving later checkbox.dataset.settingKey = tab.settingKey; container.style.marginBottom = '5px'; tabsGrid.appendChild(container); }); section.appendChild(tabsGrid); return section; } /** * Create settings group for a provider * @param {string} providerName - Display name of the provider * @param {Object} settings - Current settings * @param {string} providerKey - Enable flag key suffix (e.g., ollama_rest) * @param {string} urlKeyPrefix - URL key prefix (ollama or lmstudio) * @returns {HTMLElement} Provider group element */ function createProviderGroup(providerName, settings, providerKey, urlKeyPrefix) { const group = document.createElement('div'); group.style.cssText = 'margin-bottom: 15px;'; // Enable checkbox const enableKey = `enable_${providerKey}`; const enableSetting = settings[enableKey]; const { container: enableContainer, checkbox: enableCheckbox } = createCheckbox( `Enable ${providerName}`, { checked: enableSetting ? enableSetting.current_value : true, id: `setting-${enableKey}` } ); enableCheckbox.dataset.settingKey = enableKey; group.appendChild(enableContainer); // Custom URL checkbox const useCustomUrlKey = `${urlKeyPrefix}_use_custom_url`; const useCustomUrlSetting = settings[useCustomUrlKey]; const { container: customUrlCheckboxContainer, checkbox: customUrlCheckbox } = createCheckbox( `Use custom URL for ${providerName}`, { checked: useCustomUrlSetting ? useCustomUrlSetting.current_value : false, id: `setting-${useCustomUrlKey}` } ); customUrlCheckbox.dataset.settingKey = useCustomUrlKey; customUrlCheckboxContainer.style.marginLeft = '20px'; group.appendChild(customUrlCheckboxContainer); // Custom URL input const customUrlKey = `${urlKeyPrefix}_custom_url`; const customUrlSetting = settings[customUrlKey]; const { container: customUrlInputContainer, input: customUrlInput } = createTextInput( `${providerName} URL`, customUrlSetting ? customUrlSetting.current_value : '', customUrlKey, `e.g., http://localhost:${urlKeyPrefix === 'ollama' ? '11434' : '1234'}` ); customUrlInputContainer.style.marginLeft = '40px'; customUrlInputContainer.style.marginTop = '10px'; // Show/hide URL input based on checkbox const updateUrlInputVisibility = () => { customUrlInputContainer.style.display = customUrlCheckbox.checked ? 'block' : 'none'; }; updateUrlInputVisibility(); customUrlCheckbox.addEventListener('change', updateUrlInputVisibility); group.appendChild(customUrlInputContainer); if (urlKeyPrefix === 'lmstudio' || urlKeyPrefix === 'ollama') { const tokenKey = urlKeyPrefix === 'ollama' ? 'ollama_api_key' : 'lmstudio_api_token'; const tokenLabel = urlKeyPrefix === 'ollama' ? 'Ollama API Key' : 'LM Studio API Token'; const tokenPlaceholder = urlKeyPrefix === 'ollama' ? 'Optional bearer key' : 'Optional bearer token'; const tokenSetting = settings[tokenKey]; const { container: tokenInputContainer, input: tokenInput } = createTextInput( tokenLabel, tokenSetting ? tokenSetting.current_value : '', tokenKey, tokenPlaceholder ); tokenInput.type = 'password'; tokenInput.autocomplete = 'off'; tokenInputContainer.style.marginLeft = '20px'; tokenInputContainer.style.marginTop = '10px'; group.appendChild(tokenInputContainer); } return group; } /** * Create OpenAI settings group with API key and custom URL fields * @param {Object} settings - Current settings * @returns {HTMLElement} OpenAI group element */ function createOpenAIProviderGroup(settings) { const group = document.createElement('div'); group.style.cssText = 'margin-bottom: 15px;'; const enableKey = 'enable_openai'; const enableSetting = settings[enableKey]; const { container: enableContainer, checkbox: enableCheckbox } = createCheckbox( 'Enable OpenAI', { checked: enableSetting ? enableSetting.current_value : false, id: `setting-${enableKey}` } ); enableCheckbox.dataset.settingKey = enableKey; group.appendChild(enableContainer); const useCustomUrlKey = 'openai_use_custom_url'; const useCustomUrlSetting = settings[useCustomUrlKey]; const { container: customUrlCheckboxContainer, checkbox: customUrlCheckbox } = createCheckbox( 'Use custom URL for OpenAI', { checked: useCustomUrlSetting ? useCustomUrlSetting.current_value : false, id: `setting-${useCustomUrlKey}` } ); customUrlCheckbox.dataset.settingKey = useCustomUrlKey; customUrlCheckboxContainer.style.marginLeft = '20px'; customUrlCheckboxContainer.style.marginTop = '10px'; group.appendChild(customUrlCheckboxContainer); const baseUrlKey = 'openai_base_url'; const baseUrlSetting = settings[baseUrlKey]; const { container: baseUrlInputContainer } = createTextInput( 'OpenAI Base URL', baseUrlSetting ? baseUrlSetting.current_value : '', baseUrlKey, 'e.g., https://api.openai.com' ); baseUrlInputContainer.style.marginLeft = '40px'; baseUrlInputContainer.style.marginTop = '10px'; const updateUrlInputVisibility = () => { baseUrlInputContainer.style.display = customUrlCheckbox.checked ? 'block' : 'none'; }; updateUrlInputVisibility(); customUrlCheckbox.addEventListener('change', updateUrlInputVisibility); group.appendChild(baseUrlInputContainer); const apiKeyKey = 'openai_api_key'; const apiKeySetting = settings[apiKeyKey]; const { container: apiKeyInputContainer, input: apiKeyInput } = createTextInput( 'OpenAI API Key', apiKeySetting ? apiKeySetting.current_value : '', apiKeyKey, 'sk-...' ); apiKeyInput.type = 'password'; apiKeyInput.autocomplete = 'off'; apiKeyInputContainer.style.marginLeft = '20px'; apiKeyInputContainer.style.marginTop = '10px'; group.appendChild(apiKeyInputContainer); return group; } /** * Create a text input field * @param {string} label - Label text * @param {string} value - Initial value * @param {string} settingKey - Setting key * @param {string} placeholder - Placeholder text * @returns {Object} Object with container and input elements */ function createTextInput(label, value, settingKey, placeholder = '') { const input = createInput({ type: 'text', id: `setting-${settingKey}`, value: value || '', placeholder: placeholder, style: { width: '100%', padding: '8px', background: '#2a2a2a', border: '1px solid #555', borderRadius: '4px', color: '#ccc', fontSize: '13px', boxSizing: 'border-box' } }); input.dataset.settingKey = settingKey; const container = document.createElement('div'); container.style.cssText = 'margin-bottom: 10px;'; const labelElement = document.createElement('label'); labelElement.textContent = label; labelElement.htmlFor = `setting-${settingKey}`; labelElement.style.cssText = ` display: block; color: #ccc; font-size: 13px; margin-bottom: 5px; `; container.appendChild(labelElement); container.appendChild(input); return { container, input }; } /** * Save settings * @param {Object} dialog - Dialog instance * @param {HTMLElement} container - Settings container * @param {Object} originalSettings - Original settings for reference */ async function saveSettings(dialog, container, originalSettings) { try { // Collect all setting inputs const inputs = container.querySelectorAll('[data-setting-key]'); const updates = {}; inputs.forEach(input => { const key = input.dataset.settingKey; let value; if (input.type === 'checkbox') { value = input.checked; } else if (input.type === 'radio') { // Only get value from checked radio button if (input.checked) { value = input.value; } else { // Skip unchecked radio buttons return; } } else { value = input.value; } updates[key] = value; }); console.log('Saving settings:', updates); // Send update request const response = await api.fetchApi('/sage_utils/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updates) }); if (!response.ok) { throw new Error(`Failed to save settings: ${response.statusText}`); } const result = await response.json(); console.log('Save result:', result); if (!result.success) { throw new Error(result.error || 'Failed to save settings'); } // Update ComfyUI settings to reflect the changes for (const [key, value] of Object.entries(updates)) { const settingId = SETTING_KEY_TO_ID_MAP[key]; if (settingId && app.extensionManager && app.extensionManager.setting) { try { await app.extensionManager.setting.set(settingId, value); console.log(`Updated ComfyUI setting ${settingId} to:`, value); } catch (error) { console.warn(`Could not update ComfyUI setting ${settingId}:`, error); } } } // Check if any tab visibility settings changed const tabVisibilityChanged = hasTabVisibilityUpdates(updates); // If tab visibility changed, reload the sidebar if (tabVisibilityChanged) { try { const { reloadCacheSidebar } = await import('../sidebar/cacheSidebar.js'); await reloadCacheSidebar(); console.log('Sidebar reloaded due to tab visibility changes'); } catch (error) { console.error('Failed to reload sidebar:', error); if (notifications && notifications.show) { notifications.show('Settings saved but sidebar reload failed. Please refresh the page.', 'warning'); } } } // Show success notification if (notifications && notifications.show) { notifications.show('Settings saved successfully', 'success'); } // Close dialog dialog.close(); // Apply local-only performance settings (handled after dialog close for snappy UI) try { const localInputs = container.querySelectorAll('[data-local-setting]'); const localUpdates = {}; localInputs.forEach(input => { const key = input.dataset.localSetting; if (!key) return; const value = input.type === 'checkbox' ? !!input.checked : input.value; localUpdates[key] = value; }); // Persist to localStorage and apply immediately if (Object.keys(localUpdates).length > 0) { if (typeof localUpdates.perf_monitoring === 'boolean') { localStorage.setItem('sageutils_perf_monitoring', String(localUpdates.perf_monitoring)); if (localUpdates.perf_monitoring) { javascriptTimer.enable?.(); uiTimer.enable?.(); } else { javascriptTimer.disable?.(); uiTimer.disable?.(); } } if (typeof localUpdates.send_timing === 'boolean') { localStorage.setItem('sageutils_send_timing', String(localUpdates.send_timing)); } if (typeof localUpdates.print_timing === 'boolean') { localStorage.setItem('sageutils_print_timing', String(localUpdates.print_timing)); if (localUpdates.print_timing) { // Optional: print a short report immediately for confirmation try { printTimingReport(javascriptTimer); } catch {} } } console.log('[Settings] Updated local performance settings:', localUpdates); } } catch (e) { console.warn('[Settings] Failed to apply local performance settings:', e); } } catch (error) { console.error('Error saving settings:', error); handleError(error, 'Failed to save settings'); if (notifications && notifications.show) { notifications.show('Failed to save settings: ' + error.message, 'error'); } } } /** * Create Performance & Telemetry settings section (frontend-only) * Uses localStorage flags and immediate runtime toggles; does not hit backend */ function createPerformanceSection() { const section = document.createElement('div'); section.style.cssText = ` padding: 15px; background: #1e1e1e; border-radius: 6px; border: 1px solid #444; `; const title = document.createElement('h3'); title.textContent = 'Performance & Telemetry'; title.style.cssText = ` margin: 0 0 10px 0; color: #4CAF50; font-size: 16px; font-weight: 600; `; section.appendChild(title); const note = document.createElement('div'); note.textContent = 'These options affect local performance monitoring and telemetry upload.'; note.style.cssText = ` margin: 0 0 12px 0; color: #888; font-size: 12px; font-style: italic; `; section.appendChild(note); const grid = document.createElement('div'); grid.style.cssText = ` display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px 16px; align-items: center; `; // Current values from localStorage const perfMonitoring = localStorage.getItem('sageutils_perf_monitoring') === 'true'; const sendTiming = localStorage.getItem('sageutils_send_timing') === 'true'; const printTiming = localStorage.getItem('sageutils_print_timing') === 'true'; // Enable performance monitoring { const { container, checkbox } = createCheckbox('Enable performance monitoring', { checked: perfMonitoring, id: 'perf-monitoring-checkbox' }); checkbox.dataset.localSetting = 'perf_monitoring'; grid.appendChild(container); } // Send timing to server { const { container, checkbox } = createCheckbox('Send timing telemetry to server', { checked: sendTiming, id: 'perf-sendtiming-checkbox' }); checkbox.dataset.localSetting = 'send_timing'; grid.appendChild(container); } // Print timing to console { const { container, checkbox } = createCheckbox('Print timing report to console', { checked: printTiming, id: 'perf-printtiming-checkbox' }); checkbox.dataset.localSetting = 'print_timing'; grid.appendChild(container); } section.appendChild(grid); // Action buttons row const actions = document.createElement('div'); actions.style.cssText = ` display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap; `; const btn = (label, onClick, variant = 'default') => { const b = document.createElement('button'); b.textContent = label; b.style.cssText = ` padding: 6px 10px; background: ${variant === 'primary' ? '#4CAF50' : '#2a2a2a'}; color: #fff; border: 1px solid #444; border-radius: 4px; cursor: pointer; font-size: 12px; `; b.addEventListener('click', onClick); return b; }; // Send timing now actions.appendChild(btn('Send timing now', async () => { try { await javascriptTimer.sendTimingDataToServer?.(); notifications?.show?.('Timing data sent', 'success'); } catch (e) { notifications?.show?.('Failed to send timing data', 'error'); } }, 'primary')); // Reset collected timing actions.appendChild(btn('Reset collected timing', () => { try { javascriptTimer.reset?.(); uiTimer.reset?.(); notifications?.show?.('Timing data reset', 'info'); } catch (e) { notifications?.show?.('Failed to reset timing data', 'error'); } })); // Print report now actions.appendChild(btn('Print timing report', () => { try { printTimingReport(javascriptTimer); } catch (e) { console.warn('Failed to print timing report', e); } })); section.appendChild(actions); return section; } /** * Reset all settings to defaults * @param {Object} dialog - Dialog instance */ async function resetSettings(dialog) { try { const response = await api.fetchApi('/sage_utils/settings/reset', { method: 'POST' }); if (!response.ok) { throw new Error(`Failed to reset settings: ${response.statusText}`); } const result = await response.json(); if (!result.success) { throw new Error(result.error || 'Failed to reset settings'); } // Reload the sidebar since tab visibility may have changed try { const { reloadCacheSidebar } = await import('../sidebar/cacheSidebar.js'); await reloadCacheSidebar(); console.log('Sidebar reloaded after settings reset'); } catch (error) { console.error('Failed to reload sidebar:', error); } // Show success notification if (notifications && notifications.show) { notifications.show('Settings reset to defaults', 'success'); } // Close and reopen dialog to show new values dialog.close(); setTimeout(() => showSettingsDialog(), 100); } catch (error) { handleError(error, 'Failed to reset settings'); if (notifications && notifications.show) { notifications.show('Failed to reset settings: ' + error.message, 'error'); } } }