diff --git a/pyproject.toml b/pyproject.toml index cbb004e..c4d4566 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "promptmanager" description = "A powerful ComfyUI custom node that extends the standard text encoder with persistent prompt storage, advanced search capabilities, and an automatic image gallery system using SQLite." -version = "3.0.19" +version = "3.0.20" license = {file = "LICENSE"} dependencies = ["# Core dependencies for PromptManager", "# Note: Most dependencies are already included with ComfyUI", "# Already included with Python standard library:", "# - sqlite3", "# - hashlib", "# - json", "# - datetime", "# - os", "# - typing", "# - threading", "# - uuid", "# Required for gallery functionality:", "watchdog>=2.1.0 # For file system monitoring", "Pillow>=8.0.0 # For image metadata extraction (usually included with ComfyUI)", "# Optional dependencies for enhanced search functionality:", "# fuzzywuzzy[speedup]>=0.18.0 # For fuzzy string matching (optional)", "# sqlalchemy>=1.4.0 # For advanced ORM features (optional)", "# Development dependencies (optional):", "# pytest>=6.0.0 # For running tests", "# black>=22.0.0 # For code formatting", "# flake8>=4.0.0 # For linting", "# mypy>=0.910 # For type checking"] diff --git a/tests/test_basic.py b/tests/test_basic.py index 619b217..22ab420 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1,5 +1,5 @@ """ -Basic tests for KikoTextEncode functionality. +Basic tests for PromptManager functionality. """ import os @@ -17,7 +17,7 @@ from utils.validators import validate_prompt_text, validate_rating, validate_tag class TestBasicFunctionality(unittest.TestCase): - """Test basic functionality of KikoTextEncode components.""" + """Test basic functionality of PromptManager components.""" def setUp(self): """Set up test fixtures.""" @@ -118,7 +118,6 @@ class TestBasicFunctionality(unittest.TestCase): tags=["test", "database"], rating=4, notes="Test notes", - workflow_name="test_workflow", prompt_hash=generate_prompt_hash("Test prompt for database") ) @@ -133,7 +132,6 @@ class TestBasicFunctionality(unittest.TestCase): self.assertEqual(retrieved['tags'], ["test", "database"]) self.assertEqual(retrieved['rating'], 4) self.assertEqual(retrieved['notes'], "Test notes") - self.assertEqual(retrieved['workflow_name'], "test_workflow") def test_duplicate_detection(self): """Test duplicate prompt detection.""" @@ -220,7 +218,7 @@ class TestNodeIntegration(unittest.TestCase): if os.path.exists(self.temp_db.name): os.unlink(self.temp_db.name) - @patch('kiko_text_encode.PromptDatabase') + @patch('prompt_manager.PromptDatabase') def test_node_encode_function(self, mock_db_class): """Test the node's encode function with mocked dependencies.""" # Mock the database @@ -233,44 +231,36 @@ class TestNodeIntegration(unittest.TestCase): mock_clip.encode_from_tokens_scheduled.return_value = "mock_conditioning" # Import and test the node - from kiko_text_encode import KikoTextEncode + from prompt_manager import PromptManager - node = KikoTextEncode() + node = PromptManager() # Test encoding result = node.encode( clip=mock_clip, text="Test prompt", - category="test", - tags="tag1, tag2", - rating=4, - notes="Test notes", - workflow_name="test_workflow" + search_text="" ) # Verify CLIP was called correctly mock_clip.tokenize.assert_called_once_with("Test prompt") mock_clip.encode_from_tokens_scheduled.assert_called_once_with("mock_tokens") - # Verify result - self.assertEqual(result, ("mock_conditioning",)) + # Verify result - PromptManager returns both conditioning and prompt text + self.assertEqual(result, ("mock_conditioning", "Test prompt")) def test_node_input_types(self): """Test the node's input type definitions.""" - from kiko_text_encode import KikoTextEncode + from prompt_manager import PromptManager - input_types = KikoTextEncode.INPUT_TYPES() + input_types = PromptManager.INPUT_TYPES() # Check required inputs self.assertIn("text", input_types["required"]) self.assertIn("clip", input_types["required"]) # Check optional inputs - self.assertIn("category", input_types["optional"]) - self.assertIn("tags", input_types["optional"]) - self.assertIn("rating", input_types["optional"]) - self.assertIn("notes", input_types["optional"]) - self.assertIn("workflow_name", input_types["optional"]) + self.assertIn("search_text", input_types["optional"]) if __name__ == '__main__': diff --git a/web/admin.html b/web/admin.html index 9e84032..640efdc 100644 --- a/web/admin.html +++ b/web/admin.html @@ -2969,23 +2969,25 @@ // Prompts - need to identify which is positive vs negative if (node.class_type === 'PromptManager' && node.inputs && node.inputs.text) { // PromptManager typically contains the positive prompt - positivePrompt = node.inputs.text; - console.log('Found PromptManager with text:', positivePrompt.substring(0, 100)); + const promptValue = typeof node.inputs.text === 'string' ? node.inputs.text : (Array.isArray(node.inputs.text) ? node.inputs.text[0] : String(node.inputs.text)); + positivePrompt = promptValue; + console.log('Found PromptManager with text:', promptValue.substring(0, 100)); } if (node.class_type === 'CLIPTextEncode' && node.inputs && node.inputs.text) { - console.log('Found CLIPTextEncode:', node.inputs.text.substring(0, 50)); // Check if this looks like a negative prompt - const text = node.inputs.text.toLowerCase(); + const textValue = typeof node.inputs.text === 'string' ? node.inputs.text : (Array.isArray(node.inputs.text) ? node.inputs.text[0] : String(node.inputs.text)); + console.log('Found CLIPTextEncode:', textValue.substring(0, 50)); + const text = textValue.toLowerCase(); if (text.includes('bad anatomy') || text.includes('unfinished') || text.includes('censored') || text.includes('weird anatomy') || text.includes('negative') || text.includes('embedding:')) { - negativePrompt = node.inputs.text; - console.log('Found negative prompt:', negativePrompt.substring(0, 100)); + negativePrompt = textValue; + console.log('Found negative prompt:', textValue.substring(0, 100)); } else if (positivePrompt === 'No prompt found') { // If we haven't found a positive prompt yet, this might be it - positivePrompt = node.inputs.text; - console.log('Found potential positive prompt:', positivePrompt.substring(0, 100)); + positivePrompt = textValue; + console.log('Found potential positive prompt:', textValue.substring(0, 100)); } } @@ -3385,19 +3387,21 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`; // Prompts - need to identify which is positive vs negative if (node.class_type === 'PromptManager' && node.inputs && node.inputs.text) { // PromptManager typically contains the positive prompt - positivePrompt = node.inputs.text; + const promptValue = typeof node.inputs.text === 'string' ? node.inputs.text : (Array.isArray(node.inputs.text) ? node.inputs.text[0] : String(node.inputs.text)); + positivePrompt = promptValue; } if (node.class_type === 'CLIPTextEncode' && node.inputs && node.inputs.text) { // Check if this looks like a negative prompt - const text = node.inputs.text.toLowerCase(); + const textValue = typeof node.inputs.text === 'string' ? node.inputs.text : (Array.isArray(node.inputs.text) ? node.inputs.text[0] : String(node.inputs.text)); + const text = textValue.toLowerCase(); if (text.includes('bad anatomy') || text.includes('unfinished') || text.includes('censored') || text.includes('weird anatomy') || text.includes('negative') || text.includes('embedding:')) { - negativePrompt = node.inputs.text; + negativePrompt = textValue; } else if (positivePrompt === 'No prompt found') { // If we haven't found a positive prompt yet, this might be it - positivePrompt = node.inputs.text; + positivePrompt = textValue; } } @@ -3928,12 +3932,12 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`; document.addEventListener('keydown', function(e) { if (e.key === 'Escape') { // Close any open modals - if (!document.getElementById('galleryModal').classList.contains('hidden')) { + if (document.getElementById('galleryModal') && !document.getElementById('galleryModal').classList.contains('hidden')) { admin.closeGallery(); - } else if (!document.getElementById('imageViewerModal').classList.contains('hidden')) { + } else if (document.getElementById('imageViewerModal') && !document.getElementById('imageViewerModal').classList.contains('hidden')) { admin.closeImageViewer(); } - } else if (!document.getElementById('imageViewerModal').classList.contains('hidden')) { + } else if (document.getElementById('imageViewerModal') && !document.getElementById('imageViewerModal').classList.contains('hidden')) { // Handle arrow keys in image viewer if (e.key === 'ArrowLeft') { e.preventDefault(); @@ -3948,7 +3952,7 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`; // Window resize listener for responsive image sizing window.addEventListener('resize', function() { // Only apply resize adjustments if image viewer is open and in fit mode - if (!document.getElementById('imageViewerModal').classList.contains('hidden') && admin.imageViewMode === 'fit') { + if (document.getElementById('imageViewerModal') && !document.getElementById('imageViewerModal').classList.contains('hidden') && admin.imageViewMode === 'fit') { admin.applyImageSizing(); } }); diff --git a/web/prompt_manager.js b/web/prompt_manager.js index 3c8cb09..eb35cf7 100644 --- a/web/prompt_manager.js +++ b/web/prompt_manager.js @@ -35,25 +35,57 @@ app.registerExtension({ // Load settings from API this.loadSettings(); - // Create DOM widget container + // Create DOM widget container with unique ID for scoped styling const container = document.createElement("div"); - container.style.padding = "8px"; - container.style.backgroundColor = "#2a2a2a"; - container.style.borderRadius = "4px"; - container.style.marginTop = "5px"; + const uniqueId = `pm-${Math.random().toString(36).substring(2, 9)}`; + container.id = uniqueId; + + // Add inline styles for perfect sizing like KikoLocalImageLoader + const styleSheet = document.createElement("style"); + styleSheet.textContent = ` + #${uniqueId} { + width: 100%; + height: 100%; + padding: 8px; + background-color: #2a2a2a; + border-radius: 4px; + box-sizing: border-box; + display: flex; + flex-direction: column; + overflow: hidden; + } + #${uniqueId} > div { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + gap: 8px; + } + `; + document.head.appendChild(styleSheet); + + // Store style reference for cleanup + this._styleSheet = styleSheet; this.createSearchUI(container); - // Add as DOM widget + // Add as DOM widget with proper configuration this.searchWidget = this.addDOMWidget( "prompt_manager_search_ui", "div", container, + { + serialize: false, // Don't save UI state + } ); // Set initial node size, but preserve user resizes if (!this._userHasResized) { - this.size = [400, 400]; // width=400, height=300 pixels + // For nodes with text widgets, we need more height + const baseHeight = 400; + const widgetHeight = this.widgets ? this.widgets.length * 30 : 0; + this.size = [600, baseHeight + widgetHeight]; // Dynamic size based on widgets + this.setSize(this.size); // IMPORTANT: Actually apply the size } // Hook into resize to track user changes @@ -97,19 +129,44 @@ app.registerExtension({ this.setDirtyCanvas(true, true); }; + // Override onRemoved to cleanup styles + const onRemoved = nodeType.prototype.onRemoved; + nodeType.prototype.onRemoved = function () { + // Cleanup styles when node is removed + if (this._styleSheet && this._styleSheet.parentNode) { + this._styleSheet.parentNode.removeChild(this._styleSheet); + } + + // Call original onRemoved if it exists + if (onRemoved) { + onRemoved.apply(this, arguments); + } + }; + // Method to create search UI elements nodeType.prototype.createSearchUI = function (container) { container.innerHTML = ""; + // Main wrapper to match KikoLocalImageLoader structure + const wrapper = document.createElement("div"); + wrapper.style.width = "100%"; + wrapper.style.height = "100%"; + wrapper.style.display = "flex"; + wrapper.style.flexDirection = "column"; + wrapper.style.gap = "8px"; + // Search controls section const searchSection = document.createElement("div"); - searchSection.style.marginBottom = "10px"; + searchSection.style.display = "flex"; + searchSection.style.flexDirection = "column"; + searchSection.style.height = "100%"; + searchSection.style.gap = "8px"; // Search buttons row const buttonRow = document.createElement("div"); buttonRow.style.display = "flex"; buttonRow.style.gap = "8px"; - buttonRow.style.marginBottom = "8px"; + buttonRow.style.flexShrink = "0"; // Don't shrink buttons // Search button const searchButton = document.createElement("button"); @@ -178,7 +235,8 @@ app.registerExtension({ // Results section const resultsSection = document.createElement("div"); - resultsSection.style.maxHeight = "200px"; + resultsSection.style.flex = "1"; // Take remaining space + resultsSection.style.minHeight = "100px"; // Minimum height resultsSection.style.overflowY = "auto"; resultsSection.style.border = "1px solid #444"; resultsSection.style.borderRadius = "4px"; @@ -192,7 +250,9 @@ app.registerExtension({ this.resultsSection = resultsSection; searchSection.appendChild(resultsSection); - container.appendChild(searchSection); + // Add search section to wrapper, then wrapper to container + wrapper.appendChild(searchSection); + container.appendChild(wrapper); }; // Method to perform search