`;
@@ -301,13 +324,6 @@ class LinkerManagerDialog extends ComfyDialog {
html += `
Category: ${missing.category || 'unknown'}
`;
if (hasMatches) {
- // Filter out matches below 70% confidence threshold
- const filteredMatches = allMatches.filter(m => m.confidence >= 70);
-
- // Separate 100% matches from others (from filtered list)
- const perfectMatches = filteredMatches.filter(m => m.confidence === 100);
- const otherMatches = filteredMatches.filter(m => m.confidence < 100 && m.confidence >= 70);
-
// If we have 100% matches, only show those. Otherwise, show other matches sorted by confidence
const matchesToShow = perfectMatches.length > 0
? perfectMatches
@@ -347,9 +363,61 @@ class LinkerManagerDialog extends ComfyDialog {
}
} else if (allMatches.length > 0 && filteredMatches.length === 0) {
// Had matches but all were below 70% threshold
- html += `
No matches found above 70% confidence threshold.
`;
+ html += `
No local matches found above 70% confidence threshold.
`;
} else {
- html += `
No matches found.
`;
+ html += `
No local matches found.
`;
+ }
+
+ // Show download option when no 100% local match exists
+ const filename = missing.original_path?.split('/').pop()?.split('\\').pop() || '';
+ const downloadSource = missing.download_source;
+
+ // Always show download/search when there's no perfect local match
+ if (perfectMatches.length === 0) {
+ html += `
`;
+
+ if (downloadSource && downloadSource.url) {
+ // We have a known download URL - show Download button
+ const isExact = downloadSource.match_type === 'exact' || downloadSource.source === 'popular' || downloadSource.source === 'huggingface' || downloadSource.source === 'civitai';
+ const confidence = downloadSource.confidence ? ` (${downloadSource.confidence}% match)` : '';
+ const sourceLabels = {
+ 'popular': 'Popular Models',
+ 'model_list': 'Model Database',
+ 'huggingface': 'HuggingFace',
+ 'civitai': 'CivitAI'
+ };
+ const sourceLabel = sourceLabels[downloadSource.source] || 'Online';
+ const downloadFilename = downloadSource.filename || filename;
+ const modelName = downloadSource.name ? ` (${downloadSource.name})` : '';
+ const size = downloadSource.size ? ` [${downloadSource.size}]` : '';
+
+ html += `
`;
+ html += `
+ Download
+ `;
+ html += `
`;
+ html += `
`;
+ html += isExact ? `✓ Found on ${sourceLabel}` : `~ Similar model found${confidence}`;
+ html += `
`;
+ html += `
`;
+ html += `${downloadFilename}${modelName}${size}`;
+ html += `
`;
+ html += `
`;
+ html += `
`;
+ } else {
+ // No known download - offer search
+ html += `
+ 🔍 Search Online
+ `;
+ html += `
`;
+ }
+
+ html += `
`;
+ html += `
`;
}
html += '
';
@@ -621,6 +689,395 @@ class LinkerManagerDialog extends ComfyDialog {
}
}
+ /**
+ * Download a model from a known source
+ */
+ async downloadModel(missing) {
+ const source = missing.download_source;
+ if (!source || !source.url) {
+ this.showNotification('No download URL available', 'error');
+ return;
+ }
+
+ // Use filename from download source if available (may be different from original)
+ const originalFilename = missing.original_path?.split('/').pop()?.split('\\').pop() || 'model.safetensors';
+ const filename = source.filename || originalFilename;
+ const category = source.directory || missing.category || 'checkpoints';
+ const progressId = `download-progress-${missing.node_id}-${missing.widget_index}`;
+ const progressDiv = this.contentElement?.querySelector(`#${progressId}`);
+ const downloadBtn = this.contentElement?.querySelector(`#download-${missing.node_id}-${missing.widget_index}`);
+
+ try {
+ // Disable button and show progress
+ if (downloadBtn) {
+ downloadBtn.disabled = true;
+ downloadBtn.textContent = 'Starting...';
+ }
+ if (progressDiv) {
+ progressDiv.style.display = 'block';
+ progressDiv.innerHTML = '';
+
+ // Popular models result (highest priority)
+ if (popular) {
+ html += `
`;
+ html += `
✓ Found in Popular Models: `;
+ html += `
`;
+ html += `
+ Download
+ `;
+ html += `
`;
+ }
+
+ // Model list result (ComfyUI Manager database with fuzzy matching)
+ if (modelListResult && modelListResult.url) {
+ const confidence = modelListResult.confidence ? ` (${modelListResult.confidence}% match)` : '';
+ const matchType = modelListResult.match_type === 'exact' ? '✓ Exact match' : '~ Similar model';
+ const bgColor = modelListResult.match_type === 'exact' ? 'rgba(76, 175, 80, 0.1)' : 'rgba(156, 39, 176, 0.1)';
+ const textColor = modelListResult.match_type === 'exact' ? '#4CAF50' : '#9C27B0';
+
+ html += `
`;
+ html += `
${matchType} in Model Database${confidence}: `;
+ html += `
`;
+ html += `${modelListResult.filename}`;
+ if (modelListResult.name) {
+ html += ` (${modelListResult.name}) `;
+ }
+ if (modelListResult.size) {
+ html += ` [${modelListResult.size}] `;
+ }
+ html += `
`;
+ html += `
`;
+ html += `
+ Download
+ `;
+ html += `
`;
+ }
+
+ // HuggingFace result
+ if (hfResult && hfResult.url) {
+ html += `
`;
+ html += `
✓ Found on HuggingFace: `;
+ html += `
`;
+ html += `${hfResult.filename} `;
+ html += `(${hfResult.repo}) `;
+ html += `
`;
+ html += `
`;
+ html += `
+ Download
+ `;
+ html += `
`;
+ }
+
+ // CivitAI result
+ if (civitaiResult && civitaiResult.download_url) {
+ html += `
`;
+ html += `
✓ Found on CivitAI: `;
+ html += `
`;
+ html += `${civitaiResult.filename || civitaiResult.name} `;
+ html += `(${civitaiResult.type || civitaiResult.name}) `;
+ html += `
`;
+ html += `
`;
+ html += `
+ Download
+ `;
+ html += `
`;
+ }
+
+ html += '
';
+ container.innerHTML = html;
+
+ // Attach download listeners
+ const downloadBtns = container.querySelectorAll('.search-download-btn');
+ downloadBtns.forEach(btn => {
+ btn.addEventListener('click', () => {
+ const url = btn.dataset.url;
+ const filename = btn.dataset.filename;
+ const category = btn.dataset.category;
+ this.downloadFromSearch(missing, url, filename, category, btn);
+ });
+ });
+ }
+
+ /**
+ * Download from search results
+ */
+ async downloadFromSearch(missing, url, filename, category, btn) {
+ const progressId = `download-progress-${missing.node_id}-${missing.widget_index}`;
+ const progressDiv = this.contentElement?.querySelector(`#${progressId}`);
+
+ try {
+ btn.disabled = true;
+ btn.textContent = 'Starting...';
+
+ if (progressDiv) {
+ progressDiv.style.display = 'block';
+ progressDiv.innerHTML = '