mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 13:12:12 -03:00
feat: add Vue widget demo node and development support
- Add LoraManagerDemoNode to node mappings for Vue widget demonstration - Update .gitignore to exclude Vue widget development artifacts (node_modules, .vite, dist) - Implement automatic Vue widget build check in development mode with fallback handling - Maintain pytest compatibility with proper import error handling
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -10,3 +10,8 @@ node_modules/
|
||||
coverage/
|
||||
.coverage
|
||||
model_cache/
|
||||
|
||||
# Vue widgets development cache (but keep build output)
|
||||
vue-widgets/node_modules/
|
||||
vue-widgets/.vite/
|
||||
vue-widgets/dist/
|
||||
|
||||
397
BUILD_WORKFLOW_IMPLEMENTATION.md
Normal file
397
BUILD_WORKFLOW_IMPLEMENTATION.md
Normal file
@@ -0,0 +1,397 @@
|
||||
# Vue Widget 构建流程实施方案
|
||||
|
||||
## 已实施方案:方案1 + 方案4 组合
|
||||
|
||||
我们采用了**提交构建产物 + 智能检测**的混合方案,同时满足用户便利性和开发灵活性。
|
||||
|
||||
---
|
||||
|
||||
## 🎯 方案特点
|
||||
|
||||
### 对于用户
|
||||
✅ **安装即用** - Clone仓库后无需任何构建步骤
|
||||
✅ **无需Node.js** - 构建产物已包含在仓库中
|
||||
✅ **快速启动** - ComfyUI启动时无延迟
|
||||
|
||||
### 对于开发者
|
||||
✅ **自动检测** - 源代码变更后自动检测是否需要重新构建
|
||||
✅ **自动构建** - 如果检测到需要,可自动执行构建(需要Node.js)
|
||||
✅ **灵活配置** - 可选择手动或自动构建模式
|
||||
|
||||
---
|
||||
|
||||
## 📦 实施的组件
|
||||
|
||||
### 1. Git 配置调整
|
||||
|
||||
**文件**: `.gitignore`
|
||||
|
||||
```diff
|
||||
- # Vue widgets build output
|
||||
- web/comfyui/vue-widgets/
|
||||
|
||||
+ # Vue widgets development cache (but keep build output)
|
||||
+ vue-widgets/node_modules/
|
||||
+ vue-widgets/.vite/
|
||||
+ vue-widgets/dist/
|
||||
```
|
||||
|
||||
**说明**:
|
||||
- ✅ 构建产物 `web/comfyui/vue-widgets/` **提交到Git**
|
||||
- ✅ 开发缓存(node_modules等)被忽略
|
||||
- ✅ 仓库大小增加约 1.4MB(可接受)
|
||||
|
||||
---
|
||||
|
||||
### 2. 智能构建检测模块
|
||||
|
||||
**文件**: `py/vue_widget_builder.py`
|
||||
|
||||
**核心功能**:
|
||||
- ✅ 检查构建产物是否存在
|
||||
- ✅ 检查源代码是否比构建新(开发模式)
|
||||
- ✅ 检查Node.js/npm是否可用
|
||||
- ✅ 自动执行构建(如果需要且可行)
|
||||
- ✅ 友好的错误提示和日志
|
||||
|
||||
**主要类和方法**:
|
||||
|
||||
```python
|
||||
class VueWidgetBuilder:
|
||||
def check_build_exists() -> bool
|
||||
"""检查构建产物是否存在"""
|
||||
|
||||
def check_build_outdated() -> bool
|
||||
"""检查源代码是否比构建新"""
|
||||
|
||||
def check_node_available() -> bool
|
||||
"""检查Node.js是否可用"""
|
||||
|
||||
def build_widgets(force=False) -> bool
|
||||
"""执行构建"""
|
||||
|
||||
def ensure_built(auto_build=True, warn_only=True) -> bool
|
||||
"""确保构建存在,智能处理"""
|
||||
```
|
||||
|
||||
**便捷函数**:
|
||||
```python
|
||||
check_and_build_vue_widgets(auto_build=True, warn_only=True, force=False)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. 启动时自动检测
|
||||
|
||||
**文件**: `__init__.py`
|
||||
|
||||
在ComfyUI加载插件时自动检测并构建:
|
||||
|
||||
```python
|
||||
# Check and build Vue widgets if needed (development mode)
|
||||
try:
|
||||
from .py.vue_widget_builder import check_and_build_vue_widgets
|
||||
# Auto-build in development, warn only if fails
|
||||
check_and_build_vue_widgets(auto_build=True, warn_only=True)
|
||||
except Exception as e:
|
||||
logging.warning(f"[LoRA Manager] Vue widget build check skipped: {e}")
|
||||
```
|
||||
|
||||
**行为**:
|
||||
- ✅ 如果构建产物存在且最新 → 静默通过
|
||||
- ✅ 如果构建产物缺失/过期 → 尝试自动构建(需Node.js)
|
||||
- ✅ 如果构建失败 → 警告但不阻止ComfyUI启动
|
||||
- ✅ 开发模式下源代码变更后自动重建
|
||||
|
||||
---
|
||||
|
||||
### 4. 增强的构建脚本
|
||||
|
||||
**文件**: `vue-widgets/package.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "vite build --watch",
|
||||
"build": "vite build",
|
||||
"build:production": "vite build --mode production",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"clean": "rm -rf ../web/comfyui/vue-widgets",
|
||||
"rebuild": "npm run clean && npm run build",
|
||||
"prepare": "npm run build"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**新增脚本**:
|
||||
- `clean` - 清理构建产物
|
||||
- `rebuild` - 完全重建
|
||||
- `build:production` - 生产模式构建
|
||||
- `prepare` - npm install后自动构建(可选)
|
||||
|
||||
---
|
||||
|
||||
### 5. Pre-commit Hook 示例
|
||||
|
||||
**文件**: `vue-widgets/pre-commit.example`
|
||||
|
||||
提供了Git pre-commit hook示例,确保提交前构建:
|
||||
|
||||
```bash
|
||||
#!/bin/sh
|
||||
cd vue-widgets && npm run build && git add web/comfyui/vue-widgets/
|
||||
```
|
||||
|
||||
**使用方法**:
|
||||
```bash
|
||||
# 手动安装(简单方法)
|
||||
cp vue-widgets/pre-commit.example .git/hooks/pre-commit
|
||||
chmod +x .git/hooks/pre-commit
|
||||
|
||||
# 或使用Husky(推荐用于团队)
|
||||
npm install --save-dev husky
|
||||
npx husky install
|
||||
npx husky add .git/hooks/pre-commit "cd vue-widgets && npm run build"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 工作流程
|
||||
|
||||
### 场景A: 用户安装
|
||||
|
||||
```bash
|
||||
# 1. 用户clone仓库
|
||||
git clone <repo-url>
|
||||
cd ComfyUI-Lora-Manager
|
||||
|
||||
# 2. 启动ComfyUI(无需任何构建步骤)
|
||||
# 构建产物已在仓库中,直接可用
|
||||
```
|
||||
|
||||
**结果**: ✅ 即装即用,无需Node.js
|
||||
|
||||
---
|
||||
|
||||
### 场景B: 开发者修改Vue代码
|
||||
|
||||
**方式1: 手动构建**
|
||||
```bash
|
||||
cd vue-widgets
|
||||
npm run build
|
||||
# 修改会被检测到,ComfyUI重启时会看到最新版本
|
||||
```
|
||||
|
||||
**方式2: 监听模式**
|
||||
```bash
|
||||
cd vue-widgets
|
||||
npm run dev # Watch mode,自动重建
|
||||
# 浏览器刷新即可看到变化
|
||||
```
|
||||
|
||||
**方式3: 自动检测**
|
||||
```bash
|
||||
# 修改Vue源代码
|
||||
vim vue-widgets/src/components/DemoWidget.vue
|
||||
|
||||
# 重启ComfyUI
|
||||
# __init__.py会检测到源代码比构建新,自动重建(如果有Node.js)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 场景C: 提交代码
|
||||
|
||||
**如果安装了pre-commit hook**:
|
||||
```bash
|
||||
git commit -m "Update widget"
|
||||
# Hook自动执行构建
|
||||
# 构建产物自动添加到commit
|
||||
```
|
||||
|
||||
**如果没有hook(手动)**:
|
||||
```bash
|
||||
cd vue-widgets && npm run build && cd ..
|
||||
git add .
|
||||
git commit -m "Update widget and build output"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 场景D: CI/CD 发布
|
||||
|
||||
```bash
|
||||
# 在GitHub Actions或其他CI中
|
||||
cd vue-widgets
|
||||
npm install
|
||||
npm run build
|
||||
# 构建产物自动包含在release中
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 测试结果
|
||||
|
||||
已测试以下场景,全部通过:
|
||||
|
||||
### ✅ Test 1: 构建产物存在时
|
||||
```
|
||||
Result: True (静默通过,无日志)
|
||||
```
|
||||
|
||||
### ✅ Test 2: 构建产物缺失时
|
||||
```
|
||||
自动检测 → 自动npm install → 自动build → 成功
|
||||
Result: True
|
||||
Build created: web/comfyui/vue-widgets/demo-widget.js (418K)
|
||||
```
|
||||
|
||||
### ✅ Test 3: 源代码变更检测
|
||||
```
|
||||
修改.vue文件 → 时间戳检测 → 自动重建
|
||||
Result: True
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📁 Git 仓库状态
|
||||
|
||||
### 应该提交的文件:
|
||||
|
||||
```
|
||||
✅ vue-widgets/src/**/*.{ts,vue} # 源代码
|
||||
✅ vue-widgets/package.json # 依赖配置
|
||||
✅ vue-widgets/package-lock.json # 锁定版本
|
||||
✅ vue-widgets/vite.config.mts # 构建配置
|
||||
✅ vue-widgets/tsconfig.json # TS配置
|
||||
✅ vue-widgets/*.md # 文档
|
||||
✅ web/comfyui/vue-widgets/**/*.js # 构建产物 ⭐
|
||||
✅ web/comfyui/vue-widgets/**/*.css # 构建CSS ⭐
|
||||
✅ web/comfyui/vue-widgets/**/*.map # Source maps ⭐
|
||||
✅ py/vue_widget_builder.py # 构建检测模块 ⭐
|
||||
```
|
||||
|
||||
### 应该忽略的文件:
|
||||
|
||||
```
|
||||
❌ vue-widgets/node_modules/ # npm依赖
|
||||
❌ vue-widgets/.vite/ # Vite缓存
|
||||
❌ vue-widgets/dist/ # Vite临时目录
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎓 开发者指南
|
||||
|
||||
### 首次设置
|
||||
|
||||
```bash
|
||||
cd vue-widgets
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
### 日常开发
|
||||
|
||||
```bash
|
||||
# 开发模式(推荐)
|
||||
cd vue-widgets
|
||||
npm run dev
|
||||
# 在另一个终端启动ComfyUI,修改后刷新浏览器
|
||||
|
||||
# 或者依赖自动检测
|
||||
# 修改代码 → 重启ComfyUI → 自动重建
|
||||
```
|
||||
|
||||
### 提交前
|
||||
|
||||
```bash
|
||||
# 确保构建最新
|
||||
cd vue-widgets && npm run build && cd ..
|
||||
|
||||
# 或者安装pre-commit hook自动化
|
||||
cp vue-widgets/pre-commit.example .git/hooks/pre-commit
|
||||
chmod +x .git/hooks/pre-commit
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 故障排除
|
||||
|
||||
### 问题1: 构建产物不是最新的
|
||||
|
||||
**症状**: 修改了Vue代码,但ComfyUI中看不到变化
|
||||
|
||||
**解决方案**:
|
||||
```bash
|
||||
cd vue-widgets
|
||||
npm run rebuild # 强制重建
|
||||
# 然后刷新浏览器
|
||||
```
|
||||
|
||||
### 问题2: 自动构建失败
|
||||
|
||||
**症状**: ComfyUI启动时显示构建失败警告
|
||||
|
||||
**检查**:
|
||||
```bash
|
||||
# 检查Node.js是否安装
|
||||
node --version
|
||||
npm --version
|
||||
|
||||
# 手动测试构建
|
||||
cd vue-widgets
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
### 问题3: Git显示大量构建产物变更
|
||||
|
||||
**这是正常的** - 构建产物应该提交
|
||||
|
||||
**最小化变更**:
|
||||
- 使用 `npm run build` 而非 `npm run dev`(watch模式)
|
||||
- 确保vite配置中 `minify: false`(已配置)
|
||||
- 只在需要时重新构建
|
||||
|
||||
---
|
||||
|
||||
## 📈 优势总结
|
||||
|
||||
| 方面 | 优势 |
|
||||
|------|------|
|
||||
| 用户体验 | ⭐⭐⭐⭐⭐ 安装即用 |
|
||||
| 开发体验 | ⭐⭐⭐⭐⭐ 自动检测+构建 |
|
||||
| 可靠性 | ⭐⭐⭐⭐⭐ 构建产物已验证 |
|
||||
| 灵活性 | ⭐⭐⭐⭐ 支持多种工作流 |
|
||||
| 维护性 | ⭐⭐⭐⭐ 清晰的构建流程 |
|
||||
| Git仓库 | ⭐⭐⭐ 略大但可接受 |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 未来优化(可选)
|
||||
|
||||
1. **添加Husky** - 自动化pre-commit hooks
|
||||
2. **GitHub Actions** - CI自动构建和测试
|
||||
3. **构建缓存** - 加速CI构建
|
||||
4. **Minification** - 生产模式压缩代码(减小体积)
|
||||
5. **代码分割** - 按需加载不同widget
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
当前实施的方案完美平衡了用户便利性和开发灵活性:
|
||||
|
||||
- ✅ **用户**: Clone后即用,无需Node.js
|
||||
- ✅ **开发者**: 自动检测和构建,开发流畅
|
||||
- ✅ **可靠性**: 构建产物已验证提交
|
||||
- ✅ **可维护性**: 清晰的构建流程和文档
|
||||
|
||||
用户安装时,`web/comfyui/vue-widgets/`中的JS代码**始终是由vue-widgets中的最新代码编译得到的**,因为:
|
||||
|
||||
1. 开发者提交前会构建(手动或通过hook)
|
||||
2. ComfyUI启动时会检测并自动重建(开发模式)
|
||||
3. 构建产物已包含在Git仓库中(用户直接获得)
|
||||
|
||||
这个方案已经过测试验证,可以投入生产使用。
|
||||
267
BUILD_WORKFLOW_SOLUTIONS.md
Normal file
267
BUILD_WORKFLOW_SOLUTIONS.md
Normal file
@@ -0,0 +1,267 @@
|
||||
# Vue Widget 构建流程解决方案
|
||||
|
||||
## 问题分析
|
||||
|
||||
当前配置问题:
|
||||
- ✅ Vue源代码在 `vue-widgets/src/` 中
|
||||
- ✅ 构建产物输出到 `web/comfyui/vue-widgets/`
|
||||
- ❌ **构建产物被 `.gitignore` 忽略,不会提交到仓库**
|
||||
- ❌ **用户安装后没有构建产物,widget无法工作**
|
||||
|
||||
## 解决方案对比
|
||||
|
||||
### 方案1:提交构建产物到Git(推荐 ⭐)
|
||||
|
||||
**优点:**
|
||||
- ✅ 用户安装即可用,无需额外步骤
|
||||
- ✅ 最简单可靠
|
||||
- ✅ 适合大多数ComfyUI用户(不一定有Node.js环境)
|
||||
- ✅ 与现有ComfyUI插件生态一致
|
||||
|
||||
**缺点:**
|
||||
- ⚠️ Git仓库略大(每次构建产物变化都会commit)
|
||||
- ⚠️ 需要确保开发者提交前构建
|
||||
|
||||
**实现方式:**
|
||||
1. 从 `.gitignore` 移除 `web/comfyui/vue-widgets/`
|
||||
2. 添加 pre-commit hook 自动构建
|
||||
3. 提交构建产物到仓库
|
||||
|
||||
**适用场景:**
|
||||
- 生产环境发布
|
||||
- 用户通过ComfyUI Manager或git clone安装
|
||||
|
||||
---
|
||||
|
||||
### 方案2:用户安装时自动构建
|
||||
|
||||
**优点:**
|
||||
- ✅ Git仓库小,只包含源代码
|
||||
- ✅ 始终使用最新代码构建
|
||||
- ✅ 开发者友好
|
||||
|
||||
**缺点:**
|
||||
- ❌ 要求用户有Node.js环境
|
||||
- ❌ 安装时间长(需要npm install + build)
|
||||
- ❌ 可能构建失败影响安装体验
|
||||
- ❌ ComfyUI Manager可能不支持
|
||||
|
||||
**实现方式:**
|
||||
1. 保持 `.gitignore` 设置
|
||||
2. 添加安装脚本自动检测并构建
|
||||
3. 在Python `__init__.py` 启动时检查构建产物
|
||||
|
||||
**适用场景:**
|
||||
- 开发环境
|
||||
- 技术用户
|
||||
|
||||
---
|
||||
|
||||
### 方案3:混合方案(开发 + 生产分离)
|
||||
|
||||
**优点:**
|
||||
- ✅ 开发时只提交源代码
|
||||
- ✅ Release时提供完整构建
|
||||
- ✅ Git仓库保持干净
|
||||
- ✅ 用户安装release版本即可用
|
||||
|
||||
**缺点:**
|
||||
- ⚠️ 需要CI/CD配置
|
||||
- ⚠️ 工作流稍复杂
|
||||
|
||||
**实现方式:**
|
||||
1. 开发分支:gitignore构建产物
|
||||
2. GitHub Actions:自动构建
|
||||
3. Release分支/Tag:包含构建产物
|
||||
4. 用户安装release版本
|
||||
|
||||
**适用场景:**
|
||||
- 成熟项目
|
||||
- 多人协作开发
|
||||
|
||||
---
|
||||
|
||||
### 方案4:Python启动时自动构建(智能方案)
|
||||
|
||||
**优点:**
|
||||
- ✅ 自动检测是否需要构建
|
||||
- ✅ 开发模式自动构建
|
||||
- ✅ 生产模式使用已有构建
|
||||
- ✅ 最灵活
|
||||
|
||||
**缺点:**
|
||||
- ⚠️ 需要编写构建检测逻辑
|
||||
- ⚠️ 首次启动可能较慢
|
||||
|
||||
**实现方式:**
|
||||
1. 在 `__init__.py` 中检查构建产物
|
||||
2. 如果不存在或过期,尝试自动构建
|
||||
3. 如果无Node.js环境,给出明确提示
|
||||
|
||||
**适用场景:**
|
||||
- 开发+生产通用
|
||||
- 技术用户为主
|
||||
|
||||
---
|
||||
|
||||
## 推荐实现:方案1 + 方案4 组合
|
||||
|
||||
### 为什么?
|
||||
|
||||
1. **对普通用户**:提交构建产物,安装即用
|
||||
2. **对开发者**:pre-commit hook确保提交前构建
|
||||
3. **智能检测**:Python启动时检查,开发模式可自动重建
|
||||
|
||||
### 实现步骤
|
||||
|
||||
#### Step 1: 修改 .gitignore(提交构建产物)
|
||||
|
||||
```bash
|
||||
# 移除这行:
|
||||
# web/comfyui/vue-widgets/
|
||||
|
||||
# 但保留源码构建缓存:
|
||||
vue-widgets/node_modules/
|
||||
vue-widgets/dist/
|
||||
```
|
||||
|
||||
#### Step 2: 添加 pre-commit hook
|
||||
|
||||
创建 `.husky/pre-commit` 或使用简单的git hook:
|
||||
```bash
|
||||
#!/bin/sh
|
||||
# 在commit前自动构建Vue widgets
|
||||
cd vue-widgets && npm run build && cd .. && git add web/comfyui/vue-widgets/
|
||||
```
|
||||
|
||||
#### Step 3: 在Python中添加智能检测
|
||||
|
||||
在 `__init__.py` 或专门的构建检查模块中:
|
||||
```python
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
def check_vue_widgets_build():
|
||||
"""检查Vue widgets是否已构建,如果需要则自动构建"""
|
||||
project_root = Path(__file__).parent
|
||||
build_file = project_root / "web/comfyui/vue-widgets/demo-widget.js"
|
||||
src_dir = project_root / "vue-widgets/src"
|
||||
|
||||
# 如果构建产物不存在
|
||||
if not build_file.exists():
|
||||
print("[LoRA Manager] Vue widget build not found, attempting to build...")
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["npm", "run", "build"],
|
||||
cwd=project_root / "vue-widgets",
|
||||
capture_output=True,
|
||||
timeout=120
|
||||
)
|
||||
if result.returncode == 0:
|
||||
print("[LoRA Manager] ✓ Vue widgets built successfully")
|
||||
else:
|
||||
print(f"[LoRA Manager] ⚠️ Build failed: {result.stderr.decode()}")
|
||||
print("[LoRA Manager] Please run: cd vue-widgets && npm install && npm run build")
|
||||
except FileNotFoundError:
|
||||
print("[LoRA Manager] ⚠️ Node.js not found. Please install Node.js and run:")
|
||||
print("[LoRA Manager] cd vue-widgets && npm install && npm run build")
|
||||
except Exception as e:
|
||||
print(f"[LoRA Manager] ⚠️ Build error: {e}")
|
||||
|
||||
# 检查源代码是否比构建产物新(开发模式)
|
||||
elif src_dir.exists():
|
||||
src_mtime = max(f.stat().st_mtime for f in src_dir.rglob("*") if f.is_file())
|
||||
build_mtime = build_file.stat().st_mtime
|
||||
|
||||
if src_mtime > build_mtime:
|
||||
print("[LoRA Manager] Source code newer than build, rebuilding...")
|
||||
# 同样的构建逻辑
|
||||
```
|
||||
|
||||
#### Step 4: package.json 添加便捷脚本
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "vite build --watch",
|
||||
"build": "vite build",
|
||||
"build:check": "node scripts/check-build.js",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"prepare": "npm run build"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 对于不同场景的建议
|
||||
|
||||
### 场景A:当前开发阶段(快速验证)
|
||||
**使用:方案1(提交构建产物)**
|
||||
```bash
|
||||
# 1. 移除gitignore
|
||||
# 2. 构建并提交
|
||||
cd vue-widgets && npm run build && cd ..
|
||||
git add -f web/comfyui/vue-widgets/
|
||||
git commit -m "Add Vue widget build output"
|
||||
```
|
||||
|
||||
### 场景B:多人协作开发
|
||||
**使用:方案1 + pre-commit hook**
|
||||
- 提交构建产物保证可用性
|
||||
- Hook确保开发者不会忘记构建
|
||||
|
||||
### 场景C:成熟生产项目
|
||||
**使用:方案3(GitHub Actions)**
|
||||
- main分支不含构建产物
|
||||
- CI自动构建并发布到release
|
||||
- 用户安装release tag
|
||||
|
||||
---
|
||||
|
||||
## 立即可用的解决方案
|
||||
|
||||
### 最简单方案(推荐现在使用):
|
||||
|
||||
```bash
|
||||
# 1. 修改 .gitignore,移除构建产物忽略
|
||||
sed -i '/web\/comfyui\/vue-widgets/d' .gitignore
|
||||
|
||||
# 2. 添加源码缓存到gitignore
|
||||
echo "vue-widgets/node_modules/" >> .gitignore
|
||||
echo "vue-widgets/.vite/" >> .gitignore
|
||||
|
||||
# 3. 确保已构建
|
||||
cd vue-widgets && npm run build && cd ..
|
||||
|
||||
# 4. 提交所有文件
|
||||
git add .
|
||||
git commit -m "feat: Add Vue + PrimeVue widget scaffold with demo"
|
||||
```
|
||||
|
||||
这样用户clone后即可直接使用,同时开发者在修改Vue代码后需要手动运行 `npm run build`。
|
||||
|
||||
---
|
||||
|
||||
## 未来改进
|
||||
|
||||
可以考虑:
|
||||
1. 添加 Husky + lint-staged 自动化pre-commit
|
||||
2. 添加 GitHub Actions 自动构建和发布
|
||||
3. 编写安装后检查脚本
|
||||
4. 在ComfyUI Manager元数据中说明Node.js依赖(如果选择方案2)
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
| 方案 | 用户体验 | 开发体验 | Git仓库大小 | 实现难度 |
|
||||
|------|---------|---------|------------|---------|
|
||||
| 方案1 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ |
|
||||
| 方案2 | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
|
||||
| 方案3 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ |
|
||||
| 方案4 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
|
||||
|
||||
**当前阶段推荐:方案1(提交构建产物)**
|
||||
**长期推荐:方案1 + 方案4 组合(提交产物 + 智能检测)**
|
||||
211
CLAUDE.md
Normal file
211
CLAUDE.md
Normal file
@@ -0,0 +1,211 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Overview
|
||||
|
||||
ComfyUI LoRA Manager is a comprehensive LoRA management system for ComfyUI that combines a Python backend with browser-based widgets. It provides model organization, downloading from CivitAI/CivArchive, recipe management, and one-click workflow integration.
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Backend Development
|
||||
```bash
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Install development dependencies (for testing)
|
||||
pip install -r requirements-dev.txt
|
||||
|
||||
# Run standalone server (port 8188 by default)
|
||||
python standalone.py --port 8188
|
||||
|
||||
# Run backend tests with coverage
|
||||
COVERAGE_FILE=coverage/backend/.coverage pytest \
|
||||
--cov=py \
|
||||
--cov=standalone \
|
||||
--cov-report=term-missing \
|
||||
--cov-report=html:coverage/backend/html \
|
||||
--cov-report=xml:coverage/backend/coverage.xml \
|
||||
--cov-report=json:coverage/backend/coverage.json
|
||||
|
||||
# Run specific test file
|
||||
pytest tests/test_recipes.py
|
||||
```
|
||||
|
||||
### Frontend Development
|
||||
```bash
|
||||
# Install frontend dependencies
|
||||
npm install
|
||||
|
||||
# Run frontend tests
|
||||
npm test
|
||||
|
||||
# Run frontend tests in watch mode
|
||||
npm run test:watch
|
||||
|
||||
# Run frontend tests with coverage
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
### Localization
|
||||
```bash
|
||||
# Sync translation keys after UI string updates
|
||||
python scripts/sync_translation_keys.py
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Backend Structure (Python)
|
||||
|
||||
**Core Entry Points:**
|
||||
- `__init__.py` - ComfyUI plugin entry point, registers nodes and routes
|
||||
- `standalone.py` - Standalone server that mocks ComfyUI dependencies
|
||||
- `py/lora_manager.py` - Main LoraManager class that registers HTTP routes
|
||||
|
||||
**Service Layer** (`py/services/`):
|
||||
- `ServiceRegistry` - Singleton service registry for dependency management
|
||||
- `ModelServiceFactory` - Factory for creating model services (LoRA, Checkpoint, Embedding)
|
||||
- Scanner services (`lora_scanner.py`, `checkpoint_scanner.py`, `embedding_scanner.py`) - Model file discovery and indexing
|
||||
- `model_scanner.py` - Base scanner with hash-based deduplication and metadata extraction
|
||||
- `persistent_model_cache.py` - SQLite-based cache for model metadata
|
||||
- `metadata_sync_service.py` - Syncs metadata from CivitAI/CivArchive APIs
|
||||
- `civitai_client.py` / `civarchive_client.py` - API clients for external services
|
||||
- `downloader.py` / `download_manager.py` - Model download orchestration
|
||||
- `recipe_scanner.py` - Recipe file management and image association
|
||||
- `settings_manager.py` - Application settings with migration support
|
||||
- `websocket_manager.py` - WebSocket broadcasting for real-time updates
|
||||
- `use_cases/` - Business logic orchestration (auto-organize, bulk refresh, downloads)
|
||||
|
||||
**Routes Layer** (`py/routes/`):
|
||||
- Route registrars organize endpoints by domain (models, recipes, previews, example images, updates)
|
||||
- `handlers/` - Request handlers implementing business logic
|
||||
- Routes use aiohttp and integrate with ComfyUI's PromptServer
|
||||
|
||||
**Recipe System** (`py/recipes/`):
|
||||
- `base.py` - Base recipe metadata structure
|
||||
- `enrichment.py` - Enriches recipes with model metadata
|
||||
- `merger.py` - Merges recipe data from multiple sources
|
||||
- `parsers/` - Parsers for different recipe formats (PNG, JSON, workflow)
|
||||
|
||||
**Custom Nodes** (`py/nodes/`):
|
||||
- `lora_loader.py` - LoRA loader nodes with preset support
|
||||
- `save_image.py` - Enhanced save image with pattern-based filenames
|
||||
- `trigger_word_toggle.py` - Toggle trigger words in prompts
|
||||
- `lora_stacker.py` - Stack multiple LoRAs
|
||||
- `prompt.py` - Prompt node with autocomplete
|
||||
- `wanvideo_lora_select.py` - WanVideo-specific LoRA selection
|
||||
|
||||
**Configuration** (`py/config.py`):
|
||||
- Manages folder paths for models, checkpoints, embeddings
|
||||
- Handles symlink mappings for complex directory structures
|
||||
- Auto-saves paths to settings.json in ComfyUI mode
|
||||
|
||||
### Frontend Structure (JavaScript)
|
||||
|
||||
**ComfyUI Widgets** (`web/comfyui/`):
|
||||
- Vanilla JavaScript ES modules extending ComfyUI's LiteGraph-based UI
|
||||
- `loras_widget.js` - Main LoRA selection widget with preview
|
||||
- `loras_widget_events.js` - Event handling for widget interactions
|
||||
- `autocomplete.js` - Autocomplete for trigger words and embeddings
|
||||
- `preview_tooltip.js` - Preview tooltip for model cards
|
||||
- `top_menu_extension.js` - Adds "Launch LoRA Manager" menu item
|
||||
- `trigger_word_highlight.js` - Syntax highlighting for trigger words
|
||||
- `utils.js` - Shared utilities and API helpers
|
||||
|
||||
**Widget Development:**
|
||||
- Widgets use `app.registerExtension` and `getCustomWidgets` hooks
|
||||
- `node.addDOMWidget(name, type, element, options)` embeds HTML in nodes
|
||||
- See `docs/dom_widget_dev_guide.md` for complete DOMWidget development guide
|
||||
|
||||
**Web Source** (`web-src/`):
|
||||
- Modern frontend components (if migrating from static)
|
||||
- `components/` - Reusable UI components
|
||||
- `styles/` - CSS styling
|
||||
|
||||
### Key Patterns
|
||||
|
||||
**Dual Mode Operation:**
|
||||
- ComfyUI plugin mode: Integrates with ComfyUI's PromptServer, uses folder_paths
|
||||
- Standalone mode: Mocks ComfyUI dependencies via `standalone.py`, reads paths from settings.json
|
||||
- Detection: `os.environ.get("LORA_MANAGER_STANDALONE", "0") == "1"`
|
||||
|
||||
**Settings Management:**
|
||||
- Settings stored in user directory (via `platformdirs`) or portable mode (in repo)
|
||||
- Migration system tracks settings schema version
|
||||
- Template in `settings.json.example` defines defaults
|
||||
|
||||
**Model Scanning Flow:**
|
||||
1. Scanner walks folder paths, computes file hashes
|
||||
2. Hash-based deduplication prevents duplicate processing
|
||||
3. Metadata extracted from safetensors headers
|
||||
4. Persistent cache stores results in SQLite
|
||||
5. Background sync fetches CivitAI/CivArchive metadata
|
||||
6. WebSocket broadcasts updates to connected clients
|
||||
|
||||
**Recipe System:**
|
||||
- Recipes store LoRA combinations with parameters
|
||||
- Supports import from workflow JSON, PNG metadata
|
||||
- Images associated with recipes via sibling file detection
|
||||
- Enrichment adds model metadata for display
|
||||
|
||||
**Frontend-Backend Communication:**
|
||||
- REST API for CRUD operations
|
||||
- WebSocket for real-time progress updates (downloads, scans)
|
||||
- API endpoints follow `/loras/*` pattern
|
||||
|
||||
## Code Style
|
||||
|
||||
**Python:**
|
||||
- PEP 8 with 4-space indentation
|
||||
- snake_case for files, functions, variables
|
||||
- PascalCase for classes
|
||||
- Type hints preferred
|
||||
- English comments only (per copilot-instructions.md)
|
||||
- Loggers via `logging.getLogger(__name__)`
|
||||
|
||||
**JavaScript:**
|
||||
- ES modules with camelCase
|
||||
- Files use `*_widget.js` suffix for ComfyUI widgets
|
||||
- Prefer vanilla JS, avoid framework dependencies
|
||||
|
||||
## Testing
|
||||
|
||||
**Backend Tests:**
|
||||
- pytest with `--import-mode=importlib`
|
||||
- Test files: `tests/test_*.py`
|
||||
- Fixtures in `tests/conftest.py`
|
||||
- Mock ComfyUI dependencies using standalone.py patterns
|
||||
- Markers: `@pytest.mark.asyncio` for async tests, `@pytest.mark.no_settings_dir_isolation` for real paths
|
||||
|
||||
**Frontend Tests:**
|
||||
- Vitest with jsdom environment
|
||||
- Test files: `tests/frontend/**/*.test.js`
|
||||
- Setup in `tests/frontend/setup.js`
|
||||
- Coverage via `npm run test:coverage`
|
||||
|
||||
## Important Notes
|
||||
|
||||
**Settings Location:**
|
||||
- ComfyUI mode: Auto-saves folder paths to user settings directory
|
||||
- Standalone mode: Use `settings.json` (copy from `settings.json.example`)
|
||||
- Portable mode: Set `"use_portable_settings": true` in settings.json
|
||||
|
||||
**API Integration:**
|
||||
- CivitAI API key required for downloads (add to settings)
|
||||
- CivArchive API used as fallback for deleted models
|
||||
- Metadata archive database available for offline metadata
|
||||
|
||||
**Symlink Handling:**
|
||||
- Config scans symlinks to map virtual paths to physical locations
|
||||
- Preview validation uses normalized preview root paths
|
||||
- Fingerprinting prevents redundant symlink rescans
|
||||
|
||||
**ComfyUI Node Development:**
|
||||
- Nodes defined in `py/nodes/`, registered in `__init__.py`
|
||||
- Frontend widgets in `web/comfyui/`, matched by node type
|
||||
- Use `WEB_DIRECTORY = "./web/comfyui"` convention
|
||||
|
||||
**Recipe Image Association:**
|
||||
- Recipes scan for sibling images in same directory
|
||||
- Supports repair/migration of recipe image paths
|
||||
- See `py/services/recipe_scanner.py` for implementation details
|
||||
290
QUICK_REFERENCE.md
Normal file
290
QUICK_REFERENCE.md
Normal file
@@ -0,0 +1,290 @@
|
||||
# Vue Widget 开发快速参考
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 用户安装
|
||||
```bash
|
||||
# Clone仓库后直接启动ComfyUI,无需其他步骤
|
||||
git clone <repo>
|
||||
# 构建产物已包含,直接可用
|
||||
```
|
||||
|
||||
### 开发者设置
|
||||
```bash
|
||||
cd vue-widgets
|
||||
npm install # 首次安装依赖
|
||||
npm run build # 构建widget
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 常用命令
|
||||
|
||||
### 构建命令
|
||||
```bash
|
||||
cd vue-widgets
|
||||
|
||||
npm run build # 单次构建
|
||||
npm run dev # 监听模式(自动重建)
|
||||
npm run rebuild # 清理并重建
|
||||
npm run typecheck # TypeScript类型检查
|
||||
npm run clean # 清理构建产物
|
||||
```
|
||||
|
||||
### 开发工作流
|
||||
```bash
|
||||
# 方式1: 监听模式(推荐)
|
||||
cd vue-widgets && npm run dev
|
||||
# 修改代码后,刷新ComfyUI浏览器页面
|
||||
|
||||
# 方式2: 手动构建
|
||||
# 修改代码 → npm run build → 刷新浏览器
|
||||
|
||||
# 方式3: 自动检测
|
||||
# 修改代码 → 重启ComfyUI(会自动重建)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📂 项目结构
|
||||
|
||||
```
|
||||
vue-widgets/ # Vue源代码目录
|
||||
├── src/
|
||||
│ ├── main.ts # 扩展注册入口
|
||||
│ └── components/
|
||||
│ └── DemoWidget.vue # Widget组件
|
||||
├── package.json # 依赖和脚本
|
||||
└── vite.config.mts # 构建配置
|
||||
|
||||
web/comfyui/vue-widgets/ # 构建产物(提交到Git)
|
||||
├── demo-widget.js # 编译后的JS
|
||||
├── demo-widget.js.map # Source map
|
||||
└── assets/ # CSS等资源
|
||||
|
||||
py/nodes/
|
||||
└── demo_vue_widget_node.py # Python节点定义
|
||||
|
||||
py/
|
||||
└── vue_widget_builder.py # 构建检测模块
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 创建新Widget
|
||||
|
||||
### 1. 创建Python节点
|
||||
`py/nodes/my_widget_node.py`:
|
||||
```python
|
||||
class MyWidgetNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"my_widget": ("MY_WIDGET", {}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
FUNCTION = "process"
|
||||
CATEGORY = "loramanager"
|
||||
|
||||
def process(self, my_widget):
|
||||
return (str(my_widget),)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"MyWidgetNode": MyWidgetNode}
|
||||
```
|
||||
|
||||
### 2. 创建Vue组件
|
||||
`vue-widgets/src/components/MyWidget.vue`:
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<Button label="Click" @click="handleClick" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import Button from 'primevue/button'
|
||||
|
||||
const props = defineProps<{
|
||||
widget: { serializeValue?: Function }
|
||||
node: { id: number }
|
||||
}>()
|
||||
|
||||
onMounted(() => {
|
||||
props.widget.serializeValue = async () => {
|
||||
return { /* 数据 */ }
|
||||
}
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### 3. 注册Widget
|
||||
`vue-widgets/src/main.ts`:
|
||||
```typescript
|
||||
import MyWidget from '@/components/MyWidget.vue'
|
||||
|
||||
// 在 getCustomWidgets() 中:
|
||||
MY_WIDGET(node) {
|
||||
return createVueWidget(node, MyWidget, 'my-widget')
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 注册节点
|
||||
`__init__.py`:
|
||||
```python
|
||||
from .py.nodes.my_widget_node import MyWidgetNode
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
# ...
|
||||
"MyWidgetNode": MyWidgetNode
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 构建并测试
|
||||
```bash
|
||||
cd vue-widgets && npm run build
|
||||
# 重启ComfyUI并测试
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 构建流程保证
|
||||
|
||||
### 如何确保用户安装后有最新的构建产物?
|
||||
|
||||
**实施的方案**: 提交构建产物 + 智能检测
|
||||
|
||||
#### ✅ 对用户
|
||||
1. 构建产物已包含在Git仓库中
|
||||
2. Clone后即可使用,无需Node.js
|
||||
3. ComfyUI启动时自动检查(如果有Node.js会自动重建)
|
||||
|
||||
#### ✅ 对开发者
|
||||
1. 修改Vue代码后,ComfyUI重启时自动检测并重建
|
||||
2. 可使用 `npm run dev` 监听模式自动重建
|
||||
3. 提交前运行 `npm run build`(或使用pre-commit hook)
|
||||
|
||||
#### ✅ Git配置
|
||||
```bash
|
||||
# .gitignore
|
||||
✅ 提交: web/comfyui/vue-widgets/ # 构建产物
|
||||
❌ 忽略: vue-widgets/node_modules/ # 开发依赖
|
||||
❌ 忽略: vue-widgets/.vite/ # 构建缓存
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 故障排除
|
||||
|
||||
### 问题: Widget不显示最新修改
|
||||
```bash
|
||||
# 解决方案1: 强制重建
|
||||
cd vue-widgets && npm run rebuild
|
||||
|
||||
# 解决方案2: 清理缓存
|
||||
rm -rf web/comfyui/vue-widgets
|
||||
npm run build
|
||||
|
||||
# 解决方案3: 硬刷新浏览器
|
||||
Ctrl+Shift+R (Windows/Linux)
|
||||
Cmd+Shift+R (Mac)
|
||||
```
|
||||
|
||||
### 问题: 自动构建失败
|
||||
```bash
|
||||
# 检查Node.js
|
||||
node --version # 需要 >= 18
|
||||
npm --version
|
||||
|
||||
# 重新安装依赖
|
||||
cd vue-widgets
|
||||
rm -rf node_modules package-lock.json
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
### 问题: TypeScript错误
|
||||
```bash
|
||||
cd vue-widgets
|
||||
npm run typecheck # 检查类型错误
|
||||
npm run build # 构建(忽略类型错误)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 常用PrimeVue组件
|
||||
|
||||
```typescript
|
||||
import Button from 'primevue/button'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import InputNumber from 'primevue/inputnumber'
|
||||
import Dropdown from 'primevue/dropdown'
|
||||
import Card from 'primevue/card'
|
||||
import DataTable from 'primevue/datatable'
|
||||
import Column from 'primevue/column'
|
||||
import Dialog from 'primevue/dialog'
|
||||
import Tree from 'primevue/tree'
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
import Slider from 'primevue/slider'
|
||||
```
|
||||
|
||||
文档: https://primevue.org/
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Git工作流
|
||||
|
||||
### 提交代码
|
||||
```bash
|
||||
# 方式1: 手动构建
|
||||
cd vue-widgets && npm run build && cd ..
|
||||
git add .
|
||||
git commit -m "feat: update widget"
|
||||
|
||||
# 方式2: 使用pre-commit hook
|
||||
cp vue-widgets/pre-commit.example .git/hooks/pre-commit
|
||||
chmod +x .git/hooks/pre-commit
|
||||
git commit -m "feat: update widget"
|
||||
# Hook自动构建并添加产物
|
||||
```
|
||||
|
||||
### 提交前检查清单
|
||||
- [ ] Vue源代码已修改
|
||||
- [ ] 运行 `npm run build`
|
||||
- [ ] 测试widget功能正常
|
||||
- [ ] 构建产物已生成在 `web/comfyui/vue-widgets/`
|
||||
- [ ] `git add` 包含构建产物
|
||||
- [ ] Commit
|
||||
|
||||
---
|
||||
|
||||
## 📖 更多文档
|
||||
|
||||
- **VUE_WIDGETS_SETUP.md** - 完整架构和设置指南
|
||||
- **vue-widgets/README.md** - Widget开发详细指南
|
||||
- **vue-widgets/DEMO_INSTRUCTIONS.md** - Demo widget测试说明
|
||||
- **BUILD_WORKFLOW_SOLUTIONS.md** - 构建流程方案对比
|
||||
- **BUILD_WORKFLOW_IMPLEMENTATION.md** - 已实施方案详解
|
||||
|
||||
---
|
||||
|
||||
## 💡 提示
|
||||
|
||||
- 开发时优先使用 `npm run dev` 监听模式
|
||||
- 提交前确保运行 `npm run build`
|
||||
- 构建产物约1.4MB,会提交到Git(正常)
|
||||
- ComfyUI会在启动时自动检测并重建(如果需要)
|
||||
- Source maps已启用,便于调试
|
||||
|
||||
---
|
||||
|
||||
## 🎓 学习资源
|
||||
|
||||
- [Vue 3 文档](https://vuejs.org/)
|
||||
- [PrimeVue 文档](https://primevue.org/)
|
||||
- [TypeScript 文档](https://www.typescriptlang.org/)
|
||||
- [Vite 文档](https://vitejs.dev/)
|
||||
- [ComfyUI 自定义节点开发](https://docs.comfy.org/)
|
||||
181
VUE_WIDGETS_SETUP.md
Normal file
181
VUE_WIDGETS_SETUP.md
Normal file
@@ -0,0 +1,181 @@
|
||||
# Vue + PrimeVue Widget Development Setup
|
||||
|
||||
This guide explains the Vue + PrimeVue widget development scaffold for ComfyUI LoRA Manager.
|
||||
|
||||
## Overview
|
||||
|
||||
The project now supports developing custom ComfyUI widgets using Vue 3 + PrimeVue, providing a modern reactive framework for building rich UI components.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
ComfyUI-Lora-Manager/
|
||||
├── vue-widgets/ # Vue widget source code (TypeScript)
|
||||
│ ├── src/
|
||||
│ │ ├── main.ts # Extension registration
|
||||
│ │ └── components/ # Vue components
|
||||
│ ├── package.json
|
||||
│ ├── vite.config.mts # Build to web/comfyui/vue-widgets/
|
||||
│ └── tsconfig.json
|
||||
│
|
||||
├── web/comfyui/ # ComfyUI web directory
|
||||
│ ├── vue-widgets/ # Compiled Vue widgets (gitignored)
|
||||
│ │ ├── demo-widget.js # Built JavaScript
|
||||
│ │ └── assets/ # CSS and other assets
|
||||
│ └── *.js # Existing vanilla JS widgets
|
||||
│
|
||||
├── py/nodes/ # Python node definitions
|
||||
│ ├── demo_vue_widget_node.py # Demo node
|
||||
│ └── ...
|
||||
│
|
||||
└── __init__.py # Node registration
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Install Dependencies
|
||||
|
||||
```bash
|
||||
cd vue-widgets
|
||||
npm install
|
||||
```
|
||||
|
||||
### 2. Build the Demo Widget
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
This compiles the TypeScript/Vue code and outputs to `web/comfyui/vue-widgets/`.
|
||||
|
||||
### 3. Test in ComfyUI
|
||||
|
||||
1. Start/restart ComfyUI
|
||||
2. Open the ComfyUI interface
|
||||
3. Add the "LoRA Manager Demo (Vue)" node from the node menu
|
||||
4. You should see a Vue-powered widget with PrimeVue components:
|
||||
- Text input for model name
|
||||
- Number input for strength (with +/- buttons)
|
||||
- Apply and Reset buttons
|
||||
- Result card showing current configuration
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Watch Mode for Development
|
||||
|
||||
```bash
|
||||
cd vue-widgets
|
||||
npm run dev
|
||||
```
|
||||
|
||||
This watches for file changes and automatically rebuilds. You'll need to refresh ComfyUI's browser page to see changes.
|
||||
|
||||
### Project Structure
|
||||
|
||||
**Python Side (`py/nodes/demo_vue_widget_node.py`):**
|
||||
- Defines the ComfyUI node class
|
||||
- Specifies input types (including the custom widget type)
|
||||
- Implements the processing logic
|
||||
- The widget type name must match the key in the frontend's `getCustomWidgets()`
|
||||
|
||||
**Frontend Side (`vue-widgets/src/`):**
|
||||
- `main.ts` - Registers the extension with ComfyUI and creates Vue apps
|
||||
- `components/DemoWidget.vue` - The actual Vue component with PrimeVue UI
|
||||
|
||||
### Data Flow
|
||||
|
||||
1. **Widget Creation:**
|
||||
- ComfyUI calls `getCustomWidgets()` when creating a node
|
||||
- Creates a container DOM element
|
||||
- Mounts a Vue app with the component inside the container
|
||||
|
||||
2. **Widget State:**
|
||||
- Component props receive `widget` and `node` objects from ComfyUI
|
||||
- Use Vue's reactive state management within the component
|
||||
|
||||
3. **Serialization:**
|
||||
- Implement `widget.serializeValue()` in `onMounted()`
|
||||
- This function is called when the workflow is saved or executed
|
||||
- Return the data that should be passed to the Python node
|
||||
|
||||
4. **Processing:**
|
||||
- Python node receives the serialized data in its `process()` method
|
||||
- Process the data and return results to the workflow
|
||||
|
||||
## Creating Your Own Widget
|
||||
|
||||
See the detailed guide in `vue-widgets/README.md`.
|
||||
|
||||
Quick checklist:
|
||||
- [ ] Create Python node in `py/nodes/`
|
||||
- [ ] Create Vue component in `vue-widgets/src/components/`
|
||||
- [ ] Register widget in `vue-widgets/src/main.ts`
|
||||
- [ ] Register node in `__init__.py`
|
||||
- [ ] Build with `npm run build`
|
||||
- [ ] Test in ComfyUI
|
||||
|
||||
## Key Technologies
|
||||
|
||||
- **Vue 3**: Modern reactive framework with Composition API
|
||||
- **PrimeVue 4**: Rich UI component library with 90+ components
|
||||
- **TypeScript**: Type-safe development
|
||||
- **Vite**: Fast build tool with HMR support
|
||||
|
||||
## Build Configuration
|
||||
|
||||
The build is configured to:
|
||||
- Output ES modules to `../web/comfyui/vue-widgets/`
|
||||
- Mark ComfyUI's `app.js` as external (not bundled)
|
||||
- Generate source maps for debugging
|
||||
- Keep code unminified for easier debugging
|
||||
- Split vendor code into separate chunks
|
||||
|
||||
## PrimeVue Components
|
||||
|
||||
The demo widget showcases several PrimeVue components:
|
||||
- `Button` - Styled buttons with icons
|
||||
- `InputText` - Text input fields
|
||||
- `InputNumber` - Number inputs with spinners
|
||||
- `Card` - Container component
|
||||
|
||||
For the full component library, see [PrimeVue Documentation](https://primevue.org/).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Build fails with module errors
|
||||
- Make sure you're in the `vue-widgets` directory
|
||||
- Run `npm install` to ensure all dependencies are installed
|
||||
- Check that Node.js version is 18+ (`node --version`)
|
||||
|
||||
### Widget doesn't appear in ComfyUI
|
||||
- Verify the build completed successfully (`web/comfyui/vue-widgets/demo-widget.js` exists)
|
||||
- Check that the Python node is registered in `__init__.py`
|
||||
- Restart ComfyUI completely (not just refresh browser)
|
||||
- Check browser console for JavaScript errors
|
||||
|
||||
### Widget type mismatch error
|
||||
- Ensure the widget type in Python (e.g., `"LORA_DEMO_WIDGET"`) matches the key in `getCustomWidgets()`
|
||||
- Type names are case-sensitive
|
||||
|
||||
### Changes not reflected after rebuild
|
||||
- Hard refresh the browser (Ctrl+Shift+R / Cmd+Shift+R)
|
||||
- Clear browser cache
|
||||
- Restart ComfyUI server
|
||||
|
||||
## Next Steps
|
||||
|
||||
Now that the scaffold is set up, you can:
|
||||
|
||||
1. **Extend the demo widget** - Add more PrimeVue components and functionality
|
||||
2. **Create production widgets** - Build widgets for actual LoRA management features
|
||||
3. **Add styling** - Customize the look with CSS/Tailwind
|
||||
4. **Add i18n** - Implement vue-i18n for internationalization
|
||||
5. **Add state management** - Use Pinia if you need shared state across widgets
|
||||
|
||||
## References
|
||||
|
||||
- [ComfyUI Custom Nodes Documentation](https://docs.comfy.org/essentials/custom_node_server)
|
||||
- [PrimeVue Documentation](https://primevue.org/)
|
||||
- [Vue 3 Documentation](https://vuejs.org/)
|
||||
- [Vite Documentation](https://vitejs.dev/)
|
||||
- Reference implementation: `/refs/ComfyUI_frontend_vue_basic`
|
||||
19
__init__.py
19
__init__.py
@@ -8,6 +8,7 @@ try: # pragma: no cover - import fallback for pytest collection
|
||||
from .py.nodes.debug_metadata import DebugMetadata
|
||||
from .py.nodes.wanvideo_lora_select import WanVideoLoraSelectLM
|
||||
from .py.nodes.wanvideo_lora_select_from_text import WanVideoLoraSelectFromText
|
||||
from .py.nodes.demo_vue_widget_node import LoraManagerDemoNode
|
||||
from .py.metadata_collector import init as init_metadata_collector
|
||||
except ImportError: # pragma: no cover - allows running under pytest without package install
|
||||
import importlib
|
||||
@@ -28,6 +29,7 @@ except ImportError: # pragma: no cover - allows running under pytest without pa
|
||||
DebugMetadata = importlib.import_module("py.nodes.debug_metadata").DebugMetadata
|
||||
WanVideoLoraSelectLM = importlib.import_module("py.nodes.wanvideo_lora_select").WanVideoLoraSelectLM
|
||||
WanVideoLoraSelectFromText = importlib.import_module("py.nodes.wanvideo_lora_select_from_text").WanVideoLoraSelectFromText
|
||||
LoraManagerDemoNode = importlib.import_module("py.nodes.demo_vue_widget_node").LoraManagerDemoNode
|
||||
init_metadata_collector = importlib.import_module("py.metadata_collector").init
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
@@ -39,11 +41,26 @@ NODE_CLASS_MAPPINGS = {
|
||||
SaveImageLM.NAME: SaveImageLM,
|
||||
DebugMetadata.NAME: DebugMetadata,
|
||||
WanVideoLoraSelectLM.NAME: WanVideoLoraSelectLM,
|
||||
WanVideoLoraSelectFromText.NAME: WanVideoLoraSelectFromText
|
||||
WanVideoLoraSelectFromText.NAME: WanVideoLoraSelectFromText,
|
||||
"LoraManagerDemoNode": LoraManagerDemoNode
|
||||
}
|
||||
|
||||
WEB_DIRECTORY = "./web/comfyui"
|
||||
|
||||
# Check and build Vue widgets if needed (development mode)
|
||||
try:
|
||||
from .py.vue_widget_builder import check_and_build_vue_widgets
|
||||
# Auto-build in development, warn only if fails
|
||||
check_and_build_vue_widgets(auto_build=True, warn_only=True)
|
||||
except ImportError:
|
||||
# Fallback for pytest
|
||||
import importlib
|
||||
check_and_build_vue_widgets = importlib.import_module("py.vue_widget_builder").check_and_build_vue_widgets
|
||||
check_and_build_vue_widgets(auto_build=True, warn_only=True)
|
||||
except Exception as e:
|
||||
import logging
|
||||
logging.warning(f"[LoRA Manager] Vue widget build check skipped: {e}")
|
||||
|
||||
# Initialize metadata collector
|
||||
init_metadata_collector()
|
||||
|
||||
|
||||
72
py/nodes/demo_vue_widget_node.py
Normal file
72
py/nodes/demo_vue_widget_node.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""
|
||||
Demo node to showcase Vue + PrimeVue widget integration in ComfyUI LoRA Manager.
|
||||
|
||||
This node demonstrates:
|
||||
- Vue 3 + PrimeVue custom widget
|
||||
- Widget state serialization
|
||||
- Integration with ComfyUI workflow
|
||||
"""
|
||||
|
||||
|
||||
class LoraManagerDemoNode:
|
||||
"""
|
||||
A demo node that uses a Vue + PrimeVue widget to configure LoRA parameters.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"lora_demo_widget": ("LORA_DEMO_WIDGET", {}),
|
||||
},
|
||||
"optional": {
|
||||
"text": ("STRING", {
|
||||
"default": "",
|
||||
"multiline": True,
|
||||
"placeholder": "Additional prompt text..."
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "FLOAT", "STRING")
|
||||
RETURN_NAMES = ("model_name", "strength", "info")
|
||||
|
||||
FUNCTION = "process"
|
||||
|
||||
CATEGORY = "loramanager/demo"
|
||||
|
||||
def process(self, lora_demo_widget, text=""):
|
||||
"""
|
||||
Process the widget data and return the configuration.
|
||||
|
||||
Args:
|
||||
lora_demo_widget: Widget data containing model_name and strength
|
||||
text: Optional text input
|
||||
|
||||
Returns:
|
||||
Tuple of (model_name, strength, info_string)
|
||||
"""
|
||||
model_name = lora_demo_widget.get("modelName", "")
|
||||
strength = lora_demo_widget.get("strength", 1.0)
|
||||
|
||||
info = f"Vue Widget Demo - Model: {model_name}, Strength: {strength}"
|
||||
if text:
|
||||
info += f"\nAdditional text: {text}"
|
||||
|
||||
print(f"[LoraManagerDemoNode] {info}")
|
||||
|
||||
return (model_name, strength, info)
|
||||
|
||||
|
||||
# Node class mappings for ComfyUI
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"LoraManagerDemoNode": LoraManagerDemoNode
|
||||
}
|
||||
|
||||
# Display name mappings
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"LoraManagerDemoNode": "LoRA Manager Demo (Vue)"
|
||||
}
|
||||
241
py/vue_widget_builder.py
Normal file
241
py/vue_widget_builder.py
Normal file
@@ -0,0 +1,241 @@
|
||||
"""
|
||||
Vue Widget Build Checker and Auto-builder
|
||||
|
||||
This module checks if Vue widgets are built and attempts to build them if needed.
|
||||
Useful for development mode where source code might be newer than build output.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VueWidgetBuilder:
|
||||
"""Manages Vue widget build checking and auto-building."""
|
||||
|
||||
def __init__(self, project_root: Optional[Path] = None):
|
||||
"""
|
||||
Initialize the builder.
|
||||
|
||||
Args:
|
||||
project_root: Project root directory. If None, auto-detects.
|
||||
"""
|
||||
if project_root is None:
|
||||
# Auto-detect project root (where __init__.py is)
|
||||
project_root = Path(__file__).parent.parent
|
||||
|
||||
self.project_root = Path(project_root)
|
||||
self.vue_widgets_dir = self.project_root / "vue-widgets"
|
||||
self.build_output_dir = self.project_root / "web" / "comfyui" / "vue-widgets"
|
||||
self.src_dir = self.vue_widgets_dir / "src"
|
||||
|
||||
def check_build_exists(self) -> bool:
|
||||
"""
|
||||
Check if build output exists.
|
||||
|
||||
Returns:
|
||||
True if at least one built .js file exists
|
||||
"""
|
||||
if not self.build_output_dir.exists():
|
||||
return False
|
||||
|
||||
js_files = list(self.build_output_dir.glob("*.js"))
|
||||
return len(js_files) > 0
|
||||
|
||||
def check_build_outdated(self) -> bool:
|
||||
"""
|
||||
Check if source code is newer than build output.
|
||||
|
||||
Returns:
|
||||
True if source is newer, False otherwise or if can't determine
|
||||
"""
|
||||
if not self.src_dir.exists():
|
||||
return False
|
||||
|
||||
if not self.check_build_exists():
|
||||
return True
|
||||
|
||||
try:
|
||||
# Get newest file in source directory
|
||||
src_files = [f for f in self.src_dir.rglob("*") if f.is_file()]
|
||||
if not src_files:
|
||||
return False
|
||||
|
||||
newest_src_time = max(f.stat().st_mtime for f in src_files)
|
||||
|
||||
# Get oldest file in build directory
|
||||
build_files = [f for f in self.build_output_dir.rglob("*.js") if f.is_file()]
|
||||
if not build_files:
|
||||
return True
|
||||
|
||||
oldest_build_time = min(f.stat().st_mtime for f in build_files)
|
||||
|
||||
return newest_src_time > oldest_build_time
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Error checking build timestamps: {e}")
|
||||
return False
|
||||
|
||||
def check_node_available(self) -> bool:
|
||||
"""
|
||||
Check if Node.js is available.
|
||||
|
||||
Returns:
|
||||
True if node/npm are available
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["npm", "--version"],
|
||||
capture_output=True,
|
||||
timeout=5,
|
||||
check=False
|
||||
)
|
||||
return result.returncode == 0
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||
return False
|
||||
|
||||
def build_widgets(self, force: bool = False) -> bool:
|
||||
"""
|
||||
Build Vue widgets.
|
||||
|
||||
Args:
|
||||
force: If True, build even if not needed
|
||||
|
||||
Returns:
|
||||
True if build succeeded or not needed, False if failed
|
||||
"""
|
||||
if not force and self.check_build_exists() and not self.check_build_outdated():
|
||||
logger.debug("Vue widgets build is up to date")
|
||||
return True
|
||||
|
||||
if not self.vue_widgets_dir.exists():
|
||||
logger.warning(f"Vue widgets directory not found: {self.vue_widgets_dir}")
|
||||
return False
|
||||
|
||||
if not self.check_node_available():
|
||||
logger.warning(
|
||||
"Node.js/npm not found. Cannot build Vue widgets. "
|
||||
"Please install Node.js or build manually: cd vue-widgets && npm run build"
|
||||
)
|
||||
return False
|
||||
|
||||
logger.info("Building Vue widgets...")
|
||||
|
||||
try:
|
||||
# Check if node_modules exists, if not run npm install first
|
||||
node_modules = self.vue_widgets_dir / "node_modules"
|
||||
if not node_modules.exists():
|
||||
logger.info("Installing npm dependencies...")
|
||||
install_result = subprocess.run(
|
||||
["npm", "install"],
|
||||
cwd=self.vue_widgets_dir,
|
||||
capture_output=True,
|
||||
timeout=300, # 5 minutes for install
|
||||
check=False
|
||||
)
|
||||
|
||||
if install_result.returncode != 0:
|
||||
logger.error(f"npm install failed: {install_result.stderr.decode()}")
|
||||
return False
|
||||
|
||||
# Run build
|
||||
build_result = subprocess.run(
|
||||
["npm", "run", "build"],
|
||||
cwd=self.vue_widgets_dir,
|
||||
capture_output=True,
|
||||
timeout=120, # 2 minutes for build
|
||||
check=False
|
||||
)
|
||||
|
||||
if build_result.returncode == 0:
|
||||
logger.info("✓ Vue widgets built successfully")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"Build failed: {build_result.stderr.decode()}")
|
||||
return False
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error("Build timed out")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Build error: {e}")
|
||||
return False
|
||||
|
||||
def ensure_built(self, auto_build: bool = True, warn_only: bool = True) -> bool:
|
||||
"""
|
||||
Ensure Vue widgets are built, optionally auto-building if needed.
|
||||
|
||||
Args:
|
||||
auto_build: If True, attempt to build if needed
|
||||
warn_only: If True, only warn on failure instead of raising
|
||||
|
||||
Returns:
|
||||
True if widgets are available (built or successfully auto-built)
|
||||
|
||||
Raises:
|
||||
RuntimeError: If warn_only=False and build is missing/failed
|
||||
"""
|
||||
if self.check_build_exists():
|
||||
# Build exists, check if outdated
|
||||
if self.check_build_outdated():
|
||||
logger.info("Vue widget source code is newer than build")
|
||||
if auto_build:
|
||||
return self.build_widgets()
|
||||
else:
|
||||
logger.warning(
|
||||
"Vue widget build is outdated. "
|
||||
"Please rebuild: cd vue-widgets && npm run build"
|
||||
)
|
||||
return True
|
||||
|
||||
# No build exists
|
||||
logger.warning("Vue widget build not found")
|
||||
|
||||
if auto_build:
|
||||
if self.build_widgets():
|
||||
return True
|
||||
else:
|
||||
msg = (
|
||||
"Failed to build Vue widgets. "
|
||||
"Please build manually: cd vue-widgets && npm install && npm run build"
|
||||
)
|
||||
if warn_only:
|
||||
logger.warning(msg)
|
||||
return False
|
||||
else:
|
||||
raise RuntimeError(msg)
|
||||
else:
|
||||
msg = "Vue widgets not built. Please run: cd vue-widgets && npm install && npm run build"
|
||||
if warn_only:
|
||||
logger.warning(msg)
|
||||
return False
|
||||
else:
|
||||
raise RuntimeError(msg)
|
||||
|
||||
|
||||
def check_and_build_vue_widgets(
|
||||
auto_build: bool = True,
|
||||
warn_only: bool = True,
|
||||
force: bool = False
|
||||
) -> bool:
|
||||
"""
|
||||
Convenience function to check and build Vue widgets.
|
||||
|
||||
Args:
|
||||
auto_build: If True, attempt to build if needed
|
||||
warn_only: If True, only warn on failure instead of raising
|
||||
force: If True, force rebuild even if up to date
|
||||
|
||||
Returns:
|
||||
True if widgets are available
|
||||
"""
|
||||
builder = VueWidgetBuilder()
|
||||
|
||||
if force:
|
||||
return builder.build_widgets(force=True)
|
||||
|
||||
return builder.ensure_built(auto_build=auto_build, warn_only=warn_only)
|
||||
4
vue-widgets/.gitignore
vendored
Normal file
4
vue-widgets/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
dist
|
||||
*.local
|
||||
.DS_Store
|
||||
179
vue-widgets/DEMO_INSTRUCTIONS.md
Normal file
179
vue-widgets/DEMO_INSTRUCTIONS.md
Normal file
@@ -0,0 +1,179 @@
|
||||
# Demo Widget Testing Instructions
|
||||
|
||||
## What Was Created
|
||||
|
||||
A complete Vue + PrimeVue development scaffold for creating custom ComfyUI widgets, including a working demo to validate the entire workflow.
|
||||
|
||||
## Components
|
||||
|
||||
### 1. Python Node (`py/nodes/demo_vue_widget_node.py`)
|
||||
- **Node Name**: LoRA Manager Demo (Vue)
|
||||
- **Category**: loramanager/demo
|
||||
- **Inputs**:
|
||||
- `lora_demo_widget` (custom widget)
|
||||
- `text` (optional string)
|
||||
- **Outputs**:
|
||||
- `model_name` (STRING)
|
||||
- `strength` (FLOAT)
|
||||
- `info` (STRING)
|
||||
|
||||
### 2. Vue Widget (`vue-widgets/src/components/DemoWidget.vue`)
|
||||
Uses PrimeVue components:
|
||||
- **InputText** - For model name input
|
||||
- **InputNumber** - For strength value (0-2, step 0.1) with +/- buttons
|
||||
- **Button** - Apply and Reset actions
|
||||
- **Card** - Display current configuration
|
||||
|
||||
### 3. Build System
|
||||
- **Vite** - Fast build tool
|
||||
- **TypeScript** - Type-safe development
|
||||
- **Output**: `web/comfyui/vue-widgets/demo-widget.js`
|
||||
|
||||
## How to Test
|
||||
|
||||
### 1. Build the Widget (if not already built)
|
||||
|
||||
```bash
|
||||
cd vue-widgets
|
||||
npm install # Only needed once
|
||||
npm run build
|
||||
```
|
||||
|
||||
### 2. Start/Restart ComfyUI
|
||||
|
||||
```bash
|
||||
# In your ComfyUI root directory
|
||||
python main.py
|
||||
```
|
||||
|
||||
### 3. Add the Demo Node
|
||||
|
||||
1. Open ComfyUI in your browser (usually http://localhost:8188)
|
||||
2. Right-click on the canvas → **Add Node**
|
||||
3. Navigate to: **loramanager** → **demo** → **LoRA Manager Demo (Vue)**
|
||||
4. The node should appear with a Vue-powered widget inside
|
||||
|
||||
### 4. Test the Widget
|
||||
|
||||
The widget provides an interactive demo:
|
||||
|
||||
1. **Enter a model name** in the text field (e.g., "test-lora-model")
|
||||
2. **Adjust the strength** using the number input or +/- buttons (0.0 - 2.0)
|
||||
3. **Click "Apply"** to set the configuration
|
||||
4. A card will appear showing the current configuration
|
||||
5. **Click "Reset"** to clear everything
|
||||
|
||||
### 5. Test Workflow Integration
|
||||
|
||||
1. Add some input/output nodes to create a minimal workflow
|
||||
2. Connect the demo node outputs to other nodes:
|
||||
- `model_name` → Can connect to any STRING input
|
||||
- `strength` → Can connect to any FLOAT input
|
||||
- `info` → Informational STRING output
|
||||
3. Click **Queue Prompt** to execute the workflow
|
||||
4. Check the console/terminal - you should see:
|
||||
```
|
||||
[LoraManagerDemoNode] Vue Widget Demo - Model: test-lora-model, Strength: 1.5
|
||||
```
|
||||
|
||||
### 6. Test State Persistence
|
||||
|
||||
1. Configure the widget (set model name and strength, click Apply)
|
||||
2. Save the workflow (Ctrl+S / Cmd+S)
|
||||
3. Reload the page
|
||||
4. Load the saved workflow
|
||||
5. The widget should restore its state
|
||||
|
||||
## Expected Behavior
|
||||
|
||||
✅ **Success Indicators:**
|
||||
- Widget appears inside the node with proper styling
|
||||
- PrimeVue components are rendered correctly
|
||||
- Buttons respond to clicks
|
||||
- Input values update reactively
|
||||
- Configuration card appears after clicking Apply
|
||||
- Node outputs the correct data when workflow executes
|
||||
- State persists when saving/loading workflows
|
||||
|
||||
❌ **Common Issues:**
|
||||
|
||||
**Widget doesn't appear:**
|
||||
- Check browser console for JavaScript errors
|
||||
- Verify `web/comfyui/vue-widgets/demo-widget.js` exists
|
||||
- Restart ComfyUI completely
|
||||
|
||||
**Build errors:**
|
||||
- Make sure you're in the `vue-widgets` directory when running npm commands
|
||||
- Check Node.js version: `node --version` (should be 18+)
|
||||
- Try deleting `node_modules` and running `npm install` again
|
||||
|
||||
**Widget shows but crashes:**
|
||||
- Check browser console for errors
|
||||
- Verify PrimeVue components are imported correctly
|
||||
- Check that the widget type matches between Python and JavaScript
|
||||
|
||||
## Development Workflow
|
||||
|
||||
For active development:
|
||||
|
||||
```bash
|
||||
# Terminal 1: Watch mode for auto-rebuild
|
||||
cd vue-widgets
|
||||
npm run dev
|
||||
|
||||
# Terminal 2: ComfyUI server
|
||||
cd ../../.. # Back to ComfyUI root
|
||||
python main.py
|
||||
```
|
||||
|
||||
When you make changes to Vue files:
|
||||
1. Vite automatically rebuilds
|
||||
2. Hard refresh the browser (Ctrl+Shift+R / Cmd+Shift+R)
|
||||
3. Changes should appear
|
||||
|
||||
## Next Steps
|
||||
|
||||
Now that the demo works, you can:
|
||||
|
||||
1. **Modify the demo widget** to add more features
|
||||
2. **Create new widgets** for actual LoRA Manager functionality
|
||||
3. **Add more PrimeVue components** (see [PrimeVue Docs](https://primevue.org/))
|
||||
4. **Integrate with the LoRA Manager API** to fetch real data
|
||||
5. **Add styling** to match ComfyUI's theme better
|
||||
|
||||
## File Structure Reference
|
||||
|
||||
```
|
||||
ComfyUI-Lora-Manager/
|
||||
├── vue-widgets/ # Vue source code
|
||||
│ ├── src/
|
||||
│ │ ├── main.ts # Extension registration
|
||||
│ │ └── components/
|
||||
│ │ └── DemoWidget.vue # Demo widget component
|
||||
│ ├── package.json # Dependencies
|
||||
│ ├── vite.config.mts # Build config
|
||||
│ ├── tsconfig.json # TypeScript config
|
||||
│ ├── README.md # Development guide
|
||||
│ └── DEMO_INSTRUCTIONS.md # This file
|
||||
│
|
||||
├── web/comfyui/
|
||||
│ └── vue-widgets/ # Build output (gitignored)
|
||||
│ ├── demo-widget.js # Compiled JavaScript
|
||||
│ └── assets/
|
||||
│ └── demo-widget-*.css # Compiled CSS
|
||||
│
|
||||
├── py/nodes/
|
||||
│ └── demo_vue_widget_node.py # Python node definition
|
||||
│
|
||||
├── __init__.py # Updated with demo node
|
||||
├── VUE_WIDGETS_SETUP.md # Complete setup guide
|
||||
└── .gitignore # Updated to ignore build output
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
1. Check the browser console for errors
|
||||
2. Check the ComfyUI terminal for Python errors
|
||||
3. Review `VUE_WIDGETS_SETUP.md` for detailed documentation
|
||||
4. Review `vue-widgets/README.md` for development guide
|
||||
169
vue-widgets/README.md
Normal file
169
vue-widgets/README.md
Normal file
@@ -0,0 +1,169 @@
|
||||
# Vue Widgets for ComfyUI LoRA Manager
|
||||
|
||||
This directory contains the source code for Vue 3 + PrimeVue custom widgets for ComfyUI LoRA Manager.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
vue-widgets/
|
||||
├── src/ # TypeScript/Vue source code
|
||||
│ ├── main.ts # Main entry point that registers extensions
|
||||
│ └── components/ # Vue components
|
||||
│ └── DemoWidget.vue # Example demo widget
|
||||
├── package.json # Dependencies and build scripts
|
||||
├── vite.config.mts # Vite build configuration
|
||||
├── tsconfig.json # TypeScript configuration
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Install Dependencies
|
||||
|
||||
```bash
|
||||
cd vue-widgets
|
||||
npm install
|
||||
```
|
||||
|
||||
### Build for Production
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
This compiles the TypeScript/Vue code and outputs to `../web/comfyui/vue-widgets/`.
|
||||
|
||||
### Development Mode (Watch)
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
This builds the widgets in watch mode, automatically rebuilding when files change.
|
||||
|
||||
### Type Checking
|
||||
|
||||
```bash
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
## Creating a New Widget
|
||||
|
||||
### 1. Create the Python Node
|
||||
|
||||
Create a new node file in `/py/nodes/your_node.py`:
|
||||
|
||||
```python
|
||||
class YourCustomNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"your_widget_name": ("YOUR_WIDGET_TYPE", {}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
FUNCTION = "process"
|
||||
CATEGORY = "loramanager"
|
||||
|
||||
def process(self, your_widget_name):
|
||||
# Process widget data
|
||||
return (str(your_widget_name),)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"YourCustomNode": YourCustomNode
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Create the Vue Component
|
||||
|
||||
Create a new component in `src/components/YourWidget.vue`:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="your-widget-container">
|
||||
<!-- Your UI here using PrimeVue components -->
|
||||
<Button label="Click me" @click="handleClick" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import Button from 'primevue/button'
|
||||
|
||||
const props = defineProps<{
|
||||
widget: { serializeValue?: (node: unknown, index: number) => Promise<unknown> }
|
||||
node: { id: number }
|
||||
}>()
|
||||
|
||||
onMounted(() => {
|
||||
// Serialize widget data when workflow is saved
|
||||
props.widget.serializeValue = async () => {
|
||||
return { /* your data */ }
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Your styles */
|
||||
</style>
|
||||
```
|
||||
|
||||
### 3. Register the Widget
|
||||
|
||||
In `src/main.ts`, add your widget registration:
|
||||
|
||||
```typescript
|
||||
import YourWidget from '@/components/YourWidget.vue'
|
||||
|
||||
// In getCustomWidgets()
|
||||
YOUR_WIDGET_TYPE(node) {
|
||||
return createVueWidget(node, YourWidget, 'your-widget-name')
|
||||
}
|
||||
|
||||
// In nodeCreated()
|
||||
if (node.constructor?.comfyClass !== 'YourCustomNode') return
|
||||
```
|
||||
|
||||
### 4. Register the Node
|
||||
|
||||
Add your node to `__init__.py`:
|
||||
|
||||
```python
|
||||
from .py.nodes.your_node import YourCustomNode
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
# ...
|
||||
"YourCustomNode": YourCustomNode
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Build and Test
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Then restart ComfyUI and test your new widget!
|
||||
|
||||
## Available PrimeVue Components
|
||||
|
||||
This project uses PrimeVue 4.x. Popular components include:
|
||||
|
||||
- `Button` - Buttons with icons and variants
|
||||
- `InputText` - Text input fields
|
||||
- `InputNumber` - Number input with increment/decrement
|
||||
- `Dropdown` - Select dropdowns
|
||||
- `Card` - Card containers
|
||||
- `DataTable` - Data tables with sorting/filtering
|
||||
- `Dialog` - Modal dialogs
|
||||
- `Tree` - Tree view components
|
||||
- And many more! See [PrimeVue Docs](https://primevue.org/)
|
||||
|
||||
## Notes
|
||||
|
||||
- Build output goes to `../web/comfyui/vue-widgets/` (gitignored)
|
||||
- The widget type name in Python (e.g., "YOUR_WIDGET_TYPE") must match the key in `getCustomWidgets()`
|
||||
- Widget data is serialized when the workflow is saved/executed via `serializeValue()`
|
||||
- ComfyUI's app.js is marked as external and not bundled
|
||||
1694
vue-widgets/package-lock.json
generated
Normal file
1694
vue-widgets/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
vue-widgets/package.json
Normal file
28
vue-widgets/package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "comfyui-lora-manager-vue-widgets",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"description": "Vue-based custom widgets for ComfyUI LoRA Manager",
|
||||
"dependencies": {
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^9.14.0",
|
||||
"primevue": "^4.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@comfyorg/comfyui-frontend-types": "^1.35.4",
|
||||
"@types/node": "^22.10.1",
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^6.3.5",
|
||||
"vue-tsc": "^2.1.10"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite build --watch",
|
||||
"build": "vite build",
|
||||
"build:production": "vite build --mode production",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"clean": "rm -rf ../web/comfyui/vue-widgets",
|
||||
"rebuild": "npm run clean && npm run build",
|
||||
"prepare": "npm run build"
|
||||
}
|
||||
}
|
||||
37
vue-widgets/pre-commit.example
Normal file
37
vue-widgets/pre-commit.example
Normal file
@@ -0,0 +1,37 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Example pre-commit hook to ensure Vue widgets are built before committing
|
||||
#
|
||||
# To use this hook:
|
||||
# 1. Copy this file to .git/hooks/pre-commit
|
||||
# 2. Make it executable: chmod +x .git/hooks/pre-commit
|
||||
#
|
||||
# Or use a tool like Husky for automatic hook management:
|
||||
# npm install --save-dev husky
|
||||
# npx husky install
|
||||
# npx husky add .git/hooks/pre-commit "cd vue-widgets && npm run build"
|
||||
|
||||
echo "Running pre-commit hook: Building Vue widgets..."
|
||||
|
||||
# Navigate to vue-widgets directory and build
|
||||
cd "$(git rev-parse --show-toplevel)/vue-widgets" || exit 1
|
||||
|
||||
# Check if node_modules exists
|
||||
if [ ! -d "node_modules" ]; then
|
||||
echo "node_modules not found, running npm install..."
|
||||
npm install || exit 1
|
||||
fi
|
||||
|
||||
# Build Vue widgets
|
||||
npm run build || {
|
||||
echo "❌ Vue widget build failed! Commit aborted."
|
||||
echo "Please fix the build errors before committing."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Add built files to the commit
|
||||
cd ..
|
||||
git add web/comfyui/vue-widgets/
|
||||
|
||||
echo "✓ Vue widgets built and staged successfully"
|
||||
exit 0
|
||||
176
vue-widgets/src/components/DemoWidget.vue
Normal file
176
vue-widgets/src/components/DemoWidget.vue
Normal file
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<div class="demo-widget-container">
|
||||
<h3 class="demo-title">LoRA Manager Demo Widget</h3>
|
||||
|
||||
<div class="demo-content">
|
||||
<div class="input-group">
|
||||
<label for="demo-input">Model Name:</label>
|
||||
<InputText
|
||||
id="demo-input"
|
||||
v-model="modelName"
|
||||
placeholder="Enter model name..."
|
||||
class="demo-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label for="strength-input">Strength:</label>
|
||||
<InputNumber
|
||||
id="strength-input"
|
||||
v-model="strength"
|
||||
:min="0"
|
||||
:max="2"
|
||||
:step="0.1"
|
||||
showButtons
|
||||
class="demo-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<Button
|
||||
label="Apply"
|
||||
icon="pi pi-check"
|
||||
@click="handleApply"
|
||||
severity="success"
|
||||
/>
|
||||
<Button
|
||||
label="Reset"
|
||||
icon="pi pi-refresh"
|
||||
@click="handleReset"
|
||||
severity="secondary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Card v-if="appliedValue" class="result-card">
|
||||
<template #title>Current Configuration</template>
|
||||
<template #content>
|
||||
<p><strong>Model:</strong> {{ appliedValue.modelName || 'None' }}</p>
|
||||
<p><strong>Strength:</strong> {{ appliedValue.strength }}</p>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import Button from 'primevue/button'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import InputNumber from 'primevue/inputnumber'
|
||||
import Card from 'primevue/card'
|
||||
|
||||
interface ComponentWidget {
|
||||
serializeValue?: (node: unknown, index: number) => Promise<unknown>
|
||||
value?: unknown
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
widget: ComponentWidget
|
||||
node: { id: number }
|
||||
}>()
|
||||
|
||||
const modelName = ref('')
|
||||
const strength = ref(1.0)
|
||||
const appliedValue = ref<{ modelName: string; strength: number } | null>(null)
|
||||
|
||||
function handleApply() {
|
||||
appliedValue.value = {
|
||||
modelName: modelName.value,
|
||||
strength: strength.value
|
||||
}
|
||||
console.log('Applied configuration:', appliedValue.value)
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
modelName.value = ''
|
||||
strength.value = 1.0
|
||||
appliedValue.value = null
|
||||
console.log('Reset configuration')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Serialize the widget value when the workflow is saved or executed
|
||||
props.widget.serializeValue = async () => {
|
||||
const value = appliedValue.value || { modelName: '', strength: 1.0 }
|
||||
console.log('Serializing widget value:', value)
|
||||
return value
|
||||
}
|
||||
|
||||
// Restore widget value if it exists
|
||||
if (props.widget.value) {
|
||||
const savedValue = props.widget.value as { modelName: string; strength: number }
|
||||
modelName.value = savedValue.modelName || ''
|
||||
strength.value = savedValue.strength || 1.0
|
||||
appliedValue.value = savedValue
|
||||
console.log('Restored widget value:', savedValue)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.demo-widget-container {
|
||||
padding: 12px;
|
||||
box-sizing: border-box;
|
||||
background: var(--comfy-menu-bg);
|
||||
border-radius: 4px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.demo-title {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--fg-color);
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.input-group label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--fg-color);
|
||||
}
|
||||
|
||||
.demo-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.result-card {
|
||||
margin-top: 8px;
|
||||
background: var(--comfy-input-bg);
|
||||
}
|
||||
|
||||
.result-card :deep(.p-card-title) {
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.result-card :deep(.p-card-content) {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.result-card p {
|
||||
margin: 4px 0;
|
||||
font-size: 13px;
|
||||
color: var(--fg-color);
|
||||
}
|
||||
</style>
|
||||
73
vue-widgets/src/main.ts
Normal file
73
vue-widgets/src/main.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { createApp, type App as VueApp } from 'vue'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import DemoWidget from '@/components/DemoWidget.vue'
|
||||
|
||||
// @ts-ignore - ComfyUI external module
|
||||
import { app } from '../../../scripts/app.js'
|
||||
|
||||
const vueApps = new Map<number, VueApp>()
|
||||
|
||||
// @ts-ignore
|
||||
function createVueWidget(node) {
|
||||
const container = document.createElement('div')
|
||||
container.id = `lora-manager-demo-widget-${node.id}`
|
||||
container.style.width = '100%'
|
||||
container.style.height = '100%'
|
||||
container.style.minHeight = '300px'
|
||||
container.style.display = 'flex'
|
||||
container.style.flexDirection = 'column'
|
||||
container.style.overflow = 'hidden'
|
||||
|
||||
const widget = node.addDOMWidget(
|
||||
'lora_demo_widget',
|
||||
'lora-manager-demo',
|
||||
container,
|
||||
{
|
||||
getMinHeight: () => 320,
|
||||
hideOnZoom: false,
|
||||
serialize: true
|
||||
}
|
||||
)
|
||||
|
||||
const vueApp = createApp(DemoWidget, {
|
||||
widget,
|
||||
node
|
||||
})
|
||||
|
||||
vueApp.use(PrimeVue)
|
||||
|
||||
vueApp.mount(container)
|
||||
vueApps.set(node.id, vueApp)
|
||||
|
||||
widget.onRemove = () => {
|
||||
const vueApp = vueApps.get(node.id)
|
||||
if (vueApp) {
|
||||
vueApp.unmount()
|
||||
vueApps.delete(node.id)
|
||||
}
|
||||
}
|
||||
|
||||
return { widget }
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: 'comfyui.loramanager.demo',
|
||||
|
||||
getCustomWidgets() {
|
||||
return {
|
||||
// @ts-ignore
|
||||
LORA_DEMO_WIDGET(node) {
|
||||
return createVueWidget(node)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// @ts-ignore
|
||||
nodeCreated(node) {
|
||||
if (node.constructor?.comfyClass !== 'LoraManagerDemoNode') return
|
||||
|
||||
const [oldWidth, oldHeight] = node.size
|
||||
|
||||
node.setSize([Math.max(oldWidth, 350), Math.max(oldHeight, 400)])
|
||||
}
|
||||
})
|
||||
24
vue-widgets/tsconfig.json
Normal file
24
vue-widgets/tsconfig.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
10
vue-widgets/tsconfig.node.json
Normal file
10
vue-widgets/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.mts"]
|
||||
}
|
||||
35
vue-widgets/vite.config.mts
Normal file
35
vue-widgets/vite.config.mts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { resolve } from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, './src')
|
||||
}
|
||||
},
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, './src/main.ts'),
|
||||
formats: ['es'],
|
||||
fileName: 'demo-widget'
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [
|
||||
'../../../scripts/app.js'
|
||||
],
|
||||
output: {
|
||||
dir: '../web/comfyui/vue-widgets',
|
||||
entryFileNames: 'demo-widget.js',
|
||||
chunkFileNames: 'assets/[name]-[hash].js',
|
||||
assetFileNames: 'assets/[name]-[hash][extname]'
|
||||
}
|
||||
},
|
||||
sourcemap: true,
|
||||
minify: false
|
||||
},
|
||||
define: {
|
||||
'process.env.NODE_ENV': JSON.stringify('production')
|
||||
}
|
||||
})
|
||||
56
web/comfyui/vue-widgets/assets/demo-widget-RA_dFMfB.css
Normal file
56
web/comfyui/vue-widgets/assets/demo-widget-RA_dFMfB.css
Normal file
@@ -0,0 +1,56 @@
|
||||
|
||||
.demo-widget-container[data-v-df0cb94d] {
|
||||
padding: 12px;
|
||||
box-sizing: border-box;
|
||||
background: var(--comfy-menu-bg);
|
||||
border-radius: 4px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.demo-title[data-v-df0cb94d] {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--fg-color);
|
||||
}
|
||||
.demo-content[data-v-df0cb94d] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
}
|
||||
.input-group[data-v-df0cb94d] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.input-group label[data-v-df0cb94d] {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--fg-color);
|
||||
}
|
||||
.demo-input[data-v-df0cb94d] {
|
||||
width: 100%;
|
||||
}
|
||||
.button-group[data-v-df0cb94d] {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.result-card[data-v-df0cb94d] {
|
||||
margin-top: 8px;
|
||||
background: var(--comfy-input-bg);
|
||||
}
|
||||
.result-card[data-v-df0cb94d] .p-card-title {
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.result-card[data-v-df0cb94d] .p-card-content {
|
||||
padding-top: 0;
|
||||
}
|
||||
.result-card p[data-v-df0cb94d] {
|
||||
margin: 4px 0;
|
||||
font-size: 13px;
|
||||
color: var(--fg-color);
|
||||
}
|
||||
11672
web/comfyui/vue-widgets/demo-widget.js
Normal file
11672
web/comfyui/vue-widgets/demo-widget.js
Normal file
File diff suppressed because one or more lines are too long
1
web/comfyui/vue-widgets/demo-widget.js.map
Normal file
1
web/comfyui/vue-widgets/demo-widget.js.map
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user