From 3e2ee75f37be9852686db73e500f18b911403b14 Mon Sep 17 00:00:00 2001 From: DominikDoom Date: Sat, 13 Apr 2024 19:38:32 +0200 Subject: [PATCH] Move the previously global util functions to TacUtils class & update references (to prevent naming conflicts and have cleaner modularization) --- javascript/_utils.js | 883 +++++++++++++++++++-------------- javascript/ext_chants.js | 4 +- javascript/ext_embeddings.js | 4 +- javascript/ext_hypernets.js | 4 +- javascript/ext_loras.js | 6 +- javascript/ext_lycos.js | 6 +- javascript/ext_modelKeyword.js | 6 +- javascript/ext_styles.js | 6 +- javascript/ext_umi.js | 6 +- javascript/ext_wildcards.js | 12 +- javascript/tagAutocomplete.js | 90 ++-- 11 files changed, 584 insertions(+), 443 deletions(-) diff --git a/javascript/_utils.js b/javascript/_utils.js index c72ab2a..6105f1b 100644 --- a/javascript/_utils.js +++ b/javascript/_utils.js @@ -1,425 +1,566 @@ // Utility functions for tag autocomplete -// Parse the CSV file into a 2D array. Doesn't use regex, so it is very lightweight. -// We are ignoring newlines in quote fields since we expect one-line entries and parsing would break for unclosed quotes otherwise -function parseCSV(str) { - const arr = []; - let quote = false; // 'true' means we're inside a quoted field +class TacUtils { + /** + * Parses a CSV file into a 2D array. Doesn't use regex, so it is very lightweight. + * We are ignoring newlines in quote fields since we expect one-line entries and parsing would break for unclosed quotes otherwise + * @param {String} str - The CSV string to parse (likely from a file with multiple lines) + * @returns {string[][]} A 2D array of CSV entries (rows and columns of that row) + */ + static parseCSV(str) { + const arr = []; + let quote = false; // 'true' means we're inside a quoted field - // Iterate over each character, keep track of current row and column (of the returned array) - for (let row = 0, col = 0, c = 0; c < str.length; c++) { - let cc = str[c], nc = str[c+1]; // Current character, next character - arr[row] = arr[row] || []; // Create a new row if necessary - arr[row][col] = arr[row][col] || ''; // Create a new column (start with empty string) if necessary + // Iterate over each character, keep track of current row and column (of the returned array) + for (let row = 0, col = 0, c = 0; c < str.length; c++) { + let cc = str[c], nc = str[c+1]; // Current character, next character + arr[row] = arr[row] || []; // Create a new row if necessary + arr[row][col] = arr[row][col] || ''; // Create a new column (start with empty string) if necessary - // If the current character is a quotation mark, and we're inside a - // quoted field, and the next character is also a quotation mark, - // add a quotation mark to the current column and skip the next character - if (cc == '"' && quote && nc == '"') { arr[row][col] += cc; ++c; continue; } + // If the current character is a quotation mark, and we're inside a + // quoted field, and the next character is also a quotation mark, + // add a quotation mark to the current column and skip the next character + if (cc == '"' && quote && nc == '"') { arr[row][col] += cc; ++c; continue; } - // If it's just one quotation mark, begin/end quoted field - if (cc == '"') { quote = !quote; continue; } + // If it's just one quotation mark, begin/end quoted field + if (cc == '"') { quote = !quote; continue; } - // If it's a comma and we're not in a quoted field, move on to the next column - if (cc == ',' && !quote) { ++col; continue; } + // If it's a comma and we're not in a quoted field, move on to the next column + if (cc == ',' && !quote) { ++col; continue; } - // If it's a newline (CRLF), skip the next character and move on to the next row and move to column 0 of that new row - if (cc == '\r' && nc == '\n') { ++row; col = 0; ++c; quote = false; continue; } + // If it's a newline (CRLF), skip the next character and move on to the next row and move to column 0 of that new row + if (cc == '\r' && nc == '\n') { ++row; col = 0; ++c; quote = false; continue; } - // If it's a newline (LF or CR) move on to the next row and move to column 0 of that new row - if (cc == '\n') { ++row; col = 0; quote = false; continue; } - if (cc == '\r') { ++row; col = 0; quote = false; continue; } + // If it's a newline (LF or CR) move on to the next row and move to column 0 of that new row + if (cc == '\n') { ++row; col = 0; quote = false; continue; } + if (cc == '\r') { ++row; col = 0; quote = false; continue; } - // Otherwise, append the current character to the current column - arr[row][col] += cc; - } - return arr; -} - -// Load file -async function readFile(filePath, json = false, cache = false) { - if (!cache) - filePath += `?${new Date().getTime()}`; - - let response = await fetch(`file=${filePath}`); - - if (response.status != 200) { - console.error(`Error loading file "${filePath}": ` + response.status, response.statusText); - return null; + // Otherwise, append the current character to the current column + arr[row][col] += cc; + } + return arr; } - if (json) + /** Wrapper function to read a file from a path, using Gradio's "file="" accessor API + * @param {String} filePath - The path to the file + * @param {Boolean} json - Whether to parse the file as JSON + * @param {Boolean} cache - Whether to cache the response + * @returns {Promise} The file content as a string or JSON object (if json is true) + */ + static async readFile(filePath, json = false, cache = false) { + if (!cache) + filePath += `?${new Date().getTime()}`; + + let response = await fetch(`file=${filePath}`); + + if (response.status != 200) { + console.error(`Error loading file "${filePath}": ` + response.status, response.statusText); + return null; + } + + if (json) + return await response.json(); + else + return await response.text(); + } + + /** Wrapper function to read a file from the path and parse it as CSV + * @param {String} path - The path to the CSV file + * @returns {Promise} A 2D array of CSV entries + */ + static async loadCSV(path) { + let text = await this.readFile(path); + return this.parseCSV(text); + } + + /** + * Calls the TAC API for a GET request + * @param {String} url - The URL to fetch from + * @param {Boolean} json - Whether to parse the response as JSON or plain text + * @param {Boolean} cache - Whether to cache the response + * @returns {Promise} JSON or text response from the API, depending on the "json" parameter + */ + static async fetchAPI(url, json = true, cache = false) { + if (!cache) { + const appendChar = url.includes("?") ? "&" : "?"; + url += `${appendChar}${new Date().getTime()}` + } + + let response = await fetch(url); + + if (response.status != 200) { + console.error(`Error fetching API endpoint "${url}": ` + response.status, response.statusText); + return null; + } + + if (json) + return await response.json(); + else + return await response.text(); + } + + /** + * Posts to the TAC API + * @param {String} url - The URL to post to + * @param {String} body - (optional) The body of the POST request as a JSON string + * @returns JSON response from the API + */ + static async postAPI(url, body = null) { + let response = await fetch(url, { + method: "POST", + headers: {'Content-Type': 'application/json'}, + body: body + }); + + if (response.status != 200) { + console.error(`Error posting to API endpoint "${url}": ` + response.status, response.statusText); + return null; + } + return await response.json(); - else - return await response.text(); -} - -// Load CSV -async function loadCSV(path) { - let text = await readFile(path); - return parseCSV(text); -} - -// Fetch API -async function fetchTacAPI(url, json = true, cache = false) { - if (!cache) { - const appendChar = url.includes("?") ? "&" : "?"; - url += `${appendChar}${new Date().getTime()}` } - let response = await fetch(url); + /** + * Puts to the TAC API + * @param {String} url - The URL to post to + * @param {String} body - (optional) The body of the PUT request as a JSON string + * @returns JSON response from the API + */ + static async putAPI(url, body = null) { + let response = await fetch(url, { method: "PUT", body: body }); + + if (response.status != 200) { + console.error(`Error putting to API endpoint "${url}": ` + response.status, response.statusText); + return null; + } - if (response.status != 200) { - console.error(`Error fetching API endpoint "${url}": ` + response.status, response.statusText); - return null; - } - - if (json) return await response.json(); - else - return await response.text(); -} - -async function postTacAPI(url, body = null) { - let response = await fetch(url, { - method: "POST", - headers: {'Content-Type': 'application/json'}, - body: body - }); - - if (response.status != 200) { - console.error(`Error posting to API endpoint "${url}": ` + response.status, response.statusText); - return null; } - return await response.json(); -} - -async function putTacAPI(url, body = null) { - let response = await fetch(url, { method: "PUT", body: body }); - - if (response.status != 200) { - console.error(`Error putting to API endpoint "${url}": ` + response.status, response.statusText); - return null; - } - - return await response.json(); -} - -// Extra network preview thumbnails -async function getTacExtraNetworkPreviewURL(filename, type) { - const previewJSON = await fetchTacAPI(`tacapi/v1/thumb-preview/${filename}?type=${type}`, true, true); - if (previewJSON?.url) { - const properURL = `sd_extra_networks/thumb?filename=${previewJSON.url}`; - if ((await fetch(properURL)).status == 200) { - return properURL; + /** + * Get a preview image URL for a given extra network file. + * Uses the official webui endpoint if available, otherwise creates a blob URL. + * @param {String} filename - The filename of the extra network file + * @param {String} type - One of "embed", "hyper", "lora", or "lyco", to determine the lookup location + * @returns {Promise} URL to a preview image for the extra network file, if available + */ + static async getExtraNetworkPreviewURL(filename, type) { + const previewJSON = await this.fetchAPI(`tacapi/v1/thumb-preview/${filename}?type=${type}`, true, true); + if (previewJSON?.url) { + const properURL = `sd_extra_networks/thumb?filename=${previewJSON.url}`; + if ((await fetch(properURL)).status == 200) { + return properURL; + } else { + // create blob url + const blob = await (await fetch(`tacapi/v1/thumb-preview-blob/${filename}?type=${type}`)).blob(); + return URL.createObjectURL(blob); + } } else { - // create blob url - const blob = await (await fetch(`tacapi/v1/thumb-preview-blob/${filename}?type=${type}`)).blob(); - return URL.createObjectURL(blob); + return null; } - } else { - return null; } -} -lastStyleRefresh = 0; -// Refresh style file if needed -async function refreshStyleNamesIfChanged() { - // Only refresh once per second - currentTimestamp = new Date().getTime(); - if (currentTimestamp - lastStyleRefresh < 1000) return; - lastStyleRefresh = currentTimestamp; + static lastStyleRefresh = 0; + /** + * Refreshes the styles.txt file if it has changed since the last check. + * Checks at most once per second to prevent spamming the API. + */ + static async refreshStyleNamesIfChanged() { + // Only refresh once per second + let currentTimestamp = new Date().getTime(); + if (currentTimestamp - lastStyleRefresh < 1000) return; + this.lastStyleRefresh = currentTimestamp; - const response = await fetch(`tacapi/v1/refresh-styles-if-changed?${new Date().getTime()}`) - if (response.status === 304) { - // Not modified - } else if (response.status === 200) { - // Reload - QUEUE_FILE_LOAD.forEach(async fn => { - if (fn.toString().includes("styleNames")) - await fn.call(null, true); - }) - } else { - // Error - console.error(`Error refreshing styles.txt: ` + response.status, response.statusText); + const response = await fetch(`tacapi/v1/refresh-styles-if-changed?${new Date().getTime()}`) + if (response.status === 304) { + // Not modified + } else if (response.status === 200) { + // Reload + QUEUE_FILE_LOAD.forEach(async fn => { + if (fn.toString().includes("styleNames")) + await fn.call(null, true); + }) + } else { + // Error + console.error(`Error refreshing styles.txt: ` + response.status, response.statusText); + } } -} -// Debounce function to prevent spamming the autocomplete function -var dbTimeOut; -const debounce = (func, wait = 300) => { - return function (...args) { - if (dbTimeOut) { - clearTimeout(dbTimeOut); + static dbTimeOut; + /** + * Generic debounce function to prevent spamming the autocompletion during fast typing + * @param {Function} func - The function to debounce + * @param {Number} wait - The debounce time in milliseconds + * @returns {Function} The debounced function + */ + static debounce = (func, wait = 300) => { + return function (...args) { + if (this.dbTimeOut) { + clearTimeout(this.dbTimeOut); + } + + this.dbTimeOut = setTimeout(() => { + func.apply(this, args); + }, wait); + } + } + + /** + * Calculates the difference between two arrays (order-sensitive). + * Fixes duplicates not being seen as changes in a normal filter function. + * @param {Array} a + * @param {Array} b + * @returns {Array} The difference between the two arrays + */ + static difference(a, b) { + if (a.length == 0) { + return b; + } + if (b.length == 0) { + return a; } - dbTimeOut = setTimeout(() => { - func.apply(this, args); - }, wait); - } -} - -// Difference function to fix duplicates not being seen as changes in normal filter -function difference(a, b) { - if (a.length == 0) { - return b; - } - if (b.length == 0) { - return a; + return [...b.reduce((acc, v) => acc.set(v, (acc.get(v) || 0) - 1), + a.reduce((acc, v) => acc.set(v, (acc.get(v) || 0) + 1), new Map()) + )].reduce((acc, [v, count]) => acc.concat(Array(Math.abs(count)).fill(v)), []); } - return [...b.reduce((acc, v) => acc.set(v, (acc.get(v) || 0) - 1), - a.reduce((acc, v) => acc.set(v, (acc.get(v) || 0) + 1), new Map()) - )].reduce((acc, [v, count]) => acc.concat(Array(Math.abs(count)).fill(v)), []); -} - -// Object flatten function adapted from https://stackoverflow.com/a/61602592 -// $roots keeps previous parent properties as they will be added as a prefix for each prop. -// $sep is just a preference if you want to seperate nested paths other than dot. -function flatten(obj, roots = [], sep = ".") { - return Object.keys(obj).reduce( - (memo, prop) => - Object.assign( - // create a new object - {}, - // include previously returned object - memo, - Object.prototype.toString.call(obj[prop]) === "[object Object]" - ? // keep working if value is an object - flatten(obj[prop], roots.concat([prop]), sep) - : // include current prop and value and prefix prop with the roots - { [roots.concat([prop]).join(sep)]: obj[prop] } - ), - {} - ); -} - -// Calculate biased tag score based on post count and frequent usage -function calculateUsageBias(result, count, uses) { - // Check setting conditions - if (uses < TAC_CFG.frequencyMinCount) { - uses = 0; - } else if (uses != 0) { - result.usageBias = true; + /** + * Object flatten function adapted from https://stackoverflow.com/a/61602592 + * @param {*} obj - The object to flatten + * @param {Array} roots - Keeps previous parent properties as they will be added as a prefix for each prop. + * @param {String} sep - Just a preference if you want to seperate nested paths other than dot. + * @returns The flattened object + */ + static flatten(obj, roots = [], sep = ".") { + return Object.keys(obj).reduce( + (memo, prop) => + Object.assign( + // create a new object + {}, + // include previously returned object + memo, + Object.prototype.toString.call(obj[prop]) === "[object Object]" + ? // keep working if value is an object + flatten(obj[prop], roots.concat([prop]), sep) + : // include current prop and value and prefix prop with the roots + { [roots.concat([prop]).join(sep)]: obj[prop] } + ), + {} + ); } - switch (TAC_CFG.frequencyFunction) { - case "Logarithmic (weak)": - return Math.log(1 + count) + Math.log(1 + uses); - case "Logarithmic (strong)": - return Math.log(1 + count) + 2 * Math.log(1 + uses); - case "Usage first": - return uses; - default: - return count; + /** + * Calculate biased tag score based on post count and frequent usage + * @param {AutocompleteResult} result - The unbiased result + * @param {Number} count - The post count (or similar base metric) + * @param {Number} uses - The usage count + * @returns {Number} The biased score for sorting + */ + static calculateUsageBias(result, count, uses) { + // Check setting conditions + if (uses < TAC_CFG.frequencyMinCount) { + uses = 0; + } else if (uses != 0) { + result.usageBias = true; + } + + switch (TAC_CFG.frequencyFunction) { + case "Logarithmic (weak)": + return Math.log(1 + count) + Math.log(1 + uses); + case "Logarithmic (strong)": + return Math.log(1 + count) + 2 * Math.log(1 + uses); + case "Usage first": + return uses; + default: + return count; + } } -} -// Beautify return type for easier parsing -function mapUseCountArray(useCounts, posAndNeg = false) { - return useCounts.map(useCount => { - if (posAndNeg) { + /** + * Utility function to map the use count array from the database to a more readable format, + * since FastAPI omits the field names in the response. + * @param {Array} useCounts + * @param {Boolean} posAndNeg - Whether to include negative counts + */ + static mapUseCountArray(useCounts, posAndNeg = false) { + return useCounts.map(useCount => { + if (posAndNeg) { + return { + "name": useCount[0], + "type": useCount[1], + "count": useCount[2], + "negCount": useCount[3], + "lastUseDate": useCount[4] + } + } return { "name": useCount[0], "type": useCount[1], "count": useCount[2], - "negCount": useCount[3], - "lastUseDate": useCount[4] + "lastUseDate": useCount[3] } - } - return { - "name": useCount[0], - "type": useCount[1], - "count": useCount[2], - "lastUseDate": useCount[3] - } - }); -} -// Call API endpoint to increase bias of tag in the database -function increaseUseCount(tagName, type, negative = false) { - postTacAPI(`tacapi/v1/increase-use-count?tagname=${tagName}&ttype=${type}&neg=${negative}`); -} -// Get use count of tag from the database -async function getUseCount(tagName, type, negative = false) { - return (await fetchTacAPI(`tacapi/v1/get-use-count?tagname=${tagName}&ttype=${type}&neg=${negative}`, true, false))["result"]; -} -async function getUseCounts(tagNames, types, negative = false) { - // While semantically weird, we have to use POST here for the body, as urls are limited in length - const body = JSON.stringify({"tagNames": tagNames, "tagTypes": types, "neg": negative}); - const rawArray = (await postTacAPI(`tacapi/v1/get-use-count-list`, body))["result"] - return mapUseCountArray(rawArray); -} -async function getAllUseCounts() { - const rawArray = (await fetchTacAPI(`tacapi/v1/get-all-use-counts`))["result"]; - return mapUseCountArray(rawArray, true); -} -async function resetUseCount(tagName, type, resetPosCount, resetNegCount) { - await putTacAPI(`tacapi/v1/reset-use-count?tagname=${tagName}&ttype=${type}&pos=${resetPosCount}&neg=${resetNegCount}`); -} - -function createTagUsageTable(tagCounts) { - // Create table - let tagTable = document.createElement("table"); - tagTable.innerHTML = - ` - - Name - Type - Count(+) - Count(-) - Last used - - `; - tagTable.id = "tac_tagUsageTable" - - tagCounts.forEach(t => { - let tr = document.createElement("tr"); - - // Fill values - let values = [t.name, t.type-1, t.count, t.negCount, t.lastUseDate] - values.forEach(v => { - let td = document.createElement("td"); - td.innerText = v; - tr.append(td); }); - // Add delete/reset button - let delButton = document.createElement("button"); - delButton.innerText = "🗑️"; - delButton.title = "Reset count"; - tr.append(delButton); - - tagTable.append(tr) - }); - - return tagTable; -} - -// Sliding window function to get possible combination groups of an array -function toNgrams(inputArray, size) { - return Array.from( - { length: inputArray.length - (size - 1) }, //get the appropriate length - (_, index) => inputArray.slice(index, index + size) //create the windows - ); -} - -function escapeRegExp(string, wildcardMatching = false) { - if (wildcardMatching) { - // Escape all characters except asterisks and ?, which should be treated separately as placeholders. - return string.replace(/[-[\]{}()+.,\\^$|#\s]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.'); } - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string -} -function escapeHTML(unsafeText) { - let div = document.createElement('div'); - div.textContent = unsafeText; - return div.innerHTML; -} - -// For black/whitelisting -function updateModelName() { - let sdm = gradioApp().querySelector("#setting_sd_model_checkpoint"); - let modelDropdown = sdm?.querySelector("input") || sdm?.querySelector("select"); - if (modelDropdown) { - currentModelName = modelDropdown.value; - } else { - // Fallback for intermediate versions - modelDropdown = sdm?.querySelector("span.single-select"); - currentModelName = modelDropdown?.textContent || ""; + /** + * Calls API endpoint to increase the count of a tag in the database. + * Not awaited as it is non-critical and can be executed as fire-and-forget. + * @param {String} tagName - The name of the tag + * @param {ResultType} type - The type of the tag as mapped in {@link ResultType} + * @param {Boolean} negative - Whether the tag was typed in a negative prompt field + */ + static increaseUseCount(tagName, type, negative = false) { + this.postAPI(`tacapi/v1/increase-use-count?tagname=${tagName}&ttype=${type}&neg=${negative}`); } -} -// From https://stackoverflow.com/a/61975440, how to detect JS value changes -function observeElement(element, property, callback, delay = 0) { - let elementPrototype = Object.getPrototypeOf(element); - if (elementPrototype.hasOwnProperty(property)) { - let descriptor = Object.getOwnPropertyDescriptor(elementPrototype, property); - Object.defineProperty(element, property, { - get: function() { - return descriptor.get.apply(this, arguments); - }, - set: function () { - let oldValue = this[property]; - descriptor.set.apply(this, arguments); - let newValue = this[property]; - if (typeof callback == "function") { - setTimeout(callback.bind(this, oldValue, newValue), delay); + /** + * Get the use count of a tag from the database + * @param {String} tagName - The name of the tag + * @param {ResultType} type - The type of the tag as mapped in {@link ResultType} + * @param {Boolean} negative - Whether we are currently in a negative prompt field + * @returns {Promise} The use count of the tag + */ + static async getUseCount(tagName, type, negative = false) { + return (await this.fetchAPI(`tacapi/v1/get-use-count?tagname=${tagName}&ttype=${type}&neg=${negative}`, true, false))["result"]; + } + /** + * Retrieves the use counts of multiple tags at once from the database for improved performance + * during typing. + * @param {String[]} tagNames - An array of tag names + * @param {ResultType[]} types - An array of tag types as mapped in {@link ResultType} + * @param {Boolean} negative - Whether we are currently in a negative prompt field + * @returns {Promise} The use count array mapped to named fields by {@link mapUseCountArray} + */ + static async getUseCounts(tagNames, types, negative = false) { + // While semantically weird, we have to use POST here for the body, as urls are limited in length + const body = JSON.stringify({"tagNames": tagNames, "tagTypes": types, "neg": negative}); + const rawArray = (await this.postAPI(`tacapi/v1/get-use-count-list`, body))["result"] + return this.mapUseCountArray(rawArray); + } + /** + * Gets all use counts existing in the database. + * @returns {Array} The use count array mapped to named fields by {@link mapUseCountArray} + */ + static async getAllUseCounts() { + const rawArray = (await this.fetchAPI(`tacapi/v1/get-all-use-counts`))["result"]; + return this.mapUseCountArray(rawArray, true); + } + /** + * Resets the use count of the given tag back to zero. + * @param {String} tagName - The name of the tag + * @param {ResultType} type - The type of the tag as mapped in {@link ResultType} + * @param {Boolean} resetPosCount - Whether to reset the positive count + * @param {Boolean} resetNegCount - Whether to reset the negative count + */ + static async resetUseCount(tagName, type, resetPosCount, resetNegCount) { + await TacUtils.putAPI(`tacapi/v1/reset-use-count?tagname=${tagName}&ttype=${type}&pos=${resetPosCount}&neg=${resetNegCount}`); + } + + /** + * Creates a table to display an overview of tag usage statistics. + * Currently unused. + * @param {Array} tagCounts - The use count array to use, mapped to named fields by {@link mapUseCountArray} + * @returns + */ + static createTagUsageTable(tagCounts) { + // Create table + let tagTable = document.createElement("table"); + tagTable.innerHTML = + ` + + Name + Type + Count(+) + Count(-) + Last used + + `; + tagTable.id = "tac_tagUsageTable" + + tagCounts.forEach(t => { + let tr = document.createElement("tr"); + + // Fill values + let values = [t.name, t.type-1, t.count, t.negCount, t.lastUseDate] + values.forEach(v => { + let td = document.createElement("td"); + td.innerText = v; + tr.append(td); + }); + // Add delete/reset button + let delButton = document.createElement("button"); + delButton.innerText = "🗑️"; + delButton.title = "Reset count"; + tr.append(delButton); + + tagTable.append(tr) + }); + + return tagTable; + } + + /** + * Sliding window function to get possible combination groups of an array + * @param {Array} inputArray + * @param {Number} size + * @returns {Array[]} ngram permutations of the input array + */ + static toNgrams(inputArray, size) { + return Array.from( + { length: inputArray.length - (size - 1) }, //get the appropriate length + (_, index) => inputArray.slice(index, index + size) //create the windows + ); + } + + /** + * Escapes a string for use in a regular expression. + * @param {String} string + * @param {Boolean} wildcardMatching - Wildcard matching mode doesn't escape asterisks and question marks as they are handled separately there. + * @returns {String} The escaped string + */ + static escapeRegExp(string, wildcardMatching = false) { + if (wildcardMatching) { + // Escape all characters except asterisks and ?, which should be treated separately as placeholders. + return string.replace(/[-[\]{}()+.,\\^$|#\s]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.'); + } + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string + } + /** + * Escapes a string for use in HTML to not break formatting. + * @param {String} unsafeText + * @returns {String} The escaped HTML string + */ + static escapeHTML(unsafeText) { + let div = document.createElement('div'); + div.textContent = unsafeText; + return div.innerHTML; + } + + /** Updates {@link currentModelName} to the current model */ + static updateModelName() { + let sdm = gradioApp().querySelector("#setting_sd_model_checkpoint"); + let modelDropdown = sdm?.querySelector("input") || sdm?.querySelector("select"); + if (modelDropdown) { + currentModelName = modelDropdown.value; + } else { + // Fallback for intermediate versions + modelDropdown = sdm?.querySelector("span.single-select"); + currentModelName = modelDropdown?.textContent || ""; + } + } + + /** + * From https://stackoverflow.com/a/61975440. + * Detects value changes in an element that were triggered programmatically + * @param {HTMLElement} element - The DOM element to observe + * @param {String} property - The object property to observe + * @param {Function} callback - The callback function to call when the property changes + * @param {Number} delay - The delay in milliseconds to wait before calling the callback + */ + static observeElement(element, property, callback, delay = 0) { + let elementPrototype = Object.getPrototypeOf(element); + if (elementPrototype.hasOwnProperty(property)) { + let descriptor = Object.getOwnPropertyDescriptor(elementPrototype, property); + Object.defineProperty(element, property, { + get: function() { + return descriptor.get.apply(this, arguments); + }, + set: function () { + let oldValue = this[property]; + descriptor.set.apply(this, arguments); + let newValue = this[property]; + if (typeof callback == "function") { + setTimeout(callback.bind(this, oldValue, newValue), delay); + } + return newValue; } - return newValue; + }); + } + } + + /** + * Returns a matching sort function based on the current configuration + * @returns {((a: any, b: any) => number)} + */ + static getSortFunction() { + let criterion = TAC_CFG.modelSortOrder || "Name"; + + const textSort = (a, b, reverse = false) => { + // Assign keys so next sort is faster + if (!a.sortKey) { + a.sortKey = a.type === ResultType.chant + ? a.aliases + : a.text; + } + if (!b.sortKey) { + b.sortKey = b.type === ResultType.chant + ? b.aliases + : b.text; } - }); - } -} -// Sort functions -function getSortFunction() { - let criterion = TAC_CFG.modelSortOrder || "Name"; - - const textSort = (a, b, reverse = false) => { - // Assign keys so next sort is faster - if (!a.sortKey) { - a.sortKey = a.type === ResultType.chant - ? a.aliases - : a.text; + return reverse ? b.sortKey.localeCompare(a.sortKey) : a.sortKey.localeCompare(b.sortKey); } - if (!b.sortKey) { - b.sortKey = b.type === ResultType.chant - ? b.aliases - : b.text; + const numericSort = (a, b, reverse = false) => { + const noKey = reverse ? "-1" : Number.MAX_SAFE_INTEGER; + let aParsed = parseFloat(a.sortKey || noKey); + let bParsed = parseFloat(b.sortKey || noKey); + + if (aParsed === bParsed) { + return textSort(a, b, false); + } + + return reverse ? bParsed - aParsed : aParsed - bParsed; } - return reverse ? b.sortKey.localeCompare(a.sortKey) : a.sortKey.localeCompare(b.sortKey); - } - const numericSort = (a, b, reverse = false) => { - const noKey = reverse ? "-1" : Number.MAX_SAFE_INTEGER; - let aParsed = parseFloat(a.sortKey || noKey); - let bParsed = parseFloat(b.sortKey || noKey); - - if (aParsed === bParsed) { - return textSort(a, b, false); - } - - return reverse ? bParsed - aParsed : aParsed - bParsed; - } - - return (a, b) => { - switch (criterion) { - case "Date Modified (newest first)": - return numericSort(a, b, true); - case "Date Modified (oldest first)": - return numericSort(a, b, false); - default: - return textSort(a, b); + return (a, b) => { + switch (criterion) { + case "Date Modified (newest first)": + return numericSort(a, b, true); + case "Date Modified (oldest first)": + return numericSort(a, b, false); + default: + return textSort(a, b); + } } } -} -// Queue calling function to process global queues -async function processQueue(queue, context, ...args) { - for (let i = 0; i < queue.length; i++) { - await queue[i].call(context, ...args); + /** + * Queue calling function to process global queues + * @param {Array} queue - The queue to process + * @param {*} context - The context to call the functions in (null for global) + * @param {...any} args - Arguments to pass to the functions + */ + static async processQueue(queue, context, ...args) { + for (let i = 0; i < queue.length; i++) { + await queue[i].call(context, ...args); + } } -} -// The same but with return values -async function processQueueReturn(queue, context, ...args) -{ - let qeueueReturns = []; - for (let i = 0; i < queue.length; i++) { - let returnValue = await queue[i].call(context, ...args); - if (returnValue) - qeueueReturns.push(returnValue); - } - return qeueueReturns; -} -// Specific to tag completion parsers -async function processParsers(textArea, prompt) { - // Get all parsers that have a successful trigger condition - let matchingParsers = PARSERS.filter(parser => parser.triggerCondition()); - // Guard condition - if (matchingParsers.length === 0) { - return null; + /** The same as {@link processQueue}, but can accept and return results from the queued functions. */ + static async processQueueReturn(queue, context, ...args) + { + let qeueueReturns = []; + for (let i = 0; i < queue.length; i++) { + let returnValue = await queue[i].call(context, ...args); + if (returnValue) + qeueueReturns.push(returnValue); + } + return qeueueReturns; } + /** + * A queue processing function specific to tag completion parsers + * @param {HTMLTextAreaElement} textArea - The current text area used by TAC + * @param {String} prompt - The current prompt + * @returns The results of the parsers + */ + static async processParsers(textArea, prompt) { + // Get all parsers that have a successful trigger condition + let matchingParsers = PARSERS.filter(parser => parser.triggerCondition()); + // Guard condition + if (matchingParsers.length === 0) { + return null; + } - let parseFunctions = matchingParsers.map(parser => parser.parse); - // Process them and return the results - return await processQueueReturn(parseFunctions, null, textArea, prompt); + let parseFunctions = matchingParsers.map(parser => parser.parse); + // Process them and return the results + return await this.processQueueReturn(parseFunctions, null, textArea, prompt); + } } \ No newline at end of file diff --git a/javascript/ext_chants.js b/javascript/ext_chants.js index c32cc85..6cba806 100644 --- a/javascript/ext_chants.js +++ b/javascript/ext_chants.js @@ -8,7 +8,7 @@ class ChantParser extends BaseTagParser { if (tagword !== "<" && tagword !== " { - let regex = new RegExp(escapeRegExp(searchTerm, true), 'i'); + let regex = new RegExp(TacUtils.escapeRegExp(searchTerm, true), 'i'); return regex.test(x.terms.toLowerCase()) || regex.test(x.name.toLowerCase()); }; tempResults = chants.filter(x => filterCondition(x)); // Filter by tagword @@ -33,7 +33,7 @@ class ChantParser extends BaseTagParser { async function load() { if (TAC_CFG.chantFile && TAC_CFG.chantFile !== "None") { try { - chants = await readFile(`${tagBasePath}/${TAC_CFG.chantFile}?`, true); + chants = await TacUtils.readFile(`${tagBasePath}/${TAC_CFG.chantFile}?`, true); } catch (e) { console.error("Error loading chants.json: " + e); } diff --git a/javascript/ext_embeddings.js b/javascript/ext_embeddings.js index bc0ade8..b1ff607 100644 --- a/javascript/ext_embeddings.js +++ b/javascript/ext_embeddings.js @@ -17,7 +17,7 @@ class EmbeddingParser extends BaseTagParser { } let filterCondition = x => { - let regex = new RegExp(escapeRegExp(searchTerm, true), 'i'); + let regex = new RegExp(TacUtils.escapeRegExp(searchTerm, true), 'i'); return regex.test(x[0].toLowerCase()) || regex.test(x[0].toLowerCase().replaceAll(" ", "_")); }; @@ -49,7 +49,7 @@ class EmbeddingParser extends BaseTagParser { async function load() { if (embeddings.length === 0) { try { - embeddings = (await loadCSV(`${tagBasePath}/temp/emb.txt`)) + embeddings = (await TacUtils.loadCSV(`${tagBasePath}/temp/emb.txt`)) .filter(x => x[0]?.trim().length > 0) // Remove empty lines .map(x => [x[0].trim(), x[1], x[2]]); // Return name, sortKey, hash tuples } catch (e) { diff --git a/javascript/ext_hypernets.js b/javascript/ext_hypernets.js index 8d6031a..b2851ba 100644 --- a/javascript/ext_hypernets.js +++ b/javascript/ext_hypernets.js @@ -8,7 +8,7 @@ class HypernetParser extends BaseTagParser { if (tagword !== "<" && tagword !== " { - let regex = new RegExp(escapeRegExp(searchTerm, true), 'i'); + let regex = new RegExp(TacUtils.escapeRegExp(searchTerm, true), 'i'); return regex.test(x.toLowerCase()) || regex.test(x.toLowerCase().replaceAll(" ", "_")); }; tempResults = hypernetworks.filter(x => filterCondition(x[0])); // Filter by tagword @@ -32,7 +32,7 @@ class HypernetParser extends BaseTagParser { async function load() { if (hypernetworks.length === 0) { try { - hypernetworks = (await loadCSV(`${tagBasePath}/temp/hyp.txt`)) + hypernetworks = (await TacUtils.loadCSV(`${tagBasePath}/temp/hyp.txt`)) .filter(x => x[0]?.trim().length > 0) //Remove empty lines .map(x => [x[0]?.trim(), x[1]]); // Remove carriage returns and padding if it exists } catch (e) { diff --git a/javascript/ext_loras.js b/javascript/ext_loras.js index fc67a3e..e8a9d91 100644 --- a/javascript/ext_loras.js +++ b/javascript/ext_loras.js @@ -8,7 +8,7 @@ class LoraParser extends BaseTagParser { if (tagword !== "<" && tagword !== " { - let regex = new RegExp(escapeRegExp(searchTerm, true), 'i'); + let regex = new RegExp(TacUtils.escapeRegExp(searchTerm, true), 'i'); return regex.test(x.toLowerCase()) || regex.test(x.toLowerCase().replaceAll(" ", "_")); }; tempResults = loras.filter(x => filterCondition(x[0])); // Filter by tagword @@ -38,7 +38,7 @@ class LoraParser extends BaseTagParser { async function load() { if (loras.length === 0) { try { - loras = (await loadCSV(`${tagBasePath}/temp/lora.txt`)) + loras = (await TacUtils.loadCSV(`${tagBasePath}/temp/lora.txt`)) .filter(x => x[0]?.trim().length > 0) // Remove empty lines .map(x => [x[0]?.trim(), x[1], x[2]]); // Trim filenames and return the name, sortKey, hash pairs } catch (e) { @@ -50,7 +50,7 @@ async function load() { async function sanitize(tagType, text) { if (tagType === ResultType.lora) { let multiplier = TAC_CFG.extraNetworksDefaultMultiplier; - let info = await fetchTacAPI(`tacapi/v1/lora-info/${text}`) + let info = await TacUtils.fetchAPI(`tacapi/v1/lora-info/${text}`) if (info && info["preferred weight"]) { multiplier = info["preferred weight"]; } diff --git a/javascript/ext_lycos.js b/javascript/ext_lycos.js index 1342efd..8224edf 100644 --- a/javascript/ext_lycos.js +++ b/javascript/ext_lycos.js @@ -8,7 +8,7 @@ class LycoParser extends BaseTagParser { if (tagword !== "<" && tagword !== " { - let regex = new RegExp(escapeRegExp(searchTerm, true), 'i'); + let regex = new RegExp(TacUtils.escapeRegExp(searchTerm, true), 'i'); return regex.test(x.toLowerCase()) || regex.test(x.toLowerCase().replaceAll(" ", "_")); }; tempResults = lycos.filter(x => filterCondition(x[0])); // Filter by tagword @@ -38,7 +38,7 @@ class LycoParser extends BaseTagParser { async function load() { if (lycos.length === 0) { try { - lycos = (await loadCSV(`${tagBasePath}/temp/lyco.txt`)) + lycos = (await TacUtils.loadCSV(`${tagBasePath}/temp/lyco.txt`)) .filter(x => x[0]?.trim().length > 0) // Remove empty lines .map(x => [x[0]?.trim(), x[1], x[2]]); // Trim filenames and return the name, sortKey, hash pairs } catch (e) { @@ -50,7 +50,7 @@ async function load() { async function sanitize(tagType, text) { if (tagType === ResultType.lyco) { let multiplier = TAC_CFG.extraNetworksDefaultMultiplier; - let info = await fetchTacAPI(`tacapi/v1/lyco-info/${text}`) + let info = await TacUtils.fetchAPI(`tacapi/v1/lyco-info/${text}`) if (info && info["preferred weight"]) { multiplier = info["preferred weight"]; } diff --git a/javascript/ext_modelKeyword.js b/javascript/ext_modelKeyword.js index ac88747..570a010 100644 --- a/javascript/ext_modelKeyword.js +++ b/javascript/ext_modelKeyword.js @@ -1,5 +1,5 @@ async function load() { - let modelKeywordParts = (await readFile(`tmp/modelKeywordPath.txt`)).split(",") + let modelKeywordParts = (await TacUtils.readFile(`tmp/modelKeywordPath.txt`)).split(",") modelKeywordPath = modelKeywordParts[0]; let customFileExists = modelKeywordParts[1] === "True"; @@ -8,10 +8,10 @@ async function load() { let csv_lines = []; // Only add default keywords if wanted by the user if (TAC_CFG.modelKeywordCompletion !== "Only user list") - csv_lines = (await loadCSV(`${modelKeywordPath}/lora-keyword.txt`)); + csv_lines = (await TacUtils.loadCSV(`${modelKeywordPath}/lora-keyword.txt`)); // Add custom user keywords if the file exists if (customFileExists) - csv_lines = csv_lines.concat((await loadCSV(`${modelKeywordPath}/lora-keyword-user.txt`))); + csv_lines = csv_lines.concat((await TacUtils.loadCSV(`${modelKeywordPath}/lora-keyword-user.txt`))); if (csv_lines.length === 0) return; diff --git a/javascript/ext_styles.js b/javascript/ext_styles.js index a12edf5..a22ee8d 100644 --- a/javascript/ext_styles.js +++ b/javascript/ext_styles.js @@ -6,7 +6,7 @@ var lastStyleVarIndex = ""; class StyleParser extends BaseTagParser { async parse() { // Refresh if needed - await refreshStyleNamesIfChanged(); + await TacUtils.refreshStyleNamesIfChanged(); // Show styles let tempResults = []; @@ -19,7 +19,7 @@ class StyleParser extends BaseTagParser { let searchTerm = tagword.replace(matchGroups[1], ""); let filterCondition = x => { - let regex = new RegExp(escapeRegExp(searchTerm, true), 'i'); + let regex = new RegExp(TacUtils.escapeRegExp(searchTerm, true), 'i'); return regex.test(x[0].toLowerCase()) || regex.test(x[0].toLowerCase().replaceAll(" ", "_")); }; tempResults = styleNames.filter(x => filterCondition(x)); // Filter by tagword @@ -42,7 +42,7 @@ class StyleParser extends BaseTagParser { async function load(force = false) { if (styleNames.length === 0 || force) { try { - styleNames = (await loadCSV(`${tagBasePath}/temp/styles.txt`)) + styleNames = (await TacUtils.loadCSV(`${tagBasePath}/temp/styles.txt`)) .filter(x => x[0]?.trim().length > 0) // Remove empty lines .filter(x => x[0] !== "None") // Remove "None" style .map(x => [x[0].trim()]); // Trim name diff --git a/javascript/ext_umi.js b/javascript/ext_umi.js index ea5067a..bc1d6e9 100644 --- a/javascript/ext_umi.js +++ b/javascript/ext_umi.js @@ -117,7 +117,7 @@ class UmiParser extends BaseTagParser { if (umiTags.length > 0) { // Get difference for subprompt let tagCountChange = umiTags.length - umiPreviousTags.length; - let diff = difference(umiTags, umiPreviousTags); + let diff = TacUtils.difference(umiTags, umiPreviousTags); umiPreviousTags = umiTags; // Show all condition @@ -136,7 +136,7 @@ class UmiParser extends BaseTagParser { originalTagword = tagword; tagword = umiTagword; let filteredWildcardsSorted = filteredWildcards(umiTagword); - let searchRegex = new RegExp(`(^|[^a-zA-Z])${escapeRegExp(umiTagword)}`, 'i') + let searchRegex = new RegExp(`(^|[^a-zA-Z])${TacUtils.escapeRegExp(umiTagword)}`, 'i') let baseFilter = x => x[0].toLowerCase().search(searchRegex) > -1; let spaceIncludeFilter = x => x[0].toLowerCase().replaceAll(" ", "_").search(searchRegex) > -1; tempResults = filteredWildcardsSorted.filter(x => baseFilter(x) || spaceIncludeFilter(x)) // Filter by tagword @@ -210,7 +210,7 @@ function updateUmiTags(tagType, sanitizedText, newPrompt, textArea) { async function load() { if (umiWildcards.length === 0) { try { - let umiTags = (await readFile(`${tagBasePath}/temp/umi_tags.txt`)).split("\n"); + let umiTags = (await TacUtils.readFile(`${tagBasePath}/temp/umi_tags.txt`)).split("\n"); // Split into tag, count pairs umiWildcards = umiTags.map(x => x .trim() diff --git a/javascript/ext_wildcards.js b/javascript/ext_wildcards.js index 515e982..e95d718 100644 --- a/javascript/ext_wildcards.js +++ b/javascript/ext_wildcards.js @@ -38,7 +38,7 @@ class WildcardParser extends BaseTagParser { } wildcards = wildcards.concat(getDescendantProp(yamlWildcards[basePath], fileName)); } else { - const fileContent = (await fetchTacAPI(`tacapi/v1/wildcard-contents?basepath=${basePath}&filename=${fileName}.txt`, false)) + const fileContent = (await TacUtils.fetchAPI(`tacapi/v1/wildcard-contents?basepath=${basePath}&filename=${fileName}.txt`, false)) .split("\n") .filter(x => x.trim().length > 0 && !x.startsWith('#')); // Remove empty lines and comments wildcards = wildcards.concat(fileContent); @@ -92,7 +92,7 @@ class WildcardFileParser extends BaseTagParser { alreadyAdded.set(wcFile[1], true); }); - finalResults.sort(getSortFunction()); + finalResults.sort(TacUtils.getSortFunction()); return finalResults; } @@ -101,7 +101,7 @@ class WildcardFileParser extends BaseTagParser { async function load() { if (wildcardFiles.length === 0 && wildcardExtFiles.length === 0) { try { - let wcFileArr = await loadCSV(`${tagBasePath}/temp/wc.txt`); + let wcFileArr = await TacUtils.loadCSV(`${tagBasePath}/temp/wc.txt`); if (wcFileArr && wcFileArr.length > 0) { let wcBasePath = wcFileArr[0][0].trim(); // First line should be the base path wildcardFiles = wcFileArr.slice(1) @@ -110,7 +110,7 @@ async function load() { } // To support multiple sources, we need to separate them using the provided "-----" strings - let wcExtFileArr = await loadCSV(`${tagBasePath}/temp/wce.txt`); + let wcExtFileArr = await TacUtils.loadCSV(`${tagBasePath}/temp/wce.txt`); let splitIndices = []; for (let index = 0; index < wcExtFileArr.length; index++) { if (wcExtFileArr[index][0].trim() === "-----") { @@ -134,11 +134,11 @@ async function load() { } // Load the yaml wildcard json file and append it as a wildcard file, appending each key as a path component until we reach the end - yamlWildcards = await readFile(`${tagBasePath}/temp/wc_yaml.json`, true); + yamlWildcards = await TacUtils.readFile(`${tagBasePath}/temp/wc_yaml.json`, true); // Append each key as a path component until we reach a leaf Object.keys(yamlWildcards).forEach(file => { - const flattened = flatten(yamlWildcards[file], [], "/"); + const flattened = TacUtils.flatten(yamlWildcards[file], [], "/"); Object.keys(flattened).forEach(key => { wildcardExtFiles.push([file, key]); }); diff --git a/javascript/tagAutocomplete.js b/javascript/tagAutocomplete.js index 35b01e1..a2d86c1 100644 --- a/javascript/tagAutocomplete.js +++ b/javascript/tagAutocomplete.js @@ -151,7 +151,7 @@ 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}`); + allTags = await TacUtils.loadCSV(`${tagBasePath}/${c.tagFile}`); } catch (e) { console.error("Error loading tags file: " + e); return; @@ -163,7 +163,7 @@ async function loadTags(c) { async function loadExtraTags(c) { if (c.extra.extraFile && c.extra.extraFile !== "None") { try { - extras = await loadCSV(`${tagBasePath}/${c.extra.extraFile}`); + extras = await TacUtils.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]); @@ -178,7 +178,7 @@ async function loadExtraTags(c) { async function loadTranslations(c) { if (c.translation.translationFile && c.translation.translationFile !== "None") { try { - let tArray = await loadCSV(`${tagBasePath}/${c.translation.translationFile}`); + let tArray = await TacUtils.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]); @@ -313,7 +313,7 @@ async function syncOptions() { TAC_CFG = newCFG; // Callback - await processQueue(QUEUE_AFTER_CONFIG_CHANGE, null); + await TacUtils.processQueue(QUEUE_AFTER_CONFIG_CHANGE, null); } // Create the result list div and necessary styling @@ -424,7 +424,7 @@ async function insertTextAtCursor(textArea, result, tagword, tabCompletedWithout var sanitizedText = text // Run sanitize queue and use first result as sanitized text - sanitizeResults = await processQueueReturn(QUEUE_SANITIZE, null, tagType, text); + sanitizeResults = await TacUtils.processQueueReturn(QUEUE_SANITIZE, null, tagType, text); if (sanitizeResults && sanitizeResults.length > 0) { sanitizedText = sanitizeResults[0]; @@ -445,12 +445,12 @@ async function insertTextAtCursor(textArea, result, tagword, tabCompletedWithout && TAC_CFG.wildcardCompletionMode !== "Always fully" && sanitizedText.includes("/")) { if (TAC_CFG.wildcardCompletionMode === "To next folder level") { - let regexMatch = sanitizedText.match(new RegExp(`${escapeRegExp(tagword)}([^/]*\\/?)`, "i")); + let regexMatch = sanitizedText.match(new RegExp(`${TacUtils.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]; + pathPart = sanitizedText.match(new RegExp(`${TacUtils.escapeRegExp(tagword)}\\/([^/]*\\/?)`, "i"))[0]; } sanitizedText = pathPart; } @@ -503,7 +503,7 @@ async function insertTextAtCursor(textArea, result, tagword, tabCompletedWithout // Sanitize name for API call name = encodeURIComponent(name) // Call API & update db - increaseUseCount(name, tagType, isNegative) + TacUtils.increaseUseCount(name, tagType, isNegative) } } @@ -513,7 +513,7 @@ async function insertTextAtCursor(textArea, result, tagword, tabCompletedWithout 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 match = surrounding.match(new RegExp(TacUtils.escapeRegExp(`${tagword}`), "i")); let afterInsertCursorPos = editStart + match.index + sanitizedText.length; var optionalSeparator = ""; @@ -521,7 +521,7 @@ async function insertTextAtCursor(textArea, result, tagword, tabCompletedWithout 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; + let beforeComma = surrounding.match(new RegExp(`${TacUtils.escapeRegExp(tagword)}[,:]`, "i")) !== null; if (TAC_CFG.appendComma) optionalSeparator = beforeComma ? "" : ","; // Add space if enabled @@ -529,7 +529,7 @@ async function insertTextAtCursor(textArea, result, tagword, tabCompletedWithout 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 ? " " : ""; + optionalSeparator += surrounding.match(new RegExp(`${TacUtils.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 || " "; @@ -552,7 +552,7 @@ async function insertTextAtCursor(textArea, result, tagword, tabCompletedWithout 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}`) + let info = await TacUtils.fetchAPI(`tacapi/v1/lora-info/${result.text}`) if (info && info["activation text"]) { keywords = info["activation text"]; } @@ -564,7 +564,7 @@ async function insertTextAtCursor(textArea, result, tagword, tabCompletedWithout // 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}`) + const sha256 = await TacUtils.fetchAPI(`/tacapi/v1/lora-cached-hash/${result.text}`) if (sha256) { nameDict = modelKeywordDict.get(sha256); } @@ -630,7 +630,7 @@ async function insertTextAtCursor(textArea, result, tagword, tabCompletedWithout previousTags = tags; // Callback - let returns = await processQueueReturn(QUEUE_AFTER_INSERT, null, tagType, sanitizedText, newPrompt, textArea); + let returns = await TacUtils.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; @@ -680,7 +680,7 @@ function addResultsToList(textArea, results, tagword, resetList) { 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); + displayText = TacUtils.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)); @@ -696,7 +696,7 @@ function addResultsToList(textArea, results, tagword, resetList) { } } - displayText = escapeHTML(bestAlias); + displayText = TacUtils.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) @@ -705,7 +705,7 @@ function addResultsToList(textArea, results, tagword, resetList) { if (!TAC_CFG.alias.onlyShowAlias && result.text !== bestAlias) displayText += " ➝ " + result.text; } else { // No alias - displayText = escapeHTML(result.text); + displayText = TacUtils.escapeHTML(result.text); } // Append translation for result if it exists @@ -821,7 +821,7 @@ function addResultsToList(textArea, results, tagword, resetList) { // Add listener li.addEventListener("click", (e) => { if (e.ctrlKey || e.metaKey) { - resetUseCount(result.text, result.type, !isNegative, isNegative); + TacUtils.resetUseCount(result.text, result.type, !isNegative, isNegative); flexDiv.querySelector(".acMetaText").classList.remove("biased"); } else { insertTextAtCursor(textArea, result, tagword); @@ -887,7 +887,7 @@ async function updateSelectionStyle(textArea, newIndex, oldIndex) { let img = previewDiv.querySelector("img"); - let url = await getTacExtraNetworkPreviewURL(selectedFilename, shorthandType); + let url = await TacUtils.getExtraNetworkPreviewURL(selectedFilename, shorthandType); if (url) { img.src = url; previewDiv.style.display = "block"; @@ -933,17 +933,17 @@ function updateRuby(textArea, prompt) { const translation = translations?.get(tag) || translations?.get(unsanitizedTag); - let escapedTag = escapeRegExp(tag); + let escapedTag = TacUtils.escapeRegExp(tag); return { tag, escapedTag, translation }; } const replaceOccurences = (text, tuple) => { let { tag, escapedTag, translation } = tuple; let searchRegex = new RegExp(`(?)(?:\\b)${escapedTag}(?:\\b|$|(?=[,|: \\t\\n\\r]))(?!)`, "g"); - return text.replaceAll(searchRegex, `${escapeHTML(tag)}${translation}`); + return text.replaceAll(searchRegex, `${TacUtils.escapeHTML(tag)}${translation}`); } - let html = escapeHTML(prompt); + let html = TacUtils.escapeHTML(prompt); // First try to find direct matches [...rubyTags].forEach(tag => { @@ -972,11 +972,11 @@ function updateRuby(textArea, prompt) { } // Perform n-gram sliding window search - translateNgram(toNgrams(subTags, 3)); - translateNgram(toNgrams(subTags, 2)); - translateNgram(toNgrams(subTags, 1)); + translateNgram(TacUtils.toNgrams(subTags, 3)); + translateNgram(TacUtils.toNgrams(subTags, 1)); + translateNgram(TacUtils.toNgrams(subTags, 2)); - let escapedTag = escapeRegExp(tuple.tag); + let escapedTag = TacUtils.escapeRegExp(tuple.tag); let searchRegex = new RegExp(`(?)(?:\\b)${escapedTag}(?:\\b|$|(?=[,|: \\t\\n\\r]))(?!)`, "g"); html = html.replaceAll(searchRegex, subHtml); @@ -1070,7 +1070,7 @@ async function autocomplete(textArea, prompt, fixedTag = null) { } let tagCountChange = tags.length - previousTags.length; - let diff = difference(tags, previousTags); + let diff = TacUtils.difference(tags, previousTags); previousTags = tags; // Guard for no difference / only whitespace remaining / last edited tag was fully removed @@ -1098,14 +1098,14 @@ async function autocomplete(textArea, prompt, fixedTag = null) { let normalTags = false; // Process all parsers - let resultCandidates = (await processParsers(textArea, prompt))?.filter(x => x.length > 0); + let resultCandidates = (await TacUtils.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()); + results = results.sort(TacUtils.getSortFunction()); } // Else search the normal tag list if (!resultCandidates || resultCandidates.length === 0 @@ -1118,9 +1118,9 @@ async function autocomplete(textArea, prompt, fixedTag = null) { let searchRegex; if (tagword.startsWith("*")) { tagword = tagword.slice(1); - searchRegex = new RegExp(`${escapeRegExp(tagword)}`, 'i'); + searchRegex = new RegExp(`${TacUtils.escapeRegExp(tagword)}`, 'i'); } else { - searchRegex = new RegExp(`(^|[^a-zA-Z])${escapeRegExp(tagword)}`, 'i'); + searchRegex = new RegExp(`(^|[^a-zA-Z])${TacUtils.escapeRegExp(tagword)}`, 'i'); } // Both normal tags and aliases/translations are included depending on the config @@ -1201,7 +1201,7 @@ async function autocomplete(textArea, prompt, fixedTag = null) { // Request use counts from the DB const names = TAC_CFG.frequencyIncludeAlias ? tagNames.concat(aliasNames) : tagNames; - const counts = await getUseCounts(names, types, isNegative); + const counts = await TacUtils.getUseCounts(names, types, isNegative); // Pre-calculate weights to prevent duplicate work const resultBiasMap = new Map(); @@ -1212,7 +1212,7 @@ async function autocomplete(textArea, prompt, fixedTag = null) { 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) + const weight = TacUtils.calculateUsageBias(result, result.count, uses) resultBiasMap.set(result, weight); }); // Actual sorting with the pre-calculated weights @@ -1355,13 +1355,13 @@ async function refreshTacTempFiles(api = false) { loras = []; lycos = []; modelKeywordDict.clear(); - await processQueue(QUEUE_FILE_LOAD, null); + await TacUtils.processQueue(QUEUE_FILE_LOAD, null); console.log("TAC: Refreshed temp files"); } if (api) { - await postTacAPI("tacapi/v1/refresh-temp-files"); + await TacUtils.postAPI("tacapi/v1/refresh-temp-files"); await reload(); } else { setTimeout(async () => { @@ -1371,9 +1371,9 @@ async function refreshTacTempFiles(api = false) { } async function refreshEmbeddings() { - await postTacAPI("tacapi/v1/refresh-embeddings", null); + await TacUtils.postAPI("tacapi/v1/refresh-embeddings", null); embeddings = []; - await processQueue(QUEUE_FILE_LOAD, null); + await TacUtils.processQueue(QUEUE_FILE_LOAD, null); console.log("TAC: Refreshed embeddings"); } @@ -1403,11 +1403,11 @@ function addAutocompleteToArea(area) { if (!e.inputType && !tacSelfTrigger) return; tacSelfTrigger = false; - debounce(autocomplete(area, area.value), TAC_CFG.delayTime); + TacUtils.debounce(autocomplete(area, area.value), TAC_CFG.delayTime); checkKeywordInsertionUndo(area, e); }); // Add focusout event listener - area.addEventListener('focusout', debounce(() => { + area.addEventListener('focusout', TacUtils.debounce(() => { if (!hideBlocked) hideResults(area); }, 400)); @@ -1428,7 +1428,7 @@ function addAutocompleteToArea(area) { // One-time setup, triggered from onUiUpdate async function setup() { // Load external files needed by completion extensions - await processQueue(QUEUE_FILE_LOAD, null); + await TacUtils.processQueue(QUEUE_FILE_LOAD, null); // Find all textareas let textAreas = getTextAreas(); @@ -1455,7 +1455,7 @@ async function setup() { }); }); quicksettings?.querySelectorAll(`[id^=setting_tac].gradio-dropdown input`).forEach(e => { - observeElement(e, "value", () => { + TacUtils.observeElement(e, "value", () => { setTimeout(async () => { await syncOptions(); }, 500); @@ -1473,14 +1473,14 @@ async function setup() { // Add mutation observer for the model hash text to also allow hash-based blacklist again let modelHashText = gradioApp().querySelector("#sd_checkpoint_hash"); - updateModelName(); + TacUtils.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(); + TacUtils.updateModelName(); refreshEmbeddings(); } } @@ -1524,7 +1524,7 @@ async function setup() { gradioApp().appendChild(acStyle); // Callback - await processQueue(QUEUE_AFTER_SETUP, null); + await TacUtils.processQueue(QUEUE_AFTER_SETUP, null); } var tacLoading = false; onUiUpdate(async () => { @@ -1533,7 +1533,7 @@ onUiUpdate(async () => { if (TAC_CFG) return; tacLoading = true; // Get our tag base path from the temp file - tagBasePath = await readFile(`tmp/tagAutocompletePath.txt`); + tagBasePath = await TacUtils.readFile(`tmp/tagAutocompletePath.txt`); // Load config from webui opts await syncOptions(); // Rest of setup