您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
AI-powered batch image generation for GLIF
// ==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(); })();