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