Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1cd3fb3fff | ||
|
|
5b087713b9 | ||
|
|
6a2da8e4e9 | ||
|
|
54fc57344a | ||
|
|
1cc0e94724 | ||
|
|
823f1ebd54 |
@@ -1,5 +1,299 @@
|
||||
# DEVLOG - ComfyUI-Workflow-Studio
|
||||
|
||||
## 2026-05-09: v0.3.5 GenerateUI — Feeder サブタブ・ワークフロー解析精度向上
|
||||
|
||||
### 概要
|
||||
|
||||
- comfyui-image-feeder カスタムノード対応の Feeder サブタブを GenerateUI タブに追加
|
||||
- `analyzeWorkflow` の精度向上(SDXL 多段 CONDITIONING チェーン BFS 展開、新ノード種対応、KSamplerAdvanced seed 修正)
|
||||
- Help タブに Feeder サブタブの説明カードを追加(EN/JA/ZH i18n 対応)
|
||||
|
||||
### 変更内容
|
||||
|
||||
#### `static/js/feeder-tab.js` — 新規作成
|
||||
|
||||
- `_s = { dir, images, selected (Set), presets, running }` モジュール状態
|
||||
- `_feederNodes()` — currentWorkflow から `class_type === "ImageFeeder"` のノードを列挙
|
||||
- `refreshFeederNodeList()` — ノードセレクターを再描画(ワークフロー読み込み時に呼ばれる)
|
||||
- `_loadFromNode(nodeId)` — ノード inputs を左ペインの各フィールドに反映
|
||||
- `_applyToNode(nodeId)` — フィールド値を currentWorkflow のノード inputs に書き戻す
|
||||
- `_applyToWorkflow()` — Apply ボタン用(`_applyToNode` 呼び出し+toast)
|
||||
- フォルダツリー: `_loadTree()` / `_renderTree()` / `_makeTreeRow()` / `_selectDir()` / `_highlightTreeRow()` — `/image_loop/tree` API からツリーを描画
|
||||
- 画像グリッド: `_loadImages()` / `_renderGrid()` / `_makeCard()` / `_refreshGridCbs()` / `_selectAll()` / `_deselectAll()` — `/image_loop/images` API からサムネイルカードを描画、チェックボックスで選択管理
|
||||
- プレビュー: `_showPreview()` — カードクリックで右ペインに拡大プレビューと `/image_loop/image_info` の情報を表示
|
||||
- プリセット: `_loadPresets()` / `_renderPresets()` / `_savePreset()` / `_applyPreset()` / `_deletePreset()` — `/image_feeder/presets` API を使用
|
||||
- ステータスバー: `_updateStatus()` — 現在フォルダと選択枚数/総枚数を表示
|
||||
- Run ループ: `_startRun()` / `_stopRun()` / `_setRunUI()`
|
||||
- Run 前に WebSocket を接続し `image_loop_node_sync` メッセージを専用ハンドラで捕捉
|
||||
- 各反復で `_applyToNode` → `comfyUI.generate()` → 結果画像を右ペインに表示
|
||||
- `comfyUI.generate` には右ペインの seed mode/value を使用(KSampler のシードを正しく制御)
|
||||
- After gen モード (`wfm-feeder-control-after`):
|
||||
- `loop`: `_lastSync.next_index` で index を更新し無限継続(`has_next=false` でも折り返して継続)
|
||||
- `increment`: index を更新し `has_next=false` で自動停止
|
||||
- `fixed`: index を変えず同じ画像で繰り返し
|
||||
- 生成ごとに右ペインの seed 表示を更新
|
||||
- `initFeederTab()` — 全コントロールのイベントリスナーを登録し `_loadTree()` / `_loadPresets()` を並列実行
|
||||
|
||||
#### `static/js/generate-tab.js`
|
||||
|
||||
- `import { initFeederTab, refreshFeederNodeList } from "./feeder-tab.js"` を追加
|
||||
- `loadWorkflowIntoEditor()` に `refreshFeederNodeList()` 呼び出しを追加
|
||||
- `moveRawJsonToTab()`: `tabKey === "feeder"` の場合は Raw JSON ウィジェットを非表示(feeder タブには rawjson-col がないため)
|
||||
- `initGenerateTab()` に `await initFeederTab()` を追加
|
||||
|
||||
#### `templates/index.html`
|
||||
|
||||
- サブタブナビに `<button data-subtab="feeder">Feeder</button>` を追加
|
||||
- `wfm-gen-subtab-feeder` コンテンツ div を追加:
|
||||
- 左ペイン (`.wfm-feeder-settings`): ノードセレクター・Apply・全パラメータフィールド・プリセット管理・Run セクション(After gen セレクト + Run/Stop ボタン)
|
||||
- 中央ペイン (`.wfm-feeder-library`): ステータスバー + ライブラリ本体(フォルダツリー・画像グリッド・プレビューパネル)
|
||||
- Help タブ: GenerateUI Tab カードの説明を "4-tab layout" に更新
|
||||
- Help タブ: Feeder サブタブ説明カードを追加(`wfm-help-feeder-title` / `wfm-help-feeder-desc` / `wfm-help-feeder-1`〜`9` の id 付き)
|
||||
|
||||
#### `static/css/main.css`
|
||||
|
||||
Feeder 専用スタイルを末尾に追加:
|
||||
- `.wfm-feeder-layout` — flex 横並び全体レイアウト
|
||||
- `.wfm-feeder-settings` — 左ペイン(幅 220px、縦スクロール)
|
||||
- `.wfm-feeder-pane-header` / `.wfm-feeder-field-row` / `.wfm-feeder-label` / `.wfm-feeder-input` — フォームレイアウト
|
||||
- `.wfm-feeder-library` / `.wfm-feeder-status-bar` / `.wfm-feeder-library-body` — 中央ペイン構造
|
||||
- `.wfm-feeder-tree-pane` / `.wfm-feeder-tree-scroll` / `.wfm-feeder-tree-row` (+ `:hover` / `.active`) / `.wfm-feeder-tree-msg` — フォルダツリー
|
||||
- `.wfm-feeder-grid-pane` / `.wfm-feeder-grid` / `.wfm-feeder-grid-msg` — 画像グリッドコンテナ
|
||||
- `.wfm-feeder-card` (+ `:hover` / `.selected`) / `.wfm-feeder-card-cb` / `.wfm-feeder-card-img` / `.wfm-feeder-card-name` — 画像カード
|
||||
- `.wfm-feeder-preview-pane` / `.wfm-feeder-preview-img` / `.wfm-feeder-preview-name` / `.wfm-feeder-preview-info` — プレビューパネル
|
||||
|
||||
#### `static/js/comfyui-workflow.js`
|
||||
|
||||
`analyzeWorkflow` を3パス構成に再構成:
|
||||
|
||||
- **Pass 1**: KSampler / KSamplerAdvanced を検出し `sampler_nodes` に追加
|
||||
- `seedKey`: `"seed" in inputs ? "seed" : "noise_seed"` で KSamplerAdvanced の `noise_seed` に対応
|
||||
- LoraLoader 検出を追加
|
||||
- **Pass 1b**: BFS 展開(最大5イテレーション)で CONDITIONING チェーンの positive/negative 帰属を伝播
|
||||
- `COND_PASSTHROUGH` セット: ConditioningCombine / ConditioningConcat / ConditioningAverage / ConditioningZeroOut / ConditioningSetTimestepRange / ControlNetApply / ControlNetApplyAdvanced / IPAdapterApply / IPAdapterApplyFaceID / StyleModelApply
|
||||
- リンク接続入力を持つノードも帰属を伝播(CLIPTextEncodeSDXL の text_g / text_l が他ノードにリンクされている場合)
|
||||
- **Pass 2**: 新ノード種の検出
|
||||
- `CLIPTextEncodeSDXL` / `CLIPTextEncodeSDXLRefiner`: `text_g` / `text_l` が文字列値の場合のみ prompt_nodes に追加
|
||||
- `TextEncodeQwenImageEditPlus`: `inputs.prompt` を prompt_nodes に追加
|
||||
- `SDXLPromptStyler` / `SDXLPromptStylerAdvanced`: `text_positive` / `text_negative` をそれぞれ role 付きで追加
|
||||
- `CheckpointLoader` (WAS): `ct === "Checkpoint Loader"`(スペースあり)を含む判定に修正
|
||||
- `PrimitiveStringMultiline` / `PrimitiveString`: positive/negative ref がある場合のみ prompt_nodes に追加
|
||||
- `Power Lora Loader (rgthree)`: `lora_\d+` キーをスキャンして lora_nodes に追加
|
||||
|
||||
#### `static/js/comfyui-editor.js`
|
||||
|
||||
- KSampler パラメータエディタの hidden input に `data-seed-key` 属性を追加
|
||||
- Apply 時に `dataset.seedKey`(`"seed"` または `"noise_seed"`)を参照して正しいキーに書き込む
|
||||
|
||||
#### `static/js/i18n.js`
|
||||
|
||||
- EN / JA / ZH: `helpGen3` を "4-tab layout" / "4タブ構成" / "4标签布局" に更新
|
||||
- EN / JA / ZH: `helpFeederTitle` / `helpFeederDesc` / `helpFeeder1`〜`9` を追加
|
||||
|
||||
#### `static/js/app.js`
|
||||
|
||||
- `helpIdMap` に `wfm-help-feeder-title` / `wfm-help-feeder-desc` / `wfm-help-feeder-1`〜`9` を追加
|
||||
|
||||
---
|
||||
|
||||
## 2026-04-29: v0.3.4 GenerateUI — Checkpoint Batch 刷新・Settings 横並びレイアウト・ヘルプ i18n 修正
|
||||
|
||||
### 概要
|
||||
|
||||
- Checkpoint Batch: フォルダツリー型ドロップダウン選択に刷新(テキスト入力の Include/Exclude フォルダ指定を廃止)
|
||||
- Checkpoint Batch: 第二階層以下のサブフォルダにも対応
|
||||
- Checkpoint Batch: 一時停止 / 再開(Pause/Resume)ボタンを追加
|
||||
- GenerateUI Settings タブ: KSampler と Latent Image を横並び(各50%幅)に変更
|
||||
- Help タブ: 最新機能に合わせて説明を更新し、i18n 対応漏れ(Gallery 5〜11、Gen 11)を修正
|
||||
|
||||
### 変更内容
|
||||
|
||||
#### `templates/index.html`
|
||||
|
||||
- Checkpoint Batch パネルの Include/Exclude テキスト入力を廃止し、チェックボックス付きドロップダウンに置き換え
|
||||
- フォルダヘッダー行: チェックボックス + ▶ 展開矢印 + フォルダ名 + ファイル数
|
||||
- ファイル行: インデント + チェックボックス + ファイル名(フルパスを tooltip 表示)
|
||||
- パネル上部: Filter 検索入力 + All / None ボタン
|
||||
- バッチ進行エリアのプログレスバー下に Pause ボタンを追加(実行中のみ有効)
|
||||
- ヘルプ: `wfm-help-gen-6`(横並びレイアウト)・`wfm-help-gen-11`(新バッチ機能)の初期テキストを更新
|
||||
|
||||
#### `static/js/generate-tab.js`
|
||||
|
||||
- `_parseFolderList()` / `_getModelFolder()` / `_filterCheckpoints()` を削除
|
||||
- `_ckptState = { mode: "all"|"some"|"none", selected: Set<modelPath> }` で選択状態を管理
|
||||
- `"all"`: 全選択(selected は空)、`"some"`: 部分選択、`"none"`: 全解除
|
||||
- `_buildFolderTree(models)` — モデルパスをフォルダ → モデルリストの Map に変換(`lastIndexOf("/")` で第二階層以下にも対応)
|
||||
- `_getFolderCheckState(folderModels)` — `"checked"` / `"indeterminate"` / `"unchecked"` を返す
|
||||
- `_toggleSingleModel()` / `_toggleFolderModels()` — 単体・フォルダ一括のトグルロジック(all/none/some の状態遷移を管理)
|
||||
- `_rebuildCkptList()` — フォルダグループ形式の DOM を動的生成(▶ 展開・折りたたみ、`checkbox.indeterminate` 対応)
|
||||
- `_getSelectedCheckpoints()` — mode に応じた選択リストを返す
|
||||
- `_ckptBatch.paused` フラグと `_waitIfPaused()` で一時停止・再開を実装(Promise 待機 + resolve で解除)
|
||||
- `_setPauseBtnState(paused)` — ボタンのテキスト・スタイルを Pause ↔ Resume で切り替え
|
||||
- `initCheckpointBatch()` に Pause/Resume ボタンのハンドラを追加
|
||||
- `_runBatchGenerate()`: ループ先頭で `await _waitIfPaused()`、一時停止時にステータステキストを "Paused" 表示、finally ブロックで状態クリーンアップ
|
||||
- Interrupt(Stop)ボタン: `_ckptBatch.paused = false` と `_resumeResolve()` を呼んで一時停止待機を即解除
|
||||
|
||||
#### `static/js/comfyui-editor.js`
|
||||
|
||||
- `renderSettingsTab()`: 外枠を `flex-direction: row` に変更し KSampler(左)と Latent Image(右)を横並び(各 `flex:1; min-width:0`)、間に縦の境界線(`border-right: 1px solid var(--wfm-border)`)
|
||||
|
||||
#### `static/css/main.css`
|
||||
|
||||
- `.wfm-ckpt-dropdown-wrap` / `.wfm-ckpt-dropdown-trigger` / `.wfm-ckpt-dropdown-arrow` / `.wfm-ckpt-dropdown-panel` — ドロップダウン UI スタイル
|
||||
- `.wfm-ckpt-folder-group` / `.wfm-ckpt-folder-header` / `.wfm-ckpt-folder-toggle` / `.wfm-ckpt-folder-name` / `.wfm-ckpt-folder-count` / `.wfm-ckpt-folder-files` — フォルダツリー UI スタイル
|
||||
- `.wfm-ckpt-item--indented` — ファイル行のインデント(`padding-left: 24px`)
|
||||
|
||||
#### `static/js/i18n.js`
|
||||
|
||||
- EN / JA / ZH: `helpGen6` を横並びレイアウトの説明に更新
|
||||
- EN / JA / ZH: `helpGen11` を追加(フォルダツリードロップダウン、Pause/Resume、Stop の説明)
|
||||
- EN / JA / ZH: `helpGallery5`〜`helpGallery10` を現在の HTML 構成に合わせて修正(旧版から5項目ずれていたのを解消)
|
||||
- EN / JA / ZH: `helpGallery11` を追加(PNG/JPEG/WebP/GIF 対応・メタデータ抽出の説明)
|
||||
|
||||
#### `static/js/app.js`
|
||||
|
||||
- `helpIdMap` に `"wfm-help-gen-11": "helpGen11"`、`"wfm-help-gallery-11": "helpGallery11"` を追加
|
||||
|
||||
---
|
||||
|
||||
## 2026-04-28: v0.3.3 Promptタブ — ワイルドカード支援パネル・Impact Packシンボリックリンク連携・サブディレクトリ対応・GenerateUI修正
|
||||
|
||||
### 概要
|
||||
|
||||
- Promptタブのレイアウトを3カラム構成に変更
|
||||
- 中央ペイン(Col2): Presets / Preset Manager をタブ切り替え化
|
||||
- 右ペイン(Col3): ワイルドカード支援パネルを新設
|
||||
- ワイルドカードファイル管理(txt/yaml)の CRUD API を追加
|
||||
- Settingsタブに Wildcard Integration セクションを追加(Impact Pack ジャンクション/シンボリックリンク連携)
|
||||
- ワイルドカードフォルダのサブディレクトリファイルに対応(再帰スキャン・ディレクトリグループ表示・`__folder/name__` 形式)
|
||||
- GenerateUI: `ImpactWildcardEncode` ノードの実行エラーを修正
|
||||
|
||||
### 変更内容
|
||||
|
||||
#### `py/config.py`
|
||||
|
||||
- `COMFYUI_ROOT = _COMFYUI_ROOT` を追加(外部モジュールから参照可能に)
|
||||
- `WILDCARD_DIR = DATA_DIR / "wildcard"` を追加
|
||||
|
||||
#### `py/services/wildcard_service.py` — 新規作成
|
||||
|
||||
- `WildcardService(wildcard_dir)` クラス
|
||||
- `list_wildcards()` — txt/yaml/yml ファイル一覧を返す
|
||||
- `get_content(filename)` — ファイル内容を取得
|
||||
- `save_file(filename, content)` — ファイルを保存(新規/上書き)
|
||||
- `delete_file(filename)` — ファイルを削除
|
||||
- `_safe_path(filename)` — `..` / スラッシュ / 拡張子チェックによるパストラバーサル防止
|
||||
- Impact Pack 連携メソッド
|
||||
- `find_impact_pack_wildcards(comfyui_root)` — `custom_nodes/` 以下で "impact-pack" ディレクトリを検索
|
||||
- `get_link_status(comfyui_root)` — インストール状況・リンク状態・パスを返す dict
|
||||
- `create_link(comfyui_root)` — 既存 WFS ファイルを Impact Pack dir へ移行後、ジャンクション/シンボリックリンクを作成
|
||||
- `remove_link()` — ジャンクション/シンボリックリンクを削除して通常ディレクトリに復元
|
||||
- プラットフォームヘルパー
|
||||
- `_is_junction(path)` — Windows の `GetFileAttributesW` + `FILE_ATTRIBUTE_REPARSE_POINT` でジャンクション検出(`os.path.islink()` では検出不可)
|
||||
- `_create_junction_or_symlink(link, target)` — Windows は `mklink /J`、他 OS は `os.symlink`
|
||||
- `_remove_junction_or_symlink(path)` — Windows は `rmdir`(ジャンクションを削除、中身に影響なし)、他は `os.unlink`
|
||||
- コンストラクタ: `_is_junction()` で事前確認し、ジャンクション/シンボリックリンクの場合は mkdir をスキップ
|
||||
|
||||
#### `py/routes/wildcard_routes.py` — 新規作成
|
||||
|
||||
- `GET /api/wfm/wildcards` — ファイル一覧
|
||||
- `GET /api/wfm/wildcards/content?filename=` — ファイル内容取得
|
||||
- `POST /api/wfm/wildcards/save` — ファイル保存
|
||||
- `POST /api/wfm/wildcards/delete` — ファイル削除
|
||||
- `GET /api/wfm/wildcards/link-status` — Impact Pack リンク状態取得
|
||||
- `POST /api/wfm/wildcards/create-link` — ジャンクション/シンボリックリンク作成(移行ファイルリストを返す)
|
||||
- `POST /api/wfm/wildcards/remove-link` — リンク解除
|
||||
|
||||
#### `py/wfm.py`
|
||||
|
||||
- `wildcard_routes.setup_routes(app)` を追加
|
||||
|
||||
#### `templates/index.html`
|
||||
|
||||
- Promptタブ Col2 をタブ切り替え構造(`.wfm-prompt-center-tabnav` + `.wfm-prompt-center-pane`)に変更
|
||||
- Promptタブ Col3 にワイルドカード支援パネル(`.wfm-wc-panel`)を新設
|
||||
- ツールバー: `{|}`, `|`, `__`, `:`, `;`, `$`, `<lora:>`, `[]`, `{n$|}`, ファイルピッカーボタン
|
||||
- プロンプトテキストエリア(`.wfm-wc-prompt-ta`)
|
||||
- ファイル管理セクション(ファイル一覧 + インラインエディタ)
|
||||
- Helpタブ: `wfm-help-prompt-8`、`wfm-help-prompt-9`、`wfm-help-settings-10` を追加
|
||||
|
||||
#### `static/js/prompt-tab.js`
|
||||
|
||||
- ワイルドカード API ヘルパー: `wcFetchFiles()`、`wcFetchContent()`、`wcSaveFile()`、`wcDeleteFile()`
|
||||
- `wcInsertAtCursor(textarea, open, close)` — 選択テキストをラップ、または `open+close` を挿入してカーソルを `open` の末尾に移動
|
||||
- `wcRenderFileList()`、`wcOpenEditor()`、`wcCloseEditor()`、`wcRefreshFiles()`、`wcUpdateFilePicker()`
|
||||
- `initPromptTab()` に追加:
|
||||
- 中央ペインのタブ切り替えハンドラ
|
||||
- ワイルドカードツールバーボタン(`data-wc-open` / `data-wc-close` 属性で制御)
|
||||
- Loraボタン: `<lora::1:LBW=;>` を挿入してカーソル位置調整
|
||||
- `{n$|}` ボタン: `prompt()` で n を入力し `{n$|}` を挿入
|
||||
- ファイルピッカー(ポップアップ型、クリックで `__filename__` 挿入)
|
||||
- ファイルマネージャのイベントハンドラ(新規作成・保存・削除)
|
||||
|
||||
#### `static/js/settings-tab.js`
|
||||
|
||||
- Data Management セクションの直後に **Wildcard Integration** `<details>` セクションを追加
|
||||
- セクション展開時に `GET /api/wfm/wildcards/link-status` を呼び出して動的 UI をレンダリング
|
||||
- Impact Pack 未インストール: インストール誘導メッセージ + GitHub リンク
|
||||
- インストール済み・リンクなし: Impact Pack パス表示 + 「リンクを作成」ボタン(移行ファイル名を表示)
|
||||
- リンク済み: リンク先パス表示 + 「リンクを解除」ボタン
|
||||
|
||||
#### `static/js/app.js`
|
||||
|
||||
- `wfm-help-prompt-8`、`wfm-help-prompt-9`、`wfm-help-settings-10` の ID マッピングを追加
|
||||
|
||||
#### `static/js/i18n.js`
|
||||
|
||||
- `helpPrompt8`、`helpPrompt9`、`helpSettings10` を EN/JA/ZH に追加
|
||||
|
||||
#### `static/css/main.css`
|
||||
|
||||
- `.wfm-prompt-center-tabnav`、`.wfm-prompt-center-tab`、`.wfm-prompt-center-pane` — 中央ペインタブ UI
|
||||
- `.wfm-wc-panel`、`.wfm-wc-toolbar`、`.wfm-wc-btn`、`.wfm-wc-prompt-ta` — ワイルドカードパネル
|
||||
- `.wfm-wc-file-picker-wrap`、`.wfm-wc-file-picker`、`.wfm-wc-picker-item` — ファイルピッカー
|
||||
- `.wfm-wc-files-section`、`.wfm-wc-file-list`、`.wfm-wc-file-item`、`.wfm-wc-editor` — ファイルマネージャ
|
||||
|
||||
### ワイルドカードサブディレクトリ対応
|
||||
|
||||
#### `py/services/wildcard_service.py`
|
||||
|
||||
- `list_wildcards()` — `iterdir()` → `rglob("*")` に変更して再帰スキャン。レスポンスに `dir`(サブディレクトリパス、ルートは空文字)と `wc_name`(拡張子なし相対パス、例: `color/animals`)を追加
|
||||
- `_safe_path()` — `/` 区切りの相対パスを受け付けるよう変更。各パスコンポーネントを個別に検証してパストラバーサルをブロック
|
||||
- `save_file()` — `path.parent.mkdir(parents=True, exist_ok=True)` を追加してサブディレクトリを自動作成。レスポンスに `dir`、`wc_name` を追加
|
||||
|
||||
#### `static/js/prompt-tab.js`
|
||||
|
||||
- `wcRenderFileList()` — ファイルをディレクトリごとにグループ化して表示(ルートが先、サブディレクトリはヘッダ付き)。サブディレクトリ内ファイルは `wfm-wc-file-item--sub` クラスでインデント。挿入テキストを `__f.name__` → `__f.wc_name__`(例: `__color/animals__`)に変更
|
||||
- `wcUpdateFilePicker()` — 同様にディレクトリグループ表示。挿入テキストを `wc_name` ベースに変更
|
||||
- Editor Save ハンドラ — `name` のバリデーションを `folder/name` 形式に対応。`\` を `/` に正規化して各コンポーネントを個別検証。`filename` を `name.ext`(例: `color/animals.txt`)として構築
|
||||
|
||||
#### `templates/index.html`
|
||||
|
||||
- エディタ名入力の placeholder を `"name or folder/name"` に更新
|
||||
|
||||
#### `static/css/main.css`
|
||||
|
||||
- `.wfm-wc-dir-header` — ファイル一覧のディレクトリ区切りヘッダ
|
||||
- `.wfm-wc-file-item--sub` — サブディレクトリファイルの左インデント
|
||||
- `.wfm-wc-picker-dir` — ファイルピッカーのディレクトリ区切りラベル
|
||||
- `.wfm-wc-picker-item--sub` — ファイルピッカー内サブディレクトリファイルのインデント
|
||||
- Safari 向け `-webkit-user-select` を追加
|
||||
|
||||
### GenerateUI: ImpactWildcardEncode バリデーションエラー修正
|
||||
|
||||
#### `static/js/comfyui-workflow.js`
|
||||
|
||||
- `convertUiToApi()` — COMBO 型ウィジェット値を `/object_info` の選択肢リストと照合し、値が存在しない場合は先頭の有効な選択肢にフォールバック。Impact Pack の `ImpactWildcardEncode` ノードが `"Select Wildcard 🟢 Full Cache"` 等の動的プレースホルダー値を持つ場合でも `"Prompt outputs failed validation"` エラーが発生しなくなる
|
||||
- `analyzeWorkflow()` — `ImpactWildcardEncode` / `ImpactWildcardProcessor` を `prompt_nodes` として認識(KSampler の positive/negative 接続からロールを判定)。テキストフィールドキーを `text` ではなく `wildcard_text` として記録
|
||||
|
||||
#### `static/js/comfyui-editor.js`
|
||||
|
||||
- `renderPromptTab()` — select の option に `data-text-key` 属性を追加(`CLIPTextEncode` = `"text"`、`ImpactWildcardEncode` = `"wildcard_text"`)
|
||||
- Apply ハンドラ — `analysis.prompt_nodes` から `textKey` を取得して `inputs[textKey]` に書き込むよう変更(従来は `inputs.text` 固定)
|
||||
- `syncToWorkflow()` — 選択中の option の `data-text-key` を読んで正しいフィールドに書き込むよう変更。生成前の同期(`_coreGenerate` 直前の `comfyEditor.syncToWorkflow()` 経由)でも正しく反映される
|
||||
|
||||
---
|
||||
|
||||
## 2026-04-28: v0.3.2 生成UIタブ — Checkpoint Batch・Seed UIレイアウト修正
|
||||
|
||||
### 概要
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -50,20 +50,33 @@ 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 (v0.3.2)
|
||||
- **3-tab layout** — Input / Model / Settings tabs, each with a Raw JSON column on the right for instant preview and direct editing
|
||||
### GenerateUI Tab (v0.3.5)
|
||||
- **4-tab layout** — Input / Model / Settings / Feeder tabs; Input, Model, and Settings each include 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
|
||||
- **Settings tab** — KSampler and Latent Image side by side at 50% width each; 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; seed input and mode selector stacked vertically for readability
|
||||
- **Checkpoint Batch** — enable via checkbox in the right panel to sequentially generate with every checkpoint model; filter by include folders (comma-separated subfolder names, empty = all) and/or exclude folders; Stop button aborts after the current generation completes; amber progress bar tracks per-model progress
|
||||
- **UI-to-API conversion** — automatic conversion supporting subgraphs (nested workflows), COMBO types, and display-only node exclusion
|
||||
- **Checkpoint Batch** — enable via checkbox in the right panel to sequentially generate with every checkpoint model; select checkpoints by folder from the dropdown (check a folder to select all its files, expand with ▶ for individual file selection, supports any subfolder depth); Filter input to search; All / None buttons for quick selection; **Pause/Resume** suspends processing between models; Stop aborts after the current generation; amber progress bar tracks per-model progress
|
||||
- **UI-to-API conversion** — automatic conversion supporting subgraphs (nested workflows), COMBO types, and display-only node exclusion; improved analysis covers SDXL multi-hop CONDITIONING chains, CLIPTextEncodeSDXL, SDXLPromptStyler, KSamplerAdvanced, and more
|
||||
- **Eagle integration** — auto-save generated images to [Eagle](https://eagle.cool/) with metadata
|
||||
|
||||
### Feeder subtab (v0.3.5)
|
||||
Requires the **[comfyui-image-feeder](https://github.com/ketle-man/comfyui-image-feeder)** custom node.
|
||||
- **ImageFeeder node control** — select the target node from a dropdown auto-populated from the loaded workflow; edit all node parameters (Directory, Sort Mode, Index, Start/End Index, Batch Size, Seed, Use Selection) and Apply to the workflow
|
||||
- **Image library** — 3-pane layout: folder tree (left) browsing `user/default/image-loop-data/`, image grid with checkbox selection (center), preview panel with resolution and file size (right)
|
||||
- **Selection management** — check individual images; All / None buttons for the current folder; selected files are reflected in `selected_files` on Apply
|
||||
- **Presets** — save the current directory + selection as a named preset; load or delete presets (server-side persistence via `image-feeder-presets.json`)
|
||||
- **Continuous Run loop** — Run / Stop buttons below the presets; **After gen** mode controls index behavior after each generation:
|
||||
- **Loop** — advance index and wrap back to 0 when all images are exhausted (runs indefinitely)
|
||||
- **Increment** — advance index and auto-stop when all images are consumed
|
||||
- **Fixed** — always use the same index
|
||||
- **Index sync** — after each generation the node returns `next_index` via WebSocket (`image_loop_node_sync`); the Index field updates automatically
|
||||
- **Seed** — Run loop uses the right-pane seed setting (Random / Fixed / Increment / Decrement); the node's own Seed field only affects random sort order
|
||||
|
||||
### Prompt Tab
|
||||
- **3-column layout** — AI Assistant, Presets editor, and Preset Manager displayed side-by-side
|
||||
- **3-column layout** — AI Assistant (left), Presets/Preset Manager tab-panel (center), Wildcard support (right)
|
||||
- **AI chat assistant** — powered by [Ollama](https://ollama.com/), generate and refine prompts interactively
|
||||
- **Image attachment** — attach reference images for vision-capable models
|
||||
- **Translation** — JA/EN/ZH translation buttons for multilingual prompt creation
|
||||
@@ -72,6 +85,8 @@ Browse, organize, and execute workflows directly from a dedicated studio interfa
|
||||
- **Group management** — create groups, assign/remove presets, delete groups from the Presets panel
|
||||
- **Clipboard copy** — copy positive/negative prompts individually (PP Copy / NP Copy)
|
||||
- **GenUI Set** — apply preset prompts directly to the GenerateUI interface
|
||||
- **Wildcard input toolbar** — one-click buttons to insert `{|}`, `{n$|}`, `__|__`, `<lora::1:LBW=;>` and other wildcard syntax; wraps selected text when applicable
|
||||
- **Wildcard file manager** — create, view, and edit `.txt` / `.yaml` wildcard files stored in `user/default/Workflow-Studio/wildcard/`; click a filename in the file picker to insert `__filename__` at cursor
|
||||
|
||||
### Settings Tab
|
||||
- **2-column layout** — left column for all settings; right column shows the Theme panel fixed in place (sticky)
|
||||
@@ -84,6 +99,7 @@ Browse, organize, and execute workflows directly from a dedicated studio interfa
|
||||
- **Ollama connection** — configure Ollama server URL
|
||||
- **Default workflow** — set a workflow to auto-load on startup
|
||||
- **Data Management** — export all plugin data (settings, metadata, prompts, etc.) to a single JSON file; import to restore data (useful when migrating or reinstalling)
|
||||
- **Wildcard Integration** — link the WFS wildcard directory to ComfyUI-Impact-Pack's `wildcards/` directory (directory junction on Windows, symlink on other OS); existing WFS wildcard files are automatically migrated; requires [ComfyUI-Impact-Pack](https://github.com/ltdrdata/ComfyUI-Impact-Pack)
|
||||
- **Language** — English / Japanese / Chinese
|
||||
|
||||
### Gallery Tab (v0.3.1)
|
||||
@@ -215,6 +231,17 @@ Click the **camera icon** (next to the W button) in ComfyUI's top bar to capture
|
||||
|
||||
## Changelog
|
||||
|
||||
### v0.3.5
|
||||
- **Feeder subtab** — new subtab in GenerateUI for controlling [comfyui-image-feeder](https://github.com/ketle-man/comfyui-image-feeder) nodes; left pane: node selector + all ImageFeeder parameters + presets + Run/Stop controls; center pane: folder tree, image grid with checkbox selection, and preview panel
|
||||
- **Run loop with After gen modes** — Loop (wrap and continue), Increment (advance and auto-stop), Fixed (constant index); index updates automatically via WebSocket `image_loop_node_sync` after each generation; seed comes from the right-pane seed setting
|
||||
- **Workflow analysis improvements** — `analyzeWorkflow` now handles multi-hop CONDITIONING chains via BFS (up to 5 iterations), CLIPTextEncodeSDXL / CLIPTextEncodeSDXLRefiner, SDXLPromptStyler / SDXLPromptStylerAdvanced, KSamplerAdvanced (`noise_seed`), TextEncodeQwenImageEditPlus, PrimitiveStringMultiline, Power Lora Loader (rgthree), and `Checkpoint Loader` (WAS) with space in the class name
|
||||
|
||||
### v0.3.4
|
||||
- **Wildcard support panel** — new right column in the Prompt tab with a one-click toolbar for inserting wildcard syntax (`{|}`, `{n$|}`, `__|__`, `:`, `;`, `$`, `<lora::1:LBW=;>`, `[]`, single/multi pick) and wrapping selected text; dedicated prompt textarea and wildcard file picker (click to insert `__filename__`)
|
||||
- **Wildcard file manager** — create, view, edit, and delete `.txt` / `.yaml` wildcard files stored in `user/default/Workflow-Studio/wildcard/`; editor opens inline with filename input and save/cancel controls
|
||||
- **Preset/Preset Manager tab switch** — the center pane of the Prompt tab is now a two-tab panel (Presets / Preset Manager) to reclaim horizontal space for the new wildcard panel
|
||||
- **Wildcard Integration setting** — new accordion section in Settings tab; auto-detects ComfyUI-Impact-Pack; when installed, provides a button to create a directory junction (Windows) or symlink (other OS) from the WFS wildcard directory to Impact Pack's `wildcards/` directory; existing WFS files are migrated automatically; a Remove Link button reverses the operation
|
||||
|
||||
### v0.3.2
|
||||
- **Checkpoint Batch** — new panel in the GenerateUI right column (below Seed, above results); enable via checkbox to auto-generate once per checkpoint model; filter included/excluded subfolders by comma-separated names (empty = all checkpoints); real-time preview of matched model count; amber progress bar shows current model name and index; Stop button aborts the loop after the current generation completes; failed models are counted and reported in the summary toast
|
||||
- **Seed layout fix** — seed value input and mode selector (Random / Fixed / Increment / Decrement) now stacked vertically so neither is clipped in narrow panels
|
||||
@@ -374,6 +401,7 @@ ComfyUI-Workflow-Studio/
|
||||
│ ├── app.js # App initialization & routing
|
||||
│ ├── workflow-tab.js # Workflow browser
|
||||
│ ├── generate-tab.js # Generation UI
|
||||
│ ├── feeder-tab.js # Feeder subtab (ImageFeeder node control + image library)
|
||||
│ ├── prompt-tab.js # AI assistant & presets
|
||||
│ ├── settings-tab.js # Settings panel
|
||||
│ ├── comfyui-client.js # ComfyUI WebSocket/API client
|
||||
|
||||
@@ -13,6 +13,7 @@ STATIC_DIR = PLUGIN_DIR / "static"
|
||||
|
||||
# ComfyUI root is custom_nodes/../../ = two levels up
|
||||
_COMFYUI_ROOT = PLUGIN_DIR.parent.parent
|
||||
COMFYUI_ROOT = _COMFYUI_ROOT # exposed for external use
|
||||
|
||||
# Data directory: prefer ComfyUI's user/default/Workflow-Studio/, fallback to plugin data/
|
||||
_COMFYUI_USER_DEFAULT = _COMFYUI_ROOT / "user" / "default"
|
||||
@@ -67,3 +68,4 @@ NODE_METADATA_FILE = DATA_DIR / "node_metadata.json"
|
||||
NODE_SETS_FILE = DATA_DIR / "node_sets.json"
|
||||
PROMPTS_FILE = DATA_DIR / "prompts.json"
|
||||
MODEL_METADATA_FILE = DATA_DIR / "model_metadata.json"
|
||||
WILDCARD_DIR = DATA_DIR / "wildcard"
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
"""Wildcard file management API routes."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from ..config import DATA_DIR, COMFYUI_ROOT
|
||||
from ..services.wildcard_service import WildcardService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_service = WildcardService(DATA_DIR / "wildcard")
|
||||
|
||||
|
||||
def setup_routes(app: web.Application):
|
||||
app.router.add_get("/api/wfm/wildcards", handle_list)
|
||||
app.router.add_get("/api/wfm/wildcards/content", handle_get_content)
|
||||
app.router.add_post("/api/wfm/wildcards/save", handle_save)
|
||||
app.router.add_post("/api/wfm/wildcards/delete", handle_delete)
|
||||
app.router.add_get("/api/wfm/wildcards/link-status", handle_link_status)
|
||||
app.router.add_post("/api/wfm/wildcards/create-link", handle_create_link)
|
||||
app.router.add_post("/api/wfm/wildcards/remove-link", handle_remove_link)
|
||||
|
||||
|
||||
async def handle_list(request: web.Request) -> web.Response:
|
||||
try:
|
||||
result = await asyncio.to_thread(_service.list_wildcards)
|
||||
return web.json_response(result)
|
||||
except Exception as e:
|
||||
logger.error("Error listing wildcards: %s", e)
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
|
||||
async def handle_get_content(request: web.Request) -> web.Response:
|
||||
filename = request.rel_url.query.get("filename", "")
|
||||
try:
|
||||
content = await asyncio.to_thread(_service.get_content, filename)
|
||||
return web.json_response({"content": content})
|
||||
except FileNotFoundError:
|
||||
return web.json_response({"error": "not found"}, status=404)
|
||||
except Exception as e:
|
||||
logger.error("Error getting wildcard content: %s", e)
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
|
||||
async def handle_save(request: web.Request) -> web.Response:
|
||||
try:
|
||||
body = await request.json()
|
||||
filename = body.get("filename", "")
|
||||
content = body.get("content", "")
|
||||
result = await asyncio.to_thread(_service.save_file, filename, content)
|
||||
return web.json_response({"status": "ok", "file": result})
|
||||
except Exception as e:
|
||||
logger.error("Error saving wildcard: %s", e)
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
|
||||
async def handle_delete(request: web.Request) -> web.Response:
|
||||
try:
|
||||
body = await request.json()
|
||||
filename = body.get("filename", "")
|
||||
await asyncio.to_thread(_service.delete_file, filename)
|
||||
return web.json_response({"status": "ok"})
|
||||
except Exception as e:
|
||||
logger.error("Error deleting wildcard: %s", e)
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
|
||||
async def handle_link_status(request: web.Request) -> web.Response:
|
||||
try:
|
||||
result = await asyncio.to_thread(_service.get_link_status, COMFYUI_ROOT)
|
||||
return web.json_response(result)
|
||||
except Exception as e:
|
||||
logger.error("Error getting wildcard link status: %s", e)
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
|
||||
async def handle_create_link(request: web.Request) -> web.Response:
|
||||
try:
|
||||
result = await asyncio.to_thread(_service.create_link, COMFYUI_ROOT)
|
||||
return web.json_response({"status": "ok", **result})
|
||||
except RuntimeError as e:
|
||||
return web.json_response({"error": str(e)}, status=400)
|
||||
except Exception as e:
|
||||
logger.error("Error creating wildcard link: %s", e)
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
|
||||
async def handle_remove_link(request: web.Request) -> web.Response:
|
||||
try:
|
||||
await asyncio.to_thread(_service.remove_link)
|
||||
return web.json_response({"status": "ok"})
|
||||
except RuntimeError as e:
|
||||
return web.json_response({"error": str(e)}, status=400)
|
||||
except Exception as e:
|
||||
logger.error("Error removing wildcard link: %s", e)
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
@@ -0,0 +1,205 @@
|
||||
"""Wildcard file management service."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ALLOWED_EXTS = {"txt", "yaml", "yml"}
|
||||
|
||||
|
||||
class WildcardService:
|
||||
def __init__(self, wildcard_dir: Path):
|
||||
self.wildcard_dir = wildcard_dir
|
||||
# Only mkdir if it is a real directory (not a junction/symlink)
|
||||
if not self.wildcard_dir.exists() and not self._is_junction(self.wildcard_dir):
|
||||
self.wildcard_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# File CRUD
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def list_wildcards(self) -> list[dict]:
|
||||
"""Return sorted list of wildcard files recursively (follows junction/symlink)."""
|
||||
if not self.wildcard_dir.exists():
|
||||
return []
|
||||
files = []
|
||||
for path in sorted(self.wildcard_dir.rglob("*")):
|
||||
if path.is_file() and path.suffix.lstrip(".").lower() in ALLOWED_EXTS:
|
||||
rel = path.relative_to(self.wildcard_dir)
|
||||
dir_posix = rel.parent.as_posix()
|
||||
if dir_posix == ".":
|
||||
dir_posix = ""
|
||||
files.append({
|
||||
"name": path.stem,
|
||||
"filename": rel.as_posix(),
|
||||
"ext": path.suffix.lstrip(".").lower(),
|
||||
"size": path.stat().st_size,
|
||||
"dir": dir_posix,
|
||||
"wc_name": rel.with_suffix("").as_posix(),
|
||||
})
|
||||
return files
|
||||
|
||||
def get_content(self, filename: str) -> str:
|
||||
path = self._safe_path(filename)
|
||||
if path is None:
|
||||
raise ValueError(f"Invalid filename: {filename}")
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError(f"File not found: {filename}")
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
def save_file(self, filename: str, content: str) -> dict:
|
||||
path = self._safe_path(filename)
|
||||
if path is None:
|
||||
raise ValueError(f"Invalid filename: {filename}")
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(content, encoding="utf-8")
|
||||
rel = path.relative_to(self.wildcard_dir)
|
||||
dir_posix = rel.parent.as_posix()
|
||||
if dir_posix == ".":
|
||||
dir_posix = ""
|
||||
return {
|
||||
"name": path.stem,
|
||||
"filename": rel.as_posix(),
|
||||
"ext": path.suffix.lstrip(".").lower(),
|
||||
"size": path.stat().st_size,
|
||||
"dir": dir_posix,
|
||||
"wc_name": rel.with_suffix("").as_posix(),
|
||||
}
|
||||
|
||||
def delete_file(self, filename: str) -> None:
|
||||
path = self._safe_path(filename)
|
||||
if path is None:
|
||||
raise ValueError(f"Invalid filename: {filename}")
|
||||
if path.is_file():
|
||||
path.unlink()
|
||||
|
||||
def _safe_path(self, filename: str) -> Path | None:
|
||||
if not filename or not filename.strip():
|
||||
return None
|
||||
# Normalize to forward slashes and strip leading/trailing slashes
|
||||
filename = filename.replace("\\", "/").strip("/")
|
||||
# Validate each path component — block traversal and empty parts
|
||||
parts = filename.split("/")
|
||||
for p in parts:
|
||||
if not p or p == ".." or p == ".":
|
||||
return None
|
||||
ext = Path(parts[-1]).suffix.lstrip(".").lower()
|
||||
if ext not in ALLOWED_EXTS:
|
||||
return None
|
||||
return self.wildcard_dir.joinpath(*parts)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Impact Pack integration
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def find_impact_pack_wildcards(comfyui_root: Path) -> Path | None:
|
||||
"""Return Impact Pack wildcards directory path, or None if not installed."""
|
||||
custom_nodes = comfyui_root / "custom_nodes"
|
||||
if not custom_nodes.is_dir():
|
||||
return None
|
||||
for d in custom_nodes.iterdir():
|
||||
if d.is_dir() and "impact-pack" in d.name.lower():
|
||||
wildcards = d / "wildcards"
|
||||
if wildcards.is_dir():
|
||||
return wildcards
|
||||
return None
|
||||
|
||||
def get_link_status(self, comfyui_root: Path) -> dict:
|
||||
"""Return current Impact Pack integration status."""
|
||||
impact_dir = self.find_impact_pack_wildcards(comfyui_root)
|
||||
is_linked = self._is_junction(self.wildcard_dir) or self.wildcard_dir.is_symlink()
|
||||
|
||||
resolved_target = None
|
||||
if is_linked:
|
||||
try:
|
||||
resolved_target = str(self.wildcard_dir.resolve())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {
|
||||
"impact_pack_installed": impact_dir is not None,
|
||||
"impact_pack_wildcards_dir": str(impact_dir) if impact_dir else None,
|
||||
"wfs_wildcard_dir": str(self.wildcard_dir),
|
||||
"is_linked": is_linked,
|
||||
"link_target": resolved_target,
|
||||
}
|
||||
|
||||
def create_link(self, comfyui_root: Path) -> dict:
|
||||
"""Replace WFS wildcard dir with a junction/symlink → Impact Pack wildcards."""
|
||||
impact_dir = self.find_impact_pack_wildcards(comfyui_root)
|
||||
if impact_dir is None:
|
||||
raise RuntimeError("ComfyUI-Impact-Pack is not installed.")
|
||||
|
||||
if self._is_junction(self.wildcard_dir) or self.wildcard_dir.is_symlink():
|
||||
raise RuntimeError("Link already exists.")
|
||||
|
||||
# Migrate existing WFS files to Impact Pack dir
|
||||
migrated: list[str] = []
|
||||
if self.wildcard_dir.is_dir():
|
||||
for f in self.wildcard_dir.iterdir():
|
||||
if f.is_file():
|
||||
dest = impact_dir / f.name
|
||||
if not dest.exists():
|
||||
shutil.copy2(str(f), str(dest))
|
||||
migrated.append(f.name)
|
||||
shutil.rmtree(str(self.wildcard_dir))
|
||||
|
||||
# Create junction (Windows) or symlink (others)
|
||||
self._create_junction_or_symlink(self.wildcard_dir, impact_dir)
|
||||
logger.info("Wildcard link created: %s -> %s", self.wildcard_dir, impact_dir)
|
||||
return {"migrated_files": migrated}
|
||||
|
||||
def remove_link(self) -> None:
|
||||
"""Remove junction/symlink and restore WFS wildcard dir as a plain directory."""
|
||||
if not (self._is_junction(self.wildcard_dir) or self.wildcard_dir.is_symlink()):
|
||||
raise RuntimeError("No link to remove.")
|
||||
self._remove_junction_or_symlink(self.wildcard_dir)
|
||||
self.wildcard_dir.mkdir(parents=True, exist_ok=True)
|
||||
logger.info("Wildcard link removed, restored plain dir: %s", self.wildcard_dir)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Platform helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def _is_junction(path: Path) -> bool:
|
||||
"""Return True if path is a Windows directory junction."""
|
||||
if platform.system() != "Windows":
|
||||
return False
|
||||
try:
|
||||
import ctypes
|
||||
FILE_ATTRIBUTE_REPARSE_POINT = 0x0400
|
||||
attrs = ctypes.windll.kernel32.GetFileAttributesW(str(path))
|
||||
return attrs != 0xFFFFFFFF and bool(attrs & FILE_ATTRIBUTE_REPARSE_POINT)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _create_junction_or_symlink(link: Path, target: Path) -> None:
|
||||
if platform.system() == "Windows":
|
||||
result = subprocess.run(
|
||||
["cmd", "/c", "mklink", "/J", str(link), str(target)],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"mklink /J failed: {result.stderr.strip()}")
|
||||
else:
|
||||
os.symlink(target, link, target_is_directory=True)
|
||||
|
||||
@staticmethod
|
||||
def _remove_junction_or_symlink(path: Path) -> None:
|
||||
if platform.system() == "Windows":
|
||||
result = subprocess.run(
|
||||
["cmd", "/c", "rmdir", str(path)],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"rmdir failed: {result.stderr.strip()}")
|
||||
else:
|
||||
os.unlink(str(path))
|
||||
@@ -61,7 +61,7 @@ class WorkflowStudio:
|
||||
app.router.add_get("/wfm", serve_index_page)
|
||||
|
||||
# API routes
|
||||
from .routes import workflow_routes, settings_routes, ollama_routes, eagle_routes, nodes_routes, prompts_routes, models_routes, gallery_routes
|
||||
from .routes import workflow_routes, settings_routes, ollama_routes, eagle_routes, nodes_routes, prompts_routes, models_routes, gallery_routes, wildcard_routes
|
||||
|
||||
workflow_routes.setup_routes(app)
|
||||
settings_routes.setup_routes(app)
|
||||
@@ -71,5 +71,6 @@ class WorkflowStudio:
|
||||
prompts_routes.setup_routes(app)
|
||||
models_routes.setup_routes(app)
|
||||
gallery_routes.setup_routes(app)
|
||||
wildcard_routes.setup_routes(app)
|
||||
|
||||
logger.info("Workflow Studio: Routes registered successfully")
|
||||
|
||||
@@ -688,6 +688,140 @@ body {
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
/* Checkpoint batch dropdown */
|
||||
.wfm-ckpt-dropdown-wrap {
|
||||
position: relative;
|
||||
}
|
||||
.wfm-ckpt-dropdown-trigger {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 10px;
|
||||
background: var(--wfm-input-bg, var(--wfm-bg));
|
||||
border: 1px solid var(--wfm-border);
|
||||
border-radius: var(--wfm-radius);
|
||||
color: var(--wfm-text-primary);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
.wfm-ckpt-dropdown-trigger:hover {
|
||||
border-color: var(--wfm-primary);
|
||||
}
|
||||
.wfm-ckpt-dropdown-arrow {
|
||||
font-size: 9px;
|
||||
color: var(--wfm-text-secondary);
|
||||
transition: transform 0.15s;
|
||||
flex-shrink: 0;
|
||||
margin-left: 6px;
|
||||
}
|
||||
.wfm-ckpt-dropdown-wrap.open .wfm-ckpt-dropdown-arrow {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.wfm-ckpt-dropdown-panel {
|
||||
position: absolute;
|
||||
z-index: 200;
|
||||
top: calc(100% + 2px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--wfm-surface);
|
||||
border: 1px solid var(--wfm-border);
|
||||
border-radius: var(--wfm-radius);
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.35);
|
||||
}
|
||||
.wfm-ckpt-dropdown-list {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.wfm-ckpt-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
color: var(--wfm-text-primary);
|
||||
}
|
||||
.wfm-ckpt-item:hover {
|
||||
background: rgba(255,255,255,0.05);
|
||||
}
|
||||
.wfm-ckpt-item input[type=checkbox] {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
accent-color: var(--wfm-primary);
|
||||
}
|
||||
.wfm-ckpt-item-label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.wfm-ckpt-item--indented {
|
||||
padding-left: 24px;
|
||||
}
|
||||
.wfm-ckpt-folder-group {
|
||||
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||
}
|
||||
.wfm-ckpt-folder-group:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.wfm-ckpt-folder-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--wfm-text-secondary);
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.wfm-ckpt-folder-header:hover {
|
||||
background: rgba(255,255,255,0.05);
|
||||
}
|
||||
.wfm-ckpt-folder-header input[type=checkbox] {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
accent-color: var(--wfm-primary);
|
||||
}
|
||||
.wfm-ckpt-folder-toggle {
|
||||
font-size: 8px;
|
||||
color: var(--wfm-text-secondary);
|
||||
transition: transform 0.15s;
|
||||
flex-shrink: 0;
|
||||
width: 10px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
.wfm-ckpt-folder-group.open .wfm-ckpt-folder-toggle {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
.wfm-ckpt-folder-name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
.wfm-ckpt-folder-count {
|
||||
font-size: 10px;
|
||||
color: var(--wfm-text-secondary);
|
||||
opacity: 0.65;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.wfm-ckpt-folder-files {
|
||||
display: none;
|
||||
}
|
||||
.wfm-ckpt-folder-group.open .wfm-ckpt-folder-files {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Generation panel in right column */
|
||||
.wfm-gen-panel {
|
||||
background: var(--wfm-surface);
|
||||
@@ -1923,6 +2057,321 @@ body {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Prompt Tab — Center column tabs
|
||||
============================================ */
|
||||
|
||||
.wfm-prompt-center-tabnav {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--wfm-border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.wfm-prompt-center-tab {
|
||||
flex: 1;
|
||||
padding: 8px 4px;
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
color: var(--wfm-text-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.wfm-prompt-center-tab:hover {
|
||||
color: var(--wfm-text);
|
||||
}
|
||||
|
||||
.wfm-prompt-center-tab.active {
|
||||
color: var(--wfm-text);
|
||||
border-bottom-color: var(--wfm-primary);
|
||||
}
|
||||
|
||||
.wfm-prompt-center-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Wildcard Panel
|
||||
============================================ */
|
||||
|
||||
.wfm-wc-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 10px 12px;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
/* Input assistance section */
|
||||
.wfm-wc-input-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Wildcard syntax button toolbar */
|
||||
.wfm-wc-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.wfm-wc-btn {
|
||||
padding: 3px 7px;
|
||||
font-size: 11px;
|
||||
font-family: monospace;
|
||||
background: var(--wfm-surface);
|
||||
border: 1px solid var(--wfm-border);
|
||||
border-radius: var(--wfm-radius-sm);
|
||||
color: var(--wfm-text);
|
||||
cursor: pointer;
|
||||
transition: background 0.12s, border-color 0.12s;
|
||||
white-space: nowrap;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.wfm-wc-btn:hover {
|
||||
background: var(--wfm-surface-2, var(--wfm-surface));
|
||||
border-color: var(--wfm-primary);
|
||||
color: var(--wfm-primary);
|
||||
}
|
||||
|
||||
.wfm-wc-prompt-ta {
|
||||
resize: vertical;
|
||||
min-height: 70px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.wfm-wc-prompt-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* File picker popup */
|
||||
.wfm-wc-file-picker-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.wfm-wc-file-picker {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
left: 0;
|
||||
z-index: 200;
|
||||
background: var(--wfm-bg);
|
||||
border: 1px solid var(--wfm-border);
|
||||
border-radius: var(--wfm-radius);
|
||||
min-width: 160px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.wfm-wc-picker-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 6px 10px;
|
||||
text-align: left;
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--wfm-border);
|
||||
color: var(--wfm-text);
|
||||
font-size: 11px;
|
||||
font-family: monospace;
|
||||
cursor: pointer;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
|
||||
.wfm-wc-picker-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.wfm-wc-picker-item:hover {
|
||||
background: var(--wfm-surface);
|
||||
color: var(--wfm-primary);
|
||||
}
|
||||
|
||||
.wfm-wc-picker-dir {
|
||||
padding: 4px 10px 2px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--wfm-text-secondary);
|
||||
background: var(--wfm-bg);
|
||||
border-bottom: 1px solid var(--wfm-border);
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.wfm-wc-picker-item--sub {
|
||||
padding-left: 18px;
|
||||
}
|
||||
|
||||
/* File manager section */
|
||||
.wfm-wc-files-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 10px;
|
||||
border-top: 1px solid var(--wfm-border);
|
||||
padding-top: 8px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.wfm-wc-files-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.wfm-wc-files-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--wfm-text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.wfm-wc-file-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--wfm-border);
|
||||
border-radius: var(--wfm-radius-sm);
|
||||
max-height: 180px;
|
||||
}
|
||||
|
||||
.wfm-wc-dir-header {
|
||||
padding: 4px 8px 2px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--wfm-text-secondary);
|
||||
background: var(--wfm-bg);
|
||||
border-bottom: 1px solid var(--wfm-border);
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.wfm-wc-file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 8px;
|
||||
border-bottom: 1px solid var(--wfm-border);
|
||||
cursor: pointer;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
|
||||
.wfm-wc-file-item--sub {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.wfm-wc-file-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.wfm-wc-file-item:hover {
|
||||
background: var(--wfm-surface);
|
||||
}
|
||||
|
||||
.wfm-wc-file-item-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wfm-wc-file-name {
|
||||
font-size: 12px;
|
||||
font-family: monospace;
|
||||
color: var(--wfm-text);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.wfm-wc-file-ext {
|
||||
font-size: 10px;
|
||||
color: var(--wfm-text-secondary);
|
||||
background: var(--wfm-surface);
|
||||
border: 1px solid var(--wfm-border);
|
||||
border-radius: 3px;
|
||||
padding: 1px 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.wfm-wc-file-item-actions {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.12s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.wfm-wc-file-item:hover .wfm-wc-file-item-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.wfm-wc-use-btn {
|
||||
font-size: 10px !important;
|
||||
color: var(--wfm-primary) !important;
|
||||
}
|
||||
|
||||
/* File editor */
|
||||
.wfm-wc-editor {
|
||||
margin-top: 8px;
|
||||
border: 1px solid var(--wfm-border);
|
||||
border-radius: var(--wfm-radius);
|
||||
padding: 8px;
|
||||
background: var(--wfm-surface);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.wfm-wc-editor-header {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.wfm-wc-editor-hint {
|
||||
font-size: 10px;
|
||||
color: var(--wfm-text-secondary);
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.wfm-wc-editor-ta {
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
resize: vertical;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.wfm-wc-editor-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Image Input (I2I) Slots
|
||||
============================================ */
|
||||
@@ -3378,3 +3827,237 @@ body {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
/* ── Feeder Tab ─────────────────────────────────────────────────── */
|
||||
.wfm-feeder-layout {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wfm-feeder-settings {
|
||||
width: 220px;
|
||||
min-width: 180px;
|
||||
flex-shrink: 0;
|
||||
padding: 10px;
|
||||
border-right: 1px solid var(--wfm-border, rgba(255,255,255,0.1));
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.wfm-feeder-pane-header {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--wfm-text-secondary, #8892a4);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
padding: 4px 0 6px;
|
||||
border-bottom: 1px solid var(--wfm-border, rgba(255,255,255,0.1));
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.wfm-feeder-field-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.wfm-feeder-label {
|
||||
font-size: 11px;
|
||||
color: var(--wfm-text-secondary, #8892a4);
|
||||
white-space: nowrap;
|
||||
width: 82px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.wfm-feeder-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.wfm-feeder-library {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wfm-feeder-status-bar {
|
||||
font-size: 11px;
|
||||
color: var(--wfm-text-secondary, #8892a4);
|
||||
padding: 4px 10px;
|
||||
border-bottom: 1px solid var(--wfm-border, rgba(255,255,255,0.1));
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.wfm-feeder-library-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wfm-feeder-tree-pane {
|
||||
width: 180px;
|
||||
min-width: 140px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid var(--wfm-border, rgba(255,255,255,0.1));
|
||||
padding: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wfm-feeder-tree-scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.wfm-feeder-tree-row {
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--wfm-text, #e0e4f0);
|
||||
}
|
||||
|
||||
.wfm-feeder-tree-row:hover {
|
||||
background: rgba(108, 142, 245, 0.12);
|
||||
}
|
||||
|
||||
.wfm-feeder-tree-row.active {
|
||||
background: rgba(108, 142, 245, 0.25);
|
||||
color: var(--wfm-primary, #6c8ef5);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.wfm-feeder-tree-msg {
|
||||
font-size: 12px;
|
||||
color: var(--wfm-text-secondary, #8892a4);
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.wfm-feeder-grid-pane {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.wfm-feeder-grid {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-content: flex-start;
|
||||
gap: 6px;
|
||||
padding: 4px 2px;
|
||||
}
|
||||
|
||||
.wfm-feeder-grid-msg {
|
||||
font-size: 12px;
|
||||
color: var(--wfm-text-secondary, #8892a4);
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wfm-feeder-card {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
border-radius: 6px;
|
||||
border: 2px solid transparent;
|
||||
background: rgba(255,255,255,0.04);
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.15s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.wfm-feeder-card:hover {
|
||||
border-color: rgba(108, 142, 245, 0.4);
|
||||
}
|
||||
|
||||
.wfm-feeder-card.selected {
|
||||
border-color: var(--wfm-primary, #6c8ef5);
|
||||
background: rgba(108, 142, 245, 0.1);
|
||||
}
|
||||
|
||||
.wfm-feeder-card-cb {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
cursor: pointer;
|
||||
accent-color: var(--wfm-primary, #6c8ef5);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.wfm-feeder-card-img {
|
||||
width: 100%;
|
||||
height: 90px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.wfm-feeder-card-name {
|
||||
font-size: 10px;
|
||||
color: var(--wfm-text-secondary, #8892a4);
|
||||
padding: 3px 4px;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.wfm-feeder-preview-pane {
|
||||
width: 160px;
|
||||
min-width: 130px;
|
||||
flex-shrink: 0;
|
||||
border-left: 1px solid var(--wfm-border, rgba(255,255,255,0.1));
|
||||
padding: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wfm-feeder-preview-img {
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
object-fit: contain;
|
||||
max-height: 140px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.wfm-feeder-preview-name {
|
||||
font-size: 11px;
|
||||
color: var(--wfm-text, #e0e4f0);
|
||||
font-weight: 600;
|
||||
word-break: break-all;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.wfm-feeder-preview-info {
|
||||
font-size: 10px;
|
||||
color: var(--wfm-text-secondary, #8892a4);
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
+12
-2
@@ -265,15 +265,24 @@ function applyI18nToHtml() {
|
||||
"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-gen-11": "helpGen11",
|
||||
"wfm-help-feeder-title": "helpFeederTitle",
|
||||
"wfm-help-feeder-desc": "helpFeederDesc",
|
||||
"wfm-help-feeder-1": "helpFeeder1", "wfm-help-feeder-2": "helpFeeder2",
|
||||
"wfm-help-feeder-3": "helpFeeder3", "wfm-help-feeder-4": "helpFeeder4",
|
||||
"wfm-help-feeder-5": "helpFeeder5", "wfm-help-feeder-6": "helpFeeder6",
|
||||
"wfm-help-feeder-7": "helpFeeder7", "wfm-help-feeder-8": "helpFeeder8",
|
||||
"wfm-help-feeder-9": "helpFeeder9",
|
||||
"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",
|
||||
"wfm-help-prompt-7": "helpPrompt7",
|
||||
"wfm-help-prompt-7": "helpPrompt7", "wfm-help-prompt-8": "helpPrompt8",
|
||||
"wfm-help-prompt-9": "helpPrompt9",
|
||||
"wfm-help-settings-1": "helpSettings1", "wfm-help-settings-2": "helpSettings2",
|
||||
"wfm-help-settings-3": "helpSettings3", "wfm-help-settings-4": "helpSettings4",
|
||||
"wfm-help-settings-5": "helpSettings5", "wfm-help-settings-6": "helpSettings6",
|
||||
"wfm-help-settings-7": "helpSettings7", "wfm-help-settings-8": "helpSettings8",
|
||||
"wfm-help-settings-9": "helpSettings9",
|
||||
"wfm-help-settings-9": "helpSettings9", "wfm-help-settings-10": "helpSettings10",
|
||||
"wfm-help-nodes-1": "helpNodes1", "wfm-help-nodes-2": "helpNodes2",
|
||||
"wfm-help-nodes-3": "helpNodes3", "wfm-help-nodes-4": "helpNodes4",
|
||||
"wfm-help-nodes-5": "helpNodes5", "wfm-help-nodes-6": "helpNodes6",
|
||||
@@ -289,6 +298,7 @@ function applyI18nToHtml() {
|
||||
"wfm-help-gallery-5": "helpGallery5", "wfm-help-gallery-6": "helpGallery6",
|
||||
"wfm-help-gallery-7": "helpGallery7", "wfm-help-gallery-8": "helpGallery8",
|
||||
"wfm-help-gallery-9": "helpGallery9", "wfm-help-gallery-10": "helpGallery10",
|
||||
"wfm-help-gallery-11": "helpGallery11",
|
||||
"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",
|
||||
|
||||
+23
-13
@@ -77,7 +77,7 @@ export const comfyEditor = {
|
||||
<label>Positive Prompt</label>
|
||||
<div style="display:flex;gap:8px;margin-bottom:6px;">
|
||||
<select id="wfm-prompt-pos-target" class="wfm-select" style="width:auto;flex:1">
|
||||
${positiveNodes.map((n) => `<option value="${n.id}" selected>ID:${n.id} (${n.title})</option>`).join("")}
|
||||
${positiveNodes.map((n) => `<option value="${n.id}" data-text-key="${n.textKey || "text"}" selected>ID:${n.id} (${n.title})</option>`).join("")}
|
||||
${nodeOpts}
|
||||
</select>
|
||||
<button class="wfm-btn wfm-btn-sm" id="wfm-prompt-pos-apply">Apply</button>
|
||||
@@ -88,7 +88,7 @@ export const comfyEditor = {
|
||||
<label>Negative Prompt</label>
|
||||
<div style="display:flex;gap:8px;margin-bottom:6px;">
|
||||
<select id="wfm-prompt-neg-target" class="wfm-select" style="width:auto;flex:1">
|
||||
${negativeNodes.map((n) => `<option value="${n.id}" selected>ID:${n.id} (${n.title})</option>`).join("")}
|
||||
${negativeNodes.map((n) => `<option value="${n.id}" data-text-key="${n.textKey || "text"}" selected>ID:${n.id} (${n.title})</option>`).join("")}
|
||||
${nodeOpts}
|
||||
</select>
|
||||
<button class="wfm-btn wfm-btn-sm" id="wfm-prompt-neg-apply">Apply</button>
|
||||
@@ -101,7 +101,9 @@ export const comfyEditor = {
|
||||
const nodeId = document.getElementById("wfm-prompt-pos-target")?.value;
|
||||
const text = document.getElementById("wfm-prompt-pos-text")?.value;
|
||||
if (nodeId && comfyUI.currentWorkflow?.[nodeId]) {
|
||||
comfyUI.currentWorkflow[nodeId].inputs.text = text;
|
||||
const promptNode = (analysis.prompt_nodes || []).find(n => n.id === nodeId);
|
||||
const textKey = promptNode?.textKey || "text";
|
||||
comfyUI.currentWorkflow[nodeId].inputs[textKey] = text;
|
||||
_syncRawJson();
|
||||
}
|
||||
});
|
||||
@@ -110,7 +112,9 @@ export const comfyEditor = {
|
||||
const nodeId = document.getElementById("wfm-prompt-neg-target")?.value;
|
||||
const text = document.getElementById("wfm-prompt-neg-text")?.value;
|
||||
if (nodeId && comfyUI.currentWorkflow?.[nodeId]) {
|
||||
comfyUI.currentWorkflow[nodeId].inputs.text = text;
|
||||
const promptNode = (analysis.prompt_nodes || []).find(n => n.id === nodeId);
|
||||
const textKey = promptNode?.textKey || "text";
|
||||
comfyUI.currentWorkflow[nodeId].inputs[textKey] = text;
|
||||
_syncRawJson();
|
||||
}
|
||||
});
|
||||
@@ -209,11 +213,11 @@ export const comfyEditor = {
|
||||
const latent = analysis.latent_nodes?.[0];
|
||||
|
||||
el.innerHTML = `
|
||||
<div style="display:flex;flex-direction:column;gap:0;">
|
||||
<div style="padding-bottom:14px;border-bottom:1px solid var(--wfm-border);margin-bottom:14px;">
|
||||
<div style="display:flex;flex-direction:row;gap:0;align-items:flex-start;">
|
||||
<div style="flex:1;min-width:0;padding-right:14px;border-right:1px solid var(--wfm-border);">
|
||||
<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}">
|
||||
<input type="hidden" id="wfm-settings-sampler-id" value="${sampler.id}" data-seed-key="${sampler.seedKey || "seed"}">
|
||||
<div class="wfm-form-group">
|
||||
<label>Seed</label>
|
||||
<input type="number" class="wfm-input" id="wfm-settings-seed" value="${sampler.seed ?? -1}">
|
||||
@@ -245,7 +249,7 @@ export const comfyEditor = {
|
||||
<button class="wfm-btn wfm-btn-sm" id="wfm-settings-sampler-apply">Apply</button>
|
||||
` : "<p class='wfm-placeholder'>No KSampler node found</p>"}
|
||||
</div>
|
||||
<div>
|
||||
<div style="flex:1;min-width:0;padding-left: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;">Latent Image</h3>
|
||||
${latent ? `
|
||||
<input type="hidden" id="wfm-settings-latent-id" value="${latent.id}">
|
||||
@@ -271,7 +275,9 @@ export const comfyEditor = {
|
||||
const nodeId = document.getElementById("wfm-settings-sampler-id")?.value;
|
||||
if (!nodeId || !comfyUI.currentWorkflow?.[nodeId]) return;
|
||||
const inputs = comfyUI.currentWorkflow[nodeId].inputs;
|
||||
inputs.seed = parseInt(document.getElementById("wfm-settings-seed")?.value) || -1;
|
||||
// KSamplerAdvanced uses noise_seed; fall back to seed
|
||||
const seedKey = document.getElementById("wfm-settings-sampler-id")?.dataset?.seedKey || "seed";
|
||||
inputs[seedKey] = parseInt(document.getElementById("wfm-settings-seed")?.value) || -1;
|
||||
inputs.steps = parseInt(document.getElementById("wfm-settings-steps")?.value) || 20;
|
||||
inputs.cfg = parseFloat(document.getElementById("wfm-settings-cfg")?.value) || 7;
|
||||
inputs.sampler_name = document.getElementById("wfm-settings-sampler-name")?.value;
|
||||
@@ -402,16 +408,20 @@ export const comfyEditor = {
|
||||
|
||||
syncToWorkflow() {
|
||||
// Sync prompt texts before generation
|
||||
const posTarget = document.getElementById("wfm-prompt-pos-target")?.value;
|
||||
const posSelect = document.getElementById("wfm-prompt-pos-target");
|
||||
const posTarget = posSelect?.value;
|
||||
const posText = document.getElementById("wfm-prompt-pos-text")?.value;
|
||||
if (posTarget && comfyUI.currentWorkflow?.[posTarget]) {
|
||||
comfyUI.currentWorkflow[posTarget].inputs.text = posText;
|
||||
const posTextKey = posSelect.selectedOptions[0]?.dataset?.textKey || "text";
|
||||
comfyUI.currentWorkflow[posTarget].inputs[posTextKey] = posText;
|
||||
}
|
||||
|
||||
const negTarget = document.getElementById("wfm-prompt-neg-target")?.value;
|
||||
const negSelect = document.getElementById("wfm-prompt-neg-target");
|
||||
const negTarget = negSelect?.value;
|
||||
const negText = document.getElementById("wfm-prompt-neg-text")?.value;
|
||||
if (negTarget && comfyUI.currentWorkflow?.[negTarget]) {
|
||||
comfyUI.currentWorkflow[negTarget].inputs.text = negText;
|
||||
const negTextKey = negSelect.selectedOptions[0]?.dataset?.textKey || "text";
|
||||
comfyUI.currentWorkflow[negTarget].inputs[negTextKey] = negText;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
+183
-10
@@ -343,6 +343,24 @@ export const comfyWorkflow = {
|
||||
wIdx++;
|
||||
}
|
||||
}
|
||||
// For COMBO inputs, validate the value against the available choices.
|
||||
// Dynamic COMBO options (e.g. Impact Pack's "Select to add Wildcard") may
|
||||
// differ between the saved workflow and the current ComfyUI instance
|
||||
// (e.g. "Select Wildcard 🟢 Full Cache" vs "Select Wildcard"), causing
|
||||
// "Prompt outputs failed validation". Fall back to first choice when mismatch.
|
||||
if (expectedType === "COMBO") {
|
||||
const allInputDefs = {
|
||||
...(objectInfo[node.type]?.input?.required || {}),
|
||||
...(objectInfo[node.type]?.input?.optional || {}),
|
||||
};
|
||||
const spec = allInputDefs[name];
|
||||
if (spec) {
|
||||
const choices = Array.isArray(spec[0]) ? spec[0] : null;
|
||||
if (choices && choices.length > 0 && !choices.includes(val)) {
|
||||
val = choices[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
inputs[name] = val;
|
||||
}
|
||||
} else {
|
||||
@@ -446,7 +464,7 @@ export const comfyWorkflow = {
|
||||
|
||||
if (!workflow) return result;
|
||||
|
||||
// Build sampler connections for prompt classification
|
||||
// === Pass 1: Collect sampler refs + all_nodes ===
|
||||
const samplerPositiveRef = {};
|
||||
const samplerNegativeRef = {};
|
||||
|
||||
@@ -462,40 +480,174 @@ export const comfyWorkflow = {
|
||||
if (Array.isArray(inputs.positive)) samplerPositiveRef[inputs.positive[0]] = true;
|
||||
if (Array.isArray(inputs.negative)) samplerNegativeRef[inputs.negative[0]] = true;
|
||||
|
||||
// KSamplerAdvanced uses noise_seed instead of seed
|
||||
const seedKey = "seed" in inputs ? "seed" : "noise_seed";
|
||||
result.sampler_nodes.push({
|
||||
id, type: ct, title,
|
||||
seed: inputs.seed, steps: inputs.steps, cfg: inputs.cfg,
|
||||
seed: inputs.seed ?? inputs.noise_seed,
|
||||
seedKey,
|
||||
steps: inputs.steps, cfg: inputs.cfg,
|
||||
sampler_name: inputs.sampler_name, scheduler: inputs.scheduler,
|
||||
denoise: inputs.denoise,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// === Pass 1b: Expand refs through CONDITIONING graph (BFS, 5 iterations) ===
|
||||
// Nodes that pass CONDITIONING through without changing positive/negative role
|
||||
const COND_PASSTHROUGH = new Set([
|
||||
"ConditioningCombine", "ConditioningConcat", "ConditioningAverage",
|
||||
"ConditioningZeroOut", "ConditioningSetTimestepRange",
|
||||
"ControlNetApply", "ControlNetApplyAdvanced",
|
||||
"IPAdapterApply", "IPAdapterApplyFaceID",
|
||||
"StyleModelApply",
|
||||
]);
|
||||
|
||||
for (let iter = 0; iter < 5; iter++) {
|
||||
for (const [id, node] of Object.entries(workflow)) {
|
||||
if (!node?.class_type) continue;
|
||||
const ct = node.class_type;
|
||||
const inputs = node.inputs || {};
|
||||
const isPos = !!samplerPositiveRef[id];
|
||||
const isNeg = !!samplerNegativeRef[id];
|
||||
if (!isPos && !isNeg) continue;
|
||||
|
||||
if (COND_PASSTHROUGH.has(ct)) {
|
||||
for (const v of Object.values(inputs)) {
|
||||
if (Array.isArray(v)) {
|
||||
if (isPos) samplerPositiveRef[v[0]] = true;
|
||||
if (isNeg) samplerNegativeRef[v[0]] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Propagate role through text encoder linked inputs (CLIPTextEncode → upstream text source)
|
||||
if (ct === "CLIPTextEncode") {
|
||||
const tv = inputs.text;
|
||||
if (Array.isArray(tv)) {
|
||||
if (isPos) samplerPositiveRef[tv[0]] = true;
|
||||
if (isNeg) samplerNegativeRef[tv[0]] = true;
|
||||
}
|
||||
}
|
||||
if (ct === "CLIPTextEncodeSDXL" || ct === "CLIPTextEncodeSDXLRefiner") {
|
||||
for (const key of ["text_g", "text_l"]) {
|
||||
const tv = inputs[key];
|
||||
if (Array.isArray(tv)) {
|
||||
if (isPos) samplerPositiveRef[tv[0]] = true;
|
||||
if (isNeg) samplerNegativeRef[tv[0]] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ct === "ImpactWildcardEncode" || ct === "ImpactWildcardProcessor") {
|
||||
const tv = inputs.wildcard_text;
|
||||
if (Array.isArray(tv)) {
|
||||
if (isPos) samplerPositiveRef[tv[0]] = true;
|
||||
if (isNeg) samplerNegativeRef[tv[0]] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === Pass 2: Main analysis ===
|
||||
for (const [id, node] of Object.entries(workflow)) {
|
||||
if (!node?.class_type) continue;
|
||||
const ct = node.class_type;
|
||||
const inputs = node.inputs || {};
|
||||
const title = node._meta?.title || ct;
|
||||
|
||||
if (ct.includes("CheckpointLoader")) {
|
||||
const isPos = !!samplerPositiveRef[id];
|
||||
const isNeg = !!samplerNegativeRef[id];
|
||||
|
||||
const getRole = () => {
|
||||
if (isPos && !isNeg) return "positive";
|
||||
if (isNeg && !isPos) return "negative";
|
||||
if (/pos|正/i.test(title)) return "positive";
|
||||
if (/neg|負/i.test(title)) return "negative";
|
||||
return "unknown";
|
||||
};
|
||||
|
||||
// --- Checkpoint nodes ---
|
||||
// Matches: CheckpointLoaderSimple, CheckpointLoader, "Checkpoint Loader" (WAS), etc.
|
||||
if (ct.includes("CheckpointLoader") || ct === "Checkpoint Loader") {
|
||||
result.checkpoint_nodes.push({
|
||||
id, type: ct, title,
|
||||
ckpt_name: inputs.ckpt_name,
|
||||
});
|
||||
}
|
||||
|
||||
// --- Prompt nodes ---
|
||||
|
||||
if (ct === "CLIPTextEncode") {
|
||||
let role = "unknown";
|
||||
if (samplerPositiveRef[id]) role = "positive";
|
||||
else if (samplerNegativeRef[id]) role = "negative";
|
||||
else if (/pos|正/i.test(title)) role = "positive";
|
||||
else if (/neg|負/i.test(title)) role = "negative";
|
||||
// Only add when text is a direct string (if linked, upstream node will be detected)
|
||||
const textVal = inputs.text;
|
||||
if (typeof textVal === "string") {
|
||||
result.prompt_nodes.push({
|
||||
id, type: ct, title, role: getRole(),
|
||||
text: textVal, textKey: "text",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// SDXL dual-text encoder
|
||||
if (ct === "CLIPTextEncodeSDXL" || ct === "CLIPTextEncodeSDXLRefiner") {
|
||||
const textG = inputs.text_g;
|
||||
const textL = inputs.text_l;
|
||||
// Add only if at least one text field is a direct string
|
||||
if (typeof textG === "string" || typeof textL === "string") {
|
||||
result.prompt_nodes.push({
|
||||
id, type: ct, title, role: getRole(),
|
||||
text: typeof textG === "string" ? textG : (typeof textL === "string" ? textL : ""),
|
||||
textKey: typeof textG === "string" ? "text_g" : "text_l",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Qwen-based image+text encoder
|
||||
if (ct === "TextEncodeQwenImageEditPlus") {
|
||||
result.prompt_nodes.push({
|
||||
id, type: ct, title, role,
|
||||
text: inputs.text,
|
||||
id, type: ct, title, role: getRole(),
|
||||
text: inputs.prompt || "",
|
||||
textKey: "prompt",
|
||||
});
|
||||
}
|
||||
|
||||
// SDXLPromptStyler / SDXLPromptStylerAdvanced — contains both pos & neg in one node
|
||||
if (ct === "SDXLPromptStyler" || ct === "SDXLPromptStylerAdvanced") {
|
||||
if (inputs.text_positive !== undefined) {
|
||||
result.prompt_nodes.push({
|
||||
id, type: ct, title: `${title} [positive]`, role: "positive",
|
||||
text: inputs.text_positive || "", textKey: "text_positive",
|
||||
});
|
||||
}
|
||||
if (inputs.text_negative !== undefined) {
|
||||
result.prompt_nodes.push({
|
||||
id, type: ct, title: `${title} [negative]`, role: "negative",
|
||||
text: inputs.text_negative || "", textKey: "text_negative",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ImpactWildcardEncode / ImpactWildcardProcessor — treat wildcard_text as prompt
|
||||
if (ct === "ImpactWildcardEncode" || ct === "ImpactWildcardProcessor") {
|
||||
const textVal = inputs.wildcard_text;
|
||||
if (typeof textVal === "string") {
|
||||
result.prompt_nodes.push({
|
||||
id, type: ct, title, role: getRole(),
|
||||
text: textVal, textKey: "wildcard_text",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// PrimitiveStringMultiline / PrimitiveString — when feeding a prompt node
|
||||
if ((ct === "PrimitiveStringMultiline" || ct === "PrimitiveString") && (isPos || isNeg)) {
|
||||
result.prompt_nodes.push({
|
||||
id, type: ct, title, role: getRole(),
|
||||
text: inputs.value || "",
|
||||
textKey: "value",
|
||||
});
|
||||
}
|
||||
|
||||
// --- Latent nodes ---
|
||||
if (ct === "EmptyLatentImage" || ct === "EmptySD3LatentImage") {
|
||||
result.latent_nodes.push({
|
||||
id, type: ct, title,
|
||||
@@ -504,6 +656,7 @@ export const comfyWorkflow = {
|
||||
});
|
||||
}
|
||||
|
||||
// --- LoRA nodes ---
|
||||
if (ct === "LoraLoader" || ct === "LoraLoaderModelOnly") {
|
||||
result.lora_nodes.push({
|
||||
id, type: ct, title,
|
||||
@@ -513,14 +666,31 @@ export const comfyWorkflow = {
|
||||
});
|
||||
}
|
||||
|
||||
// Power Lora Loader (rgthree) — dynamic lora_N inputs in API format
|
||||
if (ct === "Power Lora Loader (rgthree)") {
|
||||
for (const [k, v] of Object.entries(inputs)) {
|
||||
if (/^lora_\d+$/.test(k) && typeof v === "string") {
|
||||
result.lora_nodes.push({
|
||||
id, type: ct, title,
|
||||
lora_name: v,
|
||||
strength_model: inputs[k.replace("lora_", "strength_")] ?? 1,
|
||||
strength_clip: inputs[k.replace("lora_", "strength_clip_")] ?? 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- VAE nodes ---
|
||||
if (ct === "VAELoader") {
|
||||
result.vae_nodes.push({ id, type: ct, title, vae_name: inputs.vae_name });
|
||||
}
|
||||
|
||||
// --- Save/preview nodes ---
|
||||
if (ct === "SaveImage" || ct === "PreviewImage") {
|
||||
result.save_nodes.push({ id, type: ct, title });
|
||||
}
|
||||
|
||||
// --- Load image nodes ---
|
||||
if (ct === "LoadImage" || ct === "LoadImageMask"
|
||||
|| (ct.toLowerCase().includes("load") && ct.toLowerCase().includes("image") && inputs.image !== undefined)) {
|
||||
result.load_image_nodes.push({
|
||||
@@ -528,6 +698,7 @@ export const comfyWorkflow = {
|
||||
});
|
||||
}
|
||||
|
||||
// --- Text encoder (CLIP) nodes ---
|
||||
if (ct === "DualCLIPLoader" || ct === "CLIPLoader") {
|
||||
result.text_encoder_nodes.push({
|
||||
id, type: ct, title,
|
||||
@@ -536,12 +707,14 @@ export const comfyWorkflow = {
|
||||
});
|
||||
}
|
||||
|
||||
// --- Diffusion model nodes ---
|
||||
if (ct === "UNETLoader" || ct === "UnetLoaderGGUF") {
|
||||
result.diffusion_model_nodes.push({
|
||||
id, type: ct, title, unet_name: inputs.unet_name,
|
||||
});
|
||||
}
|
||||
|
||||
// --- ControlNet loader nodes ---
|
||||
if (ct.includes("ControlNetLoader")) {
|
||||
result.controlnet_nodes.push({
|
||||
id, type: ct, title,
|
||||
|
||||
@@ -0,0 +1,510 @@
|
||||
/**
|
||||
* Feeder Tab - ImageFeeder node control with embedded image library
|
||||
*/
|
||||
|
||||
import { comfyUI } from "./comfyui-client.js";
|
||||
import { showToast } from "./app.js";
|
||||
|
||||
// ── Module State ──────────────────────────────────────────────────
|
||||
const _s = {
|
||||
dir: "", // current folder relative path
|
||||
images: [], // filenames in current folder
|
||||
selected: new Set(),// selected filenames (across all interactions)
|
||||
presets: {}, // {presetName: {directory, selected_files}}
|
||||
running: false, // feeder run loop active
|
||||
};
|
||||
|
||||
// ── Node helpers ──────────────────────────────────────────────────
|
||||
|
||||
function _feederNodes() {
|
||||
const wf = comfyUI.currentWorkflow;
|
||||
if (!wf) return [];
|
||||
return Object.entries(wf)
|
||||
.filter(([, n]) => n?.class_type === "ImageFeeder")
|
||||
.map(([id, n]) => ({ id, title: n._meta?.title || `ImageFeeder` }));
|
||||
}
|
||||
|
||||
export function refreshFeederNodeList() {
|
||||
const sel = document.getElementById("wfm-feeder-node-sel");
|
||||
if (!sel) return;
|
||||
const nodes = _feederNodes();
|
||||
if (nodes.length === 0) {
|
||||
sel.innerHTML = `<option value="">-- No ImageFeeder node --</option>`;
|
||||
return;
|
||||
}
|
||||
const prevVal = sel.value;
|
||||
sel.innerHTML = nodes.map(n =>
|
||||
`<option value="${n.id}">${n.title} (ID:${n.id})</option>`
|
||||
).join("");
|
||||
if (prevVal && nodes.find(n => n.id === prevVal)) sel.value = prevVal;
|
||||
_loadFromNode(sel.value || nodes[0].id);
|
||||
}
|
||||
|
||||
function _loadFromNode(nodeId) {
|
||||
const wf = comfyUI.currentWorkflow;
|
||||
if (!wf?.[nodeId]) return;
|
||||
const inp = wf[nodeId].inputs || {};
|
||||
|
||||
_setVal("wfm-feeder-dir-input", inp.directory || "");
|
||||
_setVal("wfm-feeder-sort", inp.sort_mode || "ascending");
|
||||
_setVal("wfm-feeder-index", inp.index ?? 0);
|
||||
_setVal("wfm-feeder-start", inp.start_index ?? 0);
|
||||
_setVal("wfm-feeder-end", inp.end_index ?? 0);
|
||||
_setVal("wfm-feeder-batch", inp.batch_size ?? 1);
|
||||
_setVal("wfm-feeder-seed", inp.seed ?? 0);
|
||||
_setCheck("wfm-feeder-use-sel", inp.use_selection ?? true);
|
||||
|
||||
// Restore selected_files
|
||||
_s.selected.clear();
|
||||
try {
|
||||
const arr = JSON.parse(inp.selected_files || "[]");
|
||||
if (Array.isArray(arr)) arr.forEach(f => _s.selected.add(f));
|
||||
} catch {}
|
||||
|
||||
const dir = inp.directory || "";
|
||||
if (_s.dir !== dir) {
|
||||
_s.dir = dir;
|
||||
_loadImages(dir);
|
||||
_highlightTreeRow(dir);
|
||||
} else {
|
||||
_refreshGridCbs();
|
||||
}
|
||||
_updateStatus();
|
||||
}
|
||||
|
||||
function _applyToWorkflow() {
|
||||
const nodeId = document.getElementById("wfm-feeder-node-sel")?.value;
|
||||
const wf = comfyUI.currentWorkflow;
|
||||
if (!nodeId || !wf?.[nodeId]) {
|
||||
showToast("No ImageFeeder node selected", "error");
|
||||
return;
|
||||
}
|
||||
_applyToNode(nodeId);
|
||||
showToast("Applied to workflow", "success");
|
||||
}
|
||||
|
||||
// ── Run Loop ──────────────────────────────────────────────────────
|
||||
|
||||
function _setRunUI(running) {
|
||||
const runBtn = document.getElementById("wfm-feeder-run-btn");
|
||||
const stopBtn = document.getElementById("wfm-feeder-stop-btn");
|
||||
if (runBtn) runBtn.disabled = running;
|
||||
if (stopBtn) stopBtn.disabled = !running;
|
||||
}
|
||||
|
||||
function _applyToNode(nodeId) {
|
||||
const wf = comfyUI.currentWorkflow;
|
||||
const inp = wf[nodeId].inputs;
|
||||
inp.directory = _s.dir;
|
||||
inp.sort_mode = document.getElementById("wfm-feeder-sort")?.value || "ascending";
|
||||
inp.index = parseInt(document.getElementById("wfm-feeder-index")?.value) || 0;
|
||||
inp.start_index = parseInt(document.getElementById("wfm-feeder-start")?.value) || 0;
|
||||
inp.end_index = parseInt(document.getElementById("wfm-feeder-end")?.value) || 0;
|
||||
inp.batch_size = parseInt(document.getElementById("wfm-feeder-batch")?.value) || 1;
|
||||
inp.seed = parseInt(document.getElementById("wfm-feeder-seed")?.value) || 0;
|
||||
inp.use_selection = document.getElementById("wfm-feeder-use-sel")?.checked ?? true;
|
||||
inp.selected_files = JSON.stringify([..._s.selected]);
|
||||
}
|
||||
|
||||
async function _startRun() {
|
||||
if (_s.running) return;
|
||||
if (!comfyUI.currentWorkflow) { showToast("No workflow loaded", "error"); return; }
|
||||
if (!comfyUI.connected) { showToast("Not connected to ComfyUI", "error"); return; }
|
||||
|
||||
const nodeId = document.getElementById("wfm-feeder-node-sel")?.value;
|
||||
if (!nodeId || !comfyUI.currentWorkflow[nodeId]) {
|
||||
showToast("No ImageFeeder node selected", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// WebSocket を先に接続してノード sync メッセージを受け取れるようにする
|
||||
const wsOk = await comfyUI.connectWebSocket();
|
||||
if (!wsOk) { showToast("WebSocket connection failed", "error"); return; }
|
||||
|
||||
_s.running = true;
|
||||
_setRunUI(true);
|
||||
|
||||
const progressBar = document.getElementById("wfm-gen-progress-bar");
|
||||
const progressText = document.getElementById("wfm-gen-progress-text");
|
||||
const resultImg = document.getElementById("wfm-gen-result-img");
|
||||
|
||||
// image_loop_node_sync メッセージを捕捉するリスナー
|
||||
let _lastSync = null;
|
||||
const _syncHandler = (ev) => {
|
||||
try {
|
||||
const msg = JSON.parse(ev.data);
|
||||
if (msg.type === "image_loop_node_sync" && String(msg.data?.node_id) === String(nodeId)) {
|
||||
_lastSync = msg.data;
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
comfyUI.socket.addEventListener("message", _syncHandler);
|
||||
|
||||
let count = 0;
|
||||
try {
|
||||
while (_s.running) {
|
||||
_lastSync = null;
|
||||
_applyToNode(nodeId);
|
||||
|
||||
const wf = comfyUI.currentWorkflow;
|
||||
const inp = wf[nodeId].inputs;
|
||||
|
||||
if (progressBar) progressBar.style.width = "0%";
|
||||
if (progressText) progressText.textContent = `#${++count} 0%`;
|
||||
|
||||
// 右ペインのseed設定を使用(KSamplerに適用される)
|
||||
const seedMode = document.getElementById("wfm-gen-seed-mode")?.value || "random";
|
||||
const seedValue = parseInt(document.getElementById("wfm-gen-seed-value")?.value) || -1;
|
||||
|
||||
const { images, seed } = await comfyUI.generate(
|
||||
{ ...wf },
|
||||
{
|
||||
seedMode,
|
||||
seedValue,
|
||||
onProgress: (pct) => {
|
||||
if (progressBar) progressBar.style.width = `${(pct * 100).toFixed(1)}%`;
|
||||
if (progressText) progressText.textContent = `#${count} ${(pct * 100).toFixed(0)}%`;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// 右ペインのseed表示を更新(通常の生成と同じ挙動)
|
||||
const seedEl = document.getElementById("wfm-gen-seed-value");
|
||||
if (seedEl) seedEl.value = seed;
|
||||
|
||||
if (progressBar) progressBar.style.width = "100%";
|
||||
|
||||
if (images.length > 0 && resultImg) {
|
||||
const blob = await comfyUI.getImageBlob(images[0]);
|
||||
resultImg.src = URL.createObjectURL(blob);
|
||||
resultImg.style.display = "block";
|
||||
}
|
||||
|
||||
if (!_s.running) break;
|
||||
|
||||
// control after generate: ノードから返された next_index を反映
|
||||
const control = document.getElementById("wfm-feeder-control-after")?.value || "loop";
|
||||
if (control === "fixed") {
|
||||
// index を変えない
|
||||
} else if (_lastSync) {
|
||||
const indexEl = document.getElementById("wfm-feeder-index");
|
||||
if (indexEl) indexEl.value = _lastSync.next_index;
|
||||
if (control === "increment" && !_lastSync.has_next) {
|
||||
_s.running = false;
|
||||
showToast(`Feeder complete (${count} generated)`, "success");
|
||||
break;
|
||||
}
|
||||
// loop: has_next=false でも次は index=0 からなので続行
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (_s.running) showToast("Feeder run error: " + err.message, "error");
|
||||
} finally {
|
||||
comfyUI.socket?.removeEventListener("message", _syncHandler);
|
||||
_s.running = false;
|
||||
_setRunUI(false);
|
||||
if (progressText) progressText.textContent = `Done (${count} generated)`;
|
||||
}
|
||||
}
|
||||
|
||||
async function _stopRun() {
|
||||
_s.running = false;
|
||||
try { await comfyUI.interrupt(); } catch {}
|
||||
_setRunUI(false);
|
||||
showToast("Feeder stopped", "info");
|
||||
}
|
||||
|
||||
// ── Folder Tree ───────────────────────────────────────────────────
|
||||
|
||||
async function _loadTree() {
|
||||
const container = document.getElementById("wfm-feeder-tree");
|
||||
if (!container) return;
|
||||
container.innerHTML = `<div class="wfm-feeder-tree-msg">Loading...</div>`;
|
||||
try {
|
||||
const res = await fetch("/image_loop/tree");
|
||||
if (!res.ok) throw new Error(res.status);
|
||||
const data = await res.json();
|
||||
_renderTree(data.tree || [], container);
|
||||
} catch (err) {
|
||||
container.innerHTML = `<div class="wfm-feeder-tree-msg">Cannot reach image-loop API</div>`;
|
||||
console.warn("[Feeder] tree load failed:", err);
|
||||
}
|
||||
}
|
||||
|
||||
function _renderTree(items, container, depth = 0) {
|
||||
if (depth === 0) {
|
||||
container.innerHTML = "";
|
||||
container.appendChild(_makeTreeRow("", "🏠 (root)", 0));
|
||||
}
|
||||
for (const item of items) {
|
||||
const icon = item.children?.length > 0 ? "📂" : "📁";
|
||||
container.appendChild(_makeTreeRow(item.path, `${icon} ${item.name}`, depth + 1));
|
||||
if (item.children?.length > 0) _renderTree(item.children, container, depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
function _makeTreeRow(path, label, depth) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "wfm-feeder-tree-row" + (_s.dir === path ? " active" : "");
|
||||
row.style.paddingLeft = `${8 + depth * 12}px`;
|
||||
row.dataset.path = path;
|
||||
row.textContent = label;
|
||||
row.title = path || "(root)";
|
||||
row.addEventListener("click", () => _selectDir(path));
|
||||
return row;
|
||||
}
|
||||
|
||||
function _selectDir(path) {
|
||||
if (_s.dir !== path) {
|
||||
_s.selected.clear();
|
||||
_s.dir = path;
|
||||
_setVal("wfm-feeder-dir-input", path);
|
||||
}
|
||||
_highlightTreeRow(path);
|
||||
_loadImages(path);
|
||||
_updateStatus();
|
||||
}
|
||||
|
||||
function _highlightTreeRow(path) {
|
||||
document.querySelectorAll(".wfm-feeder-tree-row").forEach(el => {
|
||||
el.classList.toggle("active", el.dataset.path === path);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Image Grid ────────────────────────────────────────────────────
|
||||
|
||||
async function _loadImages(dir) {
|
||||
const grid = document.getElementById("wfm-feeder-grid");
|
||||
if (!grid) return;
|
||||
grid.innerHTML = `<div class="wfm-feeder-grid-msg">Loading...</div>`;
|
||||
try {
|
||||
const url = "/image_loop/images" + (dir ? `?dir=${encodeURIComponent(dir)}` : "");
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error(res.status);
|
||||
const data = await res.json();
|
||||
_s.images = data.images || [];
|
||||
_renderGrid();
|
||||
} catch (err) {
|
||||
_s.images = [];
|
||||
grid.innerHTML = `<div class="wfm-feeder-grid-msg" style="color:var(--wfm-danger)">Load failed: ${err.message}</div>`;
|
||||
}
|
||||
_updateStatus();
|
||||
}
|
||||
|
||||
function _renderGrid() {
|
||||
const grid = document.getElementById("wfm-feeder-grid");
|
||||
if (!grid) return;
|
||||
if (_s.images.length === 0) {
|
||||
grid.innerHTML = `<div class="wfm-feeder-grid-msg">No images found</div>`;
|
||||
return;
|
||||
}
|
||||
grid.innerHTML = "";
|
||||
for (const fname of _s.images) grid.appendChild(_makeCard(fname));
|
||||
}
|
||||
|
||||
function _makeCard(fname) {
|
||||
const relPath = _s.dir ? `${_s.dir}/${fname}` : fname;
|
||||
const checked = _s.selected.has(fname);
|
||||
|
||||
const card = document.createElement("div");
|
||||
card.className = "wfm-feeder-card" + (checked ? " selected" : "");
|
||||
card.dataset.name = fname;
|
||||
|
||||
const cb = document.createElement("input");
|
||||
cb.type = "checkbox";
|
||||
cb.className = "wfm-feeder-card-cb";
|
||||
cb.checked = checked;
|
||||
cb.addEventListener("change", e => {
|
||||
e.stopPropagation();
|
||||
if (cb.checked) _s.selected.add(fname);
|
||||
else _s.selected.delete(fname);
|
||||
card.classList.toggle("selected", cb.checked);
|
||||
_updateStatus();
|
||||
});
|
||||
|
||||
const img = document.createElement("img");
|
||||
img.className = "wfm-feeder-card-img";
|
||||
img.loading = "lazy";
|
||||
img.src = `/image_loop/thumbnail?path=${encodeURIComponent(relPath)}`;
|
||||
img.onerror = () => { img.style.display = "none"; card.style.minHeight = "110px"; };
|
||||
|
||||
const name = document.createElement("div");
|
||||
name.className = "wfm-feeder-card-name";
|
||||
name.textContent = fname;
|
||||
name.title = fname;
|
||||
|
||||
card.append(cb, img, name);
|
||||
card.addEventListener("click", e => {
|
||||
if (e.target === cb) return;
|
||||
_showPreview(relPath, fname);
|
||||
});
|
||||
return card;
|
||||
}
|
||||
|
||||
function _refreshGridCbs() {
|
||||
document.querySelectorAll(".wfm-feeder-card").forEach(card => {
|
||||
const sel = _s.selected.has(card.dataset.name);
|
||||
card.classList.toggle("selected", sel);
|
||||
const cb = card.querySelector("input[type=checkbox]");
|
||||
if (cb) cb.checked = sel;
|
||||
});
|
||||
}
|
||||
|
||||
function _selectAll() {
|
||||
_s.images.forEach(f => _s.selected.add(f));
|
||||
_refreshGridCbs();
|
||||
_updateStatus();
|
||||
}
|
||||
|
||||
function _deselectAll() {
|
||||
_s.images.forEach(f => _s.selected.delete(f));
|
||||
_refreshGridCbs();
|
||||
_updateStatus();
|
||||
}
|
||||
|
||||
// ── Status Bar ───────────────────────────────────────────────────
|
||||
|
||||
function _updateStatus() {
|
||||
const el = document.getElementById("wfm-feeder-status");
|
||||
if (!el) return;
|
||||
const inCurrent = _s.images.filter(f => _s.selected.has(f)).length;
|
||||
el.textContent = `📂 ${_s.dir || "(root)"} | ${inCurrent} / ${_s.images.length} selected`;
|
||||
}
|
||||
|
||||
// ── Preview Panel ────────────────────────────────────────────────
|
||||
|
||||
async function _showPreview(relPath, fname) {
|
||||
const imgEl = document.getElementById("wfm-feeder-preview-img");
|
||||
const nameEl = document.getElementById("wfm-feeder-preview-name");
|
||||
const infoEl = document.getElementById("wfm-feeder-preview-info");
|
||||
|
||||
if (imgEl) {
|
||||
imgEl.src = `/image_loop/thumbnail?path=${encodeURIComponent(relPath)}`;
|
||||
imgEl.style.display = "block";
|
||||
}
|
||||
if (nameEl) nameEl.textContent = fname;
|
||||
if (infoEl) infoEl.textContent = "Loading...";
|
||||
|
||||
try {
|
||||
const res = await fetch(`/image_loop/image_info?path=${encodeURIComponent(relPath)}`);
|
||||
if (res.ok && infoEl) {
|
||||
const d = await res.json();
|
||||
const kb = d.size_bytes ? (d.size_bytes / 1024).toFixed(1) + " KB" : "";
|
||||
infoEl.textContent = `${d.width ?? "?"}×${d.height ?? "?"} px\n${kb}`;
|
||||
}
|
||||
} catch { if (infoEl) infoEl.textContent = ""; }
|
||||
}
|
||||
|
||||
// ── Presets ──────────────────────────────────────────────────────
|
||||
|
||||
async function _loadPresets() {
|
||||
try {
|
||||
const res = await fetch("/image_feeder/presets");
|
||||
if (!res.ok) return;
|
||||
_s.presets = await res.json();
|
||||
} catch { _s.presets = {}; }
|
||||
_renderPresets();
|
||||
}
|
||||
|
||||
function _renderPresets() {
|
||||
const sel = document.getElementById("wfm-feeder-preset-sel");
|
||||
if (!sel) return;
|
||||
const names = Object.keys(_s.presets).sort();
|
||||
sel.innerHTML = `<option value="">-- Select preset --</option>` +
|
||||
names.map(n => `<option value="${n}">${n}</option>`).join("");
|
||||
}
|
||||
|
||||
async function _savePreset() {
|
||||
const name = document.getElementById("wfm-feeder-preset-name")?.value.trim();
|
||||
if (!name) { showToast("Enter a preset name", "error"); return; }
|
||||
const sel = _s.images.filter(f => _s.selected.has(f));
|
||||
try {
|
||||
const res = await fetch("/image_feeder/presets", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name, directory: _s.dir, selected_files: sel }),
|
||||
});
|
||||
if (res.ok) {
|
||||
showToast(`Preset saved: ${name}`, "success");
|
||||
const nameInput = document.getElementById("wfm-feeder-preset-name");
|
||||
if (nameInput) nameInput.value = "";
|
||||
await _loadPresets();
|
||||
const selEl = document.getElementById("wfm-feeder-preset-sel");
|
||||
if (selEl) selEl.value = name;
|
||||
} else { showToast("Save failed", "error"); }
|
||||
} catch (err) { showToast("Save error: " + err.message, "error"); }
|
||||
}
|
||||
|
||||
async function _applyPreset() {
|
||||
const selEl = document.getElementById("wfm-feeder-preset-sel");
|
||||
const name = selEl?.value;
|
||||
if (!name || !_s.presets[name]) { showToast("Select a preset first", "error"); return; }
|
||||
const preset = _s.presets[name];
|
||||
const dir = preset.directory || "";
|
||||
|
||||
_s.selected.clear();
|
||||
(preset.selected_files || []).forEach(f => _s.selected.add(f));
|
||||
_setVal("wfm-feeder-dir-input", dir);
|
||||
|
||||
if (_s.dir !== dir) {
|
||||
_s.dir = dir;
|
||||
_highlightTreeRow(dir);
|
||||
await _loadImages(dir);
|
||||
}
|
||||
_refreshGridCbs();
|
||||
_updateStatus();
|
||||
showToast(`Preset loaded: ${name}`, "success");
|
||||
}
|
||||
|
||||
async function _deletePreset() {
|
||||
const name = document.getElementById("wfm-feeder-preset-sel")?.value;
|
||||
if (!name) return;
|
||||
try {
|
||||
await fetch(`/image_feeder/presets/${encodeURIComponent(name)}`, { method: "DELETE" });
|
||||
showToast(`Preset deleted: ${name}`, "info");
|
||||
await _loadPresets();
|
||||
} catch (err) { showToast("Delete error", "error"); }
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────
|
||||
|
||||
function _setVal(id, val) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.value = val ?? "";
|
||||
}
|
||||
function _setCheck(id, val) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.checked = !!val;
|
||||
}
|
||||
|
||||
// ── Init ─────────────────────────────────────────────────────────
|
||||
|
||||
export async function initFeederTab() {
|
||||
document.getElementById("wfm-feeder-node-sel")?.addEventListener("change", e => {
|
||||
_loadFromNode(e.target.value);
|
||||
});
|
||||
|
||||
document.getElementById("wfm-feeder-apply-btn")?.addEventListener("click", _applyToWorkflow);
|
||||
|
||||
document.getElementById("wfm-feeder-dir-input")?.addEventListener("change", e => {
|
||||
const val = e.target.value.trim();
|
||||
if (_s.dir !== val) {
|
||||
_s.dir = val;
|
||||
_highlightTreeRow(val);
|
||||
_loadImages(val);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("wfm-feeder-reload-tree")?.addEventListener("click", _loadTree);
|
||||
document.getElementById("wfm-feeder-sel-all")?.addEventListener("click", _selectAll);
|
||||
document.getElementById("wfm-feeder-desel-all")?.addEventListener("click", _deselectAll);
|
||||
|
||||
document.getElementById("wfm-feeder-preset-save")?.addEventListener("click", _savePreset);
|
||||
document.getElementById("wfm-feeder-preset-load")?.addEventListener("click", _applyPreset);
|
||||
document.getElementById("wfm-feeder-preset-del")?.addEventListener("click", _deletePreset);
|
||||
|
||||
document.getElementById("wfm-feeder-run-btn")?.addEventListener("click", _startRun);
|
||||
document.getElementById("wfm-feeder-stop-btn")?.addEventListener("click", _stopRun);
|
||||
|
||||
await Promise.all([_loadTree(), _loadPresets()]);
|
||||
}
|
||||
+322
-45
@@ -8,6 +8,7 @@ import { comfyWorkflow } from "./comfyui-workflow.js";
|
||||
import { comfyEditor } from "./comfyui-editor.js";
|
||||
import { t } from "./i18n.js";
|
||||
import { syncJsonHighlight, syncScroll } from "./json-highlight.js";
|
||||
import { initFeederTab, refreshFeederNodeList } from "./feeder-tab.js";
|
||||
|
||||
// ============================================
|
||||
// Eagle Auto-Save
|
||||
@@ -145,6 +146,8 @@ export async function loadWorkflowIntoEditor(workflow, filename) {
|
||||
const genBtn = document.getElementById("wfm-gen-generate-btn");
|
||||
if (genBtn) genBtn.disabled = !comfyUI.connected;
|
||||
|
||||
refreshFeederNodeList();
|
||||
|
||||
showToast(`Workflow loaded: ${filename || ""}`, "success");
|
||||
return true;
|
||||
}
|
||||
@@ -153,49 +156,286 @@ export async function loadWorkflowIntoEditor(workflow, filename) {
|
||||
// Checkpoint Batch
|
||||
// ============================================
|
||||
|
||||
const _ckptBatch = { aborted: false };
|
||||
const _ckptBatch = { aborted: false, paused: false, _resumeResolve: null };
|
||||
|
||||
function _parseFolderList(str) {
|
||||
return str.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
|
||||
async function _waitIfPaused() {
|
||||
if (!_ckptBatch.paused) return;
|
||||
await new Promise((resolve) => { _ckptBatch._resumeResolve = resolve; });
|
||||
}
|
||||
|
||||
function _getModelFolder(modelPath) {
|
||||
const normalized = modelPath.replace(/\\/g, "/");
|
||||
const idx = normalized.indexOf("/");
|
||||
return idx === -1 ? "" : normalized.substring(0, idx).toLowerCase();
|
||||
function _setPauseBtnState(paused) {
|
||||
const btn = document.getElementById("wfm-ckpt-batch-pause-btn");
|
||||
if (!btn) return;
|
||||
btn.textContent = paused ? "Resume" : "Pause";
|
||||
btn.style.background = paused ? "var(--wfm-success, #22c55e)" : "";
|
||||
btn.style.color = paused ? "#fff" : "";
|
||||
}
|
||||
|
||||
function _filterCheckpoints(checkpoints, includeStr, excludeStr) {
|
||||
const inc = _parseFolderList(includeStr);
|
||||
const exc = _parseFolderList(excludeStr);
|
||||
return checkpoints.filter((m) => {
|
||||
const folder = _getModelFolder(m);
|
||||
if (inc.length > 0 && !inc.includes(folder)) return false;
|
||||
if (exc.length > 0 && exc.includes(folder)) return false;
|
||||
return true;
|
||||
});
|
||||
// mode: "all" = 全選択, "some" = 一部選択, "none" = 全解除
|
||||
const _ckptState = { mode: "all", selected: new Set() };
|
||||
|
||||
function _getSelectedCheckpoints() {
|
||||
const all = comfyEditor.models.checkpoints || [];
|
||||
if (_ckptState.mode === "all") return all;
|
||||
if (_ckptState.mode === "none") return [];
|
||||
return all.filter((m) => _ckptState.selected.has(m));
|
||||
}
|
||||
|
||||
function _updateDropdownLabel() {
|
||||
const labelEl = document.getElementById("wfm-ckpt-dropdown-label");
|
||||
if (!labelEl) return;
|
||||
const total = (comfyEditor.models.checkpoints || []).length;
|
||||
if (_ckptState.mode === "all") {
|
||||
labelEl.textContent = total > 0 ? `All (${total})` : "All checkpoints";
|
||||
} else if (_ckptState.mode === "none") {
|
||||
labelEl.textContent = `0 / ${total} selected`;
|
||||
} else {
|
||||
labelEl.textContent = `${_ckptState.selected.size} / ${total} selected`;
|
||||
}
|
||||
}
|
||||
|
||||
// モデルパスリストをフォルダ → [modelPath, ...] のMapに変換
|
||||
function _buildFolderTree(models) {
|
||||
const map = new Map();
|
||||
for (const m of models) {
|
||||
const normalized = m.replace(/\\/g, "/");
|
||||
const lastSlash = normalized.lastIndexOf("/");
|
||||
const folder = lastSlash === -1 ? "" : normalized.substring(0, lastSlash);
|
||||
if (!map.has(folder)) map.set(folder, []);
|
||||
map.get(folder).push(m);
|
||||
}
|
||||
return new Map([...map.entries()].sort((a, b) => {
|
||||
if (a[0] === b[0]) return 0;
|
||||
if (a[0] === "") return -1;
|
||||
if (b[0] === "") return 1;
|
||||
return a[0].localeCompare(b[0]);
|
||||
}));
|
||||
}
|
||||
|
||||
// フォルダ内の選択状態を返す: "checked" | "indeterminate" | "unchecked"
|
||||
function _getFolderCheckState(folderModels) {
|
||||
if (_ckptState.mode === "all") return "checked";
|
||||
if (_ckptState.mode === "none") return "unchecked";
|
||||
const selCount = folderModels.filter((m) => _ckptState.selected.has(m)).length;
|
||||
if (selCount === 0) return "unchecked";
|
||||
if (selCount === folderModels.length) return "checked";
|
||||
return "indeterminate";
|
||||
}
|
||||
|
||||
// 単一モデルの選択トグル(_ckptState を更新)
|
||||
function _toggleSingleModel(m, checked, all) {
|
||||
if (_ckptState.mode === "all" && !checked) {
|
||||
_ckptState.mode = "some";
|
||||
_ckptState.selected.clear();
|
||||
all.forEach((x) => _ckptState.selected.add(x));
|
||||
} else if (_ckptState.mode === "none" && checked) {
|
||||
_ckptState.mode = "some";
|
||||
_ckptState.selected.clear();
|
||||
}
|
||||
if (checked) _ckptState.selected.add(m);
|
||||
else _ckptState.selected.delete(m);
|
||||
if (_ckptState.selected.size === all.length) { _ckptState.mode = "all"; _ckptState.selected.clear(); }
|
||||
if (_ckptState.mode === "some" && _ckptState.selected.size === 0) _ckptState.mode = "none";
|
||||
}
|
||||
|
||||
// フォルダ単位の一括トグル(_ckptState を更新)
|
||||
function _toggleFolderModels(folderModels, checked, all) {
|
||||
if (_ckptState.mode === "all" && !checked) {
|
||||
_ckptState.mode = "some";
|
||||
_ckptState.selected.clear();
|
||||
all.forEach((x) => _ckptState.selected.add(x));
|
||||
} else if (_ckptState.mode === "none" && checked) {
|
||||
_ckptState.mode = "some";
|
||||
_ckptState.selected.clear();
|
||||
}
|
||||
folderModels.forEach((m) => { if (checked) _ckptState.selected.add(m); else _ckptState.selected.delete(m); });
|
||||
if (_ckptState.selected.size === all.length) { _ckptState.mode = "all"; _ckptState.selected.clear(); }
|
||||
if (_ckptState.mode === "some" && _ckptState.selected.size === 0) _ckptState.mode = "none";
|
||||
}
|
||||
|
||||
function _rebuildCkptList() {
|
||||
const listEl = document.getElementById("wfm-ckpt-list");
|
||||
if (!listEl) return;
|
||||
const all = comfyEditor.models.checkpoints || [];
|
||||
const search = document.getElementById("wfm-ckpt-search")?.value.toLowerCase() || "";
|
||||
const filtered = search ? all.filter((m) => m.toLowerCase().includes(search)) : all;
|
||||
|
||||
if (filtered.length === 0) {
|
||||
listEl.innerHTML = `<div style="padding:8px 10px;font-size:11px;color:var(--wfm-text-secondary);">No checkpoints</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const folderTree = _buildFolderTree(filtered);
|
||||
listEl.innerHTML = "";
|
||||
|
||||
for (const [folder, models] of folderTree) {
|
||||
const group = document.createElement("div");
|
||||
group.className = "wfm-ckpt-folder-group open";
|
||||
|
||||
// --- フォルダヘッダー ---
|
||||
const header = document.createElement("div");
|
||||
header.className = "wfm-ckpt-folder-header";
|
||||
|
||||
const folderCb = document.createElement("input");
|
||||
folderCb.type = "checkbox";
|
||||
const folderState = _getFolderCheckState(models);
|
||||
folderCb.checked = folderState === "checked";
|
||||
folderCb.indeterminate = folderState === "indeterminate";
|
||||
|
||||
const toggle = document.createElement("span");
|
||||
toggle.className = "wfm-ckpt-folder-toggle";
|
||||
toggle.textContent = "▶";
|
||||
|
||||
const nameSpan = document.createElement("span");
|
||||
nameSpan.className = "wfm-ckpt-folder-name";
|
||||
nameSpan.textContent = folder || "(root)";
|
||||
nameSpan.title = folder || "(root)";
|
||||
|
||||
const countSpan = document.createElement("span");
|
||||
countSpan.className = "wfm-ckpt-folder-count";
|
||||
countSpan.textContent = models.length;
|
||||
|
||||
// --- ファイルリスト ---
|
||||
const filesDiv = document.createElement("div");
|
||||
filesDiv.className = "wfm-ckpt-folder-files";
|
||||
|
||||
for (const m of models) {
|
||||
const itemLabel = document.createElement("label");
|
||||
itemLabel.className = "wfm-ckpt-item wfm-ckpt-item--indented";
|
||||
|
||||
const fileCb = document.createElement("input");
|
||||
fileCb.type = "checkbox";
|
||||
fileCb.value = m;
|
||||
if (_ckptState.mode === "all") fileCb.checked = true;
|
||||
else if (_ckptState.mode === "none") fileCb.checked = false;
|
||||
else fileCb.checked = _ckptState.selected.has(m);
|
||||
|
||||
const normalized = m.replace(/\\/g, "/");
|
||||
const lastSlash = normalized.lastIndexOf("/");
|
||||
const fileName = lastSlash === -1 ? m : m.substring(lastSlash + 1);
|
||||
const fileSpan = document.createElement("span");
|
||||
fileSpan.className = "wfm-ckpt-item-label";
|
||||
fileSpan.textContent = fileName;
|
||||
fileSpan.title = m;
|
||||
|
||||
fileCb.addEventListener("change", () => {
|
||||
_toggleSingleModel(m, fileCb.checked, all);
|
||||
const s = _getFolderCheckState(models);
|
||||
folderCb.checked = s === "checked";
|
||||
folderCb.indeterminate = s === "indeterminate";
|
||||
_updateDropdownLabel();
|
||||
_updateBatchInfo();
|
||||
});
|
||||
|
||||
itemLabel.appendChild(fileCb);
|
||||
itemLabel.appendChild(fileSpan);
|
||||
filesDiv.appendChild(itemLabel);
|
||||
}
|
||||
|
||||
// フォルダCBのchange
|
||||
folderCb.addEventListener("change", () => {
|
||||
_toggleFolderModels(models, folderCb.checked, all);
|
||||
folderCb.indeterminate = false;
|
||||
filesDiv.querySelectorAll("input[type=checkbox]").forEach((cb) => { cb.checked = folderCb.checked; });
|
||||
_updateDropdownLabel();
|
||||
_updateBatchInfo();
|
||||
});
|
||||
|
||||
// フォルダ名 / トグル矢印クリックで展開・折りたたみ
|
||||
[toggle, nameSpan].forEach((el) => {
|
||||
el.addEventListener("click", () => group.classList.toggle("open"));
|
||||
});
|
||||
|
||||
header.appendChild(folderCb);
|
||||
header.appendChild(toggle);
|
||||
header.appendChild(nameSpan);
|
||||
header.appendChild(countSpan);
|
||||
group.appendChild(header);
|
||||
group.appendChild(filesDiv);
|
||||
listEl.appendChild(group);
|
||||
}
|
||||
}
|
||||
|
||||
function _updateBatchInfo() {
|
||||
const infoEl = document.getElementById("wfm-ckpt-batch-info");
|
||||
if (!infoEl) return;
|
||||
const inc = document.getElementById("wfm-ckpt-batch-include")?.value || "";
|
||||
const exc = document.getElementById("wfm-ckpt-batch-exclude")?.value || "";
|
||||
const n = _filterCheckpoints(comfyEditor.models.checkpoints, inc, exc).length;
|
||||
const n = _getSelectedCheckpoints().length;
|
||||
infoEl.textContent = `${n} checkpoint${n !== 1 ? "s" : ""} will be processed`;
|
||||
}
|
||||
|
||||
function initCheckpointBatch() {
|
||||
const checkbox = document.getElementById("wfm-ckpt-batch-enabled");
|
||||
const body = document.getElementById("wfm-ckpt-batch-body");
|
||||
const wrap = document.getElementById("wfm-ckpt-dropdown-wrap");
|
||||
const btn = document.getElementById("wfm-ckpt-dropdown-btn");
|
||||
const panel = document.getElementById("wfm-ckpt-dropdown-panel");
|
||||
|
||||
checkbox?.addEventListener("change", () => {
|
||||
if (body) body.style.display = checkbox.checked ? "block" : "none";
|
||||
if (checkbox.checked) _updateBatchInfo();
|
||||
if (checkbox.checked) {
|
||||
_updateDropdownLabel();
|
||||
_updateBatchInfo();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("wfm-ckpt-batch-include")?.addEventListener("input", _updateBatchInfo);
|
||||
document.getElementById("wfm-ckpt-batch-exclude")?.addEventListener("input", _updateBatchInfo);
|
||||
// ドロップダウン開閉
|
||||
btn?.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
const isOpen = panel.style.display !== "none";
|
||||
if (isOpen) {
|
||||
panel.style.display = "none";
|
||||
wrap.classList.remove("open");
|
||||
} else {
|
||||
_rebuildCkptList();
|
||||
panel.style.display = "block";
|
||||
wrap.classList.add("open");
|
||||
document.getElementById("wfm-ckpt-search")?.focus();
|
||||
}
|
||||
});
|
||||
|
||||
// 外側クリックで閉じる
|
||||
document.addEventListener("click", (e) => {
|
||||
if (panel && panel.style.display !== "none" && !wrap?.contains(e.target)) {
|
||||
panel.style.display = "none";
|
||||
wrap?.classList.remove("open");
|
||||
}
|
||||
});
|
||||
|
||||
// 検索フィルター
|
||||
document.getElementById("wfm-ckpt-search")?.addEventListener("input", _rebuildCkptList);
|
||||
|
||||
// 全選択
|
||||
document.getElementById("wfm-ckpt-select-all")?.addEventListener("click", () => {
|
||||
_ckptState.mode = "all";
|
||||
_ckptState.selected.clear();
|
||||
_rebuildCkptList();
|
||||
_updateDropdownLabel();
|
||||
_updateBatchInfo();
|
||||
});
|
||||
|
||||
// 全解除
|
||||
document.getElementById("wfm-ckpt-deselect-all")?.addEventListener("click", () => {
|
||||
_ckptState.mode = "none";
|
||||
_ckptState.selected.clear();
|
||||
_rebuildCkptList();
|
||||
_updateDropdownLabel();
|
||||
_updateBatchInfo();
|
||||
});
|
||||
|
||||
// Pause / Resume
|
||||
document.getElementById("wfm-ckpt-batch-pause-btn")?.addEventListener("click", () => {
|
||||
if (!_ckptBatch.paused) {
|
||||
_ckptBatch.paused = true;
|
||||
_setPauseBtnState(true);
|
||||
} else {
|
||||
_ckptBatch.paused = false;
|
||||
_setPauseBtnState(false);
|
||||
if (_ckptBatch._resumeResolve) {
|
||||
_ckptBatch._resumeResolve();
|
||||
_ckptBatch._resumeResolve = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -283,12 +523,10 @@ async function _runBatchGenerate() {
|
||||
return;
|
||||
}
|
||||
|
||||
const inc = document.getElementById("wfm-ckpt-batch-include")?.value || "";
|
||||
const exc = document.getElementById("wfm-ckpt-batch-exclude")?.value || "";
|
||||
const list = _filterCheckpoints(comfyEditor.models.checkpoints, inc, exc);
|
||||
const list = _getSelectedCheckpoints();
|
||||
|
||||
if (list.length === 0) {
|
||||
showToast("No checkpoints match the folder filter", "error");
|
||||
showToast("No checkpoints selected", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -299,33 +537,54 @@ async function _runBatchGenerate() {
|
||||
const progressText = document.getElementById("wfm-gen-progress-text");
|
||||
|
||||
if (batchProgress) batchProgress.style.display = "block";
|
||||
const pauseBtn = document.getElementById("wfm-ckpt-batch-pause-btn");
|
||||
if (pauseBtn) { pauseBtn.style.display = "block"; pauseBtn.disabled = false; }
|
||||
_setPauseBtnState(false);
|
||||
|
||||
let completed = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (_ckptBatch.aborted) break;
|
||||
try {
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (_ckptBatch.aborted) break;
|
||||
|
||||
const model = list[i];
|
||||
if (batchCurrentName) batchCurrentName.textContent = model;
|
||||
if (batchCount) batchCount.textContent = `${i + 1} / ${list.length}`;
|
||||
if (batchBar) batchBar.style.width = `${((i / list.length) * 100).toFixed(1)}%`;
|
||||
if (progressText) progressText.textContent = `[${i + 1}/${list.length}] Loading...`;
|
||||
// 一時停止中は次のモデルへ進む前に待機
|
||||
if (_ckptBatch.paused) {
|
||||
if (batchCurrentName) batchCurrentName.textContent = "Paused...";
|
||||
if (progressText) progressText.textContent = "Paused";
|
||||
}
|
||||
await _waitIfPaused();
|
||||
if (_ckptBatch.aborted) break;
|
||||
|
||||
for (const node of ckptNodes) {
|
||||
if (comfyUI.currentWorkflow?.[node.id]) {
|
||||
comfyUI.currentWorkflow[node.id].inputs.ckpt_name = model;
|
||||
const model = list[i];
|
||||
if (batchCurrentName) batchCurrentName.textContent = model;
|
||||
if (batchCount) batchCount.textContent = `${i + 1} / ${list.length}`;
|
||||
if (batchBar) batchBar.style.width = `${((i / list.length) * 100).toFixed(1)}%`;
|
||||
if (progressText) progressText.textContent = `[${i + 1}/${list.length}] Loading...`;
|
||||
|
||||
for (const node of ckptNodes) {
|
||||
if (comfyUI.currentWorkflow?.[node.id]) {
|
||||
comfyUI.currentWorkflow[node.id].inputs.ckpt_name = model;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await _coreGenerate(true);
|
||||
completed++;
|
||||
} catch (err) {
|
||||
if (_ckptBatch.aborted) break;
|
||||
failed++;
|
||||
showToast(`[${i + 1}/${list.length}] Failed: ${err.message}`, "error");
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await _coreGenerate(true);
|
||||
completed++;
|
||||
} catch (err) {
|
||||
if (_ckptBatch.aborted) break;
|
||||
failed++;
|
||||
showToast(`[${i + 1}/${list.length}] Failed: ${err.message}`, "error");
|
||||
} finally {
|
||||
if (pauseBtn) pauseBtn.disabled = true;
|
||||
_ckptBatch.paused = false;
|
||||
if (_ckptBatch._resumeResolve) {
|
||||
_ckptBatch._resumeResolve();
|
||||
_ckptBatch._resumeResolve = null;
|
||||
}
|
||||
_setPauseBtnState(false);
|
||||
}
|
||||
|
||||
if (batchBar) batchBar.style.width = "100%";
|
||||
@@ -360,6 +619,8 @@ async function handleGenerate() {
|
||||
genBtn.disabled = true;
|
||||
if (interruptBtn) interruptBtn.style.display = "inline-block";
|
||||
_ckptBatch.aborted = false;
|
||||
_ckptBatch.paused = false;
|
||||
_ckptBatch._resumeResolve = null;
|
||||
|
||||
const batchEnabled = document.getElementById("wfm-ckpt-batch-enabled")?.checked;
|
||||
|
||||
@@ -408,6 +669,7 @@ export async function initGenerateTab() {
|
||||
comfyEditor.renderAll(comfyUI.currentAnalysis, comfyUI.currentWorkflow);
|
||||
}
|
||||
showToast("Model lists refreshed", "success");
|
||||
_updateDropdownLabel();
|
||||
_updateBatchInfo();
|
||||
});
|
||||
|
||||
@@ -417,6 +679,12 @@ export async function initGenerateTab() {
|
||||
// Interrupt button (stops both single generation and batch loop)
|
||||
document.getElementById("wfm-gen-interrupt-btn")?.addEventListener("click", async () => {
|
||||
_ckptBatch.aborted = true;
|
||||
// 一時停止中でもループを抜けられるよう待機を解除
|
||||
_ckptBatch.paused = false;
|
||||
if (_ckptBatch._resumeResolve) {
|
||||
_ckptBatch._resumeResolve();
|
||||
_ckptBatch._resumeResolve = null;
|
||||
}
|
||||
await comfyUI.interrupt();
|
||||
showToast("Interrupted", "info");
|
||||
});
|
||||
@@ -424,8 +692,13 @@ export async function initGenerateTab() {
|
||||
// Move shared Raw JSON widget into the active tab's rawjson-col
|
||||
function moveRawJsonToTab(tabKey) {
|
||||
const widget = document.getElementById("wfm-gen-rawjson-widget");
|
||||
if (!widget) return;
|
||||
if (tabKey === "feeder") {
|
||||
widget.style.display = "none";
|
||||
return;
|
||||
}
|
||||
const col = document.getElementById(`wfm-gen-rawjson-col-${tabKey}`);
|
||||
if (widget && col) {
|
||||
if (col) {
|
||||
col.appendChild(widget);
|
||||
widget.style.display = "flex";
|
||||
}
|
||||
@@ -487,11 +760,15 @@ export async function initGenerateTab() {
|
||||
// Checkpoint batch UI
|
||||
initCheckpointBatch();
|
||||
|
||||
// Feeder tab
|
||||
await initFeederTab();
|
||||
|
||||
// Auto-connect on init
|
||||
const connected = await comfyUI.checkConnection();
|
||||
updateStatus(connected);
|
||||
if (connected) {
|
||||
await comfyEditor.loadModelLists();
|
||||
_updateDropdownLabel();
|
||||
_updateBatchInfo();
|
||||
}
|
||||
|
||||
|
||||
+72
-24
@@ -340,14 +340,26 @@ const LANGUAGES = {
|
||||
helpWf7: "Badge management: add, rename, delete badges with custom colors shared with the Models tab (⚙ Badge button)",
|
||||
helpGen1: "Connect/disconnect ComfyUI and check connection status",
|
||||
helpGen2: "Load workflows from Workflow tab (App format .app.json not supported)",
|
||||
helpGen3: "3-tab layout — Input (Prompt + Image), Model, Settings — each with a Raw JSON column for instant preview and direct editing",
|
||||
helpGen3: "4-tab layout — Input (Prompt + Image), Model, Settings, Feeder — Input/Model/Settings each include 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",
|
||||
helpGen6: "Settings tab: KSampler and Latent Image side by side, each at 50% width",
|
||||
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",
|
||||
helpGen11: "Checkpoint Batch: select checkpoints by folder from the dropdown — check a folder to select all its files, or expand with ▶ to check individual files; use Filter to search; Pause/Resume suspends and continues between models; Stop aborts after the current generation",
|
||||
helpFeederTitle: "GenerateUI Tab — Feeder subtab",
|
||||
helpFeederDesc: "Requires the comfyui-image-feeder custom node. Controls ImageFeeder nodes and runs continuous generation loops over an image library stored in user/default/image-loop-data/.",
|
||||
helpFeeder1: "Left pane — Node settings: select the target ImageFeeder node from the dropdown; edit all node parameters (Directory, Sort Mode, Index, Start/End Index, Batch Size, Seed, Use Selection); click Apply to write changes to the loaded workflow",
|
||||
helpFeeder2: "Left pane — Presets: save the current directory + selection as a named preset; load or delete saved presets",
|
||||
helpFeeder3: "Left pane — Run: After gen mode determines index behavior after each generation — Loop (wrap back to 0 and continue indefinitely), Increment (advance index and stop when all images are exhausted), Fixed (always use the same index); click Run to start the loop, Stop to interrupt",
|
||||
helpFeeder4: "Center pane — Folder tree: browse subfolders inside image-loop-data; click a folder to load its images; click ↺ to reload the tree",
|
||||
helpFeeder5: "Center pane — Image grid: displays thumbnails of all images in the selected folder; check individual images to add them to the selection; All / None buttons select or clear the entire folder; click an image to open the preview panel",
|
||||
helpFeeder6: "Center pane — Preview panel: shows the clicked image at larger size with filename, resolution, and file size",
|
||||
helpFeeder7: "Status bar: shows the current folder path and how many images are selected out of the total in the folder",
|
||||
helpFeeder8: "Seed during Run: uses the right-pane seed setting (Random / Fixed / Increment / Decrement) — the ImageFeeder's own Seed field controls random sort order only",
|
||||
helpFeeder9: "Index sync: after each generation the node returns the next index via WebSocket; the Index field updates automatically so the display stays in sync",
|
||||
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",
|
||||
@@ -355,6 +367,8 @@ const LANGUAGES = {
|
||||
helpPrompt5: "Preset Manager: browse all presets, favorites, and group-based filtering",
|
||||
helpPrompt6: "Group management: create groups, assign/remove presets, delete groups",
|
||||
helpPrompt7: "GenUI Set: apply preset prompts directly to GenerateUI",
|
||||
helpPrompt8: "Wildcard toolbar: one-click buttons to insert wildcard syntax ({|}, {n$|}, __|__, <lora:>, etc.); wraps selected text",
|
||||
helpPrompt9: "Wildcard file manager: create/edit/delete .txt/.yaml wildcard files; click filename to insert __filename__ at cursor",
|
||||
helpSettings1: "UI language and summary language",
|
||||
helpSettings2: "Theme selection (13 presets) with customization: color overrides, background patterns (stripes, polka dot, checkerboard, SVG tiling), and font selection (16 Google Fonts including Japanese display fonts)",
|
||||
helpSettings3: "Workflow data folder path",
|
||||
@@ -364,6 +378,7 @@ const LANGUAGES = {
|
||||
helpSettings7: "Eagle integration URL and auto-save toggle",
|
||||
helpSettings8: "Data Management: export all plugin data (settings, metadata, prompts, etc.) to a JSON file, or import to restore. After importing, restart ComfyUI.",
|
||||
helpSettings9: "All sections are collapsible accordion panels",
|
||||
helpSettings10: "Wildcard Integration: link WFS wildcard dir to ComfyUI-Impact-Pack wildcards (directory junction on Windows / symlink on other OS); existing files migrated automatically",
|
||||
helpNodes1: "Node Browser: search by name/description/alias/tag, filter by category, package, tag, group, favorites",
|
||||
helpNodes2: "Card and Table view modes with pagination",
|
||||
helpNodes3: "Favorite nodes and add custom tags for organization",
|
||||
@@ -386,12 +401,13 @@ const LANGUAGES = {
|
||||
helpGallery2: "3-column layout: folder tree (left) | image grid (center) | detail panel (right)",
|
||||
helpGallery3: "Thumbnail and Table view modes; double-click any image to open fullscreen lightbox",
|
||||
helpGallery4: "Toolbar filters: favorites (★), text search, tag filter, group filter, sort by date/name",
|
||||
helpGallery5: "Multi-select with Ctrl+click — bulk operations: Add to Group, Favorite All, Unfavorite All",
|
||||
helpGallery6: "Detail panel — Info tab: filename, size, date, tags (add/remove), memo with save",
|
||||
helpGallery7: "Detail panel — Metadata tab: embedded ComfyUI workflow JSON viewer with one-click clipboard copy",
|
||||
helpGallery8: "Detail panel — Groups tab: create, assign, remove, rename, and delete groups per image",
|
||||
helpGallery9: "Supports PNG (tEXt/iTXt metadata), JPEG, WebP, GIF; PNG workflow extraction reads ComfyUI-embedded JSON",
|
||||
helpGallery10: "Folder tree: click a folder to load its images; expand subfolders with ▶ arrow",
|
||||
helpGallery5: "Folder tree: click to load images, expand with ▶; '+ New' creates a subfolder inside the selected folder, 'Del' deletes it with all contents",
|
||||
helpGallery6: "Multi-select with Ctrl+click — bulk operations: Add to Group, Favorite All, Unfavorite All, Move To..., Delete",
|
||||
helpGallery7: "Detail panel — Info tab: filename, size, date, tags (add/remove), memo; 'Move To...' moves the image to another folder, 'Delete' removes it permanently",
|
||||
helpGallery8: "Detail panel — Metadata tab: displays workflow JSON from PNG embedded data (prompt / workflow keys) or from metadata saved by the Generate UI tab; one-click clipboard copy",
|
||||
helpGallery9: "Detail panel — Groups tab: create, assign, remove, rename, and delete groups per image",
|
||||
helpGallery10: "Workflow auto-save: images generated in the Generate UI tab automatically have their workflow saved to gallery metadata and shown in the Metadata tab",
|
||||
helpGallery11: "Supports PNG (tEXt/iTXt), JPEG, WebP, GIF; workflow extraction reads ComfyUI prompt and workflow keys embedded in PNG metadata",
|
||||
helpSidepanelTitle: "Workflow Studio Library (ComfyUI Side Panel)",
|
||||
helpSidepanel1: "Open from the Workflow Studio Library button in the ComfyUI toolbar",
|
||||
helpSidepanel2: "Workflows tab: All / ★ Favorites / Groups sub-tabs; ★ shown for favorites in All view",
|
||||
@@ -863,14 +879,26 @@ const LANGUAGES = {
|
||||
helpWf7: "バッジ管理:バッジの追加・名前変更・削除、カスタム色設定(Modelsタブと共有)— ⚙ Badgeボタン",
|
||||
helpGen1: "ComfyUIの接続・切断、接続状態の確認",
|
||||
helpGen2: "Workflowタブからワークフローを読み込み(App形式 .app.json は非対応)",
|
||||
helpGen3: "3タブ構成 — Input(プロンプト+画像)、Model、Settings — 各タブにRaw JSON列を常時表示",
|
||||
helpGen3: "4タブ構成 — Input(プロンプト+画像)、Model、Settings、Feeder — Input/Model/SettingsタブにはRaw JSON列を常時表示",
|
||||
helpGen4: "Inputタブ:左列にPrompt(上段)+ Image ドラッグ&ドロップ(下段)",
|
||||
helpGen5: "Modelタブ:Checkpoint/VAE/LoRA/ControlNet/UNET/TextEncoderのセレクタ(フィルタ検索付き)",
|
||||
helpGen6: "Settingsタブ:左列にKSampler(上段)+ LatentImage(下段)",
|
||||
helpGen6: "Settingsタブ:KSamplerとLatent Imageを横並び(各50%幅)",
|
||||
helpGen7: "Raw JSONを各タブの右列に常時表示 — 直接編集してApplyでワークフロー即時反映",
|
||||
helpGen8: "シード管理:ランダム / 固定 / 増加 / 減少",
|
||||
helpGen9: "画像生成、進捗表示、生成停止、結果表示",
|
||||
helpGen10: "Eagle連携:生成画像をEagleへ自動保存",
|
||||
helpGen11: "Checkpoint Batch:ドロップダウンのフォルダツリーからチェックポイントを選択 — フォルダにチェックでそのフォルダ内を一括選択、▶で展開して個別選択も可能;Filterで絞り込み;Pause/Resumeでモデル間の処理を一時停止・再開;Stopで現在の生成完了後に中断",
|
||||
helpFeederTitle: "GenerateUI Tab — Feeder サブタブ",
|
||||
helpFeederDesc: "comfyui-image-feeder カスタムノードが必要です。ImageFeeder ノードを操作し、user/default/image-loop-data/ に保存された画像ライブラリを使って連続生成ループを実行します。",
|
||||
helpFeeder1: "左ペイン — ノード設定:ドロップダウンで対象 ImageFeeder ノードを選択;Directory・Sort Mode・Index・Start/End Index・Batch Size・Seed・Use Selection の各パラメータを編集;Apply で読み込み済みワークフローに反映",
|
||||
helpFeeder2: "左ペイン — プリセット:現在のフォルダ+選択ファイルを名前付きプリセットとして保存;保存済みプリセットの読み込み・削除",
|
||||
helpFeeder3: "左ペイン — Run:After gen モードで各生成後の index 動作を設定 — Loop(index を 0 に折り返して無限継続)、Increment(index を進め全画像消化で自動停止)、Fixed(常に同じ index を使用);Run ボタンでループ開始、Stop で中断",
|
||||
helpFeeder4: "中央ペイン — フォルダツリー:image-loop-data 内のサブフォルダを階層表示;クリックで画像を読み込み;↺ ボタンでツリー再読み込み",
|
||||
helpFeeder5: "中央ペイン — 画像グリッド:選択フォルダ内の画像をサムネイル表示;チェックボックスで選択に追加;All/None ボタンで一括選択・解除;クリックでプレビューパネルに表示",
|
||||
helpFeeder6: "中央ペイン — プレビューパネル:クリックした画像を拡大表示し、ファイル名・解像度・ファイルサイズを確認",
|
||||
helpFeeder7: "ステータスバー:現在のフォルダパスと、そのフォルダ内の総枚数に対する選択枚数を表示",
|
||||
helpFeeder8: "Run 時のシード:右ペインのシード設定(ランダム / 固定 / 増加 / 減少)を使用 — ImageFeeder 自身の Seed フィールドはランダムソートの並び順のみに影響",
|
||||
helpFeeder9: "Index 同期:生成完了後にノードが次の index を WebSocket で返送;Index フィールドが自動更新されて表示と実行状態が一致",
|
||||
helpPrompt1: "AIアシスタント(Ollama):モデル選択、チャットでプロンプト相談、画像添付(ビジョンモデル対応)",
|
||||
helpPrompt2: "AIの応答をGenerateUIのプロンプトに適用",
|
||||
helpPrompt3: "翻訳機能:日⇔英、中⇔英",
|
||||
@@ -878,6 +906,8 @@ const LANGUAGES = {
|
||||
helpPrompt5: "プリセットマネージャー:全プリセット一覧、お気に入り、グループ別フィルタリング",
|
||||
helpPrompt6: "グループ管理:グループ作成、プリセットの割り当て・解除、グループ削除",
|
||||
helpPrompt7: "GenUI Set:プリセットのプロンプトをGenerateUIに直接適用",
|
||||
helpPrompt8: "ワイルドカードツールバー:{|}・{n$|}・__|__・<lora:>などの構文をワンクリック挿入。選択テキストのラップも対応",
|
||||
helpPrompt9: "ワイルドカードファイルマネージャー:.txt/.yamlファイルの作成・編集・削除。ファイル名をクリックすると__ファイル名__をカーソル位置に挿入",
|
||||
helpSettings1: "UI言語と要約言語の設定",
|
||||
helpSettings2: "テーマ選択(13種プリセット)+カスタマイズ:カラー調整、背景パターン(ストライプ、ポルカドット、チェック、SVGタイリング)、フォント選択(日本語デザインフォント含む16種のGoogle Fonts)",
|
||||
helpSettings3: "ワークフローデータフォルダのパス変更・リセット",
|
||||
@@ -887,6 +917,7 @@ const LANGUAGES = {
|
||||
helpSettings7: "Eagle連携URL、自動保存の有効/無効",
|
||||
helpSettings8: "データ管理:設定・メタデータ・プロンプトなど全データをJSONにエクスポート、またはインポートして復元。インポート後はComfyUIを再起動してください。",
|
||||
helpSettings9: "各セクションはアコーディオン式で折りたたみ可能",
|
||||
helpSettings10: "Wildcard Integration:WFSのワイルドカードディレクトリをComfyUI-Impact-Packのwildcardsディレクトリへリンク(Windowsはジャンクション、他OSはシンボリックリンク)。既存ファイルは自動で移行されます",
|
||||
helpNodes1: "ノードブラウザ:名前/説明/エイリアス/タグで検索、カテゴリ・パッケージ・タグ・グループ・お気に入りフィルタ",
|
||||
helpNodes2: "カード / テーブル表示モード切り替え(ページネーション付き)",
|
||||
helpNodes3: "ノードのお気に入り登録、カスタムタグの追加で整理",
|
||||
@@ -909,12 +940,13 @@ const LANGUAGES = {
|
||||
helpGallery2: "3カラムレイアウト: フォルダツリー (左) | 画像グリッド (中央) | 詳細パネル (右)",
|
||||
helpGallery3: "サムネイル・テーブル表示モード切替;ダブルクリックで全画面ライトボックス表示",
|
||||
helpGallery4: "ツールバーフィルタ:お気に入り(★)、テキスト検索、タグフィルタ、グループフィルタ、日付・名前ソート",
|
||||
helpGallery5: "Ctrl+クリックで複数選択 — 一括操作:グループへ追加、一括お気に入り、一括お気に入り解除",
|
||||
helpGallery6: "詳細パネル — Infoタブ:ファイル名、サイズ、日付、タグ(追加/削除)、メモ(保存機能付き)",
|
||||
helpGallery7: "詳細パネル — Metadataタブ:埋め込みComfyUIワークフローJSONビューアとワンクリッククリップボードコピー",
|
||||
helpGallery8: "詳細パネル — Groupsタブ:画像ごとのグループ作成、割り当て、解除、名前変更、削除",
|
||||
helpGallery9: "PNG (tEXt/iTXt メタデータ), JPEG, WebP, GIF対応; PNGはComfyUIの埋め込みJSONを読み込んでワークフロー抽出可能",
|
||||
helpGallery10: "フォルダツリー:フォルダをクリックして画像を読み込む;▶矢印でサブフォルダを展開",
|
||||
helpGallery5: "フォルダツリー:クリックで画像を読み込み;▶で展開;「+ New」で選択フォルダ内にサブフォルダを作成;「Del」でそのフォルダとすべての内容を削除",
|
||||
helpGallery6: "Ctrl+クリックで複数選択 — 一括操作:グループへ追加、一括お気に入り、一括お気に入り解除、移動(Move To...)、削除",
|
||||
helpGallery7: "詳細パネル — Infoタブ:ファイル名、サイズ、日付、タグ(追加/削除)、メモ;「Move To...」で他フォルダへ移動;「Delete」で完全削除",
|
||||
helpGallery8: "詳細パネル — Metadataタブ:PNGの埋め込みデータ(prompt / workflow キー)またはGenerateUIタブで保存したメタデータからワークフローJSONを表示;ワンクリックでクリップボードコピー",
|
||||
helpGallery9: "詳細パネル — Groupsタブ:画像ごとのグループ作成、割り当て、解除、名前変更、削除",
|
||||
helpGallery10: "ワークフロー自動保存:GenerateUIタブで生成した画像はワークフローが自動でギャラリーメタデータに保存され、Metadataタブで確認可能",
|
||||
helpGallery11: "PNG (tEXt/iTXt)、JPEG、WebP、GIF対応;PNG形式はComfyUIの埋め込みJSON(prompt / workflow キー)を読み込んでワークフロー抽出が可能",
|
||||
helpSidepanelTitle: "Workflow Studio Library(ComfyUIサイドパネル)",
|
||||
helpSidepanel1: "ComfyUIツールバーのWorkflow Studio Libraryボタンから開く",
|
||||
helpSidepanel2: "Workflowsタブ:All / ★ Favorites / Groupsサブタブ、Allビューでお気に入りに★表示",
|
||||
@@ -1382,14 +1414,26 @@ const LANGUAGES = {
|
||||
helpWf7: "徽章管理:添加、重命名、删除徽章并自定义颜色(与Models标签共享)— ⚙ Badge按钮",
|
||||
helpGen1: "连接/断开ComfyUI,检查连接状态",
|
||||
helpGen2: "从工作流标签页加载工作流(不支持App格式.app.json)",
|
||||
helpGen3: "3标签布局 — Input(提示词+图片)、Model、Settings — 每个标签含Raw JSON列实时预览",
|
||||
helpGen3: "4标签布局 — Input(提示词+图片)、Model、Settings、Feeder — Input/Model/Settings标签各含Raw JSON列实时预览",
|
||||
helpGen4: "Input标签:左列为Prompt(上)+ Image拖放(下)",
|
||||
helpGen5: "Model标签:Checkpoint/VAE/LoRA/ControlNet/UNET/TextEncoder选择器(含筛选)",
|
||||
helpGen6: "Settings标签:左列为KSampler(上)+ LatentImage(下)",
|
||||
helpGen6: "Settings标签:KSampler与Latent Image并排显示(各占50%宽度)",
|
||||
helpGen7: "Raw JSON在每个标签右列常驻显示 — 直接编辑并Apply即时重载工作流",
|
||||
helpGen8: "种子管理:随机/固定/递增/递减",
|
||||
helpGen9: "生成图片、进度显示、停止生成、查看结果",
|
||||
helpGen10: "Eagle集成:自动保存生成图片到Eagle",
|
||||
helpGen11: "Checkpoint Batch:从下拉框文件夹树中选择检查点 — 勾选文件夹可一键选中该文件夹内的所有文件,▶展开后也可单独选择;使用Filter筛选;Pause/Resume可在模型之间暂停和继续;Stop在当前生成完成后中止",
|
||||
helpFeederTitle: "GenerateUI Tab — Feeder 子标签",
|
||||
helpFeederDesc: "需要 comfyui-image-feeder 自定义节点。用于操控 ImageFeeder 节点,并对存储在 user/default/image-loop-data/ 中的图片库执行连续生成循环。",
|
||||
helpFeeder1: "左侧面板 — 节点设置:从下拉框选择目标 ImageFeeder 节点;编辑所有节点参数(Directory、Sort Mode、Index、Start/End Index、Batch Size、Seed、Use Selection);点击 Apply 将更改写入当前工作流",
|
||||
helpFeeder2: "左侧面板 — 预设:将当前目录+选择文件保存为命名预设;加载或删除已保存的预设",
|
||||
helpFeeder3: "左侧面板 — Run:After gen 模式决定每次生成后的 index 行为 — Loop(回到0并无限循环)、Increment(递增 index,全部图片用完后自动停止)、Fixed(始终使用相同 index);点击 Run 开始循环,Stop 中断",
|
||||
helpFeeder4: "中央面板 — 文件夹树:浏览 image-loop-data 内的子文件夹;点击文件夹加载图片;点击 ↺ 重新加载目录树",
|
||||
helpFeeder5: "中央面板 — 图片网格:缩略图显示所选文件夹内的所有图片;勾选图片加入选择集;All/None 按钮一键全选或清除;点击图片打开预览面板",
|
||||
helpFeeder6: "中央面板 — 预览面板:放大显示点击的图片,并显示文件名、分辨率和文件大小",
|
||||
helpFeeder7: "状态栏:显示当前文件夹路径,以及该文件夹内已选中图片数占总数的比例",
|
||||
helpFeeder8: "Run 时的种子:使用右侧面板的种子设置(随机/固定/递增/递减)— ImageFeeder 自身的 Seed 字段仅影响随机排序的顺序",
|
||||
helpFeeder9: "Index 同步:每次生成完成后节点通过 WebSocket 返回下一个 index;Index 字段自动更新,保持显示与执行状态一致",
|
||||
helpPrompt1: "AI助手(Ollama):模型选择、聊天咨询提示词、图片附件(视觉模型)",
|
||||
helpPrompt2: "将AI回复应用到生成UI的提示词",
|
||||
helpPrompt3: "翻译功能:日\u2194英、中\u2194英",
|
||||
@@ -1397,6 +1441,8 @@ const LANGUAGES = {
|
||||
helpPrompt5: "预设管理器:浏览所有预设、收藏、按组筛选",
|
||||
helpPrompt6: "组管理:创建组、分配/移除预设、删除组",
|
||||
helpPrompt7: "GenUI Set:将预设提示词直接应用到生成UI",
|
||||
helpPrompt8: "通配符工具栏:一键插入通配符语法({|}、{n$|}、__|__、<lora:>等);支持包裹选中文本",
|
||||
helpPrompt9: "通配符文件管理:创建/编辑/删除.txt/.yaml通配符文件;点击文件名在光标处插入__文件名__",
|
||||
helpSettings1: "UI语言和摘要语言设置",
|
||||
helpSettings2: "主题选择(13种预设)+ 自定义:颜色调整、背景图案(条纹、波点、棋盘格、SVG平铺)、字体选择(含日文显示字体在内的16种Google Fonts)",
|
||||
helpSettings3: "工作流数据文件夹路径更改/重置",
|
||||
@@ -1406,6 +1452,7 @@ const LANGUAGES = {
|
||||
helpSettings7: "Eagle集成URL、自动保存开关",
|
||||
helpSettings8: "数据管理:将所有插件数据(设置、元数据、提示词等)导出为JSON文件,或导入以恢复。导入后请重启ComfyUI。",
|
||||
helpSettings9: "各部分可折叠展开",
|
||||
helpSettings10: "Wildcard Integration:将WFS通配符目录链接到ComfyUI-Impact-Pack的wildcards目录(Windows为目录联结,其他OS为符号链接);现有文件自动迁移",
|
||||
helpNodes1: "节点浏览器:按名称/描述/别名/标签搜索,按分类、包、标签、组、收藏筛选",
|
||||
helpNodes2: "卡片和表格视图模式切换(带分页)",
|
||||
helpNodes3: "收藏节点,添加自定义标签进行整理",
|
||||
@@ -1428,12 +1475,13 @@ const LANGUAGES = {
|
||||
helpGallery2: "三列布局:文件夹树(左) | 图像网格(中) | 详情面板(右)",
|
||||
helpGallery3: "缩略图和表格视图模式;双击任何图像可在全屏灯箱中打开",
|
||||
helpGallery4: "工具栏过滤器:收藏 (★)、文本搜索、标签过滤、分组过滤、按日期/名称排序",
|
||||
helpGallery5: "使用 Ctrl+点击 多选 — 批量操作:添加到分组、全部收藏、取消全部收藏",
|
||||
helpGallery6: "详情面板 — 信息(Info)标签:文件名、大小、日期、标签(添加/移除)、包含保存功能的备注",
|
||||
helpGallery7: "详情面板 — 元数据(Metadata)标签:ComfyUI 嵌入的内置工作流 JSON 查看器及一键复制功能",
|
||||
helpGallery8: "详情面板 — 分组(Groups)标签:对每个图像进行组的创建、分配、移除、重命名和删除",
|
||||
helpGallery9: "支持 PNG(tEXt/iTXt 元数据)、JPEG、WebP、GIF;对于 PNG 会读取 ComfyUI 内置的 JSON 提取工作流",
|
||||
helpGallery10: "文件夹树:点击文件夹加载其包含的图像;使用 ▶ 箭头展开子文件夹",
|
||||
helpGallery5: "文件夹树:点击加载图像,▶展开;'+ New'在选定文件夹内创建子文件夹;'Del'删除该文件夹及其所有内容",
|
||||
helpGallery6: "使用Ctrl+点击多选 — 批量操作:添加到分组、全部收藏、取消全部收藏、移动到...、删除",
|
||||
helpGallery7: "详情面板 — 信息(Info)标签:文件名、大小、日期、标签(添加/移除)、备注;'移动到...'将图像移至另一文件夹;'删除'永久删除",
|
||||
helpGallery8: "详情面板 — 元数据(Metadata)标签:显示来自PNG嵌入数据(prompt / workflow键)或Generate UI标签保存的工作流JSON;一键复制到剪贴板",
|
||||
helpGallery9: "详情面板 — 分组(Groups)标签:对每个图像进行组的创建、分配、移除、重命名和删除",
|
||||
helpGallery10: "工作流自动保存:在Generate UI标签生成的图像,其工作流会自动保存到图库元数据,并在Metadata标签中显示",
|
||||
helpGallery11: "支持PNG(tEXt/iTXt)、JPEG、WebP、GIF;PNG格式读取ComfyUI嵌入的JSON(prompt / workflow键)提取工作流",
|
||||
helpSidepanelTitle: "Workflow Studio Library(ComfyUI侧面板)",
|
||||
helpSidepanel1: "从ComfyUI工具栏的Workflow Studio Library按钮打开",
|
||||
helpSidepanel2: "Workflows标签:All / ★ Favorites / Groups子标签,All视图中收藏项显示★",
|
||||
|
||||
@@ -593,6 +593,233 @@ function applyToGenerateUI(text) {
|
||||
showToast(t("appliedToGenerateUI"), "success");
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Wildcard API helpers
|
||||
// ============================================
|
||||
|
||||
async function wcFetchFiles() {
|
||||
try {
|
||||
const res = await fetch("/api/wfm/wildcards");
|
||||
return res.ok ? await res.json() : [];
|
||||
} catch { return []; }
|
||||
}
|
||||
|
||||
async function wcFetchContent(filename) {
|
||||
try {
|
||||
const res = await fetch(`/api/wfm/wildcards/content?filename=${encodeURIComponent(filename)}`);
|
||||
const data = await res.json();
|
||||
return data.content ?? null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
async function wcSaveFile(filename, content) {
|
||||
try {
|
||||
const res = await fetch("/api/wfm/wildcards/save", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ filename, content }),
|
||||
});
|
||||
const data = await res.json();
|
||||
return data.status === "ok" ? data.file : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
async function wcDeleteFile(filename) {
|
||||
try {
|
||||
await fetch("/api/wfm/wildcards/delete", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ filename }),
|
||||
});
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Wildcard state & helpers
|
||||
// ============================================
|
||||
|
||||
let wcFiles = [];
|
||||
let wcEditingFilename = null; // null = new file
|
||||
|
||||
// Insert text at cursor; if open+close provided wraps selection
|
||||
function wcInsertAtCursor(textarea, open, close = "") {
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const selected = textarea.value.substring(start, end);
|
||||
let inserted, cursorStart, cursorEnd;
|
||||
|
||||
if (selected && close) {
|
||||
inserted = open + selected + close;
|
||||
cursorStart = start + open.length;
|
||||
cursorEnd = cursorStart + selected.length;
|
||||
} else if (close) {
|
||||
inserted = open + close;
|
||||
cursorStart = start + open.length;
|
||||
cursorEnd = cursorStart;
|
||||
} else {
|
||||
inserted = open;
|
||||
cursorStart = start + open.length;
|
||||
cursorEnd = cursorStart;
|
||||
}
|
||||
|
||||
textarea.value =
|
||||
textarea.value.substring(0, start) +
|
||||
inserted +
|
||||
textarea.value.substring(end);
|
||||
textarea.setSelectionRange(cursorStart, cursorEnd);
|
||||
textarea.focus();
|
||||
textarea.dispatchEvent(new Event("input"));
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Wildcard file list rendering
|
||||
// ============================================
|
||||
|
||||
function wcRenderFileList() {
|
||||
const list = document.getElementById("wfm-wc-file-list");
|
||||
if (!list) return;
|
||||
list.innerHTML = "";
|
||||
|
||||
if (wcFiles.length === 0) {
|
||||
list.innerHTML = `<div class="wfm-pm-empty" style="font-size:12px;">No wildcard files yet.<br><small>Click "+ New" to create one.</small></div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Group by directory; root ("") first, then alphabetical
|
||||
const grouped = {};
|
||||
for (const f of wcFiles) {
|
||||
const dir = f.dir || "";
|
||||
if (!grouped[dir]) grouped[dir] = [];
|
||||
grouped[dir].push(f);
|
||||
}
|
||||
const dirs = Object.keys(grouped).sort((a, b) => {
|
||||
if (a === "") return -1;
|
||||
if (b === "") return 1;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
|
||||
function addFileItem(f, indented) {
|
||||
const item = document.createElement("div");
|
||||
item.className = "wfm-wc-file-item" + (indented ? " wfm-wc-file-item--sub" : "");
|
||||
item.innerHTML = `
|
||||
<div class="wfm-wc-file-item-info">
|
||||
<span class="wfm-wc-file-name">${esc(f.name)}</span>
|
||||
<span class="wfm-wc-file-ext">.${esc(f.ext)}</span>
|
||||
</div>
|
||||
<div class="wfm-wc-file-item-actions">
|
||||
<button class="wfm-pm-action-btn wfm-wc-use-btn" title="Insert __${esc(f.wc_name)}__ at cursor">Use</button>
|
||||
<button class="wfm-pm-action-btn wfm-wc-edit-btn" title="Edit file">✎</button>
|
||||
</div>
|
||||
`;
|
||||
item.querySelector(".wfm-wc-use-btn").addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
const ta = document.getElementById("wfm-wc-prompt");
|
||||
if (ta) wcInsertAtCursor(ta, `__${f.wc_name}__`);
|
||||
});
|
||||
item.querySelector(".wfm-wc-edit-btn").addEventListener("click", async (e) => {
|
||||
e.stopPropagation();
|
||||
const content = await wcFetchContent(f.filename);
|
||||
wcOpenEditor(f.wc_name, f.ext, content ?? "", f.filename);
|
||||
});
|
||||
item.addEventListener("click", () => {
|
||||
const ta = document.getElementById("wfm-wc-prompt");
|
||||
if (ta) wcInsertAtCursor(ta, `__${f.wc_name}__`);
|
||||
});
|
||||
list.appendChild(item);
|
||||
}
|
||||
|
||||
for (const dir of dirs) {
|
||||
if (dir) {
|
||||
const header = document.createElement("div");
|
||||
header.className = "wfm-wc-dir-header";
|
||||
header.textContent = dir + "/";
|
||||
list.appendChild(header);
|
||||
}
|
||||
for (const f of grouped[dir]) {
|
||||
addFileItem(f, dir !== "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Wildcard file editor
|
||||
// ============================================
|
||||
|
||||
function wcOpenEditor(name, ext, content, existingFilename) {
|
||||
wcEditingFilename = existingFilename || null;
|
||||
|
||||
const editor = document.getElementById("wfm-wc-editor");
|
||||
const nameInput = document.getElementById("wfm-wc-editor-name");
|
||||
const extSelect = document.getElementById("wfm-wc-editor-ext");
|
||||
const contentTA = document.getElementById("wfm-wc-editor-content");
|
||||
const deleteBtn = document.getElementById("wfm-wc-editor-delete-btn");
|
||||
|
||||
if (!editor) return;
|
||||
nameInput.value = name || "";
|
||||
extSelect.value = ext || "txt";
|
||||
contentTA.value = content || "";
|
||||
deleteBtn.style.display = existingFilename ? "" : "none";
|
||||
editor.style.display = "";
|
||||
nameInput.focus();
|
||||
}
|
||||
|
||||
function wcCloseEditor() {
|
||||
wcEditingFilename = null;
|
||||
const editor = document.getElementById("wfm-wc-editor");
|
||||
if (editor) editor.style.display = "none";
|
||||
}
|
||||
|
||||
async function wcRefreshFiles() {
|
||||
wcFiles = await wcFetchFiles();
|
||||
wcRenderFileList();
|
||||
wcUpdateFilePicker();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Wildcard file picker popup
|
||||
// ============================================
|
||||
|
||||
function wcUpdateFilePicker() {
|
||||
const picker = document.getElementById("wfm-wc-file-picker");
|
||||
if (!picker) return;
|
||||
picker.innerHTML = "";
|
||||
if (wcFiles.length === 0) {
|
||||
picker.innerHTML = `<div style="padding:8px;font-size:11px;color:var(--wfm-text-secondary);">No wildcard files</div>`;
|
||||
return;
|
||||
}
|
||||
// Group by directory same as file list
|
||||
const grouped = {};
|
||||
for (const f of wcFiles) {
|
||||
const dir = f.dir || "";
|
||||
if (!grouped[dir]) grouped[dir] = [];
|
||||
grouped[dir].push(f);
|
||||
}
|
||||
const dirs = Object.keys(grouped).sort((a, b) => {
|
||||
if (a === "") return -1;
|
||||
if (b === "") return 1;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
for (const dir of dirs) {
|
||||
if (dir) {
|
||||
const sep = document.createElement("div");
|
||||
sep.className = "wfm-wc-picker-dir";
|
||||
sep.textContent = dir + "/";
|
||||
picker.appendChild(sep);
|
||||
}
|
||||
for (const f of grouped[dir]) {
|
||||
const btn = document.createElement("button");
|
||||
btn.className = "wfm-wc-picker-item" + (dir ? " wfm-wc-picker-item--sub" : "");
|
||||
btn.textContent = `__${f.wc_name}__`;
|
||||
btn.addEventListener("click", () => {
|
||||
const ta = document.getElementById("wfm-wc-prompt");
|
||||
if (ta) wcInsertAtCursor(ta, `__${f.wc_name}__`);
|
||||
picker.style.display = "none";
|
||||
});
|
||||
picker.appendChild(btn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Initialize
|
||||
// ============================================
|
||||
@@ -853,4 +1080,157 @@ export function initPromptTab() {
|
||||
pmSearchText = e.target.value.trim();
|
||||
renderPresetManager();
|
||||
});
|
||||
|
||||
// ── Center column tab switching ──────────────────────────
|
||||
document.querySelectorAll(".wfm-prompt-center-tab").forEach(btn => {
|
||||
btn.addEventListener("click", () => {
|
||||
const target = btn.dataset.ptab;
|
||||
document.querySelectorAll(".wfm-prompt-center-tab").forEach(b => b.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
document.querySelectorAll(".wfm-prompt-center-pane").forEach(p => {
|
||||
p.style.display = p.id === `wfm-ptab-${target}` ? "" : "none";
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ── Wildcard syntax buttons ──────────────────────────────
|
||||
document.querySelectorAll(".wfm-wc-btn[data-wc-open]").forEach(btn => {
|
||||
btn.addEventListener("click", () => {
|
||||
const ta = document.getElementById("wfm-wc-prompt");
|
||||
if (!ta) return;
|
||||
const open = btn.dataset.wcOpen || "";
|
||||
const close = btn.dataset.wcClose || "";
|
||||
wcInsertAtCursor(ta, open, close);
|
||||
});
|
||||
});
|
||||
|
||||
// LoRA LBW template button
|
||||
document.getElementById("wfm-wc-lora-btn")?.addEventListener("click", () => {
|
||||
const ta = document.getElementById("wfm-wc-prompt");
|
||||
if (!ta) return;
|
||||
wcInsertAtCursor(ta, "<lora::1:LBW=;>");
|
||||
// Move cursor to after "lora:" for the name
|
||||
const pos = ta.selectionStart - "<lora::1:LBW=;>".length + 6;
|
||||
ta.setSelectionRange(pos, pos);
|
||||
ta.focus();
|
||||
});
|
||||
|
||||
// n$${ } button — asks for n then inserts multi-pick template
|
||||
document.getElementById("wfm-wc-nset-btn")?.addEventListener("click", () => {
|
||||
const ta = document.getElementById("wfm-wc-prompt");
|
||||
if (!ta) return;
|
||||
const n = prompt("Number of items to pick (n):", "2");
|
||||
if (n === null) return;
|
||||
const num = parseInt(n, 10);
|
||||
if (isNaN(num) || num < 1) { showToast("Enter a positive integer", "error"); return; }
|
||||
const template = "{" + num + "$$|}";
|
||||
const start = ta.selectionStart;
|
||||
wcInsertAtCursor(ta, template);
|
||||
// Move cursor between $$ and |
|
||||
const inner = start + ("{" + num + "$$").length;
|
||||
ta.setSelectionRange(inner, inner);
|
||||
ta.focus();
|
||||
});
|
||||
|
||||
// File picker toggle
|
||||
const filePickBtn = document.getElementById("wfm-wc-filepick-btn");
|
||||
const filePicker = document.getElementById("wfm-wc-file-picker");
|
||||
filePickBtn?.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
if (!filePicker) return;
|
||||
const isOpen = filePicker.style.display !== "none";
|
||||
filePicker.style.display = isOpen ? "none" : "";
|
||||
if (!isOpen) wcUpdateFilePicker();
|
||||
});
|
||||
document.addEventListener("click", () => {
|
||||
if (filePicker) filePicker.style.display = "none";
|
||||
});
|
||||
|
||||
// ── Wildcard prompt actions ──────────────────────────────
|
||||
document.getElementById("wfm-wc-copy-btn")?.addEventListener("click", () => {
|
||||
const ta = document.getElementById("wfm-wc-prompt");
|
||||
const text = ta?.value || "";
|
||||
if (!text.trim()) { showToast(t("noTextToCopy"), "error"); return; }
|
||||
navigator.clipboard.writeText(text).then(() => showToast(t("copiedToClipboard"), "success"));
|
||||
});
|
||||
|
||||
document.getElementById("wfm-wc-to-pos-btn")?.addEventListener("click", () => {
|
||||
const src = document.getElementById("wfm-wc-prompt");
|
||||
const dst = document.getElementById("wfm-preset-pos");
|
||||
if (!src || !dst) return;
|
||||
dst.value = src.value;
|
||||
dst.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
showToast("Sent to Positive Prompt", "success");
|
||||
});
|
||||
|
||||
document.getElementById("wfm-wc-to-neg-btn")?.addEventListener("click", () => {
|
||||
const src = document.getElementById("wfm-wc-prompt");
|
||||
const dst = document.getElementById("wfm-preset-neg");
|
||||
if (!src || !dst) return;
|
||||
dst.value = src.value;
|
||||
dst.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
showToast("Sent to Negative Prompt", "success");
|
||||
});
|
||||
|
||||
document.getElementById("wfm-wc-clear-btn")?.addEventListener("click", () => {
|
||||
const ta = document.getElementById("wfm-wc-prompt");
|
||||
if (ta) { ta.value = ""; ta.focus(); }
|
||||
});
|
||||
|
||||
// ── Wildcard file manager ────────────────────────────────
|
||||
document.getElementById("wfm-wc-new-file-btn")?.addEventListener("click", () => {
|
||||
wcOpenEditor("", "txt", "", null);
|
||||
});
|
||||
|
||||
document.getElementById("wfm-wc-files-refresh-btn")?.addEventListener("click", () => {
|
||||
wcRefreshFiles();
|
||||
});
|
||||
|
||||
// Editor: save
|
||||
document.getElementById("wfm-wc-editor-save-btn")?.addEventListener("click", async () => {
|
||||
const nameInput = document.getElementById("wfm-wc-editor-name");
|
||||
const extSelect = document.getElementById("wfm-wc-editor-ext");
|
||||
const contentTA = document.getElementById("wfm-wc-editor-content");
|
||||
|
||||
// Normalize separators and strip leading/trailing slashes
|
||||
const rawName = (nameInput?.value || "").trim().replace(/\\/g, "/").replace(/\/+/g, "/").replace(/^\/|\/$/g, "");
|
||||
const ext = extSelect?.value || "txt";
|
||||
const content = contentTA?.value || "";
|
||||
|
||||
if (!rawName) { showToast("Enter a filename", "error"); return; }
|
||||
// Validate each path component (allow letters, numbers, -, _, ., space)
|
||||
const nameParts = rawName.split("/");
|
||||
if (nameParts.some(p => !p || !/^[\w\-. ]+$/.test(p))) {
|
||||
showToast("Invalid path. Use folder/name format, each part: letters, numbers, -, _, . only", "error");
|
||||
return;
|
||||
}
|
||||
const name = rawName;
|
||||
const filename = `${name}.${ext}`;
|
||||
const saved = await wcSaveFile(filename, content);
|
||||
if (saved) {
|
||||
showToast(`Saved: ${filename}`, "success");
|
||||
wcCloseEditor();
|
||||
wcRefreshFiles();
|
||||
} else {
|
||||
showToast("Failed to save file", "error");
|
||||
}
|
||||
});
|
||||
|
||||
// Editor: delete
|
||||
document.getElementById("wfm-wc-editor-delete-btn")?.addEventListener("click", async () => {
|
||||
if (!wcEditingFilename) return;
|
||||
if (!confirm(`Delete "${wcEditingFilename}"?`)) return;
|
||||
await wcDeleteFile(wcEditingFilename);
|
||||
showToast(`Deleted: ${wcEditingFilename}`, "success");
|
||||
wcCloseEditor();
|
||||
wcRefreshFiles();
|
||||
});
|
||||
|
||||
// Editor: cancel
|
||||
document.getElementById("wfm-wc-editor-cancel-btn")?.addEventListener("click", () => {
|
||||
wcCloseEditor();
|
||||
});
|
||||
|
||||
// Initial wildcard file load
|
||||
wcRefreshFiles();
|
||||
}
|
||||
|
||||
@@ -620,6 +620,12 @@ export async function initSettingsTab() {
|
||||
<div id="wfm-settings-data-status" style="font-size:12px;margin-top:8px;"></div>
|
||||
</details>
|
||||
|
||||
<!-- Wildcard Integration -->
|
||||
<details class="wfm-settings-section" id="wfm-wildcard-integration-section">
|
||||
<summary class="wfm-settings-summary">Wildcard Integration</summary>
|
||||
<div id="wfm-wildcard-link-status" style="font-size:13px;color:var(--wfm-text-secondary);">Loading...</div>
|
||||
</details>
|
||||
|
||||
<!-- Save Button -->
|
||||
<button class="wfm-btn wfm-btn-primary" id="wfm-settings-save" style="min-width:120px;">${t("saveSettings")}</button>
|
||||
|
||||
@@ -1051,6 +1057,89 @@ export async function initSettingsTab() {
|
||||
comfyUI.updateUrl(savedUrl);
|
||||
}
|
||||
|
||||
// --- Wildcard Integration ---
|
||||
async function wcLoadLinkStatus() {
|
||||
const el = document.getElementById("wfm-wildcard-link-status");
|
||||
if (!el) return;
|
||||
try {
|
||||
const res = await fetch("/api/wfm/wildcards/link-status");
|
||||
const data = await res.json();
|
||||
wcRenderLinkStatus(el, data);
|
||||
} catch (e) {
|
||||
el.innerHTML = `<span style="color:var(--wfm-error,#f44336)">Error: ${e.message}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
function wcRenderLinkStatus(el, data) {
|
||||
if (!data.impact_pack_installed) {
|
||||
el.innerHTML = `
|
||||
<p style="margin:0 0 8px;">ComfyUI-Impact-Pack がインストールされていません。</p>
|
||||
<p style="margin:0;font-size:12px;color:var(--wfm-text-secondary);">
|
||||
ワイルドカードを Impact Pack と共有するには、先に
|
||||
<a href="https://github.com/ltdrdata/ComfyUI-Impact-Pack" target="_blank" rel="noopener"
|
||||
style="color:var(--wfm-accent);">ComfyUI-Impact-Pack</a>
|
||||
をインストールしてください。
|
||||
</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.is_linked) {
|
||||
el.innerHTML = `
|
||||
<p style="margin:0 0 4px;color:var(--wfm-success,#4caf50);">✓ リンク済み</p>
|
||||
<p style="margin:0 0 8px;font-size:12px;color:var(--wfm-text-secondary);">
|
||||
WFS wildcard → ${data.link_target || data.impact_pack_wildcards_dir}
|
||||
</p>
|
||||
<button class="wfm-btn wfm-btn-sm wfm-btn-danger" id="wfm-wc-remove-link">リンクを解除</button>
|
||||
<div id="wfm-wc-link-msg" style="font-size:12px;margin-top:8px;"></div>`;
|
||||
document.getElementById("wfm-wc-remove-link")?.addEventListener("click", async () => {
|
||||
const msg = document.getElementById("wfm-wc-link-msg");
|
||||
try {
|
||||
const r = await fetch("/api/wfm/wildcards/remove-link", { method: "POST" });
|
||||
const d = await r.json();
|
||||
if (!r.ok) throw new Error(d.error || r.statusText);
|
||||
msg.textContent = "リンクを解除しました。";
|
||||
msg.style.color = "var(--wfm-success,#4caf50)";
|
||||
setTimeout(wcLoadLinkStatus, 800);
|
||||
} catch (e) {
|
||||
msg.textContent = "エラー: " + e.message;
|
||||
msg.style.color = "var(--wfm-error,#f44336)";
|
||||
}
|
||||
});
|
||||
} else {
|
||||
el.innerHTML = `
|
||||
<p style="margin:0 0 4px;">Impact Pack wildcards ディレクトリ:</p>
|
||||
<p style="margin:0 0 8px;font-size:12px;color:var(--wfm-text-secondary);">${data.impact_pack_wildcards_dir}</p>
|
||||
<p style="margin:0 0 8px;font-size:12px;color:var(--wfm-text-secondary);">
|
||||
WFS の wildcard ディレクトリを Impact Pack の wildcards ディレクトリへのリンクに置き換えます。
|
||||
既存の WFS ワイルドカードファイルは Impact Pack ディレクトリへ移行されます。
|
||||
</p>
|
||||
<button class="wfm-btn wfm-btn-sm" id="wfm-wc-create-link">リンクを作成</button>
|
||||
<div id="wfm-wc-link-msg" style="font-size:12px;margin-top:8px;"></div>`;
|
||||
document.getElementById("wfm-wc-create-link")?.addEventListener("click", async () => {
|
||||
const msg = document.getElementById("wfm-wc-link-msg");
|
||||
try {
|
||||
const r = await fetch("/api/wfm/wildcards/create-link", { method: "POST" });
|
||||
const d = await r.json();
|
||||
if (!r.ok) throw new Error(d.error || r.statusText);
|
||||
const migrated = d.migrated_files?.length
|
||||
? ` (移行: ${d.migrated_files.join(", ")})`
|
||||
: "";
|
||||
msg.textContent = "リンクを作成しました。" + migrated;
|
||||
msg.style.color = "var(--wfm-success,#4caf50)";
|
||||
setTimeout(wcLoadLinkStatus, 800);
|
||||
} catch (e) {
|
||||
msg.textContent = "エラー: " + e.message;
|
||||
msg.style.color = "var(--wfm-error,#f44336)";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Load wildcard link status when section is opened
|
||||
document.getElementById("wfm-wildcard-integration-section")?.addEventListener("toggle", (e) => {
|
||||
if (e.target.open) wcLoadLinkStatus();
|
||||
});
|
||||
|
||||
// --- Data Export ---
|
||||
document.getElementById("wfm-settings-export")?.addEventListener("click", async () => {
|
||||
const statusEl = document.getElementById("wfm-settings-data-status");
|
||||
|
||||
+282
-55
@@ -264,6 +264,7 @@
|
||||
<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="feeder">Feeder</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Input — left sub-col: Prompt(top)+Image(bottom), right sub-col: Raw JSON -->
|
||||
@@ -310,6 +311,121 @@
|
||||
<div class="wfm-gen-rawjson-col" id="wfm-gen-rawjson-col-settings"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Tab: Feeder — full-width: settings(left) + library(center) -->
|
||||
<div id="wfm-gen-subtab-feeder" class="wfm-gen-subtab-content">
|
||||
<div class="wfm-feeder-layout">
|
||||
<!-- Left: Node selector + params + presets -->
|
||||
<div class="wfm-feeder-settings">
|
||||
<div class="wfm-feeder-pane-header">ImageFeeder Node</div>
|
||||
<div style="display:flex;gap:6px;align-items:center;margin-bottom:8px;">
|
||||
<select id="wfm-feeder-node-sel" class="wfm-select" style="flex:1;min-width:0;"></select>
|
||||
<button id="wfm-feeder-apply-btn" class="wfm-btn wfm-btn-primary" style="white-space:nowrap;">Apply</button>
|
||||
</div>
|
||||
<div class="wfm-feeder-field-row">
|
||||
<label class="wfm-feeder-label">Directory</label>
|
||||
<input type="text" id="wfm-feeder-dir-input" class="wfm-input wfm-feeder-input" placeholder="e.g. subdir">
|
||||
</div>
|
||||
<div class="wfm-feeder-field-row">
|
||||
<label class="wfm-feeder-label">Sort Mode</label>
|
||||
<select id="wfm-feeder-sort" class="wfm-select wfm-feeder-input">
|
||||
<option value="ascending">ascending</option>
|
||||
<option value="descending">descending</option>
|
||||
<option value="random">random</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="wfm-feeder-field-row">
|
||||
<label class="wfm-feeder-label">Index</label>
|
||||
<input type="number" id="wfm-feeder-index" class="wfm-input wfm-feeder-input" value="0" min="0">
|
||||
</div>
|
||||
<div class="wfm-feeder-field-row">
|
||||
<label class="wfm-feeder-label">Start Index</label>
|
||||
<input type="number" id="wfm-feeder-start" class="wfm-input wfm-feeder-input" value="0" min="0">
|
||||
</div>
|
||||
<div class="wfm-feeder-field-row">
|
||||
<label class="wfm-feeder-label">End Index</label>
|
||||
<input type="number" id="wfm-feeder-end" class="wfm-input wfm-feeder-input" value="0" min="0">
|
||||
</div>
|
||||
<div class="wfm-feeder-field-row">
|
||||
<label class="wfm-feeder-label">Batch Size</label>
|
||||
<input type="number" id="wfm-feeder-batch" class="wfm-input wfm-feeder-input" value="1" min="1">
|
||||
</div>
|
||||
<div class="wfm-feeder-field-row">
|
||||
<label class="wfm-feeder-label">Seed</label>
|
||||
<input type="number" id="wfm-feeder-seed" class="wfm-input wfm-feeder-input" value="0">
|
||||
</div>
|
||||
<div class="wfm-feeder-field-row">
|
||||
<label class="wfm-feeder-label">Use Selection</label>
|
||||
<input type="checkbox" id="wfm-feeder-use-sel" checked style="width:16px;height:16px;accent-color:var(--wfm-primary);">
|
||||
</div>
|
||||
|
||||
<div class="wfm-feeder-pane-header" style="margin-top:12px;">Presets</div>
|
||||
<div style="display:flex;gap:6px;margin-bottom:6px;">
|
||||
<input type="text" id="wfm-feeder-preset-name" class="wfm-input" style="flex:1;min-width:0;" placeholder="Preset name">
|
||||
<button id="wfm-feeder-preset-save" class="wfm-btn wfm-btn-sm" style="white-space:nowrap;">Save</button>
|
||||
</div>
|
||||
<div style="display:flex;gap:6px;">
|
||||
<select id="wfm-feeder-preset-sel" class="wfm-select" style="flex:1;min-width:0;"></select>
|
||||
<button id="wfm-feeder-preset-load" class="wfm-btn wfm-btn-sm wfm-btn-primary" style="white-space:nowrap;">Load</button>
|
||||
<button id="wfm-feeder-preset-del" class="wfm-btn wfm-btn-sm wfm-btn-danger" style="white-space:nowrap;">Del</button>
|
||||
</div>
|
||||
|
||||
<div class="wfm-feeder-pane-header" style="margin-top:12px;">Run</div>
|
||||
<div class="wfm-feeder-field-row">
|
||||
<label class="wfm-feeder-label">After gen</label>
|
||||
<select id="wfm-feeder-control-after" class="wfm-select wfm-feeder-input">
|
||||
<option value="loop">Loop</option>
|
||||
<option value="increment">Increment</option>
|
||||
<option value="fixed">Fixed</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;margin-top:4px;">
|
||||
<button id="wfm-feeder-run-btn" class="wfm-btn wfm-btn-primary" style="flex:1;">▶ Run</button>
|
||||
<button id="wfm-feeder-stop-btn" class="wfm-btn wfm-btn-danger" style="flex:1;" disabled>■ Stop</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Center: Image Library -->
|
||||
<div class="wfm-feeder-library">
|
||||
<!-- Status bar -->
|
||||
<div id="wfm-feeder-status" class="wfm-feeder-status-bar">Select a folder</div>
|
||||
|
||||
<div class="wfm-feeder-library-body">
|
||||
<!-- Folder tree -->
|
||||
<div class="wfm-feeder-tree-pane">
|
||||
<div class="wfm-feeder-pane-header" style="display:flex;align-items:center;justify-content:space-between;">
|
||||
<span>Folders</span>
|
||||
<button id="wfm-feeder-reload-tree" class="wfm-btn wfm-btn-sm" title="Reload">↺</button>
|
||||
</div>
|
||||
<div id="wfm-feeder-tree" class="wfm-feeder-tree-scroll">
|
||||
<div class="wfm-feeder-tree-msg">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image grid -->
|
||||
<div class="wfm-feeder-grid-pane">
|
||||
<div class="wfm-feeder-pane-header" style="display:flex;align-items:center;justify-content:space-between;">
|
||||
<span>Images</span>
|
||||
<div style="display:flex;gap:4px;">
|
||||
<button id="wfm-feeder-sel-all" class="wfm-btn wfm-btn-sm">All</button>
|
||||
<button id="wfm-feeder-desel-all" class="wfm-btn wfm-btn-sm">None</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="wfm-feeder-grid" class="wfm-feeder-grid">
|
||||
<div class="wfm-feeder-grid-msg">Select a folder</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preview -->
|
||||
<div class="wfm-feeder-preview-pane">
|
||||
<div class="wfm-feeder-pane-header">Preview</div>
|
||||
<img id="wfm-feeder-preview-img" class="wfm-feeder-preview-img" style="display:none;" alt="">
|
||||
<div id="wfm-feeder-preview-name" class="wfm-feeder-preview-name"></div>
|
||||
<pre id="wfm-feeder-preview-info" class="wfm-feeder-preview-info"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Generation Controls & Results -->
|
||||
@@ -352,12 +468,23 @@
|
||||
</div>
|
||||
<div id="wfm-ckpt-batch-body" style="display:none;margin-top:8px;">
|
||||
<div style="margin-bottom:6px;">
|
||||
<div style="font-size:11px;color:var(--wfm-text-secondary);margin-bottom:3px;">Include Folders (empty = all, comma-separated)</div>
|
||||
<input type="text" id="wfm-ckpt-batch-include" class="wfm-input" placeholder="e.g. sdxl, flux">
|
||||
</div>
|
||||
<div style="margin-bottom:6px;">
|
||||
<div style="font-size:11px;color:var(--wfm-text-secondary);margin-bottom:3px;">Exclude Folders (comma-separated)</div>
|
||||
<input type="text" id="wfm-ckpt-batch-exclude" class="wfm-input" placeholder="e.g. old, test">
|
||||
<div style="font-size:11px;color:var(--wfm-text-secondary);margin-bottom:3px;">Checkpoints</div>
|
||||
<div class="wfm-ckpt-dropdown-wrap" id="wfm-ckpt-dropdown-wrap">
|
||||
<button type="button" id="wfm-ckpt-dropdown-btn" class="wfm-ckpt-dropdown-trigger">
|
||||
<span id="wfm-ckpt-dropdown-label">All checkpoints</span>
|
||||
<span class="wfm-ckpt-dropdown-arrow">▼</span>
|
||||
</button>
|
||||
<div id="wfm-ckpt-dropdown-panel" class="wfm-ckpt-dropdown-panel" style="display:none;">
|
||||
<div style="padding:6px 8px 4px;">
|
||||
<input type="text" id="wfm-ckpt-search" class="wfm-input" style="height:28px;padding:4px 8px;font-size:11px;" placeholder="Filter...">
|
||||
</div>
|
||||
<div style="display:flex;gap:4px;padding:0 8px 4px;">
|
||||
<button type="button" id="wfm-ckpt-select-all" class="wfm-btn wfm-btn-sm" style="flex:1;font-size:10px;">All</button>
|
||||
<button type="button" id="wfm-ckpt-deselect-all" class="wfm-btn wfm-btn-sm" style="flex:1;font-size:10px;">None</button>
|
||||
</div>
|
||||
<div id="wfm-ckpt-list" class="wfm-ckpt-dropdown-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="wfm-ckpt-batch-info" style="font-size:11px;color:var(--wfm-text-secondary);"></div>
|
||||
<div id="wfm-ckpt-batch-progress" style="margin-top:8px;display:none;">
|
||||
@@ -368,6 +495,9 @@
|
||||
<div class="wfm-gen-progress-track">
|
||||
<div id="wfm-ckpt-batch-bar" class="wfm-gen-progress-bar" style="background:var(--wfm-warning,#f59e0b);"></div>
|
||||
</div>
|
||||
<div style="margin-top:6px;">
|
||||
<button id="wfm-ckpt-batch-pause-btn" class="wfm-btn wfm-btn-sm" style="width:100%;">Pause</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -434,61 +564,139 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Col 2: Preset Editor -->
|
||||
<!-- Col 2: Presets + Preset Manager (tabbed) -->
|
||||
<div class="wfm-prompt-split-col">
|
||||
<div class="wfm-prompt-split-header">Presets</div>
|
||||
<div class="wfm-preset-panel">
|
||||
<div style="display:flex;gap:6px;margin-bottom:10px;align-items:center;">
|
||||
<select id="wfm-preset-select" class="wfm-select" style="flex:1;">
|
||||
<option value="">New Preset (none selected)</option>
|
||||
</select>
|
||||
<button id="wfm-preset-apply-btn" class="wfm-btn wfm-btn-sm wfm-btn-primary">GenUI Set</button>
|
||||
<!-- Center column tab navigation -->
|
||||
<div class="wfm-prompt-center-tabnav">
|
||||
<button class="wfm-prompt-center-tab active" data-ptab="presets">Presets</button>
|
||||
<button class="wfm-prompt-center-tab" data-ptab="manager">Preset Manager</button>
|
||||
</div>
|
||||
|
||||
<!-- Presets pane -->
|
||||
<div id="wfm-ptab-presets" class="wfm-prompt-center-pane">
|
||||
<div class="wfm-preset-panel">
|
||||
<div style="display:flex;gap:6px;margin-bottom:10px;align-items:center;">
|
||||
<select id="wfm-preset-select" class="wfm-select" style="flex:1;">
|
||||
<option value="">New Preset (none selected)</option>
|
||||
</select>
|
||||
<button id="wfm-preset-apply-btn" class="wfm-btn wfm-btn-sm wfm-btn-primary">GenUI Set</button>
|
||||
</div>
|
||||
<div class="wfm-form-group">
|
||||
<label>Preset Name</label>
|
||||
<input type="text" id="wfm-preset-name" class="wfm-input" placeholder="Enter preset name...">
|
||||
</div>
|
||||
<div class="wfm-form-group">
|
||||
<label>Category</label>
|
||||
<input type="text" id="wfm-preset-category" class="wfm-input" placeholder="e.g. Portrait, Landscape...">
|
||||
</div>
|
||||
<div class="wfm-form-group" style="flex:1;min-height:0;display:flex;flex-direction:column;">
|
||||
<label>Positive Prompt</label>
|
||||
<textarea id="wfm-preset-pos" class="wfm-textarea" style="flex:1;" placeholder="Positive prompt..."></textarea>
|
||||
</div>
|
||||
<div class="wfm-form-group" style="flex:1;min-height:0;display:flex;flex-direction:column;">
|
||||
<label>Negative Prompt</label>
|
||||
<textarea id="wfm-preset-neg" class="wfm-textarea" style="flex:1;" placeholder="Negative prompt..."></textarea>
|
||||
</div>
|
||||
<div style="display:flex; gap:4px;">
|
||||
<button id="wfm-preset-copy-pos-btn" class="wfm-btn" style="flex:2;" title="Copy Positive Prompt">PP Copy</button>
|
||||
<button id="wfm-preset-copy-neg-btn" class="wfm-btn" style="flex:2;" title="Copy Negative Prompt">NP Copy</button>
|
||||
<button id="wfm-preset-save-btn" class="wfm-btn wfm-btn-primary" style="flex:1;">Save</button>
|
||||
</div>
|
||||
<div style="display:flex; gap:4px; margin-top:4px;">
|
||||
<select id="wfm-preset-group-select" class="wfm-select" style="flex:1;">
|
||||
<option value="">Select group...</option>
|
||||
</select>
|
||||
<button id="wfm-preset-add-to-group-btn" class="wfm-btn wfm-btn-sm">Add to Group</button>
|
||||
<button id="wfm-preset-new-group-btn" class="wfm-btn wfm-btn-sm">+ Group</button>
|
||||
<button id="wfm-preset-del-group-btn" class="wfm-btn wfm-btn-sm wfm-btn-danger" title="Delete selected group">Del Group</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wfm-form-group">
|
||||
<label>Preset Name</label>
|
||||
<input type="text" id="wfm-preset-name" class="wfm-input" placeholder="Enter preset name...">
|
||||
</div>
|
||||
<div class="wfm-form-group">
|
||||
<label>Category</label>
|
||||
<input type="text" id="wfm-preset-category" class="wfm-input" placeholder="e.g. Portrait, Landscape...">
|
||||
</div>
|
||||
<div class="wfm-form-group" style="flex:1;min-height:0;display:flex;flex-direction:column;">
|
||||
<label>Positive Prompt</label>
|
||||
<textarea id="wfm-preset-pos" class="wfm-textarea" style="flex:1;" placeholder="Positive prompt..."></textarea>
|
||||
</div>
|
||||
<div class="wfm-form-group" style="flex:1;min-height:0;display:flex;flex-direction:column;">
|
||||
<label>Negative Prompt</label>
|
||||
<textarea id="wfm-preset-neg" class="wfm-textarea" style="flex:1;" placeholder="Negative prompt..."></textarea>
|
||||
</div>
|
||||
<div style="display:flex; gap:4px;">
|
||||
<button id="wfm-preset-copy-pos-btn" class="wfm-btn" style="flex:2;" title="Copy Positive Prompt">PP Copy</button>
|
||||
<button id="wfm-preset-copy-neg-btn" class="wfm-btn" style="flex:2;" title="Copy Negative Prompt">NP Copy</button>
|
||||
<button id="wfm-preset-save-btn" class="wfm-btn wfm-btn-primary" style="flex:1;">Save</button>
|
||||
</div>
|
||||
<div style="display:flex; gap:4px; margin-top:4px;">
|
||||
<select id="wfm-preset-group-select" class="wfm-select" style="flex:1;">
|
||||
<option value="">Select group...</option>
|
||||
</select>
|
||||
<button id="wfm-preset-add-to-group-btn" class="wfm-btn wfm-btn-sm">Add to Group</button>
|
||||
<button id="wfm-preset-new-group-btn" class="wfm-btn wfm-btn-sm">+ Group</button>
|
||||
<button id="wfm-preset-del-group-btn" class="wfm-btn wfm-btn-sm wfm-btn-danger" title="Delete selected group">Del Group</button>
|
||||
</div>
|
||||
|
||||
<!-- Preset Manager pane -->
|
||||
<div id="wfm-ptab-manager" class="wfm-prompt-center-pane" style="display:none;">
|
||||
<div class="wfm-preset-manager">
|
||||
<div class="wfm-pm-tabs">
|
||||
<button class="wfm-pm-tab active" data-pmtab="all">All</button>
|
||||
<button class="wfm-pm-tab" data-pmtab="favorites">★</button>
|
||||
<button class="wfm-pm-tab" data-pmtab="groups">Groups</button>
|
||||
</div>
|
||||
<div class="wfm-pm-search">
|
||||
<input type="text" id="wfm-pm-search-input" class="wfm-input" placeholder="Search presets...">
|
||||
</div>
|
||||
<div id="wfm-pm-list" class="wfm-pm-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Col 3: Preset Manager (list, favorites, groups) -->
|
||||
<!-- Col 3: Wildcard support -->
|
||||
<div class="wfm-prompt-split-col">
|
||||
<div class="wfm-prompt-split-header">Preset Manager</div>
|
||||
<div class="wfm-preset-manager">
|
||||
<div class="wfm-pm-tabs">
|
||||
<button class="wfm-pm-tab active" data-pmtab="all">All</button>
|
||||
<button class="wfm-pm-tab" data-pmtab="favorites">★</button>
|
||||
<button class="wfm-pm-tab" data-pmtab="groups">Groups</button>
|
||||
<div class="wfm-prompt-split-header">Wildcard</div>
|
||||
<div class="wfm-wc-panel">
|
||||
|
||||
<!-- Input assistance section -->
|
||||
<div class="wfm-wc-input-section">
|
||||
<!-- Wildcard syntax buttons -->
|
||||
<div class="wfm-wc-toolbar">
|
||||
<button class="wfm-wc-btn" data-wc-open="{" data-wc-close="}" title="Random pick set e.g. {cat|dog}">{ }</button>
|
||||
<button class="wfm-wc-btn" data-wc-open="|" title="Separator">|</button>
|
||||
<button class="wfm-wc-btn" data-wc-open="__" data-wc-close="__" title="Wildcard file reference e.g. __animals__">__</button>
|
||||
<button class="wfm-wc-btn" data-wc-open=":" title="Colon">:</button>
|
||||
<button class="wfm-wc-btn" data-wc-open=";" title="Semicolon">;</button>
|
||||
<button class="wfm-wc-btn" data-wc-open="$$" title="Multi-pick e.g. {2$$cat|dog|bird}">$$</button>
|
||||
<button class="wfm-wc-btn" data-wc-open="[" data-wc-close="]" title="Bracket set e.g. [cat|dog]">[ ]</button>
|
||||
<button class="wfm-wc-btn" id="wfm-wc-lora-btn" title="LoRA with LBW e.g. <lora:name:1:LBW=...;...>"><lora></button>
|
||||
<button class="wfm-wc-btn" id="wfm-wc-nset-btn" title="Multi-pick set e.g. {2$$cat|dog|bird}">n$${ }</button>
|
||||
<div class="wfm-wc-file-picker-wrap">
|
||||
<button class="wfm-wc-btn" id="wfm-wc-filepick-btn" title="Insert wildcard file reference">📂 File</button>
|
||||
<div id="wfm-wc-file-picker" class="wfm-wc-file-picker" style="display:none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Prompt textarea -->
|
||||
<textarea id="wfm-wc-prompt" class="wfm-textarea wfm-wc-prompt-ta" rows="4" placeholder="Compose wildcard prompt here... e.g. a photo of {cat|dog}, __colors__ background"></textarea>
|
||||
<!-- Action buttons -->
|
||||
<div class="wfm-wc-prompt-actions">
|
||||
<button id="wfm-wc-copy-btn" class="wfm-btn wfm-btn-sm">Copy</button>
|
||||
<button id="wfm-wc-to-pos-btn" class="wfm-btn wfm-btn-sm wfm-btn-primary" title="Send to Preset Positive Prompt">→ Pos</button>
|
||||
<button id="wfm-wc-to-neg-btn" class="wfm-btn wfm-btn-sm" title="Send to Preset Negative Prompt">→ Neg</button>
|
||||
<button id="wfm-wc-clear-btn" class="wfm-btn wfm-btn-sm" style="margin-left:auto;">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wfm-pm-search">
|
||||
<input type="text" id="wfm-pm-search-input" class="wfm-input" placeholder="Search presets...">
|
||||
|
||||
<!-- Wildcard file manager section -->
|
||||
<div class="wfm-wc-files-section">
|
||||
<div class="wfm-wc-files-header">
|
||||
<span class="wfm-wc-files-title">Wildcard Files</span>
|
||||
<div style="display:flex;gap:4px;">
|
||||
<button id="wfm-wc-new-file-btn" class="wfm-btn wfm-btn-sm wfm-btn-primary">+ New</button>
|
||||
<button id="wfm-wc-files-refresh-btn" class="wfm-btn wfm-btn-sm" title="Reload file list">↻</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- File list -->
|
||||
<div id="wfm-wc-file-list" class="wfm-wc-file-list">
|
||||
<p class="wfm-placeholder" style="padding:12px;font-size:12px;">Loading...</p>
|
||||
</div>
|
||||
<!-- Editor (shown when a file is selected or New is clicked) -->
|
||||
<div id="wfm-wc-editor" class="wfm-wc-editor" style="display:none;">
|
||||
<div class="wfm-wc-editor-header">
|
||||
<input type="text" id="wfm-wc-editor-name" class="wfm-input" placeholder="name or folder/name" style="flex:1;min-width:0;">
|
||||
<select id="wfm-wc-editor-ext" class="wfm-select" style="width:70px;">
|
||||
<option value="txt">txt</option>
|
||||
<option value="yaml">yaml</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="wfm-wc-editor-hint">
|
||||
txt: one item per line | yaml: key: [item1, item2, ...]
|
||||
</div>
|
||||
<textarea id="wfm-wc-editor-content" class="wfm-textarea wfm-wc-editor-ta" rows="8" placeholder="one item per line (txt) or YAML format (yaml)"></textarea>
|
||||
<div class="wfm-wc-editor-actions">
|
||||
<button id="wfm-wc-editor-save-btn" class="wfm-btn wfm-btn-sm wfm-btn-primary">Save</button>
|
||||
<button id="wfm-wc-editor-delete-btn" class="wfm-btn wfm-btn-sm wfm-btn-danger" style="display:none;">Delete</button>
|
||||
<button id="wfm-wc-editor-cancel-btn" class="wfm-btn wfm-btn-sm" style="margin-left:auto;">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="wfm-pm-list" class="wfm-pm-list"></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -534,15 +742,31 @@
|
||||
<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">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-3">4-tab layout — Input (Prompt + Image), Model, Settings, Feeder — Input/Model/Settings each include 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-6">Settings tab: KSampler and Latent Image side by side, each at 50% width</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 — seed input and mode selector stacked vertically for readability</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>
|
||||
<li id="wfm-help-gen-11">Checkpoint Batch: enable via checkbox in the right panel to auto-generate once per checkpoint model; filter by include folders (comma-separated), exclude folders, or leave blank to use all checkpoints; Stop button aborts the loop after the current generation; per-model progress bar shown in amber</li>
|
||||
<li id="wfm-help-gen-11">Checkpoint Batch: select checkpoints by folder from the dropdown — check a folder to select all its files, or expand with ▶ to check individual files; use Filter to search; Pause/Resume suspends and continues between models; Stop aborts after the current generation</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-feeder-title">GenerateUI Tab — Feeder subtab</h3>
|
||||
<p id="wfm-help-feeder-desc">Requires the comfyui-image-feeder custom node. Controls ImageFeeder nodes and runs continuous generation loops over an image library stored in user/default/image-loop-data/.</p>
|
||||
<ul>
|
||||
<li id="wfm-help-feeder-1">Left pane — Node settings: select the target ImageFeeder node from the dropdown; edit all node parameters (Directory, Sort Mode, Index, Start/End Index, Batch Size, Seed, Use Selection); click Apply to write changes to the loaded workflow</li>
|
||||
<li id="wfm-help-feeder-2">Left pane — Presets: save the current directory + selection as a named preset; load or delete saved presets</li>
|
||||
<li id="wfm-help-feeder-3">Left pane — Run: After gen mode determines index behavior after each generation — Loop (wrap back to 0 and continue indefinitely), Increment (advance index and stop when all images are exhausted), Fixed (always use the same index); click Run to start the loop, Stop to interrupt</li>
|
||||
<li id="wfm-help-feeder-4">Center pane — Folder tree: browse subfolders inside image-loop-data; click a folder to load its images; click ↺ to reload the tree</li>
|
||||
<li id="wfm-help-feeder-5">Center pane — Image grid: displays thumbnails of all images in the selected folder; check individual images to add them to the selection; All / None buttons select or clear the entire folder; click an image to open the preview panel</li>
|
||||
<li id="wfm-help-feeder-6">Center pane — Preview panel: shows the clicked image at larger size with filename, resolution, and file size</li>
|
||||
<li id="wfm-help-feeder-7">Status bar: shows the current folder path and how many images are selected out of the total in the folder</li>
|
||||
<li id="wfm-help-feeder-8">Seed during Run: uses the right-pane seed setting (Random / Fixed / Increment / Decrement) — the ImageFeeder's own Seed field controls random sort order only</li>
|
||||
<li id="wfm-help-feeder-9">Index sync: after each generation the node returns the next index via WebSocket; the Index field updates automatically so the display stays in sync</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -556,6 +780,8 @@
|
||||
<li id="wfm-help-prompt-5">Preset Manager: browse all presets, favorites, and group-based filtering</li>
|
||||
<li id="wfm-help-prompt-6">Group management: create groups, assign/remove presets, delete groups</li>
|
||||
<li id="wfm-help-prompt-7">GenUI Set: apply preset prompts directly to GenerateUI</li>
|
||||
<li id="wfm-help-prompt-8">Wildcard toolbar: one-click buttons to insert wildcard syntax ({|}, {n$|}, __|__, <lora:>, etc.); wraps selected text</li>
|
||||
<li id="wfm-help-prompt-9">Wildcard file manager: create/edit/delete .txt/.yaml wildcard files; click filename to insert __filename__ at cursor</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -571,6 +797,7 @@
|
||||
<li id="wfm-help-settings-7">Eagle integration URL and auto-save toggle</li>
|
||||
<li id="wfm-help-settings-8">Data Management: export all plugin data to JSON / import to restore</li>
|
||||
<li id="wfm-help-settings-9">All sections are collapsible accordion panels</li>
|
||||
<li id="wfm-help-settings-10">Wildcard Integration: link WFS wildcard dir to ComfyUI-Impact-Pack wildcards (junction/symlink); existing files migrated automatically</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user