P01yH3dr0n commited on
Commit
e893ce4
1 Parent(s): a48c731

Upload 14 files

Browse files
tagcomplete/javascript/__globals.js ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Core components
2
+ var TAC_CFG = {
3
+ // Main tag file
4
+ tagFile: "danbooru.csv",
5
+ // Active in settings
6
+ activeIn: {
7
+ txt2img: true,
8
+ img2img: true,
9
+ negativePrompts: true,
10
+ thirdParty: true,
11
+ },
12
+ // Results related settings
13
+ slidingPopup: true,
14
+ maxResults: 5,
15
+ showAllResults: false,
16
+ resultStepLength: 100,
17
+ delayTime: 100,
18
+ includeEmbeddingsInNormalResults: false,
19
+ showWikiLinks: true,
20
+ showExtraNetworkPreviews: false,
21
+ // Insertion related settings
22
+ replaceUnderscores: true,
23
+ escapeParentheses: true,
24
+ appendComma: true,
25
+ appendSpace: true,
26
+ alwaysSpaceAtEnd: false,
27
+ // Alias settings
28
+ alias: {
29
+ searchByAlias: true,
30
+ onlyShowAlias: false
31
+ },
32
+ // Translation settings
33
+ translation: {
34
+ searchByTranslation: false,
35
+ },
36
+ // Extra file settings
37
+ extra: {
38
+ extraFile: "danbooru-0-zh.csv",
39
+ addMode: "Insert before"
40
+ },
41
+ // Chant file settings
42
+ chantFile: "demo-chants.json",
43
+ // Settings not from tac but still used by the script
44
+ // Custom mapping settings
45
+ keymap: {
46
+ "MoveUp": "ArrowUp",
47
+ "MoveDown": "ArrowDown",
48
+ "JumpUp": "PageUp",
49
+ "JumpDown": "PageDown",
50
+ "JumpToStart": "Home",
51
+ "JumpToEnd": "End",
52
+ "ChooseSelected": "Enter",
53
+ "ChooseFirstOrSelected": "Tab",
54
+ "Close": "Escape"
55
+ },
56
+ colorMap: {
57
+ "danbooru": {
58
+ "-1": ["red", "maroon"],
59
+ "0": ["lightblue", "dodgerblue"],
60
+ "1": ["indianred", "firebrick"],
61
+ "3": ["violet", "darkorchid"],
62
+ "4": ["lightgreen", "darkgreen"],
63
+ "5": ["orange", "darkorange"]
64
+ },
65
+ "e621": {
66
+ "-1": ["red", "maroon"],
67
+ "0": ["lightblue", "dodgerblue"],
68
+ "1": ["gold", "goldenrod"],
69
+ "3": ["violet", "darkorchid"],
70
+ "4": ["lightgreen", "darkgreen"],
71
+ "5": ["tomato", "darksalmon"],
72
+ "6": ["red", "maroon"],
73
+ "7": ["whitesmoke", "black"],
74
+ "8": ["seagreen", "darkseagreen"]
75
+ }
76
+ }
77
+ };
78
+ var tagBasePath = "tagcomplete/tags";
79
+ var modelKeywordPath = "";
80
+ var tacSelfTrigger = false;
81
+
82
+ // Tag completion data loaded from files
83
+ var allTags = [];
84
+ var translations = new Map();
85
+ var extras = [];
86
+ // Same for tag-likes
87
+ var yamlWildcards = [];
88
+ var umiWildcards = [];
89
+ var modelKeywordDict = new Map();
90
+ var chants = [];
91
+
92
+ // Current results
93
+ var results = [];
94
+ var resultCount = 0;
95
+
96
+ // Relevant for parsing
97
+ var previousTags = [];
98
+ var tagword = "";
99
+ var originalTagword = "";
100
+ let hideBlocked = false;
101
+
102
+ // Tag selection for keyboard navigation
103
+ var selectedTag = null;
104
+ var oldSelectedTag = null;
105
+ var resultCountBeforeNormalTags = 0;
106
+
107
+ // Lora keyword undo/redo history
108
+ var textBeforeKeywordInsertion = "";
109
+ var textAfterKeywordInsertion = "";
110
+ var lastEditWasKeywordInsertion = false;
111
+ var keywordInsertionUndone = false;
112
+
113
+ // UMI
114
+ var umiPreviousTags = [];
115
+
116
+ /// Extendability system:
117
+ /// Provides "queues" for other files of the script (or really any js)
118
+ /// to add functions to be called at certain points in the script.
119
+ /// Similar to a callback system, but primitive.
120
+
121
+ // Queues
122
+ const QUEUE_AFTER_INSERT = [];
123
+ const QUEUE_AFTER_SETUP = [];
124
+ const QUEUE_FILE_LOAD = [];
125
+ const QUEUE_AFTER_CONFIG_CHANGE = [];
126
+ const QUEUE_SANITIZE = [];
127
+
128
+ // List of parsers to try
129
+ const PARSERS = [];
tagcomplete/javascript/_baseParser.js ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class FunctionNotOverriddenError extends Error {
2
+ constructor(message = "", ...args) {
3
+ super(message, ...args);
4
+ this.message = message + " is an abstract base function and must be overwritten.";
5
+ }
6
+ }
7
+
8
+ class BaseTagParser {
9
+ triggerCondition = null;
10
+
11
+ constructor (triggerCondition) {
12
+ if (new.target === BaseTagParser) {
13
+ throw new TypeError("Cannot construct abstract BaseCompletionParser directly");
14
+ }
15
+ this.triggerCondition = triggerCondition;
16
+ }
17
+
18
+ parse() {
19
+ throw new FunctionNotOverriddenError("parse()");
20
+ }
21
+ }
tagcomplete/javascript/_caretPosition.js ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // From https://github.com/component/textarea-caret-position
2
+
3
+ // We'll copy the properties below into the mirror div.
4
+ // Note that some browsers, such as Firefox, do not concatenate properties
5
+ // into their shorthand (e.g. padding-top, padding-bottom etc. -> padding),
6
+ // so we have to list every single property explicitly.
7
+ var properties = [
8
+ 'direction', // RTL support
9
+ 'boxSizing',
10
+ 'width', // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does
11
+ 'height',
12
+ 'overflowX',
13
+ 'overflowY', // copy the scrollbar for IE
14
+
15
+ 'borderTopWidth',
16
+ 'borderRightWidth',
17
+ 'borderBottomWidth',
18
+ 'borderLeftWidth',
19
+ 'borderStyle',
20
+
21
+ 'paddingTop',
22
+ 'paddingRight',
23
+ 'paddingBottom',
24
+ 'paddingLeft',
25
+
26
+ // https://developer.mozilla.org/en-US/docs/Web/CSS/font
27
+ 'fontStyle',
28
+ 'fontVariant',
29
+ 'fontWeight',
30
+ 'fontStretch',
31
+ 'fontSize',
32
+ 'fontSizeAdjust',
33
+ 'lineHeight',
34
+ 'fontFamily',
35
+
36
+ 'textAlign',
37
+ 'textTransform',
38
+ 'textIndent',
39
+ 'textDecoration', // might not make a difference, but better be safe
40
+
41
+ 'letterSpacing',
42
+ 'wordSpacing',
43
+
44
+ 'tabSize',
45
+ 'MozTabSize'
46
+
47
+ ];
48
+
49
+ var isBrowser = (typeof window !== 'undefined');
50
+ var isFirefox = (isBrowser && window.mozInnerScreenX != null);
51
+
52
+ function getCaretCoordinates(element, position, options) {
53
+ if (!isBrowser) {
54
+ throw new Error('textarea-caret-position#getCaretCoordinates should only be called in a browser');
55
+ }
56
+
57
+ var debug = options && options.debug || false;
58
+ if (debug) {
59
+ var el = document.querySelector('#input-textarea-caret-position-mirror-div');
60
+ if (el) el.parentNode.removeChild(el);
61
+ }
62
+
63
+ // The mirror div will replicate the textarea's style
64
+ var div = document.createElement('div');
65
+ div.id = 'input-textarea-caret-position-mirror-div';
66
+ document.body.appendChild(div);
67
+
68
+ var style = div.style;
69
+ var computed = window.getComputedStyle ? window.getComputedStyle(element) : element.currentStyle; // currentStyle for IE < 9
70
+ var isInput = element.nodeName === 'INPUT';
71
+
72
+ // Default textarea styles
73
+ style.whiteSpace = 'pre-wrap';
74
+ if (!isInput)
75
+ style.wordWrap = 'break-word'; // only for textarea-s
76
+
77
+ // Position off-screen
78
+ style.position = 'absolute'; // required to return coordinates properly
79
+ if (!debug)
80
+ style.visibility = 'hidden'; // not 'display: none' because we want rendering
81
+
82
+ // Transfer the element's properties to the div
83
+ properties.forEach(function (prop) {
84
+ if (isInput && prop === 'lineHeight') {
85
+ // Special case for <input>s because text is rendered centered and line height may be != height
86
+ if (computed.boxSizing === "border-box") {
87
+ var height = parseInt(computed.height);
88
+ var outerHeight =
89
+ parseInt(computed.paddingTop) +
90
+ parseInt(computed.paddingBottom) +
91
+ parseInt(computed.borderTopWidth) +
92
+ parseInt(computed.borderBottomWidth);
93
+ var targetHeight = outerHeight + parseInt(computed.lineHeight);
94
+ if (height > targetHeight) {
95
+ style.lineHeight = height - outerHeight + "px";
96
+ } else if (height === targetHeight) {
97
+ style.lineHeight = computed.lineHeight;
98
+ } else {
99
+ style.lineHeight = 0;
100
+ }
101
+ } else {
102
+ style.lineHeight = computed.height;
103
+ }
104
+ } else {
105
+ style[prop] = computed[prop];
106
+ }
107
+ });
108
+
109
+ if (isFirefox) {
110
+ // Firefox lies about the overflow property for textareas: https://bugzilla.mozilla.org/show_bug.cgi?id=984275
111
+ if (element.scrollHeight > parseInt(computed.height))
112
+ style.overflowY = 'scroll';
113
+ } else {
114
+ style.overflow = 'hidden'; // for Chrome to not render a scrollbar; IE keeps overflowY = 'scroll'
115
+ }
116
+
117
+ div.textContent = element.value.substring(0, position);
118
+ // The second special handling for input type="text" vs textarea:
119
+ // spaces need to be replaced with non-breaking spaces - http://stackoverflow.com/a/13402035/1269037
120
+ if (isInput)
121
+ div.textContent = div.textContent.replace(/\s/g, '\u00a0');
122
+
123
+ var span = document.createElement('span');
124
+ // Wrapping must be replicated *exactly*, including when a long word gets
125
+ // onto the next line, with whitespace at the end of the line before (#7).
126
+ // The *only* reliable way to do that is to copy the *entire* rest of the
127
+ // textarea's content into the <span> created at the caret position.
128
+ // For inputs, just '.' would be enough, but no need to bother.
129
+ span.textContent = element.value.substring(position) || '.'; // || because a completely empty faux span doesn't render at all
130
+ div.appendChild(span);
131
+
132
+ var coordinates = {
133
+ top: span.offsetTop + parseInt(computed['borderTopWidth']),
134
+ left: span.offsetLeft + parseInt(computed['borderLeftWidth']),
135
+ height: parseInt(computed['lineHeight'])
136
+ };
137
+
138
+ if (debug) {
139
+ span.style.backgroundColor = '#aaa';
140
+ } else {
141
+ document.body.removeChild(div);
142
+ }
143
+
144
+ return coordinates;
145
+ }
tagcomplete/javascript/_result.js ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Result data type for cleaner use of optional completion result properties
2
+
3
+ // Type enum
4
+ const ResultType = Object.freeze({
5
+ "tag": 1,
6
+ "extra": 2,
7
+ "embedding": 3,
8
+ "wildcardTag": 4,
9
+ "wildcardFile": 5,
10
+ "yamlWildcard": 6,
11
+ "umiWildcard": 7,
12
+ "hypernetwork": 8,
13
+ "lora": 9,
14
+ "lyco": 10,
15
+ "chant": 11,
16
+ "styleName": 12
17
+ });
18
+
19
+ // Class to hold result data and annotations to make it clearer to use
20
+ class AutocompleteResult {
21
+ // Main properties
22
+ text = "";
23
+ type = ResultType.tag;
24
+
25
+ // Additional info, only used in some cases
26
+ category = null;
27
+ count = null;
28
+ aliases = null;
29
+ meta = null;
30
+ hash = null;
31
+ sortKey = null;
32
+
33
+ // Constructor
34
+ constructor(text, type) {
35
+ this.text = text;
36
+ this.type = type;
37
+ }
38
+ }
tagcomplete/javascript/_textAreas.js ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Utility functions to select text areas the script should work on,
2
+ // including third party options.
3
+ // Supported third party options so far:
4
+ // - Dataset Tag Editor
5
+
6
+ // Core text area selectors
7
+ const core = [
8
+ "#txt2img_prompt > label > textarea",
9
+ "#img2img_prompt > label > textarea",
10
+ "#txt2img_qua_prompt > label > textarea",
11
+ "#txt2img_neg_prompt > label > textarea",
12
+ "#img2img_neg_prompt > label > textarea",
13
+ ".prompt > label > textarea",
14
+ "#txt2img_edit_style_prompt > label > textarea",
15
+ "#txt2img_edit_style_neg_prompt > label > textarea",
16
+ "#img2img_edit_style_prompt > label > textarea",
17
+ "#img2img_edit_style_neg_prompt > label > textarea"
18
+ ];
19
+
20
+ // Third party text area selectors
21
+ const thirdParty = {
22
+ "dataset-tag-editor": {
23
+ "base": "#tab_dataset_tag_editor_interface",
24
+ "hasIds": false,
25
+ "selectors": [
26
+ "Caption of Selected Image",
27
+ "Interrogate Result",
28
+ "Edit Caption",
29
+ "Edit Tags"
30
+ ]
31
+ },
32
+ "image browser": {
33
+ "base": "#tab_image_browser",
34
+ "hasIds": false,
35
+ "selectors": [
36
+ "Filename keyword search",
37
+ "EXIF keyword search"
38
+ ]
39
+ },
40
+ "tab_tagger": {
41
+ "base": "#tab_tagger",
42
+ "hasIds": false,
43
+ "selectors": [
44
+ "Additional tags (split by comma)",
45
+ "Exclude tags (split by comma)"
46
+ ]
47
+ },
48
+ "tiled-diffusion-t2i": {
49
+ "base": "#txt2img_script_container",
50
+ "hasIds": true,
51
+ "onDemand": true,
52
+ "selectors": [
53
+ "[id^=MD-t2i][id$=prompt] textarea",
54
+ "[id^=MD-t2i][id$=prompt] input[type='text']"
55
+ ]
56
+ },
57
+ "tiled-diffusion-i2i": {
58
+ "base": "#img2img_script_container",
59
+ "hasIds": true,
60
+ "onDemand": true,
61
+ "selectors": [
62
+ "[id^=MD-i2i][id$=prompt] textarea",
63
+ "[id^=MD-i2i][id$=prompt] input[type='text']"
64
+ ]
65
+ },
66
+ "adetailer-t2i": {
67
+ "base": "#txt2img_script_container",
68
+ "hasIds": true,
69
+ "onDemand": true,
70
+ "selectors": [
71
+ "[id^=script_txt2img_adetailer_ad_prompt] textarea",
72
+ "[id^=script_txt2img_adetailer_ad_negative_prompt] textarea"
73
+ ]
74
+ },
75
+ "adetailer-i2i": {
76
+ "base": "#img2img_script_container",
77
+ "hasIds": true,
78
+ "onDemand": true,
79
+ "selectors": [
80
+ "[id^=script_img2img_adetailer_ad_prompt] textarea",
81
+ "[id^=script_img2img_adetailer_ad_negative_prompt] textarea"
82
+ ]
83
+ },
84
+ "deepdanbooru-object-recognition": {
85
+ "base": "#tab_deepdanboru_object_recg_tab",
86
+ "hasIds": false,
87
+ "selectors": [
88
+ "Found tags",
89
+ ]
90
+ }
91
+ }
92
+
93
+ function getTextAreas() {
94
+ // First get all core text areas
95
+ let textAreas = [...gradioApp().querySelectorAll(core.join(", "))];
96
+
97
+ for (const [key, entry] of Object.entries(thirdParty)) {
98
+ if (entry.hasIds) { // If the entry has proper ids, we can just select them
99
+ textAreas = textAreas.concat([...gradioApp().querySelectorAll(entry.selectors.join(", "))]);
100
+ } else { // Otherwise, we have to find the text areas by their adjacent labels
101
+ let base = gradioApp().querySelector(entry.base);
102
+
103
+ // Safety check
104
+ if (!base) continue;
105
+
106
+ let allTextAreas = [...base.querySelectorAll("textarea, input[type='text']")];
107
+
108
+ // Filter the text areas where the adjacent label matches one of the selectors
109
+ let matchingTextAreas = allTextAreas.filter(ta => [...ta.parentElement.childNodes].some(x => entry.selectors.includes(x.innerText)));
110
+ textAreas = textAreas.concat(matchingTextAreas);
111
+ }
112
+ };
113
+
114
+ return textAreas;
115
+ }
116
+
117
+ function addOnDemandObservers(setupFunction) {
118
+ for (const [key, entry] of Object.entries(thirdParty)) {
119
+ if (!entry.onDemand) continue;
120
+
121
+ let base = gradioApp().querySelector(entry.base);
122
+ if (!base) continue;
123
+
124
+ let accordions = [...base?.querySelectorAll(".gradio-accordion")];
125
+ if (!accordions) continue;
126
+
127
+ accordions.forEach(acc => {
128
+ let accObserver = new MutationObserver((mutationList, observer) => {
129
+ for (const mutation of mutationList) {
130
+ if (mutation.type === "childList") {
131
+ let newChildren = mutation.addedNodes;
132
+ if (!newChildren) {
133
+ accObserver.disconnect();
134
+ continue;
135
+ }
136
+
137
+ newChildren.forEach(child => {
138
+ if (child.classList.contains("gradio-accordion") || child.querySelector(".gradio-accordion")) {
139
+ let newAccordions = [...child.querySelectorAll(".gradio-accordion")];
140
+ newAccordions.forEach(nAcc => accObserver.observe(nAcc, { childList: true }));
141
+ }
142
+ });
143
+
144
+ if (entry.hasIds) { // If the entry has proper ids, we can just select them
145
+ [...gradioApp().querySelectorAll(entry.selectors.join(", "))].forEach(x => setupFunction(x));
146
+ } else { // Otherwise, we have to find the text areas by their adjacent labels
147
+ let base = gradioApp().querySelector(entry.base);
148
+
149
+ // Safety check
150
+ if (!base) continue;
151
+
152
+ let allTextAreas = [...base.querySelectorAll("textarea, input[type='text']")];
153
+
154
+ // Filter the text areas where the adjacent label matches one of the selectors
155
+ let matchingTextAreas = allTextAreas.filter(ta => [...ta.parentElement.childNodes].some(x => entry.selectors.includes(x.innerText)));
156
+ matchingTextAreas.forEach(x => setupFunction(x));
157
+ }
158
+ }
159
+ }
160
+ });
161
+ accObserver.observe(acc, { childList: true });
162
+ });
163
+ };
164
+ }
165
+
166
+ const thirdPartyIdSet = new Set();
167
+ // Get the identifier for the text area to differentiate between positive and negative
168
+ function getTextAreaIdentifier(textArea) {
169
+ let txt2img_p = gradioApp().querySelector('#txt2img_prompt > label > textarea');
170
+ let txt2img_n = gradioApp().querySelector('#txt2img_neg_prompt > label > textarea');
171
+ let img2img_p = gradioApp().querySelector('#img2img_prompt > label > textarea');
172
+ let img2img_n = gradioApp().querySelector('#img2img_neg_prompt > label > textarea');
173
+
174
+ let modifier = "";
175
+ switch (textArea) {
176
+ case txt2img_p:
177
+ modifier = ".txt2img.p";
178
+ break;
179
+ case txt2img_n:
180
+ modifier = ".txt2img.n";
181
+ break;
182
+ case img2img_p:
183
+ modifier = ".img2img.p";
184
+ break;
185
+ case img2img_n:
186
+ modifier = ".img2img.n";
187
+ break;
188
+ default:
189
+ // If the text area is not a core text area, it must be a third party text area
190
+ // Add it to the set of third party text areas and get its index as a unique identifier
191
+ if (!thirdPartyIdSet.has(textArea))
192
+ thirdPartyIdSet.add(textArea);
193
+
194
+ modifier = `.thirdParty.ta${[...thirdPartyIdSet].indexOf(textArea)}`;
195
+ break;
196
+ }
197
+ return modifier;
198
+ }
tagcomplete/javascript/_utils.js ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Utility functions for tag autocomplete
2
+
3
+ // Parse the CSV file into a 2D array. Doesn't use regex, so it is very lightweight.
4
+ // We are ignoring newlines in quote fields since we expect one-line entries and parsing would break for unclosed quotes otherwise
5
+ function parseCSV(str) {
6
+ const arr = [];
7
+ let quote = false; // 'true' means we're inside a quoted field
8
+
9
+ // Iterate over each character, keep track of current row and column (of the returned array)
10
+ for (let row = 0, col = 0, c = 0; c < str.length; c++) {
11
+ let cc = str[c], nc = str[c+1]; // Current character, next character
12
+ arr[row] = arr[row] || []; // Create a new row if necessary
13
+ arr[row][col] = arr[row][col] || ''; // Create a new column (start with empty string) if necessary
14
+
15
+ // If the current character is a quotation mark, and we're inside a
16
+ // quoted field, and the next character is also a quotation mark,
17
+ // add a quotation mark to the current column and skip the next character
18
+ if (cc == '"' && quote && nc == '"') { arr[row][col] += cc; ++c; continue; }
19
+
20
+ // If it's just one quotation mark, begin/end quoted field
21
+ if (cc == '"') { quote = !quote; continue; }
22
+
23
+ // If it's a comma and we're not in a quoted field, move on to the next column
24
+ if (cc == ',' && !quote) { ++col; continue; }
25
+
26
+ // If it's a newline (CRLF), skip the next character and move on to the next row and move to column 0 of that new row
27
+ if (cc == '\r' && nc == '\n') { ++row; col = 0; ++c; quote = false; continue; }
28
+
29
+ // If it's a newline (LF or CR) move on to the next row and move to column 0 of that new row
30
+ if (cc == '\n') { ++row; col = 0; quote = false; continue; }
31
+ if (cc == '\r') { ++row; col = 0; quote = false; continue; }
32
+
33
+ // Otherwise, append the current character to the current column
34
+ arr[row][col] += cc;
35
+ }
36
+ return arr;
37
+ }
38
+
39
+ // Load file
40
+ async function readFile(filePath, json = false, cache = false) {
41
+ if (!cache)
42
+ filePath += `?${new Date().getTime()}`;
43
+
44
+ let response = await fetch(`file=${filePath}`);
45
+
46
+ if (response.status != 200) {
47
+ console.error(`Error loading file "${filePath}": ` + response.status, response.statusText);
48
+ return null;
49
+ }
50
+
51
+ if (json)
52
+ return await response.json();
53
+ else
54
+ return await response.text();
55
+ }
56
+
57
+ // Load CSV
58
+ async function loadCSV(path) {
59
+ let text = await readFile(path);
60
+ return parseCSV(text);
61
+ }
62
+
63
+ // Fetch API
64
+ async function fetchAPI(url, json = true, cache = false) {
65
+ if (!cache) {
66
+ const appendChar = url.includes("?") ? "&" : "?";
67
+ url += `${appendChar}${new Date().getTime()}`
68
+ }
69
+
70
+ let response = await fetch(url);
71
+
72
+ if (response.status != 200) {
73
+ console.error(`Error fetching API endpoint "${url}": ` + response.status, response.statusText);
74
+ return null;
75
+ }
76
+
77
+ if (json)
78
+ return await response.json();
79
+ else
80
+ return await response.text();
81
+ }
82
+
83
+ // Extra network preview thumbnails
84
+ async function getExtraNetworkPreviewURL(filename, type) {
85
+ const previewJSON = await fetchAPI(`tacapi/v1/thumb-preview/${filename}?type=${type}`, true, true);
86
+ if (previewJSON?.url) {
87
+ const properURL = `sd_extra_networks/thumb?filename=${previewJSON.url}`;
88
+ if ((await fetch(properURL)).status == 200) {
89
+ return properURL;
90
+ } else {
91
+ // create blob url
92
+ const blob = await (await fetch(`tacapi/v1/thumb-preview-blob/${filename}?type=${type}`)).blob();
93
+ return URL.createObjectURL(blob);
94
+ }
95
+ } else {
96
+ return null;
97
+ }
98
+ }
99
+
100
+ // Debounce function to prevent spamming the autocomplete function
101
+ var dbTimeOut;
102
+ const debounce = (func, wait = 300) => {
103
+ return function (...args) {
104
+ if (dbTimeOut) {
105
+ clearTimeout(dbTimeOut);
106
+ }
107
+
108
+ dbTimeOut = setTimeout(() => {
109
+ func.apply(this, args);
110
+ }, wait);
111
+ }
112
+ }
113
+
114
+ // Difference function to fix duplicates not being seen as changes in normal filter
115
+ function difference(a, b) {
116
+ if (a.length == 0) {
117
+ return b;
118
+ }
119
+ if (b.length == 0) {
120
+ return a;
121
+ }
122
+
123
+ return [...b.reduce((acc, v) => acc.set(v, (acc.get(v) || 0) - 1),
124
+ a.reduce((acc, v) => acc.set(v, (acc.get(v) || 0) + 1), new Map())
125
+ )].reduce((acc, [v, count]) => acc.concat(Array(Math.abs(count)).fill(v)), []);
126
+ }
127
+
128
+ // Sliding window function to get possible combination groups of an array
129
+ function toNgrams(inputArray, size) {
130
+ return Array.from(
131
+ { length: inputArray.length - (size - 1) }, //get the appropriate length
132
+ (_, index) => inputArray.slice(index, index + size) //create the windows
133
+ );
134
+ }
135
+
136
+ function escapeRegExp(string, wildcardMatching = false) {
137
+ if (wildcardMatching) {
138
+ // Escape all characters except asterisks and ?, which should be treated separately as placeholders.
139
+ return string.replace(/[-[\]{}()+.,\\^$|#\s]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.');
140
+ }
141
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
142
+ }
143
+ function escapeHTML(unsafeText) {
144
+ let div = document.createElement('div');
145
+ div.textContent = unsafeText;
146
+ return div.innerHTML;
147
+ }
148
+
149
+ // Sort functions
150
+ function getSortFunction() {
151
+ let criterion = TAC_CFG.modelSortOrder || "Name";
152
+
153
+ const textSort = (a, b, reverse = false) => {
154
+ const textHolderA = a.type === ResultType.chant ? a.aliases : a.text;
155
+ const textHolderB = b.type === ResultType.chant ? b.aliases : b.text;
156
+
157
+ const aKey = a.sortKey || textHolderA;
158
+ const bKey = b.sortKey || textHolderB;
159
+ return reverse ? bKey.localeCompare(aKey) : aKey.localeCompare(bKey);
160
+ }
161
+ const numericSort = (a, b, reverse = false) => {
162
+ const noKey = reverse ? "-1" : Number.MAX_SAFE_INTEGER;
163
+ let aParsed = parseFloat(a.sortKey || noKey);
164
+ let bParsed = parseFloat(b.sortKey || noKey);
165
+
166
+ if (aParsed === bParsed) {
167
+ return textSort(a, b, false);
168
+ }
169
+
170
+ return reverse ? bParsed - aParsed : aParsed - bParsed;
171
+ }
172
+
173
+ return (a, b) => {
174
+ switch (criterion) {
175
+ case "Date Modified (newest first)":
176
+ return numericSort(a, b, true);
177
+ case "Date Modified (oldest first)":
178
+ return numericSort(a, b, false);
179
+ default:
180
+ return textSort(a, b);
181
+ }
182
+ }
183
+ }
184
+
185
+ // Queue calling function to process global queues
186
+ async function processQueue(queue, context, ...args) {
187
+ for (let i = 0; i < queue.length; i++) {
188
+ await queue[i].call(context, ...args);
189
+ }
190
+ }
191
+ // The same but with return values
192
+ async function processQueueReturn(queue, context, ...args)
193
+ {
194
+ let qeueueReturns = [];
195
+ for (let i = 0; i < queue.length; i++) {
196
+ let returnValue = await queue[i].call(context, ...args);
197
+ if (returnValue)
198
+ qeueueReturns.push(returnValue);
199
+ }
200
+ return qeueueReturns;
201
+ }
202
+ // Specific to tag completion parsers
203
+ async function processParsers(textArea, prompt) {
204
+ // Get all parsers that have a successful trigger condition
205
+ let matchingParsers = PARSERS.filter(parser => parser.triggerCondition());
206
+ // Guard condition
207
+ if (matchingParsers.length === 0) {
208
+ return null;
209
+ }
210
+
211
+ let parseFunctions = matchingParsers.map(parser => parser.parse);
212
+ // Process them and return the results
213
+ return await processQueueReturn(parseFunctions, null, textArea, prompt);
214
+ }
tagcomplete/javascript/ext_chants.js ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const CHANT_REGEX = /<(?!e:|h:|l:)[^,> ]*>?/g;
2
+ const CHANT_TRIGGER = () => TAC_CFG.chantFile && TAC_CFG.chantFile !== "None" && tagword.match(CHANT_REGEX);
3
+
4
+ class ChantParser extends BaseTagParser {
5
+ parse() {
6
+ // Show Chant
7
+ let tempResults = [];
8
+ if (tagword !== "<" && tagword !== "<c:") {
9
+ let searchTerm = tagword.replace("<chant:", "").replace("<c:", "").replace("<", "");
10
+ let filterCondition = x => {
11
+ let regex = new RegExp(escapeRegExp(searchTerm, true), 'i');
12
+ return regex.test(x.terms.toLowerCase()) || regex.test(x.name.toLowerCase());
13
+ };
14
+ tempResults = chants.filter(x => filterCondition(x)); // Filter by tagword
15
+ } else {
16
+ tempResults = chants;
17
+ }
18
+
19
+ // Add final results
20
+ let finalResults = [];
21
+ tempResults.forEach(t => {
22
+ let result = new AutocompleteResult(t.content.trim(), ResultType.chant)
23
+ result.meta = "Chant";
24
+ result.aliases = t.name;
25
+ result.category = t.color;
26
+ finalResults.push(result);
27
+ });
28
+
29
+ return finalResults;
30
+ }
31
+ }
32
+
33
+ async function load() {
34
+ if (TAC_CFG.chantFile && TAC_CFG.chantFile !== "None") {
35
+ try {
36
+ chants = await readFile(`${tagBasePath}/${TAC_CFG.chantFile}?`, true);
37
+ } catch (e) {
38
+ console.error("Error loading chants.json: " + e);
39
+ }
40
+ } else {
41
+ chants = [];
42
+ }
43
+ }
44
+
45
+ function sanitize(tagType, text) {
46
+ if (tagType === ResultType.chant) {
47
+ return text;
48
+ }
49
+ return null;
50
+ }
51
+
52
+ PARSERS.push(new ChantParser(CHANT_TRIGGER));
53
+
54
+ // Add our utility functions to their respective queues
55
+ QUEUE_FILE_LOAD.push(load);
56
+ QUEUE_SANITIZE.push(sanitize);
57
+ QUEUE_AFTER_CONFIG_CHANGE.push(load);
tagcomplete/javascript/tagAutocomplete.js ADDED
@@ -0,0 +1,1085 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const styleColors = {
2
+ "--results-neutral-text": ["#e0e0e0","black"],
3
+ "--results-bg": ["#0b0f19", "#ffffff"],
4
+ "--results-border-color": ["#4b5563", "#e5e7eb"],
5
+ "--results-border-width": ["1px", "1.5px"],
6
+ "--results-bg-odd": ["#111827", "#f9fafb"],
7
+ "--results-hover": ["#1f2937", "#f5f6f8"],
8
+ "--results-selected": ["#374151", "#e5e7eb"],
9
+ "--meta-text-color": ["#6b6f7b", "#a2a9b4"],
10
+ "--embedding-v1-color": ["lightsteelblue", "#2b5797"],
11
+ "--embedding-v2-color": ["skyblue", "#2d89ef"],
12
+ "--live-translation-rt": ["whitesmoke", "#222"],
13
+ "--live-translation-color-1": ["lightskyblue", "#2d89ef"],
14
+ "--live-translation-color-2": ["palegoldenrod", "#eb5700"],
15
+ "--live-translation-color-3": ["darkseagreen", "darkgreen"],
16
+ }
17
+ const browserVars = {
18
+ "--results-overflow-y": {
19
+ "firefox": "scroll",
20
+ "other": "auto"
21
+ }
22
+ }
23
+ // Style for new elements. Gets appended to the Gradio root.
24
+ const autocompleteCSS = `
25
+ #quicksettings [id^=setting_tac] {
26
+ background-color: transparent;
27
+ min-width: fit-content;
28
+ }
29
+ .autocompleteParent {
30
+ display: flex;
31
+ position: absolute;
32
+ z-index: 9999;
33
+ max-width: calc(100% - 1.5rem);
34
+ margin: 5px 0 0 0;
35
+ }
36
+ .autocompleteResults {
37
+ background-color: var(--results-bg) !important;
38
+ border: var(--results-border-width) solid var(--results-border-color) !important;
39
+ color: var(--results-neutral-text) !important;
40
+ border-radius: 12px !important;
41
+ height: fit-content;
42
+ flex-basis: fit-content;
43
+ flex-shrink: 0;
44
+ overflow-y: var(--results-overflow-y);
45
+ overflow-x: hidden;
46
+ word-break: break-word;
47
+ }
48
+ .sideInfo {
49
+ display: none;
50
+ position: relative;
51
+ margin-left: 10px;
52
+ height: 18rem;
53
+ max-width: 16rem;
54
+ }
55
+ .sideInfo > img {
56
+ object-fit: cover;
57
+ height: 100%;
58
+ width: 100%;
59
+ }
60
+ .autocompleteResultsList > li:nth-child(odd) {
61
+ background-color: var(--results-bg-odd);
62
+ }
63
+ .autocompleteResultsList > li {
64
+ list-style-type: none;
65
+ padding: 10px;
66
+ cursor: pointer;
67
+ }
68
+ .autocompleteResultsList > li:hover {
69
+ background-color: var(--results-hover);
70
+ }
71
+ .autocompleteResultsList > li.selected {
72
+ background-color: var(--results-selected);
73
+ }
74
+ .resultsFlexContainer {
75
+ display: flex;
76
+ }
77
+ .acListItem {
78
+ white-space: break-spaces;
79
+ min-width: 100px;
80
+ }
81
+ .acMetaText {
82
+ position: relative;
83
+ flex-grow: 1;
84
+ text-align: end;
85
+ padding: 0 0 0 15px;
86
+ white-space: nowrap;
87
+ color: var(--meta-text-color);
88
+ }
89
+ .acWikiLink {
90
+ padding: 0.5rem;
91
+ margin: -0.5rem 0 -0.5rem -0.5rem;
92
+ }
93
+ .acWikiLink:hover {
94
+ text-decoration: underline;
95
+ }
96
+ .acListItem.acEmbeddingV1 {
97
+ color: var(--embedding-v1-color);
98
+ }
99
+ .acListItem.acEmbeddingV2 {
100
+ color: var(--embedding-v2-color);
101
+ }
102
+ .acListItem .acPathPart:nth-child(3n+1) {
103
+ color: var(--live-translation-color-1);
104
+ }
105
+ .acListItem .acPathPart:nth-child(3n+2) {
106
+ color: var(--live-translation-color-2);
107
+ }
108
+ .acListItem .acPathPart:nth-child(3n+3) {
109
+ color: var(--live-translation-color-3);
110
+ }
111
+ `;
112
+
113
+ function gradioApp() {
114
+ const elems = document.getElementsByTagName('gradio-app');
115
+ const elem = elems.length == 0 ? document : elems[0];
116
+
117
+ if (elem !== document) {
118
+ elem.getElementById = function(id) {
119
+ return document.getElementById(id);
120
+ };
121
+ }
122
+ return elem.shadowRoot ? elem.shadowRoot : elem;
123
+ }
124
+
125
+ function updateInput(target) {
126
+ let e = new Event("input", {bubbles: true});
127
+ Object.defineProperty(e, "target", {value: target});
128
+ target.dispatchEvent(e);
129
+ }
130
+
131
+ async function loadTags(c) {
132
+ // Load main tags and aliases
133
+ if (allTags.length === 0 && c.tagFile && c.tagFile !== "None") {
134
+ try {
135
+ allTags = await loadCSV(`${tagBasePath}/${c.tagFile}`);
136
+ } catch (e) {
137
+ console.error("Error loading tags file: " + e);
138
+ return;
139
+ }
140
+ }
141
+ await loadExtraTags(c);
142
+ }
143
+
144
+ async function loadExtraTags(c) {
145
+ if (c.extra.extraFile && c.extra.extraFile !== "None") {
146
+ try {
147
+ extras = await loadCSV(`${tagBasePath}/${c.extra.extraFile}`);
148
+ // Add translations to the main translation map for extra tags that have them
149
+ extras.forEach(e => {
150
+ if (e[4]) translations.set(e[0], e[4]);
151
+ });
152
+ } catch (e) {
153
+ console.error("Error loading extra file: " + e);
154
+ return;
155
+ }
156
+ }
157
+ }
158
+
159
+ // Create the result list div and necessary styling
160
+ function createResultsDiv(textArea) {
161
+ let parentDiv = document.createElement("div");
162
+ let resultsDiv = document.createElement("div");
163
+ let resultsList = document.createElement("ul");
164
+ let sideDiv = document.createElement("div");
165
+ let sideDivImg = document.createElement("img");
166
+
167
+ let textAreaId = getTextAreaIdentifier(textArea);
168
+ let typeClass = textAreaId.replaceAll(".", " ");
169
+
170
+ parentDiv.setAttribute("class", `autocompleteParent${typeClass}`);
171
+
172
+ resultsDiv.style.maxHeight = `${TAC_CFG.maxResults * 50}px`;
173
+ resultsDiv.setAttribute("class", `autocompleteResults${typeClass} notranslate`);
174
+ resultsDiv.setAttribute("translate", "no");
175
+ resultsList.setAttribute("class", "autocompleteResultsList");
176
+ resultsDiv.appendChild(resultsList);
177
+
178
+ sideDiv.setAttribute("class", `autocompleteResults${typeClass} sideInfo`);
179
+ sideDiv.appendChild(sideDivImg);
180
+
181
+ parentDiv.appendChild(resultsDiv);
182
+ parentDiv.appendChild(sideDiv);
183
+
184
+ return parentDiv;
185
+ }
186
+
187
+ // Show or hide the results div
188
+ function isVisible(textArea) {
189
+ let textAreaId = getTextAreaIdentifier(textArea);
190
+ let parentDiv = gradioApp().querySelector('.autocompleteParent' + textAreaId);
191
+ return parentDiv.style.display === "flex";
192
+ }
193
+
194
+ function showResults(textArea) {
195
+ let textAreaId = getTextAreaIdentifier(textArea);
196
+ let parentDiv = gradioApp().querySelector('.autocompleteParent' + textAreaId);
197
+ parentDiv.style.display = "flex";
198
+
199
+ if (TAC_CFG.slidingPopup) {
200
+ let caretPosition = getCaretCoordinates(textArea, textArea.selectionEnd).left;
201
+ let offset = Math.min(textArea.offsetLeft - textArea.scrollLeft + caretPosition, textArea.offsetWidth - parentDiv.offsetWidth);
202
+
203
+ parentDiv.style.left = `${offset}px`;
204
+ } else {
205
+ if (parentDiv.style.left)
206
+ parentDiv.style.removeProperty("left");
207
+ }
208
+ // Reset here too to make absolutely sure the browser registers it
209
+ parentDiv.scrollTop = 0;
210
+
211
+ // Ensure preview is hidden
212
+ let previewDiv = gradioApp().querySelector(`.autocompleteParent${textAreaId} .sideInfo`);
213
+ previewDiv.style.display = "none";
214
+ }
215
+
216
+ function hideResults(textArea) {
217
+ let textAreaId = getTextAreaIdentifier(textArea);
218
+ let resultsDiv = gradioApp().querySelector('.autocompleteParent' + textAreaId);
219
+
220
+ if (!resultsDiv) return;
221
+
222
+ resultsDiv.style.display = "none";
223
+ selectedTag = null;
224
+ }
225
+
226
+ const WEIGHT_REGEX = /[([]([^()[\]:|]+)(?::(?:\d+(?:\.\d+)?|\.\d+))?[)\]]/g;
227
+ const POINTY_REGEX = /<[^\s,<](?:[^\t\n\r,<>]*>|[^\t\n\r,> ]*)/g;
228
+ const COMPLETED_WILDCARD_REGEX = /__[^\s,_][^\t\n\r,_]*[^\s,_]__[^\s,_]*/g;
229
+ const STYLE_VAR_REGEX = /\$\(?[^$|\[\],\s]*\)?/g;
230
+ const NORMAL_TAG_REGEX = /[^\s,|<>\[\]:]+_\([^\s,|<>\[\]:]*\)?|[^\s,|<>():\[\]]+|</g;
231
+ const TAG_REGEX = new RegExp(`${POINTY_REGEX.source}|${COMPLETED_WILDCARD_REGEX.source}|${STYLE_VAR_REGEX.source}|${NORMAL_TAG_REGEX.source}`, "g");
232
+
233
+ // On click, insert the tag into the prompt textbox with respect to the cursor position
234
+ async function insertTextAtCursor(textArea, result, tagword, tabCompletedWithoutChoice = false) {
235
+ let text = result.text;
236
+ let tagType = result.type;
237
+
238
+ let cursorPos = textArea.selectionStart;
239
+ var sanitizedText = text
240
+
241
+ // Run sanitize queue and use first result as sanitized text
242
+ sanitizeResults = await processQueueReturn(QUEUE_SANITIZE, null, tagType, text);
243
+
244
+ if (sanitizeResults && sanitizeResults.length > 0) {
245
+ sanitizedText = sanitizeResults[0];
246
+ } else {
247
+ sanitizedText = TAC_CFG.replaceUnderscores ? text.replaceAll("_", " ") : text;
248
+
249
+ if (TAC_CFG.escapeParentheses && tagType === ResultType.tag) {
250
+ sanitizedText = sanitizedText
251
+ .replaceAll("(", "\\(")
252
+ .replaceAll(")", "\\)")
253
+ .replaceAll("[", "\\[")
254
+ .replaceAll("]", "\\]");
255
+ }
256
+ }
257
+
258
+ if ((tagType === ResultType.wildcardFile || tagType === ResultType.yamlWildcard)
259
+ && tabCompletedWithoutChoice
260
+ && TAC_CFG.wildcardCompletionMode !== "Always fully"
261
+ && sanitizedText.includes("/")) {
262
+ if (TAC_CFG.wildcardCompletionMode === "To next folder level") {
263
+ let regexMatch = sanitizedText.match(new RegExp(`${escapeRegExp(tagword)}([^/]*\\/?)`, "i"));
264
+ if (regexMatch) {
265
+ let pathPart = regexMatch[0];
266
+ // In case the completion would have just added a slash, try again one level deeper
267
+ if (pathPart === `${tagword}/`) {
268
+ pathPart = sanitizedText.match(new RegExp(`${escapeRegExp(tagword)}\\/([^/]*\\/?)`, "i"))[0];
269
+ }
270
+ sanitizedText = pathPart;
271
+ }
272
+ } else if (TAC_CFG.wildcardCompletionMode === "To first difference") {
273
+ let firstDifference = 0;
274
+ let longestResult = results.map(x => x.text.length).reduce((a, b) => Math.max(a, b));
275
+ // Compare the results to each other to find the first point where they differ
276
+ for (let i = 0; i < longestResult; i++) {
277
+ let char = results[0].text[i];
278
+ if (results.every(x => x.text[i] === char)) {
279
+ firstDifference++;
280
+ } else {
281
+ break;
282
+ }
283
+ }
284
+ // Don't cut off the __ at the end if it is already the full path
285
+ if (firstDifference > 0 && firstDifference < longestResult) {
286
+ // +2 because the sanitized text already has the __ at the start but the matched text doesn't
287
+ sanitizedText = sanitizedText.substring(0, firstDifference + 2);
288
+ } else if (firstDifference === 0) {
289
+ sanitizedText = tagword;
290
+ }
291
+ }
292
+ }
293
+
294
+ var prompt = textArea.value;
295
+
296
+ // Edit prompt text
297
+ let editStart = Math.max(cursorPos - tagword.length, 0);
298
+ let editEnd = Math.min(cursorPos + tagword.length, prompt.length);
299
+ let surrounding = prompt.substring(editStart, editEnd);
300
+ let match = surrounding.match(new RegExp(escapeRegExp(`${tagword}`), "i"));
301
+ let afterInsertCursorPos = editStart + match.index + sanitizedText.length;
302
+
303
+ var optionalSeparator = "";
304
+ let extraNetworkTypes = [ResultType.hypernetwork, ResultType.lora];
305
+ let noCommaTypes = [ResultType.wildcardFile, ResultType.yamlWildcard, ResultType.umiWildcard].concat(extraNetworkTypes);
306
+ if (!noCommaTypes.includes(tagType)) {
307
+ // Append comma if enabled and not already present
308
+ let beforeComma = surrounding.match(new RegExp(`${escapeRegExp(tagword)}[,:]`, "i")) !== null;
309
+ if (TAC_CFG.appendComma)
310
+ optionalSeparator = beforeComma ? "" : ",";
311
+ // Add space if enabled
312
+ if (TAC_CFG.appendSpace && !beforeComma)
313
+ optionalSeparator += " ";
314
+ // If at end of prompt and enabled, override the normal setting if not already added
315
+ if (!TAC_CFG.appendSpace && TAC_CFG.alwaysSpaceAtEnd)
316
+ optionalSeparator += surrounding.match(new RegExp(`${escapeRegExp(tagword)}$`, "im")) !== null ? " " : "";
317
+ } else if (extraNetworkTypes.includes(tagType)) {
318
+ // Use the dedicated separator for extra networks if it's defined, otherwise fall back to space
319
+ optionalSeparator = TAC_CFG.extraNetworksSeparator || " ";
320
+ }
321
+
322
+ // Escape $ signs since they are special chars for the replace function
323
+ // We need four since we're also escaping them in replaceAll in the first place
324
+ sanitizedText = sanitizedText.replaceAll("$", "$$$$");
325
+
326
+ // Replace partial tag word with new text, add comma if needed
327
+ let insert = surrounding.replace(match, sanitizedText + optionalSeparator);
328
+
329
+ // Add back start
330
+ var newPrompt = prompt.substring(0, editStart) + insert + prompt.substring(editEnd);
331
+
332
+ // Add lora/lyco keywords if enabled and found
333
+ let keywordsLength = 0;
334
+
335
+ if (TAC_CFG.modelKeywordCompletion !== "Never" && (tagType === ResultType.lora || tagType === ResultType.lyco)) {
336
+ let keywords = null;
337
+ // Check built-in activation words first
338
+ if (tagType === ResultType.lora || tagType === ResultType.lyco) {
339
+ let info = await fetchAPI(`tacapi/v1/lora-info/${result.text}`)
340
+ if (info && info["activation text"]) {
341
+ keywords = info["activation text"];
342
+ }
343
+ }
344
+
345
+ if (!keywords && modelKeywordPath.length > 0 && result.hash && result.hash !== "NOFILE" && result.hash.length > 0) {
346
+ let nameDict = modelKeywordDict.get(result.hash);
347
+ let names = [result.text + ".safetensors", result.text + ".pt", result.text + ".ckpt"];
348
+
349
+ // No match, try to find a sha256 match from the cache file
350
+ if (!nameDict) {
351
+ const sha256 = await fetchAPI(`/tacapi/v1/lora-cached-hash/${result.text}`)
352
+ if (sha256) {
353
+ nameDict = modelKeywordDict.get(sha256);
354
+ }
355
+ }
356
+
357
+ if (nameDict) {
358
+ let found = false;
359
+ names.forEach(name => {
360
+ if (!found && nameDict.has(name)) {
361
+ found = true;
362
+ keywords = nameDict.get(name);
363
+ }
364
+ });
365
+
366
+ if (!found)
367
+ keywords = nameDict.get("none");
368
+ }
369
+ }
370
+
371
+ if (keywords && keywords.length > 0) {
372
+ textBeforeKeywordInsertion = newPrompt;
373
+
374
+ if (TAC_CFG.modelKeywordLocation === "Start of prompt")
375
+ newPrompt = `${keywords}, ${newPrompt}`; // Insert keywords
376
+ else if (TAC_CFG.modelKeywordLocation === "End of prompt")
377
+ newPrompt = `${newPrompt}, ${keywords}`; // Insert keywords
378
+ else {
379
+ let keywordStart = prompt[editStart - 1] === " " ? editStart - 1 : editStart;
380
+ newPrompt = prompt.substring(0, keywordStart) + `, ${keywords} ${insert}` + prompt.substring(editEnd);
381
+ }
382
+
383
+
384
+ textAfterKeywordInsertion = newPrompt;
385
+ keywordInsertionUndone = false;
386
+ setTimeout(() => lastEditWasKeywordInsertion = true, 200)
387
+
388
+ keywordsLength = keywords.length + 2; // +2 for the comma and space
389
+ }
390
+ }
391
+
392
+ // Insert into prompt textbox and reposition cursor
393
+ textArea.value = newPrompt;
394
+ textArea.selectionStart = afterInsertCursorPos + optionalSeparator.length + keywordsLength;
395
+ textArea.selectionEnd = textArea.selectionStart
396
+
397
+ // Set self trigger flag to show wildcard contents after the filename was inserted
398
+ if ([ResultType.wildcardFile, ResultType.yamlWildcard, ResultType.umiWildcard].includes(result.type))
399
+ tacSelfTrigger = true;
400
+ // Since we've modified a Gradio Textbox component manually, we need to simulate an `input` DOM event to ensure it's propagated back to python.
401
+ // Uses a built-in method from the webui's ui.js which also already accounts for event target
402
+ updateInput(textArea);
403
+
404
+ // Update previous tags with the edited prompt to prevent re-searching the same term
405
+ let weightedTags = [...newPrompt.matchAll(WEIGHT_REGEX)]
406
+ .map(match => match[1]);
407
+ let tags = newPrompt.match(TAG_REGEX)
408
+ if (weightedTags !== null) {
409
+ tags = tags.filter(tag => !weightedTags.some(weighted => tag.includes(weighted)))
410
+ .concat(weightedTags);
411
+ }
412
+ previousTags = tags;
413
+
414
+ // Callback
415
+ let returns = await processQueueReturn(QUEUE_AFTER_INSERT, null, tagType, sanitizedText, newPrompt, textArea);
416
+ // Return if any queue function returned true (has handled hide/show already)
417
+ if (returns.some(x => x === true))
418
+ return;
419
+
420
+ // Hide results after inserting, if it hasn't been hidden already by a queue function
421
+ if (!hideBlocked && isVisible(textArea)) {
422
+ hideResults(textArea);
423
+ }
424
+ }
425
+
426
+ function addResultsToList(textArea, results, tagword, resetList) {
427
+ let textAreaId = getTextAreaIdentifier(textArea);
428
+ let resultDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
429
+ let resultsList = resultDiv.querySelector('ul');
430
+
431
+ // Reset list, selection and scrollTop since the list changed
432
+ if (resetList) {
433
+ resultsList.innerHTML = "";
434
+ selectedTag = null;
435
+ oldSelectedTag = null;
436
+ resultDiv.scrollTop = 0;
437
+ resultCount = 0;
438
+ }
439
+
440
+ // Find right colors from config
441
+ let tagFileName = TAC_CFG.tagFile.split(".")[0];
442
+ let tagColors = TAC_CFG.colorMap;
443
+ let mode = (document.querySelector(".dark") || gradioApp().querySelector(".dark")) ? 0 : 1;
444
+ let nextLength = Math.min(results.length, resultCount + TAC_CFG.resultStepLength);
445
+
446
+ for (let i = resultCount; i < nextLength; i++) {
447
+ let result = results[i];
448
+
449
+ // Skip if the result is null or undefined
450
+ if (!result)
451
+ continue;
452
+
453
+ let li = document.createElement("li");
454
+
455
+ let flexDiv = document.createElement("div");
456
+ flexDiv.classList.add("resultsFlexContainer");
457
+ li.appendChild(flexDiv);
458
+
459
+ let itemText = document.createElement("div");
460
+ itemText.classList.add("acListItem");
461
+
462
+ let displayText = "";
463
+ // If the tag matches the tagword, we don't need to display the alias
464
+ if(result.type === ResultType.chant) {
465
+ displayText = escapeHTML(result.aliases);
466
+ } else if (result.aliases && !result.text.includes(tagword)) { // Alias
467
+ let splitAliases = result.aliases.split(",");
468
+ let bestAlias = splitAliases.find(a => a.toLowerCase().includes(tagword));
469
+
470
+ // search in translations if no alias matches
471
+ if (!bestAlias) {
472
+ let tagOrAlias = pair => pair[0] === result.text || splitAliases.includes(pair[0]);
473
+ var tArray = [...translations];
474
+ if (tArray) {
475
+ var translationKey = [...translations].find(pair => tagOrAlias(pair) && pair[1].includes(tagword));
476
+ if (translationKey)
477
+ bestAlias = translationKey[0];
478
+ }
479
+ }
480
+
481
+ displayText = escapeHTML(bestAlias);
482
+
483
+ // Append translation for alias if it exists and is not what the user typed
484
+ if (translations.has(bestAlias) && translations.get(bestAlias) !== bestAlias && bestAlias !== result.text)
485
+ displayText += `[${translations.get(bestAlias)}]`;
486
+
487
+ if (!TAC_CFG.alias.onlyShowAlias && result.text !== bestAlias)
488
+ displayText += " ➝ " + result.text;
489
+ } else { // No alias
490
+ displayText = escapeHTML(result.text);
491
+ }
492
+
493
+ // Append translation for result if it exists
494
+ if (translations.has(result.text))
495
+ displayText += `[${translations.get(result.text)}]`;
496
+
497
+ // Print search term bolded in result
498
+ itemText.innerHTML = displayText.replace(tagword, `<b>${tagword}</b>`);
499
+
500
+ const splitTypes = [ResultType.wildcardFile, ResultType.yamlWildcard]
501
+ if (splitTypes.includes(result.type) && itemText.innerHTML.includes("/")) {
502
+ let parts = itemText.innerHTML.split("/");
503
+ let lastPart = parts[parts.length - 1];
504
+ parts = parts.slice(0, parts.length - 1);
505
+
506
+ itemText.innerHTML = "<span class='acPathPart'>" + parts.join("</span><span class='acPathPart'>/") + "</span>" + "/" + lastPart;
507
+ }
508
+
509
+ // Add wiki link if the setting is enabled and a supported tag set loaded
510
+ if (TAC_CFG.showWikiLinks
511
+ && (result.type === ResultType.tag)
512
+ && (tagFileName.toLowerCase().startsWith("danbooru") || tagFileName.toLowerCase().startsWith("e621"))) {
513
+ let wikiLink = document.createElement("a");
514
+ wikiLink.classList.add("acWikiLink");
515
+ wikiLink.innerText = "?";
516
+
517
+ let linkPart = displayText;
518
+ // Only use alias result if it is one
519
+ if (displayText.includes("➝"))
520
+ linkPart = displayText.split(" ➝ ")[1];
521
+
522
+ // Remove any trailing translations
523
+ if (linkPart.includes("[")) {
524
+ linkPart = linkPart.split("[")[0]
525
+ }
526
+
527
+ linkPart = encodeURIComponent(linkPart);
528
+
529
+ // Set link based on selected file
530
+ let tagFileNameLower = tagFileName.toLowerCase();
531
+ if (tagFileNameLower.startsWith("danbooru")) {
532
+ wikiLink.href = `https://danbooru.donmai.us/wiki_pages/${linkPart}`;
533
+ } else if (tagFileNameLower.startsWith("e621")) {
534
+ wikiLink.href = `https://e621.net/wiki_pages/${linkPart}`;
535
+ }
536
+
537
+ wikiLink.target = "_blank";
538
+ flexDiv.appendChild(wikiLink);
539
+ }
540
+
541
+ flexDiv.appendChild(itemText);
542
+
543
+ // Add post count & color if it's a tag
544
+ // Wildcards & Embeds have no tag category
545
+ if (result.category) {
546
+ // Set the color of the tag
547
+ let cat = result.category;
548
+ let colorGroup = tagColors[tagFileName];
549
+ // Default to danbooru scheme if no matching one is found
550
+ if (!colorGroup)
551
+ colorGroup = tagColors["danbooru"];
552
+
553
+ // Set tag type to invalid if not found
554
+ if (!colorGroup[cat])
555
+ cat = "-1";
556
+
557
+ flexDiv.style = `color: ${colorGroup[cat][mode]};`;
558
+ }
559
+
560
+ // Post count
561
+ if (result.count && !isNaN(result.count)) {
562
+ let postCount = result.count;
563
+ let formatter;
564
+
565
+ // Danbooru formats numbers with a padded fraction for 1M or 1k, but not for 10/100k
566
+ if (postCount >= 1000000 || (postCount >= 1000 && postCount < 10000))
567
+ formatter = Intl.NumberFormat("en", { notation: "compact", minimumFractionDigits: 1, maximumFractionDigits: 1 });
568
+ else
569
+ formatter = Intl.NumberFormat("en", {notation: "compact"});
570
+
571
+ let formattedCount = formatter.format(postCount);
572
+
573
+ let countDiv = document.createElement("div");
574
+ countDiv.textContent = formattedCount;
575
+ countDiv.classList.add("acMetaText");
576
+ flexDiv.appendChild(countDiv);
577
+ } else if (result.meta) { // Check if there is meta info to display
578
+ let metaDiv = document.createElement("div");
579
+ metaDiv.textContent = result.meta;
580
+ metaDiv.classList.add("acMetaText");
581
+
582
+ // Add version info classes if it is an embedding
583
+ if (result.type === ResultType.embedding) {
584
+ if (result.meta.startsWith("v1"))
585
+ itemText.classList.add("acEmbeddingV1");
586
+ else if (result.meta.startsWith("v2"))
587
+ itemText.classList.add("acEmbeddingV2");
588
+ }
589
+
590
+ flexDiv.appendChild(metaDiv);
591
+ }
592
+
593
+ // Add listener
594
+ li.addEventListener("click", function () { insertTextAtCursor(textArea, result, tagword); });
595
+ // Add element to list
596
+ resultsList.appendChild(li);
597
+ }
598
+ resultCount = nextLength;
599
+
600
+ if (resetList) {
601
+ selectedTag = null;
602
+ oldSelectedTag = null;
603
+ resultDiv.scrollTop = 0;
604
+ }
605
+ }
606
+
607
+ async function updateSelectionStyle(textArea, newIndex, oldIndex) {
608
+ let textAreaId = getTextAreaIdentifier(textArea);
609
+ let resultDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
610
+ let resultsList = resultDiv.querySelector('ul');
611
+ let items = resultsList.getElementsByTagName('li');
612
+
613
+ if (oldIndex != null) {
614
+ items[oldIndex].classList.remove('selected');
615
+ }
616
+
617
+ // make it safer
618
+ if (newIndex !== null) {
619
+ let selected = items[newIndex];
620
+ selected.classList.add('selected');
621
+
622
+ // Set scrolltop to selected item
623
+ resultDiv.scrollTop = selected.offsetTop - resultDiv.offsetTop;
624
+ }
625
+
626
+ // Show preview if enabled and the selected type supports it
627
+ if (newIndex !== null) {
628
+ let selected = items[newIndex];
629
+ let previewTypes = ["v1 Embedding", "v2 Embedding", "Hypernetwork", "Lora", "Lyco"];
630
+ let selectedType = selected.querySelector(".acMetaText").innerText;
631
+ let selectedFilename = selected.querySelector(".acListItem").innerText;
632
+
633
+ let previewDiv = gradioApp().querySelector(`.autocompleteParent${textAreaId} .sideInfo`);
634
+
635
+ if (TAC_CFG.showExtraNetworkPreviews && previewTypes.includes(selectedType)) {
636
+ let shorthandType = "";
637
+ switch (selectedType) {
638
+ case "v1 Embedding":
639
+ case "v2 Embedding":
640
+ shorthandType = "embed";
641
+ break;
642
+ case "Hypernetwork":
643
+ shorthandType = "hyper";
644
+ break;
645
+ case "Lora":
646
+ shorthandType = "lora";
647
+ break;
648
+ case "Lyco":
649
+ shorthandType = "lyco";
650
+ break;
651
+ }
652
+
653
+ let img = previewDiv.querySelector("img");
654
+
655
+ let url = await getExtraNetworkPreviewURL(selectedFilename, shorthandType);
656
+ if (url) {
657
+ img.src = url;
658
+ previewDiv.style.display = "block";
659
+ } else {
660
+ previewDiv.style.display = "none";
661
+ }
662
+ } else {
663
+ previewDiv.style.display = "none";
664
+ }
665
+ }
666
+ }
667
+
668
+ // Check if the last edit was the keyword insertion, and catch undo/redo in that case
669
+ function checkKeywordInsertionUndo(textArea, event) {
670
+ if (TAC_CFG.modelKeywordCompletion === "Never") return;
671
+
672
+ switch (event.inputType) {
673
+ case "historyUndo":
674
+ if (lastEditWasKeywordInsertion && !keywordInsertionUndone) {
675
+ keywordInsertionUndone = true;
676
+ textArea.value = textBeforeKeywordInsertion;
677
+ tacSelfTrigger = true;
678
+ updateInput(textArea);
679
+ }
680
+ break;
681
+ case "historyRedo":
682
+ if (lastEditWasKeywordInsertion && keywordInsertionUndone) {
683
+ keywordInsertionUndone = false;
684
+ textArea.value = textAfterKeywordInsertion;
685
+ tacSelfTrigger = true;
686
+ updateInput(textArea);
687
+ }
688
+ case undefined:
689
+ // undefined is caused by the updateInput event firing, so we just ignore it
690
+ break;
691
+ default:
692
+ // Everything else deactivates the keyword undo and returns to normal undo behavior
693
+ lastEditWasKeywordInsertion = false;
694
+ keywordInsertionUndone = false;
695
+ textBeforeKeywordInsertion = "";
696
+ textAfterKeywordInsertion = "";
697
+ break;
698
+ }
699
+ }
700
+
701
+ async function autocomplete(textArea, prompt, fixedTag = null) {
702
+ // Return if the function is deactivated in the UI
703
+
704
+ // Guard for empty prompt
705
+ if (prompt.length === 0) {
706
+ hideResults(textArea);
707
+ previousTags = [];
708
+ tagword = "";
709
+ return;
710
+ }
711
+
712
+ if (fixedTag === null) {
713
+ // Match tags with RegEx to get the last edited one
714
+ // We also match for the weighting format (e.g. "tag:1.0") here, and combine the two to get the full tag word set
715
+ let weightedTags = [...prompt.matchAll(WEIGHT_REGEX)]
716
+ .map(match => match[1]);
717
+ let tags = prompt.match(TAG_REGEX)
718
+ if (weightedTags !== null && tags !== null) {
719
+ tags = tags.filter(tag => !weightedTags.some(weighted => tag.includes(weighted) && !tag.startsWith("<[") && !tag.startsWith("$(")))
720
+ .concat(weightedTags);
721
+ }
722
+
723
+ // Guard for no tags
724
+ if (!tags || tags.length === 0) {
725
+ previousTags = [];
726
+ tagword = "";
727
+ hideResults(textArea);
728
+ return;
729
+ }
730
+
731
+ let tagCountChange = tags.length - previousTags.length;
732
+ let diff = difference(tags, previousTags);
733
+ previousTags = tags;
734
+
735
+ // Guard for no difference / only whitespace remaining / last edited tag was fully removed
736
+ if (diff === null || diff.length === 0 || (diff.length === 1 && tagCountChange < 0)) {
737
+ if (!hideBlocked) hideResults(textArea);
738
+ return;
739
+ }
740
+
741
+ tagword = diff[0]
742
+
743
+ // Guard for empty tagword
744
+ if (tagword === null || tagword.length === 0) {
745
+ hideResults(textArea);
746
+ return;
747
+ }
748
+ } else {
749
+ tagword = fixedTag;
750
+ }
751
+
752
+ results = [];
753
+ resultCountBeforeNormalTags = 0;
754
+ tagword = tagword.toLowerCase().replace(/[\n\r]/g, "");
755
+
756
+ // Process all parsers
757
+ let resultCandidates = (await processParsers(textArea, prompt))?.filter(x => x.length > 0);
758
+ // If one ore more result candidates match, use their results
759
+ if (resultCandidates && resultCandidates.length > 0) {
760
+ // Flatten our candidate(s)
761
+ results = resultCandidates.flat();
762
+ // Sort results, but not if it's umi tags since they are sorted by count
763
+ if (!(resultCandidates.length === 1 && results[0].type === ResultType.umiWildcard))
764
+ results = results.sort(getSortFunction());
765
+
766
+ // Since some tags are kaomoji, we have to add the normal results in some cases
767
+ if (tagword.startsWith("<") || tagword.startsWith("*<")) {
768
+ // Create escaped search regex with support for * as a start placeholder
769
+ let searchRegex;
770
+ if (tagword.startsWith("*")) {
771
+ tagword = tagword.slice(1);
772
+ searchRegex = new RegExp(`${escapeRegExp(tagword)}`, 'i');
773
+ } else {
774
+ searchRegex = new RegExp(`(^|[^a-zA-Z])${escapeRegExp(tagword)}`, 'i');
775
+ }
776
+ let genericResults = allTags.filter(x => x[0].toLowerCase().search(searchRegex) > -1).slice(0, TAC_CFG.maxResults);
777
+
778
+ genericResults.forEach(g => {
779
+ let result = new AutocompleteResult(g[0].trim(), ResultType.tag)
780
+ result.category = g[1];
781
+ result.count = g[2];
782
+ result.aliases = g[3];
783
+ results.push(result);
784
+ });
785
+ }
786
+ }
787
+ // Else search the normal tag list
788
+ if (!resultCandidates || resultCandidates.length === 0
789
+ || (TAC_CFG.includeEmbeddingsInNormalResults && !(tagword.startsWith("<") || tagword.startsWith("*<")))
790
+ ) {
791
+ resultCountBeforeNormalTags = results.length;
792
+
793
+ // Create escaped search regex with support for * as a start placeholder
794
+ let searchRegex;
795
+ if (tagword.startsWith("*")) {
796
+ tagword = tagword.slice(1);
797
+ searchRegex = new RegExp(`${escapeRegExp(tagword)}`, 'i');
798
+ } else {
799
+ searchRegex = new RegExp(`(^|[^a-zA-Z])${escapeRegExp(tagword)}`, 'i');
800
+ }
801
+
802
+ // Both normal tags and aliases/translations are included depending on the config
803
+ let baseFilter = (x) => x[0].toLowerCase().search(searchRegex) > -1;
804
+ let aliasFilter = (x) => x[3] && x[3].toLowerCase().search(searchRegex) > -1;
805
+ let translationFilter = (x) => (translations.has(x[0]) && translations.get(x[0]).toLowerCase().search(searchRegex) > -1)
806
+ || x[3] && x[3].split(",").some(y => translations.has(y) && translations.get(y).toLowerCase().search(searchRegex) > -1);
807
+
808
+ let fil;
809
+ if (TAC_CFG.alias.searchByAlias && TAC_CFG.translation.searchByTranslation)
810
+ fil = (x) => baseFilter(x) || aliasFilter(x) || translationFilter(x);
811
+ else if (TAC_CFG.alias.searchByAlias && !TAC_CFG.translation.searchByTranslation)
812
+ fil = (x) => baseFilter(x) || aliasFilter(x);
813
+ else if (TAC_CFG.translation.searchByTranslation && !TAC_CFG.alias.searchByAlias)
814
+ fil = (x) => baseFilter(x) || translationFilter(x);
815
+ else
816
+ fil = (x) => baseFilter(x);
817
+
818
+ // Add final results
819
+ allTags.filter(fil).forEach(t => {
820
+ let result = new AutocompleteResult(t[0].trim(), ResultType.tag)
821
+ result.category = t[1];
822
+ result.count = t[2];
823
+ result.aliases = t[3];
824
+ results.push(result);
825
+ });
826
+
827
+ // Add extras
828
+ if (TAC_CFG.extra.extraFile) {
829
+ let extraResults = [];
830
+
831
+ extras.filter(fil).forEach(e => {
832
+ let result = new AutocompleteResult(e[0].trim(), ResultType.extra)
833
+ result.category = e[1] || 0; // If no category is given, use 0 as the default
834
+ result.meta = e[2] || "Custom tag";
835
+ result.aliases = e[3] || "";
836
+ extraResults.push(result);
837
+ });
838
+
839
+ if (TAC_CFG.extra.addMode === "Insert before") {
840
+ results = extraResults.concat(results);
841
+ } else {
842
+ results = results.concat(extraResults);
843
+ }
844
+ }
845
+
846
+ // Slice if the user has set a max result count
847
+ if (!TAC_CFG.showAllResults) {
848
+ results = results.slice(0, TAC_CFG.maxResults + resultCountBeforeNormalTags);
849
+ }
850
+ }
851
+
852
+ // Guard for empty results
853
+ if (!results || results.length === 0) {
854
+ //console.log('No results found for "' + tagword + '"');
855
+ hideResults(textArea);
856
+ return;
857
+ }
858
+
859
+ addResultsToList(textArea, results, tagword, true);
860
+ showResults(textArea);
861
+ }
862
+
863
+ function navigateInList(textArea, event) {
864
+ // Return if the function is deactivated in the UI or the current model is excluded due to white/blacklist settings
865
+
866
+ let keys = TAC_CFG.keymap;
867
+
868
+ // Close window if Home or End is pressed while not a keybinding, since it would break completion on leaving the original tag
869
+ if ((event.key === "Home" || event.key === "End") && !Object.values(keys).includes(event.key)) {
870
+ hideResults(textArea);
871
+ return;
872
+ }
873
+
874
+ // All set keys that are not None or empty are valid
875
+ // Default keys are: ArrowUp, ArrowDown, PageUp, PageDown, Home, End, Enter, Tab, Escape
876
+ validKeys = Object.values(keys).filter(x => x !== "None" && x !== "");
877
+
878
+ if (!validKeys.includes(event.key)) return;
879
+ if (!isVisible(textArea)) return
880
+ // Add modifier keys to base as text+.
881
+ let modKey = "";
882
+ if (event.ctrlKey) modKey += "Ctrl+";
883
+ if (event.altKey) modKey += "Alt+";
884
+ if (event.shiftKey) modKey += "Shift+";
885
+ if (event.metaKey) modKey += "Meta+";
886
+ modKey += event.key;
887
+
888
+ oldSelectedTag = selectedTag;
889
+
890
+ switch (modKey) {
891
+ case keys["MoveUp"]:
892
+ if (selectedTag === null) {
893
+ selectedTag = resultCount - 1;
894
+ } else {
895
+ selectedTag = (selectedTag - 1 + resultCount) % resultCount;
896
+ }
897
+ break;
898
+ case keys["MoveDown"]:
899
+ if (selectedTag === null) {
900
+ selectedTag = 0;
901
+ } else {
902
+ selectedTag = (selectedTag + 1) % resultCount;
903
+ }
904
+ break;
905
+ case keys["JumpUp"]:
906
+ if (selectedTag === null || selectedTag === 0) {
907
+ selectedTag = resultCount - 1;
908
+ } else {
909
+ selectedTag = (Math.max(selectedTag - 5, 0) + resultCount) % resultCount;
910
+ }
911
+ break;
912
+ case keys["JumpDown"]:
913
+ if (selectedTag === null || selectedTag === resultCount - 1) {
914
+ selectedTag = 0;
915
+ } else {
916
+ selectedTag = Math.min(selectedTag + 5, resultCount - 1) % resultCount;
917
+ }
918
+ break;
919
+ case keys["JumpToStart"]:
920
+ if (TAC_CFG.includeEmbeddingsInNormalResults &&
921
+ selectedTag > resultCountBeforeNormalTags &&
922
+ resultCountBeforeNormalTags > 0
923
+ ) {
924
+ selectedTag = resultCountBeforeNormalTags;
925
+ } else {
926
+ selectedTag = 0;
927
+ }
928
+ break;
929
+ case keys["JumpToEnd"]:
930
+ // Jump to the end of the list, or the end of embeddings if they are included in the normal results
931
+ if (TAC_CFG.includeEmbeddingsInNormalResults &&
932
+ selectedTag < resultCountBeforeNormalTags &&
933
+ resultCountBeforeNormalTags > 0
934
+ ) {
935
+ selectedTag = Math.min(resultCountBeforeNormalTags, resultCount - 1);
936
+ } else {
937
+ selectedTag = resultCount - 1;
938
+ }
939
+ break;
940
+ case keys["ChooseSelected"]:
941
+ if (selectedTag !== null) {
942
+ insertTextAtCursor(textArea, results[selectedTag], tagword);
943
+ } else {
944
+ hideResults(textArea);
945
+ return;
946
+ }
947
+ break;
948
+ case keys["ChooseFirstOrSelected"]:
949
+ let withoutChoice = false;
950
+ if (selectedTag === null) {
951
+ selectedTag = 0;
952
+ withoutChoice = true;
953
+ } else if (TAC_CFG.wildcardCompletionMode === "To next folder level") {
954
+ withoutChoice = true;
955
+ }
956
+ insertTextAtCursor(textArea, results[selectedTag], tagword, withoutChoice);
957
+ break;
958
+ case keys["Close"]:
959
+ hideResults(textArea);
960
+ break;
961
+ default:
962
+ if (event.ctrlKey || event.altKey || event.shiftKey || event.metaKey) return;
963
+ }
964
+ let moveKeys = [keys["MoveUp"], keys["MoveDown"], keys["JumpUp"], keys["JumpDown"], keys["JumpToStart"], keys["JumpToEnd"]];
965
+ if (selectedTag === resultCount - 1 && moveKeys.includes(event.key)) {
966
+ addResultsToList(textArea, results, tagword, false);
967
+ }
968
+ // Update highlighting
969
+ if (selectedTag !== null)
970
+ updateSelectionStyle(textArea, selectedTag, oldSelectedTag);
971
+
972
+ // Prevent default behavior
973
+ event.preventDefault();
974
+ event.stopPropagation();
975
+ }
976
+
977
+ function addAutocompleteToArea(area) {
978
+ // Return if autocomplete is disabled for the current area type in config
979
+ let textAreaId = getTextAreaIdentifier(area);
980
+ if ((!TAC_CFG.activeIn.img2img && textAreaId.includes("img2img"))
981
+ || (!TAC_CFG.activeIn.txt2img && textAreaId.includes("txt2img"))
982
+ || (!TAC_CFG.activeIn.negativePrompts && textAreaId.includes("n"))
983
+ || (!TAC_CFG.activeIn.thirdParty && textAreaId.includes("thirdParty"))) {
984
+ return;
985
+ }
986
+
987
+ // Only add listeners once
988
+ if (!area.classList.contains('autocomplete')) {
989
+ // Add our new element
990
+ var resultsDiv = createResultsDiv(area);
991
+ area.parentNode.insertBefore(resultsDiv, area.nextSibling);
992
+ // Hide by default so it doesn't show up on page load
993
+ hideResults(area);
994
+
995
+ // Add autocomplete event listener
996
+ area.addEventListener('input', (e) => {
997
+
998
+ // Cancel autocomplete itself if the event has no inputType (e.g. because it was triggered by the updateInput() function)
999
+ if (!e.inputType && !tacSelfTrigger) return;
1000
+ tacSelfTrigger = false;
1001
+
1002
+ debounce(autocomplete(area, area.value), TAC_CFG.delayTime);
1003
+ checkKeywordInsertionUndo(area, e);
1004
+ });
1005
+ // Add focusout event listener
1006
+ area.addEventListener('focusout', debounce(() => {
1007
+ if (!hideBlocked)
1008
+ hideResults(area);
1009
+ }, 400));
1010
+ // Add up and down arrow event listener
1011
+ area.addEventListener('keydown', (e) => navigateInList(area, e));
1012
+ // CompositionEnd fires after the user has finished IME composing
1013
+ // We need to block hide here to prevent the enter key from insta-closing the results
1014
+ area.addEventListener('compositionend', () => {
1015
+ hideBlocked = true;
1016
+ setTimeout(() => { hideBlocked = false; }, 100);
1017
+ });
1018
+
1019
+ // Add class so we know we've already added the listeners
1020
+ area.classList.add('autocomplete');
1021
+ }
1022
+ }
1023
+
1024
+ // One-time setup, triggered from onUiUpdate
1025
+ async function setup() {
1026
+ // Load external files needed by completion extensions
1027
+ await processQueue(QUEUE_FILE_LOAD, null);
1028
+
1029
+ // Find all textareas
1030
+ let textAreas = getTextAreas();
1031
+
1032
+ // Add mutation observer to accordions inside a base that has onDemand set to true
1033
+ addOnDemandObservers(addAutocompleteToArea);
1034
+
1035
+ // Not found, we're on a page without prompt textareas
1036
+ if (textAreas.every(v => v === null || v === undefined)) return;
1037
+ // Already added or unnecessary to add
1038
+ if (gradioApp().querySelector('.autocompleteParent.p')) {
1039
+ if (gradioApp().querySelector('.autocompleteParent.n') || !TAC_CFG.activeIn.negativePrompts) {
1040
+ return;
1041
+ }
1042
+ } else if (!TAC_CFG.activeIn.txt2img && !TAC_CFG.activeIn.img2img) {
1043
+ return;
1044
+ }
1045
+
1046
+ textAreas.forEach(area => addAutocompleteToArea(area));
1047
+ textAreas.forEach(area => area.offsetParent.style.setProperty('overflow', 'visible'));
1048
+ textAreas[0].offsetParent.parentElement.style.overflow = 'visible';
1049
+ // Add style to dom
1050
+ let acStyle = document.createElement('style');
1051
+ let mode = (document.querySelector(".dark") || gradioApp().querySelector(".dark")) ? 0 : 1;
1052
+ // Check if we are on webkit
1053
+ let browser = navigator.userAgent.toLowerCase().indexOf('firefox') > -1 ? "firefox" : "other";
1054
+
1055
+ let css = autocompleteCSS;
1056
+ // Replace vars with actual values (can't use actual css vars because of the way we inject the css)
1057
+ Object.keys(styleColors).forEach((key) => {
1058
+ css = css.replaceAll(`var(${key})`, styleColors[key][mode]);
1059
+ })
1060
+ Object.keys(browserVars).forEach((key) => {
1061
+ css = css.replaceAll(`var(${key})`, browserVars[key][browser]);
1062
+ })
1063
+
1064
+ if (acStyle.styleSheet) {
1065
+ acStyle.styleSheet.cssText = css;
1066
+ } else {
1067
+ acStyle.appendChild(document.createTextNode(css));
1068
+ }
1069
+ gradioApp().appendChild(acStyle);
1070
+
1071
+ // Callback
1072
+ await processQueue(QUEUE_AFTER_SETUP, null);
1073
+ }
1074
+ var tacLoading = false;
1075
+ async function run() {
1076
+ if (tacLoading) return;
1077
+ if (!TAC_CFG) return;
1078
+ tacLoading = true;
1079
+ // Get our tag base path from the temp file
1080
+ // Rest of setup
1081
+ await loadTags(TAC_CFG);
1082
+ setup();
1083
+ tacLoading = false;
1084
+ }
1085
+ run();
tagcomplete/tags/danbooru-0-zh.csv ADDED
The diff for this file is too large to render. See raw diff
 
tagcomplete/tags/danbooru.csv ADDED
The diff for this file is too large to render. See raw diff
 
tagcomplete/tags/demo-chants.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "name": "Basic-NegativePrompt",
4
+ "terms": "Basic,Negative,Low,Quality",
5
+ "content": "(worst quality, low quality, normal quality)",
6
+ "color": 3
7
+ },
8
+ {
9
+ "name": "Basic-HighQuality",
10
+ "terms": "Basic,Best,High,Quality",
11
+ "content": "(masterpiece, best quality, high quality, highres, ultra-detailed)",
12
+ "color": 1
13
+ },
14
+ {
15
+ "name": "Basic-Start",
16
+ "terms": "Basic, Start, Simple, Demo",
17
+ "content": "(masterpiece, best quality, high quality, highres), 1girl, extremely beautiful detailed face, short curly hair, light smile, flower dress, outdoors, leaf, tree, best shadow",
18
+ "color": 5
19
+ },
20
+ {
21
+ "name": "Fancy-FireMagic",
22
+ "terms": "Fire, Magic, Fancy",
23
+ "content": "(extremely detailed CG unity 8k wallpaper), (masterpiece), (best quality), (ultra-detailed), (best illustration),(best shadow), (an extremely delicate and beautiful), dynamic angle, floating, fine detail, (bloom), (shine), glinting stars, classic, (painting), (sketch),\n\na girl, solo, bare shoulders, flat_chest, diamond and glaring eyes, beautiful detailed cold face, very long blue and sliver hair, floating black feathers, wavy hair, extremely delicate and beautiful girls, beautiful detailed eyes, glowing eyes,\n\npalace, the best building, ((Fire butterflies, Flying sparks, Flames))",
24
+ "color": 5
25
+ },
26
+ {
27
+ "name": "Fancy-WaterMagic",
28
+ "terms": "Water, Magic, Fancy",
29
+ "content": "(extremely detailed CG unity 8k wallpaper), (masterpiece), (best quality), (ultra-detailed), (best illustration),(best shadow), (an extremely delicate and beautiful), classic, dynamic angle, floating, fine detail, Depth of field, classic, (painting), (sketch), (bloom), (shine), glinting stars,\n\na girl, solo, bare shoulders, flat chest, diamond and glaring eyes, beautiful detailed cold face, very long blue and sliver hair, floating black feathers, wavy hair, extremely delicate and beautiful girls, beautiful detailed eyes, glowing eyes,\n\nriver, (forest),palace, (fairyland,feather,flowers, nature),(sunlight),Hazy fog, mist",
30
+ "color": 5
31
+ }
32
+ ]
tagcomplete/tags/e621.csv ADDED
The diff for this file is too large to render. See raw diff
 
tagcomplete/tags/e621_sfw.csv ADDED
The diff for this file is too large to render. See raw diff
 
tagcomplete/tags/extra-quality-tags.csv ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ masterpiece,5,Quality tag,,
2
+ best_quality,5,Quality tag,,
3
+ high_quality,5,Quality tag,,
4
+ normal_quality,5,Quality tag,,
5
+ low_quality,5,Quality tag,,
6
+ worst_quality,5,Quality tag,,