feat: add base model filtering across search sources, API routes, and frontend UI
This commit is contained in:
@@ -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);
|
||||
},
|
||||
|
||||
|
||||
@@ -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?.();
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user