Merge pull request #17 from ComfyAssets/scanner-pageination

Scanner pageination
This commit is contained in:
Vito
2025-05-28 11:02:44 -07:00
committed by GitHub
3 changed files with 291 additions and 51 deletions
+22 -5
View File
@@ -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]]:
"""
+164 -43
View File
@@ -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):
+105 -3
View File
@@ -207,7 +207,20 @@
<div class="bg-gradient-to-br from-gray-800 to-gray-850 rounded-xl border border-gray-700 h-full flex flex-col">
<!-- Results Header -->
<div class="flex items-center justify-between p-6 border-b border-gray-700">
<h2 class="text-xl font-semibold text-gray-100" id="resultsTitle">Recent Prompts</h2>
<div class="flex items-center space-x-4">
<h2 class="text-xl font-semibold text-gray-100" id="resultsTitle">Recent Prompts</h2>
<div class="flex items-center space-x-2">
<label class="text-sm text-gray-400">Show:</label>
<select id="limitSelector" class="px-3 py-1 bg-gray-700 border border-gray-600 rounded text-gray-100 text-sm">
<option value="25">25</option>
<option value="50" selected>50</option>
<option value="100">100</option>
<option value="200">200</option>
<option value="500">500</option>
</select>
<span class="text-sm text-gray-400">per page</span>
</div>
</div>
<span class="text-sm font-medium text-gray-400 bg-gray-700 px-3 py-1 rounded-full" id="resultsCount">Loading...</span>
</div>
@@ -223,6 +236,32 @@
<div id="resultsList" class="flex-1 overflow-y-auto custom-scrollbar p-6 space-y-4 hidden">
<!-- Prompts will be inserted here -->
</div>
<!-- Pagination Controls -->
<div id="paginationControls" class="border-t border-gray-700 p-4 hidden">
<div class="flex items-center justify-between">
<div class="text-sm text-gray-400">
Showing <span id="showingStart">1</span> to <span id="showingEnd">50</span> of <span id="totalResults">0</span> prompts
</div>
<div class="flex items-center space-x-2">
<button id="firstPageBtn" class="px-3 py-1 bg-gray-700 hover:bg-gray-600 text-gray-300 rounded disabled:opacity-50 disabled:cursor-not-allowed text-sm">
First
</button>
<button id="prevPageBtn" class="px-3 py-1 bg-gray-700 hover:bg-gray-600 text-gray-300 rounded disabled:opacity-50 disabled:cursor-not-allowed text-sm">
Previous
</button>
<span class="text-sm text-gray-400">
Page <span id="currentPage">1</span> of <span id="totalPages">1</span>
</span>
<button id="nextPageBtn" class="px-3 py-1 bg-gray-700 hover:bg-gray-600 text-gray-300 rounded disabled:opacity-50 disabled:cursor-not-allowed text-sm">
Next
</button>
<button id="lastPageBtn" class="px-3 py-1 bg-gray-700 hover:bg-gray-600 text-gray-300 rounded disabled:opacity-50 disabled:cursor-not-allowed text-sm">
Last
</button>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -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