Migrate all remaining hardcoded Tailwind color classes to --pm-* design tokens across gallery.html, metadata.html, admin.js, gallery.js, and tags-page.js. Zero hardcoded color classes remain in HTML/JS files. - gallery.html: remove inline styles (now in comfyui-theme.css), strip gradients/decorative elements, tighten spacing for ComfyUI density - metadata.html: add theme imports, replace all color classes - admin.js: replace 164 color occurrences in template literals, convert raw hex values to CSS variable references - gallery.js: replace 161 color occurrences in template literals - tags-page.js: replace 30 color occurrences in template literals
662 lines
33 KiB
HTML
662 lines
33 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="/prompt_manager/js/theme-bridge.js"></script>
|
|
<link rel="stylesheet" href="css/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) {
|
|
// For Windows file explorer
|
|
if (navigator.platform.indexOf('Win') > -1) {
|
|
// This will work in Electron apps or desktop environments
|
|
try {
|
|
window.require('child_process').exec(`explorer /select,"${path}"`);
|
|
} catch (e) {
|
|
// Fallback: copy path to clipboard and show alert
|
|
navigator.clipboard.writeText(path).then(() => {
|
|
alert('File path copied to clipboard: ' + path);
|
|
}).catch(() => {
|
|
alert('File path: ' + path);
|
|
});
|
|
}
|
|
} else {
|
|
// For other platforms, just copy to clipboard
|
|
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);
|
|
}
|
|
|
|
async function parsePNGMetadata(arrayBuffer) {
|
|
const dataView = new DataView(arrayBuffer);
|
|
let offset = 8; // Skip PNG signature
|
|
const metadata = {};
|
|
|
|
while (offset < arrayBuffer.byteLength - 8) {
|
|
const length = dataView.getUint32(offset);
|
|
const type = new TextDecoder().decode(arrayBuffer.slice(offset + 4, offset + 8));
|
|
|
|
if (type === 'tEXt' || type === 'iTXt' || type === 'zTXt') {
|
|
const chunkData = arrayBuffer.slice(offset + 8, offset + 8 + length);
|
|
let text;
|
|
|
|
if (type === 'tEXt') {
|
|
text = new TextDecoder().decode(chunkData);
|
|
} else if (type === 'iTXt') {
|
|
// iTXt format: keyword\0compression\0language\0translated_keyword\0text
|
|
const textData = new TextDecoder().decode(chunkData);
|
|
const parts = textData.split('\0');
|
|
if (parts.length >= 5) {
|
|
metadata[parts[0]] = parts[4];
|
|
}
|
|
text = textData;
|
|
} else if (type === 'zTXt') {
|
|
// zTXt is compressed - basic parsing (might need proper decompression)
|
|
text = new TextDecoder().decode(chunkData);
|
|
}
|
|
|
|
// Parse the text chunk for key-value pairs
|
|
const nullIndex = text.indexOf('\0');
|
|
if (nullIndex !== -1) {
|
|
const key = text.substring(0, nullIndex);
|
|
const value = text.substring(nullIndex + 1);
|
|
metadata[key] = value;
|
|
}
|
|
}
|
|
|
|
offset += 8 + length + 4; // Move to next chunk (8 = length + type, 4 = CRC)
|
|
}
|
|
|
|
return metadata;
|
|
}
|
|
|
|
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.log('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.log('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;
|
|
|
|
// Extract information from ComfyUI workflow
|
|
let checkpoint = 'Unknown';
|
|
let positivePrompt = 'No prompt found';
|
|
let negativePrompt = 'No negative prompt found';
|
|
let steps = 'Unknown';
|
|
let cfgScale = 'Unknown';
|
|
let sampler = 'Unknown';
|
|
let seed = 'Unknown';
|
|
|
|
// Parse prompt data first (more reliable for actual generation parameters)
|
|
if (comfyData.prompt) {
|
|
console.log('Parsing prompt data...');
|
|
const promptNodes = comfyData.prompt;
|
|
|
|
// Track all text nodes and try to identify positive/negative
|
|
const textNodes = [];
|
|
|
|
for (const nodeId in promptNodes) {
|
|
const node = promptNodes[nodeId];
|
|
console.log(`Node ${nodeId}:`, node.class_type, node);
|
|
|
|
// Checkpoint - check multiple types
|
|
if ((node.class_type === 'CheckpointLoaderSimple' ||
|
|
node.class_type === 'UNETLoader' ||
|
|
node.class_type === 'DualCLIPLoader') && node.inputs) {
|
|
checkpoint = node.inputs.ckpt_name || node.inputs.unet_name || checkpoint;
|
|
}
|
|
|
|
// Collect all text nodes for analysis
|
|
if (node.inputs && node.inputs.text) {
|
|
textNodes.push({
|
|
nodeId: nodeId,
|
|
classType: node.class_type,
|
|
text: node.inputs.text,
|
|
inputs: node.inputs
|
|
});
|
|
}
|
|
|
|
// Special handling for PromptManager nodes
|
|
if (node.class_type === 'PromptManager' && node.inputs) {
|
|
// PromptManager always contains the positive prompt
|
|
if (node.inputs.text) {
|
|
positivePrompt = node.inputs.text;
|
|
console.log('Found PromptManager with text:', positivePrompt.substring(0, 100));
|
|
}
|
|
// Also check for loaded prompt
|
|
if (node.inputs.selected_prompt) {
|
|
positivePrompt = node.inputs.selected_prompt;
|
|
}
|
|
}
|
|
|
|
// PromptManagerText node
|
|
if (node.class_type === 'PromptManagerText' && node.inputs) {
|
|
if (node.inputs.text) {
|
|
positivePrompt = node.inputs.text;
|
|
}
|
|
if (node.inputs.selected_prompt) {
|
|
positivePrompt = node.inputs.selected_prompt;
|
|
}
|
|
}
|
|
|
|
// Sampling parameters - check multiple sampler types
|
|
if ((node.class_type === 'KSampler' ||
|
|
node.class_type === 'KSamplerAdvanced' ||
|
|
node.class_type === 'SamplerCustom' ||
|
|
node.class_type === 'SamplerCustomAdvanced') && node.inputs) {
|
|
steps = node.inputs.steps || steps;
|
|
cfgScale = node.inputs.cfg || cfgScale;
|
|
sampler = node.inputs.sampler_name || sampler;
|
|
seed = node.inputs.seed || node.inputs.noise_seed || seed;
|
|
}
|
|
|
|
// New ComfyUI node types for modern workflows
|
|
if (node.class_type === 'CFGGuider' && node.inputs && node.inputs.cfg) {
|
|
cfgScale = node.inputs.cfg;
|
|
}
|
|
|
|
if (node.class_type === 'BasicScheduler' && node.inputs && node.inputs.steps) {
|
|
steps = node.inputs.steps;
|
|
}
|
|
|
|
if (node.class_type === 'KSamplerSelect' && node.inputs && node.inputs.sampler_name) {
|
|
sampler = node.inputs.sampler_name;
|
|
}
|
|
|
|
if ((node.class_type === 'RandomNoise' || node.class_type === 'SeedHistory') && node.inputs) {
|
|
if (node.inputs.noise_seed) {
|
|
// RandomNoise nodes might reference other nodes, resolve if needed
|
|
seed = Array.isArray(node.inputs.noise_seed) ? node.inputs.noise_seed[0] : node.inputs.noise_seed;
|
|
} else if (node.inputs.seed) {
|
|
seed = node.inputs.seed;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process collected text nodes to identify positive/negative prompts
|
|
// if we haven't found them from PromptManager
|
|
for (const textNode of textNodes) {
|
|
const text = textNode.text;
|
|
// Ensure text is a string before calling toLowerCase
|
|
if (!text || typeof text !== 'string') {
|
|
continue;
|
|
}
|
|
const textLower = text.toLowerCase();
|
|
|
|
// Skip if this is the PromptManager text we already have
|
|
if (textNode.classType === 'PromptManager' || textNode.classType === 'PromptManagerText') {
|
|
continue;
|
|
}
|
|
|
|
// Identify negative prompts by common patterns
|
|
if (textNode.classType === 'CLIPTextEncode') {
|
|
if (textLower.includes('bad anatomy') || textLower.includes('unfinished') ||
|
|
textLower.includes('censored') || textLower.includes('weird anatomy') ||
|
|
textLower.includes('negative') || textLower.includes('embedding:') ||
|
|
textLower.includes('worst quality') || textLower.includes('low quality')) {
|
|
negativePrompt = text;
|
|
console.log('Found negative prompt:', negativePrompt.substring(0, 100));
|
|
} else if (positivePrompt === 'No prompt found') {
|
|
// If we haven't found a positive prompt yet, this might be it
|
|
positivePrompt = text;
|
|
console.log('Found potential positive prompt:', positivePrompt.substring(0, 100));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Also try to parse from workflow data if we didn't find everything in prompt
|
|
if (comfyData.workflow && comfyData.workflow.nodes) {
|
|
for (const node of comfyData.workflow.nodes) {
|
|
// Checkpoint loaders
|
|
if ((node.type === 'CheckpointLoaderSimple' ||
|
|
node.type === 'UNETLoader' ||
|
|
node.type === 'DualCLIPLoader') && node.widgets_values) {
|
|
checkpoint = node.widgets_values[0] || checkpoint;
|
|
}
|
|
|
|
// PromptManager nodes in workflow
|
|
if (node.type === 'PromptManager' && node.widgets_values) {
|
|
// The first widget value is usually the text
|
|
if (node.widgets_values[0] && positivePrompt === 'No prompt found') {
|
|
positivePrompt = node.widgets_values[0];
|
|
}
|
|
}
|
|
|
|
if (node.type === 'PromptManagerText' && node.widgets_values) {
|
|
if (node.widgets_values[0] && positivePrompt === 'No prompt found') {
|
|
positivePrompt = node.widgets_values[0];
|
|
}
|
|
}
|
|
|
|
// CLIPTextEncode nodes
|
|
if (node.type === 'CLIPTextEncode' && node.widgets_values && node.widgets_values[0]) {
|
|
const text = node.widgets_values[0];
|
|
// Ensure text is a string before calling toLowerCase
|
|
if (text && typeof text === 'string') {
|
|
const textLower = text.toLowerCase();
|
|
|
|
if (textLower.includes('bad anatomy') || textLower.includes('unfinished') ||
|
|
textLower.includes('censored') || textLower.includes('negative') ||
|
|
textLower.includes('worst quality') || textLower.includes('low quality')) {
|
|
negativePrompt = text;
|
|
} else if (positivePrompt === 'No prompt found') {
|
|
positivePrompt = text;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Samplers
|
|
if ((node.type === 'KSampler' || node.type === 'KSamplerAdvanced' ||
|
|
node.type === 'SamplerCustom' || node.type === 'SamplerCustomAdvanced') && node.widgets_values) {
|
|
if (node.widgets_values.length >= 4) {
|
|
seed = node.widgets_values[0] || seed;
|
|
steps = node.widgets_values[1] || steps;
|
|
cfgScale = node.widgets_values[2] || cfgScale;
|
|
sampler = node.widgets_values[3] || sampler;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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" onclick="openInFileExplorer('${filename}')">
|
|
${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">${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">
|
|
${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">
|
|
${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: ${cfgScale}</span>
|
|
<span class="px-2 py-1 text-xs bg-pm-surface text-pm-secondary rounded-pm-sm">STEPS: ${steps}</span>
|
|
<span class="px-2 py-1 text-xs bg-pm-surface text-pm-secondary rounded-pm-sm">SAMPLER: ${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: ${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>
|
|
`;
|
|
|
|
// Store the full data for later use
|
|
currentWorkflowData = {
|
|
positivePrompt,
|
|
negativePrompt,
|
|
checkpoint,
|
|
steps,
|
|
cfgScale,
|
|
sampler,
|
|
seed,
|
|
workflow: comfyData.workflow,
|
|
prompt: comfyData.prompt
|
|
};
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
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;">${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);
|
|
|
|
console.log('Extracted metadata:', metadata);
|
|
console.log('ComfyUI data:', comfyData);
|
|
|
|
} 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-lg 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-lg font-medium 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-lg 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> |