Compare commits

..

13 Commits

Author SHA1 Message Date
Will Miao
3079131337 feat: Update version to 0.8.30 and add release notes for automatic model path correction and UI enhancements 2025-08-24 19:22:42 +08:00
Will Miao
a34ade0120 feat: Enhance preview tooltip loading behavior for smoother display 2025-08-24 19:02:08 +08:00
Will Miao
e9ada70088 feat: Add ClownsharKSampler_Beta to NODE_EXTRACTORS for enhanced sampler support 2025-08-23 08:08:51 +08:00
Will Miao
597cc48248 feat: Refactor selection state handling for LoRA entries to avoid style conflicts 2025-08-22 17:19:37 +08:00
Will Miao
ec3f857ef1 feat: Add expand/collapse button functionality and improve drag event handling 2025-08-22 16:51:55 +08:00
Will Miao
383b4de539 feat: Improve cursor handling during drag operations for better user experience 2025-08-22 15:36:27 +08:00
Will Miao
1bf9326604 feat: Enhance download path template handling to support JSON strings and ensure defaults 2025-08-22 11:13:37 +08:00
Will Miao
d9f5459d46 feat: Add additional checkpoint loaders to PATH_CORRECTION_TARGETS for improved model support 2025-08-22 10:18:20 +08:00
Will Miao
e45a1b1e19 feat: Add new WAN video models to BASE_MODELS for enhanced support 2025-08-22 08:48:07 +08:00
Will Miao
331ad8f644 feat: Update showToast function to support options object and improve notification handling
fix: Adjust modal max-height for better responsiveness
2025-08-22 08:18:43 +08:00
Will Miao
52fa88b04c feat: Add widget configuration for "Checkpoint Loader with Name (Image Saver)" in path correction targets 2025-08-21 15:03:26 +08:00
Will Miao
8895a64d24 feat: Enhance path correction functionality for widget nodes with pattern matching and user notifications 2025-08-21 13:39:35 +08:00
Will Miao
fdec535559 fix: Normalize path separators in relative path handling for improved compatibility across platforms 2025-08-21 11:52:46 +08:00
14 changed files with 408 additions and 71 deletions

View File

@@ -34,6 +34,11 @@ Enhance your Civitai browsing experience with our companion browser extension! S
## Release Notes
### v0.8.30
* **Automatic Model Path Correction** - Added auto-correction for model paths in built-in nodes such as Load Checkpoint, Load Diffusion Model, Load LoRA, and other custom nodes with similar functionality. Workflows containing outdated or incorrect model paths will now be automatically updated to reflect the current location of your models.
* **Node UI Enhancements** - Improved node interface for a smoother and more intuitive user experience.
* **Bug Fixes** - Addressed various bugs to enhance stability and reliability.
### v0.8.29
* **Enhanced Recipe Imports** - Improved recipe importing with new target folder selection, featuring path input autocomplete and interactive folder tree navigation. Added a "Use Default Path" option when downloading missing LoRAs.
* **WanVideo Lora Select Node Update** - Updated the WanVideo Lora Select node with a 'merge_loras' option to match the counterpart node in the WanVideoWrapper node package.

View File

@@ -644,6 +644,7 @@ NODE_EXTRACTORS = {
"KSamplerAdvanced": KSamplerAdvancedExtractor,
"SamplerCustom": KSamplerAdvancedExtractor,
"SamplerCustomAdvanced": SamplerCustomAdvancedExtractor,
"ClownsharKSampler_Beta": SamplerExtractor,
"TSC_KSampler": TSCKSamplerExtractor, # Efficient Nodes
"TSC_KSamplerAdvanced": TSCKSamplerAdvancedExtractor, # Efficient Nodes
"KSamplerBasicPipe": KSamplerBasicPipeExtractor, # comfyui-impact-pack

View File

@@ -398,12 +398,12 @@ class BaseModelService(ABC):
relative_path = None
for root in model_roots:
# Normalize paths for comparison
normalized_root = os.path.normpath(root).replace(os.sep, '/')
normalized_file = os.path.normpath(file_path).replace(os.sep, '/')
normalized_root = os.path.normpath(root)
normalized_file = os.path.normpath(file_path)
if normalized_file.startswith(normalized_root):
# Remove root and leading slash to get relative path
relative_path = normalized_file[len(normalized_root):].lstrip('/')
# Remove root and leading separator to get relative path
relative_path = normalized_file[len(normalized_root):].lstrip(os.sep)
break
if relative_path and search_lower in relative_path.lower():

View File

@@ -167,6 +167,7 @@ class LoraService(BaseModelService):
if file_path:
# Convert to forward slashes and extract relative path
file_path_normalized = file_path.replace('\\', '/')
relative_path = relative_path.replace('\\', '/')
# Find the relative path part by looking for the relative_path in the full path
if file_path_normalized.endswith(relative_path) or relative_path in file_path_normalized:
return lora.get('usage_tips', '')

View File

@@ -110,6 +110,43 @@ class SettingsManager:
Template string for the model type, defaults to '{base_model}/{first_tag}'
"""
templates = self.settings.get('download_path_templates', {})
# Handle edge case where templates might be stored as JSON string
if isinstance(templates, str):
try:
# Try to parse JSON string
parsed_templates = json.loads(templates)
if isinstance(parsed_templates, dict):
# Update settings with parsed dictionary
self.settings['download_path_templates'] = parsed_templates
self._save_settings()
templates = parsed_templates
logger.info("Successfully parsed download_path_templates from JSON string")
else:
raise ValueError("Parsed JSON is not a dictionary")
except (json.JSONDecodeError, ValueError) as e:
# If parsing fails, set default values
logger.warning(f"Failed to parse download_path_templates JSON string: {e}. Setting default values.")
default_template = '{base_model}/{first_tag}'
templates = {
'lora': default_template,
'checkpoint': default_template,
'embedding': default_template
}
self.settings['download_path_templates'] = templates
self._save_settings()
# Ensure templates is a dictionary
if not isinstance(templates, dict):
default_template = '{base_model}/{first_tag}'
templates = {
'lora': default_template,
'checkpoint': default_template,
'embedding': default_template
}
self.settings['download_path_templates'] = templates
self._save_settings()
return templates.get(model_type, '{base_model}/{first_tag}')
settings = SettingsManager()

View File

@@ -1,7 +1,7 @@
[project]
name = "comfyui-lora-manager"
description = "Revolutionize your workflow with the ultimate LoRA companion for ComfyUI!"
version = "0.8.29"
version = "0.8.30"
license = {file = "LICENSE"}
dependencies = [
"aiohttp",

View File

@@ -23,7 +23,7 @@ body.modal-open {
position: relative;
max-width: 800px;
height: auto;
/* max-height: calc(90vh - 48px); */
max-height: calc(90vh);
margin: 1rem auto; /* Keep reduced top margin */
background: var(--lora-surface);
border-radius: var(--border-radius-base);

View File

@@ -45,6 +45,9 @@ export const BASE_MODELS = {
WAN_VIDEO_14B_T2V: "Wan Video 14B t2v",
WAN_VIDEO_14B_I2V_480P: "Wan Video 14B i2v 480p",
WAN_VIDEO_14B_I2V_720P: "Wan Video 14B i2v 720p",
WAN_VIDEO_2_2_TI2V_5B: "Wan Video 2.2 TI2V-5B",
WAN_VIDEO_2_2_T2V_A14B: "Wan Video 2.2 T2V-A14B",
WAN_VIDEO_2_2_I2V_A14B: "Wan Video 2.2 I2V-A14B",
HUNYUAN_VIDEO: "Hunyuan Video",
// Default
UNKNOWN: "Other"

View File

@@ -248,7 +248,7 @@ class AutoComplete {
if (!this.previewTooltip) return;
// Extract filename without extension for preview
const fileName = relativePath.split('/').pop();
const fileName = relativePath.split(/[/\\]/).pop();
const loraName = fileName.replace(/\.(safetensors|ckpt|pt|bin)$/i, '');
// Get item position for tooltip positioning
@@ -256,7 +256,7 @@ class AutoComplete {
const x = rect.right + 10;
const y = rect.top;
this.previewTooltip.show(loraName, x, y);
this.previewTooltip.show(loraName, x, y, true); // Pass true for fromAutocomplete flag
}
hidePreview() {
@@ -380,7 +380,7 @@ class AutoComplete {
async insertSelection(relativePath) {
// Extract just the filename for LoRA name
const fileName = relativePath.split('/').pop().replace(/\.(safetensors|ckpt|pt|bin)$/i, '');
const fileName = relativePath.split(/[/\\]/).pop().replace(/\.(safetensors|ckpt|pt|bin)$/i, '');
// Get usage tips and extract strength
let strength = 1.0; // Default strength

View File

@@ -1,4 +1,4 @@
import { createToggle, createArrowButton, PreviewTooltip, createDragHandle, updateEntrySelection } from "./loras_widget_components.js";
import { createToggle, createArrowButton, PreviewTooltip, createDragHandle, updateEntrySelection, createExpandButton, updateExpandButtonState } from "./loras_widget_components.js";
import {
parseLoraValue,
formatLoraValue,
@@ -215,7 +215,8 @@ export function addLorasWidget(node, name, opts, callback) {
if (e.target.closest('.comfy-lora-toggle') ||
e.target.closest('input') ||
e.target.closest('.comfy-lora-arrow') ||
e.target.closest('.comfy-lora-drag-handle')) {
e.target.closest('.comfy-lora-drag-handle') ||
e.target.closest('.comfy-lora-expand-button')) {
return;
}
@@ -225,41 +226,6 @@ export function addLorasWidget(node, name, opts, callback) {
container.focus(); // Focus container for keyboard events
});
// Add double-click handler to toggle clip entry
loraEl.addEventListener('dblclick', (e) => {
// Skip if clicking on toggle or strength control areas
if (e.target.closest('.comfy-lora-toggle') ||
e.target.closest('input') ||
e.target.closest('.comfy-lora-arrow')) {
return;
}
// Prevent default behavior
e.preventDefault();
e.stopPropagation();
// Toggle the clip entry expanded state
const lorasData = parseLoraValue(widget.value);
const loraIndex = lorasData.findIndex(l => l.name === name);
if (loraIndex >= 0) {
// Explicitly toggle the expansion state
const currentExpanded = shouldShowClipEntry(lorasData[loraIndex]);
lorasData[loraIndex].expanded = !currentExpanded;
// If collapsing, set clipStrength = strength
if (!lorasData[loraIndex].expanded) {
lorasData[loraIndex].clipStrength = lorasData[loraIndex].strength;
}
// Update the widget value
widget.value = formatLoraValue(lorasData);
// Re-render to show/hide clip entry
renderLoras(widget.value, widget);
}
});
// Create drag handle for reordering
const dragHandle = createDragHandle();
@@ -280,32 +246,46 @@ export function addLorasWidget(node, name, opts, callback) {
}
});
// Create expand button
const expandButton = createExpandButton(isExpanded, (shouldExpand) => {
// Toggle the clip entry expanded state
const lorasData = parseLoraValue(widget.value);
const loraIndex = lorasData.findIndex(l => l.name === name);
if (loraIndex >= 0) {
// Set the expansion state
lorasData[loraIndex].expanded = shouldExpand;
// If collapsing, set clipStrength = strength
if (!shouldExpand) {
lorasData[loraIndex].clipStrength = lorasData[loraIndex].strength;
}
// Update the widget value
widget.value = formatLoraValue(lorasData);
// Re-render to show/hide clip entry
renderLoras(widget.value, widget);
}
});
// Create name display
const nameEl = document.createElement("div");
nameEl.textContent = name;
Object.assign(nameEl.style, {
marginLeft: "10px",
marginLeft: "4px",
flex: "1",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
color: active ? "rgba(226, 232, 240, 0.9)" : "rgba(226, 232, 240, 0.6)",
fontSize: "13px",
cursor: "pointer",
userSelect: "none",
WebkitUserSelect: "none",
MozUserSelect: "none",
msUserSelect: "none",
});
// Add expand indicator to name element
const expandIndicator = document.createElement("span");
expandIndicator.textContent = isExpanded ? " ▼" : " ▶";
expandIndicator.style.opacity = "0.7";
expandIndicator.style.fontSize = "9px";
expandIndicator.style.marginLeft = "4px";
nameEl.appendChild(expandIndicator);
// Move preview tooltip events to nameEl instead of loraEl
nameEl.addEventListener('mouseenter', async (e) => {
e.stopPropagation();
@@ -464,6 +444,7 @@ export function addLorasWidget(node, name, opts, callback) {
leftSection.appendChild(dragHandle); // Add drag handle first
leftSection.appendChild(toggle);
leftSection.appendChild(expandButton); // Add expand button
leftSection.appendChild(nameEl);
loraEl.appendChild(leftSection);
@@ -471,9 +452,6 @@ export function addLorasWidget(node, name, opts, callback) {
container.appendChild(loraEl);
// Update selection state
updateEntrySelection(loraEl, name === selectedLora);
// If expanded, show the clip entry
if (isExpanded) {
totalVisibleEntries++;
@@ -657,6 +635,13 @@ 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, 12) * LORA_ENTRY_HEIGHT);
updateWidgetHeight(container, calculatedHeight, defaultHeight, node);
// After all LoRA elements are created, apply selection state as the last step
// This ensures the selection state is not overwritten
container.querySelectorAll('.comfy-lora-entry').forEach(entry => {
const entryLoraName = entry.dataset.loraName;
updateEntrySelection(entry, entryLoraName === selectedLora);
});
};
// Store the value in a variable to avoid recursion

View File

@@ -115,6 +115,10 @@ export function createDragHandle() {
handle.onmousedown = () => {
handle.style.cursor = "grabbing";
};
handle.onmouseup = () => {
handle.style.cursor = "grab";
};
return handle;
}
@@ -143,15 +147,22 @@ export function createDropIndicator() {
// Function to update entry selection state
export function updateEntrySelection(entryEl, isSelected) {
const baseColor = entryEl.dataset.active === 'true' ?
// Remove any conflicting styles first
entryEl.style.removeProperty('border');
entryEl.style.removeProperty('box-shadow');
const baseColor = entryEl.dataset.active === 'true' ?
"rgba(45, 55, 72, 0.7)" : "rgba(35, 40, 50, 0.5)";
const selectedColor = entryEl.dataset.active === 'true' ?
const selectedColor = entryEl.dataset.active === 'true' ?
"rgba(66, 153, 225, 0.3)" : "rgba(66, 153, 225, 0.2)";
// Update data attribute to track selection state
entryEl.dataset.selected = isSelected ? 'true' : 'false';
if (isSelected) {
entryEl.style.backgroundColor = selectedColor;
entryEl.style.border = "1px solid rgba(66, 153, 225, 0.6)";
entryEl.style.boxShadow = "0 0 0 1px rgba(66, 153, 225, 0.3)";
entryEl.style.setProperty('backgroundColor', selectedColor, 'important');
entryEl.style.setProperty('border', "1px solid rgba(66, 153, 225, 0.6)", 'important');
entryEl.style.setProperty('box-shadow', "0 0 0 1px rgba(66, 153, 225, 0.3)", 'important');
} else {
entryEl.style.backgroundColor = baseColor;
entryEl.style.border = "1px solid transparent";
@@ -301,8 +312,6 @@ export class PreviewTooltip {
mediaElement.controls = false;
}
mediaElement.src = data.preview_url;
// Create name label with absolute positioning
const nameLabel = document.createElement('div');
nameLabel.textContent = loraName;
@@ -328,12 +337,43 @@ export class PreviewTooltip {
mediaContainer.appendChild(nameLabel);
this.element.appendChild(mediaContainer);
// Add fade-in effect
// Show element with opacity 0 first to get dimensions
this.element.style.opacity = '0';
this.element.style.display = 'block';
this.position(x, y);
// Wait for media to load before positioning
const waitForLoad = () => {
return new Promise((resolve) => {
if (isVideo) {
if (mediaElement.readyState >= 2) { // HAVE_CURRENT_DATA
resolve();
} else {
mediaElement.addEventListener('loadeddata', resolve, { once: true });
mediaElement.addEventListener('error', resolve, { once: true });
}
} else {
if (mediaElement.complete) {
resolve();
} else {
mediaElement.addEventListener('load', resolve, { once: true });
mediaElement.addEventListener('error', resolve, { once: true });
}
}
// Set a timeout to prevent hanging
setTimeout(resolve, 1000);
});
};
// Set source after setting up load listeners
mediaElement.src = data.preview_url;
// Wait for content to load, then position and show
await waitForLoad();
// Small delay to ensure layout is complete
requestAnimationFrame(() => {
this.position(x, y);
this.element.style.transition = 'opacity 0.15s ease';
this.element.style.opacity = '1';
});
@@ -399,3 +439,86 @@ export class PreviewTooltip {
this.element.remove();
}
}
// Function to create expand/collapse button
export function createExpandButton(isExpanded, onClick) {
const button = document.createElement("button");
button.className = "comfy-lora-expand-button";
button.type = "button";
Object.assign(button.style, {
width: "20px",
height: "20px",
display: "flex",
alignItems: "center",
justifyContent: "center",
cursor: "pointer",
userSelect: "none",
fontSize: "10px",
color: "rgba(226, 232, 240, 0.7)",
backgroundColor: "rgba(45, 55, 72, 0.3)",
border: "1px solid rgba(226, 232, 240, 0.2)",
borderRadius: "3px",
transition: "all 0.2s ease",
marginLeft: "6px",
marginRight: "4px",
flexShrink: "0",
outline: "none"
});
// Set icon based on expanded state
updateExpandButtonState(button, isExpanded);
button.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
onClick(!isExpanded);
});
// Add hover effects
button.addEventListener("mouseenter", () => {
button.style.backgroundColor = "rgba(66, 153, 225, 0.2)";
button.style.borderColor = "rgba(66, 153, 225, 0.4)";
button.style.color = "rgba(226, 232, 240, 0.9)";
button.style.transform = "scale(1.05)";
});
button.addEventListener("mouseleave", () => {
button.style.backgroundColor = "rgba(45, 55, 72, 0.3)";
button.style.borderColor = "rgba(226, 232, 240, 0.2)";
button.style.color = "rgba(226, 232, 240, 0.7)";
button.style.transform = "scale(1)";
});
// Add active (pressed) state
button.addEventListener("mousedown", () => {
button.style.transform = "scale(0.95)";
button.style.backgroundColor = "rgba(66, 153, 225, 0.3)";
});
button.addEventListener("mouseup", () => {
button.style.transform = "scale(1.05)"; // Return to hover state
});
// Add focus state for keyboard accessibility
button.addEventListener("focus", () => {
button.style.boxShadow = "0 0 0 2px rgba(66, 153, 225, 0.5)";
});
button.addEventListener("blur", () => {
button.style.boxShadow = "none";
});
return button;
}
// Helper function to update expand button state
export function updateExpandButtonState(button, isExpanded) {
if (isExpanded) {
button.innerHTML = "▼"; // Down arrow for expanded
button.title = "Collapse clip controls";
} else {
button.innerHTML = "▶"; // Right arrow for collapsed
button.title = "Expand clip controls";
}
}

View File

@@ -120,7 +120,9 @@ export function initDrag(dragEl, name, widget, isClipStrength = false, previewTo
// Skip if clicking on toggle or strength control areas
if (e.target.closest('.comfy-lora-toggle') ||
e.target.closest('input') ||
e.target.closest('.comfy-lora-arrow')) {
e.target.closest('.comfy-lora-arrow') ||
e.target.closest('.comfy-lora-drag-handle') ||
e.target.closest('.comfy-lora-expand-button')) {
return;
}
@@ -304,6 +306,9 @@ export function initReorderDrag(dragHandle, loraName, widget, renderFunction) {
});
document.addEventListener('mouseup', (e) => {
// Always reset cursor regardless of isDragging state
document.body.style.cursor = '';
if (!isDragging || !draggedElement) return;
const targetIndex = getDropTargetIndex(container, e.clientY);
@@ -356,10 +361,13 @@ export function initReorderDrag(dragHandle, loraName, widget, renderFunction) {
dropIndicator = null;
}
// Reset cursor
document.body.style.cursor = '';
container = null;
});
// Also reset cursor when mouse leaves the document
document.addEventListener('mouseleave', () => {
document.body.style.cursor = '';
});
}
// Function to handle keyboard navigation

View File

@@ -1,6 +1,21 @@
// ComfyUI extension to track model usage statistics
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
import { showToast } from "./utils.js";
// Define target nodes and their widget configurations
const PATH_CORRECTION_TARGETS = [
{ comfyClass: "CheckpointLoaderSimple", widgetName: "ckpt_name", modelType: "checkpoints" },
{ comfyClass: "Checkpoint Loader with Name (Image Saver)", widgetName: "ckpt_name", modelType: "checkpoints" },
{ comfyClass: "UNETLoader", widgetName: "unet_name", modelType: "checkpoints" },
{ comfyClass: "easy comfyLoader", widgetName: "ckpt_name", modelType: "checkpoints" },
{ comfyClass: "CheckpointLoader|pysssss", widgetName: "ckpt_name", modelType: "checkpoints" },
{ comfyClass: "Efficient Loader", widgetName: "ckpt_name", modelType: "checkpoints" },
{ comfyClass: "UnetLoaderGGUF", widgetName: "unet_name", modelType: "checkpoints" },
{ comfyClass: "UnetLoaderGGUFAdvanced", widgetName: "unet_name", modelType: "checkpoints" },
{ comfyClass: "LoraLoader", widgetName: "lora_name", modelType: "loras" },
{ comfyClass: "easy loraStack", widgetNamePattern: "lora_\\d+_name", modelType: "loras" }
];
// Register the extension
app.registerExtension({
@@ -80,5 +95,110 @@ app.registerExtension({
} catch (error) {
console.error("Error refreshing registry:", error);
}
},
async loadedGraphNode(node) {
// Check if this node type needs path correction
const target = PATH_CORRECTION_TARGETS.find(t => t.comfyClass === node.comfyClass);
if (!target) {
return;
}
await this.correctNodePaths(node, target);
},
async correctNodePaths(node, target) {
try {
if (target.widgetNamePattern) {
// Handle pattern-based widget names (like lora_1_name, lora_2_name, etc.)
const pattern = new RegExp(target.widgetNamePattern);
const widgetIndexes = [];
if (node.widgets) {
node.widgets.forEach((widget, index) => {
if (pattern.test(widget.name)) {
widgetIndexes.push(index);
}
});
}
// Process each matching widget
for (const widgetIndex of widgetIndexes) {
await this.correctWidgetPath(node, widgetIndex, target.modelType);
}
} else {
// Handle single widget name
if (node.widgets) {
const widgetIndex = node.widgets.findIndex(w => w.name === target.widgetName);
if (widgetIndex !== -1) {
await this.correctWidgetPath(node, widgetIndex, target.modelType);
}
}
}
} catch (error) {
console.error("Error correcting node paths:", error);
}
},
async correctWidgetPath(node, widgetIndex, modelType) {
if (!node.widgets_values || !node.widgets_values[widgetIndex]) {
return;
}
const currentPath = node.widgets_values[widgetIndex];
if (!currentPath || typeof currentPath !== 'string') {
return;
}
// Extract filename from path (after last separator)
const fileName = currentPath.split(/[/\\]/).pop();
if (!fileName) {
return;
}
try {
// Search for current relative path
const response = await api.fetchApi(`/${modelType}/relative-paths?search=${encodeURIComponent(fileName)}&limit=2`);
const data = await response.json();
if (!data.success || !data.relative_paths || data.relative_paths.length === 0) {
return;
}
const foundPaths = data.relative_paths;
const firstPath = foundPaths[0];
// Check if we need to update the path
if (firstPath !== currentPath) {
// Update the widget value
// node.widgets_values[widgetIndex] = firstPath;
node.widgets[widgetIndex].value = firstPath;
if (foundPaths.length === 1) {
// Single match found - success
showToast({
severity: 'info',
summary: 'LoRA Manager Path Correction',
detail: `Updated path for ${fileName}: ${firstPath}`,
life: 5000
});
} else {
// Multiple matches found - warning
showToast({
severity: 'warn',
summary: 'LoRA Manager Path Correction',
detail: `Multiple paths found for ${fileName}, using: ${firstPath}`,
life: 5000
});
}
// Mark node as modified
if (node.setDirtyCanvas) {
node.setDirtyCanvas(true);
}
}
} catch (error) {
console.error(`Error correcting path for ${fileName}:`, error);
}
}
});

View File

@@ -23,6 +23,60 @@ export function getComfyUIFrontendVersion() {
return window['__COMFYUI_FRONTEND_VERSION__'] || "0.0.0";
}
/**
* Show a toast notification
* @param {Object|string} options - Toast options object or message string for backward compatibility
* @param {string} [options.severity] - Message severity level (success, info, warn, error, secondary, contrast)
* @param {string} [options.summary] - Short title for the toast
* @param {any} [options.detail] - Detailed message content
* @param {boolean} [options.closable] - Whether user can close the toast (default: true)
* @param {number} [options.life] - Duration in milliseconds before auto-closing
* @param {string} [options.group] - Group identifier for managing related toasts
* @param {any} [options.styleClass] - Style class of the message
* @param {any} [options.contentStyleClass] - Style class of the content
* @param {string} [type] - Deprecated: severity type for backward compatibility
*/
export function showToast(options, type = 'info') {
// Handle backward compatibility: showToast(message, type)
if (typeof options === 'string') {
options = {
detail: options,
severity: type
};
}
// Set defaults
const toastOptions = {
severity: options.severity || 'info',
summary: options.summary,
detail: options.detail,
closable: options.closable !== false, // default to true
life: options.life,
group: options.group,
styleClass: options.styleClass,
contentStyleClass: options.contentStyleClass
};
// Remove undefined properties
Object.keys(toastOptions).forEach(key => {
if (toastOptions[key] === undefined) {
delete toastOptions[key];
}
});
if (app && app.extensionManager && app.extensionManager.toast) {
app.extensionManager.toast.add(toastOptions);
} else {
const message = toastOptions.detail || toastOptions.summary || 'No message';
const severity = toastOptions.severity.toUpperCase();
console.log(`${severity}: ${message}`);
// Fallback alert for critical errors only
if (toastOptions.severity === 'error') {
alert(message);
}
}
}
// Dynamically import the appropriate widget based on app version
export async function dynamicImportByVersion(latestModulePath, legacyModulePath) {
// Parse app version and compare with 1.12.6 (version when tags widget API changed)