Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40955f2822 | ||
|
|
de6da654ca | ||
|
|
e7fdfc41ad | ||
|
|
fe47460e00 |
@@ -1,5 +1,322 @@
|
||||
# DEVLOG - ComfyUI-Workflow-Studio
|
||||
|
||||
## 2026-04-06: v0.2.6 UI改善・バッジ保存修正・フォルダフィルター追加
|
||||
|
||||
### 概要
|
||||
- ワークフロー/モデル/ノードタブのサイドパネルを常時表示固定に変更
|
||||
- グリッドをページネーションからスクロール全件表示に変更
|
||||
- ワークフロータブのバッジが再起動後に消える問題を修正
|
||||
- バッジフィルターボタンをパレット定義バッジで統一(ワークフロータブ)
|
||||
- モデルタブにフォルダ(サブディレクトリ)フィルターを追加
|
||||
- モデルカードビューから拡張子・フォルダバッジを削除(詳細パネルで確認可能)
|
||||
- サムネイルカードサイズを 178×200px に調整、グリッドレイアウトを修正
|
||||
|
||||
### 変更内容
|
||||
|
||||
#### `py/services/workflow_service.py` — バッジ保存修正
|
||||
|
||||
**`save_metadata` のキーリストに `badges` を追加:**
|
||||
- 変更前: `("tags", "memo", "summary", "modelTypesOverride", "favorite")`
|
||||
- 変更後: `("tags", "memo", "summary", "modelTypesOverride", "favorite", "badges")`
|
||||
|
||||
**`list_workflows` のレスポンスに `badges` フィールドを追加:**
|
||||
- `metadata` dict に `"badges": meta.get("badges", [])` を追加
|
||||
- これにより再起動後もバッジが保持される
|
||||
|
||||
#### `static/js/workflow-tab.js` — バッジフィルター・サイドパネル・スクロール
|
||||
|
||||
**バッジフィルターをパレット定義バッジで表示:**
|
||||
- `renderModelFilters()`: `getAllBadges()`(ワークフローに付いているバッジのみ)→ `getBadgePalette()`(パレット全バッジ)に変更
|
||||
- フィルターボタンにバッジカラーを適用
|
||||
|
||||
**サイドパネル常時表示化:**
|
||||
- `showSidePanel()`: `panel.style.display = "flex"` を削除
|
||||
- `closeSidePanel()`: `panel.style.display = "none"` を削除、タイトルをクリアするのみ
|
||||
- ×ボタンのイベントリスナー削除
|
||||
|
||||
**ページネーション廃止・スクロール全件表示:**
|
||||
- `WF_PER_PAGE` 定数削除
|
||||
- `updateWfPagination()` 関数削除
|
||||
- `renderGrid()`: ページスライス処理を削除、全件描画に変更
|
||||
|
||||
#### `static/js/models-tab.js` — サイドパネル・スクロール・フォルダフィルター・カードビュー
|
||||
|
||||
**サイドパネル常時表示化:**
|
||||
- `showSidePanel()`: `panel.style.display = "flex"` を削除
|
||||
- `closeSidePanel()`: `panel.style.display = "none"` を削除
|
||||
|
||||
**ページネーション廃止・スクロール全件表示:**
|
||||
- `MODELS_PER_PAGE` 定数削除、`updatePagination()` 関数削除
|
||||
- `renderThumbView()` / `renderCardView()` / `renderTableView()`: `totalPages` 引数削除、全件描画に変更
|
||||
|
||||
**フォルダフィルター追加:**
|
||||
- `state.dirFilter: ""` 追加
|
||||
- `filterModels()` に `dirFilter` による絞り込みを追加
|
||||
- `renderDirFilter()` 関数追加(現在のモデルタイプのサブディレクトリ一覧を `<select>` に反映)
|
||||
- モデルロード時・モデルタイプ切替時に呼び出し
|
||||
- `wfm-models-dir-filter` change イベントリスナー追加
|
||||
|
||||
**モデルカードビューからバッジ削除:**
|
||||
- `renderCardView()`: `dir` バッジ・`ext` バッジを削除。ユーザーバッジ・タグのみ表示
|
||||
|
||||
#### `static/js/nodes-tab.js` — サイドパネル・スクロール
|
||||
|
||||
**サイドパネル常時表示化:**
|
||||
- `showNodeSidePanel()`: `panel.style.display = "flex"` を削除
|
||||
- `closeNodeSidePanel()`: `panel.style.display = "none"` を削除
|
||||
|
||||
**ページネーション廃止・スクロール全件表示:**
|
||||
- `NODES_PER_PAGE` / `renderPagination()` / `clearPagination()` / `scrollGridToTop()` 削除
|
||||
- `renderNodeGrid()`: 全件描画に変更
|
||||
|
||||
#### `templates/index.html` — サイドパネル・ページネーション・フォルダフィルター
|
||||
|
||||
- 3タブ(ワークフロー/ノード/モデル)のサイドパネルから `style="display:none"` と×ボタンを削除
|
||||
- ページネーション用 `<div>` を3箇所から削除
|
||||
- モデルタブのツールバーに `<select id="wfm-models-dir-filter">` を追加(TagとGroupの間)
|
||||
|
||||
#### `static/css/main.css` — グリッド・カードサイズ・サイドパネル
|
||||
|
||||
**グリッド・カードサイズ変更:**
|
||||
- グリッド列幅: `161px` → `178px`
|
||||
- カード幅: `161px` → `178px`
|
||||
- サムネイル: `161×162px` → `178×200px`
|
||||
- `grid-auto-rows: max-content` / `align-items: start` / `justify-items: start` 追加(行高さ圧縮防止)
|
||||
|
||||
**カードビュー:**
|
||||
- グリッド列幅: `minmax(180px, 1fr)` → `repeat(auto-fill, 220px)` 固定幅に変更(横伸び防止)
|
||||
- カード: `flex-direction: row` / `min-height: 56px` / `width: 220px` 固定
|
||||
|
||||
**サイドパネル:**
|
||||
- `.wfm-side-panel-close` スタイル削除
|
||||
- `.wfm-side-panel-empty` クラス追加(選択前の空状態表示用)
|
||||
|
||||
#### `static/js/i18n.js` — フォルダフィルター翻訳追加
|
||||
|
||||
- `modelsAllDirs`: 英語 `"All Folders"` / 日本語 `"すべてのフォルダ"` / 中国語 `"所有文件夹"` を追加
|
||||
|
||||
---
|
||||
|
||||
## 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拡充
|
||||
|
||||
### 概要
|
||||
- ワークフローとモデルのバッジ体系を統一(共有パレット `wfm_models_badge_palette`、自動解析廃止)
|
||||
- ModelsタブにGenUI Modelボタン追加(詳細モーダル・サイドパネル両対応)
|
||||
- ComfyUIサイドパネルのModelsタブにFavorites/Groups/By Typeサブタブを追加
|
||||
- サイドパネルのWorkflows・Nodesサブタブ名を「All」に統一、お気に入りに★表示
|
||||
- サイドパネル幅を280px→310pxに拡張
|
||||
|
||||
### 変更内容
|
||||
|
||||
#### `web/comfyui/node_sets_menu.js` — サイドパネルModels拡充
|
||||
|
||||
- **Modelsタブのサブタブ構成を2段に変更:**
|
||||
- row1: All / ★ Favorites / 📁 Groups
|
||||
- row2: ◦ By Type
|
||||
- `state.modelSubTab2` で行2の選択を管理
|
||||
- **`renderModelFavorites(container)`** — 全モデルを取得し `state.modelMetadata[name]?.favorite === true` のものを表示
|
||||
- **`renderModelGroups(container)`** — `state.modelGroups` からコラプス式セクションを生成
|
||||
- **Workflowsサブタブ名:** `"Workflows"` → `"All"`
|
||||
- **Nodesサブタブ名:** `"Nodes"` → `"All"`
|
||||
- **Allビューでお気に入りに★表示:**
|
||||
- `createDraggableWfItem`: `wf.metadata?.favorite` に応じて `<span class="wfm-nlp-fav-star">★</span>` を追加
|
||||
- `renderAllNodes`: `state.metadata[name]?.favorite` に応じて★を表示
|
||||
- **`.wfm-nlp-fav-star`** スタイル追加(`color: #f5c518; font-size: 11px; margin-right: 3px`)
|
||||
- **サイドパネル幅:** `width: 280px` → `width: 310px`
|
||||
- **`getWfModelTypes` → `getWfBadges`** にリネーム(`wf.metadata?.badges` ベースに変更)
|
||||
- **`wfModelTypes` state → `wfBadgeTypes`** にリネーム
|
||||
- `renderWfModelType` がバッジ表示に `state.wfBadgeTypes` を使用
|
||||
|
||||
#### `static/js/models-tab.js` — GenUI Modelボタン・バッジ表示改善
|
||||
|
||||
- **型バッジの削除:** ThumbView・CardView・SidePanelの `renderSideInfo` から `typeBadge = badgeHtml(typeLabel)` を削除。ユーザー定義バッジのみ表示
|
||||
- **`GENUI_TYPE_MAP` 追加:**
|
||||
```javascript
|
||||
const GENUI_TYPE_MAP = {
|
||||
checkpoint: { key: "checkpoints", inputKey: "ckpt_name" },
|
||||
lora: { key: "loras", inputKey: "lora_name" },
|
||||
vae: { key: "vaes", inputKey: "vae_name" },
|
||||
controlnet: { key: "controlNets", inputKey: "control_net_name" },
|
||||
unet: { key: "diffusionModels", inputKey: "unet_name" },
|
||||
textencoder: { key: "textEncoders", inputKey: "clip_name1" },
|
||||
};
|
||||
```
|
||||
hypernetwork / embedding は対象外
|
||||
- **`applyToGenUI(modelName, modelType)`** 追加:
|
||||
- `comfyUI.currentWorkflow` からターゲット `inputKey` を持つノードを検索
|
||||
- `node.inputs[inputKey] = modelName` で直接セット
|
||||
- `<select id="wfm-model-${key}">` の表示値も同期
|
||||
- `<textarea id="wfm-gen-raw-json">` も同期
|
||||
- 成功トースト表示
|
||||
- **詳細モーダルの actions に「GenUI Model」ボタン追加**(`GENUI_TYPE_MAP` に対応するタイプのみ表示)
|
||||
- **サイドパネルの Save ボタン右隣に「GenUI Model」ボタン追加**(同上)
|
||||
- **`export function openBadgeEditModal(onPaletteChange = null)`** — workflow-tab.js から呼び出せるよう export 化
|
||||
- **`bindBadgeModalEvents(onPaletteChange)`** — パレット変更後に `renderBadgeFilter()`, `renderModelGrid()`, `onPaletteChange()` を実行
|
||||
|
||||
#### `static/js/workflow-tab.js` — バッジ体系をモデルタブと統一
|
||||
|
||||
- **`import { openBadgeEditModal } from "./models-tab.js"`** 追加
|
||||
- **自動解析によるバッジ体系を廃止:**
|
||||
- `state.badgeColors`, `BADGE_DEFAULT_COLORS` 削除
|
||||
- 「Analysis」「Override」セクションを詳細モーダルから完全削除
|
||||
- `getAllModelTypes()` → `getAllBadges()` (`wf.metadata?.badges` ベース)
|
||||
- `state.activeModel` → `state.activeBadge`
|
||||
- **共有パレット関数を追加:** `getBadgePalette()`, `saveBadgePalette()` (`wfm_models_badge_palette` localStorage キー共有)
|
||||
- **`badgeHtml(label)`** が共有パレットを参照するように変更
|
||||
- **`wfBadgesHtml(wf)`** ヘルパー追加(`wf.metadata?.badges` から表示用バッジHTML生成)
|
||||
- **`filterWorkflows()`** が `wf.metadata?.badges` でフィルタリング
|
||||
- **詳細モーダルにバッジチェックボックスUI追加**(Modelsタブと同じ管理UI)
|
||||
- **⚙ボタン** → `renderViewSettings()`(ビュー設定のみ)
|
||||
- **「⚙ Badge」ボタン追加** → `openBadgeEditModal(() => renderGrid())`
|
||||
|
||||
#### `templates/index.html` — Workflowツールバー更新
|
||||
|
||||
- `<button id="wfm-reanalyze-btn">Reanalyze All</button>` 削除
|
||||
- `<button id="wfm-badge-btn">⚙ Badge</button>` 追加
|
||||
- ⚙設定ボタンの title を "View settings" に変更
|
||||
|
||||
#### `static/js/i18n.js` — 翻訳追加(EN/JA/ZH)
|
||||
|
||||
- **GenUI Model関連(3言語):**
|
||||
- `modelsGenUIBtn`: "GenUI Model"
|
||||
- `modelsGenUITitle`: ボタンのツールチップ
|
||||
- `modelsGenUIUnsupported`: 非対応タイプへの警告
|
||||
- `modelsGenUINoWorkflow`: GenUIにワークフローが未読み込みの警告
|
||||
- `modelsGenUINoNode(type)`: 対応ノードが見つからない場合の警告(アロー関数)
|
||||
|
||||
### 技術的な判断
|
||||
|
||||
- **バッジ統一の方針:** 自動解析モデルタイプはユーザー操作が必要なオーバーライドが多く UX が複雑だったため廃止。完全にユーザー定義バッジへ移行し、ワークフロー/モデル間で同じパレットを共有する設計に統一
|
||||
- **GenUI連携方式:** `comfyUI.currentWorkflow` を直接ミューテーション。`<select>` 要素の DOM 同期も行うことで視覚的なフィードバックを確保
|
||||
- **`openBadgeEditModal` のexport:** モデルタブとワークフロータブが同じパレット管理 UI を共有するため、`export function` に変更。`onPaletteChange` コールバックで各タブが独立してグリッドを再描画
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-28: v0.2.2 Modelsタブ・CivitAI連携
|
||||
|
||||
### 概要
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
## Screenshots
|
||||
|
||||
@@ -36,10 +36,10 @@ Browse, organize, and execute workflows directly from a dedicated studio interfa
|
||||
### Workflow Tab
|
||||
- **Thumbnail / Card / Table views** — switch between view modes to browse your workflow library
|
||||
- **Thumbnail side panel** — preview workflow canvas snapshots in the side panel
|
||||
- **Tag-based filtering** — filter by base model (SD1.5, SDXL, etc.) and custom groups
|
||||
- **Badge filtering** — filter by user-defined badges (free labels you assign to each workflow)
|
||||
- **Search** — full-text search across workflow names and metadata
|
||||
- **Side panel tabs** — Thumbnail preview, JSON viewer with syntax highlighting, and Group management
|
||||
- **Batch analysis** — auto-detect checkpoint, model type, prompt, and I/O node counts
|
||||
- **Badge management** — add, rename, delete badges with custom colors shared with the Models tab (⚙ Badge button)
|
||||
- **AI summary** — generate workflow descriptions using Ollama
|
||||
- **Import / Export** — import workflows from files or clipboard, open in ComfyUI directly
|
||||
- **Default view setting** — persist your preferred view mode (Thumbnail / Card / Table)
|
||||
@@ -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
|
||||
|
||||
@@ -87,21 +90,26 @@ Browse, organize, and execute workflows directly from a dedicated studio interfa
|
||||
- **Node Sets** — save multiple nodes + connections as reusable sets from the ComfyUI canvas
|
||||
- **Right-click context menu** — "Save as Node Set" option on any node in ComfyUI
|
||||
|
||||
### Models Tab (v0.2.2)
|
||||
- **Model Browser** — browse all installed ComfyUI models (Checkpoint, LoRA, VAE, ControlNet, UNET, TextEncoder) with sub-tab switching
|
||||
### Models Tab (v0.2.3)
|
||||
- **Model Browser** — browse all installed ComfyUI models (Checkpoint, LoRA, VAE, ControlNet, UNET, TextEncoder, Hypernetwork, Embedding) with sub-tab switching
|
||||
- **Thumbnail / Card / Table views** — switch between view modes with pagination (24 items per page)
|
||||
- **Search & Filter** — full-text search, filter by tags, groups, and favorites
|
||||
- **User-defined badges** — assign free-label badges to models; badge colors shared with the Workflow tab palette
|
||||
- **Side panel tabs** — Info (file path display with click-to-copy, tags, memo), Groups management, CivitAI integration
|
||||
- **CivitAI integration** — fetch model metadata by SHA256 hash, view base model, trained words, tags, and model page link
|
||||
- **Batch CivitAI fetch** — one-click batch fetch for all models of a type with SSE progress streaming
|
||||
- **Detail modal** — preview image, CivitAI info, thumbnail change via file upload
|
||||
- **GenUI Model button** — apply the selected model directly to the corresponding node in GenerateUI's current workflow (Checkpoint, LoRA, VAE, ControlNet, UNET, TextEncoder)
|
||||
- **Group management** — create, rename, delete groups and assign/remove models
|
||||
- **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.1.8)
|
||||
- **Workflows tab** — browse favorite workflows, filter by model type, filter by group
|
||||
- **Nodes tab** — browse favorite nodes, node sets, and node groups
|
||||
### 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 / 📂 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
|
||||
- **Drag & drop nodes** — drag nodes/node sets onto the canvas to place them
|
||||
@@ -109,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
|
||||
@@ -185,6 +194,25 @@ 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)
|
||||
- **Sidebar Models tab** — added All / ★ Favorites / Groups / By Type sub-tabs to the Models section of Workflow Studio Library
|
||||
- **Sidebar All views** — ★ star displayed for favorite items in Workflows and Nodes All sub-tabs
|
||||
- **Sidebar panel width** — widened from 280px to 310px
|
||||
|
||||
### v0.2.2
|
||||
- **Models tab** — browse, search, and manage all installed ComfyUI models (Checkpoint, LoRA, VAE, ControlNet, UNET, TextEncoder) with Thumbnail/Card/Table views
|
||||
- **CivitAI integration** — fetch model metadata by SHA256 hash (individual or batch per model type) with SSE progress streaming
|
||||
|
||||
@@ -21,6 +21,8 @@ MODEL_TYPE_FOLDER_KEYS = {
|
||||
"controlnet": "controlnet",
|
||||
"unet": "diffusion_models",
|
||||
"textencoder": "text_encoders",
|
||||
"hypernetwork": "hypernetworks",
|
||||
"embedding": "embeddings",
|
||||
}
|
||||
|
||||
|
||||
@@ -91,6 +93,8 @@ class ModelsService:
|
||||
entry["memo"] = updates["memo"]
|
||||
if "sha256" in updates:
|
||||
entry["sha256"] = updates["sha256"]
|
||||
if "badges" in updates:
|
||||
entry["badges"] = updates["badges"]
|
||||
entry["updatedAt"] = self._now_iso()
|
||||
data[model_name] = entry
|
||||
self._save_metadata(data)
|
||||
|
||||
@@ -112,6 +112,7 @@ class WorkflowService:
|
||||
"summary": meta.get("summary", ""),
|
||||
"modelTypesOverride": override_types,
|
||||
"favorite": bool(meta.get("favorite", False)),
|
||||
"badges": meta.get("badges", []),
|
||||
},
|
||||
"mtime": stat.st_mtime,
|
||||
"thumbnail": thumbnail,
|
||||
@@ -132,7 +133,7 @@ class WorkflowService:
|
||||
"""Save metadata (tags, memo, summary, etc.) for a workflow."""
|
||||
metadata = self._load_metadata()
|
||||
entry = metadata.get(filename, {})
|
||||
for key in ("tags", "memo", "summary", "modelTypesOverride", "favorite"):
|
||||
for key in ("tags", "memo", "summary", "modelTypesOverride", "favorite", "badges"):
|
||||
if key in updates:
|
||||
entry[key] = updates[key]
|
||||
entry["updatedAt"] = self._now_iso()
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
[project]
|
||||
name = "comfyui-workflow-studio"
|
||||
description = "A comprehensive workflow management and generation UI plugin for ComfyUI. Browse, organize, and execute workflows from a dedicated studio interface."
|
||||
version = "0.2.6"
|
||||
license = "MIT"
|
||||
requires-python = ">= 3.10"
|
||||
dependencies = ["jinja2"]
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://github.com/ketle-man/ComfyUI-Workflow-Studio"
|
||||
|
||||
[tool.comfy]
|
||||
PublisherId = "ketle-man"
|
||||
DisplayName = "Workflow Studio"
|
||||
Icon = "https://raw.githubusercontent.com/ketle-man/ComfyUI-Workflow-Studio/main/assets/icon.svg"
|
||||
Banner = "https://raw.githubusercontent.com/ketle-man/ComfyUI-Workflow-Studio/main/assets/banner.svg"
|
||||
+214
-37
@@ -250,12 +250,15 @@ body {
|
||||
|
||||
.wfm-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, 161px);
|
||||
grid-template-columns: repeat(auto-fill, 178px);
|
||||
grid-auto-rows: max-content;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
align-content: start;
|
||||
align-items: start;
|
||||
justify-items: start;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
@@ -263,8 +266,8 @@ body {
|
||||
============================================ */
|
||||
|
||||
.wfm-card {
|
||||
width: 161px;
|
||||
min-width: 161px;
|
||||
width: 178px;
|
||||
min-width: 178px;
|
||||
background: var(--wfm-surface);
|
||||
border: 1px solid var(--wfm-border);
|
||||
border-radius: var(--wfm-radius);
|
||||
@@ -280,8 +283,8 @@ body {
|
||||
}
|
||||
|
||||
.wfm-card-thumb {
|
||||
width: 161px;
|
||||
height: 162px;
|
||||
width: 178px;
|
||||
height: 200px;
|
||||
background: var(--wfm-bg-tertiary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -505,11 +508,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 +550,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 +618,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 +648,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;
|
||||
@@ -743,26 +830,38 @@ body {
|
||||
============================================ */
|
||||
|
||||
.wfm-view-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)) !important;
|
||||
gap: 6px !important;
|
||||
grid-template-columns: repeat(auto-fill, 220px) !important;
|
||||
grid-auto-rows: max-content !important;
|
||||
gap: 8px !important;
|
||||
justify-items: stretch !important;
|
||||
align-items: start !important;
|
||||
}
|
||||
|
||||
.wfm-view-card .wfm-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: 220px !important;
|
||||
min-width: 220px !important;
|
||||
min-height: 56px;
|
||||
}
|
||||
|
||||
.wfm-view-card .wfm-card-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 8px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.wfm-view-card .wfm-fav-btn {
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.wfm-view-card .wfm-card-title {
|
||||
margin-bottom: 4px;
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
margin-bottom: 2px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -794,6 +893,13 @@ body {
|
||||
.wfm-view-table {
|
||||
display: block !important;
|
||||
overflow-y: auto;
|
||||
justify-items: stretch !important;
|
||||
}
|
||||
|
||||
.wfm-view-table .wfm-card,
|
||||
.wfm-view-table .wfm-model-card {
|
||||
width: auto !important;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
.wfm-table {
|
||||
@@ -923,6 +1029,7 @@ body {
|
||||
border-bottom: 1px solid var(--wfm-border);
|
||||
background: var(--wfm-surface);
|
||||
flex-shrink: 0;
|
||||
min-height: 37px;
|
||||
}
|
||||
|
||||
.wfm-side-panel-title {
|
||||
@@ -934,18 +1041,13 @@ body {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.wfm-side-panel-close {
|
||||
background: none;
|
||||
border: none;
|
||||
.wfm-side-panel-empty {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--wfm-text-secondary);
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
padding: 0 4px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.wfm-side-panel-close:hover {
|
||||
color: var(--wfm-text);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Side Panel Tabs */
|
||||
@@ -1195,6 +1297,79 @@ body {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Badge Filter Bar (model type row)
|
||||
============================================ */
|
||||
|
||||
.wfm-models-type-divider {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
background: var(--wfm-border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.wfm-badge-filter-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.wfm-badge-filter-btn {
|
||||
padding: 2px 10px;
|
||||
font-size: 11px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid transparent;
|
||||
background: var(--wfm-bg-tertiary);
|
||||
color: var(--wfm-text-secondary);
|
||||
cursor: pointer;
|
||||
transition: opacity 0.15s, box-shadow 0.15s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wfm-badge-filter-btn:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.wfm-badge-filter-btn.active {
|
||||
box-shadow: 0 0 0 2px var(--wfm-primary);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Badge checkbox list in modal
|
||||
============================================ */
|
||||
|
||||
.wfm-badge-checkboxes {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.wfm-badge-check-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wfm-badge-check-label input[type="checkbox"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wfm-badge-check-label input[type="checkbox"]:not(:checked) + .wfm-badge {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.wfm-badge-clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wfm-badge-clickable:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Modal Detail - Two Column Layout
|
||||
============================================ */
|
||||
@@ -1820,7 +1995,7 @@ body {
|
||||
}
|
||||
|
||||
.wfm-side-thumb-img-wrap {
|
||||
width: 100%;
|
||||
width: 90%;
|
||||
aspect-ratio: 1 / 1;
|
||||
background: var(--wfm-bg);
|
||||
border: 1px solid var(--wfm-border);
|
||||
@@ -1829,6 +2004,7 @@ body {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.wfm-side-thumb-img-wrap img {
|
||||
@@ -2727,6 +2903,7 @@ body {
|
||||
#wfm-nodes-grid.wfm-view-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 8px;
|
||||
justify-items: stretch;
|
||||
}
|
||||
|
||||
/* Node card: package color left border, auto width */
|
||||
|
||||
+5
-2
@@ -252,10 +252,11 @@ function applyI18nToHtml() {
|
||||
"wfm-help-about-1": "helpAbout1", "wfm-help-about-2": "helpAbout2",
|
||||
"wfm-help-wf-1": "helpWf1", "wfm-help-wf-2": "helpWf2", "wfm-help-wf-3": "helpWf3",
|
||||
"wfm-help-wf-4": "helpWf4", "wfm-help-wf-5": "helpWf5", "wfm-help-wf-6": "helpWf6",
|
||||
"wfm-help-wf-7": "helpWf7", "wfm-help-wf-8": "helpWf8",
|
||||
"wfm-help-wf-7": "helpWf7",
|
||||
"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",
|
||||
@@ -270,13 +271,15 @@ function applyI18nToHtml() {
|
||||
"wfm-help-models-1": "helpModels1", "wfm-help-models-2": "helpModels2",
|
||||
"wfm-help-models-3": "helpModels3", "wfm-help-models-4": "helpModels4",
|
||||
"wfm-help-models-5": "helpModels5", "wfm-help-models-6": "helpModels6",
|
||||
"wfm-help-models-7": "helpModels7",
|
||||
"wfm-help-models-7": "helpModels7", "wfm-help-models-8": "helpModels8",
|
||||
"wfm-help-models-9": "helpModels9",
|
||||
"wfm-help-sidepanel-title": "helpSidepanelTitle",
|
||||
"wfm-help-sidepanel-1": "helpSidepanel1", "wfm-help-sidepanel-2": "helpSidepanel2",
|
||||
"wfm-help-sidepanel-3": "helpSidepanel3", "wfm-help-sidepanel-4": "helpSidepanel4",
|
||||
"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-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",
|
||||
|
||||
@@ -134,6 +134,25 @@ export const comfyUI = {
|
||||
);
|
||||
},
|
||||
|
||||
async fetchHypernetworks() {
|
||||
return this._fetchModelList(
|
||||
["HypernetworkLoader"],
|
||||
"hypernetwork_name"
|
||||
);
|
||||
},
|
||||
|
||||
async fetchEmbeddings() {
|
||||
// ComfyUI exposes embeddings via /embeddings endpoint
|
||||
try {
|
||||
const res = await fetch(`${this.baseUrl}/embeddings`);
|
||||
if (res.ok) {
|
||||
const list = await res.json();
|
||||
return Array.isArray(list) ? list : [];
|
||||
}
|
||||
} catch {}
|
||||
return [];
|
||||
},
|
||||
|
||||
async fetchSamplers() {
|
||||
return this._fetchModelList(["KSampler"], "sampler_name");
|
||||
},
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
+108
-33
@@ -31,6 +31,13 @@ const LANGUAGES = {
|
||||
import: "Import",
|
||||
badgeColors: "Badge Colors",
|
||||
resetColors: "Reset",
|
||||
badgeManage: "Manage Badges",
|
||||
badgeAdd: "Add",
|
||||
badgeDelete: "Delete",
|
||||
badgeNone: "No badges defined yet.",
|
||||
badgeNoneHint: "No badges defined. Click \u2699 Manage Badges to add.",
|
||||
badgeNewLabel: "Badge label...",
|
||||
badgeColorHint: "Change color",
|
||||
defaultView: "Default View",
|
||||
viewThumbnail: "Thumbnail",
|
||||
viewCard: "Card",
|
||||
@@ -160,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",
|
||||
@@ -225,11 +236,18 @@ const LANGUAGES = {
|
||||
modelsTags: "Tags",
|
||||
modelsTagsPlaceholder: "tag1, tag2, ...",
|
||||
modelsModelType: "Model Type",
|
||||
modelsBadges: "Badges",
|
||||
modelsInfo: "Info",
|
||||
modelsFavorite: "Favorite",
|
||||
modelsMemo: "Memo",
|
||||
modelsMemoPlaceholder: "Notes about this model...",
|
||||
modelsSave: "Save",
|
||||
modelsSaved: "Metadata saved",
|
||||
modelsGenUIBtn: "GenUI Model",
|
||||
modelsGenUITitle: "Set this model in GenerateUI",
|
||||
modelsGenUIUnsupported: "This model type cannot be set in GenerateUI",
|
||||
modelsGenUINoWorkflow: "No workflow loaded in GenerateUI",
|
||||
modelsGenUINoNode: (type) => `No ${type} node found in current workflow`,
|
||||
modelsCopyName: "Click to copy model name",
|
||||
modelsCopiedName: "Model name copied",
|
||||
modelsFilePath: "File Path",
|
||||
@@ -237,6 +255,7 @@ const LANGUAGES = {
|
||||
modelsCopiedPath: "File path copied",
|
||||
modelsNoPreview: "No preview",
|
||||
modelsTagsHint: "(comma separated)",
|
||||
modelsAllDirs: "All Folders",
|
||||
modelsAllGroups: "All Groups",
|
||||
modelsCurrentGroups: "Current Groups",
|
||||
modelsNoGroup: "Not assigned to any group",
|
||||
@@ -283,16 +302,18 @@ const LANGUAGES = {
|
||||
helpWf3: "Create, rename, delete groups and assign/remove workflows",
|
||||
helpWf4: "Import JSON/PNG files via button or drag & drop",
|
||||
helpWf5: "Side panel: thumbnail, JSON viewer (format badge: UI/API/App), clipboard copy, group management",
|
||||
helpWf6: "Detail modal: rename, favorite, thumbnail change, analysis, category override, tags, memo, AI summary, open in ComfyUI, load in GenerateUI, set as default, delete",
|
||||
helpWf7: "Customize badge colors per category",
|
||||
helpWf8: "Batch re-analysis of all workflow I/O (P: prompt nodes, I: image nodes, output: save nodes)",
|
||||
helpWf6: "Detail modal: rename, favorite, thumbnail change, badge assignment, tags, memo, AI summary, open in ComfyUI, load in GenerateUI, set as default, delete",
|
||||
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",
|
||||
@@ -314,17 +335,19 @@ const LANGUAGES = {
|
||||
helpNodes4: "Create, rename, delete node groups and assign/remove nodes",
|
||||
helpNodes5: "Side panel: Details (class name, package, category, description, flags), I/O Spec (inputs/outputs with types and defaults), Groups management",
|
||||
helpNodes6: "Node Sets: create reusable node collections with descriptions, tags, and link definitions between nodes",
|
||||
helpModels1: "Browse installed models: Checkpoint, LoRA, VAE, ControlNet, UNET, TextEncoder with sub-tab switching",
|
||||
helpModels1: "Browse installed models: Checkpoint, LoRA, VAE, ControlNet, UNET, TextEncoder, Hypernetwork, Embedding with sub-tab switching",
|
||||
helpModels2: "Thumbnail, Card, and Table view modes with pagination",
|
||||
helpModels3: "Search, tag filter, group filter, and favorites filter",
|
||||
helpModels4: "Side panel: Info (file path, tags, memo), Groups management, CivitAI integration",
|
||||
helpModels5: "CivitAI integration: fetch model info by SHA256 hash, batch fetch per model type",
|
||||
helpModels6: "Detail modal: preview image, CivitAI info (base model, trained words, tags), thumbnail change",
|
||||
helpModels7: "Group management: create, rename, delete groups and assign/remove models",
|
||||
helpModels8: "GenUI Model button: apply the selected model to the corresponding node in GenerateUI's current workflow (Checkpoint / LoRA / VAE / ControlNet / UNET / TextEncoder)",
|
||||
helpModels9: "User-defined badges: assign free-label badges with custom colors shared with the Workflow tab",
|
||||
helpSidepanelTitle: "Workflow Studio Library (ComfyUI Side Panel)",
|
||||
helpSidepanel1: "Open from the Workflow Studio Library button in the ComfyUI toolbar",
|
||||
helpSidepanel2: "Workflows tab: browse favorite workflows, filter by model type, and filter by group",
|
||||
helpSidepanel3: "Nodes tab: browse favorite nodes, node sets, and node groups",
|
||||
helpSidepanel2: "Workflows tab: All / ★ Favorites / Groups 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",
|
||||
@@ -332,6 +355,8 @@ const LANGUAGES = {
|
||||
helpSidepanel8: "Copy individual positive (P) or negative (N) prompts from sidebar items",
|
||||
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",
|
||||
@@ -473,6 +498,13 @@ const LANGUAGES = {
|
||||
import: "インポート",
|
||||
badgeColors: "バッジ色",
|
||||
resetColors: "リセット",
|
||||
badgeManage: "バッジ管理",
|
||||
badgeAdd: "追加",
|
||||
badgeDelete: "削除",
|
||||
badgeNone: "バッジが未定義です。",
|
||||
badgeNoneHint: "バッジが未定義です。⚙ バッジ管理から追加できます。",
|
||||
badgeNewLabel: "バッジ名...",
|
||||
badgeColorHint: "色を変更",
|
||||
defaultView: "デフォルト表示",
|
||||
viewThumbnail: "サムネイル",
|
||||
viewCard: "カード",
|
||||
@@ -602,6 +634,10 @@ const LANGUAGES = {
|
||||
tabNodes: "ノード",
|
||||
nodesAllNodes: "全ノード",
|
||||
nodesNodeSets: "ノードセット",
|
||||
nodesCategoryTab: "カテゴリ",
|
||||
nodesPackageTab: "パッケージ",
|
||||
nodesSelectCategory: "カテゴリを選択...",
|
||||
nodesSelectPackage: "パッケージを選択...",
|
||||
nodesSearchPlaceholder: "ノードを検索...",
|
||||
nodesAllCategories: "全カテゴリ",
|
||||
nodesAllPackages: "全パッケージ",
|
||||
@@ -667,11 +703,18 @@ const LANGUAGES = {
|
||||
modelsTags: "タグ",
|
||||
modelsTagsPlaceholder: "タグ1, タグ2, ...",
|
||||
modelsModelType: "モデルタイプ",
|
||||
modelsBadges: "バッジ",
|
||||
modelsInfo: "情報",
|
||||
modelsFavorite: "お気に入り",
|
||||
modelsMemo: "メモ",
|
||||
modelsMemoPlaceholder: "このモデルについてのメモ...",
|
||||
modelsSave: "保存",
|
||||
modelsSaved: "メタデータを保存しました",
|
||||
modelsGenUIBtn: "GenUI Model",
|
||||
modelsGenUITitle: "GenerateUIにこのモデルをセット",
|
||||
modelsGenUIUnsupported: "このモデルタイプはGenerateUIに設定できません",
|
||||
modelsGenUINoWorkflow: "GenerateUIにワークフローが読み込まれていません",
|
||||
modelsGenUINoNode: (type) => `現在のワークフローに${type}ノードが見つかりません`,
|
||||
modelsCopyName: "クリックでモデル名をコピー",
|
||||
modelsCopiedName: "モデル名をコピーしました",
|
||||
modelsFilePath: "ファイルパス",
|
||||
@@ -679,6 +722,7 @@ const LANGUAGES = {
|
||||
modelsCopiedPath: "ファイルパスをコピーしました",
|
||||
modelsNoPreview: "プレビューなし",
|
||||
modelsTagsHint: "(カンマ区切り)",
|
||||
modelsAllDirs: "すべてのフォルダ",
|
||||
modelsAllGroups: "すべてのグループ",
|
||||
modelsCurrentGroups: "所属グループ",
|
||||
modelsNoGroup: "グループに未所属",
|
||||
@@ -725,16 +769,18 @@ const LANGUAGES = {
|
||||
helpWf3: "グループの作成・名前変更・削除、ワークフローの割り当て・解除",
|
||||
helpWf4: "JSON/PNGファイルのインポート(ボタンまたはドラッグ&ドロップ)",
|
||||
helpWf5: "サイドパネル:サムネイル、JSONビューア(フォーマットバッジ:UI/API/App形式)、クリップボードコピー、グループ管理",
|
||||
helpWf6: "詳細モーダル:名前変更、お気に入り、サムネイル変更、分析、カテゴリオーバーライド、タグ、メモ、AI要約、ComfyUIで開く、生成UIに読み込み、デフォルト設定、削除",
|
||||
helpWf7: "カテゴリごとのバッジ色カスタマイズ",
|
||||
helpWf8: "全ワークフローの入出力一括再分析(P:プロンプト入力、I:画像入力、出力:saveノード)",
|
||||
helpWf6: "詳細モーダル:名前変更、お気に入り、サムネイル変更、バッジ付与、タグ、メモ、AI要約、ComfyUIで開く、生成UIに読み込み、デフォルト設定、削除",
|
||||
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: "翻訳機能:日⇔英、中⇔英",
|
||||
@@ -756,17 +802,19 @@ const LANGUAGES = {
|
||||
helpNodes4: "ノードグループの作成・名前変更・削除、ノードの割り当て・解除",
|
||||
helpNodes5: "サイドパネル:詳細(クラス名、パッケージ、カテゴリ、説明、フラグ)、I/O仕様(入出力の型・デフォルト値)、グループ管理",
|
||||
helpNodes6: "ノードセット:再利用可能なノードコレクションの作成(説明、タグ、ノード間リンク定義付き)",
|
||||
helpModels1: "インストール済みモデルの閲覧:Checkpoint、LoRA、VAE、ControlNet、UNET、TextEncoderをサブタブで切り替え",
|
||||
helpModels1: "インストール済みモデルの閲覧:Checkpoint、LoRA、VAE、ControlNet、UNET、TextEncoder、Hypernetwork、Embeddingをサブタブで切り替え",
|
||||
helpModels2: "サムネイル / カード / テーブル表示モード切り替え(ページネーション付き)",
|
||||
helpModels3: "検索、タグフィルタ、グループフィルタ、お気に入りフィルタ",
|
||||
helpModels4: "サイドパネル:基本情報(ファイルパス、タグ、メモ)、グループ管理、CivitAI連携",
|
||||
helpModels5: "CivitAI連携:SHA256ハッシュでモデル情報取得、モデルタイプごとの一括取得",
|
||||
helpModels6: "詳細モーダル:プレビュー画像、CivitAI情報(ベースモデル、トリガーワード、タグ)、サムネイル変更",
|
||||
helpModels7: "グループ管理:グループの作成・名前変更・削除、モデルの割り当て・解除",
|
||||
helpModels8: "GenUI Modelボタン:選択モデルをGenerateUIの現在のワークフローの該当ノードに直接適用(Checkpoint / LoRA / VAE / ControlNet / UNET / TextEncoder)",
|
||||
helpModels9: "ユーザー定義バッジ:自由ラベルのバッジをモデルに付与(Workflowタブと色パレットを共有)",
|
||||
helpSidepanelTitle: "Workflow Studio Library(ComfyUIサイドパネル)",
|
||||
helpSidepanel1: "ComfyUIツールバーのWorkflow Studio Libraryボタンから開く",
|
||||
helpSidepanel2: "Workflowsタブ:お気に入りワークフロー閲覧、モデル種別フィルタ、グループフィルタ",
|
||||
helpSidepanel3: "Nodesタブ:お気に入りノード、ノードセット、ノードグループの閲覧",
|
||||
helpSidepanel2: "Workflowsタブ:All / ★ Favorites / Groupsサブタブ、Allビューでお気に入りに★表示",
|
||||
helpSidepanel3: "Nodesタブ:All / ★ Favorites / Groups / Sets / 📂 Category / 🧩 Packageサブタブ;CategoryとPackageはドロップダウンでカテゴリ・カスタムノードパッケージ別に絞り込み可能",
|
||||
helpSidepanel4: "Promptsタブ:プロンプトプリセットの閲覧(All/お気に入り/カテゴリ サブタブ)",
|
||||
helpSidepanel5: "ワークフローをキャンバスにドラッグ&ドロップで読み込み",
|
||||
helpSidepanel6: "ノード/ノードセットをキャンバスにドラッグ&ドロップで配置",
|
||||
@@ -774,6 +822,8 @@ const LANGUAGES = {
|
||||
helpSidepanel8: "サイドバーアイテムからポジティブ(P)/ネガティブ(N)プロンプトを個別コピー",
|
||||
helpSidepanel9: "ダブルクリックでもアイテムの読み込み・配置が可能",
|
||||
helpSidepanel10: "各サブタブ内で検索してアイテムをすばやく検索",
|
||||
helpSidepanel11: "Modelsタブ:All / ★ Favorites / Groups / By Typeサブタブ",
|
||||
helpSidepanel12: "⚙ テーマボタン(ヘッダー):パネル背景・サブヘッダー背景・文字色・ボーダー・補助テキスト色をカスタマイズ可能、設定はlocalStorageに保存され次回起動時も反映",
|
||||
helpShortcutsTitle: "キーボード操作",
|
||||
helpShortcuts1: "Escape:モーダル・パネルを閉じる",
|
||||
helpShortcuts2: "Enter:名前変更の確定、チャット送信",
|
||||
@@ -915,6 +965,13 @@ const LANGUAGES = {
|
||||
import: "导入",
|
||||
badgeColors: "标签颜色",
|
||||
resetColors: "重置",
|
||||
badgeManage: "管理标签",
|
||||
badgeAdd: "添加",
|
||||
badgeDelete: "删除",
|
||||
badgeNone: "尚未定义标签。",
|
||||
badgeNoneHint: "尚未定义标签。点击 ⚙ 管理标签来添加。",
|
||||
badgeNewLabel: "标签名称...",
|
||||
badgeColorHint: "更改颜色",
|
||||
defaultView: "默认视图",
|
||||
viewThumbnail: "缩略图",
|
||||
viewCard: "卡片",
|
||||
@@ -1040,6 +1097,10 @@ const LANGUAGES = {
|
||||
tabNodes: "节点",
|
||||
nodesAllNodes: "所有节点",
|
||||
nodesNodeSets: "节点集",
|
||||
nodesCategoryTab: "分类",
|
||||
nodesPackageTab: "包",
|
||||
nodesSelectCategory: "选择分类...",
|
||||
nodesSelectPackage: "选择包...",
|
||||
nodesSearchPlaceholder: "搜索节点...",
|
||||
nodesAllCategories: "所有分类",
|
||||
nodesAllPackages: "所有包",
|
||||
@@ -1105,11 +1166,18 @@ const LANGUAGES = {
|
||||
modelsTags: "标签",
|
||||
modelsTagsPlaceholder: "标签1, 标签2, ...",
|
||||
modelsModelType: "模型类型",
|
||||
modelsBadges: "标签",
|
||||
modelsInfo: "信息",
|
||||
modelsFavorite: "收藏",
|
||||
modelsMemo: "备注",
|
||||
modelsMemoPlaceholder: "关于此模型的备注...",
|
||||
modelsSave: "保存",
|
||||
modelsSaved: "元数据已保存",
|
||||
modelsGenUIBtn: "GenUI Model",
|
||||
modelsGenUITitle: "在GenerateUI中设置此模型",
|
||||
modelsGenUIUnsupported: "此模型类型无法在GenerateUI中设置",
|
||||
modelsGenUINoWorkflow: "GenerateUI中未加载工作流",
|
||||
modelsGenUINoNode: (type) => `当前工作流中未找到${type}节点`,
|
||||
modelsCopyName: "点击复制模型名称",
|
||||
modelsCopiedName: "模型名称已复制",
|
||||
modelsFilePath: "文件路径",
|
||||
@@ -1117,6 +1185,7 @@ const LANGUAGES = {
|
||||
modelsCopiedPath: "文件路径已复制",
|
||||
modelsNoPreview: "无预览",
|
||||
modelsTagsHint: "(逗号分隔)",
|
||||
modelsAllDirs: "所有文件夹",
|
||||
modelsAllGroups: "所有分组",
|
||||
modelsCurrentGroups: "当前分组",
|
||||
modelsNoGroup: "未分配到任何分组",
|
||||
@@ -1163,16 +1232,18 @@ const LANGUAGES = {
|
||||
helpWf3: "创建、重命名、删除分组,分配/移除工作流",
|
||||
helpWf4: "通过按钮或拖放导入JSON/PNG文件",
|
||||
helpWf5: "侧面板:缩略图、JSON查看器(格式徽章:UI/API/App)、剪贴板复制、分组管理",
|
||||
helpWf6: "详情弹窗:重命名、收藏、缩略图更换、分析、分类覆盖、标签、备注、AI摘要、在ComfyUI中打开、加载到生成UI、设为默认、删除",
|
||||
helpWf7: "按分类自定义徽章颜色",
|
||||
helpWf8: "批量重新分析所有工作流I/O(P:提示词节点、I:图片节点、输出:保存节点)",
|
||||
helpWf6: "详情弹窗:重命名、收藏、缩略图更换、徽章分配、标签、备注、AI摘要、在ComfyUI中打开、加载到生成UI、设为默认、删除",
|
||||
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英",
|
||||
@@ -1194,17 +1265,19 @@ const LANGUAGES = {
|
||||
helpNodes4: "创建、重命名、删除节点组,分配/移除节点",
|
||||
helpNodes5: "侧面板:详情(类名、包、分类、描述、标志)、I/O规格(输入输出类型和默认值)、组管理",
|
||||
helpNodes6: "节点集:创建可复用的节点集合(含描述、标签、节点间链接定义)",
|
||||
helpModels1: "浏览已安装模型:Checkpoint、LoRA、VAE、ControlNet、UNET、TextEncoder,通过子标签切换",
|
||||
helpModels1: "浏览已安装模型:Checkpoint、LoRA、VAE、ControlNet、UNET、TextEncoder、Hypernetwork、Embedding,通过子标签切换",
|
||||
helpModels2: "缩略图、卡片和表格视图模式切换(带分页)",
|
||||
helpModels3: "搜索、标签筛选、分组筛选、收藏筛选",
|
||||
helpModels4: "侧面板:基本信息(文件路径、标签、备注)、分组管理、CivitAI集成",
|
||||
helpModels5: "CivitAI集成:通过SHA256哈希获取模型信息,按模型类型批量获取",
|
||||
helpModels6: "详情弹窗:预览图片、CivitAI信息(基础模型、触发词、标签)、缩略图更改",
|
||||
helpModels7: "分组管理:创建、重命名、删除分组,分配/移除模型",
|
||||
helpModels8: "GenUI Model按钮:将选中模型直接应用到GenerateUI当前工作流的对应节点(Checkpoint / LoRA / VAE / ControlNet / UNET / TextEncoder)",
|
||||
helpModels9: "用户自定义徽章:为模型添加自由标签徽章(颜色调色板与Workflow标签共享)",
|
||||
helpSidepanelTitle: "Workflow Studio Library(ComfyUI侧面板)",
|
||||
helpSidepanel1: "从ComfyUI工具栏的Workflow Studio Library按钮打开",
|
||||
helpSidepanel2: "Workflows标签:浏览收藏工作流、按模型类型筛选、按分组筛选",
|
||||
helpSidepanel3: "Nodes标签:浏览收藏节点、节点集、节点组",
|
||||
helpSidepanel2: "Workflows标签:All / ★ Favorites / Groups子标签,All视图中收藏项显示★",
|
||||
helpSidepanel3: "Nodes标签:All / ★ Favorites / Groups / Sets / 📂 Category / 🧩 Package子标签;Category和Package可通过下拉菜单按分类或自定义节点包筛选节点",
|
||||
helpSidepanel4: "Prompts标签:浏览提示词预设(全部/收藏/分类 子标签)",
|
||||
helpSidepanel5: "将工作流拖放到画布上即可加载",
|
||||
helpSidepanel6: "将节点/节点集拖放到画布上即可放置",
|
||||
@@ -1212,6 +1285,8 @@ const LANGUAGES = {
|
||||
helpSidepanel8: "从侧边栏项目中单独复制正面(P)或负面(N)提示词",
|
||||
helpSidepanel9: "双击项目也可加载/放置",
|
||||
helpSidepanel10: "在各子标签内搜索快速查找项目",
|
||||
helpSidepanel11: "Models标签:All / ★ Favorites / Groups / By Type子标签",
|
||||
helpSidepanel12: "⚙ 主题按钮(标题栏):可自定义面板背景、子标题背景、文字颜色、边框及辅助文字颜色,设置保存至localStorage并在下次打开时生效",
|
||||
helpShortcutsTitle: "键盘操作",
|
||||
helpShortcuts1: "Escape:关闭弹窗/面板",
|
||||
helpShortcuts2: "Enter:确认重命名、发送聊天",
|
||||
|
||||
+311
-96
@@ -6,6 +6,7 @@
|
||||
import { showToast, openModal, closeModal } from "./app.js";
|
||||
import { t } from "./i18n.js";
|
||||
import { comfyUI } from "./comfyui-client.js";
|
||||
import { comfyEditor } from "./comfyui-editor.js";
|
||||
|
||||
// ── State ─────────────────────────────────────────────────
|
||||
|
||||
@@ -17,12 +18,16 @@ const state = {
|
||||
controlnet: [],
|
||||
unet: [],
|
||||
textencoder: [],
|
||||
hypernetwork: [],
|
||||
embedding: [],
|
||||
},
|
||||
modelMetadata: {},
|
||||
modelGroups: {},
|
||||
civitaiCache: {},
|
||||
searchText: "",
|
||||
tagFilter: "",
|
||||
badgeFilter: "",
|
||||
dirFilter: "",
|
||||
groupFilter: "",
|
||||
showFavoritesOnly: false,
|
||||
viewMode: localStorage.getItem("wfm_models_view") || "thumb",
|
||||
@@ -30,10 +35,8 @@ const state = {
|
||||
selectedModel: null,
|
||||
loaded: {},
|
||||
currentPage: 0,
|
||||
badgeColors: JSON.parse(localStorage.getItem("wfm_models_badge_colors") || "{}"),
|
||||
};
|
||||
|
||||
const MODELS_PER_PAGE = 24;
|
||||
|
||||
const FETCH_MAP = {
|
||||
checkpoint: () => comfyUI.fetchCheckpoints(),
|
||||
@@ -42,6 +45,8 @@ const FETCH_MAP = {
|
||||
controlnet: () => comfyUI.fetchControlNets(),
|
||||
unet: () => comfyUI.fetchDiffusionModels(),
|
||||
textencoder: () => comfyUI.fetchTextEncoders(),
|
||||
hypernetwork: () => comfyUI.fetchHypernetworks(),
|
||||
embedding: () => comfyUI.fetchEmbeddings(),
|
||||
};
|
||||
|
||||
const TYPE_LABELS = {
|
||||
@@ -51,18 +56,80 @@ const TYPE_LABELS = {
|
||||
controlnet: "ControlNet",
|
||||
unet: "UNET",
|
||||
textencoder: "TextEncoder",
|
||||
hypernetwork: "Hypernetwork",
|
||||
embedding: "Embedding",
|
||||
};
|
||||
|
||||
// Default badge colors for model types
|
||||
const DEFAULT_BADGE_COLORS = {
|
||||
Checkpoint: "#6366f1",
|
||||
LoRA: "#f59e0b",
|
||||
VAE: "#10b981",
|
||||
ControlNet: "#ef4444",
|
||||
UNET: "#8b5cf6",
|
||||
TextEncoder: "#06b6d4",
|
||||
// Mapping from models-tab type → comfyui-editor key + inputKey
|
||||
const GENUI_TYPE_MAP = {
|
||||
checkpoint: { key: "checkpoints", inputKey: "ckpt_name" },
|
||||
lora: { key: "loras", inputKey: "lora_name" },
|
||||
vae: { key: "vaes", inputKey: "vae_name" },
|
||||
controlnet: { key: "controlNets", inputKey: "control_net_name" },
|
||||
unet: { key: "diffusionModels",inputKey: "unet_name" },
|
||||
textencoder: { key: "textEncoders", inputKey: "clip_name1" },
|
||||
};
|
||||
|
||||
function applyToGenUI(modelName, modelType) {
|
||||
const mapping = GENUI_TYPE_MAP[modelType];
|
||||
if (!mapping) {
|
||||
showToast(t("modelsGenUIUnsupported"), "warning");
|
||||
return;
|
||||
}
|
||||
if (!comfyUI.currentWorkflow) {
|
||||
showToast(t("modelsGenUINoWorkflow"), "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
const { key, inputKey } = mapping;
|
||||
|
||||
// Find the first matching node in the current workflow
|
||||
const nodeId = Object.keys(comfyUI.currentWorkflow).find((id) => {
|
||||
const node = comfyUI.currentWorkflow[id];
|
||||
return node.inputs && inputKey in node.inputs;
|
||||
});
|
||||
|
||||
if (!nodeId) {
|
||||
showToast(t("modelsGenUINoNode", TYPE_LABELS[modelType] || modelType), "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
comfyUI.currentWorkflow[nodeId].inputs[inputKey] = modelName;
|
||||
|
||||
// Sync GenUI select element if visible
|
||||
const selectEl = document.getElementById(`wfm-model-${key}`);
|
||||
if (selectEl) {
|
||||
// Add option if not present (subdir paths may not be in list)
|
||||
if (![...selectEl.options].some(o => o.value === modelName)) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = modelName;
|
||||
opt.textContent = modelName;
|
||||
selectEl.appendChild(opt);
|
||||
}
|
||||
selectEl.value = modelName;
|
||||
}
|
||||
|
||||
// Sync raw JSON display
|
||||
const rawTextarea = document.getElementById("wfm-gen-raw-json");
|
||||
if (rawTextarea) {
|
||||
rawTextarea.value = JSON.stringify(comfyUI.currentWorkflow, null, 2);
|
||||
}
|
||||
|
||||
showToast(`${TYPE_LABELS[modelType]}: ${modelName.split("/").pop().split("\\").pop()}`, "success");
|
||||
}
|
||||
|
||||
// User-defined badge colors (stored in metadata per model)
|
||||
// Global badge palette: label → color, stored in localStorage
|
||||
function getBadgePalette() {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem("wfm_models_badge_palette") || "{}");
|
||||
} catch { return {}; }
|
||||
}
|
||||
|
||||
function saveBadgePalette(palette) {
|
||||
localStorage.setItem("wfm_models_badge_palette", JSON.stringify(palette));
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────
|
||||
|
||||
function escapeHtml(s) {
|
||||
@@ -112,11 +179,25 @@ function loadPreviewImage(imgEl, placeholderEl, modelName, modelType) {
|
||||
}
|
||||
|
||||
function badgeHtml(label) {
|
||||
const color = state.badgeColors[label] || DEFAULT_BADGE_COLORS[label] || "";
|
||||
const palette = getBadgePalette();
|
||||
const color = palette[label] || "";
|
||||
const style = color ? ` style="background:${color};color:#fff;"` : "";
|
||||
return `<span class="wfm-badge wfm-badge-model"${style}>${escapeHtml(label)}</span>`;
|
||||
}
|
||||
|
||||
function modelBadgesHtml(modelName, clickable = false) {
|
||||
const meta = state.modelMetadata[modelName] || {};
|
||||
const badges = meta.badges || [];
|
||||
if (badges.length === 0) return "";
|
||||
return badges.map((label) => {
|
||||
const palette = getBadgePalette();
|
||||
const color = palette[label] || "";
|
||||
const style = color ? ` style="background:${color};color:#fff;"` : "";
|
||||
const dataAttr = clickable ? ` data-badge-filter="${escapeHtml(label)}"` : "";
|
||||
return `<span class="wfm-badge wfm-badge-model${clickable ? " wfm-badge-clickable" : ""}"${style}${dataAttr}>${escapeHtml(label)}</span>`;
|
||||
}).join("");
|
||||
}
|
||||
|
||||
// ── API ───────────────────────────────────────────────────
|
||||
|
||||
async function fetchModelMetadata() {
|
||||
@@ -169,6 +250,20 @@ function filterModels() {
|
||||
});
|
||||
}
|
||||
|
||||
if (state.badgeFilter) {
|
||||
models = models.filter((m) => {
|
||||
const meta = state.modelMetadata[m];
|
||||
return meta && meta.badges && meta.badges.includes(state.badgeFilter);
|
||||
});
|
||||
}
|
||||
|
||||
if (state.dirFilter) {
|
||||
models = models.filter((m) => {
|
||||
const { dir } = parseModelPath(m);
|
||||
return dir === state.dirFilter;
|
||||
});
|
||||
}
|
||||
|
||||
if (state.groupFilter) {
|
||||
const members = state.modelGroups[state.groupFilter] || [];
|
||||
models = models.filter((m) => members.includes(m));
|
||||
@@ -198,28 +293,121 @@ function getAllTags() {
|
||||
return [...set].sort();
|
||||
}
|
||||
|
||||
// ── Badge Color Settings ──────────────────────────────────
|
||||
// ── Badge Management ──────────────────────────────────────
|
||||
|
||||
function renderBadgeSettings() {
|
||||
const panel = document.getElementById("wfm-models-badge-settings-panel");
|
||||
if (!panel) return;
|
||||
export function openBadgeEditModal(onPaletteChange = null) {
|
||||
const palette = getBadgePalette();
|
||||
const labels = Object.keys(palette).sort();
|
||||
|
||||
const labels = Object.keys(TYPE_LABELS).map((k) => TYPE_LABELS[k]);
|
||||
panel.innerHTML = labels
|
||||
.map((label) => {
|
||||
const color = state.badgeColors[label] || DEFAULT_BADGE_COLORS[label] || "#6366f1";
|
||||
return `<div class="wfm-badge-color-row">
|
||||
const rowsHtml = labels.map((label) => {
|
||||
const color = palette[label] || "#6366f1";
|
||||
return `<div class="wfm-badge-color-row" data-badge-label="${escapeHtml(label)}">
|
||||
${badgeHtml(label)}
|
||||
<input type="color" value="${color}" data-badge-label="${escapeHtml(label)}" class="wfm-badge-color-input" title="${t("badgeColorHint")}">
|
||||
<button class="wfm-btn wfm-btn-sm wfm-btn-danger wfm-badge-delete-btn" data-badge-label="${escapeHtml(label)}" title="${t("badgeDelete")}">×</button>
|
||||
</div>`;
|
||||
}).join("");
|
||||
|
||||
const html = `
|
||||
<div style="min-width:320px;">
|
||||
<div id="wfm-badge-list">${rowsHtml || `<p style="color:var(--wfm-text-secondary);font-size:12px;">${t("badgeNone")}</p>`}</div>
|
||||
<div style="border-top:1px solid var(--wfm-border);margin-top:12px;padding-top:12px;display:flex;gap:6px;align-items:center;">
|
||||
<input type="text" id="wfm-badge-new-label" class="wfm-input" style="flex:1;" placeholder="${t("badgeNewLabel")}">
|
||||
<input type="color" id="wfm-badge-new-color" value="#6366f1" style="width:36px;height:28px;padding:1px;border-radius:4px;cursor:pointer;">
|
||||
<button class="wfm-btn wfm-btn-sm wfm-btn-primary" id="wfm-badge-add-btn">${t("badgeAdd")}</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
openModal(t("badgeManage"), html);
|
||||
bindBadgeModalEvents(onPaletteChange);
|
||||
}
|
||||
|
||||
function bindBadgeModalEvents(onPaletteChange = null) {
|
||||
const afterChange = () => {
|
||||
renderBadgeFilter();
|
||||
renderModelGrid();
|
||||
if (onPaletteChange) onPaletteChange();
|
||||
};
|
||||
|
||||
const refreshList = () => {
|
||||
const palette = getBadgePalette();
|
||||
const labels = Object.keys(palette).sort();
|
||||
const listEl = document.getElementById("wfm-badge-list");
|
||||
if (!listEl) return;
|
||||
listEl.innerHTML = labels.map((label) => {
|
||||
const color = palette[label] || "#6366f1";
|
||||
return `<div class="wfm-badge-color-row" data-badge-label="${escapeHtml(label)}">
|
||||
${badgeHtml(label)}
|
||||
<input type="color" value="${color}" data-badge-label="${escapeHtml(label)}" class="wfm-badge-color-input">
|
||||
<input type="color" value="${color}" data-badge-label="${escapeHtml(label)}" class="wfm-badge-color-input" title="${t("badgeColorHint")}">
|
||||
<button class="wfm-btn wfm-btn-sm wfm-btn-danger wfm-badge-delete-btn" data-badge-label="${escapeHtml(label)}" title="${t("badgeDelete")}">×</button>
|
||||
</div>`;
|
||||
})
|
||||
.join("");
|
||||
}).join("") || `<p style="color:var(--wfm-text-secondary);font-size:12px;">${t("badgeNone")}</p>`;
|
||||
bindBadgeRowEvents(refreshList, afterChange);
|
||||
afterChange();
|
||||
};
|
||||
|
||||
panel.querySelectorAll(".wfm-badge-color-input").forEach((input) => {
|
||||
bindBadgeRowEvents(refreshList, afterChange);
|
||||
|
||||
document.getElementById("wfm-badge-add-btn")?.addEventListener("click", () => {
|
||||
const labelInput = document.getElementById("wfm-badge-new-label");
|
||||
const colorInput = document.getElementById("wfm-badge-new-color");
|
||||
const label = labelInput?.value.trim();
|
||||
if (!label) return;
|
||||
const palette = getBadgePalette();
|
||||
palette[label] = colorInput?.value || "#6366f1";
|
||||
saveBadgePalette(palette);
|
||||
if (labelInput) labelInput.value = "";
|
||||
refreshList();
|
||||
});
|
||||
}
|
||||
|
||||
function bindBadgeRowEvents(refreshList, afterChange = null) {
|
||||
document.querySelectorAll(".wfm-badge-color-input").forEach((input) => {
|
||||
input.addEventListener("input", (e) => {
|
||||
state.badgeColors[e.target.dataset.badgeLabel] = e.target.value;
|
||||
localStorage.setItem("wfm_models_badge_colors", JSON.stringify(state.badgeColors));
|
||||
renderBadgeSettings();
|
||||
const label = e.target.dataset.badgeLabel;
|
||||
const palette = getBadgePalette();
|
||||
palette[label] = e.target.value;
|
||||
saveBadgePalette(palette);
|
||||
refreshList();
|
||||
});
|
||||
});
|
||||
document.querySelectorAll(".wfm-badge-delete-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
const label = btn.dataset.badgeLabel;
|
||||
const palette = getBadgePalette();
|
||||
delete palette[label];
|
||||
saveBadgePalette(palette);
|
||||
refreshList();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Badge Filter Bar ──────────────────────────────────────
|
||||
|
||||
function renderBadgeFilter() {
|
||||
const container = document.getElementById("wfm-models-badge-filter-bar");
|
||||
if (!container) return;
|
||||
const palette = getBadgePalette();
|
||||
const labels = Object.keys(palette).sort();
|
||||
|
||||
if (labels.length === 0) {
|
||||
container.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = labels.map((label) => {
|
||||
const color = palette[label] || "";
|
||||
const isActive = state.badgeFilter === label;
|
||||
const style = color ? `background:${color};color:#fff;` : "";
|
||||
return `<button class="wfm-badge-filter-btn${isActive ? " active" : ""}" data-badge="${escapeHtml(label)}" style="${style}">${escapeHtml(label)}</button>`;
|
||||
}).join("");
|
||||
|
||||
container.querySelectorAll(".wfm-badge-filter-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
const label = btn.dataset.badge;
|
||||
state.badgeFilter = state.badgeFilter === label ? "" : label;
|
||||
state.currentPage = 0;
|
||||
renderBadgeFilter();
|
||||
renderModelGrid();
|
||||
});
|
||||
});
|
||||
@@ -247,6 +435,20 @@ function renderGroupFilter() {
|
||||
select.value = state.groupFilter;
|
||||
}
|
||||
|
||||
function renderDirFilter() {
|
||||
const select = document.getElementById("wfm-models-dir-filter");
|
||||
if (!select) return;
|
||||
const dirs = [...new Set(
|
||||
getCurrentModels()
|
||||
.map((m) => parseModelPath(m).dir)
|
||||
.filter((d) => d !== "")
|
||||
)].sort();
|
||||
select.innerHTML =
|
||||
`<option value="">${t("modelsAllDirs")}</option>` +
|
||||
dirs.map((d) => `<option value="${escapeHtml(d)}">${escapeHtml(d)}</option>`).join("");
|
||||
select.value = state.dirFilter;
|
||||
}
|
||||
|
||||
async function fetchModelGroups() {
|
||||
try {
|
||||
const res = await fetch("/api/wfm/models/groups");
|
||||
@@ -285,29 +487,23 @@ function renderModelGrid() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Pagination
|
||||
const start = state.currentPage * MODELS_PER_PAGE;
|
||||
const page = filtered.slice(start, start + MODELS_PER_PAGE);
|
||||
const totalPages = Math.ceil(filtered.length / MODELS_PER_PAGE);
|
||||
|
||||
if (state.viewMode === "table") {
|
||||
renderTableView(grid, page, totalPages);
|
||||
renderTableView(grid, filtered);
|
||||
} else if (state.viewMode === "card") {
|
||||
renderCardView(grid, page, totalPages);
|
||||
renderCardView(grid, filtered);
|
||||
} else {
|
||||
renderThumbView(grid, page, totalPages);
|
||||
renderThumbView(grid, filtered);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Thumbnail View (same as Workflow tab) ─────────────────
|
||||
|
||||
function renderThumbView(grid, models, totalPages) {
|
||||
function renderThumbView(grid, models) {
|
||||
grid.innerHTML = "";
|
||||
models.forEach((modelName) => {
|
||||
const meta = state.modelMetadata[modelName] || {};
|
||||
const { name } = parseModelPath(modelName);
|
||||
const typeLabel = TYPE_LABELS[state.activeModelType];
|
||||
const badges = badgeHtml(typeLabel);
|
||||
const userBadges = modelBadgesHtml(modelName);
|
||||
const tagsHtml = (meta.tags || []).map((tag) => `<span class="wfm-badge wfm-badge-sm">${escapeHtml(tag)}</span>`).join("");
|
||||
const favStar = meta.favorite ? "\u2605" : "\u2606";
|
||||
const favClass = meta.favorite ? "wfm-fav-btn active" : "wfm-fav-btn";
|
||||
@@ -324,7 +520,7 @@ function renderThumbView(grid, models, totalPages) {
|
||||
</div>
|
||||
<div class="wfm-card-body">
|
||||
<div class="wfm-card-title" title="${escapeHtml(modelName)}">${escapeHtml(getStem(name))}</div>
|
||||
<div class="wfm-card-meta">${badges} ${tagsHtml}</div>
|
||||
<div class="wfm-card-meta">${userBadges} ${tagsHtml}</div>
|
||||
</div>
|
||||
<button class="${favClass}" title="${t("modelsFavorite")}">${favStar}</button>`;
|
||||
|
||||
@@ -344,20 +540,17 @@ function renderThumbView(grid, models, totalPages) {
|
||||
});
|
||||
grid.appendChild(card);
|
||||
});
|
||||
|
||||
updatePagination(totalPages);
|
||||
}
|
||||
|
||||
// ── Card View (no thumbnail, compact) ─────────────────────
|
||||
|
||||
function renderCardView(grid, models, totalPages) {
|
||||
function renderCardView(grid, models) {
|
||||
grid.innerHTML = "";
|
||||
models.forEach((modelName) => {
|
||||
const meta = state.modelMetadata[modelName] || {};
|
||||
const { dir, name } = parseModelPath(modelName);
|
||||
const ext = getExtension(name);
|
||||
const typeLabel = TYPE_LABELS[state.activeModelType];
|
||||
const badges = badgeHtml(typeLabel);
|
||||
const userBadges = modelBadgesHtml(modelName);
|
||||
const tagsHtml = (meta.tags || []).map((tag) => `<span class="wfm-badge wfm-badge-sm">${escapeHtml(tag)}</span>`).join("");
|
||||
const favStar = meta.favorite ? "\u2605" : "\u2606";
|
||||
const favClass = meta.favorite ? "wfm-fav-btn active" : "wfm-fav-btn";
|
||||
@@ -371,9 +564,7 @@ function renderCardView(grid, models, totalPages) {
|
||||
<div class="wfm-card-body">
|
||||
<div class="wfm-card-title" title="${escapeHtml(modelName)}">${escapeHtml(name)}</div>
|
||||
<div class="wfm-card-meta">
|
||||
${dir ? `<span class="wfm-badge wfm-badge-sm wfm-badge-dir">${escapeHtml(dir)}</span>` : ""}
|
||||
<span class="wfm-badge wfm-badge-sm">${escapeHtml(ext)}</span>
|
||||
${badges} ${tagsHtml}
|
||||
${userBadges} ${tagsHtml}
|
||||
</div>
|
||||
</div>
|
||||
<button class="${favClass}" title="${t("modelsFavorite")}">${favStar}</button>`;
|
||||
@@ -389,13 +580,11 @@ function renderCardView(grid, models, totalPages) {
|
||||
});
|
||||
grid.appendChild(card);
|
||||
});
|
||||
|
||||
updatePagination(totalPages);
|
||||
}
|
||||
|
||||
// ── Table View ────────────────────────────────────────────
|
||||
|
||||
function renderTableView(grid, models, totalPages) {
|
||||
function renderTableView(grid, models) {
|
||||
const rows = models
|
||||
.map((modelName) => {
|
||||
const meta = state.modelMetadata[modelName] || {};
|
||||
@@ -442,25 +631,6 @@ function renderTableView(grid, models, totalPages) {
|
||||
toggleFavorite(mn);
|
||||
});
|
||||
});
|
||||
|
||||
updatePagination(totalPages);
|
||||
}
|
||||
|
||||
function updatePagination(totalPages) {
|
||||
const container = document.getElementById("wfm-models-pagination");
|
||||
if (!container) return;
|
||||
if (totalPages <= 1) { container.innerHTML = ""; return; }
|
||||
container.innerHTML = `
|
||||
<button class="wfm-btn wfm-btn-sm" ${state.currentPage === 0 ? "disabled" : ""} data-page="prev">«</button>
|
||||
<span>${state.currentPage + 1} / ${totalPages}</span>
|
||||
<button class="wfm-btn wfm-btn-sm" ${state.currentPage >= totalPages - 1 ? "disabled" : ""} data-page="next">»</button>
|
||||
`;
|
||||
container.querySelector('[data-page="prev"]').addEventListener("click", () => {
|
||||
if (state.currentPage > 0) { state.currentPage--; renderModelGrid(); }
|
||||
});
|
||||
container.querySelector('[data-page="next"]').addEventListener("click", () => {
|
||||
if (state.currentPage < totalPages - 1) { state.currentPage++; renderModelGrid(); }
|
||||
});
|
||||
}
|
||||
|
||||
// ── Favorite toggle ───────────────────────────────────────
|
||||
@@ -478,11 +648,22 @@ async function toggleFavorite(modelName) {
|
||||
function openDetailModal(modelName) {
|
||||
const meta = state.modelMetadata[modelName] || {};
|
||||
const { dir, name } = parseModelPath(modelName);
|
||||
const typeLabel = TYPE_LABELS[state.activeModelType];
|
||||
const typeBadge = badgeHtml(typeLabel);
|
||||
const ext = getExtension(name);
|
||||
const isFav = !!meta.favorite;
|
||||
const tagsStr = (meta.tags || []).join(", ");
|
||||
const selectedBadges = meta.badges || [];
|
||||
const palette = getBadgePalette();
|
||||
const allBadgeLabels = Object.keys(palette).sort();
|
||||
|
||||
const badgeCheckboxes = allBadgeLabels.map((label) => {
|
||||
const checked = selectedBadges.includes(label) ? " checked" : "";
|
||||
const color = palette[label] || "";
|
||||
const style = color ? `background:${color};color:#fff;` : "";
|
||||
return `<label class="wfm-badge-check-label">
|
||||
<input type="checkbox" class="wfm-badge-checkbox" value="${escapeHtml(label)}"${checked}>
|
||||
<span class="wfm-badge wfm-badge-model" style="${style}">${escapeHtml(label)}</span>
|
||||
</label>`;
|
||||
}).join("");
|
||||
|
||||
const html = `
|
||||
<div class="wfm-modal-thumb-section">
|
||||
@@ -496,10 +677,17 @@ function openDetailModal(modelName) {
|
||||
<div class="wfm-modal-two-col">
|
||||
<div class="wfm-modal-left">
|
||||
<section>
|
||||
<h4>${t("modelsModelType")}</h4>
|
||||
<div>${typeBadge} <span class="wfm-badge wfm-badge-sm">${escapeHtml(ext)}</span></div>
|
||||
<h4>${t("modelsInfo")}</h4>
|
||||
<div><span class="wfm-badge wfm-badge-sm">${escapeHtml(ext)}</span>${dir ? ` <span class="wfm-badge wfm-badge-sm wfm-badge-dir">${escapeHtml(dir)}</span>` : ""}</div>
|
||||
</section>
|
||||
<section>
|
||||
<h4>${t("modelsBadges")} <button class="wfm-btn wfm-btn-sm" id="wfm-modal-badge-manage" style="margin-left:6px;font-size:10px;">⚙ ${t("badgeManage")}</button></h4>
|
||||
<div id="wfm-modal-badge-checkboxes" class="wfm-badge-checkboxes">
|
||||
${allBadgeLabels.length === 0
|
||||
? `<span style="color:var(--wfm-text-secondary);font-size:12px;">${t("badgeNoneHint")}</span>`
|
||||
: badgeCheckboxes}
|
||||
</div>
|
||||
</section>
|
||||
${dir ? `<section><h4>${t("modelsSubdir")}</h4><div>${escapeHtml(dir)}</div></section>` : ""}
|
||||
<section>
|
||||
<h4>${t("modelsTags")} <span style="font-weight:normal;font-size:11px;">${t("modelsTagsHint")}</span></h4>
|
||||
<input type="text" class="wfm-input" id="wfm-modal-model-tags" value="${escapeHtml(tagsStr)}" placeholder="${t("modelsTagsPlaceholder")}">
|
||||
@@ -510,6 +698,9 @@ function openDetailModal(modelName) {
|
||||
</section>
|
||||
<div class="wfm-modal-actions">
|
||||
<button class="wfm-btn wfm-btn-primary wfm-btn-sm" id="wfm-modal-model-save">${t("modelsSave")}</button>
|
||||
${GENUI_TYPE_MAP[state.activeModelType]
|
||||
? `<button class="wfm-btn wfm-btn-sm" id="wfm-modal-genui-model" title="${t("modelsGenUITitle")}">${t("modelsGenUIBtn")}</button>`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
@@ -540,18 +731,30 @@ function openDetailModal(modelName) {
|
||||
titleEl.parentNode.insertBefore(favBtn, titleEl);
|
||||
}
|
||||
|
||||
// Badge manage button → open badge edit modal
|
||||
document.getElementById("wfm-modal-badge-manage")?.addEventListener("click", () => {
|
||||
openBadgeEditModal();
|
||||
});
|
||||
|
||||
// Save
|
||||
document.getElementById("wfm-modal-model-save")?.addEventListener("click", async () => {
|
||||
const tagsInput = document.getElementById("wfm-modal-model-tags");
|
||||
const memoInput = document.getElementById("wfm-modal-model-memo");
|
||||
const tags = tagsInput ? tagsInput.value.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
||||
const memo = memoInput ? memoInput.value : "";
|
||||
await saveModelMetadata(modelName, { tags, memo });
|
||||
const badges = [...document.querySelectorAll(".wfm-badge-checkbox:checked")].map((cb) => cb.value);
|
||||
await saveModelMetadata(modelName, { tags, memo, badges });
|
||||
showToast(t("modelsSaved"), "success");
|
||||
renderTagFilter();
|
||||
renderDirFilter();
|
||||
renderModelGrid();
|
||||
});
|
||||
|
||||
// GenUI Model button
|
||||
document.getElementById("wfm-modal-genui-model")?.addEventListener("click", () => {
|
||||
applyToGenUI(modelName, state.activeModelType);
|
||||
});
|
||||
|
||||
// Change thumbnail
|
||||
const changeBtn = document.getElementById("wfm-modal-change-thumb");
|
||||
const thumbFile = document.getElementById("wfm-modal-thumb-file");
|
||||
@@ -594,9 +797,6 @@ function openDetailModal(modelName) {
|
||||
|
||||
function showSidePanel(modelName) {
|
||||
state.selectedModel = modelName;
|
||||
const panel = document.getElementById("wfm-models-side-panel");
|
||||
if (!panel) return;
|
||||
panel.style.display = "flex";
|
||||
|
||||
// Highlight selected
|
||||
document.querySelectorAll("#wfm-models-grid .wfm-card, #wfm-models-grid .wfm-models-table-row").forEach((el) => {
|
||||
@@ -619,9 +819,9 @@ function showSidePanel(modelName) {
|
||||
}
|
||||
|
||||
function closeSidePanel() {
|
||||
const panel = document.getElementById("wfm-models-side-panel");
|
||||
if (panel) panel.style.display = "none";
|
||||
state.selectedModel = null;
|
||||
const titleEl = document.getElementById("wfm-models-panel-title");
|
||||
if (titleEl) titleEl.textContent = "";
|
||||
document.querySelectorAll("#wfm-models-grid .wfm-card, #wfm-models-grid .wfm-models-table-row").forEach((el) => {
|
||||
el.classList.remove("wfm-card-selected");
|
||||
});
|
||||
@@ -636,8 +836,8 @@ function renderSideInfo(modelName) {
|
||||
const meta = state.modelMetadata[modelName] || {};
|
||||
const { dir, name } = parseModelPath(modelName);
|
||||
const ext = getExtension(name);
|
||||
const typeLabel = TYPE_LABELS[state.activeModelType] || state.activeModelType;
|
||||
const tagsStr = (meta.tags || []).join(", ");
|
||||
const userBadgesHtml = modelBadgesHtml(modelName, false);
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="wfm-side-thumb-container">
|
||||
@@ -648,8 +848,9 @@ function renderSideInfo(modelName) {
|
||||
<div class="wfm-side-thumb-info">
|
||||
<div class="wfm-side-thumb-name wfm-model-name-copy" title="${t("modelsCopyName")}">${escapeHtml(name)}</div>
|
||||
<div class="wfm-side-thumb-meta">
|
||||
${badgeHtml(typeLabel)} <span class="wfm-badge wfm-badge-sm">${escapeHtml(ext)}</span>
|
||||
<span class="wfm-badge wfm-badge-sm">${escapeHtml(ext)}</span>
|
||||
${dir ? `<span class="wfm-badge wfm-badge-sm wfm-badge-dir">${escapeHtml(dir)}</span>` : ""}
|
||||
${userBadgesHtml}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -671,8 +872,11 @@ function renderSideInfo(modelName) {
|
||||
<textarea id="wfm-models-side-memo" class="wfm-textarea" rows="4" placeholder="${t("modelsMemoPlaceholder")}">${escapeHtml(meta.memo || "")}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wfm-node-detail-section">
|
||||
<div class="wfm-node-detail-section" style="display:flex;gap:6px;">
|
||||
<button id="wfm-models-side-save-btn" class="wfm-btn wfm-btn-sm wfm-btn-primary">${t("modelsSave")}</button>
|
||||
${GENUI_TYPE_MAP[state.activeModelType]
|
||||
? `<button id="wfm-models-side-genui-btn" class="wfm-btn wfm-btn-sm" title="${t("modelsGenUITitle")}">${t("modelsGenUIBtn")}</button>`
|
||||
: ""}
|
||||
</div>`;
|
||||
|
||||
// Load preview image
|
||||
@@ -726,6 +930,11 @@ function renderSideInfo(modelName) {
|
||||
renderModelGrid();
|
||||
});
|
||||
});
|
||||
|
||||
// GenUI Model button
|
||||
el.querySelector("#wfm-models-side-genui-btn")?.addEventListener("click", () => {
|
||||
applyToGenUI(modelName, state.activeModelType);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Side Panel: Group Tab ─────────────────────────────────
|
||||
@@ -989,6 +1198,7 @@ async function loadModelsForCurrentType() {
|
||||
|
||||
if (state.loaded[type] && state.modelsByType[type].length > 0) {
|
||||
renderTagFilter();
|
||||
renderDirFilter();
|
||||
renderModelGrid();
|
||||
return;
|
||||
}
|
||||
@@ -1004,6 +1214,7 @@ async function loadModelsForCurrentType() {
|
||||
state.loaded[type] = true;
|
||||
|
||||
renderTagFilter();
|
||||
renderDirFilter();
|
||||
renderModelGrid();
|
||||
} catch (err) {
|
||||
console.error("Failed to load models:", err);
|
||||
@@ -1122,6 +1333,8 @@ export function initModelsTab() {
|
||||
state.activeModelType = btn.dataset.modelType;
|
||||
state.searchText = "";
|
||||
state.tagFilter = "";
|
||||
state.badgeFilter = "";
|
||||
state.dirFilter = "";
|
||||
state.currentPage = 0;
|
||||
state.selectedModel = null;
|
||||
closeSidePanel();
|
||||
@@ -1130,6 +1343,7 @@ export function initModelsTab() {
|
||||
if (searchInput) searchInput.value = "";
|
||||
const tagFilter = document.getElementById("wfm-models-tag-filter");
|
||||
if (tagFilter) tagFilter.value = "";
|
||||
renderBadgeFilter();
|
||||
|
||||
loadModelsForCurrentType();
|
||||
});
|
||||
@@ -1152,6 +1366,13 @@ export function initModelsTab() {
|
||||
renderModelGrid();
|
||||
});
|
||||
|
||||
// Dir filter
|
||||
document.getElementById("wfm-models-dir-filter")?.addEventListener("change", (e) => {
|
||||
state.dirFilter = e.target.value;
|
||||
state.currentPage = 0;
|
||||
renderModelGrid();
|
||||
});
|
||||
|
||||
// Group filter
|
||||
document.getElementById("wfm-models-group-filter")?.addEventListener("change", (e) => {
|
||||
state.groupFilter = e.target.value;
|
||||
@@ -1185,16 +1406,10 @@ export function initModelsTab() {
|
||||
});
|
||||
});
|
||||
|
||||
// Badge settings
|
||||
const badgeBtn = document.getElementById("wfm-models-badge-settings-btn");
|
||||
const badgePanel = document.getElementById("wfm-models-badge-settings-panel");
|
||||
if (badgeBtn && badgePanel) {
|
||||
badgeBtn.addEventListener("click", () => {
|
||||
const visible = badgePanel.style.display !== "none";
|
||||
badgePanel.style.display = visible ? "none" : "block";
|
||||
if (!visible) renderBadgeSettings();
|
||||
});
|
||||
}
|
||||
// Badge manage button (toolbar)
|
||||
document.getElementById("wfm-models-badge-settings-btn")?.addEventListener("click", () => {
|
||||
openBadgeEditModal();
|
||||
});
|
||||
|
||||
// Refresh
|
||||
document.getElementById("wfm-models-refresh-btn")?.addEventListener("click", () => {
|
||||
@@ -1208,9 +1423,6 @@ export function initModelsTab() {
|
||||
batchFetchCivitai();
|
||||
});
|
||||
|
||||
// Side panel close
|
||||
document.getElementById("wfm-models-panel-close")?.addEventListener("click", closeSidePanel);
|
||||
|
||||
// Side panel tab switching
|
||||
document.querySelectorAll(".wfm-models-side-tab-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
@@ -1224,6 +1436,9 @@ export function initModelsTab() {
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize badge filter bar
|
||||
renderBadgeFilter();
|
||||
|
||||
// Lazy load on first tab click
|
||||
let firstLoad = false;
|
||||
document.querySelector('[data-tab="models"]')?.addEventListener("click", () => {
|
||||
|
||||
+30
-77
@@ -8,7 +8,6 @@ import { t } from "./i18n.js";
|
||||
|
||||
// ── State ─────────────────────────────────────────────────
|
||||
|
||||
const NODES_PER_PAGE = 50;
|
||||
|
||||
const state = {
|
||||
allNodes: [],
|
||||
@@ -257,91 +256,50 @@ function renderNodeGrid() {
|
||||
|
||||
if (filtered.length === 0) {
|
||||
grid.innerHTML = `<p class="wfm-placeholder">${t("noNodesFound")}</p>`;
|
||||
clearPagination();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.viewMode === "table") {
|
||||
renderNodeTableView(grid, filtered);
|
||||
clearPagination();
|
||||
return;
|
||||
}
|
||||
|
||||
// Card view with pagination
|
||||
const totalPages = Math.ceil(filtered.length / NODES_PER_PAGE);
|
||||
if (state.currentPage >= totalPages) state.currentPage = totalPages - 1;
|
||||
if (state.currentPage < 0) state.currentPage = 0;
|
||||
|
||||
const start = state.currentPage * NODES_PER_PAGE;
|
||||
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)}`;
|
||||
filtered.forEach(node => grid.appendChild(createNodeCard(node)));
|
||||
}
|
||||
|
||||
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";
|
||||
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)}`;
|
||||
|
||||
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>` : ""}
|
||||
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>
|
||||
<button class="${favClass}" title="Favorite">${favStar}</button>`;
|
||||
${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);
|
||||
card.querySelector(".wfm-fav-btn").addEventListener("click", e => {
|
||||
e.stopPropagation();
|
||||
toggleFavorite(node.name, e.currentTarget);
|
||||
});
|
||||
|
||||
renderPagination(filtered.length, totalPages);
|
||||
card.addEventListener("click", () => showNodeSidePanel(node));
|
||||
return card;
|
||||
}
|
||||
|
||||
function renderPagination(totalItems, totalPages) {
|
||||
const container = document.getElementById("wfm-nodes-pagination");
|
||||
if (!container) return;
|
||||
if (totalPages <= 1) { container.innerHTML = ""; return; }
|
||||
|
||||
const start = state.currentPage * NODES_PER_PAGE + 1;
|
||||
const end = Math.min((state.currentPage + 1) * NODES_PER_PAGE, totalItems);
|
||||
container.innerHTML = `
|
||||
<button class="wfm-btn wfm-btn-sm" ${state.currentPage === 0 ? "disabled" : ""} data-page="prev">«</button>
|
||||
<span>${start}-${end} / ${totalItems}</span>
|
||||
<button class="wfm-btn wfm-btn-sm" ${state.currentPage >= totalPages - 1 ? "disabled" : ""} data-page="next">»</button>
|
||||
`;
|
||||
container.querySelector('[data-page="prev"]').addEventListener("click", () => {
|
||||
if (state.currentPage > 0) { state.currentPage--; renderNodeGrid(); scrollGridToTop(); }
|
||||
});
|
||||
container.querySelector('[data-page="next"]').addEventListener("click", () => {
|
||||
if (state.currentPage < totalPages - 1) { state.currentPage++; renderNodeGrid(); scrollGridToTop(); }
|
||||
});
|
||||
}
|
||||
|
||||
function clearPagination() {
|
||||
const container = document.getElementById("wfm-nodes-pagination");
|
||||
if (container) container.innerHTML = "";
|
||||
}
|
||||
|
||||
function scrollGridToTop() {
|
||||
document.getElementById("wfm-nodes-grid")?.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
function renderNodeTableView(grid, filtered) {
|
||||
grid.innerHTML = "";
|
||||
@@ -399,9 +357,6 @@ async function toggleFavorite(nodeName, btnEl) {
|
||||
|
||||
function showNodeSidePanel(node) {
|
||||
state.selectedNode = node;
|
||||
const panel = document.getElementById("wfm-nodes-side-panel");
|
||||
if (!panel) return;
|
||||
panel.style.display = "flex";
|
||||
|
||||
// Highlight selected card
|
||||
document.querySelectorAll("#wfm-nodes-grid .wfm-card, #wfm-nodes-grid tr").forEach(el => {
|
||||
@@ -621,9 +576,9 @@ function renderSideGroups(node) {
|
||||
}
|
||||
|
||||
function closeNodeSidePanel() {
|
||||
const panel = document.getElementById("wfm-nodes-side-panel");
|
||||
if (panel) panel.style.display = "none";
|
||||
state.selectedNode = null;
|
||||
const titleEl = document.getElementById("wfm-nodes-panel-title");
|
||||
if (titleEl) titleEl.textContent = "";
|
||||
document.querySelectorAll("#wfm-nodes-grid .wfm-card, #wfm-nodes-grid tr").forEach(el => {
|
||||
el.classList.remove("wfm-card-selected");
|
||||
});
|
||||
@@ -1083,8 +1038,6 @@ export function initNodesTab() {
|
||||
loadNodesData();
|
||||
});
|
||||
|
||||
// Side panel close
|
||||
document.getElementById("wfm-nodes-panel-close")?.addEventListener("click", closeNodeSidePanel);
|
||||
|
||||
// Side panel tab switching
|
||||
document.querySelectorAll(".wfm-nodes-side-tab-btn").forEach(btn => {
|
||||
|
||||
+105
-177
@@ -9,6 +9,7 @@ import { comfyWorkflow } from "./comfyui-workflow.js";
|
||||
import { loadWorkflowIntoEditor } from "./generate-tab.js";
|
||||
import { t, getSummaryPrompt } from "./i18n.js";
|
||||
import { highlightJSON } from "./json-highlight.js";
|
||||
import { openBadgeEditModal } from "./models-tab.js";
|
||||
|
||||
// ============================================
|
||||
// State
|
||||
@@ -16,29 +17,31 @@ import { highlightJSON } from "./json-highlight.js";
|
||||
|
||||
const state = {
|
||||
workflows: [],
|
||||
activeModel: "ALL",
|
||||
activeBadge: "ALL",
|
||||
searchText: "",
|
||||
viewMode: localStorage.getItem("wfm_default_view") || "thumb", // thumb | card | table
|
||||
selectedWf: null,
|
||||
groupFilter: "", // empty = all groups
|
||||
currentPage: 0,
|
||||
badgeColors: (() => {
|
||||
try { return JSON.parse(localStorage.getItem("wfm_badge_colors") || "{}"); }
|
||||
catch { return {}; }
|
||||
})(),
|
||||
};
|
||||
|
||||
const WF_PER_PAGE = 24;
|
||||
|
||||
const BADGE_DEFAULT_COLORS = {
|
||||
"SD1.5": "#e67e22",
|
||||
SDXL: "#2ecc71",
|
||||
Flux: "#9b59b6",
|
||||
Flux2: "#8e44ad",
|
||||
SD3: "#3498db",
|
||||
Qwen: "#e74c3c",
|
||||
"Z-IMAGE": "#1abc9c",
|
||||
};
|
||||
function getBadgePalette() {
|
||||
try { return JSON.parse(localStorage.getItem("wfm_models_badge_palette") || "{}"); }
|
||||
catch { return {}; }
|
||||
}
|
||||
|
||||
function saveBadgePalette(palette) {
|
||||
localStorage.setItem("wfm_models_badge_palette", JSON.stringify(palette));
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Open in ComfyUI
|
||||
@@ -217,15 +220,22 @@ async function getRawWorkflow(filename) {
|
||||
// Rendering Helpers
|
||||
// ============================================
|
||||
|
||||
function badgeHtml(type) {
|
||||
const color = state.badgeColors[type] || BADGE_DEFAULT_COLORS[type] || "#0077ff";
|
||||
return `<span class="wfm-badge wfm-badge-model" style="background:${color}">${type}</span>`;
|
||||
function badgeHtml(label) {
|
||||
const palette = getBadgePalette();
|
||||
const color = palette[label] || "";
|
||||
const style = color ? ` style="background:${color};color:#fff;"` : "";
|
||||
return `<span class="wfm-badge wfm-badge-model"${style}>${escapeHtml(label)}</span>`;
|
||||
}
|
||||
|
||||
function getAllModelTypes() {
|
||||
function wfBadgesHtml(wf) {
|
||||
const badges = wf.metadata?.badges || [];
|
||||
return badges.map(label => badgeHtml(label)).join("");
|
||||
}
|
||||
|
||||
function getAllBadges() {
|
||||
const set = new Set();
|
||||
state.workflows.forEach((wf) =>
|
||||
(wf.analysis.modelTypes || []).forEach((t) => set.add(t))
|
||||
(wf.metadata?.badges || []).forEach((b) => set.add(b))
|
||||
);
|
||||
return [...set].sort();
|
||||
}
|
||||
@@ -234,17 +244,22 @@ function renderModelFilters() {
|
||||
const container = document.getElementById("wfm-model-filters");
|
||||
if (!container) return;
|
||||
|
||||
const types = getAllModelTypes();
|
||||
const palette = getBadgePalette();
|
||||
const badges = Object.keys(palette).sort();
|
||||
|
||||
container.innerHTML = `
|
||||
<button class="wfm-filter-btn ${state.activeModel === "ALL" ? "active" : ""}" data-filter="ALL">ALL</button>
|
||||
<button class="wfm-filter-btn ${state.activeModel === "FAVORITE" ? "active" : ""}" data-filter="FAVORITE">★</button>
|
||||
${types.map((t) => `<button class="wfm-filter-btn ${state.activeModel === t ? "active" : ""}" data-filter="${t}">${t}</button>`).join("")}
|
||||
<button class="wfm-filter-btn ${state.activeBadge === "ALL" ? "active" : ""}" data-filter="ALL">ALL</button>
|
||||
<button class="wfm-filter-btn ${state.activeBadge === "FAVORITE" ? "active" : ""}" data-filter="FAVORITE">★</button>
|
||||
${badges.map((b) => {
|
||||
const color = palette[b] || "";
|
||||
const style = color ? ` style="background:${color};color:#fff;"` : "";
|
||||
return `<button class="wfm-filter-btn ${state.activeBadge === b ? "active" : ""}" data-filter="${escapeHtml(b)}"${style}>${escapeHtml(b)}</button>`;
|
||||
}).join("")}
|
||||
`;
|
||||
|
||||
container.querySelectorAll(".wfm-filter-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
state.activeModel = btn.dataset.filter;
|
||||
state.activeBadge = btn.dataset.filter;
|
||||
state.currentPage = 0;
|
||||
renderModelFilters();
|
||||
renderGrid();
|
||||
@@ -254,10 +269,12 @@ function renderModelFilters() {
|
||||
|
||||
function filterWorkflows() {
|
||||
return state.workflows.filter((wf) => {
|
||||
if (state.activeModel === "FAVORITE") {
|
||||
if (wf.filename === ".index.json") return false;
|
||||
|
||||
if (state.activeBadge === "FAVORITE") {
|
||||
if (!wf.metadata.favorite) return false;
|
||||
} else if (state.activeModel !== "ALL") {
|
||||
if (!(wf.analysis.modelTypes || []).includes(state.activeModel))
|
||||
} else if (state.activeBadge !== "ALL") {
|
||||
if (!(wf.metadata?.badges || []).includes(state.activeBadge))
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -301,32 +318,21 @@ function renderGrid() {
|
||||
if (filtered.length === 0) {
|
||||
grid.innerHTML =
|
||||
'<p class="wfm-placeholder">No workflows found</p>';
|
||||
updateWfPagination(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.viewMode === "table") {
|
||||
renderTableView(grid, filtered);
|
||||
updateWfPagination(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Thumbnail / Card view with pagination
|
||||
const totalPages = Math.ceil(filtered.length / WF_PER_PAGE);
|
||||
if (state.currentPage >= totalPages) state.currentPage = totalPages - 1;
|
||||
if (state.currentPage < 0) state.currentPage = 0;
|
||||
const start = state.currentPage * WF_PER_PAGE;
|
||||
const pageItems = filtered.slice(start, start + WF_PER_PAGE);
|
||||
|
||||
updateWfPagination(totalPages);
|
||||
|
||||
grid.innerHTML = "";
|
||||
pageItems.forEach((wf) => {
|
||||
filtered.forEach((wf) => {
|
||||
const card = document.createElement("div");
|
||||
card.className = "wfm-card";
|
||||
card.dataset.filename = wf.filename;
|
||||
const hasThumb = !!wf.thumbnail;
|
||||
const badges = (wf.analysis.modelTypes || []).map((t) => badgeHtml(t)).join("");
|
||||
const badges = wfBadgesHtml(wf);
|
||||
const tags = (wf.metadata.tags || []).map((t) => `<span class="wfm-tag">#${t}</span>`).join("");
|
||||
const favStar = wf.metadata.favorite ? "\u2605" : "\u2606";
|
||||
const favClass = wf.metadata.favorite ? "wfm-fav-btn active" : "wfm-fav-btn";
|
||||
@@ -371,22 +377,6 @@ function renderGrid() {
|
||||
}
|
||||
}
|
||||
|
||||
function updateWfPagination(totalPages) {
|
||||
const container = document.getElementById("wfm-workflow-pagination");
|
||||
if (!container) return;
|
||||
if (totalPages <= 1) { container.innerHTML = ""; return; }
|
||||
container.innerHTML = `
|
||||
<button class="wfm-btn wfm-btn-sm" ${state.currentPage === 0 ? "disabled" : ""} data-page="prev">«</button>
|
||||
<span>${state.currentPage + 1} / ${totalPages}</span>
|
||||
<button class="wfm-btn wfm-btn-sm" ${state.currentPage >= totalPages - 1 ? "disabled" : ""} data-page="next">»</button>
|
||||
`;
|
||||
container.querySelector('[data-page="prev"]').addEventListener("click", () => {
|
||||
if (state.currentPage > 0) { state.currentPage--; renderGrid(); }
|
||||
});
|
||||
container.querySelector('[data-page="next"]').addEventListener("click", () => {
|
||||
if (state.currentPage < totalPages - 1) { state.currentPage++; renderGrid(); }
|
||||
});
|
||||
}
|
||||
|
||||
function renderTableView(grid, filtered) {
|
||||
const table = document.createElement("table");
|
||||
@@ -407,7 +397,7 @@ function renderTableView(grid, filtered) {
|
||||
const tr = document.createElement("tr");
|
||||
tr.className = "wfm-table-row";
|
||||
tr.dataset.filename = wf.filename;
|
||||
const badges = (wf.analysis.modelTypes || []).map((t) => badgeHtml(t)).join("");
|
||||
const badges = wfBadgesHtml(wf);
|
||||
const tags = (wf.metadata.tags || []).map((t) => `<span class="wfm-tag">#${t}</span>`).join("");
|
||||
const thumbHtml = wf.thumbnail
|
||||
? `<img src="${wf.thumbnail}" class="wfm-table-thumb" loading="lazy" />`
|
||||
@@ -473,7 +463,6 @@ async function showSidePanel(wf, cardEl) {
|
||||
|
||||
titleEl.textContent = wf.filename.replace(/\.json$/, "");
|
||||
contentEl.textContent = t("loading");
|
||||
panel.style.display = "flex";
|
||||
|
||||
state.selectedWf = wf;
|
||||
|
||||
@@ -517,12 +506,12 @@ async function showSidePanel(wf, cardEl) {
|
||||
}
|
||||
|
||||
function closeSidePanel() {
|
||||
const panel = document.getElementById("wfm-side-panel");
|
||||
if (panel) panel.style.display = "none";
|
||||
document.querySelectorAll(".wfm-card, .wfm-table-row").forEach((c) =>
|
||||
c.classList.remove("wfm-card-selected")
|
||||
);
|
||||
state.selectedWf = null;
|
||||
const titleEl = document.getElementById("wfm-side-panel-title");
|
||||
if (titleEl) titleEl.textContent = "";
|
||||
const listLoadBtn = document.getElementById("wfm-list-load-btn");
|
||||
if (listLoadBtn) { listLoadBtn.disabled = true; listLoadBtn.title = t("selectCardFirst"); }
|
||||
const listOpenComfyBtn = document.getElementById("wfm-list-open-comfyui-btn");
|
||||
@@ -550,7 +539,7 @@ function sidePanelThumbUpdate(wf) {
|
||||
nameEl.textContent = wf ? wf.filename.replace(/\.json$/, "") : "";
|
||||
}
|
||||
if (metaEl && wf) {
|
||||
const badges = (wf.analysis.modelTypes || []).map((mt) => badgeHtml(mt)).join(" ");
|
||||
const badges = wfBadgesHtml(wf);
|
||||
const io = `P:${wf.analysis.inputs?.prompts || 0} I:${wf.analysis.inputs?.images || 0} → ${wf.analysis.outputs?.images || 0}`;
|
||||
metaEl.innerHTML = `${badges} <span style="margin-left:4px;">${io}</span>`;
|
||||
} else if (metaEl) {
|
||||
@@ -608,14 +597,13 @@ function refreshGroupSelects() {
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Badge Color Settings
|
||||
// View Settings Panel
|
||||
// ============================================
|
||||
|
||||
function renderBadgeSettings() {
|
||||
function renderViewSettings() {
|
||||
const panel = document.getElementById("wfm-badge-settings-panel");
|
||||
if (!panel) return;
|
||||
|
||||
// === Default View section ===
|
||||
const currentView = localStorage.getItem("wfm_default_view") || "thumb";
|
||||
const viewOptions = [
|
||||
{ value: "thumb", label: t("viewThumbnail") },
|
||||
@@ -629,57 +617,20 @@ function renderBadgeSettings() {
|
||||
</label>`
|
||||
).join("");
|
||||
|
||||
// === Badge Colors section ===
|
||||
const types = getAllModelTypes();
|
||||
const allKnown = new Set([...Object.keys(BADGE_DEFAULT_COLORS), ...types]);
|
||||
const rows = [...allKnown].sort().map((type) => {
|
||||
const color = state.badgeColors[type] || BADGE_DEFAULT_COLORS[type] || "#0077ff";
|
||||
return `<div class="wfm-badge-color-row" data-type="${type}">
|
||||
<span class="wfm-badge wfm-badge-model" style="background:${color}">${type}</span>
|
||||
<input type="color" class="wfm-badge-color-input" value="${color}" data-type="${type}" title="Change color for ${type}" />
|
||||
</div>`;
|
||||
}).join("");
|
||||
|
||||
panel.innerHTML = `
|
||||
<div class="wfm-badge-settings-title">${t("defaultView")}</div>
|
||||
<div class="wfm-settings-radio-group">${viewRadios}</div>
|
||||
<hr style="border:none;border-top:1px solid var(--wfm-border);margin:8px 0;">
|
||||
<div class="wfm-badge-settings-title">${t("badgeColors")}</div>
|
||||
${rows}
|
||||
<button id="wfm-badge-color-reset" class="wfm-btn wfm-btn-sm" style="margin-top:6px;width:100%;">${t("resetColors")}</button>`;
|
||||
<div class="wfm-settings-radio-group">${viewRadios}</div>`;
|
||||
|
||||
// Default view change
|
||||
panel.querySelectorAll('input[name="wfm-default-view"]').forEach((radio) => {
|
||||
radio.addEventListener("change", () => {
|
||||
localStorage.setItem("wfm_default_view", radio.value);
|
||||
state.viewMode = radio.value;
|
||||
// Update active state on view mode buttons
|
||||
document.querySelectorAll(".wfm-view-btn").forEach((b) => {
|
||||
b.classList.toggle("active", b.dataset.view === radio.value);
|
||||
});
|
||||
renderGrid();
|
||||
});
|
||||
});
|
||||
|
||||
// Badge color change
|
||||
panel.querySelectorAll(".wfm-badge-color-input").forEach((input) => {
|
||||
input.addEventListener("input", (e) => {
|
||||
const type = e.target.dataset.type;
|
||||
const color = e.target.value;
|
||||
state.badgeColors[type] = color;
|
||||
localStorage.setItem("wfm_badge_colors", JSON.stringify(state.badgeColors));
|
||||
const badge = e.target.closest(".wfm-badge-color-row")?.querySelector(".wfm-badge");
|
||||
if (badge) badge.style.background = color;
|
||||
renderGrid();
|
||||
});
|
||||
});
|
||||
|
||||
panel.querySelector("#wfm-badge-color-reset")?.addEventListener("click", () => {
|
||||
state.badgeColors = {};
|
||||
localStorage.removeItem("wfm_badge_colors");
|
||||
renderBadgeSettings();
|
||||
renderGrid();
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -704,10 +655,21 @@ async function saveModalMeta(wf) {
|
||||
|
||||
function openDetailModal(wf) {
|
||||
const meta = wf.metadata;
|
||||
const types = (wf.analysis.modelTypes || []).map((t) => badgeHtml(t)).join(" ");
|
||||
const io = `P:${wf.analysis.inputs?.prompts || 0} I:${wf.analysis.inputs?.images || 0} → ${wf.analysis.outputs?.images || 0}`;
|
||||
const overrideText = (meta.modelTypesOverride || []).join(", ");
|
||||
const isFav = !!meta.favorite;
|
||||
const palette = getBadgePalette();
|
||||
const allBadgeLabels = Object.keys(palette).sort();
|
||||
const activeBadges = meta.badges || [];
|
||||
const badgeCheckboxes = allBadgeLabels.length === 0
|
||||
? `<span style="color:var(--wfm-text-secondary);font-size:12px;">${t("badgeNoneHint")}</span>`
|
||||
: allBadgeLabels.map((label) => {
|
||||
const color = palette[label] || "";
|
||||
const style = color ? ` style="background:${color};color:#fff;"` : "";
|
||||
const checked = activeBadges.includes(label) ? " checked" : "";
|
||||
return `<label class="wfm-badge-check-label">
|
||||
<input type="checkbox" class="wfm-badge-checkbox" value="${escapeHtml(label)}"${checked}>
|
||||
<span class="wfm-badge wfm-badge-model"${style}>${escapeHtml(label)}</span>
|
||||
</label>`;
|
||||
}).join("");
|
||||
|
||||
const thumbSrc = wf.thumbnail || "";
|
||||
const html = `
|
||||
@@ -720,23 +682,9 @@ function openDetailModal(wf) {
|
||||
<div class="wfm-modal-two-col">
|
||||
<div class="wfm-modal-left">
|
||||
<section>
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
|
||||
<h4>${t("analysis")}</h4>
|
||||
<button class="wfm-btn wfm-btn-sm" id="wfm-detail-analyze">${t("analyze")}</button>
|
||||
</div>
|
||||
<div id="wfm-detail-analysis" class="wfm-detail-analysis">
|
||||
<div>${types || `<span style="color:var(--wfm-text-secondary)">${t("noModelDetected")}</span>`}</div>
|
||||
<div style="font-size:11px;color:var(--wfm-text-secondary);margin-top:4px;">${io}</div>
|
||||
</div>
|
||||
<div id="wfm-detail-override-row" style="margin-top:8px;">
|
||||
<div style="display:flex;gap:4px;align-items:center;">
|
||||
<span style="font-size:12px;color:var(--wfm-text-secondary);white-space:nowrap;">${t("modelCategory")}</span>
|
||||
<input type="text" class="wfm-input" id="wfm-detail-model-override"
|
||||
value="${overrideText}"
|
||||
placeholder="${t("modelOverridePlaceholder")}" style="flex:1;font-size:12px;padding:4px 6px;min-width:0;">
|
||||
<button class="wfm-btn wfm-btn-sm wfm-btn-primary" id="wfm-detail-override-save">${t("save")}</button>
|
||||
</div>
|
||||
</div>
|
||||
<h4>${t("modelsBadges")} <button class="wfm-btn wfm-btn-sm" id="wfm-detail-badge-manage">⚙ ${t("badgeManage")}</button></h4>
|
||||
<div id="wfm-detail-badge-checkboxes" class="wfm-badge-checkboxes">${badgeCheckboxes}</div>
|
||||
<button class="wfm-btn wfm-btn-sm wfm-btn-primary" id="wfm-detail-badges-save" style="margin-top:6px;">${t("save")}</button>
|
||||
</section>
|
||||
<section>
|
||||
<h4>${t("tags")} <span style="font-weight:normal;font-size:11px;">${t("tagsHint")}</span></h4>
|
||||
@@ -844,51 +792,40 @@ function openDetailModal(wf) {
|
||||
document.getElementById(id)?.addEventListener("blur", () => saveModalMeta(wf));
|
||||
});
|
||||
|
||||
// Analyze button
|
||||
document.getElementById("wfm-detail-analyze")?.addEventListener("click", async () => {
|
||||
const btn = document.getElementById("wfm-detail-analyze");
|
||||
btn.disabled = true;
|
||||
btn.textContent = t("analyzing");
|
||||
try {
|
||||
const res = await fetch("/api/wfm/workflows/analyze", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ filename: wf.filename }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.error) throw new Error(data.error);
|
||||
wf.analysis = data.analysis;
|
||||
const cached = state.workflows.find((w) => w.filename === wf.filename);
|
||||
if (cached) cached.analysis = data.analysis;
|
||||
// Re-render analysis section
|
||||
const analysisEl = document.getElementById("wfm-detail-analysis");
|
||||
if (analysisEl) {
|
||||
const newTypes = (data.analysis.modelTypes || []).map((mt) => badgeHtml(mt)).join(" ");
|
||||
const newIo = `P:${data.analysis.inputs?.prompts || 0} I:${data.analysis.inputs?.images || 0} → ${data.analysis.outputs?.images || 0}`;
|
||||
analysisEl.innerHTML = `<div>${newTypes || `<span style="color:var(--wfm-text-secondary)">${t("noModelDetected")}</span>`}</div>
|
||||
<div style="font-size:11px;color:var(--wfm-text-secondary);margin-top:4px;">${newIo}</div>`;
|
||||
}
|
||||
renderGrid();
|
||||
showToast(t("analysisComplete"), "success");
|
||||
} catch (err) {
|
||||
showToast(t("analyzeError") + ": " + err.message, "error");
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = t("analyze");
|
||||
}
|
||||
// Badge manage button
|
||||
document.getElementById("wfm-detail-badge-manage")?.addEventListener("click", () => {
|
||||
openBadgeEditModal(() => {
|
||||
// Rebuild checkboxes after palette changes
|
||||
const palette = getBadgePalette();
|
||||
const allLabels = Object.keys(palette).sort();
|
||||
const currentBadges = [...document.querySelectorAll(".wfm-badge-checkbox:checked")].map(cb => cb.value);
|
||||
const el = document.getElementById("wfm-detail-badge-checkboxes");
|
||||
if (!el) return;
|
||||
el.innerHTML = allLabels.length === 0
|
||||
? `<span style="color:var(--wfm-text-secondary);font-size:12px;">${t("badgeNoneHint")}</span>`
|
||||
: allLabels.map((label) => {
|
||||
const color = palette[label] || "";
|
||||
const style = color ? ` style="background:${color};color:#fff;"` : "";
|
||||
const checked = currentBadges.includes(label) ? " checked" : "";
|
||||
return `<label class="wfm-badge-check-label">
|
||||
<input type="checkbox" class="wfm-badge-checkbox" value="${escapeHtml(label)}"${checked}>
|
||||
<span class="wfm-badge wfm-badge-model"${style}>${escapeHtml(label)}</span>
|
||||
</label>`;
|
||||
}).join("");
|
||||
});
|
||||
});
|
||||
|
||||
// Model type override save
|
||||
document.getElementById("wfm-detail-override-save")?.addEventListener("click", async () => {
|
||||
const overrideStr = document.getElementById("wfm-detail-model-override")?.value || "";
|
||||
const modelTypesOverride = overrideStr.split(",").map((t) => t.trim()).filter(Boolean);
|
||||
// Badge save
|
||||
document.getElementById("wfm-detail-badges-save")?.addEventListener("click", async () => {
|
||||
const badges = [...document.querySelectorAll(".wfm-badge-checkbox:checked")].map(cb => cb.value);
|
||||
try {
|
||||
await saveMetadata(wf.filename, { modelTypesOverride });
|
||||
wf.metadata.modelTypesOverride = modelTypesOverride;
|
||||
await saveMetadata(wf.filename, { badges });
|
||||
wf.metadata.badges = badges;
|
||||
const cached = state.workflows.find((w) => w.filename === wf.filename);
|
||||
if (cached) cached.metadata.modelTypesOverride = modelTypesOverride;
|
||||
if (cached) cached.metadata.badges = badges;
|
||||
renderGrid();
|
||||
showToast(t("overrideSaved"), "success");
|
||||
renderModelFilters();
|
||||
showToast(t("modelsSaved"), "success");
|
||||
} catch (err) {
|
||||
showToast(t("saveError") + ": " + err.message, "error");
|
||||
}
|
||||
@@ -1129,9 +1066,6 @@ export function initWorkflowTab() {
|
||||
});
|
||||
}
|
||||
|
||||
// Side panel: close button
|
||||
document.getElementById("wfm-side-panel-close")?.addEventListener("click", closeSidePanel);
|
||||
|
||||
// Side panel: tab switching
|
||||
document.querySelectorAll(".wfm-side-tab-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
@@ -1271,7 +1205,7 @@ export function initWorkflowTab() {
|
||||
showToast("Refreshed", "success");
|
||||
});
|
||||
|
||||
// Toolbar: Badge color settings
|
||||
// Toolbar: ⚙ settings button (view settings panel)
|
||||
const badgeSettingsBtn = document.getElementById("wfm-badge-settings-btn");
|
||||
const badgeSettingsPanel = document.getElementById("wfm-badge-settings-panel");
|
||||
if (badgeSettingsBtn && badgeSettingsPanel) {
|
||||
@@ -1282,7 +1216,7 @@ export function initWorkflowTab() {
|
||||
badgeSettingsPanel.style.display = "none";
|
||||
return;
|
||||
}
|
||||
renderBadgeSettings();
|
||||
renderViewSettings();
|
||||
badgeSettingsPanel.style.display = "";
|
||||
});
|
||||
document.addEventListener("click", (e) => {
|
||||
@@ -1292,15 +1226,9 @@ export function initWorkflowTab() {
|
||||
});
|
||||
}
|
||||
|
||||
// Toolbar: Reanalyze All button
|
||||
document.getElementById("wfm-reanalyze-btn")?.addEventListener("click", async () => {
|
||||
try {
|
||||
const result = await reanalyzeAll();
|
||||
showToast(`Reanalyzed ${result.count ?? 0} workflows`, "success");
|
||||
await loadWorkflows();
|
||||
} catch (err) {
|
||||
showToast("Reanalyze error: " + err.message, "error");
|
||||
}
|
||||
// Toolbar: ⚙ Badge button
|
||||
document.getElementById("wfm-badge-btn")?.addEventListener("click", () => {
|
||||
openBadgeEditModal(() => renderGrid());
|
||||
});
|
||||
|
||||
// Initial load
|
||||
|
||||
+80
-62
@@ -50,16 +50,15 @@
|
||||
<button class="wfm-view-btn" data-view="table" title="Table">≡</button>
|
||||
</div>
|
||||
<button id="wfm-refresh-btn" class="wfm-btn wfm-btn-sm">Refresh</button>
|
||||
<button id="wfm-reanalyze-btn" class="wfm-btn wfm-btn-sm" title="Re-analyze all workflows">Reanalyze All</button>
|
||||
<label class="wfm-btn wfm-btn-sm" style="cursor:pointer;">
|
||||
Import
|
||||
<input type="file" id="wfm-import-input" multiple accept=".json,.png" style="display:none">
|
||||
</label>
|
||||
<button id="wfm-badge-btn" class="wfm-btn wfm-btn-sm" title="Badge settings">⚙ Badge</button>
|
||||
<div class="wfm-badge-settings-wrap">
|
||||
<button id="wfm-badge-settings-btn" class="wfm-btn wfm-btn-sm" title="Badge color settings">⚙</button>
|
||||
<button id="wfm-badge-settings-btn" class="wfm-btn wfm-btn-sm" title="View settings">⚙</button>
|
||||
<div id="wfm-badge-settings-panel" class="wfm-badge-settings-panel" style="display:none;"></div>
|
||||
</div>
|
||||
<div id="wfm-workflow-pagination" class="wfm-pagination-inline"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wfm-workflow-area">
|
||||
@@ -67,10 +66,9 @@
|
||||
<p class="wfm-placeholder">Loading workflows...</p>
|
||||
</div>
|
||||
<!-- Side Panel -->
|
||||
<div id="wfm-side-panel" class="wfm-side-panel" style="display:none;">
|
||||
<div id="wfm-side-panel" class="wfm-side-panel">
|
||||
<div class="wfm-side-panel-header">
|
||||
<span id="wfm-side-panel-title" class="wfm-side-panel-title"></span>
|
||||
<button id="wfm-side-panel-close" class="wfm-side-panel-close">×</button>
|
||||
</div>
|
||||
<div class="wfm-side-tab-nav">
|
||||
<button class="wfm-side-tab-btn active" data-side-tab="thumb">Thumbnail</button>
|
||||
@@ -156,7 +154,6 @@
|
||||
</div>
|
||||
<span id="wfm-nodes-count" class="wfm-nodes-count"></span>
|
||||
<button id="wfm-nodes-refresh-btn" class="wfm-btn wfm-btn-sm">Refresh</button>
|
||||
<div id="wfm-nodes-pagination" class="wfm-pagination-inline"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -166,10 +163,9 @@
|
||||
<p class="wfm-placeholder" id="wfm-nodes-placeholder">Click to load nodes...</p>
|
||||
</div>
|
||||
<!-- Side Panel -->
|
||||
<div id="wfm-nodes-side-panel" class="wfm-side-panel" style="display:none;">
|
||||
<div id="wfm-nodes-side-panel" class="wfm-side-panel">
|
||||
<div class="wfm-side-panel-header">
|
||||
<span id="wfm-nodes-panel-title" class="wfm-side-panel-title"></span>
|
||||
<button id="wfm-nodes-panel-close" class="wfm-side-panel-close">×</button>
|
||||
</div>
|
||||
<div class="wfm-side-tab-nav">
|
||||
<button class="wfm-nodes-side-tab-btn active" data-side-tab="details">Details</button>
|
||||
@@ -199,8 +195,8 @@
|
||||
<!-- Models Tab -->
|
||||
<section id="wfm-tab-models" class="wfm-tab-content">
|
||||
<div class="wfm-toolbar">
|
||||
<!-- Row 1: Model type sub-tabs -->
|
||||
<div class="wfm-toolbar-row" style="gap:8px;">
|
||||
<!-- Row 1: Model type sub-tabs + badge filter -->
|
||||
<div class="wfm-toolbar-row" style="gap:8px;flex-wrap:wrap;align-items:center;">
|
||||
<div class="wfm-models-type-nav">
|
||||
<button class="wfm-models-type-btn active" data-model-type="checkpoint">Checkpoint</button>
|
||||
<button class="wfm-models-type-btn" data-model-type="lora">LoRA</button>
|
||||
@@ -208,13 +204,18 @@
|
||||
<button class="wfm-models-type-btn" data-model-type="controlnet">ControlNet</button>
|
||||
<button class="wfm-models-type-btn" data-model-type="unet">UNET</button>
|
||||
<button class="wfm-models-type-btn" data-model-type="textencoder">TextEncoder</button>
|
||||
<button class="wfm-models-type-btn" data-model-type="hypernetwork">Hypernetwork</button>
|
||||
<button class="wfm-models-type-btn" data-model-type="embedding">Embedding</button>
|
||||
</div>
|
||||
<div class="wfm-models-type-divider"></div>
|
||||
<div id="wfm-models-badge-filter-bar" class="wfm-badge-filter-bar"></div>
|
||||
</div>
|
||||
<!-- Row 2: Search / filter toolbar -->
|
||||
<div id="wfm-models-toolbar" class="wfm-toolbar-row" style="gap:6px;flex-wrap:wrap;">
|
||||
<button class="wfm-filter-btn" id="wfm-models-fav-btn" title="Favorites">★</button>
|
||||
<input type="text" id="wfm-models-search" class="wfm-search-input" placeholder="Search models...">
|
||||
<select id="wfm-models-tag-filter" class="wfm-select wfm-toolbar-select"><option value="">All Tags</option></select>
|
||||
<select id="wfm-models-dir-filter" class="wfm-select wfm-toolbar-select"><option value="">All Folders</option></select>
|
||||
<select id="wfm-models-group-filter" class="wfm-select wfm-toolbar-select"><option value="">All Groups</option></select>
|
||||
<div class="wfm-view-btns">
|
||||
<button class="wfm-view-btn active" data-models-view="thumb" title="Thumbnail">⊞</button>
|
||||
@@ -225,11 +226,7 @@
|
||||
<button id="wfm-models-refresh-btn" class="wfm-btn wfm-btn-sm">Refresh</button>
|
||||
<button id="wfm-models-civitai-batch-btn" class="wfm-btn wfm-btn-sm" title="Fetch CivitAI info for all models">CivitAI</button>
|
||||
<span id="wfm-models-civitai-progress" style="font-size:12px;color:var(--wfm-text-secondary);"></span>
|
||||
<div class="wfm-badge-settings-wrap">
|
||||
<button id="wfm-models-badge-settings-btn" class="wfm-btn wfm-btn-sm" title="Badge color settings">⚙</button>
|
||||
<div id="wfm-models-badge-settings-panel" class="wfm-badge-settings-panel" style="display:none;"></div>
|
||||
</div>
|
||||
<div id="wfm-models-pagination" class="wfm-pagination-inline"></div>
|
||||
<button id="wfm-models-badge-settings-btn" class="wfm-btn wfm-btn-sm" title="Badge settings">⚙ Badge</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -239,10 +236,9 @@
|
||||
<p class="wfm-placeholder" id="wfm-models-placeholder">Click Refresh to load models...</p>
|
||||
</div>
|
||||
<!-- Side Panel -->
|
||||
<div id="wfm-models-side-panel" class="wfm-side-panel" style="display:none;">
|
||||
<div id="wfm-models-side-panel" class="wfm-side-panel">
|
||||
<div class="wfm-side-panel-header">
|
||||
<span id="wfm-models-panel-title" class="wfm-side-panel-title"></span>
|
||||
<button id="wfm-models-panel-close" class="wfm-side-panel-close">×</button>
|
||||
</div>
|
||||
<div class="wfm-side-tab-nav">
|
||||
<button class="wfm-models-side-tab-btn active" data-side-tab="info">Info</button>
|
||||
@@ -278,53 +274,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>
|
||||
@@ -349,7 +349,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>
|
||||
@@ -366,6 +366,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 -->
|
||||
@@ -499,9 +511,8 @@
|
||||
<li id="wfm-help-wf-3">Create, rename, delete groups and assign/remove workflows</li>
|
||||
<li id="wfm-help-wf-4">Import JSON/PNG files via button or drag & drop</li>
|
||||
<li id="wfm-help-wf-5">Side panel: thumbnail, JSON viewer (format badge: UI/API/App), clipboard copy, group management</li>
|
||||
<li id="wfm-help-wf-6">Detail modal: rename, favorite, thumbnail change, analysis, category override, tags, memo, AI summary, open in ComfyUI, load in GenerateUI, set as default, delete</li>
|
||||
<li id="wfm-help-wf-7">Customize badge colors per category</li>
|
||||
<li id="wfm-help-wf-8">Batch re-analysis of all workflow I/O (P: prompt nodes, I: image nodes, output: save nodes)</li>
|
||||
<li id="wfm-help-wf-6">Detail modal: rename, favorite, thumbnail change, badge assignment, tags, memo, AI summary, open in ComfyUI, load in GenerateUI, set as default, delete</li>
|
||||
<li id="wfm-help-wf-7">Badge management: add, rename, delete badges with custom colors shared with the Models tab (⚙ Badge button)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -510,11 +521,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 & 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>
|
||||
|
||||
@@ -560,13 +574,15 @@
|
||||
<div class="wfm-help-card">
|
||||
<h3>Models Tab</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-models-1">Browse installed models: Checkpoint, LoRA, VAE, ControlNet, UNET, TextEncoder with sub-tab switching</li>
|
||||
<li id="wfm-help-models-1">Browse installed models: Checkpoint, LoRA, VAE, ControlNet, UNET, TextEncoder, Hypernetwork, Embedding with sub-tab switching</li>
|
||||
<li id="wfm-help-models-2">Thumbnail, Card, and Table view modes with pagination</li>
|
||||
<li id="wfm-help-models-3">Search, tag filter, group filter, and favorites filter</li>
|
||||
<li id="wfm-help-models-4">Side panel: Info (file path, tags, memo), Groups management, CivitAI integration</li>
|
||||
<li id="wfm-help-models-5">CivitAI integration: fetch model info by SHA256 hash, batch fetch per model type</li>
|
||||
<li id="wfm-help-models-6">Detail modal: preview image, CivitAI info (base model, trained words, tags), thumbnail change</li>
|
||||
<li id="wfm-help-models-7">Group management: create, rename, delete groups and assign/remove models</li>
|
||||
<li id="wfm-help-models-8">GenUI Model button: apply the selected model to the corresponding node in GenerateUI's current workflow (Checkpoint / LoRA / VAE / ControlNet / UNET / TextEncoder)</li>
|
||||
<li id="wfm-help-models-9">User-defined badges: assign free-label badges with custom colors shared with the Workflow tab</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -574,8 +590,9 @@
|
||||
<h3 id="wfm-help-sidepanel-title">Workflow Studio Library (ComfyUI Side Panel)</h3>
|
||||
<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: browse favorite workflows, filter by model type, and filter by group</li>
|
||||
<li id="wfm-help-sidepanel-3">Nodes tab: browse favorite nodes, node sets, and node groups</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 / 📂 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 & drop workflows onto the canvas to load them</li>
|
||||
<li id="wfm-help-sidepanel-6">Drag & drop nodes/node sets onto the canvas to place them</li>
|
||||
@@ -583,6 +600,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>
|
||||
|
||||
|
||||
+869
-32
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user