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 = `

Download Sources

${sourceDefaultsRows}

CivitAI

Not checked
How to find the session token
  1. Sign in on civitai.com in your browser.
  2. Open DevTools, then Application or Storage.
  3. Open Cookies for civitai.com and copy __Secure-civitai-token.

Treat it like a password. It is only needed when session-aware CivitAI search is required.

Not checked
CivitAI Models To Inspect ?

HuggingFace

Not checked
Comfy-Org files Loading...
Last refresh Loading...
Status Checking local index...
Comfy-Org fallback uses a cached HuggingFace file index.
Not checked
`; 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(); }); } } };