290 lines
12 KiB
JavaScript
290 lines
12 KiB
JavaScript
import { app } from "../../scripts/app.js";
|
|
import { api } from "../../scripts/api.js";
|
|
import { ttN_CreateDropdown, ttN_RemoveDropdown } from "./ttNdropdown.js";
|
|
|
|
// Initialize some global lists and objects.
|
|
let autoCompleteDict = {}; // {prefix: [suggestions]}
|
|
let autoCompleteHierarchy = {};
|
|
let nsp_keys = ['3d-terms', 'adj-architecture', 'adj-beauty', 'adj-general', 'adj-horror', 'album-cover', 'animals', 'artist', 'artist-botanical', 'artist-surreal', 'aspect-ratio', 'band', 'bird', 'body-fit', 'body-heavy', 'body-light', 'body-poor', 'body-shape', 'body-short', 'body-tall', 'bodyshape', 'camera', 'camera-manu', 'celeb', 'color', 'color-palette', 'comic', 'cosmic-galaxy', 'cosmic-nebula', 'cosmic-star', 'cosmic-terms', 'details', 'dinosaur', 'eyecolor', 'f-stop', 'fantasy-creature', 'fantasy-setting', 'fish', 'flower', 'focal-length', 'foods', 'forest-type', 'fruit', 'games', 'gen-modifier', 'gender', 'gender-ext', 'hair', 'hd', 'identity', 'identity-adult', 'identity-young', 'iso-stop', 'landscape-type', 'movement', 'movie', 'movie-director', 'nationality', 'natl-park', 'neg-weight', 'noun-beauty', 'noun-emote', 'noun-fantasy', 'noun-general', 'noun-horror', 'occupation', 'penciller', 'photo-term', 'pop-culture', 'pop-location', 'portrait-type', 'punk', 'quantity', 'rpg-Item', 'scenario-desc', 'site', 'skin-color', 'style', 'tree', 'trippy', 'water', 'wh-site']
|
|
|
|
function getFileName(path) {
|
|
return path.split(/[\/:\\]/).pop();
|
|
}
|
|
|
|
function getCurrentWord(widget) {
|
|
const formattedInput = widget.inputEl.value.replace(/>\s*/g, '> ').replace(/\s+/g, ' ');
|
|
const words = formattedInput.split(' ');
|
|
|
|
const adjustedInput = widget.inputEl.value.substring(0, widget.inputEl.selectionStart)
|
|
.replace(/>\s*/g, '> ').replace(/\s+/g, ' ');
|
|
|
|
const currentWordPosition = adjustedInput.split(' ').length - 1;
|
|
|
|
return words[currentWordPosition].toLowerCase();
|
|
}
|
|
|
|
function isTriggerWord(word) {
|
|
for (let prefix in autoCompleteDict) {
|
|
if ((prefix.startsWith(word) && word.length > 1) || word.startsWith(prefix)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
const _generatePrefixes = (str) => {
|
|
const prefixes = [];
|
|
while (str.length > 1) {
|
|
prefixes.push(str);
|
|
str = str.substring(0, str.length - 1);
|
|
}
|
|
return prefixes;
|
|
};
|
|
|
|
function _cleanInputWord(word) {
|
|
let prefixesToRemove = [];
|
|
for (let prefix in autoCompleteDict) {
|
|
prefixesToRemove = [...prefixesToRemove, ..._generatePrefixes(prefix)];
|
|
}
|
|
let cleanedWord = prefixesToRemove.reduce((acc, prefix) => acc.replace(prefix, ''), word.toLowerCase());
|
|
if (cleanedWord.includes(':')) {
|
|
const parts = cleanedWord.split(':');
|
|
cleanedWord = parts[0];
|
|
}
|
|
return cleanedWord.replace(/\//g, "\\");
|
|
}
|
|
|
|
function getSuggestionsForWord(word) {
|
|
let suggestions = [];
|
|
for (let prefix in autoCompleteDict) {
|
|
if ((prefix.startsWith(word) && word.length > 1) || word.startsWith(prefix)) {
|
|
suggestions = autoCompleteDict['fpath_' + prefix]; // Get suggestions from the dictionary
|
|
break;
|
|
}
|
|
}
|
|
const cleanedWord = _cleanInputWord(word);
|
|
// Filter suggestions based on the cleaned word
|
|
return suggestions.filter(suggestion =>
|
|
suggestion.toLowerCase().includes(cleanedWord) || getFileName(suggestion).toLowerCase().includes(cleanedWord)
|
|
);
|
|
}
|
|
|
|
|
|
function _convertListToHierarchy(list) {
|
|
const hierarchy = {};
|
|
list.forEach(item => {
|
|
const parts = item.split(/:\\|\\/);
|
|
let node = hierarchy;
|
|
parts.forEach((part, idx) => {
|
|
node = node[part] = (idx === parts.length - 1) ? null : (node[part] || {});
|
|
});
|
|
});
|
|
return hierarchy;
|
|
}
|
|
|
|
function _insertSuggestion(widget, suggestion) {
|
|
const formattedInput = widget.inputEl.value.replace(/>\s*/g, '> ').replace(/\s+/g, ' ');
|
|
const inputSegments = formattedInput.split(' ');
|
|
|
|
const adjustedInput = widget.inputEl.value.substring(0, widget.inputEl.selectionStart)
|
|
.replace(/>\s*/g, '> ').replace(/\s+/g, ' ');
|
|
const currentSegmentIndex = adjustedInput.split(' ').length - 1;
|
|
|
|
let matchedPrefix = '';
|
|
let currentSegment = inputSegments[currentSegmentIndex].toLowerCase();
|
|
if (["loras", "refiner_loras"].includes(widget.name) && ['', ' ','<','<l'].includes(currentSegment)) {
|
|
currentSegment = '<lora:';
|
|
}
|
|
|
|
for (let prefix in autoCompleteDict) {
|
|
const shortPrefix = prefix.substring(0, 1).toLowerCase();
|
|
if (currentSegment.startsWith(shortPrefix)) {
|
|
matchedPrefix = prefix;
|
|
break;
|
|
}
|
|
}
|
|
|
|
let suffix = '';
|
|
if (matchedPrefix === '<lora:') {
|
|
let oldSuffix = currentSegment.replace('<lora:', '').split(':', 2)[1];
|
|
if (oldSuffix && oldSuffix.includes('>')) {
|
|
oldSuffix = oldSuffix.split('>')[0] + '>';
|
|
}
|
|
suffix = oldSuffix ? ':' + oldSuffix : ':1>';
|
|
}
|
|
if (matchedPrefix === '__') {
|
|
suffix = '__';
|
|
}
|
|
|
|
inputSegments[currentSegmentIndex] = matchedPrefix + suggestion + suffix;
|
|
return inputSegments.join(' ');
|
|
}
|
|
|
|
function showSuggestionsDropdown(widget, suggestions) {
|
|
const hierarchy = _convertListToHierarchy(suggestions);
|
|
ttN_CreateDropdown(widget.inputEl, hierarchy, selected => {
|
|
widget.inputEl.value = _insertSuggestion(widget, selected);
|
|
}, true);
|
|
}
|
|
|
|
|
|
function _initializeAutocompleteData(initialList, prefix) {
|
|
autoCompleteDict['fpath_' + prefix] = initialList
|
|
autoCompleteDict[prefix] = initialList.map(getFileName).map(item => prefix + item);
|
|
}
|
|
|
|
function _initializeAutocompleteList(initialList, prefix) {
|
|
autoCompleteDict['fpath_' + prefix] = initialList
|
|
autoCompleteDict[prefix] = initialList.map(item => prefix + item);
|
|
}
|
|
|
|
function _isRelevantWidget(widget) {
|
|
return (["customtext", "ttNhidden"].includes(widget.type) && (widget.dynamicPrompts !== false) || widget.dynamicPrompts) && !_isLorasWidget(widget);
|
|
}
|
|
|
|
function _isLorasWidget(widget) {
|
|
return (["customtext", "ttNhidden"].includes(widget.type) && ["loras", "refiner_loras"].includes(widget.name));
|
|
}
|
|
|
|
function findPysssss(lora=false) {
|
|
const found = JSON.parse(localStorage.getItem("Comfy.Settings.pysssss.AutoCompleter")) || false;
|
|
if (found && lora) {
|
|
return JSON.parse(localStorage.getItem("pysssss.AutoCompleter.ShowLoras")) || false;
|
|
}
|
|
return found;
|
|
}
|
|
|
|
function _attachInputHandler(widget) {
|
|
if (!widget.ttNhandleInput) {
|
|
widget.ttNhandleInput = () => {
|
|
if (findPysssss()) {
|
|
return
|
|
}
|
|
|
|
let currentWord = getCurrentWord(widget);
|
|
if (isTriggerWord(currentWord)) {
|
|
const suggestions = getSuggestionsForWord(currentWord);
|
|
if (suggestions.length > 0) {
|
|
showSuggestionsDropdown(widget, suggestions);
|
|
} else {
|
|
ttN_RemoveDropdown();
|
|
}
|
|
} else {
|
|
ttN_RemoveDropdown();
|
|
}
|
|
};
|
|
}
|
|
['input', 'mousedown'].forEach(event => {
|
|
widget?.inputEl?.removeEventListener(event, widget.ttNhandleInput);
|
|
if (findPysssss()) {
|
|
return
|
|
}
|
|
widget?.inputEl?.addEventListener(event, widget.ttNhandleInput);
|
|
});
|
|
}
|
|
|
|
function _attachLorasHandler(widget) {
|
|
if (!widget.ttNhandleLorasInput) {
|
|
widget.ttNhandleLorasInput = () => {
|
|
if (findPysssss(true)) {
|
|
return
|
|
}
|
|
let currentWord = getCurrentWord(widget);
|
|
if (['',' ','<','<l'].includes(currentWord)) {
|
|
currentWord = '<lora:';
|
|
}
|
|
if (isTriggerWord(currentWord)) {
|
|
const suggestions = getSuggestionsForWord(currentWord);
|
|
if (suggestions.length > 0) {
|
|
showSuggestionsDropdown(widget, suggestions);
|
|
} else {
|
|
ttN_RemoveDropdown();
|
|
}
|
|
} else {
|
|
ttN_RemoveDropdown();
|
|
}
|
|
};
|
|
}
|
|
|
|
['input', 'mouseup'].forEach(event => {
|
|
widget?.inputEl?.removeEventListener(event, widget.ttNhandleLorasInput);
|
|
if (findPysssss(true)) {
|
|
return
|
|
}
|
|
widget?.inputEl?.addEventListener(event, widget.ttNhandleLorasInput);
|
|
});
|
|
|
|
if (!widget.ttNhandleScrollInput) {
|
|
widget.ttNhandleScrollInput = (event) => {
|
|
event.preventDefault();
|
|
|
|
const step = event.ctrlKey ? 0.1 : 0.01;
|
|
|
|
// Determine the scroll direction
|
|
const direction = Math.sign(event.deltaY); // Will be -1 for scroll up, 1 for scroll down
|
|
|
|
// Get the current selection
|
|
const inputEl = widget.inputEl;
|
|
let selectionStart = inputEl.selectionStart;
|
|
let selectionEnd = inputEl.selectionEnd;
|
|
const selected = inputEl.value.substring(selectionStart, selectionEnd);
|
|
|
|
if (selected === 'lora' || selected === 'skip') {
|
|
const swapWith = selected === 'lora' ? 'skip' : 'lora';
|
|
inputEl.value = inputEl.value.substring(0, selectionStart) + swapWith + inputEl.value.substring(selectionEnd);
|
|
inputEl.setSelectionRange(selectionStart, selectionStart + swapWith.length);
|
|
return
|
|
}
|
|
|
|
// Expand the selection to make sure the whole number is selected
|
|
while (selectionStart > 0 && /\d|\.|-/.test(inputEl.value.charAt(selectionStart - 1))) {
|
|
selectionStart--;
|
|
}
|
|
while (selectionEnd < inputEl.value.length && /\d|\.|-/.test(inputEl.value.charAt(selectionEnd))) {
|
|
selectionEnd++;
|
|
}
|
|
|
|
const selectedText = inputEl.value.substring(selectionStart, selectionEnd);
|
|
|
|
// Check if the selected text is a number
|
|
if (!isNaN(selectedText) && selectedText.trim() !== '') {
|
|
let trail = selectedText.split('.')[1]?.length;
|
|
if (!trail || trail < 2) {
|
|
trail = 2;
|
|
}
|
|
|
|
const currentValue = parseFloat(selectedText);
|
|
let modifiedValue = currentValue - direction * step;
|
|
|
|
// Format the number to avoid floating point precision issues and then convert back to a float
|
|
modifiedValue = parseFloat(modifiedValue.toFixed(trail));
|
|
|
|
// Replace the selected text with the new value, keeping the selection
|
|
inputEl.value = inputEl.value.substring(0, selectionStart) + modifiedValue + inputEl.value.substring(selectionEnd);
|
|
const newSelectionEnd = selectionStart + modifiedValue.toString().length;
|
|
inputEl.setSelectionRange(selectionStart, newSelectionEnd);
|
|
}
|
|
};
|
|
}
|
|
|
|
widget.inputEl.removeEventListener('wheel', widget.ttNhandleScrollInput);
|
|
widget.inputEl.addEventListener('wheel', widget.ttNhandleScrollInput);
|
|
}
|
|
|
|
app.registerExtension({
|
|
name: "comfy.ttN.AutoComplete",
|
|
async init() {
|
|
const embs = await api.fetchApi("/embeddings")
|
|
const loras = await api.fetchApi("/ttN/loras")
|
|
|
|
_initializeAutocompleteData(await embs.json(), 'embedding:');
|
|
_initializeAutocompleteData(await loras.json(), '<lora:');
|
|
_initializeAutocompleteList(nsp_keys, '__');
|
|
},
|
|
nodeCreated(node) {
|
|
if (node.widgets && !["xyPlot", "advanced xyPlot"].includes(node.constructor.title)) {
|
|
const relevantWidgets = node.widgets.filter(_isRelevantWidget);
|
|
relevantWidgets.forEach(_attachInputHandler);
|
|
const lorasWidgets = node.widgets.filter(_isLorasWidget);
|
|
lorasWidgets.forEach(_attachLorasHandler);
|
|
}
|
|
}
|
|
}); |