diff --git a/AF_Find_Node_by_ID.js b/AF_Find_Node_by_ID.js index 93c539e..e1f5f3d 100644 --- a/AF_Find_Node_by_ID.js +++ b/AF_Find_Node_by_ID.js @@ -1,12 +1,12 @@ // ****** ComfyUI-AF-Find Node by ID ****** // -// Creator: Alex Furer - Co-Creator(s): Claude AI & QWEN3 Coder +// Creator: Alex Furer - Co-Creator(s): Claude AI & QWEN3 Coder & DeepSeek // // Praise, comment, bugs, improvements: https://github.com/alFrame/ComfyUI_AF_FindNodeByID // // LICENSE: MIT License // -// v0.0.03 +// v0.0.04 // // Description: // A ComfyUI extension that allows you to search for and locate nodes by their ID in complex workflows. @@ -29,6 +29,10 @@ // - Error messages in red // - Fixed dialog width to 340px // +// v0.0.04 +// - Added tabs to search by "ID", by "Title", by "Pack", by "Type" +// - Search 'By Pack' and 'By Type' are experimental features. Due to inconsistencies in how nodes are coded and distributed across different packs, these searches may produce unexpected results or false positives. Use with caution !! +// // Feature Requests / Wet Dreams // - @@ -41,15 +45,27 @@ if (window.AF_Find_Node_by_ID_Widget) { } else { class AF_Find_Node_by_ID_Widget { - constructor() { - this.searchPanel = null; - this.isVisible = false; - this.highlightedNode = null; - this.originalNodeColors = new Map(); - this.searchHistory = []; - this.maxHistory = 10; - this.inspectorMode = false; - } + constructor() { + this.searchPanel = null; + this.isVisible = false; + this.highlightedNode = null; + this.originalNodeColors = new Map(); + this.searchHistory = []; + this.maxHistory = 10; + this.inspectorMode = false; + this.searchTimeout = null; + this.experimentalEnabled = localStorage.getItem('af-find-node-experimental') === 'true'; // ← Change to ONLY true if explicitly set + + // Remember last tab state - default to 'id' if not set + this.currentTab = localStorage.getItem('af-find-node-last-tab') || 'id'; + this.tabInputs = {}; + this.tabHistory = { + id: JSON.parse(localStorage.getItem('af-find-node-history-id') || '[]'), + title: JSON.parse(localStorage.getItem('af-find-node-history-title') || '[]'), + pack: JSON.parse(localStorage.getItem('af-find-node-history-pack') || '[]'), + type: JSON.parse(localStorage.getItem('af-find-node-history-type') || '[]') + }; + } createSearchPanel() { // Create main container @@ -68,7 +84,7 @@ class AF_Find_Node_by_ID_Widget { font-size: 12px; color: #fff; box-shadow: 0 4px 12px rgba(0,0,0,0.5); - width: 340px; /* Fixed width */ + width: 380px; /* Fixed width */ display: none; `; @@ -105,24 +121,138 @@ class AF_Find_Node_by_ID_Widget { titleBar.appendChild(title); titleBar.appendChild(closeBtn); + // Create tab container + const tabContainer = document.createElement('div'); + tabContainer.style.cssText = ` + display: flex; + margin-bottom: 10px; + border-bottom: 1px solid #555; + `; + + // Define tabs + this.tabs = [ + { id: 'id', name: '🔍 By ID', placeholder: 'Enter node ID (e.g., 42)' }, + { id: 'title', name: '📛 By Title', placeholder: 'Search node titles, colors, cnr_id, aux_id...' }, + { id: 'pack', name: '📦 By Pack', placeholder: 'Search node packs (e.g., rgthree, WAS, efficiency)' }, + { id: 'type', name: '🔎 By Type', placeholder: 'Search node types (e.g., KSampler, CLIPTextEncode)' } + ]; + + this.tabInputs = {}; + + // Create tab buttons + this.tabs.forEach(tab => { + const tabBtn = document.createElement('button'); + tabBtn.textContent = tab.name; + tabBtn.dataset.tab = tab.id; + + const isExperimental = tab.id === 'pack' || tab.id === 'type'; + const isActive = tab.id === this.currentTab; + + tabBtn.style.cssText = ` + flex: 1; + padding: 6px 4px; + background: ${isActive ? (isExperimental ? '#7a4a1a' : '#555') : (isExperimental ? '#5a2a0a' : '#333')}; + border: none; + border-bottom: ${isActive ? (isExperimental ? '2px solid #ff9800' : '2px solid #4CAF50') : 'none'}; + border-right: 1px solid #666; + color: white; + cursor: pointer; + font-size: 10px; + border-radius: 0; + opacity: ${isExperimental && !this.experimentalEnabled ? '0.6' : '1'}; + `; + + // Remove right border from last tab + if (tab.id === 'type') { + tabBtn.style.borderRight = 'none'; + } + + tabBtn.onclick = () => { + if ((tab.id === 'pack' || tab.id === 'type') && !this.experimentalEnabled) { + this.AF_Find_Node_by_ID_UpdateResults('Experimental features are disabled. Enable them in settings below.', true); + return; + } + this.switchTab(tab.id); + }; + + tabContainer.appendChild(tabBtn); + }); + + // Add experimental features toggle section + const experimentalSection = document.createElement('div'); + experimentalSection.style.cssText = ` + border-top: 1px solid #555; + padding: 12px 0; + font-size: 10px; + `; + + const experimentalLabel = document.createElement('label'); + experimentalLabel.style.cssText = ` + display: flex; + align-items: center; + gap: 6px; + color: #ff9800; + cursor: pointer; + `; + + const experimentalCheckbox = document.createElement('input'); + experimentalCheckbox.type = 'checkbox'; + experimentalCheckbox.checked = this.experimentalEnabled; + experimentalCheckbox.style.cssText = ` + margin: 0; + `; + + const experimentalText = document.createElement('span'); + experimentalText.innerHTML = 'Enable experimental features (may not work consistently)'; + + experimentalCheckbox.onchange = (e) => { + this.experimentalEnabled = e.target.checked; + localStorage.setItem('af-find-node-experimental', this.experimentalEnabled.toString()); + + // Update tab appearances + this.updateTabAppearance(); + + if (!this.experimentalEnabled && (this.currentTab === 'pack' || this.currentTab === 'type')) { + this.switchTab('id'); // Switch to safe tab if experimental was active + } + + this.AF_Find_Node_by_ID_UpdateResults( + this.experimentalEnabled ? + 'Experimental features enabled. Use with caution!' : + 'Experimental features disabled.' + ); + }; + + experimentalLabel.appendChild(experimentalCheckbox); + experimentalLabel.appendChild(experimentalText); + experimentalSection.appendChild(experimentalLabel); + + // Create tab-specific input + const searchInput = document.createElement('input'); + searchInput.type = 'text'; + searchInput.placeholder = this.tabs[0].placeholder; + searchInput.id = 'af-find-node-by-id-input'; + searchInput.style.cssText = ` + width: 100%; + padding: 6px; + margin-bottom: 8px; + background: #1a1a1a; + border: 1px solid #555; + border-radius: 4px; + color: #fff; + font-family: monospace; + `; + + // Store reference to main input + this.tabInputs.main = searchInput; + // Search input section const searchSection = document.createElement('div'); searchSection.style.marginBottom = '10px'; - const searchInput = document.createElement('input'); - searchInput.type = 'text'; - searchInput.placeholder = 'Enter node ID (e.g., 42)'; + searchInput.placeholder = this.tabs[0].placeholder; searchInput.id = 'af-find-node-by-id-input'; - searchInput.style.cssText = ` - width: 100%; - padding: 6px; - margin-bottom: 8px; - background: #1a1a1a; - border: 1px solid #555; - border-radius: 4px; - color: #fff; - font-family: monospace; - `; + ; const buttonRow = document.createElement('div'); buttonRow.style.cssText = ` @@ -169,21 +299,32 @@ class AF_Find_Node_by_ID_Widget { color: white; cursor: pointer; font-size: 11px; + display: ${this.currentTab === 'id' ? 'block' : 'none'}; `; // Results section - const resultsSection = document.createElement('div'); - resultsSection.id = 'af-find-node-by-id-results'; - resultsSection.style.cssText = ` - max-height: 200px; - overflow-y: auto; - background: #1a1a1a; - border: 1px solid #555; - border-radius: 4px; - padding: 8px; - margin-bottom: 10px; - `; - + const resultsSection = document.createElement('div'); + resultsSection.id = 'af-find-node-by-id-results'; + resultsSection.style.cssText = ` + max-height: 200px; + overflow-y: auto; + background: #1a1a1a; + border: 1px solid #555; + border-radius: 4px; + padding: 8px; + margin-bottom: 10px; + min-height: 40px; // Ensure it's always visible + `; + + const statusSection = document.createElement('div'); + statusSection.id = 'af-find-node-by-id-status'; + statusSection.style.cssText = ` + font-size: 11px; + color: #aaa; + margin-bottom: 8px; + min-height: 14px; + `; + // History section const historySection = document.createElement('div'); historySection.style.cssText = ` @@ -210,30 +351,53 @@ class AF_Find_Node_by_ID_Widget { // Event listeners searchBtn.onclick = () => this.AF_Find_Node_by_ID_Search(); clearBtn.onclick = () => this.AF_Find_Node_by_ID_ClearAll(); - inspectorBtn.onclick = () => this.AF_Find_Node_by_ID_ToggleInspector(); + + inspectorBtn.onclick = () => { + if (this.currentTab === 'id') { + this.AF_Find_Node_by_ID_ToggleInspector(); + } else { + this.AF_Find_Node_by_ID_UpdateResults('Inspector mode is only available in "By ID" tab.', true); + } + }; searchInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { this.AF_Find_Node_by_ID_Search(); } }); + + // Add debouncing to the search input + searchInput.addEventListener('input', (e) => { + // Only auto-search for text-based tabs, not ID tab + if (this.currentTab !== 'id') { + clearTimeout(this.searchTimeout); + this.searchTimeout = setTimeout(() => { + if (e.target.value.trim().length >= 2) { // Only search if 2+ characters + this.AF_Find_Node_by_ID_Search(); + } + }, 300); // Wait 300ms after typing stops + } + }); // Assemble the panel buttonRow.appendChild(searchBtn); buttonRow.appendChild(clearBtn); buttonRow.appendChild(inspectorBtn); - searchSection.appendChild(searchInput); - searchSection.appendChild(buttonRow); + searchSection.appendChild(tabContainer); + searchSection.appendChild(searchInput); + searchSection.appendChild(buttonRow); - historySection.appendChild(historyTitle); - historySection.appendChild(historyList); - - panel.appendChild(titleBar); - panel.appendChild(searchSection); - panel.appendChild(resultsSection); - panel.appendChild(historySection); + historySection.appendChild(historyTitle); + historySection.appendChild(historyList); + panel.appendChild(titleBar); + panel.appendChild(searchSection); + panel.appendChild(statusSection); + panel.appendChild(resultsSection); + panel.appendChild(experimentalSection); + panel.appendChild(historySection); + document.body.appendChild(panel); this.searchPanel = panel; @@ -241,16 +405,80 @@ class AF_Find_Node_by_ID_Widget { this.AF_Find_Node_by_ID_UpdateResults('Ready to search. Enter a node ID or use Inspector mode.'); } - AF_Find_Node_by_ID_ShowPanel() { - if (!this.searchPanel) { - this.createSearchPanel(); - } - this.searchPanel.style.display = 'block'; - this.isVisible = true; - // Clear the search field when dialog is shown - document.getElementById('af-find-node-by-id-input').value = ''; - document.getElementById('af-find-node-by-id-input').focus(); - } + switchTab(tabId) { + if ((tabId === 'pack' || tabId === 'type') && !this.experimentalEnabled) { + this.AF_Find_Node_by_ID_UpdateResults('Experimental features are disabled. Enable them below.', true); + return; + } + this.currentTab = tabId; + + // Save to localStorage + localStorage.setItem('af-find-node-last-tab', tabId); + + // Update tab buttons + this.updateTabAppearance(); + + // Update input placeholder + const tabConfig = this.tabs.find(t => t.id === tabId); + this.tabInputs.main.placeholder = tabConfig.placeholder; + + // Show/hide inspector button based on tab + this.updateInspectorButtonVisibility(); + + // Clear previous results, input, and highlight + this.tabInputs.main.value = ''; + this.AF_Find_Node_by_ID_ClearHighlight(); + this.showResultsList([], ''); // Clear the results list + this.AF_Find_Node_by_ID_UpdateResults(`Switched to ${tabConfig.name} search. Ready to search.`); + + // Update history for current tab + this.AF_Find_Node_by_ID_UpdateHistory(); + } + + updateInspectorButtonVisibility() { + const inspectorBtn = document.getElementById('af-find-node-by-id-inspector-toggle'); + if (inspectorBtn) { + if (this.currentTab === 'id') { + inspectorBtn.style.display = 'block'; + } else { + inspectorBtn.style.display = 'none'; + // Also disable inspector mode if switching away from ID tab + if (this.inspectorMode) { + this.AF_Find_Node_by_ID_SetInspectorMode(false); + } + } + } + } + + updateTabAppearance() { + document.querySelectorAll('#af-find-node-by-id-panel [data-tab]').forEach(btn => { + const tabId = btn.dataset.tab; + const isExperimental = tabId === 'pack' || tabId === 'type'; + const isActive = tabId === this.currentTab; + + btn.style.background = isActive ? (isExperimental ? '#7a4a1a' : '#555') : (isExperimental ? '#5a2a0a' : '#333'); + btn.style.borderBottom = isActive ? (isExperimental ? '2px solid #ff9800' : '2px solid #4CAF50') : 'none'; + btn.style.opacity = isExperimental && !this.experimentalEnabled ? '0.6' : '1'; + }); + } + + AF_Find_Node_by_ID_ShowPanel() { + if (!this.searchPanel) { + this.createSearchPanel(); + } + this.searchPanel.style.display = 'block'; + this.isVisible = true; + + // Set to remembered tab instead of always 'id' + this.switchTab(this.currentTab); + + // Clear the search field when dialog is shown but keep results + document.getElementById('af-find-node-by-id-input').value = ''; + document.getElementById('af-find-node-by-id-input').focus(); + + // Update tab appearance to reflect experimental setting + this.updateTabAppearance(); // Add this line + } AF_Find_Node_by_ID_HidePanel() { if (this.searchPanel) { @@ -269,31 +497,277 @@ class AF_Find_Node_by_ID_Widget { } } - AF_Find_Node_by_ID_Search() { - const input = document.getElementById('af-find-node-by-id-input'); - const searchId = input.value.trim(); - - if (!searchId) { - this.AF_Find_Node_by_ID_UpdateResults('Please enter a node ID', true); - return; - } + AF_Find_Node_by_ID_Search() { + const searchTerm = this.tabInputs.main.value.trim(); + + if (!searchTerm) { + this.AF_Find_Node_by_ID_UpdateResults('Please enter a search term', true); + return; + } - const nodeId = parseInt(searchId); - if (isNaN(nodeId)) { - this.AF_Find_Node_by_ID_UpdateResults('Invalid node ID. Please enter a number.', true); - return; - } + let results = []; + + switch (this.currentTab) { + case 'id': + results = this.searchById(searchTerm); + break; + case 'title': + results = this.searchByTitle(searchTerm); + break; + case 'pack': + results = this.searchByPack(searchTerm); + break; + case 'type': + results = this.searchByType(searchTerm); + break; + } + + this.handleSearchResults(results, searchTerm); + } - const node = this.AF_Find_Node_by_ID_FindNodeById(nodeId); - if (node) { - this.AF_Find_Node_by_ID_HighlightNode(node); - this.AF_Find_Node_by_ID_CenterOnNode(node); - this.AF_Find_Node_by_ID_AddToHistory(nodeId); - this.AF_Find_Node_by_ID_UpdateResults(`Found node ${nodeId}: ${node.type || 'Unknown Type'}`); - } else { - this.AF_Find_Node_by_ID_UpdateResults(`Node ${nodeId} not found in current workflow`, true); - } - } + searchById(searchTerm) { + const nodeId = parseInt(searchTerm); + if (isNaN(nodeId)) { + this.AF_Find_Node_by_ID_UpdateResults('Invalid node ID. Please enter a number.', true); + return []; + } + + const node = this.AF_Find_Node_by_ID_FindNodeById(nodeId); + return node ? [node] : []; + } + + searchByTitle(searchTerm) { + const term = searchTerm.toLowerCase(); + return app.graph.nodes.filter(node => { + return ( + (node.title && node.title.toLowerCase().includes(term)) || + (node.color && node.color.toLowerCase().includes(term)) || + (node.cnr_id && node.cnr_id.toString().includes(term)) || + (node.aux_id && node.aux_id.toString().includes(term)) || + (node.name && node.name.toLowerCase().includes(term)) + ); + }); + } + + getNodeBadgeInfo(node) { + if (!node) return null; + + // Check ComfyUI's node registry first - this is where pack info is usually stored + if (app.node_types && node.type) { + const nodeDef = app.node_types[node.type]; + if (nodeDef?.category) { + return nodeDef.category; + } + if (nodeDef?.name) { + return nodeDef.name; + } + } + + // Check node constructor properties + if (node.constructor?.nodeData?.category) { + return node.constructor.nodeData.category; + } + if (node.constructor?.category) { + return node.constructor.category; + } + if (node.constructor?.title) { + return node.constructor.title; + } + + // Check for comfyClass which sometimes contains pack info + if (node.comfyClass) { + return node.comfyClass; + } + + // Check cnr_id as a pack identifier + if (node.cnr_id) { + return node.cnr_id.toString(); + } + + // For specific known packs, we can also check the type string + const nodeType = node.type || ''; + if (nodeType.includes('WAS')) return 'WAS Suite'; + if (nodeType.includes('rgthree')) return 'rgthree'; + if (nodeType.includes('efficiency')) return 'Efficiency'; + if (nodeType.includes('ComfyUI-Impact-Pack')) return 'Impact Pack'; + if (nodeType.includes('pysssss')) return 'Custom Scripts'; + if (nodeType.includes('easy')) return 'EasyUse'; + + return null; + } + + getPackAliases(packName) { + const aliasMap = { + 'pysssss': ['custom scripts', 'custom-scripts', 'pysssss'], + 'was': ['was suite', 'was', 'wolfang'], + 'rgthree': ['rgthree', 'rg3'], + 'efficiency': ['efficiency', 'eff'], + 'comfyui-manager': ['manager', 'comfyui-manager'], + 'easyuse': ['easy use', 'easy-use', 'easyuse', 'easy'], + 'controlnet': ['controlnet', 'control net', 'comfyui_controlnet_aux'] // Add this + }; + + const searchTerm = packName.toLowerCase(); + + // First check for exact matches + for (const [canonicalName, aliases] of Object.entries(aliasMap)) { + if (aliases.includes(searchTerm)) { + return canonicalName; + } + } + + // Then check for partial matches (if the search term is part of any alias) + for (const [canonicalName, aliases] of Object.entries(aliasMap)) { + for (const alias of aliases) { + if (alias.includes(searchTerm) || searchTerm.includes(alias)) { + return canonicalName; + } + } + } + + return packName; + } + + checkNodeFilePath(node, term) { + // Some nodes store file path info that contains pack names + if (node.constructor?.nodeData?.filename) { + return node.constructor.nodeData.filename.toLowerCase().includes(term); + } + return false; + } + + searchByPack(searchTerm) { + const term = searchTerm.toLowerCase().trim(); + if (!term) { + return []; + } + + if (!app.graph || !app.graph.nodes) { + console.warn("AF_Find_Node: No graph or nodes available"); + return []; + } + + // Use aliases to normalize the search term + const normalizedTerm = this.getPackAliases(term); + + console.log("Pack search - original:", searchTerm, "normalized:", normalizedTerm); + + return app.graph.nodes.filter(node => { + // Get node category/source from multiple possible locations + const nodeSource = this.getNodeBadgeInfo(node); + + // Also check the node type which often contains pack info + const nodeType = (node.type || '').toLowerCase(); + + // Check pack-specific identifiers + const nodeCnrId = (node.cnr_id || '').toString().toLowerCase(); + + // Make ALL comparisons case-insensitive by converting to lowercase + const nodeSourceLower = (nodeSource || '').toLowerCase(); + + // ONLY check pack-related fields, not title or name! + // REMOVED: any checks for node.title, node.name, etc. + return (nodeSource && nodeSourceLower.includes(normalizedTerm)) || + nodeType.includes(normalizedTerm) || + nodeCnrId.includes(normalizedTerm) || + this.checkNodeFilePath(node, normalizedTerm); + }); + } + + searchByType(searchTerm) { + const term = searchTerm.toLowerCase().trim(); + if (!term) { + return []; + } + + if (!app.graph || !app.graph.nodes) { + return []; + } + + return app.graph.nodes.filter(node => { + const nodeType = node.type || ''; + return nodeType.toLowerCase().includes(term); + }); + } + + handleSearchResults(nodes, searchTerm) { + if (nodes.length === 0) { + this.showResultsList([], searchTerm); + this.AF_Find_Node_by_ID_UpdateResults(`No nodes found for "${searchTerm}"`, true); + return; + } + + this.showResultsList(nodes, searchTerm); + this.AF_Find_Node_by_ID_AddToHistory(searchTerm); + + if (nodes.length === 1) { + this.selectFromList(nodes[0].id); + this.AF_Find_Node_by_ID_UpdateResults(`Found: ${this.getNodeDescription(nodes[0])}`); + } else { + this.AF_Find_Node_by_ID_UpdateResults(`Found ${nodes.length} nodes. Click any to select.`); + } + } + + showResultsList(nodes, searchTerm) { + let resultsHTML = ''; + + if (nodes.length === 0) { + resultsHTML = `
No nodes found for "${searchTerm}"
`; + } else { + resultsHTML = `
Found ${nodes.length} nodes:
`; + + nodes.forEach((node, index) => { + const isHighlighted = this.highlightedNode && this.highlightedNode.id === node.id; + const nodeSource = this.getNodeBadgeInfo(node); + + resultsHTML += ` +
+
[${node.id}] ${node.type || 'Unknown'}
+
${node.title || 'Untitled'}
+ ${nodeSource ? `
Source: ${nodeSource}
` : ''} +
+ `; + }); + } + + const results = document.getElementById('af-find-node-by-id-results'); + results.innerHTML = resultsHTML; + } + + selectFromList(nodeId) { + const node = this.AF_Find_Node_by_ID_FindNodeById(nodeId); + if (node) { + this.AF_Find_Node_by_ID_HighlightNode(node); + this.AF_Find_Node_by_ID_CenterOnNode(node); + this.AF_Find_Node_by_ID_UpdateResults(`Selected: ${this.getNodeDescription(node)}`); + + // Keep the list open, just update the selection highlight + const currentSearch = this.tabInputs.main.value.trim(); + const currentNodes = this.getCurrentSearchResults(currentSearch); + this.showResultsList(currentNodes, currentSearch); + } + } + + getCurrentSearchResults(searchTerm) { + switch (this.currentTab) { + case 'id': + return this.searchById(searchTerm); + case 'title': + return this.searchByTitle(searchTerm); + case 'pack': + return this.searchByPack(searchTerm); + case 'type': + return this.searchByType(searchTerm); + default: + return []; + } + } + + getNodeDescription(node) { + return `[${node.id}] ${node.type || 'Unknown Type'}: ${node.title || 'Untitled'}`; + } AF_Find_Node_by_ID_FindNodeById(nodeId) { if (!app.graph || !app.graph.nodes) return null; @@ -330,17 +804,18 @@ class AF_Find_Node_by_ID_Widget { } } - AF_Find_Node_by_ID_ClearAll() { - // Clear the search field - const input = document.getElementById('af-find-node-by-id-input'); - if (input) { - input.value = ''; - } - // Clear highlight - this.AF_Find_Node_by_ID_ClearHighlight(); - // Update results - this.AF_Find_Node_by_ID_UpdateResults('Search field cleared.'); - } + AF_Find_Node_by_ID_ClearAll() { + // Clear the search field + const input = document.getElementById('af-find-node-by-id-input'); + if (input) { + input.value = ''; + } + // Clear highlight + this.AF_Find_Node_by_ID_ClearHighlight(); + // Clear results display but keep the container visible + this.showResultsList([], ''); + this.AF_Find_Node_by_ID_UpdateResults('Search field cleared.'); + } AF_Find_Node_by_ID_CenterOnNode(node) { if (!app.canvas || !node) return; @@ -381,25 +856,29 @@ class AF_Find_Node_by_ID_Widget { AF_Find_Node_by_ID_ToggleInspector() { this.AF_Find_Node_by_ID_SetInspectorMode(!this.inspectorMode); // Fix width when toggling inspector - if (this.searchPanel) { + /* if (this.searchPanel) { this.searchPanel.style.width = '340px'; - } + } */ } - AF_Find_Node_by_ID_SetInspectorMode(enabled) { - this.inspectorMode = enabled; - const btn = document.getElementById('af-find-node-by-id-inspector-toggle'); - if (btn) { - btn.style.background = enabled ? '#ff9800' : '#2196F3'; - btn.textContent = enabled ? 'Exit Inspector' : 'Inspector'; - } - - if (enabled) { - this.AF_Find_Node_by_ID_UpdateResults('Inspector mode active. Click any node to see its ID.'); - } else { - this.AF_Find_Node_by_ID_UpdateResults('Inspector mode disabled.'); - } - } + AF_Find_Node_by_ID_SetInspectorMode(enabled) { + if (enabled && this.currentTab !== 'id') { + this.switchTab('id'); + } + + this.inspectorMode = enabled; + const btn = document.getElementById('af-find-node-by-id-inspector-toggle'); + if (btn) { + btn.style.background = enabled ? '#ff9800' : '#2196F3'; + btn.textContent = enabled ? 'Exit Inspector' : 'Inspector'; + } + + if (enabled) { + this.AF_Find_Node_by_ID_UpdateResults('Inspector mode active. Click any node to see its ID.'); + } else { + this.AF_Find_Node_by_ID_UpdateResults('Inspector mode disabled.'); + } + } AF_Find_Node_by_ID_HandleNodeClick(node) { if (this.inspectorMode && node) { @@ -413,51 +892,69 @@ class AF_Find_Node_by_ID_Widget { } } - AF_Find_Node_by_ID_AddToHistory(nodeId) { - // Remove if already exists - this.searchHistory = this.searchHistory.filter(id => id !== nodeId); - // Add to front - this.searchHistory.unshift(nodeId); - // Limit size - if (this.searchHistory.length > this.maxHistory) { - this.searchHistory = this.searchHistory.slice(0, this.maxHistory); - } - this.AF_Find_Node_by_ID_UpdateHistory(); - } + AF_Find_Node_by_ID_AddToHistory(searchTerm) { + const history = this.tabHistory[this.currentTab]; + + // Remove if already exists + this.tabHistory[this.currentTab] = history.filter(item => item !== searchTerm); + + // Add to front + this.tabHistory[this.currentTab].unshift(searchTerm); + + // Limit size + if (this.tabHistory[this.currentTab].length > this.maxHistory) { + this.tabHistory[this.currentTab] = this.tabHistory[this.currentTab].slice(0, this.maxHistory); + } + + // Save to localStorage + localStorage.setItem(`af-find-node-history-${this.currentTab}`, JSON.stringify(this.tabHistory[this.currentTab])); + + this.AF_Find_Node_by_ID_UpdateHistory(); + } - AF_Find_Node_by_ID_UpdateHistory() { - const historyContainer = document.getElementById('af-find-node-by-id-history'); - if (!historyContainer) return; + AF_Find_Node_by_ID_UpdateHistory() { + const historyContainer = document.getElementById('af-find-node-by-id-history'); + if (!historyContainer) return; - historyContainer.innerHTML = ''; - - this.searchHistory.forEach(nodeId => { - const historyBtn = document.createElement('button'); - historyBtn.textContent = nodeId.toString(); - historyBtn.style.cssText = ` - background: #444; - border: none; - color: #fff; - padding: 2px 6px; - border-radius: 3px; - cursor: pointer; - font-size: 10px; - `; - historyBtn.onclick = () => { - document.getElementById('af-find-node-by-id-input').value = nodeId.toString(); - this.AF_Find_Node_by_ID_Search(); - }; - historyContainer.appendChild(historyBtn); - }); - } - - AF_Find_Node_by_ID_UpdateResults(message, isError = false) { - const results = document.getElementById('af-find-node-by-id-results'); - if (results) { - results.textContent = message; - results.style.color = isError ? '#ff6b6b' : '#fff'; // Red for errors, white otherwise - } - } + historyContainer.innerHTML = ''; + + const currentHistory = this.tabHistory[this.currentTab]; + + currentHistory.forEach(item => { + const historyBtn = document.createElement('button'); + historyBtn.textContent = item; + historyBtn.style.cssText = ` + background: #444; + border: none; + color: #fff; + padding: 2px 6px; + border-radius: 3px; + cursor: pointer; + font-size: 10px; + margin: 1px; + `; + historyBtn.onclick = () => { + this.tabInputs.main.value = item; + this.AF_Find_Node_by_ID_Search(); + }; + historyContainer.appendChild(historyBtn); + }); + + if (currentHistory.length === 0) { + const noHistory = document.createElement('div'); + noHistory.textContent = 'No recent searches'; + noHistory.style.cssText = 'color: #666; font-size: 10px; font-style: italic;'; + historyContainer.appendChild(noHistory); + } + } + + AF_Find_Node_by_ID_UpdateResults(message, isError = false) { + const status = document.getElementById('af-find-node-by-id-status'); + if (status) { + status.textContent = message; + status.style.color = isError ? '#ff6b6b' : '#aaa'; /// Red for errors, white otherwise + } + } // Keyboard shortcut handler AF_Find_Node_by_ID_HandleKeyboard(event) { @@ -511,39 +1008,71 @@ app.registerExtension({ } }; - // Check selection periodically when inspector is active - setInterval(() => { - if (window.AF_Find_Node_by_ID_Widget.inspectorMode) { - checkNodeSelection(); - } else { - lastSelectedNodes = []; - } - }, 100); + // Check selection periodically when inspector is active + let selectionInterval = null; + + // In the setup() function, replace the setInterval with: + const setupSelectionMonitor = () => { + if (window.AF_Find_Node_by_ID_Widget.inspectorMode && !selectionInterval) { + selectionInterval = setInterval(() => { + checkNodeSelection(); + }, 300); // Reduced from 100ms to 300ms + } else if (!window.AF_Find_Node_by_ID_Widget.inspectorMode && selectionInterval) { + clearInterval(selectionInterval); + selectionInterval = null; + lastSelectedNodes = []; + } + }; + + // Monitor inspector mode changes + const originalSetInspectorMode = window.AF_Find_Node_by_ID_Widget.AF_Find_Node_by_ID_SetInspectorMode; + window.AF_Find_Node_by_ID_Widget.AF_Find_Node_by_ID_SetInspectorMode = function(enabled) { + originalSetInspectorMode.call(this, enabled); + setupSelectionMonitor(); + }; // Also try hooking into the canvas click event const canvas = app.canvas.canvas; - canvas.addEventListener('click', (e) => { - if (!window.AF_Find_Node_by_ID_Widget.inspectorMode) return; - - // Get canvas coordinates - const rect = canvas.getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; - - // Convert to canvas coordinates - const canvasX = (x - app.canvas.ds.offset[0]) / app.canvas.ds.scale; - const canvasY = (y - app.canvas.ds.offset[1]) / app.canvas.ds.scale; - - // Find node at position - const node = app.graph.getNodeOnPos(canvasX, canvasY); - - if (node) { - window.AF_Find_Node_by_ID_Widget.AF_Find_Node_by_ID_HandleNodeClick(node); - e.stopPropagation(); - } - }, true); + + let lastInspectorClick = 0; + canvas.addEventListener('click', (e) => { + if (!window.AF_Find_Node_by_ID_Widget.inspectorMode) return; + + // Debounce clicks to prevent multiple rapid triggers + const now = Date.now(); + if (now - lastInspectorClick < 200) return; // 200ms debounce + lastInspectorClick = now; + + // Only proceed if we're actually in inspector mode + if (!window.AF_Find_Node_by_ID_Widget.inspectorMode) return; + + // Get canvas coordinates + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + // Convert to canvas coordinates + const canvasX = (x - app.canvas.ds.offset[0]) / app.canvas.ds.scale; + const canvasY = (y - app.canvas.ds.offset[1]) / app.canvas.ds.scale; + + // Find node at position + const node = app.graph.getNodeOnPos(canvasX, canvasY); + + if (node) { + window.AF_Find_Node_by_ID_Widget.AF_Find_Node_by_ID_HandleNodeClick(node); + e.stopPropagation(); + } + }, true); console.log("AF - Find Node by ID extension loaded. Use Ctrl+Shift+F to open search panel."); + }, + + async destroyed() { + // Clean up interval when extension is destroyed + if (selectionInterval) { + clearInterval(selectionInterval); + selectionInterval = null; + } } }); diff --git a/README.md b/README.md index a013f1d..10a837e 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,69 @@ # ComfyUI-AF-FindNodeByID -A ComfyUI extension that allows you to find and locate nodes by their ID in complex workflows. +A ComfyUI extension that allows you to find and locate nodes by their ID, title, pack, or type in complex workflows. -Ever had this in the console ??? +Ever had this in the console? image -Here's the solution !! +Here's the solution! -ComfyUI is reporting node IDs when it encounters an error, but there is no built in tool to find those node IDs quickly. - -That's now solved with this custom node/tool. - -Here's a quick video showing the functionality. - -[VIDEO] +ComfyUI reports node IDs when it encounters errors, but there's no built-in tool to find those node IDs quickly. That's now solved with this custom node/tool. ## Features -### 🔍 **Find by Node ID** +### 🔍 **Multiple Search Modes** +The extension now features a tabbed interface with four search modes: + +#### 📍 By ID - Enter any node ID to instantly locate and highlight it - Auto-centers the canvas on the found node - Perfect for debugging when error messages reference specific node IDs +#### 📛 By Title +- Search nodes by their title, color, cnr_id, aux_id, or name +- Finds nodes based on your custom naming and organization +- Real-time search as you type (searches after 2+ characters) + +#### 📦 By Pack (Experimental) +- Search for all nodes from a specific pack/extension +- Examples: "rgthree", "WAS", "efficiency", "controlnet" +- Includes smart alias matching (e.g., "easy" finds "EasyUse") + +#### 🔎 By Type (Experimental) +- Search by node type/class name +- Examples: "KSampler", "CLIPTextEncode", "LoadImage" +- Useful for finding all instances of a specific node type + ### 🎯 **Inspector Mode** +- Available in "By ID" tab only - Click any node to see its ID - Automatically fills the search field with clicked node ID - Great for exploring and mapping your workflow -### 📜 **Search History** -- Keeps track of your recent searches +### 📜 **Tab-Specific Search History** +- Each search mode maintains its own history +- Stores up to 10 recent searches per tab - Click any history item to quickly re-search -- Stores up to 10 recent searches +- History persists between sessions ### ⌨️ **Keyboard Shortcuts** - `Ctrl+Shift+F`: Toggle search panel - `Escape`: Close search panel -- `Enter`: Execute search +- `Enter`: Execute search (in ID mode) ### 🎨 **Visual Highlighting** -- Found nodes are highlighted in red +- Found nodes are highlighted in orange +- Multiple results shown in an interactive list +- Click any result to center and highlight that node - Original colors are preserved and restored - Clear highlighting with one click +### ⚙️ **Experimental Features Toggle** +- Enable/disable experimental search modes (Pack & Type) +- Setting persists between sessions +- Warning indicators on experimental tabs + ## Installation 1. Download or clone this repository to your `ComfyUI/custom_nodes/` directory: @@ -62,28 +83,53 @@ Here's a quick video showing the functionality. ### Opening the Search Panel - Press `Ctrl+Shift+F` anywhere in ComfyUI to open the search panel +- The panel remembers your last active tab -### Finding Nodes -1. Enter a node ID in the search field (e.g., `42`) -2. Click "Find" or press Enter -3. The node will be highlighted and centered on screen +### Finding Nodes by ID +1. Make sure you're on the "🔍 By ID" tab +2. Enter a node ID in the search field (e.g., `42`) +3. Click "Find" or press Enter +4. The node will be highlighted and centered on screen + +### Finding Nodes by Title +1. Switch to the "📛 By Title" tab +2. Start typing any part of a node's title, color, or identifier +3. Results appear automatically as you type +4. Click any result to jump to that node + +### Finding Nodes by Pack (Experimental) +1. Enable experimental features at the bottom of the panel +2. Switch to the "📦 By Pack" tab +3. Enter a pack name (e.g., "rgthree", "WAS", "controlnet") +4. Browse results and click to navigate + +### Finding Nodes by Type (Experimental) +1. Enable experimental features at the bottom of the panel +2. Switch to the "🔎 By Type" tab +3. Enter a node type (e.g., "KSampler", "CLIP") +4. View all matching nodes in your workflow ### Using Inspector Mode -1. Click the "Inspector" button to enter inspector mode -2. Click any node in your workflow to see its ID -3. The ID will automatically appear in the search field -4. Click "Exit Inspector" to return to normal mode +1. Switch to the "🔍 By ID" tab +2. Click the "Inspector" button to enter inspector mode +3. Click any node in your workflow to see its ID +4. The ID will automatically appear in the search field +5. Click "Exit Inspector" to return to normal mode ### Search History - Recent searches appear as clickable buttons below the search field -- Click any history item to quickly search for that node again +- Each tab maintains its own independent history +- Click any history item to quickly search for that node/term again +- History is saved and persists between ComfyUI sessions ## Perfect For - **Debugging workflows** when error messages reference node IDs - **Large workflow navigation** - quickly jump to specific nodes +- **Workflow organization** - finding nodes by your custom titles +- **Pack exploration** - locating all nodes from a specific extension +- **Type analysis** - finding all instances of a node type - **Workflow documentation** - mapping node IDs for reference -- **Subgraph exploration** - understanding complex node relationships ## Error Message Integration @@ -97,14 +143,27 @@ Simply: 2. Type `147` and press Enter 3. Instantly locate the problematic node! +## Known Limitations + +### ⚠️ Experimental Features Notice +The "By Pack" and "By Type" search modes are marked as experimental because: +- Different node packs use varying conventions for storing metadata +- Pack information may not always be consistently available +- Some nodes may not be correctly identified by their pack +- Results may include false positives or miss some nodes + +These features work best with well-structured node packs but may produce unexpected results with others. + +### Not Compatible with Subgraphs +This extension does not currently support searching within subgraphs or nested workflows. + ## Compatibility - Works with all ComfyUI workflows - Compatible with custom nodes - No dependencies on other extensions - Pure JavaScript implementation - -## Not compatible with subgraphs !! +- Tested with ComfyUI v0.0.04+ ## File Structure @@ -115,14 +174,52 @@ ComfyUI-AF-Find-Node-by-ID/ └── README.md # This file ``` +## Changelog + +### v0.0.04 +- Added tabbed interface with four search modes +- New "By Title" search for finding nodes by name +- New "By Pack" experimental search (with aliases support) +- New "By Type" experimental search +- Tab-specific search history +- Experimental features toggle with persistent settings +- Results now shown in interactive list format +- Auto-search for text-based tabs +- Improved visual feedback and status messages + +### v0.0.03 +- Clear button now also clears the search field +- Dialog content cleared when closed or opened +- Error messages shown in red +- Fixed dialog width to 340px + +### v0.0.02 +- Fixed double initialization error +- Removed right-click on canvas to trigger the dialog +- Fixed zooming in to node + +### v0.0.01 +- Initial release + +## Contributing + +Issues and pull requests welcome at: https://github.com/alFrame/ComfyUI-AF-Find-Node-by-ID + +Please report: +- Bugs or unexpected behavior +- Node packs that don't work well with experimental features +- Feature suggestions +- Compatibility issues + ## License MIT License - Feel free to use, modify, and distribute! -## Contributing +## Credits -Issues and pull requests welcome at: [https://github.com/alFrame/ComfyUI-AF-Find-Node-by-ID](https://github.com/alFrame/ComfyUI-AF-FindNodeByID) +**Creator:** Alex Furer +**Co-Creators:** Claude AI, QWEN3 Coder, DeepSeek --- -**Creator:** Alex Furer with Claude AI +*Praise, comments, bugs, improvements:* https://github.com/alFrame/ComfyUI_AF_FindNodeByID \ No newline at end of file diff --git a/__init__.py b/__init__.py index 7f52b8b..64afb65 100644 --- a/__init__.py +++ b/__init__.py @@ -20,6 +20,8 @@ NODE_CLASS_MAPPINGS = {} NODE_DISPLAY_NAME_MAPPINGS = {} __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] +__version__ = "0.0.04" +__author__ = "Alex Furer" print("* * * * * * * * * * * * * * * * * * * * * * * * * *") print(r"""