Compare commits

...
2 Commits
Author SHA1 Message Date
ketle-manandClaude Sonnet 4.6 de6da654ca Add sidebar Category/Package tabs, theme settings, and bug fixes (v0.2.5)
- Sidebar Nodes tab: add Category and Package sub-tabs with dropdown filtering
  - Category: filter nodes by top-level category via /object_info
  - Package: filter nodes by custom node package name (DualCLIPLoader/CLIPLoader fallback)
- Sidebar theme settings: add gear button to customize panel colors (bg, text, border, etc.) with live preview; saved to localStorage
- Sidebar Workflows: hide .index.json from workflow list
- Sidebar Models By Type: fix TextEncoder not appearing (use DualCLIPLoader→CLIPLoader fallback for clip_name1)
- Workflow tab: hide .index.json from workflow list
- Help tab: update Workflow Studio Library section with new features
- Remove unused buildDropdown function

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 12:07:49 +09:00
ketle-manandClaude Sonnet 4.6 e7fdfc41ad Redesign GenerateUI tab layout: 3-tab structure with inline Raw JSON (v0.2.4)
- Reorganized from 5 tabs (Prompt/Image/Model/Settings/Raw JSON) to 3 tabs (Input/Model/Settings)
- Each tab shows a Raw JSON column (540px) for instant preview and direct editing from any tab
- Input tab: Prompt (top) + Image (bottom) stacked vertically in left column
- Model tab: model selectors with filter in left column
- Settings tab: KSampler (top) + LatentImage (bottom) stacked vertically in left column
- Raw JSON widget shared across tabs via DOM appendChild on tab switch
- Right column slimmed to 280px: Generate/Seed/Results only
- Docs: DEVLOG, README, i18n help keys, index.html help tab updated for v0.2.4

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 18:01:45 +09:00
11 changed files with 729 additions and 121 deletions
+125
View File
@@ -1,5 +1,130 @@
# DEVLOG - ComfyUI-Workflow-Studio
## 2026-04-04: v0.2.5 追加修正
### 概要
- サイドパネルModels→By TypeでTextEncoderが表示されない問題を修正
- サイドパネルのWorkflowsタブでも `.index.json` を非表示に
- 未使用の `buildDropdown` 関数を削除
### 変更内容
#### `web/comfyui/node_sets_menu.js`
**TextEncoder取得ロジック修正:**
- 変更前: `/api/wfm/models?type=textencoder`(存在しないエンドポイント)
- 変更後: `DualCLIPLoader` → `CLIPLoader` の順に `/object_info/{cls}` を試して `clip_name1` を取得(`comfyui-client.js` の `fetchTextEncoders()` と同じロジック)
**`.index.json` 非表示:**
- `loadWfData()`: `state.wfList = workflows.filter(w => w.filename !== ".index.json")` でサイドパネルのWorkflowsタブからも除外
**未使用関数削除:**
- `buildDropdown()` 関数を削除(Category/Packageドロップダウンは個別実装に置き換え済みだったため不要)
---
## 2026-04-03: v0.2.5 サイドパネル Category/Package サブタブ・テーマ設定・Workflow修正
### 概要
- サイドパネル(Workflow Studio Library)のNodesタブに📂 Category / 🧩 Packageサブタブを追加
- サイドパネルのヘッダーに⚙テーマ設定ボタンを追加(背景・文字・ボーダー色カスタマイズ)
- ワークフロータブで `.index.json` を非表示に
### 変更内容
#### `web/comfyui/node_sets_menu.js` — Nodesサブタブ追加・テーマ設定
**Nodesタブのrow2サブタブ構成変更:**
- 変更前: row2 = `☰ Sets`
- 変更後: row2 = `☰ Sets` / `📂 Category` / `🧩 Package`
**state追加:**
- `activeNodeCategory: ""` — Categoryサブタブで選択中のカテゴリ値
- `activeNodePackage: ""` — Packageサブタブで選択中のパッケージ値
- `objectInfo: {}` — `/object_info` APIから取得した全ノード情報(パッケージ判定に使用)
**`loadData()` 変更:**
- `fetchObjectInfo()` を追加して `/object_info` を並行取得
- `state.objectInfo` に保存
**新規関数:**
- `fetchObjectInfo()` — `GET /object_info` で全ノード情報を取得
- `extractPackageName(pythonModule)` — `python_module` からパッケージ名を抽出
- `getNodeCategory(nodeType)` — `state.objectInfo` 優先でカテゴリを取得(LiteGraphフォールバック)
- `getNodePackage(nodeType)` — `state.objectInfo[nodeType].python_module` からパッケージ名を取得
- `renderNodesByCategory(container)` — カテゴリドロップダウンをcontent上部に挿入してリスト表示
- `renderNodesByCategoryList(container)` — 選択カテゴリ+検索テキストでフィルタしてカード表示
- `renderNodesByPackage(container)` — パッケージドロップダウンをcontent上部に挿入してリスト表示
- `renderNodesByPackageList(container)` — 選択パッケージ+検索テキストでフィルタしてカード表示
**ドロップダウンのDOM挿入方式:**
- `.wfm-nlp-filter-row` クラスのdivを `.wfm-nlp-content` の直前に `insertBefore` で挿入
- `renderContent()` 冒頭で `panelEl.querySelectorAll(".wfm-nlp-filter-row").forEach(e => e.remove())` により他タブ移動時に削除
**テーマ設定:**
- `THEME_KEY = "wfm_nlp_theme"` — localStorage保存キー
- `THEME_VARS` — 設定可能な5変数: `--comfy-menu-bg`(背景)、`--comfy-input-bg`(サブヘッダーBG)、`--input-text`(文字色)、`--border-color`(ボーダー)、`--descrip-text`(補助テキスト色)
- `loadTheme()` — localStorageから読み込み
- `applyTheme(panel, theme)` — パネル要素にCSS変数をインラインsetPropertyで上書き
- `buildThemePanel(panel)` — カラーピッカーUIを動的生成(ライブプレビュー・Save・Resetボタン付き)
- パネルのHTMLに `.wfm-nlp-theme-btn`(⚙)と `.wfm-nlp-theme-panel` を追加
- `createPanel()` 内でテーマボタンのイベントリスナーを登録、`applyTheme(panel, loadTheme())` で起動時に保存済みテーマを適用
#### `static/js/workflow-tab.js` — `.index.json` 非表示
- `filterWorkflows()` の先頭に `if (wf.filename === ".index.json") return false;` を追加
### 技術的な判断
- **`python_module` の取得元をAPIに変更:** `LiteGraph.registered_node_types` にはバックエンド由来の `python_module` が存在しないため、全ノードが `ComfyUI (Built-in)` 判定になる問題があった。`/object_info` APIを `loadData()` 時に並行取得することで正確なパッケージ判定を実現
- **CSS変数のインライン上書き:** テーマ色はパネル要素自体に `style.setProperty` で適用。ComfyUI本体の `:root` 変数を書き換えず、パネルスコープ内のみに影響させる設計
- **ドロップダウンのDOMインジェクション:** Category/Packageビューのドロップダウンは `renderContent` の都度生成・削除する方式を採用。タブ切替時のクリーンアップを `renderContent` 冒頭の `.wfm-nlp-filter-row` 削除処理で一元管理
---
## 2026-03-29: v0.2.4 GenerateUIタブ レイアウト再設計
### 概要
- GenerateUIのサブタブを5タブ(Prompt/Image/Model/Settings/RawJSON)→ 3タブ(Input/Model/Settings)に整理
- 各タブ内にRaw JSONエディタを右列として常時表示。どのタブでもJSON変更をリアルタイム確認・直接編集・Apply可能
- Raw JSONタブを廃止(独立タブとしての Raw JSON は不要)
- 右列(生成コントロール)はGenerate/Seed/結果画像のみにスリム化
### 変更内容
#### `templates/index.html` — タブ構成刷新
- **Inputタブ**: 左列=Prompt(上段)+ Image(下段)の縦2段、右列=Raw JSON(540px)
- **Modelタブ**: 左列=モデル選択フォーム(スクロール)、右列=Raw JSON(540px)
- **Settingsタブ**: 左列=KSampler(上段)+ LatentImage(下段)の縦2段、右列=Raw JSON(540px)
- Raw JSONのDOMノード(`wfm-gen-rawjson-widget`)はページ内に1つ定義し、タブ切替時にJSで対応する列へ移動
- 右列(`.wfm-gen-right`): 280px幅、Generate/Stop/Progress/Seed+結果画像のみ
#### `static/js/generate-tab.js` — タブ切替時にRaw JSONを移動
- `moveRawJsonToTab(tabKey)` 追加: `wfm-gen-rawjson-widget` を `wfm-gen-rawjson-col-{tabKey}` へ `appendChild`
- 初期表示(Input)とタブクリック時に自動移動
#### `static/js/comfyui-editor.js` — Settings縦2段化
- `renderSettingsTab`: KSampler / LatentImageを横2列グリッド → 縦2段(`flex-direction:column`)に変更
- `renderAll` の末尾に `_syncRawJson()` 追加(ワークフロー読込直後にRaw JSONが即時反映)
#### `static/css/main.css` — 新レイアウト用スタイル
- `.wfm-gen-tab-cols`: タブ内2列レイアウト(flex行)
- `.wfm-gen-params-col`: 左サブ列(flex:1、縦積み)
- `.wfm-gen-params-col--scroll`: Model/Settings用スクロール変種
- `.wfm-gen-params-section`: Input用上下2段の各セクション(flex:1、overflow-y:auto)
- `.wfm-gen-rawjson-col`: Raw JSON列(540px固定幅)
- `#wfm-gen-rawjson-widget` / `.wfm-gen-rawjson-header` / `.wfm-gen-rawjson-editor`: Raw JSONウィジェットスタイル
### 技術的な判断
- **Raw JSONをDOMで移動**: タブごとにtextareaのIDは1つである必要があるため、DOMノードをappendChildで移動する方式を採用。イベントリスナーはノードに付いたまま引き継がれる
- **Raw JSON列幅540px**: テキストエディタとして十分な視認性を確保しつつ、パラメータ列にも十分な幅を残す設定
---
## 2026-03-29: v0.2.3 バッジ統一・GenUI Model・サイドパネルModels拡充
### 概要
+24 -6
View File
@@ -5,7 +5,7 @@ A comprehensive workflow management and generation UI plugin for [ComfyUI](https
Browse, organize, and execute workflows directly from a dedicated studio interface — without switching between windows or manually editing JSON.
![Workflow Studio](https://img.shields.io/badge/ComfyUI-Custom_Node-blue)
![Version](https://img.shields.io/badge/version-0.2.3-green)
![Version](https://img.shields.io/badge/version-0.2.5-green)
## Screenshots
@@ -50,11 +50,14 @@ Browse, organize, and execute workflows directly from a dedicated studio interfa
- **Embedded workflow metadata** — workflow JSON is embedded in the PNG (tEXt chunk), compatible with ComfyUI's drag-and-drop import
- **Auto-import** — the captured workflow is automatically imported and appears in the Workflow tab
### GenerateUI Tab
- **Auto-generated parameter UI** — prompt, model, sampler, image, and other settings extracted from the workflow
### GenerateUI Tab (v0.2.4)
- **3-tab layout** — Input / Model / Settings tabs, each with a Raw JSON column on the right for instant preview and direct editing
- **Input tab** — Prompt (top) and Image drag-and-drop (bottom) in the left column; Raw JSON (540px) in the right column
- **Model tab** — Checkpoint, VAE, LoRA, ControlNet, UNET, TextEncoder selectors with filter; Raw JSON on the right
- **Settings tab** — KSampler (top) and LatentImage (bottom) in the left column; Raw JSON on the right
- **Always-visible Raw JSON** — edit the API-format JSON directly from any tab with syntax highlighting; Apply button reloads the workflow
- **One-click generation** — queue prompts to ComfyUI without leaving the studio
- **Seed control** — randomize, lock, or manually set seeds
- **Raw JSON editor** — view and edit the API-format JSON with syntax highlighting
- **UI-to-API conversion** — automatic conversion supporting subgraphs (nested workflows), COMBO types, and display-only node exclusion
- **Eagle integration** — auto-save generated images to [Eagle](https://eagle.cool/) with metadata
@@ -101,9 +104,11 @@ Browse, organize, and execute workflows directly from a dedicated studio interfa
- **Table view memo** — memo column displayed in table view for quick reference
- **Preview images** — auto-detect `{model_stem}.preview.png` next to model files
### Workflow Studio Library (ComfyUI Side Panel) (v0.2.3)
### Workflow Studio Library (ComfyUI Side Panel) (v0.2.5)
- **Workflows tab** — browse favorite workflows (All / ★ Favorites / Groups / By Badge sub-tabs), ★ star shown for favorites in All view
- **Nodes tab** — browse favorite nodes (All / ★ Favorites / Groups / Sets sub-tabs), ★ star shown for favorites in All view
- **Nodes tab** — browse favorite nodes (All / ★ Favorites / Groups / Sets / 📂 Category / 🧩 Package sub-tabs), ★ star shown for favorites in All view
- **Category sub-tab** — dropdown to filter nodes by top-level category
- **Package sub-tab** — dropdown to filter nodes by custom node package name
- **Models tab** — browse installed models (All / ★ Favorites / Groups / By Type sub-tabs)
- **Prompts tab** — browse prompt presets with All / Favorites / Categories sub-tabs
- **Drag & drop workflows** — drag a workflow onto the canvas to load it
@@ -112,6 +117,7 @@ Browse, organize, and execute workflows directly from a dedicated studio interfa
- **Copy prompts** — copy individual positive (P) or negative (N) prompts from sidebar items
- **Double-click** — load workflows or place nodes without dragging
- **Search** — search within each sub-tab to quickly find items
- **⚙ Theme settings** — customize panel background, sub-header background, text, border, and secondary text colors; saved to localStorage and applied on every open
### Help & Support Tab (v0.1.3)
- **Feature list** — overview of all features organized by tab
@@ -188,6 +194,18 @@ Click the **camera icon** (next to the W button) in ComfyUI's top bar to capture
## Changelog
### v0.2.5
- **Sidebar Category/Package sub-tabs** — added 📂 Category and 🧩 Package sub-tabs to the Nodes tab in Workflow Studio Library; each shows a dropdown to filter nodes by category or custom node package (reads `/object_info` for accurate package detection)
- **Sidebar theme settings** — added ⚙ button in the panel header to customize background, sub-header background, text, border, and secondary text colors with live preview; settings saved to localStorage
- **Sidebar TextEncoder (By Type)** — fixed TextEncoder not appearing in Models → By Type; now uses `DualCLIPLoader` → `CLIPLoader` fallback to retrieve `clip_name1` list
- **Hide `.index.json`** — excluded `.index.json` from both the Workflow tab and the Workflow Studio Library side panel
### v0.2.4
- **GenerateUI tab redesign** — reorganized from 5 tabs (Prompt/Image/Model/Settings/Raw JSON) to 3 tabs (Input/Model/Settings); each tab shows a Raw JSON column (540px) for instant JSON preview and direct editing from any tab
- **Input tab** — Prompt and Image stacked vertically in the left column
- **Settings tab** — KSampler and LatentImage stacked vertically in the left column
- **Raw JSON tab removed** — Raw JSON is now always visible as a right-column panel within each tab
### v0.2.3
- **Badge system unified** — Workflow and Models tabs now share a single badge palette (`⚙ Badge` button); workflow auto-analysis model type system removed in favor of user-defined free-label badges
- **GenUI Model button** — apply any model from the Models tab directly to the corresponding node in GenerateUI's current workflow (available in detail modal and side panel; Checkpoint / LoRA / VAE / ControlNet / UNET / TextEncoder)
+96 -12
View File
@@ -505,11 +505,12 @@ body {
}
.wfm-gen-right {
width: 360px;
width: 280px;
flex-shrink: 0;
display: flex;
flex-direction: column;
overflow-y: auto;
border-left: 1px solid var(--wfm-border);
padding: 12px;
gap: 12px;
}
@@ -546,13 +547,55 @@ body {
.wfm-gen-subtab-content {
display: none;
padding: 12px 16px;
overflow-y: auto;
flex: 1;
overflow: hidden;
}
.wfm-gen-subtab-content.active {
display: block;
display: flex;
}
/* ── Tab inner layout: params col + rawjson col ── */
.wfm-gen-tab-cols {
display: flex;
flex: 1;
overflow: hidden;
}
/* Left sub-column: parameter controls */
.wfm-gen-params-col {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
border-right: 1px solid var(--wfm-border);
}
/* Scrollable variant (Model / Settings single-section) */
.wfm-gen-params-col--scroll {
overflow-y: auto;
padding: 12px 14px;
}
/* Stacked sections inside params col (Input: Prompt on top, Image below) */
.wfm-gen-params-section {
flex: 1;
overflow-y: auto;
padding: 10px 14px;
min-height: 0;
}
.wfm-gen-params-section--top-border {
border-top: 1px solid var(--wfm-border);
}
/* Right sub-column: Raw JSON */
.wfm-gen-rawjson-col {
width: 540px;
flex-shrink: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.wfm-gen-status {
@@ -572,13 +615,6 @@ body {
color: var(--wfm-danger);
}
.wfm-gen-panel {
background: var(--wfm-surface);
border: 1px solid var(--wfm-border);
border-radius: var(--wfm-radius);
padding: 12px;
}
.wfm-gen-progress-container {
display: flex;
align-items: center;
@@ -609,8 +645,56 @@ body {
text-align: right;
}
.wfm-gen-result {
/* Raw JSON widget (shared, moved between tabs by JS) */
#wfm-gen-rawjson-widget {
flex: 1;
min-height: 0;
}
.wfm-gen-rawjson-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 10px;
background: var(--wfm-bg-secondary);
border-bottom: 1px solid var(--wfm-border);
flex-shrink: 0;
}
.wfm-gen-rawjson-label {
font-size: 12px;
font-weight: 600;
color: var(--wfm-text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.wfm-gen-rawjson-editor {
flex: 1 !important;
height: auto !important;
}
.wfm-gen-col-title {
font-size: 12px;
font-weight: 600;
margin: 0 0 10px 0;
color: var(--wfm-text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 1px solid var(--wfm-border);
padding-bottom: 6px;
}
/* Generation panel in right column */
.wfm-gen-panel {
background: var(--wfm-surface);
border: 1px solid var(--wfm-border);
border-radius: var(--wfm-radius);
padding: 12px;
flex-shrink: 0;
}
.wfm-gen-result {
display: flex;
flex-direction: column;
align-items: center;
+2 -1
View File
@@ -256,6 +256,7 @@ function applyI18nToHtml() {
"wfm-help-gen-1": "helpGen1", "wfm-help-gen-2": "helpGen2",
"wfm-help-gen-3": "helpGen3", "wfm-help-gen-4": "helpGen4",
"wfm-help-gen-5": "helpGen5", "wfm-help-gen-6": "helpGen6", "wfm-help-gen-7": "helpGen7",
"wfm-help-gen-8": "helpGen8", "wfm-help-gen-9": "helpGen9", "wfm-help-gen-10": "helpGen10",
"wfm-help-prompt-1": "helpPrompt1", "wfm-help-prompt-2": "helpPrompt2",
"wfm-help-prompt-3": "helpPrompt3", "wfm-help-prompt-4": "helpPrompt4",
"wfm-help-prompt-5": "helpPrompt5", "wfm-help-prompt-6": "helpPrompt6",
@@ -278,7 +279,7 @@ function applyI18nToHtml() {
"wfm-help-sidepanel-5": "helpSidepanel5", "wfm-help-sidepanel-6": "helpSidepanel6",
"wfm-help-sidepanel-7": "helpSidepanel7", "wfm-help-sidepanel-8": "helpSidepanel8",
"wfm-help-sidepanel-9": "helpSidepanel9", "wfm-help-sidepanel-10": "helpSidepanel10",
"wfm-help-sidepanel-11": "helpSidepanel11",
"wfm-help-sidepanel-11": "helpSidepanel11", "wfm-help-sidepanel-12": "helpSidepanel12",
"wfm-help-shortcuts-title": "helpShortcutsTitle",
"wfm-help-shortcuts-1": "helpShortcuts1", "wfm-help-shortcuts-2": "helpShortcuts2",
"wfm-help-shortcuts-3": "helpShortcuts3", "wfm-help-shortcuts-4": "helpShortcuts4",
+5 -4
View File
@@ -61,6 +61,7 @@ export const comfyEditor = {
this.renderImageTab(analysis, "wfm-gen-image-fields");
this.renderModelTab(analysis, "wfm-gen-model-fields");
this.renderSettingsTab(analysis, "wfm-gen-settings-fields");
_syncRawJson();
},
renderPromptTab(analysis, containerId) {
@@ -208,9 +209,9 @@ export const comfyEditor = {
const latent = analysis.latent_nodes?.[0];
el.innerHTML = `
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
<div>
<h3 style="font-size:14px;margin-bottom:12px;">KSampler</h3>
<div style="display:flex;flex-direction:column;gap:0;">
<div style="padding-bottom:14px;border-bottom:1px solid var(--wfm-border);margin-bottom:14px;">
<h3 style="font-size:13px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;color:var(--wfm-text-secondary);margin:0 0 12px;">KSampler</h3>
${sampler ? `
<input type="hidden" id="wfm-settings-sampler-id" value="${sampler.id}">
<div class="wfm-form-group">
@@ -245,7 +246,7 @@ export const comfyEditor = {
` : "<p class='wfm-placeholder'>No KSampler node found</p>"}
</div>
<div>
<h3 style="font-size:14px;margin-bottom:12px;">Latent Image</h3>
<h3 style="font-size:13px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;color:var(--wfm-text-secondary);margin:0 0 12px;">Latent Image</h3>
${latent ? `
<input type="hidden" id="wfm-settings-latent-id" value="${latent.id}">
<div class="wfm-form-group">
+15 -1
View File
@@ -246,6 +246,16 @@ export async function initGenerateTab() {
showToast("Interrupted", "info");
});
// Move shared Raw JSON widget into the active tab's rawjson-col
function moveRawJsonToTab(tabKey) {
const widget = document.getElementById("wfm-gen-rawjson-widget");
const col = document.getElementById(`wfm-gen-rawjson-col-${tabKey}`);
if (widget && col) {
col.appendChild(widget);
widget.style.display = "flex";
}
}
// Subtab navigation
document.querySelectorAll(".wfm-gen-subtab-btn").forEach((btn) => {
btn.addEventListener("click", () => {
@@ -254,10 +264,14 @@ export async function initGenerateTab() {
btn.classList.add("active");
document.querySelectorAll(".wfm-gen-subtab-content").forEach((c) => c.classList.remove("active"));
document.getElementById(`wfm-gen-subtab-${target}`)?.classList.add("active");
moveRawJsonToTab(target);
});
});
// Raw JSON apply
// Initial placement: move to first active tab (input)
moveRawJsonToTab("input");
// Raw JSON apply (always-visible panel in right column)
document.getElementById("wfm-gen-apply-raw-btn")?.addEventListener("click", async () => {
const textarea = document.getElementById("wfm-gen-raw-json");
if (!textarea) return;
+42 -18
View File
@@ -167,6 +167,10 @@ const LANGUAGES = {
tabNodes: "Nodes",
nodesAllNodes: "All Nodes",
nodesNodeSets: "Node Sets",
nodesCategoryTab: "Category",
nodesPackageTab: "Package",
nodesSelectCategory: "Select a category...",
nodesSelectPackage: "Select a package...",
nodesSearchPlaceholder: "Search nodes...",
nodesAllCategories: "All Categories",
nodesAllPackages: "All Packages",
@@ -301,11 +305,14 @@ const LANGUAGES = {
helpWf7: "Badge management: add, rename, delete badges with custom colors shared with the Models tab (⚙ Badge button)",
helpGen1: "Connect/disconnect ComfyUI and check connection status",
helpGen2: "Load workflows from Workflow tab (App format .app.json not supported)",
helpGen3: "Edit parameters: Prompt, Image (drag & drop), Model (checkpoint/VAE/LoRA/ControlNet with filter), Settings (steps/CFG/sampler/scheduler/resolution), Raw JSON",
helpGen4: "Seed control: random, fixed, increment, decrement",
helpGen5: "Generate, progress display, and stop",
helpGen6: "View results with main image and thumbnail history",
helpGen7: "Eagle integration: auto-save generated images to Eagle",
helpGen3: "3-tab layout — Input (Prompt + Image), Model, Settings — each with a Raw JSON column for instant preview and direct editing",
helpGen4: "Input tab: Prompt (top) and Image drag-and-drop (bottom) in the left column",
helpGen5: "Model tab: Checkpoint, VAE, LoRA, ControlNet, UNET, TextEncoder selectors with filter",
helpGen6: "Settings tab: KSampler (top) and LatentImage (bottom) in the left column",
helpGen7: "Raw JSON always visible in the right column of each tab — edit directly and Apply to reload the workflow",
helpGen8: "Seed control: random, fixed, increment, decrement",
helpGen9: "Generate, progress display, stop, and view results",
helpGen10: "Eagle integration: auto-save generated images to Eagle",
helpPrompt1: "AI assistant (Ollama): model selection, chat for prompt consultation, image attachment (vision models)",
helpPrompt2: "Apply AI responses to GenerateUI prompts",
helpPrompt3: "Translation: JA\u2194EN, ZH\u2194EN",
@@ -339,7 +346,7 @@ const LANGUAGES = {
helpSidepanelTitle: "Workflow Studio Library (ComfyUI Side Panel)",
helpSidepanel1: "Open from the Workflow Studio Library button in the ComfyUI toolbar",
helpSidepanel2: "Workflows tab: All / ★ Favorites / Groups sub-tabs; ★ shown for favorites in All view",
helpSidepanel3: "Nodes tab: All / ★ Favorites / Groups / Sets sub-tabs; ★ shown for favorites in All view",
helpSidepanel3: "Nodes tab: All / ★ Favorites / Groups / Sets / 📂 Category / 🧩 Package sub-tabs; Category and Package show a dropdown to filter nodes by category or custom node package",
helpSidepanel4: "Prompts tab: browse prompt presets with All/Favorites/Categories sub-tabs",
helpSidepanel5: "Drag & drop workflows onto the canvas to load them",
helpSidepanel6: "Drag & drop nodes/node sets onto the canvas to place them",
@@ -348,6 +355,7 @@ const LANGUAGES = {
helpSidepanel9: "Double-click items to load/place without dragging",
helpSidepanel10: "Search within each sub-tab to quickly find items",
helpSidepanel11: "Models tab: All / ★ Favorites / Groups / By Type sub-tabs",
helpSidepanel12: "⚙ Theme button (header): customize panel background, sub-header background, text, border, and secondary text colors — changes are saved to localStorage and applied on next open",
helpShortcutsTitle: "Keyboard Shortcuts",
helpShortcuts1: "Escape: Close modal/panel",
helpShortcuts2: "Enter: Confirm rename, send chat",
@@ -625,6 +633,10 @@ const LANGUAGES = {
tabNodes: "ノード",
nodesAllNodes: "全ノード",
nodesNodeSets: "ノードセット",
nodesCategoryTab: "カテゴリ",
nodesPackageTab: "パッケージ",
nodesSelectCategory: "カテゴリを選択...",
nodesSelectPackage: "パッケージを選択...",
nodesSearchPlaceholder: "ノードを検索...",
nodesAllCategories: "全カテゴリ",
nodesAllPackages: "全パッケージ",
@@ -759,11 +771,14 @@ const LANGUAGES = {
helpWf7: "バッジ管理:バッジの追加・名前変更・削除、カスタム色設定(Modelsタブと共有)— ⚙ Badgeボタン",
helpGen1: "ComfyUIの接続・切断、接続状態の確認",
helpGen2: "Workflowタブからワークフローを読み込み(App形式 .app.json は非対応)",
helpGen3: "パラメータ編集:プロンプト、画像(ドラッグ&ドロップ)、モデル(checkpoint/VAE/LoRA/ControlNet、フィルタ検索)、設定(Steps/CFG/サンプラー/スケジューラ/解像度)、Raw JSON",
helpGen4: "シード管理:ランダム / 固定 / 増加 / 減少",
helpGen5: "画像生成、進捗表示、生成停止",
helpGen6: "メイン画像とサムネイル履歴で結果表示",
helpGen7: "Eagle連携:生成画像をEagleへ自動保存",
helpGen3: "3タブ構成 — Input(プロンプト+画像)、Model、Settings — 各タブにRaw JSON列を常時表示",
helpGen4: "Inputタブ:左列にPrompt(上段)+ Image ドラッグ&ドロップ(下段)",
helpGen5: "Modelタブ:Checkpoint/VAE/LoRA/ControlNet/UNET/TextEncoderのセレクタ(フィルタ検索付き)",
helpGen6: "Settingsタブ:左列にKSampler(上段)+ LatentImage(下段)",
helpGen7: "Raw JSONを各タブの右列に常時表示 — 直接編集してApplyでワークフロー即時反映",
helpGen8: "シード管理:ランダム / 固定 / 増加 / 減少",
helpGen9: "画像生成、進捗表示、生成停止、結果表示",
helpGen10: "Eagle連携:生成画像をEagleへ自動保存",
helpPrompt1: "AIアシスタント(Ollama):モデル選択、チャットでプロンプト相談、画像添付(ビジョンモデル対応)",
helpPrompt2: "AIの応答をGenerateUIのプロンプトに適用",
helpPrompt3: "翻訳機能:日⇔英、中⇔英",
@@ -797,7 +812,7 @@ const LANGUAGES = {
helpSidepanelTitle: "Workflow Studio Library(ComfyUIサイドパネル)",
helpSidepanel1: "ComfyUIツールバーのWorkflow Studio Libraryボタンから開く",
helpSidepanel2: "Workflowsタブ:All / ★ Favorites / Groupsサブタブ、Allビューでお気に入りに★表示",
helpSidepanel3: "Nodesタブ:All / ★ Favorites / Groups / Setsサブタブ、Allビューでお気に入りに★表示",
helpSidepanel3: "Nodesタブ:All / ★ Favorites / Groups / Sets / 📂 Category / 🧩 Packageサブタブ;CategoryとPackageはドロップダウンでカテゴリ・カスタムノードパッケージ別に絞り込み可能",
helpSidepanel4: "Promptsタブ:プロンプトプリセットの閲覧(All/お気に入り/カテゴリ サブタブ)",
helpSidepanel5: "ワークフローをキャンバスにドラッグ&ドロップで読み込み",
helpSidepanel6: "ノード/ノードセットをキャンバスにドラッグ&ドロップで配置",
@@ -806,6 +821,7 @@ const LANGUAGES = {
helpSidepanel9: "ダブルクリックでもアイテムの読み込み・配置が可能",
helpSidepanel10: "各サブタブ内で検索してアイテムをすばやく検索",
helpSidepanel11: "Modelsタブ:All / ★ Favorites / Groups / By Typeサブタブ",
helpSidepanel12: "⚙ テーマボタン(ヘッダー):パネル背景・サブヘッダー背景・文字色・ボーダー・補助テキスト色をカスタマイズ可能、設定はlocalStorageに保存され次回起動時も反映",
helpShortcutsTitle: "キーボード操作",
helpShortcuts1: "Escape:モーダル・パネルを閉じる",
helpShortcuts2: "Enter:名前変更の確定、チャット送信",
@@ -1079,6 +1095,10 @@ const LANGUAGES = {
tabNodes: "节点",
nodesAllNodes: "所有节点",
nodesNodeSets: "节点集",
nodesCategoryTab: "分类",
nodesPackageTab: "包",
nodesSelectCategory: "选择分类...",
nodesSelectPackage: "选择包...",
nodesSearchPlaceholder: "搜索节点...",
nodesAllCategories: "所有分类",
nodesAllPackages: "所有包",
@@ -1213,11 +1233,14 @@ const LANGUAGES = {
helpWf7: "徽章管理:添加、重命名、删除徽章并自定义颜色(与Models标签共享)— ⚙ Badge按钮",
helpGen1: "连接/断开ComfyUI,检查连接状态",
helpGen2: "从工作流标签页加载工作流(不支持App格式.app.json)",
helpGen3: "编辑参数:提示词、图片(拖放)、模型(checkpoint/VAE/LoRA/ControlNet含筛选)、设置(Steps/CFG/采样器/调度器/分辨率)、Raw JSON",
helpGen4: "种子管理:随机/固定/递增/递减",
helpGen5: "生成图片、进度显示、停止生成",
helpGen6: "主图和缩略图历史查看结果",
helpGen7: "Eagle集成:自动保存生成图片到Eagle",
helpGen3: "3标签布局 — Input(提示词+图片)、Model、Settings — 每个标签含Raw JSON列实时预览",
helpGen4: "Input标签:左列为Prompt(上)+ Image拖放(下)",
helpGen5: "Model标签:Checkpoint/VAE/LoRA/ControlNet/UNET/TextEncoder选择器(含筛选)",
helpGen6: "Settings标签:左列为KSampler(上)+ LatentImage(下)",
helpGen7: "Raw JSON在每个标签右列常驻显示 — 直接编辑并Apply即时重载工作流",
helpGen8: "种子管理:随机/固定/递增/递减",
helpGen9: "生成图片、进度显示、停止生成、查看结果",
helpGen10: "Eagle集成:自动保存生成图片到Eagle",
helpPrompt1: "AI助手(Ollama):模型选择、聊天咨询提示词、图片附件(视觉模型)",
helpPrompt2: "将AI回复应用到生成UI的提示词",
helpPrompt3: "翻译功能:日\u2194英、中\u2194英",
@@ -1251,7 +1274,7 @@ const LANGUAGES = {
helpSidepanelTitle: "Workflow Studio Library(ComfyUI侧面板)",
helpSidepanel1: "从ComfyUI工具栏的Workflow Studio Library按钮打开",
helpSidepanel2: "Workflows标签:All / ★ Favorites / Groups子标签,All视图中收藏项显示★",
helpSidepanel3: "Nodes标签:All / ★ Favorites / Groups / Sets子标签,All视图中收藏项显示★",
helpSidepanel3: "Nodes标签:All / ★ Favorites / Groups / Sets / 📂 Category / 🧩 Package子标签;Category和Package可通过下拉菜单按分类或自定义节点包筛选节点",
helpSidepanel4: "Prompts标签:浏览提示词预设(全部/收藏/分类 子标签)",
helpSidepanel5: "将工作流拖放到画布上即可加载",
helpSidepanel6: "将节点/节点集拖放到画布上即可放置",
@@ -1260,6 +1283,7 @@ const LANGUAGES = {
helpSidepanel9: "双击项目也可加载/放置",
helpSidepanel10: "在各子标签内搜索快速查找项目",
helpSidepanel11: "Models标签:All / ★ Favorites / Groups / By Type子标签",
helpSidepanel12: "⚙ 主题按钮(标题栏):可自定义面板背景、子标题背景、文字颜色、边框及辅助文字颜色,设置保存至localStorage并在下次打开时生效",
helpShortcutsTitle: "键盘操作",
helpShortcuts1: "Escape:关闭弹窗/面板",
helpShortcuts2: "Enter:确认重命名、发送聊天",
+33 -34
View File
@@ -276,44 +276,43 @@ function renderNodeGrid() {
const pageItems = filtered.slice(start, start + NODES_PER_PAGE);
grid.innerHTML = "";
pageItems.forEach(node => {
const meta = state.nodeMetadata[node.name] || {};
const card = document.createElement("div");
card.className = "wfm-card wfm-node-card";
if (state.selectedNode && state.selectedNode.name === node.name) {
card.classList.add("wfm-card-selected");
}
card.dataset.nodeName = node.name;
card.style.borderLeft = `3px solid ${packageColor(node.package)}`;
const inputCount = Object.keys(node.input.required || {}).length +
Object.keys(node.input.optional || {}).length;
const outputCount = (node.output || []).length;
const tags = (meta.tags || []).map(tg => `<span class="wfm-tag">#${escapeHtml(tg)}</span>`).join("");
const favStar = meta.favorite ? "\u2605" : "\u2606";
const favClass = meta.favorite ? "wfm-fav-btn active" : "wfm-fav-btn";
card.innerHTML = `
<div class="wfm-card-body">
<div class="wfm-card-title" title="${escapeHtml(node.name)}">${escapeHtml(node.display_name)}</div>
<div class="wfm-card-meta">
${categoryBadgeHtml(node.category)}
</div>
${tags ? `<div class="wfm-card-tags">${tags}</div>` : ""}
</div>
<button class="${favClass}" title="Favorite">${favStar}</button>`;
card.querySelector(".wfm-fav-btn").addEventListener("click", e => {
e.stopPropagation();
toggleFavorite(node.name, e.currentTarget);
});
card.addEventListener("click", () => showNodeSidePanel(node));
grid.appendChild(card);
});
pageItems.forEach(node => grid.appendChild(createNodeCard(node)));
renderPagination(filtered.length, totalPages);
}
function createNodeCard(node) {
const meta = state.nodeMetadata[node.name] || {};
const card = document.createElement("div");
card.className = "wfm-card wfm-node-card";
if (state.selectedNode && state.selectedNode.name === node.name) {
card.classList.add("wfm-card-selected");
}
card.dataset.nodeName = node.name;
card.style.borderLeft = `3px solid ${packageColor(node.package)}`;
const tags = (meta.tags || []).map(tg => `<span class="wfm-tag">#${escapeHtml(tg)}</span>`).join("");
const favStar = meta.favorite ? "\u2605" : "\u2606";
const favClass = meta.favorite ? "wfm-fav-btn active" : "wfm-fav-btn";
card.innerHTML = `
<div class="wfm-card-body">
<div class="wfm-card-title" title="${escapeHtml(node.name)}">${escapeHtml(node.display_name)}</div>
<div class="wfm-card-meta">
${categoryBadgeHtml(node.category)}
</div>
${tags ? `<div class="wfm-card-tags">${tags}</div>` : ""}
</div>
<button class="${favClass}" title="Favorite">${favStar}</button>`;
card.querySelector(".wfm-fav-btn").addEventListener("click", e => {
e.stopPropagation();
toggleFavorite(node.name, e.currentTarget);
});
card.addEventListener("click", () => showNodeSidePanel(node));
return card;
}
function renderPagination(totalItems, totalPages) {
const container = document.getElementById("wfm-nodes-pagination");
if (!container) return;
+2
View File
@@ -265,6 +265,8 @@ function renderModelFilters() {
function filterWorkflows() {
return state.workflows.filter((wf) => {
if (wf.filename === ".index.json") return false;
if (state.activeBadge === "FAVORITE") {
if (!wf.metadata.favorite) return false;
} else if (state.activeBadge !== "ALL") {
+60 -40
View File
@@ -279,53 +279,57 @@
<!-- Editor Layout -->
<div class="wfm-gen-layout">
<!-- Left Column: Parameter Editor -->
<!-- Left Column: Parameter Tabs -->
<div class="wfm-gen-left">
<!-- Subtab Navigation -->
<!-- Tab Navigation -->
<div class="wfm-gen-subtab-nav">
<button class="wfm-gen-subtab-btn active" data-subtab="prompt">Prompt</button>
<button class="wfm-gen-subtab-btn" data-subtab="image">Image</button>
<button class="wfm-gen-subtab-btn active" data-subtab="input">Input</button>
<button class="wfm-gen-subtab-btn" data-subtab="model">Model</button>
<button class="wfm-gen-subtab-btn" data-subtab="settings">Settings</button>
<button class="wfm-gen-subtab-btn" data-subtab="raw">Raw JSON</button>
</div>
<!-- Subtab: Prompt -->
<div id="wfm-gen-subtab-prompt" class="wfm-gen-subtab-content active">
<div id="wfm-gen-prompt-fields">
<p class="wfm-placeholder">Load a workflow to edit prompts</p>
<!-- Tab: Input — left sub-col: Prompt(top)+Image(bottom), right sub-col: Raw JSON -->
<div id="wfm-gen-subtab-input" class="wfm-gen-subtab-content active">
<div class="wfm-gen-tab-cols">
<div class="wfm-gen-params-col">
<div class="wfm-gen-params-section">
<h3 class="wfm-gen-col-title">Prompt</h3>
<div id="wfm-gen-prompt-fields">
<p class="wfm-placeholder">Load a workflow to edit prompts</p>
</div>
</div>
<div class="wfm-gen-params-section wfm-gen-params-section--top-border">
<h3 class="wfm-gen-col-title">Image</h3>
<div id="wfm-gen-image-fields">
<p class="wfm-placeholder">Load a workflow to set input images</p>
</div>
</div>
</div>
<div class="wfm-gen-rawjson-col" id="wfm-gen-rawjson-col-input"></div>
</div>
</div>
<!-- Subtab: Image Input -->
<div id="wfm-gen-subtab-image" class="wfm-gen-subtab-content">
<div id="wfm-gen-image-fields">
<p class="wfm-placeholder">Load a workflow to set input images</p>
</div>
</div>
<!-- Subtab: Model -->
<!-- Tab: Model — left sub-col: model selects, right sub-col: Raw JSON -->
<div id="wfm-gen-subtab-model" class="wfm-gen-subtab-content">
<div id="wfm-gen-model-fields">
<p class="wfm-placeholder">Load a workflow to edit models</p>
<div class="wfm-gen-tab-cols">
<div class="wfm-gen-params-col wfm-gen-params-col--scroll">
<div id="wfm-gen-model-fields">
<p class="wfm-placeholder">Load a workflow to edit models</p>
</div>
</div>
<div class="wfm-gen-rawjson-col" id="wfm-gen-rawjson-col-model"></div>
</div>
</div>
<!-- Subtab: Settings -->
<!-- Tab: Settings — left sub-col: KSampler(top)+LatentImage(bottom), right sub-col: Raw JSON -->
<div id="wfm-gen-subtab-settings" class="wfm-gen-subtab-content">
<div id="wfm-gen-settings-fields">
<p class="wfm-placeholder">Load a workflow to edit settings</p>
</div>
</div>
<!-- Subtab: Raw JSON -->
<div id="wfm-gen-subtab-raw" class="wfm-gen-subtab-content">
<div style="margin-bottom:8px;">
<button id="wfm-gen-apply-raw-btn" class="wfm-btn wfm-btn-sm">Apply JSON</button>
</div>
<div class="wfm-json-container" style="height:500px;">
<pre id="wfm-gen-raw-json-highlight" class="wfm-json-highlight"></pre>
<textarea id="wfm-gen-raw-json" class="wfm-json-editor" spellcheck="false" placeholder="Paste workflow JSON here..."></textarea>
<div class="wfm-gen-tab-cols">
<div class="wfm-gen-params-col wfm-gen-params-col--scroll">
<div id="wfm-gen-settings-fields">
<p class="wfm-placeholder">Load a workflow to edit settings</p>
</div>
</div>
<div class="wfm-gen-rawjson-col" id="wfm-gen-rawjson-col-settings"></div>
</div>
</div>
</div>
@@ -350,7 +354,7 @@
<!-- Seed -->
<div style="display:flex;gap:8px;align-items:center;margin-top:8px;">
<label style="font-size:12px;color:var(--wfm-text-secondary);white-space:nowrap;">Seed:</label>
<input type="number" id="wfm-gen-seed-value" class="wfm-input" value="-1" style="width:140px;">
<input type="number" id="wfm-gen-seed-value" class="wfm-input" value="-1" style="width:130px;">
<select id="wfm-gen-seed-mode" class="wfm-select" style="width:auto;">
<option value="random">Random</option>
<option value="fixed">Fixed</option>
@@ -367,6 +371,18 @@
</div>
</div>
</div>
<!-- Raw JSON shared widget (moved into active tab's rawjson-col by JS) -->
<div id="wfm-gen-rawjson-widget" style="display:none;flex-direction:column;height:100%;">
<div class="wfm-gen-rawjson-header">
<span class="wfm-gen-rawjson-label">Raw JSON</span>
<button id="wfm-gen-apply-raw-btn" class="wfm-btn wfm-btn-sm">Apply</button>
</div>
<div class="wfm-json-container wfm-gen-rawjson-editor">
<pre id="wfm-gen-raw-json-highlight" class="wfm-json-highlight"></pre>
<textarea id="wfm-gen-raw-json" class="wfm-json-editor" spellcheck="false" placeholder="Paste workflow JSON here..."></textarea>
</div>
</div>
</section>
<!-- Prompt Tab -->
@@ -510,11 +526,14 @@
<ul>
<li id="wfm-help-gen-1">Connect/disconnect ComfyUI and check connection status</li>
<li id="wfm-help-gen-2">Load workflows from Workflow tab (App format .app.json not supported)</li>
<li id="wfm-help-gen-3">Edit parameters: Prompt, Image (drag &amp; drop), Model (checkpoint/VAE/LoRA/ControlNet with filter), Settings (steps/CFG/sampler/scheduler/resolution), Raw JSON</li>
<li id="wfm-help-gen-4">Seed control: random, fixed, increment, decrement</li>
<li id="wfm-help-gen-5">Generate, progress display, and stop</li>
<li id="wfm-help-gen-6">View results with main image and thumbnail history</li>
<li id="wfm-help-gen-7">Eagle integration: auto-save generated images to Eagle</li>
<li id="wfm-help-gen-3">3-tab layout — Input (Prompt + Image), Model, Settings — each with a Raw JSON column for instant preview and direct editing</li>
<li id="wfm-help-gen-4">Input tab: Prompt (top) and Image drag-and-drop (bottom) in the left column</li>
<li id="wfm-help-gen-5">Model tab: Checkpoint, VAE, LoRA, ControlNet, UNET, TextEncoder selectors with filter</li>
<li id="wfm-help-gen-6">Settings tab: KSampler (top) and LatentImage (bottom) in the left column</li>
<li id="wfm-help-gen-7">Raw JSON always visible in the right column of each tab — edit directly and Apply to reload the workflow</li>
<li id="wfm-help-gen-8">Seed control: random, fixed, increment, decrement</li>
<li id="wfm-help-gen-9">Generate, progress display, stop, and view results</li>
<li id="wfm-help-gen-10">Eagle integration: auto-save generated images to Eagle</li>
</ul>
</div>
@@ -577,7 +596,7 @@
<ul>
<li id="wfm-help-sidepanel-1">Open from the Workflow Studio Library button in the ComfyUI toolbar</li>
<li id="wfm-help-sidepanel-2">Workflows tab: All / ★ Favorites / Groups sub-tabs; ★ shown for favorites in All view</li>
<li id="wfm-help-sidepanel-3">Nodes tab: All / ★ Favorites / Groups / Sets sub-tabs; ★ shown for favorites in All view</li>
<li id="wfm-help-sidepanel-3">Nodes tab: All / ★ Favorites / Groups / Sets / 📂 Category / 🧩 Package sub-tabs; Category and Package show a dropdown to filter nodes</li>
<li id="wfm-help-sidepanel-11">Models tab: All / ★ Favorites / Groups / By Type sub-tabs</li>
<li id="wfm-help-sidepanel-4">Prompts tab: browse prompt presets with All/Favorites/Categories sub-tabs</li>
<li id="wfm-help-sidepanel-5">Drag &amp; drop workflows onto the canvas to load them</li>
@@ -586,6 +605,7 @@
<li id="wfm-help-sidepanel-8">Copy individual positive (P) or negative (N) prompts from sidebar items</li>
<li id="wfm-help-sidepanel-9">Double-click items to load/place without dragging</li>
<li id="wfm-help-sidepanel-10">Search within each sub-tab to quickly find items</li>
<li id="wfm-help-sidepanel-12">⚙ Theme button (header): customize panel background, text, border colors — saved to localStorage</li>
</ul>
</div>
+325 -5
View File
@@ -24,7 +24,10 @@ const state = {
topTab: "workflows", // "workflows" | "nodes" | "prompts" | "models"
// Node sub-tabs (existing)
activeTab: "all", // all | favorites | groups
activeTab2: null, // 2nd row: "sets" (or null when 1st row active)
activeTab2: null, // 2nd row: "sets" | "category" | "package" (or null when 1st row active)
activeNodeCategory: "", // selected category value
activeNodePackage: "", // selected package value
objectInfo: {}, // raw /object_info data for package lookup
favorites: [], // [{name, display_name}]
nodeSets: [],
groups: {}, // {groupName: [nodeName, ...]}
@@ -79,13 +82,28 @@ const fetchGroups = async () => {
} catch { return {}; }
};
const fetchObjectInfo = async () => {
try {
const res = await fetch(`${window.location.origin}/object_info`);
return res.ok ? await res.json() : {};
} catch { return {}; }
};
const extractPackageName = (pythonModule) => {
if (!pythonModule || pythonModule === "nodes") return "ComfyUI (Built-in)";
const parts = pythonModule.split(".");
if (parts[0] === "custom_nodes" && parts.length > 1) return parts[1];
return pythonModule;
};
const loadData = async () => {
const [metadata, nodeSets, groups] = await Promise.all([
fetchMetadata(), fetchNodeSets(), fetchGroups(),
const [metadata, nodeSets, groups, objectInfo] = await Promise.all([
fetchMetadata(), fetchNodeSets(), fetchGroups(), fetchObjectInfo(),
]);
state.metadata = metadata;
state.nodeSets = nodeSets;
state.groups = groups;
state.objectInfo = objectInfo;
// Extract favorites from metadata
state.favorites = [];
@@ -149,7 +167,17 @@ const fetchModelList = async (type) => {
vae: () => fetch(`${comfyBase}/object_info/VAELoader`).then(r => r.json()).then(d => d?.VAELoader?.input?.required?.vae_name?.[0] || []),
controlnet: () => fetch(`${comfyBase}/object_info/ControlNetLoader`).then(r => r.json()).then(d => d?.ControlNetLoader?.input?.required?.control_net_name?.[0] || []),
unet: () => fetch(`${comfyBase}/object_info/UNETLoader`).then(r => r.json()).then(d => d?.UNETLoader?.input?.required?.unet_name?.[0] || []),
textencoder: () => fetch(`${comfyBase}/object_info/CLIPLoader`).then(r => r.json()).then(d => d?.CLIPLoader?.input?.required?.clip_name1?.[0] || []),
textencoder: async () => {
const base = window.location.origin;
for (const cls of ["DualCLIPLoader", "CLIPLoader"]) {
try {
const d = await fetch(`${base}/object_info/${cls}`).then(r => r.json());
const list = d?.[cls]?.input?.required?.clip_name1?.[0];
if (list?.length) return list;
} catch {}
}
return [];
},
hypernetwork:() => fetch(`${comfyBase}/object_info/HypernetworkLoader`).then(r => r.json()).then(d => d?.HypernetworkLoader?.input?.required?.hypernetwork_name?.[0] || []),
embedding: () => fetch(`${comfyBase}/embeddings`).then(r => r.json()).then(d => Array.isArray(d) ? d : []),
};
@@ -199,7 +227,7 @@ const fetchWorkflowRaw = async (filename) => {
const loadWfData = async () => {
const workflows = await fetchWorkflows();
state.wfList = workflows;
state.wfList = workflows.filter(w => w.filename !== ".index.json");
// Extract favorites
state.wfFavorites = workflows.filter(w => w.metadata?.favorite);
@@ -496,6 +524,76 @@ let panelEl = null;
const PANEL_ID = "wfm-node-library-panel";
// ============================================
// Theme
// ============================================
const THEME_KEY = "wfm_nlp_theme";
const THEME_VARS = [
{ key: "bg", label: "Background", cssVar: "--comfy-menu-bg", default: "#1e1e1e" },
{ key: "input", label: "Sub-header BG", cssVar: "--comfy-input-bg", default: "#2a2a2a" },
{ key: "text", label: "Text", cssVar: "--input-text", default: "#dddddd" },
{ key: "border", label: "Border", cssVar: "--border-color", default: "#4e4e4e" },
{ key: "desc", label: "Secondary text", cssVar: "--descrip-text", default: "#888888" },
];
function loadTheme() {
try { return JSON.parse(localStorage.getItem(THEME_KEY)) || {}; } catch { return {}; }
}
function applyTheme(panel, theme) {
THEME_VARS.forEach(({ key, cssVar, default: def }) => {
panel.style.setProperty(cssVar, theme[key] || def);
});
}
function buildThemePanel(panel) {
const themePanel = panel.querySelector(".wfm-nlp-theme-panel");
if (!themePanel) return;
const theme = loadTheme();
themePanel.innerHTML = `
<div class="wfm-nlp-theme-title">Panel Theme</div>
${THEME_VARS.map(({ key, label, default: def }) => `
<div class="wfm-nlp-theme-row">
<span class="wfm-nlp-theme-label">${label}</span>
<input type="color" class="wfm-nlp-theme-color" data-key="${key}"
value="${theme[key] || def}">
</div>`).join("")}
<div class="wfm-nlp-theme-actions">
<button class="wfm-nlp-theme-reset-btn">Reset</button>
<button class="wfm-nlp-theme-save-btn">Save</button>
</div>`;
themePanel.querySelector(".wfm-nlp-theme-save-btn").addEventListener("click", () => {
const saved = {};
themePanel.querySelectorAll(".wfm-nlp-theme-color").forEach(input => {
saved[input.dataset.key] = input.value;
});
localStorage.setItem(THEME_KEY, JSON.stringify(saved));
applyTheme(panel, saved);
themePanel.style.display = "none";
});
themePanel.querySelector(".wfm-nlp-theme-reset-btn").addEventListener("click", () => {
localStorage.removeItem(THEME_KEY);
THEME_VARS.forEach(({ cssVar, default: def }) => {
panel.style.setProperty(cssVar, def);
});
themePanel.style.display = "none";
});
// Live preview on color change
themePanel.querySelectorAll(".wfm-nlp-theme-color").forEach(input => {
input.addEventListener("input", () => {
panel.style.setProperty(
THEME_VARS.find(v => v.key === input.dataset.key).cssVar,
input.value
);
});
});
}
const createPanel = () => {
if (panelEl) return panelEl;
@@ -504,9 +602,11 @@ const createPanel = () => {
panel.innerHTML = `
<div class="wfm-nlp-header">
<span class="wfm-nlp-title">Workflow Studio Library</span>
<button class="wfm-nlp-theme-btn" title="Theme settings">&#9881;</button>
<button class="wfm-nlp-refresh" title="Refresh">&#8635;</button>
<button class="wfm-nlp-close" title="Close">&times;</button>
</div>
<div class="wfm-nlp-theme-panel" style="display:none;"></div>
<div class="wfm-nlp-tabs">
<button class="wfm-nlp-tab wfm-nlp-top-tab active" data-toptab="workflows">Workflows</button>
<button class="wfm-nlp-tab wfm-nlp-top-tab" data-toptab="nodes">Nodes</button>
@@ -584,7 +684,24 @@ const createPanel = () => {
showToast("Library refreshed", "success");
});
// Theme button
panel.querySelector(".wfm-nlp-theme-btn").addEventListener("click", () => {
const themePanel = panel.querySelector(".wfm-nlp-theme-panel");
if (!themePanel) return;
const isOpen = themePanel.style.display !== "none";
if (isOpen) {
themePanel.style.display = "none";
} else {
buildThemePanel(panel);
themePanel.style.display = "block";
}
});
injectStyles();
// Apply saved theme
applyTheme(panel, loadTheme());
rebuildSubTabs();
return panel;
};
@@ -715,6 +832,8 @@ const rebuildSubTabs = () => {
];
const row2Tabs = [
{ key: "sets", label: "\u2630 Sets" },
{ key: "category", label: "\ud83d\udcc2 Category" },
{ key: "package", label: "\ud83e\udde9 Package" },
];
const activeKey = state.activeTab2 || state.activeTab;
@@ -757,6 +876,9 @@ const renderContent = () => {
const content = panelEl?.querySelector(".wfm-nlp-content");
if (!content) return;
// Remove filter dropdowns from previous category/package views
panelEl.querySelectorAll(".wfm-nlp-filter-row").forEach(e => e.remove());
if (state.topTab === "models") {
if (!state.modelsLoaded) {
content.innerHTML = `<div class="wfm-nlp-empty">Loading...</div>`;
@@ -802,6 +924,8 @@ const renderContent = () => {
case "favorites": renderFavorites(content); break;
case "sets": renderSets(content); break;
case "groups": renderGroups(content); break;
case "category": renderNodesByCategory(content); break;
case "package": renderNodesByPackage(content); break;
}
}
};
@@ -1583,6 +1707,133 @@ const renderGroups = (container) => {
}
};
// ============================================
// Render – Nodes by Category / Package
// ============================================
const getNodeCategory = (nodeType) => {
const info = state.objectInfo[nodeType];
if (info?.category) return info.category.split("/")[0] || "uncategorized";
// Fallback: LiteGraph type string often contains category as prefix
const def = typeof LiteGraph !== "undefined"
? LiteGraph.registered_node_types[nodeType]
: null;
const cat = def?.category || "";
return cat.split("/")[0] || "uncategorized";
};
const getNodePackage = (nodeType) => {
const info = state.objectInfo[nodeType];
if (!info) return "ComfyUI (Built-in)";
return extractPackageName(info.python_module || "");
};
const renderNodesByCategory = (container) => {
const registered = typeof LiteGraph !== "undefined" ? LiteGraph.registered_node_types : {};
const nodeNames = Object.keys(registered).sort();
// Build category list
const catSet = new Set();
nodeNames.forEach(n => catSet.add(getNodeCategory(n)));
const categories = [...catSet].sort();
// Insert dropdown above content (remove old one first)
container.parentNode.querySelectorAll(".wfm-nlp-filter-row").forEach(e => e.remove());
const wrap = document.createElement("div");
wrap.className = "wfm-nlp-filter-row";
wrap.style.cssText = "padding:6px 8px;border-bottom:1px solid var(--border-color,#4e4e4e);flex-shrink:0;";
const sel = document.createElement("select");
sel.style.cssText = "width:100%;padding:4px 6px;background:var(--comfy-input-bg,#2a2a2a);border:1px solid var(--border-color,#4e4e4e);border-radius:3px;color:var(--input-text,#ddd);font-size:12px;";
sel.innerHTML = `<option value="">-- All Categories --</option>` +
categories.map(c => `<option value="${esc(c)}"${c === state.activeNodeCategory ? " selected" : ""}>${esc(c)}</option>`).join("");
sel.addEventListener("change", () => {
state.activeNodeCategory = sel.value;
renderNodesByCategoryList(container);
});
wrap.appendChild(sel);
container.parentNode.insertBefore(wrap, container);
renderNodesByCategoryList(container);
};
const renderNodesByCategoryList = (container) => {
const registered = typeof LiteGraph !== "undefined" ? LiteGraph.registered_node_types : {};
let nodeNames = Object.keys(registered).sort();
if (state.activeNodeCategory) {
nodeNames = nodeNames.filter(n => getNodeCategory(n) === state.activeNodeCategory);
}
if (state.searchText) {
nodeNames = nodeNames.filter(n => n.toLowerCase().includes(state.searchText));
}
container.innerHTML = "";
if (nodeNames.length === 0) {
container.innerHTML = `<div class="wfm-nlp-empty">No nodes found</div>`;
return;
}
for (const name of nodeNames) {
const isFav = state.metadata[name]?.favorite;
const label = isFav ? `<span class="wfm-nlp-fav-star">\u2605</span>${esc(name)}` : esc(name);
const el = createDraggableItem(label, "single", { classType: name });
el.addEventListener("dblclick", () => placeSingleNode(name));
container.appendChild(el);
}
};
const renderNodesByPackage = (container) => {
const registered = typeof LiteGraph !== "undefined" ? LiteGraph.registered_node_types : {};
const nodeNames = Object.keys(registered).sort();
// Build package list
const pkgSet = new Set();
nodeNames.forEach(n => pkgSet.add(getNodePackage(n)));
const packages = [...pkgSet].sort();
// Insert dropdown above content (remove old one first)
container.parentNode.querySelectorAll(".wfm-nlp-filter-row").forEach(e => e.remove());
const wrap = document.createElement("div");
wrap.className = "wfm-nlp-filter-row";
wrap.style.cssText = "padding:6px 8px;border-bottom:1px solid var(--border-color,#4e4e4e);flex-shrink:0;";
const sel = document.createElement("select");
sel.style.cssText = "width:100%;padding:4px 6px;background:var(--comfy-input-bg,#2a2a2a);border:1px solid var(--border-color,#4e4e4e);border-radius:3px;color:var(--input-text,#ddd);font-size:12px;";
sel.innerHTML = `<option value="">-- All Packages --</option>` +
packages.map(p => `<option value="${esc(p)}"${p === state.activeNodePackage ? " selected" : ""}>${esc(p)}</option>`).join("");
sel.addEventListener("change", () => {
state.activeNodePackage = sel.value;
renderNodesByPackageList(container);
});
wrap.appendChild(sel);
container.parentNode.insertBefore(wrap, container);
renderNodesByPackageList(container);
};
const renderNodesByPackageList = (container) => {
const registered = typeof LiteGraph !== "undefined" ? LiteGraph.registered_node_types : {};
let nodeNames = Object.keys(registered).sort();
if (state.activeNodePackage) {
nodeNames = nodeNames.filter(n => getNodePackage(n) === state.activeNodePackage);
}
if (state.searchText) {
nodeNames = nodeNames.filter(n => n.toLowerCase().includes(state.searchText));
}
container.innerHTML = "";
if (nodeNames.length === 0) {
container.innerHTML = `<div class="wfm-nlp-empty">No nodes found</div>`;
return;
}
for (const name of nodeNames) {
const isFav = state.metadata[name]?.favorite;
const label = isFav ? `<span class="wfm-nlp-fav-star">\u2605</span>${esc(name)}` : esc(name);
const el = createDraggableItem(label, "single", { classType: name });
el.addEventListener("dblclick", () => placeSingleNode(name));
container.appendChild(el);
}
};
// ============================================
// Draggable item factory (nodes)
// ============================================
@@ -2062,6 +2313,75 @@ const injectStyles = () => {
background: rgba(255,255,255,0.12);
color: var(--descrip-text, #aaa);
}
.wfm-nlp-theme-btn {
background: none;
border: none;
color: var(--input-text, #ddd);
font-size: 15px;
cursor: pointer;
opacity: 0.7;
padding: 0 2px;
line-height: 1;
}
.wfm-nlp-theme-btn:hover { opacity: 1; }
.wfm-nlp-theme-panel {
border-bottom: 1px solid var(--border-color, #4e4e4e);
background: var(--comfy-input-bg, #2a2a2a);
padding: 10px 12px;
flex-shrink: 0;
}
.wfm-nlp-theme-title {
font-size: 11px;
font-weight: 600;
color: var(--input-text, #ddd);
margin-bottom: 8px;
opacity: 0.8;
}
.wfm-nlp-theme-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.wfm-nlp-theme-label {
font-size: 11px;
color: var(--descrip-text, #aaa);
flex: 1;
white-space: nowrap;
}
.wfm-nlp-theme-color {
width: 32px;
height: 22px;
border: 1px solid var(--border-color, #555);
border-radius: 3px;
padding: 1px;
cursor: pointer;
background: transparent;
}
.wfm-nlp-theme-actions {
display: flex;
gap: 6px;
margin-top: 8px;
justify-content: flex-end;
}
.wfm-nlp-theme-save-btn, .wfm-nlp-theme-reset-btn {
font-size: 11px;
padding: 3px 10px;
border-radius: 3px;
cursor: pointer;
border: 1px solid var(--border-color, #555);
}
.wfm-nlp-theme-save-btn {
background: #4a9eff;
color: #fff;
border-color: #4a9eff;
}
.wfm-nlp-theme-save-btn:hover { background: #3a8eef; }
.wfm-nlp-theme-reset-btn {
background: none;
color: var(--descrip-text, #aaa);
}
.wfm-nlp-theme-reset-btn:hover { color: var(--input-text, #ddd); }
`;
document.head.appendChild(style);
};