mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-24 22:52:12 -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 logging
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
import asyncio
|
||||||
from server import PromptServer # type: ignore
|
from server import PromptServer # type: ignore
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from ..services.settings_manager import settings
|
from ..services.settings_manager import settings
|
||||||
from ..utils.usage_stats import UsageStats
|
from ..utils.usage_stats import UsageStats
|
||||||
from ..utils.lora_metadata import extract_trained_words
|
from ..utils.lora_metadata import extract_trained_words
|
||||||
from ..config import config
|
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
|
import re
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Download status tracking
|
standalone_mode = 'nodes' not in sys.modules
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
# Node registry for tracking active workflow nodes
|
# Node registry for tracking active workflow nodes
|
||||||
class NodeRegistry:
|
class NodeRegistry:
|
||||||
@@ -34,33 +22,45 @@ class NodeRegistry:
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._lock = threading.RLock()
|
self._lock = threading.RLock()
|
||||||
self._current_graph_id = None
|
|
||||||
self._nodes = {} # node_id -> node_info
|
self._nodes = {} # node_id -> node_info
|
||||||
|
self._registry_updated = threading.Event()
|
||||||
|
|
||||||
def register_node(self, node_id, bgcolor, title, graph_id):
|
def register_nodes(self, nodes):
|
||||||
"""Register a node for the current workflow"""
|
"""Register multiple nodes at once, replacing existing registry"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
# If graph_id changed, clear existing registry for new workflow
|
# Clear existing registry
|
||||||
if self._current_graph_id != graph_id:
|
self._nodes.clear()
|
||||||
self._current_graph_id = graph_id
|
|
||||||
self._nodes.clear()
|
|
||||||
logger.info(f"Workflow changed to {graph_id}, cleared node registry")
|
|
||||||
|
|
||||||
# Register the node
|
# Register all new nodes
|
||||||
self._nodes[node_id] = {
|
for node in nodes:
|
||||||
'id': node_id,
|
node_id = node['node_id']
|
||||||
'bgcolor': bgcolor,
|
node_type = node.get('type', '')
|
||||||
'title': title,
|
|
||||||
'graph_id': graph_id
|
# 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):
|
def get_registry(self):
|
||||||
"""Get current registry information"""
|
"""Get current registry information"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
return {
|
return {
|
||||||
'current_graph_id': self._current_graph_id,
|
|
||||||
'nodes': dict(self._nodes), # Return a copy
|
'nodes': dict(self._nodes), # Return a copy
|
||||||
'node_count': len(self._nodes)
|
'node_count': len(self._nodes)
|
||||||
}
|
}
|
||||||
@@ -68,9 +68,13 @@ class NodeRegistry:
|
|||||||
def clear_registry(self):
|
def clear_registry(self):
|
||||||
"""Clear the entire registry"""
|
"""Clear the entire registry"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self._current_graph_id = None
|
|
||||||
self._nodes.clear()
|
self._nodes.clear()
|
||||||
logger.info("Node registry cleared")
|
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
|
# Global registry instance
|
||||||
node_registry = NodeRegistry()
|
node_registry = NodeRegistry()
|
||||||
@@ -100,7 +104,7 @@ class MiscRoutes:
|
|||||||
app.router.add_get('/api/model-example-files', MiscRoutes.get_model_example_files)
|
app.router.add_get('/api/model-example-files', MiscRoutes.get_model_example_files)
|
||||||
|
|
||||||
# Node registry endpoints
|
# 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)
|
app.router.add_get('/api/get-registry', MiscRoutes.get_registry)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -135,10 +139,6 @@ class MiscRoutes:
|
|||||||
'error': f"Failed to delete {filename}: {str(e)}"
|
'error': f"Failed to delete {filename}: {str(e)}"
|
||||||
}, status=500)
|
}, 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({
|
return web.json_response({
|
||||||
'success': True,
|
'success': True,
|
||||||
'message': f"Successfully cleared {len(deleted_files)} cache files",
|
'message': f"Successfully cleared {len(deleted_files)} cache files",
|
||||||
@@ -457,70 +457,68 @@ class MiscRoutes:
|
|||||||
}, status=500)
|
}, status=500)
|
||||||
|
|
||||||
@staticmethod
|
@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:
|
Expects a JSON body with:
|
||||||
{
|
{
|
||||||
"node_id": 123,
|
"nodes": [
|
||||||
"bgcolor": "#535",
|
{
|
||||||
"title": "Lora Loader (LoraManager)",
|
"node_id": 123,
|
||||||
"graph_id": "151410b3-7845-4561-aac4-8968574e9ba2"
|
"bgcolor": "#535",
|
||||||
|
"title": "Lora Loader (LoraManager)"
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
|
|
||||||
# Validate required fields
|
# Validate required fields
|
||||||
node_id = data.get('node_id')
|
nodes = data.get('nodes', [])
|
||||||
bgcolor = data.get('bgcolor')
|
|
||||||
title = data.get('title')
|
|
||||||
graph_id = data.get('graph_id')
|
|
||||||
|
|
||||||
if node_id is None:
|
if not isinstance(nodes, list):
|
||||||
return web.json_response({
|
return web.json_response({
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': 'Missing node_id parameter'
|
'error': 'nodes must be a list'
|
||||||
}, status=400)
|
}, status=400)
|
||||||
|
|
||||||
if not bgcolor:
|
# Validate each node
|
||||||
return web.json_response({
|
for i, node in enumerate(nodes):
|
||||||
'success': False,
|
if not isinstance(node, dict):
|
||||||
'error': 'Missing bgcolor parameter'
|
return web.json_response({
|
||||||
}, status=400)
|
'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:
|
# Register all nodes
|
||||||
return web.json_response({
|
node_registry.register_nodes(nodes)
|
||||||
'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)
|
|
||||||
|
|
||||||
return web.json_response({
|
return web.json_response({
|
||||||
'success': True,
|
'success': True,
|
||||||
'message': f'Node {node_id} registered successfully'
|
'message': f'{len(nodes)} nodes registered successfully'
|
||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
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({
|
return web.json_response({
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': str(e)
|
'error': str(e)
|
||||||
@@ -528,8 +526,46 @@ class MiscRoutes:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_registry(request):
|
async def get_registry(request):
|
||||||
"""Get current node registry information"""
|
"""Get current node registry information by refreshing from frontend"""
|
||||||
try:
|
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()
|
registry_info = node_registry.get_registry()
|
||||||
|
|
||||||
return web.json_response({
|
return web.json_response({
|
||||||
@@ -541,5 +577,6 @@ class MiscRoutes:
|
|||||||
logger.error(f"Failed to get registry: {e}", exc_info=True)
|
logger.error(f"Failed to get registry: {e}", exc_info=True)
|
||||||
return web.json_response({
|
return web.json_response({
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': str(e)
|
'error': 'Internal Error',
|
||||||
|
'message': str(e)
|
||||||
}, status=500)
|
}, 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?
|
"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
|
||||||
PREVIEW_EXTENSIONS = [
|
PREVIEW_EXTENSIONS = [
|
||||||
'.webp',
|
'.webp',
|
||||||
|
|||||||
@@ -116,4 +116,83 @@
|
|||||||
background: var(--lora-accent);
|
background: var(--lora-accent);
|
||||||
color: white;
|
color: white;
|
||||||
border-color: var(--lora-accent);
|
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,
|
X: 8,
|
||||||
XXX: 16,
|
XXX: 16,
|
||||||
BLOCKED: 32
|
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 { state } from '../state/index.js';
|
||||||
import { resetAndReload } from '../api/loraApi.js';
|
import { resetAndReload } from '../api/loraApi.js';
|
||||||
import { getStorageItem, setStorageItem } from './storageHelpers.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
|
* 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() {
|
export function initBackToTop() {
|
||||||
@@ -367,33 +317,53 @@ export function getNSFWLevelName(level) {
|
|||||||
*/
|
*/
|
||||||
export async function sendLoraToWorkflow(loraSyntax, replaceMode = false, syntaxType = 'lora') {
|
export async function sendLoraToWorkflow(loraSyntax, replaceMode = false, syntaxType = 'lora') {
|
||||||
try {
|
try {
|
||||||
let loraNodes = [];
|
// Get registry information from the new endpoint
|
||||||
let isDesktopMode = false;
|
const registryResponse = await fetch('/api/get-registry');
|
||||||
|
const registryData = await registryResponse.json();
|
||||||
|
|
||||||
// Get the current workflow from localStorage
|
if (!registryData.success) {
|
||||||
const workflowData = localStorage.getItem('workflow');
|
// Handle specific error cases
|
||||||
if (workflowData) {
|
if (registryData.error === 'Standalone Mode Active') {
|
||||||
// Web browser mode - extract node IDs from workflow
|
// Standalone mode - show warning with specific message
|
||||||
const workflow = JSON.parse(workflowData);
|
showToast(registryData.message || 'Cannot interact with ComfyUI in standalone mode', 'warning');
|
||||||
|
return false;
|
||||||
// Find all Lora Loader (LoraManager) nodes
|
} else {
|
||||||
if (workflow.nodes && Array.isArray(workflow.nodes)) {
|
// Other errors - show error toast
|
||||||
for (const node of workflow.nodes) {
|
showToast(registryData.message || registryData.error || 'Failed to get workflow information', 'error');
|
||||||
if (node.type === "Lora Loader (LoraManager)") {
|
|
||||||
loraNodes.push(node.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loraNodes.length === 0) {
|
|
||||||
showToast('No Lora Loader nodes found in the workflow', 'warning');
|
|
||||||
return false;
|
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
|
// Call the backend API to update the lora code
|
||||||
const response = await fetch('/api/update-lora-code', {
|
const response = await fetch('/api/update-lora-code', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -401,7 +371,7 @@ export async function sendLoraToWorkflow(loraSyntax, replaceMode = false, syntax
|
|||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
node_ids: isDesktopMode ? undefined : loraNodes,
|
node_ids: nodeIds,
|
||||||
lora_code: loraSyntax,
|
lora_code: loraSyntax,
|
||||||
mode: replaceMode ? 'replace' : 'append'
|
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
|
* Opens the example images folder for a specific model
|
||||||
* @param {string} modelHash - The SHA256 hash of the model
|
* @param {string} modelHash - The SHA256 hash of the model
|
||||||
|
|||||||
@@ -56,4 +56,8 @@
|
|||||||
<button class="nsfw-level-btn" data-level="16">XXX</button>
|
<button class="nsfw-level-btn" data-level="16">XXX</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="nodeSelector" class="node-selector">
|
||||||
|
<!-- Dynamic node list will be populated here -->
|
||||||
</div>
|
</div>
|
||||||
@@ -76,7 +76,7 @@ app.registerExtension({
|
|||||||
|
|
||||||
// Standard mode - update a specific node
|
// Standard mode - update a specific node
|
||||||
const node = app.graph.getNodeById(+id);
|
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);
|
console.warn("Node not found or not a LoraLoader:", id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -218,9 +218,9 @@ app.registerExtension({
|
|||||||
|
|
||||||
// Ensure the node is registered after creation
|
// Ensure the node is registered after creation
|
||||||
// Call registration
|
// Call registration
|
||||||
setTimeout(() => {
|
// setTimeout(() => {
|
||||||
this.registerNode();
|
// this.registerNode();
|
||||||
}, 0);
|
// }, 0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -147,9 +147,9 @@ app.registerExtension({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Call registration
|
// Call registration
|
||||||
setTimeout(() => {
|
// setTimeout(() => {
|
||||||
this.registerNode();
|
// this.registerNode();
|
||||||
}, 0);
|
// }, 0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -601,7 +601,7 @@ export function addLorasWidget(node, name, opts, callback) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Calculate height based on number of loras and fixed sizes
|
// 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);
|
updateWidgetHeight(container, calculatedHeight, defaultHeight, node);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ app.registerExtension({
|
|||||||
this.updateUsageStats(detail.prompt_id);
|
this.updateUsageStats(detail.prompt_id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Listen for registry refresh requests
|
||||||
|
api.addEventListener("lora_registry_refresh", () => {
|
||||||
|
this.refreshRegistry();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateUsageStats(promptId) {
|
async updateUsageStats(promptId) {
|
||||||
@@ -32,5 +37,46 @@ app.registerExtension({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating usage statistics:", 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