From fb09229719d1cdc927f218682f70e0f7ae7626e1 Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Wed, 28 May 2025 11:01:10 -0700 Subject: [PATCH 1/3] feat(admin): add pagination controls to recent prompts --- web/admin.html | 108 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 105 insertions(+), 3 deletions(-) diff --git a/web/admin.html b/web/admin.html index 7dfba14..e0ade2d 100644 --- a/web/admin.html +++ b/web/admin.html @@ -207,7 +207,20 @@
-

Recent Prompts

+
+

Recent Prompts

+
+ + + per page +
+
Loading...
@@ -223,6 +236,32 @@ + + +
@@ -744,6 +783,14 @@ this.tags = []; this.imageViewMode = 'fit'; // 'fit' or 'full' this.naturalImageSize = { width: 0, height: 0 }; + + // Pagination state + this.pagination = { + currentPage: 1, + limit: 50, + total: 0, + totalPages: 1 + }; this.init(); } @@ -777,6 +824,13 @@ document.getElementById("scanBtn").addEventListener("click", () => this.showScanModal()); document.getElementById("logsBtn").addEventListener("click", () => this.showLogsModal()); + // Pagination controls + document.getElementById("limitSelector").addEventListener("change", (e) => this.changeLimit(parseInt(e.target.value))); + document.getElementById("firstPageBtn").addEventListener("click", () => this.goToPage(1)); + document.getElementById("prevPageBtn").addEventListener("click", () => this.goToPage(this.pagination.currentPage - 1)); + document.getElementById("nextPageBtn").addEventListener("click", () => this.goToPage(this.pagination.currentPage + 1)); + document.getElementById("lastPageBtn").addEventListener("click", () => this.goToPage(this.pagination.totalPages)); + // Modals this.bindModalEvents(); @@ -934,14 +988,23 @@ }); } - async loadRecentPrompts() { + async loadRecentPrompts(page = 1) { try { - const response = await fetch("/prompt_manager/recent?limit=50"); + this.pagination.currentPage = page; + const offset = (page - 1) * this.pagination.limit; + const response = await fetch(`/prompt_manager/recent?limit=${this.pagination.limit}&offset=${offset}&page=${page}`); if (response.ok) { const data = await response.json(); if (data.success) { this.prompts = data.results; + this.pagination = { + ...this.pagination, + total: data.pagination.total, + totalPages: data.pagination.total_pages, + currentPage: data.pagination.page + }; this.renderPrompts(); + this.updatePaginationControls(); // Don't update stats from local data as it's only a subset of prompts } } @@ -950,6 +1013,7 @@ } finally { document.getElementById("loadingState").classList.add("hidden"); document.getElementById("resultsList").classList.remove("hidden"); + document.getElementById("paginationControls").classList.remove("hidden"); } } @@ -2607,6 +2671,44 @@ const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; } + + // Pagination methods + updatePaginationControls() { + const { currentPage, totalPages, total, limit } = this.pagination; + + // Update pagination info + document.getElementById("currentPage").textContent = currentPage; + document.getElementById("totalPages").textContent = totalPages; + document.getElementById("totalResults").textContent = total; + + // Calculate showing range + const start = ((currentPage - 1) * limit) + 1; + const end = Math.min(currentPage * limit, total); + document.getElementById("showingStart").textContent = start; + document.getElementById("showingEnd").textContent = end; + + // Update results count + document.getElementById("resultsCount").textContent = `${this.prompts.length} of ${total}`; + + // Update button states + document.getElementById("firstPageBtn").disabled = currentPage === 1; + document.getElementById("prevPageBtn").disabled = currentPage === 1; + document.getElementById("nextPageBtn").disabled = currentPage === totalPages; + document.getElementById("lastPageBtn").disabled = currentPage === totalPages; + } + + async goToPage(page) { + if (page < 1 || page > this.pagination.totalPages || page === this.pagination.currentPage) { + return; + } + await this.loadRecentPrompts(page); + } + + async changeLimit(newLimit) { + this.pagination.limit = newLimit; + this.pagination.currentPage = 1; // Reset to first page + await this.loadRecentPrompts(1); + } } // Initialize the admin interface From 4f6d7234d673b9008d9379cb39286f638c3b8171 Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Wed, 28 May 2025 11:01:20 -0700 Subject: [PATCH 2/3] feat(api): add pagination and A1111 parsing support --- py/api.py | 207 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 164 insertions(+), 43 deletions(-) diff --git a/py/api.py b/py/api.py index 417050d..ae49e8d 100644 --- a/py/api.py +++ b/py/api.py @@ -332,17 +332,39 @@ class PromptManagerAPI: async def get_recent_prompts(self, request): """ - Get recent prompts endpoint. - GET /prompt_manager/recent?limit=... + Get recent prompts endpoint with pagination support. + GET /prompt_manager/recent?limit=50&page=2&offset=100 """ try: - limit = int(request.query.get("limit", 20)) + limit = int(request.query.get("limit", 50)) + page = int(request.query.get("page", 1)) + offset = int(request.query.get("offset", 0)) + + # If page is provided, calculate offset from page + if page > 1 and offset == 0: + offset = (page - 1) * limit + + # Ensure reasonable limits + if limit > 1000: + limit = 1000 + elif limit < 1: + limit = 1 - results = self.db.get_recent_prompts(limit=limit) + results = self.db.get_recent_prompts(limit=limit, offset=offset) - return web.json_response( - {"success": True, "results": results, "count": len(results)} - ) + return web.json_response({ + "success": True, + "results": results['prompts'], + "pagination": { + "total": results['total'], + "limit": results['limit'], + "offset": results['offset'], + "page": results['page'], + "total_pages": results['total_pages'], + "has_more": results['has_more'], + "count": len(results['prompts']) + } + }) except Exception as e: self.logger.error(f"Recent prompts error: {e}", exc_info=True) @@ -351,6 +373,7 @@ class PromptManagerAPI: "success": False, "error": f"Failed to get recent prompts: {str(e)}", "results": [], + "pagination": {"total": 0, "page": 1, "total_pages": 0} }, status=500, ) @@ -1851,42 +1874,61 @@ class PromptManagerAPI: return {} def _parse_comfyui_prompt(self, metadata): - """Parse ComfyUI prompt data from metadata.""" + """Parse ComfyUI and A1111 prompt data from metadata.""" result = { 'prompt': None, 'workflow': None, - 'parameters': {} + 'parameters': {}, + 'positive_prompt': None, + 'negative_prompt': None } - # Check for direct prompt field - if 'prompt' in metadata: - try: - prompt_data = json.loads(metadata['prompt']) - result['prompt'] = prompt_data - except json.JSONDecodeError: - result['prompt'] = metadata['prompt'] + # Check for A1111 style parameters first (like parse-metadata.py) + if "parameters" in metadata: + params = metadata["parameters"] + lines = params.splitlines() + if lines: + result['positive_prompt'] = lines[0].strip() + for line in lines: + if line.lower().startswith("negative prompt:"): + result['negative_prompt'] = line.split(":", 1)[1].strip() + break + # Store raw parameters too + result['parameters']['parameters'] = params - # Check for workflow - if 'workflow' in metadata: - try: - workflow_data = json.loads(metadata['workflow']) - result['workflow'] = workflow_data - except json.JSONDecodeError: - result['workflow'] = metadata['workflow'] - - # Check for other common ComfyUI fields - common_fields = ['parameters', 'positive', 'negative', 'steps', 'cfg', 'sampler', 'scheduler', 'seed'] - for field in common_fields: - if field in metadata: + # If no A1111 format found, proceed with ComfyUI parsing + if result['positive_prompt'] is None: + # Check for direct prompt field + if 'prompt' in metadata: try: - result['parameters'][field] = json.loads(metadata[field]) + prompt_data = json.loads(metadata['prompt']) + result['prompt'] = prompt_data except json.JSONDecodeError: - result['parameters'][field] = metadata[field] + result['prompt'] = metadata['prompt'] + + # Check for workflow + if 'workflow' in metadata: + try: + workflow_data = json.loads(metadata['workflow']) + result['workflow'] = workflow_data + except json.JSONDecodeError: + result['workflow'] = metadata['workflow'] + + # Check for other common ComfyUI fields + common_fields = ['positive', 'negative', 'steps', 'cfg', 'sampler', 'scheduler', 'seed'] + for field in common_fields: + if field in metadata: + try: + result['parameters'][field] = json.loads(metadata[field]) + except json.JSONDecodeError: + result['parameters'][field] = metadata[field] return result def _extract_readable_prompt(self, parsed_data): - """Extract human-readable prompt text from ComfyUI JSON structure.""" + """Extract human-readable prompt text from ComfyUI/A1111 data using improved logic.""" + import json + # Helper function to convert any value to string safely def safe_to_string(value): if isinstance(value, str): @@ -1898,6 +1940,10 @@ class PromptManagerAPI: return str(value) return None + # First check if we already extracted a positive prompt (A1111 format) + if parsed_data.get('positive_prompt'): + return parsed_data['positive_prompt'] + # Check if prompt is already a string if isinstance(parsed_data.get('prompt'), str): return parsed_data['prompt'] @@ -1908,24 +1954,99 @@ class PromptManagerAPI: prompt_data = parsed_data.get('prompt') if isinstance(prompt_data, dict): - # ComfyUI stores prompts in various node types - for node_id, node_data in prompt_data.items(): - if isinstance(node_data, dict): - class_type = node_data.get('class_type', '') - inputs = node_data.get('inputs', {}) - - # Common prompt node types in ComfyUI - if class_type in ['CLIPTextEncode', 'CLIPTextEncodeSDXL', 'PromptText']: - if 'text' in inputs: - return safe_to_string(inputs['text']) - elif 'text' in inputs: - return safe_to_string(inputs['text']) + # Use the enhanced logic from parse-metadata.py + positive_prompt = self._extract_positive_prompt_from_comfyui_data(prompt_data) + if positive_prompt: + return positive_prompt + + # Check workflow data if available + workflow_data = parsed_data.get('workflow') + if isinstance(workflow_data, dict): + positive_prompt = self._extract_positive_prompt_from_comfyui_data(workflow_data) + if positive_prompt: + return positive_prompt # Check parameters for positive prompt if parsed_data.get('parameters', {}).get('positive'): return safe_to_string(parsed_data['parameters']['positive']) return None + + def _extract_positive_prompt_from_comfyui_data(self, data): + """Extract positive prompt from ComfyUI data using the logic from parse-metadata.py.""" + if not isinstance(data, dict): + return None + + # Build nodes dictionary similar to parse-metadata.py + nodes_by_id = {} + if "nodes" in data: + # Handle nodes array format + for node in data["nodes"]: + nid = node.get("id") + if nid is not None: + nodes_by_id[nid] = node + else: + # Handle flat dictionary format (node_id -> node_data) + for nid_str, node in data.items(): + try: + nid = int(nid_str) + except: + nid = nid_str + if isinstance(node, dict): + if "id" in node: + nid = node["id"] + nodes_by_id[nid] = node + + if not nodes_by_id: + return None + + # First, try to find positive/negative connection pattern + pos_id = None + for node in nodes_by_id.values(): + inputs = node.get("inputs", {}) + if "positive" in inputs and "negative" in inputs: + try: + pos_id = int(inputs["positive"][0]) + break + except: + continue + + # Get text from the positive node + if pos_id is not None and pos_id in nodes_by_id: + text_val = nodes_by_id[pos_id].get("inputs", {}).get("text") + if isinstance(text_val, str): + return text_val + + # Fallback: find any text encoder node with text content + text_encoder_types = [ + 'CLIPTextEncode', 'CLIPTextEncodeSDXL', 'CLIPTextEncodeSDXLRefiner', + 'PromptManager', 'BNK_CLIPTextEncoder', 'Text Encoder', 'CLIP Text Encode' + ] + + for node in nodes_by_id.values(): + if isinstance(node, dict): + class_type = node.get('class_type', '') + inputs = node.get('inputs', {}) + + # Check if this is a text encoder node + if any(encoder_type.lower() in class_type.lower() for encoder_type in text_encoder_types): + if 'text' in inputs and isinstance(inputs['text'], str): + return inputs['text'] + + # Final fallback: collect all text fields and return the first non-empty one + text_fields = [] + for node in nodes_by_id.values(): + if isinstance(node, dict): + inputs = node.get("inputs", {}) + text_val = inputs.get("text") + if isinstance(text_val, str) and text_val.strip(): + text_fields.append(text_val) + + # Return the first text field (likely positive prompt) + if text_fields: + return text_fields[0] + + return None # Logging API endpoints async def get_logs(self, request): From 92e904767d3376c24ac6b92ee03f85835998a3ce Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Wed, 28 May 2025 11:01:35 -0700 Subject: [PATCH 3/3] feat(database): add pagination to get_recent_prompts --- database/operations.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/database/operations.py b/database/operations.py index 6c163ca..122d23f 100644 --- a/database/operations.py +++ b/database/operations.py @@ -201,22 +201,39 @@ class PromptDatabase: rows = cursor.fetchall() return [self._row_to_dict(row) for row in rows] - def get_recent_prompts(self, limit: int = 10) -> List[Dict[str, Any]]: + def get_recent_prompts(self, limit: int = 10, offset: int = 0) -> Dict[str, Any]: """ - Get the most recent prompts. + Get the most recent prompts with pagination support. Args: limit: Maximum number of prompts to return + offset: Number of prompts to skip (for pagination) Returns: - List of dictionaries containing prompt data + Dictionary containing prompt data and pagination info """ with self.model.get_connection() as conn: + # Get total count + cursor = conn.execute("SELECT COUNT(*) as total FROM prompts") + total_count = cursor.fetchone()["total"] + + # Get paginated results cursor = conn.execute( - "SELECT * FROM prompts ORDER BY created_at DESC LIMIT ?", (limit,) + "SELECT * FROM prompts ORDER BY created_at DESC LIMIT ? OFFSET ?", + (limit, offset) ) rows = cursor.fetchall() - return [self._row_to_dict(row) for row in rows] + prompts = [self._row_to_dict(row) for row in rows] + + return { + 'prompts': prompts, + 'total': total_count, + 'limit': limit, + 'offset': offset, + 'has_more': (offset + limit) < total_count, + 'page': (offset // limit) + 1, + 'total_pages': (total_count + limit - 1) // limit # Ceiling division + } def get_prompts_by_category(self, category: str, limit: int = 100) -> List[Dict[str, Any]]: """