${detailMissing ? this.renderMissingModel(detailMissing, detailIndex) : this.renderStatusMessage('Select a missing model to inspect details.', 'info')}
`;
}
comboList.innerHTML = html;
comboList.querySelectorAll('div[data-idx]').forEach(el => {
el.addEventListener('click', () => {
const idx = parseInt(el.dataset.idx, 10);
if (!Number.isNaN(idx) && idx >= 0 && idx < allModels.length) {
const chosenModel = allModels[idx];
if (chosenModel) {
this.queueResolution(missing, chosenModel);
}
}
});
});
};
if (comboList) {
populateComboOptions('');
}
if (comboInput) {
const debouncedFilter = this.debounce(() => {
populateComboOptions(comboInput.value);
}, 200);
comboInput.addEventListener('input', debouncedFilter);
comboInput.addEventListener('focus', () => {
if (comboList) {
comboList.classList.remove('mr-is-hidden');
comboList.classList.add('mr-is-visible');
}
populateComboOptions(comboInput.value);
});
comboInput.addEventListener('blur', () => {
setTimeout(() => {
if (comboList) {
comboList.classList.remove('mr-is-visible');
comboList.classList.add('mr-is-hidden');
}
}, 200);
});
}
if (comboRefresh) {
comboRefresh.addEventListener('click', async () => {
this.allModels = null;
await this.ensureAllModelsLoaded();
populateComboOptions(comboInput?.value || '');
});
}
const state = this.searchResultCache.get(this.getMissingSearchKey(missing));
const searchResultsDiv = container.querySelector(`#search-results-${missing.node_id}-${missing.widget_index}`);
if (state && searchResultsDiv && this.hasRenderableSearchState(state)) {
searchResultsDiv.classList.remove('mr-is-hidden');
searchResultsDiv.classList.add('mr-is-visible');
this.displaySearchResults(missing, state, searchResultsDiv);
}
},
/**
* Display missing models in the dialog
*/
displayMissingModels(container, data) {
const missingModels = data.missing_models || [];
const totalMissing = data.total_missing || 0;
this.missingModels = missingModels;
this.syncBatchSelectionForMissingModels(missingModels);
// Check if there are active downloads
const activeCount = Object.keys(this.activeDownloads).length;
const hasAny100Match = missingModels.some(missing =>
(missing.matches || []).some(match => match.confidence === 100)
);
this.setMissingFooterControlsVisible(totalMissing > 0 || activeCount > 0);
// Hide download all button if no missing models
if (this.downloadAllButton) {
this.downloadAllButton.style.display = (totalMissing > 0 || activeCount > 0) ? 'inline-flex' : 'none';
}
if (totalMissing === 0 && activeCount === 0) {
container.innerHTML = this.renderStatusMessage('All models are available! No missing models found.', 'success');
return;
}
// If no missing models but downloads are active, show a waiting message
if (totalMissing === 0 && activeCount > 0) {
container.innerHTML = this.renderStatusMessage(
`${activeCount} download${activeCount > 1 ? 's' : ''} in progress. Local matches will refresh when complete.`,
'info'
);
return;
}
// Skip rendering if active tab is not "missing"
if (this.activeTab !== 'missing') {
container.innerHTML = '';
return;
}
// Sort missing models: those with 100% confidence matches first, then others
const sortedMissingModels = [...missingModels].sort((a, b) => {
const aMatches = a.matches || [];
const bMatches = b.matches || [];
// Filter to 70%+ confidence
const aFiltered = aMatches.filter(m => m.confidence >= 70);
const bFiltered = bMatches.filter(m => m.confidence >= 70);
// Check if they have 100% matches
const aHas100 = aFiltered.some(m => m.confidence === 100);
const bHas100 = bFiltered.some(m => m.confidence === 100);
// If one has 100% and the other doesn't, prioritize the one with 100%
if (aHas100 && !bHas100) return -1;
if (!aHas100 && bHas100) return 1;
// If both have 100% or neither has 100%, sort by best confidence
const aBestConf = aFiltered.length > 0 ? Math.max(...aFiltered.map(m => m.confidence)) : 0;
const bBestConf = bFiltered.length > 0 ? Math.max(...bFiltered.map(m => m.confidence)) : 0;
return bBestConf - aBestConf; // Higher confidence first
});
for (let mi = 0; mi < sortedMissingModels.length; mi++) {
sortedMissingModels[mi].__displayIndex = mi;
}
const selectedStillExists = sortedMissingModels.some(missing => (
this.getMissingModelKey(missing) === this.selectedMissingModelKey
));
if (!selectedStillExists) {
this.selectedMissingModelKey = sortedMissingModels.length
? this.getMissingModelKey(sortedMissingModels[0])
: null;
}
this.missingModels = sortedMissingModels;
container.innerHTML = this.renderMissingModelsBrowser(
sortedMissingModels,
this.selectedMissingModelKey,
totalMissing,
activeCount,
hasAny100Match
);
this.wireMissingModelsBrowser(container, data, sortedMissingModels);
this.scheduleInitialUrnLocalMatchRefresh(sortedMissingModels, container, data);
this.reconnectActiveSearchProgress(sortedMissingModels);
this.updateBatchFooterButtons();
},
async refreshMissingAnalysis(button = null) {
if (button?.disabled) return;
try {
if (button) {
button.disabled = true;
button.classList.add('mr-btn-is-disabled');
}
this.showNotification('Refreshing missing models and local matches...', 'info');
this.allModels = null;
this.invalidateLoadedModelsCacheForActiveWorkflow?.();
await this.loadWorkflowData(null, { force: true });
} catch (error) {
console.error('Model Resolver: missing analysis refresh failed:', error);
this.showNotification('Refresh failed: ' + error.message, 'error');
} finally {
if (button) {
button.disabled = false;
button.classList.remove('mr-btn-is-disabled');
}
}
},
renderMissingModel(missing, missingIndex = 0) {
const allMatches = missing.matches || [];
// Filter out matches below 70% confidence threshold
const filteredMatches = allMatches.filter(m => m.confidence >= 70);
const hasMatches = filteredMatches.length > 0;
// Calculate 100% matches upfront (needed for download section)
const perfectMatches = filteredMatches.filter(m => m.confidence === 100);
const otherMatches = filteredMatches.filter(m => m.confidence < 100 && m.confidence >= 70);
// Format the missing filename for display
const missingFilename = this.formatFilename(missing.original_path, 60);
// Determine node info for the chip
const nodeDisplay = this.getMissingNodeDisplay(missing);
const nodeChipText = this.escapeHtml(nodeDisplay.text);
// Start card
let html = `
`;
// Card Header: Filename as headline + node chip
html += `
`;
html += `
`;
const titleMetaParts = [];
let titlePrimaryHtml = `${missingFilename.display}`;
let titleSecondaryHtml = '';
const modelId = missing.urn_model_id || missing.urn?.model_id;
const versionId = missing.urn_version_id || missing.urn?.version_id;
const modelUrl = missing.is_urn && modelId ? `https://civitai.com/models/${modelId}${versionId ? '?modelVersionId=' + versionId : ''}` : '';
const urnLoadingId = `urn-loading-${missing.node_id}-${missing.widget_index}`;
if (missing.is_urn) {
titleMetaParts.push(`${missingFilename.display}`);
}
if (missing.is_urn && !missing.civitai_info) {
// URN without info - show Loading and fetch async in background
titlePrimaryHtml = `Resolving CivitAI model...`;
setTimeout(() => this.resolveUrnAsync(modelId, versionId, urnLoadingId, modelUrl), 10);
} else if (missing.is_urn && missing.civitai_info) {
// URN with resolved info - show model name/version
const civitaiInfo = missing.civitai_info;
const civitaiLabelHtml = this.renderVersionedModelNameHtml(civitaiInfo.model_name, civitaiInfo.version_name);
if (civitaiLabelHtml) {
const linkHtml = modelUrl ? `${civitaiLabelHtml}` : `${civitaiLabelHtml}`;
titlePrimaryHtml = `${linkHtml}`;
}
if (civitaiInfo.expected_filename) {
titleSecondaryHtml = `Expected file: ${civitaiInfo.expected_filename}`;
}
}
html += `
`;
html += titleMetaParts.join('');
html += `
${titlePrimaryHtml}
`;
if (titleSecondaryHtml) {
html += titleSecondaryHtml;
}
html += `
`;
const locateId = `locate-${missing.node_id}-${missing.widget_index}`;
const nodeChipClasses = missing.is_top_level !== false ? 'mr-node-chip is-locatable' : 'mr-node-chip';
const nodeChipTitle = missing.is_top_level !== false ? 'Center this node in the ComfyUI graph.' : '';
html += `
`;
if (missing.category) {
html += `${this.getCategoryDisplayName(missing.category)}`;
}
html += ``;
if (missing.is_top_level !== false) {
html += this.getLocateIconHtml();
}
html += `${nodeChipText}`;
html += `
`;
html += `
`;
html += `
`;
// Selected bar - shows if this slot has a queued selection (BELOW card header)
const selectedBarId = `selected-bar-${missing.node_id}-${missing.widget_index}`;
html += ``;
// Two-column layout
html += `
`;
// LEFT COLUMN: Local Matches
html += `
`;
html += `
Local Matches
`;
html += `
`;
html += this.renderLocalMatchesContent(missing, missingIndex);
html += `