Spaces:
Running
Running
const styleColors = { | |
"--results-neutral-text": ["#e0e0e0","black"], | |
"--results-bg": ["#0b0f19", "#ffffff"], | |
"--results-border-color": ["#4b5563", "#e5e7eb"], | |
"--results-border-width": ["1px", "1.5px"], | |
"--results-bg-odd": ["#111827", "#f9fafb"], | |
"--results-hover": ["#1f2937", "#f5f6f8"], | |
"--results-selected": ["#374151", "#e5e7eb"], | |
"--meta-text-color": ["#6b6f7b", "#a2a9b4"], | |
"--embedding-v1-color": ["lightsteelblue", "#2b5797"], | |
"--embedding-v2-color": ["skyblue", "#2d89ef"], | |
"--live-translation-rt": ["whitesmoke", "#222"], | |
"--live-translation-color-1": ["lightskyblue", "#2d89ef"], | |
"--live-translation-color-2": ["palegoldenrod", "#eb5700"], | |
"--live-translation-color-3": ["darkseagreen", "darkgreen"], | |
} | |
const browserVars = { | |
"--results-overflow-y": { | |
"firefox": "scroll", | |
"other": "auto" | |
} | |
} | |
// Style for new elements. Gets appended to the Gradio root. | |
const autocompleteCSS = ` | |
#quicksettings [id^=setting_tac] { | |
background-color: transparent; | |
min-width: fit-content; | |
} | |
.autocompleteParent { | |
display: flex; | |
position: absolute; | |
z-index: 9999; | |
max-width: calc(100% - 1.5rem); | |
margin: 5px 0 0 0; | |
} | |
.autocompleteResults { | |
background-color: var(--results-bg) !important; | |
border: var(--results-border-width) solid var(--results-border-color) !important; | |
color: var(--results-neutral-text) !important; | |
border-radius: 12px !important; | |
height: fit-content; | |
flex-basis: fit-content; | |
flex-shrink: 0; | |
overflow-y: var(--results-overflow-y); | |
overflow-x: hidden; | |
word-break: break-word; | |
} | |
.sideInfo { | |
display: none; | |
position: relative; | |
margin-left: 10px; | |
height: 18rem; | |
max-width: 16rem; | |
} | |
.sideInfo > img { | |
object-fit: cover; | |
height: 100%; | |
width: 100%; | |
} | |
.autocompleteResultsList > li:nth-child(odd) { | |
background-color: var(--results-bg-odd); | |
} | |
.autocompleteResultsList > li { | |
list-style-type: none; | |
padding: 10px; | |
cursor: pointer; | |
} | |
.autocompleteResultsList > li:hover { | |
background-color: var(--results-hover); | |
} | |
.autocompleteResultsList > li.selected { | |
background-color: var(--results-selected); | |
} | |
.resultsFlexContainer { | |
display: flex; | |
} | |
.acListItem { | |
white-space: break-spaces; | |
min-width: 100px; | |
} | |
.acMetaText { | |
position: relative; | |
flex-grow: 1; | |
text-align: end; | |
padding: 0 0 0 15px; | |
white-space: nowrap; | |
color: var(--meta-text-color); | |
} | |
.acWikiLink { | |
padding: 0.5rem; | |
margin: -0.5rem 0 -0.5rem -0.5rem; | |
} | |
.acWikiLink:hover { | |
text-decoration: underline; | |
} | |
.acListItem.acEmbeddingV1 { | |
color: var(--embedding-v1-color); | |
} | |
.acListItem.acEmbeddingV2 { | |
color: var(--embedding-v2-color); | |
} | |
.acListItem .acPathPart:nth-child(3n+1) { | |
color: var(--live-translation-color-1); | |
} | |
.acListItem .acPathPart:nth-child(3n+2) { | |
color: var(--live-translation-color-2); | |
} | |
.acListItem .acPathPart:nth-child(3n+3) { | |
color: var(--live-translation-color-3); | |
} | |
`; | |
function gradioApp() { | |
const elems = document.getElementsByTagName('gradio-app'); | |
const elem = elems.length == 0 ? document : elems[0]; | |
if (elem !== document) { | |
elem.getElementById = function(id) { | |
return document.getElementById(id); | |
}; | |
} | |
return elem.shadowRoot ? elem.shadowRoot : elem; | |
} | |
function updateInput(target) { | |
let e = new Event("input", {bubbles: true}); | |
Object.defineProperty(e, "target", {value: target}); | |
target.dispatchEvent(e); | |
} | |
async function loadTags(c) { | |
// Load main tags and aliases | |
if (allTags.length === 0 && c.tagFile && c.tagFile !== "None") { | |
try { | |
allTags = await loadCSV(`${tagBasePath}/${c.tagFile}`); | |
} catch (e) { | |
console.error("Error loading tags file: " + e); | |
return; | |
} | |
} | |
await loadExtraTags(c); | |
} | |
async function loadExtraTags(c) { | |
if (c.extra.extraFile && c.extra.extraFile !== "None") { | |
try { | |
extras = await loadCSV(`${tagBasePath}/${c.extra.extraFile}`); | |
// Add translations to the main translation map for extra tags that have them | |
extras.forEach(e => { | |
if (e[4]) translations.set(e[0], e[4]); | |
}); | |
} catch (e) { | |
console.error("Error loading extra file: " + e); | |
return; | |
} | |
} | |
} | |
// Create the result list div and necessary styling | |
function createResultsDiv(textArea) { | |
let parentDiv = document.createElement("div"); | |
let resultsDiv = document.createElement("div"); | |
let resultsList = document.createElement("ul"); | |
let sideDiv = document.createElement("div"); | |
let sideDivImg = document.createElement("img"); | |
let textAreaId = getTextAreaIdentifier(textArea); | |
let typeClass = textAreaId.replaceAll(".", " "); | |
parentDiv.setAttribute("class", `autocompleteParent${typeClass}`); | |
resultsDiv.style.maxHeight = `${TAC_CFG.maxResults * 50}px`; | |
resultsDiv.setAttribute("class", `autocompleteResults${typeClass} notranslate`); | |
resultsDiv.setAttribute("translate", "no"); | |
resultsList.setAttribute("class", "autocompleteResultsList"); | |
resultsDiv.appendChild(resultsList); | |
sideDiv.setAttribute("class", `autocompleteResults${typeClass} sideInfo`); | |
sideDiv.appendChild(sideDivImg); | |
parentDiv.appendChild(resultsDiv); | |
parentDiv.appendChild(sideDiv); | |
return parentDiv; | |
} | |
// Show or hide the results div | |
function isVisible(textArea) { | |
let textAreaId = getTextAreaIdentifier(textArea); | |
let parentDiv = gradioApp().querySelector('.autocompleteParent' + textAreaId); | |
return parentDiv.style.display === "flex"; | |
} | |
function showResults(textArea) { | |
let textAreaId = getTextAreaIdentifier(textArea); | |
let parentDiv = gradioApp().querySelector('.autocompleteParent' + textAreaId); | |
parentDiv.style.display = "flex"; | |
if (TAC_CFG.slidingPopup) { | |
let caretPosition = getCaretCoordinates(textArea, textArea.selectionEnd).left; | |
let offset = Math.min(textArea.offsetLeft - textArea.scrollLeft + caretPosition, textArea.offsetWidth - parentDiv.offsetWidth); | |
parentDiv.style.left = `${offset}px`; | |
} else { | |
if (parentDiv.style.left) | |
parentDiv.style.removeProperty("left"); | |
} | |
// Reset here too to make absolutely sure the browser registers it | |
parentDiv.scrollTop = 0; | |
// Ensure preview is hidden | |
let previewDiv = gradioApp().querySelector(`.autocompleteParent${textAreaId} .sideInfo`); | |
previewDiv.style.display = "none"; | |
} | |
function hideResults(textArea) { | |
let textAreaId = getTextAreaIdentifier(textArea); | |
let resultsDiv = gradioApp().querySelector('.autocompleteParent' + textAreaId); | |
if (!resultsDiv) return; | |
resultsDiv.style.display = "none"; | |
selectedTag = null; | |
} | |
const WEIGHT_REGEX = /[([]([^()[\]:|]+)(?::(?:\d+(?:\.\d+)?|\.\d+))?[)\]]/g; | |
const POINTY_REGEX = /<[^\s,<](?:[^\t\n\r,<>]*>|[^\t\n\r,> ]*)/g; | |
const COMPLETED_WILDCARD_REGEX = /__[^\s,_][^\t\n\r,_]*[^\s,_]__[^\s,_]*/g; | |
const STYLE_VAR_REGEX = /\$\(?[^$|\[\],\s]*\)?/g; | |
const NORMAL_TAG_REGEX = /[^\s,|<>\[\]:]+_\([^\s,|<>\[\]:]*\)?|[^\s,|<>():\[\]]+|</g; | |
const TAG_REGEX = new RegExp(`${POINTY_REGEX.source}|${COMPLETED_WILDCARD_REGEX.source}|${STYLE_VAR_REGEX.source}|${NORMAL_TAG_REGEX.source}`, "g"); | |
// On click, insert the tag into the prompt textbox with respect to the cursor position | |
async function insertTextAtCursor(textArea, result, tagword, tabCompletedWithoutChoice = false) { | |
let text = result.text; | |
let tagType = result.type; | |
let cursorPos = textArea.selectionStart; | |
var sanitizedText = text | |
// Run sanitize queue and use first result as sanitized text | |
sanitizeResults = await processQueueReturn(QUEUE_SANITIZE, null, tagType, text); | |
if (sanitizeResults && sanitizeResults.length > 0) { | |
sanitizedText = sanitizeResults[0]; | |
} else { | |
sanitizedText = TAC_CFG.replaceUnderscores ? text.replaceAll("_", " ") : text; | |
if (TAC_CFG.escapeParentheses && tagType === ResultType.tag) { | |
sanitizedText = sanitizedText | |
.replaceAll("(", "\\(") | |
.replaceAll(")", "\\)") | |
.replaceAll("[", "\\[") | |
.replaceAll("]", "\\]"); | |
} | |
} | |
if ((tagType === ResultType.wildcardFile || tagType === ResultType.yamlWildcard) | |
&& tabCompletedWithoutChoice | |
&& TAC_CFG.wildcardCompletionMode !== "Always fully" | |
&& sanitizedText.includes("/")) { | |
if (TAC_CFG.wildcardCompletionMode === "To next folder level") { | |
let regexMatch = sanitizedText.match(new RegExp(`${escapeRegExp(tagword)}([^/]*\\/?)`, "i")); | |
if (regexMatch) { | |
let pathPart = regexMatch[0]; | |
// In case the completion would have just added a slash, try again one level deeper | |
if (pathPart === `${tagword}/`) { | |
pathPart = sanitizedText.match(new RegExp(`${escapeRegExp(tagword)}\\/([^/]*\\/?)`, "i"))[0]; | |
} | |
sanitizedText = pathPart; | |
} | |
} else if (TAC_CFG.wildcardCompletionMode === "To first difference") { | |
let firstDifference = 0; | |
let longestResult = results.map(x => x.text.length).reduce((a, b) => Math.max(a, b)); | |
// Compare the results to each other to find the first point where they differ | |
for (let i = 0; i < longestResult; i++) { | |
let char = results[0].text[i]; | |
if (results.every(x => x.text[i] === char)) { | |
firstDifference++; | |
} else { | |
break; | |
} | |
} | |
// Don't cut off the __ at the end if it is already the full path | |
if (firstDifference > 0 && firstDifference < longestResult) { | |
// +2 because the sanitized text already has the __ at the start but the matched text doesn't | |
sanitizedText = sanitizedText.substring(0, firstDifference + 2); | |
} else if (firstDifference === 0) { | |
sanitizedText = tagword; | |
} | |
} | |
} | |
var prompt = textArea.value; | |
// Edit prompt text | |
let editStart = Math.max(cursorPos - tagword.length, 0); | |
let editEnd = Math.min(cursorPos + tagword.length, prompt.length); | |
let surrounding = prompt.substring(editStart, editEnd); | |
let match = surrounding.match(new RegExp(escapeRegExp(`${tagword}`), "i")); | |
let afterInsertCursorPos = editStart + match.index + sanitizedText.length; | |
var optionalSeparator = ""; | |
let extraNetworkTypes = [ResultType.hypernetwork, ResultType.lora]; | |
let noCommaTypes = [ResultType.wildcardFile, ResultType.yamlWildcard, ResultType.umiWildcard].concat(extraNetworkTypes); | |
if (!noCommaTypes.includes(tagType)) { | |
// Append comma if enabled and not already present | |
let beforeComma = surrounding.match(new RegExp(`${escapeRegExp(tagword)}[,:]`, "i")) !== null; | |
if (TAC_CFG.appendComma) | |
optionalSeparator = beforeComma ? "" : ","; | |
// Add space if enabled | |
if (TAC_CFG.appendSpace && !beforeComma) | |
optionalSeparator += " "; | |
// If at end of prompt and enabled, override the normal setting if not already added | |
if (!TAC_CFG.appendSpace && TAC_CFG.alwaysSpaceAtEnd) | |
optionalSeparator += surrounding.match(new RegExp(`${escapeRegExp(tagword)}$`, "im")) !== null ? " " : ""; | |
} else if (extraNetworkTypes.includes(tagType)) { | |
// Use the dedicated separator for extra networks if it's defined, otherwise fall back to space | |
optionalSeparator = TAC_CFG.extraNetworksSeparator || " "; | |
} | |
// Escape $ signs since they are special chars for the replace function | |
// We need four since we're also escaping them in replaceAll in the first place | |
sanitizedText = sanitizedText.replaceAll("$", "$$$$"); | |
// Replace partial tag word with new text, add comma if needed | |
let insert = surrounding.replace(match, sanitizedText + optionalSeparator); | |
// Add back start | |
var newPrompt = prompt.substring(0, editStart) + insert + prompt.substring(editEnd); | |
// Add lora/lyco keywords if enabled and found | |
let keywordsLength = 0; | |
if (TAC_CFG.modelKeywordCompletion !== "Never" && (tagType === ResultType.lora || tagType === ResultType.lyco)) { | |
let keywords = null; | |
// Check built-in activation words first | |
if (tagType === ResultType.lora || tagType === ResultType.lyco) { | |
let info = await fetchAPI(`tacapi/v1/lora-info/${result.text}`) | |
if (info && info["activation text"]) { | |
keywords = info["activation text"]; | |
} | |
} | |
if (!keywords && modelKeywordPath.length > 0 && result.hash && result.hash !== "NOFILE" && result.hash.length > 0) { | |
let nameDict = modelKeywordDict.get(result.hash); | |
let names = [result.text + ".safetensors", result.text + ".pt", result.text + ".ckpt"]; | |
// No match, try to find a sha256 match from the cache file | |
if (!nameDict) { | |
const sha256 = await fetchAPI(`/tacapi/v1/lora-cached-hash/${result.text}`) | |
if (sha256) { | |
nameDict = modelKeywordDict.get(sha256); | |
} | |
} | |
if (nameDict) { | |
let found = false; | |
names.forEach(name => { | |
if (!found && nameDict.has(name)) { | |
found = true; | |
keywords = nameDict.get(name); | |
} | |
}); | |
if (!found) | |
keywords = nameDict.get("none"); | |
} | |
} | |
if (keywords && keywords.length > 0) { | |
textBeforeKeywordInsertion = newPrompt; | |
if (TAC_CFG.modelKeywordLocation === "Start of prompt") | |
newPrompt = `${keywords}, ${newPrompt}`; // Insert keywords | |
else if (TAC_CFG.modelKeywordLocation === "End of prompt") | |
newPrompt = `${newPrompt}, ${keywords}`; // Insert keywords | |
else { | |
let keywordStart = prompt[editStart - 1] === " " ? editStart - 1 : editStart; | |
newPrompt = prompt.substring(0, keywordStart) + `, ${keywords} ${insert}` + prompt.substring(editEnd); | |
} | |
textAfterKeywordInsertion = newPrompt; | |
keywordInsertionUndone = false; | |
setTimeout(() => lastEditWasKeywordInsertion = true, 200) | |
keywordsLength = keywords.length + 2; // +2 for the comma and space | |
} | |
} | |
// Insert into prompt textbox and reposition cursor | |
textArea.value = newPrompt; | |
textArea.selectionStart = afterInsertCursorPos + optionalSeparator.length + keywordsLength; | |
textArea.selectionEnd = textArea.selectionStart | |
// Set self trigger flag to show wildcard contents after the filename was inserted | |
if ([ResultType.wildcardFile, ResultType.yamlWildcard, ResultType.umiWildcard].includes(result.type)) | |
tacSelfTrigger = true; | |
// Since we've modified a Gradio Textbox component manually, we need to simulate an `input` DOM event to ensure it's propagated back to python. | |
// Uses a built-in method from the webui's ui.js which also already accounts for event target | |
updateInput(textArea); | |
// Update previous tags with the edited prompt to prevent re-searching the same term | |
let weightedTags = [...newPrompt.matchAll(WEIGHT_REGEX)] | |
.map(match => match[1]); | |
let tags = newPrompt.match(TAG_REGEX) | |
if (weightedTags !== null) { | |
tags = tags.filter(tag => !weightedTags.some(weighted => tag.includes(weighted))) | |
.concat(weightedTags); | |
} | |
previousTags = tags; | |
// Callback | |
let returns = await processQueueReturn(QUEUE_AFTER_INSERT, null, tagType, sanitizedText, newPrompt, textArea); | |
// Return if any queue function returned true (has handled hide/show already) | |
if (returns.some(x => x === true)) | |
return; | |
// Hide results after inserting, if it hasn't been hidden already by a queue function | |
if (!hideBlocked && isVisible(textArea)) { | |
hideResults(textArea); | |
} | |
} | |
function addResultsToList(textArea, results, tagword, resetList) { | |
let textAreaId = getTextAreaIdentifier(textArea); | |
let resultDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId); | |
let resultsList = resultDiv.querySelector('ul'); | |
// Reset list, selection and scrollTop since the list changed | |
if (resetList) { | |
resultsList.innerHTML = ""; | |
selectedTag = null; | |
oldSelectedTag = null; | |
resultDiv.scrollTop = 0; | |
resultCount = 0; | |
} | |
// Find right colors from config | |
let tagFileName = TAC_CFG.tagFile.split(".")[0]; | |
let tagColors = TAC_CFG.colorMap; | |
let mode = (document.querySelector(".dark") || gradioApp().querySelector(".dark")) ? 0 : 1; | |
let nextLength = Math.min(results.length, resultCount + TAC_CFG.resultStepLength); | |
for (let i = resultCount; i < nextLength; i++) { | |
let result = results[i]; | |
// Skip if the result is null or undefined | |
if (!result) | |
continue; | |
let li = document.createElement("li"); | |
let flexDiv = document.createElement("div"); | |
flexDiv.classList.add("resultsFlexContainer"); | |
li.appendChild(flexDiv); | |
let itemText = document.createElement("div"); | |
itemText.classList.add("acListItem"); | |
let displayText = ""; | |
// If the tag matches the tagword, we don't need to display the alias | |
if(result.type === ResultType.chant) { | |
displayText = escapeHTML(result.aliases); | |
} else if (result.aliases && !result.text.includes(tagword)) { // Alias | |
let splitAliases = result.aliases.split(","); | |
let bestAlias = splitAliases.find(a => a.toLowerCase().includes(tagword)); | |
// search in translations if no alias matches | |
if (!bestAlias) { | |
let tagOrAlias = pair => pair[0] === result.text || splitAliases.includes(pair[0]); | |
var tArray = [...translations]; | |
if (tArray) { | |
var translationKey = [...translations].find(pair => tagOrAlias(pair) && pair[1].includes(tagword)); | |
if (translationKey) | |
bestAlias = translationKey[0]; | |
} | |
} | |
displayText = escapeHTML(bestAlias); | |
// Append translation for alias if it exists and is not what the user typed | |
if (translations.has(bestAlias) && translations.get(bestAlias) !== bestAlias && bestAlias !== result.text) | |
displayText += `[${translations.get(bestAlias)}]`; | |
if (!TAC_CFG.alias.onlyShowAlias && result.text !== bestAlias) | |
displayText += " ➝ " + result.text; | |
} else { // No alias | |
displayText = escapeHTML(result.text); | |
} | |
// Append translation for result if it exists | |
if (translations.has(result.text)) | |
displayText += `[${translations.get(result.text)}]`; | |
// Print search term bolded in result | |
itemText.innerHTML = displayText.replace(tagword, `<b>${tagword}</b>`); | |
const splitTypes = [ResultType.wildcardFile, ResultType.yamlWildcard] | |
if (splitTypes.includes(result.type) && itemText.innerHTML.includes("/")) { | |
let parts = itemText.innerHTML.split("/"); | |
let lastPart = parts[parts.length - 1]; | |
parts = parts.slice(0, parts.length - 1); | |
itemText.innerHTML = "<span class='acPathPart'>" + parts.join("</span><span class='acPathPart'>/") + "</span>" + "/" + lastPart; | |
} | |
// Add wiki link if the setting is enabled and a supported tag set loaded | |
if (TAC_CFG.showWikiLinks | |
&& (result.type === ResultType.tag) | |
&& (tagFileName.toLowerCase().startsWith("danbooru") || tagFileName.toLowerCase().startsWith("e621"))) { | |
let wikiLink = document.createElement("a"); | |
wikiLink.classList.add("acWikiLink"); | |
wikiLink.innerText = "?"; | |
let linkPart = displayText; | |
// Only use alias result if it is one | |
if (displayText.includes("➝")) | |
linkPart = displayText.split(" ➝ ")[1]; | |
// Remove any trailing translations | |
if (linkPart.includes("[")) { | |
linkPart = linkPart.split("[")[0] | |
} | |
linkPart = encodeURIComponent(linkPart); | |
// Set link based on selected file | |
let tagFileNameLower = tagFileName.toLowerCase(); | |
if (tagFileNameLower.startsWith("danbooru")) { | |
wikiLink.href = `https://danbooru.donmai.us/wiki_pages/${linkPart}`; | |
} else if (tagFileNameLower.startsWith("e621")) { | |
wikiLink.href = `https://e621.net/wiki_pages/${linkPart}`; | |
} | |
wikiLink.target = "_blank"; | |
flexDiv.appendChild(wikiLink); | |
} | |
flexDiv.appendChild(itemText); | |
// Add post count & color if it's a tag | |
// Wildcards & Embeds have no tag category | |
if (result.category) { | |
// Set the color of the tag | |
let cat = result.category; | |
let colorGroup = tagColors[tagFileName]; | |
// Default to danbooru scheme if no matching one is found | |
if (!colorGroup) | |
colorGroup = tagColors["danbooru"]; | |
// Set tag type to invalid if not found | |
if (!colorGroup[cat]) | |
cat = "-1"; | |
flexDiv.style = `color: ${colorGroup[cat][mode]};`; | |
} | |
// Post count | |
if (result.count && !isNaN(result.count)) { | |
let postCount = result.count; | |
let formatter; | |
// Danbooru formats numbers with a padded fraction for 1M or 1k, but not for 10/100k | |
if (postCount >= 1000000 || (postCount >= 1000 && postCount < 10000)) | |
formatter = Intl.NumberFormat("en", { notation: "compact", minimumFractionDigits: 1, maximumFractionDigits: 1 }); | |
else | |
formatter = Intl.NumberFormat("en", {notation: "compact"}); | |
let formattedCount = formatter.format(postCount); | |
let countDiv = document.createElement("div"); | |
countDiv.textContent = formattedCount; | |
countDiv.classList.add("acMetaText"); | |
flexDiv.appendChild(countDiv); | |
} else if (result.meta) { // Check if there is meta info to display | |
let metaDiv = document.createElement("div"); | |
metaDiv.textContent = result.meta; | |
metaDiv.classList.add("acMetaText"); | |
// Add version info classes if it is an embedding | |
if (result.type === ResultType.embedding) { | |
if (result.meta.startsWith("v1")) | |
itemText.classList.add("acEmbeddingV1"); | |
else if (result.meta.startsWith("v2")) | |
itemText.classList.add("acEmbeddingV2"); | |
} | |
flexDiv.appendChild(metaDiv); | |
} | |
// Add listener | |
li.addEventListener("click", function () { insertTextAtCursor(textArea, result, tagword); }); | |
// Add element to list | |
resultsList.appendChild(li); | |
} | |
resultCount = nextLength; | |
if (resetList) { | |
selectedTag = null; | |
oldSelectedTag = null; | |
resultDiv.scrollTop = 0; | |
} | |
} | |
async function updateSelectionStyle(textArea, newIndex, oldIndex) { | |
let textAreaId = getTextAreaIdentifier(textArea); | |
let resultDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId); | |
let resultsList = resultDiv.querySelector('ul'); | |
let items = resultsList.getElementsByTagName('li'); | |
if (oldIndex != null) { | |
items[oldIndex].classList.remove('selected'); | |
} | |
// make it safer | |
if (newIndex !== null) { | |
let selected = items[newIndex]; | |
selected.classList.add('selected'); | |
// Set scrolltop to selected item | |
resultDiv.scrollTop = selected.offsetTop - resultDiv.offsetTop; | |
} | |
// Show preview if enabled and the selected type supports it | |
if (newIndex !== null) { | |
let selected = items[newIndex]; | |
let previewTypes = ["v1 Embedding", "v2 Embedding", "Hypernetwork", "Lora", "Lyco"]; | |
let selectedType = selected.querySelector(".acMetaText").innerText; | |
let selectedFilename = selected.querySelector(".acListItem").innerText; | |
let previewDiv = gradioApp().querySelector(`.autocompleteParent${textAreaId} .sideInfo`); | |
if (TAC_CFG.showExtraNetworkPreviews && previewTypes.includes(selectedType)) { | |
let shorthandType = ""; | |
switch (selectedType) { | |
case "v1 Embedding": | |
case "v2 Embedding": | |
shorthandType = "embed"; | |
break; | |
case "Hypernetwork": | |
shorthandType = "hyper"; | |
break; | |
case "Lora": | |
shorthandType = "lora"; | |
break; | |
case "Lyco": | |
shorthandType = "lyco"; | |
break; | |
} | |
let img = previewDiv.querySelector("img"); | |
let url = await getExtraNetworkPreviewURL(selectedFilename, shorthandType); | |
if (url) { | |
img.src = url; | |
previewDiv.style.display = "block"; | |
} else { | |
previewDiv.style.display = "none"; | |
} | |
} else { | |
previewDiv.style.display = "none"; | |
} | |
} | |
} | |
// Check if the last edit was the keyword insertion, and catch undo/redo in that case | |
function checkKeywordInsertionUndo(textArea, event) { | |
if (TAC_CFG.modelKeywordCompletion === "Never") return; | |
switch (event.inputType) { | |
case "historyUndo": | |
if (lastEditWasKeywordInsertion && !keywordInsertionUndone) { | |
keywordInsertionUndone = true; | |
textArea.value = textBeforeKeywordInsertion; | |
tacSelfTrigger = true; | |
updateInput(textArea); | |
} | |
break; | |
case "historyRedo": | |
if (lastEditWasKeywordInsertion && keywordInsertionUndone) { | |
keywordInsertionUndone = false; | |
textArea.value = textAfterKeywordInsertion; | |
tacSelfTrigger = true; | |
updateInput(textArea); | |
} | |
case undefined: | |
// undefined is caused by the updateInput event firing, so we just ignore it | |
break; | |
default: | |
// Everything else deactivates the keyword undo and returns to normal undo behavior | |
lastEditWasKeywordInsertion = false; | |
keywordInsertionUndone = false; | |
textBeforeKeywordInsertion = ""; | |
textAfterKeywordInsertion = ""; | |
break; | |
} | |
} | |
async function autocomplete(textArea, prompt, fixedTag = null) { | |
// Return if the function is deactivated in the UI | |
// Guard for empty prompt | |
if (prompt.length === 0) { | |
hideResults(textArea); | |
previousTags = []; | |
tagword = ""; | |
return; | |
} | |
if (fixedTag === null) { | |
// Match tags with RegEx to get the last edited one | |
// We also match for the weighting format (e.g. "tag:1.0") here, and combine the two to get the full tag word set | |
let weightedTags = [...prompt.matchAll(WEIGHT_REGEX)] | |
.map(match => match[1]); | |
let tags = prompt.match(TAG_REGEX) | |
if (weightedTags !== null && tags !== null) { | |
tags = tags.filter(tag => !weightedTags.some(weighted => tag.includes(weighted) && !tag.startsWith("<[") && !tag.startsWith("$("))) | |
.concat(weightedTags); | |
} | |
// Guard for no tags | |
if (!tags || tags.length === 0) { | |
previousTags = []; | |
tagword = ""; | |
hideResults(textArea); | |
return; | |
} | |
let tagCountChange = tags.length - previousTags.length; | |
let diff = difference(tags, previousTags); | |
previousTags = tags; | |
// Guard for no difference / only whitespace remaining / last edited tag was fully removed | |
if (diff === null || diff.length === 0 || (diff.length === 1 && tagCountChange < 0)) { | |
if (!hideBlocked) hideResults(textArea); | |
return; | |
} | |
tagword = diff[0] | |
// Guard for empty tagword | |
if (tagword === null || tagword.length === 0) { | |
hideResults(textArea); | |
return; | |
} | |
} else { | |
tagword = fixedTag; | |
} | |
results = []; | |
resultCountBeforeNormalTags = 0; | |
tagword = tagword.toLowerCase().replace(/[\n\r]/g, ""); | |
// Process all parsers | |
let resultCandidates = (await processParsers(textArea, prompt))?.filter(x => x.length > 0); | |
// If one ore more result candidates match, use their results | |
if (resultCandidates && resultCandidates.length > 0) { | |
// Flatten our candidate(s) | |
results = resultCandidates.flat(); | |
// Sort results, but not if it's umi tags since they are sorted by count | |
if (!(resultCandidates.length === 1 && results[0].type === ResultType.umiWildcard)) | |
results = results.sort(getSortFunction()); | |
// Since some tags are kaomoji, we have to add the normal results in some cases | |
if (tagword.startsWith("<") || tagword.startsWith("*<")) { | |
// Create escaped search regex with support for * as a start placeholder | |
let searchRegex; | |
if (tagword.startsWith("*")) { | |
tagword = tagword.slice(1); | |
searchRegex = new RegExp(`${escapeRegExp(tagword)}`, 'i'); | |
} else { | |
searchRegex = new RegExp(`(^|[^a-zA-Z])${escapeRegExp(tagword)}`, 'i'); | |
} | |
let genericResults = allTags.filter(x => x[0].toLowerCase().search(searchRegex) > -1).slice(0, TAC_CFG.maxResults); | |
genericResults.forEach(g => { | |
let result = new AutocompleteResult(g[0].trim(), ResultType.tag) | |
result.category = g[1]; | |
result.count = g[2]; | |
result.aliases = g[3]; | |
results.push(result); | |
}); | |
} | |
} | |
// Else search the normal tag list | |
if (!resultCandidates || resultCandidates.length === 0 | |
|| (TAC_CFG.includeEmbeddingsInNormalResults && !(tagword.startsWith("<") || tagword.startsWith("*<"))) | |
) { | |
resultCountBeforeNormalTags = results.length; | |
// Create escaped search regex with support for * as a start placeholder | |
let searchRegex; | |
if (tagword.startsWith("*")) { | |
tagword = tagword.slice(1); | |
searchRegex = new RegExp(`${escapeRegExp(tagword)}`, 'i'); | |
} else { | |
searchRegex = new RegExp(`(^|[^a-zA-Z])${escapeRegExp(tagword)}`, 'i'); | |
} | |
// Both normal tags and aliases/translations are included depending on the config | |
let baseFilter = (x) => x[0].toLowerCase().search(searchRegex) > -1; | |
let aliasFilter = (x) => x[3] && x[3].toLowerCase().search(searchRegex) > -1; | |
let translationFilter = (x) => (translations.has(x[0]) && translations.get(x[0]).toLowerCase().search(searchRegex) > -1) | |
|| x[3] && x[3].split(",").some(y => translations.has(y) && translations.get(y).toLowerCase().search(searchRegex) > -1); | |
let fil; | |
if (TAC_CFG.alias.searchByAlias && TAC_CFG.translation.searchByTranslation) | |
fil = (x) => baseFilter(x) || aliasFilter(x) || translationFilter(x); | |
else if (TAC_CFG.alias.searchByAlias && !TAC_CFG.translation.searchByTranslation) | |
fil = (x) => baseFilter(x) || aliasFilter(x); | |
else if (TAC_CFG.translation.searchByTranslation && !TAC_CFG.alias.searchByAlias) | |
fil = (x) => baseFilter(x) || translationFilter(x); | |
else | |
fil = (x) => baseFilter(x); | |
// Add final results | |
allTags.filter(fil).forEach(t => { | |
let result = new AutocompleteResult(t[0].trim(), ResultType.tag) | |
result.category = t[1]; | |
result.count = t[2]; | |
result.aliases = t[3]; | |
results.push(result); | |
}); | |
// Add extras | |
if (TAC_CFG.extra.extraFile) { | |
let extraResults = []; | |
extras.filter(fil).forEach(e => { | |
let result = new AutocompleteResult(e[0].trim(), ResultType.extra) | |
result.category = e[1] || 0; // If no category is given, use 0 as the default | |
result.meta = e[2] || "Custom tag"; | |
result.aliases = e[3] || ""; | |
extraResults.push(result); | |
}); | |
if (TAC_CFG.extra.addMode === "Insert before") { | |
results = extraResults.concat(results); | |
} else { | |
results = results.concat(extraResults); | |
} | |
} | |
// Slice if the user has set a max result count | |
if (!TAC_CFG.showAllResults) { | |
results = results.slice(0, TAC_CFG.maxResults + resultCountBeforeNormalTags); | |
} | |
} | |
// Guard for empty results | |
if (!results || results.length === 0) { | |
//console.log('No results found for "' + tagword + '"'); | |
hideResults(textArea); | |
return; | |
} | |
addResultsToList(textArea, results, tagword, true); | |
showResults(textArea); | |
} | |
function navigateInList(textArea, event) { | |
// Return if the function is deactivated in the UI or the current model is excluded due to white/blacklist settings | |
let keys = TAC_CFG.keymap; | |
// Close window if Home or End is pressed while not a keybinding, since it would break completion on leaving the original tag | |
if ((event.key === "Home" || event.key === "End") && !Object.values(keys).includes(event.key)) { | |
hideResults(textArea); | |
return; | |
} | |
// All set keys that are not None or empty are valid | |
// Default keys are: ArrowUp, ArrowDown, PageUp, PageDown, Home, End, Enter, Tab, Escape | |
validKeys = Object.values(keys).filter(x => x !== "None" && x !== ""); | |
if (!validKeys.includes(event.key)) return; | |
if (!isVisible(textArea)) return | |
// Add modifier keys to base as text+. | |
let modKey = ""; | |
if (event.ctrlKey) modKey += "Ctrl+"; | |
if (event.altKey) modKey += "Alt+"; | |
if (event.shiftKey) modKey += "Shift+"; | |
if (event.metaKey) modKey += "Meta+"; | |
modKey += event.key; | |
oldSelectedTag = selectedTag; | |
switch (modKey) { | |
case keys["MoveUp"]: | |
if (selectedTag === null) { | |
selectedTag = resultCount - 1; | |
} else { | |
selectedTag = (selectedTag - 1 + resultCount) % resultCount; | |
} | |
break; | |
case keys["MoveDown"]: | |
if (selectedTag === null) { | |
selectedTag = 0; | |
} else { | |
selectedTag = (selectedTag + 1) % resultCount; | |
} | |
break; | |
case keys["JumpUp"]: | |
if (selectedTag === null || selectedTag === 0) { | |
selectedTag = resultCount - 1; | |
} else { | |
selectedTag = (Math.max(selectedTag - 5, 0) + resultCount) % resultCount; | |
} | |
break; | |
case keys["JumpDown"]: | |
if (selectedTag === null || selectedTag === resultCount - 1) { | |
selectedTag = 0; | |
} else { | |
selectedTag = Math.min(selectedTag + 5, resultCount - 1) % resultCount; | |
} | |
break; | |
case keys["JumpToStart"]: | |
if (TAC_CFG.includeEmbeddingsInNormalResults && | |
selectedTag > resultCountBeforeNormalTags && | |
resultCountBeforeNormalTags > 0 | |
) { | |
selectedTag = resultCountBeforeNormalTags; | |
} else { | |
selectedTag = 0; | |
} | |
break; | |
case keys["JumpToEnd"]: | |
// Jump to the end of the list, or the end of embeddings if they are included in the normal results | |
if (TAC_CFG.includeEmbeddingsInNormalResults && | |
selectedTag < resultCountBeforeNormalTags && | |
resultCountBeforeNormalTags > 0 | |
) { | |
selectedTag = Math.min(resultCountBeforeNormalTags, resultCount - 1); | |
} else { | |
selectedTag = resultCount - 1; | |
} | |
break; | |
case keys["ChooseSelected"]: | |
if (selectedTag !== null) { | |
insertTextAtCursor(textArea, results[selectedTag], tagword); | |
} else { | |
hideResults(textArea); | |
return; | |
} | |
break; | |
case keys["ChooseFirstOrSelected"]: | |
let withoutChoice = false; | |
if (selectedTag === null) { | |
selectedTag = 0; | |
withoutChoice = true; | |
} else if (TAC_CFG.wildcardCompletionMode === "To next folder level") { | |
withoutChoice = true; | |
} | |
insertTextAtCursor(textArea, results[selectedTag], tagword, withoutChoice); | |
break; | |
case keys["Close"]: | |
hideResults(textArea); | |
break; | |
default: | |
if (event.ctrlKey || event.altKey || event.shiftKey || event.metaKey) return; | |
} | |
let moveKeys = [keys["MoveUp"], keys["MoveDown"], keys["JumpUp"], keys["JumpDown"], keys["JumpToStart"], keys["JumpToEnd"]]; | |
if (selectedTag === resultCount - 1 && moveKeys.includes(event.key)) { | |
addResultsToList(textArea, results, tagword, false); | |
} | |
// Update highlighting | |
if (selectedTag !== null) | |
updateSelectionStyle(textArea, selectedTag, oldSelectedTag); | |
// Prevent default behavior | |
event.preventDefault(); | |
event.stopPropagation(); | |
} | |
function addAutocompleteToArea(area) { | |
// Return if autocomplete is disabled for the current area type in config | |
let textAreaId = getTextAreaIdentifier(area); | |
if ((!TAC_CFG.activeIn.img2img && textAreaId.includes("img2img")) | |
|| (!TAC_CFG.activeIn.txt2img && textAreaId.includes("txt2img")) | |
|| (!TAC_CFG.activeIn.negativePrompts && textAreaId.includes("n")) | |
|| (!TAC_CFG.activeIn.thirdParty && textAreaId.includes("thirdParty"))) { | |
return; | |
} | |
// Only add listeners once | |
if (!area.classList.contains('autocomplete')) { | |
// Add our new element | |
var resultsDiv = createResultsDiv(area); | |
area.parentNode.insertBefore(resultsDiv, area.nextSibling); | |
// Hide by default so it doesn't show up on page load | |
hideResults(area); | |
// Add autocomplete event listener | |
area.addEventListener('input', (e) => { | |
// Cancel autocomplete itself if the event has no inputType (e.g. because it was triggered by the updateInput() function) | |
if (!e.inputType && !tacSelfTrigger) return; | |
tacSelfTrigger = false; | |
debounce(autocomplete(area, area.value), TAC_CFG.delayTime); | |
checkKeywordInsertionUndo(area, e); | |
}); | |
// Add focusout event listener | |
area.addEventListener('focusout', debounce(() => { | |
if (!hideBlocked) | |
hideResults(area); | |
}, 400)); | |
// Add up and down arrow event listener | |
area.addEventListener('keydown', (e) => navigateInList(area, e)); | |
// CompositionEnd fires after the user has finished IME composing | |
// We need to block hide here to prevent the enter key from insta-closing the results | |
area.addEventListener('compositionend', () => { | |
hideBlocked = true; | |
setTimeout(() => { hideBlocked = false; }, 100); | |
}); | |
// Add class so we know we've already added the listeners | |
area.classList.add('autocomplete'); | |
} | |
} | |
// One-time setup, triggered from onUiUpdate | |
async function setup() { | |
// Load external files needed by completion extensions | |
await processQueue(QUEUE_FILE_LOAD, null); | |
// Find all textareas | |
let textAreas = getTextAreas(); | |
// Add mutation observer to accordions inside a base that has onDemand set to true | |
addOnDemandObservers(addAutocompleteToArea); | |
// Not found, we're on a page without prompt textareas | |
if (textAreas.every(v => v === null || v === undefined)) return; | |
// Already added or unnecessary to add | |
if (gradioApp().querySelector('.autocompleteParent.p')) { | |
if (gradioApp().querySelector('.autocompleteParent.n') || !TAC_CFG.activeIn.negativePrompts) { | |
return; | |
} | |
} else if (!TAC_CFG.activeIn.txt2img && !TAC_CFG.activeIn.img2img) { | |
return; | |
} | |
textAreas.forEach(area => addAutocompleteToArea(area)); | |
textAreas.forEach(area => area.offsetParent.style.setProperty('overflow', 'visible')); | |
textAreas[0].offsetParent.parentElement.style.overflow = 'visible'; | |
// Add style to dom | |
let acStyle = document.createElement('style'); | |
let mode = (document.querySelector(".dark") || gradioApp().querySelector(".dark")) ? 0 : 1; | |
// Check if we are on webkit | |
let browser = navigator.userAgent.toLowerCase().indexOf('firefox') > -1 ? "firefox" : "other"; | |
let css = autocompleteCSS; | |
// Replace vars with actual values (can't use actual css vars because of the way we inject the css) | |
Object.keys(styleColors).forEach((key) => { | |
css = css.replaceAll(`var(${key})`, styleColors[key][mode]); | |
}) | |
Object.keys(browserVars).forEach((key) => { | |
css = css.replaceAll(`var(${key})`, browserVars[key][browser]); | |
}) | |
if (acStyle.styleSheet) { | |
acStyle.styleSheet.cssText = css; | |
} else { | |
acStyle.appendChild(document.createTextNode(css)); | |
} | |
gradioApp().appendChild(acStyle); | |
// Callback | |
await processQueue(QUEUE_AFTER_SETUP, null); | |
} | |
var tacLoading = false; | |
async function run() { | |
if (tacLoading) return; | |
if (!TAC_CFG) return; | |
tacLoading = true; | |
// Get our tag base path from the temp file | |
// Rest of setup | |
await loadTags(TAC_CFG); | |
setup(); | |
tacLoading = false; | |
} | |
run(); | |