feat: enhance node registration and management with support for multiple nodes and improved UI elements. Fixes #220

This commit is contained in:
Will Miao
2025-06-26 23:00:55 +08:00
parent eb57e04e95
commit 64dd2ed141
10 changed files with 514 additions and 166 deletions

View File

@@ -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;
}

View File

@@ -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";

View File

@@ -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