Files
ComfyAssets-ComfyUI_PromptM…/web/metadata.html
ghosttigerllc-bitandZayne 374ce6e7a8 Fix Prompt Manager UI routing behind a ComfyUI proxy (#164)
* Fix gallery folder filters behind SwarmUI proxy

* Route Prompt Manager UI requests through proxy mount

* Use Comfy frontend base for Prompt Manager node requests

---------

Thanks @ghosttigerllc-bit

Co-authored-by: Zayne <noreply@gtb.ai>
2026-10-14 03:18:29 -07:00

431 lines
22 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Image Gallery - Generation Data</title>
<script src="js/theme-bridge.js"></script>
<script src="js/html-escape.js"></script>
<script src="js/png-metadata.js"></script>
<script src="js/comfy-metadata.js"></script>
<link rel="stylesheet" href="lib/tailwind/comfyui-theme.css">
<link rel="stylesheet" href="lib/tailwind/styles.css">
<style>
/* Ensure the layout takes full height and prevents overflow */
html, body {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
/* Ensure image container uses flexbox properly */
#image-area {
min-height: 0; /* Allow flex child to shrink */
}
/* Make sure the image scales properly */
#main-image {
max-width: calc(100% - 2rem); /* Account for padding */
max-height: calc(100% - 2rem); /* Account for padding */
}
</style>
<script>
let currentWorkflowData = null;
function openInFileExplorer(path) {
// A browser page cannot open a file manager, so the path is copied instead.
// (The former Electron-only branch built a shell command from the file name.)
navigator.clipboard.writeText(path).then(() => {
alert('File path copied to clipboard: ' + path);
}).catch(() => {
alert('File path: ' + path);
});
}
async function copyToClipboard(text, button) {
try {
await navigator.clipboard.writeText(text);
// Show temporary feedback
const originalContent = button.innerHTML;
button.innerHTML = '✓ Copied!';
button.classList.add('text-pm-success');
setTimeout(() => {
button.innerHTML = originalContent;
button.classList.remove('text-pm-success');
}, 1000);
} catch (err) {
alert('Copy failed: ' + err);
}
}
function copyPositivePrompt(event) {
if (currentWorkflowData && currentWorkflowData.positivePrompt) {
copyToClipboard(currentWorkflowData.positivePrompt, event.target.closest('button'));
}
}
function copyNegativePrompt(event) {
if (currentWorkflowData && currentWorkflowData.negativePrompt) {
copyToClipboard(currentWorkflowData.negativePrompt, event.target.closest('button'));
}
}
function downloadWorkflowJSON() {
if (currentWorkflowData && currentWorkflowData.workflow) {
const dataStr = JSON.stringify(currentWorkflowData.workflow, null, 2);
const dataBlob = new Blob([dataStr], {type: 'application/json'});
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = 'comfyui_workflow.json';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
}
async function copyAllData() {
if (!currentWorkflowData) return;
const allData = `Checkpoint: ${currentWorkflowData.checkpoint || 'Unknown'}
Positive Prompt: ${currentWorkflowData.positivePrompt}
Negative Prompt: ${currentWorkflowData.negativePrompt}
Steps: ${currentWorkflowData.steps || 'Unknown'}
CFG Scale: ${currentWorkflowData.cfgScale || 'Unknown'}
Sampler: ${currentWorkflowData.sampler || 'Unknown'}
Seed: ${currentWorkflowData.seed || 'Unknown'}`;
await copyToClipboard(allData);
}
function parsePNGMetadata(arrayBuffer) {
return window.PngMetadata.parsePngTextChunks(arrayBuffer);
}
function extractComfyUIData(metadata) {
// Look for ComfyUI workflow data in various possible fields
let workflowData = null;
let promptData = null;
// Common ComfyUI metadata field names
const workflowFields = ['workflow', 'Workflow', 'comfy', 'ComfyUI'];
const promptFields = ['prompt', 'Prompt', 'parameters', 'Parameters'];
for (const field of workflowFields) {
if (metadata[field]) {
try {
// Clean NaN values from JSON string before parsing
let cleanedJson = metadata[field];
cleanedJson = cleanedJson.replace(/:\s*NaN\b/g, ': null');
cleanedJson = cleanedJson.replace(/\bNaN\b/g, 'null');
workflowData = JSON.parse(cleanedJson);
break;
} catch (e) {
console.warn('Failed to parse workflow field:', field, e);
}
}
}
for (const field of promptFields) {
if (metadata[field]) {
try {
// Clean NaN values from JSON string before parsing
let cleanedJson = metadata[field];
cleanedJson = cleanedJson.replace(/:\s*NaN\b/g, ': null');
cleanedJson = cleanedJson.replace(/\bNaN\b/g, 'null');
promptData = JSON.parse(cleanedJson);
break;
} catch (e) {
console.warn('Failed to parse prompt field:', field, e);
}
}
}
return { workflow: workflowData, prompt: promptData };
}
function updateDataPanel(comfyData, filename) {
const dataPanel = document.getElementById('data-panel-content');
if (!dataPanel) return;
// Trace prompts, model and sampler settings through the node graph (#75)
const {
checkpoint, positivePrompt, negativePrompt, steps, cfgScale, sampler, seed,
} = window.ComfyMetadata.extractGenerationParams(comfyData);
// Update the HTML
dataPanel.innerHTML = `
<!-- File Path -->
<div>
<h2 class="text-sm font-medium text-pm-secondary mb-2">File Path</h2>
<div class="text-sm text-pm-accent hover:bg-pm-hover cursor-pointer bg-pm-surface p-2 rounded-pm-sm break-all" data-action="open-file" data-path="${escapeHtml(filename)}">
${escapeHtml(filename)}
</div>
</div>
<!-- Resources used -->
<div>
<h2 class="text-sm font-medium text-pm-secondary mb-2">Resources used</h2>
<div class="flex items-center justify-between">
<div>
<div class="text-pm-accent cursor-pointer">${escapeHtml(checkpoint)}</div>
<div class="text-xs text-pm-muted">ComfyUI Generated</div>
</div>
<span class="px-2 py-1 text-xs bg-pm-surface text-pm-secondary rounded-pm-sm">CHECKPOINT</span>
</div>
</div>
<!-- Prompt -->
<div>
<div class="flex items-center gap-2 mb-2">
<h2 class="text-sm font-medium text-pm-secondary">Prompt</h2>
<span class="px-2 py-1 text-xs bg-pm-warning text-pm rounded-pm-sm">COMFYUI</span>
<button class="ml-auto text-pm-secondary hover:text-pm" onclick="copyPositivePrompt(event)">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
</svg>
</button>
</div>
<div class="text-sm text-pm-secondary bg-pm-surface p-3 rounded-pm-sm max-h-32 overflow-y-auto">
${escapeHtml(positivePrompt.substring(0, 200))}${positivePrompt.length > 200 ? '...' : ''}
</div>
${positivePrompt.length > 200 ? '<button class="text-pm-accent hover:bg-pm-hover text-sm mt-1" onclick="showFullPrompt(\'positive\')">Show more</button>' : ''}
</div>
<!-- Negative prompt -->
<div>
<div class="flex items-center justify-between mb-2">
<h2 class="text-sm font-medium text-pm-secondary">Negative prompt</h2>
<button class="text-pm-secondary hover:text-pm" onclick="copyNegativePrompt(event)">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
</svg>
</button>
</div>
<div class="text-sm text-pm-secondary bg-pm-surface p-3 rounded-pm-sm max-h-32 overflow-y-auto">
${escapeHtml(negativePrompt.substring(0, 200))}${negativePrompt.length > 200 ? '...' : ''}
</div>
${negativePrompt.length > 200 ? '<button class="text-pm-accent hover:bg-pm-hover text-sm mt-1" onclick="showFullPrompt(\'negative\')">Show more</button>' : ''}
</div>
<!-- Other metadata -->
<div>
<h2 class="text-sm font-medium text-pm-secondary mb-3">Other metadata</h2>
<div class="flex flex-wrap gap-2">
<span class="px-2 py-1 text-xs bg-pm-surface text-pm-secondary rounded-pm-sm">CFG SCALE: ${escapeHtml(cfgScale)}</span>
<span class="px-2 py-1 text-xs bg-pm-surface text-pm-secondary rounded-pm-sm">STEPS: ${escapeHtml(steps)}</span>
<span class="px-2 py-1 text-xs bg-pm-surface text-pm-secondary rounded-pm-sm">SAMPLER: ${escapeHtml(sampler)}</span>
</div>
<div class="mt-2">
<span class="px-2 py-1 text-xs bg-pm-surface text-pm-secondary rounded-pm-sm">SEED: ${escapeHtml(seed)}</span>
</div>
</div>
<!-- Raw Workflow Data -->
<div>
<h2 class="text-sm font-medium text-pm-secondary mb-2">ComfyUI Workflow</h2>
<div class="flex items-center gap-2">
<button class="text-pm-accent hover:bg-pm-hover text-sm" onclick="showWorkflowData()">View Raw Workflow JSON</button>
<button class="text-pm-accent hover:bg-pm-hover" onclick="downloadWorkflowJSON()" title="Download JSON">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
</button>
</div>
</div>
`;
dataPanel.querySelector('[data-action="open-file"]').addEventListener('click', (e) => {
openInFileExplorer(e.currentTarget.dataset.path);
});
// Store the full data for later use
currentWorkflowData = {
positivePrompt,
negativePrompt,
checkpoint,
steps,
cfgScale,
sampler,
seed,
workflow: comfyData.workflow,
prompt: comfyData.prompt
};
}
function showFullPrompt(type) {
if (currentWorkflowData) {
const prompt = type === 'positive' ? currentWorkflowData.positivePrompt : currentWorkflowData.negativePrompt;
const safeType = escapeHtml(type.charAt(0).toUpperCase() + type.slice(1));
const newWindow = window.open('', '_blank');
const doc = newWindow.document;
doc.open();
doc.write('<!DOCTYPE html><html><head><title>' + safeType + ' Prompt</title></head><body></body></html>');
doc.close();
doc.body.style.cssText = 'background:#111;color:#fff;font-family:monospace;padding:20px;';
const h2 = doc.createElement('h2');
h2.textContent = type.charAt(0).toUpperCase() + type.slice(1) + ' Prompt';
doc.body.appendChild(h2);
const pre = doc.createElement('pre');
pre.style.cssText = 'background:#222;padding:15px;border-radius:5px;white-space:pre-wrap;line-height:1.5;';
pre.textContent = prompt;
doc.body.appendChild(pre);
const btn = doc.createElement('button');
btn.textContent = 'Copy to Clipboard';
btn.style.cssText = 'margin-top:20px;padding:10px 20px;background:#444;color:#fff;border:none;border-radius:5px;cursor:pointer;';
btn.addEventListener('click', function() { navigator.clipboard.writeText(pre.textContent).then(function() { alert('Copied!'); }); });
doc.body.appendChild(btn);
}
}
function showWorkflowData() {
if (currentWorkflowData && currentWorkflowData.workflow) {
const newWindow = window.open('', '_blank');
newWindow.document.write(`
<html>
<head><title>ComfyUI Workflow Data</title></head>
<body style="background: #111; color: #fff; font-family: monospace; padding: 20px;">
<h2>ComfyUI Workflow JSON</h2>
<pre style="background: #222; padding: 15px; border-radius: 5px; overflow: auto;">${escapeHtml(JSON.stringify(currentWorkflowData.workflow, null, 2))}</pre>
</body>
</html>
`);
}
}
async function handleImageUpload(event) {
const file = event.target.files[0];
if (!file) return;
try {
const arrayBuffer = await file.arrayBuffer();
const metadata = await parsePNGMetadata(arrayBuffer);
const comfyData = extractComfyUIData(metadata);
// Update the image display
const img = document.getElementById('main-image');
const url = URL.createObjectURL(file);
img.src = url;
// Update data panel
updateDataPanel(comfyData, file.name);
} catch (error) {
console.error('Error processing image:', error);
alert('Error processing image: ' + error.message);
}
}
// Initialize drag and drop
function initializeDragDrop() {
const imageArea = document.getElementById('image-area');
imageArea.addEventListener('dragover', (e) => {
e.preventDefault();
imageArea.classList.add('bg-pm-surface');
});
imageArea.addEventListener('dragleave', (e) => {
e.preventDefault();
imageArea.classList.remove('bg-pm-surface');
});
imageArea.addEventListener('drop', (e) => {
e.preventDefault();
imageArea.classList.remove('bg-pm-surface');
const files = e.dataTransfer.files;
if (files.length > 0) {
const file = files[0];
if (file.type === 'image/png') {
const event = { target: { files: [file] } };
handleImageUpload(event);
} else {
alert('Please drop a PNG file');
}
}
});
}
// Initialize when page loads
window.addEventListener('load', initializeDragDrop);
// Handle window resize to ensure image stays properly sized
window.addEventListener('resize', () => {
// The CSS will handle the responsive sizing automatically
// but we can trigger any additional adjustments if needed
});
</script>
</head>
<body class="bg-pm-primary text-pm font-sans overflow-hidden">
<div class="h-screen flex">
<!-- Main Image Area -->
<div id="image-area" class="flex-1 flex items-center justify-center p-3 bg-pm-primary relative border-2 border-dashed border-transparent transition-colors min-w-0">
<!-- Upload prompt overlay -->
<div class="absolute inset-0 flex flex-col items-center justify-center text-pm-muted pointer-events-none">
<svg class="w-16 h-16 mb-4 opacity-30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>
</svg>
<p class="text-sm mb-2">Drag & Drop PNG with ComfyUI Workflow</p>
<p class="text-sm">Or click to browse files</p>
</div>
<!-- File input -->
<input type="file" id="file-input" accept=".png" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer" onchange="handleImageUpload(event)">
<!-- Navigation arrows -->
<button class="absolute left-4 top-1/2 transform -translate-y-1/2 w-10 h-10 bg-pm-surface/50 hover:bg-pm-hover rounded-full flex items-center justify-center text-pm transition-all z-10">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
</button>
<!-- Main Image -->
<div class="w-full h-full flex items-center justify-center z-10 pointer-events-none">
<img id="main-image"
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='800' height='1200' viewBox='0 0 800 1200'%3E%3Crect width='800' height='1200' fill='%23374151'/%3E%3Ctext x='400' y='600' font-family='Arial' font-size='24' fill='%23D1D5DB' text-anchor='middle'%3EDrop PNG Here%3C/text%3E%3Ctext x='400' y='640' font-family='Arial' font-size='16' fill='%239CA3AF' text-anchor='middle'%3EComfyUI Workflow%3C/text%3E%3C/svg%3E"
alt="Generated Image"
class="max-w-full max-h-full object-contain rounded-pm-md shadow-pm">
</div>
<button class="absolute right-4 top-1/2 transform -translate-y-1/2 w-10 h-10 bg-pm-surface/50 hover:bg-pm-hover rounded-full flex items-center justify-center text-pm transition-all z-10">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</button>
</div>
<!-- Generation Data Panel -->
<div class="w-96 bg-pm-primary border-l border-pm flex flex-col">
<!-- Header -->
<div class="flex items-center justify-between p-3 border-b border-pm">
<div class="flex items-center gap-2">
<svg class="w-5 h-5 text-pm-secondary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<h1 class="text-sm font-semibold text-pm">Generation data</h1>
</div>
<button class="text-xs text-pm-accent hover:bg-pm-hover transition-colors" onclick="copyAllData()">
📋 COPY ALL
</button>
</div>
<!-- Scrollable Content -->
<div id="data-panel-content" class="flex-1 overflow-y-auto p-3 space-y-3">
<!-- Initial placeholder content -->
<div class="text-center text-pm-muted py-8">
<svg class="w-16 h-16 mx-auto mb-4 opacity-30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<p class="text-sm mb-2">No Image Loaded</p>
<p class="text-sm">Upload a PNG with embedded ComfyUI workflow to view generation data</p>
</div>
</div>
</div>
</div>
</body>
</html>