Files
alFrame-ComfyUI-AF-Find-Nodes/web/AF_Find_Nodes.js
T
Alex Furer 84c6ad0df9 v0.2.2
- BUG !! Critical workflow loading/tab duplication bug
- The extension was dangerously overriding ComfyUI's core loadGraphData() function, which interfered with workflow management and caused infinite tab creation when loading workflows.
- Removed the unsafe loadGraphData override completely
- Added safe workflow monitoring using setInterval polling instead of function overriding
- Added proper cleanup of all intervals in destroyed() method
2025-12-15 19:20:49 +01:00

1676 lines
58 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ****** ComfyUI-AF-Find Nodes ******
//
// Creator: Alex Furer - Co-Creator(s): Claude AI & QWEN3 Coder & DeepSeek
//
// Description: A ComfyUI utility extension for finding nodes by ID, title, pack, or type in workflows.
//
// Repo: https://github.com/alFrame/ComfyUI-AF-Find-Nodes/
//
// Issues, praise, comment, bugs, improvements: https://github.com/alFrame/ComfyUI-AF-Find-Nodes/issues
//
// LICENSE: MIT License
//
// Usage: https://github.com/alFrame/ComfyUI-AF-Find-Nodes/blob/main/README.md
//
// Feature Requests / Wet Dreams
// -
import { app } from "/scripts/app.js";
import { api } from "/scripts/api.js";
// Prevent double initialization
if (window.AF_Find_Nodes_Widget) {
console.log("AF - Find Nodes already loaded, skipping initialization");
} else {
class AF_Find_Nodes_Widget {
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';
this.workflowPackIndex = {};
this.workflowTypeIndex = {};
this.lastWorkflowSignature = null;
this.scanCompleted = false;
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') || '[]')
};
// Add workflow change monitoring WITHOUT overriding anything
this.workflowMonitorInterval = null;
this.lastNodeCount = 0;
this.selectionInterval = null;
this.lastSelectedNodes = [];
}
setupSelectionMonitor() {
if (this.inspectorMode && !this.selectionInterval) {
this.selectionInterval = setInterval(() => {
this.checkNodeSelection();
}, 300);
} else if (!this.inspectorMode && this.selectionInterval) {
clearInterval(this.selectionInterval);
this.selectionInterval = null;
this.lastSelectedNodes = [];
}
}
checkNodeSelection() {
if (!this.inspectorMode || !app.canvas || !app.canvas.selected_nodes) return;
const currentSelected = app.canvas.selected_nodes;
if (!currentSelected || Object.keys(currentSelected).length === 0) return;
// Get the first selected node
const nodeId = Object.keys(currentSelected)[0];
const node = currentSelected[nodeId];
if (node && !this.lastSelectedNodes.includes(node.id)) {
this.AF_Find_Nodes_HandleNodeClick(node);
this.lastSelectedNodes = [node.id];
}
}
// SAFE: Monitor workflow changes without overriding ComfyUI functions
startWorkflowMonitor() {
if (this.workflowMonitorInterval) return;
this.workflowMonitorInterval = setInterval(() => {
if (!app.graph || !app.graph.nodes) return;
const currentCount = app.graph.nodes.length;
// Check if workflow changed significantly (more than just selection changes)
if (Math.abs(currentCount - this.lastNodeCount) > 0) {
this.lastNodeCount = currentCount;
// Debounce the scan
clearTimeout(this.workflowScanTimeout);
this.workflowScanTimeout = setTimeout(() => {
this.scanWorkflowForPacks();
}, 1000);
}
}, 2000); // Check every 2 seconds - very conservative
}
stopWorkflowMonitor() {
if (this.workflowMonitorInterval) {
clearInterval(this.workflowMonitorInterval);
this.workflowMonitorInterval = null;
}
if (this.workflowScanTimeout) {
clearTimeout(this.workflowScanTimeout);
this.workflowScanTimeout = null;
}
}
createSearchPanel() {
// Create main container
const panel = document.createElement('div');
panel.id = 'af-find-nodes-panel';
panel.style.cssText = `
position: fixed;
top: 80px;
right: 20px;
background: #2a2a2a;
border: 1px solid #555;
border-radius: 8px;
padding: 15px;
z-index: 10000;
font-family: monospace;
font-size: 12px;
color: #fff;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
width: 460px; /* Fixed width */
display: none;
`;
// Title bar
const titleBar = document.createElement('div');
titleBar.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
border-bottom: 1px solid #555;
padding-bottom: 8px;
`;
const title = document.createElement('div');
title.textContent = 'AF - Find Nodes';
title.style.fontWeight = 'bold';
const closeBtn = document.createElement('button');
closeBtn.textContent = '×';
closeBtn.style.cssText = `
background: #ff4444;
border: none;
color: white;
width: 20px;
height: 20px;
border-radius: 3px;
cursor: pointer;
font-size: 14px;
line-height: 1;
`;
closeBtn.onclick = () => this.AF_Find_Nodes_HidePanel();
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)' },
{ id: 'stats', name: '📊 Stats', placeholder: 'Workflow statistics and pack overview' }
];
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_Nodes_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 <span style="color: #ff6b6b;">(may not work consistently)</span>';
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_Nodes_UpdateResults(
this.experimentalEnabled ?
'Experimental features enabled. <span style="color: #ff6b6b;">Use with caution!</span>' :
'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-nodes-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';
searchInput.placeholder = this.tabs[0].placeholder;
searchInput.id = 'af-find-nodes-input';
;
const buttonRow = document.createElement('div');
buttonRow.style.cssText = `
display: flex;
gap: 8px;
margin-bottom: 10px;
`;
const searchBtn = document.createElement('button');
searchBtn.textContent = 'Find';
searchBtn.style.cssText = `
flex: 1;
padding: 6px;
background: #4CAF50;
border: none;
border-radius: 4px;
color: white;
cursor: pointer;
font-size: 11px;
`;
const clearBtn = document.createElement('button');
clearBtn.textContent = 'Clear';
clearBtn.style.cssText = `
flex: 1;
padding: 6px;
background: #666;
border: none;
border-radius: 4px;
color: white;
cursor: pointer;
font-size: 11px;
`;
const inspectorBtn = document.createElement('button');
inspectorBtn.textContent = 'Inspector';
inspectorBtn.id = 'af-find-nodes-inspector-toggle';
inspectorBtn.style.cssText = `
flex: 1;
padding: 6px;
background: #2196F3;
border: none;
border-radius: 4px;
color: white;
cursor: pointer;
font-size: 11px;
display: ${this.currentTab === 'id' ? 'block' : 'none'};
`;
// Results section
const resultsSection = document.createElement('div');
resultsSection.id = 'af-find-nodes-results';
resultsSection.style.cssText = `
max-height: 70vh; /* Use viewport height instead of fixed calculation */
overflow-y: auto;
background: #1a1a1a;
border: 1px solid #555;
border-radius: 4px;
padding: 8px;
margin-bottom: 10px;
`;
const statusSection = document.createElement('div');
statusSection.id = 'af-find-nodes-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 = `
border-top: 1px solid #555;
padding-top: 8px;
`;
const historyTitle = document.createElement('div');
historyTitle.textContent = 'Recent Searches:';
historyTitle.style.cssText = `
font-size: 10px;
color: #aaa;
margin-bottom: 5px;
`;
const historyList = document.createElement('div');
historyList.id = 'af-find-nodes-history';
historyList.style.cssText = `
display: flex;
flex-wrap: wrap;
gap: 4px;
`;
// Create separate sections for star widget and support info
const starWidgetSection = document.createElement('div');
starWidgetSection.id = 'af-find-nodes-star-widget';
starWidgetSection.style.cssText = `
border-top: 1px solid #555;
padding-top: 8px;
margin-top: 8px;
display: ${localStorage.getItem('af-find-nodes-starred') ? 'none' : 'block'};
`;
const supportSection = document.createElement('div');
supportSection.id = 'af-find-nodes-support';
supportSection.style.cssText = `
border-top: 1px solid #555;
padding-top: 8px;
margin-top: 8px;
display: block; // Always visible
`;
// Star widget (can be hidden)
const starWidget = document.createElement('div');
starWidget.style.cssText = `
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: #ccc;
cursor: pointer;
margin-bottom: 8px;
`;
const starIcon = document.createElement('span');
starIcon.textContent = '⭐';
starIcon.style.fontSize = '12px';
const starText = document.createElement('span');
starText.innerHTML = 'Please consider <span style="color: #ffd700;">giving a star</span> if you find this helpful';
starWidget.appendChild(starIcon);
starWidget.appendChild(starText);
starWidget.onclick = () => {
// Open GitHub in new tab
window.open('https://github.com/alFrame/ComfyUI-AF-Find-Nodes', '_blank');
// Hide only the star widget, not the support section
starWidgetSection.style.display = 'none';
localStorage.setItem('af-find-nodes-starred', 'true');
};
// Support info (always visible)
const supportText = document.createElement('div');
supportText.innerHTML = '<span style="font-size: 11px; color: #aaa;">If you encounter any issues, please post them <a href="https://github.com/alFrame/ComfyUI-AF-Find-Nodes/issues" target="_blank" style="color: #4da6ff;">here</a></span>';
// Assemble the sections
starWidgetSection.appendChild(starWidget);
supportSection.appendChild(supportText);
// Event listeners
searchBtn.onclick = () => this.AF_Find_Nodes_Search();
clearBtn.onclick = () => this.AF_Find_Nodes_ClearAll();
inspectorBtn.onclick = () => {
if (this.currentTab === 'id') {
this.AF_Find_Nodes_ToggleInspector();
} else {
this.AF_Find_Nodes_UpdateResults('Inspector mode is only available in "By ID" tab.', true);
}
};
searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
this.AF_Find_Nodes_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_Nodes_Search();
}
}, 300); // Wait 300ms after typing stops
}
});
// Assemble the panel
buttonRow.appendChild(searchBtn);
buttonRow.appendChild(clearBtn);
buttonRow.appendChild(inspectorBtn);
searchSection.appendChild(tabContainer);
searchSection.appendChild(searchInput);
searchSection.appendChild(buttonRow);
historySection.appendChild(historyTitle);
historySection.appendChild(historyList);
panel.appendChild(titleBar);
panel.appendChild(searchSection);
panel.appendChild(statusSection);
panel.appendChild(resultsSection);
panel.appendChild(experimentalSection);
panel.appendChild(historySection);
panel.appendChild(starWidgetSection);
panel.appendChild(supportSection);
document.body.appendChild(panel);
this.searchPanel = panel;
// Update results initially
this.AF_Find_Nodes_UpdateResults('Ready to search. Enter a node ID or use Inspector mode.');
}
switchTab(tabId) {
if ((tabId === 'pack' || tabId === 'type') && !this.experimentalEnabled) {
this.AF_Find_Nodes_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();
// Hide search input and buttons for stats tab
const searchInput = document.getElementById('af-find-nodes-input');
const searchBtn = document.querySelector('#af-find-nodes-panel button[onclick*="AF_Find_Nodes_Search"]');
const clearBtn = document.querySelector('#af-find-nodes-panel button[onclick*="AF_Find_Nodes_ClearAll"]');
if (tabId === 'stats') {
if (searchInput) searchInput.style.display = 'none';
if (searchBtn) searchBtn.style.display = 'none';
if (clearBtn) clearBtn.style.display = 'none';
} else {
if (searchInput) searchInput.style.display = 'block';
if (searchBtn) searchBtn.style.display = 'block';
if (clearBtn) clearBtn.style.display = 'block';
}
// Clear previous results, input, and highlight
this.tabInputs.main.value = '';
this.AF_Find_Nodes_ClearHighlight();
// Handle tab-specific content
if (tabId === 'stats') {
this.showWorkflowStats();
this.AF_Find_Nodes_UpdateResults('Workflow statistics loaded.');
} else {
this.showResultsList([], ''); // Clear the results list
this.AF_Find_Nodes_UpdateResults(`Switched to ${tabConfig.name} search. Ready to search.`);
}
// Update history for current tab (not for stats)
if (tabId !== 'stats') {
this.AF_Find_Nodes_UpdateHistory();
}
}
showWorkflowStats() {
// Ensure we have scanned data
this.scanWorkflowForPacks();
const totalNodes = app.graph?.nodes?.length || 0;
const packCount = Object.keys(this.workflowPackIndex).length;
const typeCount = Object.keys(this.workflowTypeIndex).length;
// Separate core packs from custom packs
const allPacks = Object.entries(this.workflowPackIndex)
.map(([pack, nodeIds]) => ({
pack,
count: nodeIds.length,
percentage: Math.round((nodeIds.length / totalNodes) * 100),
isCore: pack === 'Core' || pack.toLowerCase().includes('core')
}));
const corePacks = allPacks.filter(p => p.isCore);
const customPacks = allPacks.filter(p => !p.isCore);
const sortedCustomPacks = customPacks.sort((a, b) => b.count - a.count);
const sortedAllPacks = [...sortedCustomPacks, ...corePacks];
// Build stats HTML without nested scroll containers
let statsHTML = `
<div style="color: #4CAF50; margin-bottom: 15px; font-size: 12px; font-weight: bold;">
📊 Workflow Statistics
</div>
<div style="background: #2a2a2a; padding: 12px; border-radius: 6px; margin-bottom: 15px; border-left: 4px solid #4CAF50;">
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
<span>Total Nodes:</span>
<span style="color: #ff9800; font-weight: bold;">${totalNodes}</span>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
<span>Unique Packs:</span>
<span style="color: #ff9800; font-weight: bold;">${packCount}</span>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
<span>Custom Packs:</span>
<span style="color: #ff9800; font-weight: bold;">${customPacks.length}</span>
</div>
<div style="display: flex; justify-content: space-between;">
<span>Unique Node Types:</span>
<span style="color: #ff9800; font-weight: bold;">${typeCount}</span>
</div>
</div>
<!-- Quick Export Buttons at the Top -->
<div style="margin-bottom: 15px; padding: 10px; background: #1a2a3a; border-radius: 6px; border: 1px solid #444;">
<div style="color: #aaa; margin-bottom: 8px; font-size: 11px; text-align: center;">
⚡ Quick Export
</div>
<div style="display: flex; gap: 8px; margin-bottom: 6px;">
<button onclick="window.AF_Find_Nodes_Widget.exportPackList()"
style="flex: 1; padding: 8px; background: #7a4a1a; border: none;
border-radius: 4px; color: white; cursor: pointer; font-size: 11px;
display: flex; align-items: center; justify-content: center; gap: 6px;">
📋 Packs
</button>
<button onclick="window.AF_Find_Nodes_Widget.copyNodeTypes(false)"
style="flex: 1; padding: 8px; background: #2a4a7a; border: none;
border-radius: 4px; color: white; cursor: pointer; font-size: 11px;
display: flex; align-items: center; justify-content: center; gap: 6px;">
📝 Filtered Types
</button>
<button onclick="window.AF_Find_Nodes_Widget.copyNodeTypes(true)"
style="flex: 1; padding: 8px; background: #3a2a5a; border: none;
border-radius: 4px; color: white; cursor: pointer; font-size: 11px;
display: flex; align-items: center; justify-content: center; gap: 6px;">
📝 All Types
</button>
</div>
<div style="font-size: 10px; color: #666; text-align: center; margin-top: 4px;">
Copy lists for documentation or cleanup
</div>
</div>
`;
// Show ALL packs - LIMIT height on this section only
if (sortedAllPacks.length > 0) {
statsHTML += `
<div style="color: #aaa; margin-bottom: 10px; font-size: 11px; border-bottom: 1px solid #444; padding-bottom: 5px;">
📦 All Node Packs (${sortedAllPacks.length})
</div>
<!-- Single scroll container for packs list -->
<div style="max-height: 600px; overflow-y: auto; background: #1a1a1a; border-radius: 4px; padding: 6px; margin-bottom: 15px; border: 1px solid #333;">
`;
sortedAllPacks.forEach(({ pack, count, percentage, isCore }, index) => {
let packColor = '#ccc';
let countColor = '#4CAF50';
let borderColor = '#444';
if (isCore) {
packColor = '#4da6ff';
countColor = '#4da6ff';
borderColor = '#2196F3';
} else if (count <= 2) {
packColor = '#8BC34A';
countColor = '#8BC34A';
borderColor = '#8BC34A';
}
statsHTML += `
<div style="display: flex; justify-content: space-between; align-items: center;
padding: 8px 10px; margin: 3px 0; background: #222;
border-radius: 4px; border-left: 3px solid ${borderColor};">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 10px; color: #666; min-width: 20px; text-align: center;">${index + 1}</span>
<span style="font-size: 11px; color: ${packColor};">
${pack} ${isCore ? '🔧' : ''}
</span>
</div>
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 11px; color: ${countColor};">
${count} node${count !== 1 ? 's' : ''}
</span>
<span style="font-size: 10px; color: #666; min-width: 40px; text-align: right;">
${percentage}%
</span>
</div>
</div>
`;
});
statsHTML += `</div>`;
// Cleanup opportunity - NO scroll here
const lowUsagePacks = sortedAllPacks.filter(p => p.count <= 2 && !p.isCore);
if (lowUsagePacks.length > 0) {
statsHTML += `
<div style="background: #223322; border: 1px solid #4CAF50; border-radius: 6px; padding: 10px; margin-bottom: 15px;">
<div style="color: #8BC34A; font-size: 11px; margin-bottom: 8px; display: flex; align-items: center; gap: 6px;">
💡 Cleanup Opportunity
</div>
<div style="font-size: 10px; color: #ccc; margin-bottom: 6px;">
Packs with only 1-2 nodes (${lowUsagePacks.length}):
</div>
<div style="display: flex; flex-wrap: wrap; gap: 4px;">
`;
lowUsagePacks.forEach(({ pack, count }) => {
statsHTML += `
<span style="background: #4CAF5022; color: #8BC34A; padding: 3px 8px; border-radius: 3px; font-size: 10px;">
${pack} (${count})
</span>
`;
});
statsHTML += `
</div>
<div style="font-size: 10px; color: #8BC34A; margin-top: 8px; font-style: italic; line-height: 1.3;">
PRO TIP: Consider removing these packs, or check for nodes with the same functionality
</div>
</div>
`;
}
// Pack distribution chart - NO scroll
if (sortedAllPacks.length > 0) {
statsHTML += `
<div style="color: #aaa; margin-bottom: 8px; font-size: 11px; border-bottom: 1px solid #444; padding-bottom: 5px;">
📈 Pack Distribution
</div>
<div style="background: #1a1a1a; border-radius: 4px; padding: 10px; margin-bottom: 15px;">
`;
const topPacks = sortedAllPacks.slice(0, 5);
const maxCount = topPacks[0]?.count || 1;
topPacks.forEach(({ pack, count }) => {
const barWidth = Math.max((count / maxCount) * 100, 3);
statsHTML += `
<div style="margin-bottom: 6px;">
<div style="display: flex; justify-content: space-between; margin-bottom: 3px;">
<span style="font-size: 10px; color: #ccc;">${pack}</span>
<span style="font-size: 10px; color: #4CAF50;">${count}</span>
</div>
<div style="width: 100%; height: 6px; background: #333; border-radius: 3px; overflow: hidden;">
<div style="width: ${barWidth}%; height: 100%; background: linear-gradient(90deg, #4CAF50, #8BC34A); border-radius: 3px;"></div>
</div>
</div>
`;
});
statsHTML += `
<div style="font-size: 10px; color: #666; text-align: center; margin-top: 8px;">
Top 5 packs by node count
</div>
</div>
`;
}
}
// Node Types Summary - NO scroll
statsHTML += `
<div style="color: #aaa; margin-bottom: 8px; font-size: 11px; border-bottom: 1px solid #444; padding-bottom: 5px;">
🔧 Node Types Overview
</div>
<div style="background: #1a2a3a; padding: 10px; border-radius: 4px; margin-bottom: 15px;">
<div style="display: flex; justify-content: space-between; margin-bottom: 6px;">
<span style="font-size: 10px; color: #ccc;">Total Unique Types:</span>
<span style="font-size: 10px; color: #4da6ff;">${typeCount}</span>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 6px;">
<span style="font-size: 10px; color: #ccc;">UUID Types:</span>
<span style="font-size: 10px; color: ${Object.keys(this.workflowTypeIndex).filter(t => this.isUUID(t)).length > 0 ? '#ff9800' : '#4CAF50'}">
${Object.keys(this.workflowTypeIndex).filter(t => this.isUUID(t)).length}
</span>
</div>
<div style="font-size: 9px; color: #666; margin-top: 8px; text-align: center;">
Use export buttons above to copy detailed lists
</div>
</div>
`;
const results = document.getElementById('af-find-nodes-results');
// NO nested scroll container - just set the HTML directly
results.innerHTML = statsHTML;
}
// Add these export methods to your class:
exportPackList() {
const packList = Object.entries(this.workflowPackIndex)
.map(([pack, nodeIds]) => ({ pack, count: nodeIds.length }))
.sort((a, b) => b.count - a.count)
.map(({ pack, count }) => `• ${pack}: ${count} node${count !== 1 ? 's' : ''}`)
.join('\n');
const exportText = `Node Packs Used in This Workflow:\n\n${packList}\n\nTotal Nodes: ${app.graph?.nodes?.length || 0}\nGenerated by AF-Find-Nodes`;
// Copy to clipboard
navigator.clipboard.writeText(exportText).then(() => {
this.AF_Find_Nodes_UpdateResults('✅ Pack list copied to clipboard! Ready for README.', false);
}).catch(err => {
// Fallback: Show in alert
alert(exportText);
this.AF_Find_Nodes_UpdateResults('📋 Pack list displayed (check alert popup)', false);
});
}
copyNodeTypes(includeUUIDs = false) {
console.log('copyNodeTypes called with includeUUIDs:', includeUUIDs);
console.log('workflowTypeIndex keys:', Object.keys(this.workflowTypeIndex).length);
console.log('Sample types:', Object.keys(this.workflowTypeIndex).slice(0, 5));
let typeEntries = Object.entries(this.workflowTypeIndex)
.map(([type, nodeIds]) => ({ type, count: nodeIds.length }));
// Filter out UUIDs unless requested
if (!includeUUIDs) {
typeEntries = typeEntries.filter(({ type }) => !this.isUUID(type));
}
typeEntries = typeEntries
.sort((a, b) => b.count - a.count)
.map(({ type, count }) => `• ${type}: ${count} node${count !== 1 ? 's' : ''}`);
const filteredCount = typeEntries.length;
const totalCount = Object.keys(this.workflowTypeIndex).length;
const uuidCount = totalCount - filteredCount;
// Start building the export text
let exportText = `Node Types in This Workflow:\n\n${typeEntries.join('\n')}\n\n`;
if (includeUUIDs) {
exportText += `Total Types: ${totalCount}\n`;
} else {
exportText += `Filtered Types: ${filteredCount} (${uuidCount} UUIDs excluded)\n`;
}
exportText += `Generated by AF-Find-Nodes`;
navigator.clipboard.writeText(exportText).then(() => {
const msg = includeUUIDs ?
'✅ All node types copied to clipboard!' :
'✅ Filtered node types copied to clipboard!';
this.AF_Find_Nodes_UpdateResults(msg, false);
}).catch(err => {
alert(exportText);
this.AF_Find_Nodes_UpdateResults('📋 Node types displayed (check alert popup)', false);
});
}
updateInspectorButtonVisibility() {
const inspectorBtn = document.getElementById('af-find-nodes-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_Nodes_SetInspectorMode(false);
}
}
}
}
updateTabAppearance() {
document.querySelectorAll('#af-find-nodes-panel [data-tab]').forEach(btn => {
const tabId = btn.dataset.tab;
const isExperimental = tabId === 'pack' || tabId === 'type';
const isStats = tabId === 'stats';
const isActive = tabId === this.currentTab;
btn.style.background = isActive ?
(isExperimental ? '#7a4a1a' : (isStats ? '#2a4a7a' : '#555')) :
(isExperimental ? '#5a2a0a' : (isStats ? '#1a2a5a' : '#333'));
btn.style.borderBottom = isActive ?
(isExperimental ? '2px solid #ff9800' : (isStats ? '2px solid #4da6ff' : '2px solid #4CAF50')) :
'none';
btn.style.opacity = isExperimental && !this.experimentalEnabled ? '0.6' : '1';
});
}
AF_Find_Nodes_ShowPanel() {
if (!this.searchPanel) {
this.createSearchPanel();
}
this.searchPanel.style.display = 'block';
this.isVisible = true;
// Scan workflow when panel opens (safe, on-demand scanning)
this.scanWorkflowForPacks();
// Show quick stats if we have data
if (this.scanCompleted) {
const totalNodes = app.graph?.nodes?.length || 0;
const packCount = Object.keys(this.workflowPackIndex).length;
const typeCount = Object.keys(this.workflowTypeIndex).length;
let statsMsg = `Loaded workflow: ${totalNodes} nodes`;
if (packCount > 0) {
statsMsg += `, ${packCount} packs`;
}
if (typeCount > 0) {
statsMsg += `, ${typeCount} types`;
}
this.AF_Find_Nodes_UpdateResults(statsMsg);
} else {
this.AF_Find_Nodes_UpdateResults('Ready to search. Scan your workflow by searching packs/types.');
}
// 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-nodes-input').value = '';
document.getElementById('af-find-nodes-input').focus();
// Update tab appearance to reflect experimental setting
this.updateTabAppearance();
// Start monitoring when panel is open
this.startWorkflowMonitor();
}
AF_Find_Nodes_HidePanel() {
if (this.searchPanel) {
this.searchPanel.style.display = 'none';
}
this.isVisible = false;
this.AF_Find_Nodes_ClearAll();
this.AF_Find_Nodes_SetInspectorMode(false);
// Stop monitoring when panel is closed
this.stopWorkflowMonitor();
}
AF_Find_Nodes_TogglePanel() {
if (this.isVisible) {
this.AF_Find_Nodes_HidePanel();
} else {
this.AF_Find_Nodes_ShowPanel();
}
}
AF_Find_Nodes_Search() {
const searchTerm = this.tabInputs.main.value.trim();
if (!searchTerm) {
this.AF_Find_Nodes_UpdateResults('Please enter a search term', 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);
}
searchById(searchTerm) {
const nodeId = parseInt(searchTerm);
if (isNaN(nodeId)) {
this.AF_Find_Nodes_UpdateResults('Invalid node ID. Please enter a number.', true);
return [];
}
const node = this.AF_Find_Nodes_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) {
// Check if this is a core ComfyUI node
const category = nodeDef.category;
if (this.isCoreComfyUICategory(category)) {
return 'Core';
}
return category;
}
if (nodeDef?.name) {
return nodeDef.name;
}
}
// Check node constructor properties
if (node.constructor?.nodeData?.category) {
const category = node.constructor.nodeData.category;
if (this.isCoreComfyUICategory(category)) {
return 'Core';
}
return category;
}
if (node.constructor?.category) {
const category = node.constructor.category;
if (this.isCoreComfyUICategory(category)) {
return 'Core';
}
return 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';
if (nodeType.includes('Comfyroll')) return 'Comfyroll Studio';
if (nodeType.includes('tinyterra')) return 'TinyTerra';
// Check if it's a UUID (custom subgraph)
if (this.isUUID(nodeType)) {
return 'Custom Subgraph';
}
return null;
}
// Add this helper method to check if a category is a core ComfyUI category
isCoreComfyUICategory(category) {
if (!category) return false;
const coreCategories = [
'latent', 'conditioning', 'image', 'sampling', 'loaders', 'utils',
'mask', 'inpaint', 'model_merging', 'advanced/loaders', 'advanced/conditioning',
'latent/advanced', 'image/advanced', 'video', 'essentials'
];
const categoryLower = category.toLowerCase();
// Check exact matches
if (coreCategories.includes(categoryLower)) {
return true;
}
// Check partial matches (for nested categories like "advanced/loaders")
for (const coreCat of coreCategories) {
if (categoryLower.includes(coreCat) || coreCat.includes(categoryLower)) {
return true;
}
}
return false;
}
// Helper to check if a string is a UUID
isUUID(str) {
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
return uuidRegex.test(str);
}
getPackAliases(packName) {
if (!packName) return 'unknown';
const lowerPackName = packName.toLowerCase();
// First, check if it's a core ComfyUI category
if (this.isCoreComfyUICategory(lowerPackName)) {
return 'Core';
}
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'],
'comfyroll': ['comfyroll studio', 'comfyroll', '🧩 comfyroll studio'],
'tinyterra': ['tinyterra', '🌏 tinyterra'],
'crystools': ['crystools', 'crystools 🪛'],
'impactpack': ['impactpack', 'impact pack', 'comfyui-impact-pack'],
'fen': ["fen's simple nodes", 'fens'],
'wlsh': ['wlsh nodes', 'wlsh'],
'itools': ['itools'],
'ollama': ['ollama'],
'bootleg': ['bootleg'],
'kjnodes': ['kjnodes'],
'marigold': ['marigold'],
'sam': ['segment_anything2', 'segment anything2'],
'depthanything': ['depthanythingv2', 'depthanything']
};
// First check for exact matches
for (const [canonicalName, aliases] of Object.entries(aliasMap)) {
if (aliases.includes(lowerPackName)) {
return canonicalName;
}
}
// Then check for partial matches
for (const [canonicalName, aliases] of Object.entries(aliasMap)) {
for (const alias of aliases) {
if (lowerPackName.includes(alias) || alias.includes(lowerPackName)) {
return canonicalName;
}
}
}
// Check if it's a subgraph/custom node
if (this.isUUID(lowerPackName)) {
return 'Custom Subgraph';
}
// Return the original pack name if no aliases found
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 [];
}
// Ensure we have a scanned index
this.scanWorkflowForPacks();
// If we have a pre-scanned index, use it for lightning-fast search
if (Object.keys(this.workflowPackIndex).length > 0) {
const results = [];
const normalizedTerm = this.normalizePackName(term);
// Search through our pack index
Object.entries(this.workflowPackIndex).forEach(([packName, nodeIds]) => {
// Check if pack name matches (case-insensitive, partial match)
const packNameLower = packName.toLowerCase();
if (packNameLower.includes(normalizedTerm) ||
normalizedTerm.includes(packNameLower) ||
packNameLower === normalizedTerm) {
// Get all nodes for this pack
nodeIds.forEach(nodeId => {
const node = this.AF_Find_Nodes_FindNodeById(nodeId);
if (node) {
// Add some metadata for display
node._packSource = packName;
results.push(node);
}
});
}
});
if (results.length > 0) {
// Sort by node ID for consistency
results.sort((a, b) => a.id - b.id);
return results;
}
}
// Fallback to original search if index search finds nothing
console.log('AF-Find-Nodes: Using fallback pack search');
return this.originalSearchByPack(searchTerm);
}
// Keep the original search as fallback - add this method AFTER searchByPack:
originalSearchByPack(searchTerm) {
const term = searchTerm.toLowerCase().trim();
if (!term) {
return [];
}
if (!app.graph || !app.graph.nodes) {
console.warn("AF_Find_Nodes: No graph or nodes available");
return [];
}
const normalizedTerm = this.getPackAliases(term);
return app.graph.nodes.filter(node => {
const nodeSource = this.getNodeBadgeInfo(node);
const nodeType = (node.type || '').toLowerCase();
const nodeCnrId = (node.cnr_id || '').toString().toLowerCase();
const nodeSourceLower = (nodeSource || '').toLowerCase();
return (nodeSource && nodeSourceLower.includes(normalizedTerm)) ||
nodeType.includes(normalizedTerm) ||
nodeCnrId.includes(normalizedTerm) ||
this.checkNodeFilePath(node, normalizedTerm);
});
}
scanWorkflowForPacks() {
if (!app.graph || !app.graph.nodes || app.graph.nodes.length === 0) {
this.workflowPackIndex = {};
this.workflowTypeIndex = {};
this.scanCompleted = false;
return;
}
// Create a simple signature to detect if workflow changed
const nodeIds = app.graph.nodes.map(n => n.id).sort();
const signature = nodeIds.join('_') + '_' + app.graph.nodes.length;
// If signature matches and we already scanned, skip
if (signature === this.lastWorkflowSignature && this.scanCompleted) {
return;
}
console.log('AF-Find-Nodes: Scanning workflow for packs/types...');
// Reset indices
this.workflowPackIndex = {};
this.workflowTypeIndex = {};
let packCount = 0;
let typeCount = 0;
let uuidCount = 0;
// Quick scan through all nodes
app.graph.nodes.forEach(node => {
const nodeId = node.id;
const nodeType = node.type || 'Unknown';
// Index by type (always available)
if (!this.workflowTypeIndex[nodeType]) {
this.workflowTypeIndex[nodeType] = [];
typeCount++;
}
this.workflowTypeIndex[nodeType].push(nodeId);
// Index by pack (our main target)
const packInfo = this.getNodeBadgeInfo(node);
if (packInfo) {
// Use your existing alias system to normalize pack names
const normalizedPack = this.getPackAliases(packInfo);
// Track UUIDs separately
if (this.isUUID(normalizedPack)) {
uuidCount++;
}
if (!this.workflowPackIndex[normalizedPack]) {
this.workflowPackIndex[normalizedPack] = [];
packCount++;
}
this.workflowPackIndex[normalizedPack].push(nodeId);
}
});
this.lastWorkflowSignature = signature;
this.scanCompleted = true;
console.log(`AF-Find-Nodes: Indexed ${packCount} packs and ${typeCount} node types`);
if (uuidCount > 0) {
console.log(`AF-Find-Nodes: Found ${uuidCount} UUID-based node types`);
}
}
// Helper method to get normalized pack name
normalizePackName(packName) {
if (!packName) return 'unknown';
return this.getPackAliases(packName.toLowerCase());
}
handleSearchResults(nodes, searchTerm) {
if (nodes.length === 0) {
this.showResultsList([], searchTerm);
this.AF_Find_Nodes_UpdateResults(`No nodes found for "${searchTerm}"`, true);
return;
}
this.showResultsList(nodes, searchTerm);
this.AF_Find_Nodes_AddToHistory(searchTerm);
if (nodes.length === 1) {
this.selectFromList(nodes[0].id);
this.AF_Find_Nodes_UpdateResults(`Found: ${this.getNodeDescription(nodes[0])}`);
} else {
this.AF_Find_Nodes_UpdateResults(`Found ${nodes.length} nodes. Click any to select.`);
}
}
showResultsList(nodes, searchTerm) {
let resultsHTML = '';
if (nodes.length === 0) {
resultsHTML = `<div style="color: #666; font-style: italic;">No nodes found for "${searchTerm}"</div>`;
} else {
resultsHTML = `<div style="margin-bottom: 8px; color: #aaa; font-size: 11px;">Found ${nodes.length} nodes:</div>`;
nodes.forEach((node, index) => {
const isHighlighted = this.highlightedNode && this.highlightedNode.id === node.id;
const nodeSource = this.getNodeBadgeInfo(node);
resultsHTML += `
<div class="result-item"
style="padding: 6px; margin: 2px 0; background: ${isHighlighted ? '#ff960033' : '#2a2a2a'}; border: 1px solid ${isHighlighted ? '#ff9600' : '#444'}; border-radius: 3px; cursor: pointer;"
onclick="window.AF_Find_Nodes_Widget.selectFromList(${node.id})">
<div style="font-weight: bold;">[${node.id}] ${node.type || 'Unknown'}</div>
<div style="font-size: 10px; color: #ccc;">${node.title || 'Untitled'}</div>
${nodeSource ? `<div style="font-size: 9px; color: ${this.currentTab === 'pack' ? '#ff9800' : '#888'};">Pack: ${nodeSource}</div>` : ''}
</div>
`;
});
}
const results = document.getElementById('af-find-nodes-results');
results.innerHTML = resultsHTML;
}
selectFromList(nodeId) {
const node = this.AF_Find_Nodes_FindNodeById(nodeId);
if (node) {
this.AF_Find_Nodes_HighlightNode(node);
this.AF_Find_Nodes_CenterOnNode(node);
this.AF_Find_Nodes_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_Nodes_FindNodeById(nodeId) {
if (!app.graph || !app.graph.nodes) return null;
return app.graph.nodes.find(node => node.id === nodeId);
}
AF_Find_Nodes_HighlightNode(node) {
this.AF_Find_Nodes_ClearHighlight();
// Store original color
this.originalNodeColors.set(node.id, {
color: node.color,
bgcolor: node.bgcolor
});
// Apply highlight
node.color = '#ff9600';
node.bgcolor = '#ff960033';
this.highlightedNode = node;
app.graph.setDirtyCanvas(true, true);
}
AF_Find_Nodes_ClearHighlight() {
if (this.highlightedNode) {
const original = this.originalNodeColors.get(this.highlightedNode.id);
if (original) {
this.highlightedNode.color = original.color;
this.highlightedNode.bgcolor = original.bgcolor;
}
this.originalNodeColors.delete(this.highlightedNode.id);
this.highlightedNode = null;
app.graph.setDirtyCanvas(true, true);
}
}
AF_Find_Nodes_ClearAll() {
// Clear the search field
const input = document.getElementById('af-find-nodes-input');
if (input) {
input.value = '';
}
// Clear highlight
this.AF_Find_Nodes_ClearHighlight();
// Clear results display but keep the container visible
this.showResultsList([], '');
this.AF_Find_Nodes_UpdateResults('Search field cleared.');
}
AF_Find_Nodes_CenterOnNode(node) {
if (!app.canvas || !node) return;
try {
// Clear current selection
app.canvas.deselectAllNodes();
// Select the target node
node.is_selected = true;
app.canvas.selectNode(node);
// Reset zoom to a reasonable level for viewing
const targetScale = 1.0; // or 1.2 for a bit closer
app.canvas.ds.scale = targetScale;
// Center on the node
const canvas = app.canvas;
const nodeWidth = node.size[0];
const nodeHeight = node.size[1];
const nodeCenterX = node.pos[0] + (nodeWidth / 2);
const nodeCenterY = node.pos[1] + (nodeHeight / 2);
canvas.ds.offset[0] = (canvas.canvas.width / 2) - (nodeCenterX * canvas.ds.scale);
canvas.ds.offset[1] = (canvas.canvas.height / 2) - (nodeCenterY * canvas.ds.scale);
app.graph.setDirtyCanvas(true, true);
} catch (error) {
console.warn("AF_Find_Nodes: Could not center on node, using fallback", error);
// Simple fallback
const canvas = app.canvas;
canvas.ds.offset[0] = -node.pos[0] * canvas.ds.scale;
canvas.ds.offset[1] = -node.pos[1] * canvas.ds.scale;
canvas.setDirty(true, true);
}
}
AF_Find_Nodes_ToggleInspector() {
this.AF_Find_Nodes_SetInspectorMode(!this.inspectorMode);
// Fix width when toggling inspector
/* if (this.searchPanel) {
this.searchPanel.style.width = '340px';
} */
}
AF_Find_Nodes_SetInspectorMode(enabled) {
if (enabled && this.currentTab !== 'id') {
this.switchTab('id');
}
this.inspectorMode = enabled;
const btn = document.getElementById('af-find-nodes-inspector-toggle');
if (btn) {
btn.style.background = enabled ? '#ff9800' : '#2196F3';
btn.textContent = enabled ? 'Exit Inspector' : 'Inspector';
}
// Setup or cleanup selection monitor
this.setupSelectionMonitor();
if (enabled) {
this.AF_Find_Nodes_UpdateResults('Inspector mode active. Click any node to see its ID.');
} else {
this.AF_Find_Nodes_UpdateResults('Inspector mode disabled.');
}
}
AF_Find_Nodes_HandleNodeClick(node) {
if (this.inspectorMode && node) {
const input = document.getElementById('af-find-nodes-input');
if (input) {
input.value = node.id.toString();
}
this.AF_Find_Nodes_HighlightNode(node);
this.AF_Find_Nodes_AddToHistory(node.id);
this.AF_Find_Nodes_UpdateResults(`Selected node ${node.id}: ${node.type || 'Unknown Type'}`);
}
}
AF_Find_Nodes_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_Nodes_UpdateHistory();
}
AF_Find_Nodes_UpdateHistory() {
const historyContainer = document.getElementById('af-find-nodes-history');
if (!historyContainer) return;
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_Nodes_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_Nodes_UpdateResults(message, isError = false) {
const status = document.getElementById('af-find-nodes-status');
if (status) {
status.innerHTML = message;
status.style.color = isError ? '#ff6b6b' : '#aaa'; /// Red for errors, white otherwise
}
}
// Keyboard shortcut handler
AF_Find_Nodes_HandleKeyboard(event) {
// Ctrl+Shift+F to toggle search panel
if (event.ctrlKey && event.shiftKey && event.code === 'KeyF') {
event.preventDefault();
this.AF_Find_Nodes_TogglePanel();
}
// Escape to close panel
if (event.code === 'Escape' && this.isVisible) {
event.preventDefault();
this.AF_Find_Nodes_HidePanel();
}
}
searchByType(searchTerm) {
const term = searchTerm.toLowerCase().trim();
if (!term) {
return [];
}
// Ensure we have a scanned index
this.scanWorkflowForPacks();
// Use pre-scanned index if available
if (Object.keys(this.workflowTypeIndex).length > 0) {
const results = [];
// Search through our type index
Object.entries(this.workflowTypeIndex).forEach(([nodeType, nodeIds]) => {
if (nodeType.toLowerCase().includes(term)) {
nodeIds.forEach(nodeId => {
const node = this.AF_Find_Nodes_FindNodeById(nodeId);
if (node) {
results.push(node);
}
});
}
});
if (results.length > 0) {
results.sort((a, b) => a.id - b.id);
return results;
}
}
// Fallback to original type search
return app.graph.nodes.filter(node => {
const nodeType = node.type || '';
return nodeType.toLowerCase().includes(term);
});
}
}
// Initialize the search widget with guard against double initialization
window.AF_Find_Nodes_Widget = new AF_Find_Nodes_Widget();
// Register the extension - SAFE VERSION with NO overrides
app.registerExtension({
name: "AF-Find-Nodes",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
// We don't need to modify node definitions
},
async setup() {
// Add keyboard event listeners
document.addEventListener('keydown', (e) => {
window.AF_Find_Nodes_Widget.AF_Find_Nodes_HandleKeyboard(e);
});
console.log("AF - Find Nodes extension loaded. Use Ctrl+Shift+F to open search panel.");
// Start workflow monitoring when extension loads (but only scans when needed)
window.AF_Find_Nodes_Widget.startWorkflowMonitor();
},
async destroyed() {
// Clean up all intervals and timeouts
if (window.AF_Find_Nodes_Widget) {
window.AF_Find_Nodes_Widget.stopWorkflowMonitor();
// Clean up selection interval if it exists
if (window.AF_Find_Nodes_Widget.selectionInterval) {
clearInterval(window.AF_Find_Nodes_Widget.selectionInterval);
window.AF_Find_Nodes_Widget.selectionInterval = null;
}
}
}
});
} // End of guard against double initialization