From 994d68d958bee3003b3be1c13df371eeee4b48c5 Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Sun, 19 Apr 2026 18:59:02 +0200 Subject: [PATCH] Add queued model resolutions & combo picker Introduce queuing of model resolutions and a compact combo-style all-models picker. Adds updateSelectedBarForMissing and removeQueuedByKey to show/remove queued selections per missing slot and rebuild pending index after changes. Change individual match buttons (including alternative matches) to queue selections instead of immediately resolving, and wire remove button to key-based removal. Replace the old multi-select search with a debounced dropdown list that shows folder info, supports click-to-queue, focus/blur behavior and a refresh button to reload all models. Also insert a visible selected-bar element into the slot UI and adjust various handlers and logging to reflect the new workflow. --- web/linker.js | 217 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 168 insertions(+), 49 deletions(-) diff --git a/web/linker.js b/web/linker.js index 99c40bf..2a3c673 100644 --- a/web/linker.js +++ b/web/linker.js @@ -1413,6 +1413,75 @@ class LinkerManagerDialog extends ComfyDialog { } catch (e) { /* ignore */ } } + // Update selected bar for a specific missing model slot + updateSelectedBarForMissing(missing) { + if (!missing) return; + const nodeId = missing.node_id; + const widgetIndex = missing.widget_index; + const subgraphId = missing.subgraph_id || ''; + const isTopLevel = missing.is_top_level !== false; + const key = `${nodeId}:${widgetIndex}:${subgraphId}:${isTopLevel ? 'T' : 'F'}`; + + const selectedBar = document.getElementById(`selected-bar-${nodeId}-${widgetIndex}`); + if (!selectedBar) return; + + // Find selection for this slot + let selection = null; + let selectionIdx = -1; + if (this.pendingIndex.has(key)) { + const idx = this.pendingIndex.get(key); + if (idx >= 0 && idx < this.pendingResolutions.length) { + selection = this.pendingResolutions[idx]; + selectionIdx = idx; + } + } + + if (!selection) { + selectedBar.style.display = 'none'; + selectedBar.innerHTML = ''; + return; + } + + // Build selected bar content + const label = selection.resolved_model?.relative_path || selection.resolved_model?.filename || selection.resolved_path || ''; + const resolveBtnId = `selected-remove-${nodeId}-${widgetIndex}`; + + selectedBar.innerHTML = `
`; + selectedBar.innerHTML += `✓ Selected:`; + selectedBar.innerHTML += `${label}`; + selectedBar.innerHTML += ``; + selectedBar.innerHTML += `
`; + selectedBar.style.display = 'block'; + + // Wire remove button - use key-based removal + const removeBtn = selectedBar.querySelector(`#${resolveBtnId}`); + if (removeBtn) { + removeBtn.addEventListener('click', () => { + this.removeQueuedByKey(key); + }); + } + } + + // Remove queued by key (more reliable than index) + removeQueuedByKey(key) { + if (!key || !this.pendingIndex.has(key)) return; + + // Find and remove the item with this key + const idx = this.pendingIndex.get(key); + if (idx >= 0 && idx < this.pendingResolutions.length) { + const r = this.pendingResolutions[idx]; + // Update the selected bar before removing + const m = { node_id: r.node_id, widget_index: r.widget_index, subgraph_id: r.subgraph_id, is_top_level: r.is_top_level }; + + // Remove from array + this.pendingResolutions.splice(idx, 1); + this.rebuildPendingIndex(); + this.updateSelectedBarForMissing(m); + this.updateApplyPendingButton?.(); + this.updateQueuePanel(); + } + } + // Rebuild pending index after modification rebuildPendingIndex() { this.pendingIndex = new Map(); @@ -2016,17 +2085,29 @@ class LinkerManagerDialog extends ComfyDialog { // Remove any existing listeners to prevent duplicates resolveButton.onclick = null; resolveButton.addEventListener('click', () => { - console.log('>>> Link button clicked:', buttonId, missing.original_path, '->', match.model?.filename); - this.resolveModel(missing, match.model).catch(err => { - console.error('resolveModel error:', err); - this.showNotification('Link failed: ' + err.message, 'error'); - }); + console.log('>>> Queue button clicked:', buttonId, missing.original_path, '->', match.model?.filename); + // Queue the selection instead of directly applying + this.queueResolution(missing, match.model); }); } else { console.warn('Button not found:', buttonId); } }); + // Attach alternative (other matches below 100%) button listeners + if (otherMatches && otherMatches.length > 0) { + for (let mIdx = 0; mIdx < otherMatches.length; mIdx++) { + const match = otherMatches[mIdx]; + const altBtnId = `resolve-alt-${missingIndex}-${missing.node_id}-${missing.widget_index}-${mIdx}`; + const altBtn = container.querySelector(`#${altBtnId}`); + if (altBtn) { + altBtn.addEventListener('click', () => { + this.queueResolution(missing, match.model); + }); + } + } + } + // Attach download button listener const downloadBtnId = `download-${missing.node_id}-${missing.widget_index}`; const downloadBtn = container.querySelector(`#${downloadBtnId}`); @@ -2054,56 +2135,88 @@ class LinkerManagerDialog extends ComfyDialog { }); } - // Wire up all-models search + dropdown - const allSearchId = `search-all-${missing.node_id}-${missing.widget_index}`; - const selectAllId = `select-all-${missing.node_id}-${missing.widget_index}`; - const resolveAllId = `resolve-all-${missing.node_id}-${missing.widget_index}`; - const searchEl = container.querySelector(`#${allSearchId}`); - const selectAllEl = container.querySelector(`#${selectAllId}`); - const resolveAllBtn = container.querySelector(`#${resolveAllId}`); + // Wire up all-models search + dropdown (combo-style) + const comboId = `combo-${missing.node_id}-${missing.widget_index}`; + const comboInput = container.querySelector(`#combo-input-${comboId}`); + const comboList = container.querySelector(`#combo-list-${comboId}`); + const comboRefresh = container.querySelector(`#combo-refresh-${comboId}`); const allModels = Array.isArray(this.allModels) ? this.allModels : []; const buildLabel = (m) => `${m.category ? m.category + ': ' : ''}${m.relative_path || m.filename || ''}`; + const getFolder = (m) => m.path || m.base_directory || ''; - const populateAllOptions = (filterText) => { - if (!selectAllEl) return; + // Populate dropdown with filtered models + const populateComboOptions = (filterText, highlightIdx = -1) => { + if (!comboList) return; const f = (filterText || '').toLowerCase(); const filtered = f ? allModels.filter(m => buildLabel(m).toLowerCase().includes(f)) - : allModels; - // Build options HTML - let opts = ''; + : allModels.slice(); // Copy to avoid mutation + + let html = ''; for (let i = 0; i < filtered.length; i++) { const m = filtered[i]; const label = buildLabel(m); - // use index in allModels for value; keep stable mapping - const valueIndex = allModels.indexOf(m); - if (valueIndex >= 0) { - opts += ``; + const folder = getFolder(m); + const isHighlighted = i === highlightIdx; + const folderDisplay = folder ? folder.replace(/\\/g, '/').replace(/:/, '') : ''; + html += `
`; + html += `
`; + html += `${label}`; + html += `
`; + if (folderDisplay) { + html += `
📁 ${folderDisplay}
`; } + html += `
`; } - selectAllEl.innerHTML = opts; + comboList.innerHTML = html; + + // Add click listeners to options + comboList.querySelectorAll('div[data-idx]').forEach(el => { + el.addEventListener('click', () => { + const idx = parseInt(el.dataset.idx, 10); + if (!isNaN(idx) && idx >= 0 && idx < allModels.length) { + const chosenModel = allModels[idx]; + if (chosenModel) { + this.queueResolution(missing, chosenModel); + } + } + }); + }); }; - if (selectAllEl) { - populateAllOptions(''); + // Initial populate + if (comboList) { + populateComboOptions(''); } - if (searchEl) { + + // Filter input with debounce + if (comboInput) { const debouncedFilter = this.debounce(() => { - populateAllOptions(searchEl.value); - }, 250); - searchEl.addEventListener('input', debouncedFilter); + populateComboOptions(comboInput.value); + }, 200); + comboInput.addEventListener('input', debouncedFilter); + + // Show dropdown on focus + comboInput.addEventListener('focus', () => { + if (comboList) comboList.style.display = 'block'; + populateComboOptions(comboInput.value); + }); + + // Close on blur (with delay to allow click) + comboInput.addEventListener('blur', () => { + setTimeout(() => { + if (comboList) comboList.style.display = 'none'; + }, 200); + }); } - if (resolveAllBtn && selectAllEl) { - resolveAllBtn.addEventListener('click', () => { - const idx = parseInt(selectAllEl.value, 10); - if (!isNaN(idx) && idx >= 0 && idx < allModels.length) { - const chosenModel = allModels[idx]; - if (chosenModel) { - // Queue the resolution instead of direct apply - this.queueResolution(missing, chosenModel); - } - } + + // Refresh button - reload all models + if (comboRefresh) { + comboRefresh.addEventListener('click', async () => { + this.allModels = null; // Force reload + await this.ensureAllModelsLoaded(); + populateComboOptions(comboInput?.value || ''); }); } }); @@ -2180,6 +2293,10 @@ class LinkerManagerDialog extends ComfyDialog { // Removed - filename is already shown in title 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 += `
`; @@ -2222,12 +2339,14 @@ class LinkerManagerDialog extends ComfyDialog { html += `
${otherMatches.length} other match${otherMatches.length > 1 ? 'es' : ''} below 100%
`; // Hidden container with other matches html += ``; @@ -2238,15 +2357,15 @@ class LinkerManagerDialog extends ComfyDialog { html += `
No local matches found
`; } - // Add all-models search picker - const searchId = `search-all-${missing.node_id}-${missing.widget_index}`; - const selectAllId = `select-all-${missing.node_id}-${missing.widget_index}`; - const resolveAllId = `resolve-all-${missing.node_id}-${missing.widget_index}`; - html += `
`; - html += `
Search all models:
`; - html += ``; - html += ``; - html += ``; + // Add all-models search picker - combo-style dropdown + const comboId = `combo-${missing.node_id}-${missing.widget_index}`; + html += `
`; + html += `
`; + html += ``; + html += ``; + html += ``; + html += `
`; + html += ``; html += `
`; html += `
`; // End left column