Guest User

Untitled

a guest
Mar 31st, 2026
147
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. // ==UserScript==
  2. // @name         xAI Pro Studio Frontend (v23 - Features Update)
  3. // @namespace    http://tampermonkey.net/
  4. // @version      23.0
  5. // @description  Full-Screen UI, Exif Fix, Multi-Image Routing, Debugger, Persistence, Settings & Tab Alerts
  6. // @match        https://console.x.ai/playground/imagine*
  7. // @match        https://console.x.ai/team/*/imagine*
  8. // @grant        none
  9. // @run-at       document-start
  10. // ==/UserScript==
  11.  
  12. (function() {
  13.     'use strict';
  14.  
  15.     // ─── UTILITIES & DATA STORAGE ─────────────────────────────────────────────
  16.     const sleep = (ms) => new Promise(r => setTimeout(r, ms));
  17.     function pad2(n) { return String(n).padStart(2, '0'); }
  18.     function makeTimestamp() {
  19.         const d = new Date();
  20.         return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}-${pad2(d.getMinutes())}-${pad2(d.getSeconds())}`;
  21.     }
  22.     const DOWNLOAD_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/></svg>`;
  23.  
  24.     // Load Settings
  25.     let appSettings = JSON.parse(localStorage.getItem('xai_api_settings') || '{"retries": 20, "delayMin": 2000, "delayMax": 4000, "notifications": true}');
  26.     appSettings.retries = parseInt(appSettings.retries) || 20;
  27.     appSettings.delayMin = parseInt(appSettings.delayMin) || 2000;
  28.     appSettings.delayMax = parseInt(appSettings.delayMax) || 4000;
  29.     if (typeof appSettings.notifications === 'undefined') appSettings.notifications = true;
  30.  
  31.     let favoritePrompts = JSON.parse(localStorage.getItem('xai_api_favs') || '[]');
  32.  
  33.     // ─── BACKGROUND ALERT SYSTEM (AUDIO + TAB BLINK) ──────────────────────────
  34.     let sharedAudioCtx = null;
  35.     let titleBlinkInterval = null;
  36.     const originalTitle = document.title || "xAI Pro Studio";
  37.  
  38.     function initAudio() {
  39.         if (!sharedAudioCtx && appSettings.notifications) {
  40.             try { sharedAudioCtx = new (window.AudioContext || window.webkitAudioContext)(); }
  41.             catch(e) { console.warn("AudioContext not supported"); }
  42.         }
  43.         if (sharedAudioCtx && sharedAudioCtx.state === 'suspended') sharedAudioCtx.resume();
  44.     }
  45.  
  46.     function notifyUser(isError = false) {
  47.         if (!appSettings.notifications) return;
  48.  
  49.         if (sharedAudioCtx) {
  50.             try {
  51.                 const osc = sharedAudioCtx.createOscillator();
  52.                 const gain = sharedAudioCtx.createGain();
  53.                 osc.connect(gain);
  54.                 gain.connect(sharedAudioCtx.destination);
  55.  
  56.                 if (isError) {
  57.                     osc.type = 'sawtooth';
  58.                     osc.frequency.setValueAtTime(300, sharedAudioCtx.currentTime);
  59.                     osc.frequency.exponentialRampToValueAtTime(100, sharedAudioCtx.currentTime + 0.3);
  60.                 } else {
  61.                     osc.type = 'sine';
  62.                     osc.frequency.setValueAtTime(500, sharedAudioCtx.currentTime);
  63.                     osc.frequency.exponentialRampToValueAtTime(1000, sharedAudioCtx.currentTime + 0.2);
  64.                 }
  65.  
  66.                 gain.gain.setValueAtTime(0.1, sharedAudioCtx.currentTime);
  67.                 gain.gain.exponentialRampToValueAtTime(0.01, sharedAudioCtx.currentTime + 0.5);
  68.  
  69.                 osc.start(sharedAudioCtx.currentTime);
  70.                 osc.stop(sharedAudioCtx.currentTime + 0.5);
  71.             } catch(e) {}
  72.         }
  73.  
  74.         if (!document.hasFocus()) {
  75.             if (titleBlinkInterval) clearInterval(titleBlinkInterval);
  76.             let toggle = true;
  77.             titleBlinkInterval = setInterval(() => {
  78.                 document.title = toggle ? (isError ? "❌ FAILED" : "✅ DONE") : originalTitle;
  79.                 toggle = !toggle;
  80.             }, 1000);
  81.  
  82.             const clearBlink = () => {
  83.                 clearInterval(titleBlinkInterval);
  84.                 document.title = originalTitle;
  85.                 window.removeEventListener('focus', clearBlink);
  86.             };
  87.             window.addEventListener('focus', clearBlink);
  88.         }
  89.     }
  90.  
  91.     // ─── STEALTH IDENTITY RESETTER ────────────────────────────────────────────
  92.     function stealthIdentityReset() {
  93.         const trackingCookies =['x-anonuserid', 'mixpanel', 'mp_', 'distinct_id'];
  94.         document.cookie.split(';').forEach(c => {
  95.             const name = c.split('=')[0].trim();
  96.             if (trackingCookies.some(tc => name.includes(tc))) {['.x.ai', 'console.x.ai', ''].forEach(d => {['/', '/playground/imagine', ''].forEach(p => {
  97.                         document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=${p}${d ? ';domain=' + d : ''}`;
  98.                     });
  99.                 });
  100.             }
  101.         });
  102.  
  103.         for (let i = localStorage.length - 1; i >= 0; i--) {
  104.             const k = localStorage.key(i);
  105.             if (k && !k.startsWith('xai_api_') && trackingCookies.some(tc => k.includes(tc))) {
  106.                 localStorage.removeItem(k);
  107.             }
  108.         }
  109.     }
  110.  
  111.     // ─── EXIF LIBRARY LOADER & WINDOWS METADATA ENCODER ───────────────────────
  112.     let piexifLoaded = false;
  113.     function loadPiexif() {
  114.         if (piexifLoaded) return Promise.resolve(window.piexif);
  115.         return new Promise(resolve => {
  116.             const script = document.createElement('script');
  117.             script.src = 'https://cdn.jsdelivr.net/npm/piexifjs';
  118.             script.onload = () => { piexifLoaded = true; resolve(window.piexif); };
  119.             document.head.appendChild(script);
  120.         });
  121.     }
  122.  
  123.     function toUTF16LE(str) {
  124.         const arr =[];
  125.         for (let i = 0; i < str.length; i++) {
  126.             const code = str.charCodeAt(i);
  127.             arr.push(code & 0xFF); arr.push((code >> 8) & 0xFF);
  128.         }
  129.         arr.push(0, 0); return arr;
  130.     }
  131.  
  132.     function convertToJpegWithExif(base64PngUri, prompt) {
  133.         return new Promise((resolve, reject) => {
  134.             const img = new Image();
  135.             img.onload = async () => {
  136.                 const canvas = document.createElement('canvas');
  137.                 canvas.width = img.width; canvas.height = img.height;
  138.                 const ctx = canvas.getContext('2d');
  139.                 ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, canvas.width, canvas.height);
  140.                 ctx.drawImage(img, 0, 0);
  141.  
  142.                 let jpegUri = canvas.toDataURL('image/jpeg', 0.95);
  143.                 if (prompt) {
  144.                     try {
  145.                         const piexif = await loadPiexif();
  146.                         const exifObj = {"0th": {}, "Exif": {}, "GPS": {}, "1st": {}, "Interop": {}};
  147.                         const safePrompt = unescape(encodeURIComponent(prompt));
  148.                         exifObj['0th'][piexif.ImageIFD.ImageDescription] = safePrompt;
  149.  
  150.                         const utf16Prompt = toUTF16LE(prompt);
  151.                         exifObj['0th'][40091] = utf16Prompt;
  152.                         exifObj['0th'][40092] = utf16Prompt;
  153.                         exifObj['0th'][40093] = utf16Prompt;
  154.  
  155.                         jpegUri = piexif.insert(piexif.dump(exifObj), jpegUri);
  156.                     } catch (e) {}
  157.                 }
  158.                 resolve(jpegUri);
  159.             };
  160.             img.onerror = reject; img.src = base64PngUri;
  161.         });
  162.     }
  163.  
  164.     async function extractPromptFromImage(file) {
  165.         if (!file.type.includes('image')) return null;
  166.         try {
  167.             const piexif = await loadPiexif();
  168.             return new Promise((resolve) => {
  169.                 const reader = new FileReader();
  170.                 reader.onload = (e) => {
  171.                     try {
  172.                         const exifData = piexif.load(e.target.result);
  173.                         let prompt = exifData['0th'] && exifData['0th'][piexif.ImageIFD.ImageDescription];
  174.                         if (Array.isArray(prompt)) prompt = String.fromCharCode.apply(null, prompt).replace(/\0/g, '');
  175.                         if (prompt) { try { prompt = decodeURIComponent(escape(prompt)); } catch(e) {} }
  176.  
  177.                         if (!prompt && exifData['0th'] && exifData['0th'][40091]) {
  178.                             const xpTitleArr = exifData['0th'][40091];
  179.                             let str = '';
  180.                             for (let i = 0; i < xpTitleArr.length; i += 2) {
  181.                                 const charCode = xpTitleArr[i] | (xpTitleArr[i+1] << 8);
  182.                                 if (charCode === 0) break;
  183.                                 str += String.fromCharCode(charCode);
  184.                             }
  185.                             prompt = str;
  186.                         }
  187.                         resolve(prompt ? prompt.trim() : null);
  188.                     } catch (err) { resolve(null); }
  189.                 };
  190.                 reader.readAsDataURL(file);
  191.             });
  192.         } catch (e) { return null; }
  193.     }
  194.  
  195.     // ─── ASPECT RATIO DATA ────────────────────────────────────────────────────
  196.     const arData =[
  197.         { label: 'Auto', w: 1, h: 1, isAuto: true },
  198.         { label: '1:1', w: 1, h: 1 }, { label: '3:4', w: 3, h: 4 }, { label: '4:3', w: 4, h: 3 },
  199.         { label: '9:16', w: 9, h: 16 }, { label: '16:9', w: 16, h: 9 }, { label: '2:3', w: 2, h: 3 },
  200.         { label: '3:2', w: 3, h: 2 }, { label: '9:19.5', w: 9, h: 19.5 }, { label: '19.5:9', w: 19.5, h: 9 },
  201.         { label: '9:20', w: 9, h: 20 }, { label: '20:9', w: 20, h: 9 }, { label: '1:2', w: 1, h: 2 }, { label: '2:1', w: 2, h: 1 }
  202.     ];
  203.  
  204.     function createArIcon(w, h, isAuto) {
  205.         if (isAuto) return `<div style="width: 14px; height: 14px; border: 1px dashed #64748b; border-radius: 2px; display: flex; align-items: center; justify-content: center; font-size: 10px; color: #94a3b8;">A</div>`;
  206.         const scale = 14 / Math.max(w, h);
  207.         return `<div style="width: 16px; height: 16px; display: flex; align-items: center; justify-content: center;"><div style="width: ${w * scale}px; height: ${h * scale}px; border: 1.5px solid #cbd5e1; border-radius: 2px;"></div></div>`;
  208.     }
  209.  
  210.     // ─── UI INJECTION ─────────────────────────────────────────────────────────
  211.     const UI_CSS = `
  212.         ::-webkit-scrollbar { width: 8px; }
  213.         ::-webkit-scrollbar-track { background: #0f172a; }
  214.         ::-webkit-scrollbar-thumb { background: #475569; border-radius: 10px; }
  215.  
  216.         @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
  217.         @keyframes pulse { 0% { opacity: 0.5; } 50% { opacity: 1; } 100% { opacity: 0.5; } }
  218.  
  219.         .loading-spinner { width: 16px; height: 16px; border: 2px solid rgba(255,255,255,0.3); border-top-color: #fff; border-radius: 50%; animation: spin 1s linear infinite; flex-shrink: 0; }
  220.         .btn-content { display: flex; align-items: center; justify-content: center; gap: 8px; }
  221.         .status-pulsing { animation: pulse 2s ease-in-out infinite; color: #3b82f6 !important; font-weight: 600; }
  222.  
  223.         #xai-pro-app { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 9999999; background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); font-family: 'Inter', -apple-system, sans-serif; color: #e2e8f0; display: flex; gap: 20px; padding: 20px; box-sizing: border-box; }
  224.         #xai-pro-app * { box-sizing: border-box; }
  225.  
  226.         .panel { background: #1e293b; border-radius: 16px; border: 1px solid #334155; box-shadow: 0 4px 20px rgba(0,0,0,0.4); display: flex; flex-direction: column; overflow: hidden; }
  227.         .panel-title { font-size: 11px; font-weight: 700; color: #94a3b8; letter-spacing: 1px; padding: 15px; border-bottom: 1px solid #334155; text-transform: uppercase; }
  228.  
  229.         .col-left { width: 340px; display: flex; flex-direction: column; gap: 20px; flex-shrink: 0; }
  230.         .col-center { flex: 1; display: flex; flex-direction: column; gap: 20px; min-width: 0; min-height: 0; }
  231.         .col-right { width: 340px; display: flex; flex-direction: column; gap: 20px; flex-shrink: 0; transition: 0.2s; }
  232.         .col-right.drag-over { background: #064e3b; border-color: #10b981; box-shadow: 0 0 0 4px rgba(16,185,129,0.15); border-radius: 16px; }
  233.  
  234.         textarea, select, input[type="text"], input[type="number"], input[type="checkbox"] { background: #0f172a; border: 1px solid #334155; border-radius: 8px; padding: 10px; color: #f8fafc; font-size: 14px; outline: none; transition: 0.2s; color-scheme: dark; }
  235.         textarea, select, input[type="text"], input[type="number"] { width: 100%; }
  236.         textarea:focus, select:focus, input[type="text"]:focus, input[type="number"]:focus { border-color: #64748b; background: #1e293b; box-shadow: 0 0 0 3px rgba(100,116,139,0.2); }
  237.         .prompt-drag-over { border-color: #10b981 !important; background: #064e3b !important; box-shadow: 0 0 0 3px rgba(16,185,129,0.15) !important; }
  238.         label { font-size: 12px; font-weight: 600; color: #94a3b8; margin-bottom: 6px; display: block; }
  239.  
  240.         .btn-primary { background: linear-gradient(180deg, #6366f1 0%, #4f46e5 100%); color: #fff; border: none; border-radius: 8px; padding: 12px 20px; font-size: 14px; font-weight: 600; cursor: pointer; transition: 0.2s; box-shadow: 0 2px 4px rgba(0,0,0,0.3); display: flex; justify-content: center; align-items: center; }
  241.         .btn-primary:hover:not(:disabled) { background: linear-gradient(180deg, #4f46e5 0%, #4338ca 100%); transform: translateY(-1px); }
  242.         .btn-primary:disabled { opacity: 0.8; cursor: not-allowed; }
  243.  
  244.         .btn-secondary { background: #1e293b; color: #cbd5e1; border: 1px solid #475569; border-radius: 8px; padding: 12px 20px; font-size: 14px; font-weight: 600; cursor: pointer; transition: 0.2s; display: flex; justify-content: center; align-items: center; }
  245.         .btn-secondary:hover:not(:disabled) { background: #334155; border-color: #64748b; }
  246.  
  247.         .ar-select-box { background: #0f172a; border: 1px solid #334155; border-radius: 8px; padding: 10px; display: flex; align-items: center; gap: 10px; cursor: pointer; color: #f8fafc; font-size: 14px; }
  248.         #ar-options-container { position: fixed; background: #1e293b; border: 1px solid #334155; border-radius: 8px; box-shadow: 0 10px 25px rgba(0,0,0,0.4); max-height: 250px; overflow-y: auto; display: none; z-index: 99999999; }
  249.         .ar-option { display: flex; align-items: center; gap: 10px; padding: 8px 10px; cursor: pointer; font-size: 13px; color: #cbd5e1; }
  250.         .ar-option:hover { background: #334155; color: #f8fafc; }
  251.  
  252.         .img-wrapper { position: relative; display: block; }
  253.         .zoomable { cursor: zoom-in; transition: transform 0.2s; width: 100%; display: block; }
  254.         .overlay-dl-btn { position: absolute; top: 10px; right: 10px; background: rgba(15, 23, 42, 0.6); color: white; border-radius: 8px; width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; text-decoration: none; backdrop-filter: blur(4px); transition: 0.2s; opacity: 0.75; z-index: 50; }
  255.         .overlay-dl-btn:hover { background: rgba(15, 23, 42, 0.9); opacity: 1; transform: scale(1.05); }
  256.  
  257.         .ref-item { display: flex; align-items: center; gap: 15px; padding: 12px; background: #1e293b; border: 1px solid #334155; border-radius: 8px; margin-bottom: 10px; cursor: grab; box-shadow: 0 2px 5px rgba(0,0,0,0.2); transition: 0.1s; }
  258.         .ref-item.inactive { opacity: 0.6; filter: grayscale(0.8); }
  259.         .ref-item:active { cursor: grabbing; }
  260.         .ref-item.dragging { opacity: 0.4; }
  261.         .ref-handle { font-size: 18px; color: #475569; cursor: grab; padding: 0 5px; }
  262.         .ref-thumb { width: 140px; height: 140px; border-radius: 6px; object-fit: cover; border: 1px solid #334155; background: #0f172a; transition: 0.2s; }
  263.         .ref-info { flex: 1; display: flex; flex-direction: column; justify-content: center; gap: 8px; }
  264.         .ref-toggle-label { font-size: 12px; color: #cbd5e1; display: flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; }
  265.         .ref-toggle-label input { width: 14px; height: 14px; cursor: pointer; accent-color: #10b981; margin: 0; padding: 0; }
  266.         .ref-del { background: #450a0a; color: #fca5a5; border: 1px solid #7f1d1d; border-radius: 6px; padding: 8px; cursor: pointer; font-size: 12px; transition: 0.2s; font-weight: bold; width: 100%; text-align: center; }
  267.         .ref-del:hover { background: #7f1d1d; color: #fee2e2; }
  268.         .drag-over-top { border-top: 3px solid #22c55e !important; }
  269.         .drag-over-bottom { border-bottom: 3px solid #22c55e !important; }
  270.  
  271.         .history-card { background: #0f172a; border: 1px solid #334155; border-radius: 8px; overflow: hidden; margin-bottom: 15px; }
  272.         .history-card .overlay-dl-btn { width: 28px; height: 28px; top: 6px; right: 6px; border-radius: 6px; }
  273.         .history-card p { padding: 10px; margin: 0; font-size: 11px; color: #94a3b8; cursor: pointer; transition: 0.2s; }
  274.         .history-card p:hover { background: #1e293b; color: #e2e8f0; }
  275.         .current-card { border-radius: 12px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.3); border: 1px solid #334155; background: #1e293b; }
  276.         .current-card img, .current-card video { max-height: 50vh; object-fit: contain; background: #0f172a; }
  277.  
  278.         /* Modals Generic Classes */
  279.         .modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.8); z-index: 999999999; display: none; justify-content: center; align-items: center; backdrop-filter: blur(5px); }
  280.         .modal-box { background: #1e293b; border-radius: 12px; width: 500px; max-width: 90vw; padding: 20px; display: flex; flex-direction: column; gap: 15px; box-shadow: 0 10px 40px rgba(0,0,0,0.5); border: 1px solid #334155; max-height: 85vh; }
  281.         .fav-item { display: flex; justify-content: space-between; align-items: center; background: #0f172a; padding: 10px; border-radius: 8px; border: 1px solid #334155; }
  282.         .fav-text { flex: 1; font-size: 13px; color: #cbd5e1; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-right: 15px; transition: color 0.2s; }
  283.         .fav-text:hover { color: #10b981; }
  284.  
  285.         #settings-btn { position: fixed; top: 15px; right: 20px; font-size: 24px; cursor: pointer; z-index: 100; transition: transform 0.2s; }
  286.         #settings-btn:hover { transform: rotate(45deg); }
  287.  
  288.         #xai-lightbox { cursor: zoom-out; }
  289.         #xai-lightbox-wrapper { position: relative; display: inline-block; max-width: 95vw; max-height: 95vh; }
  290.         #xai-lightbox-img { max-width: 100%; max-height: 95vh; display: block; border-radius: 8px; box-shadow: 0 10px 40px rgba(0,0,0,0.5); }
  291.     `;
  292.  
  293.     const UI_HTML = `
  294.         <div id="xai-pro-app">
  295.             <div id="settings-btn" title="API Settings">⚙️</div>
  296.  
  297.             <!-- LEFT COLUMN -->
  298.             <div class="col-left">
  299.                 <div class="panel" style="flex-shrink: 0;">
  300.                     <div style="display: flex; background: rgba(0,0,0,0.2); border-radius: 0 0 8px 8px; margin-bottom: 15px; border-bottom: 1px solid #334155; padding: 15px;">
  301.                         <div style="flex: 1;">
  302.                             <label>Master Action</label>
  303.                             <select id="master-action" style="font-weight: bold; color: #f8fafc; border-color: #475569; box-shadow: 0 2px 4px rgba(0,0,0,0.3);">
  304.                                 <option value="gen_image">🖼️ Generate Image</option>
  305.                                 <option value="gen_video">🎥 Generate Video</option>
  306.                                 <option value="edit_video">✂️ Edit Video</option>
  307.                                 <option value="extend_video">➡️ Extend Video</option>
  308.                             </select>
  309.                         </div>
  310.                     </div>
  311.  
  312.                     <div style="padding: 15px; display: flex; flex-direction: column; gap: 15px;">
  313.                         <div id="setting-ar">
  314.                             <label>Aspect Ratio</label>
  315.                             <div class="ar-select-box" id="ar-select-box">
  316.                                 <div id="ar-selected-icon"></div>
  317.                                 <span id="ar-selected-text">Auto</span>
  318.                             </div>
  319.                         </div>
  320.  
  321.                         <div style="display: flex; gap: 10px;">
  322.                             <div id="setting-res-img" style="flex: 1;">
  323.                                 <label>Resolution</label>
  324.                                 <select id="xai-api-res-img">
  325.                                     <option value="2k" selected>2K (Pro)</option>
  326.                                     <option value="1k">1K</option>
  327.                                 </select>
  328.                             </div>
  329.                             <div id="setting-res-vid" style="flex: 1; display: none;">
  330.                                 <label>Resolution</label>
  331.                                 <select id="xai-api-res-vid">
  332.                                     <option value="720p" selected>720p (HD)</option>
  333.                                     <option value="480p">480p (SD)</option>
  334.                                 </select>
  335.                             </div>
  336.  
  337.                             <div id="setting-batch" style="flex: 1;">
  338.                                 <label>Batch Size</label>
  339.                                 <select id="xai-api-n">
  340.                                     <option value="1" selected>1 Image</option>
  341.                                     <option value="2">2 Images</option>
  342.                                     <option value="3">3 Images</option>
  343.                                     <option value="4">4 Images</option>
  344.                                     <option value="5">5 Images</option>
  345.                                     <option value="8">8 Images</option>
  346.                                     <option value="10">10 Images</option>
  347.                                 </select>
  348.                             </div>
  349.                             <div id="setting-duration" style="flex: 1; display: none;">
  350.                                 <label>Duration</label>
  351.                                 <select id="xai-api-duration">
  352.                                     <option value="5" selected>5 Seconds</option>
  353.                                     <option value="8">8 Seconds</option>
  354.                                     <option value="10">10 Seconds</option>
  355.                                     <option value="12">12 Seconds</option>
  356.                                     <option value="15">15 Seconds</option>
  357.                                     <option value="2">2s Extension</option>
  358.                                     <option value="4">4s Extension</option>
  359.                                     <option value="6">6s Extension</option>
  360.                                 </select>
  361.                             </div>
  362.                         </div>
  363.                     </div>
  364.                 </div>
  365.  
  366.                 <div class="panel" style="flex: 1; min-height: 0;">
  367.                     <div class="panel-title" style="display:flex; justify-content: space-between; align-items: center;">
  368.                         <span>Previous Results</span>
  369.                         <span id="xai-reset-btn" style="cursor: pointer; color: #ef4444; text-transform: none; text-decoration: underline;">Wipe Cache</span>
  370.                     </div>
  371.                     <div id="xai-history" style="flex: 1; overflow-y: auto; padding: 15px;">
  372.                         <div style="color: #94a3b8; font-size: 12px; text-align: center; margin-top: 20px;">No history yet.</div>
  373.                     </div>
  374.                 </div>
  375.             </div>
  376.  
  377.             <!-- CENTER COLUMN -->
  378.             <div class="col-center">
  379.                 <div class="panel" style="flex: 1; background: transparent; border: none; box-shadow: none; min-height: 0;">
  380.                     <div class="panel-title" style="background: #1e293b; border-radius: 12px; border: 1px solid #334155; margin-bottom: 15px; flex-shrink: 0;">Current Generation</div>
  381.                     <div id="xai-current-images" style="flex: 1; overflow-y: auto; display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; align-content: start; padding-bottom: 10px;"></div>
  382.                 </div>
  383.  
  384.                 <div class="panel" style="flex-shrink: 0; padding: 20px;">
  385.                     <label style="font-size: 14px; color: #e2e8f0; display: flex; justify-content: space-between; align-items: center;">
  386.                         <span style="display: flex; align-items: center; gap: 10px;">
  387.                             <span>Prompt</span>
  388.                             <button id="fav-prompts-btn" class="btn-secondary" style="padding: 4px 10px; font-size: 11px; height: 26px;">⭐ Favorites</button>
  389.                         </span>
  390.                         <span style="font-size: 11px; font-weight: 400; color: #94a3b8;">(Drop an image here to extract its EXIF prompt)</span>
  391.                     </label>
  392.                     <textarea id="xai-api-prompt" placeholder="Describe what you want to see..." style="height: 160px; min-height: 120px; margin-bottom: 15px; font-size: 15px;"></textarea>
  393.  
  394.                     <div style="display: flex; align-items: center; justify-content: space-between;">
  395.                         <div id="xai-api-status" style="font-size: 13px; color: #64748b; font-weight: 500;">Ready</div>
  396.                         <div style="display: flex; gap: 10px;">
  397.                             <button id="xai-preview-btn" class="btn-secondary">🔍 View Payload</button>
  398.                             <button id="xai-cancel-btn" class="btn-secondary" style="display: none; color: #ef4444; border-color: #ef4444;">🛑 Cancel</button>
  399.                             <button id="xai-api-generate" class="btn-primary" style="width: 150px; font-size: 15px;"><div class="btn-content">Generate</div></button>
  400.                         </div>
  401.                     </div>
  402.                 </div>
  403.             </div>
  404.  
  405.             <!-- RIGHT COLUMN -->
  406.             <div class="col-right" id="col-right-dropzone">
  407.                 <div class="panel" style="flex: 1; display: flex; flex-direction: column; min-height: 0;">
  408.                     <div class="panel-title" id="ref-panel-title">Reference Media</div>
  409.                     <div style="padding: 15px; flex: 1; display: flex; flex-direction: column; overflow: hidden;">
  410.                         <div id="xai-upload-placeholder" style="border: 2px dashed #475569; border-radius: 12px; width: 100%; padding: 20px 20px; color: #94a3b8; cursor: pointer; text-align: center; transition: 0.2s; margin-bottom: 10px; background: rgba(0,0,0,0.1);">
  411.                             <div style="font-size: 24px; margin-bottom: 5px;">📥</div>
  412.                             <div style="font-size: 13px; font-weight: 600;">Drag & Drop or Ctrl+V</div>
  413.                             <div style="font-size: 11px; margin-top: 5px;">(Click to browse)</div>
  414.                         </div>
  415.                         <input type="file" id="xai-api-file" accept="image/*, video/mp4, video/webm" multiple style="display: none;">
  416.                         <div style="font-size: 11px; color: #94a3b8; margin-bottom: 5px; text-transform: uppercase; font-weight: bold; flex-shrink: 0;">Upload Order (Top = First)</div>
  417.                         <div id="xai-ref-list" style="flex: 1; overflow-y: auto; padding-right: 5px;"></div>
  418.                     </div>
  419.                 </div>
  420.             </div>
  421.  
  422.             <!-- FIXED FLOATING DROPDOWN -->
  423.             <div id="ar-options-container"></div>
  424.  
  425.             <!-- MODALS -->
  426.             <div id="xai-lightbox" class="modal-overlay">
  427.                 <div id="xai-lightbox-wrapper">
  428.                     <img id="xai-lightbox-img" src="">
  429.                     <a id="xai-lightbox-dl" href="#" download="" target="_blank" class="overlay-dl-btn" style="top: 15px; right: 15px; width: 44px; height: 44px; border-radius: 50%; background: rgba(0,0,0,0.7);" title="Download File">${DOWNLOAD_ICON}</a>
  430.                 </div>
  431.             </div>
  432.  
  433.             <!-- SETTINGS MODAL -->
  434.             <div id="xai-settings-modal" class="modal-overlay">
  435.                 <div class="modal-box" style="width: 400px;">
  436.                     <h3 style="margin:0; color: #f8fafc;">⚙️ Studio Settings</h3>
  437.                     <div>
  438.                         <label>Max Retries (429/500 Errors)</label>
  439.                         <input type="number" id="set-retries" min="1" max="100">
  440.                     </div>
  441.                     <div style="display:flex; gap:10px;">
  442.                         <div style="flex:1;">
  443.                             <label>Min Delay Range (ms)</label>
  444.                             <input type="number" id="set-delay-min" min="500" step="500">
  445.                         </div>
  446.                         <div style="flex:1;">
  447.                             <label>Max Delay Range (ms)</label>
  448.                             <input type="number" id="set-delay-max" min="1000" step="500">
  449.                         </div>
  450.                     </div>
  451.                     <div style="display:flex; align-items:center; gap: 10px; margin-top: 10px;">
  452.                         <input type="checkbox" id="set-notifications" style="width: 18px; height: 18px; margin: 0; cursor: pointer; accent-color: #10b981;">
  453.                         <label for="set-notifications" style="margin: 0; cursor: pointer;">Enable Notifications (Sound & Tab Blink)</label>
  454.                     </div>
  455.                     <div style="display: flex; gap: 10px; margin-top: 10px;">
  456.                         <button id="save-settings-btn" class="btn-primary" style="flex:1;">Save Settings</button>
  457.                         <button id="close-settings-btn" class="btn-secondary" style="flex:1;">Cancel</button>
  458.                     </div>
  459.                 </div>
  460.             </div>
  461.  
  462.             <!-- FAVORITES MODAL -->
  463.             <div id="xai-fav-modal" class="modal-overlay">
  464.                 <div class="modal-box" style="width: 600px;">
  465.                     <div style="display: flex; justify-content: space-between; align-items: center;">
  466.                         <h3 style="margin:0; color: #f8fafc;">⭐ Favorite Prompts</h3>
  467.                         <button id="close-fav-btn" style="background: none; border: none; color: #ef4444; font-size: 24px; cursor: pointer; font-weight: bold; line-height: 1;">&times;</button>
  468.                     </div>
  469.                     <button id="add-current-fav-btn" class="btn-primary" style="background: linear-gradient(180deg, #10b981 0%, #059669 100%);">➕ Save Current Prompt to Favorites</button>
  470.                     <div id="fav-list" style="flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 8px; padding-right: 5px;"></div>
  471.                 </div>
  472.             </div>
  473.  
  474.             <!-- CUSTOM PAYLOAD DEBUGGER MODAL -->
  475.             <div id="xai-payload-modal" class="modal-overlay">
  476.                 <div class="modal-box" style="width: 600px;">
  477.                     <div style="display: flex; justify-content: space-between; align-items: center;">
  478.                         <h3 style="margin:0; font-size: 16px; color: #f8fafc;">API Payload Debugger</h3>
  479.                         <button id="close-payload-btn" style="background: #ef4444; color: #fff; border: none; border-radius: 6px; padding: 5px 10px; cursor: pointer; font-weight: bold;">Close</button>
  480.                     </div>
  481.                     <div><label>Endpoint URL:</label><input type="text" id="payload-endpoint" style="font-family: monospace;" value="/v1/images/generations"></div>
  482.                     <div><label>JSON Payload (Editable):</label><textarea id="payload-code" style="background: #0f172a; color: #e2e8f0; padding: 15px; border-radius: 8px; font-family: monospace; font-size: 13px; height: 300px; resize: vertical; border: 1px solid #334155;"></textarea></div>
  483.                     <button id="send-custom-payload-btn" class="btn-primary" style="background: linear-gradient(180deg, #10b981 0%, #059669 100%);"><div class="btn-content">🚀 Send Custom Payload</div></button>
  484.                     <div id="custom-payload-status" style="font-size: 12px; color: #64748b; text-align: center;">Ready</div>
  485.                 </div>
  486.             </div>
  487.         </div>
  488.     `;
  489.  
  490.     // ─── INITIALIZATION ───────────────────────────────────────────────────────
  491.     const initApp = () => {
  492.         if (!document.body) return setTimeout(initApp, 50);
  493.  
  494.         const hideOriginals = () => {
  495.             Array.from(document.body.children).forEach(child => {
  496.                 if (child.id !== 'xai-pro-app' && !['SCRIPT', 'STYLE', 'LINK'].includes(child.tagName)) {
  497.                     child.style.display = 'none';
  498.                 }
  499.             });
  500.         };
  501.         hideOriginals();
  502.         new MutationObserver(hideOriginals).observe(document.body, { childList: true });
  503.  
  504.         if (!document.getElementById('xai-pro-app')) {
  505.             const style = document.createElement('style');
  506.             style.innerHTML = UI_CSS;
  507.             document.head.appendChild(style);
  508.             document.body.insertAdjacentHTML('beforeend', UI_HTML);
  509.             bindLogic();
  510.         }
  511.     };
  512.  
  513.     let referenceMedia =[];
  514.     let currentAr = localStorage.getItem('xai_api_ar') || 'Auto';
  515.     let fullPayloadMemory = {};
  516.     let currentAbortController = null;
  517.     let isRequestCancelled = false;
  518.  
  519.     // Helper to safely persist references to memory
  520.     function saveRefs() {
  521.         try {
  522.             localStorage.setItem('xai_api_refs', JSON.stringify(referenceMedia));
  523.         } catch (e) {
  524.             console.warn("[xAI Studio] Could not save references to localStorage (quota exceeded). Images will not persist after refresh.");
  525.         }
  526.     }
  527.  
  528.     function bindLogic() {
  529.         const els = {
  530.             app: document.getElementById('xai-pro-app'),
  531.             prompt: document.getElementById('xai-api-prompt'),
  532.             action: document.getElementById('master-action'),
  533.             resImg: document.getElementById('xai-api-res-img'),
  534.             resVid: document.getElementById('xai-api-res-vid'),
  535.             n: document.getElementById('xai-api-n'),
  536.             duration: document.getElementById('xai-api-duration'),
  537.  
  538.             settingAr: document.getElementById('setting-ar'),
  539.             settingResImg: document.getElementById('setting-res-img'),
  540.             settingResVid: document.getElementById('setting-res-vid'),
  541.             settingBatch: document.getElementById('setting-batch'),
  542.             settingDuration: document.getElementById('setting-duration'),
  543.  
  544.             fileInput: document.getElementById('xai-api-file'),
  545.             dropzone: document.getElementById('col-right-dropzone'),
  546.             uploadPlaceholder: document.getElementById('xai-upload-placeholder'),
  547.             refList: document.getElementById('xai-ref-list'),
  548.             refTitle: document.getElementById('ref-panel-title'),
  549.  
  550.             btn: document.getElementById('xai-api-generate'),
  551.             previewBtn: document.getElementById('xai-preview-btn'),
  552.             cancelBtn: document.getElementById('xai-cancel-btn'),
  553.             status: document.getElementById('xai-api-status'),
  554.             history: document.getElementById('xai-history'),
  555.             currentImages: document.getElementById('xai-current-images'),
  556.             reset: document.getElementById('xai-reset-btn'),
  557.  
  558.             arSelectBox: document.getElementById('ar-select-box'),
  559.             arOptionsCont: document.getElementById('ar-options-container'),
  560.             arSelectedIcon: document.getElementById('ar-selected-icon'),
  561.             arSelectedText: document.getElementById('ar-selected-text'),
  562.  
  563.             lightbox: document.getElementById('xai-lightbox'),
  564.             lightboxImg: document.getElementById('xai-lightbox-img'),
  565.             lightboxDl: document.getElementById('xai-lightbox-dl'),
  566.  
  567.             payloadModal: document.getElementById('xai-payload-modal'),
  568.             payloadEndpoint: document.getElementById('payload-endpoint'),
  569.             payloadCode: document.getElementById('payload-code'),
  570.             closePayloadBtn: document.getElementById('close-payload-btn'),
  571.             sendCustomBtn: document.getElementById('send-custom-payload-btn'),
  572.             customStatus: document.getElementById('custom-payload-status'),
  573.  
  574.             settingsBtn: document.getElementById('settings-btn'),
  575.             settingsModal: document.getElementById('xai-settings-modal'),
  576.             closeSettingsBtn: document.getElementById('close-settings-btn'),
  577.             saveSettingsBtn: document.getElementById('save-settings-btn'),
  578.  
  579.             favBtn: document.getElementById('fav-prompts-btn'),
  580.             favModal: document.getElementById('xai-fav-modal'),
  581.             closeFavBtn: document.getElementById('close-fav-btn'),
  582.             addFavBtn: document.getElementById('add-current-fav-btn'),
  583.             favList: document.getElementById('fav-list')
  584.         };
  585.  
  586.         // ─── LOAD PERSISTED SETTINGS & REFERENCES ──────────────────────────────────
  587.         if (localStorage.getItem('xai_api_prompt')) els.prompt.value = localStorage.getItem('xai_api_prompt');
  588.         els.prompt.addEventListener('input', () => localStorage.setItem('xai_api_prompt', els.prompt.value));
  589.  
  590.         try {
  591.             const storedGen = JSON.parse(localStorage.getItem('xai_api_gen_settings') || '{}');
  592.             if (storedGen.action) els.action.value = storedGen.action;
  593.             if (storedGen.resImg) els.resImg.value = storedGen.resImg;
  594.             if (storedGen.resVid) els.resVid.value = storedGen.resVid;
  595.             if (storedGen.n) els.n.value = storedGen.n;
  596.             if (storedGen.duration) els.duration.value = storedGen.duration;
  597.         } catch(e) {}
  598.  
  599.         const updateGenSettingsMemory = () => {
  600.             localStorage.setItem('xai_api_gen_settings', JSON.stringify({
  601.                 action: els.action.value, resImg: els.resImg.value, resVid: els.resVid.value, n: els.n.value, duration: els.duration.value
  602.             }));
  603.         };
  604.         [els.action, els.resImg, els.resVid, els.n, els.duration].forEach(el => el.addEventListener('change', updateGenSettingsMemory));
  605.  
  606.         try {
  607.             const storedRefs = JSON.parse(localStorage.getItem('xai_api_refs'));
  608.             if (Array.isArray(storedRefs)) referenceMedia = storedRefs;
  609.         } catch(e) {}
  610.  
  611.         // ─── SETTINGS BINDINGS ───────────────────────────────────────────────────
  612.         els.settingsBtn.addEventListener('click', () => {
  613.             document.getElementById('set-retries').value = appSettings.retries;
  614.             document.getElementById('set-delay-min').value = appSettings.delayMin;
  615.             document.getElementById('set-delay-max').value = appSettings.delayMax;
  616.             document.getElementById('set-notifications').checked = appSettings.notifications;
  617.             els.settingsModal.style.display = 'flex';
  618.         });
  619.  
  620.         els.saveSettingsBtn.addEventListener('click', () => {
  621.             appSettings.retries = parseInt(document.getElementById('set-retries').value) || 20;
  622.             appSettings.delayMin = parseInt(document.getElementById('set-delay-min').value) || 2000;
  623.             appSettings.delayMax = Math.max(appSettings.delayMin, parseInt(document.getElementById('set-delay-max').value) || 4000);
  624.             appSettings.notifications = document.getElementById('set-notifications').checked;
  625.             localStorage.setItem('xai_api_settings', JSON.stringify(appSettings));
  626.             els.settingsModal.style.display = 'none';
  627.         });
  628.  
  629.         els.closeSettingsBtn.addEventListener('click', () => els.settingsModal.style.display = 'none');
  630.  
  631.         // ─── FAVORITES BINDINGS ──────────────────────────────────────────────────
  632.         function renderFavList() {
  633.             els.favList.innerHTML = '';
  634.             if (favoritePrompts.length === 0) {
  635.                 els.favList.innerHTML = '<div style="color: #64748b; font-size: 13px; text-align: center; margin-top: 20px;">No favorite prompts yet.</div>';
  636.                 return;
  637.             }
  638.             favoritePrompts.forEach((promptText, idx) => {
  639.                 const div = document.createElement('div');
  640.                 div.className = 'fav-item';
  641.                 div.innerHTML = `
  642.                     <div class="fav-text" title="${promptText.replace(/"/g, '&quot;')}">${promptText}</div>
  643.                    <button class="btn-secondary" style="padding: 4px 8px; font-size: 11px; border-color: #ef4444; color: #ef4444;">Del</button>
  644.                `;
  645.                div.querySelector('.fav-text').addEventListener('click', () => {
  646.                    els.prompt.value = promptText;
  647.                    localStorage.setItem('xai_api_prompt', promptText);
  648.                    els.favModal.style.display = 'none';
  649.                });
  650.                div.querySelector('button').addEventListener('click', () => {
  651.                    favoritePrompts.splice(idx, 1);
  652.                    localStorage.setItem('xai_api_favs', JSON.stringify(favoritePrompts));
  653.                    renderFavList();
  654.                });
  655.                els.favList.appendChild(div);
  656.            });
  657.        }
  658.  
  659.        els.favBtn.addEventListener('click', () => {
  660.            renderFavList();
  661.            els.favModal.style.display = 'flex';
  662.        });
  663.        els.closeFavBtn.addEventListener('click', () => els.favModal.style.display = 'none');
  664.        els.addFavBtn.addEventListener('click', () => {
  665.            const p = els.prompt.value.trim();
  666.            if (!p) return alert("Prompt is empty!");
  667.            if (favoritePrompts.includes(p)) return alert("Already in favorites!");
  668.            favoritePrompts.unshift(p);
  669.            localStorage.setItem('xai_api_favs', JSON.stringify(favoritePrompts));
  670.            renderFavList();
  671.        });
  672.  
  673.        // ─── PROMPT BOX EXIF DRAG & DROP ──────────────────────────────────────────
  674.        els.prompt.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); els.prompt.classList.add('prompt-drag-over'); });
  675.        els.prompt.addEventListener('dragleave', (e) => { e.preventDefault(); e.stopPropagation(); els.prompt.classList.remove('prompt-drag-over'); });
  676.        els.prompt.addEventListener('drop', async (e) => {
  677.            e.preventDefault(); e.stopPropagation();
  678.            els.prompt.classList.remove('prompt-drag-over');
  679.            if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
  680.                const file = e.dataTransfer.files[0];
  681.                const oldVal = els.prompt.value;
  682.                els.prompt.value = "Extracting prompt from image EXIF...";
  683.                const extractedPrompt = await extractPromptFromImage(file);
  684.                if (extractedPrompt) {
  685.                    els.prompt.value = extractedPrompt;
  686.                    localStorage.setItem('xai_api_prompt', extractedPrompt);
  687.                } else {
  688.                    els.prompt.value = oldVal;
  689.                    alert("No prompt found in this image's EXIF data (or not a valid JPEG).");
  690.                }
  691.            }
  692.        });
  693.  
  694.        // ─── MASTER ACTION UI LOGIC ───────────────────────────────────────────────
  695.        function getMaxMedia() {
  696.            const val = els.action.value;
  697.            if (val === 'gen_image') return 5;
  698.            if (val === 'gen_video') return 7;
  699.            return 1;
  700.        }
  701.  
  702.        function updateActionUI() {
  703.            const val = els.action.value;
  704.  
  705.            if (val === 'gen_image') {
  706.                els.settingAr.style.display = 'block'; els.settingResImg.style.display = 'block'; els.settingResVid.style.display = 'none'; els.settingBatch.style.display = 'block'; els.settingDuration.style.display = 'none';
  707.            } else if (val === 'gen_video') {
  708.                els.settingAr.style.display = 'block'; els.settingResImg.style.display = 'none'; els.settingResVid.style.display = 'block'; els.settingBatch.style.display = 'none'; els.settingDuration.style.display = 'block';
  709.                els.duration.innerHTML = `<option value="5">5 Seconds</option><option value="8">8 Seconds</option><option value="10">10 Seconds</option><option value="12">12 Seconds</option><option value="15">15 Seconds</option>`;
  710.                if (localStorage.getItem('xai_api_gen_settings') && JSON.parse(localStorage.getItem('xai_api_gen_settings')).duration <= 15) {
  711.                    els.duration.value = JSON.parse(localStorage.getItem('xai_api_gen_settings')).duration;
  712.                } else els.duration.value = "5";
  713.            } else if (val === 'edit_video') {
  714.                els.settingAr.style.display = 'none'; els.settingResImg.style.display = 'none'; els.settingResVid.style.display = 'none'; els.settingBatch.style.display = 'none'; els.settingDuration.style.display = 'none';
  715.            } else if (val === 'extend_video') {
  716.                els.settingAr.style.display = 'none'; els.settingResImg.style.display = 'none'; els.settingResVid.style.display = 'none'; els.settingBatch.style.display = 'none'; els.settingDuration.style.display = 'block';
  717.                els.duration.innerHTML = `<option value="2">2s Extension</option><option value="4">4s Extension</option><option value="6">6s Extension</option><option value="8">8s Extension</option><option value="10">10s Extension</option>`;
  718.                if (localStorage.getItem('xai_api_gen_settings') && JSON.parse(localStorage.getItem('xai_api_gen_settings')).duration <= 10) {
  719.                    els.duration.value = JSON.parse(localStorage.getItem('xai_api_gen_settings')).duration;
  720.                } else els.duration.value = "6";
  721.            }
  722.  
  723.            const max = getMaxMedia();
  724.            els.refTitle.innerText = `Reference Media (Max Active: ${max})`;
  725.  
  726.            // Instead of wiping the array, softly deactivate incompatible or excess items
  727.            let activeCount = 0;
  728.            referenceMedia.forEach(m => {
  729.                let valid = true;
  730.                if ((val === 'gen_image' || val === 'gen_video') && m.isVideo) valid = false;
  731.                if ((val === 'edit_video' || val === 'extend_video') && !m.isVideo) valid = false;
  732.  
  733.                if (!valid) m.active = false;
  734.                else if (m.active) {
  735.                    activeCount++;
  736.                    if (activeCount > max) m.active = false;
  737.                }
  738.            });
  739.            renderRefList();
  740.        }
  741.  
  742.        els.action.addEventListener('change', updateActionUI);
  743.        updateActionUI(); // Call once on load
  744.  
  745.        // ─── BUILD PAYLOAD LOGIC ──────────────────────────────────────────────────
  746.        function buildPayloadData() {
  747.            const action = els.action.value;
  748.            const payload = {
  749.                model: action === 'gen_image' ? "grok-imagine-image" : "grok-imagine-video",
  750.                prompt: els.prompt.value.trim()
  751.            };
  752.  
  753.            const ar = currentAr.toLowerCase();
  754.            const activeRefs = referenceMedia.filter(m => m.active); // Enforce only active
  755.  
  756.            if (action === 'gen_image') {
  757.                payload.n = parseInt(els.n.value); payload.resolution = els.resImg.value; payload.response_format = "b64_json";
  758.                if (ar !== 'auto') payload.aspect_ratio = ar; else if (activeRefs.length > 0) payload.aspect_ratio = 'auto';
  759.  
  760.                if (activeRefs.length > 0) {
  761.                    if (activeRefs.length === 1) payload.image = { url: activeRefs[0].base64 };
  762.                    else payload.images = activeRefs.map(m => ({ type: "image_url", url: m.base64 }));
  763.                }
  764.            } else if (action === 'gen_video') {
  765.                payload.duration = parseInt(els.duration.value); payload.resolution = els.resVid.value;
  766.                if (ar !== 'auto') payload.aspect_ratio = ar;
  767.                if (activeRefs.length > 0) payload.reference_images = activeRefs.map(m => ({ url: m.base64 }));
  768.            } else if (action === 'edit_video') {
  769.                if (activeRefs.length > 0) payload.video = { url: activeRefs[0].base64 };
  770.            } else if (action === 'extend_video') {
  771.                payload.duration = parseInt(els.duration.value);
  772.                if (activeRefs.length > 0) payload.video = { url: activeRefs[0].base64 };
  773.            }
  774.            return payload;
  775.        }
  776.  
  777.        // ─── CUSTOM PAYLOAD DEBUGGER LOGIC ────────────────────────────────────────
  778.        els.previewBtn.addEventListener('click', () => {
  779.            fullPayloadMemory = buildPayloadData();
  780.            const displayP = JSON.parse(JSON.stringify(fullPayloadMemory));
  781.            const trunc = "[BASE64_TRUNCATED_FOR_PREVIEW]";
  782.  
  783.            if (displayP.image) { if (Array.isArray(displayP.image)) displayP.image.forEach(img => img.url = trunc); else displayP.image.url = trunc; }
  784.            if (displayP.images && Array.isArray(displayP.images)) displayP.images.forEach(img => img.url = trunc);
  785.            if (displayP.reference_images && Array.isArray(displayP.reference_images)) displayP.reference_images.forEach(img => img.url = trunc);
  786.            if (displayP.video) displayP.video.url = trunc;
  787.  
  788.            const activeRefs = referenceMedia.filter(m => m.active);
  789.            let endpoint = '/v1/images/generations';
  790.            if (els.action.value === 'gen_image' && activeRefs.length > 0) endpoint = '/v1/images/edits';
  791.            else if (els.action.value === 'gen_video') endpoint = '/v1/videos/generations';
  792.            else if (els.action.value === 'edit_video') endpoint = '/v1/videos/edits';
  793.            else if (els.action.value === 'extend_video') endpoint = '/v1/videos/extensions';
  794.  
  795.            els.payloadEndpoint.value = endpoint;
  796.            els.payloadCode.value = JSON.stringify(displayP, null, 2);
  797.            els.payloadModal.style.display = 'flex';
  798.        });
  799.  
  800.        els.closePayloadBtn.addEventListener('click', () => els.payloadModal.style.display = 'none');
  801.  
  802.        function injectBase64(editedObj, originalObj) {
  803.            if (!editedObj || typeof editedObj !== 'object') return;
  804.            for (let key in editedObj) {
  805.                if (typeof editedObj[key] === 'string' && editedObj[key] === '[BASE64_TRUNCATED_FOR_PREVIEW]') {
  806.                    if (originalObj && originalObj[key]) editedObj[key] = originalObj[key];
  807.                } else if (typeof editedObj[key] === 'object') { injectBase64(editedObj[key], originalObj ? originalObj[key] : null); }
  808.            }
  809.        }
  810.  
  811.        els.sendCustomBtn.addEventListener('click', async () => {
  812.            initAudio(); // Initialize audio context on first click
  813.            let customPayload;
  814.            try { customPayload = JSON.parse(els.payloadCode.value); }
  815.            catch(e) { return alert("Invalid JSON format in textarea!"); }
  816.  
  817.            injectBase64(customPayload, fullPayloadMemory);
  818.            const endpoint = els.payloadEndpoint.value.trim();
  819.            const isImage = customPayload.model === "grok-imagine-image";
  820.  
  821.            stealthIdentityReset();
  822.            await executeGenerationLoop(endpoint, customPayload, isImage, els.sendCustomBtn, els.customStatus);
  823.        });
  824.  
  825.        // ─── CSP-SAFE DELEGATED LIGHTBOX LOGIC ────────────────────────────────────
  826.        els.app.addEventListener('click', (e) => {
  827.            if (e.target && e.target.classList.contains('zoomable') && e.target.tagName === 'IMG') {
  828.                els.lightboxImg.src = e.target.src; els.lightboxDl.href = e.target.src; els.lightboxDl.download = e.target.dataset.filename || 'Reference_Image.jpg';
  829.                els.lightbox.style.display = 'flex';
  830.            }
  831.        });
  832.        els.lightbox.addEventListener('click', (e) => {
  833.            if (e.target.closest('#xai-lightbox-dl')) return;
  834.            if (e.target.closest('.modal-box')) return;
  835.            els.lightbox.style.display = 'none';
  836.        });
  837.        document.addEventListener('keydown', (e) => { if(e.key === 'Escape') { Array.from(document.querySelectorAll('.modal-overlay')).forEach(m => m.style.display = 'none'); } });
  838.  
  839.        // ─── FIXED FLOATING AR DROPDOWN LOGIC ─────────────────────────────────────
  840.        function renderArOptions() {
  841.            els.arOptionsCont.innerHTML = '';
  842.            arData.forEach(ar => {
  843.                const opt = document.createElement('div'); opt.className = 'ar-option';
  844.                opt.innerHTML = `${createArIcon(ar.w, ar.h, ar.isAuto)} <span>${ar.label}</span>`;
  845.                opt.addEventListener('click', () => { setAr(ar); els.arOptionsCont.style.display = 'none'; });
  846.                els.arOptionsCont.appendChild(opt);
  847.            });
  848.        }
  849.        function setAr(arObj) {
  850.            currentAr = arObj.label; localStorage.setItem('xai_api_ar', currentAr);
  851.            els.arSelectedIcon.innerHTML = createArIcon(arObj.w, arObj.h, arObj.isAuto); els.arSelectedText.innerText = arObj.label;
  852.        }
  853.        renderArOptions();
  854.        const initialAr = arData.find(a => a.label === currentAr) || arData[0]; setAr(initialAr);
  855.  
  856.        els.arSelectBox.addEventListener('click', (e) => {
  857.            e.stopPropagation();
  858.            if (els.arOptionsCont.style.display === 'block') { els.arOptionsCont.style.display = 'none'; return; }
  859.            const rect = els.arSelectBox.getBoundingClientRect();
  860.            els.arOptionsCont.style.top = (rect.bottom + 5) + 'px'; els.arOptionsCont.style.left = rect.left + 'px'; els.arOptionsCont.style.width = rect.width + 'px';
  861.            els.arOptionsCont.style.display = 'block';
  862.        });
  863.        document.addEventListener('click', (e) => { if (!els.arOptionsCont.contains(e.target)) els.arOptionsCont.style.display = 'none'; });
  864.  
  865.        // ─── UNLIMITED MULTI-IMAGE DRAG/DROP ──────────────────────────────────────
  866.        function processFile(file) {
  867.            const isVid = file.type.startsWith('video/'); const isImg = file.type.startsWith('image/');
  868.            if (!isVid && !isImg) return;
  869.  
  870.            if (isVid && (els.action.value === 'gen_image' || els.action.value === 'gen_video')) { els.action.value = 'edit_video'; updateActionUI(); updateGenSettingsMemory(); }
  871.            else if (isImg && (els.action.value === 'edit_video' || els.action.value === 'extend_video')) { els.action.value = 'gen_image'; updateActionUI(); updateGenSettingsMemory(); }
  872.  
  873.            const max = getMaxMedia();
  874.            const activeCount = referenceMedia.filter(m => m.active).length;
  875.  
  876.            const reader = new FileReader();
  877.            reader.onload = (event) => {
  878.                referenceMedia.push({ id: Date.now() + Math.random(), base64: event.target.result, isVideo: isVid, active: activeCount < max });
  879.                renderRefList();
  880.                if (isImg) setAr(arData[0]);
  881.            };
  882.            reader.readAsDataURL(file);
  883.        }
  884.  
  885.        let draggedIndex = null;
  886.        function renderRefList() {
  887.            els.refList.innerHTML = '';
  888.            referenceMedia.forEach((media, index) => {
  889.                const item = document.createElement('div');
  890.                item.className = 'ref-item';
  891.                item.draggable = true;
  892.  
  893.                let thumbHtml = media.isVideo
  894.                    ? `<video src="${media.base64}" class="ref-thumb" muted autoplay loop playsinline preload="metadata" style="background: #000;"></video>`
  895.                    : `<img src="${media.base64}" class="ref-thumb zoomable" data-filename="Reference_${index+1}.jpg" title="Click to view fullscreen">`;
  896.  
  897.                item.innerHTML = `
  898.                    <div class="ref-handle">☰</div>
  899.                    ${thumbHtml}
  900.                    <div class="ref-info">
  901.                        <span style="font-size: 13px; color: #e2e8f0; font-weight: 600;">Ref ${index + 1} ${media.isVideo ? '(Video)' : ''}</span>
  902.                        <label class="ref-toggle-label">
  903.                            <input type="checkbox" class="ref-active-toggle" ${media.active ? 'checked' : ''}> Use in Payload
  904.                        </label>
  905.                        <button class="ref-del">Remove</button>
  906.                    </div>
  907.                `;
  908.  
  909.                if (!media.active) item.classList.add('inactive');
  910.  
  911.                item.querySelector('.ref-active-toggle').addEventListener('change', (e) => {
  912.                    const max = getMaxMedia();
  913.                    const currentlyActive = referenceMedia.filter(r => r.active).length;
  914.  
  915.                    if (e.target.checked && currentlyActive >= max) {
  916.                        alert(`You can only have up to ${max} active references for this mode.`);
  917.                        e.target.checked = false;
  918.                        return;
  919.                    }
  920.                    media.active = e.target.checked;
  921.                    if (media.active) item.classList.remove('inactive'); else item.classList.add('inactive');
  922.                    saveRefs(); // Save on toggle
  923.                });
  924.  
  925.                item.addEventListener('dragstart', (e) => { draggedIndex = index; e.dataTransfer.effectAllowed = 'move'; setTimeout(() => item.classList.add('dragging'), 0); });
  926.                item.addEventListener('dragend', () => { item.classList.remove('dragging'); draggedIndex = null; document.querySelectorAll('.ref-item').forEach(el => el.classList.remove('drag-over-top', 'drag-over-bottom')); });
  927.                item.addEventListener('dragover', (e) => {
  928.                    e.preventDefault(); if (draggedIndex === null || draggedIndex === index) return;
  929.                    const rect = item.getBoundingClientRect();
  930.                    if (e.clientY - rect.top < rect.height / 2) { item.classList.add('drag-over-top'); item.classList.remove('drag-over-bottom'); }
  931.                    else { item.classList.add('drag-over-bottom'); item.classList.remove('drag-over-top'); }
  932.                });
  933.                item.addEventListener('dragleave', () => item.classList.remove('drag-over-top', 'drag-over-bottom'));
  934.                item.addEventListener('drop', (e) => {
  935.                    e.preventDefault(); item.classList.remove('drag-over-top', 'drag-over-bottom');
  936.                    if (draggedIndex === null || draggedIndex === index) return;
  937.                    const rect = item.getBoundingClientRect();
  938.                    let insertIndex = (e.clientY - rect.top) < rect.height / 2 ? index : index + 1;
  939.                    if (draggedIndex < insertIndex) insertIndex--;
  940.                    const [movedImage] = referenceMedia.splice(draggedIndex, 1);
  941.                    referenceMedia.splice(insertIndex, 0, movedImage); renderRefList();
  942.                });
  943.  
  944.                item.querySelector('.ref-del').addEventListener('click', () => { referenceMedia.splice(index, 1); renderRefList(); });
  945.                els.refList.appendChild(item);
  946.            });
  947.            saveRefs(); // Save bulk modifications (adds, removals, reordering)
  948.        }
  949.  
  950.        els.uploadPlaceholder.addEventListener('click', () => { els.fileInput.click(); });
  951.        els.fileInput.addEventListener('change', (e) => { Array.from(e.target.files).forEach(processFile); els.fileInput.value = ''; });
  952.        document.addEventListener('paste', (e) => {
  953.            if (e.target && (e.target.id === 'payload-code' || e.target.id === 'xai-api-prompt' || e.target.tagName === 'INPUT')) return;
  954.            const items = e.clipboardData.items; for (let i = 0; i < items.length; i++) if (items[i].type.indexOf('image') !== -1) processFile(items[i].getAsFile());
  955.        });
  956.  
  957.        els.dropzone.addEventListener('dragover', (e) => { e.preventDefault(); els.dropzone.classList.add('drag-over'); });
  958.        els.dropzone.addEventListener('dragleave', () => els.dropzone.classList.remove('drag-over'));
  959.        els.dropzone.addEventListener('drop', (e) => { e.preventDefault(); els.dropzone.classList.remove('drag-over'); if (e.dataTransfer.files) Array.from(e.dataTransfer.files).forEach(processFile); });
  960.  
  961.        // ─── CANCEL BUTTON LOGIC ─────────────────────────────────────────────────
  962.        els.cancelBtn.addEventListener('click', () => {
  963.            isRequestCancelled = true;
  964.            if (currentAbortController) currentAbortController.abort();
  965.            els.cancelBtn.style.display = 'none'; els.previewBtn.style.display = 'flex';
  966.            els.btn.disabled = false; els.btn.innerHTML = `<div class="btn-content">Generate</div>`;
  967.            els.status.innerText = "Request Cancelled."; els.status.className = ""; els.status.style.color = "#ef4444";
  968.        });
  969.  
  970.        // ─── MASTER GENERATION LOOP ──────────────────────────────────────────────
  971.        async function executeGenerationLoop(endpoint, payload, isImageAction, btnEl, statusEl) {
  972.            btnEl.disabled = true;
  973.            btnEl.innerHTML = `<div class="btn-content"><div class="loading-spinner"></div> Processing...</div>`;
  974.            statusEl.innerText = "Processing request...";
  975.            statusEl.className = "status-pulsing";
  976.  
  977.            els.cancelBtn.style.display = 'flex';
  978.            els.previewBtn.style.display = 'none';
  979.  
  980.            isRequestCancelled = false;
  981.            currentAbortController = new AbortController();
  982.  
  983.            let attempts = 0; let success = false;
  984.            while (attempts < appSettings.retries && !success && !isRequestCancelled) {
  985.                try {
  986.                    statusEl.innerText = "Sending request to xAI...";
  987.  
  988.                    const response = await fetch(endpoint, {
  989.                        method: 'POST',
  990.                        headers: { 'Content-Type': 'application/json' },
  991.                        body: JSON.stringify(payload),
  992.                        signal: currentAbortController.signal
  993.                    });
  994.  
  995.                    if (response.status === 429 || response.status === 503 || response.status === 500 || response.status === 502) {
  996.                        attempts++;
  997.                        statusEl.innerText = `Busy (Error ${response.status}). Retrying... [${attempts}/${appSettings.retries}]`;
  998.                        const delay = Math.floor(Math.random() * (appSettings.delayMax - appSettings.delayMin + 1)) + appSettings.delayMin;
  999.                        await sleep(delay);
  1000.                        continue;
  1001.                    }
  1002.  
  1003.                    if (!response.ok) {
  1004.                        let errMsg = `HTTP ${response.status}`;
  1005.                        try {
  1006.                            const errData = await response.json();
  1007.                            if (errData.error && errData.error.message) errMsg += `\n${errData.error.message}`;
  1008.                            else if (errData.detail) errMsg += `\n${JSON.stringify(errData.detail)}`;
  1009.                        } catch(e) {}
  1010.                        throw new Error(errMsg);
  1011.                    }
  1012.  
  1013.                    const data = await response.json();
  1014.  
  1015.                    if (isImageAction) {
  1016.                        if (data && data.data && Array.isArray(data.data)) {
  1017.                            success = true;
  1018.                            statusEl.innerText = "Processing final JPEGs...";
  1019.                            statusEl.style.color = "#10b981";
  1020.                            statusEl.className = "";
  1021.  
  1022.                            await Promise.all(data.data.map((imgObj, index) =>
  1023.                                processAndRenderImage(imgObj, payload.prompt, index + 1, data.data.length, els.currentImages)
  1024.                            ));
  1025.  
  1026.                            statusEl.innerText = "Ready";
  1027.                            statusEl.style.color = "#64748b";
  1028.                            notifyUser(false);
  1029.                        } else throw new Error("Invalid response format.");
  1030.                    } else {
  1031.                        const reqId = data.request_id;
  1032.                        if (!reqId) throw new Error("No Request ID returned.");
  1033.  
  1034.                        success = true;
  1035.                        let videoReady = false;
  1036.                        let pollCount = 0;
  1037.                        const MAX_POLLS = 60;
  1038.  
  1039.                        while (!videoReady && !isRequestCancelled) {
  1040.                            pollCount++;
  1041.                            if (pollCount > MAX_POLLS) throw new Error("Timeout: Video likely dropped by filters.");
  1042.                            await sleep(5000);
  1043.                            if (isRequestCancelled) break;
  1044.  
  1045.                            const pollRes = await fetch(`/v1/videos/${reqId}`, { signal: currentAbortController.signal });
  1046.                            if (!pollRes.ok) continue;
  1047.  
  1048.                            const pollData = await pollRes.json();
  1049.                            if (pollData.error) throw new Error(`API Error: ${pollData.error.message || JSON.stringify(pollData.error)}`);
  1050.  
  1051.                            const state = (pollData.status || pollData.state || 'processing').toLowerCase();
  1052.                            statusEl.innerText = `Polling video (Status: ${state})...[${pollCount}/${MAX_POLLS}]`;
  1053.  
  1054.                            if (state === 'done' || state === 'completed') {
  1055.                                videoReady = true;
  1056.                                statusEl.innerText = "Ready"; statusEl.className = ""; statusEl.style.color = "#94a3b8";
  1057.                                renderVideoToGallery(pollData.video.url, payload.prompt, els.currentImages);
  1058.                                notifyUser(false);
  1059.                            } else if (['failed', 'expired', 'rejected', 'blocked', 'moderated', 'nsfw'].includes(state) || pollData.is_sensitive) {
  1060.                                if (pollData.video && pollData.video.url) {
  1061.                                    videoReady = true;
  1062.                                    statusEl.innerText = `Warning: Flagged as ${state}, but recovered!`;
  1063.                                    statusEl.className = ""; statusEl.style.color = "#d97706";
  1064.                                    renderVideoToGallery(pollData.video.url, payload.prompt, els.currentImages);
  1065.                                    notifyUser(false);
  1066.                                } else {
  1067.                                    throw new Error(`Generation halted. Reason: ${state}`);
  1068.                                }
  1069.                            }
  1070.                        }
  1071.                    }
  1072.  
  1073.                } catch (err) {
  1074.                    if (err.name === 'AbortError' || isRequestCancelled) {
  1075.                        statusEl.innerText = "Request Cancelled."; statusEl.className = ""; statusEl.style.color = "#ef4444";
  1076.                    } else {
  1077.                        statusEl.innerText = `Error: ${err.message}`; statusEl.className = ""; statusEl.style.color = "#ef4444";
  1078.                        notifyUser(true);
  1079.                    }
  1080.                    break;
  1081.                }
  1082.            }
  1083.  
  1084.            if (!success && !isRequestCancelled && attempts >= appSettings.retries) {
  1085.                statusEl.innerText = `Failed. Server overloaded after ${attempts} retries.`;
  1086.                statusEl.className = ""; statusEl.style.color = "#ef4444";
  1087.                notifyUser(true);
  1088.            }
  1089.  
  1090.            els.cancelBtn.style.display = 'none';
  1091.            els.previewBtn.style.display = 'flex';
  1092.            if (!isRequestCancelled) { btnEl.disabled = false; btnEl.innerHTML = `<div class="btn-content">Generate</div>`; }
  1093.        }
  1094.  
  1095.        els.btn.addEventListener('click', async () => {
  1096.            initAudio(); // Required context creation before async task for security
  1097.            if (!els.prompt.value.trim()) return alert("Please enter a prompt.");
  1098.  
  1099.            const oldImages = Array.from(els.currentImages.children);
  1100.            if (oldImages.length > 0) {
  1101.                if (els.history.innerText.includes("No history")) els.history.innerHTML = '';
  1102.                oldImages.forEach(card => {
  1103.                    card.className = 'history-card';
  1104.                    const promptEl = card.querySelector('p');
  1105.                    promptEl.title = "Click to copy prompt";
  1106.                    promptEl.onclick = () => { els.prompt.value = promptEl.innerText; localStorage.setItem('xai_api_prompt', els.prompt.value); };
  1107.                    els.history.prepend(card);
  1108.                });
  1109.            }
  1110.  
  1111.            const action = els.action.value;
  1112.            const activeRefs = referenceMedia.filter(m => m.active);
  1113.            let endpoint = '/v1/images/generations';
  1114.            if (action === 'gen_image' && activeRefs.length > 0) endpoint = '/v1/images/edits';
  1115.            else if (action === 'gen_video') endpoint = '/v1/videos/generations';
  1116.            else if (action === 'edit_video') endpoint = '/v1/videos/edits';
  1117.            else if (action === 'extend_video') endpoint = '/v1/videos/extensions';
  1118.  
  1119.            const payload = buildPayloadData();
  1120.            const isImage = action === 'gen_image';
  1121.  
  1122.            stealthIdentityReset();
  1123.            await executeGenerationLoop(endpoint, payload, isImage, els.btn, els.status);
  1124.        });
  1125.  
  1126.        // ─── SESSION RESETTER ─────────────────────────────────────────────────────
  1127.        els.reset.addEventListener('click', () => {
  1128.            if (!confirm("Wipe cache? (This forces a page reload to clear front-end tokens). Your prompt and settings will be saved.")) return;
  1129.            localStorage.setItem('xai_api_prompt', els.prompt.value);
  1130.            const patterns =[/flushAfter/i, /imagine/i, /generation/i, /grok/i, /mixpanel|mp_/i, /distinct_id/i, /_rst/i, /limit/i, /credit/i];
  1131.            for (let i = localStorage.length - 1; i >= 0; i--) {
  1132.                const k = localStorage.key(i);
  1133.                if (k && !k.startsWith('xai_api_') && patterns.some(p => p.test(k))) localStorage.removeItem(k);
  1134.            }
  1135.            document.cookie.split(';').forEach(c => {
  1136.                const name = c.split('=')[0]?.trim();
  1137.                if (!name || !patterns.some(p => name.toLowerCase().includes(p.toLowerCase()))) return;['.x.ai', 'console.x.ai', ''].forEach(d => {['/', '/playground/imagine', ''].forEach(p => { document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=${p}${d ? ';domain=' + d : ''}`; });
  1138.                });
  1139.            });
  1140.            const u = new URL(location.href);
  1141.            ['_r', '_rst'].forEach(k => u.searchParams.set(k, Date.now()));
  1142.            location.replace(u.toString());
  1143.        });
  1144.    }
  1145.  
  1146.    // ─── ASYNC PROCESSORS & RENDERERS ─────────────────────────────────────────
  1147.    async function processAndRenderImage(imgObj, originalPrompt, num, total, galleryEl) {
  1148.        if (!imgObj.b64_json) return;
  1149.        const mime = imgObj.mime_type || "image/png";
  1150.        const b64 = imgObj.b64_json;
  1151.        const pngDataUri = `data:${mime};base64,${b64}`;
  1152.  
  1153.        const finalPrompt = imgObj.revised_prompt || originalPrompt;
  1154.        const jpegDataUri = await convertToJpegWithExif(pngDataUri, finalPrompt);
  1155.        const filename = `Grok 2K - ${makeTimestamp()} - ${num}of${total}.jpg`;
  1156.  
  1157.        const card = document.createElement('div');
  1158.        card.className = 'current-card';
  1159.        card.innerHTML = `
  1160.            <div class="img-wrapper">
  1161.                <img src="${jpegDataUri}" class="zoomable" data-filename="${filename}" title="Click to view fullscreen">
  1162.                <a href="${jpegDataUri}" download="${filename}" class="overlay-dl-btn" title="Download Full JPG">${DOWNLOAD_ICON}</a>
  1163.            </div>
  1164.            <p style="padding: 15px; margin: 0; font-size: 13px; color: #cbd5e1; border-top: 1px solid #334155;">${finalPrompt}</p>
  1165.        `;
  1166.        galleryEl.prepend(card);
  1167.    }
  1168.  
  1169.    function renderVideoToGallery(videoUrl, originalPrompt, galleryEl) {
  1170.        const filename = `Grok Video - ${makeTimestamp()}.mp4`;
  1171.        const card = document.createElement('div');
  1172.        card.className = 'current-card';
  1173.        card.innerHTML = `
  1174.            <div class="img-wrapper">
  1175.                <video src="${videoUrl}" controls autoplay loop style="width: 100%; display: block; max-height: 50vh; object-fit: contain; background: #000;"></video>
  1176.                <a href="${videoUrl}" target="_blank" download="${filename}" class="overlay-dl-btn" style="z-index: 50;" title="Download MP4">${DOWNLOAD_ICON}</a>
  1177.            </div>
  1178.            <p style="padding: 15px; margin: 0; font-size: 13px; color: #cbd5e1; border-top: 1px solid #334155;">${originalPrompt}</p>
  1179.        `;
  1180.        galleryEl.prepend(card);
  1181.    }
  1182.  
  1183.    if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initApp); }
  1184.    else { initApp(); }
  1185.  
  1186. })();
Add Comment
Please, Sign In to add comment