Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afed788537 | ||
|
|
ee020cb0c0 | ||
|
|
f6942eb7fa | ||
|
|
bdfd0edb7a |
@@ -2,6 +2,46 @@
|
||||
|
||||
---
|
||||
|
||||
## v0.3.80
|
||||
|
||||
### 追加機能: Comic Creator「半自動マンガ作成」向けブリッジ(LLMプロンプト下書き・バッチ画像生成)
|
||||
|
||||
別カスタムノード`comfyui-comic-creator`が実装中の「半自動マンガ作成」機能(スクリプトのコマ割り・セリフからコマ単位でフキダシ・画像を自動生成するPhase 3)から、iframe越しに呼び出される新規ブリッジ2つを`gallery-tab.js`に追加。既存の`_wfmReceiveImageForI2I`/`_wfmReceiveInpaintRequest`/`_wfmReceiveI2IRunRequest`と同じ「`window._wfmReceiveXxx`をwindowへ直接代入し、Comic Creator側からiframe越しに呼ぶ」配置パターンを踏襲。
|
||||
|
||||
- **`_wfmReceiveLLMPromptRequest(context)`**: シーン・登場人物・セリフ・現在のプロンプト下書きをコンテキストとして受け取り、AIタブの翻訳機能が既に持つLLM接続ロジック(`ai-tab.js`の`callLLM`/`loadAiSettings`/`isValidBackendUrl`、Ollama/LM Studio対応、設定タブの接続設定をそのまま再利用)で画像生成プロンプト案を1本生成して返す。これらの関数は元々`ai-tab.js`内に閉じていたため、`export`に追加してモジュール間で再利用できるようにした。
|
||||
- **`_wfmReceiveGenerateRequest(prompt, width, height)`**: 現在Generate UIタブにロード中のワークフローでtxt2img生成を実行し、結果画像のURLを返す。専用ワークフロー選択(I2I/Inpaint連携にある`workflowData`引数)は今回は非対応。幅・高さの指定は、設定タブの「Latent Image」パネルが`analysis.latent_nodes[0]`(`EmptyLatentImage`/`EmptySD3LatentImage`ノードを検出)を使って`workflow[id].inputs.width/height`を直接書き換えているのと同じ手法をそのまま踏襲。結果URLの取得は`_runInpaintWithImages`等と同じ「`window._wfmGenerateTab.generate()`実行後に`#wfm-gen-result-img`の`src`を読む」パターンを採用。
|
||||
|
||||
**検証**: Comic Creator側からKaptureで実機E2E確認済み(Ollama接続でのLLM呼び出し、t2iワークフローでの画像生成・結果URL取得とも成功)。詳細はComic Creator側DEVLOG「半自動マンガ作成 Phase 3」の項を参照。
|
||||
|
||||
---
|
||||
|
||||
## v0.3.79
|
||||
|
||||
### 追加機能: サイドパネル(Workflow Studio Library)IタブのMage-Flow等サブグラフワークフロー対応
|
||||
|
||||
v0.3.78でMetadataタブ(`metadata-tab.js`)に行ったMage-Flowサブグラフ対応と同様の対応を、「同様にサイドパネル(ComfyUIキャンバス上のIタブ)も更新したい」という依頼を受けて`node_sets_menu.js`にも適用。調査したところ、`node_sets_menu.js`のIタブは`metadata-tab.js`とほぼ同一の抽出関数群(`_extractCheckpoints`/`_extractPrompts`/`_collectAllNodes`等)を独立に重複実装しており、`web/comfyui/`は`static/js/`と配信URLが異なるためESモジュールをimportできない(既存のAI backend URL解決等と同じ制約)。そのためMetadataタブ側の対応がそのまま反映されない状態だった。
|
||||
|
||||
- `comfyui-workflow.js`の`convertUiToApi()`相当ロジック一式(`_flattenSubgraphs`による外側ウィジェット値の内部ノードへの注入、`_getWidgetInputNames`/`_getWidgetInputTypes`/`_getDynamicComboSubNames`によるobject_infoベースのwidgetマッピング、DynamicCombo型対応を含む、約450行)を`_wfm`接頭辞を付けて`node_sets_menu.js`にローカル複製。GenerateUIタブと異なりIタブは読み取り専用表示のため、Checkpoint代替やBypass/Muteノードの記録機能(`_lastCheckpointSubstitutions`等)は複製せず省略。
|
||||
- `fromWorkflow`をasync化し、サブグラフ(`definitions.subgraphs`)を検出した場合は複製版`_wfmConvertUiToApi()`でAPI形式に正規化してから抽出するよう変更(変換失敗時は元のUI形式のまま抽出する既存ロジックへフォールバック)。
|
||||
- `_extractPromptsFromNodeSet`/`_extractPromptsAPI`に`TextEncodeMageFlowEdit`(1ノードでprompt/negative_prompt両方を直接持つ複合ノード)専用の分岐を追加。既存のtextMap(1ノード1テキストしか保持できない)構造では両ロールを区別できず、同じテキストが両方に登録されてしまうバグの修正を複製。
|
||||
|
||||
**検証**: `node_sets_menu.js`は`../../scripts/app.js`をトップレベルでimportしモジュール自体をNode.jsで直接実行できないため、複製したロジックブロック(`_sanitizeJSON`〜`_extractAllMetadata`の自己完結範囲)を`sed`で抜き出しNode.jsでテスト。`mageflow_1.json`で`metadata-tab.js`側と完全に一致する結果(モデル名・プロンプトとも外側の実際の値)を確認。既存の他サブグラフワークフロー(Flux2/Qwen/Ernie/Krea2等)8件でもエラーなく動作しリグレッションがないことを確認。
|
||||
|
||||
**How to apply**: `web/comfyui/`はimport不可のためローカル複製パターンを踏襲する場合、複製元のロジックが今後も更新される可能性があるものは、複製箇所に「変更する場合は元ファイル側も同期すること」という注意コメントを残す。複製したコードの検証は、importが困難なファイルでも`sed`等で自己完結したコードブロックを抜き出しNode.jsで実行すれば、ブラウザなしでも複製元と出力を突き合わせられる。
|
||||
|
||||
### 追加機能: AI TOOL Chatにスキル機能を追加・SVG生成品質改善・ヘルプのカード形式再設計
|
||||
|
||||
「Chatで生成されるSVGの質を上げたい」という依頼から調査を開始したところ、Chat機能(`ai-tab.js`)にはsystem promptの仕組みが一切なく、SVG生成は「モデルが会話の流れで偶然SVGコードを書いた場合に正規表現で抽出するだけ」という後付け設計だったことが判明した(`_extractSvgCode`/`_appendSvgPreview`)。これを踏まえ、単発のSVG改善ではなく汎用的な「AIスキル」機構(選択式system promptライブラリ)を新設し、SVG生成品質改善をその第一弾スキルとして実装する方針に変更した。
|
||||
|
||||
- **AIスキル機構**: `.md`ファイル(frontmatterで`name`/`description`を定義)を`user/default/Workflow-Studio/ai_skills/`に保存し、Chatペイン上部のドロップダウンで選択すると、その内容がsystem promptとして会話履歴の先頭に(表示上のチャット履歴には含めず)注入される。バックエンドは既存の`wildcard_service.py`/`wildcard_routes.py`パターンを踏襲した`skill_service.py`/`skill_routes.py`(`GET/POST /api/wfm/skills`系)を新設。管理UI(一覧・追加・編集・削除)はワイルドカードエディタのUIパターンを再利用してChatペインに内蔵。
|
||||
- **SVG Icon Generatorスキル**: アイコンライブラリ(Feather/Heroicons等)の設計慣習(`viewBox`+`currentColor`+`stroke-width`)とSVGのXSS対策(`<script>`/イベント属性/`<foreignObject>`禁止)を組み込んだ初期スキルとして同梱。実機検証(gemma3系モデル等)でカモメ/ペンギン等の生成結果が「落書き」状態だったため追加調査したところ、LLMのSVG生成研究(Chat2SVG等)で言われる「Shape Decomposition」(対象を部位に分解してから図形を組み立てる)を明示的に指示していないことが原因と判明。「Composition process」セクション(部位分解→形状割当→組み立ての手順を明示指示)を追加し、有機的な輪郭には短い`<path>`曲線を許可するよう緩和した結果、体感で大幅に改善(ユーザー確認済み)。
|
||||
- **Skill Creator(対話式スキル作成)**: 「作りたいスキルを対話形式で聞き出し、完成品を返す」ためのメタスキルを第二弾として同梱。1問ずつ質問し、十分な情報が集まったら\`\`\`skill\`\`\`フェンスでfrontmatter付きの完成スキルを出力する設計。フロント側に\`\`\`skill\`\`\`ブロック検出+「スキルとして保存」ボタン(クリックでスキル管理パネルを開きfrontmatterの`name`から自動生成したファイル名とともにエディタへプリフィル)を追加し、実際にqwen3:4bで対話→保存までのフローを検証済み。SVGプレビュー機能と同じ理由でSPA(`ai-tab.js`)側のみの実装とし、サイドパネル(`node_sets_menu.js`)はスキル選択・system prompt注入のみ対応。
|
||||
- **ヘルプのカード形式再設計**: 別件で導入中の`comfyui-comic-creator`のヘルプUIが「セクションをカード形式で表示(workflow studio のヘルプ形式)」というコメント付きで実装されており、調査したところWFS自身のヘルプUI(サイドバーナビ+検索)を参考に作られたものだったが、1トピック=1カードという構造だけはWFS側になかったことが判明。AI TOOLページを皮切りに全16ヘルプページを「ページ見出し+タブへのジャンプボタン」+「トピックごとの複数カード(見出し+`<ul>`)」の構造に再構成した。個々の項目テキスト・id・i18n翻訳キーは変更せず、既存の`<li>`要素をどのカードの`<ul>`に含めるかという入れ物の再編成のみで実施(新規に追加したのはカード見出し用のキーのみ、約50個×3言語)。再構成の過程で、AI TOOLページの`helpAi5`(Settingsペインの説明)が`helpAi6`に完全に包含される重複項目だったこと、Taggerページと一部のTrouble項目がindex.html上は空の`<li>`(実テキストはi18n.js側にのみ存在)だったことも発見し、あわせて修正・記入した。
|
||||
|
||||
**検証**: 新規追加した`/api/wfm/skills`系エンドポイントをcurlで確認。ブラウザでSVGスキル・Skill Creatorスキルの選択→送信→system prompt注入をfetchフックで実データ検証、手書きの「Bird」few-shot例は実際にレンダリングして認識可能な形状であることを確認。ヘルプの再設計は全16ページを実ブラウザでクリックし、カード表示・ジャンプボタンの遷移・翻訳適用を確認。
|
||||
|
||||
**How to apply**: 新しい「選択式system promptライブラリ」的な機能を追加する際は、`skill_service.py`のfrontmatterパース(`_FRONTMATTER_RE`)とファイル一覧/読み書きAPIパターンがそのまま再利用できる。ヘルプページを新設・改修する際は、1ページ=1個の巨大な`<ul>`ではなく、トピックごとに`.wfm-help-card`で分割し、新規カード見出しはid+i18n 3言語キー+`app.js`の`helpIdMap`の3点セットで追加すること([[project_v0336_conventions]]のヘルプ更新規約を参照)。index.html上のテキストは`applyI18nToHtml()`がi18n.jsの内容で必ず上書きするため、実際に表示される文言はi18n.js側が正であり、index.html側は空にしたり古いままにしたりせず同期を保つこと。
|
||||
|
||||
## v0.3.78
|
||||
|
||||
### 追加機能: Eagle自動保存のSVG対応
|
||||
|
||||
@@ -21,7 +21,7 @@ A comprehensive workflow, asset management, and generation UI plugin for [ComfyU
|
||||
- Built-in AI tools (translation and more)
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
## Screenshots
|
||||
|
||||
@@ -282,6 +282,7 @@ Two independent modes selectable via **[Image Loop] / [Gallery]** toggle buttons
|
||||
- **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
|
||||
- **Chat pane — image-to-image (I2I)** (v0.3.74) — when an image is attached and `generate_image` is invoked, the attachment is uploaded and swapped into the target workflow's LoadImage node instead of running text-to-image; the target workflow is the one currently loaded in GenerateUI, or a dedicated I2I workflow configured in the Settings pane
|
||||
- **Chat pane — SVG generation, no ComfyUI workflow involved** (v0.3.77) — the LLM can also produce SVG graphics directly as text; if a reply contains SVG code (wrapped in a markdown code fence or written directly as a bare `<svg>...</svg>`), it's rendered inline as a preview and a "Save to Gallery" button writes it as a `.svg` file to the Gallery tab's output folder; unlike the Tool Calling image generation flow above, this is plain text pattern matching, so it works with any model regardless of tool-calling support
|
||||
- **Chat pane — Skills** (v0.3.79) — select a `.md` skill file from a dropdown above the message list to prepend its instructions to the conversation as a system prompt (not shown in the visible chat log); a pencil button opens an in-pane manager (list + inline editor, same pattern as the Prompt tab's wildcard file manager) to create, edit, or delete skill files stored in `user/default/Workflow-Studio/ai_skills/`; a file's `---`-delimited frontmatter (`name`/`description`) is shown in the dropdown; two starter skills are bundled — **SVG Icon Generator** (explicitly decomposes the subject into parts before drawing, which markedly improves recognizability from small local models) and **Skill Creator** (interviews you conversationally and drafts a new skill as a ```skill code block; a "Save as new skill" button appears under such replies and opens the manager pre-filled with the draft)
|
||||
- **Settings pane** — choose backend (Ollama / LM Studio / Lemonade), set the API URL, test connection, select a model (with refresh button), and configure Free language names for translation source and destination
|
||||
- **Settings pane — Chat Image Generation** (v0.3.73) — "Use dedicated workflow" checkbox lets Chat-triggered image generation use a saved workflow of your choice instead of the one currently loaded in GenerateUI, without disturbing what's shown there (same pattern as Image Edit's Inpaint dedicated workflow)
|
||||
- **Settings pane — Chat I2I Generation** (v0.3.74) — same "Use dedicated workflow" pattern as above, but for image-to-image generation when an image is attached; independent of the text-to-image dedicated workflow setting
|
||||
@@ -409,6 +410,16 @@ Click the **camera icon** (next to the W button) in ComfyUI's top bar to capture
|
||||
|
||||
## Changelog
|
||||
|
||||
### v0.3.80
|
||||
|
||||
- **Comic Creator — Semi-Automatic Manga bridges** — added `window._wfmReceiveLLMPromptRequest(context)` and `window._wfmReceiveGenerateRequest(prompt, width, height)` (`gallery-tab.js`), following the same "assign to `window`, called across the iframe boundary" pattern as the existing I2I/Inpaint bridges. The LLM bridge drafts an image-generation prompt from scene/character/dialogue context using the AI tab's existing Ollama/LM Studio connection logic (`callLLM`/`loadAiSettings`/`isValidBackendUrl`, now exported from `ai-tab.js`); the generate bridge runs a txt2img generation with the workflow currently loaded in the GenerateUI tab, writing the requested width/height into the detected `EmptyLatentImage`/`EmptySD3LatentImage` node the same way the Settings tab's Latent Image panel does, and returns the result image URL.
|
||||
|
||||
### v0.3.79
|
||||
|
||||
- **AI TOOL Chat — Skills** — a selectable library of `.md` system-prompt files (frontmatter `name`/`description`) that get prepended to the conversation; managed in-pane (list + inline editor) with a new `/api/wfm/skills` backend (`skill_service.py`/`skill_routes.py`, same pattern as the existing wildcard file manager); two bundled skills: **SVG Icon Generator**, engineered around the "decompose the subject into parts before drawing" technique to fix unrecognizable SVG output from smaller local models, and **Skill Creator**, a meta-skill that interviews you and drafts a new skill as a ```skill code block with a one-click "Save as new skill" button
|
||||
- **Sidebar Info tab — Mage-Flow / subgraph workflow support** — the ComfyUI side panel's I tab now matches the Metadata tab's ability to inspect subgraph-based workflows (Flux.2 / Qwen-Image / Z-Image / Mage-Flow templates), including correct outer-widget-value injection and `TextEncodeMageFlowEdit` prompt handling
|
||||
- **Help tab — card-based redesign** — every help page now opens with a page title + "Open tab" jump button, and its content is split into topic-focused cards (heading + list) instead of one long flat bullet list per tab, matching the AI TOOL page's format introduced this release; no content was rewritten, only regrouped, and a few pre-existing gaps were fixed along the way (a duplicated Settings-pane bullet, and Tagger/Troubleshooting text that only lived in the i18n dictionary and never made it into the page markup)
|
||||
|
||||
### v0.3.78
|
||||
|
||||
- **Eagle integration — SVG support** — auto-save now handles SVG output alongside raster images: the standard `SaveSVGNode` (via `images`-key detection with correct `image/svg+xml` MIME re-tagging) and `comfyui-tosvg`'s `Save SVG String` node (via its `saved_svg`/`path` UI keys, including a fix for ComfyUI's execution engine splitting single-string UI values into per-character arrays); SVGs are sent to Eagle through `addFromPath` (resolved to a real on-disk path, restricted to ComfyUI's managed directories) instead of `addFromURL`, since `/view` forces `application/octet-stream` for SVG as an XSS guard; temp-only preview images (e.g. `SVG String Preview`) are excluded from both auto-save and the GenerateUI result display, which now shows the actual saved SVG file instead of a stale pre-vectorized preview
|
||||
|
||||
@@ -69,6 +69,7 @@ NODE_SETS_FILE = DATA_DIR / "node_sets.json"
|
||||
PROMPTS_FILE = DATA_DIR / "prompts.json"
|
||||
MODEL_METADATA_FILE = DATA_DIR / "model_metadata.json"
|
||||
WILDCARD_DIR = DATA_DIR / "wildcard"
|
||||
AI_SKILLS_DIR = DATA_DIR / "ai_skills"
|
||||
|
||||
# Tagger
|
||||
TAGGER_DB_FILE = DATA_DIR / "tagger.db"
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
"""AI skill (.md system prompt) file management API routes."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from ..config import AI_SKILLS_DIR
|
||||
from ..services.skill_service import SkillService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_service = SkillService(AI_SKILLS_DIR)
|
||||
|
||||
|
||||
def setup_routes(app: web.Application):
|
||||
app.router.add_get("/api/wfm/skills", handle_list)
|
||||
app.router.add_get("/api/wfm/skills/content", handle_get_content)
|
||||
app.router.add_post("/api/wfm/skills/save", handle_save)
|
||||
app.router.add_post("/api/wfm/skills/delete", handle_delete)
|
||||
|
||||
|
||||
async def handle_list(request: web.Request) -> web.Response:
|
||||
try:
|
||||
result = await asyncio.to_thread(_service.list_skills)
|
||||
return web.json_response(result)
|
||||
except Exception as e:
|
||||
logger.error("Error listing skills: %s", e)
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
|
||||
async def handle_get_content(request: web.Request) -> web.Response:
|
||||
filename = request.rel_url.query.get("filename", "")
|
||||
try:
|
||||
content = await asyncio.to_thread(_service.get_content, filename)
|
||||
return web.json_response({"content": content})
|
||||
except FileNotFoundError:
|
||||
return web.json_response({"error": "not found"}, status=404)
|
||||
except Exception as e:
|
||||
logger.error("Error getting skill content: %s", e)
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
|
||||
async def handle_save(request: web.Request) -> web.Response:
|
||||
try:
|
||||
body = await request.json()
|
||||
filename = body.get("filename", "")
|
||||
content = body.get("content", "")
|
||||
result = await asyncio.to_thread(_service.save_file, filename, content)
|
||||
return web.json_response({"status": "ok", "file": result})
|
||||
except Exception as e:
|
||||
logger.error("Error saving skill: %s", e)
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
|
||||
async def handle_delete(request: web.Request) -> web.Response:
|
||||
try:
|
||||
body = await request.json()
|
||||
filename = body.get("filename", "")
|
||||
await asyncio.to_thread(_service.delete_file, filename)
|
||||
return web.json_response({"status": "ok"})
|
||||
except Exception as e:
|
||||
logger.error("Error deleting skill: %s", e)
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
@@ -0,0 +1,228 @@
|
||||
"""AI skill (.md system prompt) file management service."""
|
||||
|
||||
import logging
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ALLOWED_EXTS = {"md"}
|
||||
|
||||
DEFAULT_SKILL_FILENAME = "svg-icon.md"
|
||||
|
||||
DEFAULT_SKILL_CONTENT = """---
|
||||
name: SVG Icon Generator
|
||||
description: Produces clean, minimal, valid SVG icons and simple illustrations.
|
||||
---
|
||||
|
||||
You are an expert SVG icon and illustration designer. When the user asks for an icon, logo, pictogram, diagram, character, animal, or any vector graphic, respond with valid, clean SVG markup.
|
||||
|
||||
## Composition process (do this before writing markup - skipping this step is the main cause of unrecognizable output)
|
||||
- Break the subject down into 3-8 distinct parts that make it recognizable (e.g. a bird: body, head, beak, eye, wing, tail; a house: wall, roof, door, window). Drop parts that are not essential.
|
||||
- Decide each part's approximate position, size, and draw order within the viewBox (elements listed later in the markup are drawn on top).
|
||||
- Pick the simplest shape that captures each part's silhouette: `<circle>`/`<ellipse>` for round parts, `<rect>` for rectangular parts, `<polygon>` for angular parts (beaks, ears, leaves, roofs). For a rounded organic outline that no primitive can approximate well (a body silhouette, a wing, a leaf edge), use a short `<path>` with 2-4 smooth curve commands (`Q` or `C`) instead of a plain circle - a few deliberate curve points beat a distorted primitive.
|
||||
- Never fall back to drawing an unrelated blob plus a few disconnected decorative lines when a subject is hard to render - always decompose it into the named parts above instead, even if each part is crude.
|
||||
|
||||
## Output rules
|
||||
- Always wrap the SVG markup in a fenced code block labeled `svg`, e.g. ```svg ... ```.
|
||||
- Keep any explanation brief and place it OUTSIDE the code block, never mixed inside the SVG.
|
||||
- Output exactly one `<svg>...</svg>` element per code block.
|
||||
|
||||
## Structure rules
|
||||
- Always include a `viewBox` attribute (e.g. `viewBox="0 0 24 24"`). Do not hardcode large pixel `width`/`height`; if you set them, make them match the viewBox aspect ratio.
|
||||
- Use a small, round coordinate system (0-24 or 0-100).
|
||||
- Round coordinate values to at most 2 decimal places.
|
||||
- Keep every `<path>` short (at most about 6 commands) - long, intricate paths are where quality breaks down most often.
|
||||
- Group related elements with `<g>` when it clarifies structure, but do not over-nest.
|
||||
|
||||
## Color rules
|
||||
- For single-color icons, use `fill="currentColor"` (or `stroke="currentColor"` for line icons) so the icon inherits the surrounding text color.
|
||||
- For multi-color illustrations, use explicit hex colors (e.g. `#4A90D9`) - never named CSS colors like `"blue"`.
|
||||
- Avoid gradients, filters, and blend modes unless the user explicitly asks for a glossy/gradient style - they rarely render correctly from generated coordinates.
|
||||
|
||||
## Accessibility
|
||||
- Include a `<title>` element as the first child of `<svg>` describing what the icon depicts.
|
||||
|
||||
## Security (mandatory - do not violate)
|
||||
- Never include `<script>`, `<foreignObject>`, event handler attributes (`onload`, `onclick`, etc.), or `<a>`/`href` links to external URLs.
|
||||
- Never reference external resources (no `xlink:href` to remote URLs, no `<image>` with external `src`).
|
||||
|
||||
## Example 1 - simple geometric icon (single color, primitives only)
|
||||
|
||||
Request: "Sun icon"
|
||||
|
||||
Parts: disc (circle), 8 rays (lines).
|
||||
|
||||
```svg
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>Sun</title>
|
||||
<circle cx="12" cy="12" r="5" fill="currentColor"/>
|
||||
<g stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||
<line x1="12" y1="1" x2="12" y2="4"/>
|
||||
<line x1="12" y1="20" x2="12" y2="23"/>
|
||||
<line x1="4.22" y1="4.22" x2="6.34" y2="6.34"/>
|
||||
<line x1="17.66" y1="17.66" x2="19.78" y2="19.78"/>
|
||||
<line x1="1" y1="12" x2="4" y2="12"/>
|
||||
<line x1="20" y1="12" x2="23" y2="12"/>
|
||||
<line x1="4.22" y1="19.78" x2="6.34" y2="17.66"/>
|
||||
<line x1="17.66" y1="6.34" x2="19.78" y2="4.22"/>
|
||||
</g>
|
||||
</svg>
|
||||
```
|
||||
|
||||
## Example 2 - organic subject (multi-color, decomposed into parts)
|
||||
|
||||
Request: "Bird icon"
|
||||
|
||||
Parts: body (rounded outline via a short curved path), head (circle), beak (small triangle), eye (dot), wing (small curved path) - each drawn as its own element, layered head/beak/eye on top of the body.
|
||||
|
||||
```svg
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>Bird</title>
|
||||
<path d="M4 16 Q4 10 11 9 Q18 8 20 13 Q20 17 14 18 Q7 19 4 16 Z" fill="#4A90D9"/>
|
||||
<path d="M8 14 Q5 12 3 14 Q6 16 9 16 Z" fill="#3576B8"/>
|
||||
<circle cx="17" cy="10" r="3.2" fill="#4A90D9"/>
|
||||
<polygon points="20,10 23,9.3 20,11.4" fill="#F2B807"/>
|
||||
<circle cx="18" cy="9" r="0.6" fill="#1A1A1A"/>
|
||||
</svg>
|
||||
```
|
||||
|
||||
Follow this pattern for every request: list the parts first, represent each with the simplest adequate shape, then assemble - never skip straight to unstructured shapes.
|
||||
"""
|
||||
|
||||
SKILL_CREATOR_FILENAME = "skill-creator.md"
|
||||
|
||||
SKILL_CREATOR_CONTENT = """---
|
||||
name: Skill Creator
|
||||
description: Interactively interviews you and drafts a new WFS chat skill (.md system prompt) ready to save.
|
||||
---
|
||||
|
||||
You are a skill-design assistant embedded in ComfyUI Workflow Studio's AI TOOL Chat pane. Your job is to interactively help the user design a NEW "skill" - a markdown file with frontmatter (`name`, `description`) followed by system-prompt instructions. Once saved, a skill can be selected from a dropdown and is silently prepended to future chat messages as a system prompt.
|
||||
|
||||
## How to conduct the interview
|
||||
- Ask ONE focused question at a time, never a bulleted list of questions. Wait for the user's answer before asking the next one.
|
||||
- Cover, in this order, only what is not already clear from context:
|
||||
1. What task or domain should the skill specialize in (what should the model do differently once this skill is active)?
|
||||
2. What output format is expected - plain prose, a specific fenced code block language, a fixed structure, a length limit?
|
||||
3. Any hard constraints or things to avoid (tone, forbidden content, security concerns, things past attempts got wrong)?
|
||||
4. Would a short worked example help smaller/local models follow the pattern more reliably? If yes, ask what a good example input/output looks like, or offer to draft one.
|
||||
- Keep it short - 3 to 5 questions is usually enough. If the user says "just generate it", "that's enough", or similar at any point, stop asking and produce the best skill you can from what you already have.
|
||||
|
||||
## Output format for the finished skill
|
||||
- Once you have enough information, output the complete skill file in a single fenced code block labeled `skill` (```skill ... ```). Never mix prose inside that block - explanation goes outside it.
|
||||
- The block must start with frontmatter exactly in this shape:
|
||||
---
|
||||
name: <short display name, shown in the skill dropdown>
|
||||
description: <one sentence, shown as a tooltip in the dropdown>
|
||||
---
|
||||
- After the frontmatter, write clear, imperative system-prompt instructions for the target skill: what role the model should take, concrete output-format rules, hard constraints, and - if useful - one short worked example following a "request -> ideal output" shape.
|
||||
- After the code block, tell the user in one line that they can click the "Save as new skill" button below the message to add it, or copy the block manually into a new skill.
|
||||
"""
|
||||
|
||||
_FRONTMATTER_RE = re.compile(r"^---\r?\n(.*?)\r?\n---\r?\n?", re.DOTALL)
|
||||
|
||||
|
||||
def _parse_frontmatter(content: str) -> dict:
|
||||
"""Extract a shallow `key: value` frontmatter block, if present."""
|
||||
m = _FRONTMATTER_RE.match(content)
|
||||
if not m:
|
||||
return {}
|
||||
fields: dict = {}
|
||||
for line in m.group(1).splitlines():
|
||||
if ":" not in line:
|
||||
continue
|
||||
key, _, value = line.partition(":")
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
if key:
|
||||
fields[key] = value
|
||||
return fields
|
||||
|
||||
|
||||
class SkillService:
|
||||
def __init__(self, skills_dir: Path):
|
||||
self.skills_dir = skills_dir
|
||||
self.skills_dir.mkdir(parents=True, exist_ok=True)
|
||||
self._seed_default_skill()
|
||||
|
||||
def _seed_default_skill(self) -> None:
|
||||
"""Seed a starter skill file the first time the directory is empty."""
|
||||
try:
|
||||
has_any = any(
|
||||
p.is_file() and p.suffix.lstrip(".").lower() in ALLOWED_EXTS
|
||||
for p in self.skills_dir.iterdir()
|
||||
)
|
||||
except FileNotFoundError:
|
||||
has_any = False
|
||||
if not has_any:
|
||||
(self.skills_dir / DEFAULT_SKILL_FILENAME).write_text(
|
||||
DEFAULT_SKILL_CONTENT, encoding="utf-8"
|
||||
)
|
||||
(self.skills_dir / SKILL_CREATOR_FILENAME).write_text(
|
||||
SKILL_CREATOR_CONTENT, encoding="utf-8"
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# File CRUD
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def list_skills(self) -> list[dict]:
|
||||
"""Return sorted list of skill files with parsed frontmatter metadata."""
|
||||
if not self.skills_dir.exists():
|
||||
return []
|
||||
files = []
|
||||
for path in sorted(self.skills_dir.glob("*")):
|
||||
if not (path.is_file() and path.suffix.lstrip(".").lower() in ALLOWED_EXTS):
|
||||
continue
|
||||
try:
|
||||
content = path.read_text(encoding="utf-8")
|
||||
except Exception:
|
||||
content = ""
|
||||
meta = _parse_frontmatter(content)
|
||||
files.append({
|
||||
"filename": path.name,
|
||||
"name": meta.get("name") or path.stem,
|
||||
"description": meta.get("description", ""),
|
||||
"size": path.stat().st_size,
|
||||
})
|
||||
return files
|
||||
|
||||
def get_content(self, filename: str) -> str:
|
||||
path = self._safe_path(filename)
|
||||
if path is None:
|
||||
raise ValueError(f"Invalid filename: {filename}")
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError(f"File not found: {filename}")
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
def save_file(self, filename: str, content: str) -> dict:
|
||||
path = self._safe_path(filename)
|
||||
if path is None:
|
||||
raise ValueError(f"Invalid filename: {filename}")
|
||||
path.write_text(content, encoding="utf-8")
|
||||
meta = _parse_frontmatter(content)
|
||||
return {
|
||||
"filename": path.name,
|
||||
"name": meta.get("name") or path.stem,
|
||||
"description": meta.get("description", ""),
|
||||
"size": path.stat().st_size,
|
||||
}
|
||||
|
||||
def delete_file(self, filename: str) -> None:
|
||||
path = self._safe_path(filename)
|
||||
if path is None:
|
||||
raise ValueError(f"Invalid filename: {filename}")
|
||||
if path.is_file():
|
||||
path.unlink()
|
||||
|
||||
def _safe_path(self, filename: str) -> Path | None:
|
||||
if not filename or not filename.strip():
|
||||
return None
|
||||
# Skills are a flat directory - reject any path separator (no subfolders).
|
||||
name = filename.strip()
|
||||
if "/" in name or "\\" in name or name in (".", ".."):
|
||||
return None
|
||||
ext = Path(name).suffix.lstrip(".").lower()
|
||||
if ext not in ALLOWED_EXTS:
|
||||
return None
|
||||
return self.skills_dir / name
|
||||
@@ -61,7 +61,7 @@ class WorkflowStudio:
|
||||
app.router.add_get("/wfm", serve_index_page)
|
||||
|
||||
# API routes
|
||||
from .routes import workflow_routes, settings_routes, ollama_routes, eagle_routes, nodes_routes, prompts_routes, models_routes, gallery_routes, wildcard_routes, tagger_routes, gmic_routes
|
||||
from .routes import workflow_routes, settings_routes, ollama_routes, eagle_routes, nodes_routes, prompts_routes, models_routes, gallery_routes, wildcard_routes, tagger_routes, gmic_routes, skill_routes
|
||||
|
||||
workflow_routes.setup_routes(app)
|
||||
settings_routes.setup_routes(app)
|
||||
@@ -74,5 +74,6 @@ class WorkflowStudio:
|
||||
wildcard_routes.setup_routes(app)
|
||||
tagger_routes.setup_routes(app)
|
||||
gmic_routes.setup_routes(app)
|
||||
skill_routes.setup_routes(app)
|
||||
|
||||
logger.info("Workflow Studio: Routes registered successfully")
|
||||
|
||||
+1
-1
@@ -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.3.78"
|
||||
version = "0.3.80"
|
||||
license = "MIT"
|
||||
requires-python = ">= 3.10"
|
||||
dependencies = ["jinja2"]
|
||||
|
||||
@@ -3182,6 +3182,21 @@ body {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.wfm-help-article-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.wfm-help-article-header h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--wfm-text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wfm-help-card {
|
||||
background: var(--wfm-surface);
|
||||
border: 1px solid var(--wfm-border);
|
||||
@@ -5714,3 +5729,42 @@ body {
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.wfm-ai-chat-skill-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 8px 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.wfm-ai-chat-skill-select {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wfm-ai-skill-panel {
|
||||
margin: 6px 8px 0;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--wfm-border);
|
||||
border-radius: var(--wfm-radius);
|
||||
background: var(--wfm-surface);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.wfm-ai-skill-panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.wfm-ai-skill-panel-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--wfm-text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
+253
-2
@@ -5,7 +5,7 @@
|
||||
|
||||
import { showToast } from "./app.js";
|
||||
import { t } from "./i18n.js";
|
||||
import { readJsonStorage, getAiBackendDefaultUrl } from "./util.js";
|
||||
import { readJsonStorage, getAiBackendDefaultUrl, escapeHtml } from "./util.js";
|
||||
import { comfyUI } from "./comfyui-client.js";
|
||||
import { comfyEditor } from "./comfyui-editor.js";
|
||||
import { comfyWorkflow } from "./comfyui-workflow.js";
|
||||
@@ -583,6 +583,245 @@ function _appendSvgPreview(container, svgCode) {
|
||||
container.appendChild(wrapper);
|
||||
}
|
||||
|
||||
// アシスタント応答から ```skill フェンスを抽出する(Skill Creator用スキルが出力する完成品ブロック)
|
||||
function _extractSkillBlock(text) {
|
||||
if (!text) return null;
|
||||
const m = text.match(/```skill\s*([\s\S]*?)```/i);
|
||||
return m ? m[1].trim() : null;
|
||||
}
|
||||
|
||||
// frontmatterの name から保存用ファイル名を推測する(未設定時は my-skill.md)
|
||||
function _slugifySkillName(content) {
|
||||
const m = content.match(/^---\r?\n[\s\S]*?name:\s*(.+?)\r?\n/);
|
||||
const name = (m ? m[1] : "").trim();
|
||||
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
||||
return `${slug || "my-skill"}.md`;
|
||||
}
|
||||
|
||||
// 応答内にスキル定義ブロックがあれば「スキルとして保存」ボタンを追加する。
|
||||
// クリックすると管理パネルを開き、新規スキルとしてエディタに内容を流し込む(保存前にレビュー可能)。
|
||||
function _appendSkillSaveButton(container, skillContent) {
|
||||
const btn = document.createElement("button");
|
||||
btn.className = "wfm-btn wfm-btn-sm";
|
||||
btn.textContent = t("aiChatSaveAsSkill");
|
||||
btn.addEventListener("click", () => {
|
||||
const panel = document.getElementById("wfm-ai-skill-panel");
|
||||
if (panel) panel.style.display = "";
|
||||
skillOpenEditor(null, skillContent);
|
||||
const nameInput = document.getElementById("wfm-ai-skill-editor-filename");
|
||||
if (nameInput) nameInput.value = _slugifySkillName(skillContent);
|
||||
});
|
||||
container.appendChild(btn);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// AI skills (Chat system prompt library)
|
||||
// ============================================
|
||||
|
||||
async function skillFetchFiles() {
|
||||
try {
|
||||
const res = await fetch("/api/wfm/skills");
|
||||
return res.ok ? await res.json() : [];
|
||||
} catch { return []; }
|
||||
}
|
||||
|
||||
async function skillFetchContent(filename) {
|
||||
try {
|
||||
const res = await fetch(`/api/wfm/skills/content?filename=${encodeURIComponent(filename)}`);
|
||||
const data = await res.json();
|
||||
return data.content ?? null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
async function skillSaveFile(filename, content) {
|
||||
try {
|
||||
const res = await fetch("/api/wfm/skills/save", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ filename, content }),
|
||||
});
|
||||
const data = await res.json();
|
||||
return data.status === "ok" ? data.file : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
async function skillDeleteFile(filename) {
|
||||
try {
|
||||
await fetch("/api/wfm/skills/delete", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ filename }),
|
||||
});
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// system prompt として送る前に frontmatter (---...---) を取り除く
|
||||
function _stripSkillFrontmatter(content) {
|
||||
const m = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
|
||||
return (m ? content.slice(m[0].length) : content).trim();
|
||||
}
|
||||
|
||||
let skillFiles = [];
|
||||
let skillEditingFilename = null; // null = new file
|
||||
let _activeSkillCache = null; // { filename, content } キャッシュ(送信毎の再フェッチを避ける)
|
||||
|
||||
function skillRenderList() {
|
||||
const list = document.getElementById("wfm-ai-skill-list");
|
||||
if (!list) return;
|
||||
list.innerHTML = "";
|
||||
|
||||
if (skillFiles.length === 0) {
|
||||
list.innerHTML = `<div class="wfm-pm-empty" style="font-size:12px;">${t("aiSkillNoFiles")}</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
for (const f of skillFiles) {
|
||||
const item = document.createElement("div");
|
||||
item.className = "wfm-wc-file-item";
|
||||
item.innerHTML = `
|
||||
<div class="wfm-wc-file-item-info">
|
||||
<span class="wfm-wc-file-name" title="${escapeHtml(f.description || "")}">${escapeHtml(f.name)}</span>
|
||||
<span class="wfm-wc-file-ext">.md</span>
|
||||
</div>
|
||||
<div class="wfm-wc-file-item-actions">
|
||||
<button class="wfm-pm-action-btn wfm-ai-skill-edit-btn" title="Edit">✎</button>
|
||||
</div>
|
||||
`;
|
||||
item.querySelector(".wfm-ai-skill-edit-btn").addEventListener("click", async (e) => {
|
||||
e.stopPropagation();
|
||||
const content = await skillFetchContent(f.filename);
|
||||
skillOpenEditor(f.filename, content ?? "");
|
||||
});
|
||||
list.appendChild(item);
|
||||
}
|
||||
}
|
||||
|
||||
function skillOpenEditor(filename, content) {
|
||||
skillEditingFilename = filename || null;
|
||||
|
||||
const editor = document.getElementById("wfm-ai-skill-editor");
|
||||
const nameInput = document.getElementById("wfm-ai-skill-editor-filename");
|
||||
const contentTA = document.getElementById("wfm-ai-skill-editor-content");
|
||||
const deleteBtn = document.getElementById("wfm-ai-skill-editor-delete-btn");
|
||||
|
||||
if (!editor) return;
|
||||
nameInput.value = filename || "";
|
||||
contentTA.value = content || "";
|
||||
deleteBtn.style.display = filename ? "" : "none";
|
||||
editor.style.display = "";
|
||||
nameInput.focus();
|
||||
}
|
||||
|
||||
function skillCloseEditor() {
|
||||
skillEditingFilename = null;
|
||||
const editor = document.getElementById("wfm-ai-skill-editor");
|
||||
if (editor) editor.style.display = "none";
|
||||
}
|
||||
|
||||
function skillPopulateSelect() {
|
||||
const sel = document.getElementById("wfm-ai-chat-skill-select");
|
||||
if (!sel) return;
|
||||
const settings = loadAiSettings();
|
||||
const wanted = sel.value || settings.activeSkillFilename || "";
|
||||
|
||||
sel.innerHTML = `<option value="">-- ${t("aiSkillNone")} --</option>` +
|
||||
skillFiles.map((f) => `<option value="${escapeHtml(f.filename)}" title="${escapeHtml(f.description || "")}">${escapeHtml(f.name)}</option>`).join("");
|
||||
|
||||
sel.value = skillFiles.some((f) => f.filename === wanted) ? wanted : "";
|
||||
_activeSkillCache = null;
|
||||
}
|
||||
|
||||
async function skillRefreshFiles() {
|
||||
skillFiles = await skillFetchFiles();
|
||||
skillRenderList();
|
||||
skillPopulateSelect();
|
||||
}
|
||||
|
||||
// 選択中スキルの本文を取得(system prompt注入用、選択が変わるまでキャッシュ)
|
||||
async function _getActiveSkillSystemPrompt() {
|
||||
const sel = document.getElementById("wfm-ai-chat-skill-select");
|
||||
const filename = sel?.value || "";
|
||||
if (!filename) return null;
|
||||
if (_activeSkillCache?.filename === filename) return _activeSkillCache.content;
|
||||
const raw = await skillFetchContent(filename);
|
||||
const content = _stripSkillFrontmatter(raw || "");
|
||||
_activeSkillCache = { filename, content };
|
||||
return content;
|
||||
}
|
||||
|
||||
function initSkillManager() {
|
||||
const selectEl = document.getElementById("wfm-ai-chat-skill-select");
|
||||
const manageBtn = document.getElementById("wfm-ai-chat-skill-manage-btn");
|
||||
const panel = document.getElementById("wfm-ai-skill-panel");
|
||||
const closeBtn = document.getElementById("wfm-ai-skill-close-btn");
|
||||
const newBtn = document.getElementById("wfm-ai-skill-new-btn");
|
||||
const saveBtn = document.getElementById("wfm-ai-skill-editor-save-btn");
|
||||
const deleteBtn = document.getElementById("wfm-ai-skill-editor-delete-btn");
|
||||
const cancelBtn = document.getElementById("wfm-ai-skill-editor-cancel-btn");
|
||||
|
||||
if (!selectEl) return;
|
||||
|
||||
skillRefreshFiles();
|
||||
|
||||
selectEl.addEventListener("change", () => {
|
||||
saveAiSettings({ activeSkillFilename: selectEl.value });
|
||||
_activeSkillCache = null;
|
||||
});
|
||||
|
||||
manageBtn?.addEventListener("click", () => {
|
||||
if (!panel) return;
|
||||
const show = panel.style.display === "none";
|
||||
panel.style.display = show ? "" : "none";
|
||||
if (show) skillRefreshFiles();
|
||||
});
|
||||
|
||||
closeBtn?.addEventListener("click", () => {
|
||||
if (panel) panel.style.display = "none";
|
||||
});
|
||||
|
||||
newBtn?.addEventListener("click", () => {
|
||||
skillOpenEditor(null, "---\nname: \ndescription: \n---\n\n");
|
||||
});
|
||||
|
||||
saveBtn?.addEventListener("click", async () => {
|
||||
const nameInput = document.getElementById("wfm-ai-skill-editor-filename");
|
||||
const contentTA = document.getElementById("wfm-ai-skill-editor-content");
|
||||
let filename = (nameInput?.value || "").trim();
|
||||
const content = contentTA?.value || "";
|
||||
|
||||
if (!filename) { showToast(t("pleaseEnterFilename"), "error"); return; }
|
||||
if (!/\.md$/i.test(filename)) filename += ".md";
|
||||
if (!/^[\w\-. ]+\.md$/i.test(filename)) { showToast(t("invalidPathFormat"), "error"); return; }
|
||||
|
||||
const saved = await skillSaveFile(filename, content);
|
||||
if (saved) {
|
||||
showToast(t("savedAs", filename), "success");
|
||||
skillCloseEditor();
|
||||
await skillRefreshFiles();
|
||||
selectEl.value = filename;
|
||||
saveAiSettings({ activeSkillFilename: filename });
|
||||
_activeSkillCache = null;
|
||||
} else {
|
||||
showToast(t("saveFailed"), "error");
|
||||
}
|
||||
});
|
||||
|
||||
deleteBtn?.addEventListener("click", async () => {
|
||||
if (!skillEditingFilename) return;
|
||||
if (!confirm(`Delete "${skillEditingFilename}"?`)) return;
|
||||
await skillDeleteFile(skillEditingFilename);
|
||||
showToast(t("deletedName", skillEditingFilename), "success");
|
||||
if (selectEl.value === skillEditingFilename) {
|
||||
saveAiSettings({ activeSkillFilename: "" });
|
||||
_activeSkillCache = null;
|
||||
}
|
||||
skillCloseEditor();
|
||||
await skillRefreshFiles();
|
||||
});
|
||||
|
||||
cancelBtn?.addEventListener("click", () => skillCloseEditor());
|
||||
}
|
||||
|
||||
function appendChatBubble(messagesEl, role, content, opts = {}) {
|
||||
const div = document.createElement("div");
|
||||
div.className = `wfm-ai-chat-msg wfm-ai-chat-msg-${role}`;
|
||||
@@ -602,6 +841,8 @@ function appendChatBubble(messagesEl, role, content, opts = {}) {
|
||||
if (role === "assistant" && content) {
|
||||
const svgCode = _extractSvgCode(content);
|
||||
if (svgCode) _appendSvgPreview(div, svgCode);
|
||||
const skillBlock = _extractSkillBlock(content);
|
||||
if (skillBlock) _appendSkillSaveButton(div, skillBlock);
|
||||
}
|
||||
messagesEl.appendChild(div);
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
@@ -630,6 +871,8 @@ function initChatTab() {
|
||||
_clearToolsImage();
|
||||
});
|
||||
|
||||
initSkillManager();
|
||||
|
||||
let chatHistory = [];
|
||||
|
||||
async function sendMessage() {
|
||||
@@ -662,7 +905,11 @@ function initChatTab() {
|
||||
|
||||
try {
|
||||
const tools = imgGenToggle?.checked !== false ? IMAGE_GEN_TOOLS : undefined;
|
||||
const reply = await callChat(url, backend, model, chatHistory, tools);
|
||||
const skillPrompt = await _getActiveSkillSystemPrompt();
|
||||
const messagesToSend = skillPrompt
|
||||
? [{ role: "system", content: skillPrompt }, ...chatHistory]
|
||||
: chatHistory;
|
||||
const reply = await callChat(url, backend, model, messagesToSend, tools);
|
||||
const toolCall = reply.toolCalls?.find(
|
||||
(tc) => (tc.function?.name || tc.name) === "generate_image"
|
||||
);
|
||||
@@ -878,3 +1125,7 @@ export function initAiTab() {
|
||||
initVlmTab();
|
||||
initSettingsTab();
|
||||
}
|
||||
|
||||
// gallery-tab.js の外部(Comic Creator)向けブリッジ(半自動マンガ作成の画像プロンプト
|
||||
// 下書き機能)が、設定タブと同じAI接続設定・LLM呼び出しロジックを再利用するためのexport。
|
||||
export { callLLM, loadAiSettings, isValidBackendUrl };
|
||||
|
||||
+90
-1
@@ -320,6 +320,19 @@ function applyI18nToHtml() {
|
||||
if (aiChatGenI2ITitle) aiChatGenI2ITitle.textContent = t("aiSettingsChatI2IGenTitle");
|
||||
const aiChatGenI2IDedicatedLabel = document.getElementById("wfm-ai-chatgen-i2i-dedicated-label");
|
||||
if (aiChatGenI2IDedicatedLabel) aiChatGenI2IDedicatedLabel.textContent = t("aiSettingsChatGenDedicated");
|
||||
const aiSkillSel = document.getElementById("wfm-ai-chat-skill-select");
|
||||
if (aiSkillSel) {
|
||||
const firstOpt = aiSkillSel.querySelector("option[value='']");
|
||||
if (firstOpt) firstOpt.textContent = `-- ${t("aiSkillNone")} --`;
|
||||
}
|
||||
const aiSkillManageBtn = document.getElementById("wfm-ai-chat-skill-manage-btn");
|
||||
if (aiSkillManageBtn) aiSkillManageBtn.title = t("aiSkillManageBtnTitle");
|
||||
const aiSkillPanelTitle = document.querySelector(".wfm-ai-skill-panel-title");
|
||||
if (aiSkillPanelTitle) aiSkillPanelTitle.textContent = t("aiSkillPanelTitle");
|
||||
const aiSkillNewBtn = document.getElementById("wfm-ai-skill-new-btn");
|
||||
if (aiSkillNewBtn) aiSkillNewBtn.textContent = t("aiSkillNewBtn");
|
||||
const aiSkillContentTA = document.getElementById("wfm-ai-skill-editor-content");
|
||||
if (aiSkillContentTA) aiSkillContentTA.placeholder = t("aiSkillContentPlaceholder");
|
||||
|
||||
// Group panel labels
|
||||
const propTitle = document.querySelector("#wfm-side-tab-group .wfm-group-section-title");
|
||||
@@ -432,8 +445,18 @@ function applyI18nToHtml() {
|
||||
"wfm-help-sidepanel-17": "helpSidepanel17",
|
||||
"wfm-help-ai-title": "helpAiTitle",
|
||||
"wfm-help-ai-1": "helpAi1", "wfm-help-ai-2": "helpAi2", "wfm-help-ai-3": "helpAi3",
|
||||
"wfm-help-ai-4": "helpAi4", "wfm-help-ai-5": "helpAi5", "wfm-help-ai-6": "helpAi6",
|
||||
"wfm-help-ai-4": "helpAi4", "wfm-help-ai-6": "helpAi6",
|
||||
"wfm-help-ai-7": "helpAi7",
|
||||
"wfm-help-ai-8": "helpAi8",
|
||||
"wfm-help-ai-9": "helpAi9",
|
||||
"wfm-help-ai-card-layout-title": "helpAiCardLayout",
|
||||
"wfm-help-ai-card-translate-title": "helpAiCardTranslate",
|
||||
"wfm-help-ai-card-chat-title": "helpAiCardChat",
|
||||
"wfm-help-ai-card-svg-title": "helpAiCardSvg",
|
||||
"wfm-help-ai-card-skills-title": "helpAiCardSkills",
|
||||
"wfm-help-ai-card-skilltips-title": "helpAiCardSkillTips",
|
||||
"wfm-help-ai-card-tools-title": "helpAiCardTools",
|
||||
"wfm-help-ai-card-settings-title": "helpAiCardSettings",
|
||||
"wfm-help-imageedit-title": "helpImageEditTitle",
|
||||
"wfm-help-imageedit-tools-title": "helpImageEditToolsTitle",
|
||||
"wfm-help-imageedit-layer-title": "helpImageEditLayerTitle",
|
||||
@@ -499,6 +522,60 @@ function applyI18nToHtml() {
|
||||
"wfm-help-github-desc": "helpGithubDesc",
|
||||
"wfm-help-kofi-desc": "helpKofiDesc",
|
||||
"wfm-help-thanks": "helpThanks",
|
||||
// -- Help card headings (card-based redesign) --
|
||||
"wfm-help-about-card-getting-started-title": "helpAboutCardGettingStarted",
|
||||
"wfm-help-wf-title": "helpWfTitle",
|
||||
"wfm-help-wf-card-browse-title": "helpWfCardBrowse",
|
||||
"wfm-help-wf-card-groups-title": "helpWfCardGroups",
|
||||
"wfm-help-wf-card-panel-title": "helpWfCardPanel",
|
||||
"wfm-help-gen-title": "helpGenTitle",
|
||||
"wfm-help-gen-card-connect-title": "helpGenCardConnect",
|
||||
"wfm-help-gen-card-tabs-title": "helpGenCardTabs",
|
||||
"wfm-help-gen-card-seed-title": "helpGenCardSeed",
|
||||
"wfm-help-gen-card-batchsetup-title": "helpGenCardBatchSetup",
|
||||
"wfm-help-gen-card-batchtypes-title": "helpGenCardBatchTypes",
|
||||
"wfm-help-prompt-title": "helpPromptTitle",
|
||||
"wfm-help-prompt-card-ai-title": "helpPromptCardAi",
|
||||
"wfm-help-prompt-card-presets-title": "helpPromptCardPresets",
|
||||
"wfm-help-prompt-card-wildcards-title": "helpPromptCardWildcards",
|
||||
"wfm-help-metadata-card-overview-title": "helpMetadataCardOverview",
|
||||
"wfm-help-metadata-card-extracted-title": "helpMetadataCardExtracted",
|
||||
"wfm-help-metadata-card-formats-title": "helpMetadataCardFormats",
|
||||
"wfm-help-settings-title": "helpSettingsTitle",
|
||||
"wfm-help-settings-card-general-title": "helpSettingsCardGeneral",
|
||||
"wfm-help-settings-card-connections-title": "helpSettingsCardConnections",
|
||||
"wfm-help-settings-card-workflow-title": "helpSettingsCardWorkflow",
|
||||
"wfm-help-settings-card-editor-title": "helpSettingsCardEditor",
|
||||
"wfm-help-settings-card-misc-title": "helpSettingsCardMisc",
|
||||
"wfm-help-nodes-title": "helpNodesTitle",
|
||||
"wfm-help-nodes-card-browse-title": "helpNodesCardBrowse",
|
||||
"wfm-help-nodes-card-groups-title": "helpNodesCardGroups",
|
||||
"wfm-help-nodes-card-panel-title": "helpNodesCardPanel",
|
||||
"wfm-help-gallery-card-browse-title": "helpGalleryCardBrowse",
|
||||
"wfm-help-gallery-card-folder-title": "helpGalleryCardFolder",
|
||||
"wfm-help-gallery-card-detail-title": "helpGalleryCardDetail",
|
||||
"wfm-help-gallery-card-actions-title": "helpGalleryCardActions",
|
||||
"wfm-help-gallery-card-formats-title": "helpGalleryCardFormats",
|
||||
"wfm-help-models-title": "helpModelsTitle",
|
||||
"wfm-help-models-card-browse-title": "helpModelsCardBrowse",
|
||||
"wfm-help-models-card-civitai-title": "helpModelsCardCivitai",
|
||||
"wfm-help-models-card-apply-title": "helpModelsCardApply",
|
||||
"wfm-help-models-card-groups-title": "helpModelsCardGroups",
|
||||
"wfm-help-models-card-bulk-title": "helpModelsCardBulk",
|
||||
"wfm-help-sidepanel-card-tabs-title": "helpSidepanelCardTabs",
|
||||
"wfm-help-sidepanel-card-dnd-title": "helpSidepanelCardDnd",
|
||||
"wfm-help-sidepanel-card-info-title": "helpSidepanelCardInfo",
|
||||
"wfm-help-sidepanel-card-ai-title": "helpSidepanelCardAi",
|
||||
"wfm-help-sidepanel-card-nav-title": "helpSidepanelCardNav",
|
||||
"wfm-help-tagger-card-overview-title": "helpTaggerCardOverview",
|
||||
"wfm-help-tagger-card-single-title": "helpTaggerCardSingle",
|
||||
"wfm-help-tagger-card-sending-title": "helpTaggerCardSending",
|
||||
"wfm-help-tagger-card-batch-title": "helpTaggerCardBatch",
|
||||
"wfm-help-imageedit-card-overview-title": "helpImageEditCardOverview",
|
||||
"wfm-help-shortcuts-card-general-title": "helpShortcutsCardGeneral",
|
||||
"wfm-help-trouble-card-connection-title": "helpTroubleCardConnection",
|
||||
"wfm-help-trouble-card-workflow-title": "helpTroubleCardWorkflow",
|
||||
"wfm-help-trouble-card-tagger-title": "helpTroubleCardTagger",
|
||||
};
|
||||
for (const [id, key] of Object.entries(helpIdMap)) {
|
||||
const el = document.getElementById(id);
|
||||
@@ -511,6 +588,11 @@ function applyI18nToHtml() {
|
||||
helpSearch.placeholder = t("helpSearchPlaceholder");
|
||||
helpSearch.addEventListener("input", _onHelpSearch);
|
||||
}
|
||||
|
||||
// Jump-to-tab buttons
|
||||
document.querySelectorAll(".wfm-help-jump-btn").forEach((btn) => {
|
||||
btn.textContent = t("helpJumpToTab");
|
||||
});
|
||||
}
|
||||
|
||||
function _onHelpSearch(e) {
|
||||
@@ -604,6 +686,13 @@ function initHelpTab() {
|
||||
document.getElementById(`wfm-help-page-${page}`)?.classList.add("active");
|
||||
});
|
||||
});
|
||||
|
||||
// Jump-to-tab buttons: switch to the real feature tab this help page documents
|
||||
document.querySelectorAll(".wfm-help-jump-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
document.querySelector(`.wfm-tab[data-tab="${btn.dataset.jumpTab}"]`)?.click();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
@@ -7,10 +7,11 @@ import { showToast } from "./app.js";
|
||||
import { t } from "./i18n.js";
|
||||
import { loadFileIntoMetadataTab } from "./metadata-tab.js";
|
||||
import { loadWorkflowIntoEditor } from "./generate-tab.js";
|
||||
import { escapeHtml, setupSearchClearBtn } from "./util.js";
|
||||
import { escapeHtml, setupSearchClearBtn, getAiBackendDefaultUrl } from "./util.js";
|
||||
import { comfyWorkflow } from "./comfyui-workflow.js";
|
||||
import { comfyUI } from "./comfyui-client.js";
|
||||
import { comfyEditor } from "./comfyui-editor.js";
|
||||
import { callLLM, loadAiSettings, isValidBackendUrl } from "./ai-tab.js";
|
||||
|
||||
// ── 定数 ─────────────────────────────────────────────────────
|
||||
|
||||
@@ -126,6 +127,70 @@ window._wfmReceiveI2IRunRequest = async (imageBlob, params, workflowData, workfl
|
||||
}
|
||||
};
|
||||
|
||||
// ComfyUI Comic Creater からiframe越しにコマの設定(シーン・要素・セリフ等)を受け取り、
|
||||
// AIタブの設定タブと同じLLM接続設定(Ollama/LM Studio)で画像生成プロンプトの下書きを
|
||||
// 生成して返す(半自動マンガ作成の「L」ボタン連携用)。
|
||||
// Comic Creater側のスクリプトタブ「L」ボタンから
|
||||
// iframe.contentWindow._wfmReceiveLLMPromptRequest(context) として呼ばれる。
|
||||
// context: { scene, elements, dialogues, existingPrompt }(いずれも文字列、空文字可)
|
||||
window._wfmReceiveLLMPromptRequest = async (context) => {
|
||||
try {
|
||||
const settings = loadAiSettings();
|
||||
const { backend = "ollama", backendUrl, model } = settings;
|
||||
const url = backendUrl || getAiBackendDefaultUrl(backend);
|
||||
if (!isValidBackendUrl(url)) return { ok: false, message: "AI settings: invalid backend URL (configure it in the Settings tab)" };
|
||||
if (!model) return { ok: false, message: "AI settings: no model selected (configure it in the Settings tab)" };
|
||||
|
||||
const { scene = "", elements = "", dialogues = "", existingPrompt = "" } = context || {};
|
||||
const prompt = [
|
||||
"Create a concise Stable Diffusion image generation prompt (English, comma-separated tags/phrases describing scene, composition, character appearance and action) for one manga panel, based on the following context.",
|
||||
"Output only the prompt text, nothing else.",
|
||||
scene ? `Scene: ${scene}` : null,
|
||||
elements ? `Characters/elements: ${elements}` : null,
|
||||
dialogues ? `Dialogue: ${dialogues}` : null,
|
||||
existingPrompt ? `Current draft prompt (revise/improve it): ${existingPrompt}` : null,
|
||||
].filter(Boolean).join("\n");
|
||||
|
||||
const result = await callLLM(url, backend, model, prompt);
|
||||
const text = (result || "").trim();
|
||||
if (!text) return { ok: false, message: "LLM returned an empty response" };
|
||||
return { ok: true, text };
|
||||
} catch (e) {
|
||||
return { ok: false, message: e.message };
|
||||
}
|
||||
};
|
||||
|
||||
// ComfyUI Comic Creater からiframe越しに画像プロンプト・サイズを受け取り、Generate UIで
|
||||
// テキストから画像を生成して結果URLを返す(半自動マンガ作成のコマ単位バッチ画像生成用)。
|
||||
// Comic Creater側のスクリプトタブ「画像を一括生成」から
|
||||
// iframe.contentWindow._wfmReceiveGenerateRequest(prompt, width, height) として呼ばれる。
|
||||
// I2I/Inpaintと異なり画像アップロードは行わず(txt2img)、専用ワークフロー選択には未対応で
|
||||
// 常にGenerate UIに現在ロード中のワークフローをそのまま使う。
|
||||
window._wfmReceiveGenerateRequest = async (prompt, width, height) => {
|
||||
try {
|
||||
if (!window._wfmGenerateTab?.generate) return { ok: false, message: "Generate UI is not ready yet" };
|
||||
const analysis = comfyUI.currentAnalysis;
|
||||
if (!comfyUI.currentWorkflow || !analysis) return { ok: false, message: "No workflow loaded in Generate UI" };
|
||||
|
||||
const workflow = JSON.parse(JSON.stringify(comfyUI.currentWorkflow));
|
||||
const latent = analysis.latent_nodes?.[0];
|
||||
if (latent && width && height && workflow[latent.id]) {
|
||||
workflow[latent.id].inputs.width = width;
|
||||
workflow[latent.id].inputs.height = height;
|
||||
}
|
||||
|
||||
comfyEditor.setPromptText("positive", prompt || "", { workflow, analysis });
|
||||
|
||||
await window._wfmGenerateTab.generate(workflow);
|
||||
|
||||
const resultUrl = document.getElementById("wfm-gen-result-img")?.src;
|
||||
if (!resultUrl) return { ok: false, message: "No result image produced" };
|
||||
return { ok: true, url: resultUrl };
|
||||
} catch (e) {
|
||||
return { ok: false, message: e.message };
|
||||
}
|
||||
};
|
||||
|
||||
// ── ページング ────────────────────────────────────────────────
|
||||
const PAGE_SIZE = 50;
|
||||
let _renderedCount = 0;
|
||||
|
||||
+216
-3
@@ -621,6 +621,14 @@ const LANGUAGES = {
|
||||
aiSettingsChatI2IGenTitle: "Chat I2I Generation",
|
||||
aiChatAttachmentLabel: "Image attached",
|
||||
aiChatNoLoadImageNode: "No LoadImage node found in the target workflow for image-to-image",
|
||||
aiSkillNone: "No skill",
|
||||
aiSkillManageBtnTitle: "Manage skills",
|
||||
aiSkillPanelTitle: "Skills",
|
||||
aiSkillNewBtn: "+ New",
|
||||
aiSkillNoFiles: "No skills yet. Click \"+ New\" to create one.",
|
||||
aiSkillFilenamePlaceholder: "filename.md",
|
||||
aiSkillContentPlaceholder: "---\nname: My Skill\ndescription: What this skill does\n---\n\nSystem prompt instructions for the LLM...",
|
||||
aiChatSaveAsSkill: "Save as new skill",
|
||||
|
||||
// -- Help & Support --
|
||||
tabHelp: "Help",
|
||||
@@ -772,9 +780,72 @@ const LANGUAGES = {
|
||||
helpAi2: "Translation pane: translate between Japanese, English, Chinese, or Free language; ⇄ 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)",
|
||||
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",
|
||||
helpAi5: "Settings pane: choose backend (Ollama / LM Studio / Lemonade), set API URL, test connection, select model, configure Free language names for translation",
|
||||
helpAi6: "Settings pane: choose backend (Ollama / LM Studio / Lemonade), set API URL, test connection, select model, 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",
|
||||
helpAi8: "Chat pane — Skills: pick a skill from the dropdown above the message list to prepend its instructions to the conversation as a system prompt (not shown in the visible chat log); use the pencil button to open the skill manager and create, edit, or delete .md skill files, stored in user/default/Workflow-Studio/ai_skills/; a file's first line block between --- markers can define name/description shown in the dropdown; two starter skills are included by default: \"SVG Icon Generator\" (decomposes the subject into parts before drawing, for more recognizable SVG output) and \"Skill Creator\" (interviews you conversationally and drafts a new skill file, ready to save); when a reply contains a ```skill code block, a \"Save as new skill\" button appears below it that opens the manager with the draft pre-filled",
|
||||
helpAi9: "Tips for writing your own skills: give a clear frontmatter name/description so it reads well in the dropdown and tooltip; state the exact output format you want and, if there is a deliverable to extract later (like SVG or another skill file), wrap it in a distinctive fenced code block; write concrete, imperative instructions rather than vague ones - smaller local models follow explicit rules and a short worked example far better than general guidance; state hard constraints (forbidden content, security, length limits) explicitly rather than assuming they are implied; when in doubt, select the \"Skill Creator\" skill in the dropdown and describe what you want - it will interview you and draft the file for you",
|
||||
helpAiCardLayout: "Layout",
|
||||
helpAiCardTranslate: "Translation pane",
|
||||
helpAiCardChat: "Chat pane",
|
||||
helpAiCardSvg: "Chat pane — SVG generation",
|
||||
helpAiCardSkills: "Chat pane — Skills",
|
||||
helpAiCardSkillTips: "Writing your own skills",
|
||||
helpAiCardTools: "TOOLS pane",
|
||||
helpAiCardSettings: "Settings pane",
|
||||
helpJumpToTab: "Open tab →",
|
||||
helpAboutCardGettingStarted: "Getting Started",
|
||||
helpWfTitle: "Workflow Tab",
|
||||
helpWfCardBrowse: "Browsing & Filtering",
|
||||
helpWfCardGroups: "Groups & Badges",
|
||||
helpWfCardPanel: "Side Panel & Detail Modal",
|
||||
helpGenTitle: "GenerateUI Tab",
|
||||
helpGenCardConnect: "Connecting & Loading Workflows",
|
||||
helpGenCardTabs: "Tab Layout",
|
||||
helpGenCardSeed: "Seed & Generation",
|
||||
helpGenCardBatchSetup: "Batch Tab — Setup",
|
||||
helpGenCardBatchTypes: "Batch Tab — Batch Types",
|
||||
helpPromptTitle: "Prompt Tab",
|
||||
helpPromptCardAi: "AI Assistant & Translation",
|
||||
helpPromptCardPresets: "Presets",
|
||||
helpPromptCardWildcards: "Wildcards",
|
||||
helpMetadataCardOverview: "Overview",
|
||||
helpMetadataCardExtracted: "Extracted Information",
|
||||
helpMetadataCardFormats: "Supported Formats",
|
||||
helpSettingsTitle: "Settings Tab",
|
||||
helpSettingsCardGeneral: "General",
|
||||
helpSettingsCardConnections: "Connections",
|
||||
helpSettingsCardWorkflow: "Workflow Defaults",
|
||||
helpSettingsCardEditor: "Editor Customization",
|
||||
helpSettingsCardMisc: "Data & Misc",
|
||||
helpNodesTitle: "Nodes Tab",
|
||||
helpNodesCardBrowse: "Browse & Organize",
|
||||
helpNodesCardGroups: "Groups & Sets",
|
||||
helpNodesCardPanel: "Side Panel & Multi-select",
|
||||
helpGalleryCardBrowse: "Browsing",
|
||||
helpGalleryCardFolder: "Folder & Selection",
|
||||
helpGalleryCardDetail: "Detail Panel",
|
||||
helpGalleryCardActions: "Actions Toolbar",
|
||||
helpGalleryCardFormats: "Formats & Auto-save",
|
||||
helpModelsTitle: "Models Tab",
|
||||
helpModelsCardBrowse: "Browsing",
|
||||
helpModelsCardCivitai: "Side Panel & CivitAI",
|
||||
helpModelsCardApply: "Apply & Detail",
|
||||
helpModelsCardGroups: "Groups & Badges",
|
||||
helpModelsCardBulk: "Enable/Disable & Bulk Operations",
|
||||
helpSidepanelCardTabs: "Tabs Overview",
|
||||
helpSidepanelCardDnd: "Drag & Drop / Interaction",
|
||||
helpSidepanelCardInfo: "Information (I) tab",
|
||||
helpSidepanelCardAi: "AI TOOL tab",
|
||||
helpSidepanelCardNav: "Customization & Navigation",
|
||||
helpTaggerCardOverview: "Overview & Setup",
|
||||
helpTaggerCardSingle: "Single Tab",
|
||||
helpTaggerCardSending: "Sending Results",
|
||||
helpTaggerCardBatch: "Batch & DB Tabs",
|
||||
helpImageEditCardOverview: "Overview",
|
||||
helpShortcutsCardGeneral: "General",
|
||||
helpTroubleCardConnection: "Connection Issues",
|
||||
helpTroubleCardWorkflow: "Workflow & Generation Issues",
|
||||
helpTroubleCardTagger: "Tagger & Model Issues",
|
||||
helpImageEditTitle: "Image Edit Tab",
|
||||
helpImageEdit1: "Layer-based image editor — compose images with multiple layers (Image, Text, Draw, Mask types) and export the composite",
|
||||
helpImageEdit2: "Loading images: drag & drop onto the canvas, Upload button, or send from the Gallery tab via the Image Edit button in the Gallery toolbar; the first image becomes Layer 1 (auto-locked); subsequent images are added as new layers scaled to fit the canvas",
|
||||
@@ -1632,6 +1703,14 @@ const LANGUAGES = {
|
||||
aiSettingsChatI2IGenTitle: "Chat I2I生成",
|
||||
aiChatAttachmentLabel: "画像を添付中",
|
||||
aiChatNoLoadImageNode: "対象ワークフローにI2I用のLoadImageノードが見つかりません",
|
||||
aiSkillNone: "スキルなし",
|
||||
aiSkillManageBtnTitle: "スキルを管理",
|
||||
aiSkillPanelTitle: "スキル",
|
||||
aiSkillNewBtn: "+ 新規",
|
||||
aiSkillNoFiles: "スキルがまだありません。「+ 新規」から作成してください。",
|
||||
aiSkillFilenamePlaceholder: "filename.md",
|
||||
aiSkillContentPlaceholder: "---\nname: マイスキル\ndescription: このスキルの説明\n---\n\nLLMへのsystem prompt指示...",
|
||||
aiChatSaveAsSkill: "スキルとして保存",
|
||||
|
||||
// -- Help & Support --
|
||||
tabHelp: "ヘルプ",
|
||||
@@ -1783,9 +1862,72 @@ const LANGUAGES = {
|
||||
helpAi2: "翻訳ペイン:Ollama、LM Studio、または Lemonade を使って日本語・英語・中国語・カスタム言語(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)",
|
||||
helpAi4: "TOOLS ペイン — VLM:画像(PNG/JPG/WebP)をドロップしてタスク(画像解説 / プロンプト作成 / タグ作成)を選んで Run;または「Create wildcards」を選んでカテゴリ名と件数を入力すると1行1エントリのワイルドカードを生成;ドロップした画像はChatペインの添付としても共有され(Chat側にも表示、I2I生成に使用)、✕ボタンでクリアするまで保持される",
|
||||
helpAi5: "設定ペイン:バックエンド(Ollama / LM Studio / Lemonade)・API URL・接続テスト・モデル選択・Free 言語名を設定",
|
||||
helpAi6: "Settings ペイン:バックエンド(Ollama / LM Studio / Lemonade)の選択、APIのURL設定、接続テスト、モデル選択、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非対応のモデルでも動作する",
|
||||
helpAi8: "Chat ペイン — スキル:メッセージ一覧上部のドロップダウンでスキルを選ぶと、その指示内容が system prompt として会話の先頭に付加される(表示上のチャット履歴には含まれない);鉛筆ボタンでスキル管理パネルを開き、.mdスキルファイルを作成・編集・削除できる(保存先: user/default/Workflow-Studio/ai_skills/);ファイル先頭の---で囲まれた部分にname/descriptionを書くとドロップダウンの表示に反映される;初期状態で2つのスキルを同梱: 「SVG Icon Generator」(描く前に対象を部位分解することで認識しやすいSVGを生成)と「Skill Creator」(対話形式で質問しながら新しいスキルファイルを下書きする);応答に```skillコードブロックが含まれる場合、メッセージ下部に「スキルとして保存」ボタンが表示され、内容が入った状態で管理パネルが開く",
|
||||
helpAi9: "スキル作成のヒント:ドロップダウンやツールチップで分かりやすいよう、frontmatterのname/descriptionは明確に書く;期待する出力形式を具体的に指示し、SVGや別のスキルファイルのように後で抽出したい成果物がある場合は専用のコードフェンスで囲むよう指示する;曖昧な指示より具体的で命令形の指示の方がローカルの小型モデルには効きやすく、短い実例を1つ添えるとさらに効果的;禁止事項(不適切な内容、セキュリティ、文字数制限など)は暗黙に期待せず明示的に書く;迷ったらドロップダウンで「Skill Creator」スキルを選び、作りたいスキルの概要を伝えれば対話形式で下書きしてくれる",
|
||||
helpAiCardLayout: "レイアウト",
|
||||
helpAiCardTranslate: "Translationペイン",
|
||||
helpAiCardChat: "Chatペイン",
|
||||
helpAiCardSvg: "Chatペイン — SVG生成",
|
||||
helpAiCardSkills: "Chatペイン — スキル",
|
||||
helpAiCardSkillTips: "スキル作成のヒント",
|
||||
helpAiCardTools: "TOOLSペイン",
|
||||
helpAiCardSettings: "Settingsペイン",
|
||||
helpJumpToTab: "タブを開く →",
|
||||
helpAboutCardGettingStarted: "はじめに",
|
||||
helpWfTitle: "ワークフロータブ",
|
||||
helpWfCardBrowse: "閲覧・フィルタリング",
|
||||
helpWfCardGroups: "グループ・バッジ",
|
||||
helpWfCardPanel: "サイドパネル・詳細モーダル",
|
||||
helpGenTitle: "GenerateUIタブ",
|
||||
helpGenCardConnect: "接続・ワークフロー読込",
|
||||
helpGenCardTabs: "タブレイアウト",
|
||||
helpGenCardSeed: "シード・生成",
|
||||
helpGenCardBatchSetup: "Batchタブ — セットアップ",
|
||||
helpGenCardBatchTypes: "Batchタブ — バッチ種別",
|
||||
helpPromptTitle: "プロンプトタブ",
|
||||
helpPromptCardAi: "AIアシスタント・翻訳",
|
||||
helpPromptCardPresets: "プリセット",
|
||||
helpPromptCardWildcards: "ワイルドカード",
|
||||
helpMetadataCardOverview: "概要",
|
||||
helpMetadataCardExtracted: "抽出情報",
|
||||
helpMetadataCardFormats: "対応フォーマット",
|
||||
helpSettingsTitle: "設定タブ",
|
||||
helpSettingsCardGeneral: "一般",
|
||||
helpSettingsCardConnections: "接続",
|
||||
helpSettingsCardWorkflow: "ワークフロー既定値",
|
||||
helpSettingsCardEditor: "エディタカスタマイズ",
|
||||
helpSettingsCardMisc: "データ・その他",
|
||||
helpNodesTitle: "ノードタブ",
|
||||
helpNodesCardBrowse: "閲覧・整理",
|
||||
helpNodesCardGroups: "グループ・セット",
|
||||
helpNodesCardPanel: "サイドパネル・複数選択",
|
||||
helpGalleryCardBrowse: "閲覧",
|
||||
helpGalleryCardFolder: "フォルダ・選択",
|
||||
helpGalleryCardDetail: "詳細パネル",
|
||||
helpGalleryCardActions: "アクションツールバー",
|
||||
helpGalleryCardFormats: "対応フォーマット・自動保存",
|
||||
helpModelsTitle: "モデルタブ",
|
||||
helpModelsCardBrowse: "閲覧",
|
||||
helpModelsCardCivitai: "サイドパネル・CivitAI",
|
||||
helpModelsCardApply: "適用・詳細",
|
||||
helpModelsCardGroups: "グループ・バッジ",
|
||||
helpModelsCardBulk: "有効/無効・一括操作",
|
||||
helpSidepanelCardTabs: "タブ概要",
|
||||
helpSidepanelCardDnd: "ドラッグ&ドロップ操作",
|
||||
helpSidepanelCardInfo: "Iタブ(情報)",
|
||||
helpSidepanelCardAi: "AI TOOLタブ",
|
||||
helpSidepanelCardNav: "カスタマイズ・ナビゲーション",
|
||||
helpTaggerCardOverview: "概要・セットアップ",
|
||||
helpTaggerCardSingle: "Singleタブ",
|
||||
helpTaggerCardSending: "結果の送信",
|
||||
helpTaggerCardBatch: "Batch・DBタブ",
|
||||
helpImageEditCardOverview: "概要",
|
||||
helpShortcutsCardGeneral: "一般",
|
||||
helpTroubleCardConnection: "接続の問題",
|
||||
helpTroubleCardWorkflow: "ワークフロー・生成の問題",
|
||||
helpTroubleCardTagger: "Tagger・モデルの問題",
|
||||
helpImageEditTitle: "Image Edit タブ",
|
||||
helpImageEdit1: "レイヤーベースの画像エディタ — 複数のレイヤー(画像・テキスト・描画・マスク)を重ね合わせて画像を合成・エクスポート",
|
||||
helpImageEdit2: "画像の読み込み:キャンバスへのドラッグ&ドロップ、Uploadボタン、またはGalleryタブのImage Editボタンから送信;最初の画像はLayer 1として自動ロック;以降の画像はキャンバスサイズに合わせてスケールされ新規レイヤーとして追加",
|
||||
@@ -2642,6 +2784,14 @@ const LANGUAGES = {
|
||||
aiSettingsChatI2IGenTitle: "Chat I2I 生成",
|
||||
aiChatAttachmentLabel: "已附加图像",
|
||||
aiChatNoLoadImageNode: "目标工作流中未找到用于图生图的 LoadImage 节点",
|
||||
aiSkillNone: "无技能",
|
||||
aiSkillManageBtnTitle: "管理技能",
|
||||
aiSkillPanelTitle: "技能",
|
||||
aiSkillNewBtn: "+ 新建",
|
||||
aiSkillNoFiles: "还没有技能。点击「+ 新建」创建一个。",
|
||||
aiSkillFilenamePlaceholder: "filename.md",
|
||||
aiSkillContentPlaceholder: "---\nname: 我的技能\ndescription: 该技能的说明\n---\n\n给 LLM 的 system prompt 指令...",
|
||||
aiChatSaveAsSkill: "保存为技能",
|
||||
|
||||
// -- Help & Support --
|
||||
tabHelp: "帮助",
|
||||
@@ -2793,9 +2943,72 @@ const LANGUAGES = {
|
||||
helpAi2: "翻译面板:使用 Ollama、LM Studio 或 Lemonade 在日语、英语、中文或自定义语言(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)",
|
||||
helpAi4: "TOOLS 面板 — VLM:拖放图像(PNG/JPG/WebP)并选择任务(图像描述 / 提示词生成 / 标签生成),点击 Run;或选择「Create wildcards」,输入类别名称和数量生成纯文本通配符条目(每行一条);拖放的图像也会作为 Chat 面板的附件共享(在该处显示并用于图生图生成),直到点击 ✕ 按钮清除",
|
||||
helpAi5: "设置面板:选择后端(Ollama / LM Studio / Lemonade)、设置 API URL、测试连接、选择模型、配置 Free 语言名称",
|
||||
helpAi6: "Settings 面板:选择后端(Ollama / LM Studio / Lemonade)、设置 API URL、测试连接、选择模型、配置 Free 语言名称;Chat 图像生成部分可让 Chat 触发的文生图使用专用工作流,而不是 GenerateUI 中当前加载的工作流;Chat I2I 生成部分提供相同设置,用于附加图像时的图生图生成;设置保存至 localStorage,并与 Library 面板的 A 标签共享",
|
||||
helpAi7: "Chat 面板 — SVG:LLM 还可以直接以文本形式生成 SVG 图形,完全不涉及 ComfyUI 工作流;只需要求它用 SVG 绘制内容,如果回复中包含 SVG 代码,会自动渲染为内嵌预览,并提供\"保存到图库\"按钮,将其保存为 .svg 文件到 Gallery 标签页的输出文件夹;该功能不使用 tool calling(函数调用),因此不支持 tool calling 的模型也可以使用",
|
||||
helpAi8: "Chat 面板 — 技能:在消息列表上方的下拉菜单中选择一个技能,其内容会作为 system prompt 添加到对话开头(不会显示在可见的聊天记录中);点击铅笔按钮打开技能管理面板,可创建、编辑、删除 .md 技能文件(保存路径:user/default/Workflow-Studio/ai_skills/);文件开头 --- 之间的 name/description 字段会显示在下拉菜单中;默认内置两个技能:「SVG Icon Generator」(在绘制前先将对象拆解为多个部件,以生成更易识别的 SVG)和「Skill Creator」(通过对话向你提问并起草新的技能文件);当回复中包含```skill代码块时,消息下方会出现「保存为技能」按钮,点击后会打开管理面板并预填内容",
|
||||
helpAi9: "编写技能的小贴士:为 frontmatter 的 name/description 写清楚的内容,便于在下拉菜单和提示中阅读;明确说明期望的输出格式,如果有需要后续提取的成果物(如 SVG 或另一个技能文件),指示它用专门的代码围栏包裹;具体、祈使句式的指令比笼统的说明对本地小模型更有效,附上一个简短示例效果更佳;明确写出硬性约束(禁止内容、安全性、长度限制等),不要指望模型会自行推断;如果不确定怎么写,可以在下拉菜单中选择「Skill Creator」技能并描述你想要的技能,它会通过提问帮你起草",
|
||||
helpAiCardLayout: "布局",
|
||||
helpAiCardTranslate: "Translation 面板",
|
||||
helpAiCardChat: "Chat 面板",
|
||||
helpAiCardSvg: "Chat 面板 — SVG 生成",
|
||||
helpAiCardSkills: "Chat 面板 — 技能",
|
||||
helpAiCardSkillTips: "编写技能的小贴士",
|
||||
helpAiCardTools: "TOOLS 面板",
|
||||
helpAiCardSettings: "Settings 面板",
|
||||
helpJumpToTab: "打开标签页 →",
|
||||
helpAboutCardGettingStarted: "开始使用",
|
||||
helpWfTitle: "工作流标签页",
|
||||
helpWfCardBrowse: "浏览与筛选",
|
||||
helpWfCardGroups: "分组与徽章",
|
||||
helpWfCardPanel: "侧边面板与详情弹窗",
|
||||
helpGenTitle: "GenerateUI 标签页",
|
||||
helpGenCardConnect: "连接与加载工作流",
|
||||
helpGenCardTabs: "标签布局",
|
||||
helpGenCardSeed: "种子与生成",
|
||||
helpGenCardBatchSetup: "Batch 标签 — 设置",
|
||||
helpGenCardBatchTypes: "Batch 标签 — 批量类型",
|
||||
helpPromptTitle: "提示词标签页",
|
||||
helpPromptCardAi: "AI 助手与翻译",
|
||||
helpPromptCardPresets: "预设",
|
||||
helpPromptCardWildcards: "通配符",
|
||||
helpMetadataCardOverview: "概述",
|
||||
helpMetadataCardExtracted: "提取的信息",
|
||||
helpMetadataCardFormats: "支持的格式",
|
||||
helpSettingsTitle: "设置标签页",
|
||||
helpSettingsCardGeneral: "常规",
|
||||
helpSettingsCardConnections: "连接",
|
||||
helpSettingsCardWorkflow: "工作流默认值",
|
||||
helpSettingsCardEditor: "编辑器自定义",
|
||||
helpSettingsCardMisc: "数据与其他",
|
||||
helpNodesTitle: "节点标签页",
|
||||
helpNodesCardBrowse: "浏览与整理",
|
||||
helpNodesCardGroups: "分组与节点集",
|
||||
helpNodesCardPanel: "侧边面板与多选",
|
||||
helpGalleryCardBrowse: "浏览",
|
||||
helpGalleryCardFolder: "文件夹与选择",
|
||||
helpGalleryCardDetail: "详情面板",
|
||||
helpGalleryCardActions: "操作工具栏",
|
||||
helpGalleryCardFormats: "格式与自动保存",
|
||||
helpModelsTitle: "模型标签页",
|
||||
helpModelsCardBrowse: "浏览",
|
||||
helpModelsCardCivitai: "侧边面板与 CivitAI",
|
||||
helpModelsCardApply: "应用与详情",
|
||||
helpModelsCardGroups: "分组与徽章",
|
||||
helpModelsCardBulk: "启用/禁用与批量操作",
|
||||
helpSidepanelCardTabs: "标签概览",
|
||||
helpSidepanelCardDnd: "拖放操作",
|
||||
helpSidepanelCardInfo: "I 标签(信息)",
|
||||
helpSidepanelCardAi: "AI TOOL 标签",
|
||||
helpSidepanelCardNav: "自定义与导航",
|
||||
helpTaggerCardOverview: "概述与设置",
|
||||
helpTaggerCardSingle: "Single 标签",
|
||||
helpTaggerCardSending: "发送结果",
|
||||
helpTaggerCardBatch: "Batch 与 DB 标签",
|
||||
helpImageEditCardOverview: "概述",
|
||||
helpShortcutsCardGeneral: "常规",
|
||||
helpTroubleCardConnection: "连接问题",
|
||||
helpTroubleCardWorkflow: "工作流与生成问题",
|
||||
helpTroubleCardTagger: "Tagger 与模型问题",
|
||||
helpImageEditTitle: "图像编辑选项卡",
|
||||
helpImageEdit1: "基于图层的图像编辑器 — 通过多个图层(图像、文字、绘制、遮罩类型)合成图像并导出",
|
||||
helpImageEdit2: "加载图像:拖放到画布、点击Upload按钮、或通过Gallery标签工具栏的Image Edit按钮发送;第一张图像成为图层1(自动锁定);后续图像缩放至画布大小作为新图层添加",
|
||||
|
||||
+325
-54
@@ -1057,8 +1057,11 @@
|
||||
<div class="wfm-help-content">
|
||||
|
||||
<div class="wfm-help-page active" id="wfm-help-page-about">
|
||||
<div class="wfm-help-article-header">
|
||||
<h2 id="wfm-help-about-title">About</h2>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-about-title">About</h3>
|
||||
<h3 id="wfm-help-about-card-getting-started-title">Getting Started</h3>
|
||||
<p id="wfm-help-about-desc">A centralized control UI for managing ComfyUI workflow JSON files, generating images via API, assisting with prompt creation, and browsing/organizing nodes.</p>
|
||||
<ul>
|
||||
<li id="wfm-help-about-1">Install via ComfyUI Manager or git clone to custom_nodes</li>
|
||||
@@ -1068,53 +1071,94 @@
|
||||
</div>
|
||||
|
||||
<div class="wfm-help-page" id="wfm-help-page-workflow">
|
||||
<div class="wfm-help-article-header">
|
||||
<h2 id="wfm-help-wf-title">Workflow Tab</h2>
|
||||
<button type="button" class="wfm-btn wfm-btn-sm wfm-help-jump-btn" data-jump-tab="workflow">Open tab →</button>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3>Workflow Tab</h3>
|
||||
<h3 id="wfm-help-wf-card-browse-title">Browsing & Filtering</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-wf-1">Switch between Thumbnail, Card, and Table view modes</li>
|
||||
<li id="wfm-help-wf-2">Search, category filter, favorites filter, and group filter</li>
|
||||
<li id="wfm-help-wf-3">Create, rename, delete groups and assign/remove workflows</li>
|
||||
<li id="wfm-help-wf-4">Import JSON/PNG files via button or drag & drop</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-wf-card-groups-title">Groups & Badges</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-wf-3">Create, rename, delete groups and assign/remove workflows</li>
|
||||
<li id="wfm-help-wf-7">Badge management: add, rename, delete badges with custom colors shared with the Models tab (⚙ Badge button)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-wf-card-panel-title">Side Panel & Detail Modal</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-wf-5">Side panel: thumbnail, JSON viewer (format badge: UI/API/App), clipboard copy, group management</li>
|
||||
<li id="wfm-help-wf-6">Detail modal: rename, favorite, thumbnail change, badge assignment, tags, memo, AI summary, send to canvas, load in GenerateUI, set as default, delete</li>
|
||||
<li id="wfm-help-wf-7">Badge management: add, rename, delete badges with custom colors shared with the Models tab (⚙ Badge button)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wfm-help-page" id="wfm-help-page-generate">
|
||||
<div class="wfm-help-article-header">
|
||||
<h2 id="wfm-help-gen-title">GenerateUI Tab</h2>
|
||||
<button type="button" class="wfm-btn wfm-btn-sm wfm-help-jump-btn" data-jump-tab="generate">Open tab →</button>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3>GenerateUI Tab</h3>
|
||||
<h3 id="wfm-help-gen-card-connect-title">Connecting & Loading Workflows</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-gen-1">Connect/disconnect ComfyUI and check connection status</li>
|
||||
<li id="wfm-help-gen-2">Load workflows from Workflow tab (subgraph-containing workflows are supported, e.g. Flux.2 / Qwen-Image / Z-Image / Mage-Flow templates; only ComfyUI's simplified App format [.app.json / Linear Mode] is not); Reset Workflow button re-reads the current workflow file from disk and reloads the editor; <b>Save</b> button (right end of the tab bar) saves the current workflow — including any edits made in the editor — as a new .json file to the Workflow tab; a filename dialog appears with the current workflow name as the default; <b>Style</b> checkbox and dropdown (next to Reset Workflow): enable to apply a Fooocus-style JSON style to the positive and negative prompts at generation time — style files are loaded from <code>user/default/Workflow-Studio/style/*.json</code>; the style's <code>{prompt}</code> placeholder is replaced with the original prompt, or the style text is appended if no placeholder exists</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-gen-card-tabs-title">Tab Layout</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-gen-3">5-tab layout — Input (Prompt + Image), Model, Settings, Feeder, Batch — Input/Model/Settings each include a Raw JSON column for instant preview and direct editing</li>
|
||||
<li id="wfm-help-gen-4">Input tab: inner tabs — <b>Prompt</b> and <b>Image</b> (drag-and-drop upload); Prompt tab has Positive Prompt, Negative Prompt, and an <b>Embeddings</b> selector at the bottom — select a model, set Weight (default 1.0), then click <b>Paste</b> to insert <code>(embedding:Name:weight)</code> at the cursor position of the last focused textarea (defaults to Positive if neither is focused); a <b>↺</b> button next to each Apply button reverts the textarea to the node's currently applied value; select text (or just place the cursor on a word/parenthesis block) and press <b>Ctrl+↑/↓</b> to adjust its emphasis weight in <code>(text:weight)</code> syntax by ±0.05 — the parentheses are removed automatically when the weight returns to 1.0</li>
|
||||
<li id="wfm-help-gen-5">Model tab: Checkpoint, VAE, LoRA (Single/Stack tabs), ControlNet, Diffusion Model (UNETLoader / UnetLoaderGGUF / LoaderGGUF), Text Encoder (CLIPLoader / DualCLIPLoader / ClipLoaderGGUF / DualClipLoaderGGUF — type and device selectors; DualCLIP shows two clip selectors), Hypernetwork (with Strength field) selectors with filter; LoRA Stack — Apply writes all active LoRAs to the target node and simultaneously inserts LoRA syntax and CivitAI trigger words into the Positive prompt; re-applying updates the prompt by removing inactive LoRAs and re-adding active ones</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>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-gen-card-seed-title">Seed & Generation</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-gen-8">Seed control: random, fixed, increment, decrement — seed input and mode selector stacked vertically for readability</li>
|
||||
<li id="wfm-help-gen-9">Generate, progress display, stop, and view results</li>
|
||||
<li id="wfm-help-gen-10">Eagle integration: auto-save generated images to Eagle (SVG output supported too — both the standard SaveSVGNode and comfyui-tosvg's Save SVG String node)</li>
|
||||
<li id="wfm-help-gen-18">Alt+Click any Apply button (Positive Prompt / Checkpoint / KSampler) to Apply & Generate in one step — applies the parameter to the workflow then immediately queues a generation; only fires when the target node exists in the loaded workflow</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-gen-card-batchsetup-title">Batch Tab — Setup</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-gen-11">Batch tab — enable one batch type at a time by checking the checkbox in the Batch Queue column header; Generate then runs through all queued items sequentially with Pause/Resume and Stop controls</li>
|
||||
<li id="wfm-help-gen-12">Batch tab — Left pane has 4 tabs: <b>Checkpoints</b> (folder tree with Filter/All/None; (root) header checkbox selects all), <b>Sampler</b> (KSampler sampler_name list with (root) checkbox), <b>Scheduler</b> (KSampler scheduler list with (root) checkbox), <b>Style</b> (flat list of all styles loaded from <code>Workflow-Studio/style/</code> with All/None buttons); All/None toolbar buttons available for each tab</li>
|
||||
<li id="wfm-help-gen-13">Batch tab — Center pane: group-based selection with inner tabs (Checkpoint | Lora | Prompt | Workflow); Checkpoint tab shows groups defined in the Models tab — check a group header to select all its members, or expand ▶ to check individual models; group membership always reflects the current Models tab state</li>
|
||||
<li id="wfm-help-gen-14">Batch tab — Right pane (Batch Queue): 7 columns — Checkpoint, Lora, Prompt, Workflow, Sampler, Scheduler, <b>Style</b>; each column has an enable checkbox; only one type active at a time; count shown per column</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-gen-card-batchtypes-title">Batch Tab — Batch Types</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-gen-15">Sampler/Scheduler batch: select one or more samplers or schedulers from the left pane, enable the column in Batch Queue, then Generate to run each value through the KSampler node sequentially; sampler/scheduler lists are fetched from ComfyUI (requires connection)</li>
|
||||
<li id="wfm-help-gen-17">Style batch: select styles in the left pane Style tab, enable the Style column in Batch Queue, then Generate — each selected style is applied in sequence to the positive and negative prompts; styles are applied to workflow copies so the loaded workflow is not modified; style files are re-read each time the page loads from <code>user/default/Workflow-Studio/style/*.json</code></li>
|
||||
<li id="wfm-help-gen-16">SPA-side wildcard expansion (A1111-style): when prompt text in non-ImpactWildcard nodes (e.g. CLIPTextEncode) contains __name__ syntax, the SPA expands it at generation time by randomly picking a line from the matching file in the WFS wildcard folder; nested wildcards are re-expanded up to 5 passes; unresolved wildcards are passed through unchanged; ImpactWildcardEncode/Processor nodes are skipped (handled server-side by Impact Pack); works in both single and batch generation</li>
|
||||
<li id="wfm-help-gen-18">Alt+Click any Apply button (Positive Prompt / Checkpoint / KSampler) to Apply & Generate in one step — applies the parameter to the workflow then immediately queues a generation; only fires when the target node exists in the loaded workflow</li>
|
||||
<li id="wfm-help-gen-19">Lora batch: for each LoRA, its syntax (<lora:name:strength_model:strength_clip>) and CivitAI trained words are automatically appended to the Positive prompt — rebuilt from the original prompt each time, not accumulated; the prompt is automatically restored to its pre-batch state when the batch completes or is stopped; if a selected LoRA is not yet recognized by ComfyUI's model list, a warning toast is shown (Refresh or restart ComfyUI)</li>
|
||||
<li id="wfm-help-gen-16">SPA-side wildcard expansion (A1111-style): when prompt text in non-ImpactWildcard nodes (e.g. CLIPTextEncode) contains __name__ syntax, the SPA expands it at generation time by randomly picking a line from the matching file in the WFS wildcard folder; nested wildcards are re-expanded up to 5 passes; unresolved wildcards are passed through unchanged; ImpactWildcardEncode/Processor nodes are skipped (handled server-side by Impact Pack); works in both single and batch generation</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wfm-help-page" id="wfm-help-page-feeder">
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-feeder-title">GenerateUI Tab — Feeder subtab</h3>
|
||||
<p id="wfm-help-feeder-desc">Two modes switchable via the [Image Loop] / [Gallery] toggle at the top of the left pane.</p>
|
||||
<div class="wfm-help-article-header">
|
||||
<h2 id="wfm-help-feeder-title">GenerateUI Tab — Feeder subtab</h2>
|
||||
<button type="button" class="wfm-btn wfm-btn-sm wfm-help-jump-btn" data-jump-tab="generate">Open tab →</button>
|
||||
</div>
|
||||
<p id="wfm-help-feeder-desc" style="margin-bottom:12px;">Two modes switchable via the [Image Loop] / [Gallery] toggle at the top of the left pane.</p>
|
||||
|
||||
<h4 id="wfm-help-feeder-imgloop-title" style="margin:10px 0 4px;color:var(--wfm-accent)">Image Loop mode</h4>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-feeder-imgloop-title">Image Loop mode</h3>
|
||||
<p id="wfm-help-feeder-imgloop-desc">Requires the comfyui-image-feeder custom node. Controls ImageFeeder nodes and runs continuous generation loops over an image library stored in user/default/image-loop-data/.</p>
|
||||
<ul>
|
||||
<li id="wfm-help-feeder-1">Left pane — Node settings: select the target ImageFeeder node from the dropdown; edit all node parameters (Directory, Sort Mode, Index, Start/End Index, Batch Size, Seed, Use Selection); click Apply to write changes to the loaded workflow</li>
|
||||
@@ -1127,8 +1171,10 @@
|
||||
<li id="wfm-help-feeder-8">Seed during Run: uses the right-pane seed setting (Random / Fixed / Increment / Decrement) — the ImageFeeder's own Seed field controls random sort order only</li>
|
||||
<li id="wfm-help-feeder-9">Index sync: after each generation the node returns the next index via WebSocket; the Index field updates automatically so the display stays in sync</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h4 id="wfm-help-feeder-gal-title" style="margin:14px 0 4px;color:var(--wfm-accent)">Gallery mode</h4>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-feeder-gal-title">Gallery mode</h3>
|
||||
<p id="wfm-help-feeder-gal-desc">Uses the built-in WFS_GalleryFeeder node (no external plugin required). Feeds images from a Gallery group into a continuous generation loop.</p>
|
||||
<ul>
|
||||
<li id="wfm-help-feeder-gal-1">Left pane — Node: select a WFS_GalleryFeeder node from the dropdown and click Apply to link it to the workflow; select the source group (defaults to <b>__Feeder__</b>), Sort Mode, Index (current image position), and Seed</li>
|
||||
@@ -1142,16 +1188,30 @@
|
||||
</div>
|
||||
|
||||
<div class="wfm-help-page" id="wfm-help-page-prompt">
|
||||
<div class="wfm-help-article-header">
|
||||
<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 →</button>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3>Prompt Tab</h3>
|
||||
<h3 id="wfm-help-prompt-card-ai-title">AI Assistant & Translation</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-prompt-1">AI assistant (Ollama): model selection, chat for prompt consultation, image attachment (vision models)</li>
|
||||
<li id="wfm-help-prompt-2">Apply AI responses to GenerateUI prompts</li>
|
||||
<li id="wfm-help-prompt-3">Translation: JA↔EN, ZH↔EN</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-prompt-card-presets-title">Presets</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-prompt-4">Preset management: create, edit, positive/negative prompt clipboard copy (PP/NP)</li>
|
||||
<li id="wfm-help-prompt-5">Preset Manager: browse all presets, favorites, and group-based filtering</li>
|
||||
<li id="wfm-help-prompt-6">Group management: create groups, assign/remove presets, delete groups</li>
|
||||
<li id="wfm-help-prompt-7">GenUI Set: apply preset prompts directly to GenerateUI</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-prompt-card-wildcards-title">Wildcards</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-prompt-8">Wildcard toolbar: one-click buttons to insert wildcard syntax ({|}, {n$|}, __|__, <lora:>, etc.); wraps selected text</li>
|
||||
<li id="wfm-help-prompt-9">Wildcard file manager: create/edit/delete .txt/.yaml wildcard files; click filename to insert __filename__ at cursor</li>
|
||||
</ul>
|
||||
@@ -1159,71 +1219,140 @@
|
||||
</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 →</button>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-metadata-title">Metadata Tab</h3>
|
||||
<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>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-metadata-card-extracted-title">Extracted Information</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-metadata-2">Center column: Checkpoint, VAE, Diffusion Model, and Text Encoder lists extracted from the workflow</li>
|
||||
<li id="wfm-help-metadata-3">Right column: LoRA list (with strength values) and Prompt list (click to view full text below)</li>
|
||||
<li id="wfm-help-metadata-4">Prompt actions — Copy: copy to clipboard; GenUI:P/N: set GenerateUI positive/negative prompt; Prompt:P/N: set Prompt tab preset positive/negative</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-metadata-card-formats-title">Supported Formats</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-metadata-5">Supported: ComfyUI PNG/WebP/JSON (standard + Flux.2 / Qwen-Image / Z-Image / Mage-Flow subgraph workflows), SD WebUI, SD Forge, Fooocus. Model extraction covers UNETLoader, UnetLoaderGGUF, UNETLoaderGGUF (e.g. HiDream GGUF), CLIPLoader, DualCLIPLoader, TripleCLIPLoader, and QuadrupleCLIPLoader (e.g. HiDream 4-CLIP).</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wfm-help-page" id="wfm-help-page-settings">
|
||||
<div class="wfm-help-article-header">
|
||||
<h2 id="wfm-help-settings-title">Settings Tab</h2>
|
||||
<button type="button" class="wfm-btn wfm-btn-sm wfm-help-jump-btn" data-jump-tab="settings">Open tab →</button>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3>Settings Tab</h3>
|
||||
<h3 id="wfm-help-settings-card-general-title">General</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-settings-1">UI language and summary language</li>
|
||||
<li id="wfm-help-settings-2">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)</li>
|
||||
<li id="wfm-help-settings-3">Workflow data folder path</li>
|
||||
<li id="wfm-help-settings-9">All sections are collapsible accordion panels</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-settings-card-connections-title">Connections</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-settings-4">ComfyUI connection URL and test</li>
|
||||
<li id="wfm-help-settings-5">Ollama URL, default model, and connection test</li>
|
||||
<li id="wfm-help-settings-6">Default workflow setting</li>
|
||||
<li id="wfm-help-settings-7">Eagle integration URL and auto-save toggle</li>
|
||||
<li id="wfm-help-settings-8">Data Management: export all plugin data to JSON / import to restore</li>
|
||||
<li id="wfm-help-settings-9">All sections are collapsible accordion panels</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-settings-card-workflow-title">Workflow Defaults</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-settings-3">Workflow data folder path</li>
|
||||
<li id="wfm-help-settings-6">Default workflow setting</li>
|
||||
<li id="wfm-help-settings-10">Wildcard Integration: link WFS wildcard dir to ComfyUI-Impact-Pack wildcards (junction/symlink); existing files migrated automatically</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-settings-card-editor-title">Editor Customization</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-settings-11">Text Size: one slider (10–28 px) adjusts font size for all prompt and chat textareas at once — Generate UI positive/negative prompts, AI Assistant chat input, Preset positive/negative, Wildcard prompt and file editor, and Metadata prompt full preview; takes effect immediately and saved with Save Settings</li>
|
||||
<li id="wfm-help-settings-12">RAW JSON Colors: customize the 6 syntax highlight colors for the Raw JSON editor in Generate UI — Default Text, Name/Scheduler, Title, Width/Height, Prompt/Text, Image/File; changes apply immediately on color pick; Reset Defaults restores the original color scheme</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-settings-card-misc-title">Data & Misc</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-settings-8">Data Management: export all plugin data to JSON / import to restore</li>
|
||||
<li id="wfm-help-settings-13">CivitAI Host: choose between civitai.com (SFW only) and civitai.red (unrestricted) — determines which site opens when clicking a model link in the Models tab CivitAI panel</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wfm-help-page" id="wfm-help-page-nodes">
|
||||
<div class="wfm-help-article-header">
|
||||
<h2 id="wfm-help-nodes-title">Nodes Tab</h2>
|
||||
<button type="button" class="wfm-btn wfm-btn-sm wfm-help-jump-btn" data-jump-tab="nodes">Open tab →</button>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3>Nodes Tab</h3>
|
||||
<h3 id="wfm-help-nodes-card-browse-title">Browse & Organize</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-nodes-1">Node Browser: search by name/description/alias/tag, filter by category, package, tag, group, favorites</li>
|
||||
<li id="wfm-help-nodes-2">Card and Table view modes with pagination</li>
|
||||
<li id="wfm-help-nodes-3">Favorite nodes and add custom tags for organization</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-nodes-card-groups-title">Groups & Sets</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-nodes-4">Create, rename, delete node groups and assign/remove nodes</li>
|
||||
<li id="wfm-help-nodes-5">Side panel: Details (class name, package, category, description, flags), I/O Spec (inputs/outputs with types and defaults), Groups management</li>
|
||||
<li id="wfm-help-nodes-6">Node Sets: create reusable node collections with descriptions, tags, and link definitions between nodes</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-nodes-card-panel-title">Side Panel & Multi-select</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-nodes-5">Side panel: Details (class name, package, category, description, flags), I/O Spec (inputs/outputs with types and defaults), Groups management</li>
|
||||
<li id="wfm-help-nodes-7">Multi-select: Ctrl+click to toggle individual nodes, Shift+click for range selection; bulk action bar appears with add to group / remove from group / create & add group / bulk favorite / unfavorite</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wfm-help-page" id="wfm-help-page-gallery">
|
||||
<div class="wfm-help-article-header">
|
||||
<h2 id="wfm-help-gallery-title">Gallery Tab</h2>
|
||||
<button type="button" class="wfm-btn wfm-btn-sm wfm-help-jump-btn" data-jump-tab="gallery">Open tab →</button>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-gallery-title">Gallery Tab</h3>
|
||||
<h3 id="wfm-help-gallery-card-browse-title">Browsing</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-gallery-1">Browse images in the ComfyUI output folder — set the folder path in Settings > Gallery Output Folder</li>
|
||||
<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>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-gallery-card-folder-title">Folder & Selection</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-gallery-5">Folder tree: click to load images, expand with ▶; "+ New" creates a subfolder inside the selected folder, "Del" deletes it with all contents</li>
|
||||
<li id="wfm-help-gallery-6">Multi-select with Ctrl+click — bulk bar: select group → 'Add to Group' / 'Remove from Group'; 'Favorite All' / 'Unfavorite All'; 'Compare' (2–4 images); 'Move To...'; '<b>Export</b>' (downloads all selected images as a ZIP file); 'Delete File'</li>
|
||||
<li id="wfm-help-gallery-15"><b>F button</b> (cyan overlay, top-left of each thumbnail): toggle the image in/out of the <b>__Feeder__</b> group with a single click; active state stays visible (cyan) when the image is already in the group; use this to quickly queue images for the Feeder tab Gallery mode</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-gallery-card-detail-title">Detail Panel</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-gallery-7">Detail panel — Info tab: filename, size, date, tags (add/remove), memo; <b>download icon</b> (on image hover) to download the single image; "Move To..." moves the image to another folder, "Delete File" removes it permanently</li>
|
||||
<li id="wfm-help-gallery-8">Detail panel — JSON tab: displays workflow JSON from PNG embedded data (prompt / workflow keys) or from metadata saved by the Generate UI tab; 'Copy & Send Canvas' button copies to clipboard and stores the workflow for title-drag to canvas</li>
|
||||
<li id="wfm-help-gallery-9">Detail panel — Groups tab: create, assign, remove, rename, and delete groups per image; the <b>__Feeder__</b> group is reserved (shown with 🔒) and cannot be renamed or deleted — rename and delete buttons are disabled when it is selected</li>
|
||||
<li id="wfm-help-gallery-15"><b>F button</b> (cyan overlay, top-left of each thumbnail): toggle the image in/out of the <b>__Feeder__</b> group with a single click; active state stays visible (cyan) when the image is already in the group; use this to quickly queue images for the Feeder tab Gallery mode</li>
|
||||
<li id="wfm-help-gallery-10">Workflow auto-save: images generated in the Generate UI tab automatically have their workflow saved to gallery metadata and shown in the JSON tab</li>
|
||||
<li id="wfm-help-gallery-11">Supports PNG (tEXt/iTXt), JPEG, WebP, GIF; workflow extraction reads ComfyUI prompt and workflow keys embedded in PNG metadata</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-gallery-card-actions-title">Actions Toolbar</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-gallery-12"><b>Metadata</b> button (green, in the detail panel tab bar): opens the selected image in the Metadata tab for full embedded-data inspection; Alt+click on any image thumbnail has the same effect</li>
|
||||
<li id="wfm-help-gallery-13"><b>Load GenUI</b> button (blue, in the detail panel tab bar): loads the embedded ComfyUI workflow from the selected image directly into the GenerateUI tab and switches to it; shows a warning toast if no workflow is embedded or the format is not supported</li>
|
||||
<li id="wfm-help-gallery-14"><b>Tagger</b> button (purple, in the detail panel action row): opens the selected image directly in the Tagger tab's Single mode for instant tagging</li>
|
||||
@@ -1231,21 +1360,52 @@
|
||||
<li id="wfm-help-gallery-17"><b>Send GenUI Image</b> button (toolbar, next to Image Edit): uploads the selected image to ComfyUI and sets it as the input for the first LoadImage node in the GenerateUI tab; automatically switches to GenerateUI → Input → Image; requires a workflow with at least one LoadImage node to be loaded first</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-gallery-card-formats-title">Formats & Auto-save</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-gallery-10">Workflow auto-save: images generated in the Generate UI tab automatically have their workflow saved to gallery metadata and shown in the JSON tab</li>
|
||||
<li id="wfm-help-gallery-11">Supports PNG (tEXt/iTXt), JPEG, WebP, GIF; workflow extraction reads ComfyUI prompt and workflow keys embedded in PNG metadata</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wfm-help-page" id="wfm-help-page-models">
|
||||
<div class="wfm-help-article-header">
|
||||
<h2 id="wfm-help-models-title">Models Tab</h2>
|
||||
<button type="button" class="wfm-btn wfm-btn-sm wfm-help-jump-btn" data-jump-tab="models">Open tab →</button>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3>Models Tab</h3>
|
||||
<h3 id="wfm-help-models-card-browse-title">Browsing</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-models-1">Browse installed models: Checkpoint, LoRA, VAE, ControlNet, UNET, TextEncoder, Hypernetwork, Embedding with sub-tab switching</li>
|
||||
<li id="wfm-help-models-2">Thumbnail and Table view modes with pagination; Table view shows Type and Base Model columns sourced from CivitAI data alongside filename, subdirectory, and extension; <b>column headers are clickable to sort</b> — click once for ascending (▲), again for descending (▼), a third time to clear the sort; the <b>E/D</b> column sorts by enable/disable status</li>
|
||||
<li id="wfm-help-models-3">Search, tag filter, group filter, favorites filter, and status filter (All / Enabled only / Disabled only)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-models-card-civitai-title">Side Panel & CivitAI</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-models-4">Side panel: opens to CivitAI tab by default when a model is selected; Info (file path, tags, memo), Groups management, CivitAI integration</li>
|
||||
<li id="wfm-help-models-5">CivitAI integration: fetch model info by SHA256 hash, batch fetch per model type</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-models-card-apply-title">Apply & Detail</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-models-6">Detail modal: preview image, CivitAI info (base model, trained words, tags), thumbnail change — upload any image format to replace the preview; changes take effect immediately without restarting ComfyUI; broken/empty preview files are automatically skipped; Delete button permanently removes the model file and all associated sidecar files</li>
|
||||
<li id="wfm-help-models-7">Group management: create, rename, delete groups and assign/remove models; Checkpoint and LoRA tabs support B (Batch) and S (Stack) quick-assign buttons per model in both card and table views</li>
|
||||
<li id="wfm-help-models-8">GenUI Model button: apply the selected model to the corresponding node in GenerateUI's current workflow (Checkpoint / LoRA / VAE / ControlNet / UNET / TextEncoder / Hypernetwork); <b>Embedding</b> tab shows <b>GenUI PP</b> / <b>GenUI NP</b> buttons instead — appends <code>(embedding:Name:1.0)</code> to the Positive or Negative prompt of the loaded workflow</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-models-card-groups-title">Groups & Badges</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-models-7">Group management: create, rename, delete groups and assign/remove models; Checkpoint and LoRA tabs support B (Batch) and S (Stack) quick-assign buttons per model in both card and table views</li>
|
||||
<li id="wfm-help-models-9">User-defined badges: assign free-label badges with custom colors shared with the Workflow tab</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-models-card-bulk-title">Enable/Disable & Bulk Operations</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-models-10">Enable/Disable models: toggle visibility in ComfyUI by renaming file extension (.disabled suffix). Disable per model (⏸ button on each card) or per group (Enable All / Disable All in Group panel). Changes take effect after ComfyUI refresh.</li>
|
||||
<li id="wfm-help-models-11">Multi-select & bulk operations: click Select (green) to enter selection mode, then check multiple models; the bulk action bar is organized into three rows — <b>Group row:</b> select an existing group and Add/Remove, or type a new name and Create & Add; <b>Badge row:</b> select a badge and +Badge / −Badge to apply or remove; <b>File row:</b> select a destination subfolder (or type a new folder name) and Move to relocate model files with all sidecar files (.preview.png, .json, .civitai.info, etc.), Delete Files to permanently remove selected models; header row provides Deselect All and ★/☆ Favorite toggles</li>
|
||||
<li id="wfm-help-models-12">After moving or deleting models, ComfyUI may fail to load them because it caches the old file paths. Perform a Hard Reset from the ComfyUI menu (Queue → Hard Reset) or restart ComfyUI to refresh the model cache.</li>
|
||||
@@ -1254,65 +1414,135 @@
|
||||
</div>
|
||||
|
||||
<div class="wfm-help-page" id="wfm-help-page-sidepanel">
|
||||
<div class="wfm-help-article-header">
|
||||
<h2 id="wfm-help-sidepanel-title">Workflow Studio Library (ComfyUI Side Panel)</h2>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-sidepanel-title">Workflow Studio Library (ComfyUI Side Panel)</h3>
|
||||
<h3 id="wfm-help-sidepanel-card-tabs-title">Tabs Overview</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-sidepanel-1">Open from the Workflow Studio Library button in the ComfyUI toolbar</li>
|
||||
<li id="wfm-help-sidepanel-2">Workflows tab: All / ★ Favorites / Groups sub-tabs; ★ shown for favorites in All view</li>
|
||||
<li id="wfm-help-sidepanel-3">Nodes tab: All / ★ Favorites / Groups / Sets / 📂 Category / 🧩 Package sub-tabs; Category and Package show a dropdown to filter nodes</li>
|
||||
<li id="wfm-help-sidepanel-11">Models tab: All / ★ Favorites / Groups / By Type sub-tabs</li>
|
||||
<li id="wfm-help-sidepanel-4">Prompts tab: browse prompt presets with All/Favorites/Categories sub-tabs</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-sidepanel-card-dnd-title">Drag & Drop / Interaction</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-sidepanel-5">Drag & drop workflows onto the canvas to load them</li>
|
||||
<li id="wfm-help-sidepanel-6">Drag & drop nodes/node sets onto the canvas to place them</li>
|
||||
<li id="wfm-help-sidepanel-7">Drag & drop prompt presets to create WFS_PromptText nodes with positive/negative prompts</li>
|
||||
<li id="wfm-help-sidepanel-8">Copy individual positive (P) or negative (N) prompts from sidebar items</li>
|
||||
<li id="wfm-help-sidepanel-9">Double-click items to load/place without dragging</li>
|
||||
<li id="wfm-help-sidepanel-10">Search within each sub-tab to quickly find items</li>
|
||||
<li id="wfm-help-sidepanel-12">⚙ Theme button (header): customize panel background, text, border colors — saved to localStorage</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-sidepanel-card-info-title">Information (I) tab</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-sidepanel-13">I tab (Information): drop a ComfyUI PNG/WebP/JSON to inspect model, LoRA, and prompt metadata in the side panel — sub-tabs: model / lora / Prompts; supports UnetLoaderGGUF and QuadrupleCLIPLoader node types</li>
|
||||
<li id="wfm-help-sidepanel-14">Information tab — drag items to canvas: model sub-tab places the loader node (Checkpoint → CheckpointLoaderSimple, VAE → VAELoader, Diffusion Model → UNETLoader, Text Encoder → CLIPLoader); double-click also places at canvas center</li>
|
||||
<li id="wfm-help-sidepanel-15">Information tab — LoRA sub-tab: drag individual LoRA to place LoraLoader; "Multiple LORA" section at bottom places all LoRAs in a single Lora Loader (LoraManager) node; Prompts sub-tab: drag a prompt to place CLIPTextEncode node with text pre-filled; click prompt to view full text and copy</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-sidepanel-card-ai-title">AI TOOL tab</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-sidepanel-16">AI TOOL tab: Translation, Chat, TOOLS, and Settings sub-tabs powered by Ollama, LM Studio, or Lemonade; Chat supports multi-turn conversations; TOOLS includes VLM image analysis and wildcard generation; settings shared with the SPA AI TOOL tab via localStorage</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-sidepanel-card-nav-title">Customization & Navigation</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-sidepanel-12">⚙ Theme button (header): customize panel background, text, border colors — saved to localStorage</li>
|
||||
<li id="wfm-help-sidepanel-17">Title drag (Send to Canvas): after clicking 'Send to Canvas' (Workflow tab) or 'Copy & Send Canvas' (Gallery tab), the 'Workflow Studio Library' title turns blue with a green ● — drag it onto the canvas to load the workflow; title resets automatically after loading</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wfm-help-page" id="wfm-help-page-aitool">
|
||||
<div class="wfm-help-article-header">
|
||||
<h2 id="wfm-help-ai-title">AI TOOL Tab</h2>
|
||||
<button type="button" class="wfm-btn wfm-btn-sm wfm-help-jump-btn" data-jump-tab="ai">Open tab →</button>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-ai-title">AI TOOL Tab</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-ai-1">4-pane layout — Translation | Chat | TOOLS | Settings — all panes are always visible simultaneously</li>
|
||||
<li id="wfm-help-ai-2">Translation pane: translate text between Japanese, English, Chinese, or a custom Free language using Ollama, LM Studio, or Lemonade; ⇄ swap button swaps both language selectors and text content; selections are saved automatically</li>
|
||||
<li 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)</li>
|
||||
<li id="wfm-help-ai-4">TOOLS pane — VLM: drop an image (PNG/JPG/WebP) and select a task (Describe Image, Create Prompt, Create Tags), then click Run to analyze with a vision model; result shown with Copy button; 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</li>
|
||||
<li id="wfm-help-ai-5">TOOLS pane — Wildcards: select "Create wildcards" from the dropdown, enter a category name and count, click Run to generate plain-text wildcard entries one per line</li>
|
||||
<li id="wfm-help-ai-6">Settings pane: choose backend (Ollama / LM Studio / Lemonade), set API URL, test connection, select model, 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</li>
|
||||
<li id="wfm-help-ai-7">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</li>
|
||||
</ul>
|
||||
<h3 id="wfm-help-ai-card-layout-title">Layout</h3>
|
||||
<p id="wfm-help-ai-1">4-pane layout — Translation | Chat | TOOLS | Settings — all panes are always visible simultaneously</p>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-ai-card-translate-title">Translation pane</h3>
|
||||
<p id="wfm-help-ai-2">Translation pane: translate text between Japanese, English, Chinese, or a custom Free language using Ollama, LM Studio, or Lemonade; ⇄ swap button swaps both language selectors and text content; selections are saved automatically</p>
|
||||
</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>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-ai-card-svg-title">Chat pane — SVG generation</h3>
|
||||
<p id="wfm-help-ai-7">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</p>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-ai-card-skills-title">Chat pane — Skills</h3>
|
||||
<p id="wfm-help-ai-8">Chat pane — Skills: pick a skill from the dropdown above the message list to prepend its instructions to the conversation as a system prompt (not shown in the visible chat log); use the pencil button to open the skill manager and create, edit, or delete .md skill files, stored in user/default/Workflow-Studio/ai_skills/; a file's first line block between --- markers can define name/description shown in the dropdown; two starter skills are included by default: "SVG Icon Generator" (decomposes the subject into parts before drawing, for more recognizable SVG output) and "Skill Creator" (interviews you conversationally and drafts a new skill file, ready to save); when a reply contains a ```skill code block, a "Save as new skill" button appears below it that opens the manager with the draft pre-filled</p>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-ai-card-skilltips-title">Writing your own skills</h3>
|
||||
<p id="wfm-help-ai-9">Tips for writing your own skills: give a clear frontmatter name/description so it reads well in the dropdown and tooltip; state the exact output format you want and, if there is a deliverable to extract later (like SVG or another skill file), wrap it in a distinctive fenced code block; write concrete, imperative instructions rather than vague ones - smaller local models follow explicit rules and a short worked example far better than general guidance; state hard constraints (forbidden content, security, length limits) explicitly rather than assuming they are implied; when in doubt, select the "Skill Creator" skill in the dropdown and describe what you want - it will interview you and draft the file for you</p>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-ai-card-tools-title">TOOLS pane</h3>
|
||||
<p id="wfm-help-ai-4">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</p>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-ai-card-settings-title">Settings pane</h3>
|
||||
<p id="wfm-help-ai-6">Settings pane: choose backend (Ollama / LM Studio / Lemonade), set API URL, test connection, select model, 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</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wfm-help-page" id="wfm-help-page-tagger">
|
||||
<div class="wfm-help-article-header">
|
||||
<h2 id="wfm-help-tagger-title">Tagger Tab</h2>
|
||||
<button type="button" class="wfm-btn wfm-btn-sm wfm-help-jump-btn" data-jump-tab="tagger">Open tab →</button>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-tagger-title">Tagger Tab</h3>
|
||||
<h3 id="wfm-help-tagger-card-overview-title">Overview & Setup</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-tagger-1"></li>
|
||||
<li id="wfm-help-tagger-2"></li>
|
||||
<li id="wfm-help-tagger-3"></li>
|
||||
<li id="wfm-help-tagger-4"></li>
|
||||
<li id="wfm-help-tagger-5"></li>
|
||||
<li id="wfm-help-tagger-6"></li>
|
||||
<li id="wfm-help-tagger-7"></li>
|
||||
<li id="wfm-help-tagger-8"></li>
|
||||
<li id="wfm-help-tagger-9"></li>
|
||||
<li id="wfm-help-tagger-1">Automatic image tagging using WD Tagger (ONNX), SwinV2, DeepDanbooru (.h5), and Ollama VLM — 3 sub-tabs: Single / Batch / DB</li>
|
||||
<li id="wfm-help-tagger-2">Model placement: put each model in its own subfolder under ComfyUI/models/tagger/<model-name>/; the folder must contain a .onnx file + selected_tags.csv (or .h5 + label file for DeepDanbooru)</li>
|
||||
<li id="wfm-help-tagger-9">Install dependencies into ComfyUI's embedded Python: python_embeded\python.exe -m pip install -r requirements.txt; for GPU ONNX inference install onnxruntime-gpu instead of onnxruntime; TensorFlow (for DeepDanbooru) is commented out by default due to size</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-tagger-card-single-title">Single Tab</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-tagger-3">Single tab: drag & drop an image onto the preview area, or click Upload to pick a file; the Tagger button in the Gallery detail panel opens the selected image directly here</li>
|
||||
<li id="wfm-help-tagger-4">Threshold slider: minimum confidence score for general tags (default 0.35); Character Threshold: separate threshold for character: prefix tags (default 0.85)</li>
|
||||
<li id="wfm-help-tagger-5">Ollama VLM: enable the checkbox to run a vision model alongside WD Tagger; set API URL, select model (click ↻ to refresh), and customize the prompt; results from both models are merged</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-tagger-card-sending-title">Sending Results</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-tagger-6">Result tags can be sent to: (1) GenUI:P — appended to the GenerateUI tab's positive prompt textarea and applied to the loaded workflow immediately; (2) Send to Prompt — appended to the Prompt tab's positive textarea; (3) Save to Gallery — saved to Gallery image metadata (requires image opened from Gallery); (4) Write to File — embedded into JPEG EXIF (ImageDescription) or PNG tEXt (Tags key); (5) Save to DB — stored in the internal SQLite database</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-tagger-card-batch-title">Batch & DB Tabs</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-tagger-7">Batch tab: enter a folder path, configure WD Tagger and Ollama settings; output options: Save to DB (default on), Write to File (EXIF/PNG metadata), Write .txt (creates <filename>.txt alongside each image); click Start Batch to process all images in the folder; progress bar and log are updated in real time; Stop cancels after the current image</li>
|
||||
<li id="wfm-help-tagger-8">DB tab: searchable database of all tagged images; click a row to open the edit panel and modify WD Tags / VLM Tags; Save updates the record, Delete removes it; Export CSV downloads all records as a CSV file</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wfm-help-page" id="wfm-help-page-imageedit">
|
||||
<div class="wfm-help-article-header">
|
||||
<h2 id="wfm-help-imageedit-title">Image Edit Tab</h2>
|
||||
<button type="button" class="wfm-btn wfm-btn-sm wfm-help-jump-btn" data-jump-tab="image-edit">Open tab →</button>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-imageedit-title">Image Edit Tab</h3>
|
||||
<h3 id="wfm-help-imageedit-card-overview-title">Overview</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-imageedit-1">Layer-based image editor — compose images with multiple layers (Image, Text, Draw, Mask types) and export the composite</li>
|
||||
<li id="wfm-help-imageedit-2">Loading images: drag & drop onto the canvas, Upload button, or send from the Gallery tab via the Image Edit button in the Gallery toolbar; the first image becomes Layer 1 (auto-locked); subsequent images are added as new layers scaled to fit the canvas</li>
|
||||
@@ -1435,8 +1665,11 @@
|
||||
</div>
|
||||
|
||||
<div class="wfm-help-page" id="wfm-help-page-shortcuts">
|
||||
<div class="wfm-help-article-header">
|
||||
<h2 id="wfm-help-shortcuts-title">Keyboard Shortcuts</h2>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-shortcuts-title">Keyboard Shortcuts</h3>
|
||||
<h3 id="wfm-help-shortcuts-card-general-title">General</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-shortcuts-1">Escape: Close modal/panel</li>
|
||||
<li id="wfm-help-shortcuts-2">Enter: Confirm rename, send chat</li>
|
||||
@@ -1460,17 +1693,30 @@
|
||||
</div>
|
||||
|
||||
<div class="wfm-help-page" id="wfm-help-page-trouble">
|
||||
<div class="wfm-help-article-header">
|
||||
<h2 id="wfm-help-trouble-title">Troubleshooting</h2>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-trouble-title">Troubleshooting</h3>
|
||||
<h3 id="wfm-help-trouble-card-connection-title">Connection Issues</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-trouble-1">Cannot connect to ComfyUI: Check if ComfyUI can generate images, verify URL in Settings, try refreshing or restarting</li>
|
||||
<li id="wfm-help-trouble-2">Cannot connect to Ollama: Use connection test button, check if Ollama is running and models are downloaded</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-trouble-card-workflow-title">Workflow & Generation Issues</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-trouble-3">Workflows not showing: Click refresh button, check if loadable in ComfyUI, try restarting browser/ComfyUI</li>
|
||||
<li id="wfm-help-trouble-4">Image generation fails: Check if specified models exist in ComfyUI, verify connection, try restarting</li>
|
||||
<li id="wfm-help-trouble-5">LoRA Stack fails with "No such file or directory" error: The LoRA may not be recognized by comfyui-lora-manager (known issue). Try a different LoRA or check if the file appears in Lora Manager's own UI</li>
|
||||
<li id="wfm-help-trouble-6">Model loading error after moving or deleting models: ComfyUI caches file paths and may still reference old locations. Perform a Hard Reset from the ComfyUI menu (Queue → Hard Reset) or restart ComfyUI to clear the cache.</li>
|
||||
<li id="wfm-help-trouble-7"></li>
|
||||
<li id="wfm-help-trouble-8"></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-trouble-card-tagger-title">Tagger & Model Issues</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-trouble-7">Tagger tab shows no models: verify the model folder exists at ComfyUI/models/tagger/<model-name>/ and contains a .onnx or .h5 file; if onnxruntime is not installed, run: python_embeded\python.exe -m pip install onnxruntime</li>
|
||||
<li id="wfm-help-trouble-8">Model tab shows a checkpoint you didn't expect (e.g. always the same model regardless of the loaded workflow): the checkpoint saved in the workflow file is not installed, so it was silently replaced with another available model during loading — an error toast reports the original (missing) model name when this happens. Select the correct checkpoint manually in the Model tab, or enable "Default Checkpoint" in Settings to force a known-good model on every load.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2002,6 +2248,31 @@
|
||||
<!-- Chat pane -->
|
||||
<div class="wfm-ai-pane wfm-ai-pane-chat">
|
||||
<div class="wfm-ai-pane-header">Chat</div>
|
||||
<div class="wfm-ai-chat-skill-row">
|
||||
<select id="wfm-ai-chat-skill-select" class="wfm-select wfm-ai-chat-skill-select">
|
||||
<option value="">-- No skill --</option>
|
||||
</select>
|
||||
<button id="wfm-ai-chat-skill-manage-btn" class="wfm-btn wfm-btn-sm" title="Manage skills">✎</button>
|
||||
</div>
|
||||
<div id="wfm-ai-skill-panel" class="wfm-ai-skill-panel" style="display:none;">
|
||||
<div class="wfm-ai-skill-panel-header">
|
||||
<span class="wfm-ai-skill-panel-title">Skills</span>
|
||||
<div style="display:flex;gap:4px;">
|
||||
<button id="wfm-ai-skill-new-btn" class="wfm-btn wfm-btn-sm wfm-btn-primary">+ New</button>
|
||||
<button id="wfm-ai-skill-close-btn" class="wfm-btn wfm-btn-sm" title="Close">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="wfm-ai-skill-list" class="wfm-wc-file-list"></div>
|
||||
<div id="wfm-ai-skill-editor" class="wfm-wc-editor" style="display:none;">
|
||||
<input type="text" id="wfm-ai-skill-editor-filename" class="wfm-input" placeholder="filename.md">
|
||||
<textarea id="wfm-ai-skill-editor-content" class="wfm-textarea wfm-wc-editor-ta" rows="10" placeholder="--- name: My Skill description: What this skill does --- System prompt instructions for the LLM..."></textarea>
|
||||
<div class="wfm-wc-editor-actions">
|
||||
<button id="wfm-ai-skill-editor-save-btn" class="wfm-btn wfm-btn-sm wfm-btn-primary">Save</button>
|
||||
<button id="wfm-ai-skill-editor-delete-btn" class="wfm-btn wfm-btn-sm wfm-btn-danger" style="display:none;">Delete</button>
|
||||
<button id="wfm-ai-skill-editor-cancel-btn" class="wfm-btn wfm-btn-sm" style="margin-left:auto;">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wfm-ai-chat-container">
|
||||
<div class="wfm-ai-chat-messages" id="wfm-ai-chat-messages"></div>
|
||||
<div class="wfm-ai-chat-input-area">
|
||||
|
||||
+754
-12
@@ -2394,6 +2394,461 @@ async function _readAllPNGTextChunks(file) {
|
||||
return chunks;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// UI→API変換(static/js/comfyui-workflow.js の convertUiToApi() 相当ロジックの複製)
|
||||
// web/comfyui/ は static/js/ と配信URLが異なりESモジュールをimportできないため、
|
||||
// AI backend URL解決等の既存パターンと同じくローカルに複製している。
|
||||
// サブグラフ(definitions.subgraphs)を含むワークフローは、外側の折りたたみ表示
|
||||
// ウィジェット値(実際にユーザーが編集する値)が定義テンプレート内のノードの
|
||||
// widgets_valuesへ反映されているとは限らないため、この変換を経由してから
|
||||
// モデル名・プロンプトを抽出する。ロジックを変更する場合は
|
||||
// static/js/comfyui-workflow.js の convertUiToApi()/_flattenSubgraphs() 側も
|
||||
// 同期すること。GenerateUIタブと異なりIタブは読み取り専用表示のため、
|
||||
// Checkpoint代替やBypass/Muteノードの記録機能は省いている。
|
||||
// ============================================
|
||||
|
||||
let _wfmObjectInfoCache = null;
|
||||
async function _wfmLoadObjectInfo() {
|
||||
if (_wfmObjectInfoCache) return _wfmObjectInfoCache;
|
||||
try {
|
||||
const res = await fetch("/object_info");
|
||||
if (res.ok) _wfmObjectInfoCache = await res.json();
|
||||
} catch {}
|
||||
return _wfmObjectInfoCache || {};
|
||||
}
|
||||
|
||||
function _wfmResolveBypassSource(nodeById, linkMap, nodeId, slot, visited) {
|
||||
const idStr = String(nodeId);
|
||||
const node = nodeById[idStr];
|
||||
if (!node || node.mode !== 4) return [idStr, slot];
|
||||
if (visited.has(idStr)) return null;
|
||||
visited.add(idStr);
|
||||
const outputs = node.outputs || [];
|
||||
const inputs = node.inputs || [];
|
||||
const outType = outputs[slot]?.type;
|
||||
let inIdx = -1;
|
||||
if (inputs[slot]?.type === outType && linkMap[idStr]?.[slot]) {
|
||||
inIdx = slot;
|
||||
} else {
|
||||
inIdx = inputs.findIndex((inp, i) => inp.type === outType && linkMap[idStr]?.[i]);
|
||||
}
|
||||
if (inIdx === -1) return null;
|
||||
const upstream = linkMap[idStr][inIdx];
|
||||
if (!upstream) return null;
|
||||
return _wfmResolveBypassSource(nodeById, linkMap, upstream[0], upstream[1], visited);
|
||||
}
|
||||
|
||||
function _wfmGetWidgetInputNames(objectInfo, classType) {
|
||||
const info = objectInfo[classType];
|
||||
if (!info) return [];
|
||||
const names = [];
|
||||
const required = info.input?.required || {};
|
||||
const optional = info.input?.optional || {};
|
||||
for (const [name, spec] of Object.entries(required)) {
|
||||
const type = Array.isArray(spec) ? spec[0] : spec;
|
||||
if (Array.isArray(type)) {
|
||||
names.push(name);
|
||||
} else if (typeof type === "string") {
|
||||
const upper = type.toUpperCase();
|
||||
if (upper === "INT" || upper === "FLOAT" || upper === "STRING" || upper === "BOOLEAN" || upper === "COMBO" || upper === "COMFY_DYNAMICCOMBO_V3") {
|
||||
names.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const [name, spec] of Object.entries(optional)) {
|
||||
const type = Array.isArray(spec) ? spec[0] : spec;
|
||||
if (Array.isArray(type)) {
|
||||
names.push(name);
|
||||
} else if (typeof type === "string") {
|
||||
const upper = type.toUpperCase();
|
||||
if (upper === "INT" || upper === "FLOAT" || upper === "STRING" || upper === "BOOLEAN" || upper === "COMFY_DYNAMICCOMBO_V3") {
|
||||
names.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
function _wfmGetWidgetInputTypes(objectInfo, classType) {
|
||||
const info = objectInfo[classType];
|
||||
if (!info) return [];
|
||||
const types = [];
|
||||
const required = info.input?.required || {};
|
||||
const optional = info.input?.optional || {};
|
||||
for (const [name, spec] of Object.entries(required)) {
|
||||
const type = Array.isArray(spec) ? spec[0] : spec;
|
||||
if (Array.isArray(type)) {
|
||||
types.push("COMBO");
|
||||
} else if (typeof type === "string") {
|
||||
const upper = type.toUpperCase();
|
||||
if (upper === "INT" || upper === "FLOAT" || upper === "STRING" || upper === "BOOLEAN" || upper === "COMBO" || upper === "COMFY_DYNAMICCOMBO_V3") {
|
||||
types.push(upper);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const [name, spec] of Object.entries(optional)) {
|
||||
const type = Array.isArray(spec) ? spec[0] : spec;
|
||||
if (Array.isArray(type)) {
|
||||
types.push("COMBO");
|
||||
} else if (typeof type === "string") {
|
||||
const upper = type.toUpperCase();
|
||||
if (upper === "INT" || upper === "FLOAT" || upper === "STRING" || upper === "BOOLEAN" || upper === "COMBO" || upper === "COMFY_DYNAMICCOMBO_V3") {
|
||||
types.push(upper);
|
||||
}
|
||||
}
|
||||
}
|
||||
return types;
|
||||
}
|
||||
|
||||
function _wfmGetDynamicComboSubNames(objectInfo, classType, widgetName, selectedKey) {
|
||||
const info = objectInfo[classType];
|
||||
const spec = info?.input?.required?.[widgetName] || info?.input?.optional?.[widgetName];
|
||||
const options = spec?.[1]?.options;
|
||||
if (!Array.isArray(options)) return [];
|
||||
const opt = options.find((o) => o.key === selectedKey) || options[0];
|
||||
return Object.keys(opt?.inputs?.required || {});
|
||||
}
|
||||
|
||||
const _WFM_CONTROL_AFTER_GENERATE = new Set(["fixed", "increment", "decrement", "randomize"]);
|
||||
function _wfmIsExtraWidgetValue(val, expectedType) {
|
||||
if (expectedType === "INT" || expectedType === "FLOAT") {
|
||||
if (typeof val === "string" && _WFM_CONTROL_AFTER_GENERATE.has(val)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function _wfmFlattenSubgraphs(workflow, objectInfo) {
|
||||
const defs = workflow.definitions?.subgraphs;
|
||||
if (!defs || defs.length === 0) return workflow;
|
||||
const subgraphMap = {};
|
||||
for (const sg of defs) subgraphMap[sg.id] = sg;
|
||||
if (!workflow.nodes.some(n => subgraphMap[n.type])) return workflow;
|
||||
|
||||
const wf = JSON.parse(JSON.stringify(workflow));
|
||||
const sgDefs = {};
|
||||
for (const sg of wf.definitions?.subgraphs || []) sgDefs[sg.id] = sg;
|
||||
|
||||
let maxNodeId = 0;
|
||||
let maxLinkId = 0;
|
||||
for (const n of wf.nodes) if (n.id > maxNodeId) maxNodeId = n.id;
|
||||
for (const l of wf.links) if (l[0] > maxLinkId) maxLinkId = l[0];
|
||||
|
||||
const addedNodes = [];
|
||||
const addedLinks = [];
|
||||
const nodesToRemove = new Set();
|
||||
|
||||
for (const node of wf.nodes) {
|
||||
const sgDef = sgDefs[node.type];
|
||||
if (!sgDef) continue;
|
||||
nodesToRemove.add(node.id);
|
||||
|
||||
const nodeIdRemap = {};
|
||||
for (const iNode of sgDef.nodes) {
|
||||
nodeIdRemap[iNode.id] = `${node.id}:${iNode.id}`;
|
||||
}
|
||||
|
||||
const inputPortTargets = {};
|
||||
for (let portIdx = 0; portIdx < (sgDef.inputs || []).length; portIdx++) {
|
||||
const port = sgDef.inputs[portIdx];
|
||||
for (const linkId of (port.linkIds || [])) {
|
||||
const iLink = (sgDef.links || []).find(l => l.id === linkId);
|
||||
if (iLink && iLink.origin_id === -10) {
|
||||
inputPortTargets[portIdx] = {
|
||||
targetNodeId: nodeIdRemap[iLink.target_id],
|
||||
targetSlot: iLink.target_slot,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const outputPortSources = {};
|
||||
for (let portIdx = 0; portIdx < (sgDef.outputs || []).length; portIdx++) {
|
||||
const port = sgDef.outputs[portIdx];
|
||||
for (const linkId of (port.linkIds || [])) {
|
||||
const iLink = (sgDef.links || []).find(l => l.id === linkId);
|
||||
if (iLink && iLink.target_id === -20) {
|
||||
outputPortSources[portIdx] = {
|
||||
sourceNodeId: nodeIdRemap[iLink.origin_id],
|
||||
sourceSlot: iLink.origin_slot,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const redirectedTargets = new Set();
|
||||
for (const link of wf.links) {
|
||||
if (link[3] === node.id) {
|
||||
const dstSlot = link[4];
|
||||
const inputName = node.inputs?.[dstSlot]?.name;
|
||||
let portIdx = inputName
|
||||
? (sgDef.inputs || []).findIndex(p => p.name === inputName)
|
||||
: -1;
|
||||
if (portIdx === -1) portIdx = dstSlot;
|
||||
const target = inputPortTargets[portIdx];
|
||||
if (target) {
|
||||
link[3] = target.targetNodeId;
|
||||
link[4] = target.targetSlot;
|
||||
redirectedTargets.add(`${target.targetNodeId}:${target.targetSlot}`);
|
||||
}
|
||||
}
|
||||
if (link[1] === node.id) {
|
||||
const srcSlot = link[2];
|
||||
const source = outputPortSources[srcSlot];
|
||||
if (source) {
|
||||
link[1] = source.sourceNodeId;
|
||||
link[2] = source.sourceSlot;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const remappedByOrigId = {};
|
||||
const normalizedNodeIds = new Set();
|
||||
for (const iNode of sgDef.nodes) {
|
||||
const remapped = JSON.parse(JSON.stringify(iNode));
|
||||
remapped.id = nodeIdRemap[iNode.id];
|
||||
|
||||
const allWidgetInputs = (remapped.inputs || []).filter((i) => i.widget);
|
||||
if (remapped.widgets_values && remapped.widgets_values.length === allWidgetInputs.length) {
|
||||
const linkedIdx = new Set();
|
||||
allWidgetInputs.forEach((inp, i) => {
|
||||
const slotIdx = remapped.inputs.indexOf(inp);
|
||||
if (redirectedTargets.has(`${remapped.id}:${slotIdx}`)) linkedIdx.add(i);
|
||||
});
|
||||
if (linkedIdx.size > 0) {
|
||||
remapped.widgets_values = remapped.widgets_values.filter((_, i) => !linkedIdx.has(i));
|
||||
normalizedNodeIds.add(remapped.id);
|
||||
}
|
||||
}
|
||||
|
||||
addedNodes.push(remapped);
|
||||
remappedByOrigId[iNode.id] = remapped;
|
||||
}
|
||||
|
||||
const outerWidgetInputs = (node.inputs || []).filter((inp) => inp.widget);
|
||||
outerWidgetInputs.forEach((inp, widgetIdx) => {
|
||||
if (inp.link != null) return;
|
||||
if (widgetIdx >= (node.widgets_values || []).length) return;
|
||||
const outerValue = node.widgets_values[widgetIdx];
|
||||
|
||||
const portIdx = (sgDef.inputs || []).findIndex((p) => p.name === inp.name);
|
||||
if (portIdx === -1) return;
|
||||
const target = inputPortTargets[portIdx];
|
||||
if (!target) return;
|
||||
|
||||
const origInternalId = String(target.targetNodeId).split(":").pop();
|
||||
const remappedNode = remappedByOrigId[origInternalId];
|
||||
if (!remappedNode) return;
|
||||
|
||||
const targetInputDef = (remappedNode.inputs || [])[target.targetSlot];
|
||||
if (!targetInputDef?.widget) return;
|
||||
|
||||
let iWidgetIdx;
|
||||
if (normalizedNodeIds.has(remappedNode.id)) {
|
||||
const remainingNames = (remappedNode.inputs || [])
|
||||
.filter((i) => i.widget)
|
||||
.filter((i) => {
|
||||
const slotIdx = remappedNode.inputs.indexOf(i);
|
||||
return !redirectedTargets.has(`${remappedNode.id}:${slotIdx}`);
|
||||
})
|
||||
.map((i) => i.name);
|
||||
iWidgetIdx = remainingNames.indexOf(targetInputDef.name);
|
||||
} else {
|
||||
const widgetNames = _wfmGetWidgetInputNames(objectInfo, remappedNode.type);
|
||||
iWidgetIdx = widgetNames.indexOf(targetInputDef.name);
|
||||
}
|
||||
if (iWidgetIdx === -1) return;
|
||||
|
||||
if (!remappedNode.widgets_values) remappedNode.widgets_values = [];
|
||||
remappedNode.widgets_values[iWidgetIdx] = outerValue;
|
||||
});
|
||||
|
||||
for (const iLink of (sgDef.links || [])) {
|
||||
if (iLink.origin_id === -10 || iLink.target_id === -20) continue;
|
||||
maxLinkId++;
|
||||
addedLinks.push([
|
||||
maxLinkId,
|
||||
nodeIdRemap[iLink.origin_id],
|
||||
iLink.origin_slot,
|
||||
nodeIdRemap[iLink.target_id],
|
||||
iLink.target_slot,
|
||||
iLink.type,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
wf.nodes = [...wf.nodes.filter(n => !nodesToRemove.has(n.id)), ...addedNodes];
|
||||
wf.links = [
|
||||
...wf.links.filter(l => !nodesToRemove.has(l[1]) && !nodesToRemove.has(l[3])),
|
||||
...addedLinks,
|
||||
];
|
||||
|
||||
delete wf.definitions;
|
||||
return wf;
|
||||
}
|
||||
|
||||
function _wfmIsDisplayOnlyNode(node) {
|
||||
const hasConnectedOutput = node.outputs?.some(o => o.links && o.links.length > 0);
|
||||
const hasConnectedInput = node.inputs?.some(i => i.link != null);
|
||||
return !hasConnectedOutput && !hasConnectedInput;
|
||||
}
|
||||
|
||||
function _wfmGetWidgetMapping(nodeType) {
|
||||
const mappings = {
|
||||
CheckpointLoaderSimple: ["ckpt_name"],
|
||||
KSampler: ["seed", "steps", "cfg", "sampler_name", "scheduler", "denoise"],
|
||||
CLIPTextEncode: ["text"],
|
||||
CLIPTextEncodeEditPlus: ["text_edit", "mode"],
|
||||
ImageMetadataCheckpointLoader: ["ckpt_name", "vae_name", "_metadata_json"],
|
||||
ImageMetadataPromptLoader: ["ckpt_name", "vae_name", "positive_text", "negative_text", "_metadata_json"],
|
||||
ImageMetadataLoRALoader: ["lora_1", "strength_model_1", "strength_clip_1", "lora_2", "strength_model_2", "strength_clip_2", "lora_3", "strength_model_3", "strength_clip_3"],
|
||||
EmptyLatentImage: ["width", "height", "batch_size"],
|
||||
LoraLoader: ["lora_name", "strength_model", "strength_clip"],
|
||||
VAELoader: ["vae_name"],
|
||||
SaveImage: ["filename_prefix"],
|
||||
LoadImage: ["image", "upload"],
|
||||
UNETLoader: ["unet_name", "weight_dtype"],
|
||||
LoaderGGUF: ["gguf_name"],
|
||||
LoaderGGUFAdvanced: ["gguf_name", "dequant_dtype", "patch_dtype", "patch_on_device"],
|
||||
ResolutionSelector: ["aspect_ratio", "megapixels"],
|
||||
EmptySD3LatentImage: ["width", "height", "batch_size"],
|
||||
CLIPLoader: ["clip_name", "type", "device"],
|
||||
ClipLoaderGGUF: ["clip_name", "type"],
|
||||
DualClipLoaderGGUF: ["clip_name1", "clip_name2", "type"],
|
||||
FluxGuidance: ["guidance"],
|
||||
CFGNorm: ["strength"],
|
||||
ImpactWildcardProcessor: ["wildcard_text"],
|
||||
ImpactWildcardEncode: ["wildcard_text"],
|
||||
WFS_PromptText: ["positive", "negative"],
|
||||
};
|
||||
return mappings[nodeType] || null;
|
||||
}
|
||||
|
||||
async function _wfmConvertUiToApi(workflow) {
|
||||
if (!workflow.nodes || !workflow.links) return {};
|
||||
|
||||
const objectInfo = await _wfmLoadObjectInfo();
|
||||
const flatWorkflow = _wfmFlattenSubgraphs(workflow, objectInfo);
|
||||
|
||||
const linkMap = {};
|
||||
for (const link of flatWorkflow.links) {
|
||||
const [id, srcNode, srcSlot, dstNode, dstSlot] = link;
|
||||
if (!linkMap[dstNode]) linkMap[dstNode] = {};
|
||||
linkMap[dstNode][dstSlot] = [String(srcNode), srcSlot];
|
||||
}
|
||||
const nodeById = {};
|
||||
for (const n of flatWorkflow.nodes) nodeById[String(n.id)] = n;
|
||||
|
||||
const api = {};
|
||||
for (const node of flatWorkflow.nodes) {
|
||||
if (node.mode === 2 || node.mode === 4) continue;
|
||||
if (!objectInfo[node.type] && _wfmIsDisplayOnlyNode(node)) continue;
|
||||
const nodeId = String(node.id);
|
||||
const inputs = {};
|
||||
|
||||
const nodeLinks = linkMap[node.id] || {};
|
||||
const inputDefs = node.inputs || [];
|
||||
const linkedInputNames = new Set();
|
||||
const linkedSlotNames = new Set();
|
||||
inputDefs.forEach((inp, idx) => {
|
||||
if (nodeLinks[idx]) {
|
||||
const resolved = _wfmResolveBypassSource(nodeById, linkMap, nodeLinks[idx][0], nodeLinks[idx][1], new Set());
|
||||
if (!resolved) return;
|
||||
inputs[inp.name] = resolved;
|
||||
linkedInputNames.add(inp.name);
|
||||
linkedSlotNames.add(inp.name);
|
||||
}
|
||||
});
|
||||
|
||||
const widgets = node.widgets_values || [];
|
||||
if (widgets.length > 0) {
|
||||
const lmWidgetIds = node.properties?.__lm_widget_ids;
|
||||
if (lmWidgetIds && Array.isArray(lmWidgetIds)) {
|
||||
lmWidgetIds.forEach((name, idx) => {
|
||||
if (idx >= widgets.length || linkedInputNames.has(name)) return;
|
||||
let val = widgets[idx];
|
||||
if (name === "loras" && Array.isArray(val)) {
|
||||
val = { "__value__": val };
|
||||
}
|
||||
inputs[name] = val;
|
||||
});
|
||||
} else {
|
||||
const widgetNames = _wfmGetWidgetInputNames(objectInfo, node.type);
|
||||
if (widgetNames.length > 0) {
|
||||
const widgetTypes = _wfmGetWidgetInputTypes(objectInfo, node.type);
|
||||
let wIdx = 0;
|
||||
for (let nIdx = 0; nIdx < widgetNames.length; nIdx++) {
|
||||
if (wIdx >= widgets.length) break;
|
||||
const name = widgetNames[nIdx];
|
||||
const expectedType = widgetTypes[nIdx];
|
||||
if (linkedInputNames.has(name)) {
|
||||
if (!linkedSlotNames.has(name)) {
|
||||
wIdx++;
|
||||
if (wIdx < widgets.length && _wfmIsExtraWidgetValue(widgets[wIdx], expectedType)) {
|
||||
wIdx++;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let val = widgets[wIdx];
|
||||
wIdx++;
|
||||
if (_wfmIsExtraWidgetValue(val, expectedType)) {
|
||||
if (wIdx < widgets.length) {
|
||||
val = widgets[wIdx];
|
||||
wIdx++;
|
||||
}
|
||||
}
|
||||
if (expectedType === "COMFY_DYNAMICCOMBO_V3") {
|
||||
inputs[name] = val;
|
||||
const subNames = _wfmGetDynamicComboSubNames(objectInfo, node.type, name, val);
|
||||
for (const subName of subNames) {
|
||||
if (wIdx >= widgets.length) break;
|
||||
inputs[`${name}.${subName}`] = widgets[wIdx];
|
||||
wIdx++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (expectedType === "COMBO") {
|
||||
const allInputDefs = {
|
||||
...(objectInfo[node.type]?.input?.required || {}),
|
||||
...(objectInfo[node.type]?.input?.optional || {}),
|
||||
};
|
||||
const spec = allInputDefs[name];
|
||||
if (spec) {
|
||||
const choices = Array.isArray(spec[0]) ? spec[0] : null;
|
||||
if (choices && choices.length > 0 && !choices.includes(val)) {
|
||||
val = choices[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
inputs[name] = val;
|
||||
}
|
||||
} else {
|
||||
const mapping = _wfmGetWidgetMapping(node.type);
|
||||
if (mapping) {
|
||||
mapping.forEach((key, idx) => {
|
||||
if (idx < widgets.length && key && !(key in inputs)) {
|
||||
inputs[key] = widgets[idx];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((node.type === "ImpactWildcardEncode" || node.type === "ImpactWildcardProcessor")
|
||||
&& widgets.length > 0) {
|
||||
inputs["wildcard_text"] = widgets[0];
|
||||
}
|
||||
|
||||
api[nodeId] = {
|
||||
class_type: node.type,
|
||||
inputs,
|
||||
_meta: { title: node.title || node.type },
|
||||
};
|
||||
}
|
||||
return api;
|
||||
}
|
||||
|
||||
function _collectUnique(arr) {
|
||||
const seen = new Set(), out = [];
|
||||
for (const v of arr) { if (v && typeof v === "string" && !seen.has(v)) { seen.add(v); out.push(v); } }
|
||||
@@ -2522,11 +2977,22 @@ function _extractPromptsFromNodeSet(nodes, links) {
|
||||
if (!inp || inp.link == null) continue;
|
||||
const originId = linkOrigin.get(inp.link);
|
||||
if (originId == null) continue;
|
||||
const name = inp.name ?? "";
|
||||
const isPos = name === "positive" || name.startsWith("positive");
|
||||
const isNeg = name === "negative" || name.startsWith("negative");
|
||||
if (!isPos && !isNeg) continue;
|
||||
// TextEncodeMageFlowEdit — 1ノードでprompt/negative_prompt両方を直接持つため、
|
||||
// textMap(1ノード1テキスト)では両ロールを表現できない
|
||||
const srcNode = nodeMap.get(originId);
|
||||
if (srcNode?.type === "TextEncodeMageFlowEdit") {
|
||||
const widgetIdx = isPos ? 0 : 1; // widgets_values: [prompt, negative_prompt, ...]
|
||||
const txt = srcNode.widgets_values?.[widgetIdx];
|
||||
if (txt && typeof txt === "string") { if (isPos) pos.add(txt); else neg.add(txt); }
|
||||
continue;
|
||||
}
|
||||
const txt = textMap.get(originId);
|
||||
if (!txt) continue;
|
||||
const name = inp.name ?? "";
|
||||
if (name === "positive" || name.startsWith("positive")) pos.add(txt);
|
||||
else if (name === "negative" || name.startsWith("negative")) neg.add(txt);
|
||||
if (isPos) pos.add(txt); else neg.add(txt);
|
||||
}
|
||||
}
|
||||
if (!foundSampler) return null;
|
||||
@@ -2605,10 +3071,20 @@ function _extractPromptsAPI(wf) {
|
||||
foundSampler = true;
|
||||
for (const [key, val] of Object.entries(n.inputs ?? {})) {
|
||||
if (!Array.isArray(val)) continue;
|
||||
const isPos = key === "positive" || key.startsWith("positive");
|
||||
const isNeg = key === "negative" || key.startsWith("negative");
|
||||
if (!isPos && !isNeg) continue;
|
||||
// TextEncodeMageFlowEdit — 1ノードでprompt/negative_prompt両方を直接持つため、
|
||||
// textMap(1ノード1テキスト)では両ロールを表現できない
|
||||
const srcNode = wf[String(val[0])];
|
||||
if (srcNode?.class_type === "TextEncodeMageFlowEdit") {
|
||||
const txt = isPos ? srcNode.inputs?.prompt : srcNode.inputs?.negative_prompt;
|
||||
if (txt && typeof txt === "string") { if (isPos) pos.add(txt); else neg.add(txt); }
|
||||
continue;
|
||||
}
|
||||
const txt = textMap.get(String(val[0]));
|
||||
if (!txt) continue;
|
||||
if (key === "positive" || key.startsWith("positive")) pos.add(txt);
|
||||
else if (key === "negative" || key.startsWith("negative")) neg.add(txt);
|
||||
if (isPos) pos.add(txt); else neg.add(txt);
|
||||
}
|
||||
}
|
||||
if (!foundSampler || (pos.size === 0 && neg.size === 0)) { const all = [...textMap.values()].filter(t => t && t.trim()); return { positives: [], negatives: [], texts: all }; }
|
||||
@@ -2695,9 +3171,20 @@ async function _extractAllMetadata(file) {
|
||||
const isJSON = file.type === "application/json" || name.endsWith(".json");
|
||||
const isWebP = file.type === "image/webp" || name.endsWith(".webp");
|
||||
|
||||
function fromWorkflow(wf, source) {
|
||||
async function fromWorkflow(originalWf, source) {
|
||||
let wf = originalWf;
|
||||
// サブグラフを持つワークフローは、外側の折りたたみ表示ウィジェット値(実際に
|
||||
// ユーザーが見ている値)がテンプレート内のノードのwidgets_valuesへ反映されて
|
||||
// いないことがあるため、_wfmConvertUiToApi() で正規化してから抽出する。
|
||||
if (Array.isArray(wf?.nodes) && wf.definitions?.subgraphs?.length > 0) {
|
||||
try {
|
||||
const apiWf = await _wfmConvertUiToApi(wf);
|
||||
if (apiWf && Object.keys(apiWf).length > 0) wf = apiWf;
|
||||
} catch { /* 変換失敗時は元のUI形式のまま抽出(既存ロジックにフォールバック) */ }
|
||||
}
|
||||
const base = { source, checkpoints: _extractCheckpoints(wf), vaes: _extractVAEs(wf), diffusionModels: _extractDiffusionModels(wf), textEncoders: _extractTextEncoders(wf), loras: _extractLoRAs(wf), ..._extractPrompts(wf) };
|
||||
const mdm = _extractMarkdownNoteModels(wf);
|
||||
// MarkdownNote は常に元のUI形式ワークフローから抽出する(API変換後は nodes 情報が失われるため)
|
||||
const mdm = _extractMarkdownNoteModels(originalWf);
|
||||
if (mdm) {
|
||||
if (!base.checkpoints.length) base.checkpoints = mdm.checkpoints;
|
||||
if (!base.vaes.length) base.vaes = mdm.vaes;
|
||||
@@ -2710,19 +3197,19 @@ async function _extractAllMetadata(file) {
|
||||
|
||||
if (isJSON) {
|
||||
let wf; try { wf = JSON.parse(_sanitizeJSON(await file.text())); } catch { return null; }
|
||||
return wf ? fromWorkflow(wf, "comfyui") : null;
|
||||
return wf ? await fromWorkflow(wf, "comfyui") : null;
|
||||
}
|
||||
if (isWebP) {
|
||||
const exif = await _readWebPEXIFChunk(file);
|
||||
if (!exif) return null;
|
||||
const wf = _extractWorkflowFromEXIF(exif);
|
||||
return wf ? fromWorkflow(wf, "comfyui") : null;
|
||||
return wf ? await fromWorkflow(wf, "comfyui") : null;
|
||||
}
|
||||
// PNG
|
||||
const chunks = await _readAllPNGTextChunks(file);
|
||||
if (!chunks) return null;
|
||||
if (chunks.prompt) { let wf; try { wf = JSON.parse(_sanitizeJSON(chunks.prompt)); } catch { return null; } return wf ? fromWorkflow(wf, "comfyui") : null; }
|
||||
if (chunks.workflow) { let wf; try { wf = JSON.parse(_sanitizeJSON(chunks.workflow)); } catch { return null; } return wf ? fromWorkflow(wf, "comfyui") : null; }
|
||||
if (chunks.prompt) { let wf; try { wf = JSON.parse(_sanitizeJSON(chunks.prompt)); } catch { return null; } return wf ? await fromWorkflow(wf, "comfyui") : null; }
|
||||
if (chunks.workflow) { let wf; try { wf = JSON.parse(_sanitizeJSON(chunks.workflow)); } catch { return null; } return wf ? await fromWorkflow(wf, "comfyui") : null; }
|
||||
if (chunks.fooocus_scheme === "fooocus" && chunks.parameters) {
|
||||
const f = _parseFooocusMetadata(chunks.parameters);
|
||||
if (!f) return null;
|
||||
@@ -3116,6 +3603,48 @@ async function aiFetchModels(url, backend) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- AI skills (Chat system prompt library) ----
|
||||
|
||||
async function aiSkillFetchFiles() {
|
||||
try {
|
||||
const r = await fetch("/api/wfm/skills");
|
||||
return r.ok ? await r.json() : [];
|
||||
} catch { return []; }
|
||||
}
|
||||
|
||||
async function aiSkillFetchContent(filename) {
|
||||
try {
|
||||
const r = await fetch(`/api/wfm/skills/content?filename=${encodeURIComponent(filename)}`);
|
||||
const d = await r.json();
|
||||
return d.content ?? null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
async function aiSkillSaveFile(filename, content) {
|
||||
try {
|
||||
const r = await fetch("/api/wfm/skills/save", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ filename, content }),
|
||||
});
|
||||
const d = await r.json();
|
||||
return d.status === "ok" ? d.file : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
async function aiSkillDeleteFile(filename) {
|
||||
try {
|
||||
await fetch("/api/wfm/skills/delete", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ filename }),
|
||||
});
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
function aiSkillStripFrontmatter(content) {
|
||||
const m = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
|
||||
return (m ? content.slice(m[0].length) : content).trim();
|
||||
}
|
||||
|
||||
const renderAiTab = (container) => {
|
||||
if (!container.querySelector(".wfm-nlp-ai-layout")) {
|
||||
container.innerHTML = `
|
||||
@@ -3147,6 +3676,31 @@ const renderAiTab = (container) => {
|
||||
</div>
|
||||
<!-- Chat sub -->
|
||||
<div id="wfm-nlp-ai-chat" class="wfm-nlp-ai-pane" style="display:none;">
|
||||
<div class="wfm-nlp-ai-row" style="flex-shrink:0;">
|
||||
<select id="wfm-nlp-ai-skill-select" class="wfm-nlp-ai-sel" style="flex:1;width:auto;">
|
||||
<option value="">-- No skill --</option>
|
||||
</select>
|
||||
<button id="wfm-nlp-ai-skill-manage-btn" class="wfm-nlp-ai-btn" title="Manage skills">✎</button>
|
||||
</div>
|
||||
<div id="wfm-nlp-ai-skill-panel" class="wfm-nlp-ai-skill-panel" style="display:none;">
|
||||
<div class="wfm-nlp-ai-row" style="justify-content:space-between;">
|
||||
<span class="wfm-nlp-ai-sec-title">Skills</span>
|
||||
<div style="display:flex;gap:4px;">
|
||||
<button id="wfm-nlp-ai-skill-new-btn" class="wfm-nlp-ai-btn wfm-nlp-ai-btn-primary">+ New</button>
|
||||
<button id="wfm-nlp-ai-skill-close-btn" class="wfm-nlp-ai-btn">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="wfm-nlp-ai-skill-list" class="wfm-nlp-ai-skill-list"></div>
|
||||
<div id="wfm-nlp-ai-skill-editor" class="wfm-nlp-ai-skill-editor" style="display:none;">
|
||||
<input type="text" id="wfm-nlp-ai-skill-editor-filename" class="wfm-nlp-ai-input" placeholder="filename.md">
|
||||
<textarea id="wfm-nlp-ai-skill-editor-content" class="wfm-nlp-ai-input wfm-nlp-ai-skill-editor-ta" rows="6" placeholder="--- name: My Skill description: ... --- System prompt..."></textarea>
|
||||
<div class="wfm-nlp-ai-actions">
|
||||
<button id="wfm-nlp-ai-skill-editor-save-btn" class="wfm-nlp-ai-btn wfm-nlp-ai-btn-primary">Save</button>
|
||||
<button id="wfm-nlp-ai-skill-editor-delete-btn" class="wfm-nlp-ai-btn wfm-nlp-ai-skill-danger-btn" style="display:none;">Delete</button>
|
||||
<button id="wfm-nlp-ai-skill-editor-cancel-btn" class="wfm-nlp-ai-btn">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wfm-nlp-ai-chat-msgs" id="wfm-nlp-ai-chat-msgs"></div>
|
||||
<textarea id="wfm-nlp-ai-chat-input" class="wfm-nlp-ai-textarea wfm-nlp-ai-chat-input" placeholder="Type a message..."></textarea>
|
||||
<div class="wfm-nlp-ai-actions">
|
||||
@@ -3433,7 +3987,11 @@ const setupAiHandlers = (container) => {
|
||||
chatStatusEl.className = "wfm-nlp-ai-status wfm-nlp-ai-working";
|
||||
|
||||
try {
|
||||
const reply = await aiCallChat(url, backend, model, chatHistory);
|
||||
const skillPrompt = await getActiveSkillSystemPrompt();
|
||||
const messagesToSend = skillPrompt
|
||||
? [{ role: "system", content: skillPrompt }, ...chatHistory]
|
||||
: chatHistory;
|
||||
const reply = await aiCallChat(url, backend, model, messagesToSend);
|
||||
chatHistory.push({ role: "assistant", content: reply });
|
||||
appendChatBubble("assistant", reply);
|
||||
chatStatusEl.textContent = "";
|
||||
@@ -3459,6 +4017,141 @@ const setupAiHandlers = (container) => {
|
||||
if (chatStatusEl) { chatStatusEl.textContent = ""; chatStatusEl.className = "wfm-nlp-ai-status"; }
|
||||
});
|
||||
|
||||
// ---- Skill handlers (Chat system prompt library) ----
|
||||
let skillFilesNlp = [];
|
||||
let skillEditingFilenameNlp = null;
|
||||
let activeSkillCacheNlp = null; // { filename, content }
|
||||
|
||||
const skillSelectEl = container.querySelector("#wfm-nlp-ai-skill-select");
|
||||
const skillManageBtn = container.querySelector("#wfm-nlp-ai-skill-manage-btn");
|
||||
const skillPanelEl = container.querySelector("#wfm-nlp-ai-skill-panel");
|
||||
const skillCloseBtn = container.querySelector("#wfm-nlp-ai-skill-close-btn");
|
||||
const skillNewBtn = container.querySelector("#wfm-nlp-ai-skill-new-btn");
|
||||
const skillListEl = container.querySelector("#wfm-nlp-ai-skill-list");
|
||||
const skillEditorEl = container.querySelector("#wfm-nlp-ai-skill-editor");
|
||||
const skillEditorFilenameEl = container.querySelector("#wfm-nlp-ai-skill-editor-filename");
|
||||
const skillEditorContentEl = container.querySelector("#wfm-nlp-ai-skill-editor-content");
|
||||
const skillEditorSaveBtn = container.querySelector("#wfm-nlp-ai-skill-editor-save-btn");
|
||||
const skillEditorDeleteBtn = container.querySelector("#wfm-nlp-ai-skill-editor-delete-btn");
|
||||
const skillEditorCancelBtn = container.querySelector("#wfm-nlp-ai-skill-editor-cancel-btn");
|
||||
|
||||
async function getActiveSkillSystemPrompt() {
|
||||
const filename = skillSelectEl?.value || "";
|
||||
if (!filename) return null;
|
||||
if (activeSkillCacheNlp?.filename === filename) return activeSkillCacheNlp.content;
|
||||
const raw = await aiSkillFetchContent(filename);
|
||||
const content = aiSkillStripFrontmatter(raw || "");
|
||||
activeSkillCacheNlp = { filename, content };
|
||||
return content;
|
||||
}
|
||||
|
||||
function renderSkillList() {
|
||||
if (!skillListEl) return;
|
||||
skillListEl.innerHTML = "";
|
||||
if (skillFilesNlp.length === 0) {
|
||||
skillListEl.innerHTML = `<div style="padding:8px;font-size:11px;color:var(--descrip-text,#888);">No skills yet.</div>`;
|
||||
return;
|
||||
}
|
||||
for (const f of skillFilesNlp) {
|
||||
const item = document.createElement("div");
|
||||
item.className = "wfm-nlp-ai-skill-item";
|
||||
item.title = f.description || "";
|
||||
item.innerHTML = `<span class="wfm-nlp-ai-skill-item-name">${f.name}</span><span>✎</span>`;
|
||||
item.addEventListener("click", async () => {
|
||||
const content = await aiSkillFetchContent(f.filename);
|
||||
openSkillEditor(f.filename, content ?? "");
|
||||
});
|
||||
skillListEl.appendChild(item);
|
||||
}
|
||||
}
|
||||
|
||||
function openSkillEditor(filename, content) {
|
||||
skillEditingFilenameNlp = filename || null;
|
||||
if (!skillEditorEl) return;
|
||||
if (skillEditorFilenameEl) skillEditorFilenameEl.value = filename || "";
|
||||
if (skillEditorContentEl) skillEditorContentEl.value = content || "";
|
||||
if (skillEditorDeleteBtn) skillEditorDeleteBtn.style.display = filename ? "" : "none";
|
||||
skillEditorEl.style.display = "";
|
||||
skillEditorFilenameEl?.focus();
|
||||
}
|
||||
|
||||
function closeSkillEditor() {
|
||||
skillEditingFilenameNlp = null;
|
||||
if (skillEditorEl) skillEditorEl.style.display = "none";
|
||||
}
|
||||
|
||||
function populateSkillSelect() {
|
||||
if (!skillSelectEl) return;
|
||||
const c = loadAiCfg();
|
||||
const wanted = skillSelectEl.value || c.activeSkillFilename || "";
|
||||
skillSelectEl.innerHTML = `<option value="">-- No skill --</option>` +
|
||||
skillFilesNlp.map(f => `<option value="${f.filename}" title="${f.description || ""}">${f.name}</option>`).join("");
|
||||
skillSelectEl.value = skillFilesNlp.some(f => f.filename === wanted) ? wanted : "";
|
||||
activeSkillCacheNlp = null;
|
||||
}
|
||||
|
||||
async function refreshSkillFiles() {
|
||||
skillFilesNlp = await aiSkillFetchFiles();
|
||||
renderSkillList();
|
||||
populateSkillSelect();
|
||||
}
|
||||
|
||||
refreshSkillFiles();
|
||||
|
||||
skillSelectEl?.addEventListener("change", () => {
|
||||
saveAiCfg({ activeSkillFilename: skillSelectEl.value });
|
||||
activeSkillCacheNlp = null;
|
||||
});
|
||||
|
||||
skillManageBtn?.addEventListener("click", () => {
|
||||
if (!skillPanelEl) return;
|
||||
const show = skillPanelEl.style.display === "none";
|
||||
skillPanelEl.style.display = show ? "" : "none";
|
||||
if (show) refreshSkillFiles();
|
||||
});
|
||||
|
||||
skillCloseBtn?.addEventListener("click", () => {
|
||||
if (skillPanelEl) skillPanelEl.style.display = "none";
|
||||
});
|
||||
|
||||
skillNewBtn?.addEventListener("click", () => {
|
||||
openSkillEditor(null, "---\nname: \ndescription: \n---\n\n");
|
||||
});
|
||||
|
||||
skillEditorSaveBtn?.addEventListener("click", async () => {
|
||||
let filename = (skillEditorFilenameEl?.value || "").trim();
|
||||
const content = skillEditorContentEl?.value || "";
|
||||
if (!filename) { showToast("ファイル名を入力してください", "error"); return; }
|
||||
if (!/\.md$/i.test(filename)) filename += ".md";
|
||||
if (!/^[\w\-. ]+\.md$/i.test(filename)) { showToast("無効なファイル名です", "error"); return; }
|
||||
const saved = await aiSkillSaveFile(filename, content);
|
||||
if (saved) {
|
||||
showToast(`保存しました: ${filename}`, "success");
|
||||
closeSkillEditor();
|
||||
await refreshSkillFiles();
|
||||
skillSelectEl.value = filename;
|
||||
saveAiCfg({ activeSkillFilename: filename });
|
||||
activeSkillCacheNlp = null;
|
||||
} else {
|
||||
showToast("保存に失敗しました", "error");
|
||||
}
|
||||
});
|
||||
|
||||
skillEditorDeleteBtn?.addEventListener("click", async () => {
|
||||
if (!skillEditingFilenameNlp) return;
|
||||
if (!confirm(`Delete "${skillEditingFilenameNlp}"?`)) return;
|
||||
await aiSkillDeleteFile(skillEditingFilenameNlp);
|
||||
showToast(`削除しました: ${skillEditingFilenameNlp}`, "success");
|
||||
if (skillSelectEl.value === skillEditingFilenameNlp) {
|
||||
saveAiCfg({ activeSkillFilename: "" });
|
||||
activeSkillCacheNlp = null;
|
||||
}
|
||||
closeSkillEditor();
|
||||
await refreshSkillFiles();
|
||||
});
|
||||
|
||||
skillEditorCancelBtn?.addEventListener("click", () => closeSkillEditor());
|
||||
|
||||
// ---- VLM handlers ----
|
||||
let vlmImage = null; // { base64, mimeType }
|
||||
|
||||
@@ -4455,6 +5148,55 @@ const injectStyles = () => {
|
||||
.wfm-nlp-ai-wc-count {
|
||||
width: 60px;
|
||||
}
|
||||
/* AI skills */
|
||||
.wfm-nlp-ai-skill-panel {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 6px;
|
||||
border: 1px solid var(--border-color, #4e4e4e);
|
||||
border-radius: 4px;
|
||||
background: var(--comfy-menu-bg, #1e1e1e);
|
||||
}
|
||||
.wfm-nlp-ai-skill-list {
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--border-color, #4e4e4e);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.wfm-nlp-ai-skill-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 4px;
|
||||
padding: 4px 6px;
|
||||
font-size: 11px;
|
||||
color: var(--input-text, #ddd);
|
||||
border-bottom: 1px solid var(--border-color, #3a3a3a);
|
||||
cursor: pointer;
|
||||
}
|
||||
.wfm-nlp-ai-skill-item:last-child { border-bottom: none; }
|
||||
.wfm-nlp-ai-skill-item:hover { background: rgba(74,158,255,0.1); }
|
||||
.wfm-nlp-ai-skill-item-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.wfm-nlp-ai-skill-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.wfm-nlp-ai-skill-editor-ta {
|
||||
font-family: monospace;
|
||||
resize: vertical;
|
||||
min-height: 90px;
|
||||
}
|
||||
.wfm-nlp-ai-skill-danger-btn {
|
||||
color: #ff4757;
|
||||
border-color: #ff4757;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user