diff --git a/__init__.py b/__init__.py index abc8860..70c20b1 100644 --- a/__init__.py +++ b/__init__.py @@ -13,7 +13,8 @@ except ImportError: from kikotools import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS # Tell ComfyUI where to find our JavaScript extensions -WEB_DIRECTORY = "./web" +import os +WEB_DIRECTORY = os.path.join(os.path.dirname(os.path.abspath(__file__)), "web") def setup_autocomplete_api(): """Setup API routes for embedding autocomplete.""" diff --git a/kikotools/__init__.py b/kikotools/__init__.py index 333c874..82b675d 100644 --- a/kikotools/__init__.py +++ b/kikotools/__init__.py @@ -3,27 +3,23 @@ KikoTools package initialization and node registry Handles automatic discovery and registration of all ComfyAssets tools """ -from .tools.resolution_calculator import ResolutionCalculatorNode -from .tools.width_height_selector import WidthHeightSelectorNode -from .tools.seed_history import SeedHistoryNode -from .tools.sampler_combo import SamplerComboNode, SamplerComboCompactNode -from .tools.empty_latent_batch import EmptyLatentBatchNode -from .tools.kiko_save_image import KikoSaveImageNode -from .tools.image_to_multiple_of import ImageToMultipleOfNode -from .tools.image_scale_down_by import ImageScaleDownByNode -from .tools.gemini_prompt import GeminiPromptNode from .tools.display_any import DisplayAnyNode from .tools.display_text import DisplayTextNode -from .tools.kiko_film_grain import KikoFilmGrainNode -from .tools.xyz_helpers import ( - SamplerSelectHelperNode, - SchedulerSelectHelperNode, - TextEncodeSamplerParamsNode, - FluxSamplerParamsNode, - PlotParametersNode, - LoRAFolderBatchNode, -) from .tools.embedding_autocomplete import KikoEmbeddingAutocomplete +from .tools.empty_latent_batch import EmptyLatentBatchNode +from .tools.gemini_prompt import GeminiPromptNode +from .tools.image_scale_down_by import ImageScaleDownByNode +from .tools.image_to_multiple_of import ImageToMultipleOfNode +from .tools.kiko_film_grain import KikoFilmGrainNode +from .tools.kiko_save_image import KikoSaveImageNode +from .tools.resolution_calculator import ResolutionCalculatorNode +from .tools.sampler_combo import SamplerComboCompactNode, SamplerComboNode +from .tools.seed_history import SeedHistoryNode +from .tools.width_height_selector import WidthHeightSelectorNode +from .tools.xyz_helpers import (FluxSamplerParamsNode, LoRAFolderBatchNode, + PlotParametersNode, SamplerSelectHelperNode, + SchedulerSelectHelperNode, + TextEncodeSamplerParamsNode) # ComfyUI node registration mappings NODE_CLASS_MAPPINGS = { @@ -69,7 +65,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "FluxSamplerParams": "Flux Sampler Parameters", "PlotParameters+": "Plot Parameters", "LoRAFolderBatch": "LoRA Folder Batch", - "KikoEmbeddingAutocomplete": "🫶 Embedding Autocomplete", + "KikoEmbeddingAutocomplete": "🫶 Embedding Autocomplete (Test Panel)", } __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] diff --git a/kikotools/tools/embedding_autocomplete/node.py b/kikotools/tools/embedding_autocomplete/node.py index ae6b6df..08e76f3 100644 --- a/kikotools/tools/embedding_autocomplete/node.py +++ b/kikotools/tools/embedding_autocomplete/node.py @@ -12,7 +12,7 @@ import folder_paths class KikoEmbeddingAutocomplete: """Node that provides embedding autocomplete functionality.""" - DISPLAY_NAME = "Embedding Autocomplete" + DISPLAY_NAME = "🫶 Embedding Autocomplete (Test Panel)" CATEGORY = "ComfyAssets" # Settings definition for the settings registry @@ -54,37 +54,94 @@ class KikoEmbeddingAutocomplete: @classmethod def INPUT_TYPES(cls): """Define input types for the node.""" + hints = """🫶 Embedding Autocomplete Test Panel + +Try these triggers in the text field below: +• Type 'embedding:' to list all embeddings +• Type ' 0: + output_lines.append(f"") + output_lines.append(f"Sample embeddings (first 5):") + for emb in self.embeddings_cache[:5]: + output_lines.append(f" • {emb['name']}") + + if loras_count > 0: + output_lines.append(f"") + output_lines.append(f"Sample LoRAs (first 5):") + for lora in self.loras_cache[:5]: + output_lines.append(f" • {lora['name']}") + + output_text = "\n".join(output_lines) + + return (output_text, embeddings_count, loras_count) def refresh_cache(self): """Refresh the cache of embeddings and LoRAs.""" diff --git a/tests/test_embedding_autocomplete.py b/tests/test_embedding_autocomplete.py index 06445ad..6338c82 100644 --- a/tests/test_embedding_autocomplete.py +++ b/tests/test_embedding_autocomplete.py @@ -14,7 +14,7 @@ def test_import(): """Test that the module can be imported.""" from kikotools.tools.embedding_autocomplete import KikoEmbeddingAutocomplete assert KikoEmbeddingAutocomplete is not None - assert KikoEmbeddingAutocomplete.DISPLAY_NAME == "Embedding Autocomplete" + assert KikoEmbeddingAutocomplete.DISPLAY_NAME == "🫶 Embedding Autocomplete (Test Panel)" assert KikoEmbeddingAutocomplete.CATEGORY == "ComfyAssets" @@ -44,6 +44,7 @@ def test_input_types(): input_types = KikoEmbeddingAutocomplete.INPUT_TYPES() assert "required" in input_types assert "optional" in input_types + assert "test_input" in input_types["required"] assert "refresh" in input_types["optional"] diff --git a/web/js/kikoEmbeddingAutocomplete.js b/web/js/kikoEmbeddingAutocomplete.js deleted file mode 100644 index a0ef7f8..0000000 --- a/web/js/kikoEmbeddingAutocomplete.js +++ /dev/null @@ -1,416 +0,0 @@ -/** - * KikoTools Embedding Autocomplete - * Provides autocomplete functionality for embeddings and LoRAs in ComfyUI text inputs - */ - -import { app } from "../../scripts/app.js"; -import { ComfyWidgets } from "../../scripts/widgets.js"; - -// Autocomplete class -class KikoEmbeddingAutocomplete { - constructor() { - this.suggestions = []; - this.currentIndex = -1; - this.dropdown = null; - this.activeWidget = null; - this.activeNode = null; - - // Settings - this.settings = { - enabled: true, - triggerChars: 2, - maxSuggestions: 20, - showEmbeddings: true, - showLoras: true, - caseSensitive: false - }; - - // Load settings from ComfyUI - this.loadSettings(); - } - - loadSettings() { - this.settings.enabled = app.ui.settings.getSettingValue("kikotools.embedding_autocomplete.enabled", true); - this.settings.triggerChars = app.ui.settings.getSettingValue("kikotools.embedding_autocomplete.trigger_chars", 2); - this.settings.maxSuggestions = app.ui.settings.getSettingValue("kikotools.embedding_autocomplete.max_suggestions", 20); - this.settings.showEmbeddings = app.ui.settings.getSettingValue("kikotools.embedding_autocomplete.show_embeddings", true); - this.settings.showLoras = app.ui.settings.getSettingValue("kikotools.embedding_autocomplete.show_loras", true); - this.settings.caseSensitive = app.ui.settings.getSettingValue("kikotools.embedding_autocomplete.case_sensitive", false); - } - - async fetchSuggestions(prefix) { - if (!this.settings.enabled || prefix.length < this.settings.triggerChars) { - return []; - } - - try { - const params = new URLSearchParams({ - prefix: prefix, - max: this.settings.maxSuggestions, - embeddings: this.settings.showEmbeddings, - loras: this.settings.showLoras, - case_sensitive: this.settings.caseSensitive - }); - - const response = await fetch(`/kikotools/autocomplete/suggestions?${params}`); - if (response.ok) { - return await response.json(); - } - } catch (error) { - console.error("[KikoAutocomplete] Error fetching suggestions:", error); - } - - return []; - } - - extractCurrentWord(text, cursorPos) { - // Extract word at cursor position - const beforeCursor = text.substring(0, cursorPos); - - // Look for special triggers - const embeddingMatch = beforeCursor.match(/embedding:([^,\s]*)$/); - if (embeddingMatch) { - return { - word: embeddingMatch[1], - start: beforeCursor.length - embeddingMatch[1].length, - type: "embedding" - }; - } - - const loraMatch = beforeCursor.match(/]*)$/); - if (loraMatch) { - return { - word: loraMatch[1], - start: beforeCursor.length - loraMatch[1].length, - type: "lora" - }; - } - - // General word extraction - const wordMatch = beforeCursor.match(/([^,\s]+)$/); - if (wordMatch) { - return { - word: wordMatch[1], - start: beforeCursor.length - wordMatch[1].length, - type: "general" - }; - } - - return null; - } - - createDropdown() { - if (this.dropdown) { - this.removeDropdown(); - } - - this.dropdown = document.createElement("div"); - this.dropdown.className = "kiko-autocomplete-dropdown"; - this.dropdown.style.cssText = ` - position: absolute; - background: var(--comfy-menu-bg, #353535); - border: 1px solid var(--border-color, #4e4e4e); - border-radius: 4px; - max-height: 200px; - overflow-y: auto; - z-index: 10000; - box-shadow: 0 2px 5px rgba(0,0,0,0.3); - min-width: 200px; - `; - - document.body.appendChild(this.dropdown); - } - - removeDropdown() { - if (this.dropdown) { - this.dropdown.remove(); - this.dropdown = null; - this.currentIndex = -1; - } - } - - positionDropdown(textarea) { - if (!this.dropdown) return; - - const rect = textarea.getBoundingClientRect(); - - // Simple positioning below the textarea - // In a more advanced version, we'd calculate caret position - this.dropdown.style.left = rect.left + "px"; - this.dropdown.style.top = (rect.bottom + 2) + "px"; - this.dropdown.style.width = rect.width + "px"; - } - - showSuggestions(suggestions, textarea, wordInfo) { - if (!suggestions || suggestions.length === 0) { - this.removeDropdown(); - return; - } - - this.suggestions = suggestions; - this.createDropdown(); - this.positionDropdown(textarea); - - // Populate dropdown - this.dropdown.innerHTML = ""; - suggestions.forEach((suggestion, index) => { - const item = document.createElement("div"); - item.className = "kiko-autocomplete-item"; - item.style.cssText = ` - padding: 5px 10px; - cursor: pointer; - color: var(--fg-color, #fff); - display: flex; - justify-content: space-between; - align-items: center; - `; - - // Highlight matching part - const displayText = suggestion.display || suggestion.value; - const matchIndex = displayText.toLowerCase().indexOf(wordInfo.word.toLowerCase()); - - if (matchIndex >= 0 && wordInfo.word) { - const before = displayText.substring(0, matchIndex); - const match = displayText.substring(matchIndex, matchIndex + wordInfo.word.length); - const after = displayText.substring(matchIndex + wordInfo.word.length); - - item.innerHTML = ` - ${before}${match}${after} - ${suggestion.type} - `; - } else { - item.innerHTML = ` - ${displayText} - ${suggestion.type} - `; - } - - // Hover effect - item.addEventListener("mouseenter", () => { - this.selectIndex(index); - }); - - // Click to select - item.addEventListener("click", () => { - this.applySuggestion(textarea, wordInfo, suggestion); - }); - - this.dropdown.appendChild(item); - }); - - // Select first item by default - if (suggestions.length > 0) { - this.selectIndex(0); - } - } - - selectIndex(index) { - // Remove previous selection - const items = this.dropdown?.querySelectorAll(".kiko-autocomplete-item"); - if (!items) return; - - items.forEach((item, i) => { - if (i === index) { - item.style.background = "var(--comfy-input-bg, #222)"; - this.currentIndex = i; - } else { - item.style.background = ""; - } - }); - } - - applySuggestion(textarea, wordInfo, suggestion) { - const text = textarea.value; - const beforeWord = text.substring(0, wordInfo.start); - const afterWord = text.substring(wordInfo.start + wordInfo.word.length); - - // Determine what to insert based on context - let insertion = suggestion.value; - - // Handle different insertion contexts - if (wordInfo.type === "embedding" && !suggestion.value.startsWith("embedding:")) { - insertion = "embedding:" + suggestion.value; - } else if (wordInfo.type === "lora" && !suggestion.value.startsWith("`; - } - - // Apply the suggestion - textarea.value = beforeWord + insertion + afterWord; - - // Position cursor after insertion - const newPos = beforeWord.length + insertion.length; - textarea.setSelectionRange(newPos, newPos); - - // Trigger input event to update the widget - textarea.dispatchEvent(new Event("input", { bubbles: true })); - - // Remove dropdown - this.removeDropdown(); - } - - attachToWidget(widget, node) { - const textarea = widget.inputEl; - if (!textarea || textarea.kikoAutocompleteAttached) return; - - textarea.kikoAutocompleteAttached = true; - - // Input handler with debouncing - let debounceTimer; - textarea.addEventListener("input", async (e) => { - clearTimeout(debounceTimer); - debounceTimer = setTimeout(async () => { - const cursorPos = textarea.selectionStart; - const wordInfo = this.extractCurrentWord(textarea.value, cursorPos); - - if (wordInfo && wordInfo.word.length >= this.settings.triggerChars) { - const suggestions = await this.fetchSuggestions(wordInfo.word); - this.showSuggestions(suggestions, textarea, wordInfo); - this.activeWidget = widget; - this.activeNode = node; - } else { - this.removeDropdown(); - } - }, 200); - }); - - // Keyboard navigation - textarea.addEventListener("keydown", (e) => { - if (!this.dropdown || this.suggestions.length === 0) return; - - switch(e.key) { - case "ArrowDown": - e.preventDefault(); - this.selectIndex((this.currentIndex + 1) % this.suggestions.length); - break; - - case "ArrowUp": - e.preventDefault(); - this.selectIndex(this.currentIndex <= 0 ? this.suggestions.length - 1 : this.currentIndex - 1); - break; - - case "Enter": - case "Tab": - if (this.currentIndex >= 0) { - e.preventDefault(); - const cursorPos = textarea.selectionStart; - const wordInfo = this.extractCurrentWord(textarea.value, cursorPos); - if (wordInfo) { - this.applySuggestion(textarea, wordInfo, this.suggestions[this.currentIndex]); - } - } - break; - - case "Escape": - e.preventDefault(); - this.removeDropdown(); - break; - } - }); - - // Hide on blur - textarea.addEventListener("blur", () => { - // Delay to allow click events to fire - setTimeout(() => this.removeDropdown(), 200); - }); - } -} - -// Initialize and register -app.registerExtension({ - name: "kikotools.embeddingAutocomplete", - - async init() { - // Add settings - app.ui.settings.addSetting({ - id: "kikotools.embedding_autocomplete.enabled", - name: "🫶 Embedding Autocomplete: Enabled", - defaultValue: true, - type: "boolean", - onChange(value) { - if (window.kikoAutocomplete) { - window.kikoAutocomplete.settings.enabled = value; - } - } - }); - - app.ui.settings.addSetting({ - id: "kikotools.embedding_autocomplete.trigger_chars", - name: "🫶 Embedding Autocomplete: Trigger After Characters", - defaultValue: 2, - type: "combo", - options: (value) => [ - { value: 1, text: "1", selected: value === 1 }, - { value: 2, text: "2", selected: value === 2 }, - { value: 3, text: "3", selected: value === 3 }, - { value: 4, text: "4", selected: value === 4 } - ], - onChange(value) { - if (window.kikoAutocomplete) { - window.kikoAutocomplete.settings.triggerChars = value; - } - } - }); - - app.ui.settings.addSetting({ - id: "kikotools.embedding_autocomplete.max_suggestions", - name: "🫶 Embedding Autocomplete: Maximum Suggestions", - defaultValue: 20, - type: "combo", - options: (value) => [ - { value: 10, text: "10", selected: value === 10 }, - { value: 20, text: "20", selected: value === 20 }, - { value: 30, text: "30", selected: value === 30 }, - { value: 50, text: "50", selected: value === 50 } - ], - onChange(value) { - if (window.kikoAutocomplete) { - window.kikoAutocomplete.settings.maxSuggestions = value; - } - } - }); - - app.ui.settings.addSetting({ - id: "kikotools.embedding_autocomplete.show_embeddings", - name: "🫶 Embedding Autocomplete: Show Embeddings", - defaultValue: true, - type: "boolean", - onChange(value) { - if (window.kikoAutocomplete) { - window.kikoAutocomplete.settings.showEmbeddings = value; - } - } - }); - - app.ui.settings.addSetting({ - id: "kikotools.embedding_autocomplete.show_loras", - name: "🫶 Embedding Autocomplete: Show LoRAs", - defaultValue: true, - type: "boolean", - onChange(value) { - if (window.kikoAutocomplete) { - window.kikoAutocomplete.settings.showLoras = value; - } - } - }); - }, - - async setup() { - // Create autocomplete instance - window.kikoAutocomplete = new KikoEmbeddingAutocomplete(); - - // Override STRING widget creation to add autocomplete - const originalStringWidget = ComfyWidgets.STRING; - ComfyWidgets.STRING = function(node, inputName, inputData, app) { - const widget = originalStringWidget.apply(this, arguments); - - // Only attach to multiline text widgets (prompts) - if (inputData[1]?.multiline && widget.inputEl) { - setTimeout(() => { - window.kikoAutocomplete.attachToWidget(widget, node); - }, 100); - } - - return widget; - }; - } -}); \ No newline at end of file diff --git a/web/kikoEmbeddingAutocomplete.js b/web/kikoEmbeddingAutocomplete.js new file mode 100644 index 0000000..78df545 --- /dev/null +++ b/web/kikoEmbeddingAutocomplete.js @@ -0,0 +1,399 @@ +import { app } from "../../scripts/app.js"; +import { ComfyWidgets } from "../../scripts/widgets.js"; + +// KikoEmbeddingAutocomplete - Provides autocomplete for embeddings and LoRAs in text widgets +class KikoEmbeddingAutocomplete { + constructor() { + this.embeddings = []; + this.loras = []; + this.settings = { + enabled: true, + triggerChars: 2, + maxSuggestions: 20, + showEmbeddings: true, + showLoras: true + }; + + // Load settings from localStorage + this.loadSettings(); + + // Fetch available embeddings and LoRAs + this.fetchResources(); + + // Track active widgets + this.activeWidgets = new Set(); + } + + loadSettings() { + try { + const stored = localStorage.getItem("kikotools.embedding_autocomplete.settings"); + if (stored) { + Object.assign(this.settings, JSON.parse(stored)); + } + } catch (error) { + console.error("[KikoTools] Error loading autocomplete settings:", error); + } + } + + saveSettings() { + try { + localStorage.setItem("kikotools.embedding_autocomplete.settings", JSON.stringify(this.settings)); + } catch (error) { + console.error("[KikoTools] Error saving autocomplete settings:", error); + } + } + + async fetchResources() { + try { + // Fetch embeddings + if (this.settings.showEmbeddings) { + const embResponse = await fetch("/embeddings"); + if (embResponse.ok) { + const data = await embResponse.json(); + this.embeddings = Object.keys(data || {}).map(name => ({ + name: name, + type: "embedding", + display: `embedding:${name}` + })); + } + } + + // Fetch LoRAs + if (this.settings.showLoras) { + const loraResponse = await fetch("/models/loras"); + if (loraResponse.ok) { + const data = await loraResponse.json(); + this.loras = (data || []).map(name => ({ + name: name, + type: "lora", + display: `` + })); + } + } + + console.log(`[KikoTools] Loaded ${this.embeddings.length} embeddings and ${this.loras.length} LoRAs`); + } catch (error) { + console.error("[KikoTools] Error fetching resources:", error); + } + } + + attachToWidget(widget, node) { + if (!widget.inputEl || this.activeWidgets.has(widget)) { + return; + } + + this.activeWidgets.add(widget); + const textarea = widget.inputEl; + + // Create suggestion container + const suggestionContainer = document.createElement("div"); + suggestionContainer.className = "kikotools-autocomplete-suggestions"; + suggestionContainer.style.cssText = ` + position: absolute; + z-index: 10000; + background: var(--comfy-menu-bg); + border: 1px solid var(--border-color); + border-radius: 4px; + max-height: 200px; + overflow-y: auto; + display: none; + min-width: 200px; + box-shadow: 0 2px 5px rgba(0,0,0,0.2); + `; + document.body.appendChild(suggestionContainer); + + let currentSuggestions = []; + let selectedIndex = -1; + let currentPrefix = ""; + let prefixStart = -1; + + const hideSuggestions = () => { + suggestionContainer.style.display = "none"; + selectedIndex = -1; + }; + + const showSuggestions = (suggestions, x, y) => { + if (!suggestions.length) { + hideSuggestions(); + return; + } + + suggestionContainer.innerHTML = ""; + suggestions.forEach((item, index) => { + const div = document.createElement("div"); + div.className = "kikotools-autocomplete-item"; + div.style.cssText = ` + padding: 5px 10px; + cursor: pointer; + color: var(--fg-color); + ${index === selectedIndex ? "background: var(--comfy-input-bg);" : ""} + `; + + // Add type indicator + const typeSpan = document.createElement("span"); + typeSpan.style.cssText = ` + display: inline-block; + width: 60px; + color: ${item.type === "embedding" ? "#4CAF50" : "#2196F3"}; + font-size: 0.9em; + `; + typeSpan.textContent = item.type === "embedding" ? "[EMB]" : "[LORA]"; + + const nameSpan = document.createElement("span"); + nameSpan.textContent = item.name; + + div.appendChild(typeSpan); + div.appendChild(nameSpan); + + div.addEventListener("mouseenter", () => { + selectedIndex = index; + updateSelection(); + }); + + div.addEventListener("click", () => { + insertSuggestion(item); + }); + + suggestionContainer.appendChild(div); + }); + + // Position the suggestions + const rect = textarea.getBoundingClientRect(); + suggestionContainer.style.left = Math.min(x, rect.right - 200) + "px"; + suggestionContainer.style.top = y + "px"; + suggestionContainer.style.display = "block"; + }; + + const updateSelection = () => { + const items = suggestionContainer.querySelectorAll(".kikotools-autocomplete-item"); + items.forEach((item, index) => { + item.style.background = index === selectedIndex ? "var(--comfy-input-bg)" : ""; + }); + }; + + const insertSuggestion = (suggestion) => { + const text = textarea.value; + const before = text.substring(0, prefixStart); + const after = text.substring(textarea.selectionStart); + + textarea.value = before + suggestion.display + after; + textarea.selectionStart = textarea.selectionEnd = prefixStart + suggestion.display.length; + + hideSuggestions(); + + // Trigger change event + const event = new Event("input", { bubbles: true }); + textarea.dispatchEvent(event); + }; + + const findPrefix = () => { + const text = textarea.value; + const cursor = textarea.selectionStart; + + // Look for embedding or lora prefix + let start = cursor - 1; + while (start >= 0 && text[start] !== " " && text[start] !== "\n" && text[start] !== ",") { + start--; + } + start++; + + const prefix = text.substring(start, cursor).toLowerCase(); + + // Check if we should show suggestions + if (prefix.length >= this.settings.triggerChars) { + currentPrefix = prefix; + prefixStart = start; + return true; + } + + return false; + }; + + const getSuggestions = (prefix) => { + const suggestions = []; + + if (this.settings.showEmbeddings) { + suggestions.push(...this.embeddings.filter(e => + e.name.toLowerCase().includes(prefix) || + e.display.toLowerCase().includes(prefix) + )); + } + + if (this.settings.showLoras) { + suggestions.push(...this.loras.filter(l => + l.name.toLowerCase().includes(prefix) || + l.display.toLowerCase().includes(prefix) + )); + } + + return suggestions.slice(0, this.settings.maxSuggestions); + }; + + // Event handlers + textarea.addEventListener("input", (e) => { + if (!this.settings.enabled) { + hideSuggestions(); + return; + } + + if (findPrefix()) { + currentSuggestions = getSuggestions(currentPrefix); + + if (currentSuggestions.length > 0) { + // Get cursor position for suggestion placement + const rect = textarea.getBoundingClientRect(); + const lineHeight = parseInt(window.getComputedStyle(textarea).lineHeight); + + // Simple positioning below cursor + showSuggestions(currentSuggestions, rect.left, rect.bottom); + } else { + hideSuggestions(); + } + } else { + hideSuggestions(); + } + }); + + textarea.addEventListener("keydown", (e) => { + if (suggestionContainer.style.display === "none") { + return; + } + + if (e.key === "ArrowDown") { + e.preventDefault(); + selectedIndex = Math.min(selectedIndex + 1, currentSuggestions.length - 1); + updateSelection(); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + selectedIndex = Math.max(selectedIndex - 1, 0); + updateSelection(); + } else if (e.key === "Enter" || e.key === "Tab") { + if (selectedIndex >= 0 && selectedIndex < currentSuggestions.length) { + e.preventDefault(); + insertSuggestion(currentSuggestions[selectedIndex]); + } + } else if (e.key === "Escape") { + e.preventDefault(); + hideSuggestions(); + } + }); + + textarea.addEventListener("blur", () => { + setTimeout(hideSuggestions, 200); + }); + } +} + +// Initialize and register +console.log("[KikoTools] Registering Embedding Autocomplete extension"); + +app.registerExtension({ + name: "kikotools.embeddingAutocomplete", + + async setup() { + console.log("[KikoTools] Setting up Embedding Autocomplete"); + + // Add settings + app.ui.settings.addSetting({ + id: "kikotools.embeddingAutocomplete.enabled", + name: "Enabled", + category: ["KikoTools", "Embedding Autocomplete"], + defaultValue: true, + type: "boolean", + onChange(value) { + if (window.kikoAutocomplete) { + window.kikoAutocomplete.settings.enabled = value; + window.kikoAutocomplete.saveSettings(); + } + } + }); + + app.ui.settings.addSetting({ + id: "kikotools.embeddingAutocomplete.triggerChars", + name: "Trigger After Characters", + category: ["KikoTools", "Embedding Autocomplete"], + defaultValue: 2, + type: "combo", + options: [ + { value: 1, text: "1 character" }, + { value: 2, text: "2 characters" }, + { value: 3, text: "3 characters" }, + { value: 4, text: "4 characters" } + ], + onChange(value) { + if (window.kikoAutocomplete) { + window.kikoAutocomplete.settings.triggerChars = value; + window.kikoAutocomplete.saveSettings(); + } + } + }); + + app.ui.settings.addSetting({ + id: "kikotools.embeddingAutocomplete.maxSuggestions", + name: "Maximum Suggestions", + category: ["KikoTools", "Embedding Autocomplete"], + defaultValue: 20, + type: "combo", + options: [ + { value: 10, text: "10" }, + { value: 20, text: "20" }, + { value: 30, text: "30" }, + { value: 50, text: "50" } + ], + onChange(value) { + if (window.kikoAutocomplete) { + window.kikoAutocomplete.settings.maxSuggestions = value; + window.kikoAutocomplete.saveSettings(); + } + } + }); + + app.ui.settings.addSetting({ + id: "kikotools.embeddingAutocomplete.showEmbeddings", + name: "Show Embeddings", + category: ["KikoTools", "Embedding Autocomplete"], + defaultValue: true, + type: "boolean", + onChange(value) { + if (window.kikoAutocomplete) { + window.kikoAutocomplete.settings.showEmbeddings = value; + window.kikoAutocomplete.saveSettings(); + window.kikoAutocomplete.fetchResources(); + } + } + }); + + app.ui.settings.addSetting({ + id: "kikotools.embeddingAutocomplete.showLoras", + name: "Show LoRAs", + category: ["KikoTools", "Embedding Autocomplete"], + defaultValue: true, + type: "boolean", + onChange(value) { + if (window.kikoAutocomplete) { + window.kikoAutocomplete.settings.showLoras = value; + window.kikoAutocomplete.saveSettings(); + window.kikoAutocomplete.fetchResources(); + } + } + }); + + // Create autocomplete instance + window.kikoAutocomplete = new KikoEmbeddingAutocomplete(); + + // Override STRING widget creation to add autocomplete + const originalStringWidget = ComfyWidgets.STRING; + ComfyWidgets.STRING = function(node, inputName, inputData, app) { + const widget = originalStringWidget.apply(this, arguments); + + // Only attach to multiline text widgets (prompts) + if (inputData[1]?.multiline && widget.inputEl) { + setTimeout(() => { + window.kikoAutocomplete.attachToWidget(widget, node); + }, 100); + } + + return widget; + }; + } +}); \ No newline at end of file