feat: add base model filtering across search sources, API routes, and frontend UI

This commit is contained in:
Dariusz L
2026-06-06 19:43:43 +02:00
parent 57ac022fc4
commit ab59cd231f
16 changed files with 1544 additions and 33 deletions
+11 -5
View File
@@ -1822,7 +1822,7 @@
}
.mr-search-source-bar {
display: grid;
grid-template-columns: auto minmax(178px, 1fr);
grid-template-columns: auto minmax(150px, 0.85fr) minmax(150px, 1fr);
align-items: center;
gap: 10px;
margin-bottom: 0;
@@ -1863,7 +1863,8 @@
text-transform: uppercase;
white-space: nowrap;
}
.mr-search-source-select {
.mr-search-source-select,
.mr-search-base-select {
width: 100%;
height: var(--mr-download-control-height);
min-height: var(--mr-download-control-height);
@@ -1878,16 +1879,19 @@
appearance: none;
box-sizing: border-box;
}
.mr-missing-browser .mr-search-source-select {
.mr-missing-browser .mr-search-source-select,
.mr-missing-browser .mr-search-base-select {
background: var(--mr-missing-control-bg, rgba(255,255,255,0.035));
border-color: var(--mr-missing-control-border, var(--mr-missing-line-strong, rgba(255,255,255,0.12)));
border-radius: var(--mr-missing-control-radius, 7px);
}
.mr-missing-browser .mr-search-source-select:focus {
.mr-missing-browser .mr-search-source-select:focus,
.mr-missing-browser .mr-search-base-select:focus {
border-color: var(--mr-missing-control-focus-border, rgba(114,211,138,0.4));
box-shadow: 0 0 0 2px var(--mr-missing-control-focus-ring, rgba(114,211,138,0.12));
}
.mr-search-source-select:focus {
.mr-search-source-select:focus,
.mr-search-base-select:focus {
border-color: rgba(114,211,138,0.4);
box-shadow: 0 0 0 3px rgba(114,211,138,0.12);
}
@@ -4362,6 +4366,7 @@
}
#model-resolver-modal .mr-missing-detail-pane .mr-combo-input,
#model-resolver-modal .mr-missing-detail-pane .mr-search-source-select,
#model-resolver-modal .mr-missing-detail-pane .mr-search-base-select,
#model-resolver-modal .mr-missing-detail-pane .mr-download-target-input,
#model-resolver-modal .mr-missing-detail-pane .mr-download-target-select {
min-height: var(--mr-missing-field-height, 38px);
@@ -4374,6 +4379,7 @@
}
#model-resolver-modal .mr-missing-detail-pane .mr-combo-input:focus,
#model-resolver-modal .mr-missing-detail-pane .mr-search-source-select:focus,
#model-resolver-modal .mr-missing-detail-pane .mr-search-base-select:focus,
#model-resolver-modal .mr-missing-detail-pane .mr-download-target-input:focus,
#model-resolver-modal .mr-missing-detail-pane .mr-download-target-select:focus {
background: var(--mr-missing-control-bg-hover, rgba(255,255,255,0.045));
@@ -846,6 +846,7 @@ export const resolveDownloadMethods = {
}
const selectedSourceLabel = this.getSearchSourceLabel(selectedSource);
const sourceIds = this.getSearchSourcesForSelection(selectedSource, missing);
const baseModelContext = this.getSearchBaseModelContext(missing);
// For URNs, use the resolved file/model name for searching instead of the URN itself
// and pass the URN type as category (CivitAI expects specific type names)
@@ -907,6 +908,7 @@ export const resolveDownloadMethods = {
state.lastAttemptSources = sourceIds;
state.lastAttemptFound = null;
state.lastAttemptError = null;
state.lastAttemptBaseModelContext = baseModelContext;
state.sourceProgress = {};
for (const source of sourceIds) {
this.setSourceProgress(state, source, {
@@ -944,6 +946,7 @@ export const resolveDownloadMethods = {
filename,
category,
is_urn: isUrn,
base_model_context: baseModelContext,
civitai_session_token: tokens.civitai_session_token,
civitai_candidate_limit: tokens.civitai_candidate_limit,
civitai_use_trpc_search: tokens.civitai_use_trpc_search,
@@ -991,6 +994,9 @@ export const resolveDownloadMethods = {
if (!this.isBackgroundSearchRunActive(workflowKey, missingSearchKey, searchRunId)) {
return { source, stale: true };
}
if (baseModelContext !== this.getSearchBaseModelContext(missing)) {
return { source, stale: true };
}
const responseSources = Array.isArray(data.searched_sources) && data.searched_sources.length
? data.searched_sources
@@ -1300,6 +1306,53 @@ export const resolveDownloadMethods = {
this.syncSearchSourceUi(missing, container);
}
const baseSelect = container.querySelector(`#search-base-select-${missing.node_id}-${missing.widget_index}`);
const baseList = container.querySelector(`#search-base-list-${missing.node_id}-${missing.widget_index}`);
if (baseSelect && baseList && baseSelect.dataset.mlSearchBaseBound !== 'true') {
baseSelect.dataset.mlSearchBaseBound = 'true';
const renderBaseOptions = () => {
const options = this.getKnownBaseModelOptions();
baseList.innerHTML = options
.map(option => {
const label = option.value === 'auto'
? this.getSearchBaseModelLabel('auto')
: option.label;
return `<div class="mr-download-target-option" data-value="${encodeURIComponent(option.value)}" data-label="${encodeURIComponent(label)}">${this.escapeHtml(label)}</div>`;
})
.join('');
baseList.style.display = 'block';
baseList.querySelectorAll('.mr-download-target-option').forEach(optionEl => {
optionEl.addEventListener('mousedown', (event) => {
event.preventDefault();
const value = decodeURIComponent(optionEl.dataset.value || '');
const label = decodeURIComponent(optionEl.dataset.label || optionEl.dataset.value || '');
this.setDropdownValue(baseSelect, value, label);
this.setSearchBaseModel(missing, value, container);
baseList.style.display = 'none';
});
});
};
const hideBaseList = () => {
setTimeout(() => {
baseList.style.display = 'none';
}, 150);
};
this.enableWheelScrollChaining(baseList);
baseSelect.addEventListener('focus', () => renderBaseOptions());
baseSelect.addEventListener('click', () => renderBaseOptions());
baseSelect.addEventListener('keydown', (event) => {
if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowDown') {
event.preventDefault();
renderBaseOptions();
}
});
baseSelect.addEventListener('blur', hideBaseList);
this.syncSearchSourceUi(missing, container);
}
this.wireDownloadTargetAutocomplete(container, missing);
},
+5 -4
View File
@@ -1,4 +1,4 @@
import { $el, ComfyDialog } from "../../../../scripts/ui.js";
import { $el, ComfyDialog } from "../../../../scripts/ui.js";
import { dialogShellMethods } from "./shell/dialog_shell_methods.js";
import { lifecycleGraphMethods } from "./shell/lifecycle_graph_methods.js";
import { workflowStateMethods } from "./shell/workflow_state_methods.js";
@@ -21,6 +21,7 @@ export class ResolverManagerDialog extends ComfyDialog {
this.allModels = null; // list of all available models for dropdown
this.downloadDirectories = null;
this.capabilities = null;
this.baseModels = null;
this.downloadSubfolders = new Map();
this.downloadTargetSelections = new Map(); // missing model key -> user/suggested download target
this.pendingResolutions = [];
@@ -85,21 +86,21 @@ export class ResolverManagerDialog extends ComfyDialog {
$el("div.mr-context-menu-item", {
onclick: () => this.handleContextMenuAction('showInfo')
}, [
$el("span.mr-context-menu-item-icon", { textContent: "ℹ" }),
$el("span.mr-context-menu-item-icon", { textContent: "?" }),
$el("span", { textContent: "Show Info" })
]),
$el("div.mr-context-menu-divider"),
$el("div.mr-context-menu-item", {
onclick: () => this.handleContextMenuAction('civitai')
}, [
$el("span.mr-context-menu-item-icon", { textContent: "🌐" }),
$el("span.mr-context-menu-item-icon", { textContent: "??" }),
$el("span", { textContent: "Open in CivitAI" })
]),
$el("div.mr-context-menu-divider"),
$el("div.mr-context-menu-item", {
onclick: () => this.handleContextMenuAction('openFolder')
}, [
$el("span.mr-context-menu-item-icon", { textContent: "📁" }),
$el("span.mr-context-menu-item-icon", { textContent: "??" }),
$el("span", { textContent: "Open Containing Folder" })
])
]);
@@ -50,7 +50,21 @@ export const downloadTargetMethods = {
}
},
async ensureBaseModelsLoaded() {
if (this.baseModels) return;
try {
const resp = await api.fetchApi('/model_resolver/base-models');
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const data = await resp.json();
this.baseModels = data && typeof data === 'object' ? data : {};
} catch (e) {
console.warn('Model Resolver: could not load base-models config', e);
this.baseModels = {};
}
},
isSourceAvailable(source) {
if (!source || ['all', 'local', 'huggingface', 'civitai', 'civarchive'].includes(source)) {
return true;
}
@@ -675,11 +689,13 @@ export const downloadTargetMethods = {
this.allModels = null;
this.downloadDirectories = null;
this.capabilities = null;
this.baseModels = null;
this.downloadSubfolders.clear();
this.downloadTargetSelections?.clear();
this._analysisProgressToken = null;
await this.ensureCapabilitiesLoaded();
await this.ensureBaseModelsLoaded();
this.refreshMissingListStats?.();
this.updateBatchFooterButtons?.();
this.updateDownloadAllButtonState?.();
+166 -1
View File
@@ -1,4 +1,4 @@
import { app } from "../../../../../scripts/app.js";
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";
@@ -29,6 +29,7 @@ export const searchPanelMethods = {
createEmptySearchState() {
return {
selectedSource: 'all',
selectedBaseModel: 'auto',
results: {
popular: null,
model_list: null,
@@ -38,6 +39,7 @@ export const searchPanelMethods = {
lora_manager_archive: null
},
lastAttemptSources: [],
lastAttemptBaseModelContext: '',
lastAttemptFound: null,
lastAttemptError: null,
sourceProgress: {},
@@ -45,6 +47,119 @@ export const searchPanelMethods = {
};
},
getKnownBaseModelOptions() {
const baseModelsList = this.baseModels?.base_models;
if (Array.isArray(baseModelsList) && baseModelsList.length > 0) {
return [
{ value: 'auto', label: 'Auto' },
{ value: 'none', label: 'Any model' },
...baseModelsList.map(m => ({ value: m.name, label: m.name }))
];
}
// Fallback to hardcoded list
return [
{ value: 'auto', label: 'Auto' },
{ value: 'none', label: 'Any model' },
{ value: 'Z-Image', label: 'Z-Image' },
{ value: 'Pony', label: 'Pony' },
{ value: 'Illustrious', label: 'Illustrious' },
{ value: 'SDXL 1.0', label: 'SDXL 1.0' },
{ value: 'SD 1.5', label: 'SD 1.5' },
{ value: 'Flux.1 D', label: 'Flux.1 D' },
{ value: 'Flux.1 S', label: 'Flux.1 S' },
{ value: 'Qwen Image', label: 'Qwen Image' },
{ value: 'Hunyuan 1', label: 'Hunyuan 1' },
{ value: 'WAN Video', label: 'WAN Video' },
{ value: 'NoobAI', label: 'NoobAI' },
{ value: 'HiDream', label: 'HiDream' }
];
},
normalizeBaseModelToken(value = '') {
return String(value || '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '');
},
getBaseModelAliases() {
const baseModelsList = this.baseModels?.base_models;
if (Array.isArray(baseModelsList) && baseModelsList.length > 0) {
return baseModelsList.map(m => ({ value: m.name, aliases: m.aliases || [] }));
}
// Fallback to hardcoded list
return [
{ value: 'Z-Image', aliases: ['zimage', 'z image', 'z-image', 'z_image', 'zImageTurbo', 'z image turbo'] },
{ value: 'Pony', aliases: ['pony', 'ponyxl', 'pony diffusion', 'pony realism'] },
{ value: 'Illustrious', aliases: ['illustrious', 'illustriousxl', 'illustrious xl'] },
{ value: 'SDXL 1.0', aliases: ['sdxl', 'sdxl10', 'sdxl 1.0', 'stable diffusion xl'] },
{ value: 'SD 1.5', aliases: ['sd15', 'sd 1.5', 'sd1.5', 'stable diffusion 1.5'] },
{ value: 'Flux.1 D', aliases: ['flux', 'flux1', 'flux.1', 'flux dev', 'flux.1 d', 'flux1d'] },
{ value: 'Flux.1 S', aliases: ['flux schnell', 'flux.1 s', 'flux1s'] },
{ value: 'Qwen Image', aliases: ['qwen image', 'qwenimage', 'qwen-image'] },
{ value: 'Hunyuan 1', aliases: ['hunyuan', 'hunyuan1'] },
{ value: 'WAN Video', aliases: ['wan', 'wan video', 'wanvideo'] },
{ value: 'NoobAI', aliases: ['noobai', 'noob ai'] },
{ value: 'HiDream', aliases: ['hidream', 'hi dream'] }
];
},
getWorkflowModelReferenceText() {
const workflow = this.getCurrentWorkflow?.();
if (!workflow) return '';
const values = [];
const visit = (value, key = '') => {
if (typeof value === 'string') {
if (/\.(safetensors|ckpt|pt|pth|bin|gguf|onnx)\b/i.test(value) || /model|checkpoint|unet|diffusion/i.test(key)) {
values.push(value);
}
return;
}
if (Array.isArray(value)) {
value.forEach(item => visit(item, key));
return;
}
if (value && typeof value === 'object') {
Object.entries(value).forEach(([childKey, childValue]) => visit(childValue, childKey));
}
};
visit(workflow);
return values.join(' ');
},
getDominantWorkflowBaseModel() {
const text = this.getWorkflowModelReferenceText();
if (!text) return '';
const normalizedText = this.normalizeBaseModelToken(text);
let best = null;
for (const entry of this.getBaseModelAliases()) {
const score = entry.aliases.reduce((count, alias) => {
const token = this.normalizeBaseModelToken(alias);
return token && normalizedText.includes(token) ? count + 1 : count;
}, 0);
if (score > 0 && (!best || score > best.score)) {
best = { value: entry.value, score };
}
}
return best?.value || '';
},
getSearchBaseModelLabel(value = 'auto') {
if (value === 'auto') {
const detected = this.getDominantWorkflowBaseModel();
return detected ? `Auto (${detected})` : 'Auto';
}
const option = this.getKnownBaseModelOptions().find(item => item.value === value);
return option?.label || value || 'Auto';
},
getSearchBaseModelContext(missing = {}) {
const state = this.getSearchState(missing);
const selected = state.selectedBaseModel || 'auto';
if (selected === 'none') return '';
if (selected === 'auto') return this.getDominantWorkflowBaseModel();
return selected;
},
getBackgroundSearchJobKey(workflowKey, missingSearchKey) {
return `${workflowKey || 'workflow'}\n${missingSearchKey || 'missing'}`;
},
@@ -636,6 +751,15 @@ export const searchPanelMethods = {
}
this.setDropdownValue(selectEl, state.selectedSource, this.getSearchSourceLabel(state.selectedSource));
}
const baseEl = container.querySelector(`#search-base-select-${missing.node_id}-${missing.widget_index}`);
if (baseEl) {
const options = this.getKnownBaseModelOptions();
if (!options.some(option => option.value === state.selectedBaseModel)) {
state.selectedBaseModel = 'auto';
}
this.setDropdownValue(baseEl, state.selectedBaseModel, this.getSearchBaseModelLabel(state.selectedBaseModel));
}
},
/**
@@ -648,6 +772,37 @@ export const searchPanelMethods = {
this.syncSearchSourceUi(missing, container);
},
setSearchBaseModel(missing, baseModel, container) {
const state = this.getSearchState(missing);
const nextBaseModel = baseModel || 'auto';
const changed = (state.selectedBaseModel || 'auto') !== nextBaseModel;
if (changed) {
this.clearSearchProgressTimers(state.activeSearchRunId);
state.results = this.createEmptySearchState().results;
state.lastAttemptSources = [];
state.lastAttemptBaseModelContext = '';
state.lastAttemptFound = null;
state.lastAttemptError = null;
state.sourceProgress = {};
state.activeSearchRunId = null;
const resultsDiv = container?.querySelector?.(`#search-results-${missing.node_id}-${missing.widget_index}`);
if (resultsDiv) {
resultsDiv.classList.remove('mr-is-visible');
resultsDiv.classList.add('mr-is-hidden');
resultsDiv.innerHTML = '';
}
const searchBtn = container?.querySelector?.(`#search-${missing.node_id}-${missing.widget_index}`);
if (searchBtn) {
searchBtn.disabled = false;
searchBtn.innerHTML = `${this.getSearchIconHtml()} ${this.hasSearchResultsForMissing(missing) ? 'Search Again' : 'Search'}`;
}
}
state.selectedBaseModel = nextBaseModel;
this.persistSearchStateForActiveWorkflow();
this.syncSearchSourceUi(missing, container);
this.refreshSearchUiForMissing?.(missing, state);
},
getDropdownValue(el) {
return el?.dataset?.value || el?.value || '';
},
@@ -985,8 +1140,11 @@ export const searchPanelMethods = {
const searchSourcesId = `search-sources-${missing.node_id}-${missing.widget_index}`;
const searchSourceSelectId = `search-source-select-${missing.node_id}-${missing.widget_index}`;
const searchSourceListId = `search-source-list-${missing.node_id}-${missing.widget_index}`;
const searchBaseSelectId = `search-base-select-${missing.node_id}-${missing.widget_index}`;
const searchBaseListId = `search-base-list-${missing.node_id}-${missing.widget_index}`;
const state = this.getSearchState(missing);
const selectedSource = state.selectedSource || 'all';
const selectedBaseModel = state.selectedBaseModel || 'auto';
const buttonText = options.buttonText
|| (this.hasSearchResultsForMissing(missing) ? 'Search Again' : 'Search');
@@ -1001,6 +1159,13 @@ export const searchPanelMethods = {
html += `<div id="${searchSourceListId}" class="mr-download-target-list mr-search-source-list"></div>`;
html += `</div>`;
html += `</div>`;
html += `<div class="mr-search-source-picker mr-search-base-picker">`;
html += `<label class="mr-search-source-picker-label" for="${searchBaseSelectId}">Model</label>`;
html += `<div class="mr-download-target-wrap">`;
html += `<input id="${searchBaseSelectId}" class="mr-download-target-input mr-search-base-select" type="text" readonly autocomplete="off" data-value="${this.escapeHtml(selectedBaseModel)}" value="${this.escapeHtml(this.getSearchBaseModelLabel(selectedBaseModel))}">`;
html += `<div id="${searchBaseListId}" class="mr-download-target-list mr-search-base-list"></div>`;
html += `</div>`;
html += `</div>`;
html += `</div>`;
return html;
},
@@ -32,6 +32,7 @@ export const lifecycleGraphMethods = {
// Ensure all models are loaded for dropdown
await this.ensureCapabilitiesLoaded();
await this.ensureBaseModelsLoaded();
await this.ensureAllModelsLoaded();
await this.ensureDownloadDirectoriesLoaded();
@@ -186,10 +186,12 @@ export const workflowStateMethods = {
}
clone.selectedSource = clone.selectedSource || 'all';
clone.selectedBaseModel = clone.selectedBaseModel || 'auto';
clone.results = this.mergeSearchResults({}, clone.results || {});
clone.lastAttemptSources = Array.isArray(clone.lastAttemptSources)
? clone.lastAttemptSources
: [];
clone.lastAttemptBaseModelContext = clone.lastAttemptBaseModelContext || '';
clone.lastAttemptFound = clone.lastAttemptFound ?? null;
clone.lastAttemptError = clone.lastAttemptError || null;
clone.sourceProgress = clone.sourceProgress || {};
@@ -229,6 +231,7 @@ export const workflowStateMethods = {
isPersistableSearchState(state = {}) {
if ((state?.selectedSource || 'all') !== 'all') return true;
if ((state?.selectedBaseModel || 'auto') !== 'auto') return true;
if (state?.lastAttemptError) return true;
if (this.hasSearchResults(state?.results || {})) return true;
+137
View File
@@ -136,6 +136,35 @@ export const optionsMethods = {
</div>
</div>
</div>
<div class="mr-options-section-head" style="margin-top: 24px;">
<h4 class="mr-options-section-title">Base Models Support</h4>
</div>
<div class="mr-options-grid">
<div class="mr-options-panel">
<div class="mr-options-stack">
<div class="mr-options-db-summary">
<div class="mr-options-db-row">
<span>Base Models</span>
<strong id="mr-options-base-models-count">Loading...</strong>
</div>
<div class="mr-options-db-row">
<span>Last update</span>
<strong id="mr-options-base-models-updated">Loading...</strong>
</div>
<div class="mr-options-db-row">
<span>Status</span>
<strong id="mr-options-base-models-state">Checking local file...</strong>
</div>
</div>
<div id="mr-options-base-models-message" class="mr-options-db-message">Base Models list maps new CivitAI models to local categories.</div>
<div class="mr-options-db-actions">
<button id="mr-options-base-models-check" type="button" class="mr-btn mr-btn-secondary">${getSvgIcon('refreshCw')} Check latest</button>
<button id="mr-options-base-models-update" type="button" class="mr-btn mr-btn-primary">${getSvgIcon('download')} Update Base Models</button>
<a class="mr-options-inline-link" href="https://civitai.com/api/v1/enums" target="_blank" rel="noopener noreferrer">Source</a>
</div>
</div>
</div>
</div>
</section>
<section id="mr-options-section-civitai" class="mr-options-card mr-options-section">
<div class="mr-options-section-head">
@@ -353,6 +382,12 @@ export const optionsMethods = {
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');
@@ -397,6 +432,11 @@ export const optionsMethods = {
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;
};
@@ -525,6 +565,89 @@ export const optionsMethods = {
}
};
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))
@@ -683,6 +806,7 @@ export const optionsMethods = {
setVisibleSection(targetId);
if (targetId === 'mr-options-section-local-db') {
loadModelListStatus(false);
loadBaseModelsStatus(false);
} else if (targetId === 'mr-options-section-hf') {
loadHfIndexStatus();
}
@@ -701,6 +825,18 @@ export const optionsMethods = {
});
}
if (baseModelsCheckBtn) {
baseModelsCheckBtn.addEventListener('click', () => {
loadBaseModelsStatus(true);
});
}
if (baseModelsUpdateBtn) {
baseModelsUpdateBtn.addEventListener('click', () => {
updateBaseModels();
});
}
if (hfIndexRefreshBtn) {
hfIndexRefreshBtn.addEventListener('click', () => {
refreshHfIndex();
@@ -766,6 +902,7 @@ export const optionsMethods = {
}
loadModelListStatus(false);
loadBaseModelsStatus(false);
loadHfIndexStatus();
if (saveBtn) {