Compare commits

..
Author SHA1 Message Date
newtextdoc1111 17bad4da60 fix: Bump version to 1.9.0 2025-12-01 02:30:04 +09:00
newtextdoc1111 9d12afb48c Merge pull request #55 from newtextdoc1111/dev
Merge Dev
2025-12-01 02:21:27 +09:00
newtextdoc1111 091ec15b2a feat: Improve related tags selection with F1 key wiki access support 2025-12-01 01:41:53 +09:00
newtextdoc1111 c87d92d7f8 docs: Add "Use Trailing Comma" setting doc 2025-12-01 01:34:35 +09:00
newtextdoc1111 5d5da100e1 docs: Add Wiki page opening feature doc 2025-11-30 03:56:08 +09:00
newtextdoc1111 85b86c0618 feat: Add wiki icon functionality and related methods for tag information (#45) 2025-11-30 03:37:07 +09:00
newtextdoc1111 1f93ca0259 fix: Update color variables in CSS 2025-11-29 22:01:55 +09:00
newtextdoc1111 e0486313a6 test: Update test cases for useTrailingComma setting 2025-11-27 00:22:41 +09:00
newtextdoc1111 11b4eb48f0 Merge pull request #54 from BlakeNeko/feature/add_trailing_comma
Feature/add trailing comma
2025-11-27 00:04:59 +09:00
BlakeNeko 156d569614 refactor: Update condition to use 'useTrailingComma' setting instead of 'addTrailingComma' 2025-11-26 10:05:19 +08:00
BlakeNeko 4fdef7f436 refactor: Rename 'Add Trailing Comma' to 'Use Trailing Comma' and update in localization files 2025-11-26 10:04:26 +08:00
BlakeNeko 8189b2b75d refactor: Replace 'Add Trailing Comma' setting with 'Use Trailing Comma' 2025-11-26 10:01:41 +08:00
BlakeNeko 5a76096c7e feat: Add en/zh/zh-TW translation for "addTrailingComma" option 2025-11-23 11:32:28 +08:00
BlakeNeko b6d4fb5590 feat: Add setting for trailing comma in auto-formatting 2025-11-23 11:28:45 +08:00
newtextdoc1111 3f84125262 fix: Bump version  to 1.8.0 2025-11-22 17:56:55 +09:00
newtextdoc1111 906ba108cc fix: Add "AutoFormatter" category to localization files 2025-11-22 17:51:47 +09:00
newtextdoc1111 6d54bbc7ca Merge pull request #51 from newtextdoc1111/dev
Merge dev
2025-11-22 17:32:21 +09:00
newtextdoc1111 bda6f4fa9d docs Add node link 2025-11-22 17:30:36 +09:00
newtextdoc1111 2237b0cb52 docs: Add "Auto Format Trigger" doc 2025-11-22 17:20:40 +09:00
newtextdoc1111 6e23ad89b6 feat: Added translation for "Auto Format Trigger" option 2025-11-22 17:19:19 +09:00
newtextdoc1111 389a79cac9 Merge pull request #50 from newtextdoc1111/feature/auto_format_blocklist
Feature/auto format blocklist
2025-11-22 16:52:07 +09:00
20 changed files with 460 additions and 91 deletions
+11
View File
@@ -43,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
@@ -53,6 +54,7 @@ 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`.
@@ -63,8 +65,13 @@ When a text input area loses focus (e.g., by clicking outside or pressing Tab),
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.
@@ -179,6 +186,10 @@ For example, by preparing the following CSV, you can quickly insert correspondin
### 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**: If enabled, ensures all lines end with a trailing comma when formatting. If disabled, removes trailing commas.
## Advanced Settings
+11
View File
@@ -41,6 +41,7 @@
- 入力済みのタグはグレーアウトで表示されます
- Danbooruとe621のタグを同時に表示出来ます。設定から優先順位を変更できます
- LoraとEmbeddingの入力補完に対応しています。設定から有効・無効を切り替えられます
- 「📖」アイコンをクリックするとタグのWikiページを開きます。キーボードで選択中の場合は `F1` キーで開くことが出来ます
## 関連タグ
@@ -51,6 +52,7 @@
- 表示位置は、テキストエリアの下部を基本とし、空きスペースに応じて上下に自動調整されます
- ヘッダーの「↕️|↔️」ボタンで上下と左右の表示位置に切り替えられます
- ヘッダーの「📌|🎯」ボタンで表示する関連タグの固定状態を切り替えられます。固定状態で閉じたい場合はEscキーを押します
- ヘッダーのタグをクリックするとタグのWikiページを開きます
- 入力済みのタグはグレーアウトで表示されます。グレーアウトしたタグを挿入しようとした場合、代わりに入力済みのタグを選択状態にします
- `Ctrl+Shift+Space` キーでカーソル位置の関連タグを表示できます
@@ -61,8 +63,13 @@
詳細な動作は以下になります:
- 各タグの後に自動的にカンマとスペースを追加し、適切に区切ります
- タグ間の余分なカンマとスペースを削除します
- キーボードショートカット `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 にインストール後の初回起動時に自動でダウンロードされるのでセットアップは不要です。
@@ -177,6 +184,10 @@ worst_quality,5,9999999,
### 自動フォーマット
- **Enable Auto Format**: テキストエリアがフォーカスを失った際にプロンプトテキストを自動的にフォーマットする機能の有効化/無効化
- **Auto Format Trigger**: フォーマットを適用するタイミングを選択します
- **自動**: テキスト欄からフォーカスが外れた際に自動でフォーマットします
- **手動**: キーボードショートカットでのみフォーマットします(デフォルト: `Alt+Shift+F`)
- **行末にカンマを使用**: 有効にすると、フォーマット時にすべての行末がカンマで終わるようになります。\n無効にすると行末のカンマが削除されます
## 上級者向け設定
+2 -1
View File
@@ -4,6 +4,7 @@
"Tag Source": "Tag Source",
"Autocompletion": "Autocompletion",
"Related Tags": "Related Tags",
"Display": "Display"
"Display": "Display",
"AutoFormatter": "AutoFormatter"
}
}
+12
View File
@@ -71,6 +71,18 @@
"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"
}
+2 -1
View File
@@ -4,6 +4,7 @@
"Tag Source": "タグソース",
"Autocompletion": "オートコンプリート",
"Related Tags": "関連タグ",
"Display": "表示"
"Display": "表示",
"AutoFormatter": "自動フォーマット"
}
}
+12
View File
@@ -71,6 +71,18 @@
"name": "エイリアスを非表示",
"tooltip": "オートコンプリートと関連タグで表示されるエイリアス列を非表示にします"
},
"AutocompletePlus_AutoFormatter_Trigger": {
"name": "自動フォーマットのトリガー",
"tooltip": "自動: テキスト欄からフォーカスが外れた際に自動でフォーマットします\n手動: キーボードショートカットでのみフォーマットします。デフォルトのキーバインド: (Alt+Shift+F)",
"options": {
"auto": "自動",
"manual": "手動"
}
},
"AutocompletePlus_AutoFormatter_UseTrailingComma": {
"name": "行末にカンマを使用",
"tooltip": "有効にすると、すべての行末がカンマで終わるようになります。\n無効にすると行末のカンマが削除されます。"
},
"AutocompletePlus_AutoFormatter_EnableAutoFormat": {
"name": "自動フォーマット機能の有効化"
}
+2 -1
View File
@@ -4,6 +4,7 @@
"Tag Source": "標籤來源",
"Autocompletion": "自動完成",
"Related Tags": "相關標籤",
"Display": "顯示"
"Display": "顯示",
"AutoFormatter": "自動格式化"
}
}
+12
View File
@@ -71,6 +71,18 @@
"name": "隱藏別名",
"tooltip": "隱藏自動完成和相關標籤顯示中的別名欄"
},
"AutocompletePlus_AutoFormatter_Trigger": {
"name": "自動格式化觸發方式",
"tooltip": "自動: 離開文字方塊時自動格式化\n手動: 僅透過鍵盤快速鍵格式化。預設快速鍵: (Alt+Shift+F)",
"options": {
"auto": "自動",
"manual": "手動"
}
},
"AutocompletePlus_AutoFormatter_UseTrailingComma": {
"name": "在行末添加逗號",
"tooltip": "啟用時,確保所有行都以逗號結尾。\n停用時,移除結尾逗號。"
},
"AutocompletePlus_AutoFormatter_EnableAutoFormat": {
"name": "啟用自動格式化"
}
+2 -1
View File
@@ -4,6 +4,7 @@
"Tag Source": "标签来源",
"Autocompletion": "自动完成",
"Related Tags": "相关标签",
"Display": "显示"
"Display": "显示",
"AutoFormatter": "自动格式化"
}
}
+12
View File
@@ -71,6 +71,18 @@
"name": "隐藏别名",
"tooltip": "隐藏自动完成和相关标签显示中的别名列"
},
"AutocompletePlus_AutoFormatter_Trigger": {
"name": "自动格式化触发方式",
"tooltip": "自动: 离开文本框时自动格式化\n手动: 仅通过键盘快捷键格式化。默认快捷键: (Alt+Shift+F)",
"options": {
"auto": "自动",
"manual": "手动"
}
},
"AutocompletePlus_AutoFormatter_UseTrailingComma": {
"name": "在行末添加逗号",
"tooltip": "启用时,确保所有行都以逗号结尾。\n禁用时,移除结尾逗号。"
},
"AutocompletePlus_AutoFormatter_EnableAutoFormat": {
"name": "启用自动格式化"
}
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-autocomplete-plus"
description = "Autocomplete and Related Tag display for ComfyUI"
version = "1.7.2"
version = "1.9.0"
license = {file = "LICENSE"}
dependencies = ["",]
+91 -25
View File
@@ -2,6 +2,7 @@ import {
formatPromptText,
__test__
} from "../../web/js/auto-formatter.js";
import { settingValues } from "../../web/js/settings.js";
const {
shouldAutoFormat
@@ -44,37 +45,102 @@ describe('AutoFormatter Functions', () => {
});
describe('formatPromptText', () => {
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);
// Store original setting value to restore after tests
const originalUseTrailingComma = settingValues.useTrailingComma;
afterEach(() => {
// Restore original setting after each test
settingValues.useTrailingComma = originalUseTrailingComma;
});
test('should remove extra spaces around tags', () => {
const input = ' tag1 , tag2 ';
const expected = 'tag1, tag2, ';
expect(formatPromptText(input)).toBe(expected);
describe('with useTrailingComma enabled', () => {
beforeEach(() => {
settingValues.useTrailingComma = true;
});
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);
});
});
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);
});
describe('with useTrailingComma disabled', () => {
beforeEach(() => {
settingValues.useTrailingComma = false;
});
test('should handle multiple lines', () => {
const input = 'tag1, tag2\ntag3, tag4';
const expected = 'tag1, tag2, \ntag3, tag4, ';
expect(formatPromptText(input)).toBe(expected);
});
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 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 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);
});
});
test('should handle empty input', () => {
+85 -22
View File
@@ -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;
+2 -2
View File
@@ -100,8 +100,8 @@ export function formatPromptText(text) {
// Rejoin cleaned tags with ", "
let formattedLine = cleanedTags.join(', ');
// Add a trailing comma and space if the line contains valid tags
if (formattedLine.length > 0) {
// Add a trailing comma and space if the setting is enabled and line contains valid tags
if (formattedLine.length > 0 && settingValues.useTrailingComma) {
formattedLine += ', ';
}
+42 -12
View File
@@ -1,6 +1,5 @@
import {
ModelTagSource,
TagCategory,
TagData,
autoCompleteData,
getEnabledTagSourceInPriorityOrder
@@ -19,7 +18,8 @@ import {
getViewportMargin,
getScrollbarWidth,
IconSvgHtmlString,
addWeightToLora
addWeightToLora,
openTagWikiUrl
} from './utils.js';
import { settingValues } from './settings.js';
@@ -263,7 +263,7 @@ function getCurrentPartialTag(inputElement) {
if (!inputElement) {
return "";
}
const text = inputElement.value;
const cursorPos = inputElement.selectionStart;
@@ -312,13 +312,13 @@ function insertTagToTextArea(inputElement, tagDataToInsert) {
if (!inputElement || !tagDataToInsert) {
return;
}
const text = inputElement.value;
const cursorPos = inputElement.selectionStart;
const tagRange = getCurrentTagRange(text, cursorPos);
let tagStart, tagEnd, currentTag;
if (!tagRange) {
// Fallback: insert at cursor position
tagStart = cursorPos;
@@ -327,7 +327,7 @@ function insertTagToTextArea(inputElement, tagDataToInsert) {
} else {
({ start: tagStart, end: tagEnd, tag: currentTag } = tagRange);
}
const replaceStart = Math.min(cursorPos, tagStart);
let replaceEnd = cursorPos;
@@ -407,6 +407,16 @@ 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.index !== undefined) {
const tagData = this.candidates[parseInt(row.dataset.index, 10)];
@@ -480,7 +490,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];
}
@@ -517,7 +527,7 @@ class AutocompleteUI {
* @param {boolean} isExisting
*/
#createTagElement(tagData, tagDataIndex, isExisting) {
const categoryText = TagCategory[tagData.source][tagData.category] || "unknown";
const categoryText = tagData.categoryText;
const aliasText = tagData.alias.join(', ');
const tagRow = document.createElement('div');
@@ -543,6 +553,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';
@@ -558,7 +580,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}`;
@@ -566,6 +588,7 @@ class AutocompleteUI {
tagRow.title = tooltipText;
tagRow.appendChild(tagName);
tagRow.appendChild(wikiIcon);
if (!settingValues.hideAlias) {
tagRow.appendChild(alias);
@@ -672,7 +695,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++) {
@@ -973,12 +996,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();
+17
View File
@@ -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 {
+11
View File
@@ -490,6 +490,17 @@ app.registerExtension({
},
// --- Auto format settings ---
{
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',
+88 -25
View File
@@ -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();
+1
View File
@@ -24,6 +24,7 @@ export const settingValues = {
// 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
// Internal logic settings
+44
View File
@@ -654,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;
}