Download extensions/a1111-sd-webui-tagcomplete/javascript/tagAutocomplete.js from dikdimon/ext: direct link, hf CLI and curl.
- Browser
- Download file 60.8 kB
-
https://huggingface.co/dikdimon/ext/resolve/main/extensions/a1111-sd-webui-tagcomplete/javascript/tagAutocomplete.js
- Command line
-
hf download hf://dikdimon/ext/extensions/a1111-sd-webui-tagcomplete/javascript/tagAutocomplete.js
-
curl -L -o tagAutocomplete.js https://huggingface.co/dikdimon/ext/resolve/main/extensions/a1111-sd-webui-tagcomplete/javascript/tagAutocomplete.js
60.8 kB
| const styleColors = { | |
| "--results-neutral-text": ["#e0e0e0","black"], | |
| "--results-bg": ["#0b0f19", "#ffffff"], | |
| "--results-border-color": ["#4b5563", "#e5e7eb"], | |
| "--results-border-width": ["1px", "1.5px"], | |
| "--results-bg-odd": ["#111827", "#f9fafb"], | |
| "--results-hover": ["#1f2937", "#f5f6f8"], | |
| "--results-selected": ["#374151", "#e5e7eb"], | |
| "--meta-text-color": ["#6b6f7b", "#a2a9b4"], | |
| "--embedding-v1-color": ["lightsteelblue", "#2b5797"], | |
| "--embedding-v2-color": ["skyblue", "#2d89ef"], | |
| "--live-translation-rt": ["whitesmoke", "#222"], | |
| "--live-translation-color-1": ["lightskyblue", "#2d89ef"], | |
| "--live-translation-color-2": ["palegoldenrod", "#eb5700"], | |
| "--live-translation-color-3": ["darkseagreen", "darkgreen"], | |
| } | |
| const browserVars = { | |
| "--results-overflow-y": { | |
| "firefox": "scroll", | |
| "other": "auto" | |
| } | |
| } | |
| // Style for new elements. Gets appended to the Gradio root. | |
| const autocompleteCSS = ` | |
| #quicksettings [id^=setting_tac] { | |
| background-color: transparent; | |
| min-width: fit-content; | |
| } | |
| .autocompleteParent { | |
| display: flex; | |
| position: absolute; | |
| z-index: 999; | |
| max-width: calc(100% - 1.5rem); | |
| margin: 5px 0 0 0; | |
| } | |
| .autocompleteResults { | |
| background-color: var(--results-bg) !important; | |
| border: var(--results-border-width) solid var(--results-border-color) !important; | |
| color: var(--results-neutral-text) !important; | |
| border-radius: 12px !important; | |
| height: fit-content; | |
| flex-basis: fit-content; | |
| flex-shrink: 0; | |
| overflow-y: var(--results-overflow-y); | |
| overflow-x: hidden; | |
| word-break: break-word; | |
| } | |
| .sideInfo { | |
| display: none; | |
| position: relative; | |
| margin-left: 10px; | |
| height: 18rem; | |
| max-width: 16rem; | |
| } | |
| .sideInfo > img { | |
| object-fit: cover; | |
| height: 100%; | |
| width: 100%; | |
| } | |
| .autocompleteResultsList > li:nth-child(odd) { | |
| background-color: var(--results-bg-odd); | |
| } | |
| .autocompleteResultsList > li { | |
| list-style-type: none; | |
| padding: 10px; | |
| cursor: pointer; | |
| } | |
| .autocompleteResultsList > li:hover { | |
| background-color: var(--results-hover); | |
| } | |
| .autocompleteResultsList > li.selected { | |
| background-color: var(--results-selected); | |
| } | |
| .resultsFlexContainer { | |
| display: flex; | |
| } | |
| .acListItem { | |
| white-space: break-spaces; | |
| min-width: 100px; | |
| } | |
| .acMetaText { | |
| position: relative; | |
| flex-grow: 1; | |
| text-align: end; | |
| padding: 0 0 0 15px; | |
| white-space: nowrap; | |
| color: var(--meta-text-color); | |
| } | |
| .acMetaText.biased::before { | |
| content: "✨"; | |
| margin-right: 2px; | |
| } | |
| .acWikiLink { | |
| padding: 0.5rem; | |
| margin: -0.5rem 0 -0.5rem -0.5rem; | |
| } | |
| .acWikiLink:hover { | |
| text-decoration: underline; | |
| } | |
| .acListItem.acEmbeddingV1 { | |
| color: var(--embedding-v1-color); | |
| } | |
| .acListItem.acEmbeddingV2 { | |
| color: var(--embedding-v2-color); | |
| } | |
| .acRuby { | |
| padding: var(--input-padding); | |
| color: #888; | |
| font-size: 0.8rem; | |
| user-select: none; | |
| } | |
| .acRuby > ruby { | |
| display: inline-flex; | |
| flex-direction: column-reverse; | |
| margin-top: 0.5rem; | |
| vertical-align: bottom; | |
| cursor: pointer; | |
| } | |
| .acRuby > ruby::hover { | |
| text-decoration: underline; | |
| text-shadow: 0 0 10px var(--live-translation-color-1); | |
| } | |
| .acRuby > :nth-child(3n+1) { | |
| color: var(--live-translation-color-1); | |
| } | |
| .acRuby > :nth-child(3n+2) { | |
| color: var(--live-translation-color-2); | |
| } | |
| .acRuby > :nth-child(3n+3) { | |
| color: var(--live-translation-color-3); | |
| } | |
| .acRuby > ruby > rt { | |
| line-height: 1rem; | |
| padding: 0px 5px 0px 0px; | |
| text-align: left; | |
| font-size: 1rem; | |
| color: var(--live-translation-rt); | |
| } | |
| .acListItem .acPathPart:nth-child(3n+1) { | |
| color: var(--live-translation-color-1); | |
| } | |
| .acListItem .acPathPart:nth-child(3n+2) { | |
| color: var(--live-translation-color-2); | |
| } | |
| .acListItem .acPathPart:nth-child(3n+3) { | |
| color: var(--live-translation-color-3); | |
| } | |
| `; | |
| async function loadTags(c) { | |
| // Load main tags and aliases | |
| if (allTags.length === 0 && c.tagFile && c.tagFile !== "None") { | |
| try { | |
| allTags = await loadCSV(`${tagBasePath}/${c.tagFile}`); | |
| } catch (e) { | |
| console.error("Error loading tags file: " + e); | |
| return; | |
| } | |
| } | |
| await loadExtraTags(c); | |
| } | |
| async function loadExtraTags(c) { | |
| if (c.extra.extraFile && c.extra.extraFile !== "None") { | |
| try { | |
| extras = await loadCSV(`${tagBasePath}/${c.extra.extraFile}`); | |
| // Add translations to the main translation map for extra tags that have them | |
| extras.forEach(e => { | |
| if (e[4]) translations.set(e[0], e[4]); | |
| }); | |
| } catch (e) { | |
| console.error("Error loading extra file: " + e); | |
| return; | |
| } | |
| } | |
| } | |
| async function loadTranslations(c) { | |
| if (c.translation.translationFile && c.translation.translationFile !== "None") { | |
| try { | |
| let tArray = await loadCSV(`${tagBasePath}/${c.translation.translationFile}`); | |
| tArray.forEach(t => { | |
| if (c.translation.oldFormat && t[2]) // if 2 doesn't exist, it's probably a new format file and the setting is on by mistake | |
| translations.set(t[0], t[2]); | |
| else if (t[1]) | |
| translations.set(t[0], t[1]); | |
| else | |
| translations.set(t[0], "Not found"); | |
| }); | |
| } catch (e) { | |
| console.error("Error loading translations file: " + e); | |
| return; | |
| } | |
| } | |
| } | |
| async function syncOptions() { | |
| let newCFG = { | |
| // Main tag file | |
| tagFile: opts["tac_tagFile"], | |
| // Active in settings | |
| activeIn: { | |
| global: opts["tac_active"], | |
| txt2img: opts["tac_activeIn.txt2img"], | |
| img2img: opts["tac_activeIn.img2img"], | |
| negativePrompts: opts["tac_activeIn.negativePrompts"], | |
| thirdParty: opts["tac_activeIn.thirdParty"], | |
| modelList: opts["tac_activeIn.modelList"], | |
| modelListMode: opts["tac_activeIn.modelListMode"] | |
| }, | |
| // Results related settings | |
| slidingPopup: opts["tac_slidingPopup"], | |
| maxResults: opts["tac_maxResults"], | |
| showAllResults: opts["tac_showAllResults"], | |
| resultStepLength: opts["tac_resultStepLength"], | |
| delayTime: opts["tac_delayTime"], | |
| useWildcards: opts["tac_useWildcards"], | |
| sortWildcardResults: opts["tac_sortWildcardResults"], | |
| useEmbeddings: opts["tac_useEmbeddings"], | |
| includeEmbeddingsInNormalResults: opts["tac_includeEmbeddingsInNormalResults"], | |
| useHypernetworks: opts["tac_useHypernetworks"], | |
| useLoras: opts["tac_useLoras"], | |
| useLycos: opts["tac_useLycos"], | |
| useLoraPrefixForLycos: opts["tac_useLoraPrefixForLycos"], | |
| showWikiLinks: opts["tac_showWikiLinks"], | |
| showExtraNetworkPreviews: opts["tac_showExtraNetworkPreviews"], | |
| modelSortOrder: opts["tac_modelSortOrder"], | |
| frequencySort: opts["tac_frequencySort"], | |
| frequencyFunction: opts["tac_frequencyFunction"], | |
| frequencyMinCount: opts["tac_frequencyMinCount"], | |
| frequencyMaxAge: opts["tac_frequencyMaxAge"], | |
| frequencyRecommendCap: opts["tac_frequencyRecommendCap"], | |
| frequencyIncludeAlias: opts["tac_frequencyIncludeAlias"], | |
| useStyleVars: opts["tac_useStyleVars"], | |
| // Insertion related settings | |
| replaceUnderscores: opts["tac_replaceUnderscores"], | |
| escapeParentheses: opts["tac_escapeParentheses"], | |
| appendComma: opts["tac_appendComma"], | |
| appendSpace: opts["tac_appendSpace"], | |
| alwaysSpaceAtEnd: opts["tac_alwaysSpaceAtEnd"], | |
| wildcardCompletionMode: opts["tac_wildcardCompletionMode"], | |
| modelKeywordCompletion: opts["tac_modelKeywordCompletion"], | |
| modelKeywordLocation: opts["tac_modelKeywordLocation"], | |
| wcWrap: opts["dp_parser_wildcard_wrap"] || "__", // to support custom wrapper chars set by dp_parser | |
| // Alias settings | |
| alias: { | |
| searchByAlias: opts["tac_alias.searchByAlias"], | |
| onlyShowAlias: opts["tac_alias.onlyShowAlias"] | |
| }, | |
| // Translation settings | |
| translation: { | |
| translationFile: opts["tac_translation.translationFile"], | |
| oldFormat: opts["tac_translation.oldFormat"], | |
| searchByTranslation: opts["tac_translation.searchByTranslation"], | |
| liveTranslation: opts["tac_translation.liveTranslation"], | |
| }, | |
| // Extra file settings | |
| extra: { | |
| extraFile: opts["tac_extra.extraFile"], | |
| addMode: opts["tac_extra.addMode"] | |
| }, | |
| // Chant file settings | |
| chantFile: opts["tac_chantFile"], | |
| // Settings not from tac but still used by the script | |
| extraNetworksDefaultMultiplier: opts["extra_networks_default_multiplier"], | |
| extraNetworksSeparator: opts["extra_networks_add_text_separator"], | |
| // Custom mapping settings | |
| keymap: JSON.parse(opts["tac_keymap"]), | |
| colorMap: JSON.parse(opts["tac_colormap"]) | |
| } | |
| if (newCFG.alias.onlyShowAlias) { | |
| newCFG.alias.searchByAlias = true; // if only show translation, enable search by translation is necessary | |
| } | |
| // Reload translations if the translation file changed | |
| if (!TAC_CFG || newCFG.translation.translationFile !== TAC_CFG.translation.translationFile) { | |
| translations.clear(); | |
| await loadTranslations(newCFG); | |
| await loadExtraTags(newCFG); | |
| } | |
| // Reload tags if the tag file changed (after translations so extra tag translations get re-added) | |
| if (!TAC_CFG || newCFG.tagFile !== TAC_CFG.tagFile || newCFG.extra.extraFile !== TAC_CFG.extra.extraFile) { | |
| allTags = []; | |
| await loadTags(newCFG); | |
| } | |
| // Refresh temp files if model sort order changed | |
| // Contrary to the other loads, this one shouldn't happen on a first time load | |
| if (TAC_CFG && newCFG.modelSortOrder !== TAC_CFG.modelSortOrder) { | |
| const dropdown = gradioApp().querySelector("#setting_tac_modelSortOrder"); | |
| dropdown.style.opacity = 0.5; | |
| dropdown.style.pointerEvents = "none"; | |
| await refreshTacTempFiles(true); | |
| dropdown.style.opacity = null; | |
| dropdown.style.pointerEvents = null; | |
| } | |
| // Update CSS if maxResults changed | |
| if (TAC_CFG && newCFG.maxResults !== TAC_CFG.maxResults) { | |
| gradioApp().querySelectorAll(".autocompleteResults").forEach(r => { | |
| r.style.maxHeight = `${newCFG.maxResults * 50}px`; | |
| }); | |
| } | |
| // Remove ruby div if live preview was disabled | |
| if (newCFG.translation.liveTranslation === false) { | |
| [...gradioApp().querySelectorAll('.acRuby')].forEach(r => { | |
| r.remove(); | |
| }); | |
| } | |
| // Apply changes | |
| TAC_CFG = newCFG; | |
| // Callback | |
| await processQueue(QUEUE_AFTER_CONFIG_CHANGE, null); | |
| } | |
| // Create the result list div and necessary styling | |
| function createResultsDiv(textArea) { | |
| let parentDiv = document.createElement("div"); | |
| let resultsDiv = document.createElement("div"); | |
| let resultsList = document.createElement("ul"); | |
| let sideDiv = document.createElement("div"); | |
| let sideDivImg = document.createElement("img"); | |
| let textAreaId = getTextAreaIdentifier(textArea); | |
| let typeClass = textAreaId.replaceAll(".", " "); | |
| parentDiv.setAttribute("class", `autocompleteParent${typeClass}`); | |
| resultsDiv.style.maxHeight = `${TAC_CFG.maxResults * 50}px`; | |
| resultsDiv.setAttribute("class", `autocompleteResults${typeClass} notranslate`); | |
| resultsDiv.setAttribute("translate", "no"); | |
| resultsList.setAttribute("class", "autocompleteResultsList"); | |
| resultsDiv.appendChild(resultsList); | |
| sideDiv.setAttribute("class", `autocompleteResults${typeClass} sideInfo`); | |
| sideDiv.appendChild(sideDivImg); | |
| parentDiv.appendChild(resultsDiv); | |
| parentDiv.appendChild(sideDiv); | |
| return parentDiv; | |
| } | |
| // Show or hide the results div | |
| function isVisible(textArea) { | |
| let textAreaId = getTextAreaIdentifier(textArea); | |
| let parentDiv = gradioApp().querySelector('.autocompleteParent' + textAreaId); | |
| return parentDiv.style.display === "flex"; | |
| } | |
| function showResults(textArea) { | |
| let textAreaId = getTextAreaIdentifier(textArea); | |
| let parentDiv = gradioApp().querySelector('.autocompleteParent' + textAreaId); | |
| parentDiv.style.display = "flex"; | |
| if (TAC_CFG.slidingPopup) { | |
| let caretPosition = getCaretCoordinates(textArea, textArea.selectionEnd).left; | |
| let offset = Math.min(textArea.offsetLeft - textArea.scrollLeft + caretPosition, textArea.offsetWidth - parentDiv.offsetWidth); | |
| parentDiv.style.left = `${offset}px`; | |
| } else { | |
| if (parentDiv.style.left) | |
| parentDiv.style.removeProperty("left"); | |
| } | |
| // Reset here too to make absolutely sure the browser registers it | |
| parentDiv.scrollTop = 0; | |
| // Ensure preview is hidden | |
| let previewDiv = gradioApp().querySelector(`.autocompleteParent${textAreaId} .sideInfo`); | |
| previewDiv.style.display = "none"; | |
| } | |
| function hideResults(textArea) { | |
| let textAreaId = getTextAreaIdentifier(textArea); | |
| let resultsDiv = gradioApp().querySelector('.autocompleteParent' + textAreaId); | |
| if (!resultsDiv) return; | |
| resultsDiv.style.display = "none"; | |
| selectedTag = null; | |
| } | |
| // Function to check activation criteria | |
| function isEnabled() { | |
| if (TAC_CFG.activeIn.global) { | |
| // Skip check if the current model was not correctly detected, since it could wrongly disable the script otherwise | |
| if (!currentModelName || !currentModelHash) return true; | |
| let modelList = TAC_CFG.activeIn.modelList | |
| .split(",") | |
| .map(x => x.trim()) | |
| .filter(x => x.length > 0); | |
| let shortHash = currentModelHash.substring(0, 10); | |
| let modelNameWithoutHash = currentModelName.replace(/\[.*\]$/g, "").trim(); | |
| if (TAC_CFG.activeIn.modelListMode.toLowerCase() === "blacklist") { | |
| // If the current model is in the blacklist, disable | |
| return modelList.filter(x => x === currentModelName || x === modelNameWithoutHash || x === currentModelHash || x === shortHash).length === 0; | |
| } else { | |
| // If the current model is in the whitelist, enable. | |
| // An empty whitelist is ignored. | |
| return modelList.length === 0 || modelList.filter(x => x === currentModelName || x === modelNameWithoutHash || x === currentModelHash || x === shortHash).length > 0; | |
| } | |
| } else { | |
| return false; | |
| } | |
| } | |
| const WEIGHT_REGEX = /[([]([^()[\]:|]+)(?::(?:\d+(?:\.\d+)?|\.\d+))?[)\]]/g; | |
| const POINTY_REGEX = /<[^\s,<](?:[^\t\n\r,<>]*>|[^\t\n\r,> ]*)/g; | |
| const COMPLETED_WILDCARD_REGEX = /__[^\s,_][^\t\n\r,_]*[^\s,_]__[^\s,_]*/g; | |
| const STYLE_VAR_REGEX = /\$\(?[^$|\[\],\s]*\)?/g; | |
| const NORMAL_TAG_REGEX = /[^\s,|<>\[\]:]+_\([^\s,|<>\[\]:]*\)?|[^\s,|<>():\[\]]+|</g; | |
| const RUBY_TAG_REGEX = /[\w\d<][\w\d' \-?!/$%]{2,}>?/g; | |
| const TAG_REGEX = () => { return new RegExp(`${POINTY_REGEX.source}|${COMPLETED_WILDCARD_REGEX.source.replaceAll("__", escapeRegExp(TAC_CFG.wcWrap))}|${STYLE_VAR_REGEX.source}|${NORMAL_TAG_REGEX.source}`, "g"); } | |
| // On click, insert the tag into the prompt textbox with respect to the cursor position | |
| async function insertTextAtCursor(textArea, result, tagword, tabCompletedWithoutChoice = false) { | |
| let text = result.text; | |
| let tagType = result.type; | |
| let cursorPos = textArea.selectionStart; | |
| var sanitizedText = text | |
| // Run sanitize queue and use first result as sanitized text | |
| sanitizeResults = await processQueueReturn(QUEUE_SANITIZE, null, tagType, text); | |
| if (sanitizeResults && sanitizeResults.length > 0) { | |
| sanitizedText = sanitizeResults[0]; | |
| } else { | |
| sanitizedText = TAC_CFG.replaceUnderscores ? text.replaceAll("_", " ") : text; | |
| if (TAC_CFG.escapeParentheses && tagType === ResultType.tag) { | |
| sanitizedText = sanitizedText | |
| .replaceAll("(", "\\(") | |
| .replaceAll(")", "\\)") | |
| .replaceAll("[", "\\[") | |
| .replaceAll("]", "\\]"); | |
| } | |
| } | |
| if ((tagType === ResultType.wildcardFile || tagType === ResultType.yamlWildcard) | |
| && tabCompletedWithoutChoice | |
| && TAC_CFG.wildcardCompletionMode !== "Always fully" | |
| && sanitizedText.includes("/")) { | |
| if (TAC_CFG.wildcardCompletionMode === "To next folder level") { | |
| let regexMatch = sanitizedText.match(new RegExp(`${escapeRegExp(tagword)}([^/]*\\/?)`, "i")); | |
| if (regexMatch) { | |
| let pathPart = regexMatch[0]; | |
| // In case the completion would have just added a slash, try again one level deeper | |
| if (pathPart === `${tagword}/`) { | |
| pathPart = sanitizedText.match(new RegExp(`${escapeRegExp(tagword)}\\/([^/]*\\/?)`, "i"))[0]; | |
| } | |
| sanitizedText = pathPart; | |
| } | |
| } else if (TAC_CFG.wildcardCompletionMode === "To first difference") { | |
| let firstDifference = 0; | |
| let longestResult = results.map(x => x.text.length).reduce((a, b) => Math.max(a, b)); | |
| // Compare the results to each other to find the first point where they differ | |
| for (let i = 0; i < longestResult; i++) { | |
| let char = results[0].text[i]; | |
| if (results.every(x => x.text[i] === char)) { | |
| firstDifference++; | |
| } else { | |
| break; | |
| } | |
| } | |
| // Don't cut off the __ at the end if it is already the full path | |
| if (firstDifference > 0 && firstDifference < longestResult) { | |
| // +2 because the sanitized text already has the __ at the start but the matched text doesn't | |
| sanitizedText = sanitizedText.substring(0, firstDifference + TAC_CFG.wcWrap.length); | |
| } else if (firstDifference === 0) { | |
| sanitizedText = tagword; | |
| } | |
| } | |
| } | |
| // Frequency db update | |
| if (TAC_CFG.frequencySort) { | |
| let name = null; | |
| switch (tagType) { | |
| case ResultType.wildcardFile: | |
| case ResultType.yamlWildcard: | |
| // We only want to update the frequency for a full wildcard, not partial paths | |
| if (sanitizedText.endsWith(TAC_CFG.wcWrap)) | |
| name = text | |
| break; | |
| case ResultType.chant: | |
| // Chants use a slightly different format | |
| name = result.aliases; | |
| break; | |
| default: | |
| name = text; | |
| break; | |
| } | |
| if (name && name.length > 0) { | |
| // Check if it's a negative prompt | |
| let textAreaId = getTextAreaIdentifier(textArea); | |
| let isNegative = textAreaId.includes("n"); | |
| // Sanitize name for API call | |
| name = encodeURIComponent(name) | |
| // Call API & update db | |
| increaseUseCount(name, tagType, isNegative) | |
| } | |
| } | |
| var prompt = textArea.value; | |
| // Edit prompt text | |
| let editStart = Math.max(cursorPos - tagword.length, 0); | |
| let editEnd = Math.min(cursorPos + tagword.length, prompt.length); | |
| let surrounding = prompt.substring(editStart, editEnd); | |
| let match = surrounding.match(new RegExp(escapeRegExp(`${tagword}`), "i")); | |
| let afterInsertCursorPos = editStart + match.index + sanitizedText.length; | |
| var optionalSeparator = ""; | |
| let extraNetworkTypes = [ResultType.hypernetwork, ResultType.lora]; | |
| let noCommaTypes = [ResultType.wildcardFile, ResultType.yamlWildcard, ResultType.umiWildcard].concat(extraNetworkTypes); | |
| if (!noCommaTypes.includes(tagType)) { | |
| // Append comma if enabled and not already present | |
| let beforeComma = surrounding.match(new RegExp(`${escapeRegExp(tagword)}[,:]`, "i")) !== null; | |
| if (TAC_CFG.appendComma) | |
| optionalSeparator = beforeComma ? "" : ","; | |
| // Add space if enabled | |
| if (TAC_CFG.appendSpace && !beforeComma) | |
| optionalSeparator += " "; | |
| // If at end of prompt and enabled, override the normal setting if not already added | |
| if (!TAC_CFG.appendSpace && TAC_CFG.alwaysSpaceAtEnd) | |
| optionalSeparator += surrounding.match(new RegExp(`${escapeRegExp(tagword)}$`, "im")) !== null ? " " : ""; | |
| } else if (extraNetworkTypes.includes(tagType)) { | |
| // Use the dedicated separator for extra networks if it's defined, otherwise fall back to space | |
| optionalSeparator = TAC_CFG.extraNetworksSeparator || " "; | |
| } | |
| // Escape $ signs since they are special chars for the replace function | |
| // We need four since we're also escaping them in replaceAll in the first place | |
| sanitizedText = sanitizedText.replaceAll("$", "$$$$"); | |
| // Replace partial tag word with new text, add comma if needed | |
| let insert = surrounding.replace(match, sanitizedText + optionalSeparator); | |
| // Add back start | |
| var newPrompt = prompt.substring(0, editStart) + insert + prompt.substring(editEnd); | |
| // Add lora/lyco keywords if enabled and found | |
| let keywordsLength = 0; | |
| if (TAC_CFG.modelKeywordCompletion !== "Never" && (tagType === ResultType.lora || tagType === ResultType.lyco)) { | |
| let keywords = null; | |
| // Check built-in activation words first | |
| if (tagType === ResultType.lora || tagType === ResultType.lyco) { | |
| let info = await fetchTacAPI(`tacapi/v1/lora-info/${result.text}`) | |
| if (info && info["activation text"]) { | |
| keywords = info["activation text"]; | |
| } | |
| } | |
| if (!keywords && modelKeywordPath.length > 0 && result.hash && result.hash !== "NOFILE" && result.hash.length > 0) { | |
| let nameDict = modelKeywordDict.get(result.hash); | |
| let names = [result.text + ".safetensors", result.text + ".pt", result.text + ".ckpt"]; | |
| // No match, try to find a sha256 match from the cache file | |
| if (!nameDict) { | |
| const sha256 = await fetchTacAPI(`/tacapi/v1/lora-cached-hash/${result.text}`) | |
| if (sha256) { | |
| nameDict = modelKeywordDict.get(sha256); | |
| } | |
| } | |
| if (nameDict) { | |
| let found = false; | |
| names.forEach(name => { | |
| if (!found && nameDict.has(name)) { | |
| found = true; | |
| keywords = nameDict.get(name); | |
| } | |
| }); | |
| if (!found) | |
| keywords = nameDict.get("none"); | |
| } | |
| } | |
| if (keywords && keywords.length > 0) { | |
| textBeforeKeywordInsertion = newPrompt; | |
| if (TAC_CFG.modelKeywordLocation === "Start of prompt") | |
| newPrompt = `${keywords}, ${newPrompt}`; // Insert keywords | |
| else if (TAC_CFG.modelKeywordLocation === "End of prompt") | |
| newPrompt = `${newPrompt}, ${keywords}`; // Insert keywords | |
| else { | |
| let keywordStart = prompt[editStart - 1] === " " ? editStart - 1 : editStart; | |
| newPrompt = prompt.substring(0, keywordStart) + `, ${keywords} ${insert}` + prompt.substring(editEnd); | |
| } | |
| textAfterKeywordInsertion = newPrompt; | |
| keywordInsertionUndone = false; | |
| setTimeout(() => lastEditWasKeywordInsertion = true, 200) | |
| keywordsLength = keywords.length + 2; // +2 for the comma and space | |
| } | |
| } | |
| // Insert into prompt textbox and reposition cursor | |
| textArea.value = newPrompt; | |
| textArea.selectionStart = afterInsertCursorPos + optionalSeparator.length + keywordsLength; | |
| textArea.selectionEnd = textArea.selectionStart | |
| // Set self trigger flag to show wildcard contents after the filename was inserted | |
| if ([ResultType.wildcardFile, ResultType.yamlWildcard, ResultType.umiWildcard].includes(result.type)) | |
| tacSelfTrigger = true; | |
| // 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. | |
| // Uses a built-in method from the webui's ui.js which also already accounts for event target | |
| if (tagType === ResultType.wildcardTag || tagType === ResultType.wildcardFile || tagType === ResultType.yamlWildcard) | |
| tacSelfTrigger = true; | |
| updateInput(textArea); | |
| // Update previous tags with the edited prompt to prevent re-searching the same term | |
| let weightedTags = [...newPrompt.matchAll(WEIGHT_REGEX)] | |
| .map(match => match[1]); | |
| let tags = newPrompt.match(TAG_REGEX()) | |
| if (weightedTags !== null) { | |
| tags = tags.filter(tag => !weightedTags.some(weighted => tag.includes(weighted))) | |
| .concat(weightedTags); | |
| } | |
| previousTags = tags; | |
| // Callback | |
| let returns = await processQueueReturn(QUEUE_AFTER_INSERT, null, tagType, sanitizedText, newPrompt, textArea); | |
| // Return if any queue function returned true (has handled hide/show already) | |
| if (returns.some(x => x === true)) | |
| return; | |
| // Hide results after inserting, if it hasn't been hidden already by a queue function | |
| if (!hideBlocked && isVisible(textArea)) { | |
| hideResults(textArea); | |
| } | |
| } | |
| function addResultsToList(textArea, results, tagword, resetList) { | |
| let textAreaId = getTextAreaIdentifier(textArea); | |
| let resultDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId); | |
| let resultsList = resultDiv.querySelector('ul'); | |
| // Reset list, selection and scrollTop since the list changed | |
| if (resetList) { | |
| resultsList.innerHTML = ""; | |
| selectedTag = null; | |
| oldSelectedTag = null; | |
| resultDiv.scrollTop = 0; | |
| resultCount = 0; | |
| } | |
| // Find right colors from config | |
| let tagFileName = TAC_CFG.tagFile.split(".")[0]; | |
| let tagColors = TAC_CFG.colorMap; | |
| let mode = (document.querySelector(".dark") || gradioApp().querySelector(".dark")) ? 0 : 1; | |
| let nextLength = Math.min(results.length, resultCount + TAC_CFG.resultStepLength); | |
| for (let i = resultCount; i < nextLength; i++) { | |
| let result = results[i]; | |
| // Skip if the result is null or undefined | |
| if (!result) | |
| continue; | |
| let li = document.createElement("li"); | |
| let flexDiv = document.createElement("div"); | |
| flexDiv.classList.add("resultsFlexContainer"); | |
| li.appendChild(flexDiv); | |
| let itemText = document.createElement("div"); | |
| itemText.classList.add("acListItem"); | |
| let displayText = ""; | |
| // If the tag matches the tagword, we don't need to display the alias | |
| if(result.type === ResultType.chant) { | |
| displayText = escapeHTML(result.aliases); | |
| } else if (result.aliases && !result.text.includes(tagword)) { // Alias | |
| let splitAliases = result.aliases.split(","); | |
| let bestAlias = splitAliases.find(a => a.toLowerCase().includes(tagword)); | |
| // search in translations if no alias matches | |
| if (!bestAlias) { | |
| let tagOrAlias = pair => pair[0] === result.text || splitAliases.includes(pair[0]); | |
| var tArray = [...translations]; | |
| if (tArray) { | |
| var translationKey = [...translations].find(pair => tagOrAlias(pair) && pair[1].includes(tagword)); | |
| if (translationKey) | |
| bestAlias = translationKey[0]; | |
| } | |
| } | |
| displayText = escapeHTML(bestAlias); | |
| // Append translation for alias if it exists and is not what the user typed | |
| if (translations.has(bestAlias) && translations.get(bestAlias) !== bestAlias && bestAlias !== result.text) | |
| displayText += `[${translations.get(bestAlias)}]`; | |
| if (!TAC_CFG.alias.onlyShowAlias && result.text !== bestAlias) | |
| displayText += " ➝ " + result.text; | |
| } else { // No alias | |
| displayText = escapeHTML(result.text); | |
| } | |
| // Append translation for result if it exists | |
| if (translations.has(result.text)) | |
| displayText += `[${translations.get(result.text)}]`; | |
| // Print search term bolded in result | |
| itemText.innerHTML = displayText.replace(tagword, `<b>${tagword}</b>`); | |
| const splitTypes = [ResultType.wildcardFile, ResultType.yamlWildcard] | |
| if (splitTypes.includes(result.type) && itemText.innerHTML.includes("/")) { | |
| let parts = itemText.innerHTML.split("/"); | |
| let lastPart = parts[parts.length - 1]; | |
| parts = parts.slice(0, parts.length - 1); | |
| itemText.innerHTML = "<span class='acPathPart'>" + parts.join("</span><span class='acPathPart'>/") + "</span>" + "/" + lastPart; | |
| } | |
| // Add wiki link if the setting is enabled and a supported tag set loaded | |
| if (TAC_CFG.showWikiLinks | |
| && (result.type === ResultType.tag) | |
| && (tagFileName.toLowerCase().startsWith("danbooru") || tagFileName.toLowerCase().startsWith("e621"))) { | |
| let wikiLink = document.createElement("a"); | |
| wikiLink.classList.add("acWikiLink"); | |
| wikiLink.innerText = "?"; | |
| wikiLink.title = "Open external wiki page for this tag" | |
| let linkPart = displayText; | |
| // Only use alias result if it is one | |
| if (displayText.includes("➝")) | |
| linkPart = displayText.split(" ➝ ")[1]; | |
| // Remove any trailing translations | |
| if (linkPart.includes("[")) { | |
| linkPart = linkPart.split("[")[0] | |
| } | |
| linkPart = encodeURIComponent(linkPart); | |
| // Set link based on selected file | |
| let tagFileNameLower = tagFileName.toLowerCase(); | |
| if (tagFileNameLower.startsWith("danbooru")) { | |
| wikiLink.href = `https://danbooru.donmai.us/wiki_pages/${linkPart}`; | |
| } else if (tagFileNameLower.startsWith("e621")) { | |
| wikiLink.href = `https://e621.net/wiki_pages/${linkPart}`; | |
| } | |
| wikiLink.target = "_blank"; | |
| flexDiv.appendChild(wikiLink); | |
| } | |
| flexDiv.appendChild(itemText); | |
| // Add post count & color if it's a tag | |
| // Wildcards & Embeds have no tag category | |
| if (result.category) { | |
| // Set the color of the tag | |
| let cat = result.category; | |
| let colorGroup = tagColors[tagFileName]; | |
| // Default to danbooru scheme if no matching one is found | |
| if (!colorGroup) | |
| colorGroup = tagColors["danbooru"]; | |
| // Set tag type to invalid if not found | |
| if (!colorGroup[cat]) | |
| cat = "-1"; | |
| flexDiv.style = `color: ${colorGroup[cat][mode]};`; | |
| } | |
| // Post count | |
| if (result.count && !isNaN(result.count) && result.count !== Number.MAX_SAFE_INTEGER) { | |
| let postCount = result.count; | |
| let formatter; | |
| // Danbooru formats numbers with a padded fraction for 1M or 1k, but not for 10/100k | |
| if (postCount >= 1000000 || (postCount >= 1000 && postCount < 10000)) | |
| formatter = Intl.NumberFormat("en", { notation: "compact", minimumFractionDigits: 1, maximumFractionDigits: 1 }); | |
| else | |
| formatter = Intl.NumberFormat("en", {notation: "compact"}); | |
| let formattedCount = formatter.format(postCount); | |
| let countDiv = document.createElement("div"); | |
| countDiv.textContent = formattedCount; | |
| countDiv.classList.add("acMetaText"); | |
| flexDiv.appendChild(countDiv); | |
| } else if (result.meta) { // Check if there is meta info to display | |
| let metaDiv = document.createElement("div"); | |
| metaDiv.textContent = result.meta; | |
| metaDiv.classList.add("acMetaText"); | |
| // Add version info classes if it is an embedding | |
| if (result.type === ResultType.embedding) { | |
| if (result.meta.startsWith("v1")) | |
| itemText.classList.add("acEmbeddingV1"); | |
| else if (result.meta.startsWith("v2")) | |
| itemText.classList.add("acEmbeddingV2"); | |
| } | |
| flexDiv.appendChild(metaDiv); | |
| } | |
| // Add small ✨ marker to indicate usage sorting | |
| if (result.usageBias) { | |
| flexDiv.querySelector(".acMetaText").classList.add("biased"); | |
| flexDiv.title = "✨ Frequent tag. Ctrl/Cmd + click to reset usage count." | |
| } | |
| // Check if it's a negative prompt | |
| let isNegative = textAreaId.includes("n"); | |
| // Add listener | |
| li.addEventListener("click", (e) => { | |
| if (e.ctrlKey || e.metaKey) { | |
| resetUseCount(result.text, result.type, !isNegative, isNegative); | |
| flexDiv.querySelector(".acMetaText").classList.remove("biased"); | |
| } else { | |
| insertTextAtCursor(textArea, result, tagword); | |
| } | |
| }); | |
| // Add element to list | |
| resultsList.appendChild(li); | |
| } | |
| resultCount = nextLength; | |
| if (resetList) { | |
| selectedTag = null; | |
| oldSelectedTag = null; | |
| resultDiv.scrollTop = 0; | |
| } | |
| } | |
| async function updateSelectionStyle(textArea, newIndex, oldIndex) { | |
| let textAreaId = getTextAreaIdentifier(textArea); | |
| let resultDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId); | |
| let resultsList = resultDiv.querySelector('ul'); | |
| let items = resultsList.getElementsByTagName('li'); | |
| if (oldIndex != null) { | |
| items[oldIndex].classList.remove('selected'); | |
| } | |
| // make it safer | |
| if (newIndex !== null) { | |
| let selected = items[newIndex]; | |
| selected.classList.add('selected'); | |
| // Set scrolltop to selected item | |
| resultDiv.scrollTop = selected.offsetTop - resultDiv.offsetTop; | |
| } | |
| // Show preview if enabled and the selected type supports it | |
| if (newIndex !== null) { | |
| let selectedResult = results[newIndex]; | |
| let selectedType = selectedResult.type; | |
| // These types support previews (others could technically too, but are not native to the webui gallery) | |
| let previewTypes = [ResultType.embedding, ResultType.hypernetwork, ResultType.lora, ResultType.lyco]; | |
| let previewDiv = gradioApp().querySelector(`.autocompleteParent${textAreaId} .sideInfo`); | |
| if (TAC_CFG.showExtraNetworkPreviews && previewTypes.includes(selectedType)) { | |
| let img = previewDiv.querySelector("img"); | |
| // String representation of our type enum | |
| const typeString = Object.keys(ResultType)[selectedType - 1].toLowerCase(); | |
| // Get image from API | |
| let url = await getTacExtraNetworkPreviewURL(selectedResult.text, typeString); | |
| if (url) { | |
| img.src = url; | |
| previewDiv.style.display = "block"; | |
| } else { | |
| previewDiv.style.display = "none"; | |
| } | |
| } else { | |
| previewDiv.style.display = "none"; | |
| } | |
| } | |
| } | |
| function updateRuby(textArea, prompt) { | |
| if (!TAC_CFG.translation.liveTranslation) return; | |
| if (!TAC_CFG.translation.translationFile || TAC_CFG.translation.translationFile === "None") return; | |
| let ruby = gradioApp().querySelector('.acRuby' + getTextAreaIdentifier(textArea)); | |
| if (!ruby) { | |
| let textAreaId = getTextAreaIdentifier(textArea); | |
| let typeClass = textAreaId.replaceAll(".", " "); | |
| ruby = document.createElement("div"); | |
| ruby.setAttribute("class", `acRuby${typeClass} notranslate`); | |
| textArea.parentNode.appendChild(ruby); | |
| } | |
| ruby.innerText = prompt; | |
| let bracketEscapedPrompt = prompt.replaceAll("\\(", "$").replaceAll("\\)", "%"); | |
| let rubyTags = bracketEscapedPrompt.match(RUBY_TAG_REGEX); | |
| if (!rubyTags) return; | |
| rubyTags.sort((a, b) => b.length - a.length); | |
| rubyTags = new Set(rubyTags); | |
| const prepareTag = (tag) => { | |
| tag = tag.replaceAll("$", "\\(").replaceAll("%", "\\)"); | |
| let unsanitizedTag = tag | |
| .replaceAll(" ", "_") | |
| .replaceAll("\\(", "(") | |
| .replaceAll("\\)", ")"); | |
| const translation = translations?.get(tag) || translations?.get(unsanitizedTag); | |
| let escapedTag = escapeRegExp(tag); | |
| return { tag, escapedTag, translation }; | |
| } | |
| const replaceOccurences = (text, tuple) => { | |
| let { tag, escapedTag, translation } = tuple; | |
| let searchRegex = new RegExp(`(?<!<ruby>)(?:\\b)${escapedTag}(?:\\b|$|(?=[,|: \\t\\n\\r]))(?!<rt>)`, "g"); | |
| return text.replaceAll(searchRegex, `<ruby>${escapeHTML(tag)}<rt>${translation}</rt></ruby>`); | |
| } | |
| let html = escapeHTML(prompt); | |
| // First try to find direct matches | |
| [...rubyTags].forEach(tag => { | |
| let tuple = prepareTag(tag); | |
| if (tuple.translation) { | |
| html = replaceOccurences(html, tuple); | |
| } else { | |
| let subTags = tuple.tag.split(" ").filter(x => x.trim().length > 0); | |
| // Return if there is only one word | |
| if (subTags.length === 1) return; | |
| let subHtml = tag.replaceAll("$", "\\(").replaceAll("%", "\\)"); | |
| let translateNgram = (windows) => { | |
| windows.forEach(window => { | |
| let combinedTag = window.join(" "); | |
| let subTuple = prepareTag(combinedTag); | |
| if (subTuple.tag.length <= 2) return; | |
| if (subTuple.translation) { | |
| subHtml = replaceOccurences(subHtml, subTuple); | |
| } | |
| }); | |
| } | |
| // Perform n-gram sliding window search | |
| translateNgram(toNgrams(subTags, 3)); | |
| translateNgram(toNgrams(subTags, 2)); | |
| translateNgram(toNgrams(subTags, 1)); | |
| let escapedTag = escapeRegExp(tuple.tag); | |
| let searchRegex = new RegExp(`(?<!<ruby>)(?:\\b)${escapedTag}(?:\\b|$|(?=[,|: \\t\\n\\r]))(?!<rt>)`, "g"); | |
| html = html.replaceAll(searchRegex, subHtml); | |
| } | |
| }); | |
| ruby.innerHTML = html; | |
| // Add listeners for auto selection | |
| const childNodes = [...ruby.childNodes]; | |
| [...ruby.children].forEach(child => { | |
| const textBefore = childNodes.slice(0, childNodes.indexOf(child)).map(x => x.childNodes[0]?.textContent || x.textContent).join("") | |
| child.onclick = () => rubyTagClicked(child, textBefore, prompt, textArea); | |
| }); | |
| } | |
| function rubyTagClicked(node, textBefore, prompt, textArea) { | |
| let selectionText = node.childNodes[0].textContent; | |
| // Find start and end position of the tag in the prompt | |
| let startPos = prompt.indexOf(textBefore) + textBefore.length; | |
| let endPos = startPos + selectionText.length; | |
| // Select in text area | |
| textArea.focus(); | |
| textArea.setSelectionRange(startPos, endPos); | |
| } | |
| // Check if the last edit was the keyword insertion, and catch undo/redo in that case | |
| function checkKeywordInsertionUndo(textArea, event) { | |
| if (TAC_CFG.modelKeywordCompletion === "Never") return; | |
| switch (event.inputType) { | |
| case "historyUndo": | |
| if (lastEditWasKeywordInsertion && !keywordInsertionUndone) { | |
| keywordInsertionUndone = true; | |
| textArea.value = textBeforeKeywordInsertion; | |
| tacSelfTrigger = true; | |
| updateInput(textArea); | |
| } | |
| break; | |
| case "historyRedo": | |
| if (lastEditWasKeywordInsertion && keywordInsertionUndone) { | |
| keywordInsertionUndone = false; | |
| textArea.value = textAfterKeywordInsertion; | |
| tacSelfTrigger = true; | |
| updateInput(textArea); | |
| } | |
| case undefined: | |
| // undefined is caused by the updateInput event firing, so we just ignore it | |
| break; | |
| default: | |
| // Everything else deactivates the keyword undo and returns to normal undo behavior | |
| lastEditWasKeywordInsertion = false; | |
| keywordInsertionUndone = false; | |
| textBeforeKeywordInsertion = ""; | |
| textAfterKeywordInsertion = ""; | |
| break; | |
| } | |
| } | |
| async function autocomplete(textArea, prompt, fixedTag = null) { | |
| // Return if the function is deactivated in the UI | |
| if (!isEnabled()) return; | |
| // Guard for empty prompt | |
| if (prompt.length === 0) { | |
| hideResults(textArea); | |
| previousTags = []; | |
| tagword = ""; | |
| return; | |
| } | |
| if (fixedTag === null) { | |
| // Match tags with RegEx to get the last edited one | |
| // We also match for the weighting format (e.g. "tag:1.0") here, and combine the two to get the full tag word set | |
| let weightedTags = [...prompt.matchAll(WEIGHT_REGEX)] | |
| .map(match => match[1]); | |
| let tags = prompt.match(TAG_REGEX()) | |
| if (weightedTags !== null && tags !== null) { | |
| tags = tags.filter(tag => !weightedTags.some(weighted => tag.includes(weighted) && !tag.startsWith("<[") && !tag.startsWith("$("))) | |
| .concat(weightedTags); | |
| } | |
| // Guard for no tags | |
| if (!tags || tags.length === 0) { | |
| previousTags = []; | |
| tagword = ""; | |
| hideResults(textArea); | |
| return; | |
| } | |
| let tagCountChange = tags.length - previousTags.length; | |
| let diff = difference(tags, previousTags); | |
| previousTags = tags; | |
| // Guard for no difference / only whitespace remaining / last edited tag was fully removed | |
| if (diff === null || diff.length === 0 || (diff.length === 1 && tagCountChange < 0)) { | |
| if (!hideBlocked) hideResults(textArea); | |
| return; | |
| } | |
| tagword = diff[0] | |
| // Guard for empty tagword | |
| if (tagword === null || tagword.length === 0) { | |
| hideResults(textArea); | |
| return; | |
| } | |
| } else { | |
| tagword = fixedTag; | |
| } | |
| results = []; | |
| resultCountBeforeNormalTags = 0; | |
| tagword = tagword.toLowerCase().replace(/[\n\r]/g, ""); | |
| // Needed for slicing check later | |
| let normalTags = false; | |
| // Process all parsers | |
| let resultCandidates = (await processParsers(textArea, prompt))?.filter(x => x.length > 0); | |
| // If one ore more result candidates match, use their results | |
| if (resultCandidates && resultCandidates.length > 0) { | |
| // Flatten our candidate(s) | |
| results = resultCandidates.flat(); | |
| // Sort results, but not if it's umi tags since they are sorted by count | |
| if (!(resultCandidates.length === 1 && results[0].type === ResultType.umiWildcard)) | |
| results = results.sort(getSortFunction()); | |
| } | |
| // Else search the normal tag list | |
| if (!resultCandidates || resultCandidates.length === 0 | |
| || (TAC_CFG.includeEmbeddingsInNormalResults && !(tagword.startsWith("<") || tagword.startsWith("*<"))) | |
| ) { | |
| normalTags = true; | |
| resultCountBeforeNormalTags = results.length; | |
| // Create escaped search regex with support for * as a start placeholder | |
| let searchRegex; | |
| if (tagword.startsWith("*")) { | |
| tagword = tagword.slice(1); | |
| searchRegex = new RegExp(`${escapeRegExp(tagword)}`, 'i'); | |
| } else { | |
| searchRegex = new RegExp(`(^|[^a-zA-Z])${escapeRegExp(tagword)}`, 'i'); | |
| } | |
| // Both normal tags and aliases/translations are included depending on the config | |
| let baseFilter = (x) => x[0].toLowerCase().search(searchRegex) > -1; | |
| let aliasFilter = (x) => x[3] && x[3].toLowerCase().search(searchRegex) > -1; | |
| let translationFilter = (x) => (translations.has(x[0]) && translations.get(x[0]).toLowerCase().search(searchRegex) > -1) | |
| || x[3] && x[3].split(",").some(y => translations.has(y) && translations.get(y).toLowerCase().search(searchRegex) > -1); | |
| let fil; | |
| if (TAC_CFG.alias.searchByAlias && TAC_CFG.translation.searchByTranslation) | |
| fil = (x) => baseFilter(x) || aliasFilter(x) || translationFilter(x); | |
| else if (TAC_CFG.alias.searchByAlias && !TAC_CFG.translation.searchByTranslation) | |
| fil = (x) => baseFilter(x) || aliasFilter(x); | |
| else if (TAC_CFG.translation.searchByTranslation && !TAC_CFG.alias.searchByAlias) | |
| fil = (x) => baseFilter(x) || translationFilter(x); | |
| else | |
| fil = (x) => baseFilter(x); | |
| // Add final results | |
| allTags.filter(fil).forEach(t => { | |
| let result = new AutocompleteResult(t[0].trim(), ResultType.tag) | |
| result.category = t[1]; | |
| result.count = t[2]; | |
| result.aliases = t[3]; | |
| results.push(result); | |
| }); | |
| // Add extras | |
| if (TAC_CFG.extra.extraFile) { | |
| let extraResults = []; | |
| extras.filter(fil).forEach(e => { | |
| let result = new AutocompleteResult(e[0].trim(), ResultType.extra) | |
| result.category = e[1] || 0; // If no category is given, use 0 as the default | |
| result.meta = e[2] || "Custom tag"; | |
| result.aliases = e[3] || ""; | |
| extraResults.push(result); | |
| }); | |
| if (TAC_CFG.extra.addMode === "Insert before") { | |
| results = extraResults.concat(results); | |
| } else { | |
| results = results.concat(extraResults); | |
| } | |
| } | |
| } | |
| // Guard for empty results | |
| if (!results || results.length === 0) { | |
| //console.log('No results found for "' + tagword + '"'); | |
| hideResults(textArea); | |
| return; | |
| } | |
| // Sort again with frequency / usage count if enabled | |
| if (TAC_CFG.frequencySort) { | |
| // Split our results into a list of names and types | |
| let tagNames = []; | |
| let aliasNames = []; | |
| let types = []; | |
| // Limit to 2k for performance reasons | |
| const aliasTypes = [ResultType.tag, ResultType.extra]; | |
| results.slice(0,2000).forEach(r => { | |
| const name = r.type === ResultType.chant ? r.aliases : r.text; | |
| // Add to alias list or tag list depending on if the name includes the tagword | |
| // (the same criteria is used in the filter in calculateUsageBias) | |
| if (aliasTypes.includes(r.type) && !name.includes(tagword)) { | |
| aliasNames.push(name); | |
| } else { | |
| tagNames.push(name); | |
| } | |
| types.push(r.type); | |
| }); | |
| // Check if it's a negative prompt | |
| let textAreaId = getTextAreaIdentifier(textArea); | |
| let isNegative = textAreaId.includes("n"); | |
| // Request use counts from the DB | |
| const names = TAC_CFG.frequencyIncludeAlias ? tagNames.concat(aliasNames) : tagNames; | |
| const counts = await getUseCounts(names, types, isNegative) || []; | |
| // Pre-calculate weights to prevent duplicate work | |
| const resultBiasMap = new Map(); | |
| results.forEach(result => { | |
| const name = result.type === ResultType.chant ? result.aliases : result.text; | |
| const type = result.type; | |
| // Find matching pair from DB results | |
| const useStats = counts.find(c => c.name === name && c.type === type); | |
| const uses = useStats?.count || 0; | |
| // Calculate & set weight | |
| const weight = calculateUsageBias(result, result.count, uses) | |
| resultBiasMap.set(result, weight); | |
| }); | |
| // Actual sorting with the pre-calculated weights | |
| results = results.sort((a, b) => { | |
| return resultBiasMap.get(b) - resultBiasMap.get(a); | |
| }); | |
| } | |
| // Slice if the user has set a max result count and we are not in a extra networks / wildcard list | |
| if (!TAC_CFG.showAllResults && normalTags) { | |
| results = results.slice(0, TAC_CFG.maxResults + resultCountBeforeNormalTags); | |
| } | |
| addResultsToList(textArea, results, tagword, true); | |
| showResults(textArea); | |
| } | |
| function navigateInList(textArea, event) { | |
| // Return if the function is deactivated in the UI or the current model is excluded due to white/blacklist settings | |
| if (!isEnabled()) return; | |
| let keys = TAC_CFG.keymap; | |
| // Close window if Home or End is pressed while not a keybinding, since it would break completion on leaving the original tag | |
| if ((event.key === "Home" || event.key === "End") && !Object.values(keys).includes(event.key)) { | |
| hideResults(textArea); | |
| return; | |
| } | |
| // All set keys that are not None or empty are valid | |
| // Default keys are: ArrowUp, ArrowDown, PageUp, PageDown, Home, End, Enter, Tab, Escape | |
| validKeys = Object.values(keys).filter(x => x !== "None" && x !== ""); | |
| if (!validKeys.includes(event.key)) return; | |
| if (!isVisible(textArea)) return | |
| // Add modifier keys to base as text+. | |
| let modKey = ""; | |
| if (event.ctrlKey) modKey += "Ctrl+"; | |
| if (event.altKey) modKey += "Alt+"; | |
| if (event.shiftKey) modKey += "Shift+"; | |
| if (event.metaKey) modKey += "Meta+"; | |
| modKey += event.key; | |
| oldSelectedTag = selectedTag; | |
| switch (modKey) { | |
| case keys["MoveUp"]: | |
| if (selectedTag === null) { | |
| selectedTag = resultCount - 1; | |
| } else { | |
| selectedTag = (selectedTag - 1 + resultCount) % resultCount; | |
| } | |
| break; | |
| case keys["MoveDown"]: | |
| if (selectedTag === null) { | |
| selectedTag = 0; | |
| } else { | |
| selectedTag = (selectedTag + 1) % resultCount; | |
| } | |
| break; | |
| case keys["JumpUp"]: | |
| if (selectedTag === null || selectedTag === 0) { | |
| selectedTag = resultCount - 1; | |
| } else { | |
| selectedTag = (Math.max(selectedTag - 5, 0) + resultCount) % resultCount; | |
| } | |
| break; | |
| case keys["JumpDown"]: | |
| if (selectedTag === null || selectedTag === resultCount - 1) { | |
| selectedTag = 0; | |
| } else { | |
| selectedTag = Math.min(selectedTag + 5, resultCount - 1) % resultCount; | |
| } | |
| break; | |
| case keys["JumpToStart"]: | |
| if (TAC_CFG.includeEmbeddingsInNormalResults && | |
| selectedTag > resultCountBeforeNormalTags && | |
| resultCountBeforeNormalTags > 0 | |
| ) { | |
| selectedTag = resultCountBeforeNormalTags; | |
| } else { | |
| selectedTag = 0; | |
| } | |
| break; | |
| case keys["JumpToEnd"]: | |
| // Jump to the end of the list, or the end of embeddings if they are included in the normal results | |
| if (TAC_CFG.includeEmbeddingsInNormalResults && | |
| selectedTag < resultCountBeforeNormalTags && | |
| resultCountBeforeNormalTags > 0 | |
| ) { | |
| selectedTag = Math.min(resultCountBeforeNormalTags, resultCount - 1); | |
| } else { | |
| selectedTag = resultCount - 1; | |
| } | |
| break; | |
| case keys["ChooseSelected"]: | |
| if (selectedTag !== null) { | |
| insertTextAtCursor(textArea, results[selectedTag], tagword); | |
| } else { | |
| hideResults(textArea); | |
| return; | |
| } | |
| break; | |
| case keys["ChooseFirstOrSelected"]: | |
| let withoutChoice = false; | |
| if (selectedTag === null) { | |
| selectedTag = 0; | |
| withoutChoice = true; | |
| } else if (TAC_CFG.wildcardCompletionMode === "To next folder level") { | |
| withoutChoice = true; | |
| } | |
| insertTextAtCursor(textArea, results[selectedTag], tagword, withoutChoice); | |
| break; | |
| case keys["Close"]: | |
| hideResults(textArea); | |
| break; | |
| default: | |
| if (event.ctrlKey || event.altKey || event.shiftKey || event.metaKey) return; | |
| } | |
| let moveKeys = [keys["MoveUp"], keys["MoveDown"], keys["JumpUp"], keys["JumpDown"], keys["JumpToStart"], keys["JumpToEnd"]]; | |
| if (selectedTag === resultCount - 1 && moveKeys.includes(event.key)) { | |
| addResultsToList(textArea, results, tagword, false); | |
| } | |
| // Update highlighting | |
| if (selectedTag !== null) | |
| updateSelectionStyle(textArea, selectedTag, oldSelectedTag); | |
| // Prevent default behavior | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| } | |
| async function refreshTacTempFiles(api = false) { | |
| const reload = async () => { | |
| wildcardFiles = []; | |
| wildcardExtFiles = []; | |
| umiWildcards = []; | |
| embeddings = []; | |
| hypernetworks = []; | |
| loras = []; | |
| lycos = []; | |
| modelKeywordDict.clear(); | |
| await processQueue(QUEUE_FILE_LOAD, null); | |
| console.log("TAC: Refreshed temp files"); | |
| } | |
| if (api) { | |
| await postTacAPI("tacapi/v1/refresh-temp-files"); | |
| await reload(); | |
| } else { | |
| setTimeout(async () => { | |
| await reload(); | |
| }, 2000); | |
| } | |
| } | |
| async function refreshEmbeddings() { | |
| await postTacAPI("tacapi/v1/refresh-embeddings", null); | |
| embeddings = []; | |
| await processQueue(QUEUE_FILE_LOAD, null); | |
| console.log("TAC: Refreshed embeddings"); | |
| } | |
| function addAutocompleteToArea(area) { | |
| // Return if autocomplete is disabled for the current area type in config | |
| let textAreaId = getTextAreaIdentifier(area); | |
| if ((!TAC_CFG.activeIn.img2img && textAreaId.includes("img2img")) | |
| || (!TAC_CFG.activeIn.txt2img && textAreaId.includes("txt2img")) | |
| || (!TAC_CFG.activeIn.negativePrompts && textAreaId.includes("n")) | |
| || (!TAC_CFG.activeIn.thirdParty && textAreaId.includes("thirdParty"))) { | |
| return; | |
| } | |
| // Only add listeners once | |
| if (!area.classList.contains('autocomplete')) { | |
| // Add our new element | |
| var resultsDiv = createResultsDiv(area); | |
| area.parentNode.insertBefore(resultsDiv, area.nextSibling); | |
| // Hide by default so it doesn't show up on page load | |
| hideResults(area); | |
| // Add autocomplete event listener | |
| area.addEventListener('input', (e) => { | |
| updateRuby(area, area.value); | |
| // Cancel autocomplete itself if the event has no inputType (e.g. because it was triggered by the updateInput() function) | |
| if (!e.inputType && !tacSelfTrigger) return; | |
| tacSelfTrigger = false; | |
| debounce(autocomplete(area, area.value), TAC_CFG.delayTime); | |
| checkKeywordInsertionUndo(area, e); | |
| }); | |
| // Add focusout event listener | |
| area.addEventListener('focusout', debounce(() => { | |
| if (!hideBlocked) | |
| hideResults(area); | |
| }, 400)); | |
| // Add up and down arrow event listener | |
| area.addEventListener('keydown', (e) => navigateInList(area, e)); | |
| // CompositionEnd fires after the user has finished IME composing | |
| // We need to block hide here to prevent the enter key from insta-closing the results | |
| area.addEventListener('compositionend', () => { | |
| hideBlocked = true; | |
| setTimeout(() => { hideBlocked = false; }, 100); | |
| }); | |
| // Add class so we know we've already added the listeners | |
| area.classList.add('autocomplete'); | |
| } | |
| } | |
| // One-time setup, triggered from onUiUpdate | |
| async function setup() { | |
| // Load external files needed by completion extensions | |
| await processQueue(QUEUE_FILE_LOAD, null); | |
| // Find all textareas | |
| let textAreas = getTextAreas(); | |
| // Add mutation observer to accordions inside a base that has onDemand set to true | |
| addOnDemandObservers(addAutocompleteToArea); | |
| // Add event listener to apply settings button so we can mirror the changes to our internal config | |
| let applySettingsButton = gradioApp().querySelector("#tab_settings #settings_submit") || gradioApp().querySelector("#tab_settings > div > .gr-button-primary"); | |
| applySettingsButton?.addEventListener("click", () => { | |
| // Wait 500ms to make sure the settings have been applied to the webui opts object | |
| setTimeout(async () => { | |
| await syncOptions(); | |
| }, 500); | |
| }); | |
| // Add change listener to our quicksettings to change our internal config without the apply button for them | |
| let quicksettings = gradioApp().querySelector('#quicksettings'); | |
| let commonQueryPart = "[id^=setting_tac] > label"; | |
| quicksettings?.querySelectorAll(`${commonQueryPart} input, ${commonQueryPart} textarea, ${commonQueryPart} select`).forEach(e => { | |
| e.addEventListener("change", () => { | |
| setTimeout(async () => { | |
| await syncOptions(); | |
| }, 500); | |
| }); | |
| }); | |
| quicksettings?.querySelectorAll(`[id^=setting_tac].gradio-dropdown input`).forEach(e => { | |
| observeElement(e, "value", () => { | |
| setTimeout(async () => { | |
| await syncOptions(); | |
| }, 500); | |
| }) | |
| }); | |
| // Listener for internal temp files refresh button | |
| gradioApp().querySelector("#refresh_tac_refreshTempFiles")?.addEventListener("click", refreshTacTempFiles); | |
| // Also add listener for external network refresh button (plus triggering python code) | |
| ["#img2img_extra_refresh", "#txt2img_extra_refresh"].forEach(e => { | |
| gradioApp().querySelector(e)?.addEventListener("click", ()=>{ | |
| refreshTacTempFiles(true); | |
| }); | |
| }) | |
| // Add mutation observer for the model hash text to also allow hash-based blacklist again | |
| let modelHashText = gradioApp().querySelector("#sd_checkpoint_hash"); | |
| updateModelName(); | |
| if (modelHashText) { | |
| currentModelHash = modelHashText.title | |
| let modelHashObserver = new MutationObserver((mutationList, observer) => { | |
| for (const mutation of mutationList) { | |
| if (mutation.type === "attributes" && mutation.attributeName === "title") { | |
| currentModelHash = mutation.target.title; | |
| updateModelName(); | |
| refreshEmbeddings(); | |
| } | |
| } | |
| }); | |
| modelHashObserver.observe(modelHashText, { attributes: true }); | |
| } | |
| // Not found, we're on a page without prompt textareas | |
| if (textAreas.every(v => v === null || v === undefined)) return; | |
| // Already added or unnecessary to add | |
| if (gradioApp().querySelector('.autocompleteParent.p')) { | |
| if (gradioApp().querySelector('.autocompleteParent.n') || !TAC_CFG.activeIn.negativePrompts) { | |
| return; | |
| } | |
| } else if (!TAC_CFG.activeIn.txt2img && !TAC_CFG.activeIn.img2img) { | |
| return; | |
| } | |
| textAreas.forEach(area => addAutocompleteToArea(area)); | |
| // Add style to dom | |
| let acStyle = document.createElement('style'); | |
| let mode = (document.querySelector(".dark") || gradioApp().querySelector(".dark")) ? 0 : 1; | |
| // Check if we are on webkit | |
| let browser = navigator.userAgent.toLowerCase().indexOf('firefox') > -1 ? "firefox" : "other"; | |
| let css = autocompleteCSS; | |
| // Replace vars with actual values (can't use actual css vars because of the way we inject the css) | |
| Object.keys(styleColors).forEach((key) => { | |
| css = css.replaceAll(`var(${key})`, styleColors[key][mode]); | |
| }) | |
| Object.keys(browserVars).forEach((key) => { | |
| css = css.replaceAll(`var(${key})`, browserVars[key][browser]); | |
| }) | |
| if (acStyle.styleSheet) { | |
| acStyle.styleSheet.cssText = css; | |
| } else { | |
| acStyle.appendChild(document.createTextNode(css)); | |
| } | |
| gradioApp().appendChild(acStyle); | |
| // Callback | |
| await processQueue(QUEUE_AFTER_SETUP, null); | |
| } | |
| var tacLoading = false; | |
| onUiUpdate(async () => { | |
| if (tacLoading) return; | |
| if (Object.keys(opts).length === 0) return; | |
| if (TAC_CFG) return; | |
| tacLoading = true; | |
| // Get our tag base path from the temp file | |
| tagBasePath = await readFile(`tmp/tagAutocompletePath.txt`); | |
| // Load config from webui opts | |
| await syncOptions(); | |
| // Rest of setup | |
| setup(); | |
| tacLoading = false; | |
| }); | |