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"; import { LOG_LEVEL as DEFAULT_FRONTEND_LOG_LEVEL } from "../../log_system/config.js"; import { logger as frontendLogger } from "../../log_system/logger.js"; export const optionsMethods = { displayOptions() { if (!this.contentElement) return; this.contentElement.style.overflowY = 'hidden'; const tokens = this.getStoredTokens(); const pathTemplateCategories = this.getDownloadPathTemplateCategoryDefinitions(); const defaultRootCategories = this.getDefaultRootCategoryDefinitions(); const pathTemplatePresets = this.getDownloadPathTemplatePresetDefinitions(); const pathTemplatePresetValues = new Set(pathTemplatePresets.map(preset => preset.value)); const renderPathModeOptions = (selectedValue) => { const selected = this.normalizeDownloadPathMode(selectedValue); const options = [ { value: 'suggested', label: 'Suggest existing folders', tooltip: 'Uses current Model Resolver behavior: it fills an existing matching folder when possible.' }, { value: 'template', label: 'Use path templates', tooltip: 'Creates subfolders from template placeholders such as base model, author, tags, and model name.' }, { value: 'manual', label: 'Manual only', tooltip: 'Leaves the subfolder field empty until you type or pick a folder.' } ]; return options .map(option => ``) .join(''); }; const renderDownloadBackendOptions = (selectedValue) => { const selected = this.normalizeDownloadBackend(selectedValue); const options = [ { value: 'python', label: 'Python (built-in)', tooltip: 'Uses Model Resolver built-in downloader. This remains the default and requires no external tools.' }, { value: 'aria2', label: 'aria2 (experimental)', tooltip: 'Uses an external aria2c process for large downloads, pause/resume, and segmented transfers.' } ]; return options .map(option => ``) .join(''); }; const renderTemplatePlaceholderBadges = () => ['{base_model}', '{author}', '{first_tag}', '{model_name}', '{version_name}'] .map(placeholder => `${this.escapeHtml(placeholder)}`) .join(''); const renderPathTemplateRows = () => pathTemplateCategories .map(category => { const template = tokens.download_path_templates?.[category.key] ?? this.getDefaultDownloadPathTemplates()[category.key] ?? ''; const normalizedTemplate = this.normalizeDownloadPathTemplate(template); const isPreset = pathTemplatePresetValues.has(normalizedTemplate); const selectedValue = isPreset ? normalizedTemplate : 'custom'; const options = [ ...pathTemplatePresets.map(preset => ( `` )), `` ].join(''); const preview = this.calculateDownloadPathTemplateSubfolder(category.key, { base_model: 'Flux.1 D', tags: ['style', 'character'], author: 'example_author', model_name: 'Example Model', version_name: 'v1' }) || '(flat folder)'; return `
Preview: ${this.escapeHtml(preview)}
`; }) .join(''); const renderDefaultRootRows = () => defaultRootCategories .map(category => { const value = tokens[category.settingKey] || ''; return `
`; }) .join(''); 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(''); const logLevelValues = ['DEBUG', 'INFO', 'WARN', 'ERROR', 'NONE']; const normalizeLogLevel = (value) => { const normalized = String(value || '').trim().toUpperCase(); return logLevelValues.includes(normalized) ? normalized : 'DEBUG'; }; const renderLogLevelOptions = (selectedValue) => { const selected = normalizeLogLevel(selectedValue); return logLevelValues .map(level => ``) .join(''); }; this.contentElement.innerHTML = `

Download Sources

Download Engine
?
Download backend
aria2c path ?
Backend aria2
Status Not checked
Daemon Not checked
Setup guide
Search 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 autoRefreshComfyModelsInput = this.contentElement.querySelector('#mr-options-auto-refresh-comfy-models'); const downloadBackendInput = this.contentElement.querySelector('#mr-options-download-backend'); const aria2cPathInput = this.contentElement.querySelector('#mr-options-aria2c-path'); const aria2InstallBtn = this.contentElement.querySelector('#mr-options-aria2-install'); const aria2CheckBtn = this.contentElement.querySelector('#mr-options-aria2-check'); const aria2StopBtn = this.contentElement.querySelector('#mr-options-aria2-stop'); const aria2StatusEl = this.contentElement.querySelector('#mr-options-aria2-status'); const aria2BackendStateEl = this.contentElement.querySelector('#mr-options-aria2-backend-state'); const aria2AvailabilityEl = this.contentElement.querySelector('#mr-options-aria2-availability'); const aria2DaemonEl = this.contentElement.querySelector('#mr-options-aria2-daemon'); const aria2AutoStopInput = this.contentElement.querySelector('#mr-options-aria2-auto-stop'); const aria2SettingEls = Array.from(this.contentElement.querySelectorAll('.mr-options-aria2-setting')); const downloadPathModeInput = this.contentElement.querySelector('#mr-options-download-path-mode'); const defaultRootSelectInputs = Array.from(this.contentElement.querySelectorAll('.mr-options-default-root')); const templatePresetInputs = Array.from(this.contentElement.querySelectorAll('.mr-options-template-preset')); const templateCustomInputs = Array.from(this.contentElement.querySelectorAll('.mr-options-template-custom')); const detectTemplatesBtn = this.contentElement.querySelector('#mr-options-detect-templates'); const templateDetectStatus = this.contentElement.querySelector('#mr-options-template-detect-status'); const baseModelMappingsContainer = this.contentElement.querySelector('#mr-options-base-model-mappings'); const addBaseModelMappingBtn = this.contentElement.querySelector('#mr-options-add-base-model-mapping'); const frontendLogsEnabledInput = this.contentElement.querySelector('#mr-options-frontend-logs-enabled'); const backendLogsEnabledInput = this.contentElement.querySelector('#mr-options-backend-logs-enabled'); const frontendLogLevelInput = this.contentElement.querySelector('#mr-options-frontend-log-level'); const backendLogLevelInput = this.contentElement.querySelector('#mr-options-backend-log-level'); const exportFrontendLogsBtn = this.contentElement.querySelector('#mr-options-export-frontend-logs'); const exportBackendLogsBtn = this.contentElement.querySelector('#mr-options-export-backend-logs'); const exportFrontendLogsStatus = this.contentElement.querySelector('#mr-options-export-frontend-logs-status'); const exportBackendLogsStatus = this.contentElement.querySelector('#mr-options-export-backend-logs-status'); 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, autoRefreshComfyModelsInput, downloadBackendInput, aria2cPathInput, aria2AutoStopInput, downloadPathModeInput, ...defaultRootSelectInputs, ...templatePresetInputs, ...templateCustomInputs, frontendLogsEnabledInput, backendLogsEnabledInput, frontendLogLevelInput, backendLogLevelInput, ...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 setExportLogsStatus = (statusEl, text) => { if (statusEl) statusEl.textContent = text; }; const parseDownloadFilename = (header, fallback) => { const value = String(header || ''); const utf8Match = value.match(/filename\*=UTF-8''([^;]+)/i); if (utf8Match?.[1]) { try { return decodeURIComponent(utf8Match[1].trim().replace(/^"|"$/g, '')); } catch (error) { return utf8Match[1].trim().replace(/^"|"$/g, '') || fallback; } } const match = value.match(/filename="?([^";]+)"?/i); return match?.[1]?.trim() || fallback; }; const downloadBlob = (blob, filename) => { const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); }; const exportFrontendLogs = () => { try { if (exportFrontendLogsBtn) exportFrontendLogsBtn.disabled = true; const exported = frontendLogger.exportLogs('txt'); if (!exported) { setExportLogsStatus(exportFrontendLogsStatus, 'No frontend logs captured yet.'); this.showNotification('No frontend logs to export', 'warning'); return; } setExportLogsStatus(exportFrontendLogsStatus, 'Frontend logs exported.'); this.showNotification('Frontend logs exported', 'success'); } catch (error) { console.error('Model Resolver: frontend log export failed:', error); setExportLogsStatus(exportFrontendLogsStatus, error.message || 'Frontend log export failed.'); this.showNotification('Frontend log export failed', 'error'); } finally { if (exportFrontendLogsBtn) exportFrontendLogsBtn.disabled = false; } }; const exportBackendLogs = async () => { try { if (exportBackendLogsBtn) exportBackendLogsBtn.disabled = true; setExportLogsStatus(exportBackendLogsStatus, 'Preparing backend logs...'); const response = await api.fetchApi('/model_resolver/logs/backend/export'); if (!response.ok) { let errorMsg = `Server returned ${response.status}: ${response.statusText}`; try { const errData = await response.json(); if (errData?.error) errorMsg = errData.error; } catch (_) { } throw new Error(errorMsg); } const blob = await response.blob(); const filename = parseDownloadFilename( response.headers.get('Content-Disposition'), `model_resolver_backend_logs_${new Date().toISOString().replace(/[:.]/g, '-')}.txt` ); downloadBlob(blob, filename); setExportLogsStatus(exportBackendLogsStatus, 'Backend logs exported.'); this.showNotification('Backend logs exported', 'success'); } catch (error) { console.error('Model Resolver: backend log export failed:', error); setExportLogsStatus(exportBackendLogsStatus, error.message || 'Backend log export failed.'); this.showNotification('Backend log export failed', 'error'); } finally { if (exportBackendLogsBtn) exportBackendLogsBtn.disabled = false; } }; 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 setAria2Status = (text, mode = '') => { if (!aria2StatusEl) return; aria2StatusEl.hidden = !text; aria2StatusEl.textContent = text; aria2StatusEl.classList.remove('is-valid', 'is-invalid', 'is-pending'); if (mode) aria2StatusEl.classList.add(mode); }; const setAria2SummaryValue = (element, text, mode = '') => { if (!element) return; element.textContent = text; element.classList.remove('is-valid', 'is-invalid', 'is-pending'); if (mode) element.classList.add(mode); }; const syncAria2DaemonButton = (data = {}) => { if (!aria2StopBtn) return; const available = data.available === true; const running = Boolean(data.running); aria2StopBtn.hidden = !available; aria2StopBtn.dataset.action = running ? 'stop' : 'start'; aria2StopBtn.textContent = running ? 'Stop' : 'Start'; aria2StopBtn.disabled = running && data.can_stop === false; aria2StopBtn.title = aria2StopBtn.disabled ? 'Cannot stop while aria2 has active downloads' : (running ? 'Stop aria2 daemon started by Model Resolver' : 'Start aria2 daemon now'); }; const renderAria2Summary = (data = {}, state = 'idle') => { const backend = this.normalizeDownloadBackend(data.backend || downloadBackendInput?.value || tokens.download_backend); const version = String(data.version || '').trim(); const backendLabel = backend === 'aria2' ? `aria2${version ? ` ${version}` : ''}` : 'Python'; setAria2SummaryValue(aria2BackendStateEl, backendLabel); if (state === 'checking') { syncAria2DaemonButton({ available: false, running: false }); setAria2SummaryValue(aria2AvailabilityEl, 'Checking...', 'is-pending'); setAria2SummaryValue(aria2DaemonEl, 'Checking...', 'is-pending'); return; } if (state === 'installing') { syncAria2DaemonButton({ available: false, running: false }); setAria2SummaryValue(aria2AvailabilityEl, 'Installing...', 'is-pending'); setAria2SummaryValue(aria2DaemonEl, 'Not started'); return; } if (data && typeof data.available === 'boolean') { syncAria2DaemonButton(data); setAria2SummaryValue( aria2AvailabilityEl, data.available ? 'Available' : 'Not found', data.available ? 'is-valid' : 'is-invalid' ); setAria2SummaryValue( aria2DaemonEl, data.running ? 'Running' : 'Starts on first download', data.running ? 'is-valid' : '' ); return; } syncAria2DaemonButton({ available: false, running: false }); setAria2SummaryValue(aria2AvailabilityEl, 'Not checked'); setAria2SummaryValue(aria2DaemonEl, 'Not checked'); }; const syncAria2OptionsVisibility = () => { const backend = this.normalizeDownloadBackend(downloadBackendInput?.value || tokens.download_backend); const showAria2 = backend === 'aria2'; aria2SettingEls.forEach((element) => { element.hidden = !showAria2; }); renderAria2Summary({ backend }); }; const checkAria2Status = async ({ useUnsavedPath = true } = {}) => { if (aria2CheckBtn) aria2CheckBtn.disabled = true; setAria2Status(''); renderAria2Summary({ backend: 'aria2' }, 'checking'); try { const data = await this.fetchJson('/model_resolver/aria2/status', { method: 'POST', body: JSON.stringify({ aria2c_path: useUnsavedPath ? (aria2cPathInput?.value || '') : (tokens.aria2c_path || '') }), silent: true }, 'Check aria2'); renderAria2Summary({ ...data, backend: downloadBackendInput?.value || data.backend }); if (data?.available) { setAria2Status(''); } else { setAria2Status(data?.error || 'aria2c was not found. Install aria2 or configure the aria2c path.', 'is-invalid'); } } catch (error) { setAria2Status(error.message || 'aria2 check failed.', 'is-invalid'); renderAria2Summary({ backend: 'aria2', configured_path: aria2cPathInput?.value || '', resolved_path: '', available: false, running: false }); } finally { if (aria2CheckBtn) aria2CheckBtn.disabled = false; } }; const installAria2 = async () => { if (aria2InstallBtn) aria2InstallBtn.disabled = true; if (aria2CheckBtn) aria2CheckBtn.disabled = true; setAria2Status(''); renderAria2Summary({ backend: 'aria2' }, 'installing'); try { const data = await this.fetchJson('/model_resolver/aria2/install', { method: 'POST', body: JSON.stringify({}), silent: true }, 'Install aria2'); if (!data?.success || !data?.aria2c_path) { throw new Error(data?.error || 'aria2 install did not return an executable path.'); } if (aria2cPathInput) { aria2cPathInput.value = data.aria2c_path; aria2cPathInput.dispatchEvent(new Event('input', { bubbles: true })); } if (downloadBackendInput) { downloadBackendInput.value = 'aria2'; downloadBackendInput.dispatchEvent(new Event('change', { bubbles: true })); } tokens.aria2c_path = data.aria2c_path; tokens.download_backend = 'aria2'; localStorage.setItem('ModelResolver.aria2cPath', data.aria2c_path); localStorage.setItem('ModelResolver.downloadBackend', 'aria2'); setAria2Status(''); renderAria2Summary({ backend: 'aria2', configured_path: data.aria2c_path, resolved_path: data.aria2c_path, available: true, version: data.version, running: false }); await checkAria2Status({ useUnsavedPath: true }); } catch (error) { setAria2Status(error.message || 'aria2 install failed.', 'is-invalid'); renderAria2Summary({ backend: 'aria2', configured_path: aria2cPathInput?.value || '', resolved_path: '', available: false, running: false }); } finally { if (aria2InstallBtn) aria2InstallBtn.disabled = false; if (aria2CheckBtn) aria2CheckBtn.disabled = false; } }; const requestAria2Control = async (endpoint, { method = 'POST', body = {} } = {}) => { const response = await api.fetchApi(endpoint, { method, headers: { 'Content-Type': 'application/json' }, ...(method === 'POST' ? { body: JSON.stringify(body) } : {}) }); if (!response.ok) { let errorMsg = `Server returned ${response.status}: ${response.statusText}`; try { const errData = await response.json(); if (errData?.error) errorMsg = errData.error; } catch (_) { } const error = new Error(errorMsg); error.status = response.status; throw error; } return response.json(); }; const startAria2 = async () => { if (aria2StopBtn) aria2StopBtn.disabled = true; setAria2Status(''); setAria2SummaryValue(aria2DaemonEl, 'Starting...', 'is-pending'); let refreshed = false; try { const result = await requestAria2Control('/model_resolver/aria2/start', { body: { aria2c_path: aria2cPathInput?.value || '' } }); if (!result?.success) { throw new Error(result?.error || 'Could not start aria2 daemon.'); } await checkAria2Status({ useUnsavedPath: true }); refreshed = true; } catch (error) { const message = error.message || 'aria2 start failed.'; await checkAria2Status({ useUnsavedPath: true }); refreshed = true; setAria2Status(message, 'is-invalid'); } finally { if (!refreshed && aria2StopBtn) aria2StopBtn.disabled = false; } }; const stopAria2 = async () => { if (aria2StopBtn) aria2StopBtn.disabled = true; setAria2Status(''); setAria2SummaryValue(aria2DaemonEl, 'Stopping...', 'is-pending'); let refreshed = false; try { let result; try { result = await requestAria2Control('/model_resolver/aria2/stop'); } catch (error) { if (error?.status !== 405) throw error; result = await requestAria2Control('/model_resolver/aria2/stop', { method: 'GET' }); } if (!result?.success) { throw new Error(result?.error || 'Could not stop aria2 daemon.'); } await checkAria2Status({ useUnsavedPath: true }); refreshed = true; } catch (error) { const message = error.message || 'aria2 stop failed.'; await checkAria2Status({ useUnsavedPath: true }); refreshed = true; setAria2Status(message, 'is-invalid'); } finally { if (!refreshed && aria2StopBtn) aria2StopBtn.disabled = false; } }; 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 data = await this.fetchJson(endpoint, { method: 'POST', body: JSON.stringify({ [payloadKey]: value }) }, 'Check credential'); 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 data = await this.fetchJson(`/model_resolver/model-list/status${checkRemote ? '?check_remote=1' : ''}`, {}, 'Load model list status'); 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 data = await this.fetchJson('/model_resolver/model-list/update', { method: 'POST' }, 'Update model list'); 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 data = await this.fetchJson(`/model_resolver/base-models/status${checkRemote ? '?check_remote=1' : ''}`, {}, 'Load base models status'); 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 formatNewBaseModelsSummary = (models = [], maxVisible = 6) => { const names = Array.isArray(models) ? models.map(name => String(name || '').trim()).filter(Boolean) : []; if (!names.length) return ''; const visible = names.slice(0, maxVisible); const suffix = names.length > visible.length ? ` and ${names.length - visible.length} more` : ''; return `${visible.join(', ')}${suffix}`; }; const updateBaseModels = async () => { try { setBaseModelsBusy(true); if (baseModelsStateEl) baseModelsStateEl.textContent = 'Updating...'; if (baseModelsMessageEl) baseModelsMessageEl.textContent = 'Downloading latest base models from CivitAI...'; const data = await this.fetchJson('/model_resolver/base-models/update', { method: 'POST' }, 'Update base models'); 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'; const newModelsSummary = formatNewBaseModelsSummary(data.new_models_added_list); const addedCount = Number(data.new_models_added || 0); const addedText = addedCount > 0 ? ` Added ${addedCount.toLocaleString()} new model${addedCount === 1 ? '' : 's'}${newModelsSummary ? `: ${newModelsSummary}` : ''}.` : ' No new base models were added.'; if (baseModelsMessageEl) baseModelsMessageEl.textContent = `Base Models updated. ${Number(data.local_count || 0).toLocaleString()} base models loaded.${addedText}`; this.showNotification( addedCount > 0 ? `Base Models updated${newModelsSummary ? `: ${newModelsSummary}` : ''}` : 'Base Models updated. No new models added.', '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 data = await this.fetchJson('/model_resolver/huggingface/author-index/status', {}, 'Load HuggingFace index status'); 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 data = await this.fetchJson('/model_resolver/huggingface/author-index/refresh', { method: 'POST', body: JSON.stringify({ hf_token: hfInput?.value || '' }) }, 'Refresh HuggingFace index'); if (data?.success === false) { throw new Error(data.error || 'Refresh failed'); } 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); const pathPreviewMetadata = { base_model: 'Flux.1 D', tags: ['style', 'character'], author: 'example_author', model_name: 'Example Model', version_name: 'v1' }; const getTemplateInputValue = (categoryKey) => { const select = templatePresetInputs.find(input => input.dataset.templateCategory === categoryKey); const custom = templateCustomInputs.find(input => input.dataset.templateCategory === categoryKey); if (!select) return ''; return select.value === 'custom' ? (custom?.value || '') : select.value; }; const getBaseModelMappingChoices = () => { const names = []; const seen = new Set(); const addName = (value) => { const name = String(value || '').trim(); if (!name || seen.has(name)) return; seen.add(name); names.push(name); }; const baseModelsList = this.baseModels?.base_models; if (Array.isArray(baseModelsList)) { baseModelsList.forEach(model => addName(model?.name)); } Object.keys(tokens.base_model_path_mappings || {}).forEach(addName); return names.sort((a, b) => a.localeCompare(b)); }; const getBaseModelMappingRows = () => ( baseModelMappingsContainer ? Array.from(baseModelMappingsContainer.querySelectorAll('.mr-options-mapping-row')) : [] ); const collectBaseModelPathMappings = () => { if (!baseModelMappingsContainer) { return { ...(tokens.base_model_path_mappings || {}) }; } return getBaseModelMappingRows().reduce((acc, row) => { const baseModel = row.querySelector('.mr-options-mapping-base')?.value?.trim() || ''; const pathValue = row.querySelector('.mr-options-mapping-path')?.value?.trim() || ''; if (baseModel && pathValue) { acc[baseModel] = pathValue; } return acc; }, {}); }; const renderBaseModelMappingOptions = (currentValue = '', currentRow = null) => { const usedValues = new Set( getBaseModelMappingRows() .filter(row => row !== currentRow) .map(row => row.querySelector('.mr-options-mapping-base')?.value?.trim() || '') .filter(Boolean) ); const options = [ `` ]; const choices = getBaseModelMappingChoices(); if (currentValue && !choices.includes(currentValue)) { options.push(``); } choices.forEach((name) => { if (usedValues.has(name) && name !== currentValue) return; options.push(``); }); return options.join(''); }; const refreshBaseModelMappingOptions = () => { getBaseModelMappingRows().forEach((row) => { const select = row.querySelector('.mr-options-mapping-base'); if (!select) return; const currentValue = select.value; select.innerHTML = renderBaseModelMappingOptions(currentValue, row); select.value = currentValue; }); }; const addBaseModelMappingRow = (baseModel = '', pathValue = '') => { if (!baseModelMappingsContainer) return; const row = document.createElement('div'); row.className = 'mr-options-mapping-row'; row.innerHTML = ` `; const baseSelect = row.querySelector('.mr-options-mapping-base'); const pathInput = row.querySelector('.mr-options-mapping-path'); const removeBtn = row.querySelector('.mr-options-mapping-remove'); const markChanged = () => { setStatus('You have unsaved changes.', 'is-dirty'); refreshBaseModelMappingOptions(); syncAllTemplateControls(); }; baseSelect?.addEventListener('change', markChanged); pathInput?.addEventListener('input', markChanged); pathInput?.addEventListener('keydown', (event) => { if (event.key === 'Enter') { event.preventDefault(); pathInput.blur(); } }); removeBtn?.addEventListener('click', () => { row.remove(); if (!getBaseModelMappingRows().length) { addBaseModelMappingRow('', ''); } markChanged(); }); baseModelMappingsContainer.appendChild(row); refreshBaseModelMappingOptions(); this.bindTooltips(row); }; const renderBaseModelMappingRows = (mappings = {}) => { if (!baseModelMappingsContainer) return; baseModelMappingsContainer.innerHTML = ''; const entries = Object.entries(mappings || {}); if (!entries.length) { addBaseModelMappingRow('', ''); return; } entries.forEach(([baseModel, pathValue]) => { addBaseModelMappingRow(baseModel, pathValue); }); }; const renderTemplatePreview = (template) => { const mappings = collectBaseModelPathMappings(); const mappedBase = this.resolveBaseModelPathMapping( pathPreviewMetadata.base_model, mappings ); const replacements = { '{base_model}': this.sanitizeDownloadPathValue(mappedBase, 'Unknown Base Model'), '{author}': this.sanitizeDownloadPathSegment(pathPreviewMetadata.author, 'Anonymous'), '{first_tag}': this.sanitizeDownloadPathSegment(this.getPriorityDownloadTag(pathPreviewMetadata.tags), 'no tags'), '{model_name}': this.sanitizeDownloadPathSegment(pathPreviewMetadata.model_name, 'Model'), '{version_name}': this.sanitizeDownloadPathSegment(pathPreviewMetadata.version_name, '') }; let formatted = this.normalizeDownloadPathTemplate(template); Object.entries(replacements).forEach(([token, value]) => { formatted = formatted.split(token).join(value); }); formatted = formatted.replace(/\{[^{}]+\}/g, ''); return this.normalizeTemplateSubfolder(formatted) || '(flat folder)'; }; const syncTemplateControl = (categoryKey) => { const select = templatePresetInputs.find(input => input.dataset.templateCategory === categoryKey); const custom = templateCustomInputs.find(input => input.dataset.templateCategory === categoryKey); const preview = this.contentElement.querySelector(`[data-template-preview="${categoryKey}"]`); const useCustom = select?.value === 'custom'; if (custom) { custom.classList.toggle('is-hidden', !useCustom); } if (preview) { preview.innerHTML = `Preview: ${this.escapeHtml(renderTemplatePreview(getTemplateInputValue(categoryKey)))}`; } }; const syncAllTemplateControls = () => { pathTemplateCategories.forEach(category => syncTemplateControl(category.key)); }; const getDefaultRootContextPath = (select) => { if (!select) return ''; const category = this.normalizeDownloadCategory(select.dataset.rootCategory || ''); const selectedValue = String(select.value || '').trim(); if (selectedValue) return selectedValue; const directDownloadRoot = this.downloadDirectories?.[category] || ''; if (directDownloadRoot) return directDownloadRoot; const rootOptions = this.downloadRootDirectories?.[category]; return Array.isArray(rootOptions) && rootOptions.length ? rootOptions[0] : ''; }; const getDefaultRootContextModel = (select) => { const path = getDefaultRootContextPath(select); if (!path) return null; const category = this.normalizeDownloadCategory(select.dataset.rootCategory || ''); const label = select.closest('.mr-options-template-row')?.querySelector('.mr-options-label')?.textContent?.trim() || this.getCategoryDisplayName(category); return { context_scope: 'download_root', name: label, path, resolved_path: path, folder_path: path, download_directory: path, category, open_folder_label: 'Open Root Folder' }; }; const syncDefaultRootContextTarget = (select) => { if (!select) return; const contextModel = getDefaultRootContextModel(select); const tooltip = contextModel ? `Right-click to open ${contextModel.name}` : 'No root folder available'; this.setDownloadFolderContextTarget(select, contextModel, tooltip); this.setDownloadFolderContextTarget(select.closest('.mr-options-template-row'), contextModel, tooltip); }; const setTemplateControlValue = (categoryKey, template) => { const select = templatePresetInputs.find(input => input.dataset.templateCategory === categoryKey); const custom = templateCustomInputs.find(input => input.dataset.templateCategory === categoryKey); if (!select) return false; const normalizedTemplate = this.normalizeDownloadPathTemplate(template); select.value = pathTemplatePresetValues.has(normalizedTemplate) ? normalizedTemplate : 'custom'; if (custom) { custom.value = normalizedTemplate; } syncTemplateControl(categoryKey); return true; }; const setTemplateDetectStatus = (text, mode = '') => { if (!templateDetectStatus) return; templateDetectStatus.textContent = text; templateDetectStatus.classList.remove('is-valid', 'is-invalid', 'is-pending'); if (mode) templateDetectStatus.classList.add(mode); }; const applyDetectedPathTemplates = async () => { if (!detectTemplatesBtn) return; detectTemplatesBtn.disabled = true; setTemplateDetectStatus('Scanning existing model folders...', 'is-pending'); try { const data = await this.fetchJson('/model_resolver/path-template-suggestions?force=1', {}, 'Detect path templates'); const categories = data?.categories && typeof data.categories === 'object' ? data.categories : {}; let appliedCount = 0; let inspectedCount = 0; pathTemplateCategories.forEach((category) => { const suggestion = categories[category.key]; if (!suggestion) return; if (Number(suggestion.model_count || 0) > 0) { inspectedCount += 1; } if (suggestion.apply && setTemplateControlValue(category.key, suggestion.template || '')) { appliedCount += 1; } }); let mappingCount = 0; const detectedMappings = data?.base_model_path_mappings || {}; if ( baseModelMappingsContainer && detectedMappings && typeof detectedMappings === 'object' && !Array.isArray(detectedMappings) ) { const currentMappings = collectBaseModelPathMappings(); Object.entries(detectedMappings).forEach(([source, target]) => { const sourceKey = String(source || '').trim(); const targetValue = String(target || '').trim(); if (sourceKey && targetValue && !currentMappings[sourceKey]) { currentMappings[sourceKey] = targetValue; mappingCount += 1; } }); if (mappingCount > 0) { renderBaseModelMappingRows(currentMappings); } } if (appliedCount > 0 || mappingCount > 0) { syncAllTemplateControls(); setStatus('You have unsaved changes.', 'is-dirty'); const mappingText = mappingCount ? ` Added ${mappingCount} base-model mapping${mappingCount === 1 ? '' : 's'}.` : ''; setTemplateDetectStatus( appliedCount > 0 ? `Detected ${appliedCount} template preset${appliedCount === 1 ? '' : 's'} from ${inspectedCount} model type${inspectedCount === 1 ? '' : 's'}.${mappingText}` : `Detected ${mappingCount} base-model mapping${mappingCount === 1 ? '' : 's'} from existing folders.`, 'is-valid' ); this.showNotification('Path templates detected. Review and save options.', 'success'); } else { setTemplateDetectStatus( 'No confident folder pattern found. Existing template settings were not changed.', 'is-invalid' ); this.showNotification('No confident path template pattern found.', 'info'); } } catch (error) { console.error('Model Resolver: path template detection error:', error); setTemplateDetectStatus(error.message || 'Template detection failed.', 'is-invalid'); this.showNotification('Path template detection failed', 'error'); } finally { detectTemplatesBtn.disabled = false; } }; templatePresetInputs.forEach(input => { input.addEventListener('change', () => syncTemplateControl(input.dataset.templateCategory)); }); templateCustomInputs.forEach(input => { input.addEventListener('input', () => syncTemplateControl(input.dataset.templateCategory)); }); renderBaseModelMappingRows(tokens.base_model_path_mappings || {}); if (typeof this.ensureBaseModelsLoaded === 'function') { this.ensureBaseModelsLoaded() .then(() => { refreshBaseModelMappingOptions(); }) .catch((error) => { console.warn('Model Resolver: could not load base models for mapping options', error); }); } if (addBaseModelMappingBtn) { addBaseModelMappingBtn.addEventListener('click', () => { addBaseModelMappingRow('', ''); setStatus('You have unsaved changes.', 'is-dirty'); }); } syncAllTemplateControls(); const populateDefaultRootSelects = async () => { const roots = await this.ensureDownloadRootDirectoriesLoaded(); await this.ensureDownloadDirectoriesLoaded(); defaultRootSelectInputs.forEach((select) => { const category = this.normalizeDownloadCategory(select.dataset.rootCategory || ''); const currentValue = select.value || select.dataset.currentValue || ''; const categoryRoots = Array.isArray(roots?.[category]) ? roots[category] : []; const options = [ { value: '', label: 'Auto' }, ...categoryRoots.map(path => ({ value: path, label: path })) ]; if (currentValue && !options.some(option => option.value === currentValue)) { options.push({ value: currentValue, label: `${currentValue} (not currently detected)` }); } select.innerHTML = options .map(option => ``) .join(''); syncDefaultRootContextTarget(select); }); }; populateDefaultRootSelects(); setStatus('Saved only on this machine.'); setVisibleSection('mr-options-section-sources'); trackedInputs.forEach((input) => { const eventName = input.type === 'checkbox' || input.tagName === 'SELECT' ? 'change' : 'input'; input.addEventListener(eventName, () => { setStatus('You have unsaved changes.', 'is-dirty'); }); }); defaultRootSelectInputs.forEach((select) => { select.addEventListener('change', () => { syncDefaultRootContextTarget(select); }); }); 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 (exportFrontendLogsBtn) { exportFrontendLogsBtn.addEventListener('click', () => { exportFrontendLogs(); }); } if (exportBackendLogsBtn) { exportBackendLogsBtn.addEventListener('click', () => { exportBackendLogs(); }); } if (clearAllCacheBtn) { clearAllCacheBtn.addEventListener('click', () => { clearAllCachesFromOptions(); }); } if (detectTemplatesBtn) { detectTemplatesBtn.addEventListener('click', () => { applyDetectedPathTemplates(); }); } if (aria2CheckBtn) { aria2CheckBtn.addEventListener('click', () => { checkAria2Status({ useUnsavedPath: true }); }); } if (aria2InstallBtn) { aria2InstallBtn.addEventListener('click', () => { installAria2(); }); } if (aria2StopBtn) { aria2StopBtn.addEventListener('click', () => { if (aria2StopBtn.dataset.action === 'start') { startAria2(); } else { stopAria2(); } }); } if (aria2cPathInput) { aria2cPathInput.addEventListener('input', () => { renderAria2Summary({ backend: downloadBackendInput?.value || tokens.download_backend, configured_path: aria2cPathInput.value }); setAria2Status(''); }); } if (downloadBackendInput) { downloadBackendInput.addEventListener('change', () => { const backend = this.normalizeDownloadBackend(downloadBackendInput.value); syncAria2OptionsVisibility(); if (backend === 'aria2') { checkAria2Status({ useUnsavedPath: true }); } }); } syncAria2OptionsVisibility(); if (this.normalizeDownloadBackend(tokens.download_backend) === 'aria2') { checkAria2Status({ useUnsavedPath: false }); } 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 downloadPathTemplates = {}; pathTemplateCategories.forEach(category => { downloadPathTemplates[category.key] = this.normalizeDownloadPathTemplate( getTemplateInputValue(category.key) ); }); const baseModelPathMappings = collectBaseModelPathMappings(); const downloadBackend = this.normalizeDownloadBackend(downloadBackendInput?.value || tokens.download_backend); const downloadPathMode = this.normalizeDownloadPathMode(downloadPathModeInput?.value || tokens.download_path_mode); const defaultRootSettings = {}; defaultRootSelectInputs.forEach((select) => { const settingKey = select.dataset.settingKey; if (settingKey) { defaultRootSettings[settingKey] = select.value || ''; } }); 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), auto_refresh_comfy_models_after_apply: Boolean(autoRefreshComfyModelsInput?.checked), download_backend: downloadBackend, aria2c_path: aria2cPathInput?.value?.trim() || '', aria2_auto_stop_daemon: Boolean(aria2AutoStopInput?.checked), download_path_mode: downloadPathMode, download_path_templates: downloadPathTemplates, base_model_path_mappings: baseModelPathMappings, ...defaultRootSettings, frontend_logs_enabled: Boolean(frontendLogsEnabledInput?.checked), backend_logs_enabled: Boolean(backendLogsEnabledInput?.checked), frontend_log_level: normalizeLogLevel(frontendLogLevelInput?.value || tokens.frontend_log_level), backend_log_level: normalizeLogLevel(backendLogLevelInput?.value || tokens.backend_log_level), 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.autoRefreshComfyModelsAfterApply', newSettings.auto_refresh_comfy_models_after_apply ? 'true' : 'false'); localStorage.setItem('ModelResolver.downloadBackend', newSettings.download_backend); localStorage.setItem('ModelResolver.aria2cPath', newSettings.aria2c_path); localStorage.setItem('ModelResolver.aria2AutoStopDaemon', newSettings.aria2_auto_stop_daemon ? 'true' : 'false'); localStorage.setItem('ModelResolver.downloadPathMode', newSettings.download_path_mode); localStorage.setItem('ModelResolver.downloadPathTemplates', JSON.stringify(newSettings.download_path_templates)); localStorage.setItem('ModelResolver.baseModelPathMappings', JSON.stringify(newSettings.base_model_path_mappings)); defaultRootSelectInputs.forEach((select) => { const storageKey = select.dataset.storageKey; if (storageKey) { localStorage.setItem(storageKey, select.value || ''); } }); localStorage.setItem('ModelResolver.frontendLogsEnabled', newSettings.frontend_logs_enabled ? 'true' : 'false'); localStorage.setItem('ModelResolver.backendLogsEnabled', newSettings.backend_logs_enabled ? 'true' : 'false'); localStorage.setItem('ModelResolver.frontendLogLevel', newSettings.frontend_log_level); localStorage.setItem('ModelResolver.backendLogLevel', newSettings.backend_log_level); localStorage.setItem('ModelResolver.civitaiCandidateLimit', `${civitaiCandidateLimit}`); Object.entries(sourceEnabled).forEach(([key, val]) => { localStorage.setItem(key, val ? 'true' : 'false'); }); if (civitaiLimitInput) civitaiLimitInput.value = `${civitaiCandidateLimit}`; this.applyFrontendLoggingPreference(newSettings.frontend_logs_enabled, newSettings.frontend_log_level); // 2. Persist to server (survives new browsers / profiles) try { setStatus('Saving…', ''); await this.fetchJson('/model_resolver/settings', { method: 'POST', body: JSON.stringify(newSettings) }, 'Save settings'); 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'); } this.downloadRootDirectories = null; this.downloadSubfolders?.clear(); await this.clearSearchCaches(); }); } }, normalizeDownloadBackend(value = '') { const backend = String(value || '').trim().toLowerCase(); return backend === 'aria2' ? 'aria2' : 'python'; }, getStoredTokens() { const civitaiCandidateLimitRaw = parseInt(localStorage.getItem('ModelResolver.civitaiCandidateLimit') || '5', 10); const civitai_candidate_limit = Number.isFinite(civitaiCandidateLimitRaw) ? Math.min(20, Math.max(1, civitaiCandidateLimitRaw)) : 5; const search_source_enabled = this.getSearchSourceEnabledMap(); const storedFrontendLogsEnabled = localStorage.getItem('ModelResolver.frontendLogsEnabled'); const storedBackendLogsEnabled = localStorage.getItem('ModelResolver.backendLogsEnabled'); const storedFrontendLogLevel = localStorage.getItem('ModelResolver.frontendLogLevel'); const storedBackendLogLevel = localStorage.getItem('ModelResolver.backendLogLevel'); return { civitai_key: localStorage.getItem('ModelResolver.civitaiApiKey') || '', civitai_session_token: localStorage.getItem('ModelResolver.civitaiSessionToken') || '', hf_token: localStorage.getItem('ModelResolver.huggingFaceToken') || '', brave_search_api_key: localStorage.getItem('ModelResolver.braveSearchApiKey') || '', civitai_use_trpc_search: localStorage.getItem('ModelResolver.civitaiUseTrpcSearch') !== 'false', civitai_use_html_fallback: localStorage.getItem('ModelResolver.civitaiUseHtmlFallback') !== 'false', hf_use_api_search: localStorage.getItem('ModelResolver.hfUseApiSearch') !== 'false', hf_use_comfy_org_fallback: localStorage.getItem('ModelResolver.hfUseComfyOrgFallback') !== 'false', hf_use_brave_fallback: localStorage.getItem('ModelResolver.hfUseBraveFallback') !== 'false', auto_fill_base_model: localStorage.getItem('ModelResolver.autoFillBaseModel') !== 'false', auto_fill_subfolder: localStorage.getItem('ModelResolver.autoFillSubfolder') !== 'false', auto_refresh_comfy_models_after_apply: localStorage.getItem('ModelResolver.autoRefreshComfyModelsAfterApply') !== 'false', frontend_logs_enabled: storedFrontendLogsEnabled === null ? frontendLogger.enabled !== false : storedFrontendLogsEnabled !== 'false', backend_logs_enabled: storedBackendLogsEnabled === null ? true : storedBackendLogsEnabled !== 'false', frontend_log_level: storedFrontendLogLevel || DEFAULT_FRONTEND_LOG_LEVEL, backend_log_level: storedBackendLogLevel || 'DEBUG', civitai_candidate_limit, search_source_enabled, download_backend: this.normalizeDownloadBackend(localStorage.getItem('ModelResolver.downloadBackend') || 'python'), aria2c_path: localStorage.getItem('ModelResolver.aria2cPath') || '', aria2_auto_stop_daemon: localStorage.getItem('ModelResolver.aria2AutoStopDaemon') !== 'false', download_path_mode: this.getDownloadPathMode(), download_path_templates: this.getDownloadPathTemplates(), base_model_path_mappings: this.getBaseModelPathMappings(), ...this.getDefaultRootSettings() }; }, applyFrontendLoggingPreference(enabled = true, levelName = 'DEBUG') { frontendLogger.setEnabled(Boolean(enabled)); frontendLogger.setGlobalAndModuleLevel(frontendLogger.normalizeLevel(levelName)); }, /** * Fetch settings saved on the server and sync them into localStorage. * Call this once when the dialog initialises so every browser gets the * same tokens without the user having to re-enter them. */ async loadSettingsFromServer() { try { const data = await this.fetchJson('/model_resolver/settings', { silent: true }, 'Load settings from server'); if (!data || typeof data !== 'object') { const tokens = this.getStoredTokens(); this.applyFrontendLoggingPreference(tokens.frontend_logs_enabled, tokens.frontend_log_level); return; } // Helper: write to localStorage only when the value from server is // non-empty, so we don't overwrite a key the user already has locally. const sync = (localKey, serverValue) => { if (serverValue !== undefined && serverValue !== null && serverValue !== '') { localStorage.setItem(localKey, serverValue); } }; sync('ModelResolver.civitaiApiKey', data.civitai_key); sync('ModelResolver.civitaiSessionToken', data.civitai_session_token); sync('ModelResolver.huggingFaceToken', data.hf_token); sync('ModelResolver.braveSearchApiKey', data.brave_search_api_key); if (data.civitai_use_trpc_search !== undefined) localStorage.setItem('ModelResolver.civitaiUseTrpcSearch', data.civitai_use_trpc_search ? 'true' : 'false'); if (data.civitai_use_html_fallback !== undefined) localStorage.setItem('ModelResolver.civitaiUseHtmlFallback', data.civitai_use_html_fallback ? 'true' : 'false'); if (data.hf_use_api_search !== undefined) localStorage.setItem('ModelResolver.hfUseApiSearch', data.hf_use_api_search ? 'true' : 'false'); if (data.hf_use_comfy_org_fallback !== undefined) localStorage.setItem('ModelResolver.hfUseComfyOrgFallback', data.hf_use_comfy_org_fallback ? 'true' : 'false'); if (data.hf_use_brave_fallback !== undefined) localStorage.setItem('ModelResolver.hfUseBraveFallback', data.hf_use_brave_fallback ? 'true' : 'false'); if (data.auto_fill_base_model !== undefined) localStorage.setItem('ModelResolver.autoFillBaseModel', data.auto_fill_base_model ? 'true' : 'false'); if (data.auto_fill_subfolder !== undefined) localStorage.setItem('ModelResolver.autoFillSubfolder', data.auto_fill_subfolder ? 'true' : 'false'); if (data.auto_refresh_comfy_models_after_apply !== undefined) localStorage.setItem('ModelResolver.autoRefreshComfyModelsAfterApply', data.auto_refresh_comfy_models_after_apply ? 'true' : 'false'); if (data.download_backend !== undefined) localStorage.setItem('ModelResolver.downloadBackend', this.normalizeDownloadBackend(data.download_backend)); if (data.aria2c_path !== undefined) localStorage.setItem('ModelResolver.aria2cPath', String(data.aria2c_path || '')); if (data.aria2_auto_stop_daemon !== undefined) localStorage.setItem('ModelResolver.aria2AutoStopDaemon', data.aria2_auto_stop_daemon ? 'true' : 'false'); if (data.download_path_mode !== undefined) localStorage.setItem('ModelResolver.downloadPathMode', this.normalizeDownloadPathMode(data.download_path_mode)); if (data.download_path_templates !== undefined) localStorage.setItem('ModelResolver.downloadPathTemplates', JSON.stringify(data.download_path_templates || {})); if (data.base_model_path_mappings !== undefined) localStorage.setItem('ModelResolver.baseModelPathMappings', JSON.stringify(data.base_model_path_mappings || {})); this.getDefaultRootCategoryDefinitions().forEach((item) => { if (data[item.settingKey] !== undefined) { localStorage.setItem(item.storageKey, String(data[item.settingKey] || '')); } }); if (data.civitai_candidate_limit !== undefined) localStorage.setItem('ModelResolver.civitaiCandidateLimit', `${data.civitai_candidate_limit}`); if (data.frontend_logs_enabled !== undefined) localStorage.setItem('ModelResolver.frontendLogsEnabled', data.frontend_logs_enabled ? 'true' : 'false'); if (data.backend_logs_enabled !== undefined) localStorage.setItem('ModelResolver.backendLogsEnabled', data.backend_logs_enabled ? 'true' : 'false'); if (data.frontend_log_level !== undefined) localStorage.setItem('ModelResolver.frontendLogLevel', String(data.frontend_log_level || 'DEBUG').toUpperCase()); if (data.backend_log_level !== undefined) localStorage.setItem('ModelResolver.backendLogLevel', String(data.backend_log_level || 'DEBUG').toUpperCase()); // Source-enabled flags stored as a nested object if (data.search_source_enabled && typeof data.search_source_enabled === 'object') { Object.entries(data.search_source_enabled).forEach(([key, val]) => { if (key) localStorage.setItem(key, val ? 'true' : 'false'); }); } const tokens = this.getStoredTokens(); this.applyFrontendLoggingPreference(tokens.frontend_logs_enabled, tokens.frontend_log_level); } catch (err) { console.warn('Model Resolver: could not load settings from server, using localStorage only.', err); const tokens = this.getStoredTokens(); this.applyFrontendLoggingPreference(tokens.frontend_logs_enabled, tokens.frontend_log_level); } } };