Compare commits

...
6 Commits
Author SHA1 Message Date
ketle-manandClaude Sonnet 5 cb61dc61a7 release: v0.4.7 — Prompt Tab内部整理 + Gallery Promptタブ新設 + Send to Canvas改善 + Presetsソート
- prompt-tab.js(1545行)をprompt-presets/ai-chat/wildcards/styles.jsの4ファイルに分割、
  prompt-tab.jsは薄いオーケストレーターに(prompt-table.js/app.jsは無変更でre-export経由)
- ワイルドカード入力支援ツールバーをPresetsのプロンプト編集モーダルへ追加(Wildcardモーダル
  ではなくプロンプトを書く場所に付けるべきという指摘を受けて再配置)
- Style適用とワイルドカード展開の実行順序を修正: Style内の__file__参照が展開されずに
  送信されていたバグを解消(Style適用→展開の順に変更)
- yamlワイルドカード(ImpactWildcardEncode)を使うサンプルワークフローを追加
- Galleryタブ詳細パネルに「Prompt」タブを新設(既定選択、Metadataタブと同じPOS/NEGリスト
  UIを再利用); Gallery側のプロンプト抽出はAPI形式優先のprompt_workflowを新設し、
  Metadataタブとの抽出結果の食い違いを解消
- Send to Canvasのフォールバックを、Workflow Studio LibraryのWタブを絞り込み表示する
  方式に統一(UI形式のタイトルドラッグ・API形式非対応だった従来方式を置き換え)
- PresetsテーブルツールバーにMarkdown ★/Bソートトグルボタンを追加(複合ソート対応)
- graphifyの運用を一時停止(CLAUDE.md、ユーザー指示による)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 12:19:09 +09:00
ketle-manandClaude Sonnet 5 ad9d06d3e1 graphify: v0.4.6の変更を反映しナレッジグラフを更新
v0.4.6(Metadataタブ移動・GenerateUI Save非表示・Prompt Table表示)で
変更した20ファイルをgraphify --updateで再抽出。
2320ノード・5560エッジ・157コミュニティ(旧グラフから+141ノード/+314エッジ、
重複ノード13件を整理)。DEVLOG.index.md/README.index.mdの2件はOllamaの
出力トークン上限で今回は未反映(次回--updateで再試行)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 20:33:26 +09:00
ketle-manandClaude Sonnet 5 ea408690ff release: v0.4.6 — MetadataタブをGalleryへ統合 + PromptタブにTable表示を新設
- MetadataタブをGalleryタブのサブタブ(Style_Catalog右隣)へ移動
- GenerateUI: Batch/LabサブタブでSaveボタンを非表示化
- Promptタブ: Form/Table切替を追加。Tableには編集可能な
  Presets/Presets Group/Wildcards/Styleテーブルを新設
  - #列クリックで行選択→ツールバーDelete/Deselectで一括操作
  - 番号ダブルクリックでPositive/Negative編集モーダル
  - PresetsのB列/BCボタンでBatchグループ登録
  - PP/NP Copy + グループ選択でAdd to Group(Presets Groupの
    Add Preset選択式を置き換え)
- ヘルプ(index.html + i18n.js 3言語 + helpIdMap)とREADMEを更新

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 15:00:23 +09:00
ketle-man cbb096ec85 release: v0.4.5 — Lab Model列の無意味な空キーフレーム表示を解消(対象ノードなし時は非表示化)
対象ノード(Checkpoint/LoRA/VAE)がワークフローに存在しない列でも
Model列に常にSetting 1のキーフレーム行が表示されていた問題を修正。
Run時には何の効果も持たない死んだコントロールだったため、
_hasNodeForColumn()で判定し、対象ノードが無い列はプレースホルダー
表示にして+ボタンでの追加もブロックするようにした。
2026-08-21 20:27:05 +09:00
ketle-manandClaude Sonnet 5 99f342cc89 release: v0.4.4 — LoRAノードバイパス機能(Lab/GenerateUI Model) + Lab LoRA構文/トリガーワードのプロンプト適用 + AI TOOL Chat生成パラメータ拡張
- GenerateUI ModelタブのLoRA列にバイパストグルボタン(⛔)を追加(strength=0の簡易実装)
- Lab機能のLoRAキーフレームに「LoRAノードをバイパス」「LoRA構文/トリガーワードをポジティブプロンプトに適用」チェックボックスを追加
- AI TOOL Chatのgenerate_imageツールにsteps/cfg/sampler_name/scheduler/denoise/seed/width/height/batch_size/aspect_ratioを追加
- Resolution Selectorノード(ComfyUI組み込み)のaspect_ratio COMBO値対応
- ヘルプ(index.html/i18n.js 3言語/app.js helpIdMap)・README.md更新
- README/DEVLOG索引自動化(index.html PostToolUseフック + generate_doc_index.py)を導入

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 15:44:33 +09:00
ketle-manandClaude Sonnet 5 9681e76848 release: v0.4.3 — 共有モーダルの☆/リネーム残留バグ修正 + Lab Model列の+/-をCheckpoint/LoRA/VAEで完全独立化
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 23:44:26 +09:00
39 changed files with 92768 additions and 89172 deletions
+60
View File
@@ -0,0 +1,60 @@
"""PostToolUse hook: regenerate README.index.md / DEVLOG.index.md right after
Claude Code edits README.md or DEVLOG.md, so graphify's excluded-doc index
stand-ins (see .graphifyignore) never drift from the source files within a
session. A git pre-commit hook covers edits made outside Claude Code.
"""
from __future__ import annotations
import json
import os
import subprocess
import sys
from pathlib import Path
TARGETS = {"readme.md": "readme", "devlog.md": "devlog"}
def main() -> None:
try:
payload = json.load(sys.stdin)
except (json.JSONDecodeError, ValueError):
return
if payload.get("tool_name") not in ("Edit", "Write"):
return
file_path = payload.get("tool_input", {}).get("file_path", "")
if not file_path:
return
name = Path(file_path).name.lower()
target = TARGETS.get(name)
if target is None:
return
project_dir = os.environ.get("CLAUDE_PROJECT_DIR") or str(Path(file_path).resolve().parent)
script = Path(project_dir) / "tools" / "generate_doc_index.py"
if not script.exists():
return
result = subprocess.run(
[sys.executable, str(script), target],
cwd=project_dir,
capture_output=True,
text=True,
timeout=30,
)
if result.returncode == 0:
print(json.dumps({"systemMessage": f"[doc-index] {result.stdout.strip()}"}))
else:
print(
json.dumps(
{"systemMessage": f"[doc-index] generate_doc_index.py {target} failed: {result.stderr.strip()}"}
),
file=sys.stderr,
)
if __name__ == "__main__":
main()
+12
View File
@@ -19,6 +19,18 @@
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "python \"$CLAUDE_PROJECT_DIR/.claude/hooks/on_doc_edit.py\"",
"timeout": 30
}
]
}
]
}
}
+7
View File
@@ -1,3 +1,10 @@
# Private/unpublished planning notes — never committed to git, must not leak
# into the published graphify-out/ knowledge graph either.
QWEN_CHAT_INTEGRATION_PLAN.md
# Too large for reliable local-Ollama semantic extraction (large chunks make the
# model summarize in prose instead of emitting JSON, and time out on slower
# models/hardware). README.index.md / DEVLOG.index.md are condensed stand-ins
# that graphify extracts instead — regenerate them after editing the originals.
README.md
DEVLOG.md
+16
View File
@@ -200,6 +200,8 @@ os.environ["HF_HOME"] = "D:\\hf_cache"
`pyproject.toml` のバージョン変更が GitHub Actions のトリガー(`paths: pyproject.toml`)になっており、ComfyUI Registry への自動公開もここで行われる。バージョン更新を忘れると Registry に反映されない。
<!-- graphify運用は一時停止中(2026-08-23〜、ユーザー指示により凍結)。再開時はこのコメントブロックを解除すること。graphify-out/ 自体は削除しておらずそのまま残っている。
## graphify
This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships.
@@ -209,3 +211,17 @@ Rules:
- If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing.
- Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context.
- After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost).
### README.md / DEVLOG.md の索引ファイル
`README.md`/`DEVLOG.md`はサイズが大きすぎてローカルOllamaでの意味抽出が不安定になる(大きな塊を渡すとJSONではなく散文の要約を返す、または処理がタイムアウトする)ため、`.graphifyignore`でgraphifyの抽出対象から除外している。代わりに縮小版の`README.index.md`/`DEVLOG.index.md`を抽出対象にしている。
- 生成スクリプト: `python tools/generate_doc_index.py [readme|devlog|all]`
- 自動化: `README.md`/`DEVLOG.md`を編集すると以下の2つのフックが自動でindexを再生成する
- Claude Code PostToolUseフック(`.claude/hooks/on_doc_edit.py`)— Edit/Write直後に即時実行
- git pre-commitフック(`.git/hooks/pre-commit`)— コミット時にstagedファイルを検知して再生成し、同じコミットに含める(他エディタでの編集にも対応)
- 索引ファイルを手動で編集しない(次の生成で上書きされる)
- 索引ファイル再生成後は`graphify update .`を実行してグラフに反映する
-->
+165
View File
@@ -0,0 +1,165 @@
# DEVLOG Index — ComfyUI-Workflow-Studio
Condensed version-by-version index of DEVLOG.md (each line: version + one-line summary of what changed and why). Full rationale, code details, and "How to apply" lessons live in the excluded DEVLOG.md itself; this index exists so graphify can extract a queryable semantic node per release without feeding the full log to the local Ollama model. Regenerate with `python tools/generate_doc_index.py devlog` whenever DEVLOG.md changes.
- **v0.4.7** — 機能追加: Prompt Tab内部リファクタ + Gallery Promptタブ新設 + Send to Canvas改善 + Presetsソート
- **v0.4.6** — 機能追加: MetadataタブをGalleryタブへ統合 + Promptタブに一覧編集可能なTable表示を新設
- **v0.4.5** — 調査: トップバー保存ワークフローのRMBG不具合報告(原因なし) + バグ修正: LabタブModel列の無意味な空キーフレーム表示を解消
- **v0.4.4** — 機能追加: Lab/GenerateUI ModelタブへLoRAバイパス機能 + AI TOOL Chatの画像生成パラメータ拡張
- **v0.4.3** — バグ修正: 共有モーダルのお気に入り☆/リネームがLabモーダルへ残留 + Lab Model列の+/-をCheckpoint/LoRA/VAEで完全独立化
- **v0.4.2** — 機能追加: Lab「Model」列統合(Checkpoint / LoRA Single / VAE)+ フィールド単位継承 + C/L/V表示切替
- **v0.4.1** — UX改善: Mask Editor One連携ボタンを「選択中ノード優先」方式に再設計
- **v0.4.0** — リファクタリング: image-edit-tab.js / models-tab.js の低凝集度モジュール分割
- **v0.3.99** — 機能追加: AI TOOLタブ + サイドパネルAタブにUnslothバックエンド追加(APIキーは.env管理、SSRF脆弱性修正、reasoning_content対応)
- **v0.3.98** — 機能追加・バグ修正: AI TOOL翻訳の信頼性向上 + GenerateUI Modelタブ再構成(ハイライト色・折りたたみ・並び替え)+ Thinking mode/Max tokens設定
- **v0.3.97** — 機能追加・バグ修正: KREA-2ワークフロー対応 + Labタブ拡張(Clear/Get from Previous/Bypass/ライブ反映)+ Input Imageプレースホルダ機能
- **v0.3.96** — バグ修正: カタログ作成バッチが特定件数で無言停止する問題の修正 + Style Catalog/Promptタブ機能拡張
- **v0.3.95** — 機能追加: Style/Prompt→ImagePrompt改名 + Style_Catalogギャラリー新設 + GenerateUI「カタログ作成」機能
- **v0.3.94** — 機能追加: Gallery タブのサブタブ化 + Style/Prompt ギャラリー新設(視覚的プロンプトビルダー)
- **v0.3.93** — バグ修正・機能改善: Labタブ VAEキーフレーム不発バグの修正+Style適用UI刷新+Plan JSONスクロール同期修正+キーフレーム個別削除+Batch数超過警告+ワイルドカード対応+Save index image デフォルトON
- **v0.3.92** — バグ修正: CLIPTextEncodeEditPlus使用ワークフローがGenerateUI経由だと直接実行と異なる画像になる問題(根本原因はconvertUiToApiのForceInput 未考慮バグ)+LoRA Stack機能の改善一式
- **v0.3.91** — 機能拡張: Labインデックス画像にPlanメタデータをPNG埋め込み+README構成整理(Changelogセクション廃止)
- **v0.3.90** — 機能拡張: Labタブの進化(ワークフロー呼び出し・T2I対応・Run時インデックス画像Output保存+Eagle連携・プラン接頭辞)+Galleryタブのルート表示簡略化
- **v0.3.89** — バグ修正: v0.3.88で追加したLab機能への実動作確認フィードバック3件を反映
- **v0.3.88** — 機能拡張: Labタブ Promptセルにスタイル適用・画像/GenerateUIからのプロンプト取得、Checkpoint/VAEフィルター、Plan JSONサブタブを追加
- **v0.3.87** — 追加機能: GenerateUIタブに実験的な「Lab」サブタブを追加(I2Iバッチ生成・キーフレーム方式)
- **v0.3.86** — バグ修正: 画像を一括生成(I2I)等の連続実行で "GalleryMetadataStore: save error: dictionary changed size during iteration" が発生
- **v0.3.85** — 機能拡張: Generate UI連携ブリッジ(T2I)にデフォルトワークフロー・Negativeプロンプト対応を追加
- **v0.3.84** — バグ修正: ernie_t2i.jsonをGenerateUIタブで実行すると "Prompt outputs failed validation" エラー
- **v0.3.83** — 追加機能: Ernie Image(ernie-image / ernie-image-turbo)ワークフロー対応 + プロンプトがリンク経由のノードから供給されるパターンの修正
- **v0.3.82** — バグ修正: MetadataタブとサイドパネルIタブでLongCat/Boogu/HiDream E1/FireRed等のプロンプトが表示されない不具合
- **v0.3.81** — 追加機能: GenerateUIタブでFlux.2 Klein / LongCat / Boogu / HiDream E1 / FireRedのImage Editワークフローに対応
- **v0.3.80** — 追加機能: Comic Creator「半自動マンガ作成」向けブリッジ(LLMプロンプト下書き・バッチ画像生成)
- **v0.3.79** — 追加機能: サイドパネル(Workflow Studio Library)IタブのMage-Flow等サブグラフワークフロー対応
- **v0.3.78** — 追加機能: Eagle自動保存のSVG対応
- **v0.3.76** — 追加機能: Comic Creator向けI2I実行ブリッジ`_wfmReceiveI2IRunRequest`
- **v0.3.77** — 追加機能: GalleryタブでSVGファイルの表示・管理に対応
- **v0.3.75** — Lemonadeバックエンド追加(3つ目のAIバックエンド)
- **v0.3.74** — AI TOOL Chatペインの画像添付対応 + I2I連携
- **v0.3.73** — AI TOOL Chatペインからの対話式画像生成(Tool Calling)+ Checkpoint関連の安全策
- **v0.3.72** — Comic Creator連携用の外部Inpaintエントリポイントを追加
- **v0.3.71** — Inpaint機能の追加(GenerateUI Imageタブ + Image Edit「Inpaint」タブ)
- **v0.3.70** — Workflow AI要約: Ollama 404エラー修正
- **v0.3.69** — Gallery → ComfyUI Comic Creator 連携(Send CC ボタン)
- **v0.3.68** — MASK EDITOR ONE セクション常時表示化
- **v0.3.67** — Alt+Click Apply & Generate
- **計画: Image Edit 拡張 — ABRブラシ連携 + Mask Editor One 統合** — フェーズ1: Draw ツール — ABRブラシ対応
- **2026-06-29: v0.3.66 — Mask ツール 5サブツール追加(Color / Alpha / Text / Vector / Shape)・Support リンク追加** — 概要
- **2026-06-29: v0.3.65 — Mask Editor One 連携修正・トップバーアイコン修正・Mask ABRブラシ対応** — 概要
- **2026-06-28: v0.3.64 — Image Edit マスク Add/Subtract・BiRefNet BG Remove・SAM3 セグメンテーション・ブラシカーソルバグ修正** — 概要
- **2026-06-28: v0.3.63 — ノードタブ複数選択・全タブフィルタークリア・検索Xボタン** — 概要
- **2026-06-27: WFS_PromptText の GenerateUI 対応・ドロップ時 title 固定** — 概要
- **2026-06-27: v0.3.62 — Send GenUI Image ボタン追加・tagger_settings.json バグ修正・README 更新** — 概要
- **2026-06-27: データ管理バグ修正 — tagger_settings.json をエクスポート対象に追加** — 概要
- **2026-06-27: v0.3.61 — G'MIC バグ修正・セキュリティ・Draw/Mask ブラシ操作性改善** — 概要
- **2026-06-27: v0.3.60 — Image Edit Filter Tool (G'MIC-Qt 連携) 実装** — 概要
- **2026-06-27: v0.3.59 — Image Edit Mask Tool 実装・Draw/Mask 描画バグ修正・クリッピング保存対応** — 概要
- **2026-06-27: v0.3.58 — GenerateUI Model タブ GGUF対応拡張(Diffusion Model / Text Encoder)** — 概要
- **2026-06-26: v0.3.57 — Image Edit Blur Tool / BG Remove Tool 追加** — 概要
- **2026-06-26: v0.3.56 — Style機能追加(GenerateUI ツールバー & Batchタブ)** — 概要
- **2026-06-25: v0.3.55 — グループ機能バグ修正(全タブ)** — 根本原因(データ消失): パス区切り文字の不一致
- **2026-06-25: v0.3.54 — Image Edit Shape ツール追加・Models タブ overlay バグ修正** — Image Edit Tab — Shape ツール(□ / S キー)
- **2026-06-25: v0.3.53 — Image Edit Tab 実装・レイヤーロック・ギャラリー保存** — Image Edit Tab(新規タブ)
- **2026-06-25: GenUI設定タブ LATENT IMAGE にプリセット機能を追加** — 変更内容
- **2026-06-24: GenUI ModelでLoRA適用後、Modelサブタブ再表示時に値がリセットされるバグ修正** — 問題
- **2026-06-24: v0.3.51 — Embedding GenUI PP/NP対応 + GenerateUIプロンプトタブEmbeddingセレクタ追加** — 変更内容
- **2026-06-24: v0.3.51 — GenerateUIタブ Hypernetwork対応 + ModelsタブGenUI Modelボタン追加** — 変更内容
- **2026-06-24: v0.3.51 — Hypernetwork/Embeddingモデル未表示バグ修正** — 原因
- **2026-06-24: v0.3.51 — ModelsタブサイドパネルにGenUI Modelボタン追加** — 変更内容
- **2026-06-22: v0.3.50 — ギャラリータブ プロンプト検索負荷改善(バックグラウンドインデックス)** — 問題
- **2026-06-22: v0.3.49 — 生成UIタブ A1111風ワイルドカード展開・ギャラリータブ バグ修正2件** — 生成UIタブ — A1111風SPAサイドワイルドカード展開
- **2026-06-22: v0.3.48 — ギャラリータブ 画像ダウンロード・一括エクスポート機能追加** — 詳細パネルの画像プレビューにダウンロード機能を追加
- **2026-06-22: v0.3.47 — モデルタブ ファイル移動後グループ状態修正** — モデル移動後のグループ状態不整合を修正
- **2026-06-21: v0.3.46 — プロンプトタブ AI Assistant LM Studio対応・設定タブ AI Assistant設定に変更** — 設定タブ「AI Assistant設定(プロンプトタブ)」
- **2026-06-21: v0.3.45 — グループ孤立エントリ自動クリーンアップ・Galleryタブ Shift+クリック範囲選択** — グループ孤立エントリ自動クリーンアップ
- **2026-06-21: v0.3.44 — Galleryタブ高速化・比較モード・バルク強化・バルクバーUI整理** — フェーズ2-A: サーバーサイドサムネイル生成
- **2026-06-21: ヘルプタブ改善 — Gallery mode 多言語対応・フォント拡大・検索機能追加** — Gallery mode の多言語対応
- **2026-06-21: Send to Canvas — window.opener経由のキャンバス直接ロード対応**
- **2026-06-20: API形式ワークフローのキャンバス読み込み修正・画像メタデータ埋め込み修正** — API形式ワークフローをキャンバスへドラッグすると空になる問題を修正
- **2026-06-20: セキュリティ修正(XSS・パスバリデーション)** — 修正内容
- **2026-06-20: WFS_GalleryFeeder — ComfyUI キャンバス上コントロール追加** — 新規ファイル(`web/comfyui/gallery_feeder_extension.js`)
- **2026-06-20: __Feeder__ グループ保護 + サムネイル F ボタン + seed バグ修正** — `__Feeder__` グループの予約済み保護
- **2026-06-20: Gallery Feeder 機能追加(WFS_GalleryFeeder ノード)** — 概要
- **2026-06-19: Generate UI — ImpactWildcardEncode/Processor プロンプト検出バグ修正(v0.3.41)** — 修正内容(`static/js/comfyui-workflow.js`)
- **2026-06-19: AI TOOL タブ Chat ペイン・ワイルドカード生成追加(v0.3.40)** — Chat ペイン(`templates/index.html`, `static/css/main.css`, `static/js/ai-tab.js`, `web/comfyui/node_sets_menu.js`)
- **2026-06-18: Send to Canvas機能追加(ワークフロータブ・ギャラリータブ→LibraryタイトルDnD)** — ワークフロータブ — Send to Canvas(`static/js/workflow-tab.js`, `static/js/app.js`, `templates/index.html`)
- **2026-06-15: TaggerシングルGenUI:Pボタン追加+生成UIのZITワークフロープロンプト解析修正** — TaggerシングルGenUI:Pボタン(`templates/index.html`, `static/js/tagger-tab.js`, `static/js/i18n.js`)
- **2026-06-15: Taggerバッチ.txt出力追加+Galleryタグ保存バグ修正** — Taggerバッチ.txt出力(`templates/index.html`, `static/js/tagger-tab.js`, `static/js/i18n.js`, `py/routes/tagger_routes.py`, `py/services/tagger_service.py`)
- **2026-06-15: Taggerタブ新規実装+ギャラリー詳細パネルUI改善+パストラバーサル修正** — Taggerタブ新規実装
- **2026-06-15: 全選択ボタン追加+ギャラリーバルクバーi18n対応** — 変更内容
- **2026-06-10: v0.3.36 — コードレビュー修正+リファクタリング+README刷新** — コードレビュー修正(v0.3.35の不具合10件)
- **2026-06-10: v0.3.35 — テーブルソート・Load GenUI・ワークフロー保存** — 変更内容
- **2026-06-09: v0.3.34 — Modelsタブ テーブルビュー強化・バルク操作改善** — 変更内容
- **2026-06-09: v0.3.33 — LoRAペイン・ワークフロー解析バグ修正** — 変更内容
- **2026-06-08: v0.3.32 — Lora Loader (LoraManager) LoRA検出対応・ライブラリ機能拡張** — 変更内容
- **2026-06-08: v0.3.31 — LoRAペイン Single/Stack タブ分割・GenUI Model LoRA対応強化** — 変更内容
- **2026-06-08: v0.3.30 — LoRAスタック トリガーワードバグ修正・ヘルプ更新** — 変更内容
- **2026-06-08: v0.3.29 — LoRAセクション統合・ヘルプタブサイドバー・UI細部改善** — 変更内容
- **2026-06-08: 生成UIタブ 複数改善 — LoRA Stackトリガーワード連携・Reset Workflowボタン・Inputタブタブ化** — 変更内容
- **2026-06-08: Feederタブ UI改善 — (root)自動選択・PREVIEWペイン右移動・RUN中プレビュー反映** — 変更内容
- **2026-06-07: v0.3.28 — Batchタブ Sampler/Scheduler対応 + Modelsサムネイルバグ修正** — 概要
- **2026-06-07: v0.3.27 — model-and-prompt-from-metadata カスタムノード対応 + README修正** — 概要
- **2026-06-07: v0.3.26 — Stack LORA SYNTAX 表示修正 + 全体強度調整 UI** — 概要
- **2026-06-07: v0.3.25 — Lora Loader (LoraManager) 対応 + Stack 有効無効切り替え** — 概要
- **2026-06-06: v0.3.24 — Lora Stack 登録機能 + 生成UI Model タブ 3ペイン化** — 概要
- **2026-06-05: v0.3.23 — バグ修正2件** — バグ修正
- **2026-06-05: v0.3.22 — カードビュー廃止 + Batch タイプ切り替え UI + 4タイプバッチ生成** — 概要
- **2026-06-05: (未リリース) — 生成UIタブ Batch 4タイプ対応(Lora/Prompt/Workflow グループ選択 + BATCH QUEUE 4列化)** — 概要
- **2026-06-05: (未リリース) — Prompt・Workflow Batchグループ登録UI** — 概要
- **2026-06-04: (未リリース) — モデルBatchグループ登録UI(サムネイル/カード/テーブル・フィルター・一括解除)** — 概要
- **2026-06-04: (未リリース) — Batch予約グループ・Presets Save後リセット・グループフィルター種別絞り込み** — 概要
- **2026-05-31: v0.3.21 — CivitAI ホスト設定・Sample サブタブ・URL 修正・ヘルプ i18n 整備** — 概要
- **2026-05-31: v0.3.20 — CivitAI 詳細パネル強化(Type / Hash 表示・URL 修正・画像別タブ)** — 概要
- **2026-05-31: v0.3.19 — CivitAI 連携強化(バッチ高速化・リトライ・APIキー・フィールド拡充)** — 概要
- **2026-05-31: v0.3.18 — Batch タブ追加・Models 複数選択強化** — 概要
- **2026-05-31: v0.3.17 patch — SSLコンテキスト セキュリティ強化** — 概要
- **2026-05-30: v0.3.17 — SSL証明書エラー修正・Lora Manager対応** — 概要
- **2026-05-20: v0.3.16 — CivitAIプレビューフォールバック表示** — 概要
- **2026-05-20: v0.3.15 — サンプルワークフロー同梱・CivitAI自動プレビュー・Create Tagsオプション** — 概要
- **2026-05-20: AI TOOL タブ 英語化(SPA・Libraryパネル)** — 概要
- **2026-05-19: v0.3.14 — AI TOOL タブ 3ペイン化・RAW JSON 文字色カスタマイズ** — 概要
- **2026-05-19: v0.3.13 — AI タブ(A)追加** — 概要
- **2026-05-18: v0.3.12 リリース — README スクリーンショット刷新 / workflow_analyzer 拡張** — 概要
- **2026-05-17: RAW JSON パネルに検索機能を追加** — 概要
- **2026-05-17: v0.3.11 Metadata タブ対応ノードタイプ拡張 / Settings テキストサイズ UI 追加** — 概要
- **2026-05-17: v0.3.10 Information タブ キャンバスドラッグ対応 / ノードタイプ拡張 / バグ修正** — 概要
- **2026-05-16: v0.3.9 サイドパネル I タブ追加 / トップバーアイコン修正** — 概要
- **2026-05-16: v0.3.8 Metadata タブ: data9 対応 / プロンプト判別不能テキスト** — 概要
- **2026-05-15: v0.3.7 Metadata タブ: Flux2 / Qwen / Z-Image 対応** — 概要
- **2026-05-15: v0.3.6 Metadata タブ追加** — 概要
- **2026-05-09: v0.3.5 GenerateUI — Feeder サブタブ・ワークフロー解析精度向上** — 概要
- **2026-04-29: v0.3.4 GenerateUI — Checkpoint Batch 刷新・Settings 横並びレイアウト・ヘルプ i18n 修正** — 概要
- **2026-04-28: v0.3.3 Promptタブ — ワイルドカード支援パネル・Impact Packシンボリックリンク連携・サブディレクトリ対応・GenerateUI修正** — 概要
- **2026-04-28: v0.3.2 生成UIタブ — Checkpoint Batch・Seed UIレイアウト修正** — 概要
- **2026-04-27: v0.3.1 ギャラリー拡張(フォルダ/ファイル操作・ワークフロー保存)** — 概要
- **2026-04-18: v0.3.0 モデルグループ表示改善(全タイプ横断・サイドパネル修正)** — 概要
- **2026-04-18: v0.2.9 モデル有効/無効・複数選択・一括削除** — 概要
- **2026-04-14: v0.2.8 データ保存先変更・エクスポート/インポート機能追加** — 概要
- **2026-04-06: v0.2.6 UI改善・バッジ保存修正・フォルダフィルター追加** — 概要
- **2026-04-04: v0.2.5 追加修正** — 概要
- **2026-04-03: v0.2.5 サイドパネル Category/Package サブタブ・テーマ設定・Workflow修正** — 概要
- **2026-03-29: v0.2.4 GenerateUIタブ レイアウト再設計** — 概要
- **2026-03-29: v0.2.3 バッジ統一・GenUI Model・サイドパネルModels拡充** — 概要
- **2026-03-28: v0.2.2 Modelsタブ・CivitAI連携** — 概要
- **2026-03-27: テーマカスタマイズ機能・設定タブ改善** — 概要
- **2026-03-26: トップバーボタンアイコン修正・Appモードバッジ調査** — 概要
- **2026-03-25: v0.2.0 プロンプトプリセット機能・Workflow Studio Library** — 概要
- **2026-03-24: v0.1.9 サイドパネルUI改善・保存ダイアログ・バグ修正** — 概要
- **2026-03-24: v0.1.8 WF & Node Library サイドパネル拡張** — 概要
- **2026-03-23: v0.1.7 Nodes タブ追加** — 概要
- **2026-03-23: v0.1.6 パストラバーサル脆弱性修正** — 概要
- **2026-03-22: v0.1.5 テーマシステム追加** — 概要
- **2026-03-22: v0.1.4 App形式対応・プリセットコピー・分析バグ修正** — 概要
- **2026-03-21: v0.1.3 ヘルプ&サポートタブ追加** — 概要
- **2026-03-19: v0.1.2 リリース** — 概要
- **2026-03-19: v0.1.1 リリース** — 概要
- **2026-03-18: プロジェクトリネーム & アイコン適用** — 概要
- **2026-03-18: デフォルトビュー設定追加** — 概要
- **2026-03-18: MarkdownNote表示専用ノード除外** — 概要
- **2026-03-18: サブグラフワークフロー対応 & 変換精度修正** — 概要
- **2026-03-18: Raw JSON同期修正** — 概要
- **プロジェクト構成 (現在)**
- **2026-03-18: JSONシンタックスハイライト** — 概要
- **2026-03-18: プロンプトタブ 中国語↔英語翻訳追加** — 概要
- **2026-03-18: v0.1.0 リリース** — 概要
- **2026-04-13: Gallery タブ追加 & 全タブ Group UI 統一** — 概要
+149 -2
View File
@@ -1,9 +1,156 @@
異性
# DEVLOG - ComfyUI-Workflow-Studio
---
## v0.4.7
### 機能追加: Prompt Tab内部リファクタ + Gallery Promptタブ新設 + Send to Canvas改善 + Presetsソート
ユーザーからの複数の要望を1セッションで順に実装した一連の作業。
**1. graphifyでの掘り下げを起点にprompt-tab.jsを分割**: graphifyの知識グラフでコードベースを掘り下げた結果、`prompt-tab.js`(1545行)がPreset管理・AI Chat・Wildcard管理・Style管理の4つの独立したクラスタで構成されていることが判明。`prompt-presets.js`/`prompt-ai-chat.js`/`prompt-wildcards.js`/`prompt-styles.js`の4ファイルに分割し、`prompt-tab.js`自体は各モジュールを結線するだけの薄いオーケストレーターにした。`prompt-table.js`/`app.js`は無変更(`prompt-tab.js`からのre-export経由で既存のimportをそのまま維持)。
**2. テーブルタブのWildcard入力支援ツールバー、実は配置ミス**: 「テーブルタブのワイルドカードのファイルがtxtの場合、モーダルにフォームの入力支援ボタンを表示したい」との要望を受け実装したが、直後にユーザーから「ワイルドカード構文はワイルドカードのモーダルに追加するのは誤りでプロンプトに追加するのが正しい気がします」との指摘。Form側の既存設計(構文ボタンはWildcard Composerにのみ存在し、ファイル中身のエディタには無い — ファイルの中身は単なる候補リストという前提)に照らして誤りだったと確認し、Presetsのプロンプト編集モーダルへ移設した。ワイルドカードツールバーは`prompt-wildcards.js`の`createWildcardToolbar()`として再利用可能な形に切り出し、Form側の静的ツールバーとテーブル側の動的モーダルの両方から使えるようにした。Styleモーダルには適用しない(Form側でもWildcard ComposerとStyleの連携導線は存在しないため)。
**3. Style×Wildcard展開順序のバグ修正**: 「スタイルにワイルドカード構文を使用しても使用時プロンプトに挿入されるので動作しますか?」との確認依頼から調査を開始。`generate-tab.js`の`_coreGenerate`内でワイルドカード展開(`_expandWildcardsInWorkflow`)がStyle適用(`_applyStyleToWorkflow`)より*先*に実行されていたため、Style文字列内に書いた`__file__`参照が展開されないまま生テキストのままComfyUIへ送信される不具合を発見。実行順序をStyle適用→ワイルドカード展開に入れ替えて修正。実際にComfyUIで生成を実行し、`comfyUI.generate`を一時的にモンキーパッチして送信直前のペイロードを確認、Style由来の`__flower__`が`"flower, gardenia scent"`のように正しく展開されることを実証した。
**4. yamlワイルドカードを使うサンプルワークフロー**: 「yamlファイルのワイルドカードを使用するワークフローを作成してもらうことはできますか」との依頼。調査の結果、このプラグイン自体のワイルドカード展開(SPA側の`_expandWildcardsInWorkflow`)は`.txt`拡張子固定でyaml非対応であり、yaml形式は`ImpactWildcardEncode`/`ImpactWildcardProcessor`ノード(Impact Pack、サーバー側処理)でのみ解決できることが判明。プラグインのwildcardディレクトリはjunction経由でImpact Packの`wildcards/`フォルダへ直結しているため、通常のWildcard File Manager経由で保存したyamlファイルがそのままImpactWildcardEncodeから参照可能。サンプルyamlワイルドカードファイル(`ws_sample_season_lighting.yaml`)とImpactWildcardEncodeノードを使うサンプルワークフロー(`ws_yaml_wildcard_sample.json`)を作成し、実際にComfyUIで生成→出力PNGのメタデータで`populated_text`が階層キー参照から正しく展開されていることを確認した。
**5. Galleryタブ詳細パネルに「Prompt」タブ新設**: 「メタデータでも確認できるが頻度を考えるとマウス操作が多いため、プロンプトだけはこちらでも表示としたい。ボタンは右端でデフォルトで選択状態としたい」との要望。当初は単純な全文連結表示で実装したが、「メタデータタブの表示の様に複数プロンプトがあるケースも考慮したい」とのフィードバックを受け、Metadataタブの`extractPrompts`/`buildPromptItem`をexportし、完全に同じPOS/NEGバッジ付きリスト→クリックで全文表示のUIに作り直した。
- さらに実機確認の中で、同一画像でもMetadataタブとGalleryタブでpositive数が食い違うケース(`Krea2_turbo_00133_.png`、Metadataでは2件のPositiveが見える)を発見。原因は埋め込みデータの優先順位の違い — Metadataタブは`prompt`(API形式、サブグラフ展開済みでフラット)を優先するのに対し、Gallery側サーバーAPIは`workflow`(UI形式、ノード位置等を保持)を優先していた。トップレベルとサブグラフに独立した2系統のサンプリングを持つワークフローでは、UI形式のトップレベルnodes配列に一方の系統しか含まれず、`extractPromptsLiteGraph`の早期returnロジックがその系統しか拾えないため。既存のLoad GenUI/JSON表示/Copy & Send Canvas等には一切影響しないよう、`gallery_service.py`に優先順位を選べる`_extract_embedded_workflow`ヘルパーを追加し、新規`extract_prompt_workflow_from_metadata`(API形式優先)を`/wfm/gallery/image/workflow`のレスポンスへ`prompt_workflow`として追加、Galleryの Promptタブの抽出だけこちらを使うようにした。Python側の変更を反映するため、ユーザー自身の手でComfyUIを再起動していただいた。
**6. Send to Canvasのフォールバック方式を刷新**: 「このワークフローを生成UIタブに読み込むと...またキャンバスへ送るでAPI形式のためタイトルをキャンバスへドラッグのメッセージが表示されますがこの動作を変更したい...UI形式のサムネイルがないパターンのワークフローも同様にしたい」との要望。`window.opener`経由の直接ロードに失敗した場合の従来のフォールバック(`localStorage`+パネルタイトルのドラッグ、API形式のワークフローはそもそも非対応でエラーのみ)を、Workflow Studio LibraryのW(Workflows)タブを該当ワークフローで絞り込み表示する方式に統一した。`node_sets_menu.js`に`showWorkflowInLibrary()`をexportし、`top_menu_extension.js`が新規localStorageキー`wfm_library_filter_request`への`storage`イベントを監視して呼び出す(同一オリジンの別タブでのみ発火する仕様を利用)。フォーマット(UI形式・API形式)を問わず同じ導線になり、サムネイル付きの実物のワークフローカードからユーザー自身がドラッグできるようになった。Galleryタブの「Copy & Send Canvas」(画像埋め込みのその場限りのワークフローが対象で、そもそもLibrary一覧=保存済みファイルには存在しない)は今回のスコープ外として変更していない。
**7. Presetsテーブルに★/Bソート機能**: 「プロンプトタブのテーブル(プリセット)で上部メニューに★とBボタンによるソート機能を追加したい」との要望。ツールバーに★(favorite)/B(Batch選択済み)のトグルボタンを追加し、該当する行を先頭にソートする機能を実装。両方同時にONにでき、★を主キー・Bを副キーとした複合ソートに対応、該当しない項目同士は安定ソートで元の相対順序を維持する。ソート有効時に行内の★/Bボタンで状態を変更すると即座に再ソートされる。
**8. graphifyの運用を一時停止**: ユーザー指示によりCLAUDE.md内の「graphify」セクション全体をコメントアウトし、明示的な指示があるまでコード変更後の`graphify update`は実行しないことにした(`graphify-out/`自体は削除せず保持、再開時にコメントを解除するだけで元に戻せる)。
**ヘルプ・README更新**: `project_v0336_conventions`の慣習(index.html + i18n.js 3言語 + app.jsのhelpIdMapの3点セット)に従い、Prompt Tab(Table Viewカードに★/Bソートボタンの新規項目)を更新。README.mdはGenerateUI Tab(Style×Wildcard展開順序修正)、Prompt Tab(Presetsモーダルのワイルドカードツールバー、★/Bソート)、Gallery Tab(新規Promptタブ)、Workflow Studio Library(Send to CanvasのWタブフィルタ方式)を更新。
**検証**: 全JS/Pythonファイルを`node --check`/`ast.parse()`で構文確認。開発元gitリポジトリと実行時`custom_nodes`フォルダは別実体のため([[project_dev_deploy_sync]]参照)、変更ファイルを都度両フォルダへ同期。Playwright MCP経由で実機ブラウザ操作を機能追加・修正ごとに実施: (1) prompt-tab.js分割後のPreset/AI Chat/Wildcard/Styleパネル全機能の回帰確認、(2) Wildcardツールバー移設前後でのモーダル表示・非表示の確認、(3) Style×Wildcard展開の実生成確認、(4) yamlサンプルワークフローの実生成とメタデータ確認、(5) Gallery Promptタブの複数プロンプト表示とprompt_workflow切替の確認、(6) Send to Canvasの別タブ間storage連携の実機確認、(7) Presetsテーブルの★/Bソート・複合ソート・安定ソートの確認。テスト用に作成したデータ(一時Style、yamlワイルドカードファイル等)はテストワークフロー用途のものを除き削除・復元済み。
**How to apply**: 開発時は `graphify query "<question>"` を使わず通常の Grep/Read/Explore で調査する(graphify運用停止中、明示的な再開指示があるまで)。ワイルドカード構文(`{|}`, `__|__`, `<lora>`等)は「プロンプトを書く場所」に対する入力支援であり、「ファイルの中身を編集する場所」には付けない、という区別を今後の入力支援UI追加でも踏襲する。ワークフローの実行順序に依存するバグ(Style適用とワイルドカード展開の順序等)は、実際にComfyUIへ送信される直前のペイロードをモンキーパッチや`/history`のPNGメタデータで確認するのが最も確実な検証方法。
---
## v0.4.6
### 機能追加: MetadataタブをGalleryタブへ統合 + Promptタブに一覧編集可能なTable表示を新設
ユーザーからの複数の要望を1セッションで順に実装した一連の作業。
**1. MetadataタブをGalleryタブのサブタブへ移動**: 「Metadataタブをギャラリータブ内に移動したい。Style_Catalogタブ右隣としたい」との要望。トップナビの独立タブを廃止し、Galleryタブのサブタブ列(Output/ImagePrompt/Style_Catalog)の右端に追加。外部から画像をMetadata表示させる`loadFileIntoMetadataTab()`(`metadata-tab.js`)や、Helpタブの「Open tab →」ジャンプボタンなど、旧トップレベルタブへの参照箇所(Gallery側の「Open in Metadata Tab」ボタン、Style_Catalog側の同名ボタン)はすべて「Galleryタブへ切替→Metadataサブタブへ切替」の2段階クリックへ置き換えた。
**2. GenerateUIタブ: Save ボタンをBatch/Labサブタブで非表示化**: 「ワークフローのSaveボタンをBatch、Labサブタブ選択時、非表示にしたい」との要望。両サブタブは現在ロード中のワークフロー本体ではなく、ワークフローの複製やLab独自のPlanファイルを操作するため、Saveボタンが誤解を招く(クリックしても実際に触っているワークフローとは別物が保存される)ことへの対処。サブタブ切替ハンドラ(`generate-tab.js`)にワンライナーの表示切替を追加。
**3. Promptタブに「Table」表示を新設**: 「現在のプロンプトタブをタブ化してテーブルタブを追加したい(既存はフォームタブとする)。テーブルタブではプロンプトのプリセット、ワイルドカード、スタイルがそれぞれ表形式で一覧表示、編集可能としたい」との要望を起点に、複数回のフィードバックを経て以下の形に発展した:
- 既存の3カラムレイアウト(AI Assistant / Presets・Preset Manager / Wildcard・Style)をまるごと「Form」パネルとしてラップし、新設の「Table」パネルと上部のトップレベルタブで切り替える構成にした。
- Tableパネル内にPresets・Wildcards・Styleの3つの編集可能テーブルを新設(`prompt-table.js`新規ファイル)。既存の`prompt-tab.js`が保持するAPIヘルパー(`fetchPresets`/`apiUpdatePreset`/`wcSaveFile`/`styleApiUpdate`等)とリフレッシュ関数を`export`して再利用し、Table側での編集は即座にForm側にも反映されるようにした。
- **「Presets右隣にPresets Groupを追加したい」**: グループ管理専用の4つ目のサブタブを新設。グループの作成・リネーム・削除・メンバー一覧(バッジ表示・×で削除)に対応。
- **「共通で左端に番号(自動)列を追加したい。個別の削除ボタンは誤操作が予想されるため上部にボタンを設置し番号列で該当番号選択、削除という手順にしたい」**: 4テーブル共通で自動採番の`#`列を新設し、番号クリックで行選択(複数可・赤ハイライト)→ツールバーの「Delete」ボタンで一括削除、という手順に統一。個別の行削除ボタンは全廃した。新規行追加時は番号セルの代わりに小さな✓(Add)/✕(Cancel)アイコンを表示する設計にした。
- **「バッチグループの登録方法を変更したい」**: PresetsテーブルにForm側と同じ仕組みの「B」列(クリックでグレー⇔黄色にトグルしBatchグループへ登録/解除)と、Batchグループを一括クリアする「BC」ボタンをツールバー上部に追加。Presets Groupテーブルからは予約グループ「Batch」を非表示にし、Batch管理をPresetsテーブル側のB列/BCボタンへ一本化した。
- **「PresetsのコピーボタンとグループドロップダウンをテーブルのPresetsタブに配置したい。これによりPresets GroupタブのAdd Presetsをなくしたい(プリセットが多くなった際に選択が困難なため)」**: Presets Groupテーブルの「Add Preset」列(プリセットが多いと使いづらい長大なドロップダウン)を廃止。代わりにPresetsテーブルのツールバーに「PP Copy」「NP Copy」(選択中1件のPositive/Negativeをクリップボードへコピー)と、グループ選択+「Add to Group」(選択中の複数プリセットを一括で選んだグループへ追加)を新設した。プリセット側を検索・選択してから短いグループ一覧から選ぶ流れにしたことで、プリセット数が多くても操作しやすい設計になった。
- **「選択クリアボタンも追加したい」**: 4テーブル共通で「Deselect」ボタンをツールバーへ追加し、選択状態を一括解除できるようにした。
- **「プリセット、ワイルドカード、スタイルで番号をWクリックでモーダルでプロンプトの表示、編集したい」**: `#`列のダブルクリックで、既存の共有モーダル基盤(`app.js`の`openModal`/`closeModal`)を再利用したプロンプト編集モーダルを開くようにした。Presets/Styleは正負プロンプト切り替えボタン+共有の大型テキストエリア(切り替えても両方の編集内容を保持)、Wildcardsはファイル内容のみのテキストエリアで、Save/Closeボタンを備える。
**ヘルプ・README更新**: `project_v0336_conventions`の慣習(index.html + i18n.js 3言語 + app.jsのhelpIdMapの3点セット)に従い、GenerateUI Tab(Saveボタン非表示の説明)・Prompt Tab(新規カード「Form / Table」「Table View」10項目)・Gallery Tab(Metadataサブタブの説明)・Metadata Tab(Galleryタブ内へ移動した旨の注記、`metadata-tab.js`側の独自i18n適用ロジックへ追加)を更新。README.mdはGenerateUI TabのSaveボタン説明を拡張、Prompt Tabセクションに新規ブロックを追加、「Metadata Tab」セクションをGalleryタブの4番目のサブタブとして「Metadata Gallery subtab」に改称の上、Style_Catalog Gallery subtabの直後へ移設した。
**検証**: 全JSファイルを`node --check`で構文確認。開発元gitリポジトリと実行時`custom_nodes`フォルダは別実体のため([[project_dev_deploy_sync]]参照)、変更ファイルを都度両フォルダへ同期。Playwright MCP経由で実機ブラウザ操作を機能追加ごとに実施: (1) Gallery→Metadataサブタブの表示とHelpジャンプボタンの遷移、(2) Batch/LabサブタブでのSaveボタン非表示、(3) Presets/Wildcards/Styleテーブルの実データ表示・インライン編集の永続化・行追加・削除、(4) 番号列での選択トグルとDelete/Deselectボタンの有効化、(5) Presets GroupタブでのグループCRUD・メンバー追加削除・予約グループBatchの削除保護、(6) B列/BCボタンでのBatchグループ登録・一括クリア(既存の"pony"データが誤って変更されないことも確認)、(7) PP/NP CopyとAdd to Groupの単一選択/複数選択それぞれの動作、(8) ダブルクリックモーダルの表示・Positive/Negative切り替え時のテキスト保持・保存→サーバー反映→モーダル自動クローズ、(9) ヘルプ全項目の日本語UIでの表示、を一通り確認。テスト用に作成したデータはすべて削除・復元済み。
**How to apply**:
1. 1つの機能要望(テーブル表示の追加)が実装後の実運用フィードバックを経て、削除方式・グループ登録方式・選択解除・詳細表示方式と段階的に発展していくケースでは、各段階で「既存の確立済みパターン(今回は#列クリック選択→ツールバー操作、共有モーダル基盤)を後続の要望にも一貫して適用できないか」を優先的に検討すると、機能ごとにUIパターンがバラつかず学習コストが低いまま拡張できる。
2. 一覧が長大になりうるデータ(プリセット)をドロップダウンで選ばせるUIは、件数が増えると事実上使えなくなる。件数が少ないことが保証されている別の軸(今回はグループ)を主語にした選択UIへ倒すと、データ量に強い設計になる。
3. 複数テーブルにまたがる共通パターン(番号列・選択トグル・ダブルクリックモーダル)は、テーブルごとに実装をコピーするのではなく共通ヘルパー関数(`buildNumCell`/`openPositiveNegativeModal`/`openSingleTextModal`)に切り出しておくと、後続の類似要望(今回で言えば選択クリアボタン・ダブルクリックモーダル)を全テーブルへ一度に反映しやすい。
関連: [[project_dev_deploy_sync]], [[project_v0336_conventions]]
---
## v0.4.5
### 調査: トップバー保存ワークフローのRMBG不具合報告(原因なし) + バグ修正: LabタブModel列の無意味な空キーフレーム表示を解消
ユーザーから「トップバーで保存した2つのワークフロー(I2I_rmbg.json/I2I_rmbg_2.json)を生成UIタブで読み込んで生成するとRemove Background(RMBG)ノードが機能せず透過画像が作成されない。ComfyUIのExport(API)で保存した3つ目のファイルは正しく動く」との報告を受けて調査した一連のセッション。
**1. ワークフロー解析の調査**: RMBGノードは`background_color`という「ウィジェットを持たない特殊な入力(COLORCODE型、`model`より前の位置)」を持つ特徴的な構造で、UI形式→API形式変換時に`widgets_values`の対応がずれる可能性を最初に疑った。`comfyui-workflow.js`の`convertUiToApi()`を段階的に検証:
- `/object_info/RMBG`を実機から取得し、`_getWidgetInputNames()`が`forceInput`相当(widgetキーを持たない入力)を正しく除外し、`background_color`がウィジェット名リストから漏れることを確認。
- ユーザー提供の`I2I_rmbg_2.json`をブラウザ内(Playwright MCP)で実際に`convertUiToApi()`にかけ、変換結果を確認 — `model`/`sensitivity`/`background`など全フィールドが正しくマッピングされていた。
- その変換結果を実際に`/prompt`へキューイングして実行、さらにGenerateUIタブの本物のGenerateボタンをクリックする経路でも実行 — いずれも出力PNGはRGBA・アルファチャンネル0〜255の**正しい透過画像**だった。
- 結論: 現在デプロイされているv0.4.3のワークフロー解析ロジックに該当バグは再現せず、コード変更は行わなかった。報告時点でより古いバージョンを使っていた可能性が高いと判断し、ユーザーに調査結果と再検証手順を報告した。
**2. Labタブ: モデルの無いワークフローで無意味なキーフレームが作成される問題**: 調査の副産物として、RMBGのみ(Checkpoint/LoRA/VAEいずれのノードも無い)ワークフローをLabタブで開くと、Model列に「1: (ワークフローの設定のまま)」という空のキーフレーム行が常に表示されてしまうことを実機で発見。ユーザーから「不要なため作成しない形にしたい」との要望を受けて修正。`_buildWorkflowForIteration()`は対象ノードIDが存在しない場合、書き込みを黙ってスキップするだけ(`write()`の`wf[nodeId]`ガード)なので、この行はRun時に何の効果も持たない死んだコントロールだった。`_hasNodeForColumn(col)`を新設し、`_renderColumn()`と`_initColumnButtons()`の+ボタンで判定 — 対応ノードが無い列は「このワークフローに該当ノードがありません」というプレースホルダーを表示し、キーフレーム追加もブロックする。VAEだけは特殊扱いで、専用VAELoaderが無くてもCheckpointノードが存在すれば内蔵VAEの上書き注入が意味を持つため(既存の`_buildWorkflowForIteration`のVAELoader自動注入ロジック)、Checkpoint存在時はVAE列も通常表示のままにした。
**ヘルプ・README更新**: `project_v0336_conventions`の慣習に従い、`i18n.js`に新規メッセージキー`labNoNodeForColumn`を日本語/英語/中国語すべてに追加(このケースは既存キーワードで賄えないUI状態のため新設。ヘルプページ本文の更新は対象外の細部挙動のため省略)。README.mdのLab subtabセクションに「Empty state when no matching node exists (v0.4.5)」の項目を追記し、見出しのバージョン範囲を`v0.3.87 – v0.4.5`に更新した。
**検証**: Playwright MCP経由で実機ブラウザ操作: (1) RMBGのみのモデル無しワークフローを読み込みLabタブへ切り替え、Model列(Checkpoint/LoRA/VAEいずれのビューでも)が空プレースホルダーになることを確認、(2) Checkpoint+VAELoaderを含む通常のワークフローでは従来通りライブ値付きキーフレームが表示され回帰が無いことを確認、(3) 同じworkflow内でLoRAノードだけが無いケースではLoRAビューのみ正しく空プレースホルダーになることを確認。開発元gitリポジトリと実行時`custom_nodes`フォルダは別実体のため([[project_dev_deploy_sync]]参照)、変更ファイル(`lab-tab.js`/`i18n.js`/`lab-tab.css`)を両フォルダへ同期しdiffで完全一致を確認した。
**How to apply**:
1. 「機能しない」という不具合報告を受けた際は、コードを読んで理屈上正しそうに見えても、実機(今回はPlaywright MCP経由の実ブラウザ+実ComfyUI)で変換結果と生成画像の中身(アルファチャンネルの実値など)まで直接検証してから結論を出す。理屈の検証だけでは「今のバージョンでは再現しない」という事実に辿り着けなかった。
2. 「機能しない」の調査中に見つかった別の実害あるバグ(今回のLabタブの死んだキーフレーム行)は、本題の調査がクローズしても見逃さず、その場でユーザーに報告し必要なら合わせて修正する。
3. UIに「設定できるが実行時には何の効果も持たない」コントロールを残すと、ユーザーは正常に設定できていると誤認する。対象データが存在しないなら、コントロール自体を出さない(無効化ではなく非表示)のが最も誤解を生まない。
関連: [[project_v042_lab_model_column]], [[project_dev_deploy_sync]]
---
## v0.4.4
### 機能追加: Lab/GenerateUI ModelタブへLoRAバイパス機能 + AI TOOL Chatの画像生成パラメータ拡張
ユーザーから「生成UIタブのLab機能のLora、ModelタブのLoraのLoraノード自体をバイパスする機能を追加したい。Lab機能ではキーフレームごとに切り替え対応可能としたい」との要望を受けて着手。同じセッション内で続けて「Lab機能のLoraにLora syntax、Trigger wordsの適用を可能にしたい」「AI TOOLタブのCHATの画像生成でチャットによる設定項目を増やしたい」という要望にも順次対応した一連のセッション。
**1. LoRAノードのバイパス機能**: 実装方針の分岐点(ComfyUI本来のBypass(mode:4)相当の配線迂回か、strength=0にする簡易実装か)をAskUserQuestionでユーザーに確認し、簡易実装(strength=0)を選択。理由は、Lab/GenerateUIが扱う`workflow`は既に`comfyui-workflow.js`の`convertUiToApi()`でAPI形式に変換済みで`mode`の概念自体が失われており、`mode:4`を書き込んでも実行エンジンには一切効果がないため——本物のBypass相当の配線再接続を自前実装するのは過剰投資と判断した。
- **GenerateUI Model タブ**: LoRA列ヘッダーの「Refresh Stack group」ボタン左に⛔トグルボタンを追加(`comfyui-editor.js`の`toggleLoraNodeBypass()`/`_loraNodeBypass`/`_loraBypassSnapshot`)。対象ノードのstrength値をゼロ化し、退避したスナップショットから復元をトグルする。Lora Managerノードの場合はLoRAリスト自体(複数エントリ)は差し替えず維持したまま各エントリのstrength/clipStrengthだけを0にする。Single/Stack Applyボタンを押すとバイパス状態は自動クリアされ、あとから無言で再適用されることはない。
- **Lab機能**: 既存の行単位「bypassed」(キーフレーム自体を無視するスキップ機能)とは独立した、LoRA列専用の`nodeBypass`フィールドをキーフレームの値に追加。編集モーダルに「LoRAノードをバイパス(強度を0にする)」チェックボックスを新設。`_applyLabLoraToWorkflow()`で`nodeBypass`時はstrength/active/textを0/false/空に強制する。
**2. Lab: LoRA syntax / Trigger words のポジティブプロンプト適用**: LoRA値に`applyToPrompt`フィールドを追加。ONにすると`<lora:stem:strengthModel:strengthClip>`とCivitAIキャッシュのtrainedWordsをポジティブプロンプトへカンマ区切りで追加する(`comfyui-editor.js`のSingle Applyと同じフォーマット)。「次のキーフレームでLora変更時はクリアされた上で新たなloraで追加」という要件は、`_buildWorkflowForIteration()`に`prevLoraInjection`引数を追加し、**前回のイテレーションが正確に注入した文字列だけ**を次回ビルド時に文字列一致で取り除いてから新規注入を追加する設計で実現した——正規表現による汎用パターン除去ではなく、自分が直前に足した分だけを覚えて消す方式なので、ユーザーが手で書いた他のプロンプト内容を誤って消す心配がない。metadata/civitaiCacheはRun開始時に一度だけフェッチしてキャッシュする。
**3. AI TOOL Chat: generate_imageツールのパラメータ拡張**: AskUserQuestionで追加したい項目をユーザーに確認し、KSampler系(steps/cfg/sampler_name/scheduler/denoise/seed)・画像サイズ(width/height)・生成枚数(batch_size)を選択。`comfyui-editor.js`に`setGenerationParams()`を新設し、Lab機能の`_buildWorkflowForIteration`と同じ`sampler_nodes`のstepsNodeId/cfgNodeId等のノードID間接参照パターンを再利用することで、Advanced Samplingワークフロー(Flux.1/2、SD3.5等でKSamplerが複数ノードに分割される構成)にも対応した。seedは`window._wfmGenerateTab.generate()`(実体は`handleGenerate`)へのオプション引数として渡すよう、`generate-tab.js`の`handleGenerate`/`_coreGenerate`にgenOptions引数を追加(既存の呼び出し元との後方互換は維持)。batch_size>1時は複数画像がチャットバブルに表示されるよう`appendChatBubble`に`imageUrls`(複数URL)対応を追加した。実機のOllama(ornith-1.5:9b)でツール呼び出しから画像生成までEnd-to-Endで検証し、指定した各パラメータが正しく反映されることを確認した。
**4. Resolution Selectorノード対応**: ユーザーから実際のワークフロー画像(aspect_ratio COMBO・megapixels・multipleウィジェットを持つ「Resolution Selector」ノード)を見せられ、「3:2などで指定可能か」と問われて調査。`/object_info/ResolutionSelector`で確認したところComfyUI本体組み込みノード(`comfy_extras.nodes_resolution`)で、aspect_ratioは`"3:2 (Photo)"`のような固定ラベル付きCOMBO値と判明した。当初のwidth/height直接指定実装ではこの手のノードに非対応だったため、追加で以下を実装:
- `comfyui-workflow.js`の`analyzeWorkflow()`に`resolution_selector_nodes`カテゴリを新設。
- `comfyui-editor.js`に`resolveAspectRatioOption(nodeType, requested)`を新設。LLMが渡す短い`"3:2"`形式を、`/object_info`から取得した実際のCOMBO選択肢と前方一致でマッチさせ`"3:2 (Photo)"`のような完全な文字列に解決する(未知の比率はそのままフォールバックし、ComfyUI側の検証エラーに委ねる)。
- `setGenerationParams()`にResolution Selector優先ロジックを追加:aspect_ratio指定時はEmptyLatentImageへのwidth/height直接書き込みをスキップし、ResolutionSelectorノードのaspect_ratioだけを書き換える——EmptyLatentImageのwidth/heightがResolutionSelectorの出力へのリンクになっている場合、直接数値を書き込むとそのリンクを上書きしてしまい、意図した比率指定が無視されてしまうため。
**ヘルプ・README更新**: `project_v0336_conventions`の慣習(index.html + i18n.js 3言語 + app.jsのhelpIdMapの3点セット)に従い、`helpLab16`/`helpLab17`(Lab LoRAバイパス・プロンプト適用)、`helpGen29`(GenerateUI Model LoRAバイパスボタン)を新規追加し、`helpAi3`(AI TOOL Chatの説明)を拡張パラメータの説明を含めて更新した。README.mdのGenerateUI Tab/Lab subtab/AI TOOL Tabの各セクションも同内容で更新した。
**検証**: 全JSファイルを`node --check`で構文確認。開発元gitリポジトリと実行時`custom_nodes`フォルダは別実体のため([[project_dev_deploy_sync]]参照)、変更ファイルのみ両フォルダへ同期しハッシュ一致を確認。Kapture MCP経由で実機ブラウザ操作: (1) GenerateUI ModelタブのLoRAバイパストグルでLora Managerノードのloras配列のstrength/clipStrengthが0⇔元の値へ正しく切り替わることを確認、(2) LabタブのLoRAキーフレームモーダルで両チェックボックスが表示・保存され、セル表示にラベルが反映されることを確認、(3) 実際にLabをRunし`/history`APIから最終ワークフローの`CLIPTextEncodeEditPlus.text_edit`を直接確認してLoRA構文+トリガーワードが正しく追加されていることを確認、(4) Ollama(ornith-1.5:9b)へChatで生成パラメータ付きの依頼を送りgenerate_imageのtool_callsが正しい引数を返し、実際に指定通りのwidth/height/seed/batch_sizeで2枚の画像が生成されることをネットワークログと生成結果の両方で確認、(5) `resolveAspectRatioOption`/`setGenerationParams`をモックworkflow/analysisで単体テストし、ResolutionSelectorノードのaspect_ratioが正しく書き換わりEmptyLatentImageのリンクは維持されることを確認。ヘルプページも実機で3箇所とも日本語表示が正しく反映されていることを確認した。
**How to apply**:
1. 「ノードをバイパスする」という要望を受けたら、まずそのワークフローが実行時にどのフォーマット(UI形式 mode:4対応/API形式 mode概念なし)で保持・実行されているかを確認する。今回のケースのように既にAPI形式へ変換済みの場合、本物のBypass(mode:4)相当の配線再接続を自前実装するのは過剰投資になりやすく、「効果が等価な簡易実装(strength=0)」で要件を満たせないか先にユーザーに確認するとよい。
2. LLMのfunction calling引数にCOMBO値(enum文字列)を渡させる場合、ワークフロー解析結果には「ノードの現在値」しか含まれず「取りうる全選択肢」は含まれないことが多い。`/object_info`をその場でフェッチして選択肢を取得し、LLMの短い入力("3:2")を実際の完全な文字列("3:2 (Photo)")へ解決するワンクッションを挟むと、ノードごとに異なるラベル文言に振り回されずに済む。
3. 「切り替えたらクリアしてから新しいものを追加する」という要求は、正規表現による汎用パターン除去よりも「前回自分が何を注入したかを正確に覚えておいて、その文字列だけを取り除く」設計の方が、ユーザーが手で書いた他のテキストを誤って消すリスクが低く安全。
関連: [[project_v042_lab_model_column]], [[project_dev_deploy_sync]]
---
## v0.4.3
### バグ修正: 共有モーダルのお気に入り☆/リネームがLabモーダルへ残留 + Lab Model列の+/-をCheckpoint/LoRA/VAEで完全独立化
v0.4.2リリース直後、ユーザーからLabタブに関する2件のフィードバックを受けて修正した一連のセッション。
**1. 共有モーダルのお気に入り☆・リネームがLabモーダルに残留するバグ**: ユーザーから「Labタブモーダルについて確認したい。各モーダルでお気に入り☆、名前変更が可能なようですがこれは機能していますか?」との指摘を受けて調査。Workflow Studioの全「詳細モーダル」(Workflow/Models/Nodesタブの詳細表示、Labのキーフレーム編集)は`#wfm-modal-overlay`というDOMを1つだけ共有しており、共通の`openModal()`(`app.js`)はタイトルの**テキストだけ**を書き換え、呼び出し元が動的に注入したヘッダー要素(お気に入り☆ボタン、タイトルクリックでリネームするイベントリスナー)は一切クリアしていなかった。そのため「Workflowタブでワークフローの詳細モーダルを開いた直後にLabのキーフレームモーダルを開く」と、直前のワークフロー用の☆ボタンとリネームリスナーがそのまま残留表示され、Lab側で☆をクリックすると**Labのキーフレームとは無関係に、直前に見ていたワークフローのお気に入り状態が実際に切り替わってしまう**ことをPlaywright MCPで実機再現・立証した。`openModal()`にクリーンアップ処理(`.wfm-fav-btn`の除去、タイトル要素を`cloneNode`で複製・差し替えて前の呼び出し元のクリックリスナー/属性を一掃、リネーム用隠し入力欄のリセット)を追加して修正。副次効果として、Workflow詳細モーダルを連続で複数回開いた際にタイトルのクリックリスナーが際限なく累積していく別の潜在バグ(`workflow-tab.js`側にはリネーム用clickリスナーの重複防止ガードが無かった)も同時に解消した。
**2. Lab Model列の+/-が独立していなかった問題**: v0.4.2で実装したModel列(Checkpoint/LoRA/VAEを1つの`{checkpoint, lora, vae}`マージ行として1つのキーフレーム配列にまとめる設計)には設計上の欠陥があった。ユーザーから「checkpoint/lora/vaeいずれかで+で追加すると他のモデルも追加されてしまいます。逆に-で他の2つも削除される」との指摘を受け、根本的な設計見直しを実施。マージ行方式ではなく、**Checkpoint・LoRA・VAEを(Prompt・KSamplerと全く同じ)完全に独立したキーフレーム配列に戻し**、C/L/Vトグルは「今どの配列を表示・操作するか」を切り替えるルーティングとしてのみ機能する設計に変更した。この結果、v0.4.2で新設した`_resolveModelKeyframe()`(3フィールドを1行にバンドルしたまま個別に前方向解決するカスタムロジック)が丸ごと不要になり、既存の汎用`_resolveKeyframe()`(Checkpoint/VAE列が元々使っていたもの)にLoRAを1つ追加するだけで済むようになった——継承機能・ハイライト・inheritedヒント表示など、v0.4.2で追加した機能はすべてこのシンプルな独立配列方式のままで自然に成立する。
**設計上の教訓**: 複数の独立した設定を1つのUI列にまとめる際、「見た目を1列にする」ことと「データを1行にマージする」ことは別の問題であり、後者を選ぶと今回のような「+/-が意図せず道連れになる」バグを生みやすい。データモデルは独立させたまま、UI層(表示するフィールドの選択・共有される+/-ボタンのルーティング)だけを1列に見せかける設計の方が、機能追加・バグ修正のコストが低く、直感的な挙動にもなりやすい。
**ヘルプ・README更新**: `project_v0336_conventions`の慣習に従い、Lab関連ヘルプ(`helpLab14`・`helpLab15`、英/日/中3言語+`templates/index.html`の英語フォールバック)を独立配列方式の挙動に合わせて更新(特に`helpLab15`の「Revert to #1's settingは3項目まとめて戻る」という記述はもう正しくないため修正)。README.mdのLabセクションも同内容に更新した。
**検証**: 全JSファイルを`node --check`で構文確認。開発元gitリポジトリと実行時`custom_nodes`フォルダは別実体のため([[project_dev_deploy_sync]]参照)、変更ファイルのみ両フォルダへ同期しハッシュ一致を確認。Playwright MCP経由で実機ブラウザ操作: (1) Workflow詳細モーダル→Labモーダルの順で開き、☆・リネームツールチップ・cursorスタイルが一切残留しないことを確認、(2) 「C」表示での+/-がCheckpointのみに作用しLoRA/VAEは不変であること、「L」表示での+/-がLoRAのみに作用しCheckpoint/VAEは不変であることをPlan JSONタブのデータ構造で確認、(3) v0.4.2形式(マージ行)のPlan JSONを読み込ませ、新しい独立配列形式へ正しくマイグレーションされることを確認。実際の画像生成を伴うRun実行は割愛。
**How to apply**:
1. DOM/モーダルを複数の呼び出し元で共有する設計では、各呼び出し元が「自分固有のUI要素」を動的に注入する場合、その呼び出し元自身が毎回クリーンアップするのではなく、**共有側の入り口関数(今回は`openModal()`)で毎回リセットする**設計にしないと、呼び出し順序に依存した残留バグ・誤操作バグを生む。cloneNode+replaceWithは、addEventListenerの参照を保持していなくてもリスナーを確実に一掃できる簡便な手段。
2. 複数の独立した設定を1つのUI要素(列・パネル等)にまとめる要望を実装する際、「データも1つにまとめる」のではなく「データは独立させたままUI層だけ共有する」設計を最初から検討する。今回はユーザーの2度のフィードバック(継承挙動の誤り→+/-の誤り)を経てようやくこの設計にたどり着いたが、根本原因は同じ「フィールドを1行にバンドルした」という初期設計判断にあった。
---
## v0.4.2
### 機能追加: Lab「Model」列統合(Checkpoint / LoRA Single / VAE)+ フィールド単位継承 + C/L/V表示切替
+258
View File
@@ -0,0 +1,258 @@
# README Index — ComfyUI-Workflow-Studio
Condensed index of README.md: intro summary, feature names per tab (full descriptions live in the excluded README.md), and the installation/usage/requirements/project-structure sections verbatim. Exists so graphify can extract this without feeding the full README (with its screenshot table) to the local Ollama model. Regenerate with `python tools/generate_doc_index.py readme` whenever README.md changes.
# ComfyUI-Workflow-Studio
A comprehensive workflow, asset management, and generation UI plugin for [ComfyUI](https://github.com/comfyanonymous/ComfyUI).
**📁 Management** — organize all your assets in one studio
- Workflow, model, image, and prompt management
- AI-powered prompt writing assistance, translation, and tag generation
- File drop & Gallery-linked metadata viewing — inspect and reuse generation settings instantly
**⚡ GenerateUI** — a productivity-focused generation tab connected to every other tab
- Batch generation across models, samplers, prompts, and workflows
- Image Feeder for continuous folder-based generation
- Lab: experimental I2I batch generation with independent per-iteration keyframes for Model (Checkpoint/LoRA Single/VAE)/Prompt/KSampler
**📚 Workflow Studio Library** — a multi-function side panel for smooth ComfyUI integration
- Drag & drop models, nodes, prompts, and workflows straight onto the canvas
- **Send to Canvas**: click "Send to Canvas" (Workflow tab) or "Copy & Send Canvas" (Gallery tab) to send the workflow directly to the ComfyUI canvas — UI and API formats both supported
- View metadata from images / JSON, then drop the detected models and prompts onto the canvas
- Built-in AI tools (translation and more)
![Workflow Studio](https://img.shields.io/badge/ComfyUI-Custom_Node-blue)
![Version](https://img.shields.io/badge/version-0.4.7-green)
## Features (tab -> feature names)
### Workflow Tab
Thumbnail / Table views, Thumbnail side panel, Badge filtering, Search, Side panel tabs, Badge management, AI summary, Import / Export, Default view setting, Search clear (✕), Clear all filters (✕ Clear)
### Canvas Snapshot (v0.1.2)
One-click capture, Auto-save as thumbnail, Embedded workflow metadata, Auto-import
### GenerateUI Tab (v0.3.5)
6-tab layout, Save button, Input tab, Model tab, Settings tab, Always-visible Raw JSON, Bypass/Mute node handling, One-click generation, Seed control, Style selector, Create Catalog / Catalog buttons, Batch type selector, Batch tab, UI-to-API conversion, Eagle integration
### Feeder subtab (v0.3.5 / v0.3.42)
ImageFeeder node control, Image library, Selection management, Presets, Continuous Run loop, Index sync, Seed, WFS_GalleryFeeder node, Node & group selector, Image grid, After Gen modes, Run / Stop controls
### Lab subtab (v0.3.87 – v0.4.5)
Setting / Results / Plan JSON sub-panels, Per-column keyframes, Model column — Checkpoint / LoRA Single / VAE, Empty state when no matching node exists, Wildcards, Prompt cell extras, Keyframe #1 live-reflect, Bypass checkbox, LoRA node bypass & prompt injection, T2I workflow support, Use generated image for next, Workflow recall, Plan files, Plan Load, Plan JSON tab, Save index image to Output on Run, Index images are self-contained plan files, Eagle integration
### Prompt Tab
3-column layout, AI chat assistant, Image attachment, Translation, Prompt presets, Preset Manager, Group management, Clipboard copy, GenUI Set, Wildcard/Style tab bar, Wildcard input toolbar, Wildcard file manager, Style manager, Form / Table toggle, Table view — Presets / Presets Group / Wildcards / Style
### Settings Tab
2-column layout, Collapsible sections, Theme selection, Theme customization, Workflows directory, Gallery output directory, Eagle connection, Ollama connection, CivitAI Host, CivitAI API Key, Default workflow, Default Checkpoint, Data Management, Text Size, RAW JSON Colors, GenerateUI Model Tab Highlight, Wildcard Integration, G'MIC-Qt Integration, Language
### Gallery Tab (v0.3.44)
Output / ImagePrompt / Style_Catalog / Metadata sub-tabs, Image browser, Folder tree root label, Thumbnail / Table views, Folder management, File operations, Download, Multi-select, Image Compare, Prompt search, Server-side filtering, Group management, Thumbnail F button, Favorites, Detail panel, Prompt tab, Workflow viewer, Load GenUI button, Image Edit button, Send GenUI Image button, Send CC button, Search clear (✕), Clear all filters (✕ Clear), Workflow auto-save, Output folder configurable, SVG file support, Performance
### ImagePrompt Gallery subtab (v0.3.94, renamed from Style/Prompt in v0.3.95)
Visual prompt library, 3-column layout, Prompt builder (right pane), Plain-text prompt storage, ponyxlWildcardsVault format support, Seed data importer
### Style_Catalog Gallery subtab (v0.3.95)
Visual Style picker, 3-column layout, Positive/Negative panel, Select as Style button, Load in GenerateUI / Open in Metadata Tab buttons, Double-click to enlarge
### Metadata Gallery subtab (v0.3.8, moved from a top-level tab in v0.4.6)
Now inside Gallery, 3-column layout, File drop, Model extraction, LoRA extraction, Prompt extraction, Prompt actions, Format support, Format note
### Nodes Tab (v0.1.7)
Node Browser, Search & Filter, Package badges, Node detail panel, Node Sets, Right-click context menu, Multi-select, Search clear (✕), Clear all filters (✕ Clear)
### Models Tab (v0.2.3)
Model Browser, Thumbnail / Table views, Table column sort, Search & Filter, User-defined badges, Side panel tabs, CivitAI integration, CivitAI panel — Info / Sample sub-tabs, CivitAI panel states, Batch CivitAI fetch, Detail modal, GenUI Model button, Group management, Table view memo, Preview images, Enable / Disable models, Multi-select & bulk operations, Search clear (✕), Clear all filters (✕ Clear)
### Tagger Tab (v0.3.38)
3 sub-tabs, Model support, Threshold sliders, Ollama VLM, Single tab, Single output options, Batch tab, DB tab, Dependencies
### Image Edit Tab (v0.3.65)
Layer-based image editor, Loading images, New button, Tools, Layer panel, Layer lock, Text quality, Export, Canvas navigation, Undo / Redo
### AI TOOL Tab (v0.3.14)
4-pane layout, Translation pane, Chat pane, Chat pane — image generation via Tool Calling, Chat pane — generate_image extra parameters, TOOLS pane (VLM), TOOLS pane — shared Chat attachment, TOOLS pane (Wildcards), Chat pane — image-to-image (I2I), Chat pane — SVG generation, no ComfyUI workflow involved, Chat pane — Skills, Settings pane, Unsloth backend, Settings pane — Chat Image Generation, Settings pane — Chat I2I Generation, Settings pane — Generation, Settings shared, Backend support, URL security, Lemonade's other endpoints (not integrated)
### Workflow Studio Library (ComfyUI Side Panel) (v0.3.9)
Tab layout (W / N / P / M / I / A), W — Workflows tab, N — Nodes tab, M — Models tab, P — Prompts tab, I — Information tab, A — AI TOOL tab, Drag & drop workflows, Drag & drop nodes, Drag & drop prompts, Send to Canvas, Copy prompts, Double-click, Search, ⚙ Theme settings
### Help &amp; Support Tab (v0.1.3)
Sidebar navigation, Support, Feature list, Keyboard Shortcuts
## Installation
### Via ComfyUI Manager (Recommended)
Search for **Workflow Studio** in ComfyUI Manager and install.
### Manual Installation
```bash
cd ComfyUI/custom_nodes
git clone https://github.com/ketle-man/ComfyUI-Workflow-Studio.git
```
Restart ComfyUI after installation.
---
## Sample Workflows
Sample workflows are included in the `workflows/` folder. You can open them directly in ComfyUI via drag & drop, or load them from the Workflow tab.
> **Note:** Some sample workflows require additional custom nodes.
> If a node is shown as missing (red/unknown) after loading, install the required custom nodes via **ComfyUI Manager** or by cloning the repository from GitHub into your `ComfyUI/custom_nodes/` directory.
>
> ```bash
> cd ComfyUI/custom_nodes
> git clone <repository-url>
> ```
>
> After installation, restart ComfyUI to activate the new nodes.
---
## Usage
### Launch
Click the **W** button in the ComfyUI top menu bar, or navigate to:
```
http://127.0.0.1:8188/wfm
```
> **Tip:** Shift+Click the W button to open in a new window.
### Canvas Snapshot
Click the **camera icon** (next to the W button) in ComfyUI's top bar to capture the current workflow canvas as a thumbnail. The image is automatically saved to the workflow data folder and appears in Workflow Studio's workflow list.
### Quick Start
1. **Workflow Tab** — Your workflows from `ComfyUI/user/default/workflows/` are automatically listed
2. **Click a workflow** — View thumbnail, JSON details, and metadata in the side panel
3. **Load in GenerateUI** — Click the button to load a workflow into the generation interface
4. **Adjust parameters** — Modify prompts, models, seeds, and settings via the auto-generated UI
5. **Generate** — Hit the Generate button to queue the prompt
---
## Requirements
- **ComfyUI** — any recent version (v1.33.9+ recommended for action bar integration)
- **Python 3.10+**
- **Jinja2** — `pip install jinja2` (usually included with ComfyUI)
### Optional
- **[Ollama](https://ollama.com/)** — for AI chat assistant, translation, and VLM features
- **[LM Studio](https://lmstudio.ai/)** — alternative backend for translation and VLM (OpenAI-compatible API)
- **[Lemonade Server](https://lemonade-server.ai/)** — alternative backend for translation, chat, and VLM (OpenAI-compatible API)
- **[Unsloth](https://unsloth.ai/)** — alternative backend for translation, chat, and VLM (OpenAI-compatible API); always requires an API key, set via `UNSLOTH_API_KEY` in a `.env` file — `pip install -r requirements.txt` for `python-dotenv`
- **[Eagle](https://eagle.cool/)** — for auto-saving generated images with metadata
- **[comfyui-mask-editor-one](https://github.com/ketle-man/comfyui-mask-editor-one) (v0.1.9+)** — enables BiRefNet background removal, SAM3 text-prompt segmentation, and ABR stamp-brush library in the Image Edit Mask tool; `birefnet.safetensors` must be placed in `ComfyUI/models/background_removal/` for BiRefNet
---
## Supported Languages
| Language | Status |
| -------- | ------ |
| English | Full |
| Japanese | Full |
| Chinese | Full |
---
## Project Structure
```
ComfyUI-Workflow-Studio/
├── __init__.py # ComfyUI entry point
├── py/
│ ├── wfm.py # Main class & route registration
│ ├── config.py # Path configuration
│ ├── nodes/
│ │ ├── prompt_text.py # WFS_PromptText custom node (positive/negative prompt)
│ │ └── gallery_feeder_node.py # WFS_GalleryFeeder custom node (gallery group → IMAGE)
│ ├── routes/
│ │ ├── workflow_routes.py # Workflow CRUD & analysis API
│ │ ├── nodes_routes.py # Nodes metadata & node sets API
│ │ ├── models_routes.py # Model metadata, preview, CivitAI API
│ │ ├── prompts_routes.py # Prompt presets CRUD API
│ │ ├── settings_routes.py # Settings API
│ │ ├── ollama_routes.py # Ollama proxy API
│ │ ├── eagle_routes.py # Eagle integration API
│ │ └── gallery_routes.py # Gallery tab API (Output + ImagePrompt + Style_Catalog sub-tabs)
│ └── services/
│ ├── workflow_service.py # Workflow file operations
│ ├── nodes_service.py # Node metadata & node sets
│ ├── models_service.py # Model metadata & preview images
│ ├── civitai_service.py # CivitAI API integration & cache
│ ├── prompts_service.py # Prompt presets persistence
│ ├── workflow_analyzer.py # Model/node detection
│ ├── settings_service.py # Settings persistence
│ ├── png_extractor.py # PNG metadata extraction
│ ├── gallery_service.py # Gallery image scanning, .txt sidecar & ponyxlWildcardsVault prompt resolution
│ └── gallery_metadata.py # Gallery per-image metadata store (favorites/tags/memo/groups)
├── templates/
│ └── index.html # SPA template (Workflow/GenerateUI/Prompt/Metadata/Gallery/Nodes/Models/Settings/Help/AI)
├── static/
│ ├── favicon.svg # Browser tab icon (W+S Wave)
│ ├── css/main.css # Styles
│ └── js/
│ ├── app.js # App initialization & routing
│ ├── workflow-tab.js # Workflow browser
│ ├── generate-tab.js # Generation UI
│ ├── feeder-tab.js # Feeder subtab (Image Loop + Gallery modes)
│ ├── gallery-tab.js # Gallery tab: Output sub-tab (image browser, groups, metadata) + sub-tab switching
│ ├── image-prompt-tab.js # Gallery tab: ImagePrompt sub-tab (visual prompt builder)
│ ├── style-catalog-tab.js # Gallery tab: Style_Catalog sub-tab (visual Style picker)
│ ├── tagger-tab.js # Tagger tab (WD Tagger, DeepDanbooru, Ollama VLM)
│ ├── prompt-tab.js # AI assistant & presets
│ ├── metadata-tab.js # Metadata extraction & display (PNG/WebP/JSON)
│ ├── settings-tab.js # Settings panel
│ ├── comfyui-client.js # ComfyUI WebSocket/API client
│ ├── nodes-tab.js # Node browser & node sets
│ ├── models-tab.js # Model browser & CivitAI integration
│ ├── ai-tab.js # AI TOOL tab (Translation | Chat | TOOLS/VLM | Settings)
│ ├── comfyui-workflow.js # UI-to-API format conversion
│ ├── comfyui-editor.js # Dynamic parameter editor
│ ├── json-highlight.js # JSON syntax highlighting
│ ├── util.js # Shared utilities (escapeHtml, readJsonStorage, getSettings)
│ └── i18n.js # Internationalization
├── web/comfyui/
│ ├── top_menu_extension.js # ComfyUI menu bar integration
│ ├── node_sets_menu.js # Workflow Studio Library side panel
│ └── gallery_feeder_extension.js # WFS_GalleryFeeder canvas widgets (After Gen / Run / Stop)
├── tools/
│ └── import_style_prompt_seed.py # Dev tool: pre-process a wildcards-vault source into ws_image_prompt (not run by the plugin itself)
└── data/ # Fallback data dir (used when ComfyUI user/default/ is not found)
```
---
## License
MIT License
---
## Acknowledgements
- [ComfyUI](https://github.com/comfyanonymous/ComfyUI) by comfyanonymous
- [ComfyUI-Custom-Scripts](https://github.com/pythongosssss/ComfyUI-Custom-Scripts) by pythongosssss — Canvas snapshot and PNG workflow embedding implementation reference
- [ComfyUI-Lora-Manager](https://github.com/willmiao/ComfyUI-Lora-Manager) — Plugin architecture and UI pattern reference
- [Ollama](https://ollama.com/) for local LLM inference
- [Eagle](https://eagle.cool/) for image management
- [Pillow (PIL Fork)](https://python-pillow.org/) — server-side thumbnail generation (`data/thumb_cache/`)
- [ComfyUI-Gallery](https://github.com/PanicTitan/ComfyUI-Gallery) by PanicTitan — thumbnail grid UX reference
- [infinite-image-browsing](https://github.com/zanllp/sd-webui-infinite-image-browsing) by zanllp — thumbnail caching and index strategy reference
- [Fooocus](https://github.com/lllyasviel/Fooocus) by lllyasviel — style preset JSON format reference
+41 -23
View File
@@ -22,7 +22,7 @@ A comprehensive workflow, asset management, and generation UI plugin for [ComfyU
- Built-in AI tools (translation and more)
![Workflow Studio](https://img.shields.io/badge/ComfyUI-Custom_Node-blue)
![Version](https://img.shields.io/badge/version-0.4.2-green)
![Version](https://img.shields.io/badge/version-0.4.7-green)
## Screenshots
@@ -89,7 +89,7 @@ A comprehensive workflow, asset management, and generation UI plugin for [ComfyU
<summary><h3>GenerateUI Tab (v0.3.5)</h3></summary>
- **6-tab layout** — Input / Model / Settings / Feeder / Batch / **Lab** tabs; Input, Model, and Settings each include a Raw JSON column on the right for instant preview and direct editing
- **Save button** — located at the right end of the subtab row; opens a filename dialog (default: current workflow name) and saves the current workflow as a `.json` file to the Workflow tab via the import API
- **Save button** — located at the right end of the subtab row; opens a filename dialog (default: current workflow name) and saves the current workflow as a `.json` file to the Workflow tab via the import API; hidden while the Batch or Lab subtab is active (v0.4.6), since those work on workflow copies / their own Plan files rather than the currently loaded workflow
- **Input tab** — Prompt and Image inner tabs (drag-and-drop upload); Prompt tab shows Positive Prompt and Negative Prompt textareas, plus an **Embeddings selector** at the bottom (Filter + Select + Weight input + Paste button); Paste inserts `(embedding:Name:weight)` at the cursor position of the last focused textarea (defaults to Positive when neither is focused); Raw JSON (540px) in the right column
- **Revert button** (v0.3.70) — a ↺ button next to each Apply button reverts the textarea to the node's currently applied value (the reverse of Apply)
- **Emphasis weight editing** (v0.3.70) — select text (or just place the cursor on a word/parenthesis block) and press Ctrl+↑/↓ to adjust `(text:weight)` by ±0.05, A1111/native-ComfyUI style; parentheses are removed automatically when the weight returns to 1.0
@@ -98,12 +98,14 @@ A comprehensive workflow, asset management, and generation UI plugin for [ComfyU
- **Model tab** — Checkpoint, VAE, Diffusion Model (UNETLoader / UnetLoaderGGUF / LoaderGGUF), and Text Encoder (CLIPLoader / DualCLIPLoader / ClipLoaderGGUF / DualClipLoaderGGUF — single clip: type + device; dual clip: two clip selectors + type + device) are always shown; **ControlNet** and **Hypernetwork** (with Strength field) are collapsed by default (v0.3.98) since they're used less often — click their header to expand; **LoRA** (Single/Stack tabs) has its own column; all selectors have a filter; Raw JSON on the right
- **Highlight color** (v0.3.98) — the label of each model type above, plus the LoRA column header, turns a distinct color (default bright green, customizable in the Settings tab) when the loaded workflow actually contains that type of node; stays visible even while ControlNet/Hypernetwork are collapsed, so relevance is visible without expanding
- **LoRA Stack Group toggle** (v0.3.92) — for `Lora Loader (LoraManager)` nodes, a **Use Stack Group** checkbox next to the Single/Stack tabs switches what the Stack tab shows and edits: OFF (the default whenever a workflow is loaded) displays and edits the LoRAs actually configured in the workflow's own Lora Manager node — correctly showing every LoRA it holds, not just the first one; ON switches to the LoRA group registered in the Models tab instead; toggling either direction clears the node's LoRA list first, then loads it from the newly selected source, and switching back to OFF restores exactly the list that was present when the workflow was loaded
- **LoRA Bypass button** (v0.4.4) — a ⛔ toggle next to the Refresh Stack group button in the LoRA column header; forces the target LoRA node's strength to 0 (`strength_model`/`strength_clip` for a standard LoraLoader, or every entry's `strength`/`clipStrength` for a Lora Manager node — its LoRA list stays intact, not replaced) without touching the actual ComfyUI node's mode; click again to restore the exact values that were there before toggling; targets whichever node Single/Stack Apply default to (the workflow's Lora Manager node if it has one, else the first LoRA node); clicking a Single or Stack Apply button clears the bypass state so it can't silently reassert itself later
- **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; built-in **search bar** (always shown) finds all matches as you type with count display (`3/12`); navigate with ↑/↓ buttons or Enter / Shift+Enter; Escape or ✕ clears; current match highlighted in orange, other matches in yellow
- **Bypass/Mute node handling** (v0.3.75) — loading a UI-format workflow with Bypass-mode nodes now reroutes their wires to the upstream source (matching same-type input/output slots, recursively through chained bypasses) instead of leaving a dangling reference that failed validation; Mute-mode nodes are excluded without rerouting, same as ComfyUI itself; when either is present, a note listing the affected nodes appears above the Raw JSON search bar in the Input/Model/Settings tabs
- **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
- **Style selector** — checkbox and dropdown next to the Reset Workflow button; enable to apply a Fooocus-style JSON to positive and negative prompts at generation time; style files (`*.json`) are loaded from `user/default/Workflow-Studio/style/`; the style's `{prompt}` placeholder is replaced with the original prompt text, or the style is appended if no placeholder exists; `negative_prompt` is appended to the existing negative prompt; styles are applied to a per-generation copy and do not modify the loaded workflow
- **Wildcards inside a Style now expand** (v0.4.7) — an A1111-style `__wildcard__` reference written into a Style's Positive/Negative prompt is expanded at generation time just like one typed directly into GenerateUI; the Style text is merged into the prompt *before* wildcard expansion runs (previously the order was reversed, so a Style's own wildcard references were sent to ComfyUI unexpanded)
- **Create Catalog / Catalog buttons** (v0.3.95) — next to the Style dropdown. **Create Catalog** generates one preview image per Style checked in the Batch tab's Style sub-tab (using the currently loaded workflow, typically T2I), then saves each result into a chosen `ws_style_catalog` subfolder named after the Style (existing files with the same name are overwritten); a modal lets you create a new destination folder or pick an existing one — separate folders are useful for grouping catalogs by checkpoint or by purpose. **Catalog** jumps straight to the Gallery tab's Style_Catalog sub-tab to browse the results (see below)
- **Don't keep a copy in the Output folder** (v0.3.96) — checkbox in the Create Catalog modal; each generation still writes a normal file to the Output folder first (the workflow's own Save Image node always does that), but once its copy has been saved into the catalog folder, checking this deletes that Output-folder original right away — keeps a large catalog run (e.g. every Style at once) from flooding the Output folder with near-duplicate files; if a style's catalog copy fails to save, its Output file is left alone rather than deleted
- **Batch type selector** — check one of the column header checkboxes in the Batch Queue pane (Checkpoint / Lora / Prompt / Workflow / Sampler / Scheduler / Style) to activate that batch type; only one type can be active at a time; the Batch panel below Generate shows the active type, progress, and Pause/Resume/Stop controls
@@ -146,17 +148,19 @@ Two independent modes selectable via **[Image Loop] / [Gallery]** toggle buttons
</details>
<details>
<summary><h3>Lab subtab (v0.3.87 – v0.4.2)</h3></summary>
<summary><h3>Lab subtab (v0.3.87 – v0.4.5)</h3></summary>
An experimental batch generator: runs the workflow currently loaded in GenerateUI N times, letting Model (Checkpoint/LoRA Single/VAE) / Prompt / KSampler each change independently starting at a chosen iteration — unlike the Batch tab's single-axis queue, every column keeps its own list of change points. Never mutates the loaded workflow; every iteration runs on a fresh clone. Works with both I2I and T2I workflows (v0.3.90).
- **Setting / Results / Plan JSON sub-panels** — Setting: image drop zone + 3 keyframe columns + run controls; Results: source image and up to 9 generated thumbnails (click to enlarge); Plan JSON: the current plan as raw editable JSON (v0.3.88)
- **Per-column keyframes** — Model, Prompt, KSampler each hold an independent list of `{iteration, value}` keyframes; +/− per column adds/removes the last keyframe (iteration #1 is fixed and can't be removed); any keyframe other than #1 can also be deleted individually from inside its own edit modal (v0.3.93), not just the last one; click a cell to edit its value, the iteration it starts applying from, and a "revert to #1's setting" checkbox; the effective value at iteration N is the latest keyframe with iteration ≤ N, carried forward until the next one changes it — except the Model column, which resolves each of its three fields independently (see below); lowering the Batch count below an existing keyframe's iteration doesn't delete or renumber it — a warning toast appears and it just stays dormant until Batch count is raised again (v0.3.93)
- **Model column — Checkpoint / LoRA Single / VAE** (v0.4.2) — the old separate Checkpoint and VAE columns merged into one Model column; a `[C][L][V]` toggle in its header picks which single field its cells summarize and its edit modal shows — switching the toggle never touches the other two fields, Save only updates whichever one is currently shown — and each button lights up the same highlight color as GenerateUI's Model tab (configurable in Settings) when the loaded workflow actually contains that node type; the three fields carry forward independently across keyframes, so a keyframe that only sets a LoRA doesn't reset Checkpoint/VAE back to the workflow default — it keeps whatever an earlier keyframe last set until a later one explicitly changes it (a field left blank in the edit modal shows "(inherits: ...)" instead of "(workflow default)" when there's something to inherit); "revert to #1's setting" still resets all three fields at once. LoRA support is Single only (no Stack, matching the experimental scope of this tab); if a LoRA is set but the loaded workflow has no LoRA node at all, Run warns once and skips it rather than auto-inserting one — unlike VAE (which does auto-inject a VAELoader when missing), wiring in a LoraLoader would require rewiring the Checkpoint's model/clip outputs through it
- **Per-column keyframes** — Checkpoint, LoRA, VAE, Prompt, and KSampler each hold their own fully independent list of `{iteration, value}` keyframes; +/− per column adds/removes the last keyframe (iteration #1 is fixed and can't be removed); any keyframe other than #1 can also be deleted individually from inside its own edit modal (v0.3.93), not just the last one; click a cell to edit its value, the iteration it starts applying from, and a "revert to #1's setting" checkbox; the effective value at iteration N is the latest keyframe with iteration ≤ N, carried forward until the next one changes it; lowering the Batch count below an existing keyframe's iteration doesn't delete or renumber it — a warning toast appears and it just stays dormant until Batch count is raised again (v0.3.93)
- **Model column — Checkpoint / LoRA Single / VAE** (v0.4.2, redesigned v0.4.3) — Checkpoint and VAE (and now LoRA) share one visual "Model" column; a `[C][L][V]` toggle in its header picks which of the three independent keyframe lists the shared cell list, its own +/-, and its edit modal currently operate on — each behaves exactly like its own column (same as Prompt/KSampler), just sharing the Model column's screen space, so adding, removing, editing, reverting, or bypassing a keyframe while viewing one never touches the other two. Each toggle button lights up the same highlight color as GenerateUI's Model tab (configurable in Settings) when the loaded workflow actually contains that node type. A field left blank in a later keyframe's edit modal shows "(inherits: ...)" instead of "(workflow default)" when an earlier keyframe in that same list already set something. LoRA support is Single only (no Stack, matching the experimental scope of this tab); if a LoRA is set but the loaded workflow has no LoRA node at all, Run warns once and skips it rather than auto-inserting one — unlike VAE (which does auto-inject a VAELoader when missing), wiring in a LoraLoader would require rewiring the Checkpoint's model/clip outputs through it
- **Empty state when no matching node exists** (v0.4.5) — Checkpoint/LoRA/VAE each show "No matching node in this workflow" instead of a keyframe list when the loaded workflow has no node that column could actually override, and the column's +/− is disabled too, so no dead "(workflow default)" keyframe row can be created in the first place; VAE still counts as usable whenever a Checkpoint node exists, since its baked-in VAE can still be overridden via auto-injected VAELoader as above
- **Wildcards** (v0.3.93) — Prompt keyframes' Positive/Negative text supports the same A1111-style `__name__` syntax as the main GenerateUI prompt fields; each keyframe is expanded fresh right before that iteration runs, so reusing the same keyframe across iterations can still draw a different random line each time
- **Prompt cell extras** (v0.3.88, refined in v0.3.89, v0.3.93, and v0.3.97) — **Get from GenerateUI** fills Positive/Negative from the workflow currently loaded in GenerateUI; **Get from Image** extracts a prompt from the image currently loaded in Lab's own Image drop zone (same detection as the Metadata tab; no separate file picker); **Get from Previous** (v0.3.97) copies both Positive and Negative wholesale from the keyframe immediately before this one in the same column (e.g. editing #3 copies #2's prompt; not shown on keyframe #1, which has no earlier row); **Clear** (v0.3.97) empties both fields; **Style** row — an **Apply** button merges whichever style is currently selected in GenerateUI's own top-bar Style dropdown into the Positive/Negative text below, comma-separated after any existing text (never clears it) and works regardless of that top-bar's own Style enable checkbox; a checkbox next to it tracks on/off and shows the applied style's name beside itself (not on the button) — unchecking only clears that name label, the already-merged text stays and is yours to edit/delete by hand
- **Keyframe #1 live-reflect** (v0.3.97) — in every column, keyframe #1 shows the currently loaded workflow's own live setting instead of a blank placeholder — for the Model column this covers Checkpoint and VAE only, LoRA is never live-reflected since there's no reliable way to infer "the" active LoRA back out of a workflow node — both in its grid cell (dashed border) and pre-filled into its edit modal, for as long as it hasn't been explicitly saved with an override; saving it — even unchanged — locks that value in as a real override and the dashed/live styling goes away; switching to the Lab subtab always refreshes this against whatever is currently loaded in GenerateUI
- **Bypass checkbox** (v0.3.97) — available on every keyframe including #1; skips that row entirely when picking which setting applies at a given iteration, as if it didn't exist, so whatever was active before it keeps running instead; Lab-internal only, never touches the actual ComfyUI node's mode; disables that row's value fields and its Revert checkbox while checked (mutually exclusive with Revert); the cell shows an amber "⏭ Bypassed" label, taking priority over both the Revert and live-reflect display
- **LoRA node bypass & prompt injection** (v0.4.4) — inside the LoRA keyframe's own edit modal, two checkboxes independent of the row-level Bypass above: **Bypass LoRA node** forces that keyframe's `strength_model`/`strength_clip` to 0 at apply-time — the behavioral equivalent of ComfyUI's node Bypass (mode:4), since the workflow used here is already API-format with no `mode` concept; **Apply LoRA syntax + trigger words to Positive prompt** appends `<lora:name:strengthModel:strengthClip>` plus that LoRA's cached CivitAI trigger words to the Positive prompt, comma-separated, at apply-time — the same format GenerateUI's own Model tab Apply button writes. Switching keyframes (a different LoRA, this checkbox turned off, or the row Bypassed) always strips exactly what the previous iteration injected before adding anything new, so a batch never accumulates every LoRA it has used; the prompt checkbox is disabled automatically while Bypass LoRA node is checked
- **T2I workflow support** (v0.3.90) — a **T2I workflow (no source image)** checkbox in the Image pane skips the source-image requirement and never touches a LoadImage node, so workflows with no image input (e.g. plain `EmptyLatentImage` txt2img) can also use the Model/Prompt/KSampler keyframes; the Image drop zone and "Use generated image for next" are greyed out (not cleared) while it's on; saved with the plan as `t2i_mode`
- **Use generated image for next** — optional checkbox that chains iterations: from iteration 2 onward the previous iteration's first output image is fed back in as the I2I source instead of the original dropped image, using ComfyUI's `"name [type]"` annotated-filename reference (no re-upload needed); disabled together with the Image drop zone in T2I mode
- **Workflow recall** (v0.3.90) — every saved plan records the filename of the workflow that was loaded in GenerateUI at save time (`workflow_filename` in the Plan JSON); after loading a plan, an **Open Workflow** button appears next to it — click to load that exact workflow file back into GenerateUI (asks for confirmation first, since it replaces whatever is currently loaded)
@@ -189,21 +193,17 @@ An experimental batch generator: runs the workflow currently loaded in GenerateU
- **New styles go to a dedicated file** — created via **+ New**, a style is appended to its own `custom.json` inside the style folder, so the bundled style packs are never modified directly
- **+ Add to this file** — while editing an existing style, this button clears the form for a brand-new entry that gets appended to that *same* file instead of the default `custom.json` — the way to add a style into one of the bundled packs
- Saving, renaming, or deleting a style immediately refreshes GenerateUI's Style dropdown, the Batch tab's checklist, and the Style Catalog's name list — no reload needed
</details>
<details>
<summary><h3>Metadata Tab (v0.3.8)</h3></summary>
- **3-column layout** — Drop zone (left) | Model info (center) | LoRA + Prompt (right)
- **File drop** — drop a ComfyUI-generated PNG / WebP or workflow JSON onto the drop zone (or click to open a file picker); PNG/WebP images are shown as a preview
- **Model extraction** — automatically extracts Checkpoint, VAE, Diffusion Model, and Text Encoder names from the workflow; supports both standard and subgraph-based workflows (Flux.2 Dev/Klein, Qwen-Image-Edit/2511/Layered, Z-Image Base/Turbo, Ernie Image, WAN2.2, Mage-Flow, LongCat, Boogu, HiDream E1, FireRed, Krea-2 Turbo); node types covered: UNETLoader, UnetLoaderGGUF, UNETLoaderGGUF (e.g. HiDream GGUF), CLIPLoader, DualCLIPLoader, TripleCLIPLoader, QuadrupleCLIPLoader (e.g. HiDream 4-CLIP)
- **LoRA extraction** — lists all LoRA models with `strength_model / strength_clip` values
- **Prompt extraction** — lists prompts with POS / NEG badges when positive/negative can be determined; when distinction is not possible (e.g. `SamplerCustomAdvanced`, intermediate nodes, cross-level connections), prompts are shown without a badge as plain **Text**; click any entry to view the full text below
- **CLIP Text Encode edit+ support** (v0.3.92) — `CLIPTextEncodeEditPlus` nodes are resolved using the same RAW / EDIT / front / back combination rule as the node itself, correctly merging the linked `text1` (e.g. a Lora Manager's trigger words) with the editable `text_edit` field instead of showing only one side
- **Prompt actions** — Copy to clipboard, **GenUI:P/N** (set GenerateUI positive/negative prompt), **Prompt:P/N** (set Prompt tab preset positive/negative)
- **Format support** — ComfyUI PNG/WebP/JSON (standard + Flux.2 / Qwen-Image / Z-Image / Ernie Image / WAN2.2 / Mage-Flow / LongCat / Boogu / HiDream E1 / FireRed / Krea-2 Turbo subgraph workflows), SD WebUI, SD Forge, Fooocus
- **Format note** — supported formats and covered model types are always shown in the left column
- **Form / Table toggle** (v0.4.6) — two top-level tabs at the top of Prompt Tab: **Form** is the one-item-at-a-time editor described above; **Table** shows the same Presets / Wildcards / Style data all at once as editable spreadsheets, useful once there are more than a handful of items to manage
- **Table view — Presets / Presets Group / Wildcards / Style** (v0.4.6) — four sub-tabs, each an editable table built from the exact same data as the Form tab above (changes on either side stay in sync)
- **Row selection & bulk delete** — every table shares a left-most auto-numbered **#** column; click a number to select that row (multiple at once), then use the toolbar's **Delete** button to remove every selected row in one confirm, or **Deselect** to clear the selection — replacing per-row delete buttons, which were too easy to misclick
- **Double-click to edit in a modal** — double-clicking a row's number opens a larger prompt-editing modal instead of the small in-table textarea; Presets/Style show Positive/Negative toggle buttons above one shared textarea (switching preserves unsaved text on the other side), Wildcards shows just the file content; Save writes back and closes the modal, Close discards changes
- **Wildcard syntax toolbar in the Presets modal** (v0.4.7) — the same `{|}` / `__|__` / `<lora>` / file-picker toolbar as the Form tab's Wildcard composer appears above the Presets modal's textarea, since that's where a wildcard reference typically ends up; not shown in the Wildcards or Style modals (the Wildcard modal edits the raw file content itself, and Style prompts have no such wildcard-composer connection)
- **+ New row** — the toolbar's **+ New ...** button adds a blank editable row with small ✓ (Add) / ✕ (Cancel) icons in place of its number
- **Presets table** — **★** (favorite) and **B** (Batch group) columns work as toggle switches, same as the Form tab's Preset Manager; the toolbar's **BC** button clears the whole Batch group at once; **PP Copy** / **NP Copy** copy the single selected row's positive/negative prompt to the clipboard; a **group select + Add to Group** control bulk-adds every selected preset to a chosen group in one click — searching/selecting from the table itself (rather than a long per-group preset picker) keeps group assignment usable once there are many presets
- **★ / B sort toggles** (v0.4.7) — toolbar buttons next to the search box bring favorited and/or batch-selected presets to the top of the list; both can be enabled together (★ takes priority, B breaks ties), and everything else keeps its original relative order; toggling a row's own ★/B while a sort is active re-sorts immediately
- **Presets Group table** — create, rename, and delete groups, with each group's members shown as removable badges; the reserved **Batch** group is not listed here — it's managed entirely through the Presets table's B column and BC button instead
- **Wildcards table** — same files as the Form tab's Wildcard file manager above; editing the File path or the Ext dropdown renames the file (saves the content under the new name, then removes the old file)
- **Style table** — same styles as the Form tab's Style manager above, editable inline the same way as Presets
</details>
@@ -235,7 +235,7 @@ An experimental batch generator: runs the workflow currently loaded in GenerateU
<details>
<summary><h3>Gallery Tab (v0.3.44)</h3></summary>
- **Output / Style-Prompt sub-tabs** (v0.3.94) — the Gallery tab has two sub-tabs at the top: **Output** (the image browser described below) and **Style/Prompt** (a separate visual prompt-building library — see its own section below)
- **Output / ImagePrompt / Style_Catalog / Metadata sub-tabs** (v0.3.94, v0.3.95, v0.4.6) — four sub-tabs at the top of the Gallery tab: **Output** (the image browser described below), **ImagePrompt** and **Style_Catalog** (separate visual libraries — see their own sections below), and **Metadata** (embeds the full Metadata tab — see its own section below)
- **Image browser** — browse ComfyUI output images (Thumbnail / Table views) with server-side scanning optimized for 6,000+ image libraries
- **Folder tree root label** (v0.3.90) — the root entry in the folder tree is labeled simply **[root]**, no longer suffixed with the scanned folder's own directory name (which could be a misleading, environment-specific name — e.g. StabilityMatrix installs where the output folder is a symlink named `Text2Img` — even though the folder holds every kind of generated output, not just text-to-image results)
- **Thumbnail / Table views** — switch view modes; Favorites column shown leftmost in Table view
@@ -249,7 +249,8 @@ An experimental batch generator: runs the workflow currently loaded in GenerateU
- **Group management** — create, rename, delete groups and assign/remove images using the same 4-section panel as Models tab; **`__Feeder__`** is a reserved group (🔒 prefix) that cannot be renamed or deleted; used by the Feeder Gallery mode to define the generation queue; **FC** button in the toolbar clears all members without deleting the group
- **Thumbnail F button** — cyan overlay button (top-left of each image card) toggles the image's membership in the `__Feeder__` group; cyan and always-visible when the image is in the group; visible on hover only when inactive
- **Favorites** — star images inline without reopening the detail panel
- **Detail panel** — view filename, path, tags, groups, and metadata in a slide-out panel
- **Detail panel** — view filename, path, tags, groups, and metadata in a slide-out panel; four tabs — **Info** / **JSON** / **Groups** / **Prompt**
- **Prompt tab** (v0.4.7) — selected by default (the most frequently checked piece of info shouldn't need an extra click); lists every positive/negative prompt candidate found in the embedded workflow with POS/NEG badges, reusing the exact same list→click→full-text UI as the Metadata tab; extracts from the API-format embedding when available rather than the UI-format one, since a workflow with independent top-level and subgraph sampling chains (e.g. Krea-2) can otherwise have some of its prompts missed
- **Workflow viewer** — Metadata tab displays workflow JSON from PNG embedded data (`prompt` / `workflow` keys) or from workflow saved by the Generate UI tab; **Copy & Send Canvas** button copies the JSON to the clipboard and sends the workflow directly to the ComfyUI canvas (UI and API formats both supported)
- **Load GenUI button** — loads the embedded ComfyUI workflow from the selected image directly into the GenerateUI tab; shows a warning toast if no workflow is embedded or the format is unsupported; Metadata button is styled green, Load GenUI button uses the primary accent color
- **Image Edit button** — toolbar button; sends the selected image directly to the Image Edit tab as the base layer
@@ -288,6 +289,22 @@ An experimental batch generator: runs the workflow currently loaded in GenerateU
</details>
<details>
<summary><h3>Metadata Gallery subtab (v0.3.8, moved from a top-level tab in v0.4.6)</h3></summary>
- **Now inside Gallery** (v0.4.6) — this used to be its own top-level tab; it's now the fourth Gallery sub-tab, to the right of Style_Catalog — everything below works exactly the same as before
- **3-column layout** — Drop zone (left) | Model info (center) | LoRA + Prompt (right)
- **File drop** — drop a ComfyUI-generated PNG / WebP or workflow JSON onto the drop zone (or click to open a file picker); PNG/WebP images are shown as a preview
- **Model extraction** — automatically extracts Checkpoint, VAE, Diffusion Model, and Text Encoder names from the workflow; supports both standard and subgraph-based workflows (Flux.2 Dev/Klein, Qwen-Image-Edit/2511/Layered, Z-Image Base/Turbo, Ernie Image, WAN2.2, Mage-Flow, LongCat, Boogu, HiDream E1, FireRed, Krea-2 Turbo); node types covered: UNETLoader, UnetLoaderGGUF, UNETLoaderGGUF (e.g. HiDream GGUF), CLIPLoader, DualCLIPLoader, TripleCLIPLoader, QuadrupleCLIPLoader (e.g. HiDream 4-CLIP)
- **LoRA extraction** — lists all LoRA models with `strength_model / strength_clip` values
- **Prompt extraction** — lists prompts with POS / NEG badges when positive/negative can be determined; when distinction is not possible (e.g. `SamplerCustomAdvanced`, intermediate nodes, cross-level connections), prompts are shown without a badge as plain **Text**; click any entry to view the full text below
- **CLIP Text Encode edit+ support** (v0.3.92) — `CLIPTextEncodeEditPlus` nodes are resolved using the same RAW / EDIT / front / back combination rule as the node itself, correctly merging the linked `text1` (e.g. a Lora Manager's trigger words) with the editable `text_edit` field instead of showing only one side
- **Prompt actions** — Copy to clipboard, **GenUI:P/N** (set GenerateUI positive/negative prompt), **Prompt:P/N** (set Prompt tab preset positive/negative)
- **Format support** — ComfyUI PNG/WebP/JSON (standard + Flux.2 / Qwen-Image / Z-Image / Ernie Image / WAN2.2 / Mage-Flow / LongCat / Boogu / HiDream E1 / FireRed / Krea-2 Turbo subgraph workflows), SD WebUI, SD Forge, Fooocus
- **Format note** — supported formats and covered model types are always shown in the left column
</details>
<details>
<summary><h3>Nodes Tab (v0.1.7)</h3></summary>
@@ -379,6 +396,7 @@ An experimental batch generator: runs the workflow currently loaded in GenerateU
- **Reliability fix** (v0.3.98) — switched from a raw single-string prompt to a system-role chat request (Ollama `/api/chat`, LM Studio/Lemonade `/v1/chat/completions` with a `system` message) so small local models follow the "translation only" instruction more reliably; output is cleaned of common LLM preambles/quotes, and a warning toast appears if the model appears to have echoed the source text unchanged instead of translating it
- **Chat pane** (v0.3.40) — multi-turn conversation with the LLM; full conversation history sent each turn for context; Enter to send, Shift+Enter for a newline; Clear button resets history; Ollama uses `/api/chat`, LM Studio, Lemonade, and Unsloth use `/v1/chat/completions`
- **Chat pane — image generation via Tool Calling** (v0.3.73) — if the selected model supports tool calling and the user asks for an image, a `generate_image` tool is invoked and the request runs through the same generation pipeline as the GenerateUI Generate button, with the result shown inline in the chat; an "Allow image generation" checkbox in the input row toggles this on/off (default on); the workflow used is the one currently loaded in GenerateUI, or a dedicated saved workflow configured in the Settings pane; models verified to work well for tool calling and vision: **Gemma 4:e4b**, **Qwen 3.5:9b**, **Ministral 3:3b** (Ollama)
- **Chat pane — generate_image extra parameters** (v0.4.4) — besides `prompt`/`negative_prompt`, the tool call also accepts optional `steps`, `cfg`, `sampler_name`, `scheduler`, `denoise`, `seed`, `width`, `height`, `batch_size` (number of images produced in that one call), and `aspect_ratio` (a ratio preset like `3:2`, resolved against the node's actual COMBO options via `/object_info` and applied only if the workflow has a **Resolution Selector** node feeding width/height — ignored otherwise); anything the model doesn't supply keeps the loaded workflow's own current setting; `batch_size` > 1 shows every generated image inline in the same chat bubble
- **TOOLS pane (VLM)** — drop an image into the 110px drop zone, select a task (Describe Image / Create Prompt / Create Tags), and click Run to analyze with a vision model; result shown in the output area with a Copy button
- **TOOLS pane — shared Chat attachment** (v0.3.74) — an image dropped in the TOOLS drop zone is also usable as the Chat pane's attachment: it's sent to the LLM as vision input, and used as the base image when `generate_image` runs image-to-image; a ✕ button clears it (from either the TOOLS preview or the Chat attachment indicator); it stays attached across turns until explicitly cleared
- **TOOLS pane (Wildcards)** (v0.3.40) — select "Create wildcards" from the task dropdown; enter a category name and count; click Run to generate plain-text wildcard entries one per line (no markdown, no numbering); result can be copied directly into wildcard `.txt` files
@@ -415,7 +433,7 @@ An experimental batch generator: runs the workflow currently loaded in GenerateU
- **Drag & drop workflows** — drag a workflow onto the canvas to load it
- **Drag & drop nodes** — drag nodes/node sets onto the canvas to place them
- **Drag & drop prompts** — drag a preset onto the canvas to create a WFS_PromptText node with positive/negative prompts
- **Send to Canvas** — clicking "Send to Canvas" (Workflow tab toolbar / detail modal) or "Copy & Send Canvas" (Gallery tab JSON panel) sends the workflow directly to the ComfyUI canvas via `window.opener` (UI and API formats both supported); if Workflow Studio was opened from a bookmark instead of the ComfyUI toolbar, UI-format workflows fall back to title drag (panel title highlights blue with a green ● — drag it onto the canvas to load)
- **Send to Canvas** — clicking "Send to Canvas" (Workflow tab toolbar / detail modal) sends the workflow directly to the ComfyUI canvas via `window.opener` when available (UI and API formats both supported); if Workflow Studio was opened from a bookmark instead of the ComfyUI toolbar (or in another tab), it filters the Library's **W** tab down to that workflow's card instead (v0.4.7) — drag the card onto the canvas yourself; this replaced the old title-drag fallback, which had no thumbnail and didn't work for API-format workflows at all. "Copy & Send Canvas" (Gallery tab JSON panel) still uses only `window.opener` + title-drag for UI-format workflows, since the embedded workflow it sends isn't a saved Library file to filter down to
- **Copy prompts** — copy individual positive (P) or negative (N) prompts from sidebar items
- **Double-click** — load workflows or place nodes without dragging
- **Search** — search within each sub-tab to quickly find items
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+86194 -86171
View File
File diff suppressed because it is too large Load Diff
+570 -546
View File
File diff suppressed because it is too large Load Diff
+9 -3
View File
@@ -163,9 +163,15 @@ async def get_image_workflow(request: web.Request) -> web.Response:
return web.json_response({"error": "path required"}, status=400)
try:
wf = _service.extract_workflow_from_metadata(path)
if wf is None:
return web.json_response({"workflow": None, "has_workflow": False})
return web.json_response({"workflow": wf, "has_workflow": True})
# prompt_workflow: プロンプト抽出専用(PNG[prompt]優先)。トップレベルとサブグラフに
# 独立した複数系統を持つワークフローではUI形式(workflow)からの抽出がトップレベル
# 系統しか拾えないことがあるため、Promptタブはこちらを使う(Metadataタブと揃える)。
prompt_wf = _service.extract_prompt_workflow_from_metadata(path)
return web.json_response({
"workflow": wf,
"has_workflow": wf is not None,
"prompt_workflow": prompt_wf,
})
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
+31 -20
View File
@@ -682,9 +682,15 @@ class GalleryService:
logger.debug("JPEG metadata read error: %s", e)
return result
def extract_workflow_from_metadata(self, image_path: str) -> dict | None:
"""ワークフローを抽出する。
優先順位: PNG[workflow] > PNG[prompt] > gallery_metadata.json[workflow]
def _extract_embedded_workflow(self, image_path: str, prefer: str = "workflow") -> dict | None:
"""埋め込みワークフローを抽出する(PNG[workflow]/PNG[prompt]の優先順位を選べる)。
prefer="workflow": UI形式(ノード位置・グループ等を保持)優先。GenerateUIへの
ロードやJSON表示などエディタ復元が必要な用途向け。
prefer="prompt": API形式(サブグラフ展開済みでフラット)優先。トップレベルと
サブグラフに独立した複数系統を持つワークフロー(例: Krea2)ではUI形式の
extractPromptsLiteGraphがトップレベル系統しか拾えないことがあるため、
プロンプト抽出専用にAPI形式を優先する(Metadataタブのプロンプト抽出と同じ優先順位)。
"""
path = Path(image_path).resolve()
if not self._check_path_allowed(path):
@@ -692,27 +698,32 @@ class GalleryService:
if path.suffix.lower() == ".png":
embedded = self._read_png_metadata(path)
# 1. workflow キー (ComfyUI UI形式)
for key in ("workflow", "Workflow"):
s = embedded.get(key)
if s:
try:
return json.loads(s)
except json.JSONDecodeError:
pass
# 2. prompt キー (ComfyUI API形式 — 大多数の生成画像)
for key in ("prompt", "Prompt"):
s = embedded.get(key)
if s:
try:
return json.loads(s)
except json.JSONDecodeError:
pass
key_groups = (("workflow", "Workflow"), ("prompt", "Prompt"))
if prefer == "prompt":
key_groups = tuple(reversed(key_groups))
for keys in key_groups:
for key in keys:
s = embedded.get(key)
if s:
try:
return json.loads(s)
except json.JSONDecodeError:
pass
# 3. gallery_metadata.json に保存されたworkflow
# gallery_metadata.json に保存されたworkflow
saved = self.metadata_store.get(str(path))
return saved.get("workflow") or None
def extract_workflow_from_metadata(self, image_path: str) -> dict | None:
"""ワークフローを抽出する。
優先順位: PNG[workflow] > PNG[prompt] > gallery_metadata.json[workflow]
"""
return self._extract_embedded_workflow(image_path, prefer="workflow")
def extract_prompt_workflow_from_metadata(self, image_path: str) -> dict | None:
"""プロンプト抽出専用: PNG[prompt] > PNG[workflow] > gallery_metadata.json[workflow]"""
return self._extract_embedded_workflow(image_path, prefer="prompt")
# ──────────────────────────────────────────────────────────────
# メタデータ保存
# ──────────────────────────────────────────────────────────────
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-workflow-studio"
description = "A comprehensive workflow, asset management, and generation UI plugin for ComfyUI. Manage workflows, models, images, and prompts; batch generation and Image Feeder in the GenerateUI tab; library side panel with canvas drag & drop and AI tools."
version = "0.4.2"
version = "0.4.7"
license = "MIT"
requires-python = ">= 3.10"
dependencies = ["jinja2"]
+20
View File
@@ -270,6 +270,16 @@
color: var(--wfm-warning);
}
/* LoRA node-bypass (strength forced to 0 at apply-time) — red to distinguish from the
amber row-level bypassed above; the row itself still applies, only the LoRA doesn't. */
.wfm-lab-cell.wfm-lab-cell-node-bypassed {
border-color: var(--wfm-danger);
}
.wfm-lab-cell.wfm-lab-cell-node-bypassed .wfm-lab-cell-value {
color: var(--wfm-danger);
}
.wfm-lab-cell-applied {
margin-top: 3px;
padding-top: 3px;
@@ -278,6 +288,16 @@
color: var(--wfm-text-secondary);
}
/* Shown instead of a keyframe list when the loaded workflow has no node this Model
sub-column (checkpoint/lora/vae) could override — see _hasNodeForColumn in lab-tab.js. */
.wfm-lab-cell-empty {
padding: 6px 8px;
font-size: 11px;
font-style: italic;
color: var(--wfm-text-secondary);
cursor: default;
}
.wfm-lab-cell-prompt-line {
display: block;
overflow: hidden;
+258 -6
View File
@@ -653,8 +653,8 @@ body {
align-items: center;
}
.wfm-lora-bypass-btn,
.wfm-lora-refresh-btn {
margin-left: auto;
font-size: 13px;
line-height: 1;
padding: 2px 5px;
@@ -662,8 +662,21 @@ body {
text-transform: none;
letter-spacing: 0;
}
.wfm-lora-bypass-btn {
margin-left: auto;
}
.wfm-lora-bypass-btn:hover,
.wfm-lora-refresh-btn:hover { opacity: 1; }
/* Bypass is active for the current LoRA target node: strength forced to 0 (see
comfyui-editor.js renderLoraPane / _loraNodeBypass). */
.wfm-lora-bypass-btn.active {
opacity: 1;
background: var(--wfm-danger);
border-color: var(--wfm-danger);
color: #fff;
}
.wfm-gen-lora-col-body {
flex: 1;
overflow-y: auto;
@@ -2374,6 +2387,248 @@ body {
overflow: hidden;
}
/* Prompt タブ: Form / Table 切り替え */
.wfm-prompt-toplevel-nav {
display: flex;
border-bottom: 1px solid var(--wfm-border);
flex-shrink: 0;
}
.wfm-prompt-toplevel-tab {
padding: 6px 16px;
font-size: 12px;
background: transparent;
border: none;
color: var(--wfm-text-secondary);
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: color 0.15s;
}
.wfm-prompt-toplevel-tab:hover { color: var(--wfm-text-primary); }
.wfm-prompt-toplevel-tab.active { color: var(--wfm-primary); border-bottom-color: var(--wfm-primary); }
.wfm-prompt-panel {
display: none;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
}
.wfm-prompt-panel.active { display: flex; }
/* Table パネル内サブタブ (Presets / Wildcards / Style) */
.wfm-prompt-table-subnav {
display: flex;
background: var(--wfm-surface);
border-bottom: 1px solid var(--wfm-border);
flex-shrink: 0;
}
.wfm-prompt-table-subtab {
padding: 8px 16px;
background: none;
border: none;
border-bottom: 2px solid transparent;
color: var(--wfm-text-secondary);
font-size: 13px;
font-weight: bold;
cursor: pointer;
transition: all 0.15s;
}
.wfm-prompt-table-subtab:hover { color: var(--wfm-text); }
.wfm-prompt-table-subtab.active { color: var(--wfm-text); border-bottom-color: var(--wfm-primary); }
.wfm-prompt-table-pane {
display: none;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
}
.wfm-prompt-table-pane.active { display: flex; }
.wfm-prompt-table-toolbar {
display: flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
padding: 8px;
flex-shrink: 0;
border-bottom: 1px solid var(--wfm-border);
}
.wfm-prompt-table-scroll {
flex: 1;
min-height: 0;
overflow: auto;
}
/* 編集可能テーブル (Presets / Wildcards / Style 共通) */
.wfm-prompt-edit-table {
width: 100%;
border-collapse: collapse;
}
.wfm-prompt-edit-table th,
.wfm-prompt-edit-table td {
border-bottom: 1px solid var(--wfm-border);
padding: 4px 6px;
vertical-align: top;
}
.wfm-prompt-edit-table th {
position: sticky;
top: 0;
background: var(--wfm-surface);
text-align: left;
font-size: 11px;
color: var(--wfm-text-secondary);
z-index: 1;
}
.wfm-prompt-edit-table tbody tr:hover td {
background: var(--wfm-surface);
}
.wfm-prompt-edit-input,
.wfm-prompt-edit-textarea {
width: 100%;
box-sizing: border-box;
background: transparent;
border: 1px solid transparent;
border-radius: 3px;
color: var(--wfm-text);
font-size: 12px;
padding: 3px 5px;
font-family: inherit;
}
.wfm-prompt-edit-textarea {
min-height: 40px;
resize: vertical;
}
.wfm-prompt-edit-input:hover,
.wfm-prompt-edit-textarea:hover {
border-color: var(--wfm-border);
}
.wfm-prompt-edit-input:focus,
.wfm-prompt-edit-textarea:focus {
outline: none;
border-color: var(--wfm-primary);
background: var(--wfm-bg);
}
@keyframes wfm-prompt-cell-flash {
0% { background: rgba(74, 158, 255, 0.3); }
100% { background: transparent; }
}
.wfm-prompt-edit-cell-saved {
animation: wfm-prompt-cell-flash 0.7s ease;
}
.wfm-prompt-edit-fav-btn {
background: none;
border: none;
cursor: pointer;
font-size: 14px;
color: var(--wfm-text-secondary);
padding: 2px;
line-height: 1;
}
.wfm-prompt-edit-fav-btn.active { color: #ffd700; }
.wfm-prompt-edit-batch-btn.active { color: #f0c040; }
.wfm-prompt-edit-row-new td { background: rgba(74, 158, 255, 0.06); }
.wfm-prompt-edit-file-badge {
font-size: 10px;
color: var(--wfm-text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
}
.wfm-prompt-edit-empty {
padding: 24px;
text-align: center;
color: var(--wfm-text-secondary);
font-size: 12px;
}
/* ダブルクリックで開くプロンプト編集モーダル (Presets / Wildcards / Style 共通) */
.wfm-prompt-modal-toggle {
display: flex;
gap: 6px;
}
.wfm-prompt-modal-textarea {
width: 100%;
box-sizing: border-box;
min-height: 320px;
resize: vertical;
margin-top: 8px;
}
.wfm-prompt-modal-actions {
display: flex;
gap: 6px;
justify-content: flex-end;
margin-top: 10px;
}
/* #(自動採番)列: クリックで行選択 → 上部の Delete ボタンで一括削除 */
.wfm-prompt-edit-num-cell {
text-align: center;
cursor: pointer;
-webkit-user-select: none;
user-select: none;
color: var(--wfm-text-secondary);
font-size: 11px;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.wfm-prompt-edit-num-cell:hover {
color: var(--wfm-text);
background: var(--wfm-surface-2, var(--wfm-surface));
}
.wfm-prompt-edit-table tbody tr.wfm-prompt-edit-row-selected td {
background: rgba(255, 69, 58, 0.14);
}
.wfm-prompt-edit-table tbody tr.wfm-prompt-edit-row-selected .wfm-prompt-edit-num-cell {
color: var(--wfm-danger);
font-weight: bold;
}
.wfm-prompt-edit-num-cell-new {
display: flex;
gap: 2px;
justify-content: center;
}
.wfm-prompt-edit-inline-add-btn,
.wfm-prompt-edit-inline-cancel-btn {
background: none;
border: none;
cursor: pointer;
font-size: 12px;
padding: 2px 3px;
line-height: 1;
border-radius: 3px;
}
.wfm-prompt-edit-inline-add-btn { color: var(--wfm-primary); }
.wfm-prompt-edit-inline-add-btn:hover { background: var(--wfm-primary); color: #fff; }
.wfm-prompt-edit-inline-cancel-btn { color: var(--wfm-text-secondary); }
.wfm-prompt-edit-inline-cancel-btn:hover { color: var(--wfm-danger); }
/* Presets Group テーブル: プリセットバッジ */
.wfm-prompt-edit-preset-badge {
display: inline-flex;
align-items: center;
gap: 3px;
background: var(--wfm-surface);
border: 1px solid var(--wfm-border);
border-radius: 10px;
padding: 2px 4px 2px 8px;
font-size: 11px;
margin: 2px 3px 2px 0;
}
.wfm-prompt-edit-badge-remove {
background: none;
border: none;
cursor: pointer;
color: var(--wfm-text-secondary);
font-size: 10px;
padding: 1px 3px;
line-height: 1;
}
.wfm-prompt-edit-badge-remove:hover { color: var(--wfm-danger); }
/* Ollama Assistant */
.wfm-ollama-assistant {
display: flex;
@@ -5018,15 +5273,12 @@ body {
Metadata Tab
============================================ */
#wfm-tab-metadata {
overflow: hidden;
}
.wfm-metadata-layout {
display: grid;
grid-template-columns: 220px 1fr 1fr;
gap: 0;
height: 100%;
flex: 1;
min-height: 0;
overflow: hidden;
}
+37 -7
View File
@@ -548,12 +548,22 @@ const IMAGE_GEN_TOOLS = [{
type: "function",
function: {
name: "generate_image",
description: "Generate an image using the currently loaded ComfyUI workflow. If an image is currently attached to the conversation, this performs image-to-image generation using that image as the base; otherwise it performs text-to-image. Call this when the user asks to create, draw, paint, edit, or transform an image.",
description: "Generate an image using the currently loaded ComfyUI workflow. If an image is currently attached to the conversation, this performs image-to-image generation using that image as the base; otherwise it performs text-to-image. Call this when the user asks to create, draw, paint, edit, or transform an image. All parameters besides prompt are optional — omit any the user didn't specify and the workflow's own current setting is kept.",
parameters: {
type: "object",
properties: {
prompt: { type: "string", description: "Positive prompt describing the desired image." },
negative_prompt: { type: "string", description: "What to avoid in the image. Optional." },
steps: { type: "integer", description: "Sampling steps. Optional." },
cfg: { type: "number", description: "Classifier-free guidance scale. Optional." },
sampler_name: { type: "string", description: "ComfyUI sampler name (e.g. euler, dpmpp_2m, ddim). Optional." },
scheduler: { type: "string", description: "ComfyUI scheduler name (e.g. normal, karras, simple). Optional." },
denoise: { type: "number", description: "Denoise strength 0-1 (mainly relevant for image-to-image). Optional." },
seed: { type: "integer", description: "Fixed seed. Omit for a random seed each time. Optional." },
width: { type: "integer", description: "Output image width in pixels. Ignored if the workflow has a Resolution Selector node — use aspect_ratio instead for those. Optional." },
height: { type: "integer", description: "Output image height in pixels. Ignored if the workflow has a Resolution Selector node — use aspect_ratio instead for those. Optional." },
aspect_ratio: { type: "string", enum: ["1:1", "2:3", "3:2", "3:4", "4:3", "9:16", "16:9", "21:9"], description: "Aspect ratio preset. Only applied if the workflow has a Resolution Selector node (feeding width/height from a ratio + megapixel target); ignored otherwise. Optional." },
batch_size: { type: "integer", description: "Number of images to generate in this single call. Optional." },
},
required: ["prompt"],
},
@@ -635,7 +645,7 @@ async function _loadDedicatedChatGenWorkflow(filename) {
return { workflow, analysis: comfyWorkflow.analyzeWorkflow(workflow) };
}
async function generateImageFromChat(prompt, negativePrompt) {
async function generateImageFromChat(prompt, negativePrompt, params = {}) {
if (!window._wfmGenerateTab?.generate) throw new Error(t("aiChatGenerateNotReady"));
const settings = loadAiSettings();
@@ -660,12 +670,24 @@ async function generateImageFromChat(prompt, negativePrompt) {
comfyEditor.setPromptText("positive", prompt || "", { workflow, analysis });
if (negativePrompt) comfyEditor.setPromptText("negative", negativePrompt, { workflow, analysis });
const result = await window._wfmGenerateTab.generate(workflow);
const { steps, cfg, sampler_name, scheduler, denoise, seed, width, height, batch_size, aspect_ratio } = params;
let resolvedAspectRatio = null;
const resSelector = analysis.resolution_selector_nodes?.[0];
if (aspect_ratio && resSelector) {
resolvedAspectRatio = await comfyEditor.resolveAspectRatioOption(resSelector.type, aspect_ratio);
}
comfyEditor.setGenerationParams({
steps, cfg, sampler_name, scheduler, denoise, seed, width, height, batch_size,
resolvedAspectRatio, workflow, analysis,
});
const result = await window._wfmGenerateTab.generate(workflow, seed != null ? { seedValue: seed } : {});
const images = result?.images || [];
if (images.length === 0) throw new Error(t("aiChatNoImageProduced"));
const img = images[0];
return `/view?filename=${encodeURIComponent(img.filename)}&subfolder=${encodeURIComponent(img.subfolder || "")}&type=${encodeURIComponent(img.type || "output")}`;
return images.map((img) =>
`/view?filename=${encodeURIComponent(img.filename)}&subfolder=${encodeURIComponent(img.subfolder || "")}&type=${encodeURIComponent(img.type || "output")}`
);
}
// アシスタント応答からSVGコードを抽出する(```svg フェンス or 生の <svg>...</svg>)
@@ -966,6 +988,14 @@ function appendChatBubble(messagesEl, role, content, opts = {}) {
img.className = "wfm-ai-chat-img";
div.appendChild(img);
}
if (opts.imageUrls?.length) {
opts.imageUrls.forEach((url) => {
const img = document.createElement("img");
img.src = url;
img.className = "wfm-ai-chat-img";
div.appendChild(img);
});
}
if (opts.attachThumb) {
const img = document.createElement("img");
img.src = opts.attachThumb;
@@ -1055,8 +1085,8 @@ function initChatTab() {
statusEl.textContent = t("aiStatusGeneratingImage");
const args = _parseToolArgs(toolCall);
try {
const imageUrl = await generateImageFromChat(args.prompt, args.negative_prompt);
appendChatBubble(messagesEl, "assistant", "", { imageUrl });
const imageUrls = await generateImageFromChat(args.prompt, args.negative_prompt, args);
appendChatBubble(messagesEl, "assistant", "", { imageUrls });
} catch (genErr) {
appendChatBubble(messagesEl, "assistant", `${t("aiToastImageGenFailed")}${genErr.message}`);
showToast(t("aiToastImageGenFailed") + genErr.message, "error");
+43 -2
View File
@@ -42,11 +42,32 @@ export function showToast(message, type = "info", duration = 3000) {
export function openModal(title, contentHtml) {
const overlay = document.getElementById("wfm-modal-overlay");
const titleEl = document.getElementById("wfm-modal-title");
let titleEl = document.getElementById("wfm-modal-title");
const bodyEl = document.getElementById("wfm-modal-body");
const titleInput = document.getElementById("wfm-modal-title-input");
if (!overlay || !titleEl || !bodyEl) return;
// This modal shell is shared by every caller (Workflow/Models/Nodes detail
// modals, Lab's keyframe editor, etc.). Some callers (e.g. workflow-tab.js's
// openDetailModal) inject extra header chrome after calling openModal() — a
// favorite star button, and a click-to-rename listener + tooltip/cursor on the
// title — but nothing ever tore that down again. The next caller to reuse this
// same DOM inherited it as stale leftovers: a Lab cell modal could show a
// favorite star wired to the PREVIOUS caller's workflow object, and clicking it
// silently toggled that unrelated workflow's favorite state. Clean slate every
// time, before any caller-specific chrome is added back on top.
titleEl.parentElement?.querySelectorAll(".wfm-fav-btn").forEach((el) => el.remove());
const freshTitleEl = titleEl.cloneNode(false); // drops any addEventListener from a prior caller
freshTitleEl.removeAttribute("title");
freshTitleEl.removeAttribute("style"); // also clears any leftover display:none from an interrupted rename
titleEl.replaceWith(freshTitleEl);
titleEl = freshTitleEl;
if (titleInput) {
titleInput.style.display = "none";
titleInput.value = "";
}
titleEl.textContent = title;
bodyEl.innerHTML = contentHtml;
overlay.style.display = "flex";
@@ -91,7 +112,7 @@ function initTabs() {
function applyI18nToHtml() {
// Tab labels
const tabMap = { workflow: "tabWorkflow", nodes: "tabNodes", models: "tabModels", generate: "tabGenerate", prompt: "tabPrompt", metadata: "tabMetadata", gallery: "tabGallery", "image-edit": "tabImageEdit", settings: "tabSettings", help: "tabHelp", ai: "tabAi", tagger: "tabTagger" };
const tabMap = { workflow: "tabWorkflow", nodes: "tabNodes", models: "tabModels", generate: "tabGenerate", prompt: "tabPrompt", gallery: "tabGallery", "image-edit": "tabImageEdit", settings: "tabSettings", help: "tabHelp", ai: "tabAi", tagger: "tabTagger" };
document.querySelectorAll(".wfm-tab").forEach((tab) => {
const key = tabMap[tab.dataset.tab];
if (key) tab.textContent = t(key);
@@ -195,6 +216,7 @@ function applyI18nToHtml() {
if (btn.dataset.detailTab === "info") btn.textContent = t("modelsSideInfo");
if (btn.dataset.detailTab === "meta") btn.textContent = t("gallerySideMetadata");
if (btn.dataset.detailTab === "group") btn.textContent = t("modelsSideGroup");
if (btn.dataset.detailTab === "prompt") btn.textContent = t("gallerySidePrompt");
});
const galleryBulkDeselect = document.getElementById("wfm-gallery-bulk-deselect");
if (galleryBulkDeselect) galleryBulkDeselect.textContent = t("galleryBulkDeselectAll");
@@ -229,6 +251,8 @@ function applyI18nToHtml() {
if (galleryImagePromptSubtab) galleryImagePromptSubtab.textContent = t("galleryTabImagePrompt");
const galleryStyleCatalogSubtab = document.getElementById("wfm-gallery-subtab-styleCatalog");
if (galleryStyleCatalogSubtab) galleryStyleCatalogSubtab.textContent = t("galleryTabStyleCatalog");
const galleryMetadataSubtab = document.getElementById("wfm-gallery-subtab-metadata");
if (galleryMetadataSubtab) galleryMetadataSubtab.textContent = t("galleryTabMetadata");
// ImagePrompt ギャラリーパネル
const imagePromptSearch = document.getElementById("wfm-imageprompt-search");
@@ -455,6 +479,8 @@ function applyI18nToHtml() {
"wfm-help-gen-24": "helpGen24", "wfm-help-gen-25": "helpGen25",
"wfm-help-gen-26": "helpGen26", "wfm-help-gen-27": "helpGen27",
"wfm-help-gen-28": "helpGen28",
"wfm-help-gen-29": "helpGen29",
"wfm-help-gen-30": "helpGen30",
"wfm-help-feeder-title": "helpFeederTitle",
"wfm-help-feeder-desc": "helpFeederDesc",
"wfm-help-feeder-imgloop-title": "helpFeederImgloopTitle",
@@ -480,6 +506,7 @@ function applyI18nToHtml() {
"wfm-help-lab-11": "helpLab11", "wfm-help-lab-12": "helpLab12",
"wfm-help-lab-13": "helpLab13", "wfm-help-lab-14": "helpLab14",
"wfm-help-lab-15": "helpLab15",
"wfm-help-lab-16": "helpLab16", "wfm-help-lab-17": "helpLab17",
"wfm-help-lab-card-plan-title": "helpLabCardPlanTitle",
"wfm-help-lab-plan-1": "helpLabPlan1", "wfm-help-lab-plan-2": "helpLabPlan2",
"wfm-help-lab-plan-3": "helpLabPlan3", "wfm-help-lab-plan-4": "helpLabPlan4",
@@ -499,6 +526,14 @@ function applyI18nToHtml() {
"wfm-help-prompt-10": "helpPrompt10", "wfm-help-prompt-11": "helpPrompt11",
"wfm-help-prompt-12": "helpPrompt12", "wfm-help-prompt-13": "helpPrompt13",
"wfm-help-prompt-14": "helpPrompt14",
"wfm-help-prompt-card-toplevel-title": "helpPromptCardToplevel",
"wfm-help-prompt-15": "helpPrompt15",
"wfm-help-prompt-card-table-title": "helpPromptCardTable",
"wfm-help-prompt-16": "helpPrompt16", "wfm-help-prompt-17": "helpPrompt17",
"wfm-help-prompt-18": "helpPrompt18", "wfm-help-prompt-19": "helpPrompt19",
"wfm-help-prompt-20": "helpPrompt20", "wfm-help-prompt-21": "helpPrompt21",
"wfm-help-prompt-22": "helpPrompt22", "wfm-help-prompt-23": "helpPrompt23",
"wfm-help-prompt-24": "helpPrompt24", "wfm-help-prompt-25": "helpPrompt25",
"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",
@@ -530,6 +565,7 @@ function applyI18nToHtml() {
"wfm-help-gallery-20": "helpGallery20", "wfm-help-gallery-21": "helpGallery21",
"wfm-help-gallery-22": "helpGallery22", "wfm-help-gallery-23": "helpGallery23",
"wfm-help-gallery-24": "helpGallery24", "wfm-help-gallery-25": "helpGallery25",
"wfm-help-gallery-26": "helpGallery26",
"wfm-help-gallery-card-styleprompt-title": "helpGalleryCardStyleprompt",
"wfm-help-gallery-card-stylecatalog-title": "helpGalleryCardStylecatalog",
"wfm-help-tagger-title": "helpTaggerTitle",
@@ -768,6 +804,7 @@ import { initWorkflowTab } from "./workflow-tab.js";
import { initNodesTab } from "./nodes-tab.js";
import { initGenerateTab } from "./generate-tab.js";
import { initPromptTab } from "./prompt-tab.js";
import { initPromptTableTab } from "./prompt-table.js";
import { initSettingsTab, applyTheme, getSavedTheme, applyTextareaFontSize, applyJsonColors, applyModelTabActiveColor } from "./settings-tab.js";
import { initModelsTab } from "./models-tab.js";
import { initGalleryTab } from "./gallery-tab.js";
@@ -802,6 +839,9 @@ function initHelpTab() {
document.querySelectorAll(".wfm-help-jump-btn").forEach((btn) => {
btn.addEventListener("click", () => {
document.querySelector(`.wfm-tab[data-tab="${btn.dataset.jumpTab}"]`)?.click();
if (btn.dataset.jumpSubtab) {
document.querySelector(`.wfm-gallery-subtab-btn[data-gallery-subtab="${btn.dataset.jumpSubtab}"]`)?.click();
}
});
});
}
@@ -817,6 +857,7 @@ document.addEventListener("DOMContentLoaded", () => {
initModelsTab();
initGenerateTab();
initPromptTab();
initPromptTableTab();
initMetadataTab();
initGalleryTab();
initAiTab();
+160
View File
@@ -6,6 +6,7 @@ import { comfyUI } from "./comfyui-client.js";
import { syncJsonHighlight } from "./json-highlight.js";
import { t } from "./i18n.js";
import { escapeHtml, setupSearchClearBtn } from "./util.js";
import { showToast } from "./app.js";
// ── Latent Image preset state ─────────────────────────────
const _LATENT_PRESET_KEY = "wfm_latent_presets";
@@ -156,6 +157,16 @@ function _refreshPresetSelect(customPresets) {
let _stackStrengths = {};
// { modelFullPath: boolean } — true = active (default)
let _stackActive = {};
// ── LoRA node-bypass state (GenerateUI Model tab header toggle) ───────────
// Force the LoRA node's strength to 0 without touching the actual ComfyUI node's mode —
// currentWorkflow is already-converted API format (see comfyui-workflow.js
// convertUiToApi), where mode has no effect at execution time, so a real Bypass(mode:4)
// can't be reproduced here; zeroing strength is the behavioral equivalent.
// { nodeId: boolean } — true = currently bypassed
let _loraNodeBypass = {};
// { nodeId: { loraName, strModel, strClip } | { loras, text } } — pre-bypass values to
// restore when toggled back off. Shape depends on isLoraManager at bypass-time.
let _loraBypassSnapshot = {};
// { id: string|null, selStart: number, selEnd: number }
let _lastPromptFocus = { id: null, selStart: 0, selEnd: 0 };
@@ -742,6 +753,63 @@ export const comfyEditor = {
_initTextEncoderSection(analysis, this.models.textEncoders || [], el).catch(() => {});
},
// Toggles the GenerateUI Model tab's LoRA column-header bypass button: forces the
// current target LoRA node's strength to 0 (both tabs' strength fields, whichever
// is currently in currentWorkflow), or restores whatever was there before. Targets
// the same node Single/Stack Apply default to (first LoraManager, else first LoRA
// node) so one button covers both tabs without needing its own node picker.
toggleLoraNodeBypass(analysis) {
const loraNodes = analysis?.lora_nodes || [];
let nodeId = (loraNodes.find((n) => n.is_lora_manager) || loraNodes[0])?.id;
let isLoraManager = !!loraNodes.find((n) => String(n.id) === String(nodeId))?.is_lora_manager;
if (nodeId == null) {
nodeId = Object.keys(comfyUI.currentWorkflow || {}).find((id) => comfyUI.currentWorkflow[id]?.inputs && "lora_name" in comfyUI.currentWorkflow[id].inputs);
isLoraManager = false;
}
const wfNode = nodeId != null ? comfyUI.currentWorkflow?.[nodeId] : null;
if (!wfNode) {
showToast(t("modelsGenUINoNode", "LoRA"), "warning");
return;
}
if (_loraNodeBypass[nodeId]) {
// Restore whatever was set before bypassing
const snap = _loraBypassSnapshot[nodeId];
if (snap) {
if (isLoraManager) {
wfNode.inputs.loras = { __value__: JSON.parse(JSON.stringify(snap.loras || [])) };
wfNode.inputs.text = snap.text ?? "";
} else {
wfNode.inputs.lora_name = snap.lora_name;
wfNode.inputs.strength_model = snap.strength_model;
wfNode.inputs.strength_clip = snap.strength_clip;
}
}
delete _loraNodeBypass[nodeId];
delete _loraBypassSnapshot[nodeId];
} else {
// Snapshot current values, then zero the strength (LoraManager: every
// entry in its loras list, keeping the list itself intact so un-bypassing
// restores the exact same Stack — not just a single-LoRA fallback).
if (isLoraManager) {
const loras = Array.isArray(wfNode.inputs.loras?.__value__) ? wfNode.inputs.loras.__value__ : [];
_loraBypassSnapshot[nodeId] = { loras: JSON.parse(JSON.stringify(loras)), text: wfNode.inputs.text ?? "" };
wfNode.inputs.loras = { __value__: loras.map((l) => ({ ...l, strength: 0, clipStrength: 0 })) };
wfNode.inputs.text = "";
} else {
_loraBypassSnapshot[nodeId] = {
lora_name: wfNode.inputs.lora_name,
strength_model: wfNode.inputs.strength_model,
strength_clip: wfNode.inputs.strength_clip,
};
wfNode.inputs.strength_model = 0;
wfNode.inputs.strength_clip = 0;
}
_loraNodeBypass[nodeId] = true;
}
_syncRawJson();
},
async renderLoraPane(analysis, containerId, opts = {}) {
const el = document.getElementById(containerId);
if (!el) return;
@@ -751,6 +819,15 @@ export const comfyEditor = {
const loras = this.models.loras || [];
const defaultStackTarget = (loraNodes.find((n) => n.is_lora_manager) || loraNodes[0])?.id;
const hasLoraManager = loraNodes.some((n) => n.is_lora_manager);
// Sync the column-header bypass button (lives outside this container's
// innerHTML, in the static wfm-gen-lora-col-header) to whatever the target
// node's current bypass state is.
const bypassBtn = document.getElementById("wfm-lora-bypass-toggle");
if (bypassBtn) {
bypassBtn.classList.toggle("active", defaultStackTarget != null && !!_loraNodeBypass[defaultStackTarget]);
bypassBtn.disabled = loraNodes.length === 0;
}
// Use Stack Group チェックボックスの現在状態を先読み(Stack表示の取得元判定に使う)。
// resetStackMode(新規読み込み)時は常にOFF扱い。
const stackModeChecked = hasLoraManager && !opts.resetStackMode && (el.querySelector("#wfm-lora-stack-mode-toggle")?.checked ?? false);
@@ -759,6 +836,10 @@ export const comfyEditor = {
// Stackグループ使用チェックボックスをOFFへリセットする(既存のGenUI状態への影響を避けるため
// タブ切替やStack再読込などの単なる再描画では行わない)。
if (opts.resetStackMode) {
// New workflow loaded — bypass state/snapshots belonged to the previous
// workflow's node IDs, so they can't be meaningfully carried forward.
_loraNodeBypass = {};
_loraBypassSnapshot = {};
comfyUI.loraManagerSnapshots = {};
for (const n of loraNodes) {
if (!n.is_lora_manager) continue;
@@ -1084,6 +1165,11 @@ export const comfyEditor = {
const strModel = parseFloat(document.getElementById("wfm-lora-str-model")?.value) || 1.0;
const strClip = parseFloat(document.getElementById("wfm-lora-str-clip")?.value) || 1.0;
_applyLoraToNode(nodeId, select.value, strModel, strClip, node?.is_lora_manager);
// An explicit Apply overwrites whatever bypass had zeroed — clear it so the
// header button (and a later un-bypass) don't stomp this new value.
delete _loraNodeBypass[nodeId];
delete _loraBypassSnapshot[nodeId];
document.getElementById("wfm-lora-bypass-toggle")?.classList.remove("active");
_syncRawJson();
// Sync to Positive prompt
@@ -1216,6 +1302,11 @@ export const comfyEditor = {
comfyUI.currentWorkflow[nodeId].inputs.strength_clip = _stackStrengths[first]?.c ?? 1.0;
}
}
// An explicit Apply overwrites whatever bypass had zeroed — clear it so the
// header button (and a later un-bypass) don't stomp this new value.
delete _loraNodeBypass[nodeId];
delete _loraBypassSnapshot[nodeId];
document.getElementById("wfm-lora-bypass-toggle")?.classList.remove("active");
_syncRawJson();
// Sync Stack LORA SYNTAX + TRIGGER WORDS to Positive prompt
@@ -1990,6 +2081,75 @@ export const comfyEditor = {
if (!workflow) _syncRawJson();
},
// Resolves a user/LLM-supplied aspect ratio (e.g. "3:2" or the full COMBO label
// "3:2 (Photo)") against a ResolutionSelector-type node's actual options via
// /object_info — the workflow analysis only knows the node's CURRENT value, not its
// full option list, so a bare "3:2" needs matching against whatever labeled string
// that node's aspect_ratio COMBO actually accepts. Falls back to the raw input if
// the node type can't be queried or nothing matches, so ComfyUI's own validation
// surfaces a clear error instead of this silently doing nothing.
async resolveAspectRatioOption(nodeType, requested) {
if (!requested) return null;
const ratioMatch = String(requested).match(/(\d+)\s*:\s*(\d+)/);
const wantedRatio = ratioMatch ? `${ratioMatch[1]}:${ratioMatch[2]}` : String(requested).trim();
try {
const info = await comfyUI.fetchObjectInfo(nodeType);
const options = info?.[nodeType]?.input?.required?.aspect_ratio?.[1]?.options || [];
if (options.includes(requested)) return requested;
return options.find((o) => o.startsWith(wantedRatio)) || requested;
} catch {
return requested;
}
},
// AI TOOL Chat's generate_image tool call — writes whichever of KSampler
// (steps/cfg/sampler_name/scheduler/denoise/seed), Latent Image (width/height/
// batch_size), and Resolution Selector (resolvedAspectRatio) fields the LLM
// supplied onto the given workflow/analysis (or comfyUI.currentWorkflow/
// currentAnalysis by default). Mirrors lab-tab.js's _buildWorkflowForIteration
// KSampler branch — same per-field node-id indirection (stepsNodeId/cfgNodeId/...
// falling back to sampler.id) for Advanced-Sampling workflows (Flux.1/2, SD3.5,
// Chroma...) that spread these across several nodes.
// seed is returned rather than written directly — the caller passes it through to
// comfyUI.generate()'s seedMode/seedValue instead, same as every other seed source.
// resolvedAspectRatio must already be one of the node's actual option strings (see
// resolveAspectRatioOption above) — passed in rather than resolved here because
// that lookup is async and this stays a plain synchronous writer.
setGenerationParams({ steps, cfg, sampler_name, scheduler, denoise, seed, width, height, batch_size, resolvedAspectRatio, workflow, analysis } = {}) {
const wf = workflow || comfyUI.currentWorkflow;
const an = analysis || comfyUI.currentAnalysis;
if (!wf || !an) return { seed: null };
const sampler = an.sampler_nodes?.[0];
if (sampler) {
if (steps != null) { const id = sampler.stepsNodeId ?? sampler.id; if (wf[id]) wf[id].inputs.steps = steps; }
if (cfg != null) { const id = sampler.cfgNodeId ?? sampler.id; if (wf[id]) wf[id].inputs.cfg = cfg; }
if (sampler_name) { const id = sampler.samplerNodeId ?? sampler.id; if (wf[id]) wf[id].inputs.sampler_name = sampler_name; }
if (scheduler && sampler.schedulerNodeId != null && wf[sampler.schedulerNodeId]) {
wf[sampler.schedulerNodeId].inputs.scheduler = scheduler;
}
if (denoise != null && sampler.denoiseNodeId != null && wf[sampler.denoiseNodeId]) {
wf[sampler.denoiseNodeId].inputs.denoise = denoise;
}
}
// Resolution Selector takes priority over direct width/height — writing width/
// height straight onto the EmptyLatentImage would just clobber whatever the
// selector computed, silently ignoring the requested aspect ratio.
const resSelector = an.resolution_selector_nodes?.[0];
const latent = an.latent_nodes?.[0];
if (resolvedAspectRatio && resSelector && wf[resSelector.id]) {
wf[resSelector.id].inputs.aspect_ratio = resolvedAspectRatio;
} else if (latent && wf[latent.id]) {
if (width != null) wf[latent.id].inputs.width = width;
if (height != null) wf[latent.id].inputs.height = height;
}
if (latent && wf[latent.id] && batch_size != null) wf[latent.id].inputs.batch_size = batch_size;
if (!workflow) _syncRawJson();
return { seed: seed ?? null };
},
syncToWorkflow() {
// Sync prompt texts before generation
const posSelect = document.getElementById("wfm-prompt-pos-target");
+15
View File
@@ -886,6 +886,7 @@ export const comfyWorkflow = {
prompt_nodes: [],
sampler_nodes: [],
latent_nodes: [],
resolution_selector_nodes: [],
lora_nodes: [],
vae_nodes: [],
save_nodes: [],
@@ -1349,6 +1350,20 @@ export const comfyWorkflow = {
});
}
// ResolutionSelector (ComfyUI core, comfy_extras.nodes_resolution): typically
// feeds an EmptyLatentImage's width/height rather than being a latent node
// itself. aspect_ratio is a fixed-choice COMBO (e.g. "3:2 (Photo)") — its exact
// option strings aren't known here, only the node's current value, so callers
// that want to set a new one (AI TOOL Chat's generate_image) must resolve
// against /object_info at call time (see comfyui-editor.js resolveAspectRatioOption).
if (ct === "ResolutionSelector") {
result.resolution_selector_nodes.push({
id, type: ct, title,
aspect_ratio: inputs.aspect_ratio,
megapixels: inputs.megapixels,
});
}
// TextEncodeMageFlowEdit also carries width/height/batch_size directly
// (it builds the latent internally instead of a separate EmptyLatentImage node).
// width/height are commonly wired to a ResolutionSelector-style helper node
+46 -2
View File
@@ -5,7 +5,7 @@
import { showToast } from "./app.js";
import { t } from "./i18n.js";
import { loadFileIntoMetadataTab } from "./metadata-tab.js";
import { loadFileIntoMetadataTab, extractPrompts, buildPromptItem } from "./metadata-tab.js";
import { loadWorkflowIntoEditor } from "./generate-tab.js";
import { escapeHtml, setupSearchClearBtn, getAiBackendDefaultUrl } from "./util.js";
import { comfyWorkflow } from "./comfyui-workflow.js";
@@ -914,10 +914,15 @@ async function loadImageDetail(img) {
renderWorkflowJson(state.embeddedWorkflow);
_updateCopyCanvasBtn();
renderImagePromptSection(metaRes.image_prompt);
// Promptタブは prompt_workflow (API形式優先) を使う。トップレベルとサブグラフに
// 独立した複数系統を持つワークフローでは workflow (UI形式) からの抽出だと
// トップレベル系統しか拾えないことがあるため(Metadataタブと同じ優先順位に揃える)。
renderPromptTab(wfRes.prompt_workflow ?? state.embeddedWorkflow);
} catch (e) {
renderWorkflowJson(null);
_updateCopyCanvasBtn();
renderImagePromptSection(null);
renderPromptTab(null);
}
// グループタブ更新
@@ -964,6 +969,44 @@ function renderWorkflowJson(workflow) {
}
}
// 埋め込みワークフローからpositive/negativeプロンプトを抽出してPromptタブに表示する。
// 複数候補があるケース(PromptStyler等で正負が複数生成される場合)に対応するため、
// Metadataタブと同じ POS/NEGリスト → クリックで全文表示のUI(buildPromptItem)をそのまま
// 再利用する。選択画像を切り替えるたびにマウス操作無しで読めるよう、先頭項目を自動選択する。
function renderPromptTab(workflow) {
const listEl = document.getElementById("wfm-gallery-prompt-list");
const fullArea = document.getElementById("wfm-gallery-prompt-full");
const fullLabel = document.getElementById("wfm-gallery-prompt-full-label");
if (!listEl || !fullArea || !fullLabel) return;
listEl.innerHTML = "";
fullArea.value = "";
fullLabel.textContent = "";
if (!workflow) {
listEl.innerHTML = `<div class="wfm-meta-item" style="opacity:0.6;">No workflow embedded in this image.</div>`;
return;
}
const { positives, negatives, texts } = extractPrompts(workflow);
const allPrompts = [
...positives.map(p => ({ type: "positive", text: p })),
...negatives.map(p => ({ type: "negative", text: p })),
...(texts ?? []).map(p => ({ type: "text", text: p })),
];
if (allPrompts.length === 0) {
listEl.innerHTML = `<div class="wfm-meta-item" style="opacity:0.6;">No prompt found.</div>`;
return;
}
for (const { type, text } of allPrompts) {
listEl.appendChild(buildPromptItem(text, type, text, fullArea, fullLabel, listEl));
}
// Auto-select first item (positive prompts come first in allPrompts)
listEl.querySelector(".wfm-meta-item-clickable")?.click();
}
// ── タグ操作 ─────────────────────────────────────────────────
async function addTag(tag) {
@@ -1636,7 +1679,7 @@ function onGalleryTabActivated() {
loadGroups();
}
// ── サブタブ切替 (Output / ImagePrompt / Style_Catalog) ────────────────────────
// ── サブタブ切替 (Output / ImagePrompt / Style_Catalog / Metadata) ────────────
function _initGallerySubtabToggle() {
document.querySelectorAll(".wfm-gallery-subtab-btn").forEach((btn) => {
@@ -1647,6 +1690,7 @@ function _initGallerySubtabToggle() {
document.getElementById("wfm-gallery-panel-output")?.classList.toggle("active", target === "output");
document.getElementById("wfm-gallery-panel-imageprompt")?.classList.toggle("active", target === "imageprompt");
document.getElementById("wfm-gallery-panel-styleCatalog")?.classList.toggle("active", target === "styleCatalog");
document.getElementById("wfm-gallery-panel-metadata")?.classList.toggle("active", target === "metadata");
if (target === "imageprompt") activateImagePromptTab();
if (target === "styleCatalog") activateStyleCatalogTab();
});
+28 -8
View File
@@ -1443,21 +1443,27 @@ export async function _expandWildcardsInWorkflow(workflow) {
// Generation (core — throws on error)
// ============================================
async function _coreGenerate(silent = false, workflowOverride = null) {
async function _coreGenerate(silent = false, workflowOverride = null, genOptions = {}) {
const progressBar = document.getElementById("wfm-gen-progress-bar");
const progressText = document.getElementById("wfm-gen-progress-text");
const resultImg = document.getElementById("wfm-gen-result-img");
const resultThumbs = document.getElementById("wfm-gen-result-thumbs");
const seedMode = document.getElementById("wfm-gen-seed-mode")?.value || "random";
const seedValue = parseInt(document.getElementById("wfm-gen-seed-value")?.value) || -1;
// genOptions.seedValue lets a caller (e.g. AI TOOL Chat's generate_image tool)
// force a specific seed on an override workflow without touching the GenerateUI
// seed fields — those still drive normal (non-override) generation as before.
const seedMode = genOptions.seedValue != null ? "fixed" : (document.getElementById("wfm-gen-seed-mode")?.value || "random");
const seedValue = genOptions.seedValue ?? (parseInt(document.getElementById("wfm-gen-seed-value")?.value) || -1);
if (progressBar) progressBar.style.width = "0%";
if (progressText) progressText.textContent = "Starting...";
// Style must be applied *before* wildcard expansion so a __name__ reference written
// inside a Style's prompt/negative_prompt also gets expanded, not just wildcards
// already present in the base workflow's prompt nodes.
const baseWorkflow = workflowOverride || comfyUI.currentWorkflow;
const workflowExpanded = await _expandWildcardsInWorkflow({ ...baseWorkflow });
const workflowForGenerate = _applyStyleToWorkflow(workflowExpanded);
const workflowStyled = _applyStyleToWorkflow({ ...baseWorkflow });
const workflowForGenerate = await _expandWildcardsInWorkflow(workflowStyled);
const { images, seed, svgOutputs } = await comfyUI.generate(
workflowForGenerate,
{
@@ -1972,9 +1978,11 @@ async function _runBatchGenerate() {
// ============================================
// workflowOverride: 明示的に渡された場合、comfyUI.currentWorkflow の代わりにこのワークフローで
// 生成する(GenerateUIの表示状態には触れない。Image Edit「Inpaint」の専用ワークフロー実行から利用)。
// 生成する(GenerateUIの表示状態には触れない。Image Edit「Inpaint」の専用ワークフロー実行や
// AI TOOL Chatのgenerate_imageツールから利用)。
// genOptions: 現状 seedValue のみ(指定時は_coreGenerateがseedMode "fixed"扱いにする)。
// ボタンのclickハンドラから呼ばれる場合は第一引数がMouseEventになるため、その場合は無視する。
async function handleGenerate(workflowOverride = null) {
async function handleGenerate(workflowOverride = null, genOptions = {}) {
if (workflowOverride instanceof Event) workflowOverride = null;
if (!workflowOverride && !comfyUI.currentWorkflow) {
@@ -2003,7 +2011,7 @@ async function handleGenerate(workflowOverride = null) {
await _runBatchGenerate();
} else {
try {
return await _coreGenerate(false, workflowOverride);
return await _coreGenerate(false, workflowOverride, genOptions);
} catch (err) {
const progressText = document.getElementById("wfm-gen-progress-text");
if (progressText) progressText.textContent = "Error";
@@ -2146,6 +2154,10 @@ export async function initGenerateTab() {
// Lab has its own Run/Results panel — hide the shared Generate/Seed/Batch column
const rightCol = document.querySelector(".wfm-gen-right");
if (rightCol) rightCol.style.display = (target === "lab") ? "none" : "";
// Save button writes the currently loaded workflow as-is — not meaningful while
// browsing Batch/Lab (which operate on workflow copies / their own Plan files)
const saveBtn = document.getElementById("wfm-gen-save-btn");
if (saveBtn) saveBtn.style.display = (target === "batch" || target === "lab") ? "none" : "";
});
});
@@ -2156,6 +2168,14 @@ export async function initGenerateTab() {
}
});
// Bypass button in LoRA column header — forces the target LoRA node's strength to
// 0 (or restores it), independent of the Single/Stack tabs' own Apply buttons.
document.getElementById("wfm-lora-bypass-toggle")?.addEventListener("click", () => {
if (!comfyUI.currentAnalysis) return;
comfyEditor.toggleLoraNodeBypass(comfyUI.currentAnalysis);
comfyEditor.renderLoraPane(comfyUI.currentAnalysis, "wfm-gen-lora-fields");
});
// Input inner tab (Prompt / Image)
document.querySelectorAll(".wfm-input-inner-tab").forEach((btn) => {
btn.addEventListener("click", () => {
+90 -9
View File
@@ -93,6 +93,7 @@ const LANGUAGES = {
copyAndSendCanvas: "Copy & Send Canvas",
workflowSentToCanvas: "Workflow ready — drag 'Workflow Studio Library' title to canvas",
workflowSentToCanvasDirect: "Workflow sent to canvas",
workflowFilteredInLibrary: "Filtered in Workflow Studio Library (W tab) — drag the card onto the canvas",
comfyUILoadTimeout: "ComfyUI load timeout",
popupBlocked: "Popup blocked. Please allow popups for this site.",
setAsDefault: "Set as Default",
@@ -236,6 +237,7 @@ const LANGUAGES = {
labBatchCountTooSmall: "Increase the Batch count first to add a keyframe beyond it",
labKeyframesExceedBatch: (maxIter, batchCount) => `Some keyframes are set up to iteration #${maxIter}, beyond the current Batch count (${batchCount}) — they won't be applied until Batch count is raised again`,
labUseWorkflowDefault: "(workflow default)",
labNoNodeForColumn: "No matching node in this workflow",
labInheritsFrom: (val) => `(inherits: ${val})`,
labLoraNoNode: "No LoRA node found in the workflow — LoRA setting was skipped",
labAppliedAt: (n) => `Applied: ${n}`,
@@ -244,6 +246,10 @@ const LANGUAGES = {
labRevertCheckboxLabel: "Revert to iteration #1's setting",
labBypassed: "⏭ Bypassed",
labBypassCheckboxLabel: "Bypass this setting (keep whatever was active before it)",
labLoraNodeBypassed: "⛔ (node bypassed)",
labLoraNodeBypassCheckboxLabel: "Bypass LoRA node (force strength to 0)",
labLoraApplyToPromptCheckboxLabel: "Apply LoRA syntax + trigger words to Positive prompt",
labLoraPromptAppliedTag: "→P",
labSeedEmptyRandom: "Seed (empty = random)",
labPause: "Pause",
labResume: "Resume",
@@ -547,6 +553,7 @@ const LANGUAGES = {
modelsSideGroup: "Group",
modelsSideCivitai: "CivitAI",
gallerySideMetadata: "Metadata",
gallerySidePrompt: "Prompt",
// -- Gallery --
galleryAllTags: "All Tags",
@@ -566,6 +573,7 @@ const LANGUAGES = {
galleryTabOutput: "Output",
galleryTabImagePrompt: "ImagePrompt",
galleryTabStyleCatalog: "Style_Catalog",
galleryTabMetadata: "Metadata",
galleryNoSelection: "No selection",
imagePromptSelectedPrompt: "Prompt",
imagePromptNoPrompt: "No prompt data for this image.",
@@ -783,6 +791,8 @@ const LANGUAGES = {
helpGen22: "Stack tab — Use Stack Group checkbox (shown only when a Lora Manager node exists in the workflow): OFF (the default whenever a workflow is loaded) shows and edits the LoRAs actually configured in the workflow's Lora Manager node itself; ON shows and edits the \"Stack\" group registered in the Models tab instead; switching either direction clears the node's LoRA list first, then loads it from the newly selected source; switching back to OFF restores exactly the LoRA list that was present when the workflow was loaded (captured as a snapshot at load time)",
helpGen23: "Stack tab controls: each row has an active checkbox and Str M / Str C fields; the header \"Stack\" checkbox toggles all rows at once; the +/- step buttons adjust the strength of all currently active rows together; a \"Showing: LoRAs currently in workflow\" / \"Showing: registered Stack group\" label indicates which source is currently displayed; Apply writes the full list back to the target node and also appends LoRA syntax + CivitAI trigger words to the Positive prompt",
helpGen24: "Caution for CLIP Text Encode edit+ (CLIPTextEncodeEditPlus) nodes: when a Lora Manager node's trigger_words output feeds this node's text1 input (RAW/EDIT/front/back modes combine text1 with the editable text_edit field), trigger words are already included automatically at generation time; clicking a LoRA Apply button or turning on Use Stack Group additionally writes LoRA syntax/trigger words into text_edit, duplicating them — leave Use Stack Group off and avoid the LoRA Apply buttons if you want the workflow's original prompt and LoRA composition to stay unmodified",
helpGen29: "Bypass button (⛔, in the LoRA column header, next to Refresh Stack group): forces the target LoRA node's strength to 0 — strength_model/strength_clip for a standard LoraLoader, or every entry's strength/clipStrength for a Lora Manager node (its LoRA list is kept intact, not replaced) — without touching the actual ComfyUI node's mode; click again to restore the exact values that were there before toggling; targets whichever node Single/Stack Apply default to (the workflow's Lora Manager node if it has one, else the first LoRA node); clicking a Single or Stack Apply button clears the bypass state so it can't silently reassert itself on a later toggle",
helpGen30: "The Save button (right end of the subtab bar) is hidden while the Batch or Lab subtab is active, since those work on workflow copies / their own Plan files rather than the currently loaded workflow; it reappears on Input/Model/Settings/Feeder",
helpFeederTitle: "GenerateUI Tab — Feeder subtab",
helpFeederDesc: "Two modes switchable via the [Image Loop] / [Gallery] toggle at the top of the left pane.",
helpFeederImgloopTitle: "Image Loop mode",
@@ -821,8 +831,10 @@ const LANGUAGES = {
helpLab11: "Prompt cell editor also has Clear (empties both Positive and Negative) and Get from Previous (copies both Positive and Negative from the keyframe immediately before this one in the same column — e.g. editing #3 copies #2's prompt wholesale; not shown on keyframe #1, which has no earlier row to copy from)",
helpLab12: "Keyframe #1 in every column shows the currently loaded workflow's own live setting instead of a blank placeholder — for the Model column this covers Checkpoint and VAE only (LoRA is never live-reflected, since there's no reliable way to infer \"the\" active LoRA back out of a workflow node) — both in its grid cell (dashed border) and pre-filled into its edit modal, for as long as it hasn't been explicitly saved with an override; saving it (even without changing anything) locks that value in as a real override and the dashed/live styling goes away; switching to the Lab subtab always refreshes this against whatever is currently loaded in GenerateUI",
helpLab13: "Bypass checkbox (every keyframe, including #1): skips that row entirely when picking which setting applies at a given iteration — as if the row didn't exist — so whatever was active before it (an earlier keyframe, or ultimately nothing) just keeps running; Lab-internal only, it never touches the actual ComfyUI node's mode; disables that row's value fields and its Revert checkbox while checked (the two are mutually exclusive); the cell shows an amber \"⏭ Bypassed\" label, which takes priority over both the Revert and live-value display",
helpLab14: "Model column header's C / L / V buttons pick which single field (Checkpoint / LoRA / VAE — LoRA is Single-only, no Stack support here) its cells summarize and its edit modal shows; the two you're not currently viewing are left exactly as already saved when you Save. Each button turns the same highlight color as GenerateUI's Model tab (configurable in Settings) when the loaded workflow actually contains that node type. If a LoRA is set but the workflow has no LoRA node at all, Lab warns once per Run and skips applying it — unlike VAE, it never auto-inserts a LoRA node, since that would require rewiring the Checkpoint's model/clip outputs through it",
helpLab15: "Model column fields carry forward independently: a keyframe that only sets a LoRA doesn't reset Checkpoint/VAE to the workflow default — each of the three keeps whatever an earlier keyframe last set until a later keyframe explicitly changes it. A field left blank in the edit modal shows \"(inherits: ...)\" as its placeholder instead of \"(workflow default)\" when there's something to inherit, so it's clear what actually applies. \"Revert to #1's setting\" resets all three fields at once back to keyframe #1's row, not just the one you're currently viewing",
helpLab14: "Model column header's C / L / V buttons pick which independent keyframe list — Checkpoint, LoRA (Single-only, no Stack support here), or VAE — the shared cell list, its +/-, and its edit modal all currently operate on. Each of the three is its own timeline with its own keyframes, exactly like Prompt/KSampler; they only ever look merged because they share one column's worth of screen space. Adding, removing, or editing a keyframe while viewing one never touches the other two. Each button turns the same highlight color as GenerateUI's Model tab (configurable in Settings) when the loaded workflow actually contains that node type. If a LoRA is set but the workflow has no LoRA node at all, Lab warns once per Run and skips applying it — unlike VAE, it never auto-inserts a LoRA node, since that would require rewiring the Checkpoint's model/clip outputs through it",
helpLab15: "Checkpoint / LoRA / VAE each carry forward independently, the same effective-value rule as every other column (see above): a keyframe in one never resets the other two to the workflow default. A field left blank in the edit modal shows \"(inherits: ...)\" as its placeholder instead of \"(workflow default)\" when an earlier keyframe in that same list already set something, so it's clear what actually applies. \"Revert to #1's setting\" and Bypass — like +/- — only ever affect whichever ONE of the three is currently selected by the C/L/V toggle, never the other two",
helpLab16: "Bypass LoRA node checkbox (LoRA keyframe's own edit modal only): forces that keyframe's strength_model/strength_clip to 0 at apply-time, standing in for ComfyUI's own node Bypass (mode:4) — the workflow used here is already converted to API format, which has no mode concept, so zeroing strength is the behavioral equivalent. Disables the strength fields while checked; independent of the row-level Bypass checkbox above (see helpLab13), which skips the whole row instead of touching the node",
helpLab17: "Apply LoRA syntax + trigger words to Positive prompt checkbox (LoRA column only): when checked, appends <lora:name:strengthModel:strengthClip> plus that LoRA's cached CivitAI trigger words to the Positive prompt, comma-separated, at apply-time — the same format GenerateUI's own Model tab Apply button writes. Switching keyframes (a different LoRA, this checkbox turned off, or the row Bypassed) always strips exactly what the previous iteration injected before adding anything new, so a batch never accumulates every LoRA it has used. Disabled automatically while Bypass LoRA node is checked",
helpLabCardPlanTitle: "Setting subtab — Run & Plan files",
helpLabPlan1: "Right pane — Batch: number of iterations to run; Note: free-text memo saved together with the plan",
helpLabPlan2: "Run / Pause / Cancel: Run starts the batch loop; Pause/Resume freezes it between iterations; Cancel interrupts the current generation and stops the loop",
@@ -855,6 +867,17 @@ const LANGUAGES = {
helpPrompt12: "Renaming a style keeps its position in whichever file it was originally defined in; new styles are saved into a dedicated custom.json inside the style folder so the bundled style packs are never modified directly; a name must be unique across all styles",
helpPrompt13: "Saving, renaming, or deleting a style immediately refreshes GenerateUI's Style dropdown, the Batch tab's style checklist, and the Style Catalog's name list — no reload needed",
helpPrompt14: "Each entry in the list shows its source file as a small badge next to the name (e.g. sdxl_styles_diva.json); while editing an existing style, that filename also appears above the Positive prompt field, and a \"+ Add to this file\" button appears next to Save — click it to clear the form for a brand-new style that gets appended to that same file instead of the default custom.json",
helpPrompt15: "Two top-level tabs: Form is the original one-item-at-a-time editor described below (AI Assistant / Presets / Wildcard / Style panes); Table shows the same underlying data all at once as editable spreadsheets — useful once there are more than a handful of presets, wildcard files, or styles to manage",
helpPrompt16: "The Table tab has four sub-tabs of its own: Presets, Presets Group, Wildcards, and Style — each an editable table built from the exact same data as the Form tab, so changes on either side stay in sync",
helpPrompt17: "Every table shares a left-most auto-numbered # column: click a number to select that row (highlighted red) — multiple rows can be selected at once; the toolbar's Delete button removes every selected row in one confirm, and Deselect clears the selection. This replaced per-row delete buttons, which were too easy to misclick",
helpPrompt18: "Double-click a row's number to open a larger prompt-editing modal instead of the small in-table textarea. Presets and Style show Positive/Negative toggle buttons above one shared textarea (switching preserves whatever you typed on the other side); Wildcards shows just the file's content. Save writes the change back and closes the modal; Close discards it",
helpPrompt19: "The toolbar's + New ... button adds a blank row at the top with small ✓ (Add) / ✕ (Cancel) icons in place of its number; fill in the fields and click ✓ to create it, or ✕ to discard the row without saving",
helpPrompt20: "Presets table — the ★ (favorite) and B (Batch group) columns work like toggle switches: click to turn them gold/yellow, click again to clear; the toolbar's BC button clears every preset out of the Batch group at once, same as the Form tab's Preset Manager",
helpPrompt21: "Presets table toolbar — PP Copy / NP Copy copy the single selected row's positive/negative prompt to the clipboard; the group select + Add to Group adds every currently selected preset to the chosen group in one click. Doing the group assignment from here (search/select presets, then pick from a short group list) replaced the old per-group preset picker, which became unusable once there were many presets",
helpPrompt22: "Presets Group table — create, rename, and delete groups; each group's members are shown as removable badges (✕). The reserved Batch group is not listed here — it's managed entirely through the Presets table's B column and BC button instead",
helpPrompt23: "Wildcards table — same files as the Form tab's Wildcard file manager; editing the File path or the Ext dropdown renames the file (saves the content under the new name, then removes the old file)",
helpPrompt24: "Style table — same styles as the Form tab's Style manager, editable inline the same way as Presets",
helpPrompt25: "Presets table toolbar — the ★ and B sort buttons (next to the search box) bring favorited / batch-selected presets to the top of the list; both can be enabled together (★ takes priority, B breaks ties), and everything else keeps its original relative order",
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",
@@ -914,6 +937,7 @@ const LANGUAGES = {
helpGallery23: "Same 3-column layout: folder tree (left) | thumbnails (center) | Positive/Negative prompt panel (right). Click a thumbnail to read the Positive/Negative prompt actually embedded in that generated image, each with its own Copy button",
helpGallery24: "Select as Style button: matches the selected image's filename (without extension) against a registered Style name and switches the GenerateUI tab's Style dropdown to it — a visual shortcut into the existing named-Style system, not a one-off apply of the embedded prompt (if the Style was renamed or deleted since the catalog image was created, a \"no matching style\" toast is shown instead)",
helpGallery25: "Load in GenerateUI / Open in Metadata Tab buttons: read the workflow embedded in the catalog image itself (the catalog copy carries the same PNG metadata as the original output image) and either load it straight into the GenerateUI editor or open the image in the Metadata tab for inspection — no need to hunt down the original file in the Output folder",
helpGallery26: "Four sub-tabs at the top of the Gallery tab: Output (this browser), ImagePrompt, Style_Catalog, and Metadata — the last one embeds the full Metadata Tab (see its own help page) so you can drop an image there to inspect its embedded model/prompt data without leaving Gallery",
// -- Tagger Tab --
helpTaggerTitle: "Tagger Tab",
@@ -948,7 +972,7 @@ const LANGUAGES = {
helpAiTitle: "AI Tab (A)",
helpAi1: "4-pane layout — Translation | Chat | TOOLS | Settings — all panes are always visible simultaneously",
helpAi2: "Translation pane: translate between Japanese, English, Chinese, or Free language using Ollama, LM Studio, Lemonade, or Unsloth; ⇄ swap button swaps both language selectors and text content; selections are saved automatically",
helpAi3: "Chat pane: multi-turn conversation with the LLM; Enter to send a message, Shift+Enter for a newline; Clear button resets the conversation history; if the model supports tool calling and asks for an image (\"generate a cat picture\", etc.) it will run generation on the workflow currently loaded in GenerateUI and show the result inline; the \"Allow image generation\" checkbox in the pane header toggles this on/off; if an image is attached (dropped in the TOOLS pane) it is sent to the LLM as vision input and, when generate_image is called, used as the base for image-to-image generation instead of text-to-image; models verified to work well for tool calling and vision: Gemma 4:e4b, Qwen 3.5:9b, Ministral 3:3b (Ollama)",
helpAi3: "Chat pane: multi-turn conversation with the LLM; Enter to send a message, Shift+Enter for a newline; Clear button resets the conversation history; if the model supports tool calling and asks for an image (\"generate a cat picture\", etc.) it will run generation on the workflow currently loaded in GenerateUI and show the result inline; the \"Allow image generation\" checkbox in the pane header toggles this on/off; if an image is attached (dropped in the TOOLS pane) it is sent to the LLM as vision input and, when generate_image is called, used as the base for image-to-image generation instead of text-to-image; generate_image also accepts optional generation parameters the model can fill in from the request — steps, cfg, sampler_name, scheduler, denoise, seed, width, height, batch_size (number of images produced in that one call), and aspect_ratio (a ratio preset like \"3:2\", applied only if the workflow has a Resolution Selector node feeding width/height — ignored otherwise); anything the model doesn't supply keeps whatever the loaded workflow already has set; models verified to work well for tool calling and vision: Gemma 4:e4b, Qwen 3.5:9b, Ministral 3:3b (Ollama)",
helpAi4: "TOOLS pane — VLM: drop an image (PNG/JPG/WebP) and select a task (Describe Image, Create Prompt, Create Tags), click Run; or select \"Create wildcards\", enter a category name and count to generate plain-text entries one per line; the dropped image is also shared with the Chat pane as its attachment (shown there and used for image-to-image generation) until cleared with the ✕ button",
helpAi6: "Settings pane: choose backend (Ollama / LM Studio / Lemonade / Unsloth), set API URL, test connection, select model; Unsloth always requires an API key even for local access — set UNSLOTH_API_KEY in a .env file in the plugin folder (copy .env.example) and restart ComfyUI, the key is read server-side and never sent to the browser; Unsloth also requires \"Auto model switching (OpenAI API)\" to be turned ON in the Unsloth Desktop app itself (its own Settings screen) — without it, requests fail with HTTP 404 unless a model was already manually loaded in the Unsloth Studio UI; Unsloth reasoning models return their thinking in a separate reasoning_content field rather than inline <think> tags, so a low Max tokens can make the model spend its whole budget reasoning and return an empty reply — increase Max tokens (2048+) if replies come back blank; Generation section — Thinking mode toggle (off by default; Ollama receives a think flag, and regardless of backend any <think>...</think> block is stripped from the output when off, so reasoning models don't clutter Translation/Chat/TOOLS results) and Max tokens (Ollama: num_predict, LM Studio/Lemonade/Unsloth: max_tokens; 0 = unlimited); configure Free language names; Chat Image Generation section lets you enable a dedicated workflow for Chat-triggered text-to-image generation instead of the one currently loaded in GenerateUI; Chat I2I Generation section is the same but for image-to-image generation when an image is attached; settings are shared with the Library panel's A tab via localStorage",
helpAi7: "Chat pane — SVG: the LLM can also produce SVG graphics directly as text, with no ComfyUI workflow involved; ask it to draw something in SVG and, if the reply contains SVG code, it is rendered inline as a preview with a \"Save to Gallery\" button that writes it as a .svg file to the Gallery tab's output folder; this does not use tool calling, so it works with any model regardless of tool-calling support",
@@ -976,10 +1000,12 @@ const LANGUAGES = {
helpGenCardBatchTypes: "Batch Tab — Batch Types",
helpGenCardLora: "Model Tab — LoRA Details",
helpPromptTitle: "Prompt Tab",
helpPromptCardToplevel: "Form / Table",
helpPromptCardAi: "AI Assistant & Translation",
helpPromptCardPresets: "Presets",
helpPromptCardWildcards: "Wildcards",
helpPromptCardStyleTitle: "Style",
helpPromptCardTable: "Table View",
helpMetadataCardOverview: "Overview",
helpMetadataCardExtracted: "Extracted Information",
helpMetadataCardFormats: "Supported Formats",
@@ -1263,6 +1289,7 @@ const LANGUAGES = {
helpMetadata3: "Right column: LoRA list (with strength values) and Prompt list (click to view full text below)",
helpMetadata4: "Prompt actions — Copy: copy to clipboard; GenUI:P/N: set GenerateUI positive/negative prompt; Prompt:P/N: set Prompt tab preset positive/negative",
helpMetadata5: "Supported: ComfyUI PNG/WebP/JSON (standard + Flux.2 / Qwen-Image / Z-Image / Mage-Flow / LongCat / Boogu / HiDream E1 / FireRed / Ernie Image subgraph workflows), SD WebUI, SD Forge, Fooocus.",
helpMetadata6: "This is now a sub-tab inside the Gallery tab (to the right of Style_Catalog), rather than its own top-level tab — everything else about it works exactly the same",
},
ja: {
@@ -1352,6 +1379,7 @@ const LANGUAGES = {
copyAndSendCanvas: "コピー&キャンバスへ送る",
workflowSentToCanvas: "ワークフロー準備完了 — タイトルをキャンバスへドラッグ",
workflowSentToCanvasDirect: "ワークフローをキャンバスへ送信しました",
workflowFilteredInLibrary: "Workflow Studio Library(Wタブ)で絞り込みました — カードをキャンバスへドラッグしてください",
comfyUILoadTimeout: "ComfyUIの読み込みがタイムアウトしました",
popupBlocked: "ポップアップがブロックされました。このサイトのポップアップを許可してください。",
setAsDefault: "デフォルトに設定",
@@ -1495,6 +1523,7 @@ const LANGUAGES = {
labBatchCountTooSmall: "先にBatch数を増やしてからキーフレームを追加してください",
labKeyframesExceedBatch: (maxIter, batchCount) => `${maxIter}回目まで設定されたキーフレームがありますが、現在のBatch数(${batchCount})を超えているため、Batch数を増やすまで適用されません`,
labUseWorkflowDefault: "(ワークフローの設定のまま)",
labNoNodeForColumn: "このワークフローに該当ノードがありません",
labInheritsFrom: (val) => `(引き継ぎ: ${val})`,
labLoraNoNode: "ワークフロー内にLoRAノードが見つかりません — LoRA設定はスキップされました",
labAppliedAt: (n) => `適用:${n}`,
@@ -1503,6 +1532,10 @@ const LANGUAGES = {
labRevertCheckboxLabel: "1回目の設定に戻す",
labBypassed: "⏭ バイパス中",
labBypassCheckboxLabel: "この設定をバイパス(直前まで有効だった設定を維持)",
labLoraNodeBypassed: "⛔(ノードをバイパス)",
labLoraNodeBypassCheckboxLabel: "LoRAノードをバイパス(強度を0にする)",
labLoraApplyToPromptCheckboxLabel: "LoRA構文とトリガーワードをポジティブプロンプトに適用",
labLoraPromptAppliedTag: "→P",
labSeedEmptyRandom: "Seed(空欄でランダム)",
labPause: "一時停止",
labResume: "再開",
@@ -1806,6 +1839,7 @@ const LANGUAGES = {
modelsSideGroup: "グループ",
modelsSideCivitai: "CivitAI",
gallerySideMetadata: "メタデータ",
gallerySidePrompt: "プロンプト",
// -- Gallery --
galleryAllTags: "すべてのタグ",
@@ -1825,6 +1859,7 @@ const LANGUAGES = {
galleryTabOutput: "Output",
galleryTabImagePrompt: "ImagePrompt",
galleryTabStyleCatalog: "Style_Catalog",
galleryTabMetadata: "メタデータ",
galleryNoSelection: "選択なし",
imagePromptSelectedPrompt: "プロンプト",
imagePromptNoPrompt: "この画像にはプロンプトデータがありません。",
@@ -2042,6 +2077,8 @@ const LANGUAGES = {
helpGen22: "Stackタブ — Use Stack Groupチェックボックス(ワークフロー内にLora Managerノードがある場合のみ表示):OFF(ワークフロー読み込み時の既定)はLora Managerノードに実際に設定されているLoRAをそのまま表示・編集;ONにするとModelsタブに登録済みの「Stack」グループを表示・編集;どちらへの切替もまずノードのLoRA一覧をクリアしてから切替先のソースを読み込む;OFFへ戻すとワークフロー読み込み時点のLoRA一覧(読み込み時にスナップショットとして保存済み)へ正確に復元される",
helpGen23: "Stackタブの操作:各行に有効チェックボックスとStr M / Str Cフィールド;ヘッダーの「Stack」チェックボックスで全行を一括切替;上部の+/-ステップボタンで現在有効な全行の強度をまとめて増減;「Showing: LoRAs currently in workflow」/「Showing: registered Stack group」のラベルで現在どちらのソースを表示中か分かる;Applyで対象ノードへ一覧全体を書き込み、同時にLoRA構文とCivitAIトリガーワードをPositiveプロンプトへ追加",
helpGen24: "CLIP Text Encode edit+(CLIPTextEncodeEditPlus)ノードを使う場合の注意:Lora Managerノードのtrigger_words出力がこのノードのtext1入力に接続されている構成(RAW/EDIT/front/backモードでtext1と編集可能なtext_editを結合)では、生成時にトリガーワードは既に自動的に反映済み;LoRAのApplyボタンをクリックしたりUse Stack GroupをONにしたりすると、LoRA構文・トリガーワードがtext_editへも追加で書き込まれ二重になる — ワークフロー本来のプロンプト・LoRA構成を変更せず維持したい場合は、Use Stack GroupをOFFのままにしLoRAのApply系ボタンには触れないこと",
helpGen29: "バイパスボタン(⛔、LoRA列ヘッダー内、Refresh Stack groupボタンの隣):対象のLoRAノードの強度を強制的に0にします — 通常のLoraLoaderならstrength_model/strength_clip、Lora Managerノードならリスト内の全エントリのstrength/clipStrength(LoRAリスト自体は差し替えず維持)が対象で、実際のComfyUIノードのmodeには一切触れません;もう一度クリックすると、切り替え前の値に正確に戻ります;対象はSingle/Stack Applyがデフォルトで対象とするノード(ワークフローにLora Managerノードがあればそれ、無ければ最初のLoRAノード);SingleまたはStackのApplyボタンをクリックするとバイパス状態はクリアされ、後から無言で再適用されることはありません",
helpGen30: "Saveボタン(サブタブバー右端)は、BatchまたはLabサブタブがアクティブな間は非表示になります — これらは現在読み込み中のワークフロー本体ではなく、ワークフローの複製や独自のPlanファイルを操作するためです。Input/Model/Settings/Feederでは再表示されます",
helpFeederTitle: "GenerateUI Tab — Feeder サブタブ",
helpFeederDesc: "左ペイン上部の [Image Loop] / [Gallery] トグルで2つのモードを切り替えられます。",
helpFeederImgloopTitle: "Image Loop モード",
@@ -2080,8 +2117,10 @@ const LANGUAGES = {
helpLab11: "プロンプトセルの編集モーダルにはClear(Positive/Negative両方を空にする)とGet from Previous(同じカラムの直前のキーフレームからPositive/Negative両方をコピー — 例:#3を編集中に押すと#2のプロンプトをそのままコピー)も用意されています。1回目(#1)には直前の行が無いため表示されません",
helpLab12: "各カラムの1回目(キーフレーム#1)は、明示的に上書き保存されていない間、現在GenerateUIで読み込まれているワークフロー本来の設定を、空欄プレースホルダの代わりにグリッドセル(破線枠)と編集モーダルの両方に表示します(Model列はCheckpointとVAEのみが対象 — LoRAはワークフローのノードから「どれが有効か」を安全に逆算する手段が無いため、ライブ表示の対象外です)。何も変更せずに保存しただけでも実際の上書き値として確定し、破線・ライブ表示は解除されます。Labサブタブに切り替えるたびに、その時点でGenerateUIに読み込まれている内容へ再同期されます",
helpLab13: "Bypassチェックボックス(1回目を含む全キーフレームに搭載):そのキーフレームの回数目判定において、行自体が存在しないかのように完全にスキップします — その結果、直前まで有効だった設定(それ以前のキーフレーム、なければ何も適用されない状態)がそのまま使われ続けます。Labタブ内だけの概念で、実際のComfyUIノードのmodeには一切触れません。チェック中はその行の値フィールドと「1回目に戻す」チェックボックスが無効化されます(両者は同時に使えない排他的な関係)。セルには琥珀色の「⏭ Bypassed」ラベルが表示され、Revert表示・ライブ表示のどちらよりも優先されます",
helpLab14: "Model列ヘッダーのC/L/Vボタンは、セル表示と編集モーダルがCheckpoint/LoRA/VAE(LoRAはSingleのみ、Stack非対応)のどれを表示・編集対象にするかを切り替えます。今見ていない残り2項目は、保存してもそれまでの保存値のまま変更されません。各ボタンは、読み込み中のワークフローに該当するノードが実際に存在する場合、GenerateUIのModelタブと同じハイライト色(Settingsで変更可)になります。LoRAを設定していてもワークフロー内にLoRAノードが1つも無い場合は、Run実行中に1回だけ警告し、その設定は適用されません — VAEと異なりLoRAノードの自動追加は行いません(CheckpointのModel/CLIP出力の配線を組み替える必要があるため)",
helpLab15: "Model列の3項目(Checkpoint/LoRA/VAE)はそれぞれ独立して引き継がれます:LoRAだけを設定したキーフレームがあっても、Checkpoint/VAEはワークフロー本来の設定に戻るのではなく、それ以前のキーフレームで最後に設定された値がそのまま維持されます(さらに後のキーフレームで明示的に変更されるまで)。編集モーダルで未設定のままの項目は、引き継ぐ値がある場合プレースホルダが「(ワークフローの設定のまま)」ではなく「(引き継ぎ: ...)」と表示され、実際に適用される値が分かるようになっています。「1回目の設定に戻す」は、今見ている項目だけでなく3項目すべてを一括でキーフレーム#1の内容に戻します",
helpLab14: "Model列ヘッダーのC/L/Vボタンは、Checkpoint・LoRA(Singleのみ、Stack非対応)・VAEという3つの独立したキーフレームリストのうち、今このセル表示・+/-・編集モーダルがどれを対象にしているかを切り替えます。3つはそれぞれPrompt・KSamplerと同じく完全に独立したタイムラインを持っており、1つの列にまとめて見えているのは表示スペースを共有しているだけです。1つを表示中にキーフレームを追加・削除・編集しても、残り2つには一切影響しません。各ボタンは、読み込み中のワークフローに該当するノードが実際に存在する場合、GenerateUIのModelタブと同じハイライト色(Settingsで変更可)になります。LoRAを設定していてもワークフロー内にLoRAノードが1つも無い場合は、Run実行中に1回だけ警告し、その設定は適用されません — VAEと異なりLoRAノードの自動追加は行いません(CheckpointのModel/CLIP出力の配線を組み替える必要があるため)",
helpLab15: "Checkpoint/LoRA/VAEは、他の列と全く同じ実効値ルール(上記参照)でそれぞれ独立して引き継がれます:いずれか1つのキーフレームが、残り2つをワークフロー本来の設定に戻すことはありません。編集モーダルで未設定のままの項目は、同じリスト内の以前のキーフレームで何か設定済みの場合、プレースホルダが「(ワークフローの設定のまま)」ではなく「(引き継ぎ: ...)」と表示され、実際に適用される値が分かるようになっています。「1回目の設定に戻す」やBypassも+/-と同様、現在C/L/Vトグルで選択中の1項目だけに作用し、残り2つには影響しません",
helpLab16: "LoRAノードをバイパスチェックボックス(LoRAキーフレーム自身の編集モーダルのみ):ComfyUI本来のノードBypass(mode:4)の代わりに、そのキーフレームのstrength_model/strength_clipを適用時に強制的に0にします — ここで使われるワークフローは既にAPI形式に変換済みでmodeの概念が無いため、強度をゼロにすることが実質的に同等の効果になります。チェック中は強度の入力欄が無効化されます。上記の行単位のBypassチェックボックス(helpLab13参照)とは独立しており、そちらは行全体をスキップするだけでノードには一切触れません",
helpLab17: "LoRA構文とトリガーワードをポジティブプロンプトに適用チェックボックス(LoRA列のみ):チェックすると、適用時に <lora:name:strengthModel:strengthClip> とそのLoRAのキャッシュ済みCivitAIトリガーワードを、カンマ区切りでポジティブプロンプトへ追加します — GenerateUIのModelタブのApplyボタンが書き込むのと同じ書式です。キーフレームを切り替えた場合(別のLoRAへの変更、このチェックボックスをオフにした場合、または行自体をBypassした場合)は、新しい内容を追加する前に必ず直前のイテレーションが注入した分だけを正確に取り除くため、バッチ内で使われた全LoRAが蓄積されることはありません。「LoRAノードをバイパス」がチェックされている間は自動的に無効化されます",
helpLabCardPlanTitle: "Settingサブタブ — 実行とプランファイル",
helpLabPlan1: "右ペイン — Batch:実行する回数。Note:プランと一緒に保存される自由記述のメモ",
helpLabPlan2: "Run/Pause/Cancel:Runでバッチループを開始、Pause/Resumeで回と回の間を一時停止・再開、Cancelで現在の生成を中断してループを止めます",
@@ -2114,6 +2153,17 @@ const LANGUAGES = {
helpPrompt12: "リネームしても、元々定義されていたファイル内での位置は保たれる。新規作成したStyleはstyleフォルダ内の専用custom.jsonへ保存されるため、同梱のスタイルパックファイルは直接変更されない。名前は全スタイル中で一意である必要がある",
helpPrompt13: "Styleの保存・リネーム・削除は、GenerateUIのStyleドロップダウン、Batchタブのスタイルチェックリスト、Style Catalogの名前一覧に即座に反映される(再読み込み不要)",
helpPrompt14: "一覧の各項目には、名前の横に定義元ファイル名が小さなバッジで表示される(例: sdxl_styles_diva.json)。既存スタイルを編集中は、そのファイル名がPositive promptフィールドの上にも表示され、Saveの隣に「+ Add to this file」ボタンが現れる。クリックするとフォームが空になり、デフォルトのcustom.jsonではなく同じファイルへ追加される新規スタイルを作成できる",
helpPrompt15: "上部の2つのトップレベルタブ:Formは下記で説明する従来の一件ずつ編集する画面(AI Assistant / Presets / Wildcard / Style の各パネル)。Tableは同じデータを一覧編集可能な表形式で全件表示 — プリセット・ワイルドカードファイル・スタイルが多くなってきた場合に便利",
helpPrompt16: "Tableタブにはさらに4つのサブタブがあります:Presets・Presets Group・Wildcards・Style — いずれもFormタブと全く同じデータを参照する編集可能なテーブルなので、どちらで変更しても両方に反映されます",
helpPrompt17: "各テーブルは左端に自動採番の#列を共有します。番号をクリックするとその行が選択(赤くハイライト)され、複数行を同時選択可能。ツールバーのDeleteボタンで選択した行をまとめて1回の確認で削除、Deselectで選択解除。誤操作しやすかった行ごとの削除ボタンを廃止して置き換えたものです",
helpPrompt18: "行番号をダブルクリックすると、テーブル内の小さなテキストエリアの代わりに、より大きなプロンプト編集モーダルが開きます。Presets・Styleは共通の1つのテキストエリアの上にPositive/Negative切り替えボタンが表示され(切り替えても入力済みのテキストは両方とも保持されます)、Wildcardsはファイルの内容のみを表示します。Saveで保存してモーダルを閉じ、Closeは変更を破棄します",
helpPrompt19: "ツールバーの「+ New ...」ボタンで先頭に空の編集可能な行が追加され、番号の代わりに小さな✓(Add)/✕(Cancel)アイコンが表示されます。入力してから✓をクリックすると作成、✕で保存せずに破棄します",
helpPrompt20: "Presetsテーブル — ★(お気に入り)とB(Batchグループ)の列はトグルスイッチのように動作します。クリックで金色/黄色に変化、もう一度クリックで解除。ツールバーのBCボタンでBatchグループに登録された全プリセットを一括クリアします(Formタブのプリセットマネージャーと同じ)",
helpPrompt21: "Presetsテーブルのツールバー — PP Copy/NP Copyは選択中の1行のPositive/Negativeプロンプトをクリップボードへコピー。グループ選択+Add to Groupは選択中の全プリセットを選んだグループへワンクリックで一括追加します。ここで(プリセット側を検索・選択してから短いグループ一覧から選ぶ形で)グループ割り当てを行うようにしたことで、プリセット数が多くなると使いづらかった旧来のグループごとのプリセット選択方式を置き換えました",
helpPrompt22: "Presets Groupテーブル — グループの作成・リネーム・削除。各グループのメンバーは削除可能なバッジ(✕)として表示されます。予約グループのBatchはここには表示されません — PresetsテーブルのB列とBCボタンで専用管理されます",
helpPrompt23: "Wildcardsテーブル — Formタブのワイルドカードファイルマネージャーと同じファイル一覧です。File列のパスやExtドロップダウンを編集するとファイル名が変更されます(新しい名前で内容を保存してから、古いファイルを削除します)",
helpPrompt24: "Styleテーブル — Formタブのスタイル管理と同じスタイル一覧で、Presetsと同様にインライン編集できます",
helpPrompt25: "Presetsテーブルのツールバー — 検索ボックス横の★とBソートボタンで、お気に入り/Batch登録済みのプリセットを一覧の先頭に並べ替えます。両方同時にONにでき、その場合は★が優先、Bが次点の基準になり、それ以外は元の相対順序を維持します",
helpSettings1: "UI言語と要約言語の設定",
helpSettings2: "テーマ選択(13種プリセット)+カスタマイズ:カラー調整、背景パターン(ストライプ、ポルカドット、チェック、SVGタイリング)、フォント選択(日本語デザインフォント含む16種のGoogle Fonts)",
helpSettings3: "ワークフローデータフォルダのパス変更・リセット",
@@ -2172,6 +2222,7 @@ const LANGUAGES = {
helpGallery22: "Galleryタブ先頭にある3つ目のサブタブ。登録済みStyleをプレビュー画像から視覚的に選べる — ComfyUI outputフォルダ内のws_style_catalogフォルダを管理(未作成時は自動生成)。GenerateUIタブの「カタログ作成」ボタンで生成される(GenerateUIタブのヘルプ参照)",
helpGallery23: "同じ3カラムレイアウト:フォルダツリー(左)|サムネイル一覧(中央)|Positive/Negativeプロンプトパネル(右)。サムネイルクリックで、その生成画像に実際に埋め込まれているPositive/Negativeプロンプトを表示、それぞれ個別のCopyボタン付き",
helpGallery24: "Select as Styleボタン:選択中の画像のファイル名(拡張子除く)と同名の登録済みStyleを探し、GenerateUIタブのStyleドロップダウンをそれに切り替える — 既存Styleシステムへの視覚的なショートカットであり、埋め込みプロンプトをその場限りで適用するものではない(カタログ画像作成後にStyleがリネーム・削除されていた場合は「一致するスタイルが見つかりません」トーストを表示)",
helpGallery26: "Galleryタブ上部の4つのサブタブ:Output(このブラウザ)・ImagePrompt・Style_Catalog・Metadata — 最後のMetadataは独立したMetadataタブ(該当ヘルプページ参照)をそのまま内包しているため、Galleryを離れずに画像をドロップして埋め込みモデル/プロンプト情報を確認できます",
// -- Tagger Tab --
helpTaggerTitle: "Taggerタブ",
@@ -2206,7 +2257,7 @@ const LANGUAGES = {
helpAiTitle: "AI タブ(A)",
helpAi1: "4ペインレイアウト — Translation | Chat | TOOLS | Settings — 全ペインが常時表示される",
helpAi2: "翻訳ペイン:Ollama、LM Studio、Lemonade、または Unsloth を使って日本語・英語・中国語・カスタム言語(Free)間でテキストを翻訳;⇄ ボタンで言語とテキストを同時入替;選択は自動保存",
helpAi3: "Chat ペイン:LLM とのマルチターン会話;Enter で送信、Shift+Enter で改行;Clear ボタンで会話履歴をリセット;モデルがtool callingに対応している場合、「猫の画像を作って」のように頼むとGenerateUIに読み込み中のワークフローで生成を実行し、結果をチャット内に表示する;ペインヘッダーの「画像生成を許可する」チェックボックスでこの機能のON/OFFを切り替えられる;画像が添付されている場合(TOOLSペインにドロップした画像)はLLMにもビジョン入力として送信され、generate_image呼び出し時はT2Iの代わりにI2I(image-to-image)の元画像として使われる;tool callingとビジョンの動作を確認済みのモデル: Gemma 4:e4b、Qwen 3.5:9b、Ministral 3:3b(Ollama)",
helpAi3: "Chat ペイン:LLM とのマルチターン会話;Enter で送信、Shift+Enter で改行;Clear ボタンで会話履歴をリセット;モデルがtool callingに対応している場合、「猫の画像を作って」のように頼むとGenerateUIに読み込み中のワークフローで生成を実行し、結果をチャット内に表示する;ペインヘッダーの「画像生成を許可する」チェックボックスでこの機能のON/OFFを切り替えられる;画像が添付されている場合(TOOLSペインにドロップした画像)はLLMにもビジョン入力として送信され、generate_image呼び出し時はT2Iの代わりにI2I(image-to-image)の元画像として使われる;generate_image呼び出しにはsteps・cfg・sampler_name・scheduler・denoise・seed、width・height、batch_size(その1回の呼び出しで生成する枚数)、aspect_ratio(\"3:2\"のような比率プリセット。ワークフローにwidth/heightを供給するResolution Selectorノードがある場合のみ適用され、それ以外は無視される)といった任意の生成パラメータも指定できる;モデルが指定しなかった項目は読み込み中のワークフローの現在の設定がそのまま使われる;tool callingとビジョンの動作を確認済みのモデル: Gemma 4:e4b、Qwen 3.5:9b、Ministral 3:3b(Ollama)",
helpAi4: "TOOLS ペイン — VLM:画像(PNG/JPG/WebP)をドロップしてタスク(画像解説 / プロンプト作成 / タグ作成)を選んで Run;または「Create wildcards」を選んでカテゴリ名と件数を入力すると1行1エントリのワイルドカードを生成;ドロップした画像はChatペインの添付としても共有され(Chat側にも表示、I2I生成に使用)、✕ボタンでクリアするまで保持される",
helpAi6: "Settings ペイン:バックエンド(Ollama / LM Studio / Lemonade / Unsloth)の選択、APIのURL設定、接続テスト、モデル選択;Unslothはローカルアクセスでも常にAPIキー認証が必須——プラグインフォルダの .env.example を .env にコピーして UNSLOTH_API_KEY を設定し ComfyUI を再起動する(キーはサーバー側でのみ読み込まれ、ブラウザには一切送信されない);Unsloth Desktopアプリ側の設定画面で「モデル自動切り替え (OpenAI API)」もONにする必要がある——OFFのままだと、Unsloth Studio上で事前にモデルを手動ロードしていない限り HTTP 404 で接続に失敗する;Unslothのreasoningモデルは思考内容をインラインの<think>タグではなく別のreasoning_contentフィールドで返すため、Max tokensが少ないとモデルが思考だけでトークンを使い切り、空の返信になることがある——返信が空になる場合はMax tokensを増やす(2048以上を推奨);Generationセクション — Thinking modeトグル(デフォルトOFF;OllamaにはthinkフラグとしてOFF/ONを送信、バックエンドを問わずOFF時は出力中の<think>...</think>ブロックをクライアント側で除去するため、推論モデルの思考過程がTranslation/Chat/TOOLSの結果に混入しない)とMax tokens(Ollama: num_predict、LM Studio/Lemonade/Unsloth: max_tokens;0で無制限);Free言語名の設定;Chat Image Generationセクションでは、ChatからのT2I画像生成にGenerateUIの現在の読み込みワークフローの代わりに専用ワークフローを使うよう設定できる;Chat I2I Generationセクションは同様の設定を画像添付時のI2I生成向けに提供する;設定は localStorage に保存され、Library パネルの A タブと共有される",
helpAi7: "Chat ペイン — SVG:LLMはComfyUIワークフローを一切使わず、SVGグラフィックをテキストとして直接生成することもできる;「SVGで〜を描いて」のように頼み、応答にSVGコードが含まれていれば自動でインラインプレビュー表示され、「Galleryに保存」ボタンでGalleryタブの出力フォルダに.svgファイルとして保存できる;tool calling(function calling)を使用しないため、tool calling非対応のモデルでも動作する",
@@ -2234,10 +2285,12 @@ const LANGUAGES = {
helpGenCardBatchTypes: "Batchタブ — バッチ種別",
helpGenCardLora: "Modelタブ — LoRA詳細",
helpPromptTitle: "プロンプトタブ",
helpPromptCardToplevel: "Form / Table",
helpPromptCardAi: "AIアシスタント・翻訳",
helpPromptCardPresets: "プリセット",
helpPromptCardWildcards: "ワイルドカード",
helpPromptCardStyleTitle: "Style",
helpPromptCardTable: "テーブル表示",
helpMetadataCardOverview: "概要",
helpMetadataCardExtracted: "抽出情報",
helpMetadataCardFormats: "対応フォーマット",
@@ -2520,6 +2573,7 @@ const LANGUAGES = {
helpMetadata3: "右列: LoRA 一覧(強度値付き)+プロンプト一覧(クリックで下部に全文表示)",
helpMetadata4: "プロンプトアクション — Copy: クリップボードにコピー; GenUI:P/N: GenerateUI のポジティブ/ネガティブプロンプトにセット; Prompt:P/N: Prompt タブ Presets のポジティブ/ネガティブにセット",
helpMetadata5: "対応フォーマット: ComfyUI PNG/WebP/JSON(標準形式 + Flux.2 / Qwen-Image / Z-Image / Mage-Flow / LongCat / Boogu / HiDream E1 / FireRed / Ernie Image サブグラフ形式)・SD WebUI・SD Forge・Fooocus。",
helpMetadata6: "現在は Gallery タブ内のサブタブ(Style_Catalog の右隣)になっており、独立したトップレベルタブではありません — それ以外の動作は全く同じです",
},
zh: {
@@ -2609,6 +2663,7 @@ const LANGUAGES = {
copyAndSendCanvas: "复制并发送到画布",
workflowSentToCanvas: "工作流已就绪 — 将标题拖拽到画布",
workflowSentToCanvasDirect: "工作流已发送到画布",
workflowFilteredInLibrary: "已在Workflow Studio Library(W标签)中筛选 — 请将卡片拖拽到画布",
comfyUILoadTimeout: "ComfyUI加载超时",
popupBlocked: "弹窗被阻止。请允许此站点的弹窗。",
setAsDefault: "设为默认",
@@ -2752,6 +2807,7 @@ const LANGUAGES = {
labBatchCountTooSmall: "请先增加Batch数量再添加关键帧",
labKeyframesExceedBatch: (maxIter, batchCount) => `存在设置到第${maxIter}次的关键帧,超过了当前Batch数量(${batchCount}),在增加Batch数量之前不会被应用`,
labUseWorkflowDefault: "(保持工作流原设置)",
labNoNodeForColumn: "此工作流中没有对应节点",
labInheritsFrom: (val) => `(继承: ${val})`,
labLoraNoNode: "工作流中未找到LoRA节点 — 已跳过LoRA设置",
labAppliedAt: (n) => `应用:${n}`,
@@ -2760,6 +2816,10 @@ const LANGUAGES = {
labRevertCheckboxLabel: "恢复第1次的设置",
labBypassed: "⏭ 已跳过",
labBypassCheckboxLabel: "跳过此设置(保持它之前生效的设置)",
labLoraNodeBypassed: "⛔(节点已绕过)",
labLoraNodeBypassCheckboxLabel: "绕过LoRA节点(强度设为0)",
labLoraApplyToPromptCheckboxLabel: "将LoRA语法和触发词应用到正面提示词",
labLoraPromptAppliedTag: "→P",
labSeedEmptyRandom: "Seed(留空为随机)",
labPause: "暂停",
labResume: "继续",
@@ -3063,6 +3123,7 @@ const LANGUAGES = {
modelsSideGroup: "分组",
modelsSideCivitai: "CivitAI",
gallerySideMetadata: "元数据",
gallerySidePrompt: "提示词",
// -- Gallery --
galleryAllTags: "所有标签",
@@ -3082,6 +3143,7 @@ const LANGUAGES = {
galleryTabOutput: "Output",
galleryTabImagePrompt: "ImagePrompt",
galleryTabStyleCatalog: "Style_Catalog",
galleryTabMetadata: "元数据",
galleryNoSelection: "未选择",
imagePromptSelectedPrompt: "提示词",
imagePromptNoPrompt: "此图像没有提示词数据。",
@@ -3299,6 +3361,8 @@ const LANGUAGES = {
helpGen22: "Stack标签 — Use Stack Group复选框(仅当工作流中存在Lora Manager节点时显示):OFF(加载工作流时的默认状态)显示并编辑Lora Manager节点中实际配置的LoRA;ON则改为显示并编辑Models标签中已注册的\"Stack\"分组;切换任一方向都会先清空节点的LoRA列表,再从新选择的来源加载;切回OFF会精确恢复为加载工作流那一刻的LoRA列表(加载时已保存快照)",
helpGen23: "Stack标签的操作:每行都有启用复选框及Str M / Str C字段;表头的\"Stack\"复选框可一次性切换所有行;顶部的+/-步进按钮可同时调整所有当前启用行的强度;\"Showing: LoRAs currently in workflow\" / \"Showing: registered Stack group\"标签指示当前显示的是哪个来源;点击Apply会将完整列表写回目标节点,并同时将LoRA语法和CivitAI触发词追加到Positive提示词",
helpGen24: "使用 CLIP Text Encode edit+(CLIPTextEncodeEditPlus)节点时的注意事项:当Lora Manager节点的trigger_words输出连接到该节点的text1输入时(RAW/EDIT/front/back模式会将text1与可编辑的text_edit字段组合),触发词在生成时已自动包含;此时点击LoRA的Apply按钮或开启Use Stack Group会额外把LoRA语法/触发词写入text_edit,造成重复 — 如果想保持工作流原始的提示词和LoRA配置不被修改,请保持Use Stack Group为OFF,并避免点击LoRA的Apply类按钮",
helpGen29: "绕过按钮(⛔,位于LoRA列表头,紧邻Refresh Stack group):将目标LoRA节点的强度强制设为0——普通LoraLoader对应strength_model/strength_clip,Lora Manager节点则对应列表中每一项的strength/clipStrength(LoRA列表本身保持不变,不会被替换),不会改动实际ComfyUI节点的mode;再次点击可精确恢复切换前的数值;作用于Single/Stack Apply默认所用的节点(工作流中若有Lora Manager节点则用它,否则用第一个LoRA节点);点击Single或Stack的Apply按钮会清除绕过状态,避免其在之后被无声地重新生效",
helpGen30: "Save按钮(子标签栏右端)在Batch或Lab子标签处于激活状态时会隐藏,因为它们操作的是工作流副本或各自的Plan文件,而不是当前加载的工作流本身;切换回Input/Model/Settings/Feeder时会重新显示",
helpFeederTitle: "GenerateUI Tab — Feeder 子标签",
helpFeederDesc: "通过左侧面板顶部的 [图片循环] / [图库] 切换按钮可在两种模式间切换。",
helpFeederImgloopTitle: "图片循环模式",
@@ -3337,8 +3401,10 @@ const LANGUAGES = {
helpLab11: "Prompt单元格编辑弹窗中还提供Clear(将Positive和Negative都清空)和Get from Previous(从同一列紧邻的上一个关键帧复制Positive和Negative——例如编辑#3时会整段复制#2的提示词);关键帧#1没有更早的行可复制,因此不显示该按钮",
helpLab12: "每一列的关键帧#1,只要还没有被显式保存覆盖,就会用当前GenerateUI中已加载工作流的实际设置代替空白占位显示(Model列仅覆盖Checkpoint和VAE两项——LoRA不会被实时反映,因为无法从工作流节点中安全地反推出\"当前生效的\"LoRA),同时体现在其网格单元格(虚线边框)和编辑弹窗的预填内容中;哪怕不做任何修改直接保存,也会将该值锁定为真正的覆盖值,虚线/实时显示随之消失;每次切换到Lab子标签页,都会重新根据当前GenerateUI中加载的内容刷新",
helpLab13: "Bypass复选框(包括#1在内的每个关键帧都有):在判定某次迭代应使用哪一行设置时,会把该行完全跳过,如同它不存在——于是它之前生效的设置(更早的关键帧,或最终什么都不应用)会继续保持;这只是Lab内部的概念,绝不会改动实际ComfyUI节点的mode;勾选后会禁用该行的取值字段以及其Revert复选框(二者互斥,不能同时生效);单元格会显示琥珀色的「⏭ Bypassed」标签,其优先级高于Revert显示和实时显示",
helpLab14: "Model列表头的C/L/V按钮用于切换单元格显示与编辑弹窗当前针对Checkpoint/LoRA/VAE(LoRA仅支持Single,不支持Stack)中的哪一项。未在查看的另外两项即使保存也会保持原值不变。当前加载的工作流中若确实存在对应类型的节点,相应按钮会变为与GenerateUI的Model标签页相同的高亮颜色(可在Settings中调整)。若设置了LoRA但工作流中完全没有LoRA节点,Run执行期间只会警告一次并跳过该项设置——与VAE不同,Lab不会自动插入LoRA节点,因为那需要重新连接Checkpoint的Model/CLIP输出线路",
helpLab15: "Model列的三个字段(Checkpoint/LoRA/VAE)各自独立向后延续:即使某个关键帧只设置了LoRA,Checkpoint/VAE也不会恢复为工作流原始设置,而是保持更早的关键帧最后设置的值(直到之后的关键帧明确修改它为止)。在编辑弹窗中,若某字段留空且存在可继承的值,其占位符会显示「(继承: ...)」而不是「(保持工作流原设置)」,方便确认实际会应用的值。「恢复第1次的设置」会一次性将三个字段全部恢复为关键帧#1的内容,而不仅仅是当前正在查看的这一项",
helpLab14: "Model列表头的C/L/V按钮用于切换Checkpoint、LoRA(仅支持Single,不支持Stack)、VAE这三个相互独立的关键帧列表中,当前单元格显示、+/-以及编辑弹窗针对的是哪一个。三者各自拥有完全独立的时间线,与Prompt、KSampler完全相同,只是共用同一列的显示空间,看起来像是合并在一起。查看其中一项时添加、删除或编辑关键帧,完全不会影响另外两项。当前加载的工作流中若确实存在对应类型的节点,相应按钮会变为与GenerateUI的Model标签页相同的高亮颜色(可在Settings中调整)。若设置了LoRA但工作流中完全没有LoRA节点,Run执行期间只会警告一次并跳过该项设置——与VAE不同,Lab不会自动插入LoRA节点,因为那需要重新连接Checkpoint的Model/CLIP输出线路",
helpLab15: "Checkpoint/LoRA/VAE与其他列采用完全相同的实际生效值规则(见上文),各自独立向后延续:其中一项的关键帧不会使另外两项恢复为工作流原始设置。在编辑弹窗中,若某字段留空,且同一列表中更早的关键帧已设置过值,其占位符会显示「(继承: ...)」而不是「(保持工作流原设置)」,方便确认实际会应用的值。「恢复第1次的设置」和Bypass与+/-一样,只会作用于当前C/L/V切换选中的那一项,不会影响另外两项",
helpLab16: "绕过LoRA节点复选框(仅LoRA关键帧自己的编辑弹窗中):在应用时强制将该关键帧的strength_model/strength_clip设为0,替代ComfyUI原生的节点Bypass(mode:4)——这里使用的工作流已转换为API格式,不带mode概念,因此将强度归零在效果上是等价的。勾选后会禁用强度输入框;与上方行级别的Bypass复选框(见helpLab13)相互独立,后者只是跳过整行而不会改动节点本身",
helpLab17: "将LoRA语法和触发词应用到正面提示词复选框(仅LoRA列):勾选后,会在应用时以逗号分隔的形式,将<lora:name:strengthModel:strengthClip>及该LoRA缓存的CivitAI触发词追加到正面提示词——格式与GenerateUI的Model标签页Apply按钮写入的完全一致。切换关键帧时(更换为另一个LoRA、取消勾选此复选框,或该行被Bypass)总会先精确移除上一次迭代注入的内容,再添加新内容,因此批次不会累积用过的所有LoRA。勾选「绕过LoRA节点」时会自动禁用",
helpLabCardPlanTitle: "Setting子标签 — 运行与方案文件",
helpLabPlan1: "右侧面板 — Batch:要运行的迭代次数;Note:与方案一起保存的自由备注",
helpLabPlan2: "Run/Pause/Cancel:Run开始批量循环;Pause/Resume在各次迭代之间暂停/继续;Cancel会中断当前生成并停止循环",
@@ -3371,6 +3437,17 @@ const LANGUAGES = {
helpPrompt12: "重命名后仍保留其在原定义文件中的位置;新建的Style会保存到style文件夹内专用的custom.json中,因此内置的风格包文件不会被直接修改;名称在所有Style中必须唯一",
helpPrompt13: "保存、重命名或删除Style会立即刷新GenerateUI的Style下拉菜单、Batch标签的风格勾选列表以及Style Catalog的名称列表 — 无需重新加载",
helpPrompt14: "列表中每个条目的名称旁都会以小徽章显示其来源文件(例如sdxl_styles_diva.json);编辑现有Style时,该文件名也会显示在Positive prompt字段上方,并且Save按钮旁会出现「+ Add to this file」按钮 — 点击后表单会清空,可创建一个新Style并追加到同一文件,而不是默认的custom.json",
helpPrompt15: "两个顶层标签:Form是下文所述的传统单项逐一编辑界面(AI助手/Presets/Wildcard/Style各面板);Table将同一份数据一次性以可编辑表格形式全部显示 — 当预设、通配符文件或Style数量较多时更实用",
helpPrompt16: "Table标签内还有四个子标签:Presets、Presets Group、Wildcards、Style — 均为与Form标签完全相同数据的可编辑表格,任一侧的修改都会同步到另一侧",
helpPrompt17: "每个表格左侧共用一个自动编号的#列:点击编号可选中该行(红色高亮),可同时选中多行;工具栏的Delete按钮会在一次确认后删除所有选中行,Deselect用于清除选择。此设计取代了容易误触的逐行删除按钮",
helpPrompt18: "双击行号会打开更大的提示词编辑弹窗,取代表格内较小的文本框。Presets和Style在共用的一个文本框上方显示Positive/Negative切换按钮(切换时会保留另一侧已输入的内容);Wildcards仅显示文件内容。Save会写回并关闭弹窗,Close则放弃更改",
helpPrompt19: "工具栏的「+ New ...」按钮会在顶部添加一个空白可编辑行,编号位置显示小图标✓(Add)/✕(Cancel);填写内容后点击✓即可创建,点击✕则放弃该行且不保存",
helpPrompt20: "Presets表格 — ★(收藏)和B(Batch分组)两列如同开关:点击变为金色/黄色,再次点击取消;工具栏的BC按钮可一次性清空Batch分组中的所有预设,与Form标签的Preset Manager效果相同",
helpPrompt21: "Presets表格工具栏 — PP Copy/NP Copy用于将当前选中的单行的正/负向提示词复制到剪贴板;分组下拉框+Add to Group可一键将所有已选中的预设添加到所选分组。在此处进行分组分配(先在Presets表格中搜索/选择,再从简短的分组列表中挑选)取代了旧版按分组选择预设的方式,后者在预设较多时已难以使用",
helpPrompt22: "Presets Group表格 — 创建、重命名、删除分组;每个分组的成员以可删除的徽章(✕)形式显示。保留分组Batch不会显示在这里 — 它完全通过Presets表格的B列和BC按钮进行管理",
helpPrompt23: "Wildcards表格 — 与Form标签的通配符文件管理器使用相同的文件列表;编辑File列的路径或Ext下拉框会重命名文件(先以新名称保存内容,再删除旧文件)",
helpPrompt24: "Style表格 — 与Form标签的Style管理器使用相同的Style列表,可像Presets一样进行内联编辑",
helpPrompt25: "Presets表格工具栏 — 搜索框旁边的★和B排序按钮,可将收藏/已加入Batch的预设排到列表顶部;两者可同时启用(★优先,B为次要条件),其余项保持原有的相对顺序",
helpSettings1: "UI语言和摘要语言设置",
helpSettings2: "主题选择(13种预设)+ 自定义:颜色调整、背景图案(条纹、波点、棋盘格、SVG平铺)、字体选择(含日文显示字体在内的16种Google Fonts)",
helpSettings3: "工作流数据文件夹路径更改/重置",
@@ -3430,6 +3507,7 @@ const LANGUAGES = {
helpGallery23: "相同的三栏布局:文件夹树(左)|缩略图(中)|Positive/Negative提示词面板(右)。点击缩略图可查看该生成图像中实际嵌入的Positive/Negative提示词,各自带有独立的Copy按钮",
helpGallery24: "Select as Style按钮:以选中图像的文件名(不含扩展名)匹配已注册的Style名称,并将GenerateUI标签的Style下拉框切换为该风格 — 这是通往现有命名Style系统的可视化快捷方式,而非一次性应用嵌入的提示词(如果目录图像生成后该Style已被重命名或删除,则会显示「未找到匹配的风格」提示)",
helpGallery25: "载入GenerateUI / 在Metadata标签中打开按钮:读取目录图像本身内嵌的工作流(目录副本携带与原始输出图像相同的PNG元数据),可直接载入GenerateUI编辑器,或在Metadata标签中打开该图像以供查看 — 无需在Output文件夹中寻找原始文件",
helpGallery26: "Gallery标签页顶部有四个子标签:Output(此浏览器)、ImagePrompt、Style_Catalog、Metadata — 最后一个直接内嵌完整的Metadata标签页(参见其专属帮助页),因此无需离开Gallery即可拖入图像查看其内嵌的模型/提示词数据",
// -- Tagger Tab --
helpTaggerTitle: "Tagger标签",
@@ -3464,7 +3542,7 @@ const LANGUAGES = {
helpAiTitle: "AI 标签页(A)",
helpAi1: "4 面板布局 — Translation | Chat | TOOLS | Settings — 所有面板始终可见",
helpAi2: "翻译面板:使用 Ollama、LM Studio、Lemonade 或 Unsloth 在日语、英语、中文或自定义语言(Free)之间翻译;⇄ 按钮同时互换语言和文本内容;选择自动保存",
helpAi3: "Chat 面板:与 LLM 进行多轮对话;Enter 发送消息,Shift+Enter 换行;Clear 按钮重置对话历史;如果模型支持 tool calling,当你要求生成图像(例如\"生成一张猫的图片\")时,会使用 GenerateUI 中当前加载的工作流执行生成,并在对话中显示结果;面板标题栏的\"允许生成图像\"复选框可开关此功能;如果附加了图像(在 TOOLS 面板中拖放的图像),该图像会作为视觉输入发送给 LLM,调用 generate_image 时会以该图像为基础执行图生图(I2I),而不是文生图;已验证可正常支持 tool calling 与视觉功能的模型:Gemma 4:e4b、Qwen 3.5:9b、Ministral 3:3b(Ollama)",
helpAi3: "Chat 面板:与 LLM 进行多轮对话;Enter 发送消息,Shift+Enter 换行;Clear 按钮重置对话历史;如果模型支持 tool calling,当你要求生成图像(例如\"生成一张猫的图片\")时,会使用 GenerateUI 中当前加载的工作流执行生成,并在对话中显示结果;面板标题栏的\"允许生成图像\"复选框可开关此功能;如果附加了图像(在 TOOLS 面板中拖放的图像),该图像会作为视觉输入发送给 LLM,调用 generate_image 时会以该图像为基础执行图生图(I2I),而不是文生图;generate_image 还接受可选的生成参数——steps、cfg、sampler_name、scheduler、denoise、seed、width、height、batch_size(该次调用生成的图像数量),以及 aspect_ratio(形如\"3:2\"的比例预设,仅当工作流中存在向 width/height 供值的 Resolution Selector 节点时才会生效,否则忽略);模型未提供的参数将保留当前已加载工作流的设置;已验证可正常支持 tool calling 与视觉功能的模型:Gemma 4:e4b、Qwen 3.5:9b、Ministral 3:3b(Ollama)",
helpAi4: "TOOLS 面板 — VLM:拖放图像(PNG/JPG/WebP)并选择任务(图像描述 / 提示词生成 / 标签生成),点击 Run;或选择「Create wildcards」,输入类别名称和数量生成纯文本通配符条目(每行一条);拖放的图像也会作为 Chat 面板的附件共享(在该处显示并用于图生图生成),直到点击 ✕ 按钮清除",
helpAi6: "Settings 面板:选择后端(Ollama / LM Studio / Lemonade / Unsloth)、设置 API URL、测试连接、选择模型;Unsloth 即使在本地访问时也始终需要 API 密钥认证——将插件文件夹中的 .env.example 复制为 .env 并设置 UNSLOTH_API_KEY,然后重启 ComfyUI(密钥仅在服务器端读取,绝不会发送到浏览器);还需要在 Unsloth Desktop 应用自身的设置界面中开启「Auto model switching (OpenAI API)」——若保持关闭,除非已在 Unsloth Studio 中手动加载过模型,否则请求会以 HTTP 404 失败;Unsloth 的推理模型会将思考内容放在单独的 reasoning_content 字段中,而不是内联的 <think> 标签里,因此 Max tokens 太小会导致模型把全部配额都用在思考上,返回空白回复——如果回复为空,请调高 Max tokens(建议 2048 以上);Generation 部分 — Thinking mode 开关(默认关闭;Ollama会收到think标志,无论使用哪个后端,关闭时都会从输出中去除<think>...</think>思考块,避免推理模型的思考过程混入Translation/Chat/TOOLS的结果)以及 Max tokens(Ollama: num_predict,LM Studio/Lemonade/Unsloth: max_tokens;0为不限);配置 Free 语言名称;Chat 图像生成部分可让 Chat 触发的文生图使用专用工作流,而不是 GenerateUI 中当前加载的工作流;Chat I2I 生成部分提供相同设置,用于附加图像时的图生图生成;设置保存至 localStorage,并与 Library 面板的 A 标签共享",
helpAi7: "Chat 面板 — SVG:LLM 还可以直接以文本形式生成 SVG 图形,完全不涉及 ComfyUI 工作流;只需要求它用 SVG 绘制内容,如果回复中包含 SVG 代码,会自动渲染为内嵌预览,并提供\"保存到图库\"按钮,将其保存为 .svg 文件到 Gallery 标签页的输出文件夹;该功能不使用 tool calling(函数调用),因此不支持 tool calling 的模型也可以使用",
@@ -3492,10 +3570,12 @@ const LANGUAGES = {
helpGenCardBatchTypes: "Batch 标签 — 批量类型",
helpGenCardLora: "Model 标签 — LoRA 详情",
helpPromptTitle: "提示词标签页",
helpPromptCardToplevel: "Form / Table",
helpPromptCardAi: "AI 助手与翻译",
helpPromptCardPresets: "预设",
helpPromptCardWildcards: "通配符",
helpPromptCardStyleTitle: "Style",
helpPromptCardTable: "表格视图",
helpMetadataCardOverview: "概述",
helpMetadataCardExtracted: "提取的信息",
helpMetadataCardFormats: "支持的格式",
@@ -3778,6 +3858,7 @@ const LANGUAGES = {
helpMetadata3: "右列:LoRA 列表(含强度值)和提示词列表(点击在下方显示全文)",
helpMetadata4: "提示词操作 — Copy:复制到剪贴板;GenUI:P/N:设置 GenerateUI 正/负向提示;Prompt:P/N:设置 Prompt 标签页预设正/负向提示",
helpMetadata5: "支持格式:ComfyUI PNG/WebP/JSON(标准格式 + Flux.2 / Qwen-Image / Z-Image / Mage-Flow / LongCat / Boogu / HiDream E1 / FireRed / Ernie Image 子图工作流)、SD WebUI、SD Forge、Fooocus。",
helpMetadata6: "现在它是 Gallery 标签页内的一个子标签(位于 Style_Catalog 右侧),不再是独立的顶层标签页 — 其余操作方式完全相同",
},
};
+355 -216
View File
@@ -17,70 +17,89 @@ import { extractAllMetadata, readAllPNGTextChunks } from "./metadata-tab.js";
import { syncJsonHighlight, syncScroll } from "./json-highlight.js";
import { _expandWildcardsInWorkflow } from "./generate-tab.js";
const COLUMN_KEYS = ["model", "prompt", "ksampler"];
// Checkpoint/LoRA/VAE are three fully independent keyframe timelines — same as
// Prompt/KSampler — each with its own +/-, its own atIteration sequence, its own
// forward-fill resolution (_resolveKeyframe). The Model column header's C/L/V toggle
// only picks which ONE of the three renders into the (single, shared) cell list and
// which one +/- and the cell-click modal operate on; it never bundles them together.
const COLUMN_KEYS = ["checkpoint", "lora", "vae", "prompt", "ksampler"];
const MAX_RESULTS = 9;
const RESULTS_GRID_COLS = 3;
const LAB_PLAN_PREFIX = "ws_labplan_";
const LAB_PLAN_PNG_KEY = "wfm_lab_plan";
function _defaultValueFor(col) {
if (col === "model") return { checkpoint: "", lora: { name: "", strengthModel: 1.0, strengthClip: 1.0 }, vae: "" };
if (col === "lora") return { name: "", strengthModel: 1.0, strengthClip: 1.0, nodeBypass: false, applyToPrompt: false };
if (col === "prompt") return { positive: "", negative: "", styleApplied: false, styleName: "" };
if (col === "ksampler") return { steps: null, cfg: null, sampler_name: "", scheduler: "", denoise: null, seed: null };
return "";
return ""; // checkpoint, vae
}
// Fills in defaults for a possibly-partial "model" column value — both for legacy
// plan data migrated by _migrateLabColumns (which never sets a lora) and defensively
// for any hand-edited Plan JSON.
function _normalizeModelValue(v) {
// Fills in defaults for a possibly-partial LoRA value — both for legacy plan data
// migrated by _migrateLabColumns and defensively for any hand-edited Plan JSON.
function _normalizeLoraValue(v) {
return {
checkpoint: v?.checkpoint || "",
lora: {
name: v?.lora?.name || "",
strengthModel: v?.lora?.strengthModel ?? 1.0,
strengthClip: v?.lora?.strengthClip ?? 1.0,
},
vae: v?.vae || "",
name: v?.name || "",
strengthModel: v?.strengthModel ?? 1.0,
strengthClip: v?.strengthClip ?? 1.0,
nodeBypass: !!v?.nodeBypass,
applyToPrompt: !!v?.applyToPrompt,
};
}
function _emptyColumns() {
return {
model: [{ atIteration: 1, value: _defaultValueFor("model"), revertToBase: false, bypassed: false }],
checkpoint: [{ atIteration: 1, value: _defaultValueFor("checkpoint"), revertToBase: false, bypassed: false }],
lora: [{ atIteration: 1, value: _defaultValueFor("lora"), revertToBase: false, bypassed: false }],
vae: [{ atIteration: 1, value: _defaultValueFor("vae"), revertToBase: false, bypassed: false }],
prompt: [{ atIteration: 1, value: _defaultValueFor("prompt"), revertToBase: false, bypassed: false }],
ksampler: [{ atIteration: 1, value: _defaultValueFor("ksampler"), revertToBase: false, bypassed: false }],
};
}
// Legacy Plan JSON (pre "Model column" merge) stored separate checkpoint/vae columns.
// Merges them into one "model" column keyed by atIteration so old .json/.png plans
// still load correctly. LoRA is left empty since the old format never had it.
// Two prior on-disk Plan JSON shapes need upgrading to today's independent
// checkpoint/lora/vae arrays:
// - Pre-v0.4.2: separate top-level `checkpoint`/`vae` columns, no `lora` at all.
// - v0.4.2's brief "merged Model row" design: `columns.model` was a single array of
// {atIteration, value:{checkpoint,lora,vae}, ...} rows bundling all three together.
// That bundling caused its own bugs (+/- and the cell modal affected all three
// fields at once) and was replaced the same release cycle, but a plan saved during
// that window must still load. Values are moved into their own independent arrays;
// iteration #1 is always kept (baseline row) even if a field was blank there.
function _migrateLabColumns(columns) {
if (!columns) return _emptyColumns();
if (columns.model) return columns;
if (!columns.model && Array.isArray(columns.lora)) return columns; // already current shape (independent lora array is the marker)
const ckptKfs = columns.checkpoint || [];
const vaeKfs = columns.vae || [];
const { checkpoint, vae, ...rest } = columns;
const defaults = _emptyColumns();
if (ckptKfs.length === 0 && vaeKfs.length === 0) {
return { ...rest, model: _emptyColumns().model };
if (Array.isArray(columns.model)) {
const checkpoint = [], lora = [], vae = [];
for (const kf of columns.model) {
const v = kf.value || {};
const base = { atIteration: kf.atIteration, revertToBase: !!kf.revertToBase, bypassed: !!kf.bypassed };
if (kf.atIteration === 1 || v.checkpoint) checkpoint.push({ ...base, value: v.checkpoint || "" });
if (kf.atIteration === 1 || v.lora?.name) lora.push({ ...base, value: _normalizeLoraValue(v.lora) });
if (kf.atIteration === 1 || v.vae) vae.push({ ...base, value: v.vae || "" });
}
const { model, ...rest } = columns;
return {
...rest,
checkpoint: checkpoint.length ? checkpoint : defaults.checkpoint,
lora: lora.length ? lora : defaults.lora,
vae: vae.length ? vae : defaults.vae,
};
}
const iterations = [...new Set([1, ...ckptKfs.map((k) => k.atIteration), ...vaeKfs.map((k) => k.atIteration)])].sort((a, b) => a - b);
const findAt = (kfs, iter) => kfs.find((k) => k.atIteration === iter);
const model = iterations.map((iter) => {
const ck = findAt(ckptKfs, iter);
const vk = findAt(vaeKfs, iter);
if (columns.checkpoint || columns.vae) {
return {
atIteration: iter,
bypassed: !!(ck?.bypassed || vk?.bypassed),
revertToBase: !!(ck?.revertToBase || vk?.revertToBase),
value: { checkpoint: ck?.value || "", lora: { name: "", strengthModel: 1.0, strengthClip: 1.0 }, vae: vk?.value || "" },
...columns,
checkpoint: columns.checkpoint?.length ? columns.checkpoint : defaults.checkpoint,
lora: defaults.lora,
vae: columns.vae?.length ? columns.vae : defaults.vae,
};
});
return { ...rest, model };
}
return columns;
}
function _emptyLabState() {
@@ -343,10 +362,22 @@ function _initPlanButtons() {
// Column keyframes: render / add / remove / edit
// ============================================
// A click on +/- always targets whichever of checkpoint/lora/vae the Model column
// header's C/L/V toggle currently has selected — "model" is a sentinel dataset.col
// value on those two shared buttons (there's only one +/- pair in the DOM for the
// merged-looking Model column), never a real key into _lab.columns. Resolving it here
// means adding/removing a keyframe only ever touches the ONE field being viewed, never
// the other two — Prompt/KSampler's own +/- buttons carry their real col directly and
// pass through unchanged.
function _resolveColumnKey(col) {
return col === "model" ? _modelViewMode : col;
}
function _initColumnButtons() {
document.querySelectorAll(".wfm-lab-col-add").forEach((btn) => {
btn.addEventListener("click", () => {
const col = btn.dataset.col;
const col = _resolveColumnKey(btn.dataset.col);
if (!_hasNodeForColumn(col)) return; // no matching node to apply an override to
const kfs = _lab.columns[col];
const lastIter = kfs[kfs.length - 1].atIteration;
if (lastIter >= _lab.batchCount) {
@@ -354,42 +385,83 @@ function _initColumnButtons() {
return;
}
kfs.push({ atIteration: lastIter + 1, value: _defaultValueFor(col), revertToBase: false, bypassed: false });
_renderColumn(col);
_renderColumnForKey(col);
_openCellModal(col, kfs.length - 1);
});
});
document.querySelectorAll(".wfm-lab-col-remove").forEach((btn) => {
btn.addEventListener("click", () => {
const col = btn.dataset.col;
const col = _resolveColumnKey(btn.dataset.col);
const kfs = _lab.columns[col];
if (kfs.length <= 1) return; // Row 1 (baseline) cannot be removed
kfs.pop();
_renderColumn(col);
_renderColumnForKey(col);
});
});
}
// Wires the Model column header's C/L/V buttons — switches which single field its cells
// summarize (see _modelViewMode) and re-renders just that column. Purely a display
// filter: the underlying keyframe data (all three fields) is untouched either way.
// Wires the Model column header's C/L/V buttons — switches which independent field
// (checkpoint/lora/vae) the shared cell list, +/-, and cell-click modal all operate on.
// Purely a display/routing selector: none of the three keyframe arrays are touched by
// switching it.
function _initModelViewToggle() {
document.querySelectorAll(".wfm-lab-model-view-btn").forEach((btn) => {
btn.addEventListener("click", () => {
_modelViewMode = btn.dataset.view;
document.querySelectorAll(".wfm-lab-model-view-btn").forEach((b) => b.classList.toggle("active", b === btn));
_renderColumn("model");
_renderModelColumn();
});
});
}
function _renderAllColumns() {
COLUMN_KEYS.forEach(_renderColumn);
_renderModelColumn();
_renderColumn("prompt");
_renderColumn("ksampler");
}
function _renderColumn(col) {
const container = document.getElementById(`wfm-lab-col-cells-${col}`);
// Renders the Model column's shared cell list from whichever of checkpoint/lora/vae is
// currently toggled (_modelViewMode) — the other two arrays exist in _lab.columns but
// have no DOM of their own to render into; they're only ever shown when toggled to.
function _renderModelColumn() {
_updateModelViewHighlight();
_renderColumn(_modelViewMode, "wfm-lab-col-cells-model");
}
// Routes a keyframe-array key to whichever container renders it: checkpoint/lora/vae
// all share the Model column's single cell list, everything else has its own.
function _renderColumnForKey(col) {
if (col === "checkpoint" || col === "lora" || col === "vae") _renderModelColumn();
else _renderColumn(col);
}
// True when the loaded workflow actually has a node the given Model column (checkpoint/
// lora/vae) could apply an override to. VAE is special-cased: even with no standalone
// VAELoader node, a checkpoint node means _buildWorkflowForIteration can still inject one
// to override the checkpoint's baked-in VAE — so VAE only counts as "absent" when there's
// no checkpoint either. Non-model columns (prompt/ksampler) always have somewhere to
// apply, so they're not gated by this.
function _hasNodeForColumn(col) {
const analysis = comfyUI.currentAnalysis;
if (col === "checkpoint") return (analysis?.checkpoint_nodes?.length || 0) > 0;
if (col === "lora") return (analysis?.lora_nodes?.length || 0) > 0;
if (col === "vae") {
return (analysis?.vae_nodes?.length || 0) > 0 || (analysis?.checkpoint_nodes?.length || 0) > 0;
}
return true;
}
function _renderColumn(col, containerId = `wfm-lab-col-cells-${col}`) {
const container = document.getElementById(containerId);
if (!container) return;
if (col === "model") _updateModelViewHighlight();
// No matching node in the loaded workflow for this Model sub-column: showing a
// baseline "Setting 1" keyframe here would be a no-op at Run time (_buildWorkflowForIteration
// silently skips writes when the target node id doesn't exist), so skip rendering it
// entirely rather than presenting an override control that can never do anything.
if (!_hasNodeForColumn(col)) {
container.innerHTML = `<div class="wfm-lab-cell-empty">${t("labNoNodeForColumn")}</div>`;
return;
}
const keyframes = _lab.columns[col];
container.innerHTML = keyframes.map((kf, idx) => _cellHtml(col, kf, idx)).join("");
container.querySelectorAll(".wfm-lab-cell").forEach((el) => {
@@ -404,18 +476,15 @@ function _renderColumn(col) {
// with whatever's loaded (initial render, keyframe edits, toggle clicks, plan load/
// apply, and refreshLabLiveDefaults() when switching into the Lab subtab).
function _updateModelViewHighlight() {
const analysis = comfyUI.currentAnalysis;
const hasCheckpoint = (analysis?.checkpoint_nodes?.length || 0) > 0;
const hasLora = (analysis?.lora_nodes?.length || 0) > 0;
const hasVae = (analysis?.vae_nodes?.length || 0) > 0;
document.querySelector('.wfm-lab-model-view-btn[data-view="checkpoint"]')?.classList.toggle("wfm-model-label-active", hasCheckpoint);
document.querySelector('.wfm-lab-model-view-btn[data-view="lora"]')?.classList.toggle("wfm-model-label-active", hasLora);
document.querySelector('.wfm-lab-model-view-btn[data-view="vae"]')?.classList.toggle("wfm-model-label-active", hasVae);
document.querySelector('.wfm-lab-model-view-btn[data-view="checkpoint"]')?.classList.toggle("wfm-model-label-active", _hasNodeForColumn("checkpoint"));
document.querySelector('.wfm-lab-model-view-btn[data-view="lora"]')?.classList.toggle("wfm-model-label-active", _hasNodeForColumn("lora"));
document.querySelector('.wfm-lab-model-view-btn[data-view="vae"]')?.classList.toggle("wfm-model-label-active", _hasNodeForColumn("vae"));
}
function _cellHtml(col, kf, idx) {
const revertClass = kf.revertToBase ? " wfm-lab-cell-revert" : "";
const bypassClass = kf.bypassed ? " wfm-lab-cell-bypassed" : "";
const nodeBypassClass = (col === "lora" && kf.value?.nodeBypass) ? " wfm-lab-cell-node-bypassed" : "";
if (idx === 0) {
// Bypassed takes priority over the live-reflect display — an explicit "skip this
// row" toggle shouldn't be masked by Setting 1's usual "show the workflow's
@@ -423,13 +492,13 @@ function _cellHtml(col, kf, idx) {
const live = !kf.bypassed && _isLiveDisplay(col, idx);
const liveClass = live ? " wfm-lab-cell-live" : "";
const label = live ? _cellLabel(col, { value: _liveValueFor(col), revertToBase: false, bypassed: false }) : _cellLabel(col, kf);
return `<div class="wfm-lab-cell${revertClass}${bypassClass}${liveClass}" data-idx="${idx}">
return `<div class="wfm-lab-cell${revertClass}${bypassClass}${nodeBypassClass}${liveClass}" data-idx="${idx}">
<span class="wfm-lab-cell-index">${kf.atIteration}:</span>
<span class="wfm-lab-cell-value">${label}</span>
</div>`;
}
// Iteration #2 and beyond: value on its own line, applied-at iteration below it
return `<div class="wfm-lab-cell${revertClass}${bypassClass}" data-idx="${idx}">
return `<div class="wfm-lab-cell${revertClass}${bypassClass}${nodeBypassClass}" data-idx="${idx}">
<div class="wfm-lab-cell-value">${_cellLabel(col, kf)}</div>
<div class="wfm-lab-cell-applied">${t("labAppliedAt", kf.atIteration)}</div>
</div>`;
@@ -438,19 +507,15 @@ function _cellHtml(col, kf, idx) {
function _cellLabel(col, kf) {
if (kf.bypassed) return t("labBypassed");
if (kf.revertToBase) return t("labRevertToBase");
if (col === "model") {
// The header's C/L/V toggle (_modelViewMode) picks which single field this cell
// summarizes. Real keyframes (atIteration set) resolve the field's inherited/
// effective value via _resolveModelKeyframe — e.g. a keyframe that only touches
// LoRA still shows whatever Checkpoint is actually in effect when viewing "C",
// not a blank. The synthetic live-preview object built for keyframe #1 (no
// atIteration) already IS that resolved value, so it's used as-is.
const resolved = (kf.atIteration != null) ? _resolveModelKeyframe(kf.atIteration) : _normalizeModelValue(kf.value);
let display = "";
if (_modelViewMode === "checkpoint") display = resolved.checkpoint;
else if (_modelViewMode === "vae") display = resolved.vae;
else if (_modelViewMode === "lora" && resolved.lora.name) display = `${resolved.lora.name} (${resolved.lora.strengthModel}/${resolved.lora.strengthClip})`;
return display ? escapeHtml(display) : t("labUseWorkflowDefault");
if (col === "checkpoint" || col === "vae") {
return kf.value ? escapeHtml(kf.value) : t("labUseWorkflowDefault");
}
if (col === "lora") {
const v = kf.value;
if (!v?.name) return t("labUseWorkflowDefault");
if (v.nodeBypass) return escapeHtml(`${v.name} ${t("labLoraNodeBypassed")}`);
const promptTag = v.applyToPrompt ? ` ${t("labLoraPromptAppliedTag")}` : "";
return escapeHtml(`${v.name} (${v.strengthModel}/${v.strengthClip})${promptTag}`);
}
if (col === "prompt") {
const p = kf.value?.positive || "";
@@ -471,10 +536,8 @@ function _cellLabel(col, kf) {
}
function _isEmptyValue(col, value) {
if (col === "model") {
const v = _normalizeModelValue(value);
return !v.checkpoint && !v.lora.name && !v.vae;
}
if (col === "checkpoint" || col === "vae") return !value;
if (col === "lora") return !value?.name;
if (col === "prompt") return !value || (!value.positive && !value.negative);
if (col === "ksampler") {
return !value || (value.steps == null && value.cfg == null && value.denoise == null &&
@@ -492,14 +555,11 @@ function _isEmptyValue(col, value) {
function _liveValueFor(col) {
const analysis = comfyUI.currentAnalysis;
if (!analysis) return null;
if (col === "model") {
const ckpt = analysis.checkpoint_nodes?.[0]?.ckpt_name || "";
const vae = analysis.vae_nodes?.[0]?.vae_name || "";
// LoRA is intentionally left out of the live-reflected value — there's no safe
// way to pick "the" active LoRA back out of a workflow node to prefill with.
if (!ckpt && !vae) return null;
return { checkpoint: ckpt, lora: { name: "", strengthModel: 1.0, strengthClip: 1.0 }, vae };
}
if (col === "checkpoint") return analysis.checkpoint_nodes?.[0]?.ckpt_name || null;
if (col === "vae") return analysis.vae_nodes?.[0]?.vae_name || null;
// LoRA is intentionally never live-reflected — there's no safe way to pick "the"
// active LoRA back out of a workflow node to prefill with.
if (col === "lora") return null;
if (col === "prompt") {
const posNode = analysis.prompt_nodes?.find((n) => n.role === "positive");
const negNode = analysis.prompt_nodes?.find((n) => n.role === "negative");
@@ -544,53 +604,56 @@ function _openCellModal(col, idx) {
const effectiveValue = live ? _liveValueFor(col) : kf.value;
const hasPrevious = col === "prompt" && idx > 0;
// What this row would inherit if left blank (the latest earlier keyframe in this
// SAME field's own array, not touching the other two) — shown as the blank option's
// label so it's clear leaving it untouched still carries a value forward, rather
// than resetting to the workflow's own default. Only meaningful past #1.
const inherited = isFirst ? null : _resolveKeyframe(col, kf.atIteration - 1);
let valueFieldsHtml = "";
if (col === "model") {
// Shows ONLY the field matching the column header's C/L/V toggle (_modelViewMode)
// — no in-modal tab switcher — so what's open in the modal always matches what the
// cell/header are currently showing. To edit a different field, switch the header
// toggle first, then reopen the cell; Save only touches the field being shown here
// (see the save handler below), leaving the other two exactly as already stored.
const v = _normalizeModelValue(effectiveValue);
// What this row would inherit for the shown field if left blank (everything up to,
// but not including, this keyframe's own atIteration) — shown as the blank option's
// label so it's clear leaving it untouched still carries a value forward, rather
// than resetting to the workflow's own default. Only meaningful past #1.
const inherited = isFirst ? null : _resolveModelKeyframe(kf.atIteration - 1);
const selectFieldHtml = (idPrefix, options, currentVal, inheritedVal) => `
<div class="wfm-search-wrap" style="width:100%;">
<input type="text" id="wfm-lab-modal-${idPrefix}-filter" class="wfm-input wfm-search-input" placeholder="Filter...">
<button type="button" class="wfm-search-clear-btn" id="wfm-lab-modal-${idPrefix}-filter-clear" title="Clear search">✕</button>
</div>
<select id="wfm-lab-modal-${idPrefix}-select" class="wfm-select">
<option value="">${escapeHtml(inheritedVal ? t("labInheritsFrom", inheritedVal) : t("labUseWorkflowDefault"))}</option>
${options.map((m) => `<option value="${escapeHtml(m)}" ${m === currentVal ? "selected" : ""}>${escapeHtml(m)}</option>`).join("")}
</select>`;
if (_modelViewMode === "checkpoint") {
valueFieldsHtml = `
<div class="wfm-lab-modal-row">
<label>Checkpoint</label>
${selectFieldHtml("ckpt", comfyEditor.models.checkpoints || [], v.checkpoint, inherited?.checkpoint)}
</div>`;
} else if (_modelViewMode === "lora") {
valueFieldsHtml = `
<div class="wfm-lab-modal-row">
<label>LoRA</label>
${selectFieldHtml("lora", comfyEditor.models.loras || [], v.lora.name, inherited?.lora?.name)}
<div class="wfm-lora-strength-single" style="margin-top:6px;">
<span>M</span>
<input type="number" class="wfm-input" id="wfm-lab-modal-lora-strmodel" value="${v.lora.strengthModel}" step="0.05" min="0" max="2">
<span>C</span>
<input type="number" class="wfm-input" id="wfm-lab-modal-lora-strclip" value="${v.lora.strengthClip}" step="0.05" min="0" max="2">
</div>
</div>`;
} else if (_modelViewMode === "vae") {
valueFieldsHtml = `
<div class="wfm-lab-modal-row">
<label>VAE</label>
${selectFieldHtml("vae", comfyEditor.models.vaes || [], v.vae, inherited?.vae)}
</div>`;
}
if (col === "checkpoint" || col === "vae") {
const options = comfyEditor.models[col === "checkpoint" ? "checkpoints" : "vaes"] || [];
valueFieldsHtml = `
<div class="wfm-lab-modal-row">
<label>${col === "checkpoint" ? "Checkpoint" : "VAE"}</label>
<div class="wfm-search-wrap" style="margin-bottom:4px;width:100%;">
<input type="text" id="wfm-lab-modal-value-filter" class="wfm-input wfm-search-input" placeholder="Filter...">
<button type="button" class="wfm-search-clear-btn" id="wfm-lab-modal-value-filter-clear" title="Clear search">✕</button>
</div>
<select id="wfm-lab-modal-value" class="wfm-select">
<option value="">${escapeHtml(inherited ? t("labInheritsFrom", inherited) : t("labUseWorkflowDefault"))}</option>
${options.map((m) => `<option value="${escapeHtml(m)}" ${m === effectiveValue ? "selected" : ""}>${escapeHtml(m)}</option>`).join("")}
</select>
</div>`;
} else if (col === "lora") {
const v = _normalizeLoraValue(effectiveValue);
const options = comfyEditor.models.loras || [];
valueFieldsHtml = `
<div class="wfm-lab-modal-row">
<label>LoRA</label>
<div class="wfm-search-wrap" style="width:100%;">
<input type="text" id="wfm-lab-modal-lora-filter" class="wfm-input wfm-search-input" placeholder="Filter...">
<button type="button" class="wfm-search-clear-btn" id="wfm-lab-modal-lora-filter-clear" title="Clear search">✕</button>
</div>
<select id="wfm-lab-modal-lora-select" class="wfm-select">
<option value="">${escapeHtml(inherited?.name ? t("labInheritsFrom", inherited.name) : t("labUseWorkflowDefault"))}</option>
${options.map((m) => `<option value="${escapeHtml(m)}" ${m === v.name ? "selected" : ""}>${escapeHtml(m)}</option>`).join("")}
</select>
<div class="wfm-lora-strength-single" style="margin-top:6px;">
<span>M</span>
<input type="number" class="wfm-input" id="wfm-lab-modal-lora-strmodel" value="${v.strengthModel}" step="0.05" min="0" max="2" ${v.nodeBypass ? "disabled" : ""}>
<span>C</span>
<input type="number" class="wfm-input" id="wfm-lab-modal-lora-strclip" value="${v.strengthClip}" step="0.05" min="0" max="2" ${v.nodeBypass ? "disabled" : ""}>
</div>
<div class="wfm-lab-modal-checkbox" style="margin-top:6px;">
<input type="checkbox" id="wfm-lab-modal-lora-nodebypass" ${v.nodeBypass ? "checked" : ""}>
<label for="wfm-lab-modal-lora-nodebypass">${t("labLoraNodeBypassCheckboxLabel")}</label>
</div>
<div class="wfm-lab-modal-checkbox">
<input type="checkbox" id="wfm-lab-modal-lora-prompttoggle" ${v.applyToPrompt ? "checked" : ""} ${v.nodeBypass ? "disabled" : ""}>
<label for="wfm-lab-modal-lora-prompttoggle">${t("labLoraApplyToPromptCheckboxLabel")}</label>
</div>
</div>`;
} else if (col === "prompt") {
const v = effectiveValue || {};
valueFieldsHtml = `
@@ -683,7 +746,8 @@ function _openCellModal(col, idx) {
</div>`;
openModal(`${col} #${idx + 1}`, html);
if (col === "prompt") _wireLabPromptModalExtras(idx);
if (col === "model") _wireLabModelFieldFilter();
if (col === "checkpoint" || col === "vae") _wireLabValueFilter(col);
if (col === "lora") _wireLabLoraFilter();
// Bypass disables everything else on the modal (value fields AND the Revert
// checkbox) — the two are mutually exclusive since bypass already means "ignore
@@ -692,6 +756,14 @@ function _openCellModal(col, idx) {
const bypassCb = document.getElementById("wfm-lab-modal-bypass");
const fieldsWrap = document.getElementById("wfm-lab-modal-fields");
const revertRowEl = revertCb?.closest(".wfm-lab-modal-checkbox");
// LoRA-only: the node-bypass checkbox (force strength to 0 at apply-time — see
// _applyLabLoraToWorkflow) disables just the strength fields, independent of the
// row-level bypass/revert above (which disable everything in fieldsWrap instead).
const loraNodeBypassCb = document.getElementById("wfm-lab-modal-lora-nodebypass");
const loraStrModelEl = document.getElementById("wfm-lab-modal-lora-strmodel");
const loraStrClipEl = document.getElementById("wfm-lab-modal-lora-strclip");
// Bypassing the LoRA node makes "apply its syntax/triggers to the prompt" moot too.
const loraPromptCb = document.getElementById("wfm-lab-modal-lora-prompttoggle");
const syncDisabled = () => {
const bypassed = !!bypassCb?.checked;
const fieldsDisabled = bypassed || !!revertCb?.checked;
@@ -699,14 +771,20 @@ function _openCellModal(col, idx) {
fieldsWrap?.querySelectorAll("input,select,textarea").forEach((el) => { el.disabled = fieldsDisabled; });
if (revertCb) revertCb.disabled = bypassed;
if (revertRowEl) revertRowEl.style.opacity = bypassed ? "0.4" : "1";
if (!fieldsDisabled && loraNodeBypassCb?.checked) {
if (loraStrModelEl) loraStrModelEl.disabled = true;
if (loraStrClipEl) loraStrClipEl.disabled = true;
if (loraPromptCb) loraPromptCb.disabled = true;
}
};
revertCb?.addEventListener("change", syncDisabled);
bypassCb?.addEventListener("change", syncDisabled);
loraNodeBypassCb?.addEventListener("change", syncDisabled);
syncDisabled();
document.getElementById("wfm-lab-modal-delete")?.addEventListener("click", () => {
_lab.columns[col].splice(idx, 1);
_renderColumn(col);
_renderColumnForKey(col);
closeModal();
});
@@ -721,23 +799,17 @@ function _openCellModal(col, idx) {
let value = kf.value;
if (!revert && !bypassed) {
if (col === "model") {
// Only the field the modal actually shows (_modelViewMode) gets updated —
// the other two are carried over from this row's own currently-saved
// value untouched, since their fields aren't in the DOM to read from.
const next = _normalizeModelValue(kf.value);
if (_modelViewMode === "checkpoint") {
next.checkpoint = document.getElementById("wfm-lab-modal-ckpt-select")?.value || "";
} else if (_modelViewMode === "lora") {
next.lora = {
name: document.getElementById("wfm-lab-modal-lora-select")?.value || "",
strengthModel: parseFloat(document.getElementById("wfm-lab-modal-lora-strmodel")?.value) || 1.0,
strengthClip: parseFloat(document.getElementById("wfm-lab-modal-lora-strclip")?.value) || 1.0,
};
} else if (_modelViewMode === "vae") {
next.vae = document.getElementById("wfm-lab-modal-vae-select")?.value || "";
}
value = next;
if (col === "checkpoint" || col === "vae") {
value = document.getElementById("wfm-lab-modal-value")?.value || "";
} else if (col === "lora") {
const loraNodeBypassed = !!document.getElementById("wfm-lab-modal-lora-nodebypass")?.checked;
value = {
name: document.getElementById("wfm-lab-modal-lora-select")?.value || "",
strengthModel: parseFloat(document.getElementById("wfm-lab-modal-lora-strmodel")?.value) || 1.0,
strengthClip: parseFloat(document.getElementById("wfm-lab-modal-lora-strclip")?.value) || 1.0,
nodeBypass: loraNodeBypassed,
applyToPrompt: !loraNodeBypassed && !!document.getElementById("wfm-lab-modal-lora-prompttoggle")?.checked,
};
} else if (col === "prompt") {
value = {
positive: document.getElementById("wfm-lab-modal-positive")?.value || "",
@@ -763,7 +835,7 @@ function _openCellModal(col, idx) {
_lab.columns[col][idx] = { atIteration, value, revertToBase: revert, bypassed };
_lab.columns[col].sort((a, b) => a.atIteration - b.atIteration);
_renderColumn(col);
_renderColumnForKey(col);
closeModal();
});
}
@@ -784,28 +856,46 @@ function _applyStyleToText(positive, negative, style) {
};
}
// Wires the filter box above whichever single field the Model-column modal is
// currently showing (_modelViewMode picks which one exists in the DOM — the other two
// calls below are harmless no-ops since their elements aren't present). Same filter
// behavior as GenerateUI's Model tab filter inputs (comfyui-editor.js renderModelTab).
function _wireLabModelFieldFilter() {
const wireFilter = (idPrefix, modelsKey) => {
const filterInput = document.getElementById(`wfm-lab-modal-${idPrefix}-filter`);
const select = document.getElementById(`wfm-lab-modal-${idPrefix}-select`);
if (!filterInput || !select) return;
const options = comfyEditor.models[modelsKey] || [];
const applyFilter = () => {
const filter = filterInput.value.toLowerCase();
select.innerHTML = `<option value="">${t("labUseWorkflowDefault")}</option>` +
options.filter((m) => m.toLowerCase().includes(filter))
.map((m) => `<option value="${escapeHtml(m)}">${escapeHtml(m)}</option>`).join("");
};
filterInput.addEventListener("input", applyFilter);
setupSearchClearBtn(filterInput.id, `${filterInput.id}-clear`, applyFilter);
// Filter box above the Checkpoint/VAE modal's <select>, same behavior as the
// Model tab's filter inputs (comfyui-editor.js renderModelTab): rebuild the
// option list on every keystroke, always keeping the "workflow default" option.
// The overlay ✕ clear button reuses the same setupSearchClearBtn() helper as the
// Models/Nodes/Workflow/Gallery tab search boxes and the Raw JSON search bar.
function _wireLabValueFilter(col) {
const filterInput = document.getElementById("wfm-lab-modal-value-filter");
const select = document.getElementById("wfm-lab-modal-value");
if (!filterInput || !select) return;
const options = comfyEditor.models[col === "checkpoint" ? "checkpoints" : "vaes"] || [];
const applyFilter = () => {
const filter = filterInput.value.toLowerCase();
select.innerHTML = `<option value="">${t("labUseWorkflowDefault")}</option>` +
options.filter((m) => m.toLowerCase().includes(filter))
.map((m) => `<option value="${escapeHtml(m)}">${escapeHtml(m)}</option>`).join("");
};
wireFilter("ckpt", "checkpoints");
wireFilter("lora", "loras");
wireFilter("vae", "vaes");
filterInput.addEventListener("input", applyFilter);
setupSearchClearBtn("wfm-lab-modal-value-filter", "wfm-lab-modal-value-filter-clear", applyFilter);
}
// Same filter behavior as _wireLabValueFilter, for the LoRA modal's own select (it
// has separate element ids since a LoRA keyframe row also carries Strength M/C
// inputs alongside it, unlike the plain single-select Checkpoint/VAE modal).
function _wireLabLoraFilter() {
const filterInput = document.getElementById("wfm-lab-modal-lora-filter");
const select = document.getElementById("wfm-lab-modal-lora-select");
if (!filterInput || !select) return;
const options = comfyEditor.models.loras || [];
const applyFilter = () => {
const filter = filterInput.value.toLowerCase();
select.innerHTML = `<option value="">${t("labUseWorkflowDefault")}</option>` +
options.filter((m) => m.toLowerCase().includes(filter))
.map((m) => `<option value="${escapeHtml(m)}">${escapeHtml(m)}</option>`).join("");
};
filterInput.addEventListener("input", applyFilter);
setupSearchClearBtn("wfm-lab-modal-lora-filter", "wfm-lab-modal-lora-filter-clear", applyFilter);
}
// Wires the Prompt-column modal's extra actions: fetch the live GenerateUI prompt,
@@ -941,77 +1031,65 @@ function _resolveKeyframe(col, iteration) {
return _isEmptyValue(col, applicable.value) ? null : applicable.value;
}
// Unlike _resolveKeyframe (which swaps in one keyframe's whole value), the Model column
// bundles three independent settings — Checkpoint/LoRA/VAE — into a single row, so a
// keyframe that only sets a LoRA must NOT blank out a Checkpoint/VAE chosen by an earlier
// keyframe. This resolves each of the three fields separately: walking keyframes up to
// `iteration` in order, each field keeps whatever value it last saw set (non-empty),
// carrying forward across keyframes that leave it blank. A `revertToBase` keyframe resets
// all three fields at once to iteration #1's row (mirrors _resolveKeyframe's revert
// semantics) rather than being treated as a per-field value of its own. Bypassed keyframes
// are skipped entirely, as elsewhere.
function _resolveModelKeyframe(iteration) {
const kfs = _lab.columns.model;
const row1 = kfs.find((k) => k.atIteration === 1);
const row1Value = (row1 && !row1.bypassed) ? _normalizeModelValue(row1.value) : null;
let current = _defaultValueFor("model");
for (const kf of kfs) {
if (kf.atIteration > iteration) break;
if (kf.bypassed) continue;
if (kf.revertToBase) {
current = row1Value ? _normalizeModelValue(row1Value) : _defaultValueFor("model");
continue;
}
const v = _normalizeModelValue(kf.value);
if (v.checkpoint) current.checkpoint = v.checkpoint;
if (v.lora.name) current.lora = { ...v.lora };
if (v.vae) current.vae = v.vae;
}
return current;
}
function _buildWorkflowForIteration(iteration, chainImageRef) {
function _buildWorkflowForIteration(iteration, chainImageRef, prevLoraInjection) {
const wf = JSON.parse(JSON.stringify(comfyUI.currentWorkflow || {}));
const analysis = comfyUI.currentAnalysis;
if (!analysis) return { workflow: wf, seed: null };
if (!analysis) return { workflow: wf, seed: null, loraInjection: null };
const write = (nodeId, key, val) => {
if (nodeId != null && wf[nodeId]) wf[nodeId].inputs[key] = val;
};
const model = _resolveModelKeyframe(iteration);
if (model.checkpoint && analysis.checkpoint_nodes?.[0]) {
write(analysis.checkpoint_nodes[0].id, "ckpt_name", model.checkpoint);
const ckpt = _resolveKeyframe("checkpoint", iteration);
if (ckpt && analysis.checkpoint_nodes?.[0]) {
write(analysis.checkpoint_nodes[0].id, "ckpt_name", ckpt);
}
if (model.vae) {
const vae = _resolveKeyframe("vae", iteration);
if (vae) {
const vaeNode = analysis.vae_nodes?.[0];
if (vaeNode && wf[vaeNode.id]) {
write(vaeNode.id, "vae_name", model.vae);
write(vaeNode.id, "vae_name", vae);
} else {
// Workflow has no standalone VAELoader node (VAE baked into the checkpoint) —
// inject one and repoint every "vae" input link at it so the override actually applies.
const newId = String(Math.max(0, ...Object.keys(wf).map(Number)) + 1);
wf[newId] = { class_type: "VAELoader", inputs: { vae_name: model.vae }, _meta: { title: "VAE Loader" } };
wf[newId] = { class_type: "VAELoader", inputs: { vae_name: vae }, _meta: { title: "VAE Loader" } };
for (const node of Object.values(wf)) {
if (Array.isArray(node?.inputs?.vae)) node.inputs.vae = [newId, 0];
}
}
}
if (model.lora.name) {
_applyLabLoraToWorkflow(wf, analysis, model.lora);
const lora = _resolveKeyframe("lora", iteration);
if (lora?.name) {
_applyLabLoraToWorkflow(wf, analysis, lora);
}
const prompt = _resolveKeyframe("prompt", iteration);
const posNode = analysis.prompt_nodes?.find((n) => n.role === "positive");
const negNode = analysis.prompt_nodes?.find((n) => n.role === "negative");
if (prompt) {
const posNode = analysis.prompt_nodes?.find((n) => n.role === "positive");
const negNode = analysis.prompt_nodes?.find((n) => n.role === "negative");
if (posNode && prompt.positive) write(posNode.id, posNode.textKey || "text", prompt.positive);
if (negNode && prompt.negative) write(negNode.id, negNode.textKey || "text", prompt.negative);
}
// LoRA syntax/trigger-word injection into the Positive prompt (opt-in per keyframe
// via lora.applyToPrompt). Always strip whatever the previous iteration injected
// first, whether or not this iteration re-injects — so switching to a different
// LoRA (or a keyframe with the option off) starts from a clean prompt instead of
// accumulating every LoRA the batch has used so far. See _stripLoraPromptInjection.
let loraInjection = null;
if (posNode && wf[posNode.id]) {
const textKey = posNode.textKey || "text";
let posText = _stripLoraPromptInjection(wf[posNode.id].inputs[textKey] || "", prevLoraInjection);
if (lora?.name && lora.applyToPrompt && !lora.nodeBypass) {
loraInjection = _buildLoraPromptInjection(lora);
posText = posText ? `${posText}, ${loraInjection}` : loraInjection;
}
write(posNode.id, textKey, posText);
}
let seed = null;
const ks = _resolveKeyframe("ksampler", iteration);
const sampler = analysis.sampler_nodes?.[0];
@@ -1029,7 +1107,7 @@ function _buildWorkflowForIteration(iteration, chainImageRef) {
write(analysis.load_image_nodes[0].id, "image", imageRef);
}
return { workflow: wf, seed };
return { workflow: wf, seed, loraInjection };
}
// True once a "no LoRA node in this workflow" warning has already been shown during the
@@ -1037,6 +1115,58 @@ function _buildWorkflowForIteration(iteration, chainImageRef) {
// run instead of once per iteration.
let _labLoraWarned = false;
// metadata.json (sha256 lookup) + CivitAI cache (sha256 -> trainedWords), fetched once
// per Run (see _runLabBatch) and used only to resolve trigger words for the Positive-
// prompt LoRA injection below — same two endpoints comfyui-editor.js's renderLoraPane
// uses for the GenerateUI Model tab's own LORA SYNTAX/TRIGGER WORDS display.
let _labModelMetadata = {};
let _labCivitaiCache = {};
async function _fetchLabLoraMetadataCache() {
try {
const [metaRes, civRes] = await Promise.all([
fetch("/api/wfm/models/metadata"),
fetch("/api/wfm/models/civitai/cache"),
]);
_labModelMetadata = metaRes.ok ? await metaRes.json() : {};
_labCivitaiCache = civRes.ok ? await civRes.json() : {};
} catch {
_labModelMetadata = {};
_labCivitaiCache = {};
}
}
function _labTriggerWordsFor(loraName) {
const sha = (_labModelMetadata[loraName] || {}).sha256;
const civInfo = sha && _labCivitaiCache[sha];
return civInfo?.trainedWords || [];
}
// Builds the exact "<lora:stem:m:c>[, trigger, words...]" string appended to the
// Positive prompt for this LoRA keyframe — the same format comfyui-editor.js's Single
// Apply writes. Returned as-is so _buildWorkflowForIteration can hand it back unchanged
// to the next iteration for removal (see _stripLoraPromptInjection).
function _buildLoraPromptInjection(lora) {
const stem = lora.name.replace(/\\/g, "/").split("/").pop().replace(/\.[^.]+$/, "");
const strModel = lora.strengthModel ?? 1.0;
const strClip = lora.strengthClip ?? 1.0;
const syntax = `<lora:${stem}:${strModel}:${strClip}>`;
const triggers = _labTriggerWordsFor(lora.name);
return triggers.length > 0 ? `${syntax}, ${triggers.join(", ")}` : syntax;
}
// Removes an exact previously-injected string (as returned by _buildLoraPromptInjection)
// from a prompt, plus the comma that joined it to whatever came before — so switching
// keyframes/LoRAs mid-batch never accumulates every LoRA the batch has used so far.
// A plain string search (no regex) is safe here since `injection` is always something
// _buildLoraPromptInjection generated, never arbitrary user text.
function _stripLoraPromptInjection(text, injection) {
if (!injection || !text) return text || "";
let out = text.split(`, ${injection}`).join("");
out = out.split(injection).join("");
return out.replace(/,\s*,/g, ",").replace(/^\s*,\s*/, "").replace(/\s*,\s*$/, "").trim();
}
// Applies a Lab-configured LoRA (Single only — no Stack support here) to the given
// per-iteration workflow clone. Mirrors models-tab.js's applyToGenUI LoRA branch: prefer
// analysis.lora_nodes[0] (correctly picks up "Lora Loader (LoraManager)" nodes via
@@ -1060,14 +1190,20 @@ function _applyLabLoraToWorkflow(wf, analysis, lora) {
return;
}
const strModel = lora.strengthModel ?? 1.0;
const strClip = lora.strengthClip ?? 1.0;
// nodeBypass: user-requested "bypass the LoRA node" toggle (Lab keyframe-level,
// independent of the row's own bypassed flag above). There's no real node to set
// mode:4 on here — currentWorkflow is already-converted API format (see
// comfyui-workflow.js convertUiToApi) where mode has no effect — so this forces
// strength to 0 instead, which is behaviorally equivalent to bypassing the node.
const bypass = !!lora.nodeBypass;
const strModel = bypass ? 0 : (lora.strengthModel ?? 1.0);
const strClip = bypass ? 0 : (lora.strengthClip ?? 1.0);
if (isLoraManager) {
const stem = lora.name.replace(/\\/g, "/").split("/").pop().replace(/\.[^.]+$/, "");
wf[nodeId].inputs.loras = {
__value__: [{ name: stem, strength: strModel, active: true, expanded: false, clipStrength: strClip, locked: false }],
__value__: [{ name: stem, strength: strModel, active: !bypass, expanded: false, clipStrength: strClip, locked: false }],
};
wf[nodeId].inputs.text = `<lora:${stem}:${strModel}:${strClip}>`;
wf[nodeId].inputs.text = bypass ? "" : `<lora:${stem}:${strModel}:${strClip}>`;
} else {
wf[nodeId].inputs.lora_name = comfyEditor.resolveLoraName(lora.name);
wf[nodeId].inputs.strength_model = strModel;
@@ -1120,12 +1256,14 @@ async function _runLabBatch() {
_lab.results.images = [];
_renderResultsGrid();
_setRunUiState(true);
await _fetchLabLoraMetadataCache();
const progressBar = document.getElementById("wfm-lab-progress-bar");
const progressText = document.getElementById("wfm-lab-progress-text");
const eagleAutoSave = getEagleSettings().autoSave;
let completed = 0, failed = 0;
let prevLoraInjection = null;
try {
for (let i = 1; i <= _lab.batchCount; i++) {
if (_aborted) break;
@@ -1134,7 +1272,8 @@ async function _runLabBatch() {
if (progressText) progressText.textContent = `[${i}/${_lab.batchCount}] ...`;
const chainRef = (!_lab.t2iMode && _lab.chainImage && i > 1) ? _lastGeneratedImageRef : null;
const { workflow, seed } = _buildWorkflowForIteration(i, chainRef);
const { workflow, seed, loraInjection } = _buildWorkflowForIteration(i, chainRef, prevLoraInjection);
prevLoraInjection = loraInjection;
const workflowExpanded = await _expandWildcardsInWorkflow(workflow);
try {
+5 -3
View File
@@ -203,7 +203,7 @@ function resolveEditPlusText(mode, textEdit, text1, text2) {
}
}
function extractPrompts(wf) {
export function extractPrompts(wf) {
if (!wf || typeof wf !== "object") return { positives: [], negatives: [] };
return Array.isArray(wf.nodes) ? extractPromptsLiteGraph(wf) : extractPromptsAPI(wf);
}
@@ -693,7 +693,7 @@ function buildLoRAItem(lora) {
return el;
}
function buildPromptItem(label, type, full, fullArea, fullLabel, listEl) {
export function buildPromptItem(label, type, full, fullArea, fullLabel, listEl) {
const el = document.createElement("div");
el.className = "wfm-meta-item wfm-meta-item-clickable";
const snippet = label.length > 60 ? label.slice(0, 60) + "…" : label;
@@ -723,7 +723,8 @@ function renderSection(sectionEl, listEl, items, buildFn) {
let _externalHandleFile = null;
export async function loadFileIntoMetadataTab(file) {
document.querySelector('.wfm-tab[data-tab="metadata"]')?.click();
document.querySelector('.wfm-tab[data-tab="gallery"]')?.click();
document.querySelector('.wfm-gallery-subtab-btn[data-gallery-subtab="metadata"]')?.click();
await new Promise(r => setTimeout(r, 0));
if (_externalHandleFile) await _externalHandleFile(file);
}
@@ -790,6 +791,7 @@ export function initMetadataTab() {
"wfm-help-metadata-3": "helpMetadata3",
"wfm-help-metadata-4": "helpMetadata4",
"wfm-help-metadata-5": "helpMetadata5",
"wfm-help-metadata-6": "helpMetadata6",
};
for (const [id, key] of Object.entries(helpIds)) {
const el = document.getElementById(id);
+394
View File
@@ -0,0 +1,394 @@
/**
* Prompt Tab - AI Assistant (Ollama / LM Studio chat, translate, apply to GenerateUI)
*/
import { showToast } from "./app.js";
import { t } from "./i18n.js";
import { getAiBackendDefaultUrl } from "./util.js";
// ============================================
// State
// ============================================
const ollamaState = {
chatHistory: [],
attachedImage: null,
};
const PROMPT_AI_KEY = "wfm_prompt_ai_settings";
function loadPromptAiSettings() {
try { return JSON.parse(localStorage.getItem(PROMPT_AI_KEY) || "{}"); } catch { return {}; }
}
// バックエンド変更検知用: モデル一覧を取得した時点のバックエンド名を記録
let _promptAiBackendForModels = null;
export function getPromptAiConfig() {
const s = loadPromptAiSettings();
const backend = s.backend || "ollama";
const url = (s.backendUrl || getAiBackendDefaultUrl(backend)).replace(/\/$/, "");
return { backend, url };
}
export async function fetchAiModels() {
const { backend, url } = getPromptAiConfig();
if (backend === "ollama") {
const res = await fetch(`${url}/api/tags`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return (await res.json()).models?.map(m => m.name) || [];
} else {
const res = await fetch(`${url}/v1/models`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return ((await res.json()).data || []).map(m => m.id);
}
}
async function chatWithAi(model, messages) {
const { backend, url } = getPromptAiConfig();
if (backend === "ollama") {
const res = await fetch(`${url}/api/chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model, messages, stream: false }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return (await res.json()).message?.content || "";
} else {
// LM Studio / Lemonade (OpenAI-compatible): convert Ollama-style messages (with .images) to OpenAI content arrays
const openAiMessages = messages.map(msg => {
if (msg.images?.length) {
return {
role: msg.role,
content: [
{ type: "text", text: msg.content },
...msg.images.map(b64 => ({ type: "image_url", image_url: { url: `data:image/jpeg;base64,${b64}` } })),
],
};
}
return { role: msg.role, content: msg.content };
});
const res = await fetch(`${url}/v1/chat/completions`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model, messages: openAiMessages, stream: false }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return (await res.json()).choices?.[0]?.message?.content || "";
}
}
// ============================================
// Chat UI
// ============================================
function addChatMessage(role, content) {
const chat = document.getElementById("wfm-ollama-chat");
if (!chat) return;
const welcome = chat.querySelector(".wfm-ollama-welcome");
if (welcome) welcome.remove();
const msg = document.createElement("div");
msg.className = `wfm-ollama-msg ${role}`;
const roleLabel = document.createElement("div");
roleLabel.className = "wfm-ollama-msg-role";
roleLabel.textContent = role === "user" ? t("you") : "AI";
msg.appendChild(roleLabel);
const contentDiv = document.createElement("div");
contentDiv.className = "wfm-ollama-msg-content";
contentDiv.textContent = content;
msg.appendChild(contentDiv);
// Apply button for assistant messages
if (role === "assistant") {
const applyRow = document.createElement("div");
applyRow.className = "wfm-ollama-apply-row";
const applyBtn = document.createElement("button");
applyBtn.className = "wfm-btn wfm-btn-sm wfm-btn-primary";
applyBtn.textContent = t("applyToGenerateUI");
applyBtn.addEventListener("click", () => applyToGenerateUI(content));
applyRow.appendChild(applyBtn);
const copyBtn = document.createElement("button");
copyBtn.className = "wfm-btn wfm-btn-sm";
copyBtn.textContent = t("copy");
copyBtn.addEventListener("click", () => {
navigator.clipboard.writeText(content).then(() => {
copyBtn.textContent = t("copied");
setTimeout(() => { copyBtn.textContent = t("copy"); }, 1500);
});
});
applyRow.appendChild(copyBtn);
msg.appendChild(applyRow);
}
chat.appendChild(msg);
chat.scrollTop = chat.scrollHeight;
}
function clearChat() {
ollamaState.chatHistory = [];
ollamaState.attachedImage = null;
updateAttachmentDisplay();
const chat = document.getElementById("wfm-ollama-chat");
if (chat) {
chat.innerHTML = `
<div class="wfm-ollama-welcome">
<p>AI Assistant</p>
<p>${t("assistantWelcome")}</p>
</div>
`;
}
}
// ============================================
// Image Attachment
// ============================================
function attachFile(file) {
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
ollamaState.attachedImage = { name: file.name, dataUrl: e.target.result };
updateAttachmentDisplay();
};
reader.readAsDataURL(file);
}
function updateAttachmentDisplay() {
const container = document.getElementById("wfm-ollama-attachments");
if (!container) return;
container.innerHTML = "";
if (!ollamaState.attachedImage) return;
const item = document.createElement("div");
item.className = "wfm-ollama-attachment";
const img = document.createElement("img");
img.src = ollamaState.attachedImage.dataUrl;
item.appendChild(img);
const name = document.createElement("span");
name.className = "wfm-ollama-attachment-name";
name.textContent = ollamaState.attachedImage.name;
item.appendChild(name);
const removeBtn = document.createElement("button");
removeBtn.className = "wfm-ollama-attachment-remove";
removeBtn.textContent = "×";
removeBtn.addEventListener("click", () => {
ollamaState.attachedImage = null;
updateAttachmentDisplay();
});
item.appendChild(removeBtn);
container.appendChild(item);
}
// ============================================
// Send Message
// ============================================
async function sendMessage() {
const input = document.getElementById("wfm-ollama-input");
const modelSelect = document.getElementById("wfm-ollama-model");
if (!input || !modelSelect) return;
const message = input.value.trim();
if (!message) return;
// バックエンドが変更されていたらモデル一覧を自動更新
const { backend } = getPromptAiConfig();
if (backend !== _promptAiBackendForModels) {
const status = document.getElementById("wfm-ollama-status");
if (status) { status.textContent = `Switching to ${backend}...`; status.className = "wfm-ollama-status"; }
try {
const models = await fetchAiModels();
const savedModel = loadPromptAiSettings().model || "";
modelSelect.innerHTML = models.length
? models.map(name => `<option value="${name}" ${name === savedModel ? "selected" : ""}>${name}</option>`).join("")
: `<option value="">${t("noModelsFound")}</option>`;
_promptAiBackendForModels = backend;
if (status) { status.textContent = `${models.length} models [${backend}]`; status.className = "wfm-ollama-status connected"; }
} catch {
if (status) { status.textContent = t("failedConnect"); status.className = "wfm-ollama-status error"; }
}
}
const model = modelSelect.value;
if (!model) {
showToast(t("selectModelFirst"), "error");
return;
}
addChatMessage("user", message);
input.value = "";
const userMsg = { role: "user", content: message };
if (ollamaState.attachedImage) {
userMsg.images = [ollamaState.attachedImage.dataUrl.split(",")[1]];
}
ollamaState.chatHistory.push(userMsg);
const sendBtn = document.getElementById("wfm-ollama-send-btn");
if (sendBtn) sendBtn.disabled = true;
try {
const reply = await chatWithAi(model, ollamaState.chatHistory);
addChatMessage("assistant", reply);
ollamaState.chatHistory.push({ role: "assistant", content: reply });
ollamaState.attachedImage = null;
updateAttachmentDisplay();
} catch (err) {
showToast(t("error") + ": " + err.message, "error");
} finally {
if (sendBtn) sendBtn.disabled = false;
}
}
// ============================================
// Translation
// ============================================
async function sendTranslate(direction) {
const input = document.getElementById("wfm-ollama-input");
const modelSelect = document.getElementById("wfm-ollama-model");
if (!input || !modelSelect) return;
const text = input.value.trim();
if (!text) return;
const model = modelSelect.value;
if (!model) {
showToast(t("selectModelFirst"), "error");
return;
}
const labels = { ja2en: "JA→EN", en2ja: "EN→JA", zh2en: "ZH→EN", en2zh: "EN→ZH" };
addChatMessage("user", `[${labels[direction] || direction}]\n${text}`);
input.value = "";
const btns = ["wfm-ollama-send-btn", "wfm-ollama-ja2en-btn", "wfm-ollama-en2ja-btn", "wfm-ollama-zh2en-btn", "wfm-ollama-en2zh-btn"]
.map((id) => document.getElementById(id));
btns.forEach((b) => { if (b) b.disabled = true; });
try {
const prompts = {
ja2en: `Translate the following Japanese text into English. Output only the translated text, nothing else.\n\n${text}`,
en2ja: `以下の英語のテキストを日本語に翻訳してください。翻訳結果のみを出力し、他の説明は不要です。\n\n${text}`,
zh2en: `Translate the following Chinese text into English. Output only the translated text, nothing else.\n\n${text}`,
en2zh: `请将以下英文翻译成中文。只输出翻译结果,不需要其他说明。\n\n${text}`,
};
const prompt = prompts[direction];
const reply = await chatWithAi(model, [{ role: "user", content: prompt }]);
addChatMessage("assistant", reply);
} catch (err) {
showToast(t("error") + ": " + err.message, "error");
} finally {
btns.forEach((b) => { if (b) b.disabled = false; });
}
}
// ============================================
// Apply to GenerateUI
// ============================================
function applyToGenerateUI(text) {
const textareas = document.querySelectorAll("#wfm-gen-prompt-fields textarea");
if (textareas.length === 0) {
showToast(t("noPromptFields"), "error");
return;
}
textareas[0].value = text;
textareas[0].dispatchEvent(new Event("input", { bubbles: true }));
textareas[0].dispatchEvent(new Event("change", { bubbles: true }));
showToast(t("appliedToGenerateUI"), "success");
}
// ============================================
// UI wiring (called from prompt-tab.js's initPromptTab)
// ============================================
export function initAiChatUI() {
// AI model refresh
async function refreshModels() {
const select = document.getElementById("wfm-ollama-model");
const status = document.getElementById("wfm-ollama-status");
const { backend } = getPromptAiConfig();
try {
const models = await fetchAiModels();
const savedModel = loadPromptAiSettings().model || "";
if (select) {
select.innerHTML = models.length
? models.map(name => `<option value="${name}" ${name === savedModel ? "selected" : ""}>${name}</option>`).join("")
: `<option value="">${t("noModelsFound")}</option>`;
}
_promptAiBackendForModels = backend;
if (status) {
status.textContent = `${models.length} models [${backend}]`;
status.className = "wfm-ollama-status connected";
}
} catch {
if (status) {
status.textContent = t("failedConnect");
status.className = "wfm-ollama-status error";
}
}
}
// Auto-load models on init
refreshModels();
document.getElementById("wfm-ollama-refresh-btn")?.addEventListener("click", refreshModels);
// Test connection
document.getElementById("wfm-ollama-test-btn")?.addEventListener("click", async () => {
const status = document.getElementById("wfm-ollama-status");
if (status) { status.textContent = "Testing..."; status.className = "wfm-ollama-status"; }
try {
await fetchAiModels();
if (status) { status.textContent = t("connectedCheck"); status.className = "wfm-ollama-status connected"; }
await refreshModels();
} catch {
if (status) { status.textContent = t("failedConnect"); status.className = "wfm-ollama-status error"; }
}
});
// Send message
document.getElementById("wfm-ollama-send-btn")?.addEventListener("click", sendMessage);
// Enter to send
document.getElementById("wfm-ollama-input")?.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
// Clear chat
document.getElementById("wfm-ollama-clear-btn")?.addEventListener("click", clearChat);
// File attachment
const fileInput = document.getElementById("wfm-ollama-file-input");
document.getElementById("wfm-ollama-attach-btn")?.addEventListener("click", () => fileInput?.click());
fileInput?.addEventListener("change", () => {
if (fileInput.files.length > 0) {
attachFile(fileInput.files[0]);
fileInput.value = "";
}
});
// Translation
document.getElementById("wfm-ollama-ja2en-btn")?.addEventListener("click", () => sendTranslate("ja2en"));
document.getElementById("wfm-ollama-en2ja-btn")?.addEventListener("click", () => sendTranslate("en2ja"));
document.getElementById("wfm-ollama-zh2en-btn")?.addEventListener("click", () => sendTranslate("zh2en"));
document.getElementById("wfm-ollama-en2zh-btn")?.addEventListener("click", () => sendTranslate("en2zh"));
}
+560
View File
@@ -0,0 +1,560 @@
/**
* Prompt Tab - Presets (API CRUD, groups, Preset Manager panel)
*/
import { showToast } from "./app.js";
import { t } from "./i18n.js";
import { readJsonStorage } from "./util.js";
// ============================================
// State
// ============================================
// Preset data (loaded from API, with localStorage migration)
let promptPresets = [];
let pmActiveTab = "all"; // "all" | "favorites" | "groups"
let pmSearchText = "";
let pmSelectedId = null; // currently selected preset id
export let pmGroups = {}; // { groupName: [presetId, ...] }
const PM_GROUPS_KEY = "wfm_prompt_preset_groups";
export const PROMPT_RESERVED_GROUPS = ["Batch"];
// ============================================
// Preset API helpers
// ============================================
export async function fetchPresets() {
try {
const res = await fetch("/api/wfm/prompts");
return res.ok ? await res.json() : [];
} catch { return []; }
}
export async function apiCreatePreset(data) {
try {
const res = await fetch("/api/wfm/prompts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
const result = await res.json();
return result.status === "ok" ? result.prompt : null;
} catch { return null; }
}
export async function apiUpdatePreset(id, updates) {
try {
const res = await fetch("/api/wfm/prompts/update", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id, ...updates }),
});
const result = await res.json();
return result.status === "ok" ? result.prompt : null;
} catch { return null; }
}
export async function apiDeletePreset(id) {
try {
await fetch("/api/wfm/prompts/delete", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id }),
});
} catch { /* ignore */ }
}
// ============================================
// Migrate localStorage presets to API
// ============================================
const PRESETS_KEY = "wfm_prompt_presets";
async function migrateLocalStoragePresets() {
try {
const raw = localStorage.getItem(PRESETS_KEY);
if (!raw) return;
const local = JSON.parse(raw);
if (!Array.isArray(local) || local.length === 0) return;
// Only migrate if API has no presets yet
const existing = await fetchPresets();
if (existing.length > 0) {
localStorage.removeItem(PRESETS_KEY);
return;
}
for (const p of local) {
await apiCreatePreset({
name: p.name || "Untitled",
text: p.posText || p.text || "",
negText: p.negText || "",
category: "",
tags: [],
favorite: false,
});
}
localStorage.removeItem(PRESETS_KEY);
} catch { /* ignore migration errors */ }
}
// ============================================
// Preset data management
// ============================================
export async function loadAllPresets() {
await migrateLocalStoragePresets();
promptPresets = await fetchPresets();
// Load groups from localStorage
pmGroups = readJsonStorage(PM_GROUPS_KEY);
// Clean stale entries from groups (preserve reserved groups even if empty)
const validIds = new Set(promptPresets.map(p => p.id));
for (const g of Object.keys(pmGroups)) {
pmGroups[g] = (pmGroups[g] || []).filter(id => validIds.has(id));
if (pmGroups[g].length === 0 && !PROMPT_RESERVED_GROUPS.includes(g)) delete pmGroups[g];
}
// Ensure reserved groups always exist
for (const g of PROMPT_RESERVED_GROUPS) {
if (!pmGroups[g]) pmGroups[g] = [];
}
saveGroups();
renderPresetSelect();
renderGroupSelect();
renderPresetManager();
}
export function saveGroups() {
localStorage.setItem(PM_GROUPS_KEY, JSON.stringify(pmGroups));
renderGroupSelect();
}
function renderGroupSelect() {
const select = document.getElementById("wfm-preset-group-select");
if (!select) return;
const prevVal = select.value;
select.innerHTML = `<option value="">Select group...</option>`;
for (const g of Object.keys(pmGroups).sort()) {
const opt = document.createElement("option");
opt.value = g;
opt.textContent = g;
select.appendChild(opt);
}
if (prevVal && pmGroups[prevVal]) select.value = prevVal;
}
function renderPresetSelect() {
const select = document.getElementById("wfm-preset-select");
if (!select) return;
const prevVal = select.value;
select.innerHTML = `<option value="">${t("newPreset")}</option>`;
promptPresets.forEach((p) => {
const opt = document.createElement("option");
opt.value = p.id;
opt.textContent = p.name;
select.appendChild(opt);
});
// Restore selection if still valid
if (prevVal && promptPresets.find(p => p.id === prevVal)) {
select.value = prevVal;
}
}
function selectPresetInEditor(preset) {
const presetSelect = document.getElementById("wfm-preset-select");
const presetName = document.getElementById("wfm-preset-name");
const presetCategory = document.getElementById("wfm-preset-category");
const presetPos = document.getElementById("wfm-preset-pos");
const presetNeg = document.getElementById("wfm-preset-neg");
if (presetSelect) presetSelect.value = preset ? preset.id : "";
if (presetName) presetName.value = preset ? preset.name : "";
if (presetCategory) presetCategory.value = preset ? (preset.category || "") : "";
if (presetPos) presetPos.value = preset ? (preset.text || preset.posText || "") : "";
if (presetNeg) presetNeg.value = preset ? (preset.negText || "") : "";
pmSelectedId = preset ? preset.id : null;
renderPresetManager();
}
// ============================================
// Preset Manager rendering
// ============================================
export function esc(s) {
return s ? String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;") : "";
}
export function renderPresetManager() {
const container = document.getElementById("wfm-pm-list");
if (!container) return;
container.innerHTML = "";
switch (pmActiveTab) {
case "all": renderPmAll(container); break;
case "favorites": renderPmFavorites(container); break;
case "groups": renderPmGroups(container); break;
}
}
function matchesSearch(p) {
if (!pmSearchText) return true;
const s = pmSearchText.toLowerCase();
return (p.name || "").toLowerCase().includes(s) ||
(p.text || "").toLowerCase().includes(s) ||
(p.category || "").toLowerCase().includes(s) ||
(p.tags || []).some(t => t.toLowerCase().includes(s));
}
export function isInBatchPreset(id) {
return (pmGroups["Batch"] || []).includes(id);
}
export function toggleBatchPreset(id) {
const batch = pmGroups["Batch"] || [];
const idx = batch.indexOf(id);
if (idx >= 0) { batch.splice(idx, 1); } else { batch.push(id); }
pmGroups["Batch"] = batch;
saveGroups();
}
export function clearBatchPresets() {
pmGroups["Batch"] = [];
saveGroups();
renderPresetManager();
showToast(t("promptBatchClear"), "success");
}
function createPmItem(preset) {
const el = document.createElement("div");
el.className = "wfm-pm-item" + (pmSelectedId === preset.id ? " active" : "");
const previewText = (preset.text || "").length > 50
? preset.text.substring(0, 50) + "..."
: (preset.text || "");
const catBadge = preset.category
? `<span style="font-size:9px;color:var(--wfm-primary);margin-left:4px;">[${esc(preset.category)}]</span>`
: "";
const inBatch = isInBatchPreset(preset.id);
el.innerHTML = `
<div class="wfm-pm-item-body">
<div class="wfm-pm-item-name">${preset.favorite ? '<span style="color:#ffd700;">&#9733;</span> ' : ""}${esc(preset.name)}${catBadge}</div>
<div class="wfm-pm-item-sub">${esc(previewText)}</div>
</div>
<div class="wfm-pm-item-actions">
<button class="wfm-pm-action-btn pm-batch-btn${inBatch ? " batch-active" : ""}" title="Batch">B</button>
<button class="wfm-pm-action-btn pm-fav-btn${preset.favorite ? " fav-active" : ""}" title="Favorite">&#9733;</button>
<button class="wfm-pm-action-btn pm-del-btn" title="Delete" style="color:var(--wfm-danger);">&#10005;</button>
</div>
`;
// Click to select in editor
el.addEventListener("click", (e) => {
if (e.target.closest(".wfm-pm-item-actions")) return;
selectPresetInEditor(preset);
});
// Batch toggle
el.querySelector(".pm-batch-btn").addEventListener("click", (e) => {
e.stopPropagation();
toggleBatchPreset(preset.id);
renderPresetManager();
});
// Favorite toggle
el.querySelector(".pm-fav-btn").addEventListener("click", async (e) => {
e.stopPropagation();
const updated = await apiUpdatePreset(preset.id, { favorite: !preset.favorite });
if (updated) {
const idx = promptPresets.findIndex(p => p.id === preset.id);
if (idx >= 0) promptPresets[idx] = updated;
renderPresetManager();
}
});
// Delete
el.querySelector(".pm-del-btn").addEventListener("click", async (e) => {
e.stopPropagation();
if (!confirm(`Delete "${preset.name}"?`)) return;
await apiDeletePreset(preset.id);
promptPresets = promptPresets.filter(p => p.id !== preset.id);
if (pmSelectedId === preset.id) {
pmSelectedId = null;
selectPresetInEditor(null);
}
renderPresetSelect();
renderPresetManager();
showToast(t("deleted"), "success");
});
return el;
}
function renderPmAll(container) {
const items = promptPresets.filter(matchesSearch);
if (items.length === 0) {
container.innerHTML = `<div class="wfm-pm-empty">${promptPresets.length === 0 ? "No presets yet.<br><small>Create one in the Presets panel</small>" : "No matches"}</div>`;
return;
}
for (const p of items) {
container.appendChild(createPmItem(p));
}
}
function renderPmFavorites(container) {
const favs = promptPresets.filter(p => p.favorite);
const items = favs.filter(matchesSearch);
if (items.length === 0) {
container.innerHTML = `<div class="wfm-pm-empty">${favs.length === 0 ? "No favorites yet.<br><small>Star presets to add them here</small>" : "No matches"}</div>`;
return;
}
for (const p of items) {
container.appendChild(createPmItem(p));
}
}
function renderPmGroups(container) {
const groupNames = Object.keys(pmGroups).sort();
if (groupNames.length === 0) {
container.innerHTML = `<div class="wfm-pm-empty">No groups yet.<br><small>Create groups from the Presets panel below</small></div>`;
return;
}
for (const groupName of groupNames) {
const ids = pmGroups[groupName] || [];
const presets = ids.map(id => promptPresets.find(p => p.id === id)).filter(Boolean);
const section = document.createElement("div");
const header = document.createElement("div");
header.className = "wfm-pm-group-header collapsed";
header.innerHTML = `<span>${esc(groupName)}</span> <span class="wfm-pm-badge">${presets.length}</span>`;
const list = document.createElement("div");
list.style.display = "none";
header.addEventListener("click", () => {
list.style.display = list.style.display === "none" ? "block" : "none";
header.classList.toggle("collapsed");
});
for (const p of presets) {
const item = createPmItem(p);
// Add remove-from-group button
const removeBtn = document.createElement("button");
removeBtn.className = "wfm-pm-action-btn";
removeBtn.title = "Remove from group";
removeBtn.textContent = "➖";
removeBtn.style.fontSize = "10px";
removeBtn.addEventListener("click", (e) => {
e.stopPropagation();
pmGroups[groupName] = pmGroups[groupName].filter(id => id !== p.id);
// 予約グループ (Batch) は空になってもキーを保持する
if (pmGroups[groupName].length === 0 && !PROMPT_RESERVED_GROUPS.includes(groupName)) delete pmGroups[groupName];
saveGroups();
renderPresetManager();
});
item.querySelector(".wfm-pm-item-actions").prepend(removeBtn);
list.appendChild(item);
}
section.appendChild(header);
section.appendChild(list);
container.appendChild(section);
}
}
// ============================================
// UI wiring (called from prompt-tab.js's initPromptTab)
// ============================================
export function initPresetsUI() {
loadAllPresets();
const presetSelect = document.getElementById("wfm-preset-select");
const presetName = document.getElementById("wfm-preset-name");
const presetCategory = document.getElementById("wfm-preset-category");
const presetPos = document.getElementById("wfm-preset-pos");
const presetNeg = document.getElementById("wfm-preset-neg");
// Load preset on selection
presetSelect?.addEventListener("change", () => {
const id = presetSelect.value;
const p = promptPresets.find(pp => pp.id === id);
selectPresetInEditor(p || null);
});
// Copy positive prompt to clipboard
document.getElementById("wfm-preset-copy-pos-btn")?.addEventListener("click", () => {
const text = presetPos?.value || "";
if (!text.trim()) {
showToast(t("noTextToCopy"), "error");
return;
}
navigator.clipboard.writeText(text).then(() => {
showToast(t("copiedToClipboard"), "success");
});
});
// Copy negative prompt to clipboard
document.getElementById("wfm-preset-copy-neg-btn")?.addEventListener("click", () => {
const text = presetNeg?.value || "";
if (!text.trim()) {
showToast(t("noTextToCopy"), "error");
return;
}
navigator.clipboard.writeText(text).then(() => {
showToast(t("copiedToClipboard"), "success");
});
});
// Save preset (create or update via API)
document.getElementById("wfm-preset-save-btn")?.addEventListener("click", async () => {
const name = presetName?.value.trim();
const category = presetCategory?.value.trim() || "";
const pos = presetPos?.value || "";
const neg = presetNeg?.value || "";
if (!name) {
showToast(t("enterPresetName"), "error");
return;
}
if (!pos.trim() && !neg.trim()) {
showToast(t("noPromptToSave"), "error");
return;
}
const selectedId = presetSelect?.value;
const existing = selectedId ? promptPresets.find(p => p.id === selectedId) : null;
if (existing) {
// Update
const updated = await apiUpdatePreset(existing.id, {
name, text: pos, negText: neg, category,
});
if (updated) {
const idx = promptPresets.findIndex(p => p.id === existing.id);
if (idx >= 0) promptPresets[idx] = updated;
showToast(t("presetSaved"), "success");
}
} else {
// Create
const created = await apiCreatePreset({
name, text: pos, negText: neg, category,
tags: [], favorite: false,
});
if (created) {
promptPresets.push(created);
showToast(t("presetSaved"), "success");
}
}
pmSelectedId = null;
renderPresetSelect();
renderPresetManager();
if (presetSelect) presetSelect.value = "";
});
// Apply preset to GenerateUI
document.getElementById("wfm-preset-apply-btn")?.addEventListener("click", () => {
const pos = presetPos?.value || "";
const neg = presetNeg?.value || "";
const textareas = document.querySelectorAll("#wfm-gen-prompt-fields textarea");
if (textareas.length === 0) {
showToast(t("noPromptFields"), "error");
return;
}
let applied = false;
if (textareas.length > 0 && pos) {
textareas[0].value = pos;
textareas[0].dispatchEvent(new Event("input", { bubbles: true }));
applied = true;
}
if (textareas.length > 1 && neg) {
textareas[1].value = neg;
textareas[1].dispatchEvent(new Event("input", { bubbles: true }));
applied = true;
}
if (applied) showToast(t("appliedToGenerateUI"), "success");
});
// --- Group management (Presets side) ---
document.getElementById("wfm-preset-new-group-btn")?.addEventListener("click", () => {
const name = prompt("Group name:");
if (!name || !name.trim()) return;
const key = name.trim();
if (pmGroups[key]) {
showToast(t("groupExists"), "error");
return;
}
pmGroups[key] = [];
saveGroups();
renderPresetManager();
showToast(t("groupCreated", key), "success");
});
document.getElementById("wfm-preset-add-to-group-btn")?.addEventListener("click", () => {
const groupSelect = document.getElementById("wfm-preset-group-select");
const groupName = groupSelect?.value;
if (!groupName) {
showToast(t("selectGroupFirst"), "error");
return;
}
const id = presetSelect?.value;
if (!id) {
showToast(t("selectPresetFirst"), "error");
return;
}
if (!pmGroups[groupName]) pmGroups[groupName] = [];
if (pmGroups[groupName].includes(id)) {
showToast(t("alreadyInGroup"), "info");
return;
}
pmGroups[groupName].push(id);
saveGroups();
renderPresetManager();
showToast(t("addedToGroup"), "success");
});
document.getElementById("wfm-preset-del-group-btn")?.addEventListener("click", () => {
const groupSelect = document.getElementById("wfm-preset-group-select");
const groupName = groupSelect?.value;
if (!groupName) {
showToast(t("selectGroupFirst"), "error");
return;
}
if (PROMPT_RESERVED_GROUPS.includes(groupName)) {
showToast(t("modelsGroupReserved"), "warning");
return;
}
if (!confirm(`Delete group "${groupName}"?`)) return;
delete pmGroups[groupName];
saveGroups();
renderPresetManager();
showToast(t("groupDeleted", groupName), "success");
});
// --- Preset Manager ---
document.querySelectorAll(".wfm-pm-tab").forEach(btn => {
btn.addEventListener("click", () => {
pmActiveTab = btn.dataset.pmtab;
document.querySelectorAll(".wfm-pm-tab").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
renderPresetManager();
});
});
document.getElementById("wfm-pm-search-input")?.addEventListener("input", (e) => {
pmSearchText = e.target.value.trim();
renderPresetManager();
});
document.getElementById("wfm-pm-batch-clear-btn")?.addEventListener("click", () => {
clearBatchPresets();
});
}
+220
View File
@@ -0,0 +1,220 @@
/**
* Prompt Tab - Style manager (Col3 "Style" タブ)
*/
// Wildcardファイルマネージャーと同じCRUDパターン(一覧→エディタ→保存/削除/キャンセル)。
// 保存・削除後は generate-tab.js の refreshStylesList() を呼び、GenerateUIのStyleドロップダウンや
// Batchタブのチェックリストを再読み込み無しで最新状態へ同期する。
import { showToast } from "./app.js";
import { t } from "./i18n.js";
import { setupSearchClearBtn } from "./util.js";
import { refreshStylesList } from "./generate-tab.js";
import { esc } from "./prompt-presets.js";
let styleListData = [];
let styleSearchText = "";
let styleEditingOriginalName = null; // null = 新規作成
let styleEditingFile = null; // 編集中/追加先スタイルの定義元ファイル名(style/配下、null = デフォルトのcustom.json)
export async function styleFetchList() {
try {
const res = await fetch("/api/wfm/styles");
return res.ok ? await res.json() : [];
} catch { return []; }
}
export async function styleApiCreate(name, prompt, negativePrompt, targetFile) {
try {
const body = { name, prompt, negative_prompt: negativePrompt };
if (targetFile) body.file = targetFile;
const res = await fetch("/api/wfm/styles", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const data = await res.json().catch(() => ({}));
return res.ok ? { ok: true } : { ok: false, error: data.error || `HTTP ${res.status}` };
} catch (e) {
return { ok: false, error: e.message };
}
}
export async function styleApiUpdate(originalName, name, prompt, negativePrompt) {
try {
const res = await fetch(`/api/wfm/styles/${encodeURIComponent(originalName)}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, prompt, negative_prompt: negativePrompt }),
});
const data = await res.json().catch(() => ({}));
return res.ok ? { ok: true } : { ok: false, error: data.error || `HTTP ${res.status}` };
} catch (e) {
return { ok: false, error: e.message };
}
}
export async function styleApiDelete(name) {
try {
const res = await fetch(`/api/wfm/styles/${encodeURIComponent(name)}`, { method: "DELETE" });
const data = await res.json().catch(() => ({}));
return res.ok ? { ok: true } : { ok: false, error: data.error || `HTTP ${res.status}` };
} catch (e) {
return { ok: false, error: e.message };
}
}
function styleRenderList() {
const list = document.getElementById("wfm-style-list");
if (!list) return;
list.innerHTML = "";
const q = styleSearchText.trim().toLowerCase();
const filtered = q ? styleListData.filter(s => s.name.toLowerCase().includes(q)) : styleListData;
if (filtered.length === 0) {
list.innerHTML = styleListData.length === 0
? `<div class="wfm-pm-empty" style="font-size:12px;">No styles yet.<br><small>Click "+ New" to create one.</small></div>`
: `<div class="wfm-pm-empty" style="font-size:12px;">No matching styles.</div>`;
return;
}
for (const s of filtered) {
const item = document.createElement("div");
item.className = "wfm-wc-file-item";
item.innerHTML = `
<div class="wfm-wc-file-item-info">
<span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${esc(s.name)}</span>
${s.file ? `<span class="wfm-wc-file-ext" title="${esc(s.file)}">${esc(s.file)}</span>` : ""}
</div>
<div class="wfm-wc-file-item-actions">
<button class="wfm-pm-action-btn wfm-style-edit-btn" title="Edit style">&#9998;</button>
</div>
`;
item.querySelector(".wfm-style-edit-btn").addEventListener("click", (e) => {
e.stopPropagation();
styleOpenEditor(s);
});
item.addEventListener("click", () => styleOpenEditor(s));
list.appendChild(item);
}
}
// forcedFile: 「+ Add to this file」から呼ばれた場合に、既存スタイルではなく
// 特定ファイルを追加先として指定するために使う(styleは渡さずnullにする)
function styleOpenEditor(style, forcedFile) {
styleEditingOriginalName = style ? style.name : null;
styleEditingFile = style ? (style.file || null) : (forcedFile || null);
const editor = document.getElementById("wfm-style-editor");
const nameInput = document.getElementById("wfm-style-editor-name");
const promptTA = document.getElementById("wfm-style-editor-prompt");
const negativeTA = document.getElementById("wfm-style-editor-negative");
const deleteBtn = document.getElementById("wfm-style-editor-delete-btn");
const addFileBtn = document.getElementById("wfm-style-editor-addfile-btn");
const fileLabel = document.getElementById("wfm-style-editor-file-label");
if (!editor) return;
nameInput.value = style?.name || "";
promptTA.value = style?.prompt || "";
negativeTA.value = style?.negative_prompt || "";
deleteBtn.style.display = style ? "" : "none";
// 「このファイルへ追加」は既存スタイルを編集中(=対象ファイルが分かっている)時だけ表示
addFileBtn.style.display = style ? "" : "none";
if (styleEditingFile) {
fileLabel.textContent = `File: ${styleEditingFile}`;
fileLabel.style.display = "";
} else {
fileLabel.style.display = "none";
}
editor.style.display = "";
nameInput.focus();
}
function styleCloseEditor() {
styleEditingOriginalName = null;
styleEditingFile = null;
const editor = document.getElementById("wfm-style-editor");
if (editor) editor.style.display = "none";
}
export async function styleRefreshList() {
styleListData = await styleFetchList();
styleRenderList();
}
// ============================================
// UI wiring (called from prompt-tab.js's initPromptTab)
// ============================================
export function initStylesUI() {
document.getElementById("wfm-style-new-btn")?.addEventListener("click", () => {
styleOpenEditor(null);
});
document.getElementById("wfm-style-refresh-btn")?.addEventListener("click", () => {
styleRefreshList();
});
// 編集中の既存スタイルと同じファイルへ、新しい空のスタイルを追加する
document.getElementById("wfm-style-editor-addfile-btn")?.addEventListener("click", () => {
if (!styleEditingFile) return;
styleOpenEditor(null, styleEditingFile);
});
setupSearchClearBtn("wfm-style-search", "wfm-style-search-clear-btn", () => {
styleSearchText = "";
styleRenderList();
});
document.getElementById("wfm-style-search")?.addEventListener("input", (e) => {
styleSearchText = e.target.value;
styleRenderList();
});
document.getElementById("wfm-style-editor-save-btn")?.addEventListener("click", async () => {
const nameInput = document.getElementById("wfm-style-editor-name");
const promptTA = document.getElementById("wfm-style-editor-prompt");
const negativeTA = document.getElementById("wfm-style-editor-negative");
const name = (nameInput?.value || "").trim();
const prompt = promptTA?.value || "";
const negativePrompt = negativeTA?.value || "";
if (!name) { showToast(t("pleaseEnterStyleName"), "error"); return; }
const result = styleEditingOriginalName
? await styleApiUpdate(styleEditingOriginalName, name, prompt, negativePrompt)
: await styleApiCreate(name, prompt, negativePrompt, styleEditingFile);
if (result.ok) {
showToast(t("savedAs", name), "success");
styleCloseEditor();
await styleRefreshList();
await refreshStylesList();
} else {
showToast(t("errorWithMsg", result.error), "error");
}
});
// Editor: delete
document.getElementById("wfm-style-editor-delete-btn")?.addEventListener("click", async () => {
if (!styleEditingOriginalName) return;
if (!confirm(`Delete "${styleEditingOriginalName}"?`)) return;
const result = await styleApiDelete(styleEditingOriginalName);
if (result.ok) {
showToast(t("deletedName", styleEditingOriginalName), "success");
styleCloseEditor();
await styleRefreshList();
await refreshStylesList();
} else {
showToast(t("errorWithMsg", result.error), "error");
}
});
// Editor: cancel
document.getElementById("wfm-style-editor-cancel-btn")?.addEventListener("click", () => {
styleCloseEditor();
});
// Initial style list load
styleRefreshList();
}
+30 -1509
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+475
View File
@@ -0,0 +1,475 @@
/**
* Prompt Tab - Wildcard file manager (list, editor, insert-at-cursor helpers)
*/
import { showToast } from "./app.js";
import { t } from "./i18n.js";
import { esc } from "./prompt-presets.js";
// ============================================
// Wildcard API helpers
// ============================================
export async function wcFetchFiles() {
try {
const res = await fetch("/api/wfm/wildcards");
return res.ok ? await res.json() : [];
} catch { return []; }
}
export 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; }
}
export 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; }
}
export 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">&#9998;</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";
}
export async function wcRefreshFiles() {
wcFiles = await wcFetchFiles();
wcRenderFileList();
wcUpdateFilePicker();
}
// ============================================
// Wildcard file picker popup
// ============================================
// Renders the wildcard-file-reference list into `picker`, inserting into `targetTa` on click.
// Shared by the Form tab's fixed file-picker popup and any dynamically built toolbar
// (see createWildcardToolbar) so both stay in sync with a single implementation.
function renderFilePicker(picker, targetTa) {
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", () => {
wcInsertAtCursor(targetTa, `__${f.wc_name}__`);
picker.style.display = "none";
});
picker.appendChild(btn);
}
}
}
function wcUpdateFilePicker() {
const picker = document.getElementById("wfm-wc-file-picker");
const ta = document.getElementById("wfm-wc-prompt");
if (!picker || !ta) return;
renderFilePicker(picker, ta);
}
// ============================================
// Reusable wildcard syntax toolbar (buttons + file-reference picker)
// ============================================
const WC_SYNTAX_BUTTONS = [
{ open: "{", close: "}", label: "{ }", title: "Random pick set e.g. {cat|dog}" },
{ open: "|", close: "", label: "|", title: "Separator" },
{ open: "__", close: "__", label: "__", title: "Wildcard file reference e.g. __animals__" },
{ open: ":", close: "", label: ":", title: "Colon" },
{ open: ";", close: "", label: ";", title: "Semicolon" },
{ open: "$$", close: "", label: "$$", title: "Multi-pick e.g. {2$$cat|dog|bird}" },
{ open: "[", close: "]", label: "[ ]", title: "Bracket set e.g. [cat|dog]" },
];
// Builds a standalone wildcard-syntax toolbar (syntax buttons, LoRA/n$${} helpers, and the
// file-reference picker) wired to insert into the given textarea. The Form tab's Col3 panel
// uses the static toolbar markup in index.html instead (wired in initWildcardsUI below); this
// is for callers that need the same toolbar attached to a textarea built at runtime, such as
// the Table tab's wildcard edit modal (see prompt-table.js).
export function createWildcardToolbar(textareaEl) {
const toolbar = document.createElement("div");
toolbar.className = "wfm-wc-toolbar";
for (const b of WC_SYNTAX_BUTTONS) {
const btn = document.createElement("button");
btn.className = "wfm-wc-btn";
btn.title = b.title;
btn.textContent = b.label;
btn.addEventListener("click", () => wcInsertAtCursor(textareaEl, b.open, b.close));
toolbar.appendChild(btn);
}
const loraBtn = document.createElement("button");
loraBtn.className = "wfm-wc-btn";
loraBtn.title = "LoRA with LBW e.g. <lora:name:1:LBW=...;...>";
loraBtn.textContent = "<lora>";
loraBtn.addEventListener("click", () => {
wcInsertAtCursor(textareaEl, "<lora::1:LBW=;>");
// Move cursor to after "lora:" for the name
const pos = textareaEl.selectionStart - "<lora::1:LBW=;>".length + 6;
textareaEl.setSelectionRange(pos, pos);
textareaEl.focus();
});
toolbar.appendChild(loraBtn);
const nsetBtn = document.createElement("button");
nsetBtn.className = "wfm-wc-btn";
nsetBtn.title = "Multi-pick set e.g. {2$$cat|dog|bird}";
nsetBtn.textContent = "n$${ }";
nsetBtn.addEventListener("click", () => {
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(t("enterPositiveInt"), "error"); return; }
const template = "{" + num + "$$|}";
const start = textareaEl.selectionStart;
wcInsertAtCursor(textareaEl, template);
// Move cursor between $$ and |
const inner = start + ("{" + num + "$$").length;
textareaEl.setSelectionRange(inner, inner);
textareaEl.focus();
});
toolbar.appendChild(nsetBtn);
const pickerWrap = document.createElement("div");
pickerWrap.className = "wfm-wc-file-picker-wrap";
const pickBtn = document.createElement("button");
pickBtn.className = "wfm-wc-btn";
pickBtn.title = "Insert wildcard file reference";
pickBtn.innerHTML = "&#128194; File";
const picker = document.createElement("div");
picker.className = "wfm-wc-file-picker";
picker.style.display = "none";
pickBtn.addEventListener("click", (e) => {
e.stopPropagation();
const isOpen = picker.style.display !== "none";
picker.style.display = isOpen ? "none" : "";
if (!isOpen) renderFilePicker(picker, textareaEl);
});
document.addEventListener("click", () => { picker.style.display = "none"; });
pickerWrap.appendChild(pickBtn);
pickerWrap.appendChild(picker);
toolbar.appendChild(pickerWrap);
return toolbar;
}
// ============================================
// UI wiring (called from prompt-tab.js's initPromptTab)
// ============================================
export function initWildcardsUI() {
// ── 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(t("enterPositiveInt"), "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(t("sentToPositive"), "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(t("sentToNegative"), "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(t("pleaseEnterFilename"), "error"); return; }
// Validate each path component (allow letters, numbers, -, _, ., space)
const nameParts = rawName.split("/");
if (nameParts.some(p => !p || !/^[\w\-. ]+$/.test(p))) {
showToast(t("invalidPathFormat"), "error");
return;
}
const name = rawName;
const filename = `${name}.${ext}`;
const saved = await wcSaveFile(filename, content);
if (saved) {
showToast(t("savedAs", filename), "success");
wcCloseEditor();
wcRefreshFiles();
} else {
showToast(t("saveFailed"), "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(t("deletedName", wcEditingFilename), "success");
wcCloseEditor();
wcRefreshFiles();
});
// Editor: cancel
document.getElementById("wfm-wc-editor-cancel-btn")?.addEventListener("click", () => {
wcCloseEditor();
});
// Initial wildcard file load
wcRefreshFiles();
}
+10 -11
View File
@@ -46,7 +46,7 @@ function saveBadgePalette(palette) {
// Open in ComfyUI / Send to Canvas
// ============================================
function sendToCanvas(workflowData) {
function sendToCanvas(workflowData, filename) {
try {
// window.opener経由でComfyUIキャンバスに直接ロード(推奨)
if (window.opener && typeof window.opener.wfmReceiveWorkflow === "function") {
@@ -54,14 +54,13 @@ function sendToCanvas(workflowData) {
showToast(t("workflowSentToCanvasDirect"), "success");
return;
}
// フォールバック: localStorage + タイトルドラッグ(UI形式のみ)
const fmt = comfyWorkflow.detectFormat(workflowData);
if (fmt === "api") {
showToast(t("apiFormatCanvasNoOpener"), "error");
return;
}
localStorage.setItem("wfm_pending_workflow", JSON.stringify(workflowData));
showToast(t("workflowSentToCanvas"), "success");
// フォールバック: Workflow Studio LibraryのWorkflows(W)タブを当該ワークフローで
// 絞り込み表示する。以前はUI形式限定で「タイトルをドラッグ」させていたが、
// サムネイルが見えず分かりにくい上、API形式のワークフローはそもそも非対応だった。
// 絞り込み表示なら実物のワークフローカード(サムネイル付き)が出るので、
// フォーマットを問わずユーザー自身がそこからドラッグできる。
localStorage.setItem("wfm_library_filter_request", filename || "");
showToast(t("workflowFilteredInLibrary"), "success");
} catch (err) {
showToast(t("errorWithMsg", err.message), "error");
}
@@ -1112,7 +1111,7 @@ function openDetailModal(wf) {
document.getElementById("wfm-detail-open-comfyui")?.addEventListener("click", async () => {
try {
const wfData = await getRawWorkflow(wf.filename);
sendToCanvas(wfData);
sendToCanvas(wfData, wf.filename);
} catch (err) {
showToast(t("loadError") + ": " + err.message, "error");
}
@@ -1404,7 +1403,7 @@ export function initWorkflowTab() {
if (!state.selectedWf) return;
try {
const wfData = await getRawWorkflow(state.selectedWf.filename);
sendToCanvas(wfData);
sendToCanvas(wfData, state.selectedWf.filename);
} catch (err) {
showToast(t("loadError") + ": " + err.message, "error");
}
+255 -66
View File
@@ -23,7 +23,6 @@
<button class="wfm-tab" data-tab="models">Models</button>
<button class="wfm-tab" data-tab="generate">GenerateUI</button>
<button class="wfm-tab" data-tab="prompt">Prompt</button>
<button class="wfm-tab" data-tab="metadata">Metadata</button>
<button class="wfm-tab" data-tab="gallery">Gallery</button>
<button class="wfm-tab" data-tab="image-edit">Image Edit</button>
<button class="wfm-tab" data-tab="tagger">Tagger</button>
@@ -351,6 +350,7 @@
<div class="wfm-gen-lora-col">
<div class="wfm-gen-lora-col-header">
<span id="wfm-gen-lora-col-title">LoRA</span>
<button class="wfm-btn wfm-btn-xs wfm-lora-bypass-btn" id="wfm-lora-bypass-toggle" title="Bypass LoRA node (force strength to 0)">⛔</button>
<button class="wfm-btn wfm-btn-xs wfm-lora-refresh-btn" id="wfm-lora-stack-refresh" title="Refresh Stack group">🔄</button>
</div>
<div id="wfm-gen-lora-fields" class="wfm-gen-lora-col-body">
@@ -927,6 +927,13 @@
<!-- Prompt Tab -->
<section id="wfm-tab-prompt" class="wfm-tab-content">
<div class="wfm-prompt-toplevel-nav">
<button id="wfm-prompt-toplevel-form" class="wfm-prompt-toplevel-tab active" data-prompt-toplevel="form">Form</button>
<button id="wfm-prompt-toplevel-table" class="wfm-prompt-toplevel-tab" data-prompt-toplevel="table">Table</button>
</div>
<!-- Form パネル -->
<div id="wfm-prompt-panel-form" class="wfm-prompt-panel active">
<div class="wfm-prompt-split wfm-prompt-split-3col">
<!-- Col 1: AI Assistant -->
<div class="wfm-prompt-split-col">
@@ -1152,76 +1159,143 @@
<!-- /Style タブ -->
</div>
</div>
</section>
</div>
<!-- /Form パネル -->
<!-- Metadata Tab -->
<section id="wfm-tab-metadata" class="wfm-tab-content">
<div class="wfm-metadata-layout">
<!-- Col1: Drop zone + preview -->
<div class="wfm-metadata-col1">
<div class="wfm-metadata-drop-zone" id="wfm-meta-drop">
<div class="wfm-meta-drop-label" id="wfm-meta-drop-label">
<div class="wfm-meta-drop-main">Drop image / JSON</div>
<div class="wfm-meta-drop-sub">PNG · WebP · JSON</div>
</div>
<img id="wfm-meta-preview-img" class="wfm-meta-preview-img" style="display:none;" alt="preview">
</div>
<div class="wfm-meta-file-info" id="wfm-meta-file-info"></div>
<div class="wfm-meta-format-note" id="wfm-meta-format-note">
<div class="wfm-meta-format-note-title" id="wfm-meta-format-note-title">Supported formats</div>
<ul class="wfm-meta-format-list">
<li id="wfm-meta-fmt-comfyui">ComfyUI — PNG / WebP / JSON</li>
<li id="wfm-meta-fmt-sdwebui">SD WebUI / SD Forge — PNG (parameters)</li>
<li id="wfm-meta-fmt-fooocus">Fooocus — PNG (fooocus_scheme)</li>
</ul>
<div class="wfm-meta-format-todo" id="wfm-meta-format-todo">Flux.2 / Qwen-Image / Z-Image subgraph workflows supported</div>
</div>
<input type="file" id="wfm-meta-file-input" accept="image/png,image/webp,.json,application/json" style="display:none;">
<!-- Table パネル -->
<div id="wfm-prompt-panel-table" class="wfm-prompt-panel">
<div class="wfm-prompt-table-subnav">
<button class="wfm-prompt-table-subtab active" data-ptable-subtab="presets">Presets</button>
<button class="wfm-prompt-table-subtab" data-ptable-subtab="presetsGroup">Presets Group</button>
<button class="wfm-prompt-table-subtab" data-ptable-subtab="wildcards">Wildcards</button>
<button class="wfm-prompt-table-subtab" data-ptable-subtab="style">Style</button>
</div>
<!-- Col2: Model info -->
<div class="wfm-metadata-col2">
<div class="wfm-meta-section" id="wfm-meta-ckpt-section">
<div class="wfm-meta-section-title">Checkpoint</div>
<div class="wfm-meta-list" id="wfm-meta-ckpt-list"></div>
<!-- Presets テーブル -->
<div id="wfm-ptable-presets" class="wfm-prompt-table-pane active">
<div class="wfm-prompt-table-toolbar">
<div class="wfm-search-wrap" style="flex:1;max-width:280px;">
<input type="text" id="wfm-ptable-presets-search" class="wfm-search-input" placeholder="Search presets...">
<button class="wfm-search-clear-btn" id="wfm-ptable-presets-search-clear-btn" title="Clear search">✕</button>
</div>
<button id="wfm-ptable-presets-sort-fav-btn" class="wfm-btn wfm-btn-sm" title="Sort favorites to top">★</button>
<button id="wfm-ptable-presets-sort-batch-btn" class="wfm-btn wfm-btn-sm" title="Sort batch-selected to top">B</button>
<span id="wfm-ptable-presets-count" class="wfm-nodes-count"></span>
<span style="flex:1;"></span>
<button id="wfm-ptable-presets-copy-pos-btn" class="wfm-btn wfm-btn-sm" title="Copy selected preset's positive prompt">PP Copy</button>
<button id="wfm-ptable-presets-copy-neg-btn" class="wfm-btn wfm-btn-sm" title="Copy selected preset's negative prompt">NP Copy</button>
<select id="wfm-ptable-presets-group-select" class="wfm-select" style="font-size:12px;padding:3px 6px;width:160px;flex:0 0 auto;">
<option value="">Select group...</option>
</select>
<button id="wfm-ptable-presets-add-to-group-btn" class="wfm-btn wfm-btn-sm">Add to Group</button>
<button id="wfm-ptable-presets-batch-clear-btn" class="wfm-btn wfm-btn-sm" title="Clear all batch presets">BC</button>
<button id="wfm-ptable-presets-deselect-btn" class="wfm-btn wfm-btn-sm" disabled title="Clear row selection">Deselect</button>
<button id="wfm-ptable-presets-delete-btn" class="wfm-btn wfm-btn-sm wfm-btn-danger" disabled>Delete</button>
<button id="wfm-ptable-presets-add-btn" class="wfm-btn wfm-btn-sm wfm-btn-primary">+ New Preset</button>
</div>
<div class="wfm-meta-section" id="wfm-meta-vae-section">
<div class="wfm-meta-section-title">VAE</div>
<div class="wfm-meta-list" id="wfm-meta-vae-list"></div>
</div>
<div class="wfm-meta-section" id="wfm-meta-diff-section">
<div class="wfm-meta-section-title">Diffusion Model</div>
<div class="wfm-meta-list" id="wfm-meta-diff-list"></div>
</div>
<div class="wfm-meta-section" id="wfm-meta-te-section">
<div class="wfm-meta-section-title">Text Encoder</div>
<div class="wfm-meta-list" id="wfm-meta-te-list"></div>
<div class="wfm-prompt-table-scroll">
<table class="wfm-table wfm-prompt-edit-table">
<thead>
<tr>
<th style="width:56px;">#</th>
<th style="width:28px;"></th>
<th style="width:160px;">Name</th>
<th style="width:110px;">Category</th>
<th>Positive</th>
<th>Negative</th>
<th style="width:28px;">B</th>
</tr>
</thead>
<tbody id="wfm-ptable-presets-tbody"></tbody>
</table>
</div>
</div>
<!-- Col3: LoRA + Prompt -->
<div class="wfm-metadata-col3">
<div class="wfm-meta-section" id="wfm-meta-lora-section">
<div class="wfm-meta-section-title">LoRA</div>
<div class="wfm-meta-list" id="wfm-meta-lora-list"></div>
</div>
<div class="wfm-meta-section wfm-meta-prompt-section" id="wfm-meta-prompt-section">
<div class="wfm-meta-section-title">Prompt</div>
<div class="wfm-meta-list" id="wfm-meta-prompt-list"></div>
<div class="wfm-meta-prompt-full-wrap">
<div class="wfm-meta-prompt-full-label" id="wfm-meta-prompt-full-label"></div>
<textarea class="wfm-meta-prompt-full" id="wfm-meta-prompt-full" readonly placeholder="Select a prompt above to view full text..."></textarea>
<div class="wfm-meta-prompt-actions">
<button class="wfm-btn wfm-btn-sm" id="wfm-meta-copy-btn">Copy</button>
<button class="wfm-btn wfm-btn-sm" id="wfm-meta-genui-pos-btn">GenUI:P</button>
<button class="wfm-btn wfm-btn-sm" id="wfm-meta-genui-neg-btn">GenUI:N</button>
<button class="wfm-btn wfm-btn-sm" id="wfm-meta-preset-pos-btn">Prompt:P</button>
<button class="wfm-btn wfm-btn-sm" id="wfm-meta-preset-neg-btn">Prompt:N</button>
</div>
<!-- Presets Group テーブル -->
<div id="wfm-ptable-presetsGroup" class="wfm-prompt-table-pane">
<div class="wfm-prompt-table-toolbar">
<div class="wfm-search-wrap" style="flex:1;max-width:280px;">
<input type="text" id="wfm-ptable-presetsgroup-search" class="wfm-search-input" placeholder="Search groups...">
<button class="wfm-search-clear-btn" id="wfm-ptable-presetsgroup-search-clear-btn" title="Clear search">✕</button>
</div>
<span id="wfm-ptable-presetsgroup-count" class="wfm-nodes-count"></span>
<span style="flex:1;"></span>
<button id="wfm-ptable-presetsgroup-deselect-btn" class="wfm-btn wfm-btn-sm" disabled title="Clear row selection">Deselect</button>
<button id="wfm-ptable-presetsgroup-delete-btn" class="wfm-btn wfm-btn-sm wfm-btn-danger" disabled>Delete</button>
<button id="wfm-ptable-presetsgroup-add-btn" class="wfm-btn wfm-btn-sm wfm-btn-primary">+ New Group</button>
</div>
<div class="wfm-prompt-table-scroll">
<table class="wfm-table wfm-prompt-edit-table">
<thead>
<tr>
<th style="width:56px;">#</th>
<th style="width:160px;">Group Name</th>
<th>Presets</th>
</tr>
</thead>
<tbody id="wfm-ptable-presetsgroup-tbody"></tbody>
</table>
</div>
</div>
<!-- Wildcards テーブル -->
<div id="wfm-ptable-wildcards" class="wfm-prompt-table-pane">
<div class="wfm-prompt-table-toolbar">
<div class="wfm-search-wrap" style="flex:1;max-width:280px;">
<input type="text" id="wfm-ptable-wildcards-search" class="wfm-search-input" placeholder="Search wildcard files...">
<button class="wfm-search-clear-btn" id="wfm-ptable-wildcards-search-clear-btn" title="Clear search">✕</button>
</div>
<span id="wfm-ptable-wildcards-count" class="wfm-nodes-count"></span>
<span style="flex:1;"></span>
<button id="wfm-ptable-wildcards-deselect-btn" class="wfm-btn wfm-btn-sm" disabled title="Clear row selection">Deselect</button>
<button id="wfm-ptable-wildcards-delete-btn" class="wfm-btn wfm-btn-sm wfm-btn-danger" disabled>Delete</button>
<button id="wfm-ptable-wildcards-add-btn" class="wfm-btn wfm-btn-sm wfm-btn-primary">+ New File</button>
</div>
<div class="wfm-prompt-table-scroll">
<table class="wfm-table wfm-prompt-edit-table">
<thead>
<tr>
<th style="width:56px;">#</th>
<th style="width:220px;">File (folder/name)</th>
<th style="width:70px;">Ext</th>
<th>Content (one item per line, or YAML)</th>
</tr>
</thead>
<tbody id="wfm-ptable-wildcards-tbody"></tbody>
</table>
</div>
</div>
<!-- Style テーブル -->
<div id="wfm-ptable-style" class="wfm-prompt-table-pane">
<div class="wfm-prompt-table-toolbar">
<div class="wfm-search-wrap" style="flex:1;max-width:280px;">
<input type="text" id="wfm-ptable-style-search" class="wfm-search-input" placeholder="Search styles...">
<button class="wfm-search-clear-btn" id="wfm-ptable-style-search-clear-btn" title="Clear search">✕</button>
</div>
<span id="wfm-ptable-style-count" class="wfm-nodes-count"></span>
<span style="flex:1;"></span>
<button id="wfm-ptable-style-deselect-btn" class="wfm-btn wfm-btn-sm" disabled title="Clear row selection">Deselect</button>
<button id="wfm-ptable-style-delete-btn" class="wfm-btn wfm-btn-sm wfm-btn-danger" disabled>Delete</button>
<button id="wfm-ptable-style-add-btn" class="wfm-btn wfm-btn-sm wfm-btn-primary">+ New Style</button>
</div>
<div class="wfm-prompt-table-scroll">
<table class="wfm-table wfm-prompt-edit-table">
<thead>
<tr>
<th style="width:56px;">#</th>
<th style="width:160px;">Name</th>
<th style="width:110px;">File</th>
<th>Positive</th>
<th>Negative</th>
</tr>
</thead>
<tbody id="wfm-ptable-style-tbody"></tbody>
</table>
</div>
</div>
</div>
<!-- /Table パネル -->
</section>
<!-- Settings Tab -->
@@ -1331,6 +1405,7 @@
<li id="wfm-help-gen-28">Model tab — Highlight color: the label of each model type (Checkpoint, VAE, Diffusion Model, Text Encoder, ControlNet, Hypernetwork) and the LoRA column header turn a distinct color (default bright green) when the loaded workflow actually contains that type of node; this stays visible even while ControlNet/Hypernetwork are collapsed, so you can tell at a glance whether they're in use without expanding them; the color is customizable in the Settings tab</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; if the loaded workflow has Bypass or Mute nodes, a note listing them appears above the search bar (Bypass nodes are excluded with their wires reconnected around them; Mute nodes are excluded with no reconnection, matching ComfyUI itself)</li>
<li id="wfm-help-gen-30">The <b>Save</b> button (right end of the subtab bar) is hidden while the <b>Batch</b> or <b>Lab</b> subtab is active, since those work on workflow copies / their own Plan files rather than the currently loaded workflow; it reappears on Input/Model/Settings/Feeder</li>
</ul>
</div>
<div class="wfm-help-card">
@@ -1368,6 +1443,7 @@
<li id="wfm-help-gen-22">Stack tab — Use Stack Group checkbox (shown only when a Lora Manager node exists in the workflow): <b>OFF</b> (the default whenever a workflow is loaded) shows and edits the LoRAs actually configured in the workflow's Lora Manager node itself; <b>ON</b> shows and edits the "Stack" group registered in the Models tab instead; switching either direction clears the node's LoRA list first, then loads it from the newly selected source; switching back to OFF restores exactly the LoRA list that was present when the workflow was loaded (captured as a snapshot at load time)</li>
<li id="wfm-help-gen-23">Stack tab controls: each row has an active checkbox and Str M / Str C fields; the header "Stack" checkbox toggles all rows at once; the +/- step buttons adjust the strength of all currently active rows together; a "Showing: LoRAs currently in workflow" / "Showing: registered Stack group" label indicates which source is currently displayed; Apply writes the full list back to the target node and also appends LoRA syntax + CivitAI trigger words to the Positive prompt</li>
<li id="wfm-help-gen-24">Caution for <b>CLIP Text Encode edit+</b> (CLIPTextEncodeEditPlus) nodes: when a Lora Manager node's trigger_words output feeds this node's text1 input (RAW/EDIT/front/back modes combine text1 with the editable text_edit field), trigger words are already included automatically at generation time; clicking a LoRA Apply button or turning on Use Stack Group additionally writes LoRA syntax/trigger words into text_edit, duplicating them — leave Use Stack Group off and avoid the LoRA Apply buttons if you want the workflow's original prompt and LoRA composition to stay unmodified</li>
<li id="wfm-help-gen-29">Bypass button (⛔, in the LoRA column header, next to Refresh Stack group): forces the target LoRA node's strength to 0 — strength_model/strength_clip for a standard LoraLoader, or every entry's strength/clipStrength for a Lora Manager node (its LoRA list is kept intact, not replaced) — without touching the actual ComfyUI node's mode; click again to restore the exact values that were there before toggling; targets whichever node Single/Stack Apply default to (the workflow's Lora Manager node if it has one, else the first LoRA node); clicking a Single or Stack Apply button clears the bypass state so it can't silently reassert itself on a later toggle</li>
</ul>
</div>
</div>
@@ -1432,8 +1508,10 @@
<li id="wfm-help-lab-11">Prompt cell editor also has <b>Clear</b> (empties both Positive and Negative) and <b>Get from Previous</b> (copies both Positive and Negative from the keyframe immediately before this one in the same column — e.g. editing #3 copies #2's prompt wholesale; not shown on keyframe #1, which has no earlier row to copy from)</li>
<li id="wfm-help-lab-12">Keyframe #1 in every column shows the currently loaded workflow's own live setting instead of a blank placeholder — for the Model column this covers Checkpoint and VAE only (LoRA is never live-reflected, since there's no reliable way to infer "the" active LoRA back out of a workflow node) — both in its grid cell (dashed border) and pre-filled into its edit modal, for as long as it hasn't been explicitly saved with an override; saving it (even without changing anything) locks that value in as a real override and the dashed/live styling goes away; switching to the Lab subtab always refreshes this against whatever is currently loaded in GenerateUI</li>
<li id="wfm-help-lab-13"><b>Bypass</b> checkbox (every keyframe, including #1): skips that row entirely when picking which setting applies at a given iteration — as if the row didn't exist — so whatever was active before it (an earlier keyframe, or ultimately nothing) just keeps running; Lab-internal only, it never touches the actual ComfyUI node's mode; disables that row's value fields and its Revert checkbox while checked (the two are mutually exclusive); the cell shows an amber "⏭ Bypassed" label, which takes priority over both the Revert and live-value display</li>
<li id="wfm-help-lab-14"><b>Model</b> column header's <b>C / L / V</b> buttons pick which single field (Checkpoint / LoRA / VAE — LoRA is Single-only, no Stack support here) its cells summarize and its edit modal shows; the two you're not currently viewing are left exactly as already saved when you Save. Each button turns the same highlight color as GenerateUI's Model tab (configurable in Settings) when the loaded workflow actually contains that node type. If a LoRA is set but the workflow has no LoRA node at all, Lab warns once per Run and skips applying it — unlike VAE, it never auto-inserts a LoRA node, since that would require rewiring the Checkpoint's model/clip outputs through it</li>
<li id="wfm-help-lab-15">Model column fields carry forward independently: a keyframe that only sets a LoRA doesn't reset Checkpoint/VAE to the workflow default — each of the three keeps whatever an earlier keyframe last set until a later keyframe explicitly changes it. A field left blank in the edit modal shows "(inherits: ...)" as its placeholder instead of "(workflow default)" when there's something to inherit, so it's clear what actually applies. "Revert to #1's setting" resets all three fields at once back to keyframe #1's row, not just the one you're currently viewing</li>
<li id="wfm-help-lab-14"><b>Model</b> column header's <b>C / L / V</b> buttons pick which independent keyframe list — Checkpoint, LoRA (Single-only, no Stack support here), or VAE — the shared cell list, its +/-, and its edit modal all currently operate on. Each of the three is its own timeline with its own keyframes, exactly like Prompt/KSampler; they only ever look merged because they share one column's worth of screen space. Adding, removing, or editing a keyframe while viewing one never touches the other two. Each button turns the same highlight color as GenerateUI's Model tab (configurable in Settings) when the loaded workflow actually contains that node type. If a LoRA is set but the workflow has no LoRA node at all, Lab warns once per Run and skips applying it — unlike VAE, it never auto-inserts a LoRA node, since that would require rewiring the Checkpoint's model/clip outputs through it</li>
<li id="wfm-help-lab-15">Checkpoint / LoRA / VAE each carry forward independently, the same effective-value rule as every other column (see above): a keyframe in one never resets the other two to the workflow default. A field left blank in the edit modal shows "(inherits: ...)" as its placeholder instead of "(workflow default)" when an earlier keyframe in that same list already set something, so it's clear what actually applies. "Revert to #1's setting" and Bypass — like +/- — only ever affect whichever ONE of the three is currently selected by the C/L/V toggle, never the other two</li>
<li id="wfm-help-lab-16">Bypass LoRA node checkbox (LoRA keyframe's own edit modal only): forces that keyframe's strength_model/strength_clip to 0 at apply-time, standing in for ComfyUI's own node Bypass (mode:4) — the workflow used here is already converted to API format, which has no mode concept, so zeroing strength is the behavioral equivalent. Disables the strength fields while checked; independent of the row-level Bypass checkbox above, which skips the whole row instead of touching the node</li>
<li id="wfm-help-lab-17">Apply LoRA syntax + trigger words to Positive prompt checkbox (LoRA column only): when checked, appends &lt;lora:name:strengthModel:strengthClip&gt; plus that LoRA's cached CivitAI trigger words to the Positive prompt, comma-separated, at apply-time — the same format GenerateUI's own Model tab Apply button writes. Switching keyframes (a different LoRA, this checkbox turned off, or the row Bypassed) always strips exactly what the previous iteration injected before adding anything new, so a batch never accumulates every LoRA it has used. Disabled automatically while Bypass LoRA node is checked</li>
</ul>
</div>
@@ -1469,6 +1547,12 @@
<h2 id="wfm-help-prompt-title">Prompt Tab</h2>
<button type="button" class="wfm-btn wfm-btn-sm wfm-help-jump-btn" data-jump-tab="prompt">Open tab &rarr;</button>
</div>
<div class="wfm-help-card">
<h3 id="wfm-help-prompt-card-toplevel-title">Form / Table</h3>
<ul>
<li id="wfm-help-prompt-15">Two top-level tabs: <b>Form</b> is the original one-item-at-a-time editor described below (AI Assistant / Presets / Wildcard / Style panes); <b>Table</b> shows the same underlying data all at once as editable spreadsheets — useful once there are more than a handful of presets, wildcard files, or styles to manage</li>
</ul>
</div>
<div class="wfm-help-card">
<h3 id="wfm-help-prompt-card-ai-title">AI Assistant &amp; Translation</h3>
<ul>
@@ -1503,18 +1587,34 @@
<li id="wfm-help-prompt-14">Each entry in the list shows its source file as a small badge next to the name (e.g. <code>sdxl_styles_diva.json</code>); while editing an existing style, that filename also appears above the Positive prompt field, and a <b>+ Add to this file</b> button appears next to Save — click it to clear the form for a brand-new style that gets appended to that same file instead of the default <code>custom.json</code></li>
</ul>
</div>
<div class="wfm-help-card">
<h3 id="wfm-help-prompt-card-table-title">Table View</h3>
<ul>
<li id="wfm-help-prompt-16">The Table tab has four sub-tabs of its own: <b>Presets</b>, <b>Presets Group</b>, <b>Wildcards</b>, and <b>Style</b> — each an editable table built from the exact same data as the Form tab, so changes on either side stay in sync</li>
<li id="wfm-help-prompt-17">Every table shares a left-most auto-numbered <b>#</b> column: click a number to select that row (highlighted red) — multiple rows can be selected at once; the toolbar's <b>Delete</b> button removes every selected row in one confirm, and <b>Deselect</b> clears the selection. This replaced per-row delete buttons, which were too easy to misclick</li>
<li id="wfm-help-prompt-18"><b>Double-click</b> a row's number to open a larger prompt-editing modal instead of the small in-table textarea. Presets and Style show <b>Positive</b>/<b>Negative</b> toggle buttons above one shared textarea (switching preserves whatever you typed on the other side); Wildcards shows just the file's content. <b>Save</b> writes the change back and closes the modal; <b>Close</b> discards it</li>
<li id="wfm-help-prompt-19">The toolbar's <b>+ New ...</b> button adds a blank row at the top with small &#10003; (Add) / &#10005; (Cancel) icons in place of its number; fill in the fields and click &#10003; to create it, or &#10005; to discard the row without saving</li>
<li id="wfm-help-prompt-20">Presets table — the <b>&#9733;</b> (favorite) and <b>B</b> (Batch group) columns work like toggle switches: click to turn them gold/yellow, click again to clear; the toolbar's <b>BC</b> button clears every preset out of the Batch group at once, same as the Form tab's Preset Manager</li>
<li id="wfm-help-prompt-21">Presets table toolbar — <b>PP Copy</b> / <b>NP Copy</b> copy the single selected row's positive/negative prompt to the clipboard; the <b>group select + Add to Group</b> adds every currently selected preset to the chosen group in one click. Doing the group assignment from here (search/select presets, then pick from a short group list) replaced the old per-group preset picker, which became unusable once there were many presets</li>
<li id="wfm-help-prompt-22">Presets Group table — create, rename, and delete groups; each group's members are shown as removable badges (&#10005;). The reserved <b>Batch</b> group is not listed here — it's managed entirely through the Presets table's B column and BC button instead</li>
<li id="wfm-help-prompt-23">Wildcards table — same files as the Form tab's Wildcard file manager; editing the <b>File</b> path or the <b>Ext</b> dropdown renames the file (saves the content under the new name, then removes the old file)</li>
<li id="wfm-help-prompt-24">Style table — same styles as the Form tab's Style manager, editable inline the same way as Presets</li>
<li id="wfm-help-prompt-25">Presets table toolbar — the ★ and B sort buttons (next to the search box) bring favorited / batch-selected presets to the top of the list; both can be enabled together (★ takes priority, B breaks ties), and everything else keeps its original relative order</li>
</ul>
</div>
</div>
<div class="wfm-help-page" id="wfm-help-page-metadata">
<div class="wfm-help-article-header">
<h2 id="wfm-help-metadata-title">Metadata Tab</h2>
<button type="button" class="wfm-btn wfm-btn-sm wfm-help-jump-btn" data-jump-tab="metadata">Open tab &rarr;</button>
<button type="button" class="wfm-btn wfm-btn-sm wfm-help-jump-btn" data-jump-tab="gallery" data-jump-subtab="metadata">Open tab &rarr;</button>
</div>
<div class="wfm-help-card">
<h3 id="wfm-help-metadata-card-overview-title">Overview</h3>
<p id="wfm-help-metadata-desc">Drop a ComfyUI-generated PNG/WebP or workflow JSON to instantly extract and view the embedded model and prompt information.</p>
<ul>
<li id="wfm-help-metadata-1">Left column: drop zone (image preview for PNG/WebP) and file info; click or drop to load</li>
<li id="wfm-help-metadata-6">This is now a sub-tab inside the <b>Gallery</b> tab (to the right of Style_Catalog), rather than its own top-level tab — everything else about it works exactly the same</li>
</ul>
</div>
<div class="wfm-help-card">
@@ -1620,6 +1720,7 @@
<li id="wfm-help-gallery-2">3-column layout: folder tree (left) | image grid (center) | detail panel (right)</li>
<li id="wfm-help-gallery-3">Thumbnail and Table view modes; double-click any image to open fullscreen lightbox</li>
<li id="wfm-help-gallery-4">Toolbar filters: favorites (★), text search (filename / tag / prompt), tag filter, group filter, sort by date/name</li>
<li id="wfm-help-gallery-26">Four sub-tabs at the top of the Gallery tab: <b>Output</b> (this browser), <b>ImagePrompt</b>, <b>Style_Catalog</b>, and <b>Metadata</b> — the last one embeds the full Metadata Tab (see its own help page) so you can drop an image there to inspect its embedded model/prompt data without leaving Gallery</li>
</ul>
</div>
<div class="wfm-help-card">
@@ -1782,7 +1883,7 @@
</div>
<div class="wfm-help-card">
<h3 id="wfm-help-ai-card-chat-title">Chat pane</h3>
<p id="wfm-help-ai-3">Chat pane: multi-turn conversation with the LLM; Enter to send a message, Shift+Enter for a newline; Clear button resets the conversation history; if the model supports tool calling and asks for an image ("generate a cat picture", etc.) it will run generation on the workflow currently loaded in GenerateUI and show the result inline; the "Allow image generation" checkbox in the pane header toggles this on/off; if an image is attached (dropped in the TOOLS pane) it is sent to the LLM as vision input and, when generate_image is called, used as the base for image-to-image generation instead of text-to-image; models verified to work well for tool calling and vision: Gemma 4:e4b, Qwen 3.5:9b, Ministral 3:3b (Ollama)</p>
<p id="wfm-help-ai-3">Chat pane: multi-turn conversation with the LLM; Enter to send a message, Shift+Enter for a newline; Clear button resets the conversation history; if the model supports tool calling and asks for an image ("generate a cat picture", etc.) it will run generation on the workflow currently loaded in GenerateUI and show the result inline; the "Allow image generation" checkbox in the pane header toggles this on/off; if an image is attached (dropped in the TOOLS pane) it is sent to the LLM as vision input and, when generate_image is called, used as the base for image-to-image generation instead of text-to-image; generate_image also accepts optional generation parameters the model can fill in from the request &mdash; steps, cfg, sampler_name, scheduler, denoise, seed, width, height, batch_size (number of images produced in that one call), and aspect_ratio (a ratio preset like "3:2", applied only if the workflow has a Resolution Selector node feeding width/height &mdash; ignored otherwise); anything the model doesn't supply keeps whatever the loaded workflow already has set; models verified to work well for tool calling and vision: Gemma 4:e4b, Qwen 3.5:9b, Ministral 3:3b (Ollama)</p>
</div>
<div class="wfm-help-card">
<h3 id="wfm-help-ai-card-svg-title">Chat pane — SVG generation</h3>
@@ -2078,6 +2179,7 @@
<button id="wfm-gallery-subtab-output" class="wfm-gallery-subtab-btn active" data-gallery-subtab="output">Output</button>
<button id="wfm-gallery-subtab-imageprompt" class="wfm-gallery-subtab-btn" data-gallery-subtab="imageprompt">ImagePrompt</button>
<button id="wfm-gallery-subtab-styleCatalog" class="wfm-gallery-subtab-btn" data-gallery-subtab="styleCatalog">Style_Catalog</button>
<button id="wfm-gallery-subtab-metadata" class="wfm-gallery-subtab-btn" data-gallery-subtab="metadata">Metadata</button>
</div>
<!-- Output ギャラリーパネル -->
@@ -2167,9 +2269,10 @@
</div>
<!-- タブナビ -->
<div class="wfm-side-tab-nav">
<button class="wfm-gallery-detail-tab-btn active" data-detail-tab="info">Info</button>
<button class="wfm-gallery-detail-tab-btn" data-detail-tab="info">Info</button>
<button class="wfm-gallery-detail-tab-btn" data-detail-tab="meta">JSON</button>
<button class="wfm-gallery-detail-tab-btn" data-detail-tab="group">Groups</button>
<button class="wfm-gallery-detail-tab-btn active" data-detail-tab="prompt">Prompt</button>
</div>
<div class="wfm-gallery-detail-action-row">
<button id="wfm-gallery-open-metadata-btn" class="wfm-gallery-action-btn wfm-gallery-action-btn-green" title="Open in Metadata tab (or Alt+click image)">Metadata</button>
@@ -2177,7 +2280,7 @@
<button id="wfm-gallery-open-tagger-btn" class="wfm-gallery-action-btn wfm-gallery-action-btn-tagger" title="Open in Tagger"></button>
</div>
<!-- Info タブ -->
<div id="wfm-gallery-detail-info" class="wfm-gallery-detail-tab-content active">
<div id="wfm-gallery-detail-info" class="wfm-gallery-detail-tab-content" style="display:none;">
<div class="wfm-gallery-info-section">
<div class="wfm-gallery-info-row" id="wfm-gallery-info-name"></div>
<div class="wfm-gallery-info-row" id="wfm-gallery-info-size"></div>
@@ -2226,6 +2329,21 @@
<div id="wfm-gallery-detail-group" class="wfm-gallery-detail-tab-content" style="display:none;overflow-y:auto;">
<!-- JS動的生成: renderDetailGroup() -->
</div>
<!-- Prompt タブ(埋め込みワークフローから抽出したpositive/negative。Metadataタブより
手数少なく確認できるよう、頻度の高いプロンプト確認用に既定選択タブとしている。
複数プロンプト候補がある場合に備え、Metadataタブと同じPOS/NEGリスト
→ クリックで全文表示のUIをそのまま再利用している(見た目・挙動を統一)。 -->
<div id="wfm-gallery-detail-prompt" class="wfm-gallery-detail-tab-content">
<div class="wfm-meta-section wfm-meta-prompt-section" id="wfm-gallery-prompt-section">
<div class="wfm-meta-section-title">Prompt</div>
<div class="wfm-meta-list" id="wfm-gallery-prompt-list"></div>
<div class="wfm-meta-prompt-full-wrap">
<div class="wfm-meta-prompt-full-label" id="wfm-gallery-prompt-full-label"></div>
<textarea class="wfm-meta-prompt-full" id="wfm-gallery-prompt-full" readonly placeholder="Select a prompt above to view full text..."></textarea>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -2371,6 +2489,77 @@
</div>
</div>
<!-- /Style_Catalog ギャラリーパネル -->
<!-- Metadata ギャラリーパネル -->
<div id="wfm-gallery-panel-metadata" class="wfm-gallery-panel">
<div class="wfm-metadata-layout">
<!-- Col1: Drop zone + preview -->
<div class="wfm-metadata-col1">
<div class="wfm-metadata-drop-zone" id="wfm-meta-drop">
<div class="wfm-meta-drop-label" id="wfm-meta-drop-label">
<div class="wfm-meta-drop-main">Drop image / JSON</div>
<div class="wfm-meta-drop-sub">PNG · WebP · JSON</div>
</div>
<img id="wfm-meta-preview-img" class="wfm-meta-preview-img" style="display:none;" alt="preview">
</div>
<div class="wfm-meta-file-info" id="wfm-meta-file-info"></div>
<div class="wfm-meta-format-note" id="wfm-meta-format-note">
<div class="wfm-meta-format-note-title" id="wfm-meta-format-note-title">Supported formats</div>
<ul class="wfm-meta-format-list">
<li id="wfm-meta-fmt-comfyui">ComfyUI — PNG / WebP / JSON</li>
<li id="wfm-meta-fmt-sdwebui">SD WebUI / SD Forge — PNG (parameters)</li>
<li id="wfm-meta-fmt-fooocus">Fooocus — PNG (fooocus_scheme)</li>
</ul>
<div class="wfm-meta-format-todo" id="wfm-meta-format-todo">Flux.2 / Qwen-Image / Z-Image subgraph workflows supported</div>
</div>
<input type="file" id="wfm-meta-file-input" accept="image/png,image/webp,.json,application/json" style="display:none;">
</div>
<!-- Col2: Model info -->
<div class="wfm-metadata-col2">
<div class="wfm-meta-section" id="wfm-meta-ckpt-section">
<div class="wfm-meta-section-title">Checkpoint</div>
<div class="wfm-meta-list" id="wfm-meta-ckpt-list"></div>
</div>
<div class="wfm-meta-section" id="wfm-meta-vae-section">
<div class="wfm-meta-section-title">VAE</div>
<div class="wfm-meta-list" id="wfm-meta-vae-list"></div>
</div>
<div class="wfm-meta-section" id="wfm-meta-diff-section">
<div class="wfm-meta-section-title">Diffusion Model</div>
<div class="wfm-meta-list" id="wfm-meta-diff-list"></div>
</div>
<div class="wfm-meta-section" id="wfm-meta-te-section">
<div class="wfm-meta-section-title">Text Encoder</div>
<div class="wfm-meta-list" id="wfm-meta-te-list"></div>
</div>
</div>
<!-- Col3: LoRA + Prompt -->
<div class="wfm-metadata-col3">
<div class="wfm-meta-section" id="wfm-meta-lora-section">
<div class="wfm-meta-section-title">LoRA</div>
<div class="wfm-meta-list" id="wfm-meta-lora-list"></div>
</div>
<div class="wfm-meta-section wfm-meta-prompt-section" id="wfm-meta-prompt-section">
<div class="wfm-meta-section-title">Prompt</div>
<div class="wfm-meta-list" id="wfm-meta-prompt-list"></div>
<div class="wfm-meta-prompt-full-wrap">
<div class="wfm-meta-prompt-full-label" id="wfm-meta-prompt-full-label"></div>
<textarea class="wfm-meta-prompt-full" id="wfm-meta-prompt-full" readonly placeholder="Select a prompt above to view full text..."></textarea>
<div class="wfm-meta-prompt-actions">
<button class="wfm-btn wfm-btn-sm" id="wfm-meta-copy-btn">Copy</button>
<button class="wfm-btn wfm-btn-sm" id="wfm-meta-genui-pos-btn">GenUI:P</button>
<button class="wfm-btn wfm-btn-sm" id="wfm-meta-genui-neg-btn">GenUI:N</button>
<button class="wfm-btn wfm-btn-sm" id="wfm-meta-preset-pos-btn">Prompt:P</button>
<button class="wfm-btn wfm-btn-sm" id="wfm-meta-preset-neg-btn">Prompt:N</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- /Metadata ギャラリーパネル -->
</section>
<!-- Image Edit Tab -->
+121
View File
@@ -0,0 +1,121 @@
"""Regenerate README.index.md / DEVLOG.index.md from README.md / DEVLOG.md.
README.md and DEVLOG.md are excluded from graphify's semantic extraction
(.graphifyignore) because they are too large for reliable local-Ollama
extraction: big chunks make the model summarize in prose instead of emitting
JSON, and time out on slower models/hardware. These condensed index files are
extracted instead. Run this whenever README.md or DEVLOG.md changes, then
`graphify --update` to refresh the graph.
Usage:
python tools/generate_doc_index.py [readme|devlog|all]
(defaults to "all")
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
def generate_readme_index() -> Path:
text = (ROOT / "README.md").read_text(encoding="utf-8")
details_re = re.compile(
r"<details>\s*<summary><h3>(.*?)</h3></summary>\s*(.*?)</details>",
re.S,
)
bullet_re = re.compile(r"^- \*\*(.+?)\*\*", re.M)
feature_lines: list[str] = []
for title, body in details_re.findall(text):
names = bullet_re.findall(body)
feature_lines.append(f"### {title.strip()}")
if names:
feature_lines.append(", ".join(names))
feature_lines.append("")
tail_start = text.find("## Installation")
tail = text[tail_start:] if tail_start != -1 else ""
intro_end = text.find("## Screenshots")
intro = text[:intro_end].strip() if intro_end != -1 else ""
out = [
"# README Index — ComfyUI-Workflow-Studio",
"",
"Condensed index of README.md: intro summary, feature names per tab (full "
"descriptions live in the excluded README.md), and the installation/usage/"
"requirements/project-structure sections verbatim. Exists so graphify can "
"extract this without feeding the full README (with its screenshot table) to "
"the local Ollama model. Regenerate with "
"`python tools/generate_doc_index.py readme` whenever README.md changes.",
"",
intro,
"",
"## Features (tab -> feature names)",
"",
*feature_lines,
tail,
]
content = "\n".join(out)
out_path = ROOT / "README.index.md"
out_path.write_text(content, encoding="utf-8")
return out_path
def generate_devlog_index() -> Path:
text = (ROOT / "DEVLOG.md").read_text(encoding="utf-8")
entries = re.split(r"^## ", text, flags=re.M)[1:]
lines: list[str] = []
for entry in entries:
parts = entry.split("\n", 1)
version_line = parts[0].strip()
rest = parts[1] if len(parts) > 1 else ""
m = re.search(r"^### (.+)$", rest, re.M)
subtitle = m.group(1).strip() if m else ""
lines.append(
f"- **{version_line}** — {subtitle}" if subtitle else f"- **{version_line}**"
)
out = [
"# DEVLOG Index — ComfyUI-Workflow-Studio",
"",
"Condensed version-by-version index of DEVLOG.md (each line: version + "
'one-line summary of what changed and why). Full rationale, code details, '
'and "How to apply" lessons live in the excluded DEVLOG.md itself; this '
"index exists so graphify can extract a queryable semantic node per release "
"without feeding the full log to the local Ollama model. Regenerate with "
"`python tools/generate_doc_index.py devlog` whenever DEVLOG.md changes.",
"",
*lines,
"",
]
content = "\n".join(out)
out_path = ROOT / "DEVLOG.index.md"
out_path.write_text(content, encoding="utf-8")
return out_path
def main() -> None:
target = sys.argv[1] if len(sys.argv) > 1 else "all"
written: list[Path] = []
if target in ("readme", "all"):
written.append(generate_readme_index())
if target in ("devlog", "all"):
written.append(generate_devlog_index())
if not written:
print(f"unknown target {target!r}; use readme|devlog|all", file=sys.stderr)
raise SystemExit(2)
for p in written:
print(f"wrote {p.relative_to(ROOT)} ({p.stat().st_size:,} bytes)")
if __name__ == "__main__":
main()
+29
View File
@@ -2376,6 +2376,35 @@ const createDraggableItem = (label, type, data, subtitle) => {
// Toggle & exports
// ============================================
// Opens the Library panel on the Workflows (W) tab, filtered down to a single
// workflow by filename. Used as the "Send to Canvas" fallback when window.opener
// isn't available (or the workflow is API-format, which the title-drag mechanism
// can't handle at all) — the user gets the actual workflow card, thumbnail
// included, to drag onto the canvas themselves instead of a bare title element.
export const showWorkflowInLibrary = async (filename) => {
const panel = createPanel();
state.visible = true;
panel.style.display = "flex";
state.topTab = "workflows";
panel.querySelectorAll(".wfm-nlp-top-tab").forEach(b => b.classList.toggle("active", b.dataset.toptab === "workflows"));
state.wfSubTab = "wf-all";
state.wfSubTab2 = null;
if (!state.wfLoaded) await loadWfData();
state.searchText = (filename || "").toLowerCase().trim();
const searchInput = panel.querySelector(".wfm-nlp-search-input");
if (searchInput) {
searchInput.value = filename || "";
searchInput.placeholder = "Search workflows...";
}
rebuildSubTabs();
renderContent();
installCanvasDropHandler();
};
export const togglePanel = async () => {
const panel = createPanel();
state.visible = !state.visible;
+18 -1
View File
@@ -1,5 +1,5 @@
import { app } from "../../scripts/app.js";
import { togglePanel, getNodeSetsIcon, NODE_SETS_TOOLTIP, saveSelectedAsNodeSet } from "./node_sets_menu.js";
import { togglePanel, getNodeSetsIcon, NODE_SETS_TOOLTIP, saveSelectedAsNodeSet, showWorkflowInLibrary } from "./node_sets_menu.js";
const BUTTON_TOOLTIP = "Launch Workflow Studio (Shift+Click opens in new window)";
const SNAPSHOT_TOOLTIP = "Save workflow canvas image as thumbnail";
@@ -572,6 +572,23 @@ app.registerExtension({
};
},
async setup() {
// Send to Canvas fallback: when the Workflow Studio SPA can't reach this page
// directly via window.opener (e.g. it isn't the opener that spawned it, or
// the workflow is API-format and window.opener.wfmReceiveWorkflow can't
// handle it either), the SPA writes the workflow's filename here instead of
// the old title-drag mechanism. localStorage "storage" events fire in every
// same-origin tab except the one that wrote the value, so this listener
// picks it up here and opens the Library panel pre-filtered to that
// workflow's card for the user to drag onto the canvas themselves.
window.addEventListener("storage", (e) => {
if (e.key === "wfm_library_filter_request" && e.newValue != null) {
showWorkflowInLibrary(e.newValue);
// Consume it so a future page load (which reads the stale value but
// never sees a "storage" event for it) doesn't re-trigger the filter.
localStorage.removeItem("wfm_library_filter_request");
}
});
const injectStyles = () => {
const styleId = "wfm-top-menu-button-styles";
if (document.getElementById(styleId)) return;
+627
View File
@@ -0,0 +1,627 @@
{
"id": "8e2c1a4f-6b3d-4a9e-9c7f-1d5e8f3a2b6c",
"revision": 0,
"last_node_id": 10,
"last_link_id": 11,
"nodes": [
{
"id": 7,
"type": "CLIPTextEncode",
"pos": [
413,
389
],
"size": [
425.27801513671875,
180.6060791015625
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"localized_name": "clip",
"name": "clip",
"type": "CLIP",
"link": 5
},
{
"localized_name": "text",
"name": "text",
"type": "STRING",
"widget": {
"name": "text"
},
"link": null
}
],
"outputs": [
{
"localized_name": "CONDITIONING",
"name": "CONDITIONING",
"type": "CONDITIONING",
"slot_index": 0,
"links": [
6
]
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode",
"ue_properties": {
"widget_ue_connectable": {},
"version": "7.8",
"input_ue_unconnectable": {}
}
},
"widgets_values": [
"text, watermark"
]
},
{
"id": 3,
"type": "KSampler",
"pos": [
863,
186
],
"size": [
315,
474
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"localized_name": "model",
"name": "model",
"type": "MODEL",
"link": 1
},
{
"localized_name": "positive",
"name": "positive",
"type": "CONDITIONING",
"link": 4
},
{
"localized_name": "negative",
"name": "negative",
"type": "CONDITIONING",
"link": 6
},
{
"localized_name": "latent_image",
"name": "latent_image",
"type": "LATENT",
"link": 2
},
{
"localized_name": "seed",
"name": "seed",
"type": "INT",
"widget": {
"name": "seed"
},
"link": null
},
{
"localized_name": "steps",
"name": "steps",
"type": "INT",
"widget": {
"name": "steps"
},
"link": null
},
{
"localized_name": "cfg",
"name": "cfg",
"type": "FLOAT",
"widget": {
"name": "cfg"
},
"link": null
},
{
"localized_name": "sampler_name",
"name": "sampler_name",
"type": "COMBO",
"widget": {
"name": "sampler_name"
},
"link": null
},
{
"localized_name": "scheduler",
"name": "scheduler",
"type": "COMBO",
"widget": {
"name": "scheduler"
},
"link": null
},
{
"localized_name": "denoise",
"name": "denoise",
"type": "FLOAT",
"widget": {
"name": "denoise"
},
"link": null
}
],
"outputs": [
{
"localized_name": "LATENT",
"name": "LATENT",
"type": "LATENT",
"slot_index": 0,
"links": [
7
]
}
],
"properties": {
"Node name for S&R": "KSampler",
"ue_properties": {
"widget_ue_connectable": {},
"version": "7.8",
"input_ue_unconnectable": {}
}
},
"widgets_values": [
378124453179007,
"randomize",
20,
8,
"euler",
"normal",
1
]
},
{
"id": 8,
"type": "VAEDecode",
"pos": [
1209,
188
],
"size": [
210,
46
],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"localized_name": "samples",
"name": "samples",
"type": "LATENT",
"link": 7
},
{
"localized_name": "vae",
"name": "vae",
"type": "VAE",
"link": 10
}
],
"outputs": [
{
"localized_name": "IMAGE",
"name": "IMAGE",
"type": "IMAGE",
"slot_index": 0,
"links": [
9
]
}
],
"properties": {
"Node name for S&R": "VAEDecode",
"ue_properties": {
"widget_ue_connectable": {},
"version": "7.8",
"input_ue_unconnectable": {}
}
},
"widgets_values": []
},
{
"id": 9,
"type": "SaveImage",
"pos": [
1451,
189
],
"size": [
210,
270
],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"localized_name": "images",
"name": "images",
"type": "IMAGE",
"link": 9
},
{
"localized_name": "filename_prefix",
"name": "filename_prefix",
"type": "STRING",
"widget": {
"name": "filename_prefix"
},
"link": null
}
],
"outputs": [],
"properties": {
"Node name for S&R": "SaveImage",
"ue_properties": {
"widget_ue_connectable": {},
"version": "7.8",
"input_ue_unconnectable": {}
}
},
"widgets_values": [
"ComfyUI"
]
},
{
"id": 5,
"type": "EmptyLatentImage",
"pos": [
473,
609
],
"size": [
315,
106
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"localized_name": "width",
"name": "width",
"type": "INT",
"widget": {
"name": "width"
},
"link": null
},
{
"localized_name": "height",
"name": "height",
"type": "INT",
"widget": {
"name": "height"
},
"link": null
},
{
"localized_name": "batch_size",
"name": "batch_size",
"type": "INT",
"widget": {
"name": "batch_size"
},
"link": null
}
],
"outputs": [
{
"localized_name": "LATENT",
"name": "LATENT",
"type": "LATENT",
"slot_index": 0,
"links": [
2
]
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage",
"ue_properties": {
"widget_ue_connectable": {},
"version": "7.8",
"input_ue_unconnectable": {}
}
},
"widgets_values": [
1024,
1024,
1
]
},
{
"id": 4,
"type": "CheckpointLoaderSimple",
"pos": [
51.3,
182.49999999999997
],
"size": [
315,
98
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"localized_name": "ckpt_name",
"name": "ckpt_name",
"type": "COMBO",
"widget": {
"name": "ckpt_name"
},
"link": null
}
],
"outputs": [
{
"localized_name": "MODEL",
"name": "MODEL",
"type": "MODEL",
"slot_index": 0,
"links": [
1,
11
]
},
{
"localized_name": "CLIP",
"name": "CLIP",
"type": "CLIP",
"slot_index": 1,
"links": [
3,
5
]
},
{
"localized_name": "VAE",
"name": "VAE",
"type": "VAE",
"slot_index": 2,
"links": []
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple",
"ue_properties": {
"widget_ue_connectable": {},
"version": "7.8",
"input_ue_unconnectable": {}
}
},
"widgets_values": [
"v1-5-pruned-emaonly-fp16.safetensors"
]
},
{
"id": 10,
"type": "VAELoader",
"pos": [
61.897062649999256,
347.8818042399999
],
"size": [
270,
58
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"localized_name": "vae_name",
"name": "vae_name",
"type": "COMBO",
"widget": {
"name": "vae_name"
},
"link": null
}
],
"outputs": [
{
"localized_name": "VAE",
"name": "VAE",
"type": "VAE",
"links": [
10
]
}
],
"properties": {
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {}
},
"Node name for S&R": "VAELoader"
},
"widgets_values": [
"vae-ft-mse-840000-ema-pruned.safetensors"
]
},
{
"id": 6,
"type": "ImpactWildcardEncode",
"pos": [
415,
150
],
"size": [
420,
322
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"localized_name": "model",
"name": "model",
"type": "MODEL",
"link": 11
},
{
"localized_name": "clip",
"name": "clip",
"type": "CLIP",
"link": 3
}
],
"outputs": [
{
"localized_name": "model",
"name": "model",
"type": "MODEL",
"slot_index": 0,
"links": []
},
{
"localized_name": "clip",
"name": "clip",
"type": "CLIP",
"slot_index": 1,
"links": []
},
{
"localized_name": "conditioning",
"name": "conditioning",
"type": "CONDITIONING",
"slot_index": 2,
"links": [
4
]
},
{
"localized_name": "populated_text",
"name": "populated_text",
"type": "STRING",
"slot_index": 3,
"links": []
}
],
"properties": {
"Node name for S&R": "ImpactWildcardEncode"
},
"widgets_values": [
"a photo of __sample_season/spring__, with __sample_lighting/natural__, highly detailed",
"",
"populate",
"Select the LoRA to add to the text",
"Select the Wildcard to add to the text",
123456789,
"randomize"
]
}
],
"links": [
[
1,
4,
0,
3,
0,
"MODEL"
],
[
2,
5,
0,
3,
3,
"LATENT"
],
[
3,
4,
1,
6,
1,
"CLIP"
],
[
4,
6,
2,
3,
1,
"CONDITIONING"
],
[
5,
4,
1,
7,
0,
"CLIP"
],
[
6,
7,
0,
3,
2,
"CONDITIONING"
],
[
7,
3,
0,
8,
0,
"LATENT"
],
[
9,
8,
0,
9,
0,
"IMAGE"
],
[
10,
10,
0,
8,
1,
"VAE"
],
[
11,
4,
0,
6,
0,
"MODEL"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"offset": [
0,
0
],
"scale": 1
},
"ue_links": [],
"links_added_by_ue": []
},
"version": 0.4
}