From 50a51c2e794496c1011acf5237237c7902f26a2e Mon Sep 17 00:00:00 2001
From: Will Miao <13051207myq@gmail.com>
Date: Mon, 7 Apr 2025 09:02:36 +0800
Subject: [PATCH] Refactor Lora widget and dynamic module loading
- Updated lora_loader.js to dynamically import the appropriate loras widget based on ComfyUI version, enhancing compatibility and maintainability.
- Enhanced loras_widget.js with improved height management and styling for better user experience.
- Introduced utility functions in utils.js for version checking and dynamic imports, streamlining widget loading processes.
- Improved overall structure and readability of the code, ensuring better performance and easier future updates.
---
web/comfyui/legacy_loras_widget.js | 882 +++++++++++++++++++++++++++++
web/comfyui/lora_loader.js | 13 +-
web/comfyui/loras_widget.js | 174 +++---
web/comfyui/trigger_word_toggle.js | 36 +-
web/comfyui/utils.js | 27 +
5 files changed, 1032 insertions(+), 100 deletions(-)
create mode 100644 web/comfyui/legacy_loras_widget.js
diff --git a/web/comfyui/legacy_loras_widget.js b/web/comfyui/legacy_loras_widget.js
new file mode 100644
index 00000000..0e6c0129
--- /dev/null
+++ b/web/comfyui/legacy_loras_widget.js
@@ -0,0 +1,882 @@
+import { api } from "../../scripts/api.js";
+import { app } from "../../scripts/app.js";
+
+export function addLorasWidget(node, name, opts, callback) {
+ // Create container for loras
+ const container = document.createElement("div");
+ container.className = "comfy-loras-container";
+ Object.assign(container.style, {
+ display: "flex",
+ flexDirection: "column",
+ gap: "8px",
+ padding: "6px",
+ backgroundColor: "rgba(40, 44, 52, 0.6)",
+ borderRadius: "6px",
+ width: "100%",
+ });
+
+ // Initialize default value
+ const defaultValue = opts?.defaultVal || [];
+
+ // Parse LoRA entries from value
+ const parseLoraValue = (value) => {
+ if (!value) return [];
+ return Array.isArray(value) ? value : [];
+ };
+
+ // Format LoRA data
+ const formatLoraValue = (loras) => {
+ return loras;
+ };
+
+ // Function to create toggle element
+ const createToggle = (active, onChange) => {
+ const toggle = document.createElement("div");
+ toggle.className = "comfy-lora-toggle";
+
+ updateToggleStyle(toggle, active);
+
+ toggle.addEventListener("click", (e) => {
+ e.stopPropagation();
+ onChange(!active);
+ });
+
+ return toggle;
+ };
+
+ // Helper function to update toggle style
+ function updateToggleStyle(toggleEl, active) {
+ Object.assign(toggleEl.style, {
+ width: "18px",
+ height: "18px",
+ borderRadius: "4px",
+ cursor: "pointer",
+ transition: "all 0.2s ease",
+ backgroundColor: active ? "rgba(66, 153, 225, 0.9)" : "rgba(45, 55, 72, 0.7)",
+ border: `1px solid ${active ? "rgba(66, 153, 225, 0.9)" : "rgba(226, 232, 240, 0.2)"}`,
+ });
+
+ // Add hover effect
+ toggleEl.onmouseenter = () => {
+ toggleEl.style.transform = "scale(1.05)";
+ toggleEl.style.boxShadow = "0 2px 4px rgba(0,0,0,0.15)";
+ };
+
+ toggleEl.onmouseleave = () => {
+ toggleEl.style.transform = "scale(1)";
+ toggleEl.style.boxShadow = "none";
+ };
+ }
+
+ // Create arrow button for strength adjustment
+ const createArrowButton = (direction, onClick) => {
+ const button = document.createElement("div");
+ button.className = `comfy-lora-arrow comfy-lora-arrow-${direction}`;
+
+ Object.assign(button.style, {
+ width: "16px",
+ height: "16px",
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ cursor: "pointer",
+ userSelect: "none",
+ fontSize: "12px",
+ color: "rgba(226, 232, 240, 0.8)",
+ transition: "all 0.2s ease",
+ });
+
+ button.textContent = direction === "left" ? "◀" : "▶";
+
+ button.addEventListener("click", (e) => {
+ e.stopPropagation();
+ onClick();
+ });
+
+ // Add hover effect
+ button.onmouseenter = () => {
+ button.style.color = "white";
+ button.style.transform = "scale(1.2)";
+ };
+
+ button.onmouseleave = () => {
+ button.style.color = "rgba(226, 232, 240, 0.8)";
+ button.style.transform = "scale(1)";
+ };
+
+ return button;
+ };
+
+ // 添加预览弹窗组件
+ class PreviewTooltip {
+ constructor() {
+ this.element = document.createElement('div');
+ Object.assign(this.element.style, {
+ position: 'fixed',
+ zIndex: 9999,
+ background: 'rgba(0, 0, 0, 0.85)',
+ borderRadius: '6px',
+ boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
+ display: 'none',
+ overflow: 'hidden',
+ maxWidth: '300px',
+ });
+ document.body.appendChild(this.element);
+ this.hideTimeout = null; // 添加超时处理变量
+
+ // 添加全局点击事件来隐藏tooltip
+ document.addEventListener('click', () => this.hide());
+
+ // 添加滚动事件监听
+ document.addEventListener('scroll', () => this.hide(), true);
+ }
+
+ async show(loraName, x, y) {
+ try {
+ // 清除之前的隐藏定时器
+ if (this.hideTimeout) {
+ clearTimeout(this.hideTimeout);
+ this.hideTimeout = null;
+ }
+
+ // 如果已经显示同一个lora的预览,则不重复显示
+ if (this.element.style.display === 'block' && this.currentLora === loraName) {
+ return;
+ }
+
+ this.currentLora = loraName;
+
+ // 获取预览URL
+ const response = await api.fetchApi(`/lora-preview-url?name=${encodeURIComponent(loraName)}`, {
+ method: 'GET'
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch preview URL');
+ }
+
+ const data = await response.json();
+ if (!data.success || !data.preview_url) {
+ throw new Error('No preview available');
+ }
+
+ // 清除现有内容
+ while (this.element.firstChild) {
+ this.element.removeChild(this.element.firstChild);
+ }
+
+ // Create media container with relative positioning
+ const mediaContainer = document.createElement('div');
+ Object.assign(mediaContainer.style, {
+ position: 'relative',
+ maxWidth: '300px',
+ maxHeight: '300px',
+ });
+
+ const isVideo = data.preview_url.endsWith('.mp4');
+ const mediaElement = isVideo ? document.createElement('video') : document.createElement('img');
+
+ Object.assign(mediaElement.style, {
+ maxWidth: '300px',
+ maxHeight: '300px',
+ objectFit: 'contain',
+ display: 'block',
+ });
+
+ if (isVideo) {
+ mediaElement.autoplay = true;
+ mediaElement.loop = true;
+ mediaElement.muted = true;
+ mediaElement.controls = false;
+ }
+
+ mediaElement.src = data.preview_url;
+
+ // Create name label with absolute positioning
+ const nameLabel = document.createElement('div');
+ nameLabel.textContent = loraName;
+ Object.assign(nameLabel.style, {
+ position: 'absolute',
+ bottom: '0',
+ left: '0',
+ right: '0',
+ padding: '8px',
+ color: 'rgba(255, 255, 255, 0.95)',
+ fontSize: '13px',
+ fontFamily: "'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif",
+ background: 'linear-gradient(transparent, rgba(0, 0, 0, 0.8))',
+ whiteSpace: 'nowrap',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ textAlign: 'center',
+ backdropFilter: 'blur(4px)',
+ WebkitBackdropFilter: 'blur(4px)',
+ });
+
+ mediaContainer.appendChild(mediaElement);
+ mediaContainer.appendChild(nameLabel);
+ this.element.appendChild(mediaContainer);
+
+ // 添加淡入效果
+ this.element.style.opacity = '0';
+ this.element.style.display = 'block';
+ this.position(x, y);
+
+ requestAnimationFrame(() => {
+ this.element.style.transition = 'opacity 0.15s ease';
+ this.element.style.opacity = '1';
+ });
+ } catch (error) {
+ console.warn('Failed to load preview:', error);
+ }
+ }
+
+ position(x, y) {
+ // 确保预览框不超出视窗边界
+ const rect = this.element.getBoundingClientRect();
+ const viewportWidth = window.innerWidth;
+ const viewportHeight = window.innerHeight;
+
+ let left = x + 10; // 默认在鼠标右侧偏移10px
+ let top = y + 10; // 默认在鼠标下方偏移10px
+
+ // 检查右边界
+ if (left + rect.width > viewportWidth) {
+ left = x - rect.width - 10;
+ }
+
+ // 检查下边界
+ if (top + rect.height > viewportHeight) {
+ top = y - rect.height - 10;
+ }
+
+ Object.assign(this.element.style, {
+ left: `${left}px`,
+ top: `${top}px`
+ });
+ }
+
+ hide() {
+ // 使用淡出效果
+ if (this.element.style.display === 'block') {
+ this.element.style.opacity = '0';
+ this.hideTimeout = setTimeout(() => {
+ this.element.style.display = 'none';
+ this.currentLora = null;
+ // 停止视频播放
+ const video = this.element.querySelector('video');
+ if (video) {
+ video.pause();
+ }
+ this.hideTimeout = null;
+ }, 150);
+ }
+ }
+
+ cleanup() {
+ if (this.hideTimeout) {
+ clearTimeout(this.hideTimeout);
+ }
+ // 移除所有事件监听器
+ document.removeEventListener('click', () => this.hide());
+ document.removeEventListener('scroll', () => this.hide(), true);
+ this.element.remove();
+ }
+ }
+
+ // 创建预览tooltip实例
+ const previewTooltip = new PreviewTooltip();
+
+ // Function to create menu item
+ const createMenuItem = (text, icon, onClick) => {
+ const menuItem = document.createElement('div');
+ Object.assign(menuItem.style, {
+ padding: '6px 20px',
+ cursor: 'pointer',
+ color: 'rgba(226, 232, 240, 0.9)',
+ fontSize: '13px',
+ userSelect: 'none',
+ display: 'flex',
+ alignItems: 'center',
+ gap: '8px',
+ });
+
+ // Create icon element
+ const iconEl = document.createElement('div');
+ iconEl.innerHTML = icon;
+ Object.assign(iconEl.style, {
+ width: '14px',
+ height: '14px',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ });
+
+ // Create text element
+ const textEl = document.createElement('span');
+ textEl.textContent = text;
+
+ menuItem.appendChild(iconEl);
+ menuItem.appendChild(textEl);
+
+ menuItem.addEventListener('mouseenter', () => {
+ menuItem.style.backgroundColor = 'rgba(66, 153, 225, 0.2)';
+ });
+
+ menuItem.addEventListener('mouseleave', () => {
+ menuItem.style.backgroundColor = 'transparent';
+ });
+
+ if (onClick) {
+ menuItem.addEventListener('click', onClick);
+ }
+
+ return menuItem;
+ };
+
+ // Function to create context menu
+ const createContextMenu = (x, y, loraName, widget) => {
+ // Hide preview tooltip first
+ previewTooltip.hide();
+
+ // Remove existing context menu if any
+ const existingMenu = document.querySelector('.comfy-lora-context-menu');
+ if (existingMenu) {
+ existingMenu.remove();
+ }
+
+ const menu = document.createElement('div');
+ menu.className = 'comfy-lora-context-menu';
+ Object.assign(menu.style, {
+ position: 'fixed',
+ left: `${x}px`,
+ top: `${y}px`,
+ backgroundColor: 'rgba(30, 30, 30, 0.95)',
+ border: '1px solid rgba(255, 255, 255, 0.1)',
+ borderRadius: '4px',
+ padding: '4px 0',
+ zIndex: 1000,
+ boxShadow: '0 2px 10px rgba(0,0,0,0.2)',
+ minWidth: '180px',
+ });
+
+ // View on Civitai option with globe icon
+ const viewOnCivitaiOption = createMenuItem(
+ 'View on Civitai',
+ '',
+ async () => {
+ menu.remove();
+ document.removeEventListener('click', closeMenu);
+
+ try {
+ // Get Civitai URL from API
+ const response = await api.fetchApi(`/lora-civitai-url?name=${encodeURIComponent(loraName)}`, {
+ method: 'GET'
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(errorText || 'Failed to get Civitai URL');
+ }
+
+ const data = await response.json();
+ if (data.success && data.civitai_url) {
+ // Open the URL in a new tab
+ window.open(data.civitai_url, '_blank');
+ } else {
+ // Show error message if no Civitai URL
+ if (app && app.extensionManager && app.extensionManager.toast) {
+ app.extensionManager.toast.add({
+ severity: 'warning',
+ summary: 'Not Found',
+ detail: 'This LoRA has no associated Civitai URL',
+ life: 3000
+ });
+ } else {
+ alert('This LoRA has no associated Civitai URL');
+ }
+ }
+ } catch (error) {
+ console.error('Error getting Civitai URL:', error);
+ if (app && app.extensionManager && app.extensionManager.toast) {
+ app.extensionManager.toast.add({
+ severity: 'error',
+ summary: 'Error',
+ detail: error.message || 'Failed to get Civitai URL',
+ life: 5000
+ });
+ } else {
+ alert('Error: ' + (error.message || 'Failed to get Civitai URL'));
+ }
+ }
+ }
+ );
+
+ // Delete option with trash icon
+ const deleteOption = createMenuItem(
+ 'Delete',
+ '',
+ () => {
+ menu.remove();
+ document.removeEventListener('click', closeMenu);
+
+ const lorasData = parseLoraValue(widget.value).filter(l => l.name !== loraName);
+ widget.value = formatLoraValue(lorasData);
+
+ if (widget.callback) {
+ widget.callback(widget.value);
+ }
+ }
+ );
+
+ // Save recipe option with bookmark icon
+ const saveOption = createMenuItem(
+ 'Save Recipe',
+ '',
+ () => {
+ menu.remove();
+ document.removeEventListener('click', closeMenu);
+ saveRecipeDirectly(widget);
+ }
+ );
+
+ // Add separator
+ const separator = document.createElement('div');
+ Object.assign(separator.style, {
+ margin: '4px 0',
+ borderTop: '1px solid rgba(255, 255, 255, 0.1)',
+ });
+
+ menu.appendChild(viewOnCivitaiOption); // Add the new menu option
+ menu.appendChild(deleteOption);
+ menu.appendChild(separator);
+ menu.appendChild(saveOption);
+
+ document.body.appendChild(menu);
+
+ // Close menu when clicking outside
+ const closeMenu = (e) => {
+ if (!menu.contains(e.target)) {
+ menu.remove();
+ document.removeEventListener('click', closeMenu);
+ }
+ };
+ setTimeout(() => document.addEventListener('click', closeMenu), 0);
+ };
+
+ // Function to render loras from data
+ const renderLoras = (value, widget) => {
+ // Clear existing content
+ while (container.firstChild) {
+ container.removeChild(container.firstChild);
+ }
+
+ // Parse the loras data
+ const lorasData = parseLoraValue(value);
+
+ if (lorasData.length === 0) {
+ // Show message when no loras are added
+ const emptyMessage = document.createElement("div");
+ emptyMessage.textContent = "No LoRAs added";
+ Object.assign(emptyMessage.style, {
+ textAlign: "center",
+ padding: "20px 0",
+ color: "rgba(226, 232, 240, 0.8)",
+ fontStyle: "italic",
+ userSelect: "none", // Add this line to prevent text selection
+ WebkitUserSelect: "none", // For Safari support
+ MozUserSelect: "none", // For Firefox support
+ msUserSelect: "none", // For IE/Edge support
+ });
+ container.appendChild(emptyMessage);
+ return;
+ }
+
+ // Create header
+ const header = document.createElement("div");
+ header.className = "comfy-loras-header";
+ Object.assign(header.style, {
+ display: "flex",
+ justifyContent: "space-between",
+ alignItems: "center",
+ padding: "4px 8px",
+ borderBottom: "1px solid rgba(226, 232, 240, 0.2)",
+ marginBottom: "8px"
+ });
+
+ // Add toggle all control
+ const allActive = lorasData.every(lora => lora.active);
+ const toggleAll = createToggle(allActive, (active) => {
+ // Update all loras active state
+ const lorasData = parseLoraValue(widget.value);
+ lorasData.forEach(lora => lora.active = active);
+
+ const newValue = formatLoraValue(lorasData);
+ widget.value = newValue;
+ });
+
+ // Add label to toggle all
+ const toggleLabel = document.createElement("div");
+ toggleLabel.textContent = "Toggle All";
+ Object.assign(toggleLabel.style, {
+ color: "rgba(226, 232, 240, 0.8)",
+ fontSize: "13px",
+ marginLeft: "8px",
+ userSelect: "none", // Add this line to prevent text selection
+ WebkitUserSelect: "none", // For Safari support
+ MozUserSelect: "none", // For Firefox support
+ msUserSelect: "none", // For IE/Edge support
+ });
+
+ const toggleContainer = document.createElement("div");
+ Object.assign(toggleContainer.style, {
+ display: "flex",
+ alignItems: "center",
+ });
+ toggleContainer.appendChild(toggleAll);
+ toggleContainer.appendChild(toggleLabel);
+
+ // Strength label
+ const strengthLabel = document.createElement("div");
+ strengthLabel.textContent = "Strength";
+ Object.assign(strengthLabel.style, {
+ color: "rgba(226, 232, 240, 0.8)",
+ fontSize: "13px",
+ marginRight: "8px",
+ userSelect: "none", // Add this line to prevent text selection
+ WebkitUserSelect: "none", // For Safari support
+ MozUserSelect: "none", // For Firefox support
+ msUserSelect: "none", // For IE/Edge support
+ });
+
+ header.appendChild(toggleContainer);
+ header.appendChild(strengthLabel);
+ container.appendChild(header);
+
+ // Render each lora entry
+ lorasData.forEach((loraData) => {
+ const { name, strength, active } = loraData;
+
+ const loraEl = document.createElement("div");
+ loraEl.className = "comfy-lora-entry";
+ Object.assign(loraEl.style, {
+ display: "flex",
+ justifyContent: "space-between",
+ alignItems: "center",
+ padding: "8px",
+ borderRadius: "6px",
+ backgroundColor: active ? "rgba(45, 55, 72, 0.7)" : "rgba(35, 40, 50, 0.5)",
+ transition: "all 0.2s ease",
+ marginBottom: "6px",
+ });
+
+ // Create toggle for this lora
+ const toggle = createToggle(active, (newActive) => {
+ // Update this lora's active state
+ const lorasData = parseLoraValue(widget.value);
+ const loraIndex = lorasData.findIndex(l => l.name === name);
+
+ if (loraIndex >= 0) {
+ lorasData[loraIndex].active = newActive;
+
+ const newValue = formatLoraValue(lorasData);
+ widget.value = newValue;
+ }
+ });
+
+ // Create name display
+ const nameEl = document.createElement("div");
+ nameEl.textContent = name;
+ Object.assign(nameEl.style, {
+ marginLeft: "10px",
+ 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", // Add pointer cursor to indicate hoverable area
+ userSelect: "none", // Add this line to prevent text selection
+ WebkitUserSelect: "none", // For Safari support
+ MozUserSelect: "none", // For Firefox support
+ msUserSelect: "none", // For IE/Edge support
+ });
+
+ // Move preview tooltip events to nameEl instead of loraEl
+ nameEl.addEventListener('mouseenter', async (e) => {
+ e.stopPropagation();
+ const rect = nameEl.getBoundingClientRect();
+ await previewTooltip.show(name, rect.right, rect.top);
+ });
+
+ nameEl.addEventListener('mouseleave', (e) => {
+ e.stopPropagation();
+ previewTooltip.hide();
+ });
+
+ // Remove the preview tooltip events from loraEl
+ loraEl.onmouseenter = () => {
+ loraEl.style.backgroundColor = active ? "rgba(50, 60, 80, 0.8)" : "rgba(40, 45, 55, 0.6)";
+ };
+
+ loraEl.onmouseleave = () => {
+ loraEl.style.backgroundColor = active ? "rgba(45, 55, 72, 0.7)" : "rgba(35, 40, 50, 0.5)";
+ };
+
+ // Add context menu event
+ loraEl.addEventListener('contextmenu', (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ createContextMenu(e.clientX, e.clientY, name, widget);
+ });
+
+ // Create strength control
+ const strengthControl = document.createElement("div");
+ Object.assign(strengthControl.style, {
+ display: "flex",
+ alignItems: "center",
+ gap: "8px",
+ });
+
+ // Left arrow
+ const leftArrow = createArrowButton("left", () => {
+ // Decrease strength
+ const lorasData = parseLoraValue(widget.value);
+ const loraIndex = lorasData.findIndex(l => l.name === name);
+
+ if (loraIndex >= 0) {
+ lorasData[loraIndex].strength = (lorasData[loraIndex].strength - 0.05).toFixed(2);
+
+ const newValue = formatLoraValue(lorasData);
+ widget.value = newValue;
+ }
+ });
+
+ // Strength display
+ const strengthEl = document.createElement("input");
+ strengthEl.type = "text";
+ strengthEl.value = typeof strength === 'number' ? strength.toFixed(2) : Number(strength).toFixed(2);
+ Object.assign(strengthEl.style, {
+ minWidth: "50px",
+ width: "50px",
+ textAlign: "center",
+ color: active ? "rgba(226, 232, 240, 0.9)" : "rgba(226, 232, 240, 0.6)",
+ fontSize: "13px",
+ background: "none",
+ border: "1px solid transparent",
+ padding: "2px 4px",
+ borderRadius: "3px",
+ outline: "none",
+ });
+
+ // 添加hover效果
+ strengthEl.addEventListener('mouseenter', () => {
+ strengthEl.style.border = "1px solid rgba(226, 232, 240, 0.2)";
+ });
+
+ strengthEl.addEventListener('mouseleave', () => {
+ if (document.activeElement !== strengthEl) {
+ strengthEl.style.border = "1px solid transparent";
+ }
+ });
+
+ // 处理焦点
+ strengthEl.addEventListener('focus', () => {
+ strengthEl.style.border = "1px solid rgba(66, 153, 225, 0.6)";
+ strengthEl.style.background = "rgba(0, 0, 0, 0.2)";
+ // 自动选中所有内容
+ strengthEl.select();
+ });
+
+ strengthEl.addEventListener('blur', () => {
+ strengthEl.style.border = "1px solid transparent";
+ strengthEl.style.background = "none";
+ });
+
+ // 处理输入变化
+ strengthEl.addEventListener('change', () => {
+ let newValue = parseFloat(strengthEl.value);
+
+ // 验证输入
+ if (isNaN(newValue)) {
+ newValue = 1.0;
+ }
+
+ // 更新数值
+ const lorasData = parseLoraValue(widget.value);
+ const loraIndex = lorasData.findIndex(l => l.name === name);
+
+ if (loraIndex >= 0) {
+ lorasData[loraIndex].strength = newValue.toFixed(2);
+
+ // 更新值并触发回调
+ const newLorasValue = formatLoraValue(lorasData);
+ widget.value = newLorasValue;
+ }
+ });
+
+ // 处理按键事件
+ strengthEl.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter') {
+ strengthEl.blur();
+ }
+ });
+
+ // Right arrow
+ const rightArrow = createArrowButton("right", () => {
+ // Increase strength
+ const lorasData = parseLoraValue(widget.value);
+ const loraIndex = lorasData.findIndex(l => l.name === name);
+
+ if (loraIndex >= 0) {
+ lorasData[loraIndex].strength = (parseFloat(lorasData[loraIndex].strength) + 0.05).toFixed(2);
+
+ const newValue = formatLoraValue(lorasData);
+ widget.value = newValue;
+ }
+ });
+
+ strengthControl.appendChild(leftArrow);
+ strengthControl.appendChild(strengthEl);
+ strengthControl.appendChild(rightArrow);
+
+ // Assemble entry
+ const leftSection = document.createElement("div");
+ Object.assign(leftSection.style, {
+ display: "flex",
+ alignItems: "center",
+ flex: "1",
+ minWidth: "0", // Allow shrinking
+ });
+
+ leftSection.appendChild(toggle);
+ leftSection.appendChild(nameEl);
+
+ loraEl.appendChild(leftSection);
+ loraEl.appendChild(strengthControl);
+
+ container.appendChild(loraEl);
+ });
+ };
+
+ // Store the value in a variable to avoid recursion
+ let widgetValue = defaultValue;
+
+ // Create widget with initial properties
+ const widget = node.addDOMWidget(name, "loras", container, {
+ getValue: function() {
+ return widgetValue;
+ },
+ setValue: function(v) {
+ // Remove duplicates by keeping the last occurrence of each lora name
+ const uniqueValue = (v || []).reduce((acc, lora) => {
+ // Remove any existing lora with the same name
+ const filtered = acc.filter(l => l.name !== lora.name);
+ // Add the current lora
+ return [...filtered, lora];
+ }, []);
+
+ widgetValue = uniqueValue;
+ renderLoras(widgetValue, widget);
+
+ // Update container height after rendering
+ requestAnimationFrame(() => {
+ const minHeight = this.getMinHeight();
+ container.style.height = `${minHeight}px`;
+
+ // Force node to update size
+ node.setSize([node.size[0], node.computeSize()[1]]);
+ node.setDirtyCanvas(true, true);
+ });
+ },
+ getMinHeight: function() {
+ // Calculate height based on content
+ const lorasCount = parseLoraValue(widgetValue).length;
+ return Math.max(
+ 100,
+ lorasCount > 0 ? 60 + lorasCount * 44 : 60
+ );
+ },
+ });
+
+ widget.value = defaultValue;
+
+ widget.callback = callback;
+
+ widget.serializeValue = () => {
+ // Add dummy items to avoid the 2-element serialization issue, a bug in comfyui
+ return [...widgetValue,
+ { name: "__dummy_item1__", strength: 0, active: false, _isDummy: true },
+ { name: "__dummy_item2__", strength: 0, active: false, _isDummy: true }
+ ];
+ }
+
+ widget.onRemove = () => {
+ container.remove();
+ previewTooltip.cleanup();
+ };
+
+ return { minWidth: 400, minHeight: 200, widget };
+}
+
+// Function to directly save the recipe without dialog
+async function saveRecipeDirectly(widget) {
+ try {
+ // Get the workflow data from the ComfyUI app
+ const prompt = await app.graphToPrompt();
+ console.log('Prompt:', prompt);
+
+ // Show loading toast
+ if (app && app.extensionManager && app.extensionManager.toast) {
+ app.extensionManager.toast.add({
+ severity: 'info',
+ summary: 'Saving Recipe',
+ detail: 'Please wait...',
+ life: 2000
+ });
+ }
+
+ // Prepare the data - only send workflow JSON
+ const formData = new FormData();
+ formData.append('workflow_json', JSON.stringify(prompt.output));
+
+ // Send the request
+ const response = await fetch('/api/recipes/save-from-widget', {
+ method: 'POST',
+ body: formData
+ });
+
+ const result = await response.json();
+
+ // Show result toast
+ if (app && app.extensionManager && app.extensionManager.toast) {
+ if (result.success) {
+ app.extensionManager.toast.add({
+ severity: 'success',
+ summary: 'Recipe Saved',
+ detail: 'Recipe has been saved successfully',
+ life: 3000
+ });
+ } else {
+ app.extensionManager.toast.add({
+ severity: 'error',
+ summary: 'Error',
+ detail: result.error || 'Failed to save recipe',
+ life: 5000
+ });
+ }
+ }
+ } catch (error) {
+ console.error('Error saving recipe:', error);
+
+ // Show error toast
+ if (app && app.extensionManager && app.extensionManager.toast) {
+ app.extensionManager.toast.add({
+ severity: 'error',
+ summary: 'Error',
+ detail: 'Failed to save recipe: ' + (error.message || 'Unknown error'),
+ life: 5000
+ });
+ }
+ }
+}
diff --git a/web/comfyui/lora_loader.js b/web/comfyui/lora_loader.js
index 815b1a1c..62f01f1c 100644
--- a/web/comfyui/lora_loader.js
+++ b/web/comfyui/lora_loader.js
@@ -1,9 +1,14 @@
import { app } from "../../scripts/app.js";
-import { addLorasWidget } from "./loras_widget.js";
+import { dynamicImportByVersion } from "./utils.js";
// Extract pattern into a constant for consistent use
const LORA_PATTERN = //g;
+// Function to get the appropriate loras widget based on ComfyUI version
+async function getLorasWidgetModule() {
+ return await dynamicImportByVersion("./loras_widget.js", "./legacy_loras_widget.js");
+}
+
function mergeLoras(lorasText, lorasArr) {
const result = [];
let match;
@@ -44,7 +49,7 @@ app.registerExtension({
});
// Wait for node to be properly initialized
- requestAnimationFrame(() => {
+ requestAnimationFrame(async () => {
// Restore saved value if exists
let existingLoras = [];
if (node.widgets_values && node.widgets_values.length > 0) {
@@ -67,6 +72,10 @@ app.registerExtension({
// Add flag to prevent callback loops
let isUpdating = false;
+
+ // Dynamically load the appropriate widget module
+ const lorasModule = await getLorasWidgetModule();
+ const { addLorasWidget } = lorasModule;
// Get the widget object directly from the returned object
const result = addLorasWidget(node, "loras", {
diff --git a/web/comfyui/loras_widget.js b/web/comfyui/loras_widget.js
index 0e6c0129..4e38394e 100644
--- a/web/comfyui/loras_widget.js
+++ b/web/comfyui/loras_widget.js
@@ -5,6 +5,13 @@ export function addLorasWidget(node, name, opts, callback) {
// Create container for loras
const container = document.createElement("div");
container.className = "comfy-loras-container";
+
+ // Set initial height using CSS variables approach
+ const defaultHeight = 200;
+ container.style.setProperty('--comfy-widget-min-height', `${defaultHeight}px`);
+ container.style.setProperty('--comfy-widget-max-height', `${defaultHeight * 2}px`);
+ container.style.setProperty('--comfy-widget-height', `${defaultHeight}px`);
+
Object.assign(container.style, {
display: "flex",
flexDirection: "column",
@@ -13,11 +20,19 @@ export function addLorasWidget(node, name, opts, callback) {
backgroundColor: "rgba(40, 44, 52, 0.6)",
borderRadius: "6px",
width: "100%",
+ boxSizing: "border-box",
+ overflow: "auto"
});
// Initialize default value
const defaultValue = opts?.defaultVal || [];
+ // Fixed sizes for component calculations
+ const LORA_ENTRY_HEIGHT = 44; // Height of a single lora entry
+ const HEADER_HEIGHT = 40; // Height of the header section
+ const CONTAINER_PADDING = 12; // Top and bottom padding
+ const EMPTY_CONTAINER_HEIGHT = 100; // Height when no loras are present
+
// Parse LoRA entries from value
const parseLoraValue = (value) => {
if (!value) return [];
@@ -29,6 +44,23 @@ export function addLorasWidget(node, name, opts, callback) {
return loras;
};
+ // Function to update widget height consistently
+ const updateWidgetHeight = (height) => {
+ // Ensure minimum height
+ const finalHeight = Math.max(defaultHeight, height);
+
+ // Update CSS variables
+ container.style.setProperty('--comfy-widget-min-height', `${finalHeight}px`);
+ container.style.setProperty('--comfy-widget-height', `${finalHeight}px`);
+
+ // Force node to update size after a short delay to ensure DOM is updated
+ if (node) {
+ setTimeout(() => {
+ node.setDirtyCanvas(true, true);
+ }, 10);
+ }
+ };
+
// Function to create toggle element
const createToggle = (active, onChange) => {
const toggle = document.createElement("div");
@@ -107,7 +139,7 @@ export function addLorasWidget(node, name, opts, callback) {
return button;
};
- // 添加预览弹窗组件
+ // Preview tooltip class
class PreviewTooltip {
constructor() {
this.element = document.createElement('div');
@@ -122,31 +154,31 @@ export function addLorasWidget(node, name, opts, callback) {
maxWidth: '300px',
});
document.body.appendChild(this.element);
- this.hideTimeout = null; // 添加超时处理变量
+ this.hideTimeout = null;
- // 添加全局点击事件来隐藏tooltip
+ // Add global click event to hide tooltip
document.addEventListener('click', () => this.hide());
- // 添加滚动事件监听
+ // Add scroll event listener
document.addEventListener('scroll', () => this.hide(), true);
}
async show(loraName, x, y) {
try {
- // 清除之前的隐藏定时器
+ // Clear previous hide timer
if (this.hideTimeout) {
clearTimeout(this.hideTimeout);
this.hideTimeout = null;
}
- // 如果已经显示同一个lora的预览,则不重复显示
+ // Don't redisplay the same lora preview
if (this.element.style.display === 'block' && this.currentLora === loraName) {
return;
}
this.currentLora = loraName;
- // 获取预览URL
+ // Get preview URL
const response = await api.fetchApi(`/lora-preview-url?name=${encodeURIComponent(loraName)}`, {
method: 'GET'
});
@@ -160,7 +192,7 @@ export function addLorasWidget(node, name, opts, callback) {
throw new Error('No preview available');
}
- // 清除现有内容
+ // Clear existing content
while (this.element.firstChild) {
this.element.removeChild(this.element.firstChild);
}
@@ -217,7 +249,7 @@ export function addLorasWidget(node, name, opts, callback) {
mediaContainer.appendChild(nameLabel);
this.element.appendChild(mediaContainer);
- // 添加淡入效果
+ // Add fade-in effect
this.element.style.opacity = '0';
this.element.style.display = 'block';
this.position(x, y);
@@ -232,20 +264,20 @@ export function addLorasWidget(node, name, opts, callback) {
}
position(x, y) {
- // 确保预览框不超出视窗边界
+ // Ensure preview box doesn't exceed viewport boundaries
const rect = this.element.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
- let left = x + 10; // 默认在鼠标右侧偏移10px
- let top = y + 10; // 默认在鼠标下方偏移10px
+ let left = x + 10; // Default 10px offset to the right of mouse
+ let top = y + 10; // Default 10px offset below mouse
- // 检查右边界
+ // Check right boundary
if (left + rect.width > viewportWidth) {
left = x - rect.width - 10;
}
- // 检查下边界
+ // Check bottom boundary
if (top + rect.height > viewportHeight) {
top = y - rect.height - 10;
}
@@ -257,13 +289,13 @@ export function addLorasWidget(node, name, opts, callback) {
}
hide() {
- // 使用淡出效果
+ // Use fade-out effect
if (this.element.style.display === 'block') {
this.element.style.opacity = '0';
this.hideTimeout = setTimeout(() => {
this.element.style.display = 'none';
this.currentLora = null;
- // 停止视频播放
+ // Stop video playback
const video = this.element.querySelector('video');
if (video) {
video.pause();
@@ -277,14 +309,14 @@ export function addLorasWidget(node, name, opts, callback) {
if (this.hideTimeout) {
clearTimeout(this.hideTimeout);
}
- // 移除所有事件监听器
+ // Remove all event listeners
document.removeEventListener('click', () => this.hide());
document.removeEventListener('scroll', () => this.hide(), true);
this.element.remove();
}
}
- // 创建预览tooltip实例
+ // Create preview tooltip instance
const previewTooltip = new PreviewTooltip();
// Function to create menu item
@@ -357,7 +389,7 @@ export function addLorasWidget(node, name, opts, callback) {
padding: '4px 0',
zIndex: 1000,
boxShadow: '0 2px 10px rgba(0,0,0,0.2)',
- minWidth: '180px',
+ minWidth: '180px',
});
// View on Civitai option with globe icon
@@ -447,7 +479,7 @@ export function addLorasWidget(node, name, opts, callback) {
borderTop: '1px solid rgba(255, 255, 255, 0.1)',
});
- menu.appendChild(viewOnCivitaiOption); // Add the new menu option
+ menu.appendChild(viewOnCivitaiOption);
menu.appendChild(deleteOption);
menu.appendChild(separator);
menu.appendChild(saveOption);
@@ -483,12 +515,16 @@ export function addLorasWidget(node, name, opts, callback) {
padding: "20px 0",
color: "rgba(226, 232, 240, 0.8)",
fontStyle: "italic",
- userSelect: "none", // Add this line to prevent text selection
- WebkitUserSelect: "none", // For Safari support
- MozUserSelect: "none", // For Firefox support
- msUserSelect: "none", // For IE/Edge support
+ userSelect: "none",
+ WebkitUserSelect: "none",
+ MozUserSelect: "none",
+ msUserSelect: "none",
+ width: "100%"
});
container.appendChild(emptyMessage);
+
+ // Set fixed height for empty state
+ updateWidgetHeight(EMPTY_CONTAINER_HEIGHT);
return;
}
@@ -522,10 +558,10 @@ export function addLorasWidget(node, name, opts, callback) {
color: "rgba(226, 232, 240, 0.8)",
fontSize: "13px",
marginLeft: "8px",
- userSelect: "none", // Add this line to prevent text selection
- WebkitUserSelect: "none", // For Safari support
- MozUserSelect: "none", // For Firefox support
- msUserSelect: "none", // For IE/Edge support
+ userSelect: "none",
+ WebkitUserSelect: "none",
+ MozUserSelect: "none",
+ msUserSelect: "none",
});
const toggleContainer = document.createElement("div");
@@ -543,10 +579,10 @@ export function addLorasWidget(node, name, opts, callback) {
color: "rgba(226, 232, 240, 0.8)",
fontSize: "13px",
marginRight: "8px",
- userSelect: "none", // Add this line to prevent text selection
- WebkitUserSelect: "none", // For Safari support
- MozUserSelect: "none", // For Firefox support
- msUserSelect: "none", // For IE/Edge support
+ userSelect: "none",
+ WebkitUserSelect: "none",
+ MozUserSelect: "none",
+ msUserSelect: "none",
});
header.appendChild(toggleContainer);
@@ -595,11 +631,11 @@ export function addLorasWidget(node, name, opts, callback) {
whiteSpace: "nowrap",
color: active ? "rgba(226, 232, 240, 0.9)" : "rgba(226, 232, 240, 0.6)",
fontSize: "13px",
- cursor: "pointer", // Add pointer cursor to indicate hoverable area
- userSelect: "none", // Add this line to prevent text selection
- WebkitUserSelect: "none", // For Safari support
- MozUserSelect: "none", // For Firefox support
- msUserSelect: "none", // For IE/Edge support
+ cursor: "pointer",
+ userSelect: "none",
+ WebkitUserSelect: "none",
+ MozUserSelect: "none",
+ msUserSelect: "none",
});
// Move preview tooltip events to nameEl instead of loraEl
@@ -645,7 +681,7 @@ export function addLorasWidget(node, name, opts, callback) {
const loraIndex = lorasData.findIndex(l => l.name === name);
if (loraIndex >= 0) {
- lorasData[loraIndex].strength = (lorasData[loraIndex].strength - 0.05).toFixed(2);
+ lorasData[loraIndex].strength = (parseFloat(lorasData[loraIndex].strength) - 0.05).toFixed(2);
const newValue = formatLoraValue(lorasData);
widget.value = newValue;
@@ -669,7 +705,7 @@ export function addLorasWidget(node, name, opts, callback) {
outline: "none",
});
- // 添加hover效果
+ // Add hover effect
strengthEl.addEventListener('mouseenter', () => {
strengthEl.style.border = "1px solid rgba(226, 232, 240, 0.2)";
});
@@ -680,11 +716,11 @@ export function addLorasWidget(node, name, opts, callback) {
}
});
- // 处理焦点
+ // Handle focus
strengthEl.addEventListener('focus', () => {
strengthEl.style.border = "1px solid rgba(66, 153, 225, 0.6)";
strengthEl.style.background = "rgba(0, 0, 0, 0.2)";
- // 自动选中所有内容
+ // Auto-select all content
strengthEl.select();
});
@@ -693,29 +729,29 @@ export function addLorasWidget(node, name, opts, callback) {
strengthEl.style.background = "none";
});
- // 处理输入变化
+ // Handle input changes
strengthEl.addEventListener('change', () => {
let newValue = parseFloat(strengthEl.value);
- // 验证输入
+ // Validate input
if (isNaN(newValue)) {
newValue = 1.0;
}
- // 更新数值
+ // Update value
const lorasData = parseLoraValue(widget.value);
const loraIndex = lorasData.findIndex(l => l.name === name);
if (loraIndex >= 0) {
lorasData[loraIndex].strength = newValue.toFixed(2);
- // 更新值并触发回调
+ // Update value and trigger callback
const newLorasValue = formatLoraValue(lorasData);
widget.value = newLorasValue;
}
});
- // 处理按键事件
+ // Handle key events
strengthEl.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
strengthEl.blur();
@@ -757,13 +793,17 @@ export function addLorasWidget(node, name, opts, callback) {
container.appendChild(loraEl);
});
+
+ // Calculate height based on number of loras and fixed sizes
+ const calculatedHeight = CONTAINER_PADDING + HEADER_HEIGHT + (lorasData.length * LORA_ENTRY_HEIGHT);
+ updateWidgetHeight(calculatedHeight);
};
// Store the value in a variable to avoid recursion
let widgetValue = defaultValue;
- // Create widget with initial properties
- const widget = node.addDOMWidget(name, "loras", container, {
+ // Create widget with new DOM Widget API
+ const widget = node.addDOMWidget(name, "custom", container, {
getValue: function() {
return widgetValue;
},
@@ -778,29 +818,28 @@ export function addLorasWidget(node, name, opts, callback) {
widgetValue = uniqueValue;
renderLoras(widgetValue, widget);
-
- // Update container height after rendering
- requestAnimationFrame(() => {
- const minHeight = this.getMinHeight();
- container.style.height = `${minHeight}px`;
-
- // Force node to update size
- node.setSize([node.size[0], node.computeSize()[1]]);
- node.setDirtyCanvas(true, true);
- });
},
getMinHeight: function() {
- // Calculate height based on content
- const lorasCount = parseLoraValue(widgetValue).length;
- return Math.max(
- 100,
- lorasCount > 0 ? 60 + lorasCount * 44 : 60
- );
+ return parseInt(container.style.getPropertyValue('--comfy-widget-min-height')) || defaultHeight;
},
+ getMaxHeight: function() {
+ return parseInt(container.style.getPropertyValue('--comfy-widget-max-height')) || defaultHeight * 2;
+ },
+ getHeight: function() {
+ return parseInt(container.style.getPropertyValue('--comfy-widget-height')) || defaultHeight;
+ },
+ hideOnZoom: true,
+ selectOn: ['click', 'focus'],
+ afterResize: function(node) {
+ // Re-render after node resize
+ if (this.value && this.value.length > 0) {
+ renderLoras(this.value, this);
+ }
+ }
});
widget.value = defaultValue;
-
+
widget.callback = callback;
widget.serializeValue = () => {
@@ -816,7 +855,7 @@ export function addLorasWidget(node, name, opts, callback) {
previewTooltip.cleanup();
};
- return { minWidth: 400, minHeight: 200, widget };
+ return { minWidth: 400, minHeight: defaultHeight, widget };
}
// Function to directly save the recipe without dialog
@@ -824,7 +863,6 @@ async function saveRecipeDirectly(widget) {
try {
// Get the workflow data from the ComfyUI app
const prompt = await app.graphToPrompt();
- console.log('Prompt:', prompt);
// Show loading toast
if (app && app.extensionManager && app.extensionManager.toast) {
@@ -879,4 +917,4 @@ async function saveRecipeDirectly(widget) {
});
}
}
-}
+}
\ No newline at end of file
diff --git a/web/comfyui/trigger_word_toggle.js b/web/comfyui/trigger_word_toggle.js
index 3b61da84..f33ec919 100644
--- a/web/comfyui/trigger_word_toggle.js
+++ b/web/comfyui/trigger_word_toggle.js
@@ -1,34 +1,10 @@
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
+import { CONVERTED_TYPE, dynamicImportByVersion } from "./utils.js";
-const CONVERTED_TYPE = 'converted-widget'
-
-function getComfyUIFrontendVersion() {
- // 直接访问全局变量
- return window['__COMFYUI_FRONTEND_VERSION__'];
-}
-
-// Dynamically import the appropriate tags widget based on app version
-function getTagsWidgetModule() {
- // Parse app version and compare with 1.12.6
- const currentVersion = getComfyUIFrontendVersion() || "0.0.0";
- console.log("currentVersion", currentVersion);
- const versionParts = currentVersion.split('.').map(part => parseInt(part, 10));
- const requiredVersion = [1, 12, 6];
-
- // Compare version numbers
- for (let i = 0; i < 3; i++) {
- if (versionParts[i] > requiredVersion[i]) {
- console.log("Using tags_widget.js");
- return import("./tags_widget.js");
- } else if (versionParts[i] < requiredVersion[i]) {
- console.log("Using legacy_tags_widget.js");
- return import("./legacy_tags_widget.js");
- }
- }
-
- // If we get here, versions are equal, use the new module
- return import("./tags_widget.js");
+// Function to get the appropriate tags widget based on ComfyUI version
+async function getTagsWidgetModule() {
+ return await dynamicImportByVersion("./tags_widget.js", "./legacy_tags_widget.js");
}
// TriggerWordToggle extension for ComfyUI
@@ -55,8 +31,8 @@ app.registerExtension({
// Wait for node to be properly initialized
requestAnimationFrame(async () => {
// Dynamically import the appropriate tags widget module
- const tagsWidgetModule = await getTagsWidgetModule();
- const { addTagsWidget } = tagsWidgetModule;
+ const tagsModule = await getTagsWidgetModule();
+ const { addTagsWidget } = tagsModule;
// Get the widget object directly from the returned object
const result = addTagsWidget(node, "toggle_trigger_words", {
diff --git a/web/comfyui/utils.js b/web/comfyui/utils.js
index d7252a85..8d9c1f6c 100644
--- a/web/comfyui/utils.js
+++ b/web/comfyui/utils.js
@@ -1,5 +1,32 @@
export const CONVERTED_TYPE = 'converted-widget';
+export function getComfyUIFrontendVersion() {
+ return window['__COMFYUI_FRONTEND_VERSION__'] || "0.0.0";
+}
+
+// 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)
+ const currentVersion = getComfyUIFrontendVersion();
+ const versionParts = currentVersion.split('.').map(part => parseInt(part, 10));
+ const requiredVersion = [1, 12, 6];
+
+ // Compare version numbers
+ for (let i = 0; i < 3; i++) {
+ if (versionParts[i] > requiredVersion[i]) {
+ console.log(`Using latest widget: ${latestModulePath}`);
+ return import(latestModulePath);
+ } else if (versionParts[i] < requiredVersion[i]) {
+ console.log(`Using legacy widget: ${legacyModulePath}`);
+ return import(legacyModulePath);
+ }
+ }
+
+ // If we get here, versions are equal, use the latest module
+ console.log(`Using latest widget: ${latestModulePath}`);
+ return import(latestModulePath);
+}
+
export function hideWidgetForGood(node, widget, suffix = "") {
widget.origType = widget.type;
widget.origComputeSize = widget.computeSize;