import { app } from "../../../../../scripts/app.js"; import { api } from "../../../../../scripts/api.js"; import { $el } from "../../../../../scripts/ui.js"; import { getSvgIcon } from "../../utils/icon_utils.js"; export const optionsMethods = { displayOptions() { if (!this.contentElement) return; this.contentElement.style.overflowY = 'hidden'; const tokens = this.getStoredTokens(); const sourceDefaultsRows = this.getSearchSourceDefinitions() .filter(def => this.isSourceAvailable(def.source)) .map(def => { const iconName = this.getSearchSourceIconName(def.source); const checked = tokens.search_source_enabled?.[def.source] !== false; const inputId = `mr-options-source-${def.source.replace(/_/g, '-')}`; return ` `; }) .join(''); this.contentElement.innerHTML = `
`; const civitaiInput = this.contentElement.querySelector('#mr-options-civitai'); const civitaiSessionInput = this.contentElement.querySelector('#mr-options-civitai-session'); const hfInput = this.contentElement.querySelector('#mr-options-hf'); const braveInput = this.contentElement.querySelector('#mr-options-brave'); const civitaiToggle = this.contentElement.querySelector('#mr-options-civitai-toggle'); const civitaiSessionToggle = this.contentElement.querySelector('#mr-options-civitai-session-toggle'); const hfToggle = this.contentElement.querySelector('#mr-options-hf-toggle'); const braveToggle = this.contentElement.querySelector('#mr-options-brave-toggle'); const civitaiLimitInput = this.contentElement.querySelector('#mr-options-civitai-limit'); const civitaiUseTrpcSearchInput = this.contentElement.querySelector('#mr-options-civitai-use-trpc-search'); const civitaiUseHtmlFallbackInput = this.contentElement.querySelector('#mr-options-civitai-use-html-fallback'); const civitaiKeyCheckBtn = this.contentElement.querySelector('#mr-options-civitai-key-check'); const civitaiKeyCheckStatus = this.contentElement.querySelector('#mr-options-civitai-key-check-status'); const civitaiSessionCheckBtn = this.contentElement.querySelector('#mr-options-civitai-session-check'); const civitaiSessionCheckStatus = this.contentElement.querySelector('#mr-options-civitai-session-check-status'); const hfUseApiSearchInput = this.contentElement.querySelector('#mr-options-hf-use-api-search'); const hfUseComfyOrgFallbackInput = this.contentElement.querySelector('#mr-options-hf-use-comfy-org-fallback'); const hfUseBraveFallbackInput = this.contentElement.querySelector('#mr-options-hf-use-brave-fallback'); const autoFillBaseModelInput = this.contentElement.querySelector('#mr-options-auto-fill-base-model'); const autoFillSubfolderInput = this.contentElement.querySelector('#mr-options-auto-fill-subfolder'); const hfTokenCheckBtn = this.contentElement.querySelector('#mr-options-hf-token-check'); const hfTokenCheckStatus = this.contentElement.querySelector('#mr-options-hf-token-check-status'); const braveKeyCheckBtn = this.contentElement.querySelector('#mr-options-brave-key-check'); const braveKeyCheckStatus = this.contentElement.querySelector('#mr-options-brave-key-check-status'); const sourceEnabledInputs = Array.from(this.contentElement.querySelectorAll('.mr-options-source-enabled')); const status = this.contentElement.querySelector('#mr-options-status'); const saveBtn = this.contentElement.querySelector('#mr-options-save'); const modelListCountEl = this.contentElement.querySelector('#mr-options-model-list-count'); const modelListUpdatedEl = this.contentElement.querySelector('#mr-options-model-list-updated'); const modelListStateEl = this.contentElement.querySelector('#mr-options-model-list-state'); const modelListMessageEl = this.contentElement.querySelector('#mr-options-model-list-message'); const modelListCheckBtn = this.contentElement.querySelector('#mr-options-model-list-check'); const modelListUpdateBtn = this.contentElement.querySelector('#mr-options-model-list-update'); const baseModelsCountEl = this.contentElement.querySelector('#mr-options-base-models-count'); const baseModelsUpdatedEl = this.contentElement.querySelector('#mr-options-base-models-updated'); const baseModelsStateEl = this.contentElement.querySelector('#mr-options-base-models-state'); const baseModelsMessageEl = this.contentElement.querySelector('#mr-options-base-models-message'); const baseModelsCheckBtn = this.contentElement.querySelector('#mr-options-base-models-check'); const baseModelsUpdateBtn = this.contentElement.querySelector('#mr-options-base-models-update'); const hfIndexCountEl = this.contentElement.querySelector('#mr-options-hf-index-count'); const hfIndexUpdatedEl = this.contentElement.querySelector('#mr-options-hf-index-updated'); const hfIndexStateEl = this.contentElement.querySelector('#mr-options-hf-index-state'); const hfIndexMessageEl = this.contentElement.querySelector('#mr-options-hf-index-message'); const hfIndexRefreshBtn = this.contentElement.querySelector('#mr-options-hf-index-refresh'); const clearAllCacheBtn = this.contentElement.querySelector('#mr-options-clear-all-cache'); const clearAllCacheStatus = this.contentElement.querySelector('#mr-options-clear-cache-status'); const navButtons = Array.from(this.contentElement.querySelectorAll('.mr-options-nav-btn')); const optionSections = Array.from(this.contentElement.querySelectorAll('.mr-options-section')); const trackedInputs = [ civitaiInput, civitaiSessionInput, hfInput, braveInput, civitaiLimitInput, civitaiUseTrpcSearchInput, civitaiUseHtmlFallbackInput, hfUseApiSearchInput, hfUseComfyOrgFallbackInput, hfUseBraveFallbackInput, autoFillBaseModelInput, autoFillSubfolderInput, ...sourceEnabledInputs, ].filter(Boolean); const setStatus = (text, mode = '') => { if (!status) return; status.textContent = text; status.classList.remove('is-dirty', 'is-saved'); if (mode) status.classList.add(mode); }; const formatOptionDate = (value) => { if (!value) return 'Bundled'; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return date.toLocaleString(); }; const shortSha = (value) => value ? String(value).slice(0, 8) : ''; const setModelListBusy = (busy) => { if (modelListCheckBtn) modelListCheckBtn.disabled = busy; if (modelListUpdateBtn) modelListUpdateBtn.disabled = busy; }; const setBaseModelsBusy = (busy) => { if (baseModelsCheckBtn) baseModelsCheckBtn.disabled = busy; if (baseModelsUpdateBtn) baseModelsUpdateBtn.disabled = busy; }; const setHfIndexBusy = (busy) => { if (hfIndexRefreshBtn) hfIndexRefreshBtn.disabled = busy; }; const setTokenCheckStatus = (statusEl, text, mode = '') => { if (!statusEl) return; statusEl.textContent = text; statusEl.classList.remove('is-valid', 'is-invalid', 'is-pending'); if (mode) statusEl.classList.add(mode); }; const checkCredential = async ({ input, button, statusEl, endpoint, payloadKey, missingText }) => { const value = (input?.value || '').trim(); if (!value) { setTokenCheckStatus(statusEl, missingText, 'is-invalid'); return; } try { if (button) button.disabled = true; setTokenCheckStatus(statusEl, 'Checking...', 'is-pending'); const response = await api.fetchApi(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ [payloadKey]: value }) }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || `Check failed: ${response.status}`); } setTokenCheckStatus( statusEl, data.message || (data.valid ? 'Valid' : 'Invalid'), data.valid ? 'is-valid' : 'is-invalid' ); } catch (error) { console.error('Model Resolver: Credential check error:', error); setTokenCheckStatus(statusEl, error.message || 'Check failed', 'is-invalid'); } finally { if (button) button.disabled = false; } }; const renderModelListStatus = (data = {}, checkedRemote = false) => { if (modelListCountEl) { modelListCountEl.textContent = Number.isFinite(Number(data.local_count)) ? Number(data.local_count).toLocaleString() : '-'; } if (modelListUpdatedEl) { modelListUpdatedEl.textContent = formatOptionDate(data.local_updated_at); } if (modelListStateEl) { if (checkedRemote && data.can_compare && data.update_available) { modelListStateEl.textContent = 'Update available'; } else if (checkedRemote && data.can_compare) { modelListStateEl.textContent = 'Up to date'; } else if (checkedRemote && !data.can_compare) { modelListStateEl.textContent = 'Remote checked'; } else { modelListStateEl.textContent = data.local_sha ? 'Tracked' : 'Bundled'; } } if (modelListMessageEl) { const localSha = shortSha(data.local_sha); const remoteSha = shortSha(data.remote_sha); if (checkedRemote && data.update_available) { modelListMessageEl.textContent = `A newer ComfyUI-Manager model-list.json is available (${localSha || 'local'} -> ${remoteSha}).`; } else if (checkedRemote && data.can_compare) { modelListMessageEl.textContent = `Local Database is current (${localSha}).`; } else if (checkedRemote) { modelListMessageEl.textContent = `Remote file was checked. Local SHA is not recorded yet; run update once to track future changes.`; } else { modelListMessageEl.textContent = `Local Database uses ComfyUI-Manager model-list.json.`; } } }; const loadModelListStatus = async (checkRemote = false) => { try { setModelListBusy(true); if (modelListStateEl) { modelListStateEl.textContent = checkRemote ? 'Checking GitHub...' : 'Loading...'; } const response = await api.fetchApi(`/model_resolver/model-list/status${checkRemote ? '?check_remote=1' : ''}`); if (!response.ok) { throw new Error(`Status failed: ${response.status}`); } const data = await response.json(); renderModelListStatus(data, checkRemote); return data; } catch (error) { console.error('Model Resolver: model-list status error:', error); if (modelListStateEl) modelListStateEl.textContent = 'Error'; if (modelListMessageEl) modelListMessageEl.textContent = error.message || 'Failed to read Local Database status.'; return null; } finally { setModelListBusy(false); } }; const updateModelList = async () => { try { setModelListBusy(true); if (modelListStateEl) modelListStateEl.textContent = 'Updating...'; if (modelListMessageEl) modelListMessageEl.textContent = 'Downloading latest ComfyUI-Manager model-list.json...'; const response = await api.fetchApi('/model_resolver/model-list/update', { method: 'POST' }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || `Update failed: ${response.status}`); } await this.clearSearchCaches(); renderModelListStatus(data, false); if (modelListStateEl) modelListStateEl.textContent = 'Updated'; if (modelListMessageEl) modelListMessageEl.textContent = `Local Database updated. ${Number(data.local_count || 0).toLocaleString()} models loaded.`; this.showNotification('Local Database updated', 'success'); } catch (error) { console.error('Model Resolver: model-list update error:', error); if (modelListStateEl) modelListStateEl.textContent = 'Error'; if (modelListMessageEl) modelListMessageEl.textContent = error.message || 'Failed to update Local Database.'; this.showNotification('Local Database update failed', 'error'); } finally { setModelListBusy(false); } }; const renderBaseModelsStatus = (data = {}, checkedRemote = false) => { if (baseModelsCountEl) { baseModelsCountEl.textContent = Number.isFinite(Number(data.local_count)) ? Number(data.local_count).toLocaleString() : '-'; } if (baseModelsUpdatedEl) { baseModelsUpdatedEl.textContent = formatOptionDate(data.local_updated_at); } if (baseModelsStateEl) { if (checkedRemote && data.update_available) { baseModelsStateEl.textContent = 'Update available'; } else if (checkedRemote) { baseModelsStateEl.textContent = 'Up to date'; } else { baseModelsStateEl.textContent = data.local_updated_at ? 'Tracked' : 'Bundled'; } } if (baseModelsMessageEl) { if (checkedRemote && data.update_available) { baseModelsMessageEl.textContent = `New base models are available from CivitAI.`; } else if (checkedRemote) { baseModelsMessageEl.textContent = `Base Models list is current.`; } else { baseModelsMessageEl.textContent = `Base Models list maps new CivitAI models to local categories.`; } } }; const loadBaseModelsStatus = async (checkRemote = false) => { try { setBaseModelsBusy(true); if (baseModelsStateEl) { baseModelsStateEl.textContent = checkRemote ? 'Checking CivitAI...' : 'Loading...'; } const response = await api.fetchApi(`/model_resolver/base-models/status${checkRemote ? '?check_remote=1' : ''}`); if (!response.ok) { throw new Error(`Status failed: ${response.status}`); } const data = await response.json(); renderBaseModelsStatus(data, checkRemote); return data; } catch (error) { console.error('Model Resolver: base-models status error:', error); if (baseModelsStateEl) baseModelsStateEl.textContent = 'Error'; if (baseModelsMessageEl) baseModelsMessageEl.textContent = error.message || 'Failed to read Base Models status.'; return null; } finally { setBaseModelsBusy(false); } }; const updateBaseModels = async () => { try { setBaseModelsBusy(true); if (baseModelsStateEl) baseModelsStateEl.textContent = 'Updating...'; if (baseModelsMessageEl) baseModelsMessageEl.textContent = 'Downloading latest base models from CivitAI...'; const response = await api.fetchApi('/model_resolver/base-models/update', { method: 'POST' }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || `Update failed: ${response.status}`); } await this.clearSearchCaches(); // Invalidate the in-memory cache so the base model dropdown // reloads fresh data from the server next time it is opened. this.baseModels = null; await this.ensureBaseModelsLoaded(); renderBaseModelsStatus(data, false); if (baseModelsStateEl) baseModelsStateEl.textContent = 'Updated'; if (baseModelsMessageEl) baseModelsMessageEl.textContent = `Base Models updated. ${Number(data.local_count || 0).toLocaleString()} base models loaded.`; this.showNotification('Base Models updated', 'success'); } catch (error) { console.error('Model Resolver: base-models update error:', error); if (baseModelsStateEl) baseModelsStateEl.textContent = 'Error'; if (baseModelsMessageEl) baseModelsMessageEl.textContent = error.message || 'Failed to update Base Models.'; this.showNotification('Base Models update failed', 'error'); } finally { setBaseModelsBusy(false); } }; const renderHfIndexStatus = (data = {}) => { if (hfIndexCountEl) { hfIndexCountEl.textContent = Number.isFinite(Number(data.file_count)) ? Number(data.file_count).toLocaleString() : '-'; } if (hfIndexUpdatedEl) { hfIndexUpdatedEl.textContent = data.updated_at ? formatOptionDate(Number(data.updated_at) * 1000) : 'Never'; } if (hfIndexStateEl) { if (!data.exists) { hfIndexStateEl.textContent = 'Not cached'; } else if (data.stale) { hfIndexStateEl.textContent = 'Refresh due'; } else { hfIndexStateEl.textContent = 'Cached'; } } if (hfIndexMessageEl) { if (!data.exists) { hfIndexMessageEl.textContent = 'The first Comfy-Org fallback search will build the local HuggingFace file index.'; } else if (data.stale) { hfIndexMessageEl.textContent = `Index has ${Number(data.repo_count || 0).toLocaleString()} repos and should be refreshed.`; } else { hfIndexMessageEl.textContent = `Index has ${Number(data.repo_count || 0).toLocaleString()} repos and is used before per-repo fallback checks.`; } } }; const loadHfIndexStatus = async () => { try { setHfIndexBusy(true); if (hfIndexStateEl) hfIndexStateEl.textContent = 'Loading...'; const response = await api.fetchApi('/model_resolver/huggingface/author-index/status'); if (!response.ok) { throw new Error(`Status failed: ${response.status}`); } const data = await response.json(); renderHfIndexStatus(data); return data; } catch (error) { console.error('Model Resolver: HuggingFace index status error:', error); if (hfIndexStateEl) hfIndexStateEl.textContent = 'Error'; if (hfIndexMessageEl) hfIndexMessageEl.textContent = error.message || 'Failed to read HuggingFace index status.'; return null; } finally { setHfIndexBusy(false); } }; const refreshHfIndex = async () => { try { setHfIndexBusy(true); if (hfIndexStateEl) hfIndexStateEl.textContent = 'Refreshing...'; if (hfIndexMessageEl) hfIndexMessageEl.textContent = 'Downloading Comfy-Org file index from HuggingFace...'; const response = await api.fetchApi('/model_resolver/huggingface/author-index/refresh', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ hf_token: hfInput?.value || '' }) }); const data = await response.json(); if (!response.ok || data.success === false) { throw new Error(data.error || `Refresh failed: ${response.status}`); } await this.clearSearchCaches(); renderHfIndexStatus(data); if (hfIndexStateEl) hfIndexStateEl.textContent = 'Refreshed'; this.showNotification('HuggingFace Comfy-Org index refreshed', 'success'); } catch (error) { console.error('Model Resolver: HuggingFace index refresh error:', error); if (hfIndexStateEl) hfIndexStateEl.textContent = 'Error'; if (hfIndexMessageEl) hfIndexMessageEl.textContent = error.message || 'Failed to refresh HuggingFace index.'; this.showNotification('HuggingFace index refresh failed', 'error'); } finally { setHfIndexBusy(false); } }; const clearAllCachesFromOptions = async () => { try { if (clearAllCacheBtn) clearAllCacheBtn.disabled = true; if (clearAllCacheStatus) clearAllCacheStatus.textContent = 'Clearing frontend and backend cache...'; await this.clearAllResolverCaches(); if (clearAllCacheStatus) clearAllCacheStatus.textContent = 'Frontend and backend cache cleared.'; this.showNotification('Resolver cache cleared', 'success'); if (this.activeTab === 'options') { this.displayOptions(); } } catch (error) { console.error('Model Resolver: clear all cache error:', error); if (clearAllCacheStatus) clearAllCacheStatus.textContent = error.message || 'Failed to clear cache.'; this.showNotification('Cache clear failed', 'error'); } finally { if (clearAllCacheBtn) clearAllCacheBtn.disabled = false; } }; const setActiveNav = (targetId) => { navButtons.forEach((btn) => { btn.classList.toggle('is-active', btn.dataset.target === targetId); }); }; const setVisibleSection = (targetId) => { optionSections.forEach((section) => { section.classList.toggle('is-hidden', section.id !== targetId); }); setActiveNav(targetId); }; const getVisibilityIcon = (visible) => visible ? getSvgIcon('eye') : getSvgIcon('eyeOff'); const syncVisibilityToggle = (input, button) => { if (!input || !button) return; const visible = input.type === 'text'; button.innerHTML = getVisibilityIcon(visible); button.classList.toggle('mr-is-active-text', visible); button.classList.toggle('mr-is-muted', !visible); button.setAttribute('aria-pressed', visible ? 'true' : 'false'); this.setTooltip(button, visible ? 'Hide saved value' : 'Show saved value'); }; const bindVisibilityToggle = (input, button) => { if (!input || !button) return; syncVisibilityToggle(input, button); button.addEventListener('click', () => { input.type = input.type === 'password' ? 'text' : 'password'; syncVisibilityToggle(input, button); }); }; bindVisibilityToggle(civitaiInput, civitaiToggle); bindVisibilityToggle(civitaiSessionInput, civitaiSessionToggle); bindVisibilityToggle(hfInput, hfToggle); bindVisibilityToggle(braveInput, braveToggle); setStatus('Saved only on this machine.'); setVisibleSection('mr-options-section-sources'); trackedInputs.forEach((input) => { const eventName = input.type === 'checkbox' ? 'change' : 'input'; input.addEventListener(eventName, () => { setStatus('You have unsaved changes.', 'is-dirty'); }); }); this.bindTooltips(this.contentElement); navButtons.forEach((btn) => { btn.addEventListener('click', () => { const targetId = btn.dataset.target; if (!targetId) return; setVisibleSection(targetId); if (targetId === 'mr-options-section-local-db') { loadModelListStatus(false); loadBaseModelsStatus(false); } else if (targetId === 'mr-options-section-hf') { loadHfIndexStatus(); } }); }); if (modelListCheckBtn) { modelListCheckBtn.addEventListener('click', () => { loadModelListStatus(true); }); } if (modelListUpdateBtn) { modelListUpdateBtn.addEventListener('click', () => { updateModelList(); }); } if (baseModelsCheckBtn) { baseModelsCheckBtn.addEventListener('click', () => { loadBaseModelsStatus(true); }); } if (baseModelsUpdateBtn) { baseModelsUpdateBtn.addEventListener('click', () => { updateBaseModels(); }); } if (hfIndexRefreshBtn) { hfIndexRefreshBtn.addEventListener('click', () => { refreshHfIndex(); }); } if (clearAllCacheBtn) { clearAllCacheBtn.addEventListener('click', () => { clearAllCachesFromOptions(); }); } if (civitaiSessionCheckBtn) { civitaiSessionCheckBtn.addEventListener('click', () => { checkCredential({ input: civitaiSessionInput, button: civitaiSessionCheckBtn, statusEl: civitaiSessionCheckStatus, endpoint: '/model_resolver/civitai/session-token/check', payloadKey: 'civitai_session_token', missingText: 'Paste token first' }); }); } if (civitaiKeyCheckBtn) { civitaiKeyCheckBtn.addEventListener('click', () => { checkCredential({ input: civitaiInput, button: civitaiKeyCheckBtn, statusEl: civitaiKeyCheckStatus, endpoint: '/model_resolver/civitai/api-key/check', payloadKey: 'civitai_key', missingText: 'Paste API key first' }); }); } if (hfTokenCheckBtn) { hfTokenCheckBtn.addEventListener('click', () => { checkCredential({ input: hfInput, button: hfTokenCheckBtn, statusEl: hfTokenCheckStatus, endpoint: '/model_resolver/huggingface/token/check', payloadKey: 'hf_token', missingText: 'Paste token first' }); }); } if (braveKeyCheckBtn) { braveKeyCheckBtn.addEventListener('click', () => { checkCredential({ input: braveInput, button: braveKeyCheckBtn, statusEl: braveKeyCheckStatus, endpoint: '/model_resolver/brave/api-key/check', payloadKey: 'brave_search_api_key', missingText: 'Paste API key first' }); }); } loadModelListStatus(false); loadBaseModelsStatus(false); loadHfIndexStatus(); if (saveBtn) { saveBtn.addEventListener('click', async () => { const civitaiCandidateLimitRaw = parseInt(civitaiLimitInput?.value || `${tokens.civitai_candidate_limit}`, 10); const civitaiCandidateLimit = Number.isFinite(civitaiCandidateLimitRaw) ? Math.min(20, Math.max(1, civitaiCandidateLimitRaw)) : 5; // Collect source-enabled flags const sourceEnabled = {}; if (sourceEnabledInputs.length && !sourceEnabledInputs.some(input => input.checked)) { const localInput = sourceEnabledInputs.find(input => input.dataset.source === 'local'); if (localInput) localInput.checked = true; } sourceEnabledInputs.forEach((input) => { const key = input.dataset.storageKey; if (key) { sourceEnabled[key] = input.checked; } }); const newSettings = { civitai_key: civitaiInput?.value || '', civitai_session_token: civitaiSessionInput?.value || '', civitai_use_trpc_search: Boolean(civitaiUseTrpcSearchInput?.checked), civitai_use_html_fallback: Boolean(civitaiUseHtmlFallbackInput?.checked), civitai_candidate_limit: civitaiCandidateLimit, hf_token: hfInput?.value || '', hf_use_api_search: Boolean(hfUseApiSearchInput?.checked), hf_use_comfy_org_fallback: Boolean(hfUseComfyOrgFallbackInput?.checked), hf_use_brave_fallback: Boolean(hfUseBraveFallbackInput?.checked), auto_fill_base_model: Boolean(autoFillBaseModelInput?.checked), auto_fill_subfolder: Boolean(autoFillSubfolderInput?.checked), brave_search_api_key: braveInput?.value || '', search_source_enabled: sourceEnabled, }; // 1. Write to localStorage (fast local cache) localStorage.setItem('ModelResolver.civitaiApiKey', newSettings.civitai_key); localStorage.setItem('ModelResolver.civitaiSessionToken', newSettings.civitai_session_token); localStorage.setItem('ModelResolver.civitaiUseTrpcSearch', newSettings.civitai_use_trpc_search ? 'true' : 'false'); localStorage.setItem('ModelResolver.civitaiUseHtmlFallback', newSettings.civitai_use_html_fallback ? 'true' : 'false'); localStorage.setItem('ModelResolver.huggingFaceToken', newSettings.hf_token); localStorage.setItem('ModelResolver.braveSearchApiKey', newSettings.brave_search_api_key); localStorage.setItem('ModelResolver.hfUseApiSearch', newSettings.hf_use_api_search ? 'true' : 'false'); localStorage.setItem('ModelResolver.hfUseComfyOrgFallback', newSettings.hf_use_comfy_org_fallback ? 'true' : 'false'); localStorage.setItem('ModelResolver.hfUseBraveFallback', newSettings.hf_use_brave_fallback ? 'true' : 'false'); localStorage.setItem('ModelResolver.autoFillBaseModel', newSettings.auto_fill_base_model ? 'true' : 'false'); localStorage.setItem('ModelResolver.autoFillSubfolder', newSettings.auto_fill_subfolder ? 'true' : 'false'); localStorage.setItem('ModelResolver.civitaiCandidateLimit', `${civitaiCandidateLimit}`); Object.entries(sourceEnabled).forEach(([key, val]) => { localStorage.setItem(key, val ? 'true' : 'false'); }); if (civitaiLimitInput) civitaiLimitInput.value = `${civitaiCandidateLimit}`; // 2. Persist to server (survives new browsers / profiles) try { setStatus('Saving…', ''); const resp = await api.fetchApi('/model_resolver/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(newSettings), }); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); setStatus('Saved on this machine (all browsers).', 'is-saved'); this.showNotification('Options saved', 'success'); } catch (err) { console.warn('Model Resolver: could not save settings to server, kept locally only.', err); setStatus('Saved locally only (server write failed).', 'is-saved'); this.showNotification('Options saved locally only', 'warning'); } await this.clearSearchCaches(); }); } } };