Last active
January 17, 2024 23:57
-
-
Save nedius/bd5a1af78dc71a762fe76bd6d05631d5 to your computer and use it in GitHub Desktop.
Booru style tag autocompletion for cmdr2's Stable Diffusion UI | Ported from DominikDoom/a1111-sd-webui-tagcomplete
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Tag Autocomplete | |
// @version 0.2.1 | |
// @description Booru style tag autocompletion for cmdr2's Stable Diffusion UI | Ported from DominikDoom/a1111-sd-webui-tagcomplete | |
// @author nedius | |
// @source https://gist.github.com/nedius/bd5a1af78dc71a762fe76bd6d05631d5 | |
// @info It is not perfect, but it works. There is a lot of unnecessary code that needs to be deleted. I will try to improve it in future. | |
// Changelog | |
// 0.2.1 | |
// - Fixed a bug where the tag list would prevent loading txt files with drag and drop | |
// 0.2 | |
// - Made config file local to the script | |
// - Reimplemented settings | |
// | |
// 0.1 | |
// - Initial release | |
// Todo: | |
// - Add tag base editor | |
// - Add a way to add custom tags to the list | |
// - Add save settings functionallity | |
// - Add modifiers to autocomplete | |
// - Tag popup max height should be decided | |
(() => { | |
const v_major = 0; | |
const v_minor = 2; | |
const v_patch = 0; | |
const pluginUrl = "https://gist.github.com/nedius/bd5a1af78dc71a762fe76bd6d05631d5"; | |
const DominikDoomGithuBasebUrl = "https://raw.githubusercontent.com/DominikDoom/a1111-sd-webui-tagcomplete/main"; | |
// let acConfig = null; | |
let acConfig = { | |
"tagFile": "danbooru.csv", | |
"activeIn": { | |
"txt2img": true, | |
"img2img": true, | |
"negativePrompts": true | |
}, | |
"hideUIOptions": false, | |
"maxResults": 5, | |
"resultStepLength": 500, | |
"delayTime": 100, | |
"showAllResults": false, | |
"useLeftRightArrowKeys": false, | |
"replaceUnderscores": true, | |
"escapeParentheses": true, | |
"appendComma": true, | |
"useWildcards": true, | |
"useEmbeddings": true, | |
"alias": { | |
"searchByAlias": true, | |
"onlyShowAlias": false | |
}, | |
"translation": { | |
"translationFile": "", | |
"oldFormat": false, | |
"searchByTranslation": true | |
}, | |
"extra": { | |
"extraFile": "", | |
"onlyAliasExtraFile": false | |
}, | |
"colors": { | |
"danbooru": { | |
"-1": ["red", "maroon"], | |
"0": ["lightblue", "dodgerblue"], | |
"1": ["indianred", "firebrick"], | |
"3": ["violet", "darkorchid"], | |
"4": ["lightgreen", "darkgreen"], | |
"5": ["orange", "darkorange"] | |
}, | |
"e621": { | |
"-1": ["red", "maroon"], | |
"0": ["lightblue", "dodgerblue"], | |
"1": ["gold", "goldenrod"], | |
"3": ["violet", "darkorchid"], | |
"4": ["lightgreen", "darkgreen"], | |
"5": ["tomato", "darksalmon"], | |
"6": ["red", "maroon"], | |
"7": ["whitesmoke", "black"], | |
"8": ["seagreen", "darkseagreen"] | |
} | |
} | |
} | |
let acActive = true; | |
// Style for new elements. Gets appended to the Gradio root. | |
let autocompleteCSS = ` | |
.autocompleteResults { | |
position: absolute; | |
z-index: 999; | |
margin: -5px 0 0 0; | |
background-color: var(--background-color2); | |
border: 1px solid var(--background-color3); | |
border-radius: 7px; | |
overflow-y: auto; | |
box-shadow: 0 4px 8px 0 rgb(0 0 0 / 15%), 0 6px 20px 0 rgb(0 0 0 / 15%); | |
} | |
.autocompleteResultsList > li:nth-child(odd) { | |
background-color: var(--background-color3); | |
} | |
.autocompleteResultsList > li { | |
list-style-type: none; | |
padding: 10px; | |
cursor: pointer; | |
} | |
.autocompleteResultsList > li:hover { | |
background-color: #1f2937; | |
} | |
.autocompleteResultsList > li.selected { | |
background-color: #374151; | |
} | |
.resultsFlexContainer { | |
display: flex; | |
} | |
.acListItem { | |
overflow: hidden; | |
white-space: nowrap; | |
text-overflow: ellipsis; | |
} | |
.acPostCount { | |
position: relative; | |
text-align: end; | |
padding: 0 0 0 15px; | |
flex-grow: 1; | |
color: #6b6f7b; | |
} | |
`; | |
// Parse the CSV file into a 2D array. Doesn't use regex, so it is very lightweight. | |
function parseCSV(str) { | |
var arr = []; | |
var 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 (var row = 0, col = 0, c = 0; c < str.length; c++) { | |
var 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 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 newline (CRLF) and we're not in a quoted field, 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' && !quote) { ++row; col = 0; ++c; continue; } | |
// If it's a newline (LF or CR) and we're not in a quoted field, | |
// move on to the next row and move to column 0 of that new row | |
if (cc == '\n' && !quote) { ++row; col = 0; continue; } | |
if (cc == '\r' && !quote) { ++row; col = 0; continue; } | |
// Otherwise, append the current character to the current column | |
arr[row][col] += cc; | |
} | |
return arr; | |
} | |
// Load file | |
function readFile(filePath) { | |
return new Promise(function (resolve, reject) { | |
let request = new XMLHttpRequest(); | |
request.open("GET", filePath, true); | |
request.onload = function () { | |
var status = request.status; | |
if (status == 200) { | |
resolve(request.responseText); | |
} else { | |
reject(status); | |
} | |
}; | |
request.send(null); | |
}); | |
} | |
// Load CSV | |
async function loadCSV(path) { | |
let text = await readFile(path); | |
return parseCSV(text); | |
} | |
// Debounce function to prevent spamming the autocomplete function | |
var dbTimeOut; | |
const debounce = (func, wait = 300) => { | |
return function (...args) { | |
if (dbTimeOut) { | |
clearTimeout(dbTimeOut); | |
} | |
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)), []); | |
} | |
// Get the identifier for the text area to differentiate between positive and negative | |
function getTextAreaIdentifier(textArea) { | |
let txt2img_p = document.querySelector('#prompt'); | |
let txt2img_n = document.querySelector('#negative_prompt'); | |
// let txt2img_p = document.querySelector('#txt2img_prompt > label > textarea'); | |
// let txt2img_n = document.querySelector('#txt2img_neg_prompt > label > textarea'); | |
// let img2img_p = document.querySelector('#img2img_prompt > label > textarea'); | |
// let img2img_n = document.querySelector('#img2img_neg_prompt > label > textarea'); | |
let modifier = ""; | |
switch (textArea) { | |
case txt2img_p: | |
modifier = ".txt2img.p"; | |
break; | |
case txt2img_n: | |
modifier = ".txt2img.n"; | |
break; | |
// case img2img_p: | |
// modifier = ".img2img.p"; | |
// break; | |
// case img2img_n: | |
// modifier = ".img2img.n"; | |
// break; | |
default: | |
break; | |
} | |
return modifier; | |
} | |
// Create the result list div and necessary styling | |
function createResultsDiv(textArea) { | |
let resultsDiv = document.createElement("div"); | |
let resultsList = document.createElement('ul'); | |
let textAreaId = getTextAreaIdentifier(textArea); | |
let typeClass = textAreaId.replaceAll(".", " "); | |
// resultsDiv.style.setProperty("max-height", acConfig.maxResults * 50 + "px"); | |
resultsDiv.setAttribute('class', `autocompleteResults ${typeClass}`); | |
resultsList.setAttribute('class', 'autocompleteResultsList'); | |
resultsList.style.padding = "0"; | |
resultsList.style.margin = "0"; | |
resultsDiv.appendChild(resultsList); | |
return resultsDiv; | |
} | |
// Create the checkbox to enable/disable autocomplete | |
function createCheckbox(text) { | |
let label = document.createElement("label"); | |
let input = document.createElement("input"); | |
let span = document.createElement("span"); | |
label.setAttribute('id', 'acActiveCheckbox'); | |
// label.setAttribute('class', '"flex items-center text-gray-700 text-sm rounded-lg cursor-pointer dark:bg-transparent'); | |
input.setAttribute('type', 'checkbox'); | |
// input.setAttribute('class', 'gr-check-radio gr-checkbox') | |
// span.setAttribute('class', 'ml-2'); | |
span.textContent = text; | |
label.appendChild(input); | |
label.appendChild(span); | |
return label; | |
} | |
// The selected tag index. Needs to be up here so hide can access it. | |
var selectedTag = null; | |
var previousTags = []; | |
// Show or hide the results div | |
function isVisible(textArea) { | |
let textAreaId = getTextAreaIdentifier(textArea); | |
let resultsDiv = document.querySelector('.autocompleteResults' + textAreaId); | |
return resultsDiv.style.display === "block"; | |
} | |
function showResults(textArea) { | |
let textAreaId = getTextAreaIdentifier(textArea); | |
let resultsDiv = document.querySelector('.autocompleteResults' + textAreaId); | |
resultsDiv.style.display = "block"; | |
} | |
function hideResults(textArea) { | |
let textAreaId = getTextAreaIdentifier(textArea); | |
let resultsDiv = document.querySelector('.autocompleteResults' + textAreaId); | |
resultsDiv.style.display = "none"; | |
selectedTag = null; | |
} | |
function escapeRegExp(string) { | |
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string | |
} | |
function escapeHTML(unsafeText) { | |
let div = document.createElement('div'); | |
div.textContent = unsafeText; | |
return div.innerHTML; | |
} | |
const WEIGHT_REGEX = /[([]([^,()[\]:| ]+)(?::(?:\d+(?:\.\d+)?|\.\d+))?[)\]]/g; | |
const TAG_REGEX = /([^\s,|]+)/g | |
let hideBlocked = false; | |
// On click, insert the tag into the prompt textbox with respect to the cursor position | |
function insertTextAtCursor(textArea, result, tagword) { | |
let text = result[0]; | |
let tagType = result[1]; | |
let cursorPos = textArea.selectionStart; | |
var sanitizedText = text | |
// Replace differently depending on if it's a tag or wildcard | |
if (tagType === "wildcardFile") { | |
sanitizedText = "__" + text.replace("Wildcards: ", "") + "__"; | |
} else if (tagType === "wildcardTag") { | |
sanitizedText = text.replace(/^.*?: /g, ""); | |
} else if (tagType === "embedding") { | |
sanitizedText = `<${text.replace(/^.*?: /g, "")}>`; | |
} else { | |
sanitizedText = acConfig.replaceUnderscores ? text.replaceAll("_", " ") : text; | |
} | |
if (acConfig.escapeParentheses) { | |
sanitizedText = sanitizedText | |
.replaceAll("(", "\\(") | |
.replaceAll(")", "\\)") | |
.replaceAll("[", "\\[") | |
.replaceAll("]", "\\]"); | |
} | |
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 optionalComma = ""; | |
if (acAppendComma && tagType !== "wildcardFile") { | |
optionalComma = surrounding.match(new RegExp(`${escapeRegExp(tagword)}[,:]`, "i")) !== null ? "" : ", "; | |
} | |
// Replace partial tag word with new text, add comma if needed | |
let insert = surrounding.replace(match, sanitizedText + optionalComma); | |
// Add back start | |
var newPrompt = prompt.substring(0, editStart) + insert + prompt.substring(editEnd); | |
textArea.value = newPrompt; | |
textArea.selectionStart = afterInsertCursorPos + optionalComma.length; | |
textArea.selectionEnd = textArea.selectionStart | |
// Since we've modified a Gradio Textbox component manually, we need to simulate an `input` DOM event to ensure its | |
// internal Svelte data binding remains in sync. | |
textArea.dispatchEvent(new Event("input", { bubbles: true })); | |
// 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; | |
// Hide results after inserting | |
if (tagType === "wildcardFile") { | |
// If it's a wildcard, we want to keep the results open so the user can select another wildcard | |
hideBlocked = true; | |
autocomplete(textArea, prompt, sanitizedText); | |
setTimeout(() => { hideBlocked = false; }, 100); | |
} else { | |
hideResults(textArea); | |
} | |
} | |
function addResultsToList(textArea, results, tagword, resetList) { | |
let textAreaId = getTextAreaIdentifier(textArea); | |
let resultDiv = document.querySelector('.autocompleteResults' + textAreaId); | |
let resultsList = resultDiv.querySelector('ul'); | |
// Reset list, selection and scrollTop since the list changed | |
if (resetList) { | |
resultsList.innerHTML = ""; | |
selectedTag = null; | |
resultDiv.scrollTop = 0; | |
resultCount = 0; | |
} | |
// Find right colors from config | |
let tagFileName = acConfig.tagFile.split(".")[0]; | |
let tagColors = acConfig.colors; | |
let mode = document.querySelector('.dark') ? 0 : 1; | |
let nextLength = Math.min(results.length, resultCount + acConfig.resultStepLength); | |
for (let i = resultCount; i < nextLength; i++) { | |
let result = results[i]; | |
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"); | |
flexDiv.appendChild(itemText); | |
let displayText = ""; | |
// If the tag matches the tagword, we don't need to display the alias | |
if (result[3] && !result[0].includes(tagword)) { // Alias | |
let splitAliases = result[3].split(","); | |
let bestAlias = splitAliases.find(a => a.toLowerCase().includes(tagword)); | |
// search in translations if no alias matches | |
if (!bestAlias) { | |
var translationKey = [...translations].find(pair => pair[0] === result[0] && pair[1].includes(tagword))[0]; | |
bestAlias = translationKey// ? translations.get(translationKey) : null; | |
} | |
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[0]) | |
displayText += `[${translations.get(bestAlias)}]`; | |
if (!acConfig.alias.onlyShowAlias && result[0] !== bestAlias) | |
displayText += " ➝ " + result[0]; | |
} else { // No alias | |
displayText = escapeHTML(result[0]); | |
} | |
// Append translation for result if it exists | |
if (translations.has(result[0])) | |
displayText += `[${translations.get(result[0])}]`; | |
// Print search term bolded in result | |
itemText.innerHTML = displayText.replace(tagword, `<b>${tagword}</b>`); | |
// Add post count & color if it's a tag | |
// Wildcards & Embeds have no tag type | |
if (!result[1].startsWith("wildcard") && result[1] !== "embedding") { | |
// Set the color of the tag | |
let tagType = result[1]; | |
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[tagType]) | |
tagType = "-1"; | |
itemText.style = `color: ${colorGroup[tagType][mode]};`; | |
// Post count | |
if (result[2] && !isNaN(result[2])) { | |
let postCount = result[2]; | |
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("acPostCount"); | |
flexDiv.appendChild(countDiv); | |
} | |
} | |
// Add listener | |
li.addEventListener("click", function () { insertTextAtCursor(textArea, result, tagword); }); | |
// Add element to list | |
resultsList.appendChild(li); | |
} | |
resultCount = nextLength; | |
} | |
function updateSelectionStyle(textArea, newIndex, oldIndex) { | |
let textAreaId = getTextAreaIdentifier(textArea); | |
let resultDiv = document.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) { | |
items[newIndex].classList.add('selected'); | |
} | |
// Set scrolltop to selected item if we are showing more than max results | |
if (items.length > acConfig.maxResults) { | |
let selected = items[newIndex]; | |
resultDiv.scrollTop = selected.offsetTop - resultDiv.offsetTop; | |
} | |
} | |
var wildcardFiles = []; | |
var wildcardExtFiles = []; | |
var embeddings = []; | |
var allTags = []; | |
var translations = new Map(); | |
var results = []; | |
// let modifiers = []; | |
var tagword = ""; | |
var resultCount = 0; | |
async function autocomplete(textArea, prompt, fixedTag = null) { | |
// Return if the function is deactivated in the UI | |
if (!acActive) return; | |
// Guard for empty prompt | |
if (prompt.length === 0) { | |
hideResults(textArea); | |
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 = tags.filter(tag => !weightedTags.some(weighted => tag.includes(weighted))) | |
.concat(weightedTags); | |
} | |
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; | |
} | |
tagword = tagword.toLowerCase().replace(/[\n\r]/g, ""); | |
if (acConfig.useWildcards && [...tagword.matchAll(/\b__([^, ]+)__([^, ]*)\b/g)].length > 0) { | |
// Show wildcards from a file with that name | |
wcMatch = [...tagword.matchAll(/\b__([^, ]+)__([^, ]*)\b/g)] | |
let wcFile = wcMatch[0][1]; | |
let wcWord = wcMatch[0][2]; | |
var wcPair; | |
// Look in normal wildcard files | |
if (wcFound = wildcardFiles.find(x => x[1].toLowerCase() === wcFile)) | |
wcPair = wcFound; | |
else // Look in extensions wildcard files | |
wcPair = wildcardExtFiles.find(x => x[1].toLowerCase() === wcFile); | |
let wildcards = (await readFile(`file/${wcPair[0]}/${wcPair[1]}.txt?${new Date().getTime()}`)).split("\n") | |
.filter(x => x.trim().length > 0 && !x.startsWith('#')); // Remove empty lines and comments | |
results = wildcards.filter(x => (wcWord !== null && wcWord.length > 0) ? x.toLowerCase().includes(wcWord) : x) // Filter by tagword | |
.map(x => [wcFile + ": " + x.trim(), "wildcardTag"]); // Mark as wildcard | |
} else if (acConfig.useWildcards && (tagword.startsWith("__") && !tagword.endsWith("__") || tagword === "__")) { | |
// Show available wildcard files | |
let tempResults = []; | |
if (tagword !== "__") { | |
let lmb = (x) => x[1].toLowerCase().includes(tagword.replace("__", "")) | |
tempResults = wildcardFiles.filter(lmb).concat(wildcardExtFiles.filter(lmb)) // Filter by tagword | |
} else { | |
tempResults = wildcardFiles.concat(wildcardExtFiles); | |
} | |
results = tempResults.map(x => ["Wildcards: " + x[1].trim(), "wildcardFile"]); // Mark as wildcard | |
} else if (acConfig.useEmbeddings && tagword.match(/<[^,> ]*>?/g)) { | |
// Show embeddings | |
let tempResults = []; | |
if (tagword !== "<") { | |
tempResults = embeddings.filter(x => x.toLowerCase().includes(tagword.replace("<", ""))) // Filter by tagword | |
} else { | |
tempResults = embeddings; | |
} | |
// Since some tags are kaomoji, we have to still get the normal results first. | |
genericResults = allTags.filter(x => x[0].toLowerCase().includes(tagword)).slice(0, acConfig.maxResults); | |
results = genericResults.concat(tempResults.map(x => ["Embeddings: " + x.trim(), "embedding"])); // Mark as embedding | |
} else { | |
// If onlyShowAlias is enabled, we don't need to include normal results | |
if (acConfig.alias.onlyShowAlias) { | |
results = allTags.filter(x => x[3] && x[3].toLowerCase().includes(tagword)); | |
} else { | |
// Else both normal tags and aliases/translations are included depending on the config | |
let baseFilter = (x) => x[0].toLowerCase().includes(tagword); | |
let aliasFilter = (x) => x[3] && x[3].toLowerCase().includes(tagword); | |
let translationFilter = (x) => (translations.has(x[0]) && translations.get(x[0]).toLowerCase().includes(tagword)) | |
|| x[3] && x[3].split(",").some(y => translations.has(y) && translations.get(y).toLowerCase().includes(tagword)); | |
let fil; | |
if (acConfig.alias.searchByAlias && acConfig.translation.searchByTranslation) | |
fil = (x) => baseFilter(x) || aliasFilter(x) || translationFilter(x); | |
else if (acConfig.alias.searchByAlias && !acConfig.translation.searchByTranslation) | |
fil = (x) => baseFilter(x) || aliasFilter(x); | |
else if (acConfig.translation.searchByTranslation && !acConfig.alias.searchByAlias) | |
fil = (x) => baseFilter(x) || translationFilter(x); | |
else | |
fil = (x) => baseFilter(x); | |
results = allTags.filter(fil); | |
} | |
// Slice if the user has set a max result count | |
if (!acConfig.showAllResults) { | |
results = results.slice(0, acConfig.maxResults); | |
} | |
} | |
// Guard for empty results | |
if (!results.length) { | |
hideResults(textArea); | |
return; | |
} | |
showResults(textArea); | |
addResultsToList(textArea, results, tagword, true); | |
} | |
var oldSelectedTag = null; | |
function navigateInList(textArea, event) { | |
// Return if the function is deactivated in the UI | |
if (!acActive) return; | |
validKeys = ["ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End", "Enter", "Tab", "Escape"]; | |
if (acConfig.useLeftRightArrowKeys) | |
validKeys.push("ArrowLeft", "ArrowRight"); | |
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) return; | |
oldSelectedTag = selectedTag; | |
switch (event.key) { | |
case "ArrowUp": | |
if (selectedTag === null) { | |
selectedTag = resultCount - 1; | |
} else { | |
selectedTag = (selectedTag - 1 + resultCount) % resultCount; | |
} | |
break; | |
case "ArrowDown": | |
if (selectedTag === null) { | |
selectedTag = 0; | |
} else { | |
selectedTag = (selectedTag + 1) % resultCount; | |
} | |
break; | |
case "PageUp": | |
if (selectedTag === null || selectedTag === 0) { | |
selectedTag = resultCount - 1; | |
} else { | |
selectedTag = (Math.max(selectedTag - 5, 0) + resultCount) % resultCount; | |
} | |
break; | |
case "PageDown": | |
if (selectedTag === null || selectedTag === resultCount - 1) { | |
selectedTag = 0; | |
} else { | |
selectedTag = Math.min(selectedTag + 5, resultCount - 1) % resultCount; | |
} | |
break; | |
case "Home": | |
selectedTag = 0; | |
break; | |
case "End": | |
selectedTag = resultCount - 1; | |
break; | |
case "ArrowLeft": | |
selectedTag = 0; | |
break; | |
case "ArrowRight": | |
selectedTag = resultCount - 1; | |
break; | |
case "Enter": | |
if (selectedTag !== null) { | |
insertTextAtCursor(textArea, results[selectedTag], tagword); | |
} | |
break; | |
case "Tab": | |
if (selectedTag === null) { | |
selectedTag = 0; | |
} | |
insertTextAtCursor(textArea, results[selectedTag], tagword); | |
break; | |
case "Escape": | |
hideResults(textArea); | |
break; | |
} | |
if (selectedTag === resultCount - 1 | |
&& (event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "ArrowLeft" || event.key === "ArrowRight")) { | |
addResultsToList(textArea, results, tagword, false); | |
} | |
// Update highlighting | |
if (selectedTag !== null) | |
updateSelectionStyle(textArea, selectedTag, oldSelectedTag); | |
// Prevent default behavior | |
event.preventDefault(); | |
event.stopPropagation(); | |
} | |
// One-time setup | |
(async () => { | |
// Load config | |
if (acConfig === null) { | |
try { | |
acConfig = JSON.parse(await readFile(`${DominikDoomGithuBasebUrl}/tags/config.json?${new Date().getTime()}`)); | |
if (acConfig.alias.onlyShowAlias) { | |
acConfig.alias.searchByAlias = true; // if only show translation, enable search by translation is necessary | |
} | |
} catch (e) { | |
console.error("Error loading config.json: " + e); | |
return; | |
} | |
} | |
// Load main tags and aliases | |
if (allTags.length === 0) { | |
try { | |
allTags = await loadCSV(`${DominikDoomGithuBasebUrl}/tags/${acConfig.tagFile}?${new Date().getTime()}`); | |
} catch (e) { | |
console.error("Error loading tags file: " + e); | |
return; | |
} | |
if (acConfig.extra.extraFile) { | |
try { | |
extras = await loadCSV(`file/${tagBasePath}/${acConfig.extra.extraFile}?${new Date().getTime()}`); | |
if (acConfig.extra.onlyAliasExtraFile) { | |
// This works purely on index, so it's not very robust. But a lot faster. | |
for (let i = 0, n = extras.length; i < n; i++) { | |
if (extras[i][0]) { | |
let aliasStr = allTags[i][3] || ""; | |
let optComma = aliasStr.length > 0 ? "," : ""; | |
allTags[i][3] = aliasStr + optComma + extras[i][0]; | |
} | |
} | |
} else { | |
extras.forEach(e => { | |
let hasCount = e[2] && e[3] || (!isNaN(e[2]) && !e[3]); | |
// Check if a tag in allTags has the same name & category as the extra tag | |
if (tag = allTags.find(t => t[0] === e[0] && t[1] == e[1])) { | |
if (hasCount && e[3] || isNaN(e[2])) { // If the extra tag has a translation / alias, add it to the normal tag | |
let aliasStr = tag[3] || ""; | |
let optComma = aliasStr.length > 0 ? "," : ""; | |
let alias = hasCount && e[3] || isNaN(e[2]) ? e[2] : e[3]; | |
tag[3] = aliasStr + optComma + alias; | |
} | |
} else { | |
let count = hasCount ? e[2] : null; | |
let aliases = hasCount && e[3] ? e[3] : e[2]; | |
// If the tag doesn't exist, add it to allTags | |
let newTag = [e[0], e[1], count, aliases]; | |
allTags.push(newTag); | |
} | |
}); | |
} | |
} catch (e) { | |
console.error("Error loading extra file: " + e); | |
return; | |
} | |
} | |
} | |
// Load translations | |
if (acConfig.translation.translationFile) { | |
try { | |
let tArray = await loadCSV(`file/${tagBasePath}/${acConfig.translation.translationFile}?${new Date().getTime()}`); | |
tArray.forEach(t => { | |
if (acConfig.translation.oldFormat) | |
translations.set(t[0], t[2]); | |
else | |
translations.set(t[0], t[1]); | |
}); | |
} catch (e) { | |
console.error("Error loading translations file: " + e); | |
return; | |
} | |
} | |
// Load wildcards | |
// if (acConfig.useWildcards && wildcardFiles.length === 0) { | |
// try { | |
// let wcFileArr = (await readFile(`file/${tagBasePath}/temp/wc.txt?${new Date().getTime()}`)).split("\n"); | |
// let wcBasePath = wcFileArr[0].trim(); // First line should be the base path | |
// wildcardFiles = wcFileArr.slice(1) | |
// .filter(x => x.trim().length > 0) // Remove empty lines | |
// .map(x => [wcBasePath, x.trim().replace(".txt", "")]); // Remove file extension & newlines | |
// // To support multiple sources, we need to separate them using the provided "-----" strings | |
// let wcExtFileArr = (await readFile(`file/${tagBasePath}/temp/wce.txt?${new Date().getTime()}`)).split("\n"); | |
// let splitIndices = []; | |
// for (let index = 0; index < wcExtFileArr.length; index++) { | |
// if (wcExtFileArr[index].trim() === "-----") { | |
// splitIndices.push(index); | |
// } | |
// } | |
// // For each group, add them to the wildcardFiles array with the base path as the first element | |
// for (let i = 0; i < splitIndices.length; i++) { | |
// let start = splitIndices[i - 1] || 0; | |
// if (i > 0) start++; // Skip the "-----" line | |
// let end = splitIndices[i]; | |
// let wcExtFile = wcExtFileArr.slice(start, end); | |
// let base = wcExtFile[0].trim() + "/"; | |
// wcExtFile = wcExtFile.slice(1) | |
// .filter(x => x.trim().length > 0) // Remove empty lines | |
// .map(x => x.trim().replace(base, "").replace(".txt", "")); // Remove file extension & newlines; | |
// wcExtFile = wcExtFile.map(x => [base, x]); | |
// wildcardExtFiles.push(...wcExtFile); | |
// } | |
// } catch (e) { | |
// console.error("Error loading wildcards: " + e); | |
// } | |
// } | |
// Load embeddings | |
// if (acConfig.useEmbeddings && embeddings.length === 0) { | |
// try { | |
// embeddings = (await readFile(`file/${tagBasePath}/temp/emb.txt?${new Date().getTime()}`)).split("\n") | |
// .filter(x => x.trim().length > 0) // Remove empty lines | |
// .map(x => x.replace(".bin", "").replace(".pt", "").replace(".png", "")); // Remove file extensions | |
// } catch (e) { | |
// console.error("Error loading embeddings.txt: " + e); | |
// } | |
// } | |
// Load modifiers | |
// if (acConfig.modifier.modifierFile) { | |
// try { | |
// let modifierArr = await loadCSV(`modifiers?${new Date().getTime()}`); | |
// modifierArr.forEach(m => { | |
// }); | |
// } catch (e) { | |
// console.error("Error loading modifiers file: " + e); | |
// } | |
// } | |
// Find all textareas | |
// let txt2imgTextArea = document.querySelector('#txt2img_prompt > label > textarea'); | |
// let img2imgTextArea = document.querySelector('#img2img_prompt > label > textarea'); | |
// let txt2imgTextArea_n = document.querySelector('#txt2img_neg_prompt > label > textarea'); | |
// let img2imgTextArea_n = document.querySelector('#img2img_neg_prompt > label > textarea'); | |
// let textAreas = [txt2imgTextArea, img2imgTextArea, txt2imgTextArea_n, img2imgTextArea_n]; | |
let txt2imgTextArea = document.querySelector('#prompt'); | |
let txt2imgTextArea_n = document.querySelector('#negative_prompt'); | |
let textAreas = [txt2imgTextArea, txt2imgTextArea_n]; | |
let quicksettings = document.querySelector('#tab-content-settings'); | |
if(quicksettings === null || quicksettings === undefined) { | |
quicksettings = document.querySelector('#system-settings > div'); | |
} | |
// 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 (document.querySelector('.autocompleteResults.p')) { | |
if (document.querySelector('.autocompleteResults.n') || !acConfig.activeIn.negativePrompts) { | |
return; | |
} | |
} else if (!acConfig.activeIn.txt2img && !acConfig.activeIn.img2img) { | |
return; | |
} | |
textAreas.forEach(area => { | |
// Return if autocomplete is disabled for the current area type in config | |
let textAreaId = getTextAreaIdentifier(area); | |
if ((!acConfig.activeIn.img2img && textAreaId.includes("img2img")) | |
|| (!acConfig.activeIn.txt2img && textAreaId.includes("txt2img")) | |
|| (!acConfig.activeIn.negativePrompts && textAreaId.includes("n"))) { | |
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', debounce(() => autocomplete(area, area.value), acConfig.delayTime)); | |
// Add focusout event listener | |
area.addEventListener('focusout', debounce(() => 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'); | |
} | |
}); | |
acAppendComma = acConfig.appendComma; | |
// Add our custom options elements | |
if (!acConfig.hideUIOptions && document.querySelector("#tag-autocomplete-settings") === null) { | |
let optionsDiv = document.createElement("div"); | |
optionsDiv.id = "tag-autocomplete-settings"; | |
optionsDiv.classList.add("tab-content-inner"); | |
optionsDiv.style.marginTop = "10px"; | |
let optionsInner = document.createElement("div"); | |
// optionsInner.classList.add("flex", "flex-row", "p-1", "gap-4", "text-gray-700"); | |
optionsInner.style.marginTop = "5px"; | |
// Add label | |
let title = document.createElement("h1"); | |
// title.classList.add("settings-subheader"); | |
title.textContent = "Autocomplete Settings"; | |
optionsDiv.appendChild(title); | |
// Add table | |
let table = document.createElement("table"); | |
table.classList.add("form-table"); | |
optionsDiv.appendChild(table); | |
// // Add toggle switch | |
// let cbActive = createCheckbox("Enable Autocomplete"); | |
// cbActive.querySelector("input").checked = acActive; | |
// cbActive.querySelector("input").addEventListener("change", (e) => { | |
// acActive = e.target.checked; | |
// }); | |
// // Add comma switch | |
// let cbComma = createCheckbox("Append commas"); | |
// cbComma.querySelector("input").checked = acAppendComma; | |
// cbComma.querySelector("input").addEventListener("change", (e) => { | |
// acAppendComma = e.target.checked; | |
// }); | |
// Add options to optionsDiv | |
// optionsInner.appendChild(cbActive); | |
// optionsInner.appendChild(cbComma); | |
// optionsDiv.appendChild(optionsInner); | |
// Add options div to DOM | |
quicksettings.append(optionsDiv); | |
} | |
var PARAMETERS = [ | |
{ | |
id: "ac_toggle", | |
type: ParameterType.checkbox, | |
label: "Enable Autocomplete", | |
note: "Enable or disable autocomplete", | |
default: true, | |
}, | |
{ | |
id: "ac_append_comma", | |
type: ParameterType.checkbox, | |
label: "Append commas", | |
note: "Append commas after each autocomplete suggestion", | |
default: true, | |
}, | |
{ | |
id: "ac_max_results", | |
type: ParameterType.custom, | |
label: "Max results count", | |
render: (parameter) => { | |
return `<input type="number" min="1" id="${parameter.id}" name="${parameter.id}" size="30" value="${acConfig.maxResults}" onChange="acConfig.maxResults = document.querySelector('#${parameter.id}').value">` | |
}, | |
}, | |
]; | |
let parametersTable = document.querySelector("#tag-autocomplete-settings table"); | |
/* fill in the system settings popup table */ | |
PARAMETERS.forEach(parameter => { | |
let element = getParameterElement(parameter); | |
let note = parameter.note ? `<small>${parameter.note}</small>` : ""; | |
let newrow = document.createElement('tr'); | |
newrow.innerHTML = ` | |
<td><label for="${parameter.id}">${parameter.label}</label></td> | |
<td><div>${element}${note}<div></td>`; | |
parametersTable.appendChild(newrow); | |
parameter.settingsEntry = newrow; | |
}) | |
// Add style to dom | |
let acStyle = document.createElement('style'); | |
// let css = document.querySelector('.dark') ? autocompleteCSS_dark : autocompleteCSS_light; | |
let css = autocompleteCSS; | |
if (acStyle.styleSheet) { | |
acStyle.styleSheet.cssText = css; | |
} else { | |
acStyle.appendChild(document.createTextNode(css)); | |
} | |
document.body.appendChild(acStyle); | |
styleAdded = true; | |
})(); | |
})(); |
Yes i found what caused it.
Fixed it
i'm not getting any difference with or without this. is it still working?
could you resume working on this? I'd really like custom tags and Spell Tokenizer has been abandoned by its original creator.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi :) I like this plugin, but using it currently breaks drop&drop functionality of .txt-files in SDUI
The console shows this error while drop&drop: "INFO: 127.0.0.1:56149 - "GET /%5Bobject%20File%5D HTTP/1.1" 404 Not Found"