171 lines
7.0 KiB
JavaScript
171 lines
7.0 KiB
JavaScript
// Imports specific objects from other modules.
|
|
import { app } from "../../scripts/app.js";
|
|
import { ttN_CreateDropdown, ttN_RemoveDropdown } from "./ttN.js";
|
|
|
|
// Initialize some global lists and objects.
|
|
let embeddingsList = [];
|
|
let embeddingFiles = [];
|
|
let embeddingsHierarchy = {};
|
|
|
|
// Convert a list of strings into a hierarchical structure.
|
|
function convertListToHierarchy(list) {
|
|
const hierarchy = {}; // Initialize an empty hierarchy object.
|
|
|
|
// Iterate over each item in the list.
|
|
for (var item of list) {
|
|
item = item.replace("embedding:", ""); // Remove any "embedding:" prefix from the item.
|
|
const parts = item.split(/:\\|\\/); // Split the item by either ':\' or '\'.
|
|
let currentNode = hierarchy; // Start at the root of the hierarchy.
|
|
|
|
// For each part of the split item...
|
|
parts.forEach((part, index) => {
|
|
// If it's the last part, set its value to null in the hierarchy.
|
|
if (index === parts.length - 1) {
|
|
currentNode[part] = null;
|
|
} else {
|
|
// Otherwise, initialize the node if it doesn't exist yet and move deeper into the hierarchy.
|
|
currentNode[part] = currentNode[part] || {};
|
|
currentNode = currentNode[part];
|
|
}
|
|
});
|
|
}
|
|
|
|
return hierarchy; // Return the filled hierarchy.
|
|
}
|
|
|
|
// Register an extension to the app.
|
|
app.registerExtension({
|
|
name: "comfy.ttN.embeddingAC",
|
|
// Before a node definition is registered...
|
|
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
|
// If the node name matches a specific type...
|
|
if (nodeData.name === "ttN pipeKSampler") {
|
|
initializeEmbeddingData(nodeData.input.hidden.embeddingsList[0]);
|
|
}
|
|
},
|
|
// When a node is created...
|
|
nodeCreated(node) {
|
|
// If the node has widgets and its title isn't "xyPlot"...
|
|
if (node.widgets && node.getTitle() !== "xyPlot") {
|
|
const relevantWidgets = filterRelevantWidgets(node.widgets); // Filter out the relevant widgets.
|
|
addInputListenersToWidgets(relevantWidgets); // Add input listeners to these widgets.
|
|
}
|
|
}
|
|
});
|
|
|
|
// Returns a list of widgets that either have type "customtext" with dynamic prompts or just have dynamic prompts.
|
|
function filterRelevantWidgets(widgets) {
|
|
return widgets.filter(widget => (widget.type === "customtext" && widget.dynamicPrompts !== false) || widget.dynamicPrompts);
|
|
}
|
|
|
|
// Adds input listeners to the given widgets.
|
|
function addInputListenersToWidgets(widgets) {
|
|
widgets.forEach(widget => {
|
|
const inputHandler = createWidgetInputHandler(widget); // Create an input handler specific for this widget.
|
|
setWidgetInputHandler(widget, inputHandler); // Set this handler to the widget.
|
|
});
|
|
}
|
|
|
|
// Returns a function that will handle the widget's input.
|
|
function createWidgetInputHandler(widget) {
|
|
return function handleInput() {
|
|
const currentWord = getCurrentWordFromInput(widget); // Get the word at the current cursor position in the widget's input.
|
|
// Check if the current word should trigger embedding suggestions...
|
|
if (shouldProvideEmbeddingSuggestion(currentWord)) {
|
|
const suggestions = filterEmbeddingsForInput(currentWord); // Get suggestions for the current word.
|
|
if (suggestions.length > 0) { // If there are suggestions...
|
|
// Convert the suggestions to a hierarchy and create a dropdown with these suggestions.
|
|
embeddingsHierarchy = convertListToHierarchy(suggestions);
|
|
ttN_CreateDropdown(widget.inputEl, embeddingsHierarchy, selectedSuggestion => {
|
|
// Update the widget's input value with the selected suggestion when one is chosen.
|
|
widget.inputEl.value = updateInputWithSuggestion(widget.inputEl.value, selectedSuggestion, widget);
|
|
}, true);
|
|
return;
|
|
}
|
|
}
|
|
// If no suggestions, remove any existing dropdown.
|
|
ttN_RemoveDropdown();
|
|
};
|
|
}
|
|
|
|
// Adds or replaces event listeners for the widget's input.
|
|
function setWidgetInputHandler(widget, handler) {
|
|
['input', 'mousedown'].forEach(event => {
|
|
// Remove any existing listeners and then add the new handler.
|
|
widget.inputEl.removeEventListener(event, handler);
|
|
widget.inputEl.addEventListener(event, handler);
|
|
});
|
|
}
|
|
|
|
// Returns the word at the current cursor position from the widget's input.
|
|
function getCurrentWordFromInput(widget) {
|
|
const cursorPosition = widget.inputEl.selectionStart;
|
|
const segments = widget.inputEl.value.split(' ');
|
|
return segments[widget.inputEl.value.substring(0, cursorPosition).split(' ').length - 1].toLowerCase();
|
|
}
|
|
|
|
// Determines if the current word should trigger embedding suggestions.
|
|
function shouldProvideEmbeddingSuggestion(word) {
|
|
const suggestionPrefix = 'embedding:';
|
|
return suggestionPrefix.startsWith(word) && word.length > 2 || word.startsWith(suggestionPrefix);
|
|
}
|
|
|
|
// Filters embeddings based on a specific word.
|
|
function filterEmbeddingsForInput(input) {
|
|
const prefixes = ['embedding', 'embeddin', 'embeddi', 'embedd', 'embed', 'embe', 'emb']
|
|
|
|
let inputLowered = input.toLowerCase();
|
|
let cleanedInput = inputLowered.replace('embedding:', '');
|
|
|
|
prefixes.forEach(prefix => {
|
|
if (inputLowered.startsWith(prefix)) {
|
|
cleanedInput = cleanedInput.replace(prefix, '');
|
|
}
|
|
})
|
|
|
|
cleanedInput = cleanedInput.replace(/\//g, "\\");
|
|
|
|
return embeddingsList.filter(embedding => {
|
|
const embeddingName = getFileName(embedding).toLowerCase();
|
|
embedding = embedding.replace('embedding:', '').toLowerCase();
|
|
if (embeddingName.startsWith(cleanedInput) || embedding.startsWith(cleanedInput) || prefixes.includes(cleanedInput)) {
|
|
return true;
|
|
}
|
|
return false
|
|
});
|
|
}
|
|
|
|
function getFileName(path) {
|
|
const parts = path.split(/[\/:\\]/); // Split the path by '/' or ':'
|
|
const fileName = parts[parts.length - 1]; // Get the last part (filename with extension)
|
|
return fileName;
|
|
}
|
|
|
|
// Updates the widget's input text with a selected suggestion.
|
|
function updateInputWithSuggestion(inputText, selectedSuggestion, widget) {
|
|
const cursorPosition = widget.inputEl.selectionStart;
|
|
const inputSegments = inputText.split(' ');
|
|
const cursorSegmentIndex = inputText.substring(0, cursorPosition).split(' ').length - 1;
|
|
|
|
if (inputSegments[cursorSegmentIndex].startsWith('emb')) {
|
|
inputSegments[cursorSegmentIndex] = 'embedding:' + selectedSuggestion;
|
|
}
|
|
|
|
return inputSegments.join(' ');
|
|
}
|
|
|
|
// Initializes data related to embeddings.
|
|
function initializeEmbeddingData(initialEmbeddingsList) {
|
|
embeddingsList = initialEmbeddingsList;
|
|
|
|
embeddingsList.forEach(embedding => {
|
|
const fileName = embedding.split('\\').slice(-1)[0];
|
|
embeddingFiles.push(fileName);
|
|
});
|
|
|
|
embeddingsList = embeddingsList.map(embedding => {
|
|
const segments = embedding.split('/');
|
|
return segments.map((segment, index) => "embedding:" + segments.slice(0, index + 1).join('/'));
|
|
}).flat();
|
|
}
|