+ `;
+ }
+
+ /**
+ * Render a progress bar
+ * @param {number} percent - Progress percentage (0-100)
+ * @param {string} leftText - Text on the left
+ * @param {string} rightText - Text on the right
+ * @returns {string} HTML for progress bar
+ */
+ renderProgressBar(percent, leftText = '', rightText = '') {
+ return `
+
`;
@@ -352,29 +867,48 @@ class LinkerManagerDialog extends ComfyDialog {
// Check if there are active downloads
const activeCount = Object.keys(this.activeDownloads).length;
+
+ // Check if any model has a 100% confidence match
+ const hasAny100Match = missingModels.some(m =>
+ (m.matches || []).some(match => match.confidence === 100)
+ );
+
+ // Show/hide Auto-Link button based on whether 100% matches exist
+ if (this.autoResolveButton) {
+ this.autoResolveButton.style.display = hasAny100Match ? 'inline-flex' : 'none';
+ }
+
+ // Hide download all button if no missing models
+ if (this.downloadAllButton) {
+ this.downloadAllButton.style.display = totalMissing > 0 ? 'inline-flex' : 'none';
+ }
if (totalMissing === 0 && activeCount === 0) {
- container.innerHTML = '
✓ No missing models found. All models are available!
';
+ 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 = `
-
';
// Sort missing models: those with 100% confidence matches first, then others
const sortedMissingModels = missingModels.sort((a, b) => {
@@ -475,35 +1009,48 @@ class LinkerManagerDialog extends ComfyDialog {
// 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);
-
- let html = `
`;
- // Display subgraph name as primary identifier if available, otherwise show node type
- // A node type that's a UUID indicates it's a subgraph instance
+ // Format the missing filename for display
+ const missingFilename = this.formatFilename(missing.original_path, 60);
+
+ // Determine node info for the chip
const isSubgraphNode = missing.node_type && missing.node_type.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
-
+ let nodeLabel;
if (missing.subgraph_name) {
- // Show subgraph name as primary identifier
- html += `
`;
+ nodeLabel = missing.subgraph_name;
} else if (isSubgraphNode) {
- // Node type is a UUID (subgraph) but we don't have the name (shouldn't happen, but handle gracefully)
- html += `
`;
+
+ // Card Header: Filename as headline + node chip
+ html += `
`;
+ html += `
${missingFilename.display}
`;
+ html += `
`;
+ if (missing.category) {
+ html += `${missing.category}`;
+ }
+ html += `${nodeLabel} #${missing.node_id}`;
+ html += `
`;
+ html += `
`;
+
+ // Two-column layout
+ html += `
`;
+
+ // LEFT COLUMN: Local Matches
+ html += `
`;
+ html += `
Local Matches
`;
+
if (hasMatches) {
// If we have 100% matches, only show those. Otherwise, show other matches sorted by confidence
const matchesToShow = perfectMatches.length > 0
? perfectMatches
: otherMatches.sort((a, b) => b.confidence - a.confidence).slice(0, 5);
- html += `
Suggested Matches:
`;
- html += '
';
-
// Sort: 100% matches first, then by confidence descending
const sortedMatches = matchesToShow.sort((a, b) => {
if (a.confidence === 100 && b.confidence !== 100) return -1;
@@ -514,114 +1061,98 @@ class LinkerManagerDialog extends ComfyDialog {
for (let matchIndex = 0; matchIndex < sortedMatches.length; matchIndex++) {
const match = sortedMatches[matchIndex];
const buttonId = `resolve-${missing.node_id}-${missing.widget_index}-${matchIndex}`;
- html += `
`;
- html += `${match.model?.relative_path || match.filename} `;
- html += `
- (${match.confidence}% confidence)
- `;
- // Show resolve button for all matches (100% or < 100%)
- html += `
- Resolve
- `;
- html += `
`;
+ html += this.getConfidenceBadge(match.confidence);
+ html += `${formattedPath.display}`;
+ html += ``;
+ html += `🔗 Link`;
+ html += ``;
+ html += `
`;
}
- html += '
';
-
// Add note if only showing 100% matches
if (perfectMatches.length > 0 && otherMatches.length > 0) {
- html += `
Showing only 100% confidence matches. ${otherMatches.length} other match${otherMatches.length > 1 ? 'es' : ''} available.
`;
}
} else if (allMatches.length > 0 && filteredMatches.length === 0) {
- // Had matches but all were below 70% threshold
- html += `
No local matches found above 70% confidence threshold.
`;
+ html += `
No matches above 70% confidence
`;
} else {
- html += `
No local matches found.
`;
+ html += `
No local matches found
`;
}
-
- // Show download option when no 100% local match exists
+
+ html += `
`; // End left column
+
+ // RIGHT COLUMN: Download Option
+ html += `
`;
+ html += `
Download
`;
+
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 (perfectMatches.length > 0) {
+ // Has perfect local match - download not needed
+ html += `
Not needed - exact local match available
`;
+ } else if (downloadSource && downloadSource.url) {
+ // We have a known download URL
+ const isExact = downloadSource.match_type === 'exact' || downloadSource.source === 'popular' || downloadSource.source === 'huggingface' || downloadSource.source === 'civitai';
+ const isFromWorkflow = downloadSource.url_source === 'workflow';
+ const sourceLabels = {
+ 'popular': 'Popular Models',
+ 'model_list': 'Model Database',
+ 'huggingface': 'HuggingFace',
+ 'civitai': 'CivitAI',
+ 'workflow': 'Workflow'
+ };
+ const sourceLabel = isFromWorkflow ? 'Workflow' : (sourceLabels[downloadSource.source] || 'Online');
+ const downloadFilename = downloadSource.filename || filename;
+ const formattedDownloadName = this.formatFilename(downloadFilename, 45);
- 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 isFromWorkflow = downloadSource.url_source === 'workflow';
- const confidence = downloadSource.confidence ? ` (${downloadSource.confidence}% match)` : '';
- const sourceLabels = {
- 'popular': 'Popular Models',
- 'model_list': 'Model Database',
- 'huggingface': 'HuggingFace',
- 'civitai': 'CivitAI',
- 'workflow': 'Workflow'
- };
- // If URL is from workflow, show that as primary source
- const sourceLabel = isFromWorkflow ? 'Workflow' : (sourceLabels[downloadSource.source] || 'Online');
- const downloadFilename = downloadSource.filename || filename;
- const modelName = downloadSource.name ? ` (${downloadSource.name})` : '';
- // Format file size - if it's a number (bytes), format it nicely
- let sizeDisplay = '';
- if (downloadSource.size) {
- if (typeof downloadSource.size === 'number') {
- sizeDisplay = this.formatBytes(downloadSource.size);
- } else {
- sizeDisplay = downloadSource.size;
- }
- }
-
- html += `
`;
- html += `
- Download
- `;
- html += `
`;
- html += `
`;
- if (isFromWorkflow) {
- html += `✓ URL embedded in workflow`;
+ // Format file size
+ let sizeDisplay = '';
+ if (downloadSource.size) {
+ if (typeof downloadSource.size === 'number') {
+ sizeDisplay = this.formatBytes(downloadSource.size);
} else {
- html += isExact ? `✓ Found on ${sourceLabel}` : `~ Similar model found${confidence}`;
+ sizeDisplay = downloadSource.size;
}
- html += `
`;
- html += `
`;
- // Make filename a clickable link to model card if we can extract it
- const modelCardUrl = this.getModelCardUrl(downloadSource.url);
- if (modelCardUrl) {
- html += `${downloadFilename}`;
- } else {
- html += `${downloadFilename}`;
- }
- html += `${modelName}`;
- if (sizeDisplay) {
- html += ` [${sizeDisplay}]`;
- }
- html += `
`;
- html += `
`;
- html += `
`;
- } else {
- // No known download - offer search
- html += `
- 🔍 Search Online
- `;
- html += ``;
}
- html += ``;
+ html += `
`;
+ html += ``;
+ html += `☁ Download${sizeDisplay ? ` (${sizeDisplay})` : ''}`;
+ html += ``;
+ html += `
`;
+ html += `${isFromWorkflow ? 'URL from workflow' : sourceLabel}`;
+ const modelCardUrl = this.getModelCardUrl(downloadSource.url);
+ if (modelCardUrl) {
+ html += ` ${formattedDownloadName.display}`;
+ } else {
+ html += ` ${formattedDownloadName.display}`;
+ }
html += `
`;
+ html += `
`;
+ } else {
+ // No known download - offer search
+ html += `
`;
+ html += ``;
+ html += `🔍 Search Online`;
+ html += ``;
+ html += `
Search HuggingFace & CivitAI
`;
+ html += `
`;
+ html += ``;
}
-
- html += '
';
+
+ // Progress container (for downloads)
+ html += ``;
+
+ html += `
`; // End right column
+ html += `
`; // End columns
+
+ html += `
`; // End card
return html;
}
@@ -823,15 +1354,14 @@ class LinkerManagerDialog extends ComfyDialog {
/**
* Auto-resolve all 100% confidence matches
+ * @returns {object|null} The updated workflow if successful, null otherwise
*/
async autoResolve100Percent() {
- if (!this.contentElement) return;
-
try {
const workflow = this.getCurrentWorkflow();
if (!workflow) {
this.showNotification('No workflow loaded', 'error');
- return;
+ return null;
}
// Analyze workflow first
@@ -869,7 +1399,7 @@ class LinkerManagerDialog extends ComfyDialog {
if (resolutions.length === 0) {
this.showNotification('No 100% confidence matches found to auto-resolve.', 'error');
- return;
+ return null;
}
// Apply resolutions
@@ -898,16 +1428,22 @@ class LinkerManagerDialog extends ComfyDialog {
'success'
);
- // Reload dialog using the updated workflow from API response
- // This ensures we're analyzing the correct updated workflow
- await this.loadWorkflowData(resolveData.workflow);
+ // Reload dialog using the updated workflow from API response (if dialog is visible)
+ if (this.contentElement) {
+ await this.loadWorkflowData(resolveData.workflow);
+ }
+
+ // Return the updated workflow for callers who need it
+ return resolveData.workflow;
} else {
this.showNotification('Failed to resolve models: ' + (resolveData.error || 'Unknown error'), 'error');
+ return null;
}
} catch (error) {
console.error('Model Linker: Error auto-resolving:', error);
this.showNotification('Error auto-resolving: ' + error.message, 'error');
+ return null;
}
}
@@ -1096,19 +1632,20 @@ class LinkerManagerDialog extends ComfyDialog {
}
if (progressDiv) {
progressDiv.style.display = 'block';
- // Show progress bar with cancel button immediately (like WMD)
+ // Show progress bar with cancel button immediately
progressDiv.innerHTML = `
-
`;
- html += `${matchType} in Model Database${confidence}:`;
+ html += `
`;
+ html += `${matchType} in Model Database${confidence}`;
html += `
`;
- html += `${modelListResult.filename}`;
+ html += `${modelListResult.filename}`;
if (modelListResult.name) {
- html += ` (${modelListResult.name})`;
+ html += ` ${modelListResult.name}`;
}
if (modelListResult.size) {
- html += ` [${modelListResult.size}]`;
+ html += ` [${modelListResult.size}]`;
}
html += `
`;
html += `
`;
- html += `
- Download
- `;
+ html += ``;
+ html += `☁ Download`;
+ html += ``;
html += `
`;
}
// HuggingFace result
if (hfResult && hfResult.url) {
const hfRepo = hfResult.repo_id || hfResult.repo || '';
- html += `
`;
- html += `✓ Found on HuggingFace:`;
+ html += `
`;
+ html += `Found on HuggingFace`;
html += `
`;
- html += `${hfResult.filename} `;
- html += `(${hfRepo})`;
+ html += `${hfResult.filename} `;
+ html += `${hfRepo}`;
html += `
`;
html += `
`;
- html += `
- Download
- `;
+ html += ``;
+ html += `☁ Download`;
+ html += ``;
html += `
`;
}
// CivitAI result
if (civitaiResult && civitaiResult.download_url) {
- html += `
`;
- html += `✓ Found on CivitAI:`;
+ html += `
`;
+ html += `Found on CivitAI`;
html += `
`;
- html += `${civitaiResult.filename || civitaiResult.name} `;
- html += `(${civitaiResult.type || civitaiResult.name})`;
+ html += `${civitaiResult.filename || civitaiResult.name} `;
+ html += `${civitaiResult.type || civitaiResult.name}`;
html += `
`;
html += `
`;
- html += `
- Download
- `;
+ html += ``;
+ html += `☁ Download`;
+ html += ``;
html += `
`;
}
@@ -1472,19 +2000,20 @@ class LinkerManagerDialog extends ComfyDialog {
if (progressDiv) {
progressDiv.style.display = 'block';
- // Show progress bar with cancel button immediately (like WMD)
+ // Show progress bar with cancel button immediately
progressDiv.innerHTML = `
-
+
-
-
+
+
-
+
Cancel
-
Connecting...
+
+ Connecting...
+
`;
}
@@ -1522,7 +2051,7 @@ class LinkerManagerDialog extends ComfyDialog {
} catch (error) {
console.error('Model Linker: Download error:', error);
if (progressDiv) {
- progressDiv.innerHTML = `Error: ${error.message}`;
+ progressDiv.innerHTML = this.renderStatusMessage(error.message, 'error');
}
btn.disabled = false;
btn.textContent = 'Retry';
@@ -1733,20 +2262,11 @@ class ModelLinker {
// Check if we already injected buttons
if (dialog.querySelector('#model-linker-btn-container')) return;
- // Find a suitable place to inject the buttons
+ // Find a suitable place to inject the button
const injectButtons = () => {
- // Create container for our buttons
- const btnContainer = document.createElement('div');
- btnContainer.id = 'model-linker-btn-container';
- btnContainer.style.cssText = `
- display: flex;
- gap: 8px;
- margin: 12px 16px;
- `;
-
// Common button style
const btnStyle = `
- padding: 8px 14px;
+ padding: 6px 12px;
color: white;
border: none;
border-radius: 6px;
@@ -1757,59 +2277,53 @@ class ModelLinker {
white-space: nowrap;
`;
- // Auto-resolve button
+ // Auto-resolve button (green)
const autoResolveBtn = document.createElement('button');
- autoResolveBtn.id = 'model-linker-auto-resolve-btn';
+ autoResolveBtn.id = 'model-linker-btn-container'; // Use this ID to prevent duplicate injection
autoResolveBtn.textContent = '🔗 Auto-resolve 100%';
autoResolveBtn.title = 'Automatically link models with 100% confidence matches';
- autoResolveBtn.style.cssText = btnStyle + `background: #2196F3;`;
+ autoResolveBtn.style.cssText = btnStyle + `background: #4CAF50;`;
autoResolveBtn.addEventListener('mouseenter', () => {
- autoResolveBtn.style.background = '#1976D2';
+ autoResolveBtn.style.background = '#45a049';
});
autoResolveBtn.addEventListener('mouseleave', () => {
- autoResolveBtn.style.background = '#2196F3';
+ autoResolveBtn.style.background = '#4CAF50';
});
autoResolveBtn.addEventListener('click', async () => {
await this.handleAutoResolveInPopup(dialog, autoResolveBtn);
});
- // Download missing button
- const downloadBtn = document.createElement('button');
- downloadBtn.id = 'model-linker-download-btn';
- downloadBtn.textContent = '⬇ Download Missing';
- downloadBtn.title = 'Open Model Linker to download missing models with progress tracking';
- downloadBtn.style.cssText = btnStyle + `background: #4CAF50;`;
-
- downloadBtn.addEventListener('mouseenter', () => {
- downloadBtn.style.background = '#45a049';
- });
- downloadBtn.addEventListener('mouseleave', () => {
- downloadBtn.style.background = '#4CAF50';
- });
- downloadBtn.addEventListener('click', () => {
- // Close the popup
- const closeBtn = dialog.querySelector('button[class*="close"]') ||
- dialog.querySelector('svg')?.closest('button') ||
- Array.from(dialog.querySelectorAll('button')).find(b => b.textContent === '×' || b.innerHTML.includes('×'));
- if (closeBtn) {
- closeBtn.click();
+ // Find the "Don't show this again" checkbox row and add button next to it
+ const checkbox = dialog.querySelector('input[type="checkbox"]');
+ if (checkbox) {
+ const checkboxRow = checkbox.closest('label') || checkbox.parentElement;
+ if (checkboxRow && checkboxRow.parentElement) {
+ // Make the parent a flex container to align checkbox and button
+ checkboxRow.parentElement.style.cssText = `
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ padding: 0 16px;
+ margin-bottom: 8px;
+ `;
+ // Insert button at the beginning (left side)
+ checkboxRow.parentElement.insertBefore(autoResolveBtn, checkboxRow);
+ return;
}
- // Open Model Linker
- this.openLinkerManager();
- });
+ }
- btnContainer.appendChild(autoResolveBtn);
- btnContainer.appendChild(downloadBtn);
-
- // Find the list of models (usually a scrollable container with the model items)
+ // Fallback: Find the list of models and insert before it
const modelList = dialog.querySelector('[style*="overflow"]') ||
dialog.querySelector('[class*="list"]') ||
dialog.querySelector('[class*="content"]');
if (modelList) {
- // Insert before the model list
- modelList.parentElement?.insertBefore(btnContainer, modelList);
+ // Create a wrapper and insert before the model list
+ const wrapper = document.createElement('div');
+ wrapper.style.cssText = 'display: flex; justify-content: flex-end; padding: 0 16px; margin-bottom: 8px;';
+ wrapper.appendChild(autoResolveBtn);
+ modelList.parentElement?.insertBefore(wrapper, modelList);
} else {
// Find after the description text
const allElements = dialog.querySelectorAll('*');
@@ -1854,14 +2368,12 @@ class ModelLinker {
this.dialog = new LinkerManagerDialog();
}
- // Run auto-resolve for 100% matches
- await this.dialog.autoResolve100Percent();
-
- // Small delay then open the Model Linker UI to show remaining models
- await new Promise(r => setTimeout(r, 300));
+ // Run auto-resolve for 100% matches - returns the updated workflow
+ const updatedWorkflow = await this.dialog.autoResolve100Percent();
- // Open Model Linker to handle remaining unlinked/downloadable models
- this.openLinkerManager();
+ // Always open Model Linker to show remaining unresolved models
+ // Pass the updated workflow if available to avoid race condition
+ this.dialog.show(updatedWorkflow || null);
}
/**