Greasy Fork 支持简体中文。

GLIF AI Batch Generator

AI-powered batch image generation for GLIF

  1. // ==UserScript==
  2. // @name GLIF AI Batch Generator
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0
  5. // @description AI-powered batch image generation for GLIF
  6. // @author i12bp8
  7. // @match https://glif.app/*
  8. // @grant GM_setValue
  9. // @grant GM_getValue
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14.  
  15. // Modern SVG Icons
  16. const icons = {
  17. close: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  18. <line x1="18" y1="6" x2="6" y2="18"/>
  19. <line x1="6" y1="6" x2="18" y2="18"/>
  20. </svg>`,
  21. generate: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  22. <circle cx="12" cy="12" r="10"/>
  23. <line x1="12" y1="8" x2="12" y2="16"/>
  24. <line x1="8" y1="12" x2="16" y2="12"/>
  25. </svg>`,
  26. loading: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="animate-spin">
  27. <circle cx="12" cy="12" r="10"/>
  28. <path d="M12 2a10 10 0 0 1 10 10"/>
  29. </svg>`
  30. };
  31.  
  32. // Inject styles
  33. function injectStyles() {
  34. const styles = `
  35. .ai-batch-overlay {
  36. position: fixed;
  37. top: 0;
  38. left: 0;
  39. width: 100%;
  40. height: 100%;
  41. background: rgba(0, 0, 0, 0.5);
  42. display: flex;
  43. justify-content: center;
  44. align-items: center;
  45. z-index: 9999;
  46. }
  47.  
  48. .ai-batch-panel {
  49. background: white;
  50. border-radius: 12px;
  51. padding: 24px;
  52. width: 90%;
  53. max-width: 500px;
  54. max-height: 90vh;
  55. overflow-y: auto;
  56. box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  57. }
  58.  
  59. .ai-batch-header {
  60. display: flex;
  61. justify-content: space-between;
  62. align-items: center;
  63. margin-bottom: 20px;
  64. }
  65.  
  66. .ai-batch-header h2 {
  67. margin: 0;
  68. font-size: 1.5rem;
  69. font-weight: 600;
  70. }
  71.  
  72. .ai-close-button {
  73. background: none;
  74. border: none;
  75. cursor: pointer;
  76. padding: 4px;
  77. color: #666;
  78. transition: color 0.2s;
  79. }
  80.  
  81. .ai-close-button:hover {
  82. color: #000;
  83. }
  84.  
  85. .ai-input-field {
  86. margin-bottom: 16px;
  87. }
  88.  
  89. .ai-input-label {
  90. display: block;
  91. margin-bottom: 8px;
  92. font-weight: 500;
  93. color: #333;
  94. }
  95.  
  96. .ai-input {
  97. width: 100%;
  98. padding: 8px 12px;
  99. border: 1px solid #ddd;
  100. border-radius: 6px;
  101. font-size: 14px;
  102. transition: border-color 0.2s;
  103. }
  104.  
  105. .ai-input:focus {
  106. outline: none;
  107. border-color: #0066ff;
  108. }
  109.  
  110. .ai-generate-button {
  111. width: 100%;
  112. padding: 12px;
  113. background: rgb(100 48 247);
  114. color: white;
  115. border: none;
  116. border-radius: 6px;
  117. font-weight: 500;
  118. cursor: pointer;
  119. display: flex;
  120. align-items: center;
  121. justify-content: center;
  122. gap: 8px;
  123. height: 48px;
  124. transition: background-color 0.2s;
  125. }
  126.  
  127. .ai-generate-button:hover {
  128. background: #0052cc;
  129. }
  130.  
  131. .ai-generate-button:disabled {
  132. background: #ccc;
  133. cursor: not-allowed;
  134. }
  135.  
  136. .progress-container {
  137. margin-top: 16px;
  138. }
  139.  
  140. .progress-bar {
  141. width: 100%;
  142. height: 4px;
  143. background: #eee;
  144. border-radius: 2px;
  145. overflow: hidden;
  146. }
  147.  
  148. .progress-fill {
  149. height: 100%;
  150. background: #0066ff;
  151. transition: width 0.3s ease;
  152. }
  153.  
  154. .progress-info {
  155. display: flex;
  156. justify-content: space-between;
  157. margin-top: 8px;
  158. font-size: 14px;
  159. color: #666;
  160. }
  161.  
  162. .progress-message {
  163. margin-top: 8px;
  164. font-size: 14px;
  165. color: #666;
  166. text-align: center;
  167. }
  168.  
  169. .review-container {
  170. margin-top: 20px;
  171. max-height: 60vh;
  172. overflow-y: auto;
  173. padding-right: 8px;
  174. }
  175.  
  176. .review-container::-webkit-scrollbar {
  177. width: 8px;
  178. }
  179.  
  180. .review-container::-webkit-scrollbar-track {
  181. background: #f1f1f1;
  182. border-radius: 4px;
  183. }
  184.  
  185. .review-container::-webkit-scrollbar-thumb {
  186. background: rgb(100 48 247);
  187. border-radius: 4px;
  188. }
  189.  
  190. .review-item {
  191. background: #f8f9fa;
  192. border-radius: 8px;
  193. padding: 16px;
  194. margin-bottom: 12px;
  195. border: 1px solid #e9ecef;
  196. }
  197.  
  198. .review-item-header {
  199. display: flex;
  200. justify-content: space-between;
  201. align-items: center;
  202. margin-bottom: 12px;
  203. }
  204.  
  205. .review-item-number {
  206. font-weight: 600;
  207. color: rgb(100 48 247);
  208. }
  209.  
  210. .review-field {
  211. margin-bottom: 8px;
  212. }
  213.  
  214. .review-field-label {
  215. font-size: 12px;
  216. color: #666;
  217. margin-bottom: 4px;
  218. }
  219.  
  220. .review-field-input {
  221. width: 100%;
  222. padding: 8px;
  223. border: 1px solid #ddd;
  224. border-radius: 4px;
  225. font-size: 14px;
  226. transition: border-color 0.2s;
  227. }
  228.  
  229. .review-field-input:focus {
  230. outline: none;
  231. border-color: rgb(100 48 247);
  232. }
  233.  
  234. .review-actions {
  235. margin-top: 20px;
  236. display: flex;
  237. gap: 12px;
  238. }
  239.  
  240. .review-button {
  241. flex: 1;
  242. padding: 12px;
  243. border: none;
  244. border-radius: 6px;
  245. font-weight: 500;
  246. cursor: pointer;
  247. height: 48px;
  248. display: flex;
  249. align-items: center;
  250. justify-content: center;
  251. gap: 8px;
  252. transition: all 0.2s;
  253. }
  254.  
  255. .review-generate {
  256. background: rgb(100 48 247);
  257. color: white;
  258. }
  259.  
  260. .review-generate:hover {
  261. background: rgb(85 41 210);
  262. }
  263.  
  264. .review-back {
  265. background: #f8f9fa;
  266. color: #666;
  267. border: 1px solid #ddd;
  268. }
  269.  
  270. .review-back:hover {
  271. background: #e9ecef;
  272. }
  273.  
  274. .results-grid {
  275. display: grid;
  276. grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  277. gap: 20px;
  278. padding: 20px;
  279. margin-top: 20px;
  280. }
  281.  
  282. .result-card {
  283. background: white;
  284. border-radius: 12px;
  285. overflow: hidden;
  286. box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  287. transition: transform 0.2s;
  288. position: relative;
  289. }
  290.  
  291. .result-card:hover {
  292. transform: translateY(-2px);
  293. }
  294.  
  295. .result-image-container {
  296. position: relative;
  297. padding-top: 100%;
  298. background: #f8f9fa;
  299. }
  300.  
  301. .result-image {
  302. position: absolute;
  303. top: 0;
  304. left: 0;
  305. width: 100%;
  306. height: 100%;
  307. object-fit: contain;
  308. }
  309.  
  310. .result-loading {
  311. position: absolute;
  312. top: 50%;
  313. left: 50%;
  314. transform: translate(-50%, -50%);
  315. color: rgb(100 48 247);
  316. }
  317.  
  318. .result-error {
  319. position: absolute;
  320. top: 50%;
  321. left: 50%;
  322. transform: translate(-50%, -50%);
  323. color: #dc3545;
  324. text-align: center;
  325. padding: 20px;
  326. }
  327.  
  328. .result-details {
  329. padding: 16px;
  330. }
  331.  
  332. .result-field {
  333. margin-bottom: 8px;
  334. }
  335.  
  336. .result-field-label {
  337. font-size: 12px;
  338. color: #666;
  339. margin-bottom: 2px;
  340. }
  341.  
  342. .result-field-value {
  343. font-size: 14px;
  344. color: #333;
  345. word-break: break-word;
  346. }
  347.  
  348. .generation-progress {
  349. position: fixed;
  350. bottom: 20px;
  351. left: 50%;
  352. transform: translateX(-50%);
  353. background: white;
  354. padding: 16px;
  355. border-radius: 8px;
  356. box-shadow: 0 4px 6px rgba(0,0,0,0.1);
  357. display: flex;
  358. align-items: center;
  359. gap: 12px;
  360. z-index: 10000;
  361. }
  362.  
  363. .generation-progress-bar {
  364. width: 200px;
  365. height: 4px;
  366. background: #eee;
  367. border-radius: 2px;
  368. overflow: hidden;
  369. }
  370.  
  371. .generation-progress-fill {
  372. height: 100%;
  373. background: rgb(100 48 247);
  374. transition: width 0.3s ease;
  375. }
  376.  
  377. .generation-progress-text {
  378. font-size: 14px;
  379. color: #666;
  380. white-space: nowrap;
  381. }
  382.  
  383. .animate-spin {
  384. animation: spin 1s linear infinite;
  385. }
  386.  
  387. @keyframes spin {
  388. from { transform: rotate(0deg); }
  389. to { transform: rotate(360deg); }
  390. }
  391.  
  392. .batch-results-container {
  393. position: fixed;
  394. top: 50%;
  395. left: 50%;
  396. transform: translate(-50%, -50%);
  397. width: 80%;
  398. max-width: 900px;
  399. max-height: 80vh;
  400. background: white;
  401. border-radius: 12px;
  402. box-shadow: 0 4px 24px rgba(0,0,0,0.15);
  403. z-index: 10000;
  404. overflow: hidden;
  405. display: flex;
  406. flex-direction: column;
  407. }
  408.  
  409. .batch-results-header {
  410. padding: 16px;
  411. border-bottom: 1px solid #eee;
  412. display: flex;
  413. justify-content: space-between;
  414. align-items: center;
  415. background: #fafafa;
  416. }
  417.  
  418. .batch-results-header h2 {
  419. margin: 0;
  420. font-size: 18px;
  421. }
  422.  
  423. .batch-results-header .close-button {
  424. background: none;
  425. border: none;
  426. padding: 8px;
  427. cursor: pointer;
  428. color: #666;
  429. font-size: 20px;
  430. line-height: 1;
  431. }
  432.  
  433. .batch-results-content {
  434. flex: 1;
  435. overflow-y: auto;
  436. padding: 16px;
  437. }
  438.  
  439. .batch-results-grid {
  440. display: grid;
  441. grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  442. gap: 16px;
  443. }
  444. `;
  445.  
  446. const styleElement = document.createElement('style');
  447. styleElement.textContent = styles;
  448. document.head.appendChild(styleElement);
  449. }
  450.  
  451. // Get form inputs
  452. function getWorkflowInputs() {
  453. const form = document.querySelector('form');
  454. if (!form) return [];
  455.  
  456. const inputs = [];
  457. form.querySelectorAll('textarea').forEach(textarea => {
  458. if (textarea.name && !textarea.name.startsWith('__') && textarea.name !== 'spellId' && textarea.name !== 'version') {
  459. const label = textarea.closest('label')?.querySelector('span')?.textContent?.trim() || '';
  460. inputs.push({
  461. name: textarea.name,
  462. type: 'textarea',
  463. label: label,
  464. value: textarea.value.trim(),
  465. placeholder: textarea.getAttribute('placeholder') || label
  466. });
  467. }
  468. });
  469. return inputs;
  470. }
  471.  
  472. // Show toast notification
  473. function showToast(message, type = 'success') {
  474. const toast = document.createElement('div');
  475. toast.style.cssText = `
  476. position: fixed;
  477. bottom: 20px;
  478. left: 50%;
  479. transform: translateX(-50%);
  480. padding: 12px 24px;
  481. background: ${type === 'error' ? '#ff4444' : type === 'warning' ? '#ffbb33' : '#00C851'};
  482. color: white;
  483. border-radius: 6px;
  484. font-size: 14px;
  485. z-index: 10000;
  486. box-shadow: 0 2px 4px rgba(0,0,0,0.2);
  487. `;
  488. toast.textContent = message;
  489. document.body.appendChild(toast);
  490. setTimeout(() => toast.remove(), 3000);
  491. }
  492.  
  493. // Fetch AI batch inputs
  494. async function fetchAIBatchInputs(amount, content) {
  495. const formInputs = getWorkflowInputs();
  496.  
  497. // Get workflow name and description
  498. const workflowTitle = document.querySelector('h1')?.textContent || '';
  499. const workflowDescription = document.querySelector('.text-gray-500')?.textContent?.trim() || '';
  500.  
  501. // Format input fields with rich context
  502. const enrichedFields = formInputs.map(input => ({
  503. name: input.label,
  504. type: input.type,
  505. currentValue: input.value,
  506. placeholder: input.placeholder,
  507. constraints: input.type === 'number' ? {
  508. min: input.min,
  509. max: input.max,
  510. step: input.step
  511. } : null
  512. }));
  513.  
  514. // Get previous successful generations if available
  515. const previousGenerations = Array.from(document.querySelectorAll('.workflow-result'))
  516. .slice(0, 3) // Take up to 3 recent examples
  517. .map(result => {
  518. const inputs = {};
  519. result.querySelectorAll('.input-value').forEach(input => {
  520. inputs[input.getAttribute('data-name')] = input.textContent.trim();
  521. });
  522. return inputs;
  523. });
  524.  
  525. console.log('Sending enriched context:', { workflowTitle, enrichedFields, previousGenerations });
  526.  
  527. try {
  528. const enrichedContext = JSON.stringify({
  529. workflow: {
  530. title: workflowTitle,
  531. description: workflowDescription
  532. },
  533. fields: enrichedFields,
  534. examples: previousGenerations
  535. });
  536.  
  537. const payload = {
  538. id: "cm4b89oo000asm86fstry7u1e",
  539. version: "live",
  540. inputs: {
  541. amount: amount.toString(),
  542. fields: formInputs.map(input => input.name).join(' | '),
  543. content: content,
  544. enrichedContext: enrichedContext
  545. },
  546. glifRunIsPublic: !GM_getValue('isPrivate', false)
  547. };
  548.  
  549. console.log('Debug - Request payload:', JSON.stringify(payload, null, 2));
  550.  
  551. const response = await fetch("https://glif.app/api/run-glif", {
  552. method: 'POST',
  553. credentials: 'include',
  554. headers: {
  555. 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0',
  556. 'Accept': '*/*',
  557. 'Accept-Language': 'en-US,en;q=0.5',
  558. 'Content-Type': 'application/json',
  559. 'Sec-GPC': '1',
  560. 'Sec-Fetch-Dest': 'empty',
  561. 'Sec-Fetch-Mode': 'cors',
  562. 'Sec-Fetch-Site': 'same-origin',
  563. 'Priority': 'u=4'
  564. },
  565. referrer: `https://glif.app/@appelsiensam/glifs/${window.location.pathname.split('/').pop()}`,
  566. mode: 'cors',
  567. body: JSON.stringify(payload)
  568. });
  569.  
  570. if (!response.ok) {
  571. const errorText = await response.text();
  572. console.log('Debug - Error response:', errorText);
  573. throw new Error(`API request failed: ${response.status}\nResponse: ${errorText}`);
  574. }
  575.  
  576. const reader = response.body.getReader();
  577. let jsonData = '';
  578. let entries = [];
  579.  
  580. while (true) {
  581. const { done, value } = await reader.read();
  582. if (done) break;
  583.  
  584. const chunk = new TextDecoder().decode(value);
  585. jsonData += chunk;
  586.  
  587. const lines = jsonData.split('\n');
  588. jsonData = lines.pop() || '';
  589.  
  590. for (const line of lines) {
  591. if (!line.trim().startsWith('data: ')) continue;
  592.  
  593. try {
  594. const data = JSON.parse(line.slice(6));
  595. const text = data.graphExecutionState?.nodes?.text1?.output?.value;
  596.  
  597. if (text?.includes('"entries"')) {
  598. try {
  599. const parsed = JSON.parse(text);
  600. if (parsed.entries?.length) entries = parsed.entries;
  601. } catch (e) {
  602. console.log('Partial JSON:', text);
  603. }
  604. }
  605. } catch (e) {
  606. console.log('Parse error:', e);
  607. }
  608. }
  609. }
  610.  
  611. return entries;
  612. } catch (error) {
  613. console.error('fetchAIBatchInputs error:', error);
  614. throw error;
  615. }
  616. }
  617.  
  618. // Process batch generation with parallel processing
  619. async function processBatchGeneration(entries) {
  620. const spellId = window.location.pathname.split('/').pop();
  621. const isPrivate = GM_getValue('isPrivate', false);
  622.  
  623. console.log('Starting generation with spell ID:', spellId);
  624. console.log('Entries to process:', entries);
  625.  
  626. // Create results container if it doesn't exist
  627. let resultsContainer = document.querySelector('.batch-results-container');
  628. if (!resultsContainer) {
  629. resultsContainer = document.createElement('div');
  630. resultsContainer.className = 'batch-results-container';
  631. resultsContainer.style.cssText = `
  632. position: fixed;
  633. top: 50%;
  634. left: 50%;
  635. transform: translate(-50%, -50%);
  636. width: 80%;
  637. max-width: 900px;
  638. max-height: 80vh;
  639. background: white;
  640. border-radius: 12px;
  641. box-shadow: 0 4px 24px rgba(0,0,0,0.15);
  642. z-index: 10000;
  643. overflow: hidden;
  644. display: flex;
  645. flex-direction: column;
  646. `;
  647.  
  648. // Add header with title and close button
  649. const header = document.createElement('div');
  650. header.style.cssText = `
  651. padding: 16px;
  652. border-bottom: 1px solid #eee;
  653. display: flex;
  654. justify-content: space-between;
  655. align-items: center;
  656. background: #fafafa;
  657. `;
  658. header.innerHTML = `
  659. <h2 style="margin: 0; font-size: 18px;">Generated Images</h2>
  660. <button class="close-button" style="
  661. background: none;
  662. border: none;
  663. padding: 8px;
  664. cursor: pointer;
  665. color: #666;
  666. font-size: 20px;
  667. line-height: 1;
  668. ">×</button>
  669. `;
  670. resultsContainer.appendChild(header);
  671.  
  672. // Add close button functionality
  673. header.querySelector('.close-button').addEventListener('click', () => {
  674. resultsContainer.remove();
  675. });
  676.  
  677. // Add overlay
  678. const overlay = document.createElement('div');
  679. overlay.style.cssText = `
  680. position: fixed;
  681. top: 0;
  682. left: 0;
  683. right: 0;
  684. bottom: 0;
  685. background: rgba(0,0,0,0.5);
  686. z-index: 9999;
  687. `;
  688. document.body.appendChild(overlay);
  689.  
  690. // Close on overlay click
  691. overlay.addEventListener('click', () => {
  692. overlay.remove();
  693. resultsContainer.remove();
  694. });
  695.  
  696. document.body.appendChild(resultsContainer);
  697. }
  698.  
  699. // Create scrollable content area
  700. const contentArea = document.createElement('div');
  701. contentArea.style.cssText = `
  702. flex: 1;
  703. overflow-y: auto;
  704. padding: 16px;
  705. `;
  706. resultsContainer.appendChild(contentArea);
  707.  
  708. // Create results grid with improved layout
  709. const resultsGrid = document.createElement('div');
  710. resultsGrid.className = 'batch-results-grid';
  711. resultsGrid.style.cssText = `
  712. display: grid;
  713. grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  714. gap: 16px;
  715. `;
  716. contentArea.appendChild(resultsGrid);
  717.  
  718. // Create progress indicator
  719. const progress = document.createElement('div');
  720. progress.className = 'generation-progress';
  721. progress.style.cssText = `
  722. position: fixed;
  723. bottom: 0;
  724. left: 0;
  725. right: 0;
  726. background: white;
  727. padding: 8px 16px;
  728. box-shadow: 0 -2px 8px rgba(0,0,0,0.1);
  729. z-index: 10001;
  730. `;
  731. progress.innerHTML = `
  732. <div class="generation-progress-bar" style="
  733. height: 4px;
  734. background: #f0f0f0;
  735. border-radius: 2px;
  736. overflow: hidden;
  737. ">
  738. <div class="generation-progress-fill" style="
  739. width: 0%;
  740. height: 100%;
  741. background: rgb(100, 48, 247);
  742. transition: width 0.3s ease;
  743. "></div>
  744. </div>
  745. <div class="generation-progress-text" style="
  746. text-align: center;
  747. margin-top: 4px;
  748. font-size: 14px;
  749. ">Generating 0/${entries.length}</div>
  750. `;
  751. document.body.appendChild(progress);
  752.  
  753. let completed = 0;
  754. const updateProgress = () => {
  755. completed++;
  756. const percentage = (completed / entries.length) * 100;
  757. progress.querySelector('.generation-progress-fill').style.width = `${percentage}%`;
  758. progress.querySelector('.generation-progress-text').textContent =
  759. `Generating ${completed}/${entries.length}`;
  760. };
  761.  
  762. // Create result cards for each entry
  763. const resultCards = entries.map((entry, index) => {
  764. const card = document.createElement('div');
  765. card.className = 'result-card';
  766. card.style.cssText = `
  767. background: white;
  768. border-radius: 8px;
  769. box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  770. overflow: hidden;
  771. `;
  772. card.innerHTML = `
  773. <div class="result-image-container" style="
  774. aspect-ratio: ${entry.widthinput}/${entry.heightinput};
  775. background: #f5f5f5;
  776. display: flex;
  777. align-items: center;
  778. justify-content: center;
  779. ">
  780. <div class="result-loading">${icons.loading}</div>
  781. </div>
  782. <div class="result-details" style="padding: 12px;">
  783. ${Object.entries(entry).map(([key, value]) => `
  784. <div class="result-field" style="margin-bottom: 8px;">
  785. <div class="result-field-label" style="
  786. font-size: 12px;
  787. color: #666;
  788. margin-bottom: 2px;
  789. ">${key.charAt(0).toUpperCase() + key.slice(1)}</div>
  790. <div class="result-field-value" style="
  791. font-size: 14px;
  792. word-break: break-word;
  793. ">${value}</div>
  794. </div>
  795. `).join('')}
  796. </div>
  797. `;
  798. resultsGrid.appendChild(card);
  799. return card;
  800. });
  801.  
  802. // Process all entries in parallel
  803. const results = await Promise.all(entries.map(async (entry, index) => {
  804. try {
  805. console.log(`Generating image ${index + 1} with inputs:`, entry);
  806.  
  807. const requestBody = {
  808. id: spellId,
  809. version: "live",
  810. inputs: {
  811. ...entry,
  812. heightinput: String(entry.heightinput),
  813. widthinput: String(entry.widthinput)
  814. },
  815. glifRunIsPublic: !isPrivate
  816. };
  817.  
  818. console.log('Request body:', requestBody);
  819.  
  820. const response = await fetch("https://glif.app/api/run-glif", {
  821. method: 'POST',
  822. credentials: 'include',
  823. headers: {
  824. 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0',
  825. 'Accept': '*/*',
  826. 'Accept-Language': 'en-US,en;q=0.5',
  827. 'Content-Type': 'application/json',
  828. 'Sec-GPC': '1',
  829. 'Sec-Fetch-Dest': 'empty',
  830. 'Sec-Fetch-Mode': 'cors',
  831. 'Sec-Fetch-Site': 'same-origin',
  832. 'Priority': 'u=4'
  833. },
  834. referrer: `https://glif.app/@appelsiensam/glifs/${spellId}`,
  835. mode: 'cors',
  836. body: JSON.stringify(requestBody)
  837. });
  838.  
  839. if (!response.ok) {
  840. const errorText = await response.text();
  841. console.error('API error:', errorText);
  842. throw new Error(`Generation failed: ${response.status} - ${errorText}`);
  843. }
  844.  
  845. const reader = response.body.getReader();
  846. let imageUrl = null;
  847. let jsonData = '';
  848.  
  849. while (true) {
  850. const { done, value } = await reader.read();
  851. if (done) break;
  852.  
  853. const chunk = new TextDecoder().decode(value);
  854. console.log(`Chunk received for image ${index + 1}:`, chunk);
  855.  
  856. const lines = chunk.split('\n');
  857. for (const line of lines) {
  858. if (!line.trim().startsWith('data: ')) continue;
  859.  
  860. try {
  861. const data = JSON.parse(line.slice(6));
  862.  
  863. // Check for image URL in various locations
  864. const imageValue =
  865. data.graphExecutionState?.finalOutput?.value ||
  866. data.graphExecutionState?.nodes?.output1?.output?.value ||
  867. data.graphExecutionState?.nodes?.image?.output?.value;
  868.  
  869. if (imageValue) {
  870. imageUrl = imageValue;
  871. console.log(`Found image URL for ${index + 1}:`, imageUrl);
  872.  
  873. // Update card with image immediately
  874. const card = resultCards[index];
  875. const container = card.querySelector('.result-image-container');
  876. container.innerHTML = `<img src="${imageUrl}" class="result-image" style="
  877. max-width: 100%;
  878. height: auto;
  879. display: block;
  880. " alt="Generated image ${index + 1}">`;
  881.  
  882. break;
  883. }
  884.  
  885. // Check if generation is complete
  886. if (data.graphExecutionState?.status === 'done') {
  887. console.log(`Generation complete for ${index + 1}`);
  888. break;
  889. }
  890. } catch (e) {
  891. console.log('Parse error:', e);
  892. }
  893. }
  894.  
  895. if (imageUrl) break;
  896. }
  897.  
  898. updateProgress();
  899. return { success: true, imageUrl, entry };
  900.  
  901. } catch (error) {
  902. console.error(`Error generating image ${index + 1}:`, error);
  903.  
  904. // Update card with error
  905. const card = resultCards[index];
  906. const container = card.querySelector('.result-image-container');
  907. container.innerHTML = `
  908. <div class="result-error" style="
  909. padding: 16px;
  910. color: #e53935;
  911. text-align: center;
  912. ">
  913. <div>Generation failed</div>
  914. <div style="font-size: 12px; margin-top: 4px;">${error.message}</div>
  915. </div>
  916. `;
  917.  
  918. updateProgress();
  919. return { success: false, error: error.message, entry };
  920. }
  921. }));
  922.  
  923. // Keep progress indicator for a moment before removing
  924. setTimeout(() => progress.remove(), 2000);
  925.  
  926. const successfulResults = results.filter(r => r.success);
  927. console.log('Generation complete. Successful results:', successfulResults);
  928.  
  929. return results;
  930. }
  931.  
  932. // Display AI batch panel
  933. function displayAIBatchPanel() {
  934. const overlay = document.createElement('div');
  935. overlay.className = 'ai-batch-overlay';
  936.  
  937. const panel = document.createElement('div');
  938. panel.className = 'ai-batch-panel';
  939.  
  940. const header = document.createElement('div');
  941. header.className = 'ai-batch-header';
  942.  
  943. const title = document.createElement('h2');
  944. title.textContent = 'Batch Generator';
  945.  
  946. const closeButton = document.createElement('button');
  947. closeButton.className = 'ai-close-button';
  948. closeButton.innerHTML = icons.close;
  949. closeButton.addEventListener('click', () => {
  950. if (!panel.dataset.generating) {
  951. overlay.remove();
  952. } else {
  953. showToast('Please wait for generation to complete', 'warning');
  954. }
  955. });
  956.  
  957. header.appendChild(title);
  958. header.appendChild(closeButton);
  959.  
  960. const content = document.createElement('div');
  961. content.className = 'ai-batch-content';
  962.  
  963. const amountField = document.createElement('div');
  964. amountField.className = 'ai-input-field';
  965.  
  966. const amountLabel = document.createElement('label');
  967. amountLabel.className = 'ai-input-label';
  968. amountLabel.textContent = 'Number of Images';
  969.  
  970. const amountInput = document.createElement('input');
  971. amountInput.type = 'number';
  972. amountInput.className = 'ai-input';
  973. amountInput.min = '1';
  974. amountInput.max = '100';
  975. amountInput.value = '1';
  976.  
  977. amountField.appendChild(amountLabel);
  978. amountField.appendChild(amountInput);
  979.  
  980. const contentField = document.createElement('div');
  981. contentField.className = 'ai-input-field';
  982.  
  983. const contentLabel = document.createElement('label');
  984. contentLabel.className = 'ai-input-label';
  985. contentLabel.textContent = 'Description';
  986.  
  987. const contentInput = document.createElement('textarea');
  988. contentInput.className = 'ai-input';
  989. contentInput.rows = 4;
  990. contentInput.placeholder = 'Describe what you want to generate...';
  991.  
  992. contentField.appendChild(contentLabel);
  993. contentField.appendChild(contentInput);
  994.  
  995. const generateButton = document.createElement('button');
  996. generateButton.className = 'ai-generate-button';
  997. generateButton.style.cssText = `
  998. width: 100%;
  999. padding: 12px;
  1000. background: rgb(100 48 247);
  1001. color: white;
  1002. border: none;
  1003. border-radius: 6px;
  1004. font-weight: 500;
  1005. cursor: pointer;
  1006. display: flex;
  1007. align-items: center;
  1008. justify-content: center;
  1009. gap: 8px;
  1010. height: 48px;
  1011. transition: background-color 0.2s;
  1012. `;
  1013. generateButton.innerHTML = `${icons.generate}<span>Generate Images</span>`;
  1014.  
  1015. const progressContainer = document.createElement('div');
  1016. progressContainer.className = 'progress-container';
  1017. progressContainer.style.display = 'none';
  1018. progressContainer.innerHTML = `
  1019. <div class="progress-bar">
  1020. <div class="progress-fill" style="width: 0%"></div>
  1021. </div>
  1022. <div class="progress-info">
  1023. <span class="progress-count">0/${amountInput.value}</span>
  1024. <span class="progress-percentage">0%</span>
  1025. </div>
  1026. <div class="progress-message">Starting generation...</div>
  1027. `;
  1028.  
  1029. generateButton.addEventListener('click', async () => {
  1030. const amount = parseInt(amountInput.value);
  1031. const content = contentInput.value;
  1032.  
  1033. if (!content) {
  1034. showToast('Please describe the content you want to generate', 'error');
  1035. return;
  1036. }
  1037.  
  1038. if (isNaN(amount) || amount < 1 || amount > 100) {
  1039. showToast('Please enter a valid number of images (1-100)', 'error');
  1040. return;
  1041. }
  1042.  
  1043. generateButton.disabled = true;
  1044. generateButton.innerHTML = `${icons.loading}<span>Generating Prompts...</span>`;
  1045.  
  1046. try {
  1047. console.log('Fetching AI batch inputs...');
  1048. const inputs = await fetchAIBatchInputs(amount, content);
  1049. console.log('Received inputs:', inputs);
  1050.  
  1051. if (inputs && inputs.length > 0) {
  1052. // Show review screen instead of generating immediately
  1053. displayReviewScreen(inputs, panel, {
  1054. amount,
  1055. content
  1056. });
  1057. } else {
  1058. showToast('Failed to generate image prompts', 'error');
  1059. generateButton.disabled = false;
  1060. generateButton.innerHTML = `${icons.generate}<span>Generate Images</span>`;
  1061. }
  1062. } catch (error) {
  1063. console.error('Batch generation error:', error);
  1064. showToast('Failed to generate prompts: ' + error.message, 'error');
  1065. generateButton.disabled = false;
  1066. generateButton.innerHTML = `${icons.generate}<span>Generate Images</span>`;
  1067. }
  1068. });
  1069.  
  1070. content.appendChild(amountField);
  1071. content.appendChild(contentField);
  1072. content.appendChild(generateButton);
  1073. content.appendChild(progressContainer);
  1074.  
  1075. panel.appendChild(header);
  1076. panel.appendChild(content);
  1077. overlay.appendChild(panel);
  1078. document.body.appendChild(overlay);
  1079. }
  1080.  
  1081. // Display review screen
  1082. function displayReviewScreen(entries, panel, originalInputs) {
  1083. // Clear existing content
  1084. const content = panel.querySelector('.ai-batch-content');
  1085. content.innerHTML = '';
  1086.  
  1087. // Create review container
  1088. const reviewContainer = document.createElement('div');
  1089. reviewContainer.className = 'review-container';
  1090.  
  1091. // Add each entry for review
  1092. entries.forEach((entry, index) => {
  1093. const reviewItem = document.createElement('div');
  1094. reviewItem.className = 'review-item';
  1095.  
  1096. const header = document.createElement('div');
  1097. header.className = 'review-item-header';
  1098. header.innerHTML = `<span class="review-item-number">Image ${index + 1}</span>`;
  1099.  
  1100. reviewItem.appendChild(header);
  1101.  
  1102. // Add editable fields
  1103. Object.entries(entry).forEach(([key, value]) => {
  1104. const field = document.createElement('div');
  1105. field.className = 'review-field';
  1106.  
  1107. const label = document.createElement('div');
  1108. label.className = 'review-field-label';
  1109. label.textContent = key.charAt(0).toUpperCase() + key.slice(1);
  1110.  
  1111. const input = document.createElement('input');
  1112. input.className = 'review-field-input';
  1113. input.type = 'text';
  1114. input.value = value;
  1115. input.dataset.index = index;
  1116. input.dataset.field = key;
  1117.  
  1118. // Update entries object when input changes
  1119. input.addEventListener('input', (e) => {
  1120. entries[index][key] = e.target.value;
  1121. });
  1122.  
  1123. field.appendChild(label);
  1124. field.appendChild(input);
  1125. reviewItem.appendChild(field);
  1126. });
  1127.  
  1128. reviewContainer.appendChild(reviewItem);
  1129. });
  1130.  
  1131. // Add action buttons
  1132. const actions = document.createElement('div');
  1133. actions.className = 'review-actions';
  1134.  
  1135. const backButton = document.createElement('button');
  1136. backButton.className = 'review-button review-back';
  1137. backButton.innerHTML = `<span>Back</span>`;
  1138. backButton.addEventListener('click', () => {
  1139. // Restore original panel content
  1140. displayAIBatchPanel();
  1141. });
  1142.  
  1143. const generateButton = document.createElement('button');
  1144. generateButton.className = 'review-button review-generate';
  1145. generateButton.innerHTML = `${icons.generate}<span>Generate Images</span>`;
  1146. generateButton.addEventListener('click', async () => {
  1147. generateButton.disabled = true;
  1148. generateButton.innerHTML = `${icons.loading}<span>Generating...</span>`;
  1149.  
  1150. try {
  1151. const results = await processBatchGeneration(entries);
  1152. console.log('Generation complete:', results);
  1153.  
  1154. const successCount = results.filter(r => r.success).length;
  1155. showToast(`Successfully generated ${successCount} images`);
  1156.  
  1157. if (successCount === 0) {
  1158. generateButton.disabled = false;
  1159. generateButton.innerHTML = `${icons.generate}<span>Generate Images</span>`;
  1160. } else {
  1161. panel.closest('.ai-batch-overlay').remove();
  1162. }
  1163. } catch (error) {
  1164. console.error('Generation error:', error);
  1165. showToast('Failed to generate images: ' + error.message, 'error');
  1166. generateButton.disabled = false;
  1167. generateButton.innerHTML = `${icons.generate}<span>Generate Images</span>`;
  1168. }
  1169. });
  1170.  
  1171. actions.appendChild(backButton);
  1172. actions.appendChild(generateButton);
  1173.  
  1174. content.appendChild(reviewContainer);
  1175. content.appendChild(actions);
  1176. }
  1177.  
  1178. // Add the AI batch button to the page
  1179. function addAIBatchButton() {
  1180. const container = document.querySelector('form');
  1181. if (!container) return;
  1182.  
  1183. const button = document.createElement('button');
  1184. button.className = 'ai-batch-button';
  1185. button.style.cssText = `
  1186. margin-top: 12px;
  1187. padding: 8px 16px;
  1188. background: rgb(100 48 247);
  1189. color: white;
  1190. border: none;
  1191. border-radius: 6px;
  1192. font-weight: 500;
  1193. cursor: pointer;
  1194. display: flex;
  1195. align-items: center;
  1196. justify-content: center;
  1197. gap: 8px;
  1198. width: 100%;
  1199. height: 48px;
  1200. transition: background-color 0.2s;
  1201. `;
  1202. button.innerHTML = `${icons.generate}<span>Batch Generator</span>`;
  1203. button.addEventListener('click', (e) => {
  1204. e.preventDefault();
  1205. displayAIBatchPanel();
  1206. });
  1207.  
  1208. container.appendChild(button);
  1209. }
  1210.  
  1211. // Initialize
  1212. function initialize() {
  1213. injectStyles();
  1214.  
  1215. // Wait for the form to be ready
  1216. const observer = new MutationObserver((mutations, obs) => {
  1217. if (document.querySelector('form')) {
  1218. addAIBatchButton();
  1219. obs.disconnect();
  1220. }
  1221. });
  1222.  
  1223. observer.observe(document.body, {
  1224. childList: true,
  1225. subtree: true
  1226. });
  1227. }
  1228.  
  1229. // Start the script
  1230. initialize();
  1231. })();