// ==UserScript==
// @name GLIF AI Batch Generator
// @namespace http://tampermonkey.net/
// @version 1.0
// @description AI-powered batch image generation for GLIF
// @author i12bp8
// @match https://glif.app/*
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==
(function() {
'use strict';
// Modern SVG Icons
const icons = {
close: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>`,
generate: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<line x1="12" y1="8" x2="12" y2="16"/>
<line x1="8" y1="12" x2="16" y2="12"/>
</svg>`,
loading: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="animate-spin">
<circle cx="12" cy="12" r="10"/>
<path d="M12 2a10 10 0 0 1 10 10"/>
</svg>`
};
// Inject styles
function injectStyles() {
const styles = `
.ai-batch-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.ai-batch-panel {
background: white;
border-radius: 12px;
padding: 24px;
width: 90%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.ai-batch-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.ai-batch-header h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
}
.ai-close-button {
background: none;
border: none;
cursor: pointer;
padding: 4px;
color: #666;
transition: color 0.2s;
}
.ai-close-button:hover {
color: #000;
}
.ai-input-field {
margin-bottom: 16px;
}
.ai-input-label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #333;
}
.ai-input {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.2s;
}
.ai-input:focus {
outline: none;
border-color: #0066ff;
}
.ai-generate-button {
width: 100%;
padding: 12px;
background: rgb(100 48 247);
color: white;
border: none;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
height: 48px;
transition: background-color 0.2s;
}
.ai-generate-button:hover {
background: #0052cc;
}
.ai-generate-button:disabled {
background: #ccc;
cursor: not-allowed;
}
.progress-container {
margin-top: 16px;
}
.progress-bar {
width: 100%;
height: 4px;
background: #eee;
border-radius: 2px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: #0066ff;
transition: width 0.3s ease;
}
.progress-info {
display: flex;
justify-content: space-between;
margin-top: 8px;
font-size: 14px;
color: #666;
}
.progress-message {
margin-top: 8px;
font-size: 14px;
color: #666;
text-align: center;
}
.review-container {
margin-top: 20px;
max-height: 60vh;
overflow-y: auto;
padding-right: 8px;
}
.review-container::-webkit-scrollbar {
width: 8px;
}
.review-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.review-container::-webkit-scrollbar-thumb {
background: rgb(100 48 247);
border-radius: 4px;
}
.review-item {
background: #f8f9fa;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
border: 1px solid #e9ecef;
}
.review-item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.review-item-number {
font-weight: 600;
color: rgb(100 48 247);
}
.review-field {
margin-bottom: 8px;
}
.review-field-label {
font-size: 12px;
color: #666;
margin-bottom: 4px;
}
.review-field-input {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
transition: border-color 0.2s;
}
.review-field-input:focus {
outline: none;
border-color: rgb(100 48 247);
}
.review-actions {
margin-top: 20px;
display: flex;
gap: 12px;
}
.review-button {
flex: 1;
padding: 12px;
border: none;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.2s;
}
.review-generate {
background: rgb(100 48 247);
color: white;
}
.review-generate:hover {
background: rgb(85 41 210);
}
.review-back {
background: #f8f9fa;
color: #666;
border: 1px solid #ddd;
}
.review-back:hover {
background: #e9ecef;
}
.results-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
padding: 20px;
margin-top: 20px;
}
.result-card {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: transform 0.2s;
position: relative;
}
.result-card:hover {
transform: translateY(-2px);
}
.result-image-container {
position: relative;
padding-top: 100%;
background: #f8f9fa;
}
.result-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: contain;
}
.result-loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: rgb(100 48 247);
}
.result-error {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #dc3545;
text-align: center;
padding: 20px;
}
.result-details {
padding: 16px;
}
.result-field {
margin-bottom: 8px;
}
.result-field-label {
font-size: 12px;
color: #666;
margin-bottom: 2px;
}
.result-field-value {
font-size: 14px;
color: #333;
word-break: break-word;
}
.generation-progress {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: white;
padding: 16px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
display: flex;
align-items: center;
gap: 12px;
z-index: 10000;
}
.generation-progress-bar {
width: 200px;
height: 4px;
background: #eee;
border-radius: 2px;
overflow: hidden;
}
.generation-progress-fill {
height: 100%;
background: rgb(100 48 247);
transition: width 0.3s ease;
}
.generation-progress-text {
font-size: 14px;
color: #666;
white-space: nowrap;
}
.animate-spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.batch-results-container {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 80%;
max-width: 900px;
max-height: 80vh;
background: white;
border-radius: 12px;
box-shadow: 0 4px 24px rgba(0,0,0,0.15);
z-index: 10000;
overflow: hidden;
display: flex;
flex-direction: column;
}
.batch-results-header {
padding: 16px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
background: #fafafa;
}
.batch-results-header h2 {
margin: 0;
font-size: 18px;
}
.batch-results-header .close-button {
background: none;
border: none;
padding: 8px;
cursor: pointer;
color: #666;
font-size: 20px;
line-height: 1;
}
.batch-results-content {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.batch-results-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 16px;
}
`;
const styleElement = document.createElement('style');
styleElement.textContent = styles;
document.head.appendChild(styleElement);
}
// Get form inputs
function getWorkflowInputs() {
const form = document.querySelector('form');
if (!form) return [];
const inputs = [];
form.querySelectorAll('textarea').forEach(textarea => {
if (textarea.name && !textarea.name.startsWith('__') && textarea.name !== 'spellId' && textarea.name !== 'version') {
const label = textarea.closest('label')?.querySelector('span')?.textContent?.trim() || '';
inputs.push({
name: textarea.name,
type: 'textarea',
label: label,
value: textarea.value.trim(),
placeholder: textarea.getAttribute('placeholder') || label
});
}
});
return inputs;
}
// Show toast notification
function showToast(message, type = 'success') {
const toast = document.createElement('div');
toast.style.cssText = `
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
padding: 12px 24px;
background: ${type === 'error' ? '#ff4444' : type === 'warning' ? '#ffbb33' : '#00C851'};
color: white;
border-radius: 6px;
font-size: 14px;
z-index: 10000;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
}
// Fetch AI batch inputs
async function fetchAIBatchInputs(amount, content) {
const formInputs = getWorkflowInputs();
// Get workflow name and description
const workflowTitle = document.querySelector('h1')?.textContent || '';
const workflowDescription = document.querySelector('.text-gray-500')?.textContent?.trim() || '';
// Format input fields with rich context
const enrichedFields = formInputs.map(input => ({
name: input.label,
type: input.type,
currentValue: input.value,
placeholder: input.placeholder,
constraints: input.type === 'number' ? {
min: input.min,
max: input.max,
step: input.step
} : null
}));
// Get previous successful generations if available
const previousGenerations = Array.from(document.querySelectorAll('.workflow-result'))
.slice(0, 3) // Take up to 3 recent examples
.map(result => {
const inputs = {};
result.querySelectorAll('.input-value').forEach(input => {
inputs[input.getAttribute('data-name')] = input.textContent.trim();
});
return inputs;
});
console.log('Sending enriched context:', { workflowTitle, enrichedFields, previousGenerations });
try {
const enrichedContext = JSON.stringify({
workflow: {
title: workflowTitle,
description: workflowDescription
},
fields: enrichedFields,
examples: previousGenerations
});
const payload = {
id: "cm4b89oo000asm86fstry7u1e",
version: "live",
inputs: {
amount: amount.toString(),
fields: formInputs.map(input => input.name).join(' | '),
content: content,
enrichedContext: enrichedContext
},
glifRunIsPublic: !GM_getValue('isPrivate', false)
};
console.log('Debug - Request payload:', JSON.stringify(payload, null, 2));
const response = await fetch("https://glif.app/api/run-glif", {
method: 'POST',
credentials: 'include',
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0',
'Accept': '*/*',
'Accept-Language': 'en-US,en;q=0.5',
'Content-Type': 'application/json',
'Sec-GPC': '1',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
'Priority': 'u=4'
},
referrer: `https://glif.app/@appelsiensam/glifs/${window.location.pathname.split('/').pop()}`,
mode: 'cors',
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorText = await response.text();
console.log('Debug - Error response:', errorText);
throw new Error(`API request failed: ${response.status}\nResponse: ${errorText}`);
}
const reader = response.body.getReader();
let jsonData = '';
let entries = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = new TextDecoder().decode(value);
jsonData += chunk;
const lines = jsonData.split('\n');
jsonData = lines.pop() || '';
for (const line of lines) {
if (!line.trim().startsWith('data: ')) continue;
try {
const data = JSON.parse(line.slice(6));
const text = data.graphExecutionState?.nodes?.text1?.output?.value;
if (text?.includes('"entries"')) {
try {
const parsed = JSON.parse(text);
if (parsed.entries?.length) entries = parsed.entries;
} catch (e) {
console.log('Partial JSON:', text);
}
}
} catch (e) {
console.log('Parse error:', e);
}
}
}
return entries;
} catch (error) {
console.error('fetchAIBatchInputs error:', error);
throw error;
}
}
// Process batch generation with parallel processing
async function processBatchGeneration(entries) {
const spellId = window.location.pathname.split('/').pop();
const isPrivate = GM_getValue('isPrivate', false);
console.log('Starting generation with spell ID:', spellId);
console.log('Entries to process:', entries);
// Create results container if it doesn't exist
let resultsContainer = document.querySelector('.batch-results-container');
if (!resultsContainer) {
resultsContainer = document.createElement('div');
resultsContainer.className = 'batch-results-container';
resultsContainer.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 80%;
max-width: 900px;
max-height: 80vh;
background: white;
border-radius: 12px;
box-shadow: 0 4px 24px rgba(0,0,0,0.15);
z-index: 10000;
overflow: hidden;
display: flex;
flex-direction: column;
`;
// Add header with title and close button
const header = document.createElement('div');
header.style.cssText = `
padding: 16px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
background: #fafafa;
`;
header.innerHTML = `
<h2 style="margin: 0; font-size: 18px;">Generated Images</h2>
<button class="close-button" style="
background: none;
border: none;
padding: 8px;
cursor: pointer;
color: #666;
font-size: 20px;
line-height: 1;
">×</button>
`;
resultsContainer.appendChild(header);
// Add close button functionality
header.querySelector('.close-button').addEventListener('click', () => {
resultsContainer.remove();
});
// Add overlay
const overlay = document.createElement('div');
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 9999;
`;
document.body.appendChild(overlay);
// Close on overlay click
overlay.addEventListener('click', () => {
overlay.remove();
resultsContainer.remove();
});
document.body.appendChild(resultsContainer);
}
// Create scrollable content area
const contentArea = document.createElement('div');
contentArea.style.cssText = `
flex: 1;
overflow-y: auto;
padding: 16px;
`;
resultsContainer.appendChild(contentArea);
// Create results grid with improved layout
const resultsGrid = document.createElement('div');
resultsGrid.className = 'batch-results-grid';
resultsGrid.style.cssText = `
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 16px;
`;
contentArea.appendChild(resultsGrid);
// Create progress indicator
const progress = document.createElement('div');
progress.className = 'generation-progress';
progress.style.cssText = `
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
padding: 8px 16px;
box-shadow: 0 -2px 8px rgba(0,0,0,0.1);
z-index: 10001;
`;
progress.innerHTML = `
<div class="generation-progress-bar" style="
height: 4px;
background: #f0f0f0;
border-radius: 2px;
overflow: hidden;
">
<div class="generation-progress-fill" style="
width: 0%;
height: 100%;
background: rgb(100, 48, 247);
transition: width 0.3s ease;
"></div>
</div>
<div class="generation-progress-text" style="
text-align: center;
margin-top: 4px;
font-size: 14px;
">Generating 0/${entries.length}</div>
`;
document.body.appendChild(progress);
let completed = 0;
const updateProgress = () => {
completed++;
const percentage = (completed / entries.length) * 100;
progress.querySelector('.generation-progress-fill').style.width = `${percentage}%`;
progress.querySelector('.generation-progress-text').textContent =
`Generating ${completed}/${entries.length}`;
};
// Create result cards for each entry
const resultCards = entries.map((entry, index) => {
const card = document.createElement('div');
card.className = 'result-card';
card.style.cssText = `
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
overflow: hidden;
`;
card.innerHTML = `
<div class="result-image-container" style="
aspect-ratio: ${entry.widthinput}/${entry.heightinput};
background: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
">
<div class="result-loading">${icons.loading}</div>
</div>
<div class="result-details" style="padding: 12px;">
${Object.entries(entry).map(([key, value]) => `
<div class="result-field" style="margin-bottom: 8px;">
<div class="result-field-label" style="
font-size: 12px;
color: #666;
margin-bottom: 2px;
">${key.charAt(0).toUpperCase() + key.slice(1)}</div>
<div class="result-field-value" style="
font-size: 14px;
word-break: break-word;
">${value}</div>
</div>
`).join('')}
</div>
`;
resultsGrid.appendChild(card);
return card;
});
// Process all entries in parallel
const results = await Promise.all(entries.map(async (entry, index) => {
try {
console.log(`Generating image ${index + 1} with inputs:`, entry);
const requestBody = {
id: spellId,
version: "live",
inputs: {
...entry,
heightinput: String(entry.heightinput),
widthinput: String(entry.widthinput)
},
glifRunIsPublic: !isPrivate
};
console.log('Request body:', requestBody);
const response = await fetch("https://glif.app/api/run-glif", {
method: 'POST',
credentials: 'include',
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0',
'Accept': '*/*',
'Accept-Language': 'en-US,en;q=0.5',
'Content-Type': 'application/json',
'Sec-GPC': '1',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
'Priority': 'u=4'
},
referrer: `https://glif.app/@appelsiensam/glifs/${spellId}`,
mode: 'cors',
body: JSON.stringify(requestBody)
});
if (!response.ok) {
const errorText = await response.text();
console.error('API error:', errorText);
throw new Error(`Generation failed: ${response.status} - ${errorText}`);
}
const reader = response.body.getReader();
let imageUrl = null;
let jsonData = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = new TextDecoder().decode(value);
console.log(`Chunk received for image ${index + 1}:`, chunk);
const lines = chunk.split('\n');
for (const line of lines) {
if (!line.trim().startsWith('data: ')) continue;
try {
const data = JSON.parse(line.slice(6));
// Check for image URL in various locations
const imageValue =
data.graphExecutionState?.finalOutput?.value ||
data.graphExecutionState?.nodes?.output1?.output?.value ||
data.graphExecutionState?.nodes?.image?.output?.value;
if (imageValue) {
imageUrl = imageValue;
console.log(`Found image URL for ${index + 1}:`, imageUrl);
// Update card with image immediately
const card = resultCards[index];
const container = card.querySelector('.result-image-container');
container.innerHTML = `<img src="${imageUrl}" class="result-image" style="
max-width: 100%;
height: auto;
display: block;
" alt="Generated image ${index + 1}">`;
break;
}
// Check if generation is complete
if (data.graphExecutionState?.status === 'done') {
console.log(`Generation complete for ${index + 1}`);
break;
}
} catch (e) {
console.log('Parse error:', e);
}
}
if (imageUrl) break;
}
updateProgress();
return { success: true, imageUrl, entry };
} catch (error) {
console.error(`Error generating image ${index + 1}:`, error);
// Update card with error
const card = resultCards[index];
const container = card.querySelector('.result-image-container');
container.innerHTML = `
<div class="result-error" style="
padding: 16px;
color: #e53935;
text-align: center;
">
<div>Generation failed</div>
<div style="font-size: 12px; margin-top: 4px;">${error.message}</div>
</div>
`;
updateProgress();
return { success: false, error: error.message, entry };
}
}));
// Keep progress indicator for a moment before removing
setTimeout(() => progress.remove(), 2000);
const successfulResults = results.filter(r => r.success);
console.log('Generation complete. Successful results:', successfulResults);
return results;
}
// Display AI batch panel
function displayAIBatchPanel() {
const overlay = document.createElement('div');
overlay.className = 'ai-batch-overlay';
const panel = document.createElement('div');
panel.className = 'ai-batch-panel';
const header = document.createElement('div');
header.className = 'ai-batch-header';
const title = document.createElement('h2');
title.textContent = 'Batch Generator';
const closeButton = document.createElement('button');
closeButton.className = 'ai-close-button';
closeButton.innerHTML = icons.close;
closeButton.addEventListener('click', () => {
if (!panel.dataset.generating) {
overlay.remove();
} else {
showToast('Please wait for generation to complete', 'warning');
}
});
header.appendChild(title);
header.appendChild(closeButton);
const content = document.createElement('div');
content.className = 'ai-batch-content';
const amountField = document.createElement('div');
amountField.className = 'ai-input-field';
const amountLabel = document.createElement('label');
amountLabel.className = 'ai-input-label';
amountLabel.textContent = 'Number of Images';
const amountInput = document.createElement('input');
amountInput.type = 'number';
amountInput.className = 'ai-input';
amountInput.min = '1';
amountInput.max = '100';
amountInput.value = '1';
amountField.appendChild(amountLabel);
amountField.appendChild(amountInput);
const contentField = document.createElement('div');
contentField.className = 'ai-input-field';
const contentLabel = document.createElement('label');
contentLabel.className = 'ai-input-label';
contentLabel.textContent = 'Description';
const contentInput = document.createElement('textarea');
contentInput.className = 'ai-input';
contentInput.rows = 4;
contentInput.placeholder = 'Describe what you want to generate...';
contentField.appendChild(contentLabel);
contentField.appendChild(contentInput);
const generateButton = document.createElement('button');
generateButton.className = 'ai-generate-button';
generateButton.style.cssText = `
width: 100%;
padding: 12px;
background: rgb(100 48 247);
color: white;
border: none;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
height: 48px;
transition: background-color 0.2s;
`;
generateButton.innerHTML = `${icons.generate}<span>Generate Images</span>`;
const progressContainer = document.createElement('div');
progressContainer.className = 'progress-container';
progressContainer.style.display = 'none';
progressContainer.innerHTML = `
<div class="progress-bar">
<div class="progress-fill" style="width: 0%"></div>
</div>
<div class="progress-info">
<span class="progress-count">0/${amountInput.value}</span>
<span class="progress-percentage">0%</span>
</div>
<div class="progress-message">Starting generation...</div>
`;
generateButton.addEventListener('click', async () => {
const amount = parseInt(amountInput.value);
const content = contentInput.value;
if (!content) {
showToast('Please describe the content you want to generate', 'error');
return;
}
if (isNaN(amount) || amount < 1 || amount > 100) {
showToast('Please enter a valid number of images (1-100)', 'error');
return;
}
generateButton.disabled = true;
generateButton.innerHTML = `${icons.loading}<span>Generating Prompts...</span>`;
try {
console.log('Fetching AI batch inputs...');
const inputs = await fetchAIBatchInputs(amount, content);
console.log('Received inputs:', inputs);
if (inputs && inputs.length > 0) {
// Show review screen instead of generating immediately
displayReviewScreen(inputs, panel, {
amount,
content
});
} else {
showToast('Failed to generate image prompts', 'error');
generateButton.disabled = false;
generateButton.innerHTML = `${icons.generate}<span>Generate Images</span>`;
}
} catch (error) {
console.error('Batch generation error:', error);
showToast('Failed to generate prompts: ' + error.message, 'error');
generateButton.disabled = false;
generateButton.innerHTML = `${icons.generate}<span>Generate Images</span>`;
}
});
content.appendChild(amountField);
content.appendChild(contentField);
content.appendChild(generateButton);
content.appendChild(progressContainer);
panel.appendChild(header);
panel.appendChild(content);
overlay.appendChild(panel);
document.body.appendChild(overlay);
}
// Display review screen
function displayReviewScreen(entries, panel, originalInputs) {
// Clear existing content
const content = panel.querySelector('.ai-batch-content');
content.innerHTML = '';
// Create review container
const reviewContainer = document.createElement('div');
reviewContainer.className = 'review-container';
// Add each entry for review
entries.forEach((entry, index) => {
const reviewItem = document.createElement('div');
reviewItem.className = 'review-item';
const header = document.createElement('div');
header.className = 'review-item-header';
header.innerHTML = `<span class="review-item-number">Image ${index + 1}</span>`;
reviewItem.appendChild(header);
// Add editable fields
Object.entries(entry).forEach(([key, value]) => {
const field = document.createElement('div');
field.className = 'review-field';
const label = document.createElement('div');
label.className = 'review-field-label';
label.textContent = key.charAt(0).toUpperCase() + key.slice(1);
const input = document.createElement('input');
input.className = 'review-field-input';
input.type = 'text';
input.value = value;
input.dataset.index = index;
input.dataset.field = key;
// Update entries object when input changes
input.addEventListener('input', (e) => {
entries[index][key] = e.target.value;
});
field.appendChild(label);
field.appendChild(input);
reviewItem.appendChild(field);
});
reviewContainer.appendChild(reviewItem);
});
// Add action buttons
const actions = document.createElement('div');
actions.className = 'review-actions';
const backButton = document.createElement('button');
backButton.className = 'review-button review-back';
backButton.innerHTML = `<span>Back</span>`;
backButton.addEventListener('click', () => {
// Restore original panel content
displayAIBatchPanel();
});
const generateButton = document.createElement('button');
generateButton.className = 'review-button review-generate';
generateButton.innerHTML = `${icons.generate}<span>Generate Images</span>`;
generateButton.addEventListener('click', async () => {
generateButton.disabled = true;
generateButton.innerHTML = `${icons.loading}<span>Generating...</span>`;
try {
const results = await processBatchGeneration(entries);
console.log('Generation complete:', results);
const successCount = results.filter(r => r.success).length;
showToast(`Successfully generated ${successCount} images`);
if (successCount === 0) {
generateButton.disabled = false;
generateButton.innerHTML = `${icons.generate}<span>Generate Images</span>`;
} else {
panel.closest('.ai-batch-overlay').remove();
}
} catch (error) {
console.error('Generation error:', error);
showToast('Failed to generate images: ' + error.message, 'error');
generateButton.disabled = false;
generateButton.innerHTML = `${icons.generate}<span>Generate Images</span>`;
}
});
actions.appendChild(backButton);
actions.appendChild(generateButton);
content.appendChild(reviewContainer);
content.appendChild(actions);
}
// Add the AI batch button to the page
function addAIBatchButton() {
const container = document.querySelector('form');
if (!container) return;
const button = document.createElement('button');
button.className = 'ai-batch-button';
button.style.cssText = `
margin-top: 12px;
padding: 8px 16px;
background: rgb(100 48 247);
color: white;
border: none;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
height: 48px;
transition: background-color 0.2s;
`;
button.innerHTML = `${icons.generate}<span>Batch Generator</span>`;
button.addEventListener('click', (e) => {
e.preventDefault();
displayAIBatchPanel();
});
container.appendChild(button);
}
// Initialize
function initialize() {
injectStyles();
// Wait for the form to be ready
const observer = new MutationObserver((mutations, obs) => {
if (document.querySelector('form')) {
addAIBatchButton();
obs.disconnect();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
// Start the script
initialize();
})();