diff --git a/README-EN.md b/README-EN.md index dbebe00..1ef4a9d 100644 --- a/README-EN.md +++ b/README-EN.md @@ -8,6 +8,24 @@ A collection of fully customized ComfyUI nodes developed by Dontdrunk, designed ## Node List +
+ 🎯 DD Prompt Manager (New Feature) + +Intelligent prompt embedding and management system that provides powerful prompt management capabilities for all CLIP text encoder nodes: + +**Core Features:** +- 🔍 **Smart Node Recognition**: Automatically adds "Embed Prompt" buttons to various CLIP text encoders +- 📁 **Import/Export**: Supports JSON format prompt backup and sharing, allowing you to share your prompts with others and import prompts from others + +**Use Cases:** +- Manage commonly used positive/negative prompt templates +- Quickly switch between different style prompt combinations +- Share and backup prompt libraries among teams +- Improve workflow creation efficiency +- Efficiently manage LORA model prompts + +
+
DD Model Optimizer + DD Sampling Optimizer @@ -121,6 +139,12 @@ This extension now supports ComfyUI's native internationalization (i18n) system, 3. Currently only supports node translation, frontend functionality translation is not yet supported ## Version History +- v2.4.0 (2025-06-18) + - 🎉 **New Prompt Manager Feature**: + - Added intelligent prompt embedding functionality for all CLIP text encoder nodes + - Supports CRUD operations for prompts with a modern management interface + - Supports import/export of prompt files in JSON format + - v2.3.1 (2025-05-27) - Improved UI, added theme options, and fixed bugs. diff --git a/README.md b/README.md index d9191be..4baad29 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,24 @@ ## 当前节点列表 +
+ 🎯 DD 提示词管理器(全新功能) + +智能提示词嵌入和管理系统,为所有 CLIP 文本编码器节点提供强大的提示词管理功能: + +**核心特性:** +- 🔍 **智能节点识别**:自动为各种 CLIP 文本编码器添加"嵌入提示词"按钮 +- 📁 **导入导出**:支持 JSON 格式的提示词备份和分享,可以将自己的提示词分享给他人,也可以导入他人的提示词 + +**使用场景:** +- 管理常用的正面/负面提示词模板 +- 快速切换不同风格的提示词组合 +- 团队间分享和备份提示词库 +- 提高工作流创作效率 +- 高效管理LORA模型提示词 + +
+
DD 模型优化加载+DD 采样优化器 @@ -90,9 +108,8 @@
DD 界面布局 -- 已实现Bate版本,可以随机或指定节点标题的颜色,可以对杂乱节点做快速整理 -- 美化UI,添加主题系统,每一种主题有两种不同风格可以切换,支持自定义主题 -- 添加模块化预设,可以预设自己的布局,一键应用 +- 已实现Alpha版本,可以随机或指定节点标题的颜色,可以对杂乱节点做快速整理 +- 美化UI,添加主题系统,每一种主题有两种不同风格可以切换,点击硬币动画即可切换
@@ -122,10 +139,16 @@ 本扩展现已支持 ComfyUI 原生国际化(i18n)系统,可在中英文间切换: 1. 切换语言:在 ComfyUI 界面右上角设置菜单中选择语言选项 -2. 选择中文(zh)或英文(en) +2. 选择中文(zh)和 英文(en) 3. 暂时只支持节点翻译,暂不支持前端功能翻译切换 ## 版本历史 +- v2.4.0 (2025-06-18) + - 🎉 **新增提示词管理器(Prompt Manager)**: + - 为所有 CLIP 文本编码器节点添加智能提示词嵌入功能 + - 支持提示词的增删改查,提供现代化的管理界面 + - 支持导入导出 JSON 格式的提示词文件 + - v2.3.1 (2025-05-27) - 完善界面UI添加主题选项,修复BUG。 @@ -215,9 +238,9 @@ 这是一个持续成长的节点集合,主要用于分享实用的 ComfyUI 扩展工具。欢迎通过 Issues 提供建议和反馈。如果您发现任何问题或有任何改进建议,请随时提出。 ## 许可证 +- MIT授权协议 -MIT - +## 打赏作者 1. 听说项目的成长需要Money来维持 2. 不打赏的话,作者可能会: - 🦥 躺平摆烂 diff --git a/__init__.py b/__init__.py index a1dc648..8e6e6ee 100644 --- a/__init__.py +++ b/__init__.py @@ -48,4 +48,4 @@ NODE_DISPLAY_NAME_MAPPINGS = { __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] -WEB_DIRECTORY = "js" \ No newline at end of file +WEB_DIRECTORY = "extensions" \ No newline at end of file diff --git a/js/Intelligent Layout/LayoutCore.js b/extensions/Intelligent Layout/LayoutCore.js similarity index 100% rename from js/Intelligent Layout/LayoutCore.js rename to extensions/Intelligent Layout/LayoutCore.js diff --git a/js/Intelligent Layout/LayoutExt.js b/extensions/Intelligent Layout/LayoutExt.js similarity index 100% rename from js/Intelligent Layout/LayoutExt.js rename to extensions/Intelligent Layout/LayoutExt.js diff --git a/js/Intelligent Layout/styles/UIStyles.js b/extensions/Intelligent Layout/styles/UIStyles.js similarity index 100% rename from js/Intelligent Layout/styles/UIStyles.js rename to extensions/Intelligent Layout/styles/UIStyles.js diff --git a/js/Intelligent Layout/styles/UIUtils.js b/extensions/Intelligent Layout/styles/UIUtils.js similarity index 100% rename from js/Intelligent Layout/styles/UIUtils.js rename to extensions/Intelligent Layout/styles/UIUtils.js diff --git a/js/Intelligent Layout/styles/古神之眼/AncientGod1.js b/extensions/Intelligent Layout/styles/古神之眼/AncientGod1.js similarity index 100% rename from js/Intelligent Layout/styles/古神之眼/AncientGod1.js rename to extensions/Intelligent Layout/styles/古神之眼/AncientGod1.js diff --git a/js/Intelligent Layout/styles/古神之眼/AncientGod2.js b/extensions/Intelligent Layout/styles/古神之眼/AncientGod2.js similarity index 100% rename from js/Intelligent Layout/styles/古神之眼/AncientGod2.js rename to extensions/Intelligent Layout/styles/古神之眼/AncientGod2.js diff --git a/js/Intelligent Layout/styles/古神之眼/AncientGodTheme.js b/extensions/Intelligent Layout/styles/古神之眼/AncientGodTheme.js similarity index 100% rename from js/Intelligent Layout/styles/古神之眼/AncientGodTheme.js rename to extensions/Intelligent Layout/styles/古神之眼/AncientGodTheme.js diff --git a/js/Intelligent Layout/styles/古神之眼/ChaosPanelStyle.js b/extensions/Intelligent Layout/styles/古神之眼/ChaosPanelStyle.js similarity index 100% rename from js/Intelligent Layout/styles/古神之眼/ChaosPanelStyle.js rename to extensions/Intelligent Layout/styles/古神之眼/ChaosPanelStyle.js diff --git a/js/Intelligent Layout/styles/古神之眼/OrderPanelStyle.js b/extensions/Intelligent Layout/styles/古神之眼/OrderPanelStyle.js similarity index 100% rename from js/Intelligent Layout/styles/古神之眼/OrderPanelStyle.js rename to extensions/Intelligent Layout/styles/古神之眼/OrderPanelStyle.js diff --git a/js/Online Animation/ConnectionAnimationCore.js b/extensions/Online Animation/ConnectionAnimationCore.js similarity index 100% rename from js/Online Animation/ConnectionAnimationCore.js rename to extensions/Online Animation/ConnectionAnimationCore.js diff --git a/js/Online Animation/ConnectionAnimationExt.js b/extensions/Online Animation/ConnectionAnimationExt.js similarity index 100% rename from js/Online Animation/ConnectionAnimationExt.js rename to extensions/Online Animation/ConnectionAnimationExt.js diff --git a/js/Online Animation/effects/BaseEffect.js b/extensions/Online Animation/effects/BaseEffect.js similarity index 100% rename from js/Online Animation/effects/BaseEffect.js rename to extensions/Online Animation/effects/BaseEffect.js diff --git a/js/Online Animation/effects/EffectManager.js b/extensions/Online Animation/effects/EffectManager.js similarity index 100% rename from js/Online Animation/effects/EffectManager.js rename to extensions/Online Animation/effects/EffectManager.js diff --git a/js/Online Animation/effects/FlowEffect.js b/extensions/Online Animation/effects/FlowEffect.js similarity index 100% rename from js/Online Animation/effects/FlowEffect.js rename to extensions/Online Animation/effects/FlowEffect.js diff --git a/js/Online Animation/effects/PulseEffect.js b/extensions/Online Animation/effects/PulseEffect.js similarity index 100% rename from js/Online Animation/effects/PulseEffect.js rename to extensions/Online Animation/effects/PulseEffect.js diff --git a/js/Online Animation/effects/RhythmEffect.js b/extensions/Online Animation/effects/RhythmEffect.js similarity index 100% rename from js/Online Animation/effects/RhythmEffect.js rename to extensions/Online Animation/effects/RhythmEffect.js diff --git a/js/Online Animation/effects/WaveEffect.js b/extensions/Online Animation/effects/WaveEffect.js similarity index 100% rename from js/Online Animation/effects/WaveEffect.js rename to extensions/Online Animation/effects/WaveEffect.js diff --git a/js/Online Animation/styles/AngledStyle.js b/extensions/Online Animation/styles/AngledStyle.js similarity index 100% rename from js/Online Animation/styles/AngledStyle.js rename to extensions/Online Animation/styles/AngledStyle.js diff --git a/js/Online Animation/styles/BaseStyle.js b/extensions/Online Animation/styles/BaseStyle.js similarity index 100% rename from js/Online Animation/styles/BaseStyle.js rename to extensions/Online Animation/styles/BaseStyle.js diff --git a/js/Online Animation/styles/CircuitBoard1Style.js b/extensions/Online Animation/styles/CircuitBoard1Style.js similarity index 100% rename from js/Online Animation/styles/CircuitBoard1Style.js rename to extensions/Online Animation/styles/CircuitBoard1Style.js diff --git a/js/Online Animation/styles/CircuitBoard2Style.js b/extensions/Online Animation/styles/CircuitBoard2Style.js similarity index 100% rename from js/Online Animation/styles/CircuitBoard2Style.js rename to extensions/Online Animation/styles/CircuitBoard2Style.js diff --git a/js/Online Animation/styles/CurveStyle.js b/extensions/Online Animation/styles/CurveStyle.js similarity index 100% rename from js/Online Animation/styles/CurveStyle.js rename to extensions/Online Animation/styles/CurveStyle.js diff --git a/js/Online Animation/styles/DirectStyle.js b/extensions/Online Animation/styles/DirectStyle.js similarity index 100% rename from js/Online Animation/styles/DirectStyle.js rename to extensions/Online Animation/styles/DirectStyle.js diff --git a/js/Online Animation/styles/StyleManager.js b/extensions/Online Animation/styles/StyleManager.js similarity index 100% rename from js/Online Animation/styles/StyleManager.js rename to extensions/Online Animation/styles/StyleManager.js diff --git a/extensions/Prompt_Manager/__init__.py b/extensions/Prompt_Manager/__init__.py new file mode 100644 index 0000000..0485388 --- /dev/null +++ b/extensions/Prompt_Manager/__init__.py @@ -0,0 +1,12 @@ +# ComfyUI Prompt Manager Extension +# This extension adds prompt embedding functionality to CLIP text encoder nodes + +# 导入API处理程序 +from .api.prompt_api import prompt_api + +NODE_CLASS_MAPPINGS = {} +NODE_DISPLAY_NAME_MAPPINGS = {} + +WEB_DIRECTORY = "./web" + +__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY'] diff --git a/extensions/Prompt_Manager/api/__init__.py b/extensions/Prompt_Manager/api/__init__.py new file mode 100644 index 0000000..117a164 --- /dev/null +++ b/extensions/Prompt_Manager/api/__init__.py @@ -0,0 +1 @@ +# Prompt Manager API Module diff --git a/extensions/Prompt_Manager/api/prompt_api.py b/extensions/Prompt_Manager/api/prompt_api.py new file mode 100644 index 0000000..655e614 --- /dev/null +++ b/extensions/Prompt_Manager/api/prompt_api.py @@ -0,0 +1,58 @@ +import os +import json +from aiohttp import web +import aiohttp +from server import PromptServer + +# 提示词数据存储路径 +PROMPTS_FILE = os.path.join(os.path.dirname(__file__), "prompts.json") + +class PromptEmbedderAPI: + def __init__(self): + self.prompts = self.load_prompts() + + def load_prompts(self): + """从JSON文件加载提示词""" + if os.path.exists(PROMPTS_FILE): + try: + with open(PROMPTS_FILE, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception as e: + print(f"Error loading prompts: {e}") + return [] + return [] + + def save_prompts(self): + """保存提示词到JSON文件""" + try: + os.makedirs(os.path.dirname(PROMPTS_FILE), exist_ok=True) + with open(PROMPTS_FILE, 'w', encoding='utf-8') as f: + json.dump(self.prompts, f, ensure_ascii=False, indent=2) + return True + except Exception as e: + print(f"Error saving prompts: {e}") + return False + + async def get_prompts(self, request): + """获取所有提示词""" + return web.json_response(self.prompts) + + async def save_prompts_api(self, request): + """保存提示词列表""" + try: + data = await request.json() + self.prompts = data + success = self.save_prompts() + if success: + return web.json_response({"status": "success"}) + else: + return web.json_response({"status": "error", "message": "Failed to save prompts"}, status=500) + except Exception as e: + return web.json_response({"status": "error", "message": str(e)}, status=400) + +# 创建API实例 +prompt_api = PromptEmbedderAPI() + +# 注册路由 +PromptServer.instance.routes.get("/prompt_embedder/prompts")(prompt_api.get_prompts) +PromptServer.instance.routes.post("/prompt_embedder/prompts")(prompt_api.save_prompts_api) diff --git a/extensions/Prompt_Manager/preview.html b/extensions/Prompt_Manager/preview.html new file mode 100644 index 0000000..e69de29 diff --git a/extensions/Prompt_Manager/prompts.json b/extensions/Prompt_Manager/prompts.json new file mode 100644 index 0000000..43a0c67 --- /dev/null +++ b/extensions/Prompt_Manager/prompts.json @@ -0,0 +1,14 @@ +[ + { + "id": 1, + "name": "示例提示词1", + "content": "beautiful, detailed, high quality, masterpiece", + "createdAt": "2025-06-18T00:00:00.000Z" + }, + { + "id": 2, + "name": "示例提示词2", + "content": "1girl, anime style, colorful, vibrant", + "createdAt": "2025-06-18T00:00:00.000Z" + } +] diff --git a/extensions/Prompt_Manager/web/PromptManagerExt.js b/extensions/Prompt_Manager/web/PromptManagerExt.js new file mode 100644 index 0000000..8c2000d --- /dev/null +++ b/extensions/Prompt_Manager/web/PromptManagerExt.js @@ -0,0 +1,1404 @@ +// ComfyUI Prompt Manager Extension +import { app } from "/scripts/app.js"; + +const CLIP_TEXT_ENCODERS = [ + "CLIPTextEncode", + "CLIPTextEncodeSDXL", + "CLIPTextEncodeSDXLRefiner", + "BNK_CLIPTextEncodeAdvanced", + "AdvancedCLIPTextEncode", + "CLIPTextEncodeFlux", + "easy negative", + "easy positive", + "Easy Use", + "Easy Negative", + "Easy Positive", + "CLIPTextEncodeFlux(Advanced)", + "CLIP Text Encode (Prompt)" + // 添加更多CLIP文本编码器节点类型 +]; + +class PromptManager { + constructor() { + this.prompts = []; + this.loadPrompts(); + } + + async loadPrompts() { + try { + // 首先尝试从本地存储加载 + const localData = localStorage.getItem('comfyui_dd_prompts'); + if (localData) { + this.prompts = JSON.parse(localData); + return; + } + + // 如果本地存储没有数据,尝试从 JSON 文件加载 + const response = await fetch('./extensions/Prompt_Manager/prompts.json'); + if (response.ok) { + this.prompts = await response.json(); + // 将数据保存到本地存储 + this.savePrompts(); + } + } catch (error) { + console.log('加载提示词失败,使用空列表:', error); + this.prompts = []; + } + } + + savePrompts() { + try { + // 保存到本地存储 + localStorage.setItem('comfyui_dd_prompts', JSON.stringify(this.prompts)); + console.log('提示词已保存到本地存储'); + } catch (error) { + console.error('保存提示词失败:', error); + } + } + + addPrompt(name, content) { + const prompt = { + id: Date.now(), + name: name, + content: content, + createdAt: new Date().toISOString() + }; + this.prompts.push(prompt); + this.savePrompts(); + return prompt; + } + + updatePrompt(id, name, content) { + const index = this.prompts.findIndex(p => p.id === id); + if (index !== -1) { + this.prompts[index].name = name; + this.prompts[index].content = content; + this.prompts[index].updatedAt = new Date().toISOString(); + this.savePrompts(); + return this.prompts[index]; + } + return null; + } + + deletePrompt(id) { + const index = this.prompts.findIndex(p => p.id === id); + if (index !== -1) { + this.prompts.splice(index, 1); + this.savePrompts(); + return true; + } + return false; + } + + getPrompts() { + return this.prompts; + } + + // 导出提示词到 JSON 格式 + exportPrompts() { + const dataStr = JSON.stringify(this.prompts, null, 2); + const dataBlob = new Blob([dataStr], {type: 'application/json'}); + const url = URL.createObjectURL(dataBlob); + const link = document.createElement('a'); + link.href = url; + link.download = 'comfyui_prompts.json'; + link.click(); + URL.revokeObjectURL(url); + } + + // 从 JSON 文件导入提示词 + importPrompts(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (e) => { + try { + const imported = JSON.parse(e.target.result); + if (Array.isArray(imported)) { + // 合并导入的提示词,避免ID冲突 + const maxId = Math.max(...this.prompts.map(p => p.id), 0); + imported.forEach((prompt, index) => { + prompt.id = maxId + index + 1; + this.prompts.push(prompt); + }); + this.savePrompts(); + resolve(imported.length); + } else { + reject('无效的JSON格式'); + } + } catch (error) { + reject('解析JSON文件失败'); + } + }; + reader.readAsText(file); + }); + } +} + +class PromptEmbedderUI { + constructor(node, promptManager) { + this.node = node; + this.promptManager = promptManager; + this.isVisible = false; + this.editingPrompt = null; + this.createModal(); + } + + createModal() { + // 创建模态框HTML结构 + this.modal = document.createElement('div'); + this.modal.className = 'prompt-embedder-modal'; + this.modal.innerHTML = ` +
+
+
+

嵌入提示词管理

+ +
+
+
+

提示词列表

+
+ + + +
+
+
+
+ +
+
+
+ `; + + // 添加样式 + this.addStyles(); + + // 绑定事件 + this.bindEvents(); + + document.body.appendChild(this.modal); + } + + addStyles() { + if (document.getElementById('prompt-embedder-styles')) return; + + const styles = document.createElement('style'); + styles.id = 'prompt-embedder-styles'; + styles.textContent = ` + .prompt-embedder-modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 10000; + } + + .prompt-embedder-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + display: flex; + justify-content: center; + align-items: center; + } + + .prompt-embedder-container { + background: #1a1a1a; + border-radius: 8px; + border: 1px solid #3d3d3d; + width: 90%; + max-width: 800px; + max-height: 90%; + overflow: hidden; + color: #ffffff; + } + + .prompt-embedder-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 15px 20px; + border-bottom: 1px solid #3d3d3d; + background: #262626; + } + + .prompt-embedder-header h3 { + margin: 0; + color: #ffffff; + } + + .close-btn { + background: none; + border: none; + color: #ffffff; + font-size: 24px; + cursor: pointer; + padding: 0; + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + } + + .close-btn:hover { + background: #ff5733; + border-radius: 50%; + } + + .prompt-embedder-content { + padding: 20px; + max-height: 600px; + overflow-y: auto; + } .prompt-list-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; + } + + .prompt-list-header h4 { + margin: 0; + color: #ffffff; + } + + .header-actions { + display: flex; + gap: 8px; + } + + .add-prompt-btn, .import-btn, .export-btn { + background: #2a82e4; + border: none; + color: white; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + font-weight: bold; + font-size: 12px; + transition: background 0.2s; + } + + .add-prompt-btn:hover { + background: #1f6bb8; + } + + .import-btn { + background: #43cf7c; + } + + .import-btn:hover { + background: #38b569; + } + + .export-btn { + background: #ffc300; + color: #000; + } + + .export-btn:hover { + background: #e6af00; + } + + .prompt-list { + min-height: 200px; + } + + .prompt-item { + background: #262626; + border: 1px solid #3d3d3d; + border-radius: 6px; + padding: 15px; + margin-bottom: 10px; + transition: all 0.2s; + } + + .prompt-item:hover { + border-color: #2a82e4; + } + + .prompt-item-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + } + + .prompt-item-name { + font-weight: bold; + color: #ffffff; + font-size: 16px; + } + + .prompt-item-actions { + display: flex; + gap: 8px; + } .edit-btn, .apply-btn, .delete-btn { + background: #383838; + border: none; + color: white; + padding: 6px 12px; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + transition: background 0.2s; + } + + .edit-btn:hover { + background: #ffc300; + color: #000; + } + + .apply-btn:hover { + background: #43cf7c; + } + + .delete-btn { + background: #8b3a3a; + } + + .delete-btn:hover { + background: #ff5733; + } + + .prompt-item-content { + color: #c4c4c4; + line-height: 1.4; + background: #1a1a1a; + padding: 10px; + border-radius: 4px; + border: 1px solid #3d3d3d; + white-space: pre-wrap; + } + + .prompt-form-section { + border-top: 1px solid #3d3d3d; + padding-top: 20px; + margin-top: 20px; + } + + .form-title { + margin: 0 0 15px 0; + color: #ffffff; + } + + .form-group { + margin-bottom: 15px; + } + + .form-group label { + display: block; + margin-bottom: 5px; + color: #ffffff; + font-weight: bold; + } + + .form-group input, + .form-group textarea { + width: 100%; + padding: 10px; + background: #262626; + border: 1px solid #3d3d3d; + border-radius: 4px; + color: #ffffff; + font-size: 14px; + resize: vertical; + } + + .form-group input:focus, + .form-group textarea:focus { + outline: none; + border-color: #2a82e4; + } + + .form-actions { + display: flex; + gap: 10px; + justify-content: flex-end; + } + + .save-btn { + background: #43cf7c; + border: none; + color: white; + padding: 10px 20px; + border-radius: 4px; + cursor: pointer; + font-weight: bold; + } + + .save-btn:hover { + background: #38b569; + } + + .cancel-btn { + background: #ff5733; + border: none; + color: white; + padding: 10px 20px; + border-radius: 4px; + cursor: pointer; + font-weight: bold; + } + + .cancel-btn:hover { + background: #e04929; + } + + .empty-state { + text-align: center; + padding: 40px 20px; + color: #888; + } + + .empty-state-icon { + font-size: 48px; + margin-bottom: 15px; + opacity: 0.5; + } + + .widget-selector-modal { + display: block; + position: fixed; + z-index: 10001; + left: 0; + top: 0; + width: 100%; + height: 100%; + } + + .widget-selector-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + display: flex; + justify-content: center; + align-items: center; + } + + .widget-selector-container { + background: #2a2a2a; + border-radius: 8px; + min-width: 400px; + max-width: 500px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); + } + + .widget-selector-header { + background: #1a1a1a; + padding: 15px 20px; + border-radius: 8px 8px 0 0; + display: flex; + justify-content: space-between; + align-items: center; + } + + .widget-selector-content { + padding: 20px; + } + + .widget-list { + display: flex; + flex-direction: column; + gap: 10px; + margin-top: 15px; + } + + .widget-option { + background: #3a3a3a; + border: 1px solid #555; + color: white; + padding: 12px 16px; + border-radius: 4px; + cursor: pointer; + text-align: left; + transition: background 0.2s; + } + + .widget-option:hover { + background: #4a4a4a; + } + `; + document.head.appendChild(styles); + } + + bindEvents() { + // 关闭按钮 + this.modal.querySelector('.close-btn').addEventListener('click', () => { + this.hide(); + }); + + // 点击遮罩层关闭 + this.modal.querySelector('.prompt-embedder-overlay').addEventListener('click', (e) => { + if (e.target === e.currentTarget) { + this.hide(); + } + }); // 添加提示词按钮 + this.modal.querySelector('.add-prompt-btn').addEventListener('click', () => { + this.showForm(); + }); + + // 导出按钮 + this.modal.querySelector('.export-btn').addEventListener('click', () => { + this.promptManager.exportPrompts(); + }); + + // 导入按钮 + this.modal.querySelector('.import-btn').addEventListener('click', () => { + this.showImportDialog(); + }); + + // 表单提交 + this.modal.querySelector('.prompt-form').addEventListener('submit', (e) => { + e.preventDefault(); + this.handleFormSubmit(); + }); + + // 取消按钮 + this.modal.querySelector('.cancel-btn').addEventListener('click', () => { + this.hideForm(); + }); + } + + show() { + this.isVisible = true; + this.modal.style.display = 'block'; + this.renderPromptList(); + } + + hide() { + this.isVisible = false; + this.modal.style.display = 'none'; + this.hideForm(); + } + + showForm(prompt = null) { + this.editingPrompt = prompt; + const formSection = this.modal.querySelector('.prompt-form-section'); + const form = this.modal.querySelector('.prompt-form'); + const title = this.modal.querySelector('.form-title'); + + if (prompt) { + title.textContent = '编辑提示词'; + form.name.value = prompt.name; + form.content.value = prompt.content; + } else { + title.textContent = '添加提示词'; + form.reset(); + } + + formSection.style.display = 'block'; + form.name.focus(); + } + + hideForm() { + this.editingPrompt = null; + this.modal.querySelector('.prompt-form-section').style.display = 'none'; + this.modal.querySelector('.prompt-form').reset(); + } + + handleFormSubmit() { + const form = this.modal.querySelector('.prompt-form'); + const name = form.name.value.trim(); + const content = form.content.value.trim(); + + if (!name || !content) { + alert('请填写完整的提示词名称和内容'); + return; + } + + if (this.editingPrompt) { + this.promptManager.updatePrompt(this.editingPrompt.id, name, content); + } else { + this.promptManager.addPrompt(name, content); + } + + this.hideForm(); + this.renderPromptList(); + } + + renderPromptList() { + const listContainer = this.modal.querySelector('.prompt-list'); + const prompts = this.promptManager.getPrompts(); + + if (prompts.length === 0) { + listContainer.innerHTML = ` +
+
📝
+
暂无提示词,点击上方"+ 添加提示词"按钮来添加第一个提示词
+
+ `; + return; + } listContainer.innerHTML = prompts.map(prompt => ` +
+
+
${this.escapeHtml(prompt.name)}
+
+ + + +
+
+
${this.escapeHtml(prompt.content)}
+
+ `).join(''); + + // 绑定按钮事件 + listContainer.addEventListener('click', (e) => { + const action = e.target.getAttribute('data-action'); + const promptItem = e.target.closest('.prompt-item'); + const promptId = parseInt(promptItem.getAttribute('data-id')); + const prompt = prompts.find(p => p.id === promptId); if (action === 'edit') { + this.showForm(prompt); + } else if (action === 'apply') { + this.applyPrompt(prompt); + } else if (action === 'delete') { + this.deletePrompt(prompt); + } + }); + } + + applyPrompt(prompt) { + // 查找所有文本widget + const textWidgets = this.node.widgets.filter(w => this.isTextWidget(w)); + + if (textWidgets.length === 0) { + alert('未找到可用的文本输入框'); + return; + } + + // 如果有多个文本输入框,显示选择器 + if (textWidgets.length > 1) { + // 首先检查是否有当前聚焦的widget + const focusedWidget = textWidgets.find(w => + w.element && w.element === document.activeElement + ); + + if (focusedWidget) { + // 如果有聚焦的widget,直接应用到该widget + this.applyPromptToWidget(prompt, focusedWidget); + return; + } + + // 否则显示选择器 + this.showTextWidgetSelector(prompt); + return; + } + + // 如果只有一个文本输入框,直接应用 + this.applyPromptToWidget(prompt, textWidgets[0]); + } + + isTextWidget(widget) { + if (!widget) return false; + + // 检查widget类型 + if (widget.type === 'text' || + widget.type === 'string' || + widget.type === 'customtext') { + return true; + } + + // 检查widget名称 + const textWidgetNames = [ + 'text', 'prompt', 'positive', 'negative', + 'text_g', 'text_l', 'clip_l', 'clip_g', 't5xxl', + 'input', 'content', 'description' + ]; + + if (textWidgetNames.includes(widget.name)) { + return true; + } + + // 检查DOM元素 + if (widget.element) { + const tagName = widget.element.tagName; + if (tagName === 'TEXTAREA') { + return true; + } + if (tagName === 'INPUT' && + (widget.element.type === 'text' || widget.element.type === 'textarea')) { + return true; + } + } + + // 检查是否是ComfyUI的多行文本输入 + if (widget.inputEl && widget.inputEl.tagName === 'TEXTAREA') { + return true; + } + + // 检查ComfyUI特殊情况 + if (widget.options && widget.options.multiline) { + return true; + } + + return false; + } + + findTextWidget(node) { + // 常见的文本widget名称 + const textWidgetNames = [ + 'text', 'prompt', 'positive', 'negative', + 'text_g', 'text_l', // SDXL的两个文本输入 + 'clip_l', 'clip_g', // FLUX的文本输入 + 't5xxl' // FLUX的T5编码器 + ]; + + // 首先尝试精确匹配常见名称 + for (const name of textWidgetNames) { + const widget = node.widgets.find(w => w.name === name); + if (widget && this.isTextWidget(widget)) { + return widget; + } + } + + // 如果没找到,查找所有文本类型的widget + const textWidgets = node.widgets.filter(w => this.isTextWidget(w)); + + if (textWidgets.length === 1) { + return textWidgets[0]; + } else if (textWidgets.length > 1) { + // 如果有多个文本widget,优先选择第一个可见的 + const visibleWidget = textWidgets.find(w => + !w.hidden && (!w.options || !w.options.hidden) + ); + return visibleWidget || textWidgets[0]; + } + + return null; + } + + escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + + showTextWidgetSelector(prompt) { + const textWidgets = this.node.widgets.filter(w => this.isTextWidget(w)); + + if (textWidgets.length <= 1) { + return false; // 不需要显示选择器 + } + + // 创建选择器模态框 + const selectorModal = document.createElement('div'); + selectorModal.className = 'widget-selector-modal'; + selectorModal.innerHTML = ` +
+
+
+

选择文本输入框

+ +
+
+

该节点有多个文本输入框,请选择要应用提示词的输入框:

+
+ ${textWidgets.map((widget, index) => ` + + `).join('')} +
+
+
+
+ `; + + // 绑定事件 + selectorModal.querySelector('.close-btn').addEventListener('click', () => { + document.body.removeChild(selectorModal); + }); + + selectorModal.querySelector('.widget-selector-overlay').addEventListener('click', (e) => { + if (e.target === e.currentTarget) { + document.body.removeChild(selectorModal); + } + }); + + selectorModal.addEventListener('click', (e) => { + if (e.target.classList.contains('widget-option')) { + const index = parseInt(e.target.getAttribute('data-index')); + const selectedWidget = textWidgets[index]; + this.applyPromptToWidget(prompt, selectedWidget); + document.body.removeChild(selectorModal); + } + }); + + document.body.appendChild(selectorModal); + return true; + } + + getWidgetDisplayName(widget) { + const nameMap = { + 'text': '文本', + 'prompt': '提示词', + 'positive': '正面提示词', + 'negative': '负面提示词', + 'text_g': '全局文本', + 'text_l': '局部文本', + 'clip_l': 'CLIP-L', + 'clip_g': 'CLIP-G', + 't5xxl': 'T5-XXL' + }; + + return nameMap[widget.name] || widget.name || '文本输入'; + } applyPromptToWidget(prompt, widget) { + if (!widget) return; + + // 检查是否需要追加到现有内容 + const currentValue = widget.value || ''; + const shouldAppend = currentValue.trim().length > 0; + + if (shouldAppend) { + // 显示自定义确认对话框 + this.showConfirmDialog( + '当前输入框已有内容', + '是否追加提示词?', + (confirmed) => { + let newValue; + if (confirmed) { + newValue = currentValue + (currentValue.endsWith(',') || currentValue.endsWith(', ') ? ' ' : ', ') + prompt.content; + } else { + newValue = prompt.content; + } + this.setWidgetValue(widget, newValue); + } + ); + } else { + this.setWidgetValue(widget, prompt.content); + } + } + + setWidgetValue(widget, newValue) { + // 设置新值 + widget.value = newValue; + // 尝试多种方式触发更新 + + // 1. 触发widget的回调 + if (widget.callback) { + widget.callback(newValue); + } + + // 2. 如果有DOM元素,更新DOM并触发事件 + if (widget.element) { + widget.element.value = newValue; + widget.element.dispatchEvent(new Event('input', { bubbles: true })); + widget.element.dispatchEvent(new Event('change', { bubbles: true })); + } + + // 3. 如果有inputEl(ComfyUI的多行文本输入) + if (widget.inputEl) { + widget.inputEl.value = newValue; + widget.inputEl.dispatchEvent(new Event('input', { bubbles: true })); + widget.inputEl.dispatchEvent(new Event('change', { bubbles: true })); + } + + // 4. 强制触发ComfyUI的更新机制 + if (this.node.onWidgetChanged) { + this.node.onWidgetChanged(widget.name, newValue, null, widget); + } + + // 5. 标记节点为脏状态,强制重绘 + if (this.node.setDirtyCanvas) { + this.node.setDirtyCanvas(true, true); + } + + // 6. 触发图形更新 + if (this.node.graph && this.node.graph.change) { + this.node.graph.change(); + } + console.log(`已将提示词应用到 ${widget.name}: ${newValue}`); + this.hide(); + } + + deletePrompt(prompt) { + // 显示确认删除对话框 + this.showDeleteConfirmDialog(prompt.name, () => { + this.promptManager.deletePrompt(prompt.id); + this.renderPromptList(); + }); + } + + showDeleteConfirmDialog(promptName, callback) { + const confirmModal = document.createElement('div'); + confirmModal.className = 'delete-confirm-modal'; + confirmModal.innerHTML = ` +
+
+
+

确认删除

+ +
+
+

确定要删除提示词 "${this.escapeHtml(promptName)}" 吗?

+

此操作不可撤销!

+
+ + +
+
+
+
+ `; + + this.addDeleteConfirmStyles(); + + confirmModal.querySelector('.dialog-close-btn').addEventListener('click', () => { + document.body.removeChild(confirmModal); + }); + + confirmModal.querySelector('.delete-confirm-btn').addEventListener('click', () => { + callback(); + document.body.removeChild(confirmModal); + }); + + confirmModal.querySelector('.delete-cancel-btn').addEventListener('click', () => { + document.body.removeChild(confirmModal); + }); + + confirmModal.querySelector('.confirm-dialog-overlay').addEventListener('click', (e) => { + if (e.target === e.currentTarget) { + document.body.removeChild(confirmModal); + } + }); + + document.body.appendChild(confirmModal); + } + + showImportDialog() { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json'; + input.addEventListener('change', async (e) => { + const file = e.target.files[0]; + if (file) { + try { + const count = await this.promptManager.importPrompts(file); + alert(`成功导入 ${count} 个提示词!`); + this.renderPromptList(); + } catch (error) { + alert(`导入失败: ${error}`); + } + } + }); + input.click(); + }showConfirmDialog(title, message, callback) { + const confirmModal = document.createElement('div'); + confirmModal.className = 'confirm-dialog-modal'; + confirmModal.innerHTML = ` +
+
+
+

${title}

+ +
+
+

${message}

+
+
+
确定 - 追加到现有内容后面
+
保留原有内容,将新提示词添加到后面
+
+
+
取消 - 替换全部内容
+
删除原有内容,使用新提示词替换
+
+
+
+
+
+ `; + + // 添加确认对话框样式 + this.addConfirmDialogStyles(); + + // 绑定事件 + confirmModal.querySelector('.dialog-close-btn').addEventListener('click', () => { + document.body.removeChild(confirmModal); + }); + + confirmModal.querySelectorAll('.option-card').forEach(card => { + card.addEventListener('click', () => { + const action = card.getAttribute('data-action'); + callback(action === 'append'); + document.body.removeChild(confirmModal); + }); + }); + + confirmModal.querySelector('.confirm-dialog-overlay').addEventListener('click', (e) => { + if (e.target === e.currentTarget) { + document.body.removeChild(confirmModal); + } + }); + + document.body.appendChild(confirmModal); + } + + addConfirmDialogStyles() { + if (document.getElementById('confirm-dialog-styles')) return; + + const styles = document.createElement('style'); + styles.id = 'confirm-dialog-styles'; + styles.textContent = ` + .confirm-dialog-modal { + display: block; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 10002; + } + + .confirm-dialog-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + display: flex; + justify-content: center; + align-items: center; + } + + .confirm-dialog-container { + background: linear-gradient(145deg, #2a2a2a, #1f1f1f); + border: 1px solid #404040; + border-radius: 12px; + min-width: 400px; + max-width: 500px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6); + animation: confirmDialogFadeIn 0.3s ease-out; + } + + @keyframes confirmDialogFadeIn { + from { + opacity: 0; + transform: scale(0.9) translateY(-20px); + } + to { + opacity: 1; + transform: scale(1) translateY(0); + } + } .confirm-dialog-header { + background: linear-gradient(145deg, #333333, #2a2a2a); + padding: 16px 20px; + border-radius: 12px 12px 0 0; + border-bottom: 1px solid #404040; + display: flex; + justify-content: space-between; + align-items: center; + } + + .confirm-dialog-header h4 { + margin: 0; + color: #ffffff; + font-size: 16px; + font-weight: 600; + } + + .dialog-close-btn { + background: none; + border: none; + color: #999999; + font-size: 20px; + cursor: pointer; + padding: 4px; + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + transition: all 0.2s ease; + } + + .dialog-close-btn:hover { + background: #ff5733; + color: #ffffff; + transform: scale(1.1); + } + + .confirm-dialog-content { + padding: 20px; + color: #e0e0e0; + } .confirm-dialog-content p { + margin: 0 0 15px 0; + font-size: 14px; + line-height: 1.5; + } + + .confirm-dialog-options { + display: flex; + flex-direction: column; + gap: 12px; + margin-top: 15px; + } + + .option-card { + background: linear-gradient(145deg, #2a2a2a, #1f1f1f); + border: 2px solid #404040; + border-radius: 8px; + padding: 16px; + cursor: pointer; + transition: all 0.3s ease; + position: relative; + overflow: hidden; + } + + .option-card::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent); + transition: left 0.5s ease; + } + + .option-card:hover { + border-color: #2a82e4; + background: linear-gradient(145deg, #333333, #2a2a2a); + transform: translateY(-2px); + box-shadow: 0 8px 16px rgba(42, 130, 228, 0.2); + } + + .option-card:hover::before { + left: 100%; + } + + .option-card:active { + transform: translateY(0); + box-shadow: 0 4px 8px rgba(42, 130, 228, 0.3); + } + + .option-title { + font-size: 14px; + font-weight: 600; + color: #ffffff; + margin-bottom: 6px; + } + + .option-description { + font-size: 12px; + color: #c0c0c0; + line-height: 1.4; + } + + .option-card[data-action="append"] .option-title { + color: #2ea043; + }.confirm-dialog-options { + display: flex; + flex-direction: column; + gap: 12px; + margin-top: 15px; + } + + .option-card { + background: linear-gradient(145deg, #2a2a2a, #1f1f1f); + border: 2px solid #404040; + border-radius: 8px; + padding: 16px; + cursor: pointer; + transition: all 0.3s ease; + position: relative; + overflow: hidden; + } + + .option-card::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent); + transition: left 0.5s ease; + } + + .option-card:hover { + border-color: #2a82e4; + background: linear-gradient(145deg, #333333, #2a2a2a); + transform: translateY(-2px); + box-shadow: 0 8px 16px rgba(42, 130, 228, 0.2); + } + + .option-card:hover::before { + left: 100%; + } + + .option-card:active { + transform: translateY(0); + box-shadow: 0 4px 8px rgba(42, 130, 228, 0.3); + } + + .option-title { + font-size: 14px; + font-weight: 600; + color: #ffffff; + margin-bottom: 6px; + } + + .option-description { + font-size: 12px; + color: #c0c0c0; + line-height: 1.4; + } + + .option-card[data-action="append"] .option-title { + color: #2ea043; + } .option-card[data-action="replace"] .option-title { + color: #f85149; + } + `; + document.head.appendChild(styles); + } + + addDeleteConfirmStyles() { + if (document.getElementById('delete-confirm-styles')) return; + + const styles = document.createElement('style'); + styles.id = 'delete-confirm-styles'; + styles.textContent = ` + .delete-confirm-modal { + display: block; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 10003; + } + + .warning-text { + color: #ff9999; + font-size: 12px; + margin-top: 10px; + margin-bottom: 0; + } + + .delete-confirm-actions { + display: flex; + gap: 12px; + justify-content: flex-end; + margin-top: 20px; + } + + .delete-confirm-btn { + background: linear-gradient(145deg, #dc3545, #c82333); + border: none; + color: white; + padding: 10px 20px; + border-radius: 6px; + cursor: pointer; + font-weight: 600; + font-size: 14px; + transition: all 0.2s ease; + } + + .delete-confirm-btn:hover { + background: linear-gradient(145deg, #e74c3c, #dc3545); + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(220, 53, 69, 0.4); + } + + .delete-cancel-btn { + background: linear-gradient(145deg, #6c757d, #5a6268); + border: none; + color: white; + padding: 10px 20px; + border-radius: 6px; + cursor: pointer; + font-weight: 600; + font-size: 14px; + transition: all 0.2s ease; + } + + .delete-cancel-btn:hover { + background: linear-gradient(145deg, #7c8590, #6c757d); + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(108, 117, 125, 0.4); + } + `; + document.head.appendChild(styles); + } +} + +// 全局提示词管理器 +const promptManager = new PromptManager(); + +function addPromptEmbedderButton(node) { + // 创建按钮widget + const embedButton = node.addWidget("button", "嵌入提示词", null, () => { + const ui = new PromptEmbedderUI(node, promptManager); + ui.show(); + }); + + // 设置按钮属性 + embedButton.serialize = false; + embedButton.options = { hideOnZoom: false }; + + // 自定义按钮绘制 + const originalComputeSize = embedButton.computeSize; + embedButton.computeSize = function(width) { + return [width, 35]; + }; + + // 设置按钮点击区域 + embedButton.mouse = function(event, pos, node) { + if (event.type === "pointerdown") { + const ui = new PromptEmbedderUI(node, promptManager); + ui.show(); + return true; + } + return false; + }; + + return embedButton; +} + +// 注册扩展 +app.registerExtension({ + name: "ComfyUI.PromptManager", + + async beforeRegisterNodeDef(nodeType, nodeData, app) { + // 检查是否是CLIP文本编码器节点 + if (CLIP_TEXT_ENCODERS.includes(nodeData.name)) { + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function() { + const result = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; + addPromptEmbedderButton(this); + return result; + }; + } + } +}); diff --git a/extensions/Prompt_Manager/web/PromptManagerStyles.css b/extensions/Prompt_Manager/web/PromptManagerStyles.css new file mode 100644 index 0000000..76cdc86 --- /dev/null +++ b/extensions/Prompt_Manager/web/PromptManagerStyles.css @@ -0,0 +1,18 @@ +/* ComfyUI Prompt Manager Extension Styles */ + +/* 确保在ComfyUI环境中的样式兼容性 */ +.prompt-embedder-modal * { + box-sizing: border-box; +} + +/* 节点按钮样式增强 */ +.comfy-multiline-input.prompt-embedder-enhanced { + border-bottom: 2px solid #2a82e4; +} + +/* 提示词按钮悬停效果 */ +.prompt-embedder-button:hover { + background: #1f6bb8 !important; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(42, 130, 228, 0.3); +} diff --git a/js/index.js b/extensions/index.js similarity index 50% rename from js/index.js rename to extensions/index.js index 11b3e7d..57b6728 100644 --- a/js/index.js +++ b/extensions/index.js @@ -1,3 +1,4 @@ // ComfyUI-DD-Nodes 前端插件入口 import "./Online Animation/ConnectionAnimationExt.js"; -import "./Intelligent Layout/LayoutExt.js"; \ No newline at end of file +import "./Intelligent Layout/LayoutExt.js"; +import "./Prompt_Manager/web/PromptManagerExt.js"; \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 49890b2..80fd24d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui-dd-nodes" description = "Advanced utility nodes for ComfyUI with intelligent layout and animation features." -version = "2.2.0" +version = "2.4.0" license = {file = "LICENSE"} dependencies = ["git+https://github.com/Dontdrunk/ComfyUI-DD-Nodes.git"]