Merge pull request #17 from ComfyAssets/scanner-pageination
Scanner pageination
This commit is contained in:
+22
-5
@@ -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]]:
|
||||
"""
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user