From d8bb4faffa24664ef8add796012cec05cfc5f40a Mon Sep 17 00:00:00 2001 From: TinyTerra Date: Tue, 6 Jun 2023 21:09:02 +0200 Subject: [PATCH] added embedding autocomplete --- js/ttN.js | 26 +++--- js/ttNembedAC.css | 26 ++++++ js/ttNembedAC.js | 208 ++++++++++++++++++++++++++++++++++++++++++++++ js/ttNstyles.js | 10 +++ ttNdev.py | 72 ++++++++++++++++ 5 files changed, 329 insertions(+), 13 deletions(-) create mode 100644 js/ttNembedAC.css create mode 100644 js/ttNembedAC.js create mode 100644 ttNdev.py diff --git a/js/ttN.js b/js/ttN.js index eea6a36..6644e52 100644 --- a/js/ttN.js +++ b/js/ttN.js @@ -7,20 +7,20 @@ app.registerExtension({ if (nodeData.name === "ttN textDebug") { const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function () { - const r = onNodeCreated?.apply(this, arguments); - const w = ComfyWidgets["STRING"](this, "text", ["STRING", { multiline: true }], app).widget; - w.inputEl.readOnly = true; - w.inputEl.style.opacity = 0.7; - return r; + const r = onNodeCreated?.apply(this, arguments); + const w = ComfyWidgets["STRING"](this, "text", ["STRING", { multiline: true }], app).widget; + w.inputEl.readOnly = true; + w.inputEl.style.opacity = 0.7; + return r; }; const onExecuted = nodeType.prototype.onExecuted; nodeType.prototype.onExecuted = function (message) { - onExecuted?.apply(this, arguments); + onExecuted?.apply(this, arguments); - this.widgets[1].value = message.text.join(''); + this.widgets[1].value = message.text.join(''); - this.onResize?.(this.size); + this.onResize?.(this.size); }; } }, @@ -33,10 +33,10 @@ app.registerExtension({ nodeData.name === "ttN pipeKSampler" || nodeData.name === "ttN imageOutput" || nodeData.name === "ttN imageREMBG" - ) { + ) { const onNodeCreated = nodeType.prototype.onNodeCreated; - // Simple date formatter + // Simple date formatter const parts = { d: (d) => d.getDate(), M: (d) => d.getMonth() + 1, @@ -61,7 +61,7 @@ app.registerExtension({ }); } - // When the SaveImage node is created we want to override the serialization of the output name widget to run our S&R + // When the SaveImage node is created we want to override the serialization of the output name widget to run our S&R nodeType.prototype.onNodeCreated = function () { const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; @@ -70,7 +70,7 @@ app.registerExtension({ return widget.value.replace(/%([^%]+)%/g, function (match, text) { const split = text.split("."); if (split.length !== 2) { - // Special handling for dates + // Special handling for dates if (split[0].startsWith("date:")) { return formatDate(split[0].substring(5), new Date()); } @@ -112,7 +112,7 @@ app.registerExtension({ return r; }; } else { - // When any other node is created add a property to alias the node + // When any other node is created add a property to alias the node const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function () { const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; diff --git a/js/ttNembedAC.css b/js/ttNembedAC.css new file mode 100644 index 0000000..0d7cfff --- /dev/null +++ b/js/ttNembedAC.css @@ -0,0 +1,26 @@ +.autocomplete-dropdown { + position: absolute; + box-sizing: border-box; + background-color: #121212; + border-radius: 7px; + box-shadow: 0 2px 4px rgba(255, 255, 255, .25); + padding: 0; + margin: 0; + list-style: none; + z-index: 1000; + overflow: auto; + max-height: 200px; +} + +.autocomplete-dropdown li { + padding: 4px 10px; + cursor: pointer; + font-family: system-ui; + font-size: 0.7rem; +} + +.autocomplete-dropdown li:hover, +.autocomplete-dropdown li.selected { + background-color: #e5e5e5; + border-radius: 7px; +} \ No newline at end of file diff --git a/js/ttNembedAC.js b/js/ttNembedAC.js new file mode 100644 index 0000000..55268ce --- /dev/null +++ b/js/ttNembedAC.js @@ -0,0 +1,208 @@ +import { app } from "/scripts/app.js"; + +// Create a link element for the CSS file +const cssFile = document.createElement('link'); +cssFile.rel = 'stylesheet'; +cssFile.type = "text/css"; +cssFile.href = 'extensions/tinyterraNodes/ttNembedAC.css'; + +// Append the link element to the document's head +document.head.appendChild(cssFile); + +let embeddingsList = []; + +fetch('extensions/tinyterraNodes/embeddingsList.json') + .then(response => response.json()) + .then(data => { + // Use the JSON data as a constant + embeddingsList = data + .filter(embedding => embedding !== 'put_embeddings_or_textual_inversion_concepts_here') + .map(embedding => "embedding:" + embedding); // Add "embedding:" to each element; + }) + .catch(error => { + console.error('Error:', error); + }); + + +app.registerExtension({ + name: "comfy.ttN.embeddingAC", + nodeCreated(node) { + if (node.widgets) { + // Locate dynamic prompt text widgets + // Include any widgets with dynamicPrompts set to true, and customtext + const widgets = node.widgets.filter( + (n) => (n.type === "customtext" && n.dynamicPrompts !== false) || n.dynamicPrompts + ); + for (const w of widgets) { + let autocompleteActive = false; + let selectedSuggestionIndex = -1; + const autocompleteId = 'autocomplete-dropdown'; + + const onInput = function () { + const inputText = w.inputEl.value; + const cursorPosition = w.inputEl.selectionStart; + + const inputSegments = inputText.split(' '); + const cursorSegmentIndex = inputText.substring(0, cursorPosition).split(' ').length - 1; + + const currentSegment = inputSegments[cursorSegmentIndex]; + const currentSegmentLower = currentSegment.toLowerCase(); + + const suggestionkey = 'embedding:'; + + if (suggestionkey.startsWith(currentSegmentLower) && currentSegmentLower.length > 2 || currentSegmentLower.startsWith(suggestionkey)) { + const filteredEmbeddingsList = embeddingsList.filter(s => s.includes(currentSegment)); + if (filteredEmbeddingsList.length > 0) { + displayEmbeddingsList(filteredEmbeddingsList); + autocompleteActive = true; + } else { + hideEmbeddingsList(); + autocompleteActive = false; + } + } else { + hideEmbeddingsList(); + autocompleteActive = false; + } + }; + + w.inputEl.removeEventListener('input', onInput); + w.inputEl.addEventListener('input', onInput); + + + + const onKeyDown = function (event) { + const tabKeyCode = 9; + const enterKeyCode = 13; + const escKeyCode = 27; + const arrowUpKeyCode = 38; + const arrowDownKeyCode = 40; + + if (event.keyCode === tabKeyCode && autocompleteActive) { + event.preventDefault(); + if (selectedSuggestionIndex !== -1) { + const selectedSuggestion = document.getElementById('autocomplete-item-' + selectedSuggestionIndex); + const newText = replaceLastEmbeddingSegment(w.inputEl.value, selectedSuggestion.textContent); + w.inputEl.value = newText; + } + hideEmbeddingsList(); + shouldRemoveDropdown = false; + } else if (event.keyCode === enterKeyCode && autocompleteActive) { + event.preventDefault(); + if (selectedSuggestionIndex !== -1) { + const selectedSuggestion = document.getElementById('autocomplete-item-' + selectedSuggestionIndex); + const newText = replaceLastEmbeddingSegment(w.inputEl.value, selectedSuggestion.textContent); + w.inputEl.value = newText; + } + hideEmbeddingsList(); + shouldRemoveDropdown = false; + } else if (event.keyCode === arrowUpKeyCode && autocompleteActive) { + event.preventDefault(); + if (selectedSuggestionIndex > 0) { + selectedSuggestionIndex--; + highlightSuggestion(selectedSuggestionIndex); + } + } else if (event.keyCode === arrowDownKeyCode && autocompleteActive) { + event.preventDefault(); + if (selectedSuggestionIndex < embeddingsList.length - 1) { + selectedSuggestionIndex++; + highlightSuggestion(selectedSuggestionIndex); + } + } else if (event.keyCode === escKeyCode && autocompleteActive) { + event.preventDefault(); + hideEmbeddingsList(); + } + }; + + w.inputEl.removeEventListener('keydown', onKeyDown); + w.inputEl.addEventListener('keydown', onKeyDown); + + let shouldRemoveDropdown = false; + let autocompleteDropdown = null; + + function displayEmbeddingsList(filteredEmbeddingsList) { + hideEmbeddingsList(); + + const dropdown = document.createElement('ul'); + dropdown.setAttribute('id', autocompleteId); + dropdown.setAttribute('role', 'listbox'); + dropdown.classList.add('autocomplete-dropdown'); + + filteredEmbeddingsList.forEach((suggestion, index) => { + const listItem = document.createElement('li'); + listItem.setAttribute('id', 'autocomplete-item-' + index); + listItem.setAttribute('role', 'option'); + listItem.textContent = suggestion; + listItem.addEventListener('mouseover', function () { + highlightSuggestion(index); + }); + listItem.addEventListener('mousedown', function (event) { + event.preventDefault(); + const newText = replaceLastEmbeddingSegment(w.inputEl.value, suggestion); + w.inputEl.value = newText; + hideEmbeddingsList(); + shouldRemoveDropdown = false; + }); + dropdown.appendChild(listItem); + }); + + const inputRect = w.inputEl.getBoundingClientRect(); + dropdown.style.top = (inputRect.top + inputRect.height) + 'px'; + dropdown.style.left = inputRect.left + 'px'; + dropdown.style.width = inputRect.width + 'px'; + + document.body.appendChild(dropdown); + + autocompleteDropdown = dropdown; + shouldRemoveDropdown = true; + } + + document.addEventListener('click', function (event) { + if (shouldRemoveDropdown && autocompleteDropdown && !autocompleteDropdown.contains(event.target)) { + hideEmbeddingsList(); + shouldRemoveDropdown = false; + autocompleteDropdown = null; + } + }); + + function hideEmbeddingsList() { + const dropdown = document.getElementById(autocompleteId); + if (dropdown) { + dropdown.remove(); + } + selectedSuggestionIndex = -1; + } + + let previousSuggestionIndex = -1; + + function highlightSuggestion(index) { + const selectedSuggestion = document.getElementById('autocomplete-item-' + index); + const previousSelectedSuggestion = document.getElementById('autocomplete-item-' + previousSuggestionIndex); + + if (previousSelectedSuggestion) { + previousSelectedSuggestion.classList.remove('selected'); + } + + if (selectedSuggestion) { + selectedSuggestion.classList.add('selected'); + previousSuggestionIndex = index; + selectedSuggestionIndex = index; + } + } + + function replaceLastEmbeddingSegment(inputText, selectedSuggestion) { + const cursorPosition = w.inputEl.selectionStart; + + const inputSegments = inputText.split(' '); + const cursorSegmentIndex = inputText.substring(0, cursorPosition).split(' ').length - 1; + + if (inputSegments[cursorSegmentIndex].startsWith('emb')) { + inputSegments[cursorSegmentIndex] = selectedSuggestion; + } + + return inputSegments.join(' '); + } + + } + } + } +}); \ No newline at end of file diff --git a/js/ttNstyles.js b/js/ttNstyles.js index 36400ab..b62da69 100644 --- a/js/ttNstyles.js +++ b/js/ttNstyles.js @@ -34,5 +34,15 @@ app.registerExtension({ return r; }; } + if (nodeData.name == "ttN hiresfixScale") { + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + const r = onNodeCreated?.apply(this, arguments); + this.color=LGraphCanvas.node_colors.cyan.color; + this.bgcolor=LGraphCanvas.node_colors.cyan.bgcolor; + this.groupcolor = LGraphCanvas.node_colors.cyan.groupcolor; + return r; + }; + } } }); \ No newline at end of file diff --git a/ttNdev.py b/ttNdev.py new file mode 100644 index 0000000..40d1e5e --- /dev/null +++ b/ttNdev.py @@ -0,0 +1,72 @@ +# in_dev - likely broken +class ttN_debugInput: + @classmethod + def INPUT_TYPES(s): + return {"required": {"console_title": ("STRING", {"default": "ttN INPUT DEBUG"}),}, + "optional": {"debug": ("", {"default": None}),} + } + + RETURN_TYPES = tuple() + RETURN_NAMES = tuple() + FUNCTION = "debug" + CATEGORY = "ttN/dev" + OUTPUT_NODE = True + + def debug(_, **kwargs): + for key, value in kwargs.items(): + if key == "console_title": + print(value) + else: + print(f"{key}: {value}") + return tuple() + +class ttN_busIN: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "lane_0": ("",), + }} + + RETURN_TYPES = ("BUS_LINE",) + RETURN_NAMES = ("bus_line",) + FUNCTION = "roundnround" + + CATEGORY = "ttN/dev" + + @staticmethod + def roundnround(*args, **kwargs): + bus_line = [] + for key, value in kwargs.items(): + bus_line.append(value) + print("busIN + kw:--",tuple(bus_line)) + + return (tuple(bus_line),) + +class ttN_busOUT: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "bus_line": ("BUS_LINE",), + }} + + RETURN_TYPES = () + FUNCTION = "roundnround" + + CATEGORY = "ttN/dev" + + @staticmethod + def roundnround(bus_line): + print("busOUT:--",bus_line) + return (bus_line,) + +NODE_CLASS_MAPPINGS = { + "ttN debugInput": ttN_debugInput, + "ttN busIN": ttN_busIN, + "ttN busOUT": ttN_busOUT +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "ttN debugInput": "debugInput", + "ttN busIN": "busIN", + "ttN busOUT": "busOUT" +} \ No newline at end of file