Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4066f58d25 | ||
|
|
ce07964a68 | ||
|
|
d39a915888 | ||
|
|
5aa72379e8 | ||
|
|
8257ad5109 | ||
|
|
b8c250e836 | ||
|
|
4be66d7132 | ||
|
|
7f089527c5 | ||
|
|
7d8190ea07 | ||
|
|
9e2f174bcf | ||
|
|
199ee05f12 | ||
|
|
06f0157635 | ||
|
|
f96fac44a2 | ||
|
|
729574cf3b | ||
|
|
e4decf948b | ||
|
|
24dd6901ed | ||
|
|
aa192f51f0 | ||
|
|
300ee91cc1 | ||
|
|
3f3561a02b | ||
|
|
c3822bc4f8 | ||
|
|
d480c586d0 | ||
|
|
e0ce4beb21 | ||
|
|
f8cbd27c5c | ||
|
|
17bad4da60 | ||
|
|
9d12afb48c | ||
|
|
091ec15b2a | ||
|
|
c87d92d7f8 | ||
|
|
5d5da100e1 | ||
|
|
85b86c0618 | ||
|
|
1f93ca0259 | ||
|
|
e0486313a6 | ||
|
|
11b4eb48f0 | ||
|
|
156d569614 | ||
|
|
4fdef7f436 | ||
|
|
8189b2b75d | ||
|
|
5a76096c7e | ||
|
|
b6d4fb5590 | ||
|
|
3f84125262 | ||
|
|
906ba108cc | ||
|
|
6d54bbc7ca | ||
|
|
bda6f4fa9d | ||
|
|
2237b0cb52 | ||
|
|
6e23ad89b6 | ||
|
|
389a79cac9 | ||
|
|
1c4e081f9a | ||
|
|
9c57062011 | ||
|
|
38975c11ab | ||
|
|
8459e3dac9 | ||
|
|
e7eab66d8f | ||
|
|
26772ac8dd | ||
|
|
f68c0c7dc2 | ||
|
|
79fb35ff8c | ||
|
|
4f776d676a | ||
|
|
9f8078c408 | ||
|
|
ab2e83ec38 | ||
|
|
2715f1f165 | ||
|
|
56f506110d | ||
|
|
6785a2b6a5 | ||
|
|
e8b0e44cf4 | ||
|
|
4b64fae70d | ||
|
|
e622148b27 | ||
|
|
7a279d71c3 | ||
|
|
ee8b265173 | ||
|
|
80d254c6b4 | ||
|
|
8f6d451fef | ||
|
|
f2920add1d | ||
|
|
dbb5f66a1f | ||
|
|
cacee5ddad | ||
|
|
7f23b02812 | ||
|
|
edbea2bb43 | ||
|
|
70f916b825 | ||
|
|
42f5e73515 | ||
|
|
52e5d2fd5f | ||
|
|
397c90275f | ||
|
|
2c8877060c | ||
|
|
1203e2f0ef | ||
|
|
f2ff83088d | ||
|
|
d6f53ebb0f | ||
|
|
2552e8d746 | ||
|
|
96ba7d8055 |
@@ -28,7 +28,17 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-hub/stylelint@master
|
||||
env:
|
||||
PATTERN : 'web/css/*.css'
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run stylelint
|
||||
run: npx stylelint "web/css/*.css"
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## English • [日本語](docs/README_jp.md)
|
||||
|
||||

|
||||

|
||||
|
||||
## Overview
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
- **:zap:No setup required**: Automatically downloads CSV data optimized for Danbooru tags.
|
||||
- **:mag:Autocomplete**: Displays tag suggestions in real-time based on your input as you type.
|
||||
- **:file_cabinet:Related Tags Display**: Shows a list of tags highly related to the selected tag.
|
||||
- **:triangular_ruler:Auto Formatter**: Automatically formats prompt text when the textarea loses focus, cleaning up extra spaces and commas.
|
||||
- **:earth_asia:Multilingual Support**: Supports input completion in Japanese, Chinese, and Korean.
|
||||
- **:computer_mouse:Intuitive Operation**:
|
||||
- Supports both mouse and keyboard operations.
|
||||
@@ -42,6 +43,7 @@ When you type in a text input area, tags that partially match the text are displ
|
||||
- Tags that have already been entered are displayed grayed out.
|
||||
- You can display Danbooru and e621 tags at the same time. You can also change the priority from the settings.
|
||||
- Supports autocomplete for Lora and Embedding inputs. You can enable/disable this feature in the settings.
|
||||
- Clicking the 📖 icon opens the tag's Wiki page. If a tag is selected via keyboard, you can open it with the `F1` key.
|
||||
|
||||
## Related Tags
|
||||
|
||||
@@ -52,9 +54,24 @@ When you select any tag in a text input area, a list of highly related tags is d
|
||||
- The display position is primarily at the bottom of the text area and automatically adjusts vertically based on available space.
|
||||
- You can switch between vertical and horizontal display positions using the "↕️|↔️" button in the header.
|
||||
- You can toggle the pinned state of the displayed related tags using the "📌|🎯" button in the header. To close the UI when pinned, press the Esc key.
|
||||
- Clicking the tag in the header opens the tag's Wiki page.
|
||||
- Tags that have already been entered are displayed grayed out. If you try to insert a grayed-out tag, the already entered tag will instead be selected.
|
||||
- You can display related tags for the cursor position by pressing `Ctrl+Shift+Space`.
|
||||
|
||||
## Auto Formatter
|
||||
|
||||
When a text input area loses focus (e.g., by clicking outside or pressing Tab), the prompt text is automatically formatted. This feature improves readability when editing large amounts of text.
|
||||
|
||||
Detailed behavior is as follows:
|
||||
- Automatically adds a comma and space after each tag for proper separation
|
||||
- Removes extra commas and spaces between tags
|
||||
- You can manually trigger formatting using the keyboard shortcut `Alt+Shift+F` (keybinding can be customized in ComfyUI settings)
|
||||
- You can enable/disable this feature in the settings
|
||||
|
||||
> [!NOTE]
|
||||
> Auto-formatting is disabled for certain nodes to prevent errors.
|
||||
> Example: [Power Puter (rgthree)](https://github.com/rgthree/rgthree-comfy/wiki/Node:-Power-Puter) `code` field, [LoraLoaderBlockWeight (Inspire)](https://github.com/ltdrdata/ComfyUI-Inspire-Pack) `block_vector` field
|
||||
|
||||
## CSV Data
|
||||
|
||||
Two basic CSV data files are required for operation. These are managed on [HuggingFace](https://huggingface.co/datasets/newtextdoc1111/danbooru-tag-csv) and are automatically downloaded when ComfyUI is first launched after installation, so no setup is required.
|
||||
@@ -150,7 +167,9 @@ For example, by preparing the following CSV, you can quickly insert correspondin
|
||||
|
||||
- **Enable Autocomplete**: Enable/disable the autocomplete feature.
|
||||
- **Max suggestions**: Maximum number of autocomplete suggestions to display.
|
||||
- **Auto-Insert Comma**: Automatically insert a comma after tags when inserting from autocomplete.
|
||||
- **Replace '_' with 'Space'**: Replaces underscores with spaces when inserting tags. This setting also affects related tag display.
|
||||
- **String to add before artist tags**: Text to prepend when inserting an artist tag. For Anima models, specify `@`.
|
||||
- **Enable Loras and Embeddings**: Display Lora and Embedding in the suggestions.
|
||||
- **Use Fast Search**: Switch autocomplete suggestions search to fast processing (see [About Fast Search for Autocomplete](#about-fast-search-for-autocomplete) for details).
|
||||
|
||||
@@ -159,12 +178,21 @@ For example, by preparing the following CSV, you can quickly insert correspondin
|
||||
- **Enable Related Tags**: Enable/disable the related tags feature.
|
||||
- **Max related tags**: Maximum number of related tags to display.
|
||||
- **Default Display Position**: Default display position when ComfyUI starts.
|
||||
- **Related Tags Trigger Mode**: Which action will trigger displaying related tags (click only, Ctrl+click)
|
||||
- **Related Tags Trigger Mode**: Which action will trigger displaying related tags for the entered tag (click only, Ctrl+click)
|
||||
|
||||
### Display
|
||||
|
||||
- **Hide Alias**: Hide/show the Alias column in autocomplete and related tags (default is show)
|
||||
|
||||
### Auto Formatter
|
||||
|
||||
- **Enable Auto Format**: Enable/disable the automatically format prompt text when the textarea loses focus.
|
||||
- **Auto Format Trigger**: Choose when formatting is applied.
|
||||
- **Auto**: Format automatically when leaving text field
|
||||
- **Manual**: Format only via keyboard shortcut (default: `Alt+Shift+F`)
|
||||
- **Use Trailing Comma**: When enabled, ensures all lines end with a trailing comma when formatting. If disabled, removes trailing commas.
|
||||
- **Trim Surrounding Spaces**: When enabled, trim any blank lines or spaces from the beginning and end of the prompt.
|
||||
|
||||
## Advanced Settings
|
||||
|
||||
### Disabling CSV Update Check on Startup
|
||||
|
||||
+30
-2
@@ -1,6 +1,6 @@
|
||||
# ComfyUI-Autocomplete-Plus
|
||||
|
||||

|
||||

|
||||
|
||||
## 概要
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
- **:zap:セットアップ不要**: Danbooruタグに最適化された CSV データを自動でダウンロード
|
||||
- **:mag:オートコンプリート**: テキスト入力中に、入力内容に基づいてタグ候補をリアルタイムで表示
|
||||
- **:file_cabinet:関連タグ表示機能**: 選択したタグと関連性の高いタグを一覧表示
|
||||
- **:triangular_ruler:自動フォーマット**: テキストエリアがフォーカスを失った際に、プロンプトテキストを自動的にフォーマットし、余分なスペースやカンマを整理
|
||||
- **:earth_asia:多言語対応**: 日本語、中国語、韓国語での入力補完をサポート
|
||||
- **:computer_mouse:直感的な操作**:
|
||||
- マウスとキーボードどちらの操作にも対応
|
||||
@@ -40,6 +41,7 @@
|
||||
- 入力済みのタグはグレーアウトで表示されます
|
||||
- Danbooruとe621のタグを同時に表示出来ます。設定から優先順位を変更できます
|
||||
- LoraとEmbeddingの入力補完に対応しています。設定から有効・無効を切り替えられます
|
||||
- 「📖」アイコンをクリックするとタグのWikiページを開きます。キーボードで選択中の場合は `F1` キーで開くことが出来ます
|
||||
|
||||
## 関連タグ
|
||||
|
||||
@@ -50,9 +52,24 @@
|
||||
- 表示位置は、テキストエリアの下部を基本とし、空きスペースに応じて上下に自動調整されます
|
||||
- ヘッダーの「↕️|↔️」ボタンで上下と左右の表示位置に切り替えられます
|
||||
- ヘッダーの「📌|🎯」ボタンで表示する関連タグの固定状態を切り替えられます。固定状態で閉じたい場合はEscキーを押します
|
||||
- ヘッダーのタグをクリックするとタグのWikiページを開きます
|
||||
- 入力済みのタグはグレーアウトで表示されます。グレーアウトしたタグを挿入しようとした場合、代わりに入力済みのタグを選択状態にします
|
||||
- `Ctrl+Shift+Space` キーでカーソル位置の関連タグを表示できます
|
||||
|
||||
## 自動フォーマット
|
||||
|
||||
テキスト入力エリアがフォーカスを失った際(例:外側をクリック、Tabキーを押す)、プロンプトテキストを自動的にフォーマットする機能です。大量のテキストを編集する際に可読性を向上させる事が出来ます。
|
||||
|
||||
詳細な動作は以下になります:
|
||||
- 各タグの後に自動的にカンマとスペースを追加し、適切に区切ります
|
||||
- タグ間の余分なカンマとスペースを削除します
|
||||
- キーボードショートカット `Alt+Shift+F` を使って手動でフォーマットすることも出来ます(キーバインドはComfyUIの設定から変更可能です)
|
||||
- 設定から機能の有効化/無効化を切り替えられます
|
||||
|
||||
> [!NOTE]
|
||||
> 一部のノードはエラーに繋がるため、自動フォーマットは実行されません。
|
||||
> 例: [Power Puter (rgthree)](https://github.com/rgthree/rgthree-comfy/wiki/Node:-Power-Puter) の `code` フィールド、 [LoraLoaderBlockWeight (Inspire)](https://github.com/ltdrdata/ComfyUI-Inspire-Pack) の `block_vector` フィールド
|
||||
|
||||
## CSV データ
|
||||
|
||||
動作には基本となる CSV データが2つ必要です。これらは [HuggingFace](https://huggingface.co/datasets/newtextdoc1111/danbooru-tag-csv) で管理されており、 ComfyUI にインストール後の初回起動時に自動でダウンロードされるのでセットアップは不要です。
|
||||
@@ -148,7 +165,9 @@ worst_quality,5,9999999,
|
||||
|
||||
- **Enable Autocomplete**: オートコンプリート機能の有効化/無効化
|
||||
- **Max Suggestions**: オートコンプリート候補の最大表示件数
|
||||
- **Auto-Insert Comma**: タグの挿入時、末尾にカンマを追加する
|
||||
- **Replace '_' with 'Space'**: タグ挿入時にアンダースコアをスペースに置き換えます。この設定は関連タグ表示にも影響します
|
||||
- **String to add before artist tags**: アーティストタグを挿入する際に付加する文字列。 Anima モデルの場合は「@」を指定します
|
||||
- **Enable Loras and Embeddings**: LoraとEmbeddingを候補に表示する
|
||||
- **Use Fast Search**: オートコンプリート候補の検索を高速な処理に切り替える(詳細は [オートコンプリートの高速検索について](#オートコンプリートの高速検索について) を確認してください)
|
||||
|
||||
@@ -157,12 +176,21 @@ worst_quality,5,9999999,
|
||||
- **Enable Related Tags**: 関連タグ機能の有効化/無効化
|
||||
- **Max related tags**: 関連タグの最大表示件数
|
||||
- **Default Display Position**: ComfyUI起動時のデフォルト表示位置
|
||||
- **Related Tags Trigger Mode** : 関連タグを表示する際、どの操作をトリガーとするか(クリックのみ、Ctrl+クリック)
|
||||
- **Related Tags Trigger Mode** : 入力済みのタグの関連タグを表示する際、どの操作をトリガーとするか(クリックのみ、Ctrl+クリック)
|
||||
|
||||
### 表示
|
||||
|
||||
- **Hide Alias**: オートコンプリートと関連タグで表示されるエイリアス列の非表示/表示を切り替え(デフォルトは表示です)
|
||||
|
||||
### 自動フォーマット
|
||||
|
||||
- **Enable Auto Format**: テキストエリアがフォーカスを失った際にプロンプトテキストを自動的にフォーマットする機能の有効化/無効化
|
||||
- **Auto Format Trigger**: フォーマットを適用するタイミングを選択します
|
||||
- **自動**: テキスト欄からフォーカスが外れた際に自動でフォーマットします
|
||||
- **手動**: キーボードショートカットでのみフォーマットします(デフォルト: `Alt+Shift+F`)
|
||||
- **行末にカンマを使用**: 有効時、フォーマット時にすべての行末がカンマで終わるようになります。\n無効にすると行末のカンマが削除されます
|
||||
- **先頭・末尾の空白を削除**: 有効時、プロンプトの先頭と末尾にある空行やスペースをすべて削除します
|
||||
|
||||
## 上級者向け設定
|
||||
|
||||
### 起動時のCSV更新チェックを無効化する
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"settingsCategories": {
|
||||
"Autocomplete Plus": "Autocomplete Plus",
|
||||
"Tag Source": "Tag Source",
|
||||
"Autocompletion": "Autocompletion",
|
||||
"Related Tags": "Related Tags",
|
||||
"Display": "Display",
|
||||
"AutoFormatter": "AutoFormatter"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"AutocompletePlus_TagSource_IconPosition": {
|
||||
"name": "Tag Source Icon Position",
|
||||
"options": {
|
||||
"left": "Left",
|
||||
"right": "Right",
|
||||
"hidden": "Hidden"
|
||||
}
|
||||
},
|
||||
"AutocompletePlus_TagSource_PrimaryTagSource": {
|
||||
"name": "Primary source for 'all' Source",
|
||||
"tooltip": "When 'Autocomplete Tag Source' is 'all', this determines which source's tags appear first in suggestions.",
|
||||
"options": {
|
||||
"danbooru": "Danbooru",
|
||||
"e621": "e621"
|
||||
}
|
||||
},
|
||||
"AutocompletePlus_TagSource": {
|
||||
"name": "Autocomplete Tag Source",
|
||||
"tooltip": "Select the tag source for autocomplete suggestions. 'all' includes tags from all loaded sources.",
|
||||
"options": {
|
||||
"danbooru": "Danbooru",
|
||||
"e621": "e621",
|
||||
"all": "all"
|
||||
}
|
||||
},
|
||||
"AutocompletePlus_Autocompletion_UseFastSearch": {
|
||||
"name": "Use Fast Search",
|
||||
"tooltip": "Tag search processing during text input operates faster, improving responsiveness."
|
||||
},
|
||||
"AutocompletePlus_Autocompletion_EnableModels": {
|
||||
"name": "Enable Loras and Embeddings"
|
||||
},
|
||||
"AutocompletePlus_Autocompletion_PrefixArtist": {
|
||||
"name": "String to add before artist tags",
|
||||
"tooltip": "Text to prepend when inserting an artist tag via autocomplete.\ne.g. '@' -> '@artist_name'."
|
||||
},
|
||||
"AutocompletePlus_Autocompletion_ReplaceUnderscoreWithSpace": {
|
||||
"name": "Replace '_' with 'Space'",
|
||||
"tooltip": "This setting also affects related tags display."
|
||||
},
|
||||
"AutocompletePlus_Autocompletion_AutoInsertComma": {
|
||||
"name": "Auto-Insert Comma",
|
||||
"tooltip": "Automatically insert a comma after tags when inserting from autocomplete."
|
||||
},
|
||||
"AutocompletePlus_Autocompletion_MaxSuggestions": {
|
||||
"name": "Max suggestions"
|
||||
},
|
||||
"AutocompletePlus_Autocompletion_Enable": {
|
||||
"name": "Enable Autocomplete"
|
||||
},
|
||||
"AutocompletePlus_RelatedTags_RelatedTagsTriggerMode": {
|
||||
"name": "Related Tags Trigger Mode",
|
||||
"tooltip": "Which action will trigger displaying related tags for the entered tag (click only, Ctrl+click).",
|
||||
"options": {
|
||||
"click": "Click",
|
||||
"ctrl+Click": "Ctrl+Click"
|
||||
}
|
||||
},
|
||||
"AutocompletePlus_RelatedTags_DisplayPosition": {
|
||||
"name": "Default Display Position",
|
||||
"tooltip": "Display position (relative to Textarea).",
|
||||
"options": {
|
||||
"horizontal": "Horizontal",
|
||||
"vertical": "Vertical"
|
||||
}
|
||||
},
|
||||
"AutocompletePlus_RelatedTags_MaxRelatedTags": {
|
||||
"name": "Max related tags"
|
||||
},
|
||||
"AutocompletePlus_RelatedTags_Enable": {
|
||||
"name": "Enable Related Tags"
|
||||
},
|
||||
"AutocompletePlus_Display_HideAlias": {
|
||||
"name": "Hide Alias",
|
||||
"tooltip": "Hide alias column in the autocomplete and related tags display."
|
||||
},
|
||||
"AutocompletePlus_AutoFormatter_Trigger": {
|
||||
"name": "Auto Format Trigger",
|
||||
"tooltip": "Auto: Format automatically when leaving text field.\nManual: Format only via keyboard shortcut. default keybind: (Alt+Shift+F)",
|
||||
"options": {
|
||||
"auto": "Auto",
|
||||
"manual": "Manual"
|
||||
}
|
||||
},
|
||||
"AutocompletePlus_AutoFormatter_UseTrailingComma": {
|
||||
"name": "Use Trailing Comma",
|
||||
"tooltip": "When enabled, ensures all lines end with a trailing comma.\nWhen disabled, removes trailing commas."
|
||||
},
|
||||
"AutocompletePlus_AutoFormatter_EnableAutoFormat": {
|
||||
"name": "Enable Auto Format"
|
||||
},
|
||||
"AutocompletePlus_AutoFormatter_TrimSurroundingSpaces": {
|
||||
"name": "Trim Surrounding Spaces",
|
||||
"tooltip": "When enabled, trim any blank lines from the beginning and end of the prompt."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"settingsCategories": {
|
||||
"Autocomplete Plus": "Autocomplete Plus",
|
||||
"Tag Source": "タグソース",
|
||||
"Autocompletion": "オートコンプリート",
|
||||
"Related Tags": "関連タグ",
|
||||
"Display": "表示",
|
||||
"AutoFormatter": "自動フォーマット"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"AutocompletePlus_TagSource_IconPosition": {
|
||||
"name": "タグソースのアイコン位置",
|
||||
"options": {
|
||||
"left": "左",
|
||||
"right": "右",
|
||||
"hidden": "非表示"
|
||||
}
|
||||
},
|
||||
"AutocompletePlus_TagSource_PrimaryTagSource": {
|
||||
"name": "優先して表示するタグソース",
|
||||
"tooltip": "オートコンプリートのタグソースが「全て」のとき、ここで指定したタグソースが優先して表示されます",
|
||||
"options": {
|
||||
"danbooru": "Danbooru",
|
||||
"e621": "e621"
|
||||
}
|
||||
},
|
||||
"AutocompletePlus_TagSource": {
|
||||
"name": "オートコンプリートのタグソース",
|
||||
"tooltip": "オートコンプリート候補に表示するタグソース。\n「全て」を選択するとロード済みの全てのタグソースを表示します",
|
||||
"options": {
|
||||
"danbooru": "Danbooru",
|
||||
"e621": "e621",
|
||||
"all": "全て"
|
||||
}
|
||||
},
|
||||
"AutocompletePlus_Autocompletion_UseFastSearch": {
|
||||
"name": "高速検索の有効化",
|
||||
"tooltip": "候補の検索を高速な処理に切り替えます。\n入力時の遅延が改善される場合があります"
|
||||
},
|
||||
"AutocompletePlus_Autocompletion_EnableModels": {
|
||||
"name": "LoraとEmbeddingを候補に表示する"
|
||||
},
|
||||
"AutocompletePlus_Autocompletion_PrefixArtist": {
|
||||
"name": "アーティストタグの前に追加する文字列",
|
||||
"tooltip": "オートコンプリートからアーティストタグを挿入する際に付加する文字列。\n例: '@' -> '@artist_name'"
|
||||
},
|
||||
"AutocompletePlus_Autocompletion_ReplaceUnderscoreWithSpace": {
|
||||
"name": "'_' を 'スペース' に置き換える",
|
||||
"tooltip": "この設定は関連タグ表示にも影響します"
|
||||
},
|
||||
"AutocompletePlus_Autocompletion_AutoInsertComma": {
|
||||
"name": "カンマを自動で追加する",
|
||||
"tooltip": "オートコンプリート確定時、タグの末尾に自動でカンマを追加する"
|
||||
},
|
||||
"AutocompletePlus_Autocompletion_MaxSuggestions": {
|
||||
"name": "候補の最大表示件数"
|
||||
},
|
||||
"AutocompletePlus_Autocompletion_Enable": {
|
||||
"name": "オートコンプリート機能の有効化"
|
||||
},
|
||||
"AutocompletePlus_RelatedTags_RelatedTagsTriggerMode": {
|
||||
"name": "関連タグを表示する操作",
|
||||
"tooltip": "入力済みのタグの関連タグを表示する際、どの操作をトリガーとするか(クリックのみ、Ctrl+クリック)",
|
||||
"options": {
|
||||
"click": "クリック",
|
||||
"ctrl+Click": "Ctrl+クリック"
|
||||
}
|
||||
},
|
||||
"AutocompletePlus_RelatedTags_DisplayPosition": {
|
||||
"name": "デフォルト表示位置",
|
||||
"tooltip": "入力中のテキストエリアを基準に、どの位置に表示するか",
|
||||
"options": {
|
||||
"horizontal": "水平",
|
||||
"vertical": "垂直"
|
||||
}
|
||||
},
|
||||
"AutocompletePlus_RelatedTags_MaxRelatedTags": {
|
||||
"name": "関連タグの最大表示件数"
|
||||
},
|
||||
"AutocompletePlus_RelatedTags_Enable": {
|
||||
"name": "関連タグ機能の有効化"
|
||||
},
|
||||
"AutocompletePlus_Display_HideAlias": {
|
||||
"name": "エイリアスを非表示",
|
||||
"tooltip": "オートコンプリートと関連タグで表示されるエイリアス列を非表示にします"
|
||||
},
|
||||
"AutocompletePlus_AutoFormatter_Trigger": {
|
||||
"name": "自動フォーマットのトリガー",
|
||||
"tooltip": "自動: テキスト欄からフォーカスが外れた際に自動でフォーマットします\n手動: キーボードショートカットでのみフォーマットします。デフォルトのキーバインド: (Alt+Shift+F)",
|
||||
"options": {
|
||||
"auto": "自動",
|
||||
"manual": "手動"
|
||||
}
|
||||
},
|
||||
"AutocompletePlus_AutoFormatter_UseTrailingComma": {
|
||||
"name": "行末にカンマを使用",
|
||||
"tooltip": "有効にすると、すべての行末がカンマで終わるようになります。\n無効にすると行末のカンマが削除されます。"
|
||||
},
|
||||
"AutocompletePlus_AutoFormatter_EnableAutoFormat": {
|
||||
"name": "自動フォーマット機能の有効化"
|
||||
},
|
||||
"AutocompletePlus_AutoFormatter_TrimSurroundingSpaces": {
|
||||
"name": "先頭・末尾の空白を削除",
|
||||
"tooltip": "有効時、プロンプトの先頭と末尾にある空行やスペースをすべて削除します。"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"settingsCategories": {
|
||||
"Autocomplete Plus": "Autocomplete Plus",
|
||||
"Tag Source": "標籤來源",
|
||||
"Autocompletion": "自動完成",
|
||||
"Related Tags": "相關標籤",
|
||||
"Display": "顯示",
|
||||
"AutoFormatter": "自動格式化"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"AutocompletePlus_TagSource_IconPosition": {
|
||||
"name": "標籤來源圖示位置",
|
||||
"options": {
|
||||
"left": "左",
|
||||
"right": "右",
|
||||
"hidden": "隱藏"
|
||||
}
|
||||
},
|
||||
"AutocompletePlus_TagSource_PrimaryTagSource": {
|
||||
"name": "「全部」選擇時的主要來源",
|
||||
"tooltip": "當自動完成標籤來源為「全部」時,此設定決定哪個來源的標籤優先顯示在建議中。",
|
||||
"options": {
|
||||
"danbooru": "Danbooru",
|
||||
"e621": "e621"
|
||||
}
|
||||
},
|
||||
"AutocompletePlus_TagSource": {
|
||||
"name": "自動完成標籤來源",
|
||||
"tooltip": "選擇自動完成建議的標籤來源。選擇「全部」將包含所有已載入來源的標籤。",
|
||||
"options": {
|
||||
"danbooru": "Danbooru",
|
||||
"e621": "e621",
|
||||
"all": "全部"
|
||||
}
|
||||
},
|
||||
"AutocompletePlus_Autocompletion_UseFastSearch": {
|
||||
"name": "啟用快速搜尋",
|
||||
"tooltip": "標籤搜尋處理更快,提高文字輸入時的回應速度"
|
||||
},
|
||||
"AutocompletePlus_Autocompletion_EnableModels": {
|
||||
"name": "顯示Lora和Embedding"
|
||||
},
|
||||
"AutocompletePlus_Autocompletion_PrefixArtist": {
|
||||
"name": "在藝術家標籤前添加的字串",
|
||||
"tooltip": "透過自動完成插入藝術家標籤時,在標籤前添加的文字。\n例: '@' -> '@artist_name'"
|
||||
},
|
||||
"AutocompletePlus_Autocompletion_ReplaceUnderscoreWithSpace": {
|
||||
"name": "將'_'替換為'空格'",
|
||||
"tooltip": "此設定也會影響相關標籤顯示"
|
||||
},
|
||||
"AutocompletePlus_Autocompletion_AutoInsertComma": {
|
||||
"name": "自動插入逗號",
|
||||
"tooltip": "從自動完成插入標籤時自動在標籤後添加逗號"
|
||||
},
|
||||
"AutocompletePlus_Autocompletion_MaxSuggestions": {
|
||||
"name": "最大提示數"
|
||||
},
|
||||
"AutocompletePlus_Autocompletion_Enable": {
|
||||
"name": "啟用自動完成"
|
||||
},
|
||||
"AutocompletePlus_RelatedTags_RelatedTagsTriggerMode": {
|
||||
"name": "顯示相關標籤的操作",
|
||||
"tooltip": "指定觸發顯示已輸入標籤的相關標籤的操作(僅點擊、Ctrl+點擊)",
|
||||
"options": {
|
||||
"click": "點擊",
|
||||
"ctrl+Click": "Ctrl+點擊"
|
||||
}
|
||||
},
|
||||
"AutocompletePlus_RelatedTags_DisplayPosition": {
|
||||
"name": "預設顯示位置",
|
||||
"tooltip": "相對於文字區域的顯示位置",
|
||||
"options": {
|
||||
"horizontal": "水平",
|
||||
"vertical": "垂直"
|
||||
}
|
||||
},
|
||||
"AutocompletePlus_RelatedTags_MaxRelatedTags": {
|
||||
"name": "最大相關標籤數"
|
||||
},
|
||||
"AutocompletePlus_RelatedTags_Enable": {
|
||||
"name": "啟用相關標籤"
|
||||
},
|
||||
"AutocompletePlus_Display_HideAlias": {
|
||||
"name": "隱藏別名",
|
||||
"tooltip": "隱藏自動完成和相關標籤顯示中的別名欄"
|
||||
},
|
||||
"AutocompletePlus_AutoFormatter_Trigger": {
|
||||
"name": "自動格式化觸發方式",
|
||||
"tooltip": "自動: 離開文字方塊時自動格式化\n手動: 僅透過鍵盤快速鍵格式化。預設快速鍵: (Alt+Shift+F)",
|
||||
"options": {
|
||||
"auto": "自動",
|
||||
"manual": "手動"
|
||||
}
|
||||
},
|
||||
"AutocompletePlus_AutoFormatter_UseTrailingComma": {
|
||||
"name": "在行末添加逗號",
|
||||
"tooltip": "啟用時,確保所有行都以逗號結尾。\n停用時,移除結尾逗號。"
|
||||
},
|
||||
"AutocompletePlus_AutoFormatter_EnableAutoFormat": {
|
||||
"name": "啟用自動格式化"
|
||||
},
|
||||
"AutocompletePlus_AutoFormatter_TrimSurroundingSpaces": {
|
||||
"name": "去除首尾空白",
|
||||
"tooltip": "啟用時,清除提示詞開頭和結尾的所有空行和空格。"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"settingsCategories": {
|
||||
"Autocomplete Plus": "Autocomplete Plus",
|
||||
"Tag Source": "标签来源",
|
||||
"Autocompletion": "自动完成",
|
||||
"Related Tags": "相关标签",
|
||||
"Display": "显示",
|
||||
"AutoFormatter": "自动格式化"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"AutocompletePlus_TagSource_IconPosition": {
|
||||
"name": "标签来源图标位置",
|
||||
"options": {
|
||||
"left": "左",
|
||||
"right": "右",
|
||||
"hidden": "隐藏"
|
||||
}
|
||||
},
|
||||
"AutocompletePlus_TagSource_PrimaryTagSource": {
|
||||
"name": "'全部'选择时的主要来源",
|
||||
"tooltip": "当自动完成标签来源为\"全部\"时,此设置决定哪个来源的标签优先显示在建议中。",
|
||||
"options": {
|
||||
"danbooru": "Danbooru",
|
||||
"e621": "e621"
|
||||
}
|
||||
},
|
||||
"AutocompletePlus_TagSource": {
|
||||
"name": "自动完成标签来源",
|
||||
"tooltip": "选择自动完成建议的标签来源。选择\"全部\"将包含所有已加载来源的标签。",
|
||||
"options": {
|
||||
"danbooru": "Danbooru",
|
||||
"e621": "e621",
|
||||
"all": "全部"
|
||||
}
|
||||
},
|
||||
"AutocompletePlus_Autocompletion_UseFastSearch": {
|
||||
"name": "启用快速搜索",
|
||||
"tooltip": "标签搜索处理更快,提高文本输入时的响应速度"
|
||||
},
|
||||
"AutocompletePlus_Autocompletion_EnableModels": {
|
||||
"name": "显示Lora和Embedding"
|
||||
},
|
||||
"AutocompletePlus_Autocompletion_PrefixArtist": {
|
||||
"name": "在艺术家标签前添加的字符串",
|
||||
"tooltip": "通过自动完成插入艺术家标签时,在标签前添加的文本。\n例: '@' -> '@artist_name'"
|
||||
},
|
||||
"AutocompletePlus_Autocompletion_ReplaceUnderscoreWithSpace": {
|
||||
"name": "将'_'替换为'空格'",
|
||||
"tooltip": "此设置也会影响相关标签显示"
|
||||
},
|
||||
"AutocompletePlus_Autocompletion_AutoInsertComma": {
|
||||
"name": "自动插入逗号",
|
||||
"tooltip": "从自动完成插入标签时自动在标签后添加逗号"
|
||||
},
|
||||
"AutocompletePlus_Autocompletion_MaxSuggestions": {
|
||||
"name": "最大提示数"
|
||||
},
|
||||
"AutocompletePlus_Autocompletion_Enable": {
|
||||
"name": "启用自动完成"
|
||||
},
|
||||
"AutocompletePlus_RelatedTags_RelatedTagsTriggerMode": {
|
||||
"name": "显示相关标签的操作",
|
||||
"tooltip": "指定触发显示已输入标签的相关标签的操作(仅点击、Ctrl+点击)",
|
||||
"options": {
|
||||
"click": "点击",
|
||||
"ctrl+Click": "Ctrl+点击"
|
||||
}
|
||||
},
|
||||
"AutocompletePlus_RelatedTags_DisplayPosition": {
|
||||
"name": "默认显示位置",
|
||||
"tooltip": "相对于文本区域的显示位置",
|
||||
"options": {
|
||||
"horizontal": "水平",
|
||||
"vertical": "垂直"
|
||||
}
|
||||
},
|
||||
"AutocompletePlus_RelatedTags_MaxRelatedTags": {
|
||||
"name": "最大相关标签数"
|
||||
},
|
||||
"AutocompletePlus_RelatedTags_Enable": {
|
||||
"name": "启用相关标签"
|
||||
},
|
||||
"AutocompletePlus_Display_HideAlias": {
|
||||
"name": "隐藏别名",
|
||||
"tooltip": "隐藏自动完成和相关标签显示中的别名列"
|
||||
},
|
||||
"AutocompletePlus_AutoFormatter_Trigger": {
|
||||
"name": "自动格式化触发方式",
|
||||
"tooltip": "自动: 离开文本框时自动格式化\n手动: 仅通过键盘快捷键格式化。默认快捷键: (Alt+Shift+F)",
|
||||
"options": {
|
||||
"auto": "自动",
|
||||
"manual": "手动"
|
||||
}
|
||||
},
|
||||
"AutocompletePlus_AutoFormatter_UseTrailingComma": {
|
||||
"name": "在行末添加逗号",
|
||||
"tooltip": "启用时,确保所有行都以逗号结尾。\n禁用时,移除结尾逗号。"
|
||||
},
|
||||
"AutocompletePlus_AutoFormatter_EnableAutoFormat": {
|
||||
"name": "启用自动格式化"
|
||||
},
|
||||
"AutocompletePlus_AutoFormatter_TrimSurroundingSpaces": {
|
||||
"name": "去除首尾空白",
|
||||
"tooltip": "启用时,清除提示词开头和结尾的所有空行和空格。"
|
||||
}
|
||||
}
|
||||
Generated
+16
-15
@@ -4,6 +4,7 @@
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ComfyUI-Autocomplete-Plus",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.27.1",
|
||||
"@babel/preset-env": "^7.27.2",
|
||||
@@ -2005,9 +2006,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
|
||||
"version": "3.14.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
|
||||
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
|
||||
"version": "3.14.2",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
|
||||
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2266,9 +2267,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/reporters/node_modules/glob": {
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@@ -4790,9 +4791,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jest-config/node_modules/glob": {
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@@ -5130,9 +5131,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jest-runtime/node_modules/glob": {
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@@ -5335,9 +5336,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "comfyui-autocomplete-plus"
|
||||
description = "Autocomplete and Related Tag display for ComfyUI"
|
||||
version = "1.6.1"
|
||||
version = "1.10.0"
|
||||
license = {file = "LICENSE"}
|
||||
dependencies = ["",]
|
||||
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
import {
|
||||
formatPromptText,
|
||||
__test__
|
||||
} from "../../web/js/auto-formatter.js";
|
||||
import { settingValues } from "../../web/js/settings.js";
|
||||
|
||||
const {
|
||||
shouldAutoFormat
|
||||
} = __test__;
|
||||
|
||||
describe('AutoFormatter Functions', () => {
|
||||
|
||||
describe('shouldAutoFormat', () => {
|
||||
const mockNodeInfo = (nodeType, inputName) => ({
|
||||
nodeType,
|
||||
inputName
|
||||
});
|
||||
|
||||
// Store original setting value to restore after tests
|
||||
const originalTrimSurroundingSpaces = settingValues.trimSurroundingSpaces;
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original setting after each test
|
||||
settingValues.trimSurroundingSpaces = originalTrimSurroundingSpaces;
|
||||
});
|
||||
|
||||
test('should return false for empty text', () => {
|
||||
expect(shouldAutoFormat('', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(false);
|
||||
expect(shouldAutoFormat(' ', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(false);
|
||||
});
|
||||
|
||||
test('should return false for blocklisted nodes', () => {
|
||||
expect(shouldAutoFormat('some text,', mockNodeInfo('Power Puter (rgthree)', 'code'))).toBe(false);
|
||||
expect(shouldAutoFormat('some text,', mockNodeInfo('LoraLoaderBlockWeight //Inspire', 'block_vector'))).toBe(false);
|
||||
});
|
||||
|
||||
test('should return false for numeric data or single-letter placeholders', () => {
|
||||
expect(shouldAutoFormat('0,0,0,1,1,1', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(false);
|
||||
expect(shouldAutoFormat('0.5, -1.2, 0.8', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(false);
|
||||
expect(shouldAutoFormat('A,B,R', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(false);
|
||||
expect(shouldAutoFormat('X, 1.5, Y', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(false);
|
||||
});
|
||||
|
||||
test('should return true for text with "word + comma" pattern', () => {
|
||||
expect(shouldAutoFormat('1girl, blue hair,', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(true);
|
||||
expect(shouldAutoFormat('tag1, tag2', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false if "word + comma" pattern is not found', () => {
|
||||
expect(shouldAutoFormat('hello world', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(false);
|
||||
expect(shouldAutoFormat('tag1 tag2', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(false);
|
||||
});
|
||||
|
||||
describe('blocklist priority with trimSurroundingSpaces', () => {
|
||||
test('should prioritize blocklist over trimSurroundingSpaces when disabled', () => {
|
||||
settingValues.trimSurroundingSpaces = false;
|
||||
|
||||
// Blocklisted nodes should return false even with surrounding spaces
|
||||
expect(shouldAutoFormat(' some code ', mockNodeInfo('Power Puter (rgthree)', 'code'))).toBe(false);
|
||||
expect(shouldAutoFormat('\nsome code\n', mockNodeInfo('Power Puter (rgthree)', 'code'))).toBe(false);
|
||||
expect(shouldAutoFormat(' 0,0,0,1,1,1 ', mockNodeInfo('LoraLoaderBlockWeight //Inspire', 'block_vector'))).toBe(false);
|
||||
});
|
||||
|
||||
test('should prioritize blocklist over trimSurroundingSpaces when enabled', () => {
|
||||
settingValues.trimSurroundingSpaces = true;
|
||||
|
||||
// Blocklisted nodes should return false even with trimSurroundingSpaces enabled and surrounding spaces
|
||||
expect(shouldAutoFormat(' some code ', mockNodeInfo('Power Puter (rgthree)', 'code'))).toBe(false);
|
||||
expect(shouldAutoFormat('\nsome code\n', mockNodeInfo('Power Puter (rgthree)', 'code'))).toBe(false);
|
||||
expect(shouldAutoFormat(' function() { } ', mockNodeInfo('Power Puter (rgthree)', 'code'))).toBe(false);
|
||||
expect(shouldAutoFormat(' 0,0,0,1,1,1 ', mockNodeInfo('LoraLoaderBlockWeight //Inspire', 'block_vector'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('trimSurroundingSpaces with non-comma-separated text', () => {
|
||||
test('should return false for non-comma-separated text when trimSurroundingSpaces is disabled', () => {
|
||||
settingValues.trimSurroundingSpaces = false;
|
||||
|
||||
// Text without word+comma pattern should return false
|
||||
expect(shouldAutoFormat(' hello world ', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(false);
|
||||
expect(shouldAutoFormat('\nhello world\n', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(false);
|
||||
expect(shouldAutoFormat(' tag1 tag2 ', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(false);
|
||||
});
|
||||
|
||||
test('should return true for non-comma-separated text with surrounding spaces when trimSurroundingSpaces is enabled', () => {
|
||||
settingValues.trimSurroundingSpaces = true;
|
||||
|
||||
// Text with surrounding spaces should return true to format (trim them)
|
||||
expect(shouldAutoFormat(' hello world ', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(true);
|
||||
expect(shouldAutoFormat('\nhello world\n', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(true);
|
||||
expect(shouldAutoFormat(' tag1 tag2 ', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(true);
|
||||
expect(shouldAutoFormat(' \nsome text\n ', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false for text without surrounding spaces even when trimSurroundingSpaces is enabled', () => {
|
||||
settingValues.trimSurroundingSpaces = true;
|
||||
|
||||
// Text without word+comma pattern and without surrounding spaces should return false
|
||||
expect(shouldAutoFormat('hello world', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(false);
|
||||
expect(shouldAutoFormat('tag1 tag2', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatPromptText', () => {
|
||||
// Store original setting value to restore after tests
|
||||
const originalUseTrailingComma = settingValues.useTrailingComma;
|
||||
const originalTrimSurroundingSpaces = settingValues.trimSurroundingSpaces;
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original setting after each test
|
||||
settingValues.useTrailingComma = originalUseTrailingComma;
|
||||
settingValues.trimSurroundingSpaces = originalTrimSurroundingSpaces;
|
||||
});
|
||||
|
||||
describe('with useTrailingComma enabled', () => {
|
||||
beforeEach(() => {
|
||||
settingValues.useTrailingComma = true;
|
||||
settingValues.trimSurroundingSpaces = false;
|
||||
});
|
||||
|
||||
test('should format text by adding comma and space after tags', () => {
|
||||
const input = 'tag1,tag2,tag3';
|
||||
const expected = 'tag1, tag2, tag3, ';
|
||||
expect(formatPromptText(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test('should remove extra spaces around tags', () => {
|
||||
const input = ' tag1 , tag2 ';
|
||||
const expected = 'tag1, tag2, ';
|
||||
expect(formatPromptText(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test('should preserve special syntax like weights', () => {
|
||||
const input = '(tag1:1.2), [tag2]';
|
||||
// Note: The current implementation splits by comma.
|
||||
// If the input is "(tag1:1.2), [tag2]", it splits into "(tag1:1.2)" and "[tag2]".
|
||||
// Then joins with ", ".
|
||||
const expected = '(tag1:1.2), [tag2], ';
|
||||
expect(formatPromptText(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test('should handle multiple lines', () => {
|
||||
const input = 'tag1, tag2\ntag3, tag4';
|
||||
const expected = 'tag1, tag2, \ntag3, tag4, ';
|
||||
expect(formatPromptText(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test('should keep empty lines unchanged', () => {
|
||||
const input = 'tag1, tag2\n\ntag3, tag4';
|
||||
const expected = 'tag1, tag2, \n\ntag3, tag4, ';
|
||||
expect(formatPromptText(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test('should not modify text that already has trailing comma', () => {
|
||||
const input = 'tag1, tag2, ';
|
||||
const expected = 'tag1, tag2, ';
|
||||
expect(formatPromptText(input)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with useTrailingComma disabled', () => {
|
||||
beforeEach(() => {
|
||||
settingValues.useTrailingComma = false;
|
||||
settingValues.trimSurroundingSpaces = false;
|
||||
});
|
||||
|
||||
test('should format text by adding comma and space after tags without trailing comma', () => {
|
||||
const input = 'tag1,tag2,tag3';
|
||||
const expected = 'tag1, tag2, tag3';
|
||||
expect(formatPromptText(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test('should remove extra spaces around tags without trailing comma', () => {
|
||||
const input = ' tag1 , tag2 ';
|
||||
const expected = 'tag1, tag2';
|
||||
expect(formatPromptText(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test('should preserve special syntax like weights without trailing comma', () => {
|
||||
const input = '(tag1:1.2), [tag2]';
|
||||
// Note: The current implementation splits by comma.
|
||||
// If the input is "(tag1:1.2), [tag2]", it splits into "(tag1:1.2)" and "[tag2]".
|
||||
// Then joins with ", ".
|
||||
const expected = '(tag1:1.2), [tag2]';
|
||||
expect(formatPromptText(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test('should handle multiple lines without trailing comma', () => {
|
||||
const input = 'tag1, tag2\ntag3, tag4';
|
||||
const expected = 'tag1, tag2\ntag3, tag4';
|
||||
expect(formatPromptText(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test('should keep empty lines unchanged without trailing comma', () => {
|
||||
const input = 'tag1, tag2\n\ntag3, tag4';
|
||||
const expected = 'tag1, tag2\n\ntag3, tag4';
|
||||
expect(formatPromptText(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test('should remove existing trailing comma when disabled', () => {
|
||||
const input = 'tag1, tag2, ';
|
||||
const expected = 'tag1, tag2';
|
||||
expect(formatPromptText(input)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with trimSurroundingSpaces enabled', () => {
|
||||
beforeEach(() => {
|
||||
settingValues.trimSurroundingSpaces = true;
|
||||
settingValues.useTrailingComma = false;
|
||||
});
|
||||
|
||||
test('should remove trailing newlines and spaces', () => {
|
||||
const input = 'tag1, tag2\n\n ';
|
||||
const expected = 'tag1, tag2';
|
||||
expect(formatPromptText(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test('should remove leading newlines and spaces', () => {
|
||||
const input = ' \n\ntag1, tag2';
|
||||
const expected = 'tag1, tag2';
|
||||
expect(formatPromptText(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test('should remove both leading and trailing whitespaces but keep middle empty lines', () => {
|
||||
const input = ' \n\ntag1\n\ntag2\n\n ';
|
||||
const expected = 'tag1\n\ntag2';
|
||||
expect(formatPromptText(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test('should return empty string if input is only whitespace', () => {
|
||||
const input = ' \n ';
|
||||
expect(formatPromptText(input)).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with trimSurroundingSpaces disabled', () => {
|
||||
beforeEach(() => {
|
||||
settingValues.trimSurroundingSpaces = false;
|
||||
settingValues.useTrailingComma = false;
|
||||
});
|
||||
|
||||
test('should preserve trailing newlines', () => {
|
||||
const input = 'tag1, tag2\n\n';
|
||||
const expected = 'tag1, tag2\n\n';
|
||||
expect(formatPromptText(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test('should preserve leading spaces (as first line content)', () => {
|
||||
const input = ' \ntag1';
|
||||
const expected = '\ntag1';
|
||||
expect(formatPromptText(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test('should handle input with only spaces', () => {
|
||||
expect(formatPromptText(' ')).toBe(' ');
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle empty input', () => {
|
||||
expect(formatPromptText('')).toBe('');
|
||||
expect(formatPromptText(null)).toBe(null);
|
||||
expect(formatPromptText(undefined)).toBe(undefined);
|
||||
});
|
||||
|
||||
test('should handle input with only spaces', () => {
|
||||
expect(formatPromptText(' ')).toBe(' ');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -353,7 +353,18 @@ describe('Autocomplete Functions', () => {
|
||||
insertTagToTextArea(textarea, tagData);
|
||||
}).not.toThrow();
|
||||
|
||||
expect(textarea.value).toBe('<lora:my_model>, ');
|
||||
expect(textarea.value).toBe('<lora:my_model:1.0>, ');
|
||||
});
|
||||
|
||||
test('should complete embedding tag without adding weight', () => {
|
||||
const textarea = createMockTextarea('embedding:', 10);
|
||||
const tagData = { tag: 'embedding:my_embedding', source: 'embeddings' };
|
||||
|
||||
expect(() => {
|
||||
insertTagToTextArea(textarea, tagData);
|
||||
}).not.toThrow();
|
||||
|
||||
expect(textarea.value).toBe('embedding:my_embedding, ');
|
||||
});
|
||||
|
||||
test('should handle wildcard tag insertion', () => {
|
||||
|
||||
+100
-1
@@ -4,7 +4,8 @@ import {
|
||||
normalizeTagToSearch,
|
||||
normalizeTagToInsert,
|
||||
getCurrentTagRange,
|
||||
findAllTagPositions
|
||||
findAllTagPositions,
|
||||
addWeightToLora
|
||||
} from '../../web/js/utils.js';
|
||||
|
||||
// filepath: web/js/utils.test.js
|
||||
@@ -440,3 +441,101 @@ describe('findAllTagPositions', () => {
|
||||
expect(positions[5].tag).toBe('last');
|
||||
});
|
||||
});
|
||||
|
||||
describe('addWeightToModel', () => {
|
||||
test('should return null or empty string for invalid inputs', () => {
|
||||
expect(addWeightToLora(null)).toBeNull();
|
||||
expect(addWeightToLora(undefined)).toBeUndefined();
|
||||
expect(addWeightToLora('')).toBe('');
|
||||
});
|
||||
|
||||
// LoRA format tests
|
||||
test('should add default weight to LoRA without weight', () => {
|
||||
expect(addWeightToLora('<lora:my_style01>')).toBe('<lora:my_style01:1.0>');
|
||||
expect(addWeightToLora('<lora:anime_style>')).toBe('<lora:anime_style:1.0>');
|
||||
expect(addWeightToLora('<lora:character_lora>')).toBe('<lora:character_lora:1.0>');
|
||||
});
|
||||
|
||||
test('should preserve existing weight in LoRA', () => {
|
||||
expect(addWeightToLora('<lora:my_style01:0.8>')).toBe('<lora:my_style01:0.8>');
|
||||
expect(addWeightToLora('<lora:anime_style:1.2>')).toBe('<lora:anime_style:1.2>');
|
||||
expect(addWeightToLora('<lora:character_lora:0.5>')).toBe('<lora:character_lora:0.5>');
|
||||
});
|
||||
|
||||
test('should handle LoRA with path separators', () => {
|
||||
expect(addWeightToLora('<lora:folder/my_style01>')).toBe('<lora:folder/my_style01:1.0>');
|
||||
expect(addWeightToLora('<lora:folder/subfolder/style>')).toBe('<lora:folder/subfolder/style:1.0>');
|
||||
expect(addWeightToLora('<lora:folder/my_style01:0.7>')).toBe('<lora:folder/my_style01:0.7>');
|
||||
});
|
||||
|
||||
test('should handle LoRA with symbols', () => {
|
||||
expect(addWeightToLora('<lora:folder/myLora style>')).toBe('<lora:folder/myLora style:1.0>');
|
||||
expect(addWeightToLora('<lora:folder/myLora-style>')).toBe('<lora:folder/myLora-style:1.0>');
|
||||
expect(addWeightToLora('<lora:folder/myLora[style]>')).toBe('<lora:folder/myLora[style]:1.0>');
|
||||
expect(addWeightToLora('<lora:folder/myLora v1.0>')).toBe('<lora:folder/myLora v1.0:1.0>');
|
||||
expect(addWeightToLora('<lora:folder/myLora v1.0:0.75>')).toBe('<lora:folder/myLora v1.0:0.75>');
|
||||
});
|
||||
|
||||
test('should handle case-insensitive LoRA tags', () => {
|
||||
expect(addWeightToLora('<LORA:my_style01>')).toBe('<LORA:my_style01:1.0>');
|
||||
expect(addWeightToLora('<LoRa:anime_style>')).toBe('<LoRa:anime_style:1.0>');
|
||||
expect(addWeightToLora('<Lora:character_lora:0.9>')).toBe('<Lora:character_lora:0.9>');
|
||||
});
|
||||
|
||||
// Custom default weight tests
|
||||
test('should use custom default weight for LoRA', () => {
|
||||
expect(addWeightToLora('<lora:my_style01>', 0.5)).toBe('<lora:my_style01:0.5>');
|
||||
expect(addWeightToLora('<lora:anime_style>', 1.5)).toBe('<lora:anime_style:1.5>');
|
||||
expect(addWeightToLora('<lora:character_lora>', 2.0)).toBe('<lora:character_lora:2.0>');
|
||||
});
|
||||
|
||||
test('should not override existing weight even with custom default', () => {
|
||||
expect(addWeightToLora('<lora:my_style01:0.8>', 2.0)).toBe('<lora:my_style01:0.8>');
|
||||
});
|
||||
|
||||
// Non-model format tests
|
||||
test('should return non-model tags as-is', () => {
|
||||
expect(addWeightToLora('blue_hair')).toBe('blue_hair');
|
||||
expect(addWeightToLora('1girl')).toBe('1girl');
|
||||
expect(addWeightToLora('standing:1.2')).toBe('standing:1.2');
|
||||
expect(addWeightToLora('<other:tag>')).toBe('<other:tag>');
|
||||
expect(addWeightToLora('random:text:format')).toBe('random:text:format');
|
||||
});
|
||||
|
||||
// Edge cases
|
||||
test('should handle malformed LoRA tags', () => {
|
||||
expect(addWeightToLora('<lora:>')).toBe('<lora:>');
|
||||
expect(addWeightToLora('<lora>')).toBe('<lora>');
|
||||
expect(addWeightToLora('lora:my_style01')).toBe('lora:my_style01');
|
||||
});
|
||||
|
||||
test('should handle decimal weights correctly', () => {
|
||||
expect(addWeightToLora('<lora:style:0.123>')).toBe('<lora:style:0.123>');
|
||||
expect(addWeightToLora('<lora:style:1.999>')).toBe('<lora:style:1.999>');
|
||||
});
|
||||
|
||||
test('should handle special characters in model names', () => {
|
||||
expect(addWeightToLora('<lora:my-style_01>')).toBe('<lora:my-style_01:1.0>');
|
||||
expect(addWeightToLora('<lora:style.v2>')).toBe('<lora:style.v2:1.0>');
|
||||
});
|
||||
|
||||
// Embedding format tests - should not be modified
|
||||
test('should not modify embedding tags without weight', () => {
|
||||
expect(addWeightToLora('embedding:my_embedding')).toBe('embedding:my_embedding');
|
||||
expect(addWeightToLora('embedding:character_emb')).toBe('embedding:character_emb');
|
||||
expect(addWeightToLora('embedding:style_embedding')).toBe('embedding:style_embedding');
|
||||
});
|
||||
|
||||
test('should not modify embedding tags with existing weight', () => {
|
||||
expect(addWeightToLora('embedding:my_embedding:0.8')).toBe('embedding:my_embedding:0.8');
|
||||
expect(addWeightToLora('embedding:character_emb:1.2')).toBe('embedding:character_emb:1.2');
|
||||
expect(addWeightToLora('embedding:style_embedding:0.5')).toBe('embedding:style_embedding:0.5');
|
||||
});
|
||||
|
||||
test('should not modify embedding tags regardless of case', () => {
|
||||
expect(addWeightToLora('EMBEDDING:my_embedding')).toBe('EMBEDDING:my_embedding');
|
||||
expect(addWeightToLora('Embedding:character_emb')).toBe('Embedding:character_emb');
|
||||
expect(addWeightToLora('EmBeDdInG:style_emb')).toBe('EmBeDdInG:style_emb');
|
||||
expect(addWeightToLora('EMBEDDING:my_embedding:0.9')).toBe('EMBEDDING:my_embedding:0.9');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
/* Light Theme */
|
||||
body {
|
||||
--autocomplete-plus-text-color-cat-blue: var(--p-blue-600);
|
||||
--autocomplete-plus-text-color-cat-blue: var(--p-blue-700);
|
||||
--autocomplete-plus-text-color-cat-red: var(--p-red-600);
|
||||
--autocomplete-plus-text-color-cat-purple: var(--p-purple-600);
|
||||
--autocomplete-plus-text-color-cat-green: var(--p-green-600);
|
||||
--autocomplete-plus-text-color-cat-green: var(--p-green-700);
|
||||
--autocomplete-plus-text-color-cat-yellow: var(--p-yellow-600);
|
||||
--autocomplete-plus-text-color-cat-gray: var(--p-gray-600);
|
||||
--autocomplete-plus-text-color-cat-sky: var(--p-sky-500);
|
||||
--autocomplete-plus-text-color-cat-sky: var(--p-sky-600);
|
||||
--autocomplete-plus-text-color-cat-orange: var(--p-orange-600);
|
||||
--autocomplete-plus-text-color-cat-white: var(--p-neutral-700);
|
||||
--autocomplete-plus-text-color-disabled: var(--p-gray-400);
|
||||
--autocomplete-plus-list-color-even: var(--p-neutral-300);
|
||||
--autocomplete-plus-list-color-odd: var(--p-neutral-400);
|
||||
--wiki-icon-text-color: var(--input-text);
|
||||
--wiki-icon-opacity-normal: 0.7;
|
||||
--wiki-icon-opacity-hover: 1.0;
|
||||
--wiki-icon-opacity-disabled: 0;
|
||||
}
|
||||
|
||||
/* Dark Theme */
|
||||
@@ -24,6 +30,12 @@ body.dark-theme {
|
||||
--autocomplete-plus-text-color-cat-orange: var(--p-orange-400);
|
||||
--autocomplete-plus-text-color-cat-white: var(--p-neutral-200);
|
||||
--autocomplete-plus-text-color-disabled: var(--p-gray-500);
|
||||
--autocomplete-plus-list-color-even: var(--p-neutral-800);
|
||||
--autocomplete-plus-list-color-odd: var(--p-neutral-900);
|
||||
--wiki-icon-text-color: var(--input-text);
|
||||
--wiki-icon-opacity-normal: 0.5;
|
||||
--wiki-icon-opacity-hover: 0.9;
|
||||
--wiki-icon-opacity-disabled: 0;
|
||||
}
|
||||
|
||||
#autocomplete-plus-root {
|
||||
@@ -97,16 +109,17 @@ body.dark-theme {
|
||||
display: grid;
|
||||
box-shadow: 0 2px 8px rgb(0 0 0 / 30%);
|
||||
grid-auto-rows: auto;
|
||||
grid-template-columns: max-content 1fr auto;
|
||||
grid-template-columns: max-content max-content 1fr auto;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#autocomplete-plus-list.no-alias {
|
||||
grid-template-columns: max-content auto;
|
||||
grid-template-columns: max-content max-content auto;
|
||||
}
|
||||
|
||||
.autocomplete-plus-item {
|
||||
display: grid;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
cursor: pointer;
|
||||
grid-column: 1 / -1;
|
||||
grid-template-columns: subgrid;
|
||||
@@ -115,23 +128,22 @@ body.dark-theme {
|
||||
.autocomplete-plus-item span {
|
||||
align-content: center;
|
||||
padding: 4px 8px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
/* Alternating row colors */
|
||||
.autocomplete-plus-item:nth-child(even) span {
|
||||
background-color: var(--comfy-menu-bg);
|
||||
.autocomplete-plus-item:nth-child(even) {
|
||||
background-color: var(--autocomplete-plus-list-color-even);
|
||||
}
|
||||
|
||||
.autocomplete-plus-item:nth-child(odd) span {
|
||||
background-color: var(--comfy-menu-secondary-bg);
|
||||
.autocomplete-plus-item:nth-child(odd) {
|
||||
background-color: var(--autocomplete-plus-list-color-odd);
|
||||
}
|
||||
|
||||
.autocomplete-plus-item.selected span {
|
||||
.autocomplete-plus-item.selected {
|
||||
background-color: var(--border-color);
|
||||
}
|
||||
|
||||
.autocomplete-plus-item:hover span {
|
||||
.autocomplete-plus-item:hover {
|
||||
background-color: var(--comfy-hover-bg);
|
||||
}
|
||||
|
||||
@@ -160,6 +172,28 @@ body.dark-theme {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.autocomplete-plus-item .autocomplete-plus-wiki-icon {
|
||||
padding: 0 8px;
|
||||
color: var(--wiki-icon-text-color);
|
||||
cursor: pointer;
|
||||
font-size: 1.2em;
|
||||
opacity: var(--wiki-icon-opacity-normal);
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.autocomplete-plus-item .autocomplete-plus-wiki-icon:hover {
|
||||
filter: brightness(1.1);
|
||||
opacity: var(--wiki-icon-opacity-hover);
|
||||
}
|
||||
|
||||
/* Hide wiki icon for tags without wiki pages */
|
||||
.autocomplete-plus-item .autocomplete-plus-wiki-icon.disabled {
|
||||
cursor: default;
|
||||
opacity: var(--wiki-icon-opacity-disabled);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Related Tags UI Styles */
|
||||
#related-tags-root {
|
||||
position: absolute;
|
||||
@@ -236,7 +270,7 @@ body.dark-theme {
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background-color: var(--comfy-menu-bg);
|
||||
background-color: var(--autocomplete-plus-list-color-even);
|
||||
color: var(--descrip-text);
|
||||
gap: 8px;
|
||||
}
|
||||
@@ -255,10 +289,16 @@ body.dark-theme {
|
||||
}
|
||||
|
||||
.related-tags-header-tag-name {
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.related-tags-header-tag-name.disabled {
|
||||
cursor: default;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.related-tags-header-tag-name .autocomplete-plus-tag-icon-svg {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
@@ -296,16 +336,17 @@ body.dark-theme {
|
||||
display: grid;
|
||||
box-shadow: 0 2px 8px rgb(0 0 0 / 30%);
|
||||
grid-auto-rows: auto;
|
||||
grid-template-columns: max-content 1fr auto;
|
||||
grid-template-columns: max-content max-content 1fr auto;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#related-tags-list.no-alias {
|
||||
grid-template-columns: max-content auto;
|
||||
grid-template-columns: max-content max-content auto;
|
||||
}
|
||||
|
||||
.related-tag-item {
|
||||
display: grid;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
cursor: pointer;
|
||||
grid-column: 1 / -1;
|
||||
grid-template-columns: subgrid;
|
||||
@@ -315,22 +356,21 @@ body.dark-theme {
|
||||
.related-tag-item span {
|
||||
align-content: center;
|
||||
padding: 4px 8px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.related-tag-item:nth-child(even) span {
|
||||
background-color: var(--comfy-menu-bg);
|
||||
.related-tag-item:nth-child(even) {
|
||||
background-color: var(--autocomplete-plus-list-color-even);
|
||||
}
|
||||
|
||||
.related-tag-item:nth-child(odd) span {
|
||||
background-color: var(--comfy-menu-secondary-bg);
|
||||
.related-tag-item:nth-child(odd) {
|
||||
background-color: var(--autocomplete-plus-list-color-odd);
|
||||
}
|
||||
|
||||
.related-tag-item:hover span {
|
||||
.related-tag-item:hover {
|
||||
background-color: var(--p-form-field-filled-hover-background);
|
||||
}
|
||||
|
||||
.related-tag-item.selected span {
|
||||
.related-tag-item.selected {
|
||||
background-color: var(--border-color);
|
||||
}
|
||||
|
||||
@@ -355,6 +395,29 @@ body.dark-theme {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.related-tag-item .related-tag-wiki-icon {
|
||||
padding: 0 8px;
|
||||
color: var(--wiki-icon-text-color);
|
||||
cursor: pointer;
|
||||
font-size: 1.2em;
|
||||
opacity: var(--wiki-icon-opacity-normal);
|
||||
text-align: center;
|
||||
transition: opacity 0.1s ease;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.related-tag-item .related-tag-wiki-icon:hover {
|
||||
filter: brightness(1.1);
|
||||
opacity: var(--wiki-icon-opacity-hover);
|
||||
}
|
||||
|
||||
/* Hide wiki icon for tags without wiki pages */
|
||||
.related-tag-item .related-tag-wiki-icon.disabled {
|
||||
cursor: default;
|
||||
opacity: var(--wiki-icon-opacity-disabled);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.related-tags-message {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
import { settingValues } from './settings.js';
|
||||
import { NodeInfo } from './node-info.js';
|
||||
|
||||
|
||||
/**
|
||||
* Determines if the text content should be auto-formatted.
|
||||
*
|
||||
* Format conditions:
|
||||
* 1. Skip formatting if node is in blocklist
|
||||
* 2. If text is empty after trimming, format only if trimSurroundingSpaces is enabled, otherwise don't format
|
||||
* 3. Skip formatting if text contains only numbers or single letters (separated by commas)
|
||||
* 4. Format if text contains "word + comma" pattern
|
||||
* 5. Format if trimSurroundingSpaces is enabled and there are surrounding spaces or line breaks
|
||||
* 6. Otherwise, don't format
|
||||
*
|
||||
* @param {NodeInfo} nodeInfo - The node information.
|
||||
* @returns {boolean} - True if the text should be formatted, false otherwise.
|
||||
*/
|
||||
function shouldAutoFormat(text, nodeInfo) {
|
||||
if (!text) return false;
|
||||
|
||||
// 1. Check if the node name is in the blocklist
|
||||
const blocklist = [
|
||||
["Power Puter (rgthree)", "code"],
|
||||
["LoraLoaderBlockWeight //Inspire", "block_vector"]
|
||||
];
|
||||
|
||||
const isBlocklisted = blocklist.some(([type, input]) =>
|
||||
type === nodeInfo.nodeType && input === nodeInfo.inputName
|
||||
);
|
||||
|
||||
if (isBlocklisted) {
|
||||
// console.debug(`[Autocomplete-Plus] auto-formatter on blur => nodeType: ${nodeInfo.nodeType}, inputName: ${nodeInfo.inputName} => blocklisted`);
|
||||
return false;
|
||||
} else {
|
||||
// console.debug(`[Autocomplete-Plus] auto-formatter on blur => nodeType: ${nodeInfo.nodeType}, inputName: ${nodeInfo.inputName}`);
|
||||
}
|
||||
|
||||
const trimmedText = text.trim();
|
||||
|
||||
// 2. Check if the text is completely empty after trimming
|
||||
if (trimmedText.length === 0) {
|
||||
return settingValues.trimSurroundingSpaces;
|
||||
}
|
||||
|
||||
// 3. Check if the text is purely numeric data or single-letter placeholders with commas
|
||||
// (e.g., "0,0,0,1,1,1" or "0.5, -1.2, 0.8" or "A,B,R" for LoRA Block Weight)
|
||||
const elements = trimmedText.split(',').map(el => el.trim());
|
||||
const isSingleLetterOrNumeric = elements.every(el => {
|
||||
if (/^[A-Za-z]$/.test(el)) return true;
|
||||
if (/^-?\d+(\.\d+)?$/.test(el)) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
if (isSingleLetterOrNumeric && elements.length > 0) {
|
||||
return false; // Don't format numeric data or single-letter template patterns
|
||||
}
|
||||
|
||||
// 4. If text contains "word + comma" pattern, format it
|
||||
const wordCommaPattern = /\w+\s*,/g;
|
||||
if (trimmedText.match(wordCommaPattern)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 5. If trimSurroundingSpaces is enabled and there are surrounding spaces, format to trim them
|
||||
if (settingValues.trimSurroundingSpaces && text !== trimmedText) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false; // Otherwise, don't format
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the prompt text: add a comma and space after each tag, and remove extra spaces.
|
||||
* Preserve special syntax such as weights (tag:1.2), parentheses, and wildcards.
|
||||
* @param {string} text - The original text.
|
||||
* @returns {string} - The formatted text.
|
||||
*/
|
||||
export function formatPromptText(text) {
|
||||
// Handle null or undefined input
|
||||
if (text == null) return text;
|
||||
|
||||
// If the text is empty or contains only spaces...
|
||||
if (text.trim().length === 0) {
|
||||
if (settingValues.trimSurroundingSpaces) {
|
||||
// ...And trimSurroundingSpaces is enabled, return an empty string
|
||||
return '';
|
||||
} else {
|
||||
// ...Otherwise, return the original text
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
let processedText = text;
|
||||
if (settingValues.trimSurroundingSpaces) {
|
||||
processedText = text.trim();
|
||||
}
|
||||
|
||||
// Split text into individual lines for processing
|
||||
const lines = processedText.split('\n');
|
||||
const formattedLines = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim();
|
||||
|
||||
// Keep empty lines unchanged
|
||||
if (trimmedLine.length === 0) {
|
||||
formattedLines.push('');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Split the line by commas into raw tag segments
|
||||
const rawTags = trimmedLine.split(',');
|
||||
|
||||
const cleanedTags = [];
|
||||
for (const tag of rawTags) {
|
||||
// Trim spaces around each tag
|
||||
const trimmedTag = tag.trim();
|
||||
|
||||
// Keep only non-empty tags
|
||||
if (trimmedTag.length > 0) {
|
||||
cleanedTags.push(trimmedTag);
|
||||
}
|
||||
}
|
||||
|
||||
// Rejoin cleaned tags with ", "
|
||||
let formattedLine = cleanedTags.join(', ');
|
||||
|
||||
// Add a trailing comma and space if the setting is enabled and line contains valid tags
|
||||
if (formattedLine.length > 0 && settingValues.useTrailingComma) {
|
||||
formattedLine += ', ';
|
||||
}
|
||||
|
||||
formattedLines.push(formattedLine);
|
||||
}
|
||||
|
||||
// Rejoin all lines with newline characters
|
||||
return formattedLines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the content of a textarea when it loses focus.
|
||||
* @param {HTMLTextAreaElement} textarea - The target textarea element.
|
||||
*/
|
||||
export function formatTextareaOnBlur(textarea) {
|
||||
const originalText = textarea.value;
|
||||
const formattedText = formatPromptText(originalText);
|
||||
|
||||
if (originalText !== formattedText) {
|
||||
const cursorPos = textarea.selectionStart;
|
||||
textarea.value = formattedText;
|
||||
|
||||
// Try to preserve cursor position
|
||||
const newCursorPos = Math.min(cursorPos, formattedText.length);
|
||||
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler for auto-formatting functionality
|
||||
*/
|
||||
export class AutoFormatterEventHandler {
|
||||
constructor() { }
|
||||
|
||||
/**
|
||||
* Handle blur event to trigger auto-formatting
|
||||
* @param {Event} event - The blur event
|
||||
*/
|
||||
handleBlur(event, nodeInfo) {
|
||||
if (
|
||||
settingValues.enableAutoFormat &&
|
||||
settingValues.autoFormatTrigger === 'auto' &&
|
||||
event.target.tagName === 'TEXTAREA'
|
||||
) {
|
||||
const textarea = event.target;
|
||||
const text = textarea.value;
|
||||
|
||||
// Check if the content should be auto-formatted
|
||||
if (shouldAutoFormat(text, nodeInfo)) {
|
||||
formatTextareaOnBlur(textarea);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Placeholder methods to maintain consistency with other event handlers
|
||||
handleInput(event) { }
|
||||
handleFocus(event) { }
|
||||
handleKeyDown(event) { }
|
||||
handleKeyUp(event) { }
|
||||
handleMouseMove(event) { }
|
||||
handleClick(event) { }
|
||||
|
||||
/**
|
||||
* Format textarea content via manual trigger (e.g., keyboard shortcut)
|
||||
* @param {HTMLTextAreaElement} textarea - The textarea element to format
|
||||
* @param {NodeInfo} nodeInfo - The node information
|
||||
* @returns {boolean} - True if formatting was performed, false otherwise
|
||||
*/
|
||||
applyFormatTextarea(textarea, nodeInfo) {
|
||||
if (!textarea || textarea.tagName !== 'TEXTAREA') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!settingValues.enableAutoFormat) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const text = textarea.value;
|
||||
|
||||
if (shouldAutoFormat(text, nodeInfo)) {
|
||||
formatTextareaOnBlur(textarea);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Export functions for testing
|
||||
const isTestEnvironment = typeof process !== 'undefined' && process.env.NODE_ENV === 'test';
|
||||
export const __test__ = isTestEnvironment
|
||||
? {
|
||||
shouldAutoFormat
|
||||
}
|
||||
: undefined;
|
||||
+83
-23
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
ModelTagSource,
|
||||
TagCategory,
|
||||
TagData,
|
||||
autoCompleteData,
|
||||
getEnabledTagSourceInPriorityOrder
|
||||
@@ -18,7 +17,9 @@ import {
|
||||
getCurrentTagRange,
|
||||
getViewportMargin,
|
||||
getScrollbarWidth,
|
||||
IconSvgHtmlString
|
||||
IconSvgHtmlString,
|
||||
addWeightToLora,
|
||||
openTagWikiUrl
|
||||
} from './utils.js';
|
||||
import { settingValues } from './settings.js';
|
||||
|
||||
@@ -305,7 +306,7 @@ function getCurrentPartialTag(inputElement) {
|
||||
* Inserts the selected tag into the textarea, replacing the partial tag,
|
||||
* making the change undoable.
|
||||
* @param {HTMLTextAreaElement} inputElement
|
||||
* @param {TagData} tagDataToInsert The raw tag string to insert.
|
||||
* @param {TagData} tagDataToInsert
|
||||
*/
|
||||
function insertTagToTextArea(inputElement, tagDataToInsert) {
|
||||
if (!inputElement || !tagDataToInsert) {
|
||||
@@ -331,8 +332,11 @@ function insertTagToTextArea(inputElement, tagDataToInsert) {
|
||||
let replaceEnd = cursorPos;
|
||||
|
||||
let normalizedTag;
|
||||
if (Object.values(ModelTagSource).includes(tagDataToInsert.source)) {
|
||||
// If the tag is from a model tag source, don't want to normalize it
|
||||
if (tagDataToInsert.source === ModelTagSource.Lora) {
|
||||
// If the tag is from a LoRA source, add weight to it
|
||||
normalizedTag = addWeightToLora(tagDataToInsert.tag);
|
||||
} else if (Object.values(ModelTagSource).includes(tagDataToInsert.source)) {
|
||||
// If the tag is from other model tag sources (e.g., Embeddings), don't normalize it
|
||||
normalizedTag = tagDataToInsert.tag;
|
||||
} else {
|
||||
normalizedTag = normalizeTagToInsert(tagDataToInsert.tag);
|
||||
@@ -347,11 +351,13 @@ function insertTagToTextArea(inputElement, tagDataToInsert) {
|
||||
const needsSpaceBefore = text[replaceStart - 1] === ',';
|
||||
const prefix = needsSpaceBefore ? ' ' : '';
|
||||
|
||||
// Standard separator (comma + space)
|
||||
const needsSuffixAfter = !",:".includes(text[replaceEnd]); // TODO: If ":" is part of the emoticon, a suffix is required (e.g. ":o")
|
||||
const suffix = needsSuffixAfter ? ', ' : '';
|
||||
const prefixArtist = tagDataToInsert.categoryText == 'artist' ? settingValues.prefixArtist : '';
|
||||
|
||||
const textToInsertWithAffixes = prefix + normalizedTag + suffix;
|
||||
// Standard separator (comma + space, or empty if autoInsertComma is disabled)
|
||||
const needsSuffixAfter = !",:".includes(text[replaceEnd]); // TODO: If ":" is part of the emoticon, a suffix is required (e.g. ":o")
|
||||
const suffix = (needsSuffixAfter && settingValues.autoInsertComma) ? ', ' : '';
|
||||
|
||||
const textToInsertWithAffixes = prefix + prefixArtist + normalizedTag + suffix;
|
||||
|
||||
// --- Use execCommand for Undo support ---
|
||||
// 1. Select the text range to be replaced
|
||||
@@ -403,9 +409,22 @@ class AutocompleteUI {
|
||||
|
||||
// Add event listener for clicks on items
|
||||
this.tagsList.addEventListener('mousedown', (e) => {
|
||||
// Check if wiki icon was clicked first
|
||||
const wikiIcon = e.target.closest('.autocomplete-plus-wiki-icon');
|
||||
if (wikiIcon && !wikiIcon.classList.contains('disabled')) {
|
||||
openTagWikiUrl(wikiIcon.dataset.tagSource, wikiIcon.dataset.tagName);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if row was clicked (existing behavior)
|
||||
const row = e.target.closest('.autocomplete-plus-item');
|
||||
if (row && row.dataset.tag) {
|
||||
this.#insertTag(row.dataset);
|
||||
if (row && row.dataset.index !== undefined) {
|
||||
const tagData = this.candidates[parseInt(row.dataset.index, 10)];
|
||||
if (tagData) {
|
||||
this.#insertTag(tagData);
|
||||
}
|
||||
e.preventDefault(); // Prevent focus loss from input
|
||||
e.stopPropagation();
|
||||
}
|
||||
@@ -473,7 +492,7 @@ class AutocompleteUI {
|
||||
/** Selects the currently highlighted item
|
||||
* @returns {TagData|null} The selected tag data.
|
||||
*/
|
||||
getSelectedTag() {
|
||||
getSelectedTagData() {
|
||||
if (this.selectedIndex >= 0 && this.selectedIndex < this.candidates.length) {
|
||||
return this.candidates[this.selectedIndex];
|
||||
}
|
||||
@@ -496,26 +515,27 @@ class AutocompleteUI {
|
||||
const existingTags = extractTagsFromTextArea(this.target);
|
||||
const currentTag = getCurrentPartialTag(this.target);
|
||||
|
||||
this.candidates.forEach((tagData) => {
|
||||
this.candidates.forEach((tagData, index) => {
|
||||
const isExactMatch = tagData.tag === currentTag && (existingTags.filter(item => item === currentTag).length == 1);
|
||||
const isExistingTag = !isExactMatch && existingTags.includes(tagData.tag);
|
||||
this.#createTagElement(tagData, isExistingTag);
|
||||
this.#createTagElement(tagData, index, isExistingTag);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a tag element for the autocomplete list.
|
||||
* @param {TagData} tagData
|
||||
* @param {number} tagDataIndex
|
||||
* @param {boolean} isExisting
|
||||
*/
|
||||
#createTagElement(tagData, isExisting) {
|
||||
const categoryText = TagCategory[tagData.source][tagData.category] || "unknown";
|
||||
#createTagElement(tagData, tagDataIndex, isExisting) {
|
||||
const categoryText = tagData.categoryText;
|
||||
const aliasText = tagData.alias.join(', ');
|
||||
|
||||
const tagRow = document.createElement('div');
|
||||
tagRow.classList.add('autocomplete-plus-item', tagData.source);
|
||||
tagRow.dataset.tag = tagData.tag;
|
||||
tagRow.dataset.tagCategory = categoryText;
|
||||
tagRow.dataset.index = tagDataIndex;
|
||||
tagRow.dataset.tagCategory = categoryText; // Used to color by CSS
|
||||
|
||||
// Tag icon and name
|
||||
const tagSourceIconHtml = `<svg class="autocomplete-plus-tag-icon-svg"><use xlink:href="#autocomplete-plus-icon-${tagData.source}"></use></svg>`;
|
||||
@@ -535,6 +555,18 @@ class AutocompleteUI {
|
||||
tagName.classList.add('autocomplete-plus-already-exists');
|
||||
}
|
||||
|
||||
// Wiki icon
|
||||
const wikiIcon = document.createElement('span');
|
||||
wikiIcon.className = 'autocomplete-plus-wiki-icon';
|
||||
if (tagData.hasWikiPage) {
|
||||
wikiIcon.dataset.tagName = tagData.tag;
|
||||
wikiIcon.dataset.tagSource = tagData.source;
|
||||
wikiIcon.textContent = '📖'
|
||||
wikiIcon.title = 'Open wiki page';
|
||||
} else {
|
||||
wikiIcon.classList.add('disabled');
|
||||
}
|
||||
|
||||
// Alias
|
||||
const alias = document.createElement('span');
|
||||
alias.className = 'autocomplete-plus-alias';
|
||||
@@ -550,7 +582,7 @@ class AutocompleteUI {
|
||||
tagCount.className = `autocomplete-plus-tag-count`;
|
||||
tagCount.textContent = formatCountHumanReadable(tagData.count);
|
||||
|
||||
// Create tooltip with more info
|
||||
// Create tooltip with more info
|
||||
let tooltipText = `Count: ${tagData.count}\nCategory: ${categoryText}`;
|
||||
if (aliasText.length > 0) {
|
||||
tooltipText += `\nAlias: ${aliasText}`;
|
||||
@@ -558,6 +590,7 @@ class AutocompleteUI {
|
||||
tagRow.title = tooltipText;
|
||||
|
||||
tagRow.appendChild(tagName);
|
||||
tagRow.appendChild(wikiIcon);
|
||||
|
||||
if (!settingValues.hideAlias) {
|
||||
tagRow.appendChild(alias);
|
||||
@@ -664,7 +697,7 @@ class AutocompleteUI {
|
||||
|
||||
/** Highlights the item (row) at the given index */
|
||||
#highlightItem() {
|
||||
if (!this.getSelectedTag()) return; // No valid selection
|
||||
if (!this.getSelectedTagData()) return; // No valid selection
|
||||
|
||||
const items = this.tagsList.children; // Get rows from tbody
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
@@ -908,6 +941,25 @@ export class AutocompleteEventHandler {
|
||||
constructor() {
|
||||
this.autocompleteUI = new AutocompleteUI();
|
||||
this.keyDownWithModifier = new Map(); // Keep track of keydown events with modifiers
|
||||
this._debounceTimer = null; // Timer ID for debounced search
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls autocompleteUI.updateDisplay() with debounce for sequential search, or immediately for fast search.
|
||||
* @param {HTMLTextAreaElement} target
|
||||
*/
|
||||
_triggerUpdateDisplay(target) {
|
||||
if (settingValues.useFastSearch || settingValues.searchDebounceTime <= 0) {
|
||||
// FastSearch or debounce disabled: immediate update
|
||||
this.autocompleteUI.updateDisplay(target);
|
||||
} else {
|
||||
// Sequential search: debounced update
|
||||
clearTimeout(this._debounceTimer);
|
||||
this._debounceTimer = setTimeout(() => {
|
||||
this.autocompleteUI.updateDisplay(target);
|
||||
this._debounceTimer = null;
|
||||
}, settingValues.searchDebounceTime);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -921,6 +973,7 @@ export class AutocompleteEventHandler {
|
||||
|
||||
const partialTag = getCurrentPartialTag(event.target);
|
||||
if (partialTag.length <= 0) {
|
||||
clearTimeout(this._debounceTimer); // Cancel pending debounced search
|
||||
this.autocompleteUI.hide();
|
||||
}
|
||||
}
|
||||
@@ -965,12 +1018,19 @@ export class AutocompleteEventHandler {
|
||||
case 'Enter':
|
||||
case 'Tab':
|
||||
const modifierKeyPressed = event.shiftKey || event.ctrlKey || event.altKey || event.metaKey;
|
||||
if (!modifierKeyPressed && this.autocompleteUI.getSelectedTag() !== null) {
|
||||
if (!modifierKeyPressed && this.autocompleteUI.getSelectedTagData() !== null) {
|
||||
event.preventDefault();
|
||||
insertTagToTextArea(event.target, this.autocompleteUI.getSelectedTag());
|
||||
insertTagToTextArea(event.target, this.autocompleteUI.getSelectedTagData());
|
||||
}
|
||||
this.autocompleteUI.hide();
|
||||
break;
|
||||
case 'F1':
|
||||
event.preventDefault();
|
||||
const tagData = this.autocompleteUI.getSelectedTagData();
|
||||
if (tagData && tagData.hasWikiPage) {
|
||||
openTagWikiUrl(tagData.source, tagData.tag);
|
||||
}
|
||||
break;
|
||||
case 'Escape':
|
||||
event.preventDefault();
|
||||
this.autocompleteUI.hide();
|
||||
@@ -1024,7 +1084,7 @@ export class AutocompleteEventHandler {
|
||||
// and default action is not prevented, update the display.
|
||||
// This will typically be for character inputs, Delete, Backspace or IME composition.
|
||||
if (!event.defaultPrevented) {
|
||||
this.autocompleteUI.updateDisplay(event.target);
|
||||
this._triggerUpdateDisplay(event.target);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,23 @@ export class TagData {
|
||||
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the category text
|
||||
* @returns {string}
|
||||
*/
|
||||
get categoryText() {
|
||||
return TagCategory[this.source][this.category] || "unknown";
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the tag has a wiki page
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get hasWikiPage() {
|
||||
return Object.values(TagSource).includes(this.source)
|
||||
&& ['general', 'artist', 'copyright', 'character', 'species', 'lore'].includes(this.categoryText);
|
||||
}
|
||||
}
|
||||
|
||||
class AutocompleteData {
|
||||
|
||||
+179
-40
@@ -6,23 +6,27 @@ import { loadCSS } from "./utils.js";
|
||||
import { TagSource, loadDataAsync } from "./data.js";
|
||||
import { AutocompleteEventHandler } from "./autocomplete.js";
|
||||
import { RelatedTagsEventHandler } from "./related-tags.js";
|
||||
import { AutoFormatterEventHandler } from "./auto-formatter.js";
|
||||
import { NodeInfo } from "./node-info.js";
|
||||
|
||||
// --- Constants ---
|
||||
const id = "AutocompletePlus";
|
||||
const name = "Autocomplete Plus";
|
||||
|
||||
// --- Module-level variables ---
|
||||
const autocompleteEventHandler = new AutocompleteEventHandler();
|
||||
const relatedTagsEventHandler = new RelatedTagsEventHandler();
|
||||
const autoFormatterEventHandler = new AutoFormatterEventHandler();
|
||||
const attachedElementNodeInfoMap = new WeakMap(); // Map to track attached elements and their node info
|
||||
|
||||
// --- Functions ---
|
||||
/**
|
||||
* Initialize event handlers for the autocomplete and related tags features.
|
||||
*/
|
||||
function initializeEventHandlers() {
|
||||
const autocompleteEventHandler = new AutocompleteEventHandler();
|
||||
const relatedTagsEventHandler = new RelatedTagsEventHandler();
|
||||
const attachedElements = new WeakSet(); // Keep track of elements that have listeners attached
|
||||
|
||||
// Function to attach listeners
|
||||
function attachListeners(element) {
|
||||
if (attachedElements.has(element)) return; // Prevent double attachment
|
||||
function attachListeners(element, nodeInfo) {
|
||||
if (attachedElementNodeInfoMap.has(element)) return; // Prevent double attachment
|
||||
|
||||
element.addEventListener('input', handleInput);
|
||||
element.addEventListener('focus', handleFocus);
|
||||
@@ -35,7 +39,7 @@ function initializeEventHandlers() {
|
||||
element.addEventListener('mousemove', handleMouseMove);
|
||||
element.addEventListener('click', handleClick);
|
||||
|
||||
attachedElements.add(element); // Mark as attached
|
||||
attachedElementNodeInfoMap.set(element, nodeInfo); // Mark as attached and store node info
|
||||
}
|
||||
|
||||
// Attempt Widget Override as the primary method
|
||||
@@ -46,14 +50,19 @@ function initializeEventHandlers() {
|
||||
ComfyWidgets.STRING = function (node, inputName, inputData, appInstance) { // Use appInstance to avoid conflict with global app
|
||||
const result = originalStringWidget.apply(this, arguments);
|
||||
|
||||
// Check if the widget has an inputEl and if it's a TEXTAREA
|
||||
// Check if the widget has an element and if it's a TEXTAREA
|
||||
// This is to ensure we are targeting multiline text inputs, related to '.comfy-multiline-input'
|
||||
if (result && result.widget && result.widget.inputEl && result.widget.inputEl.tagName === 'TEXTAREA') {
|
||||
const widgetConfig = inputData && inputData[1] ? inputData[1] : {};
|
||||
// Future: Add checks for Autocomplete Plus specific configurations if needed
|
||||
// e.g., if (widgetConfig["AutocompletePlus.enabled"] === false) return result;
|
||||
if (result && result.widget) {
|
||||
// fallback for older Comfyui frontend versions
|
||||
const inputEl = result.widget.element ?? result.widget.inputEl;
|
||||
if (inputEl && inputEl.tagName === 'TEXTAREA' && !inputEl.readOnly) {
|
||||
const widgetConfig = inputData && inputData[1] ? inputData[1] : {};
|
||||
// Future: Add checks for Autocomplete Plus specific configurations if needed
|
||||
// e.g., if (widgetConfig["AutocompletePlus.enabled"] === false) return result;
|
||||
|
||||
attachListeners(result.widget.inputEl);
|
||||
const nodeInfo = new NodeInfo(node.comfyClass || node.constructor.name, inputName);
|
||||
attachListeners(inputEl, nodeInfo);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
@@ -73,9 +82,11 @@ function initializeEventHandlers() {
|
||||
targetSelectors.forEach(selector => {
|
||||
// Check if the added node itself matches or contains matching elements
|
||||
if (node.matches(selector)) {
|
||||
attachListeners(node);
|
||||
attachListeners(node, new NodeInfo('Fallback', 'unknown'));
|
||||
} else {
|
||||
node.querySelectorAll(selector).forEach(attachListeners);
|
||||
node.querySelectorAll(selector).forEach(el => {
|
||||
attachListeners(el, new NodeInfo('Fallback', 'unknown'));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -85,48 +96,72 @@ function initializeEventHandlers() {
|
||||
|
||||
// Initial scan for existing elements
|
||||
targetSelectors.forEach(selector => {
|
||||
document.querySelectorAll(selector).forEach(attachListeners);
|
||||
document.querySelectorAll(selector).forEach(el => {
|
||||
attachListeners(el, new NodeInfo('Fallback', 'unknown'));
|
||||
});
|
||||
});
|
||||
|
||||
// Start observing the document body for changes
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get NodeInfo for the event target element
|
||||
* @param {Event} event - The DOM event
|
||||
* @returns {Object|null} NodeInfo object or undefined if not found
|
||||
*/
|
||||
function getNodeInfo(event) {
|
||||
const nodeInfo = attachedElementNodeInfoMap.get(event.target);
|
||||
if (!nodeInfo) {
|
||||
console.warn('[Autocomplete-Plus] Node info not found for element in ', event.target);
|
||||
return null;
|
||||
}
|
||||
|
||||
return nodeInfo;
|
||||
}
|
||||
|
||||
function handleInput(event) {
|
||||
autocompleteEventHandler.handleInput(event);
|
||||
relatedTagsEventHandler.handleInput(event);
|
||||
autoFormatterEventHandler.handleInput(event);
|
||||
}
|
||||
|
||||
function handleFocus(event) {
|
||||
autocompleteEventHandler.handleFocus(event);
|
||||
relatedTagsEventHandler.handleFocus(event);
|
||||
autoFormatterEventHandler.handleFocus(event);
|
||||
}
|
||||
|
||||
function handleBlur(event) {
|
||||
const nodeInfo = getNodeInfo(event); // Get node info to pass to auto formatter
|
||||
|
||||
autocompleteEventHandler.handleBlur(event);
|
||||
relatedTagsEventHandler.handleBlur(event);
|
||||
autoFormatterEventHandler.handleBlur(event, nodeInfo);
|
||||
}
|
||||
|
||||
function handleKeyDown(event) {
|
||||
autocompleteEventHandler.handleKeyDown(event);
|
||||
relatedTagsEventHandler.handleKeyDown(event);
|
||||
autoFormatterEventHandler.handleKeyDown(event);
|
||||
}
|
||||
|
||||
function handleKeyUp(event) {
|
||||
autocompleteEventHandler.handleKeyUp(event);
|
||||
relatedTagsEventHandler.handleKeyUp(event);
|
||||
autoFormatterEventHandler.handleKeyUp(event);
|
||||
}
|
||||
|
||||
// New event handler for mousemove to show related tags on hover
|
||||
function handleMouseMove(event) {
|
||||
autocompleteEventHandler.handleMouseMove(event);
|
||||
relatedTagsEventHandler.handleMouseMove(event);
|
||||
autoFormatterEventHandler.handleMouseMove(event);
|
||||
}
|
||||
|
||||
// New event handler for click to show related tags
|
||||
function handleClick(event) {
|
||||
autocompleteEventHandler.handleClick(event);
|
||||
relatedTagsEventHandler.handleClick(event);
|
||||
autoFormatterEventHandler.handleClick(event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,11 +279,51 @@ app.registerExtension({
|
||||
await loadDataAsync();
|
||||
},
|
||||
|
||||
// --- Commands ---
|
||||
commands: [
|
||||
{
|
||||
id: id + ".formatPrompt",
|
||||
label: name + ": Format Prompt",
|
||||
function: () => {
|
||||
const activeEl = document.activeElement;
|
||||
|
||||
if (!activeEl || activeEl.tagName !== 'TEXTAREA') {
|
||||
// console.debug('[Autocomplete-Plus] Format command: No textarea is currently focused');
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeInfo = attachedElementNodeInfoMap.get(activeEl);
|
||||
if (!nodeInfo) {
|
||||
console.warn('[Autocomplete-Plus] Format command: Node info not found for focused textarea');
|
||||
// Use fallback NodeInfo
|
||||
const fallbackNodeInfo = new NodeInfo('Unknown', 'unknown');
|
||||
autoFormatterEventHandler.applyFormatTextarea(activeEl, fallbackNodeInfo);
|
||||
return;
|
||||
}
|
||||
|
||||
const formatted = autoFormatterEventHandler.applyFormatTextarea(activeEl, nodeInfo);
|
||||
if (formatted) {
|
||||
// console.debug('[Autocomplete-Plus] Format command: Formatting applied');
|
||||
} else {
|
||||
// console.debug('[Autocomplete-Plus] Format command: Formatting skipped (blocklisted or not applicable)');
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
// --- Keybindings ---
|
||||
keybindings: [
|
||||
{
|
||||
combo: { key: "f", alt: true, shift: true },
|
||||
commandId: id + ".formatPrompt"
|
||||
}
|
||||
],
|
||||
|
||||
// One the Settings Screen, displays reverse order in same category
|
||||
settings: [
|
||||
// --- Tag source Settings ---
|
||||
{
|
||||
id: id + ".tag_source_icon_position",
|
||||
id: id + ".TagSource.IconPosition",
|
||||
name: "Tag Source Icon Position",
|
||||
type: "combo",
|
||||
options: ["left", "right", "hidden"],
|
||||
@@ -259,7 +334,7 @@ app.registerExtension({
|
||||
}
|
||||
},
|
||||
{
|
||||
id: id + ".primary_tag_source",
|
||||
id: id + ".TagSource.PrimaryTagSource",
|
||||
name: "Primary source for 'all' Source",
|
||||
tooltip: "When 'Autocomplete Tag Source' is 'all', this determines which source's tags appear first in suggestions.",
|
||||
type: "combo",
|
||||
@@ -271,7 +346,7 @@ app.registerExtension({
|
||||
}
|
||||
},
|
||||
{
|
||||
id: id + ".tag_source",
|
||||
id: id + ".TagSource",
|
||||
name: "Autocomplete Tag Source",
|
||||
tooltip: "Select the tag source for autocomplete suggestions. 'all' includes tags from all loaded sources.",
|
||||
type: "combo",
|
||||
@@ -285,9 +360,9 @@ app.registerExtension({
|
||||
|
||||
// --- Autocomplete Settings ---
|
||||
{
|
||||
id: id + ".use_fast_search",
|
||||
id: id + ".Autocompletion.UseFastSearch",
|
||||
name: "Use Fast Search",
|
||||
tooltip: "Tag search processing during text input operates faster, improving responsiveness",
|
||||
tooltip: "Tag search processing during text input operates faster, improving responsiveness.",
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
category: [name, "Autocompletion", "Use Fast Search"],
|
||||
@@ -296,9 +371,8 @@ app.registerExtension({
|
||||
}
|
||||
},
|
||||
{
|
||||
id: id + ".enable_models",
|
||||
id: id + ".Autocompletion.EnableModels",
|
||||
name: "Enable Loras and Embeddings",
|
||||
tooltip: "Enable Lora and Embedding suggestions",
|
||||
type: "boolean",
|
||||
defaultValue: true,
|
||||
category: [name, "Autocompletion", "Enable Loras and Embeddings"],
|
||||
@@ -307,7 +381,18 @@ app.registerExtension({
|
||||
}
|
||||
},
|
||||
{
|
||||
id: id + ".replace_underscore_with_space",
|
||||
id: id + ".Autocompletion.PrefixArtist",
|
||||
name: "String to add before artist tags",
|
||||
tooltip: "Text to prepend when inserting an artist tag via autocomplete.\ne.g. '@' -> '@artist_name'.",
|
||||
type: "text",
|
||||
defaultValue: '',
|
||||
category: [name, "Autocompletion", "String to add before artist tags"],
|
||||
onChange: (newVal, oldVal) => {
|
||||
settingValues.prefixArtist = newVal;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: id + ".Autocompletion.ReplaceUnderscoreWithSpace",
|
||||
name: "Replace '_' with 'Space'",
|
||||
tooltip: "This setting also affects related tags display.",
|
||||
type: "boolean",
|
||||
@@ -318,7 +403,18 @@ app.registerExtension({
|
||||
}
|
||||
},
|
||||
{
|
||||
id: id + ".max_suggestions",
|
||||
id: id + ".Autocompletion.AutoInsertComma",
|
||||
name: "Auto-Insert Comma",
|
||||
tooltip: "Automatically insert a comma after tags when inserting from autocomplete.",
|
||||
type: "boolean",
|
||||
defaultValue: true,
|
||||
category: [name, "Autocompletion", "Auto-Insert Comma"],
|
||||
onChange: (newVal, oldVal) => {
|
||||
settingValues.autoInsertComma = newVal;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: id + ".Autocompletion.MaxSuggestions",
|
||||
name: "Max suggestions",
|
||||
type: "slider",
|
||||
attrs: {
|
||||
@@ -333,9 +429,8 @@ app.registerExtension({
|
||||
}
|
||||
},
|
||||
{
|
||||
id: id + ".boolean",
|
||||
id: id + ".Autocompletion.Enable",
|
||||
name: "Enable Autocomplete",
|
||||
description: "Enable or disable the autocomplete feature.",
|
||||
type: "boolean",
|
||||
defaultValue: true,
|
||||
category: [name, "Autocompletion", "Enable Autocomplete"],
|
||||
@@ -346,9 +441,9 @@ app.registerExtension({
|
||||
|
||||
// --- Related Tags Settings ---
|
||||
{
|
||||
id: id + ".related_tags_trigger_mode",
|
||||
id: id + ".RelatedTags.RelatedTagsTriggerMode",
|
||||
name: "Related Tags Trigger Mode",
|
||||
description: "Trigger mode for related tags (click or ctrl+click)",
|
||||
tooltip: "Which action will trigger displaying related tags for the entered tag (click only, Ctrl+click).",
|
||||
type: "combo",
|
||||
options: ["click", "ctrl+Click"],
|
||||
defaultValue: "click",
|
||||
@@ -358,9 +453,9 @@ app.registerExtension({
|
||||
}
|
||||
},
|
||||
{
|
||||
id: id + ".related_tags_position",
|
||||
id: id + ".RelatedTags.DisplayPosition",
|
||||
name: "Default Display Position",
|
||||
description: "Display position (relative to Textarea)",
|
||||
tooltip: "Display position (relative to Textarea).",
|
||||
type: "combo",
|
||||
options: ["horizontal", "vertical"],
|
||||
defaultValue: "horizontal",
|
||||
@@ -370,9 +465,8 @@ app.registerExtension({
|
||||
}
|
||||
},
|
||||
{
|
||||
id: id + ".max_related_tags",
|
||||
id: id + ".RelatedTags.MaxRelatedTags",
|
||||
name: "Max related tags",
|
||||
description: "Maximum number of related tags to display",
|
||||
type: "slider",
|
||||
attrs: {
|
||||
min: 5,
|
||||
@@ -386,9 +480,8 @@ app.registerExtension({
|
||||
}
|
||||
},
|
||||
{
|
||||
id: id + ".related_tags_enable",
|
||||
id: id + ".RelatedTags.Enable",
|
||||
name: "Enable Related Tags",
|
||||
description: "Enable or disable the related tags feature.",
|
||||
type: "boolean",
|
||||
defaultValue: true,
|
||||
category: [name, "Related Tags", "Enable Related Tags"],
|
||||
@@ -399,15 +492,61 @@ app.registerExtension({
|
||||
|
||||
// --- Display settings ---
|
||||
{
|
||||
id: id + ".display_hide_alias",
|
||||
id: id + ".Display.HideAlias",
|
||||
name: "Hide Alias",
|
||||
tooltip: "Hide alias in the autocomplete and related tags display",
|
||||
tooltip: "Hide alias column in the autocomplete and related tags display.",
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
category: [name, "Display", "Hide Alias"],
|
||||
onChange: (newVal, oldVal) => {
|
||||
settingValues.hideAlias = newVal;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// --- Auto format settings ---
|
||||
{
|
||||
id: id + '.AutoFormatter.TrimSurroundingSpaces',
|
||||
name: 'Trim Surrounding Spaces',
|
||||
tooltip: 'When enabled, trim any blank lines from the beginning and end of the prompt.',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
category: [name, 'AutoFormatter', 'Trim Surrounding Spaces'],
|
||||
onChange: (newVal, oldVal) => {
|
||||
settingValues.trimSurroundingSpaces = newVal;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: id + '.AutoFormatter.UseTrailingComma',
|
||||
name: 'Use Trailing Comma',
|
||||
tooltip: 'When enabled, ensures all lines end with a trailing comma.\nWhen disabled, removes trailing commas.',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
category: [name, 'AutoFormatter', 'Use Trailing Comma'],
|
||||
onChange: (newVal, oldVal) => {
|
||||
settingValues.useTrailingComma = newVal;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: id + '.AutoFormatter.Trigger',
|
||||
name: 'Auto Format Trigger',
|
||||
tooltip: 'Auto: Format automatically when leaving text field.\nManual: Format only via keyboard shortcut. default keybind: (Alt+Shift+F)',
|
||||
type: 'combo',
|
||||
options: ['auto', 'manual'],
|
||||
defaultValue: 'auto',
|
||||
category: [name, 'AutoFormatter', 'Auto Format Trigger'],
|
||||
onChange: (newVal, oldVal) => {
|
||||
settingValues.autoFormatTrigger = newVal;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: id + '.AutoFormatter.EnableAutoFormat',
|
||||
name: 'Enable Auto Format',
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
category: [name, 'AutoFormatter', 'Enable Auto Format'],
|
||||
onChange: (newVal, oldVal) => {
|
||||
settingValues.enableAutoFormat = newVal;
|
||||
},
|
||||
},
|
||||
]
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Class to hold information about the node attached to an input element.
|
||||
* Used to control behavior based on node information.
|
||||
*/
|
||||
export class NodeInfo {
|
||||
/**
|
||||
* @param {string} nodeType - The type/class name of the node
|
||||
* @param {string} inputName - The name of the input widget
|
||||
*/
|
||||
constructor(nodeType, inputName) {
|
||||
this.nodeType = nodeType;
|
||||
this.inputName = inputName;
|
||||
}
|
||||
}
|
||||
+88
-25
@@ -1,4 +1,4 @@
|
||||
import { TagCategory, TagData, TagSource, autoCompleteData } from './data.js';
|
||||
import { TagCategory, TagData, TagSource, autoCompleteData, getEnabledTagSourceInPriorityOrder } from './data.js';
|
||||
import { settingValues } from './settings.js';
|
||||
import {
|
||||
extractTagsFromTextArea,
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
isValidTag,
|
||||
normalizeTagToInsert,
|
||||
normalizeTagToSearch,
|
||||
openTagWikiUrl
|
||||
} from './utils.js';
|
||||
|
||||
// --- RelatedTags Logic ---
|
||||
@@ -95,6 +96,8 @@ function searchRelatedTags(tag) {
|
||||
category: tagData.category,
|
||||
source: tagData.source,
|
||||
count: tagData.count,
|
||||
categoryText: tagData.categoryText,
|
||||
hasWikiPage: tagData.hasWikiPage
|
||||
});
|
||||
});
|
||||
|
||||
@@ -278,11 +281,31 @@ class RelatedTagsUI {
|
||||
// Timer ID for auto-refresh
|
||||
this.autoRefreshTimerId = null;
|
||||
|
||||
// Add click handler for wiki link in header tag name
|
||||
this.headerText.addEventListener('mousedown', (e) => {
|
||||
const tagNameEl = e.target.closest('.related-tags-header-tag-name');
|
||||
if (tagNameEl && !tagNameEl.classList.contains('disabled')) {
|
||||
openTagWikiUrl(tagNameEl.dataset.tagSource, tagNameEl.dataset.tagName);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// Add click handler for tag selection
|
||||
this.tagsContainer.addEventListener('mousedown', (e) => {
|
||||
// Check if wiki icon was clicked first
|
||||
const wikiIcon = e.target.closest('.related-tag-wiki-icon');
|
||||
if (wikiIcon && !wikiIcon.classList.contains('disabled')) {
|
||||
openTagWikiUrl(wikiIcon.dataset.tagSource, wikiIcon.dataset.tagName);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
const row = e.target.closest('.related-tag-item');
|
||||
if (row && row.dataset.tag) {
|
||||
this.#insertTag(row.dataset.tag);
|
||||
if (row && row.dataset.tagName) {
|
||||
this.#insertTag(row.dataset.tagName);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
@@ -322,9 +345,6 @@ class RelatedTagsUI {
|
||||
this.target = textareaElement;
|
||||
|
||||
this.relatedTags = searchRelatedTags(this.currentTag);
|
||||
if (this.selectedIndex == -1) {
|
||||
this.selectedIndex = 0; // Reset selection to the first item
|
||||
}
|
||||
|
||||
this.#updateHeader();
|
||||
this.#updateContent();
|
||||
@@ -366,10 +386,18 @@ class RelatedTagsUI {
|
||||
}
|
||||
}
|
||||
|
||||
/** Moves the selection up or down */
|
||||
/** Moves the selection up or down
|
||||
* @param {direction} 1 for down, -1 for up
|
||||
*/
|
||||
navigate(direction) {
|
||||
if (this.relatedTags.length === 0) return;
|
||||
this.selectedIndex += direction;
|
||||
|
||||
if (this.selectedIndex == -1) {
|
||||
// Initialize selection based on navigation direction
|
||||
this.selectedIndex = direction == 1 ? 0 : this.relatedTags.length - 1;
|
||||
} else {
|
||||
this.selectedIndex += direction;
|
||||
}
|
||||
|
||||
if (this.selectedIndex < 0) {
|
||||
this.selectedIndex = this.relatedTags.length - 1; // Wrap around to bottom
|
||||
@@ -379,10 +407,27 @@ class RelatedTagsUI {
|
||||
this.#highlightItem();
|
||||
}
|
||||
|
||||
/** Selects the currently highlighted item */
|
||||
getSelectedTag() {
|
||||
/**
|
||||
* Get TagData of the current tag
|
||||
* @returns {TagData|null}
|
||||
*/
|
||||
getCurrentTagData() {
|
||||
for (const source of getEnabledTagSourceInPriorityOrder()) {
|
||||
if (source in autoCompleteData && autoCompleteData[source].tagMap.has(this.currentTag)) {
|
||||
return autoCompleteData[source].tagMap.get(this.currentTag);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the currently highlighted item
|
||||
* @return {TagData|null}
|
||||
*/
|
||||
getSelectedTagData() {
|
||||
if (this.selectedIndex >= 0 && this.selectedIndex < this.relatedTags.length) {
|
||||
return this.relatedTags[this.selectedIndex].tag;
|
||||
return this.relatedTags[this.selectedIndex];
|
||||
}
|
||||
|
||||
return null; // No valid selection
|
||||
@@ -401,14 +446,7 @@ class RelatedTagsUI {
|
||||
* Updates header content
|
||||
*/
|
||||
#updateHeader() {
|
||||
// Find the tag data for the current tag
|
||||
let tagData = Object.values(TagSource)
|
||||
.map((source) => {
|
||||
if (source in autoCompleteData && autoCompleteData[source].tagMap.has(this.currentTag)) {
|
||||
return autoCompleteData[source].tagMap.get(this.currentTag);
|
||||
}
|
||||
})
|
||||
.find((tagData) => tagData !== undefined);
|
||||
let tagData = this.getCurrentTagData();
|
||||
|
||||
if (!tagData) {
|
||||
// Create a dummy TagData if not found
|
||||
@@ -426,6 +464,8 @@ class RelatedTagsUI {
|
||||
tagName.classList.add('related-tags-header-tag-name', tagData.source);
|
||||
tagName.title = `Count: ${tagData.count}\nCategory: ${categoryText}\nAlias: ${aliasText}`;
|
||||
tagName.dataset.tagCategory = categoryText;
|
||||
tagName.dataset.tagSource = tagData.source;
|
||||
tagName.dataset.tagName = tagData.tag;
|
||||
if (tagData.source && ['left', 'right'].includes(settingValues.tagSourceIconPosition)) {
|
||||
const tagSourceIconHtml = `<svg class="autocomplete-plus-tag-icon-svg"><use xlink:href="#autocomplete-plus-icon-${tagData.source}"></use></svg>`;
|
||||
tagName.innerHTML = settingValues.tagSourceIconPosition == 'left'
|
||||
@@ -435,6 +475,9 @@ class RelatedTagsUI {
|
||||
tagName.textContent += tagData.tag;
|
||||
}
|
||||
|
||||
if (!tagData.hasWikiPage) {
|
||||
tagName.classList.add('disabled');
|
||||
}
|
||||
|
||||
this.headerText.appendChild(tagName);
|
||||
|
||||
@@ -501,12 +544,12 @@ class RelatedTagsUI {
|
||||
* @returns {HTMLTableRowElement} The tag row element
|
||||
*/
|
||||
#createTagElement(tagData, isExisting) {
|
||||
const categoryText = TagCategory[tagData.source][tagData.category] || "unknown";
|
||||
const categoryText = tagData.categoryText;
|
||||
const aliasText = tagData.alias.join(', ');
|
||||
|
||||
const tagRow = document.createElement('div');
|
||||
tagRow.classList.add('related-tag-item', tagData.source);
|
||||
tagRow.dataset.tag = tagData.tag;
|
||||
tagRow.dataset.tagName = tagData.tag;
|
||||
tagRow.dataset.tagCategory = categoryText;
|
||||
|
||||
// Tag name
|
||||
@@ -519,6 +562,18 @@ class RelatedTagsUI {
|
||||
tagName.classList.add('related-tag-already-exists');
|
||||
}
|
||||
|
||||
// Wiki icon
|
||||
const wikiIcon = document.createElement('span');
|
||||
wikiIcon.className = 'related-tag-wiki-icon';
|
||||
if (tagData.hasWikiPage) {
|
||||
wikiIcon.dataset.tagName = tagData.tag;
|
||||
wikiIcon.dataset.tagSource = tagData.source;
|
||||
wikiIcon.textContent = '📖'
|
||||
wikiIcon.title = 'Open wiki page';
|
||||
} else {
|
||||
wikiIcon.classList.add('disabled');
|
||||
}
|
||||
|
||||
// Alias
|
||||
const alias = document.createElement('span');
|
||||
alias.className = 'related-tag-alias';
|
||||
@@ -543,6 +598,7 @@ class RelatedTagsUI {
|
||||
|
||||
// Add cells to row
|
||||
tagRow.appendChild(tagName);
|
||||
tagRow.appendChild(wikiIcon);
|
||||
|
||||
if (!settingValues.hideAlias) {
|
||||
tagRow.appendChild(alias);
|
||||
@@ -588,7 +644,7 @@ class RelatedTagsUI {
|
||||
|
||||
/** Highlights the item (row) at the given index */
|
||||
#highlightItem() {
|
||||
if (this.getSelectedTag() === null) return; // No valid selection
|
||||
if (this.getSelectedTagData() === null) return; // No valid selection
|
||||
|
||||
const items = this.tagsContainer.children; // Get rows
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
@@ -621,9 +677,9 @@ class RelatedTagsUI {
|
||||
}
|
||||
|
||||
insertSelectedTag() {
|
||||
const selectedTag = this.getSelectedTag();
|
||||
const selectedTag = this.getSelectedTagData();
|
||||
if (selectedTag) {
|
||||
this.#insertTag(selectedTag);
|
||||
this.#insertTag(selectedTag.tag);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -769,13 +825,20 @@ export class RelatedTagsEventHandler {
|
||||
break;
|
||||
case 'Enter':
|
||||
case 'Tab':
|
||||
if (this.relatedTagsUI.getSelectedTag() !== null) {
|
||||
if (this.relatedTagsUI.getSelectedTagData() !== null) {
|
||||
event.preventDefault(); // Prevent Tab from changing focus
|
||||
this.relatedTagsUI.insertSelectedTag();
|
||||
} else if (!this.relatedTagsUI.isPinned) { // If nothing selected and not pinned, hide the panel
|
||||
this.relatedTagsUI.hide();
|
||||
}
|
||||
break;
|
||||
case 'F1':
|
||||
event.preventDefault();
|
||||
const tagData = this.relatedTagsUI.getSelectedTagData() || this.relatedTagsUI.getCurrentTagData();
|
||||
if (tagData && tagData.hasWikiPage) {
|
||||
openTagWikiUrl(tagData.source, tagData.tag);
|
||||
}
|
||||
break;
|
||||
case 'Escape':
|
||||
event.preventDefault();
|
||||
this.relatedTagsUI.hide();
|
||||
|
||||
@@ -10,6 +10,9 @@ export const settingValues = {
|
||||
enableModels: true, // Enable Lora and Embedding suggestions
|
||||
useFastSearch: false,
|
||||
replaceUnderscoreWithSpace: true, // Replace underscores with spaces in tag insertion
|
||||
prefixArtist: '', // Prefix to be attached before artist tags
|
||||
autoInsertComma: true,
|
||||
searchDebounceTime: 100, // Sequential search debounce time in milliseconds
|
||||
|
||||
// Related tags feature settings
|
||||
enableRelatedTags: true,
|
||||
@@ -20,6 +23,12 @@ export const settingValues = {
|
||||
// Display settings
|
||||
hideAlias: false, // Hide alias in the autocomplete and related tags display
|
||||
|
||||
// Auto format settings
|
||||
enableAutoFormat: true,
|
||||
autoFormatTrigger: 'auto', // Options: 'auto' (format on blur + shortcut), 'manual' (shortcut only)
|
||||
useTrailingComma: false, // Whether to add comma at the end of each line
|
||||
trimSurroundingSpaces: false, // Trim spaces around each tag
|
||||
|
||||
|
||||
// Internal logic settings
|
||||
_useFallbackAttachmentForEventListener: false, // Fallback to attach event listener when somthing goes wrong
|
||||
|
||||
@@ -558,6 +558,33 @@ export function getCurrentTagRange(text, cursorPos) {
|
||||
return { start: adjustedStart, end: adjustedEnd, tag: adjustedTag };
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds weight to a lora tag if it doesn't already have one.
|
||||
* @param {string} loraTag The model tag string (e.g., "<lora:my_style01>"
|
||||
* @param {number} defaultWeight The default weight to add if not present
|
||||
* @returns {string} The model tag with weight (e.g., "<lora:my_style01:1.0>")
|
||||
*/
|
||||
export function addWeightToLora(loraTag, defaultWeight = 1.0) {
|
||||
if (!loraTag) return loraTag;
|
||||
|
||||
// Match LoRA format: <lora:name> or <lora:name:weight>
|
||||
const loraMatch = loraTag.match(/^(<lora:[^>:]+)(:[0-9.]+)?>$/i);
|
||||
if (loraMatch) {
|
||||
const existingWeight = loraMatch[2];
|
||||
|
||||
// If weight already exists, return as-is
|
||||
if (existingWeight) {
|
||||
return loraTag;
|
||||
}
|
||||
|
||||
// Add default weight, preserving original case
|
||||
return `${loraMatch[1]}:${defaultWeight.toFixed(1)}>`;
|
||||
}
|
||||
|
||||
// If it's not a recognized model format, return as-is
|
||||
return loraTag;
|
||||
}
|
||||
|
||||
// --- End String Helper Functions ---
|
||||
|
||||
/**
|
||||
@@ -627,3 +654,47 @@ export function getScrollbarWidth() {
|
||||
|
||||
return _cachedScrollbarWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a wiki URL in a new browser tab for the given tag.
|
||||
* @param {string} tagSource - The source of the tag ('danbooru', 'e621', 'embeddings', 'lora')
|
||||
* @param {string} tagName - The name of the tag
|
||||
* @returns {boolean} - True if URL was opened, false if not applicable
|
||||
*/
|
||||
export function openTagWikiUrl(tagSource, tagName) {
|
||||
if (!tagSource || !tagName) {
|
||||
console.warn('[Autocomplete-Plus] Cannot open wiki: missing tagSource or tagName');
|
||||
return false;
|
||||
}
|
||||
|
||||
let wikiUrl = null;
|
||||
|
||||
switch (tagSource) {
|
||||
case 'danbooru':
|
||||
const danbooruTag = encodeURIComponent(tagName.replace(/ /g, '_'));
|
||||
wikiUrl = `https://danbooru.donmai.us/wiki_pages/${danbooruTag}`;
|
||||
break;
|
||||
|
||||
case 'e621':
|
||||
const e621Tag = encodeURIComponent(tagName.replace(/ /g, '_'));
|
||||
wikiUrl = `https://e621.net/wiki_pages/${e621Tag}`;
|
||||
break;
|
||||
|
||||
case 'embeddings':
|
||||
case 'lora':
|
||||
// console.debug('[Autocomplete-Plus] Model tags do not have wiki pages');
|
||||
return false;
|
||||
|
||||
default:
|
||||
console.warn(`[Autocomplete-Plus] Unknown tag source: ${tagSource}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (wikiUrl) {
|
||||
// console.debug(`[Autocomplete-Plus] Opening wiki URL: ${wikiUrl}`);
|
||||
window.open(wikiUrl, '_blank', 'noopener,noreferrer');
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user