- Add support for new ComfyUI node types in gallery metadata extraction: - CFGGuider for CFG scale values - BasicScheduler for steps parameter - KSamplerSelect for sampler selection - RandomNoise/SeedHistory for seed values - Update both main gallery (/gallery) and metadata viewer (/metadata.html) - Fix metadata display issues where technical parameters showed as "Unknown" - Add comprehensive test image and debug tools for validation - Maintain backward compatibility with legacy KSampler nodes This resolves metadata parsing issues with modern ComfyUI workflows that use the newer node architecture for sampling and generation.
614 lines
31 KiB
HTML
614 lines
31 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="https://cdn.tailwindcss.com"></script>
|
|
<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-green-400');
|
|
setTimeout(() => {
|
|
button.innerHTML = originalContent;
|
|
button.classList.remove('text-green-400');
|
|
}, 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 {
|
|
workflowData = JSON.parse(metadata[field]);
|
|
break;
|
|
} catch (e) {
|
|
console.log('Failed to parse workflow field:', field);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const field of promptFields) {
|
|
if (metadata[field]) {
|
|
try {
|
|
promptData = JSON.parse(metadata[field]);
|
|
break;
|
|
} catch (e) {
|
|
console.log('Failed to parse prompt field:', field);
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
for (const nodeId in promptNodes) {
|
|
const node = promptNodes[nodeId];
|
|
console.log(`Node ${nodeId}:`, node.class_type, node);
|
|
|
|
// Checkpoint
|
|
if (node.class_type === 'CheckpointLoaderSimple' && node.inputs) {
|
|
checkpoint = node.inputs.ckpt_name || checkpoint;
|
|
}
|
|
|
|
// Prompts - need to identify which is positive vs negative
|
|
if (node.class_type === 'PromptManager' && node.inputs && node.inputs.text) {
|
|
// PromptManager typically contains the positive prompt
|
|
positivePrompt = node.inputs.text;
|
|
console.log('Found PromptManager with text:', positivePrompt.substring(0, 100));
|
|
}
|
|
|
|
if (node.class_type === 'CLIPTextEncode' && node.inputs && node.inputs.text) {
|
|
// Check if this looks like a negative prompt
|
|
const text = node.inputs.text.toLowerCase();
|
|
if (text.includes('bad anatomy') || text.includes('unfinished') ||
|
|
text.includes('censored') || text.includes('weird anatomy') ||
|
|
text.includes('negative') || text.includes('embedding:')) {
|
|
negativePrompt = node.inputs.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 = node.inputs.text;
|
|
console.log('Found potential positive prompt:', positivePrompt.substring(0, 100));
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
seed = node.inputs.seed || seed;
|
|
steps = node.inputs.steps || steps;
|
|
cfgScale = node.inputs.cfg || cfgScale;
|
|
sampler = node.inputs.sampler_name || sampler;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback to workflow parsing if prompt parsing didn't work well
|
|
if (comfyData.workflow && (positivePrompt === 'No prompt found' || checkpoint === 'Unknown')) {
|
|
console.log('Falling back to workflow parsing...');
|
|
const nodes = comfyData.workflow.nodes || [];
|
|
|
|
// Look for checkpoint loader
|
|
const checkpointNode = nodes.find(node =>
|
|
node.type === 'CheckpointLoaderSimple' ||
|
|
node.type === 'CheckpointLoader'
|
|
);
|
|
if (checkpointNode && checkpointNode.widgets_values) {
|
|
checkpoint = checkpointNode.widgets_values[0] || checkpoint;
|
|
}
|
|
|
|
// Look for prompts in workflow nodes
|
|
const textEncodeNodes = nodes.filter(node => node.type === 'CLIPTextEncode');
|
|
const promptManagerNodes = nodes.filter(node => node.type === 'PromptManager');
|
|
|
|
// Check PromptManager first for positive prompt
|
|
if (promptManagerNodes.length > 0 && promptManagerNodes[0].widgets_values) {
|
|
positivePrompt = promptManagerNodes[0].widgets_values[0] || positivePrompt;
|
|
}
|
|
|
|
// For negative prompt, look for CLIPTextEncode that isn't the positive
|
|
for (const node of textEncodeNodes) {
|
|
if (node.widgets_values && node.widgets_values[0]) {
|
|
const text = node.widgets_values[0].toLowerCase();
|
|
if (text.includes('bad anatomy') || text.includes('unfinished') ||
|
|
text.includes('censored') || text.includes('weird anatomy')) {
|
|
negativePrompt = node.widgets_values[0];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Look for sampler nodes - check multiple types
|
|
const samplerNode = nodes.find(node =>
|
|
node.type === 'KSampler' ||
|
|
node.type === 'KSamplerAdvanced' ||
|
|
node.type === 'SamplerCustom' ||
|
|
node.type === 'SamplerCustomAdvanced'
|
|
);
|
|
if (samplerNode && samplerNode.widgets_values) {
|
|
seed = samplerNode.widgets_values[0] || seed;
|
|
steps = samplerNode.widgets_values[1] || steps;
|
|
cfgScale = samplerNode.widgets_values[2] || cfgScale;
|
|
sampler = samplerNode.widgets_values[3] || sampler;
|
|
}
|
|
|
|
// Look for newer node types
|
|
if (steps === 'Unknown') {
|
|
const schedulerNode = nodes.find(node => node.type === 'BasicScheduler');
|
|
if (schedulerNode && schedulerNode.widgets_values) {
|
|
steps = schedulerNode.widgets_values[1]; // steps is usually second parameter
|
|
}
|
|
}
|
|
|
|
if (cfgScale === 'Unknown') {
|
|
const cfgNode = nodes.find(node => node.type === 'CFGGuider');
|
|
if (cfgNode && cfgNode.widgets_values) {
|
|
cfgScale = cfgNode.widgets_values[0]; // cfg is usually first parameter
|
|
}
|
|
}
|
|
|
|
if (sampler === 'Unknown') {
|
|
const samplerSelectNode = nodes.find(node => node.type === 'KSamplerSelect');
|
|
if (samplerSelectNode && samplerSelectNode.widgets_values) {
|
|
sampler = samplerSelectNode.widgets_values[0]; // sampler name is usually first parameter
|
|
}
|
|
}
|
|
|
|
if (seed === 'Unknown') {
|
|
const noiseNode = nodes.find(node => node.type === 'RandomNoise');
|
|
if (noiseNode && noiseNode.widgets_values) {
|
|
seed = noiseNode.widgets_values[0]; // seed is usually first parameter
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update the HTML
|
|
dataPanel.innerHTML = `
|
|
<!-- File Path -->
|
|
<div>
|
|
<h2 class="text-sm font-medium text-gray-300 mb-2">File Path</h2>
|
|
<div class="text-sm text-blue-400 hover:text-blue-300 cursor-pointer bg-gray-800 p-2 rounded break-all" onclick="openInFileExplorer('${filename}')">
|
|
${filename}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Resources used -->
|
|
<div>
|
|
<h2 class="text-sm font-medium text-gray-300 mb-2">Resources used</h2>
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<div class="text-blue-400 hover:text-blue-300 cursor-pointer">${checkpoint}</div>
|
|
<div class="text-xs text-gray-500">ComfyUI Generated</div>
|
|
</div>
|
|
<span class="px-2 py-1 text-xs bg-gray-700 text-gray-300 rounded">CHECKPOINT</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Prompt -->
|
|
<div>
|
|
<div class="flex items-center gap-2 mb-2">
|
|
<h2 class="text-sm font-medium text-gray-300">Prompt</h2>
|
|
<span class="px-2 py-1 text-xs bg-orange-600 text-orange-100 rounded">COMFYUI</span>
|
|
<button class="ml-auto text-gray-400 hover:text-gray-300" 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-gray-300 bg-gray-800 p-3 rounded max-h-32 overflow-y-auto">
|
|
${positivePrompt.substring(0, 200)}${positivePrompt.length > 200 ? '...' : ''}
|
|
</div>
|
|
${positivePrompt.length > 200 ? '<button class="text-blue-400 hover:text-blue-300 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-gray-300">Negative prompt</h2>
|
|
<button class="text-gray-400 hover:text-gray-300" 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-gray-300 bg-gray-800 p-3 rounded max-h-32 overflow-y-auto">
|
|
${negativePrompt.substring(0, 200)}${negativePrompt.length > 200 ? '...' : ''}
|
|
</div>
|
|
${negativePrompt.length > 200 ? '<button class="text-blue-400 hover:text-blue-300 text-sm mt-1" onclick="showFullPrompt(\'negative\')">Show more</button>' : ''}
|
|
</div>
|
|
|
|
<!-- Other metadata -->
|
|
<div>
|
|
<h2 class="text-sm font-medium text-gray-300 mb-3">Other metadata</h2>
|
|
<div class="flex flex-wrap gap-2">
|
|
<span class="px-2 py-1 text-xs bg-gray-700 text-gray-300 rounded">CFG SCALE: ${cfgScale}</span>
|
|
<span class="px-2 py-1 text-xs bg-gray-700 text-gray-300 rounded">STEPS: ${steps}</span>
|
|
<span class="px-2 py-1 text-xs bg-gray-700 text-gray-300 rounded">SAMPLER: ${sampler}</span>
|
|
</div>
|
|
<div class="mt-2">
|
|
<span class="px-2 py-1 text-xs bg-gray-700 text-gray-300 rounded">SEED: ${seed}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Raw Workflow Data -->
|
|
<div>
|
|
<h2 class="text-sm font-medium text-gray-300 mb-2">ComfyUI Workflow</h2>
|
|
<div class="flex items-center gap-2">
|
|
<button class="text-blue-400 hover:text-blue-300 text-sm" onclick="showWorkflowData()">View Raw Workflow JSON</button>
|
|
<button class="text-blue-400 hover:text-blue-300" 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 showFullPrompt(type) {
|
|
if (currentWorkflowData) {
|
|
const prompt = type === 'positive' ? currentWorkflowData.positivePrompt : currentWorkflowData.negativePrompt;
|
|
const newWindow = window.open('', '_blank');
|
|
newWindow.document.write(`
|
|
<html>
|
|
<head><title>${type.charAt(0).toUpperCase() + type.slice(1)} Prompt</title></head>
|
|
<body style="background: #111; color: #fff; font-family: monospace; padding: 20px;">
|
|
<h2>${type.charAt(0).toUpperCase() + type.slice(1)} Prompt</h2>
|
|
<div style="background: #222; padding: 15px; border-radius: 5px; white-space: pre-wrap; line-height: 1.5;">${prompt}</div>
|
|
<button onclick="navigator.clipboard.writeText(\`${prompt.replace(/`/g, '\\`')}\`).then(() => alert('Copied!'))" style="margin-top: 20px; padding: 10px 20px; background: #444; color: #fff; border: none; border-radius: 5px; cursor: pointer;">Copy to Clipboard</button>
|
|
</body>
|
|
</html>
|
|
`);
|
|
}
|
|
}
|
|
|
|
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-gray-900');
|
|
});
|
|
|
|
imageArea.addEventListener('dragleave', (e) => {
|
|
e.preventDefault();
|
|
imageArea.classList.remove('bg-gray-900');
|
|
});
|
|
|
|
imageArea.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
imageArea.classList.remove('bg-gray-900');
|
|
|
|
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-black text-gray-100 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-4 bg-black 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-gray-500 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-gray-800 bg-opacity-50 hover:bg-opacity-75 rounded-full flex items-center justify-center text-white 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 shadow-2xl">
|
|
</div>
|
|
|
|
<button class="absolute right-4 top-1/2 transform -translate-y-1/2 w-10 h-10 bg-gray-800 bg-opacity-50 hover:bg-opacity-75 rounded-full flex items-center justify-center text-white 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-gray-900 border-l border-gray-700 flex flex-col">
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between p-4 border-b border-gray-700">
|
|
<div class="flex items-center gap-2">
|
|
<svg class="w-5 h-5 text-gray-400" 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-white">Generation data</h1>
|
|
</div>
|
|
<button class="text-xs text-blue-400 hover:text-blue-300 transition-colors" onclick="copyAllData()">
|
|
📋 COPY ALL
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Scrollable Content -->
|
|
<div id="data-panel-content" class="flex-1 overflow-y-auto p-4 space-y-6">
|
|
<!-- Initial placeholder content -->
|
|
<div class="text-center text-gray-500 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> |