From 64dd2ed14160fab403a4e9da1b7ff6b7a08c0c59 Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Thu, 26 Jun 2025 23:00:55 +0800 Subject: [PATCH] feat: enhance node registration and management with support for multiple nodes and improved UI elements. Fixes #220 --- py/routes/misc_routes.py | 203 ++++++++++------- py/utils/constants.py | 9 + static/css/components/menu.css | 79 +++++++ static/js/utils/constants.js | 23 +- static/js/utils/uiHelpers.js | 300 +++++++++++++++++++------ templates/components/context_menu.html | 4 + web/comfyui/lora_loader.js | 8 +- web/comfyui/lora_stacker.js | 6 +- web/comfyui/loras_widget.js | 2 +- web/comfyui/usage_stats.js | 46 ++++ 10 files changed, 514 insertions(+), 166 deletions(-) diff --git a/py/routes/misc_routes.py b/py/routes/misc_routes.py index 4da2e188..1428d5b2 100644 --- a/py/routes/misc_routes.py +++ b/py/routes/misc_routes.py @@ -1,32 +1,20 @@ import logging import os +import sys import threading +import asyncio from server import PromptServer # type: ignore from aiohttp import web from ..services.settings_manager import settings from ..utils.usage_stats import UsageStats from ..utils.lora_metadata import extract_trained_words from ..config import config -from ..utils.constants import SUPPORTED_MEDIA_EXTENSIONS +from ..utils.constants import SUPPORTED_MEDIA_EXTENSIONS, NODE_TYPES, DEFAULT_NODE_COLOR import re logger = logging.getLogger(__name__) -# Download status tracking -download_task = None -is_downloading = False -download_progress = { - 'total': 0, - 'completed': 0, - 'current_model': '', - 'status': 'idle', # idle, running, paused, completed, error - 'errors': [], - 'last_error': None, - 'start_time': None, - 'end_time': None, - 'processed_models': set(), # Track models that have been processed - 'refreshed_models': set() # Track models that had metadata refreshed -} +standalone_mode = 'nodes' not in sys.modules # Node registry for tracking active workflow nodes class NodeRegistry: @@ -34,33 +22,45 @@ class NodeRegistry: def __init__(self): self._lock = threading.RLock() - self._current_graph_id = None self._nodes = {} # node_id -> node_info + self._registry_updated = threading.Event() - def register_node(self, node_id, bgcolor, title, graph_id): - """Register a node for the current workflow""" + def register_nodes(self, nodes): + """Register multiple nodes at once, replacing existing registry""" with self._lock: - # If graph_id changed, clear existing registry for new workflow - if self._current_graph_id != graph_id: - self._current_graph_id = graph_id - self._nodes.clear() - logger.info(f"Workflow changed to {graph_id}, cleared node registry") + # Clear existing registry + self._nodes.clear() - # Register the node - self._nodes[node_id] = { - 'id': node_id, - 'bgcolor': bgcolor, - 'title': title, - 'graph_id': graph_id - } + # Register all new nodes + for node in nodes: + node_id = node['node_id'] + node_type = node.get('type', '') + + # Convert node type name to integer + type_id = NODE_TYPES.get(node_type, 0) # 0 for unknown types + + # Handle null bgcolor with default color + bgcolor = node.get('bgcolor') + if bgcolor is None: + bgcolor = DEFAULT_NODE_COLOR + + self._nodes[node_id] = { + 'id': node_id, + 'bgcolor': bgcolor, + 'title': node.get('title'), + 'type': type_id, + 'type_name': node_type + } - logger.info(f"Registered node {node_id} ({title}) for workflow {graph_id} with bgcolor {bgcolor}") + logger.debug(f"Registered {len(nodes)} nodes in registry") + + # Signal that registry has been updated + self._registry_updated.set() def get_registry(self): """Get current registry information""" with self._lock: return { - 'current_graph_id': self._current_graph_id, 'nodes': dict(self._nodes), # Return a copy 'node_count': len(self._nodes) } @@ -68,9 +68,13 @@ class NodeRegistry: def clear_registry(self): """Clear the entire registry""" with self._lock: - self._current_graph_id = None self._nodes.clear() logger.info("Node registry cleared") + + def wait_for_update(self, timeout=1.0): + """Wait for registry update with timeout""" + self._registry_updated.clear() + return self._registry_updated.wait(timeout) # Global registry instance node_registry = NodeRegistry() @@ -100,7 +104,7 @@ class MiscRoutes: app.router.add_get('/api/model-example-files', MiscRoutes.get_model_example_files) # Node registry endpoints - app.router.add_post('/api/register-node', MiscRoutes.register_node) + app.router.add_post('/api/register-nodes', MiscRoutes.register_nodes) app.router.add_get('/api/get-registry', MiscRoutes.get_registry) @staticmethod @@ -135,10 +139,6 @@ class MiscRoutes: 'error': f"Failed to delete {filename}: {str(e)}" }, status=500) - # If we want to completely remove the cache folder too (optional, - # but we'll keep the folder structure in place here) - # shutil.rmtree(cache_folder) - return web.json_response({ 'success': True, 'message': f"Successfully cleared {len(deleted_files)} cache files", @@ -457,70 +457,68 @@ class MiscRoutes: }, status=500) @staticmethod - async def register_node(request): + async def register_nodes(request): """ - Register a Lora node for the current workflow + Register multiple Lora nodes at once Expects a JSON body with: { - "node_id": 123, - "bgcolor": "#535", - "title": "Lora Loader (LoraManager)", - "graph_id": "151410b3-7845-4561-aac4-8968574e9ba2" + "nodes": [ + { + "node_id": 123, + "bgcolor": "#535", + "title": "Lora Loader (LoraManager)" + }, + ... + ] } """ try: data = await request.json() # Validate required fields - node_id = data.get('node_id') - bgcolor = data.get('bgcolor') - title = data.get('title') - graph_id = data.get('graph_id') + nodes = data.get('nodes', []) - if node_id is None: + if not isinstance(nodes, list): return web.json_response({ 'success': False, - 'error': 'Missing node_id parameter' + 'error': 'nodes must be a list' }, status=400) - if not bgcolor: - return web.json_response({ - 'success': False, - 'error': 'Missing bgcolor parameter' - }, status=400) + # Validate each node + for i, node in enumerate(nodes): + if not isinstance(node, dict): + return web.json_response({ + 'success': False, + 'error': f'Node {i} must be an object' + }, status=400) + + node_id = node.get('node_id') + if node_id is None: + return web.json_response({ + 'success': False, + 'error': f'Node {i} missing node_id parameter' + }, status=400) + + # Validate node_id is an integer + try: + node['node_id'] = int(node_id) + except (ValueError, TypeError): + return web.json_response({ + 'success': False, + 'error': f'Node {i} node_id must be an integer' + }, status=400) - if not title: - return web.json_response({ - 'success': False, - 'error': 'Missing title parameter' - }, status=400) - - if not graph_id: - return web.json_response({ - 'success': False, - 'error': 'Missing graph_id parameter' - }, status=400) - - # Validate node_id is an integer - try: - node_id = int(node_id) - except (ValueError, TypeError): - return web.json_response({ - 'success': False, - 'error': 'node_id must be an integer' - }, status=400) - - # Register the node - node_registry.register_node(node_id, bgcolor, title, graph_id) + # Register all nodes + node_registry.register_nodes(nodes) return web.json_response({ 'success': True, - 'message': f'Node {node_id} registered successfully' + 'message': f'{len(nodes)} nodes registered successfully' }) except Exception as e: - logger.error(f"Failed to register node: {e}", exc_info=True) + logger.error(f"Failed to register nodes: {e}", exc_info=True) return web.json_response({ 'success': False, 'error': str(e) @@ -528,8 +526,46 @@ class MiscRoutes: @staticmethod async def get_registry(request): - """Get current node registry information""" + """Get current node registry information by refreshing from frontend""" try: + # Check if running in standalone mode + if standalone_mode: + logger.warning("Registry refresh not available in standalone mode") + return web.json_response({ + 'success': False, + 'error': 'Standalone Mode Active', + 'message': 'Cannot interact with ComfyUI in standalone mode.' + }, status=503) + + # Send message to frontend to refresh registry + try: + PromptServer.instance.send_sync("lora_registry_refresh", {}) + logger.debug("Sent registry refresh request to frontend") + except Exception as e: + logger.error(f"Failed to send registry refresh message: {e}") + return web.json_response({ + 'success': False, + 'error': 'Communication Error', + 'message': f'Failed to communicate with ComfyUI frontend: {str(e)}' + }, status=500) + + # Wait for registry update with timeout + def wait_for_registry(): + return node_registry.wait_for_update(timeout=1.0) + + # Run the wait in a thread to avoid blocking the event loop + loop = asyncio.get_event_loop() + registry_updated = await loop.run_in_executor(None, wait_for_registry) + + if not registry_updated: + logger.warning("Registry refresh timeout after 1 second") + return web.json_response({ + 'success': False, + 'error': 'Timeout Error', + 'message': 'Registry refresh timeout - ComfyUI frontend may not be responsive' + }, status=408) + + # Get updated registry registry_info = node_registry.get_registry() return web.json_response({ @@ -541,5 +577,6 @@ class MiscRoutes: logger.error(f"Failed to get registry: {e}", exc_info=True) return web.json_response({ 'success': False, - 'error': str(e) + 'error': 'Internal Error', + 'message': str(e) }, status=500) diff --git a/py/utils/constants.py b/py/utils/constants.py index a9633ff1..f5b2820e 100644 --- a/py/utils/constants.py +++ b/py/utils/constants.py @@ -7,6 +7,15 @@ NSFW_LEVELS = { "Blocked": 32, # Probably not actually visible through the API without being logged in on model owner account? } +# Node type constants +NODE_TYPES = { + "Lora Loader (LoraManager)": 1, + "Lora Stacker (LoraManager)": 2 +} + +# Default ComfyUI node color when bgcolor is null +DEFAULT_NODE_COLOR = "#353535" + # preview extensions PREVIEW_EXTENSIONS = [ '.webp', diff --git a/static/css/components/menu.css b/static/css/components/menu.css index a1f8ae8a..108f50e3 100644 --- a/static/css/components/menu.css +++ b/static/css/components/menu.css @@ -116,4 +116,83 @@ background: var(--lora-accent); color: white; border-color: var(--lora-accent); +} + +/* Node Selector */ +.node-selector { + position: fixed; + background: var(--lora-surface); + border: 1px solid var(--border-color); + border-radius: var(--border-radius-xs); + padding: 4px 0; + min-width: 200px; + max-width: 350px; + max-height: 400px; + overflow-y: auto; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); + z-index: 1000; + display: none; + backdrop-filter: blur(10px); +} + +.node-item { + padding: 10px 15px; + cursor: pointer; + display: flex; + align-items: center; + gap: 10px; + color: var(--text-color); + background: var(--lora-surface); + transition: background-color 0.2s; + border-bottom: 1px solid var(--border-color); +} + +.node-item:last-child { + border-bottom: none; +} + +.node-item:hover { + background-color: var(--lora-accent); + color: var(--lora-text); +} + +.node-icon-indicator { + width: 24px; + height: 24px; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.node-icon-indicator i { + color: white; + font-size: 12px; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); +} + +.node-icon-indicator.all-nodes { + background: linear-gradient(45deg, #4a90e2, #357abd); +} + +/* Remove old node-color-indicator styles */ +.node-color-indicator { + display: none; +} + +.send-all-item { + border-top: 1px solid var(--border-color); + font-weight: 500; + background: var(--card-bg); +} + +.send-all-item:hover { + background-color: var(--lora-accent); + color: var(--lora-text); +} + +.send-all-item i { + width: 16px; + text-align: center; } \ No newline at end of file diff --git a/static/js/utils/constants.js b/static/js/utils/constants.js index 248c4906..2983e16a 100644 --- a/static/js/utils/constants.js +++ b/static/js/utils/constants.js @@ -101,4 +101,25 @@ export const NSFW_LEVELS = { X: 8, XXX: 16, BLOCKED: 32 -}; \ No newline at end of file +}; + +// Node type constants +export const NODE_TYPES = { + LORA_LOADER: 1, + LORA_STACKER: 2 +}; + +// Node type names to IDs mapping +export const NODE_TYPE_NAMES = { + "Lora Loader (LoraManager)": NODE_TYPES.LORA_LOADER, + "Lora Stacker (LoraManager)": NODE_TYPES.LORA_STACKER +}; + +// Node type icons +export const NODE_TYPE_ICONS = { + [NODE_TYPES.LORA_LOADER]: "fas fa-l", + [NODE_TYPES.LORA_STACKER]: "fas fa-s" +}; + +// Default ComfyUI node color when bgcolor is null +export const DEFAULT_NODE_COLOR = "#353535"; \ No newline at end of file diff --git a/static/js/utils/uiHelpers.js b/static/js/utils/uiHelpers.js index b3af63da..a74b0aa5 100644 --- a/static/js/utils/uiHelpers.js +++ b/static/js/utils/uiHelpers.js @@ -1,7 +1,7 @@ import { state } from '../state/index.js'; import { resetAndReload } from '../api/loraApi.js'; import { getStorageItem, setStorageItem } from './storageHelpers.js'; -import { NSFW_LEVELS } from './constants.js'; +import { NODE_TYPES, NODE_TYPE_ICONS, DEFAULT_NODE_COLOR } from './constants.js'; /** * Utility function to copy text to clipboard with fallback for older browsers @@ -263,56 +263,6 @@ export function updatePanelPositions() { } } } - } - -// Update the toggleFolderTags function -export function toggleFolderTags() { - const folderTags = document.querySelector('.folder-tags'); - const toggleBtn = document.querySelector('.toggle-folders-btn i'); - - if (folderTags) { - folderTags.classList.toggle('collapsed'); - - if (folderTags.classList.contains('collapsed')) { - // Change icon to indicate folders are hidden - toggleBtn.className = 'fas fa-folder-plus'; - toggleBtn.parentElement.title = 'Show folder tags'; - setStorageItem('folderTagsCollapsed', 'true'); - } else { - // Change icon to indicate folders are visible - toggleBtn.className = 'fas fa-folder-minus'; - toggleBtn.parentElement.title = 'Hide folder tags'; - setStorageItem('folderTagsCollapsed', 'false'); - } - - // Update panel positions after toggling - // Use a small delay to ensure the DOM has updated - setTimeout(() => { - updatePanelPositions(); - }, 50); - } -} - -// Add this to your existing initialization code -export function initFolderTagsVisibility() { - const isCollapsed = getStorageItem('folderTagsCollapsed'); - if (isCollapsed) { - const folderTags = document.querySelector('.folder-tags'); - const toggleBtn = document.querySelector('.toggle-folders-btn i'); - if (folderTags) { - folderTags.classList.add('collapsed'); - } - if (toggleBtn) { - toggleBtn.className = 'fas fa-folder-plus'; - toggleBtn.parentElement.title = 'Show folder tags'; - } - } else { - const toggleBtn = document.querySelector('.toggle-folders-btn i'); - if (toggleBtn) { - toggleBtn.className = 'fas fa-folder-minus'; - toggleBtn.parentElement.title = 'Hide folder tags'; - } - } } export function initBackToTop() { @@ -367,33 +317,53 @@ export function getNSFWLevelName(level) { */ export async function sendLoraToWorkflow(loraSyntax, replaceMode = false, syntaxType = 'lora') { try { - let loraNodes = []; - let isDesktopMode = false; + // Get registry information from the new endpoint + const registryResponse = await fetch('/api/get-registry'); + const registryData = await registryResponse.json(); - // Get the current workflow from localStorage - const workflowData = localStorage.getItem('workflow'); - if (workflowData) { - // Web browser mode - extract node IDs from workflow - const workflow = JSON.parse(workflowData); - - // Find all Lora Loader (LoraManager) nodes - if (workflow.nodes && Array.isArray(workflow.nodes)) { - for (const node of workflow.nodes) { - if (node.type === "Lora Loader (LoraManager)") { - loraNodes.push(node.id); - } - } - } - - if (loraNodes.length === 0) { - showToast('No Lora Loader nodes found in the workflow', 'warning'); + if (!registryData.success) { + // Handle specific error cases + if (registryData.error === 'Standalone Mode Active') { + // Standalone mode - show warning with specific message + showToast(registryData.message || 'Cannot interact with ComfyUI in standalone mode', 'warning'); + return false; + } else { + // Other errors - show error toast + showToast(registryData.message || registryData.error || 'Failed to get workflow information', 'error'); return false; } - } else { - // ComfyUI Desktop mode - don't specify node IDs and let backend handle it - isDesktopMode = true; } + // Success case - check node count + if (registryData.data.node_count === 0) { + // No nodes found - show warning + showToast('No Lora Loader or Lora Stacker nodes found in workflow', 'warning'); + return false; + } else if (registryData.data.node_count > 1) { + // Multiple nodes - show selector + showNodeSelector(registryData.data.nodes, loraSyntax, replaceMode, syntaxType); + return true; + } else { + // Single node - send directly + const nodeId = Object.keys(registryData.data.nodes)[0]; + return await sendToSpecificNode([nodeId], loraSyntax, replaceMode, syntaxType); + } + } catch (error) { + console.error('Failed to get registry:', error); + showToast('Failed to communicate with ComfyUI', 'error'); + return false; + } +} + +/** + * Send LoRA to specific nodes + * @param {Array|undefined} nodeIds - Array of node IDs or undefined for desktop mode + * @param {string} loraSyntax - The LoRA syntax to send + * @param {boolean} replaceMode - Whether to replace existing LoRAs + * @param {string} syntaxType - The type of syntax ('lora' or 'recipe') + */ +async function sendToSpecificNode(nodeIds, loraSyntax, replaceMode, syntaxType) { + try { // Call the backend API to update the lora code const response = await fetch('/api/update-lora-code', { method: 'POST', @@ -401,7 +371,7 @@ export async function sendLoraToWorkflow(loraSyntax, replaceMode = false, syntax 'Content-Type': 'application/json' }, body: JSON.stringify({ - node_ids: isDesktopMode ? undefined : loraNodes, + node_ids: nodeIds, lora_code: loraSyntax, mode: replaceMode ? 'replace' : 'append' }) @@ -428,6 +398,188 @@ export async function sendLoraToWorkflow(loraSyntax, replaceMode = false, syntax } } +// Global variable to track active node selector state +let nodeSelectorState = { + isActive: false, + clickHandler: null, + selectorClickHandler: null +}; + +/** + * Show node selector popup near mouse position + * @param {Object} nodes - Registry nodes data + * @param {string} loraSyntax - The LoRA syntax to send + * @param {boolean} replaceMode - Whether to replace existing LoRAs + * @param {string} syntaxType - The type of syntax ('lora' or 'recipe') + */ +function showNodeSelector(nodes, loraSyntax, replaceMode, syntaxType) { + const selector = document.getElementById('nodeSelector'); + if (!selector) return; + + // Clean up any existing state + hideNodeSelector(); + + // Generate node list HTML with icons and proper colors + const nodeItems = Object.values(nodes).map(node => { + const iconClass = NODE_TYPE_ICONS[node.type] || 'fas fa-question-circle'; + const bgColor = node.bgcolor || DEFAULT_NODE_COLOR; + + return ` +
+
+ +
+ #${node.id} ${node.title} +
+ `; + }).join(''); + + selector.innerHTML = ` + ${nodeItems} +
+
+ +
+ Send to All +
+ `; + + // Position near mouse + positionNearMouse(selector); + + // Show selector + selector.style.display = 'block'; + nodeSelectorState.isActive = true; + + // Setup event listeners with proper cleanup + setupNodeSelectorEvents(selector, nodes, loraSyntax, replaceMode, syntaxType); +} + +/** + * Setup event listeners for node selector + * @param {HTMLElement} selector - The selector element + * @param {Object} nodes - Registry nodes data + * @param {string} loraSyntax - The LoRA syntax to send + * @param {boolean} replaceMode - Whether to replace existing LoRAs + * @param {string} syntaxType - The type of syntax ('lora' or 'recipe') + */ +function setupNodeSelectorEvents(selector, nodes, loraSyntax, replaceMode, syntaxType) { + // Clean up any existing event listeners + cleanupNodeSelectorEvents(); + + // Handle clicks outside to close + nodeSelectorState.clickHandler = (e) => { + if (!selector.contains(e.target)) { + hideNodeSelector(); + } + }; + + // Handle node selection + nodeSelectorState.selectorClickHandler = async (e) => { + const nodeItem = e.target.closest('.node-item'); + if (!nodeItem) return; + + e.stopPropagation(); + + const action = nodeItem.dataset.action; + const nodeId = nodeItem.dataset.nodeId; + + if (action === 'send-all') { + // Send to all nodes + const allNodeIds = Object.keys(nodes); + await sendToSpecificNode(allNodeIds, loraSyntax, replaceMode, syntaxType); + } else if (nodeId) { + // Send to specific node + await sendToSpecificNode([nodeId], loraSyntax, replaceMode, syntaxType); + } + + hideNodeSelector(); + }; + + // Add event listeners with a small delay to prevent immediate triggering + setTimeout(() => { + if (nodeSelectorState.isActive) { + document.addEventListener('click', nodeSelectorState.clickHandler); + selector.addEventListener('click', nodeSelectorState.selectorClickHandler); + } + }, 100); +} + +/** + * Clean up node selector event listeners + */ +function cleanupNodeSelectorEvents() { + if (nodeSelectorState.clickHandler) { + document.removeEventListener('click', nodeSelectorState.clickHandler); + nodeSelectorState.clickHandler = null; + } + + if (nodeSelectorState.selectorClickHandler) { + const selector = document.getElementById('nodeSelector'); + if (selector) { + selector.removeEventListener('click', nodeSelectorState.selectorClickHandler); + } + nodeSelectorState.selectorClickHandler = null; + } +} + +/** + * Hide node selector + */ +function hideNodeSelector() { + const selector = document.getElementById('nodeSelector'); + if (selector) { + selector.style.display = 'none'; + selector.innerHTML = ''; // Clear content to prevent memory leaks + } + + // Clean up event listeners + cleanupNodeSelectorEvents(); + nodeSelectorState.isActive = false; +} + +/** + * Position element near mouse cursor + * @param {HTMLElement} element - Element to position + */ +function positionNearMouse(element) { + // Get current mouse position from last mouse event or use default + const mouseX = window.lastMouseX || window.innerWidth / 2; + const mouseY = window.lastMouseY || window.innerHeight / 2; + + // Show element temporarily to get dimensions + element.style.visibility = 'hidden'; + element.style.display = 'block'; + + const rect = element.getBoundingClientRect(); + const viewportWidth = document.documentElement.clientWidth; + const viewportHeight = document.documentElement.clientHeight; + + // Calculate position with offset from mouse + let x = mouseX + 10; + let y = mouseY + 10; + + // Ensure element doesn't go offscreen + if (x + rect.width > viewportWidth) { + x = mouseX - rect.width - 10; + } + + if (y + rect.height > viewportHeight) { + y = mouseY - rect.height - 10; + } + + // Apply position + element.style.left = `${x}px`; + element.style.top = `${y}px`; + element.style.visibility = 'visible'; +} + +// Track mouse position for node selector positioning +document.addEventListener('mousemove', (e) => { + window.lastMouseX = e.clientX; + window.lastMouseY = e.clientY; +}); + /** * Opens the example images folder for a specific model * @param {string} modelHash - The SHA256 hash of the model diff --git a/templates/components/context_menu.html b/templates/components/context_menu.html index f7c3e27b..e83f8489 100644 --- a/templates/components/context_menu.html +++ b/templates/components/context_menu.html @@ -56,4 +56,8 @@ + + +
+
\ No newline at end of file diff --git a/web/comfyui/lora_loader.js b/web/comfyui/lora_loader.js index fb5ab52c..cd8fcb95 100644 --- a/web/comfyui/lora_loader.js +++ b/web/comfyui/lora_loader.js @@ -76,7 +76,7 @@ app.registerExtension({ // Standard mode - update a specific node const node = app.graph.getNodeById(+id); - if (!node || node.comfyClass !== "Lora Loader (LoraManager)") { + if (!node || (node.comfyClass !== "Lora Loader (LoraManager)" && node.comfyClass !== "Lora Stacker (LoraManager)")) { console.warn("Node not found or not a LoraLoader:", id); return; } @@ -218,9 +218,9 @@ app.registerExtension({ // Ensure the node is registered after creation // Call registration - setTimeout(() => { - this.registerNode(); - }, 0); + // setTimeout(() => { + // this.registerNode(); + // }, 0); }); } }, diff --git a/web/comfyui/lora_stacker.js b/web/comfyui/lora_stacker.js index f6fb3d95..38c01799 100644 --- a/web/comfyui/lora_stacker.js +++ b/web/comfyui/lora_stacker.js @@ -147,9 +147,9 @@ app.registerExtension({ }; // Call registration - setTimeout(() => { - this.registerNode(); - }, 0); + // setTimeout(() => { + // this.registerNode(); + // }, 0); }); } }, diff --git a/web/comfyui/loras_widget.js b/web/comfyui/loras_widget.js index b2c6c1e4..066e388b 100644 --- a/web/comfyui/loras_widget.js +++ b/web/comfyui/loras_widget.js @@ -601,7 +601,7 @@ export function addLorasWidget(node, name, opts, callback) { }); // Calculate height based on number of loras and fixed sizes - const calculatedHeight = CONTAINER_PADDING + HEADER_HEIGHT + (Math.min(totalVisibleEntries, 8) * LORA_ENTRY_HEIGHT); + const calculatedHeight = CONTAINER_PADDING + HEADER_HEIGHT + (Math.min(totalVisibleEntries, 10) * LORA_ENTRY_HEIGHT); updateWidgetHeight(container, calculatedHeight, defaultHeight, node); }; diff --git a/web/comfyui/usage_stats.js b/web/comfyui/usage_stats.js index b16c2807..6babd34c 100644 --- a/web/comfyui/usage_stats.js +++ b/web/comfyui/usage_stats.js @@ -13,6 +13,11 @@ app.registerExtension({ this.updateUsageStats(detail.prompt_id); } }); + + // Listen for registry refresh requests + api.addEventListener("lora_registry_refresh", () => { + this.refreshRegistry(); + }); }, async updateUsageStats(promptId) { @@ -32,5 +37,46 @@ app.registerExtension({ } catch (error) { console.error("Error updating usage statistics:", error); } + }, + + async refreshRegistry() { + try { + // Get current workflow nodes + const prompt = await app.graphToPrompt(); + const workflow = prompt.workflow; + if (!workflow || !workflow.nodes) { + console.warn("No workflow nodes found for registry refresh"); + return; + } + + // Find all Lora nodes + const loraNodes = []; + for (const node of workflow.nodes.values()) { + if (node.type === "Lora Loader (LoraManager)" || node.type === "Lora Stacker (LoraManager)") { + loraNodes.push({ + node_id: node.id, + bgcolor: node.bgcolor || null, + title: node.title || node.type, + type: node.type + }); + } + } + + const response = await fetch('/api/register-nodes', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ nodes: loraNodes }), + }); + + if (!response.ok) { + console.warn("Failed to register Lora nodes:", response.statusText); + } else { + console.log(`Successfully registered ${loraNodes.length} Lora nodes`); + } + } catch (error) { + console.error("Error refreshing registry:", error); + } } });