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 = `