diff --git a/README.md b/README.md
index cf692ea..f1681da 100644
--- a/README.md
+++ b/README.md
@@ -26,6 +26,7 @@ ComfyUI-KikoTools provides carefully crafted, production-ready nodes grouped und
| [🤖 Gemini Prompt Engineer](#-gemini-prompt-engineer) | AI-powered image analysis and prompt generation | 🧠 Prompts |
| [🔍 Display Any](#-display-any) | Universal debugging tool for any data type | 👁️ Display |
| [🖼️ Image to Multiple Of](#️-image-to-multiple-of) | Adjust dimensions to multiples for compatibility | 🖼️ Resolution |
+| [🔤 Embedding Autocomplete](#-embedding-autocomplete) | Smart autocomplete for embeddings, LoRAs, and tags | ✍️ Text |
### 🧰 xyz-helpers Tools
@@ -311,6 +312,45 @@ Unified interface for text encoding and sampler parameter management.
- Quick template-based generation
- Batch prompt processing
+### 🔤 Embedding Autocomplete
+
+**Intelligent autocomplete for embeddings, LoRAs, and custom tags in text prompts.**
+
+
+
+This feature is an enhanced fork of the autocomplete functionality from [ComfyUI-Custom-Scripts](https://github.com/pythongosssss/ComfyUI-Custom-Scripts) by pythongosssss. We've modernized the codebase, fixed existing bugs, and added robust security features.
+
+**Key Features:**
+- **Smart Triggers**: Type `embedding:` for embeddings, ` 0:
+ print(f"[KikoTools] Sample embedding: {embeddings[0]}")
+ print(f"[KikoTools] Returning {len(embeddings)} embeddings with paths")
+ return web.json_response(embeddings)
+ except Exception as e:
+ print(f"[KikoTools] Error getting embeddings: {e}")
+ import traceback
+
+ traceback.print_exc()
+ return web.json_response([])
+
+ @PromptServer.instance.routes.get("/kikotools/autocomplete/loras")
+ async def get_loras(request):
+ """API endpoint for getting list of LoRAs."""
+ print("[KikoTools] LoRA endpoint called")
+ try:
+ lora_files = folder_paths.get_filename_list("loras")
+ print(f"[KikoTools] Found {len(lora_files)} LoRA files")
+ # Return LoRAs with paths
+ loras = []
+ for f in lora_files:
+ clean_path = os.path.splitext(f)[0]
+ loras.append(
+ {
+ "name": os.path.basename(clean_path),
+ "path": clean_path,
+ "file": f,
+ }
+ )
+ print(f"[KikoTools] Returning {len(loras)} LoRAs")
+ return web.json_response(loras)
+ except Exception as e:
+ print(f"[KikoTools] Error getting LoRAs: {e}")
+ import traceback
+
+ traceback.print_exc()
+ return web.json_response([])
+
+ print("[KikoTools] Autocomplete API endpoints registered successfully")
+ print(
+ "[KikoTools] Routes available: /kikotools/autocomplete/embeddings and /kikotools/autocomplete/loras"
+ )
+
+except ImportError as e:
+ print(f"[KikoTools] Could not import server components: {e}")
+except Exception as e:
+ print(f"[KikoTools] Unexpected error setting up API: {e}")
+ import traceback
+
+ traceback.print_exc()
+
+# API endpoints are registered above at module import time
def get_version():
diff --git a/examples/ac-emb.png b/examples/ac-emb.png
new file mode 100644
index 0000000..8f5bccd
Binary files /dev/null and b/examples/ac-emb.png differ
diff --git a/examples/ac-lora.png b/examples/ac-lora.png
new file mode 100644
index 0000000..f9160f1
Binary files /dev/null and b/examples/ac-lora.png differ
diff --git a/examples/ac-tag.png b/examples/ac-tag.png
new file mode 100644
index 0000000..e2c6a43
Binary files /dev/null and b/examples/ac-tag.png differ
diff --git a/kikotools/__init__.py b/kikotools/__init__.py
index 82b675d..cb3046b 100644
--- a/kikotools/__init__.py
+++ b/kikotools/__init__.py
@@ -16,10 +16,14 @@ 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)
+from .tools.xyz_helpers import (
+ FluxSamplerParamsNode,
+ LoRAFolderBatchNode,
+ PlotParametersNode,
+ SamplerSelectHelperNode,
+ SchedulerSelectHelperNode,
+ TextEncodeSamplerParamsNode,
+)
# ComfyUI node registration mappings
NODE_CLASS_MAPPINGS = {
@@ -65,7 +69,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"FluxSamplerParams": "Flux Sampler Parameters",
"PlotParameters+": "Plot Parameters",
"LoRAFolderBatch": "LoRA Folder Batch",
- "KikoEmbeddingAutocomplete": "🫶 Embedding Autocomplete (Test Panel)",
+ "KikoEmbeddingAutocomplete": "🫶 Embedding Autocomplete Configuration",
}
__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 c8718c2..05b129f 100644
--- a/kikotools/tools/embedding_autocomplete/node.py
+++ b/kikotools/tools/embedding_autocomplete/node.py
@@ -4,15 +4,14 @@ Provides autocomplete functionality for embeddings and LoRAs in text inputs.
"""
import os
-import json
-from typing import Dict, List, Any, Optional
+from typing import Dict, List, Any
import folder_paths
class KikoEmbeddingAutocomplete:
"""Node that provides embedding autocomplete functionality."""
- DISPLAY_NAME = "🫶 Embedding Autocomplete (Test Panel)"
+ DISPLAY_NAME = "🫶 Embedding Autocomplete Settings"
CATEGORY = "ComfyAssets"
# Settings definition for the settings registry
@@ -20,72 +19,67 @@ class KikoEmbeddingAutocomplete:
"enabled": {
"type": "boolean",
"default": True,
- "description": "Enable embedding autocomplete",
- },
- "trigger_chars": {
- "type": "combo",
- "default": 2,
- "options": [1, 2, 3, 4],
- "description": "Number of characters before showing suggestions",
- },
- "max_suggestions": {
- "type": "combo",
- "default": 20,
- "options": [10, 20, 30, 50],
- "description": "Maximum number of suggestions to show",
+ "description": "Enable autocomplete",
},
"show_embeddings": {
"type": "boolean",
"default": True,
- "description": "Show embeddings in suggestions",
+ "description": "Show embeddings in autocomplete",
},
"show_loras": {
"type": "boolean",
"default": True,
- "description": "Show LoRAs in suggestions",
+ "description": "Show LoRAs in autocomplete",
},
- "case_sensitive": {
+ "embedding_trigger": {
+ "type": "text",
+ "default": "embedding:",
+ "description": "Trigger text for embeddings (e.g., 'embedding:', 'emb:', or custom)",
+ },
+ "lora_trigger": {
+ "type": "text",
+ "default": " 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)
+ # This node doesn't actually process anything
+ # It's just a visual indicator that autocomplete is available
+ return ()
def refresh_cache(self):
"""Refresh the cache of embeddings and LoRAs."""
print("[KikoEmbeddingAutocomplete] Refreshing cache...")
self.embeddings_cache = self.get_embeddings()
self.loras_cache = self.get_loras()
- print(f"[KikoEmbeddingAutocomplete] Cached {len(self.embeddings_cache)} embeddings, {len(self.loras_cache)} LoRAs")
+ print(
+ f"[KikoEmbeddingAutocomplete] Cached {len(self.embeddings_cache)} embeddings, {len(self.loras_cache)} LoRAs"
+ )
def get_embeddings(self) -> List[Dict[str, Any]]:
"""Get list of available embeddings."""
@@ -158,7 +115,9 @@ This autocomplete works in all prompt fields!"""
try:
print("[KikoEmbeddingAutocomplete] Getting embeddings list...")
embedding_files = folder_paths.get_filename_list("embeddings")
- print(f"[KikoEmbeddingAutocomplete] Found {len(embedding_files)} embedding files")
+ print(
+ f"[KikoEmbeddingAutocomplete] Found {len(embedding_files)} embedding files"
+ )
for file in embedding_files:
name = os.path.splitext(file)[0]
embeddings.append(
@@ -212,7 +171,7 @@ This autocomplete works in all prompt fields!"""
# Return combined modification time
return os.path.getmtime(embeddings_path) + os.path.getmtime(loras_path)
- except:
+ except Exception:
return 0
diff --git a/tests/conftest.py b/tests/conftest.py
index c4769ef..b552639 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -3,10 +3,17 @@ pytest configuration and fixtures for ComfyUI-KikoTools testing
Provides mock ComfyUI environments and test data
"""
+import sys
import pytest
import torch
from unittest.mock import MagicMock
+# Mock folder_paths module before any imports that might use it
+sys.modules["folder_paths"] = MagicMock()
+sys.modules["folder_paths"].get_filename_list = MagicMock(return_value=[])
+sys.modules["folder_paths"].get_folder_paths = MagicMock(return_value=["/mock/path"])
+sys.modules["folder_paths"].base_path = "/mock/base"
+
@pytest.fixture
def mock_image_tensor():
diff --git a/tests/test_embedding_autocomplete.py b/tests/test_embedding_autocomplete.py
index 6338c82..900bdbf 100644
--- a/tests/test_embedding_autocomplete.py
+++ b/tests/test_embedding_autocomplete.py
@@ -4,77 +4,84 @@ import sys
from unittest.mock import MagicMock
# Mock ComfyUI's folder_paths module
-sys.modules['folder_paths'] = MagicMock()
-sys.modules['folder_paths'].get_filename_list = MagicMock(return_value=[])
-sys.modules['folder_paths'].get_folder_paths = MagicMock(return_value=["/mock/path"])
-sys.modules['folder_paths'].base_path = "/mock/base"
+sys.modules["folder_paths"] = MagicMock()
+sys.modules["folder_paths"].get_filename_list = MagicMock(return_value=[])
+sys.modules["folder_paths"].get_folder_paths = MagicMock(return_value=["/mock/path"])
+sys.modules["folder_paths"].base_path = "/mock/base"
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 (Test Panel)"
+ assert (
+ KikoEmbeddingAutocomplete.DISPLAY_NAME
+ == "🫶 Embedding Autocomplete Settings"
+ )
assert KikoEmbeddingAutocomplete.CATEGORY == "ComfyAssets"
def test_settings_defined():
"""Test that settings are properly defined."""
from kikotools.tools.embedding_autocomplete import KikoEmbeddingAutocomplete
-
+
settings = KikoEmbeddingAutocomplete.SETTINGS
assert "enabled" in settings
- assert "trigger_chars" in settings
+ assert "min_chars" in settings # Changed from trigger_chars
assert "max_suggestions" in settings
assert "show_embeddings" in settings
assert "show_loras" in settings
- assert "case_sensitive" in settings
-
+ assert "embedding_trigger" in settings
+ assert "lora_trigger" in settings
+ assert "quick_trigger" in settings
+ assert "sort_by_directory" in settings
+
# Check settings structure
assert settings["enabled"]["type"] == "boolean"
assert settings["enabled"]["default"] is True
- assert settings["trigger_chars"]["type"] == "combo"
- assert settings["trigger_chars"]["options"] == [1, 2, 3, 4]
+ assert settings["min_chars"]["type"] == "combo"
+ assert settings["min_chars"]["options"] == [1, 2, 3, 4, 5]
def test_input_types():
"""Test INPUT_TYPES class method."""
from kikotools.tools.embedding_autocomplete import KikoEmbeddingAutocomplete
-
+
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"]
+ assert "hidden" in input_types
+ assert input_types["required"] == {} # No required inputs
+ assert "unique_id" in input_types["hidden"]
def test_api_suggestions():
"""Test the API suggestions method."""
from kikotools.tools.embedding_autocomplete.node import KikoEmbeddingAutocompleteAPI
-
+
# Mock folder_paths to return some test files
- sys.modules['folder_paths'].get_filename_list = MagicMock(
- side_effect=lambda x: ["test1.pt", "test2.safetensors"] if x == "embeddings" else ["lora1.pt", "lora2.safetensors"]
+ sys.modules["folder_paths"].get_filename_list = MagicMock(
+ side_effect=lambda x: (
+ ["test1.pt", "test2.safetensors"]
+ if x == "embeddings"
+ else ["lora1.pt", "lora2.safetensors"]
+ )
)
-
+
# Test with embeddings
suggestions = KikoEmbeddingAutocompleteAPI.get_suggestions(
- prefix="test",
- include_embeddings=True,
- include_loras=False
+ prefix="test", include_embeddings=True, include_loras=False
)
-
+
assert len(suggestions) == 2
assert suggestions[0]["type"] == "embedding"
assert suggestions[0]["name"] == "test1"
-
+
# Test with LoRAs
suggestions = KikoEmbeddingAutocompleteAPI.get_suggestions(
- prefix="lora",
- include_embeddings=False,
- include_loras=True
+ prefix="lora", include_embeddings=False, include_loras=True
)
-
+
assert len(suggestions) == 2
assert suggestions[0]["type"] == "lora"
assert " 5:
+ print(f" ... and {len(files)-5} more files")
+ if dirs:
+ print(f" Subdirectories: {dirs}")
+
+ except ImportError as e:
+ print(f"Could not import folder_paths: {e}")
+else:
+ print(f"ComfyUI not found at {comfyui_path}")
diff --git a/web/kikoEmbeddingAutocomplete.js b/web/kikoEmbeddingAutocomplete.js
index 483b769..b8f2e0e 100644
--- a/web/kikoEmbeddingAutocomplete.js
+++ b/web/kikoEmbeddingAutocomplete.js
@@ -5,15 +5,25 @@ import { ComfyWidgets } from "../../scripts/widgets.js";
// KikoEmbeddingAutocomplete - Provides autocomplete for embeddings and LoRAs in text widgets
class KikoEmbeddingAutocomplete {
constructor() {
- console.log("[KikoAutocomplete] Initializing...");
this.embeddings = [];
this.loras = [];
+ this.customWords = [];
this.settings = {
enabled: true,
- triggerChars: 2,
- maxSuggestions: 20,
showEmbeddings: true,
- showLoras: true
+ showLoras: true,
+ showCustomWords: true,
+ embeddingTrigger: "embedding:",
+ loraTrigger: " {
+ const fullPath = item.path || item.file_name || item.model_name;
+ const nameOnly = item.name || fullPath.split('/').pop();
+
+ return {
+ name: nameOnly,
+ fullPath: fullPath,
+ type: "embedding",
+ display: `embedding:${fullPath}`,
+ value: `embedding:${fullPath}`
+ };
+ });
- // Fetch remaining pages if any
- if (totalPages > 1) {
- // Check if api.getEmbeddings accepts page parameter
- for (let page = 2; page <= totalPages; page++) {
- try {
- console.log(`[KikoAutocomplete] Fetching page ${page}...`);
-
- // Try using the api method with page parameter
- const pageData = await api.getEmbeddings(page);
- if (pageData && pageData.items) {
- allEmbeddings.push(...pageData.items);
- console.log(`[KikoAutocomplete] Page ${page} loaded: ${pageData.items.length} items`);
- } else {
- console.warn(`[KikoAutocomplete] Page ${page} has no items`);
+ useCustomEndpoint = false; // Success, don't use fallback
+ }
+ } catch (customError) {
+ // Custom endpoint not available, will use fallback
+ }
+
+ // Only fallback if custom endpoint failed
+ if (useCustomEndpoint) {
+ try {
+ const embeddingsResponse = await api.getEmbeddings();
+
+ let allEmbeddings = [];
+
+ // Check if this is a paginated response
+ if (embeddingsResponse && embeddingsResponse.items && Array.isArray(embeddingsResponse.items)) {
+ // This is a paginated response - get all pages
+ const totalPages = embeddingsResponse.total_pages || 1;
+
+ // Add items from first page
+ allEmbeddings = [...embeddingsResponse.items];
+
+ // Fetch remaining pages if any
+ if (totalPages > 1) {
+ // Check if api.getEmbeddings accepts page parameter
+ for (let page = 2; page <= totalPages; page++) {
+ try {
+
+ // Try using the api method with page parameter
+ const pageData = await api.getEmbeddings(page);
+ if (pageData && pageData.items) {
+ allEmbeddings.push(...pageData.items);
+ } else {
+ break;
+ }
+ } catch (err) {
+ // If pagination doesn't work, just use what we have
break;
}
- } catch (err) {
- console.warn(`[KikoAutocomplete] Could not fetch page ${page}:`, err);
- // If pagination doesn't work, just use what we have
- console.log("[KikoAutocomplete] Pagination not supported, using first page only");
- break;
}
}
- }
-
- console.log(`[KikoAutocomplete] Collected ${allEmbeddings.length} embeddings from paginated response`);
-
- // Process the embeddings
- console.log("[KikoAutocomplete] Sample item structure:", allEmbeddings[0]);
- this.embeddings = allEmbeddings.map((item, index) => {
- let name = "";
- // Extract name from different possible formats
- if (typeof item === 'string') {
- name = item;
- } else if (item && typeof item === 'object') {
- // Based on the logs, items have file_name and model_name properties
- name = item.file_name || item.model_name || item.name || item.filename;
+ // Process the embeddings
+ this.embeddings = allEmbeddings.map((item, index) => {
+ let fullPath = "";
- // Log first item to verify
- if (index === 0) {
- console.log("[KikoAutocomplete] Using name from item:", name, "from properties:", Object.keys(item));
+ // Extract name from different possible formats
+ if (typeof item === 'string') {
+ fullPath = item;
+ } else if (item && typeof item === 'object') {
+ // Based on the logs, items have file_name and model_name properties
+ fullPath = item.file_name || item.model_name || item.name || item.filename;
}
- }
+
+ // Clean up the path - remove file extensions but keep directory structure
+ const cleanPath = String(fullPath || "").replace(/\.(pt|safetensors|ckpt|bin)$/i, '');
+
+ // Extract just the filename (without path) for display/search purposes
+ const nameOnly = cleanPath.split('/').pop() || cleanPath;
+
+ // Only include if we have a valid name
+ if (cleanPath && cleanPath !== "undefined") {
+ return {
+ name: nameOnly, // Just the name for searching
+ fullPath: cleanPath, // Full path including subdirectory
+ type: "embedding",
+ display: `embedding:${cleanPath}`, // Show full path in display
+ value: `embedding:${cleanPath}` // Use full path in value
+ };
+ }
+ return null;
+ }).filter(item => item !== null); // Remove any null entries
+ }
+ // Check if it's a simple object with embedding names as keys (old format)
+ else if (embeddingsResponse && typeof embeddingsResponse === 'object' && !Array.isArray(embeddingsResponse)) {
+ // Try to extract embeddings from object keys
+ const keys = Object.keys(embeddingsResponse);
- // Clean up the name - remove file extensions if present
- const cleanName = String(name || "").replace(/\.(pt|safetensors|ckpt|bin)$/i, '');
-
- // Only include if we have a valid name
- if (cleanName && cleanName !== "undefined") {
- return {
- name: cleanName,
- type: "embedding",
- display: `embedding:${cleanName}`,
- value: `embedding:${cleanName}`
- };
+ // If the object has standard pagination keys, it's not the embeddings object
+ if (!keys.includes('items') && !keys.includes('total')) {
+ this.embeddings = keys.map(fullPath => {
+ const cleanPath = fullPath.replace(/\.(pt|safetensors|ckpt|bin)$/i, '');
+ const nameOnly = cleanPath.split('/').pop() || cleanPath;
+ return {
+ name: nameOnly,
+ fullPath: cleanPath,
+ type: "embedding",
+ display: `embedding:${cleanPath}`,
+ value: `embedding:${cleanPath}`
+ };
+ });
+ } else {
+ this.embeddings = [];
}
- return null;
- }).filter(item => item !== null); // Remove any null entries
- }
- // Check if it's a simple object with embedding names as keys (old format)
- else if (embeddingsResponse && typeof embeddingsResponse === 'object' && !Array.isArray(embeddingsResponse)) {
- // Try to extract embeddings from object keys
- const keys = Object.keys(embeddingsResponse);
-
- // If the object has standard pagination keys, it's not the embeddings object
- if (!keys.includes('items') && !keys.includes('total')) {
- console.log("[KikoAutocomplete] Object format detected, using keys as embedding names");
- this.embeddings = keys.map(name => {
- const cleanName = name.replace(/\.(pt|safetensors|ckpt|bin)$/i, '');
- return {
- name: cleanName,
- type: "embedding",
- display: `embedding:${cleanName}`,
- value: `embedding:${cleanName}`
- };
- });
} else {
- console.warn("[KikoAutocomplete] Unexpected embeddings format");
this.embeddings = [];
}
- } else {
- console.warn("[KikoAutocomplete] Unexpected embeddings response format:", typeof embeddingsResponse);
+ } catch (e) {
this.embeddings = [];
}
-
- console.log(`[KikoAutocomplete] Processed ${this.embeddings.length} embeddings:`, this.embeddings.slice(0, 5));
- } catch (e) {
- console.error("[KikoAutocomplete] Error fetching embeddings:", e);
- this.embeddings = [];
}
}
// Fetch LoRAs - try multiple methods
if (this.settings.showLoras) {
- console.log("[KikoAutocomplete] Fetching LoRAs...");
// Method 1: Try our custom endpoint
try {
const response = await fetch("/kikotools/autocomplete/loras");
- console.log("[KikoAutocomplete] Custom LoRA endpoint status:", response.status);
if (response.ok) {
- const loras = await response.json();
- console.log("[KikoAutocomplete] Raw LoRAs response:", loras);
- this.loras = loras.map(name => ({
- name: name,
- type: "lora",
- display: ``,
- value: ``
- }));
- console.log(`[KikoAutocomplete] Processed ${this.loras.length} LoRAs from custom endpoint`);
+ const lorasData = await response.json();
+
+ this.loras = lorasData.map(item => {
+ const path = item.path || item.name;
+ const nameOnly = item.name || path.split('/').pop();
+
+ return {
+ name: nameOnly,
+ fullPath: path,
+ type: "lora",
+ display: ``,
+ value: ``
+ };
+ });
} else {
throw new Error("Custom endpoint not available");
}
} catch (e) {
- console.log("[KikoAutocomplete] Custom endpoint failed, trying alternative method...");
// Method 2: Try using the object list API
try {
@@ -195,7 +222,6 @@ class KikoEmbeddingAutocomplete {
// Look for LoraLoader node to get available loras
if (objectInfo.LoraLoader?.input?.required?.lora_name?.[0]) {
const loraNames = objectInfo.LoraLoader.input.required.lora_name[0];
- console.log("[KikoAutocomplete] Found LoRAs from object_info:", loraNames.slice(0, 5));
this.loras = loraNames.map(name => {
const cleanName = name.replace(/\.(pt|safetensors|ckpt|bin)$/i, '');
return {
@@ -205,28 +231,144 @@ class KikoEmbeddingAutocomplete {
value: ``
};
});
- console.log(`[KikoAutocomplete] Processed ${this.loras.length} LoRAs from object_info`);
}
}
} catch (e2) {
- console.error("[KikoAutocomplete] Could not fetch LoRAs:", e2);
this.loras = [];
}
}
}
- console.log(`[KikoAutocomplete] Total loaded: ${this.embeddings.length} embeddings and ${this.loras.length} LoRAs`);
-
- // Store in window for debugging
+ // Store reference for debugging if needed
window.kikoDebug = {
embeddings: this.embeddings,
loras: this.loras,
+ customWords: this.customWords,
autocomplete: this
};
- console.log("[KikoAutocomplete] Debug info available at window.kikoDebug");
} catch (error) {
- console.error("[KikoAutocomplete] Fatal error in fetchResources:", error);
+ // Silent fail - resources will be empty
+ }
+ }
+
+ async loadCustomWords(url) {
+ if (!url) return;
+ try {
+ const response = await fetch(url);
+ if (!response.ok) {
+ throw new Error(`Failed to load custom words: ${response.status}`);
+ }
+
+ const text = await response.text();
+
+ // Security check: Validate content is safe plain text
+ // Check for potential script injections or executable code
+ const dangerousPatterns = [
+ /') ||
+ trimmed.includes('eval(') ||
+ trimmed.includes('document.') ||
+ trimmed.includes('window.')) {
+ invalidLines++;
+ continue;
+ }
+
+ // Split by comma to handle frequency data if present
+ const parts = trimmed.split(',');
+ const word = parts[0].trim();
+ const frequency = parts[1] ? parseInt(parts[1]) : 0;
+
+ // Validate word contains only safe characters
+ // Allow alphanumeric (including starting with numbers), spaces, underscores, hyphens, and common punctuation
+ // Also allow Japanese characters and other unicode letters for international tags
+ // Include semicolons, equals, brackets for tags like "steins;gate" or special formatting
+ const safeWordPattern = /^[a-zA-Z0-9_\s\-:()'\.\!\?,\/\+\*&@#;=\[\]]+$/;
+ if (word && safeWordPattern.test(word) && word.length > 0) {
+ this.customWords.push({
+ name: word,
+ fullPath: word,
+ type: "custom",
+ display: word,
+ value: word,
+ frequency: frequency
+ });
+ validLines++;
+ } else if (word) {
+ invalidLines++;
+ }
+ }
+
+ // Skip warning for invalid lines
+
+ // Sort by frequency if available, otherwise alphabetically
+ this.customWords.sort((a, b) => {
+ if (a.frequency !== b.frequency) {
+ return b.frequency - a.frequency; // Higher frequency first
+ }
+ return a.name.localeCompare(b.name);
+ });
+
+
+ // Store in debug info
+ if (window.kikoDebug) {
+ window.kikoDebug.customWords = this.customWords;
+ }
+
+ // Return count for notification
+ return this.customWords.length;
+
+ } catch (error) {
+ return 0;
}
}
@@ -234,8 +376,6 @@ class KikoEmbeddingAutocomplete {
if (!widget.inputEl || this.activeWidgets.has(widget)) {
return;
}
-
- console.log("[KikoAutocomplete] Attaching to widget:", widget, "on node:", node?.title);
this.activeWidgets.add(widget);
const textarea = widget.inputEl;
@@ -263,7 +403,9 @@ class KikoEmbeddingAutocomplete {
const hideSuggestions = () => {
suggestionContainer.style.display = "none";
+ suggestionContainer.innerHTML = ""; // Clear content to prevent artifacts
selectedIndex = -1;
+ currentSuggestions = [];
};
const showSuggestions = (suggestions, x, y) => {
@@ -285,16 +427,45 @@ class KikoEmbeddingAutocomplete {
// Add type indicator
const typeSpan = document.createElement("span");
+ let typeColor = "#2196F3"; // Default for LORA
+ let typeText = "[LORA]";
+
+ if (item.type === "embedding") {
+ typeColor = "#4CAF50";
+ typeText = "[EMB]";
+ } else if (item.type === "custom") {
+ typeColor = "#FFA500";
+ typeText = "[TAG]";
+ }
+
typeSpan.style.cssText = `
display: inline-block;
width: 60px;
- color: ${item.type === "embedding" ? "#4CAF50" : "#2196F3"};
+ color: ${typeColor};
font-size: 0.9em;
`;
- typeSpan.textContent = item.type === "embedding" ? "[EMB]" : "[LORA]";
+ typeSpan.textContent = typeText;
const nameSpan = document.createElement("span");
- nameSpan.textContent = item.name;
+ // For embeddings with paths, show the full path structure
+ if (item.type === "embedding" && item.fullPath && item.fullPath.includes('/')) {
+ // Show subdirectory in gray, filename in normal color
+ const parts = item.fullPath.split('/');
+ const dir = parts.slice(0, -1).join('/');
+ const file = parts[parts.length - 1];
+
+ const dirSpan = document.createElement("span");
+ dirSpan.style.cssText = "color: #888; font-size: 0.9em;";
+ dirSpan.textContent = dir + "/";
+
+ const fileSpan = document.createElement("span");
+ fileSpan.textContent = file;
+
+ nameSpan.appendChild(dirSpan);
+ nameSpan.appendChild(fileSpan);
+ } else {
+ nameSpan.textContent = item.name;
+ }
div.appendChild(typeSpan);
div.appendChild(nameSpan);
@@ -323,6 +494,24 @@ class KikoEmbeddingAutocomplete {
items.forEach((item, index) => {
item.style.background = index === selectedIndex ? "var(--comfy-input-bg)" : "";
});
+
+ // Auto-scroll to keep selected item visible
+ if (selectedIndex >= 0 && items[selectedIndex]) {
+ const selectedItem = items[selectedIndex];
+ const containerRect = suggestionContainer.getBoundingClientRect();
+ const itemRect = selectedItem.getBoundingClientRect();
+
+ // Check if item is below visible area
+ if (itemRect.bottom > containerRect.bottom) {
+ // Scroll down to show the item
+ suggestionContainer.scrollTop += itemRect.bottom - containerRect.bottom + 5;
+ }
+ // Check if item is above visible area
+ else if (itemRect.top < containerRect.top) {
+ // Scroll up to show the item
+ suggestionContainer.scrollTop -= containerRect.top - itemRect.top + 5;
+ }
+ }
};
const insertSuggestion = (suggestion) => {
@@ -334,13 +523,46 @@ class KikoEmbeddingAutocomplete {
let insertText = "";
let replaceLength = currentPrefix.length;
- // Check if we're completing an embedding (singular or plural)
- if (textBefore.match(/embeddings?:([a-zA-Z0-9_-]*)$/)) {
- insertText = suggestion.name; // Just the name, not the full "embedding:name"
+ // Apply replace underscores setting for custom words
+ if (suggestion.type === "custom" && this.settings.replaceUnderscores) {
+ // Create a modified suggestion with underscores replaced
+ suggestion = {
+ ...suggestion,
+ name: suggestion.name.replace(/_/g, ' '),
+ value: suggestion.value.replace(/_/g, ' '),
+ display: suggestion.display.replace(/_/g, ' ')
+ };
+ }
+
+ // Check if we're completing an embedding after the trigger
+ const embTrigger = this.settings.embeddingTrigger || "embedding:";
+ const embRegex = new RegExp(embTrigger.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + "([a-zA-Z0-9_\\/-]*)$");
+
+ if (textBefore.match(embRegex) || (embTrigger === "embedding:" && textBefore.match(/embeddings:([a-zA-Z0-9_\/-]*)$/))) {
+ // For embeddings, use the full path (including subdirectory)
+ insertText = suggestion.fullPath || suggestion.name; // Use full path if available
}
// Check if we're completing a lora
- else if (textBefore.match(/"; // Complete the lora syntax
+ else if (suggestion.type === "lora") {
+ const loraTrigger = this.settings.loraTrigger || "";
+ } else {
+ // Insert full lora syntax
+ insertText = suggestion.value || suggestion.display;
+ }
+ }
+ // Check if we're replacing a word that starts with "em" (embedding autocomplete)
+ else if (currentPrefix && textBefore.match(/\b(em|emb|embe|embed|embeddi|embeddin|embedding)$/i)) {
+ // User typed a partial "embedding" word, replace it with the full embedding syntax
+ const match = textBefore.match(/\b(em|emb|embe|embed|embeddi|embeddin|embedding)$/i);
+ insertText = suggestion.value || suggestion.display;
+ prefixStart = cursor - match[1].length;
+ replaceLength = match[1].length;
}
// General insertion
else {
@@ -357,8 +579,23 @@ class KikoEmbeddingAutocomplete {
const before = text.substring(0, prefixStart);
const after = text.substring(prefixStart + replaceLength);
- textarea.value = before + insertText + after;
- textarea.selectionStart = textarea.selectionEnd = prefixStart + insertText.length;
+ // Add comma if auto-insert comma is enabled and appropriate
+ let finalInsertText = insertText;
+ if (this.settings.autoInsertComma && (suggestion.type === "custom" || suggestion.type === "embedding" || suggestion.type === "lora")) {
+ // Check if we should add a comma
+ // Add comma if: not already followed by comma, and either at end of text or followed by non-empty content
+ const trimmedAfter = after.trim();
+ const nextChar = trimmedAfter.charAt(0);
+
+ // Add comma unless already followed by comma or punctuation
+ if (nextChar !== ',' && nextChar !== '.' && nextChar !== ';') {
+ // Add comma and space
+ finalInsertText += ', ';
+ }
+ }
+
+ textarea.value = before + finalInsertText + after;
+ textarea.selectionStart = textarea.selectionEnd = prefixStart + finalInsertText.length;
hideSuggestions();
@@ -372,40 +609,59 @@ class KikoEmbeddingAutocomplete {
const cursor = textarea.selectionStart;
const textBefore = text.substring(0, cursor);
- if (this.debug) {
- console.log("[KikoAutocomplete] findPrefix - textBefore:", textBefore);
- }
- // Check for embedding: or embeddings: trigger (both singular and plural)
- const embeddingMatch = textBefore.match(/embeddings?:([a-zA-Z0-9_-]*)$/);
+ // Check for custom embedding trigger
+ const embTrigger = this.settings.embeddingTrigger || "embedding:";
+ const embRegex = new RegExp(embTrigger.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + "([a-zA-Z0-9_\\/-]*)$");
+ const embeddingMatch = textBefore.match(embRegex);
if (embeddingMatch) {
currentPrefix = embeddingMatch[1].toLowerCase();
prefixStart = cursor - embeddingMatch[1].length;
- console.log("[KikoAutocomplete] Found embedding trigger, prefix:", currentPrefix);
return "embedding";
}
- // Check for = 0 && /[a-zA-Z0-9_-]/.test(text[start])) {
start--;
}
start++;
- const prefix = text.substring(start, cursor);
+ const word = text.substring(start, cursor);
- // Check if we should show suggestions for general matching
- if (prefix.length >= this.settings.triggerChars) {
- currentPrefix = prefix.toLowerCase();
+ // Check if we should show suggestions
+ if (word.length >= this.settings.minChars) {
+ currentPrefix = word.toLowerCase();
prefixStart = start;
+
+ // Check for quick trigger (e.g., "em" for embeddings)
+ const quickTrigger = this.settings.quickTrigger;
+ if (quickTrigger && word.toLowerCase().startsWith(quickTrigger.toLowerCase())) {
+ return "embedding-word";
+ }
+
+ // For other words, show general matches
return "general";
}
@@ -415,23 +671,29 @@ class KikoEmbeddingAutocomplete {
const getSuggestions = (prefix, type) => {
const suggestions = [];
- // If type is embedding, only show embeddings
+ // If type is embedding (after "embedding:"), only show embeddings
if (type === "embedding" && this.settings.showEmbeddings) {
suggestions.push(...this.embeddings.filter(e =>
- prefix === "" || e.name.toLowerCase().includes(prefix)
+ prefix === "" || e.name.toLowerCase().includes(prefix) || (e.fullPath && e.fullPath.toLowerCase().includes(prefix))
));
}
+ // If type is embedding-word (user typed "em", "emb", etc), ONLY show embeddings
+ else if (type === "embedding-word" && this.settings.showEmbeddings) {
+ // When user types "em", "emb", "embe", etc. we want to show ALL embeddings
+ // because they're trying to trigger the embedding autocomplete
+ suggestions.push(...this.embeddings);
+ }
// If type is lora, only show loras
else if (type === "lora" && this.settings.showLoras) {
suggestions.push(...this.loras.filter(l =>
prefix === "" || l.name.toLowerCase().includes(prefix)
));
}
- // For general matching, show both
+ // For general matching, show embeddings, loras, and custom words
else if (type === "general") {
if (this.settings.showEmbeddings) {
suggestions.push(...this.embeddings.filter(e =>
- e.name.toLowerCase().includes(prefix)
+ e.name.toLowerCase().includes(prefix) || (e.fullPath && e.fullPath.toLowerCase().includes(prefix))
));
}
@@ -440,22 +702,47 @@ class KikoEmbeddingAutocomplete {
l.name.toLowerCase().includes(prefix)
));
}
+
+ if (this.settings.showCustomWords) {
+ suggestions.push(...this.customWords.filter(w =>
+ w.name.toLowerCase().includes(prefix)
+ ));
+ }
}
// Sort suggestions - prioritize exact matches and starts-with matches
suggestions.sort((a, b) => {
const aName = a.name.toLowerCase();
const bName = b.name.toLowerCase();
+ const aPath = (a.fullPath || a.name).toLowerCase();
+ const bPath = (b.fullPath || b.name).toLowerCase();
- // Exact match
+ // Exact match on name
if (aName === prefix) return -1;
if (bName === prefix) return 1;
- // Starts with
+ // Exact match on full path
+ if (aPath === prefix) return -1;
+ if (bPath === prefix) return 1;
+
+ // Starts with on name
if (aName.startsWith(prefix) && !bName.startsWith(prefix)) return -1;
if (!aName.startsWith(prefix) && bName.startsWith(prefix)) return 1;
- // Alphabetical
+ // Starts with on path
+ if (aPath.startsWith(prefix) && !bPath.startsWith(prefix)) return -1;
+ if (!aPath.startsWith(prefix) && bPath.startsWith(prefix)) return 1;
+
+ // Group by directory if enabled
+ if (this.settings.sortByDirectory) {
+ const aDir = aPath.includes('/') ? aPath.split('/')[0] : '';
+ const bDir = bPath.includes('/') ? bPath.split('/')[0] : '';
+
+ if (aDir !== bDir) {
+ return aDir.localeCompare(bDir);
+ }
+ }
+
return aName.localeCompare(bName);
});
@@ -501,7 +788,8 @@ class KikoEmbeddingAutocomplete {
e.preventDefault();
selectedIndex = Math.max(selectedIndex - 1, 0);
updateSelection();
- } else if (e.key === "Enter" || e.key === "Tab") {
+ } else if ((e.key === "Enter" && this.settings.insertOnEnter) ||
+ (e.key === "Tab" && this.settings.insertOnTab)) {
if (selectedIndex >= 0 && selectedIndex < currentSuggestions.length) {
e.preventDefault();
insertSuggestion(currentSuggestions[selectedIndex]);
@@ -515,117 +803,667 @@ class KikoEmbeddingAutocomplete {
textarea.addEventListener("blur", () => {
setTimeout(hideSuggestions, 200);
});
+
+ // Cleanup on widget destruction
+ if (widget.onRemoved) {
+ const originalOnRemoved = widget.onRemoved;
+ widget.onRemoved = () => {
+ hideSuggestions();
+ if (suggestionContainer && suggestionContainer.parentNode) {
+ suggestionContainer.parentNode.removeChild(suggestionContainer);
+ }
+ this.activeWidgets.delete(widget);
+ originalOnRemoved.call(widget);
+ };
+ } else {
+ widget.onRemoved = () => {
+ hideSuggestions();
+ if (suggestionContainer && suggestionContainer.parentNode) {
+ suggestionContainer.parentNode.removeChild(suggestionContainer);
+ }
+ this.activeWidgets.delete(widget);
+ };
+ }
+ }
+}
+
+// Settings Dialog - Clean, modern interface
+class KikoEmbeddingAutocompleteDialog {
+ constructor(autocompleteInstance) {
+ this.autocomplete = autocompleteInstance;
+ this.element = null;
+ }
+
+ createDialog() {
+ // Create dialog backdrop
+ const backdrop = document.createElement("div");
+ backdrop.className = "kiko-dialog-backdrop";
+ backdrop.style.cssText = `
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.7);
+ z-index: 99998;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ `;
+
+ // Create dialog container
+ const dialog = document.createElement("div");
+ dialog.className = "kiko-dialog";
+ dialog.style.cssText = `
+ background: var(--comfy-menu-bg, #202020);
+ border: 1px solid var(--border-color, #4e4e4e);
+ border-radius: 8px;
+ width: 600px;
+ max-width: 90vw;
+ max-height: 80vh;
+ display: flex;
+ flex-direction: column;
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
+ color: var(--fg-color, #ffffff);
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+ `;
+
+ // Header
+ const header = document.createElement("div");
+ header.style.cssText = `
+ padding: 20px;
+ border-bottom: 1px solid var(--border-color, #4e4e4e);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ `;
+
+ const title = document.createElement("h2");
+ title.textContent = "🫶 Embedding Autocomplete Settings";
+ title.style.cssText = `
+ margin: 0;
+ font-size: 20px;
+ font-weight: 600;
+ color: var(--fg-color, #ffffff);
+ `;
+
+ const closeBtn = document.createElement("button");
+ closeBtn.innerHTML = "✕";
+ closeBtn.style.cssText = `
+ background: transparent;
+ border: none;
+ color: var(--fg-color, #ffffff);
+ font-size: 24px;
+ cursor: pointer;
+ padding: 0;
+ width: 30px;
+ height: 30px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0.7;
+ transition: opacity 0.2s;
+ `;
+ closeBtn.onmouseover = () => closeBtn.style.opacity = "1";
+ closeBtn.onmouseout = () => closeBtn.style.opacity = "0.7";
+
+ header.appendChild(title);
+ header.appendChild(closeBtn);
+
+ // Content
+ const content = document.createElement("div");
+ content.style.cssText = `
+ padding: 20px;
+ overflow-y: auto;
+ flex: 1;
+ `;
+
+ // Create settings sections
+ content.innerHTML = `
+
+
+
+
Trigger Settings
+
+
+
+
+
+ Text that triggers embedding autocomplete (e.g., "embedding:", "emb:", "e:")
+
+
+
+
+
+
+
+ Text that triggers LoRA autocomplete (e.g., "<lora:", "lora:", "l:")
+
+
+
+
+
+
Display Settings
+
+
+
+
+
+ Number of suggestions to show (5-100)
+
+
+
+
+
+
+
+ Minimum characters before showing suggestions (1-10)
+
+
+
+
+
+
Insertion Settings
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Custom Words
+
+
+
+
+
+
+
+
+ File Format Requirements:
+ • Plain text file only (no HTML, JavaScript, or code)
+ • One word/tag per line
+ • Optional format: word,frequency (comma-separated)
+ • Allowed characters: letters, numbers, spaces, underscores, hyphens, basic punctuation
+ • Maximum file size: 10MB
+ • Example: landscape or landscape,5000
+
+
+
+
+ `;
+
+ // Footer
+ const footer = document.createElement("div");
+ footer.style.cssText = `
+ padding: 15px 20px;
+ border-top: 1px solid var(--border-color, #4e4e4e);
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+ `;
+
+ const saveBtn = document.createElement("button");
+ saveBtn.textContent = "Save Settings";
+ saveBtn.style.cssText = `
+ padding: 8px 20px;
+ background: #4CAF50;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 14px;
+ font-weight: 500;
+ transition: background 0.2s;
+ `;
+ saveBtn.onmouseover = () => saveBtn.style.background = "#45a049";
+ saveBtn.onmouseout = () => saveBtn.style.background = "#4CAF50";
+
+ const cancelBtn = document.createElement("button");
+ cancelBtn.textContent = "Cancel";
+ cancelBtn.style.cssText = `
+ padding: 8px 20px;
+ background: var(--comfy-input-bg, #1a1a1a);
+ color: var(--fg-color, #ffffff);
+ border: 1px solid var(--border-color, #4e4e4e);
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 14px;
+ transition: background 0.2s;
+ `;
+ cancelBtn.onmouseover = () => cancelBtn.style.background = "var(--comfy-input-bg-hover, #2a2a2a)";
+ cancelBtn.onmouseout = () => cancelBtn.style.background = "var(--comfy-input-bg, #1a1a1a)";
+
+ footer.appendChild(cancelBtn);
+ footer.appendChild(saveBtn);
+
+ // Assemble dialog
+ dialog.appendChild(header);
+ dialog.appendChild(content);
+ dialog.appendChild(footer);
+ backdrop.appendChild(dialog);
+
+ // Event handlers
+ closeBtn.onclick = () => this.close();
+ cancelBtn.onclick = () => this.close();
+ backdrop.onclick = (e) => {
+ if (e.target === backdrop) this.close();
+ };
+
+ saveBtn.onclick = () => this.save();
+
+ this.element = backdrop;
+ this.dialog = dialog;
+
+ // Settings will be loaded after adding to DOM
+
+ return backdrop;
+ }
+
+ showCustomStatus(message, type = "info") {
+ const statusEl = document.getElementById("kiko-custom-words-status");
+ if (!statusEl) return;
+
+ statusEl.textContent = message;
+ statusEl.style.display = "block";
+
+ // Set color based on type
+ switch(type) {
+ case "success":
+ statusEl.style.color = "#4CAF50";
+ break;
+ case "error":
+ statusEl.style.color = "#f44336";
+ break;
+ case "warning":
+ statusEl.style.color = "#FFA500";
+ break;
+ default:
+ statusEl.style.color = "#888";
+ }
+ }
+
+ loadSettings() {
+ if (!this.autocomplete) return;
+
+ const settings = this.autocomplete.settings;
+
+ // Make sure elements exist before trying to set values
+ const elements = {
+ enabled: document.getElementById("kiko-enabled"),
+ showEmbeddings: document.getElementById("kiko-show-embeddings"),
+ showLoras: document.getElementById("kiko-show-loras"),
+ showCustom: document.getElementById("kiko-show-custom"),
+ groupByDir: document.getElementById("kiko-group-by-dir"),
+ embeddingTrigger: document.getElementById("kiko-embedding-trigger"),
+ loraTrigger: document.getElementById("kiko-lora-trigger"),
+ maxSuggestions: document.getElementById("kiko-max-suggestions"),
+ minChars: document.getElementById("kiko-min-chars"),
+ customWordsUrl: document.getElementById("kiko-custom-words-url"),
+ loadCustomBtn: document.getElementById("kiko-load-custom-words"),
+ customStatus: document.getElementById("kiko-custom-words-status"),
+ autoComma: document.getElementById("kiko-auto-comma"),
+ replaceUnderscores: document.getElementById("kiko-replace-underscores"),
+ insertTab: document.getElementById("kiko-insert-tab"),
+ insertEnter: document.getElementById("kiko-insert-enter")
+ };
+
+ // Check if all elements exist
+ if (!elements.enabled) {
+ console.error("[KikoAutocomplete] Settings elements not found in DOM");
+ return;
+ }
+
+ // Set values
+ if (elements.enabled) elements.enabled.checked = settings.enabled !== false;
+ if (elements.showEmbeddings) elements.showEmbeddings.checked = settings.showEmbeddings !== false;
+ if (elements.showLoras) elements.showLoras.checked = settings.showLoras !== false;
+ if (elements.showCustom) elements.showCustom.checked = settings.showCustomWords !== false;
+ if (elements.groupByDir) elements.groupByDir.checked = settings.sortByDirectory !== false;
+ if (elements.embeddingTrigger) elements.embeddingTrigger.value = settings.embeddingTrigger || "embedding:";
+ if (elements.loraTrigger) elements.loraTrigger.value = settings.loraTrigger || " {
+ const url = elements.customWordsUrl.value.trim();
+ if (!url) {
+ this.showCustomStatus("Please enter a URL", "error");
+ return;
+ }
+
+ // Show loading state
+ elements.loadCustomBtn.disabled = true;
+ elements.loadCustomBtn.textContent = "Loading...";
+ this.showCustomStatus("Loading custom words...", "info");
+
+ try {
+ const count = await this.autocomplete.loadCustomWords(url);
+ if (count > 0) {
+ this.showCustomStatus(`Successfully loaded ${count.toLocaleString()} custom words`, "success");
+ // Save the URL if successful
+ this.autocomplete.settings.customWordsUrl = url;
+ this.autocomplete.saveSettings();
+ } else {
+ this.showCustomStatus("No words found in the file", "warning");
+ }
+ } catch (error) {
+ this.showCustomStatus(`Error loading custom words: ${error.message}`, "error");
+ } finally {
+ elements.loadCustomBtn.disabled = false;
+ elements.loadCustomBtn.textContent = "Load";
+ }
+ };
+ }
+
+ // Show current custom words count if any
+ if (this.autocomplete.customWords && this.autocomplete.customWords.length > 0) {
+ this.showCustomStatus(`${this.autocomplete.customWords.length.toLocaleString()} custom words loaded`, "info");
+ }
+ }
+
+ save() {
+ if (!this.autocomplete) return;
+
+ // Gather settings
+ const newSettings = {
+ enabled: document.getElementById("kiko-enabled").checked,
+ showEmbeddings: document.getElementById("kiko-show-embeddings").checked,
+ showLoras: document.getElementById("kiko-show-loras").checked,
+ showCustomWords: document.getElementById("kiko-show-custom").checked,
+ sortByDirectory: document.getElementById("kiko-group-by-dir").checked,
+ embeddingTrigger: document.getElementById("kiko-embedding-trigger").value || "embedding:",
+ loraTrigger: document.getElementById("kiko-lora-trigger").value || " this.close(), 1000);
+ }
+
+ showMessage(message) {
+ const msgEl = document.createElement("div");
+ msgEl.textContent = message;
+ msgEl.style.cssText = `
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ background: #4CAF50;
+ color: white;
+ padding: 12px 20px;
+ border-radius: 4px;
+ box-shadow: 0 2px 10px rgba(0,0,0,0.3);
+ z-index: 100000;
+ animation: slideIn 0.3s ease;
+ `;
+
+ // Add animation
+ const style = document.createElement("style");
+ style.textContent = `
+ @keyframes slideIn {
+ from { transform: translateX(100%); opacity: 0; }
+ to { transform: translateX(0); opacity: 1; }
+ }
+ `;
+ document.head.appendChild(style);
+
+ document.body.appendChild(msgEl);
+
+ setTimeout(() => {
+ msgEl.style.animation = "slideIn 0.3s ease reverse";
+ setTimeout(() => {
+ msgEl.remove();
+ style.remove();
+ }, 300);
+ }, 2000);
+ }
+
+ show() {
+ if (!this.element) {
+ this.createDialog();
+ }
+ document.body.appendChild(this.element);
+
+ // Load settings after adding to DOM
+ setTimeout(() => this.loadSettings(), 10);
+
+ // Add open animation
+ this.dialog.style.animation = "fadeIn 0.2s ease";
+ this.element.style.animation = "fadeIn 0.2s ease";
+
+ const style = document.createElement("style");
+ style.textContent = `
+ @keyframes fadeIn {
+ from { opacity: 0; transform: scale(0.95); }
+ to { opacity: 1; transform: scale(1); }
+ }
+ `;
+ document.head.appendChild(style);
+ this.animStyle = style;
+ }
+
+ close() {
+ if (this.element) {
+ this.element.remove();
+ }
+ if (this.animStyle) {
+ this.animStyle.remove();
+ }
}
}
// 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
+ // Create autocomplete instance first
window.kikoAutocomplete = new KikoEmbeddingAutocomplete();
+ // Simple button to open settings dialog (following rgthree's pattern)
+ app.ui.settings.addSetting({
+ id: "kikotools.embeddingAutocomplete",
+ name: "🫶 Embedding Autocomplete Configuration",
+ tooltip: "Configure autocomplete settings",
+ category: ["kikotools", "🫶 Embedding Autocomplete Configuration"],
+ defaultValue: null,
+ type: () => {
+ // Create a simple row with a button to open the dialog
+ const tr = document.createElement("tr");
+ tr.className = "kikotools-settings-row";
+
+ const tdLabel = document.createElement("td");
+ tdLabel.innerHTML = `
+ 🫶
+ Embedding Autocomplete Configuration
+
`;
+
+ const tdButton = document.createElement("td");
+ const button = document.createElement("button");
+ button.textContent = "Open Settings";
+ button.className = "kiko-settings-button";
+ button.style.cssText = `
+ padding: 8px 20px;
+ background: #4CAF50;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 14px;
+ font-weight: 500;
+ transition: background 0.2s;
+ `;
+ button.onmouseover = () => button.style.background = "#45a049";
+ button.onmouseout = () => button.style.background = "#4CAF50";
+ button.onclick = () => {
+ const dialog = new KikoEmbeddingAutocompleteDialog(window.kikoAutocomplete);
+ dialog.show();
+ };
+
+ tdButton.appendChild(button);
+ tr.appendChild(tdLabel);
+ tr.appendChild(tdButton);
+
+ return tr;
+ }
+ });
+
// Override STRING widget creation to add autocomplete
const originalStringWidget = ComfyWidgets.STRING;
- console.log("[KikoAutocomplete] Original STRING widget:", originalStringWidget);
ComfyWidgets.STRING = function(node, inputName, inputData, app) {
- console.log("[KikoAutocomplete] Creating STRING widget for:", inputName, "multiline:", inputData[1]?.multiline);
const widget = originalStringWidget.apply(this, arguments);
// Only attach to multiline text widgets (prompts)
if (inputData[1]?.multiline && widget.inputEl) {
- console.log("[KikoAutocomplete] Will attach to multiline widget:", inputName);
setTimeout(() => {
window.kikoAutocomplete.attachToWidget(widget, node);
}, 100);
@@ -635,19 +1473,16 @@ app.registerExtension({
};
// Also attach to existing multiline widgets
- console.log("[KikoAutocomplete] Looking for existing widgets to attach to...");
setTimeout(() => {
app.graph._nodes.forEach(node => {
if (node.widgets) {
node.widgets.forEach(widget => {
// Check for text widgets with textareas
if (widget.type === "customtext" && widget.inputEl && widget.inputEl.tagName === "TEXTAREA") {
- console.log("[KikoAutocomplete] Found existing textarea widget on node:", node.title, "widget:", widget.name);
window.kikoAutocomplete.attachToWidget(widget, node);
}
// Also check for widgets that might be text inputs but not customtext
else if (widget.inputEl && widget.inputEl.tagName === "TEXTAREA" && !window.kikoAutocomplete.activeWidgets.has(widget)) {
- console.log("[KikoAutocomplete] Found textarea widget (non-customtext) on node:", node.title, "widget:", widget.name);
window.kikoAutocomplete.attachToWidget(widget, node);
}
});