mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
feat: enhance node registration and management with support for multiple nodes and improved UI elements. Fixes #220
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -101,4 +101,25 @@ export const NSFW_LEVELS = {
|
||||
X: 8,
|
||||
XXX: 16,
|
||||
BLOCKED: 32
|
||||
};
|
||||
};
|
||||
|
||||
// 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";
|
||||
@@ -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 `
|
||||
<div class="node-item" data-node-id="${node.id}">
|
||||
<div class="node-icon-indicator" style="background-color: ${bgColor}">
|
||||
<i class="${iconClass}"></i>
|
||||
</div>
|
||||
<span>#${node.id} ${node.title}</span>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
selector.innerHTML = `
|
||||
${nodeItems}
|
||||
<div class="node-item send-all-item" data-action="send-all">
|
||||
<div class="node-icon-indicator all-nodes">
|
||||
<i class="fas fa-broadcast-tower"></i>
|
||||
</div>
|
||||
<span>Send to All</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 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
|
||||
|
||||
@@ -56,4 +56,8 @@
|
||||
<button class="nsfw-level-btn" data-level="16">XXX</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="nodeSelector" class="node-selector">
|
||||
<!-- Dynamic node list will be populated here -->
|
||||
</div>
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -147,9 +147,9 @@ app.registerExtension({
|
||||
};
|
||||
|
||||
// Call registration
|
||||
setTimeout(() => {
|
||||
this.registerNode();
|
||||
}, 0);
|
||||
// setTimeout(() => {
|
||||
// this.registerNode();
|
||||
// }, 0);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user