Compare commits
33
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42f5e73515 | ||
|
|
52e5d2fd5f | ||
|
|
397c90275f | ||
|
|
2c8877060c | ||
|
|
1203e2f0ef | ||
|
|
f2ff83088d | ||
|
|
d6f53ebb0f | ||
|
|
2552e8d746 | ||
|
|
96ba7d8055 | ||
|
|
65960a2394 | ||
|
|
92f01b84a0 | ||
|
|
3f8eb3bc0c | ||
|
|
fe42e198d2 | ||
|
|
98d9fdfc50 | ||
|
|
504b681ab8 | ||
|
|
aca0ed6326 | ||
|
|
ab941ab329 | ||
|
|
c4e408eede | ||
|
|
f8c86203e4 | ||
|
|
5a4ded44b8 | ||
|
|
295d0c0cdc | ||
|
|
37ce793208 | ||
|
|
2386fc7aa0 | ||
|
|
937c5bb283 | ||
|
|
512dd88d7a | ||
|
|
096314933d | ||
|
|
05c890ee37 | ||
|
|
d4725dfebc | ||
|
|
c4d5eee89d | ||
|
|
fa4d87ee77 | ||
|
|
3cb5a41cc8 | ||
|
|
16a325b798 | ||
|
|
e3171940fb |
@@ -12,9 +12,9 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '22'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|||||||
@@ -118,11 +118,28 @@ When the browser is reloaded, you can check the list of loaded CSV files in the
|
|||||||
>[!NOTE]
|
>[!NOTE]
|
||||||
> If there are multiple user CSV files, they are loaded in alphabetical order. If the same tag exists in multiple files, the one loaded first is retained. The basic CSV is loaded last.
|
> If there are multiple user CSV files, they are loaded in alphabetical order. If the same tag exists in multiple files, the one loaded first is retained. The basic CSV is loaded last.
|
||||||
|
|
||||||
|
### Bulk Tag Insertion (Pseudo "Chants")
|
||||||
|
|
||||||
|
By enclosing multiple tags with `""` (double quotation marks), you can insert frequently used tags in bulk.
|
||||||
|
This is similar to the **Chants** feature in [DominikDoom/a1111-sd-webui-tagcomplete](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete?tab=readme-ov-file#chants).
|
||||||
|
|
||||||
|
For example, by preparing the following CSV, you can quickly insert corresponding tags by typing `<c:Basic-HighQuality>` or `<c:Basic-Negative>`.
|
||||||
|
|
||||||
|
**`danbooru_tags_chants.csv`:**
|
||||||
|
```
|
||||||
|
"masterpiece, best quality, high quality, highres, ultra-detailed",5,9999999,<c:Basic-HighQuality>
|
||||||
|
"(worst quality, low quality:1.4), normal quality",5,9999999,<c:Basic-Negative>
|
||||||
|
```
|
||||||
|
|
||||||
|
>[!TIP]
|
||||||
|
> * Text enclosed in `""` does not escape `()` (parentheses). Tags that originally contain parentheses should be written in the CSV with escaped parentheses. Example: `copyright_(series)` -> `copyright_\(series\)`
|
||||||
|
> * The alias column also supports `""`, allowing you to assign multiple aliases
|
||||||
|
|
||||||
## Settings
|
## Settings
|
||||||
|
|
||||||
### Tag Source
|
### Tag Source
|
||||||
|
|
||||||
> [!TIP]
|
> [!NOTE]
|
||||||
> The source of tag data such as Danbooru or e621 is called the "tag source".
|
> The source of tag data such as Danbooru or e621 is called the "tag source".
|
||||||
|
|
||||||
- **Autocomplete Tag Source**: The tag source to display in the autocomplete suggestions. Select "all" to display all loaded tag sources.
|
- **Autocomplete Tag Source**: The tag source to display in the autocomplete suggestions. Select "all" to display all loaded tag sources.
|
||||||
@@ -133,6 +150,8 @@ When the browser is reloaded, you can check the list of loaded CSV files in the
|
|||||||
|
|
||||||
- **Enable Autocomplete**: Enable/disable the autocomplete feature.
|
- **Enable Autocomplete**: Enable/disable the autocomplete feature.
|
||||||
- **Max suggestions**: Maximum number of autocomplete suggestions to display.
|
- **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.
|
||||||
- **Enable Loras and Embeddings**: Display Lora and Embedding in the suggestions.
|
- **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).
|
- **Use Fast Search**: Switch autocomplete suggestions search to fast processing (see [About Fast Search for Autocomplete](#about-fast-search-for-autocomplete) for details).
|
||||||
|
|
||||||
@@ -141,11 +160,11 @@ When the browser is reloaded, you can check the list of loaded CSV files in the
|
|||||||
- **Enable Related Tags**: Enable/disable the related tags feature.
|
- **Enable Related Tags**: Enable/disable the related tags feature.
|
||||||
- **Max related tags**: Maximum number of related tags to display.
|
- **Max related tags**: Maximum number of related tags to display.
|
||||||
- **Default Display Position**: Default display position when ComfyUI starts.
|
- **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)
|
||||||
|
|
||||||
### Miscellaneous
|
### Display
|
||||||
|
|
||||||
- **Check CSV updates**: Click the "Check Now" button to check if new CSV files are available in HuggingFace and download them if necessary.
|
- **Hide Alias**: Hide/show the Alias column in autocomplete and related tags (default is show)
|
||||||
|
|
||||||
## Advanced Settings
|
## Advanced Settings
|
||||||
|
|
||||||
|
|||||||
+23
-4
@@ -116,11 +116,28 @@ worst_quality,5,9999999,
|
|||||||
>[!NOTE]
|
>[!NOTE]
|
||||||
> ユーザー CSV が複数ある場合アルファベット順に読み込まれます。同じタグが複数のファイルに存在する場合は先に読み込まれた方が保持されます。基本 CSV は最後にロードされます。
|
> ユーザー CSV が複数ある場合アルファベット順に読み込まれます。同じタグが複数のファイルに存在する場合は先に読み込まれた方が保持されます。基本 CSV は最後にロードされます。
|
||||||
|
|
||||||
|
### 複数タグの一括挿入機能(疑似 Chants)
|
||||||
|
|
||||||
|
`""` (ダブルクォーテーション)で複数タグを囲むことで、よく使うタグを一括で挿入できます。
|
||||||
|
これは [DominikDoom/a1111-sd-webui-tagcomplete](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete?tab=readme-ov-file#chants) で **Chants** と呼ばれる機能に似ています。
|
||||||
|
|
||||||
|
例として以下のCSVを用意することで、`<c:Basic-HighQuality>` や `<c:Basic-Negative>` と入力して対応したタグを素早く挿入出来ます。
|
||||||
|
|
||||||
|
**`danbooru_tags_chants.csv`:**
|
||||||
|
```
|
||||||
|
"masterpiece, best quality, high quality, highres, ultra-detailed",5,9999999,<c:Basic-HighQuality>
|
||||||
|
"(worst quality, low quality:1.4), normal quality",5,9999999,<c:Basic-Negative>
|
||||||
|
```
|
||||||
|
|
||||||
|
>[!TIP]
|
||||||
|
> * `""` で囲まれたテキストは `()` (括弧)がエスケープされません。元々括弧が含まれるタグはエスケープした状態でCSVに記述してください。 例: `copyright_(series)` -> `copyright_\(series\)`
|
||||||
|
> * エイリアス列も `""` に対応しているので、複数のエイリアスを付けられます
|
||||||
|
|
||||||
## 設定
|
## 設定
|
||||||
|
|
||||||
### タグソース
|
### タグソース
|
||||||
|
|
||||||
> [!TIP]
|
> [!NOTE]
|
||||||
> Danbooruやe621等のタグデータの提供元を「タグソース」と呼びます
|
> Danbooruやe621等のタグデータの提供元を「タグソース」と呼びます
|
||||||
|
|
||||||
- **Autocomplete Tag Source**: オートコンプリート候補に表示するタグソース。「all」を選択するとロード済みの全てのタグソースを表示します
|
- **Autocomplete Tag Source**: オートコンプリート候補に表示するタグソース。「all」を選択するとロード済みの全てのタグソースを表示します
|
||||||
@@ -131,6 +148,8 @@ worst_quality,5,9999999,
|
|||||||
|
|
||||||
- **Enable Autocomplete**: オートコンプリート機能の有効化/無効化
|
- **Enable Autocomplete**: オートコンプリート機能の有効化/無効化
|
||||||
- **Max Suggestions**: オートコンプリート候補の最大表示件数
|
- **Max Suggestions**: オートコンプリート候補の最大表示件数
|
||||||
|
- **Auto-Insert Comma**: タグの挿入時、末尾にカンマを追加する
|
||||||
|
- **Replace '_' with 'Space'**: タグ挿入時にアンダースコアをスペースに置き換えます。この設定は関連タグ表示にも影響します
|
||||||
- **Enable Loras and Embeddings**: LoraとEmbeddingを候補に表示する
|
- **Enable Loras and Embeddings**: LoraとEmbeddingを候補に表示する
|
||||||
- **Use Fast Search**: オートコンプリート候補の検索を高速な処理に切り替える(詳細は [オートコンプリートの高速検索について](#オートコンプリートの高速検索について) を確認してください)
|
- **Use Fast Search**: オートコンプリート候補の検索を高速な処理に切り替える(詳細は [オートコンプリートの高速検索について](#オートコンプリートの高速検索について) を確認してください)
|
||||||
|
|
||||||
@@ -139,11 +158,11 @@ worst_quality,5,9999999,
|
|||||||
- **Enable Related Tags**: 関連タグ機能の有効化/無効化
|
- **Enable Related Tags**: 関連タグ機能の有効化/無効化
|
||||||
- **Max related tags**: 関連タグの最大表示件数
|
- **Max related tags**: 関連タグの最大表示件数
|
||||||
- **Default Display Position**: ComfyUI起動時のデフォルト表示位置
|
- **Default Display Position**: ComfyUI起動時のデフォルト表示位置
|
||||||
- **Related Tags Trigger Mode** : 関連タグを表示する際、どの操作をトリガーとするか(クリックのみ、Ctrl+クリック)
|
- **Related Tags Trigger Mode** : 入力済みのタグの関連タグを表示する際、どの操作をトリガーとするか(クリックのみ、Ctrl+クリック)
|
||||||
|
|
||||||
### その他
|
### 表示
|
||||||
|
|
||||||
- **Check CSV updates**: 「Check Now」ボタンを押すと新しい CSV ファイルがHuggingFaceにあるか確認し、必要に応じてダウンロードを行います
|
- **Hide Alias**: オートコンプリートと関連タグで表示されるエイリアス列の非表示/表示を切り替え(デフォルトは表示です)
|
||||||
|
|
||||||
## 上級者向け設定
|
## 上級者向け設定
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"settingsCategories": {
|
||||||
|
"Autocomplete Plus": "Autocomplete Plus",
|
||||||
|
"Tag Source": "Tag Source",
|
||||||
|
"Autocompletion": "Autocompletion",
|
||||||
|
"Related Tags": "Related Tags",
|
||||||
|
"Display": "Display"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"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_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."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"settingsCategories": {
|
||||||
|
"Autocomplete Plus": "Autocomplete Plus",
|
||||||
|
"Tag Source": "タグソース",
|
||||||
|
"Autocompletion": "オートコンプリート",
|
||||||
|
"Related Tags": "関連タグ",
|
||||||
|
"Display": "表示"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"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_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": "オートコンプリートと関連タグで表示されるエイリアス列を非表示にします"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"settingsCategories": {
|
||||||
|
"Autocomplete Plus": "Autocomplete Plus",
|
||||||
|
"Tag Source": "標籤來源",
|
||||||
|
"Autocompletion": "自動完成",
|
||||||
|
"Related Tags": "相關標籤",
|
||||||
|
"Display": "顯示"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"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_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": "隱藏自動完成和相關標籤顯示中的別名欄"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"settingsCategories": {
|
||||||
|
"Autocomplete Plus": "Autocomplete Plus",
|
||||||
|
"Tag Source": "标签来源",
|
||||||
|
"Autocompletion": "自动完成",
|
||||||
|
"Related Tags": "相关标签",
|
||||||
|
"Display": "显示"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"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_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": "隐藏自动完成和相关标签显示中的别名列"
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
-19
@@ -235,16 +235,7 @@ class Downloader:
|
|||||||
|
|
||||||
def _check_new_csv_from_hf_dataset(self, dataset_meta: dict, now_utc: datetime, force_check: bool = False):
|
def _check_new_csv_from_hf_dataset(self, dataset_meta: dict, now_utc: datetime, force_check: bool = False):
|
||||||
"""Checks HuggingFace for file updates and updates metadata."""
|
"""Checks HuggingFace for file updates and updates metadata."""
|
||||||
perform_hf_check = True
|
perform_hf_check = force_check
|
||||||
if not force_check and dataset_meta.get("last_remote_check_timestamp"):
|
|
||||||
try:
|
|
||||||
last_check_dt = datetime.fromisoformat(dataset_meta["last_remote_check_timestamp"])
|
|
||||||
if now_utc - last_check_dt < timedelta(days=7):
|
|
||||||
perform_hf_check = False
|
|
||||||
except (ValueError, KeyError, TypeError):
|
|
||||||
print(
|
|
||||||
"[Autocomplete-Plus] Invalid or missing timestamp for last_remote_check_timestamp. Will perform remote check."
|
|
||||||
)
|
|
||||||
|
|
||||||
if perform_hf_check:
|
if perform_hf_check:
|
||||||
huggingface_dataset_id = dataset_meta["hf_dataset_id"]
|
huggingface_dataset_id = dataset_meta["hf_dataset_id"]
|
||||||
@@ -303,15 +294,16 @@ class Downloader:
|
|||||||
return f"File {file_name} is missing or empty locally."
|
return f"File {file_name} is missing or empty locally."
|
||||||
|
|
||||||
# Check if the last modified date on HuggingFace is newer than the last download date
|
# Check if the last modified date on HuggingFace is newer than the last download date
|
||||||
try:
|
# Temporarily comment out until changes to remote files can be detected
|
||||||
last_download_dt = datetime.fromisoformat(file_meta_entry["last_download"])
|
# try:
|
||||||
hf_modified_dt = datetime.fromisoformat(file_meta_entry["last_modified_on_hf"])
|
# last_download_dt = datetime.fromisoformat(file_meta_entry["last_download"])
|
||||||
if hf_modified_dt > last_download_dt:
|
# hf_modified_dt = datetime.fromisoformat(file_meta_entry["last_modified_on_hf"])
|
||||||
return f"Remote file {file_name} is newer (HF: {last_download_dt}, Local Download: {hf_modified_dt})."
|
# if hf_modified_dt > last_download_dt:
|
||||||
except (ValueError, TypeError):
|
# return f"Remote file {file_name} is newer (HF: {hf_modified_dt}, Local Download: {last_download_dt})."
|
||||||
file_meta_entry["last_download"] = None
|
# except (ValueError, TypeError):
|
||||||
file_meta_entry["last_modified_on_hf"] = None
|
# file_meta_entry["last_download"] = None
|
||||||
return f"Invalid timestamp format for {file_name}. Forcing download to ensure integrity."
|
# file_meta_entry["last_modified_on_hf"] = None
|
||||||
|
# return f"Invalid timestamp format for {file_name}. Forcing download to ensure integrity."
|
||||||
|
|
||||||
# If the file is missing or empty, but the last download timestamp exists, we need to retry.
|
# If the file is missing or empty, but the last download timestamp exists, we need to retry.
|
||||||
if not check_file_valid(local_file_path) and file_meta_entry.get("last_download") is not None:
|
if not check_file_valid(local_file_path) and file_meta_entry.get("last_download") is not None:
|
||||||
|
|||||||
Generated
+1500
-738
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -6,9 +6,9 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.27.1",
|
"@babel/core": "^7.27.1",
|
||||||
"@babel/preset-env": "^7.27.2",
|
"@babel/preset-env": "^7.27.2",
|
||||||
"babel-jest": "^29.7.0",
|
"babel-jest": "^30.2.0",
|
||||||
"env-cmd": "^10.1.0",
|
"env-cmd": "^10.1.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^30.2.0",
|
||||||
"stylelint": "^16.19.1",
|
"stylelint": "^16.19.1",
|
||||||
"stylelint-config-idiomatic-order": "^10.0.0",
|
"stylelint-config-idiomatic-order": "^10.0.0",
|
||||||
"stylelint-config-standard": "^38.0.0"
|
"stylelint-config-standard": "^38.0.0"
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "comfyui-autocomplete-plus"
|
name = "comfyui-autocomplete-plus"
|
||||||
description = "Autocomplete and Related Tag display for ComfyUI"
|
description = "Autocomplete and Related Tag display for ComfyUI"
|
||||||
version = "1.4.0"
|
version = "1.7.0"
|
||||||
license = {file = "LICENSE"}
|
license = {file = "LICENSE"}
|
||||||
dependencies = ["",]
|
dependencies = ["",]
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,389 @@
|
|||||||
|
import {
|
||||||
|
__test__
|
||||||
|
} from "../../web/js/autocomplete.js";
|
||||||
|
import {
|
||||||
|
TagData,
|
||||||
|
TagSource,
|
||||||
|
ModelTagSource,
|
||||||
|
autoCompleteData
|
||||||
|
} from "../../web/js/data.js";
|
||||||
|
import {
|
||||||
|
createFlexSearchDocument,
|
||||||
|
createFlexSearchDocumentForModel
|
||||||
|
} from "../../web/js/searchengine.js"
|
||||||
|
|
||||||
|
const {
|
||||||
|
searchCompletionCandidates,
|
||||||
|
sequentialSearch,
|
||||||
|
searchWithFlexSearch,
|
||||||
|
matchWord,
|
||||||
|
getCurrentPartialTag,
|
||||||
|
insertTagToTextArea
|
||||||
|
} = __test__;
|
||||||
|
|
||||||
|
|
||||||
|
// Helper function to create mock textarea element
|
||||||
|
function createMockTextarea(value, selectionStart, selectionEnd = -1) {
|
||||||
|
return {
|
||||||
|
value: value,
|
||||||
|
selectionStart: selectionStart,
|
||||||
|
selectionEnd: selectionEnd > selectionStart ? selectionEnd : selectionStart,
|
||||||
|
nodeName: 'TEXTAREA',
|
||||||
|
focus() { document.setFocus(this); },
|
||||||
|
setSelectionRange: () => { },
|
||||||
|
getBoundingClientRect: () => ({
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: 300,
|
||||||
|
height: 100
|
||||||
|
}),
|
||||||
|
scrollTop: 0,
|
||||||
|
scrollLeft: 0,
|
||||||
|
ownerDocument: {
|
||||||
|
defaultView: typeof window !== 'undefined' ? window : {},
|
||||||
|
documentElement: typeof document !== 'undefined' ? document.documentElement : {}
|
||||||
|
},
|
||||||
|
dispatchEvent: () => { }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test data setup function
|
||||||
|
function setupTestData() {
|
||||||
|
// Clear existing data
|
||||||
|
Object.keys(autoCompleteData).forEach(key => delete autoCompleteData[key]);
|
||||||
|
|
||||||
|
// Create mock AutocompleteData structure for each source
|
||||||
|
const sources = [...Object.values(TagSource), ...Object.values(ModelTagSource)];
|
||||||
|
|
||||||
|
sources.forEach(source => {
|
||||||
|
autoCompleteData[source] = {
|
||||||
|
flexSearchDocument: null,
|
||||||
|
sortedTags: [],
|
||||||
|
tagMap: new Map(),
|
||||||
|
aliasMap: new Map(),
|
||||||
|
cooccurrenceMap: new Map(),
|
||||||
|
isInitializing: false,
|
||||||
|
initialized: true,
|
||||||
|
baseLoadingProgress: {
|
||||||
|
cooccurrence: 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add sample tag data for testing
|
||||||
|
const sampleTags = [
|
||||||
|
new TagData('1girl', 0, 1000000, ['one_girl'], TagSource.Danbooru),
|
||||||
|
new TagData('blue_hair', 0, 500000, ['blue hair'], TagSource.Danbooru),
|
||||||
|
new TagData('long_hair', 0, 800000, [], TagSource.Danbooru),
|
||||||
|
new TagData('__wildcard__', 0, 100, [], TagSource.Danbooru),
|
||||||
|
new TagData(':d', 0, 50000, [], TagSource.Danbooru),
|
||||||
|
new TagData('test_tag', 0, 1000, ['test'], TagSource.E621),
|
||||||
|
new TagData('<lora:test_model>', 0, 0, [], ModelTagSource.Lora),
|
||||||
|
new TagData('embedding:test_embedding', 0, 0, [], ModelTagSource.Embeddings)
|
||||||
|
];
|
||||||
|
|
||||||
|
// Distribute tags across sources
|
||||||
|
sampleTags.forEach(tagData => {
|
||||||
|
const source = tagData.source;
|
||||||
|
autoCompleteData[source].sortedTags.push(tagData);
|
||||||
|
autoCompleteData[source].tagMap.set(tagData.tag, tagData);
|
||||||
|
|
||||||
|
// Add aliases to alias map
|
||||||
|
if (tagData.alias && Array.isArray(tagData.alias)) {
|
||||||
|
tagData.alias.forEach(alias => {
|
||||||
|
autoCompleteData[source].aliasMap.set(alias, tagData.tag);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort tags and build flexsearch index
|
||||||
|
sources.forEach(source => {
|
||||||
|
autoCompleteData[source].sortedTags.sort((a, b) => b.count - a.count);
|
||||||
|
|
||||||
|
const isModelSrc = Object.values(ModelTagSource).includes(source);
|
||||||
|
const doc = isModelSrc ? createFlexSearchDocumentForModel() : createFlexSearchDocument();
|
||||||
|
autoCompleteData[source].sortedTags.forEach((tagData, i) => {
|
||||||
|
doc.add(i, tagData);
|
||||||
|
});
|
||||||
|
|
||||||
|
autoCompleteData[source].flexSearchDocument = doc;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Autocomplete Functions', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setupTestData();
|
||||||
|
|
||||||
|
// Mock global document
|
||||||
|
global.document = {
|
||||||
|
execCommand: (commandId, showUI, value) => {
|
||||||
|
switch (commandId) {
|
||||||
|
case "insertText":
|
||||||
|
if (document.focusedElement) {
|
||||||
|
document.focusedElement.value += value;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error('Not implemented!');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createElement: () => ({
|
||||||
|
id: '',
|
||||||
|
style: {},
|
||||||
|
innerHTML: '',
|
||||||
|
appendChild: () => { },
|
||||||
|
getBoundingClientRect: () => ({
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: 300,
|
||||||
|
height: 100
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
setFocus: (element) => {
|
||||||
|
document.focusedElement = element;
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
appendChild: () => { },
|
||||||
|
removeChild: () => { }
|
||||||
|
},
|
||||||
|
focusedElement: null
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock global window
|
||||||
|
global.window = {
|
||||||
|
getComputedStyle: () => ({
|
||||||
|
lineHeight: '20px',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontFamily: 'Arial'
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('matchWord', () => {
|
||||||
|
|
||||||
|
test('should match exact queries', () => {
|
||||||
|
const queries = new Set(['1girl', '1girls']);
|
||||||
|
const result = matchWord('1girl', queries);
|
||||||
|
|
||||||
|
expect(result.matched).toBe(true);
|
||||||
|
expect(result.isExactMatch).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should match partial queries', () => {
|
||||||
|
const queries = new Set(['girl']);
|
||||||
|
const result = matchWord('1girl', queries);
|
||||||
|
|
||||||
|
expect(result.matched).toBe(true);
|
||||||
|
expect(result.isExactMatch).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle wildcard prefixes', () => {
|
||||||
|
const queries = new Set(['__wild']);
|
||||||
|
const result = matchWord('__wildcard__', queries);
|
||||||
|
|
||||||
|
expect(result.matched).toBe(true);
|
||||||
|
expect(result.isExactMatch).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle symbol-only queries', () => {
|
||||||
|
const queries = new Set([':d']);
|
||||||
|
const result = matchWord(':d', queries);
|
||||||
|
|
||||||
|
expect(result.matched).toBe(true);
|
||||||
|
expect(result.isExactMatch).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should match after removing common symbols', () => {
|
||||||
|
const queries = new Set(['blue hair']);
|
||||||
|
const result = matchWord('blue_hair', queries);
|
||||||
|
|
||||||
|
expect(result.matched).toBe(true);
|
||||||
|
expect(result.isExactMatch).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not match unrelated queries', () => {
|
||||||
|
const queries = new Set(['test']);
|
||||||
|
const result = matchWord('1girl', queries);
|
||||||
|
|
||||||
|
expect(result.matched).toBe(false);
|
||||||
|
expect(result.isExactMatch).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getCurrentPartialTag', () => {
|
||||||
|
|
||||||
|
test('should extract partial tag before cursor', () => {
|
||||||
|
const textarea = createMockTextarea('1girl, blue_hair', 14);
|
||||||
|
const result = getCurrentPartialTag(textarea);
|
||||||
|
|
||||||
|
expect(typeof result).toBe('string');
|
||||||
|
expect(result).toBe('blue_ha');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return full tag when cursor is at the end', () => {
|
||||||
|
const textarea = createMockTextarea('1girl', 5);
|
||||||
|
const result = getCurrentPartialTag(textarea);
|
||||||
|
|
||||||
|
expect(typeof result).toBe('string');
|
||||||
|
expect(result).toBe('1girl');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle empty textarea', () => {
|
||||||
|
const textarea = createMockTextarea('', 0);
|
||||||
|
const result = getCurrentPartialTag(textarea);
|
||||||
|
|
||||||
|
expect(typeof result).toBe('string');
|
||||||
|
expect(result).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle newline separators', () => {
|
||||||
|
const textarea = createMockTextarea('1girl\nblue_h', 12);
|
||||||
|
const result = getCurrentPartialTag(textarea);
|
||||||
|
|
||||||
|
expect(typeof result).toBe('string');
|
||||||
|
expect(result).toBe('blue_h');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('searchCompletionCandidates', () => {
|
||||||
|
test('should return an empty array for empty input', () => {
|
||||||
|
const textarea = createMockTextarea('', 0);
|
||||||
|
const results = searchCompletionCandidates(textarea);
|
||||||
|
|
||||||
|
expect(Array.isArray(results)).toBe(true);
|
||||||
|
expect(results.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return an empty array for null input', () => {
|
||||||
|
const results = searchCompletionCandidates(null);
|
||||||
|
expect(Array.isArray(results)).toBe(true);
|
||||||
|
expect(results.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return an empty array for undefined input', () => {
|
||||||
|
const results = searchCompletionCandidates(undefined);
|
||||||
|
expect(Array.isArray(results)).toBe(true);
|
||||||
|
expect(results.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return multiple candidates for valid input', () => {
|
||||||
|
const textarea = createMockTextarea('test', 4);
|
||||||
|
const results = searchCompletionCandidates(textarea);
|
||||||
|
|
||||||
|
expect(Array.isArray(results)).toBe(true);
|
||||||
|
expect(results.map(tagData => tagData.tag))
|
||||||
|
.toEqual(expect.arrayContaining([
|
||||||
|
'test_tag', '<lora:test_model>', 'embedding:test_embedding'
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should find wildcard tag', () => {
|
||||||
|
const textarea = createMockTextarea('__wild', 6);
|
||||||
|
const results = searchCompletionCandidates(textarea);
|
||||||
|
|
||||||
|
expect(Array.isArray(results)).toBe(true);
|
||||||
|
expect(results.map(tagData => tagData.tag))
|
||||||
|
.toEqual(expect.arrayContaining(['__wildcard__']));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should find emoticon tag', () => {
|
||||||
|
const textarea = createMockTextarea(':d', 2);
|
||||||
|
const results = searchCompletionCandidates(textarea);
|
||||||
|
|
||||||
|
expect(Array.isArray(results)).toBe(true);
|
||||||
|
expect(results.map(tagData => tagData.tag))
|
||||||
|
.toEqual(expect.arrayContaining([':d']));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sequentialSearch', () => {
|
||||||
|
|
||||||
|
test('should find and return matching tags', () => {
|
||||||
|
const partialTag = 'test';
|
||||||
|
const queryVariations = new Set([partialTag.toLowerCase()]);
|
||||||
|
|
||||||
|
const results = sequentialSearch(partialTag, queryVariations);
|
||||||
|
|
||||||
|
expect(Array.isArray(results)).toBe(true);
|
||||||
|
expect(results.map(tagData => tagData.tag))
|
||||||
|
.toEqual(expect.arrayContaining([
|
||||||
|
'test_tag', '<lora:test_model>', 'embedding:test_embedding'
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('searchWithFlexSearch', () => {
|
||||||
|
|
||||||
|
test('should find and return matching tags using FlexSearch', () => {
|
||||||
|
const partialTag = 'test';
|
||||||
|
const queryVariations = new Set([partialTag.toLowerCase()]);
|
||||||
|
|
||||||
|
const results = searchWithFlexSearch(partialTag, queryVariations);
|
||||||
|
|
||||||
|
expect(Array.isArray(results)).toBe(true);
|
||||||
|
expect(results.map(tagData => tagData.tag))
|
||||||
|
.toEqual(expect.arrayContaining([
|
||||||
|
'test_tag', '<lora:test_model>', 'embedding:test_embedding'
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('insertTagToTextArea', () => {
|
||||||
|
|
||||||
|
test('should insert tag and replace underscore with space', () => {
|
||||||
|
const textarea = createMockTextarea('1girl, ', 7);
|
||||||
|
const tagData = { tag: 'blue_hair', source: 'danbooru' };
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
insertTagToTextArea(textarea, tagData);
|
||||||
|
}).not.toThrow();
|
||||||
|
|
||||||
|
expect(textarea.value).toBe('1girl, blue hair, ');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should complete model tag and add trailing comma', () => {
|
||||||
|
const textarea = createMockTextarea('<lora:', 6);
|
||||||
|
const tagData = { tag: '<lora:my_model>', source: 'lora' };
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
insertTagToTextArea(textarea, tagData);
|
||||||
|
}).not.toThrow();
|
||||||
|
|
||||||
|
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', () => {
|
||||||
|
const textarea = createMockTextarea('__wild', 6);
|
||||||
|
const tagData = { tag: '__wildcard__', source: 'danbooru' };
|
||||||
|
|
||||||
|
insertTagToTextArea(textarea, tagData);
|
||||||
|
|
||||||
|
expect(textarea.value).toBe('__wildcard__, ');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle emoticon tag insertion', () => {
|
||||||
|
const textarea = createMockTextarea(':d', 2);
|
||||||
|
const tagData = { tag: ':d', source: 'danbooru' };
|
||||||
|
|
||||||
|
insertTagToTextArea(textarea, tagData);
|
||||||
|
|
||||||
|
expect(textarea.value).toBe(':d, ');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
+173
-1
@@ -1,9 +1,11 @@
|
|||||||
|
import { settingValues } from '../../web/js/settings.js';
|
||||||
import {
|
import {
|
||||||
extractTagsFromTextArea,
|
extractTagsFromTextArea,
|
||||||
normalizeTagToSearch,
|
normalizeTagToSearch,
|
||||||
normalizeTagToInsert,
|
normalizeTagToInsert,
|
||||||
getCurrentTagRange,
|
getCurrentTagRange,
|
||||||
findAllTagPositions
|
findAllTagPositions,
|
||||||
|
addWeightToLora
|
||||||
} from '../../web/js/utils.js';
|
} from '../../web/js/utils.js';
|
||||||
|
|
||||||
// filepath: web/js/utils.test.js
|
// filepath: web/js/utils.test.js
|
||||||
@@ -135,6 +137,11 @@ describe('normalizeTagToSearch', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('normalizeTagToInsert', () => {
|
describe('normalizeTagToInsert', () => {
|
||||||
|
const originalValue = settingValues.replaceUnderscoreWithSpace;
|
||||||
|
afterEach(() => {
|
||||||
|
settingValues.replaceUnderscoreWithSpace = originalValue;
|
||||||
|
});
|
||||||
|
|
||||||
test('should return null or empty string for invalid inputs', () => {
|
test('should return null or empty string for invalid inputs', () => {
|
||||||
expect(normalizeTagToInsert(null)).toBeNull();
|
expect(normalizeTagToInsert(null)).toBeNull();
|
||||||
expect(normalizeTagToInsert(undefined)).toBeUndefined();
|
expect(normalizeTagToInsert(undefined)).toBeUndefined();
|
||||||
@@ -167,6 +174,73 @@ describe('normalizeTagToInsert', () => {
|
|||||||
expect(normalizeTagToInsert('year:2000')).toBe('year:2000');
|
expect(normalizeTagToInsert('year:2000')).toBe('year:2000');
|
||||||
expect(normalizeTagToInsert('foo:bar')).toBe('foo:bar');
|
expect(normalizeTagToInsert('foo:bar')).toBe('foo:bar');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should respect replaceUnderscoreWithSpace setting when true', () => {
|
||||||
|
settingValues.replaceUnderscoreWithSpace = true;
|
||||||
|
|
||||||
|
expect(normalizeTagToInsert('blue_hair')).toBe('blue hair');
|
||||||
|
expect(normalizeTagToInsert('red_eyes')).toBe('red eyes');
|
||||||
|
expect(normalizeTagToInsert('long_curly_hair')).toBe('long curly hair');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should respect replaceUnderscoreWithSpace setting when false', () => {
|
||||||
|
settingValues.replaceUnderscoreWithSpace = false;
|
||||||
|
|
||||||
|
expect(normalizeTagToInsert('blue_hair')).toBe('blue_hair');
|
||||||
|
expect(normalizeTagToInsert('red_eyes')).toBe('red_eyes');
|
||||||
|
expect(normalizeTagToInsert('long_curly_hair')).toBe('long_curly_hair');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should respect replaceUnderscoreWithSpace setting with parentheses', () => {
|
||||||
|
settingValues.replaceUnderscoreWithSpace = true;
|
||||||
|
expect(normalizeTagToInsert('blue_hair(style)')).toBe('blue hair\\(style\\)');
|
||||||
|
|
||||||
|
settingValues.replaceUnderscoreWithSpace = false;
|
||||||
|
expect(normalizeTagToInsert('blue_hair(style)')).toBe('blue_hair\\(style\\)');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not replace underscores in wildcard syntax regardless of setting', () => {
|
||||||
|
// Wildcard syntax should preserve underscores regardless of setting
|
||||||
|
settingValues.replaceUnderscoreWithSpace = true;
|
||||||
|
expect(normalizeTagToInsert('__wildcard__')).toBe('__wildcard__');
|
||||||
|
|
||||||
|
settingValues.replaceUnderscoreWithSpace = false;
|
||||||
|
expect(normalizeTagToInsert('__wildcard__')).toBe('__wildcard__');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not replace underscores in symbol-only tags regardless of setting', () => {
|
||||||
|
// Symbol-only tags should not be affected by the setting
|
||||||
|
settingValues.replaceUnderscoreWithSpace = true;
|
||||||
|
expect(normalizeTagToInsert('^_^')).toBe('^_^');
|
||||||
|
|
||||||
|
settingValues.replaceUnderscoreWithSpace = false;
|
||||||
|
expect(normalizeTagToInsert('^_^')).toBe('^_^');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not escape parentheses when tag contains commas (multi-tags)', () => {
|
||||||
|
const multiTag = 'masterpiece, best quality, (high quality), ultra-detailed';
|
||||||
|
expect(normalizeTagToInsert(multiTag)).toBe('masterpiece, best quality, (high quality), ultra-detailed');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not escape parentheses in multi-tag strings with parentheses', () => {
|
||||||
|
const multiTag = '(worst quality, low quality, normal quality), bad anatomy';
|
||||||
|
expect(normalizeTagToInsert(multiTag)).toBe('(worst quality, low quality, normal quality), bad anatomy');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle multi-tags with both underscores and parentheses correctly', () => {
|
||||||
|
settingValues.replaceUnderscoreWithSpace = true;
|
||||||
|
const multiTag = 'best_quality, (high_quality:1.2), ultra_detailed';
|
||||||
|
expect(normalizeTagToInsert(multiTag)).toBe('best quality, (high quality:1.2), ultra detailed');
|
||||||
|
|
||||||
|
settingValues.replaceUnderscoreWithSpace = false;
|
||||||
|
expect(normalizeTagToInsert(multiTag)).toBe('best_quality, (high_quality:1.2), ultra_detailed');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should still escape parentheses for single tags without commas', () => {
|
||||||
|
// Single tags (not multi-tags) should still have parentheses escaped
|
||||||
|
expect(normalizeTagToInsert('blue(hair)')).toBe('blue\\(hair\\)');
|
||||||
|
expect(normalizeTagToInsert('(tag)')).toBe('\\(tag\\)');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getCurrentTagRange', () => {
|
describe('getCurrentTagRange', () => {
|
||||||
@@ -367,3 +441,101 @@ describe('findAllTagPositions', () => {
|
|||||||
expect(positions[5].tag).toBe('last');
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -97,10 +97,14 @@ body.dark-theme {
|
|||||||
display: grid;
|
display: grid;
|
||||||
box-shadow: 0 2px 8px rgb(0 0 0 / 30%);
|
box-shadow: 0 2px 8px rgb(0 0 0 / 30%);
|
||||||
grid-auto-rows: auto;
|
grid-auto-rows: auto;
|
||||||
grid-template-columns: max-content 1fr auto auto;
|
grid-template-columns: max-content 1fr auto;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#autocomplete-plus-list.no-alias {
|
||||||
|
grid-template-columns: max-content auto;
|
||||||
|
}
|
||||||
|
|
||||||
.autocomplete-plus-item {
|
.autocomplete-plus-item {
|
||||||
display: grid;
|
display: grid;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -292,10 +296,14 @@ body.dark-theme {
|
|||||||
display: grid;
|
display: grid;
|
||||||
box-shadow: 0 2px 8px rgb(0 0 0 / 30%);
|
box-shadow: 0 2px 8px rgb(0 0 0 / 30%);
|
||||||
grid-auto-rows: auto;
|
grid-auto-rows: auto;
|
||||||
grid-template-columns: max-content 1fr auto auto;
|
grid-template-columns: max-content 1fr auto;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#related-tags-list.no-alias {
|
||||||
|
grid-template-columns: max-content auto;
|
||||||
|
}
|
||||||
|
|
||||||
.related-tag-item {
|
.related-tag-item {
|
||||||
display: grid;
|
display: grid;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
+80
-29
@@ -17,7 +17,9 @@ import {
|
|||||||
extractTagsFromTextArea,
|
extractTagsFromTextArea,
|
||||||
getCurrentTagRange,
|
getCurrentTagRange,
|
||||||
getViewportMargin,
|
getViewportMargin,
|
||||||
IconSvgHtmlString
|
getScrollbarWidth,
|
||||||
|
IconSvgHtmlString,
|
||||||
|
addWeightToLora
|
||||||
} from './utils.js';
|
} from './utils.js';
|
||||||
import { settingValues } from './settings.js';
|
import { settingValues } from './settings.js';
|
||||||
|
|
||||||
@@ -258,6 +260,10 @@ function searchWithFlexSearch(partialTag, queryVariations) {
|
|||||||
* @returns {string} The current partial tag.
|
* @returns {string} The current partial tag.
|
||||||
*/
|
*/
|
||||||
function getCurrentPartialTag(inputElement) {
|
function getCurrentPartialTag(inputElement) {
|
||||||
|
if (!inputElement) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
const text = inputElement.value;
|
const text = inputElement.value;
|
||||||
const cursorPos = inputElement.selectionStart;
|
const cursorPos = inputElement.selectionStart;
|
||||||
|
|
||||||
@@ -303,16 +309,34 @@ function getCurrentPartialTag(inputElement) {
|
|||||||
* @param {TagData} tagDataToInsert The raw tag string to insert.
|
* @param {TagData} tagDataToInsert The raw tag string to insert.
|
||||||
*/
|
*/
|
||||||
function insertTagToTextArea(inputElement, tagDataToInsert) {
|
function insertTagToTextArea(inputElement, tagDataToInsert) {
|
||||||
|
if (!inputElement || !tagDataToInsert) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const text = inputElement.value;
|
const text = inputElement.value;
|
||||||
const cursorPos = inputElement.selectionStart;
|
const cursorPos = inputElement.selectionStart;
|
||||||
|
|
||||||
const { start: tagStart, end: tagEnd, tag: currentTag } = getCurrentTagRange(text, cursorPos);
|
const tagRange = getCurrentTagRange(text, cursorPos);
|
||||||
|
let tagStart, tagEnd, currentTag;
|
||||||
|
|
||||||
|
if (!tagRange) {
|
||||||
|
// Fallback: insert at cursor position
|
||||||
|
tagStart = cursorPos;
|
||||||
|
tagEnd = cursorPos;
|
||||||
|
currentTag = '';
|
||||||
|
} else {
|
||||||
|
({ start: tagStart, end: tagEnd, tag: currentTag } = tagRange);
|
||||||
|
}
|
||||||
|
|
||||||
const replaceStart = Math.min(cursorPos, tagStart);
|
const replaceStart = Math.min(cursorPos, tagStart);
|
||||||
let replaceEnd = cursorPos;
|
let replaceEnd = cursorPos;
|
||||||
|
|
||||||
let normalizedTag;
|
let normalizedTag;
|
||||||
if(Object.values(ModelTagSource).includes(tagDataToInsert.source)){
|
if (tagDataToInsert.source === ModelTagSource.Lora) {
|
||||||
// If the tag is from a model tag source, don't want to normalize it
|
// 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;
|
normalizedTag = tagDataToInsert.tag;
|
||||||
} else {
|
} else {
|
||||||
normalizedTag = normalizeTagToInsert(tagDataToInsert.tag);
|
normalizedTag = normalizeTagToInsert(tagDataToInsert.tag);
|
||||||
@@ -327,9 +351,9 @@ function insertTagToTextArea(inputElement, tagDataToInsert) {
|
|||||||
const needsSpaceBefore = text[replaceStart - 1] === ',';
|
const needsSpaceBefore = text[replaceStart - 1] === ',';
|
||||||
const prefix = needsSpaceBefore ? ' ' : '';
|
const prefix = needsSpaceBefore ? ' ' : '';
|
||||||
|
|
||||||
// Standard separator (comma + space)
|
// 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 needsSuffixAfter = !",:".includes(text[replaceEnd]); // TODO: If ":" is part of the emoticon, a suffix is required (e.g. ":o")
|
||||||
const suffix = needsSuffixAfter ? ', ' : '';
|
const suffix = (needsSuffixAfter && settingValues.autoInsertComma) ? ', ' : '';
|
||||||
|
|
||||||
const textToInsertWithAffixes = prefix + normalizedTag + suffix;
|
const textToInsertWithAffixes = prefix + normalizedTag + suffix;
|
||||||
|
|
||||||
@@ -471,6 +495,8 @@ class AutocompleteUI {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.tagsList.classList.toggle('no-alias', settingValues.hideAlias);
|
||||||
|
|
||||||
const existingTags = extractTagsFromTextArea(this.target);
|
const existingTags = extractTagsFromTextArea(this.target);
|
||||||
const currentTag = getCurrentPartialTag(this.target);
|
const currentTag = getCurrentPartialTag(this.target);
|
||||||
|
|
||||||
@@ -488,6 +514,7 @@ class AutocompleteUI {
|
|||||||
*/
|
*/
|
||||||
#createTagElement(tagData, isExisting) {
|
#createTagElement(tagData, isExisting) {
|
||||||
const categoryText = TagCategory[tagData.source][tagData.category] || "unknown";
|
const categoryText = TagCategory[tagData.source][tagData.category] || "unknown";
|
||||||
|
const aliasText = tagData.alias.join(', ');
|
||||||
|
|
||||||
const tagRow = document.createElement('div');
|
const tagRow = document.createElement('div');
|
||||||
tagRow.classList.add('autocomplete-plus-item', tagData.source);
|
tagRow.classList.add('autocomplete-plus-item', tagData.source);
|
||||||
@@ -517,26 +544,29 @@ class AutocompleteUI {
|
|||||||
alias.className = 'autocomplete-plus-alias';
|
alias.className = 'autocomplete-plus-alias';
|
||||||
|
|
||||||
// Display alias if available
|
// Display alias if available
|
||||||
if (tagData.alias && tagData.alias.length > 0) {
|
if (aliasText.length > 0) {
|
||||||
let aliasText = tagData.alias.join(', ');
|
|
||||||
alias.textContent = `${aliasText}`;
|
alias.textContent = `${aliasText}`;
|
||||||
alias.title = tagData.alias.join(', '); // Full alias on hover
|
alias.title = aliasText; // Full alias on hover
|
||||||
}
|
}
|
||||||
|
|
||||||
// Category
|
|
||||||
const category = document.createElement('span');
|
|
||||||
category.className = `autocomplete-plus-category`;
|
|
||||||
category.textContent = `${categoryText.substring(0, 2)}`;
|
|
||||||
category.title = categoryText; // Full category on hover
|
|
||||||
|
|
||||||
// Count
|
// Count
|
||||||
const tagCount = document.createElement('span');
|
const tagCount = document.createElement('span');
|
||||||
tagCount.className = `autocomplete-plus-tag-count`;
|
tagCount.className = `autocomplete-plus-tag-count`;
|
||||||
tagCount.textContent = formatCountHumanReadable(tagData.count);
|
tagCount.textContent = formatCountHumanReadable(tagData.count);
|
||||||
|
|
||||||
|
// Create tooltip with more info
|
||||||
|
let tooltipText = `Count: ${tagData.count}\nCategory: ${categoryText}`;
|
||||||
|
if (aliasText.length > 0) {
|
||||||
|
tooltipText += `\nAlias: ${aliasText}`;
|
||||||
|
}
|
||||||
|
tagRow.title = tooltipText;
|
||||||
|
|
||||||
tagRow.appendChild(tagName);
|
tagRow.appendChild(tagName);
|
||||||
|
|
||||||
|
if (!settingValues.hideAlias) {
|
||||||
tagRow.appendChild(alias);
|
tagRow.appendChild(alias);
|
||||||
tagRow.appendChild(category);
|
}
|
||||||
|
|
||||||
tagRow.appendChild(tagCount);
|
tagRow.appendChild(tagCount);
|
||||||
this.tagsList.appendChild(tagRow);
|
this.tagsList.appendChild(tagRow);
|
||||||
}
|
}
|
||||||
@@ -574,21 +604,14 @@ class AutocompleteUI {
|
|||||||
let topPosition = targetElmOffset.top + ((caretTop - targetElmOffset.top) + caretLineHeight) * scale;
|
let topPosition = targetElmOffset.top + ((caretTop - targetElmOffset.top) + caretLineHeight) * scale;
|
||||||
let leftPosition = targetElmOffset.left + (caretLeft - targetElmOffset.left) * scale;;
|
let leftPosition = targetElmOffset.left + (caretLeft - targetElmOffset.left) * scale;;
|
||||||
|
|
||||||
const maxWidth = Math.min(rootRect.width, viewportWidth / 2);
|
|
||||||
const naturalHeight = rootRect.height;
|
const naturalHeight = rootRect.height;
|
||||||
|
|
||||||
//Horizontal Collision Detection and Adjustment
|
|
||||||
if (leftPosition + maxWidth > viewportWidth - margin.right) {
|
|
||||||
leftPosition = viewportWidth - maxWidth - margin.right;
|
|
||||||
}
|
|
||||||
if (leftPosition < margin.left) {
|
|
||||||
leftPosition = margin.left;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vertical Collision Detection and Adjustment
|
// Vertical Collision Detection and Adjustment
|
||||||
const availableSpaceBelow = viewportHeight - topPosition - margin.bottom;
|
const availableSpaceBelow = viewportHeight - topPosition - margin.bottom;
|
||||||
const availableSpaceAbove = caretTop - margin.top;
|
const availableSpaceAbove = caretTop - margin.top;
|
||||||
|
|
||||||
|
let needsVerticalScroll = false;
|
||||||
|
let maxHeight = rootRect.height;
|
||||||
|
|
||||||
if (naturalHeight <= availableSpaceBelow) {
|
if (naturalHeight <= availableSpaceBelow) {
|
||||||
// Fits perfectly below the caret
|
// Fits perfectly below the caret
|
||||||
@@ -600,14 +623,15 @@ class AutocompleteUI {
|
|||||||
topPosition = caretTop - naturalHeight - margin.top;
|
topPosition = caretTop - naturalHeight - margin.top;
|
||||||
} else {
|
} else {
|
||||||
// Doesn't fit perfectly either below or above, needs scrolling.
|
// Doesn't fit perfectly either below or above, needs scrolling.
|
||||||
|
needsVerticalScroll = true;
|
||||||
// Choose the position (above or below) that offers more space.
|
// Choose the position (above or below) that offers more space.
|
||||||
if (availableSpaceBelow >= availableSpaceAbove) {
|
if (availableSpaceBelow >= availableSpaceAbove) {
|
||||||
// Scroll below: topPosition remains as initially calculated
|
// Scroll below: topPosition remains as initially calculated
|
||||||
this.tagsList.style.maxHeight = `${availableSpaceBelow}px`;
|
maxHeight = availableSpaceBelow;
|
||||||
} else {
|
} else {
|
||||||
// Scroll above: Position near the top edge and set max-height
|
// Scroll above: Position near the top edge and set max-height
|
||||||
topPosition = margin.top;
|
topPosition = margin.top;
|
||||||
this.tagsList.style.maxHeight = `${availableSpaceAbove}px`;
|
maxHeight = availableSpaceAbove;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -616,16 +640,30 @@ class AutocompleteUI {
|
|||||||
if (topPosition < margin.top) {
|
if (topPosition < margin.top) {
|
||||||
topPosition = margin.top;
|
topPosition = margin.top;
|
||||||
// If pushed down, recalculate max-height if it was set based on top alignment
|
// If pushed down, recalculate max-height if it was set based on top alignment
|
||||||
if (this.tagsList.style.maxHeight && availableSpaceBelow < availableSpaceAbove) {
|
if (availableSpaceBelow < availableSpaceAbove) {
|
||||||
// Recalculate max-height based on space from the top margin
|
// Recalculate max-height based on space from the top margin
|
||||||
this.tagsList.style.maxHeight = `${viewportHeight - margin.top - margin.bottom}px`;
|
maxHeight = viewportHeight - margin.top - margin.bottom;
|
||||||
|
needsVerticalScroll = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate maxWidth considering scrollbar width if vertical scrolling is needed
|
||||||
|
const scrollbarWidth = needsVerticalScroll ? getScrollbarWidth() : 0;
|
||||||
|
const maxWidth = Math.min(rootRect.width + scrollbarWidth, viewportWidth / 2);
|
||||||
|
|
||||||
|
// Horizontal Collision Detection and Adjustment
|
||||||
|
if (leftPosition + maxWidth > viewportWidth - margin.right) {
|
||||||
|
leftPosition = viewportWidth - maxWidth - margin.right;
|
||||||
|
}
|
||||||
|
if (leftPosition < margin.left) {
|
||||||
|
leftPosition = margin.left;
|
||||||
|
}
|
||||||
|
|
||||||
// Apply the calculated position and display the element
|
// Apply the calculated position and display the element
|
||||||
this.root.style.left = `${leftPosition}px`;
|
this.root.style.left = `${leftPosition}px`;
|
||||||
this.root.style.top = `${topPosition}px`;
|
this.root.style.top = `${topPosition}px`;
|
||||||
this.root.style.maxWidth = `${maxWidth}px`;
|
this.root.style.maxWidth = `${maxWidth}px`;
|
||||||
|
this.tagsList.style.maxHeight = `${maxHeight}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Highlights the item (row) at the given index */
|
/** Highlights the item (row) at the given index */
|
||||||
@@ -1010,3 +1048,16 @@ export class AutocompleteEventHandler {
|
|||||||
handleClick(event) {
|
handleClick(event) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Export functions for testing
|
||||||
|
const isTestEnvironment = typeof process !== 'undefined' && process.env.NODE_ENV === 'test';
|
||||||
|
export const __test__ = isTestEnvironment
|
||||||
|
? {
|
||||||
|
searchCompletionCandidates,
|
||||||
|
sequentialSearch,
|
||||||
|
searchWithFlexSearch,
|
||||||
|
matchWord,
|
||||||
|
getCurrentPartialTag,
|
||||||
|
insertTagToTextArea
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|||||||
+8
-12
@@ -184,18 +184,8 @@ async function loadTags(csvUrl, siteName) {
|
|||||||
updateMaxTagLength(tag.length);
|
updateMaxTagLength(tag.length);
|
||||||
|
|
||||||
autoCompleteData[siteName].sortedTags.push(tagData);
|
autoCompleteData[siteName].sortedTags.push(tagData);
|
||||||
} else {
|
|
||||||
console.warn(`[Autocomplete-Plus] Invalid CSV format in line ${i + 1} of ${csvUrl}: ${line}. Expected ${TAGS_CSV_HEADER_COLUMNS.length} columns, but got ${columns.length}.`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by count in descending order
|
// Set the tag and its alias in the maps
|
||||||
autoCompleteData[siteName].sortedTags.sort((a, b) => b.count - a.count);
|
|
||||||
|
|
||||||
// Build maps as before, but ensure not to overwrite if already processed from extra files
|
|
||||||
autoCompleteData[siteName].sortedTags.forEach(tagData => {
|
|
||||||
if (!autoCompleteData[siteName].tagMap.has(tagData.tag)) {
|
|
||||||
autoCompleteData[siteName].tagMap.set(tagData.tag, tagData);
|
autoCompleteData[siteName].tagMap.set(tagData.tag, tagData);
|
||||||
if (tagData.alias && Array.isArray(tagData.alias)) {
|
if (tagData.alias && Array.isArray(tagData.alias)) {
|
||||||
tagData.alias.forEach(alias => {
|
tagData.alias.forEach(alias => {
|
||||||
@@ -204,8 +194,11 @@ async function loadTags(csvUrl, siteName) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`[Autocomplete-Plus] Invalid CSV format in line ${i + 1} of ${csvUrl}: ${line}. Expected ${TAGS_CSV_HEADER_COLUMNS.length} columns, but got ${columns.length}.`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[Autocomplete-Plus] Failed to fetch or process tags from ${csvUrl}:`, error);
|
console.error(`[Autocomplete-Plus] Failed to fetch or process tags from ${csvUrl}:`, error);
|
||||||
@@ -450,6 +443,9 @@ async function initializeDataFromCSV(csvListData, source) {
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
Promise.all(tagsLoadPromiseFactories.map(factory => factory()))
|
Promise.all(tagsLoadPromiseFactories.map(factory => factory()))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
// Sort by count in descending order
|
||||||
|
autoCompleteData[source].sortedTags.sort((a, b) => b.count - a.count);
|
||||||
|
|
||||||
// Build FlexSearch index after tags are loaded
|
// Build FlexSearch index after tags are loaded
|
||||||
return buildFlexSearchIndex(source);
|
return buildFlexSearchIndex(source);
|
||||||
})
|
})
|
||||||
|
|||||||
+83
-50
@@ -190,40 +190,41 @@ async function addExtraSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add extra setting for checking new CSV updates
|
// Add extra setting for checking new CSV updates
|
||||||
app.ui.settings.addSetting({
|
// Note: Temporarily comment out until changes to remote files can be detected
|
||||||
id: id + ".check_new_csv",
|
// app.ui.settings.addSetting({
|
||||||
defaultValue: null,
|
// id: id + ".check_new_csv",
|
||||||
name: "Check CSV updates",
|
// defaultValue: null,
|
||||||
category: [name, "Misc", "Check new CSV"],
|
// name: "Check CSV updates",
|
||||||
type: () => {
|
// category: [name, "Misc", "Check new CSV"],
|
||||||
const lastCheckSpan = $el("span", {
|
// type: () => {
|
||||||
textContent: lastCheckTimeText,
|
// const lastCheckSpan = $el("span", {
|
||||||
className: "text-sm text-gray-500",
|
// textContent: lastCheckTimeText,
|
||||||
style: {
|
// className: "text-sm text-gray-500",
|
||||||
marginRight: "16px"
|
// style: {
|
||||||
}
|
// marginRight: "16px"
|
||||||
});
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
const checkButton = $el("button", {
|
// const checkButton = $el("button", {
|
||||||
textContent: "Check now",
|
// textContent: "Check now",
|
||||||
className: "p-button p-component p-button-primary",
|
// className: "p-button p-component p-button-primary",
|
||||||
onclick: async () => {
|
// onclick: async () => {
|
||||||
await performUpdateCheck(checkButton, lastCheckSpan);
|
// await performUpdateCheck(checkButton, lastCheckSpan);
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
|
||||||
return $el("div", {
|
// return $el("div", {
|
||||||
className: "flex-row items-center gap-2",
|
// className: "flex-row items-center gap-2",
|
||||||
}, [
|
// }, [
|
||||||
$el("div", {
|
// $el("div", {
|
||||||
className: "p-component",
|
// className: "p-component",
|
||||||
}, [
|
// }, [
|
||||||
lastCheckSpan,
|
// lastCheckSpan,
|
||||||
checkButton,
|
// checkButton,
|
||||||
]),
|
// ]),
|
||||||
]);
|
// ]);
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -247,7 +248,7 @@ app.registerExtension({
|
|||||||
settings: [
|
settings: [
|
||||||
// --- Tag source Settings ---
|
// --- Tag source Settings ---
|
||||||
{
|
{
|
||||||
id: id + ".tag_source_icon_position",
|
id: id + ".TagSource.IconPosition",
|
||||||
name: "Tag Source Icon Position",
|
name: "Tag Source Icon Position",
|
||||||
type: "combo",
|
type: "combo",
|
||||||
options: ["left", "right", "hidden"],
|
options: ["left", "right", "hidden"],
|
||||||
@@ -258,7 +259,7 @@ app.registerExtension({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: id + ".primary_tag_source",
|
id: id + ".TagSource.PrimaryTagSource",
|
||||||
name: "Primary source for 'all' Source",
|
name: "Primary source for 'all' Source",
|
||||||
tooltip: "When 'Autocomplete Tag Source' is 'all', this determines which source's tags appear first in suggestions.",
|
tooltip: "When 'Autocomplete Tag Source' is 'all', this determines which source's tags appear first in suggestions.",
|
||||||
type: "combo",
|
type: "combo",
|
||||||
@@ -270,7 +271,7 @@ app.registerExtension({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: id + ".tag_source",
|
id: id + ".TagSource",
|
||||||
name: "Autocomplete Tag Source",
|
name: "Autocomplete Tag Source",
|
||||||
tooltip: "Select the tag source for autocomplete suggestions. 'all' includes tags from all loaded sources.",
|
tooltip: "Select the tag source for autocomplete suggestions. 'all' includes tags from all loaded sources.",
|
||||||
type: "combo",
|
type: "combo",
|
||||||
@@ -281,11 +282,12 @@ app.registerExtension({
|
|||||||
settingValues.tagSource = newVal;
|
settingValues.tagSource = newVal;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// --- Autocomplete Settings ---
|
// --- Autocomplete Settings ---
|
||||||
{
|
{
|
||||||
id: id + ".use_fast_search",
|
id: id + ".Autocompletion.UseFastSearch",
|
||||||
name: "Use Fast Search",
|
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",
|
type: "boolean",
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
category: [name, "Autocompletion", "Use Fast Search"],
|
category: [name, "Autocompletion", "Use Fast Search"],
|
||||||
@@ -294,9 +296,8 @@ app.registerExtension({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: id + ".enable_models",
|
id: id + ".Autocompletion.EnableModels",
|
||||||
name: "Enable Loras and Embeddings",
|
name: "Enable Loras and Embeddings",
|
||||||
tooltip: "Enable Lora and Embedding suggestions",
|
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
category: [name, "Autocompletion", "Enable Loras and Embeddings"],
|
category: [name, "Autocompletion", "Enable Loras and Embeddings"],
|
||||||
@@ -305,7 +306,29 @@ app.registerExtension({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: id + ".max_suggestions",
|
id: id + ".Autocompletion.ReplaceUnderscoreWithSpace",
|
||||||
|
name: "Replace '_' with 'Space'",
|
||||||
|
tooltip: "This setting also affects related tags display.",
|
||||||
|
type: "boolean",
|
||||||
|
defaultValue: true,
|
||||||
|
category: [name, "Autocompletion", "Replace Underscore with Space"],
|
||||||
|
onChange: (newVal, oldVal) => {
|
||||||
|
settingValues.replaceUnderscoreWithSpace = newVal;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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",
|
name: "Max suggestions",
|
||||||
type: "slider",
|
type: "slider",
|
||||||
attrs: {
|
attrs: {
|
||||||
@@ -320,9 +343,8 @@ app.registerExtension({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: id + ".boolean",
|
id: id + ".Autocompletion.Enable",
|
||||||
name: "Enable Autocomplete",
|
name: "Enable Autocomplete",
|
||||||
description: "Enable or disable the autocomplete feature.",
|
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
category: [name, "Autocompletion", "Enable Autocomplete"],
|
category: [name, "Autocompletion", "Enable Autocomplete"],
|
||||||
@@ -333,9 +355,9 @@ app.registerExtension({
|
|||||||
|
|
||||||
// --- Related Tags Settings ---
|
// --- Related Tags Settings ---
|
||||||
{
|
{
|
||||||
id: id + ".related_tags_trigger_mode",
|
id: id + ".RelatedTags.RelatedTagsTriggerMode",
|
||||||
name: "Related Tags Trigger Mode",
|
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",
|
type: "combo",
|
||||||
options: ["click", "ctrl+Click"],
|
options: ["click", "ctrl+Click"],
|
||||||
defaultValue: "click",
|
defaultValue: "click",
|
||||||
@@ -345,9 +367,9 @@ app.registerExtension({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: id + ".related_tags_position",
|
id: id + ".RelatedTags.DisplayPosition",
|
||||||
name: "Default Display Position",
|
name: "Default Display Position",
|
||||||
description: "Display position (relative to Textarea)",
|
tooltip: "Display position (relative to Textarea).",
|
||||||
type: "combo",
|
type: "combo",
|
||||||
options: ["horizontal", "vertical"],
|
options: ["horizontal", "vertical"],
|
||||||
defaultValue: "horizontal",
|
defaultValue: "horizontal",
|
||||||
@@ -357,9 +379,8 @@ app.registerExtension({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: id + ".max_related_tags",
|
id: id + ".RelatedTags.MaxRelatedTags",
|
||||||
name: "Max related tags",
|
name: "Max related tags",
|
||||||
description: "Maximum number of related tags to display",
|
|
||||||
type: "slider",
|
type: "slider",
|
||||||
attrs: {
|
attrs: {
|
||||||
min: 5,
|
min: 5,
|
||||||
@@ -373,15 +394,27 @@ app.registerExtension({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: id + ".related_tags_enable",
|
id: id + ".RelatedTags.Enable",
|
||||||
name: "Enable Related Tags",
|
name: "Enable Related Tags",
|
||||||
description: "Enable or disable the related tags feature.",
|
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
category: [name, "Related Tags", "Enable Related Tags"],
|
category: [name, "Related Tags", "Enable Related Tags"],
|
||||||
onChange: (newVal, oldVal) => {
|
onChange: (newVal, oldVal) => {
|
||||||
settingValues.enableRelatedTags = newVal;
|
settingValues.enableRelatedTags = newVal;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Display settings ---
|
||||||
|
{
|
||||||
|
id: id + ".Display.HideAlias",
|
||||||
|
name: "Hide Alias",
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
+32
-27
@@ -4,6 +4,7 @@ import {
|
|||||||
extractTagsFromTextArea,
|
extractTagsFromTextArea,
|
||||||
findAllTagPositions,
|
findAllTagPositions,
|
||||||
getCurrentTagRange,
|
getCurrentTagRange,
|
||||||
|
getScrollbarWidth,
|
||||||
getViewportMargin,
|
getViewportMargin,
|
||||||
isLongText,
|
isLongText,
|
||||||
isValidTag,
|
isValidTag,
|
||||||
@@ -410,17 +411,12 @@ class RelatedTagsUI {
|
|||||||
.find((tagData) => tagData !== undefined);
|
.find((tagData) => tagData !== undefined);
|
||||||
|
|
||||||
if (!tagData) {
|
if (!tagData) {
|
||||||
tagData = new TagData({
|
// Create a dummy TagData if not found
|
||||||
tag: this.currentTag,
|
tagData = new TagData(this.currentTag, null, 0, [], TagSource.Danbooru);
|
||||||
source: TagSource.Danbooru,
|
|
||||||
category: 'unknown',
|
|
||||||
count: 0,
|
|
||||||
alias: [],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagText = this.currentTag;
|
const categoryText = TagCategory[tagData.source][tagData.category] || "unknown";
|
||||||
const categoryText = TagCategory[tagData.source][tagData.category];
|
const aliasText = tagData.alias.join(', ');
|
||||||
|
|
||||||
// Update header text with current tag
|
// Update header text with current tag
|
||||||
this.headerText.innerHTML = ''; // Clear previous content
|
this.headerText.innerHTML = ''; // Clear previous content
|
||||||
@@ -428,15 +424,15 @@ class RelatedTagsUI {
|
|||||||
|
|
||||||
const tagName = document.createElement('span');
|
const tagName = document.createElement('span');
|
||||||
tagName.classList.add('related-tags-header-tag-name', tagData.source);
|
tagName.classList.add('related-tags-header-tag-name', tagData.source);
|
||||||
tagName.title = `Count: ${tagData.count}\nCategory: ${categoryText}`;
|
tagName.title = `Count: ${tagData.count}\nCategory: ${categoryText}\nAlias: ${aliasText}`;
|
||||||
tagName.dataset.tagCategory = categoryText;
|
tagName.dataset.tagCategory = categoryText;
|
||||||
if (tagData.source && ['left', 'right'].includes(settingValues.tagSourceIconPosition)) {
|
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>`;
|
const tagSourceIconHtml = `<svg class="autocomplete-plus-tag-icon-svg"><use xlink:href="#autocomplete-plus-icon-${tagData.source}"></use></svg>`;
|
||||||
tagName.innerHTML = settingValues.tagSourceIconPosition == 'left'
|
tagName.innerHTML = settingValues.tagSourceIconPosition == 'left'
|
||||||
? `${tagSourceIconHtml} ${tagText}`
|
? `${tagSourceIconHtml} ${tagData.tag}`
|
||||||
: `${tagText} ${tagSourceIconHtml}`;
|
: `${tagData.tag} ${tagSourceIconHtml}`;
|
||||||
} else {
|
} else {
|
||||||
tagName.textContent += tagText;
|
tagName.textContent += tagData.tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -447,8 +443,8 @@ class RelatedTagsUI {
|
|||||||
this.headerAlias.innerHTML = '';
|
this.headerAlias.innerHTML = '';
|
||||||
|
|
||||||
// Add alias if available
|
// Add alias if available
|
||||||
if (tagData.alias?.length > 0) {
|
if (aliasText.length > 0 && !settingValues.hideAlias) {
|
||||||
this.headerAlias.textContent = tagData.alias.join(', ');
|
this.headerAlias.textContent = aliasText;
|
||||||
this.headerAlias.style.display = 'block';
|
this.headerAlias.style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,6 +481,9 @@ class RelatedTagsUI {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Toggle column class based on settings
|
||||||
|
this.tagsContainer.classList.toggle('no-alias', settingValues.hideAlias);
|
||||||
|
|
||||||
const existingTags = extractTagsFromTextArea(this.target);
|
const existingTags = extractTagsFromTextArea(this.target);
|
||||||
|
|
||||||
// Create tag rows
|
// Create tag rows
|
||||||
@@ -503,6 +502,7 @@ class RelatedTagsUI {
|
|||||||
*/
|
*/
|
||||||
#createTagElement(tagData, isExisting) {
|
#createTagElement(tagData, isExisting) {
|
||||||
const categoryText = TagCategory[tagData.source][tagData.category] || "unknown";
|
const categoryText = TagCategory[tagData.source][tagData.category] || "unknown";
|
||||||
|
const aliasText = tagData.alias.join(', ');
|
||||||
|
|
||||||
const tagRow = document.createElement('div');
|
const tagRow = document.createElement('div');
|
||||||
tagRow.classList.add('related-tag-item', tagData.source);
|
tagRow.classList.add('related-tag-item', tagData.source);
|
||||||
@@ -524,33 +524,30 @@ class RelatedTagsUI {
|
|||||||
alias.className = 'related-tag-alias';
|
alias.className = 'related-tag-alias';
|
||||||
|
|
||||||
// Display alias if available
|
// Display alias if available
|
||||||
if (tagData.alias && tagData.alias.length > 0) {
|
if (aliasText.length > 0) {
|
||||||
let aliasText = tagData.alias.join(', ');
|
|
||||||
alias.textContent = `${aliasText}`;
|
alias.textContent = `${aliasText}`;
|
||||||
alias.title = tagData.alias.join(', '); // Full alias on hover
|
alias.title = aliasText; // Full alias on hover
|
||||||
}
|
}
|
||||||
|
|
||||||
// Category
|
|
||||||
const category = document.createElement('span');
|
|
||||||
category.className = `related-tag-category`;
|
|
||||||
category.textContent = `${categoryText.substring(0, 2)}`;
|
|
||||||
|
|
||||||
// Similarity
|
// Similarity
|
||||||
const similarity = document.createElement('span');
|
const similarity = document.createElement('span');
|
||||||
similarity.className = 'related-tag-similarity';
|
similarity.className = 'related-tag-similarity';
|
||||||
similarity.textContent = `${(tagData.similarity * 100).toFixed(2)}%`;
|
similarity.textContent = `${(tagData.similarity * 100).toFixed(2)}%`;
|
||||||
|
|
||||||
// Create tooltip with more info
|
// Create tooltip with more info
|
||||||
let tooltipText = `Tag: ${tagData.tag}\nSimilarity: ${(tagData.similarity * 100).toFixed(2)}%\nCount: ${tagData.count}`;
|
let tooltipText = `Similarity: ${(tagData.similarity * 100).toFixed(2)}%\nCount: ${tagData.count}\nCategory: ${categoryText}`;
|
||||||
if (tagData.alias && tagData.alias.length > 0) {
|
if (aliasText.length > 0) {
|
||||||
tooltipText += `\nAlias: ${tagData.alias.join(', ')}`;
|
tooltipText += `\nAlias: ${aliasText}`;
|
||||||
}
|
}
|
||||||
tagRow.title = tooltipText;
|
tagRow.title = tooltipText;
|
||||||
|
|
||||||
// Add cells to row
|
// Add cells to row
|
||||||
tagRow.appendChild(tagName);
|
tagRow.appendChild(tagName);
|
||||||
|
|
||||||
|
if (!settingValues.hideAlias) {
|
||||||
tagRow.appendChild(alias);
|
tagRow.appendChild(alias);
|
||||||
tagRow.appendChild(category);
|
}
|
||||||
|
|
||||||
tagRow.appendChild(similarity);
|
tagRow.appendChild(similarity);
|
||||||
|
|
||||||
return tagRow;
|
return tagRow;
|
||||||
@@ -669,6 +666,10 @@ class RelatedTagsUI {
|
|||||||
area.y = targetRect.bottom;
|
area.y = targetRect.bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate width considering scrollbar width if vertical scrolling is needed
|
||||||
|
const scrollbarWidth = area.height < elemHeight ? getScrollbarWidth() : 0;
|
||||||
|
area.width = Math.min(elemWidth + scrollbarWidth, maxWidth);
|
||||||
|
|
||||||
// Adjust x position to avoid overflow
|
// Adjust x position to avoid overflow
|
||||||
area.x = Math.min(area.x, viewportWidth - area.width - margin.right);
|
area.x = Math.min(area.x, viewportWidth - area.width - margin.right);
|
||||||
} else {
|
} else {
|
||||||
@@ -685,6 +686,10 @@ class RelatedTagsUI {
|
|||||||
area.x = targetRect.right;
|
area.x = targetRect.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate width considering scrollbar width if vertical scrolling is needed
|
||||||
|
const scrollbarWidth = area.height < elemHeight ? getScrollbarWidth() : 0;
|
||||||
|
area.width = Math.min(area.width + scrollbarWidth, viewportWidth - margin.left - margin.right);
|
||||||
|
|
||||||
// Adjust y position to avoid overflow
|
// Adjust y position to avoid overflow
|
||||||
area.y = Math.min(area.y, viewportHeight - area.height - margin.bottom);
|
area.y = Math.min(area.y, viewportHeight - area.height - margin.bottom);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ function createTagEncoder() {
|
|||||||
cache: true,
|
cache: true,
|
||||||
// filter: new Set(['and', 'to', 'be', 'on']),
|
// filter: new Set(['and', 'to', 'be', 'on']),
|
||||||
replacer: [/(?<=[a-zA-Z\)])_$/, ''], // Remove trailing underscores after letters/parentheses
|
replacer: [/(?<=[a-zA-Z\)])_$/, ''], // Remove trailing underscores after letters/parentheses
|
||||||
split: /(?<=[a-zA-Z\)])_(?=[a-zA-Z\(])|\((?=[a-zA-Z])|(?<=[a-zA-Z\)])\)|[ \n]/ // Split on underscores between words, parentheses, spaces, and newlines
|
split: /(?<=[a-zA-Z\)])_(?=[a-zA-Z\(])|\((?=[a-zA-Z])|(?<=[a-zA-Z\)])\)|[ \-\n]/ // Split on underscores between words, parentheses, spaces, hyphens, and newlines
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ export const settingValues = {
|
|||||||
maxSuggestions: 10,
|
maxSuggestions: 10,
|
||||||
enableModels: true, // Enable Lora and Embedding suggestions
|
enableModels: true, // Enable Lora and Embedding suggestions
|
||||||
useFastSearch: false,
|
useFastSearch: false,
|
||||||
|
replaceUnderscoreWithSpace: true, // Replace underscores with spaces in tag insertion
|
||||||
|
autoInsertComma: true,
|
||||||
|
|
||||||
// Related tags feature settings
|
// Related tags feature settings
|
||||||
enableRelatedTags: true,
|
enableRelatedTags: true,
|
||||||
@@ -16,6 +18,9 @@ export const settingValues = {
|
|||||||
relatedTagsDisplayPosition: 'horizontal', // 'horizontal' or 'vertical'
|
relatedTagsDisplayPosition: 'horizontal', // 'horizontal' or 'vertical'
|
||||||
relatedTagsTriggerMode: 'click', // Options: 'click', 'ctrl+Click'
|
relatedTagsTriggerMode: 'click', // Options: 'click', 'ctrl+Click'
|
||||||
|
|
||||||
|
// Display settings
|
||||||
|
hideAlias: false, // Hide alias in the autocomplete and related tags display
|
||||||
|
|
||||||
|
|
||||||
// Internal logic settings
|
// Internal logic settings
|
||||||
_useFallbackAttachmentForEventListener: false, // Fallback to attach event listener when somthing goes wrong
|
_useFallbackAttachmentForEventListener: false, // Fallback to attach event listener when somthing goes wrong
|
||||||
|
|||||||
+74
-4
@@ -223,23 +223,31 @@ export function normalizeTagToSearch(str) {
|
|||||||
* Converts underscores to spaces only if the tag contains at least one letter or number,
|
* Converts underscores to spaces only if the tag contains at least one letter or number,
|
||||||
* and is not a wildcard call (e.g., "__wildcard__").
|
* and is not a wildcard call (e.g., "__wildcard__").
|
||||||
* Keeps underscores for tags that are only symbols (e.g. "^_^") or wildcard calls.
|
* Keeps underscores for tags that are only symbols (e.g. "^_^") or wildcard calls.
|
||||||
|
* For multi-tags (multiple tags with commas), parentheses are not escaped to preserve prompt weight syntax.
|
||||||
* @param {string} str
|
* @param {string} str
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
export function normalizeTagToInsert(str) {
|
export function normalizeTagToInsert(str) {
|
||||||
if (!str) return str;
|
if (!str) return str;
|
||||||
|
|
||||||
|
// Check if multi-tag string
|
||||||
|
const isMultiTag = str.includes(',');
|
||||||
|
|
||||||
|
// Check if the string contains at least one letter or number
|
||||||
if (isContainsLetterOrNumber(str)) {
|
if (isContainsLetterOrNumber(str)) {
|
||||||
const isWildcardCall = str.startsWith('__') && str.endsWith('__') && str.length > 4;
|
const isWildcardCall = str.startsWith('__') && str.endsWith('__') && str.length > 4;
|
||||||
|
|
||||||
if (!isWildcardCall) {
|
if (!isWildcardCall && settingValues.replaceUnderscoreWithSpace) {
|
||||||
// If doesn't wildcard call, replace underscores with spaces
|
// If doesn't wildcard call and setting is enabled, replace underscores with spaces
|
||||||
return escapeParentheses(str.replace(/_/g, " "));
|
const result = str.replace(/_/g, " ");
|
||||||
|
// Only escape parentheses for single tags, not multi-tags
|
||||||
|
return isMultiTag ? result : escapeParentheses(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, keep it as is (e.g., ""^_^", "__wildcard__")
|
// Otherwise, keep it as is (e.g., ""^_^", "__wildcard__")
|
||||||
return escapeParentheses(str);
|
// Only escape parentheses for single tags, not multi-tags
|
||||||
|
return isMultiTag ? str : escapeParentheses(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -550,6 +558,33 @@ export function getCurrentTagRange(text, cursorPos) {
|
|||||||
return { start: adjustedStart, end: adjustedEnd, tag: adjustedTag };
|
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 ---
|
// --- End String Helper Functions ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -584,3 +619,38 @@ export function getViewportMargin() {
|
|||||||
right: rightBarRect.width,
|
right: rightBarRect.width,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cache for scrollbar width measurement
|
||||||
|
let _cachedScrollbarWidth;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Measures and returns the scrollbar width for the current browser.
|
||||||
|
* Creates a temporary element to measure the scrollbar width accurately.
|
||||||
|
* @returns {number} The scrollbar width in pixels
|
||||||
|
*/
|
||||||
|
export function getScrollbarWidth() {
|
||||||
|
// Return cached value if available
|
||||||
|
if (_cachedScrollbarWidth !== undefined) {
|
||||||
|
return _cachedScrollbarWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a temporary div with scrollbar
|
||||||
|
const outer = document.createElement('div');
|
||||||
|
outer.style.visibility = 'hidden';
|
||||||
|
outer.style.position = 'absolute';
|
||||||
|
outer.style.overflow = 'scroll';
|
||||||
|
outer.style.top = '-9999px';
|
||||||
|
outer.style.width = '100px';
|
||||||
|
document.body.appendChild(outer);
|
||||||
|
|
||||||
|
const inner = document.createElement('div');
|
||||||
|
inner.style.width = '100%';
|
||||||
|
outer.appendChild(inner);
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
_cachedScrollbarWidth = outer.offsetWidth - inner.offsetWidth;
|
||||||
|
|
||||||
|
document.body.removeChild(outer);
|
||||||
|
|
||||||
|
return _cachedScrollbarWidth;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user