Files
a1111-sd-webui-tagcomplete/javascript/tagAutocomplete.js
DominikDoom 3e33169a3a Disable sort order dropdown pointer events while refresh is running
Doesn't prevent keyboard focus, but changing the values there is much slower since the list doesn't stay open.
2023-09-13 22:30:37 +02:00

1411 lines
54 KiB
JavaScript

const styleColors = {
"--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;
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);
}
.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"],
showWikiLinks: opts["tac_showWikiLinks"],
showExtraNetworkPreviews: opts["tac_showExtraNetworkPreviews"],
modelSortOrder: opts["tac_modelSortOrder"],
// 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"],
// 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 NORMAL_TAG_REGEX = /[^\s,|<>\]:]+_\([^\s,|<>\]:]*\)?|[^\s,|<>():\]]+|</g;
const RUBY_TAG_REGEX = /[\w\d<][\w\d' \-?!/$%]{2,}>?/g;
const TAG_REGEX = new RegExp(`${POINTY_REGEX.source}|${COMPLETED_WILDCARD_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 + 2);
} else if (firstDifference === 0) {
sanitizedText = tagword;
}
}
}
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 || " ";
}
// 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 fetchAPI(`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"];
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
// 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
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 = "?";
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]
}
// 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)) {
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 listener
li.addEventListener("click", function () { 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 selected = items[newIndex];
let previewTypes = ["v1 Embedding", "v2 Embedding", "Hypernetwork", "Lora", "Lyco"];
let selectedType = selected.querySelector(".acMetaText").innerText;
let selectedFilename = selected.querySelector(".acListItem").innerText;
let previewDiv = gradioApp().querySelector(`.autocompleteParent${textAreaId} .sideInfo`);
if (TAC_CFG.showExtraNetworkPreviews && previewTypes.includes(selectedType)) {
let shorthandType = "";
switch (selectedType) {
case "v1 Embedding":
case "v2 Embedding":
shorthandType = "embed";
break;
case "Hypernetwork":
shorthandType = "hyper";
break;
case "Lora":
shorthandType = "lora";
break;
case "Lyco":
shorthandType = "lyco";
break;
}
let img = previewDiv.querySelector("img");
let url = await getExtraNetworkPreviewURL(selectedFilename, shorthandType);
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("<[")))
.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, "");
// 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());
// Since some tags are kaomoji, we have to add the normal results in some cases
if (tagword.startsWith("<") || tagword.startsWith("*<")) {
// 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');
}
let genericResults = allTags.filter(x => x[0].toLowerCase().search(searchRegex) > -1).slice(0, TAC_CFG.maxResults);
genericResults.forEach(g => {
let result = new AutocompleteResult(g[0].trim(), ResultType.tag)
result.category = g[1];
result.count = g[2];
result.aliases = g[3];
results.push(result);
});
}
}
// Else search the normal tag list
if (!resultCandidates || resultCandidates.length === 0
|| (TAC_CFG.includeEmbeddingsInNormalResults && !(tagword.startsWith("<") || tagword.startsWith("*<")))
) {
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);
}
}
// Slice if the user has set a max result count
if (!TAC_CFG.showAllResults) {
results = results.slice(0, TAC_CFG.maxResults + resultCountBeforeNormalTags);
}
}
// Guard for empty results
if (!results || results.length === 0) {
//console.log('No results found for "' + tagword + '"');
hideResults(textArea);
return;
}
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
// Return if ctrl key is pressed to not interfere with weight editing shortcut
if (event.ctrlKey || event.altKey || event.shiftKey || event.metaKey) return;
oldSelectedTag = selectedTag;
switch (event.key) {
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;
}
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 postAPI("tacapi/v1/refresh-temp-files", null);
await reload();
} else {
setTimeout(async () => {
await reload();
}, 2000);
}
}
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);
// 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();
}
}
});
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;
});