Guest User

Untitled

a guest
Apr 3rd, 2026
427
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. // ==UserScript==
  2. // @name         xAI Pro Studio Frontend (v33 - Extend & Auto-DL Update)
  3. // @namespace    http://tampermonkey.net/
  4. // @version      33
  5. // @description  Full-Screen UI, Exif Fix, Multi-Image, Auto-Retry, Export Settings, Video Warnings, Quick-Action Sliders, Adaptive AR, Auto-Download, Extend Support, Anti-Throttling
  6. // @match        https://console.x.ai/playground/imagine*
  7. // @match        https://console.x.ai/team/*/imagine*
  8. // @grant        GM_xmlhttpRequest
  9. // @connect      vidgen.x.ai
  10. // @run-at       document-start
  11. // ==/UserScript==
  12.  
  13. (function() {
  14.     'use strict';
  15.  
  16.     // ─── LOGGING & DEBUG ENGINE ───────────────────────────────────────────────
  17.     const appLogs = [];
  18.     const MAX_LOGS = 500;
  19.  
  20.     function stripHeavyData(val) {
  21.         if (val === null || val === undefined) return val;
  22.         if (typeof val === 'string') {
  23.             if (val.startsWith('data:image') || val.startsWith('data:video') || val.length > 2000) {
  24.                 return `[TRUNCATED_DATA_LEN_${val.length}]`;
  25.             }
  26.             return val;
  27.         }
  28.         if (typeof val !== 'object') return val;
  29.         if (Array.isArray(val)) return val.map(stripHeavyData);
  30.         const copy = {};
  31.         for (const k in val) {
  32.             if (Object.prototype.hasOwnProperty.call(val, k)) {
  33.                 copy[k] = stripHeavyData(val[k]);
  34.             }
  35.         }
  36.         return copy;
  37.     }
  38.  
  39.     function addLog(level, msg, data = null) {
  40.         try {
  41.             const ts = new Date().toISOString().replace('T', ' ').substring(0, 23);
  42.             const safeData = data ? stripHeavyData(data) : null;
  43.             appLogs.push({ ts, level, msg, data: safeData });
  44.             if (appLogs.length > MAX_LOGS) appLogs.shift();
  45.  
  46.             const consoleMsg = `[xAI Studio] [${level}] ${msg}`;
  47.             if (level === 'ERROR') console.error(consoleMsg, safeData || '');
  48.             else if (level === 'WARN') console.warn(consoleMsg, safeData || '');
  49.             else console.log(consoleMsg, safeData || '');
  50.         } catch(e) {}
  51.     }
  52.  
  53.     function formatLogsForExport() {
  54.         return appLogs.map(l => {
  55.             let text = `[${l.ts}] [${l.level}] ${l.msg}`;
  56.             if (l.data) {
  57.                 try { text += `\n${JSON.stringify(l.data, null, 2)}`; }
  58.                 catch(e) { text += `\n[Unserializable Data]`; }
  59.             }
  60.             return text;
  61.         }).join('\n\n----------------------------------------\n\n');
  62.     }
  63.  
  64.     addLog('INFO', 'Script started executing');
  65.  
  66.     // ─── UTILITIES & DATA STORAGE ─────────────────────────────────────────────
  67.     const sleep = (ms) => new Promise(r => setTimeout(r, ms));
  68.     function pad2(n) { return String(n).padStart(2, '0'); }
  69.     function makeTimestamp() {
  70.         const d = new Date();
  71.         return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}-${pad2(d.getMinutes())}-${pad2(d.getSeconds())}`;
  72.     }
  73.     const DOWNLOAD_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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>`;
  74.     const EDIT_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>`;
  75.     const EXTEND_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>`;
  76.  
  77.     let appSettings = JSON.parse(localStorage.getItem('xai_api_settings') || '{}');
  78.     appSettings.retries = parseInt(appSettings.retries) || 20;
  79.     appSettings.delayMin = parseInt(appSettings.delayMin) || 2000;
  80.     appSettings.delayMax = parseInt(appSettings.delayMax) || 4000;
  81.     if (typeof appSettings.rateLimitDelay === 'undefined') appSettings.rateLimitDelay = 60;
  82.     if (typeof appSettings.notifications === 'undefined') appSettings.notifications = true;
  83.     if (typeof appSettings.customCSS === 'undefined') appSettings.customCSS = "";
  84.     if (typeof appSettings.saveAsPng === 'undefined') appSettings.saveAsPng = false;
  85.     if (typeof appSettings.apiBaseUrl === 'undefined') appSettings.apiBaseUrl = "";
  86.     if (typeof appSettings.videoPollTimeout === 'undefined') appSettings.videoPollTimeout = 300;
  87.     if (typeof appSettings.autoRetryStuckVideo === 'undefined') appSettings.autoRetryStuckVideo = false;
  88.     if (typeof appSettings.breakLoopOnFailure === 'undefined') appSettings.breakLoopOnFailure = true;
  89.  
  90.     if (typeof appSettings.suppressEditWarning === 'undefined') appSettings.suppressEditWarning = false;
  91.     if (typeof appSettings.videoAutoplay === 'undefined') appSettings.videoAutoplay = true;
  92.     if (typeof appSettings.videoMuted === 'undefined') appSettings.videoMuted = false;
  93.     if (typeof appSettings.uiFontSize === 'undefined') appSettings.uiFontSize = 13;
  94.     if (typeof appSettings.autoDownload === 'undefined') appSettings.autoDownload = false;
  95.     if (typeof appSettings.antiThrottling === 'undefined') appSettings.antiThrottling = true;
  96.  
  97.     let favoritePrompts = JSON.parse(localStorage.getItem('xai_api_favs') || '[]');
  98.  
  99.     function getApiUrl(path) {
  100.         let base = appSettings.apiBaseUrl || '';
  101.         if (base.endsWith('/')) base = base.slice(0, -1);
  102.         if (!path.startsWith('/') && base) path = '/' + path;
  103.         return base + path;
  104.     }
  105.  
  106.     function parseDynamicPrompt(text) {
  107.         if (!text) return text;
  108.         let parsed = text; let prev;
  109.         do {
  110.             prev = parsed;
  111.             parsed = parsed.replace(/\{([^{}]+)\}/g, (m, c) => { const o = c.split('|'); return o[Math.floor(Math.random() * o.length)]; });
  112.         } while (parsed !== prev);
  113.         parsed = parsed.replace(/@randomseed/gi, () => Math.floor(1000000000 + Math.random() * 9000000000).toString());
  114.         return parsed.trim();
  115.     }
  116.  
  117.     let sharedAudioCtx = null;
  118.     let awakeOsc = null;
  119.     let titleBlinkInterval = null;
  120.     const originalTitle = document.title || "xAI Pro Studio";
  121.  
  122.     function initAudio() {
  123.         if (!sharedAudioCtx) {
  124.             try { sharedAudioCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch(e) {}
  125.         }
  126.         if (sharedAudioCtx && sharedAudioCtx.state === 'suspended') sharedAudioCtx.resume();
  127.     }
  128.  
  129.     // Anti-throttling engine using barely audible Web Audio API
  130.     function toggleKeepAwake(enable) {
  131.         if (enable && !appSettings.antiThrottling) return; // Only enable if the setting is checked
  132.         if (enable) {
  133.             initAudio();
  134.             if (sharedAudioCtx) {
  135.                 if (!awakeOsc) {
  136.                     try {
  137.                         awakeOsc = sharedAudioCtx.createOscillator();
  138.                         const gain = sharedAudioCtx.createGain();
  139.                         gain.gain.value = 0.001; // virtually silent
  140.                         awakeOsc.connect(gain);
  141.                         gain.connect(sharedAudioCtx.destination);
  142.                         awakeOsc.start();
  143.                     } catch(e) {}
  144.                 }
  145.             }
  146.         } else {
  147.             if (awakeOsc) {
  148.                 try { awakeOsc.stop(); awakeOsc.disconnect(); } catch(e) {}
  149.                 awakeOsc = null;
  150.             }
  151.         }
  152.     }
  153.  
  154.     function notifyUser(isError = false) {
  155.         if (!appSettings.notifications) return;
  156.         initAudio();
  157.         if (sharedAudioCtx) {
  158.             try {
  159.                 const osc = sharedAudioCtx.createOscillator(); const gain = sharedAudioCtx.createGain();
  160.                 osc.connect(gain); gain.connect(sharedAudioCtx.destination);
  161.                 if (isError) {
  162.                     osc.type = 'sawtooth'; osc.frequency.setValueAtTime(300, sharedAudioCtx.currentTime); osc.frequency.exponentialRampToValueAtTime(100, sharedAudioCtx.currentTime + 0.3);
  163.                 } else {
  164.                     osc.type = 'sine'; osc.frequency.setValueAtTime(500, sharedAudioCtx.currentTime); osc.frequency.exponentialRampToValueAtTime(1000, sharedAudioCtx.currentTime + 0.2);
  165.                 }
  166.                 gain.gain.setValueAtTime(0.1, sharedAudioCtx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, sharedAudioCtx.currentTime + 0.5);
  167.                 osc.start(sharedAudioCtx.currentTime); osc.stop(sharedAudioCtx.currentTime + 0.5);
  168.             } catch(e) {}
  169.         }
  170.         if (!document.hasFocus()) {
  171.             if (titleBlinkInterval) clearInterval(titleBlinkInterval);
  172.             let toggle = true;
  173.             titleBlinkInterval = setInterval(() => { document.title = toggle ? (isError ? "❌ FAILED" : "✅ DONE") : originalTitle; toggle = !toggle; }, 1000);
  174.             const clearBlink = () => { clearInterval(titleBlinkInterval); document.title = originalTitle; window.removeEventListener('focus', clearBlink); };
  175.             window.addEventListener('focus', clearBlink);
  176.         }
  177.     }
  178.  
  179.     function stealthIdentityReset() {
  180.         const trackingCookies =['x-anonuserid', 'mixpanel', 'mp_', 'distinct_id'];
  181.         document.cookie.split(';').forEach(c => {
  182.             const name = c.split('=')[0].trim();
  183.             if (trackingCookies.some(tc => name.includes(tc))) {['.x.ai', 'console.x.ai', ''].forEach(d => {['/', '/playground/imagine', ''].forEach(p => {
  184.                 document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=${p}${d ? ';domain=' + d : ''}`;
  185.             });});}
  186.         });
  187.         for (let i = localStorage.length - 1; i >= 0; i--) {
  188.             const k = localStorage.key(i);
  189.             if (k && !k.startsWith('xai_api_') && trackingCookies.some(tc => k.includes(tc))) localStorage.removeItem(k);
  190.         }
  191.     }
  192.  
  193.     let piexifLoaded = false;
  194.     function loadPiexif() {
  195.         if (piexifLoaded) return Promise.resolve(window.piexif);
  196.         return new Promise(resolve => {
  197.             const script = document.createElement('script'); script.src = 'https://cdn.jsdelivr.net/npm/piexifjs';
  198.             script.onload = () => { piexifLoaded = true; resolve(window.piexif); }; document.head.appendChild(script);
  199.         });
  200.     }
  201.  
  202.     function toUTF16LE(str) {
  203.         const arr =[];
  204.         for (let i = 0; i < str.length; i++) { const code = str.charCodeAt(i); arr.push(code & 0xFF); arr.push((code >> 8) & 0xFF); }
  205.         arr.push(0, 0); return arr;
  206.     }
  207.  
  208.     function convertToJpegWithExif(base64PngUri, prompt) {
  209.         return new Promise((resolve, reject) => {
  210.             const img = new Image();
  211.             img.onload = async () => {
  212.                 const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height;
  213.                 const ctx = canvas.getContext('2d'); ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.drawImage(img, 0, 0);
  214.                 let jpegUri = canvas.toDataURL('image/jpeg', 0.95);
  215.                 if (prompt) {
  216.                     try {
  217.                         const piexif = await loadPiexif();
  218.                         const exifObj = {"0th": {}, "Exif": {}, "GPS": {}, "1st": {}, "Interop": {}};
  219.                         exifObj['0th'][piexif.ImageIFD.ImageDescription] = unescape(encodeURIComponent(prompt));
  220.                         const utf16Prompt = toUTF16LE(prompt); exifObj['0th'][40091] = utf16Prompt; exifObj['0th'][40092] = utf16Prompt; exifObj['0th'][40093] = utf16Prompt;
  221.                         jpegUri = piexif.insert(piexif.dump(exifObj), jpegUri);
  222.                     } catch (e) {}
  223.                 }
  224.                 resolve(jpegUri);
  225.             };
  226.             img.onerror = reject; img.src = base64PngUri;
  227.         });
  228.     }
  229.  
  230.     async function extractPromptFromImage(file) {
  231.         if (!file.type.includes('image')) return null;
  232.         try {
  233.             const piexif = await loadPiexif();
  234.             return new Promise((resolve) => {
  235.                 const reader = new FileReader();
  236.                 reader.onload = (e) => {
  237.                     try {
  238.                         const exifData = piexif.load(e.target.result);
  239.                         let prompt = exifData['0th'] && exifData['0th'][piexif.ImageIFD.ImageDescription];
  240.                         if (Array.isArray(prompt)) prompt = String.fromCharCode.apply(null, prompt).replace(/\0/g, '');
  241.                         if (prompt) { try { prompt = decodeURIComponent(escape(prompt)); } catch(e) {} }
  242.                         if (!prompt && exifData['0th'] && exifData['0th'][40091]) {
  243.                             const xpTitleArr = exifData['0th'][40091]; let str = '';
  244.                             for (let i = 0; i < xpTitleArr.length; i += 2) { const charCode = xpTitleArr[i] | (xpTitleArr[i+1] << 8); if (charCode === 0) break; str += String.fromCharCode(charCode); }
  245.                             prompt = str;
  246.                         }
  247.                         resolve(prompt ? prompt.trim() : null);
  248.                     } catch (err) { resolve(null); }
  249.                 };
  250.                 reader.readAsDataURL(file);
  251.             });
  252.         } catch (e) { return null; }
  253.     }
  254.  
  255.     function generateVideoThumbnail(file) {
  256.         return new Promise((resolve) => {
  257.             const video = document.createElement('video'); video.preload = 'metadata'; video.muted = true; video.playsInline = true;
  258.             const url = URL.createObjectURL(file); video.src = url; let isSeeked = false;
  259.             video.onloadeddata = () => { video.currentTime = Math.min(0.5, video.duration / 2 || 0); file._w = video.videoWidth; file._h = video.videoHeight; };
  260.             video.onseeked = () => {
  261.                 if (isSeeked) return; isSeeked = true;
  262.                 try {
  263.                     const canvas = document.createElement('canvas'); canvas.width = video.videoWidth || 140; canvas.height = video.videoHeight || 140;
  264.                     const ctx = canvas.getContext('2d'); ctx.drawImage(video, 0, 0, canvas.width, canvas.height); URL.revokeObjectURL(url); resolve(canvas.toDataURL('image/jpeg', 0.8));
  265.                 } catch(e) { URL.revokeObjectURL(url); resolve(null); }
  266.             };
  267.             video.onerror = () => { URL.revokeObjectURL(url); resolve(null); };
  268.             setTimeout(() => { if (!isSeeked) { URL.revokeObjectURL(url); resolve(null); } }, 2000);
  269.         });
  270.     }
  271.  
  272.     function updateFontSize(size) {
  273.         let el = document.getElementById('xai-custom-font-size');
  274.         if (!el) { el = document.createElement('style'); el.id = 'xai-custom-font-size'; document.head.appendChild(el); }
  275.         if (size === 13) { el.innerHTML = ''; return; } // Default size, no overrides needed
  276.  
  277.         el.innerHTML = `
  278.             #xai-pro-app textarea, #xai-pro-app #prompt-backdrop, #xai-pro-app select, #xai-pro-app input[type="text"], #xai-pro-app input[type="number"], #xai-pro-app .btn-primary, #xai-pro-app .btn-secondary, #xai-pro-app .ar-select-box, #xai-pro-app .action-select { font-size: ${size}px !important; }
  279.             #xai-pro-app .ar-option, #xai-pro-app .fav-text, #xai-pro-app #xai-api-status { font-size: ${size - 1}px !important; }
  280.             #xai-pro-app .panel-title, #xai-pro-app label, #xai-pro-app .toggle-btn, #xai-pro-app .slider-header, #xai-pro-app .history-card p, #xai-pro-app .warning-box, #xai-pro-app .ref-info span { font-size: ${size - 2}px !important; }
  281.             #xai-pro-app .slider-ticks, #xai-pro-app .slider-ticks span, #xai-pro-app .ref-action-btn, #xai-pro-app .ref-toggle-label, #xai-pro-app .ref-thumb span, #xai-pro-app button[style*="font-size: 10px"] { font-size: ${size - 3}px !important; }
  282.             #xai-pro-app .ref-handle { font-size: ${size + 1}px !important; }
  283.             #xai-pro-app h3 { font-size: ${size + 2}px !important; }
  284.         `;
  285.     }
  286.  
  287.     // ─── DATA MAPS FOR UI SLIDERS ─────────────────────────────────────────────
  288.     const MAPS = {
  289.         batch: [1, 2, 3, 4, 5, 8, 10],
  290.         durGen: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
  291.         durExt: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  292.     };
  293.  
  294.     const arData =[
  295.         { label: 'Auto', w: 1, h: 1, isAuto: true },
  296.         { label: 'By First Ref', w: 1, h: 1, isAuto: true, isRef: true },
  297.         { label: '1:1', w: 1, h: 1 }, { label: '3:4', w: 3, h: 4 }, { label: '4:3', w: 4, h: 3 },
  298.         { label: '9:16', w: 9, h: 16 }, { label: '16:9', w: 16, h: 9 }, { label: '2:3', w: 2, h: 3 },
  299.         { 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 },
  300.         { 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 }
  301.     ];
  302.  
  303.     function createArIcon(w, h, isAuto, isRef) {
  304.         if (isRef) return `<div style="width: 12px; height: 12px; border: 1px dashed #10b981; border-radius: 2px; display: flex; align-items: center; justify-content: center; font-size: 8px; color: #10b981; font-weight:bold;">R</div>`;
  305.         if (isAuto) return `<div style="width: 12px; height: 12px; border: 1px dashed #64748b; border-radius: 2px; display: flex; align-items: center; justify-content: center; font-size: 9px; color: #94a3b8;">A</div>`;
  306.         const scale = 12 / Math.max(w, h);
  307.         return `<div style="width: 14px; height: 14px; 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>`;
  308.     }
  309.  
  310.     // ─── UI INJECTION ─────────────────────────────────────────────────────────
  311.     const UI_CSS = `
  312.         ::-webkit-scrollbar { width: 6px; }
  313.         ::-webkit-scrollbar-track { background: #0f172a; }
  314.         ::-webkit-scrollbar-thumb { background: #475569; border-radius: 6px; }
  315.  
  316.         @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
  317.         @keyframes pulse { 0% { opacity: 0.5; } 50% { opacity: 1; } 100% { opacity: 0.5; } }
  318.  
  319.         .loading-spinner { width: 14px; height: 14px; border: 2px solid rgba(255,255,255,0.3); border-top-color: #fff; border-radius: 50%; animation: spin 1s linear infinite; flex-shrink: 0; }
  320.         .btn-content { display: flex; align-items: center; justify-content: center; gap: 6px; }
  321.         .status-pulsing { animation: pulse 2s ease-in-out infinite; color: #3b82f6 !important; font-weight: 600; }
  322.  
  323.         #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: 10px; padding: 10px; box-sizing: border-box; }
  324.         #xai-pro-app * { box-sizing: border-box; }
  325.  
  326.         .panel { background: #1e293b; border-radius: 10px; border: 1px solid #334155; box-shadow: 0 4px 15px rgba(0,0,0,0.4); display: flex; flex-direction: column; overflow: hidden; }
  327.         .panel-title { font-size: 11px; font-weight: 700; color: #94a3b8; letter-spacing: 0.5px; padding: 10px; border-bottom: 1px solid #334155; text-transform: uppercase; }
  328.  
  329.         .col-left { width: 310px; display: flex; flex-direction: column; gap: 10px; flex-shrink: 0; }
  330.         .col-center { flex: 1; display: flex; flex-direction: column; gap: 10px; min-width: 0; min-height: 0; }
  331.         .col-right { width: 310px; display: flex; flex-direction: column; gap: 10px; flex-shrink: 0; transition: 0.2s; }
  332.         .col-right.drag-over { background: #064e3b; border-color: #10b981; box-shadow: 0 0 0 4px rgba(16,185,129,0.15); border-radius: 10px; }
  333.  
  334.         textarea, select, input[type="text"], input[type="number"], input[type="checkbox"] { background: #0f172a; border: 1px solid #334155; border-radius: 6px; padding: 8px; color: #f8fafc; font-size: 13px; outline: none; transition: 0.2s; color-scheme: dark; }
  335.         textarea, select, input[type="text"], input[type="number"] { width: 100%; }
  336.         textarea:focus, select:focus, input[type="text"]:focus, input[type="number"]:focus { border-color: #64748b; background: #1e293b; box-shadow: 0 0 0 2px rgba(100,116,139,0.2); }
  337.         label { font-size: 11px; font-weight: 600; color: #94a3b8; margin-bottom: 4px; display: block; }
  338.  
  339.         .btn-primary { background: linear-gradient(180deg, #6366f1 0%, #4f46e5 100%); color: #fff; border: none; border-radius: 6px; padding: 8px 14px; font-size: 13px; 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; }
  340.         .btn-primary:hover:not(:disabled) { background: linear-gradient(180deg, #4f46e5 0%, #4338ca 100%); transform: translateY(-1px); }
  341.         .btn-primary:disabled { opacity: 0.8; cursor: not-allowed; }
  342.         .btn-secondary { background: #1e293b; color: #cbd5e1; border: 1px solid #475569; border-radius: 6px; padding: 8px 14px; font-size: 13px; font-weight: 600; cursor: pointer; transition: 0.2s; display: flex; justify-content: center; align-items: center; }
  343.         .btn-secondary:hover:not(:disabled) { background: #334155; border-color: #64748b; }
  344.  
  345.         /* Sliders and Actions */
  346.         .action-select { width: 100%; font-weight: 600; font-size: 13px; color: #f8fafc; background: transparent; border: none; outline: none; overflow-y: hidden; }
  347.         .action-select option { padding: 8px 10px; margin-bottom: 4px; border-radius: 6px; cursor: pointer; transition: 0.1s; background: #0f172a; border: 1px solid #334155; }
  348.         .action-select option:checked { background: linear-gradient(180deg, #6366f1 0%, #4f46e5 100%); border-color: #4f46e5; color: #fff; box-shadow: 0 2px 4px rgba(0,0,0,0.3); }
  349.         .action-select option:hover:not(:checked) { background: #334155; }
  350.  
  351.         .slider-block { display: flex; flex-direction: column; margin-bottom: 12px; }
  352.         .toggle-btn { background: #0f172a; border: 1px solid #334155; color: #94a3b8; padding: 6px 12px; border-radius: 6px; font-size: 11px; font-weight: 600; cursor: pointer; transition: 0.2s; box-shadow: inset 0 2px 4px rgba(0,0,0,0.2); }
  353.         .toggle-btn.active { background: linear-gradient(180deg, #10b981 0%, #059669 100%); color: #fff; border-color: #047857; box-shadow: 0 2px 4px rgba(0,0,0,0.3); }
  354.         .slider-header { display: flex; justify-content: space-between; font-size: 11px; margin-bottom: 6px; font-weight: 600; color: #94a3b8; }
  355.         .slider-header span { color: #f8fafc; }
  356.         .slider-ticks { position: relative; height: 14px; width: 100%; font-size: 10px; color: #64748b; margin-top: 4px; padding: 0; }
  357.         .slider-ticks span { position: absolute; transform: translateX(-50%); text-align: center; white-space: nowrap; line-height: 1; }
  358.  
  359.         input[type=range] { -webkit-appearance: none; width: 100%; background: transparent; margin: 4px 0; outline: none; transform: scaleY(1.6); }
  360.         input[type=range]::-webkit-slider-runnable-track { width: 100%; height: 6px; cursor: pointer; background: #334155; border-radius: 3px; box-shadow: inset 0 1px 3px rgba(0,0,0,0.6); }
  361.         input[type=range]::-moz-range-track { width: 100%; height: 6px; cursor: pointer; background: #334155; border-radius: 3px; box-shadow: inset 0 1px 3px rgba(0,0,0,0.6); }
  362.         input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; height: 16px; width: 16px; border-radius: 50%; background: #10b981; cursor: pointer; margin-top: -5px; box-shadow: 0 2px 5px rgba(0,0,0,0.6); }
  363.         input[type=range]::-moz-range-thumb { height: 16px; width: 16px; border-radius: 50%; background: #10b981; cursor: pointer; border: none; box-shadow: 0 2px 5px rgba(0,0,0,0.6); }
  364.         input[type=range]:focus::-webkit-slider-thumb { box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.4); }
  365.         input[type=range]:focus::-moz-range-thumb { box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.4); }
  366.  
  367.         .ar-select-box { background: #1e293b; border: 1px solid #475569; border-radius: 6px; padding: 8px; display: flex; align-items: center; gap: 8px; cursor: pointer; color: #f8fafc; font-size: 13px; }
  368.         #ar-options-container { position: fixed; background: #1e293b; border: 1px solid #334155; border-radius: 6px; box-shadow: 0 10px 25px rgba(0,0,0,0.4); max-height: 250px; overflow-y: auto; display: none; z-index: 99999999; }
  369.         .ar-option { display: flex; align-items: center; gap: 8px; padding: 6px 8px; cursor: pointer; font-size: 12px; color: #cbd5e1; }
  370.         .ar-option:hover { background: #334155; color: #f8fafc; }
  371.  
  372.         .img-wrapper { position: relative; display: block; }
  373.         .zoomable { cursor: zoom-in; transition: transform 0.2s; width: 100%; display: block; }
  374.         .overlay-btn { position: absolute; top: 6px; background: rgba(15, 23, 42, 0.6); color: white; border-radius: 6px; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; text-decoration: none; backdrop-filter: blur(4px); transition: 0.2s; opacity: 0.75; z-index: 50; cursor: pointer; border: none; padding: 0; }
  375.         .overlay-btn:hover { background: rgba(15, 23, 42, 0.9); opacity: 1; transform: scale(1.05); }
  376.         .overlay-dl-btn { right: 6px; }
  377.         .overlay-edit-btn { right: 40px; color: #10b981; }
  378.         .overlay-extend-btn { right: 74px; color: #3b82f6; }
  379.  
  380.         .ref-item { display: flex; align-items: center; gap: 10px; padding: 8px; background: #1e293b; border: 1px solid #334155; border-radius: 6px; margin-bottom: 6px; cursor: grab; box-shadow: 0 2px 4px rgba(0,0,0,0.2); transition: 0.1s; }
  381.         .ref-item.inactive { opacity: 0.6; filter: grayscale(0.8); }
  382.         .ref-item:active { cursor: grabbing; }
  383.         .ref-item.dragging { opacity: 0.4; }
  384.         .ref-handle { font-size: 14px; color: #475569; cursor: grab; padding: 0 4px; }
  385.         .ref-thumb { width: 90px; height: 90px; border-radius: 4px; object-fit: cover; border: 1px solid #334155; background: #0f172a; transition: 0.2s; }
  386.         .ref-info { flex: 1; display: flex; flex-direction: column; justify-content: center; gap: 6px; min-width: 0; }
  387.         .ref-toggle-label { font-size: 11px; color: #cbd5e1; display: flex; align-items: center; gap: 4px; cursor: pointer; user-select: none; }
  388.         .ref-toggle-label input { width: 12px; height: 12px; cursor: pointer; accent-color: #10b981; margin: 0; padding: 0; }
  389.         .ref-btn-group { display: flex; gap: 4px; width: 100%; margin-top: auto; }
  390.         .ref-action-btn { flex: 1; border-radius: 4px; padding: 4px 0; cursor: pointer; font-size: 10px; transition: 0.2s; font-weight: bold; text-align: center; }
  391.         .ref-insert { background: #0f766e; color: #ccfbf1; border: 1px solid #115e59; }
  392.         .ref-insert:hover { background: #115e59; color: #f0fdfa; }
  393.         .ref-del { background: #450a0a; color: #fca5a5; border: 1px solid #7f1d1d; }
  394.         .ref-del:hover { background: #7f1d1d; color: #fee2e2; }
  395.         .drag-over-top { border-top: 2px solid #22c55e !important; }
  396.         .drag-over-bottom { border-bottom: 2px solid #22c55e !important; }
  397.  
  398.         .history-card { background: #0f172a; border: 1px solid #334155; border-radius: 6px; overflow: hidden; margin-bottom: 10px; }
  399.         .history-card .overlay-btn { width: 24px; height: 24px; top: 4px; border-radius: 4px; }
  400.         .history-card .overlay-dl-btn { right: 4px; }
  401.         .history-card .overlay-edit-btn { right: 32px; }
  402.         .history-card .overlay-extend-btn { right: 60px; }
  403.         .history-card p { padding: 8px; margin: 0; font-size: 11px; color: #94a3b8; cursor: pointer; transition: 0.2s; }
  404.         .history-card p:hover { background: #1e293b; color: #e2e8f0; }
  405.  
  406.         .current-card { border-radius: 8px; overflow: hidden; box-shadow: 0 4px 10px rgba(0,0,0,0.3); border: 1px solid #334155; background: #1e293b; transition: all 0.3s ease; }
  407.         .current-card.downloaded { border-color: #10b981 !important; box-shadow: 0 0 10px rgba(16,185,129,0.3) !important; }
  408.         .current-card img, .current-card video { max-height: 50vh; object-fit: contain; background: #0f172a; }
  409.  
  410.         .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); }
  411.         .modal-box { background: #1e293b; border-radius: 10px; width: 500px; max-width: 90vw; padding: 15px; display: flex; flex-direction: column; gap: 12px; box-shadow: 0 10px 40px rgba(0,0,0,0.5); border: 1px solid #334155; max-height: 90vh; }
  412.         .fav-item { display: flex; justify-content: space-between; align-items: center; background: #0f172a; padding: 8px; border-radius: 6px; border: 1px solid #334155; }
  413.         .fav-text { flex: 1; font-size: 12px; color: #cbd5e1; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-right: 10px; transition: color 0.2s; }
  414.         .fav-text:hover { color: #10b981; }
  415.  
  416.         #settings-btn { position: fixed; top: 10px; right: 15px; font-size: 20px; cursor: pointer; z-index: 100; transition: transform 0.2s; opacity: 0.8;}
  417.         #settings-btn:hover { transform: rotate(45deg); opacity: 1; }
  418.         #logs-btn { position: fixed; top: 10px; right: 45px; font-size: 20px; cursor: pointer; z-index: 100; transition: transform 0.2s; opacity: 0.8;}
  419.         #logs-btn:hover { transform: scale(1.15); opacity: 1; }
  420.  
  421.         #xai-lightbox { cursor: zoom-out; }
  422.         #xai-lightbox-wrapper { position: relative; display: inline-block; max-width: 95vw; max-height: 95vh; }
  423.         #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); }
  424.  
  425.         .prompt-container { position: relative; width: 100%; height: 100px; min-height: 70px; margin-bottom: 10px; resize: vertical; overflow: hidden; border: 1px solid #334155; border-radius: 6px; background: #0f172a; transition: 0.2s; box-sizing: border-box; font-family: 'Inter', -apple-system, sans-serif; }
  426.         .prompt-container:focus-within { border-color: #64748b; background: #1e293b; box-shadow: 0 0 0 2px rgba(100,116,139,0.2); }
  427.         .prompt-container.prompt-drag-over { border-color: #10b981 !important; background: #064e3b !important; box-shadow: 0 0 0 2px rgba(16,185,129,0.15) !important; }
  428.         #prompt-backdrop { position: absolute; top: 0; left: 0; right: 0; bottom: 0; padding: 8px; font-size: 13px; font-family: inherit; white-space: pre-wrap; word-wrap: break-word; overflow-y: auto; color: transparent; z-index: 1; opacity: 0; box-sizing: border-box; pointer-events: none; }
  429.         .prompt-tag { pointer-events: auto; }
  430.         #xai-api-prompt { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 2; resize: none; background: transparent; border: none; box-shadow: none; outline: none; padding: 8px; font-size: 13px; font-family: inherit; color: #f8fafc; box-sizing: border-box; margin: 0; }
  431.         #xai-api-prompt:focus { background: transparent; border: none; box-shadow: none; }
  432.  
  433.         .warning-box { border-radius: 6px; padding: 6px 10px; font-size: 11px; margin-bottom: 8px; font-weight: 500; display: block; }
  434.         .warning-red { background: #450a0a; color: #fca5a5; border: 1px solid #7f1d1d; }
  435.         .warning-yellow { background: #422006; color: #fcd34d; border: 1px solid #78350f; }
  436.     `;
  437.  
  438.     const UI_HTML = `
  439.         <div id="xai-pro-app">
  440.             <div id="settings-btn" title="Studio Settings">⚙️</div>
  441.             <div id="logs-btn" title="Debug Logs">🐛</div>
  442.  
  443.             <!-- LEFT COLUMN -->
  444.             <div class="col-left">
  445.                 <div class="panel" style="flex-shrink: 0;">
  446.                     <div style="display: flex; background: transparent; border-radius: 0 0 6px 6px; margin-bottom: 6px; padding: 10px; padding-bottom: 0;">
  447.                         <select id="master-action" size="4" class="action-select">
  448.                             <option value="gen_image" selected>🖼️ Generate Image</option>
  449.                             <option value="gen_video">🎥 Generate Video</option>
  450.                             <option value="edit_video">✂️ Edit Video</option>
  451.                             <option value="extend_video">➡️ Extend Video</option>
  452.                         </select>
  453.                     </div>
  454.  
  455.                     <div style="padding: 10px; display: flex; flex-direction: column; overflow-y: auto;">
  456.                         <div id="setting-ar" class="slider-block">
  457.                             <div class="slider-header"><label style="margin:0;">Aspect Ratio</label></div>
  458.                             <div class="ar-select-box" id="ar-select-box">
  459.                                 <div id="ar-selected-icon"></div>
  460.                                 <span id="ar-selected-text">Auto</span>
  461.                             </div>
  462.                         </div>
  463.  
  464.                         <div id="setting-high-res" class="slider-block" style="flex-direction: row; justify-content: space-between; align-items: center;">
  465.                             <div class="slider-header" style="margin-bottom:0;"><label style="margin:0;">High Resolution</label></div>
  466.                             <button id="high-res-btn" class="toggle-btn active">ON (2K / 720p)</button>
  467.                         </div>
  468.  
  469.                         <div id="setting-batch" class="slider-block">
  470.                             <div class="slider-header"><label style="margin:0;">Batch Size</label> <span id="val-batch">1 Image</span></div>
  471.                             <input type="range" id="xai-api-n" min="0" max="6" value="0">
  472.                             <div class="slider-ticks" id="ticks-batch"></div>
  473.                         </div>
  474.  
  475.                         <div id="setting-duration" class="slider-block" style="display: none;">
  476.                             <div class="slider-header"><label style="margin:0;">Duration</label> <span id="val-duration">5 Seconds</span></div>
  477.                             <input type="range" id="xai-api-duration" min="0" max="14" value="4">
  478.                             <div class="slider-ticks" id="ticks-duration"></div>
  479.                         </div>
  480.  
  481.                         <div id="setting-loops" class="slider-block">
  482.                             <div class="slider-header" style="margin-bottom:0;"><label style="margin:0;" title="How many consecutive API calls to perform in a row">Loops</label></div>
  483.                             <div style="display: flex; gap: 10px; align-items: center; margin-top: 4px;">
  484.                                 <input type="range" id="slider-loops" min="1" max="20" value="1" style="flex: 1; margin:0;">
  485.                                 <input type="number" id="xai-api-loops" value="1" min="1" max="100" style="width: 50px; height: 26px; padding: 2px 4px; text-align: center; border-radius: 4px; border: 1px solid #334155; background: #1e293b; color: #f8fafc;">
  486.                             </div>
  487.                         </div>
  488.                     </div>
  489.                 </div>
  490.  
  491.                 <div class="panel" style="flex: 1; min-height: 0;">
  492.                     <div class="panel-title" style="display:flex; justify-content: space-between; align-items: center;">
  493.                         <span>Previous Results</span>
  494.                         <span id="xai-reset-btn" style="cursor: pointer; color: #ef4444; text-transform: none; text-decoration: underline;">Wipe Cache</span>
  495.                     </div>
  496.                     <div id="xai-history" style="flex: 1; overflow-y: auto; padding: 10px;">
  497.                         <div style="color: #94a3b8; font-size: 11px; text-align: center; margin-top: 20px;">No history yet.</div>
  498.                     </div>
  499.                 </div>
  500.             </div>
  501.  
  502.             <!-- CENTER COLUMN -->
  503.             <div class="col-center">
  504.                 <div class="panel" style="flex: 1; background: transparent; border: none; box-shadow: none; min-height: 0;">
  505.                     <div class="panel-title" style="background: #1e293b; border-radius: 8px; border: 1px solid #334155; margin-bottom: 10px; flex-shrink: 0;">Current Generation</div>
  506.                     <div id="xai-current-images" style="flex: 1; overflow-y: auto; display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 10px; align-content: start; padding-bottom: 10px;"></div>
  507.                 </div>
  508.  
  509.                 <div class="panel" style="flex-shrink: 0; padding: 12px;">
  510.                     <label style="font-size: 13px; color: #e2e8f0; display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
  511.                         <span style="display: flex; align-items: center; gap: 8px;">
  512.                             <span>Prompt</span>
  513.                             <button id="fav-prompts-btn" class="btn-secondary" style="padding: 2px 8px; font-size: 10px; height: 22px;">⭐ Favorites</button>
  514.                         </span>
  515.                         <span style="font-size: 10px; font-weight: 400; color: #94a3b8;">(Drop image here for EXIF)</span>
  516.                     </label>
  517.                     <div id="warnings-container" style="display: flex; flex-direction: column; gap: 0px;"></div>
  518.                     <div class="prompt-container">
  519.                         <div id="prompt-backdrop"></div>
  520.                         <textarea id="xai-api-prompt" placeholder="Describe what you want to see... Supports Spintax {cat|dog} and @randomseed"></textarea>
  521.                     </div>
  522.                     <div style="display: flex; align-items: center; justify-content: space-between;">
  523.                         <div id="xai-api-status" style="font-size: 12px; color: #64748b; font-weight: 500;">Ready</div>
  524.                         <div style="display: flex; gap: 8px;">
  525.                             <button id="xai-preview-btn" class="btn-secondary">🔍 Payload</button>
  526.                             <button id="xai-cancel-btn" class="btn-secondary" style="display: none; color: #ef4444; border-color: #ef4444;">🛑 Cancel</button>
  527.                             <button id="xai-api-generate" class="btn-primary" style="width: 120px;"><div class="btn-content">Generate</div></button>
  528.                         </div>
  529.                     </div>
  530.                 </div>
  531.             </div>
  532.  
  533.             <!-- RIGHT COLUMN -->
  534.             <div class="col-right" id="col-right-dropzone">
  535.                 <div class="panel" style="flex: 1; display: flex; flex-direction: column; min-height: 0;">
  536.                     <div class="panel-title" id="ref-panel-title">Reference Media</div>
  537.                     <div style="padding: 10px; flex: 1; display: flex; flex-direction: column; overflow: hidden;">
  538.                         <div id="xai-upload-placeholder" style="border: 2px dashed #475569; border-radius: 8px; width: 100%; padding: 15px 10px; color: #94a3b8; cursor: pointer; text-align: center; transition: 0.2s; margin-bottom: 10px; background: rgba(0,0,0,0.1);">
  539.                             <div style="font-size: 20px; margin-bottom: 4px;">📥</div>
  540.                             <div style="font-size: 12px; font-weight: 600;">Drag & Drop / Ctrl+V</div>
  541.                         </div>
  542.                         <input type="file" id="xai-api-file" accept="image/*, video/mp4, video/webm" multiple style="display: none;">
  543.                         <div style="font-size: 10px; color: #94a3b8; margin-bottom: 4px; text-transform: uppercase; font-weight: bold; flex-shrink: 0;">Upload Order (Top = First)</div>
  544.                         <div id="xai-ref-list" style="flex: 1; overflow-y: auto; padding-right: 4px;"></div>
  545.                     </div>
  546.                 </div>
  547.             </div>
  548.  
  549.             <div id="ar-options-container"></div>
  550.  
  551.             <!-- MODALS -->
  552.             <div id="xai-lightbox" class="modal-overlay">
  553.                 <div id="xai-lightbox-wrapper">
  554.                     <img id="xai-lightbox-img" src="">
  555.                     <a id="xai-lightbox-dl" href="#" download="" target="_blank" class="overlay-btn 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>
  556.                 </div>
  557.             </div>
  558.  
  559.             <div id="tag-preview-box" style="position: fixed; display: none; z-index: 99999999; background: #1e293b; border: 1px solid #334155; border-radius: 6px; padding: 4px; box-shadow: 0 4px 15px rgba(0,0,0,0.5); pointer-events: none;">
  560.                 <img id="tag-preview-img" style="max-width: 150px; max-height: 150px; border-radius: 4px; display: block; object-fit: cover;">
  561.             </div>
  562.  
  563.             <!-- LOGS MODAL -->
  564.             <div id="xai-logs-modal" class="modal-overlay">
  565.                 <div class="modal-box" style="width: 800px; max-width: 95vw; height: 80vh;">
  566.                     <div style="display: flex; justify-content: space-between; align-items: center;">
  567.                         <h3 style="margin:0; font-size: 15px; color: #f8fafc;">🐛 Debug Logs</h3>
  568.                         <button id="close-logs-btn" style="background: #ef4444; color: #fff; border: none; border-radius: 6px; padding: 4px 8px; cursor: pointer; font-weight: bold; font-size: 11px;">Close</button>
  569.                     </div>
  570.                     <textarea id="logs-textarea" readonly style="flex: 1; width: 100%; background: #0f172a; color: #e2e8f0; font-family: monospace; font-size: 11px; border: 1px solid #334155; border-radius: 6px; padding: 10px; resize: none; white-space: pre;"></textarea>
  571.                     <div style="display: flex; gap: 8px;">
  572.                         <button id="copy-logs-btn" class="btn-primary" style="flex: 1;">📋 Copy Logs to Clipboard</button>
  573.                         <button id="clear-logs-btn" class="btn-secondary" style="flex: 1; color: #ef4444; border-color: #ef4444;">🗑️ Clear Logs</button>
  574.                     </div>
  575.                 </div>
  576.             </div>
  577.  
  578.             <!-- SETTINGS MODAL -->
  579.             <div id="xai-settings-modal" class="modal-overlay">
  580.                 <div class="modal-box" style="width: 500px;">
  581.                     <h3 style="margin:0; color: #f8fafc; font-size: 15px;">⚙️ Studio Settings</h3>
  582.                     <div style="display:flex; gap:10px;">
  583.                         <div style="flex:1;"><label>Max Retries</label><input type="number" id="set-retries" min="1" max="100"></div>
  584.                         <div style="flex:1;"><label>Delay Min (ms)</label><input type="number" id="set-delay-min" min="500" step="500"></div>
  585.                         <div style="flex:1;"><label>Delay Max (ms)</label><input type="number" id="set-delay-max" min="1000" step="500"></div>
  586.                     </div>
  587.                     <div><label>API Base URL (Leave empty for default console.x.ai)</label><input type="text" id="set-api-base-url" placeholder="e.g., https://api.proxy.com"></div>
  588.                     <div style="display:flex; align-items:center; gap: 8px; margin-top: 4px;"><input type="checkbox" id="set-notifications" style="width: 16px; height: 16px;"><label for="set-notifications" style="margin: 0; cursor: pointer;">Enable Notifications (Sound & Tab Blink)</label></div>
  589.                     <div style="display:flex; align-items:center; gap: 8px; margin-top: 4px;"><input type="checkbox" id="set-save-png" style="width: 16px; height: 16px;"><label for="set-save-png" style="margin: 0; cursor: pointer;">Save as Original PNG (Disable JPEG / EXIF Prompt)</label></div>
  590.                     <div style="display:flex; align-items:center; gap: 8px; margin-top: 4px;"><input type="checkbox" id="set-auto-download" style="width: 16px; height: 16px;"><label for="set-auto-download" style="margin: 0; cursor: pointer;">Auto-Download Generated Media</label></div>
  591.                     <div style="display:flex; align-items:center; gap: 8px; margin-top: 4px;"><input type="checkbox" id="set-anti-throttling" style="width: 16px; height: 16px;"><label for="set-anti-throttling" style="margin: 0; cursor: pointer;">Prevent Background Throttling (Plays silent audio)</label></div>
  592.                     <div style="display:flex; align-items:center; gap: 8px; margin-top: 4px;"><input type="checkbox" id="set-auto-retry-video" style="width: 16px; height: 16px;"><label for="set-auto-retry-video" style="margin: 0; cursor: pointer;">Auto-Retry Stuck Videos (Timeout / Moderated)</label></div>
  593.                     <div style="display:flex; align-items:center; gap: 8px; margin-top: 4px;"><input type="checkbox" id="set-break-loop-on-failure" style="width: 16px; height: 16px;"><label for="set-break-loop-on-failure" style="margin: 0; cursor: pointer;">Break loops if one iteration fails entirely.</label></div>
  594.                     <div style="display:flex; gap:10px; margin-top: 4px;">
  595.                         <div style="flex:1;"><label>Video Poll Timeout (s)</label><input type="number" id="set-video-timeout" min="10" step="10"></div>
  596.                         <div style="flex:1;"><label title="When 'Rate limit exceeded' occurs">Rate Limit Delay (s)</label><input type="number" id="set-rate-limit-delay" min="1" step="1"></div>
  597.                         <div style="flex:1;"><label>Base Font Size (px)</label><input type="number" id="set-font-size" min="10" max="24"></div>
  598.                     </div>
  599.                     <div style="border-top: 1px solid #334155; margin-top: 6px; padding-top: 6px;"></div>
  600.                     <div style="display:flex; align-items:center; gap: 8px; margin-top: 4px;"><input type="checkbox" id="set-suppress-warning" style="width: 16px; height: 16px;"><label for="set-suppress-warning" style="margin: 0; cursor: pointer; color: #fcd34d;">Suppress yellow ">8s video edit" warning</label></div>
  601.                     <div style="display:flex; align-items:center; gap: 8px; margin-top: 4px;"><input type="checkbox" id="set-video-autoplay" style="width: 16px; height: 16px;"><label for="set-video-autoplay" style="margin: 0; cursor: pointer;">Autoplay generated videos in gallery</label></div>
  602.                     <div style="display:flex; align-items:center; gap: 8px; margin-top: 4px;"><input type="checkbox" id="set-video-muted" style="width: 16px; height: 16px;"><label for="set-video-muted" style="margin: 0; cursor: pointer;">Start generated videos muted</label></div>
  603.                     <div style="border-top: 1px solid #334155; margin-top: 6px; padding-top: 6px;">
  604.                         <label>Custom CSS Overrides</label>
  605.                         <textarea id="set-custom-css" style="font-family: monospace; height: 60px; font-size: 11px;" placeholder="/* e.g., #xai-pro-app { background: red; } */"></textarea>
  606.                     </div>
  607.                     <div style="display: flex; gap: 8px; margin-top: 5px; border-top: 1px solid #334155; padding-top: 10px;">
  608.                         <button id="export-data-btn" class="btn-secondary" style="flex:1;">📤 Export Data</button>
  609.                         <button id="import-data-btn" class="btn-secondary" style="flex:1;">📥 Import Data</button>
  610.                         <input type="file" id="import-data-file" accept=".json" style="display: none;">
  611.                     </div>
  612.                     <div style="display: flex; gap: 8px; margin-top: 5px;">
  613.                         <button id="save-settings-btn" class="btn-primary" style="flex:1;">Save Settings</button>
  614.                         <button id="close-settings-btn" class="btn-secondary" style="flex:1;">Cancel</button>
  615.                     </div>
  616.                 </div>
  617.             </div>
  618.  
  619.             <!-- FAVORITES MODAL -->
  620.             <div id="xai-fav-modal" class="modal-overlay">
  621.                 <div class="modal-box" style="width: 550px;">
  622.                     <div style="display: flex; justify-content: space-between; align-items: center;">
  623.                         <h3 style="margin:0; color: #f8fafc; font-size: 15px;">⭐ Favorite Prompts</h3>
  624.                         <button id="close-fav-btn" style="background: none; border: none; color: #ef4444; font-size: 20px; cursor: pointer; font-weight: bold; line-height: 1;">&times;</button>
  625.                     </div>
  626.                     <button id="add-current-fav-btn" class="btn-primary" style="background: linear-gradient(180deg, #10b981 0%, #059669 100%);">➕ Save Current Prompt to Favorites</button>
  627.                     <div id="fav-list" style="flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 6px; padding-right: 4px;"></div>
  628.                 </div>
  629.             </div>
  630.  
  631.             <!-- PAYLOAD MODAL -->
  632.             <div id="xai-payload-modal" class="modal-overlay">
  633.                 <div class="modal-box" style="width: 550px;">
  634.                     <div style="display: flex; justify-content: space-between; align-items: center;">
  635.                         <h3 style="margin:0; font-size: 15px; color: #f8fafc;">API Payload Debugger</h3>
  636.                         <button id="close-payload-btn" style="background: #ef4444; color: #fff; border: none; border-radius: 6px; padding: 4px 8px; cursor: pointer; font-weight: bold; font-size: 11px;">Close</button>
  637.                     </div>
  638.                     <div><label>Endpoint URL:</label><input type="text" id="payload-endpoint" style="font-family: monospace;" value="/v1/images/generations"></div>
  639.                     <div><label>JSON Payload (Editable):</label><textarea id="payload-code" style="background: #0f172a; color: #e2e8f0; padding: 10px; border-radius: 6px; font-family: monospace; font-size: 12px; height: 250px; resize: vertical; border: 1px solid #334155;"></textarea></div>
  640.                     <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>
  641.                     <div id="custom-payload-status" style="font-size: 11px; color: #64748b; text-align: center;">Ready</div>
  642.                 </div>
  643.             </div>
  644.  
  645.             <!-- 401 CHALLENGE EXPIRED MODAL -->
  646.             <div id="xai-401-modal" class="modal-overlay">
  647.                 <div class="modal-box" style="width: 450px; text-align: center;">
  648.                     <h3 style="margin:0; color: #ef4444; font-size: 16px;">⚠️ Challenge Expired</h3>
  649.                     <p style="font-size: 13px; color: #cbd5e1; line-height: 1.5; margin-top: 10px;">
  650.                         Unfortunately, manual changes are required. Your cookie <strong>"x-anonuserid"</strong> for the domain <strong>"console.x.ai"</strong> needs to be deleted to continue generation, and it is impossible to do from a script.
  651.                     </p>
  652.                     <p style="font-size: 13px; color: #cbd5e1; line-height: 1.5;">
  653.                         Use developer tools (F12) to delete it manually, or open another incognito/private tab and reimport your settings/references.
  654.                     </p>
  655.                     <button id="close-401-btn" class="btn-primary" style="margin-top: 10px; background: #ef4444; border: none;">Understood</button>
  656.                 </div>
  657.             </div>
  658.         </div>
  659.     `;
  660.  
  661.     // ─── INITIALIZATION ───────────────────────────────────────────────────────
  662.     let updateUIForEditMedia = null;
  663.     let activeDurationMap = MAPS.durGen;
  664.  
  665.     const initApp = () => {
  666.         if (!document.body) return setTimeout(initApp, 50);
  667.         const hideNode = (node) => {
  668.             if (node.nodeType === 1 && node.id !== 'xai-pro-app' && !['SCRIPT', 'STYLE', 'LINK'].includes(node.tagName)) {
  669.                 node.style.display = 'none';
  670.             }
  671.         };
  672.         Array.from(document.body.children).forEach(hideNode);
  673.         new MutationObserver((mutations) => {
  674.             mutations.forEach(m => m.addedNodes.forEach(hideNode));
  675.         }).observe(document.body, { childList: true });
  676.  
  677.         if (!document.getElementById('xai-pro-app')) {
  678.             const style = document.createElement('style'); style.innerHTML = UI_CSS; document.head.appendChild(style);
  679.             const customStyle = document.createElement('style'); customStyle.id = 'xai-custom-css-block'; customStyle.innerHTML = appSettings.customCSS; document.head.appendChild(customStyle);
  680.             document.body.insertAdjacentHTML('beforeend', UI_HTML);
  681.             updateFontSize(appSettings.uiFontSize);
  682.             bindLogic();
  683.         }
  684.     };
  685.  
  686.     let referenceMedia =[];
  687.     let currentAr = localStorage.getItem('xai_api_ar') || 'Auto';
  688.     let fullPayloadMemory = {};
  689.     let currentAbortController = null;
  690.     let isRequestCancelled = false;
  691.  
  692.     function saveRefs() {
  693.         try { localStorage.setItem('xai_api_refs', JSON.stringify(referenceMedia)); } catch(e) {}
  694.     }
  695.  
  696.     function findMapIndex(arr, val) {
  697.         let idx = arr.findIndex(item => (item.v || item).toString() === val.toString());
  698.         return idx !== -1 ? idx : 0;
  699.     }
  700.  
  701.     function drawTicks(containerId, map, colorFn) {
  702.         const cont = document.getElementById(containerId);
  703.         if (!cont) return;
  704.         cont.innerHTML = map.map((item, index) => {
  705.             let val = item.v || item; let label = item.t || val;
  706.             let color = colorFn ? colorFn(val) : '#64748b';
  707.  
  708.             let percent = map.length > 1 ? (index / (map.length - 1)) * 100 : 50;
  709.             let offset = 8 - (percent / 100) * 16;
  710.  
  711.             return `<span style="color:${color}; left:calc(${percent}% + ${offset}px);">${label}</span>`;
  712.         }).join('');
  713.     }
  714.  
  715.     function getDurationColor(val) {
  716.         const v = parseInt(val);
  717.         if (v <= 8) return '#f8fafc';
  718.         if (v <= 10) return '#94a3b8';
  719.         return '#ef4444';
  720.     }
  721.  
  722.     function syncSlider(sliderEl, textEl, mapArr, formatFn) {
  723.         const val = mapArr[sliderEl.value];
  724.         textEl.innerText = formatFn ? formatFn(val) : (val.l || val);
  725.     }
  726.  
  727.     function bindLogic() {
  728.         const els = {
  729.             app: document.getElementById('xai-pro-app'),
  730.             prompt: document.getElementById('xai-api-prompt'), promptBackdrop: document.getElementById('prompt-backdrop'), warningsContainer: document.getElementById('warnings-container'), promptContainer: document.querySelector('.prompt-container'),
  731.             action: document.getElementById('master-action'),
  732.             highResBtn: document.getElementById('high-res-btn'),
  733.             n: document.getElementById('xai-api-n'), duration: document.getElementById('xai-api-duration'),
  734.             loopsSlider: document.getElementById('slider-loops'), loopsNum: document.getElementById('xai-api-loops'),
  735.  
  736.             settingAr: document.getElementById('setting-ar'), settingHighRes: document.getElementById('setting-high-res'), settingBatch: document.getElementById('setting-batch'), settingDuration: document.getElementById('setting-duration'),
  737.  
  738.             fileInput: document.getElementById('xai-api-file'), dropzone: document.getElementById('col-right-dropzone'), uploadPlaceholder: document.getElementById('xai-upload-placeholder'), refList: document.getElementById('xai-ref-list'), refTitle: document.getElementById('ref-panel-title'),
  739.  
  740.             btn: document.getElementById('xai-api-generate'), previewBtn: document.getElementById('xai-preview-btn'), cancelBtn: document.getElementById('xai-cancel-btn'), status: document.getElementById('xai-api-status'), history: document.getElementById('xai-history'), currentImages: document.getElementById('xai-current-images'), reset: document.getElementById('xai-reset-btn'),
  741.  
  742.             arSelectBox: document.getElementById('ar-select-box'), arOptionsCont: document.getElementById('ar-options-container'), arSelectedIcon: document.getElementById('ar-selected-icon'), arSelectedText: document.getElementById('ar-selected-text'),
  743.  
  744.             lightbox: document.getElementById('xai-lightbox'), lightboxImg: document.getElementById('xai-lightbox-img'), lightboxDl: document.getElementById('xai-lightbox-dl'),
  745.             previewBox: document.getElementById('tag-preview-box'), previewImg: document.getElementById('tag-preview-img'),
  746.             payloadModal: document.getElementById('xai-payload-modal'), payloadEndpoint: document.getElementById('payload-endpoint'), payloadCode: document.getElementById('payload-code'), closePayloadBtn: document.getElementById('close-payload-btn'), sendCustomBtn: document.getElementById('send-custom-payload-btn'), customStatus: document.getElementById('custom-payload-status'),
  747.             settingsBtn: document.getElementById('settings-btn'), settingsModal: document.getElementById('xai-settings-modal'), closeSettingsBtn: document.getElementById('close-settings-btn'), saveSettingsBtn: document.getElementById('save-settings-btn'), exportDataBtn: document.getElementById('export-data-btn'), importDataBtn: document.getElementById('import-data-btn'), importDataFile: document.getElementById('import-data-file'),
  748.             favBtn: document.getElementById('fav-prompts-btn'), favModal: document.getElementById('xai-fav-modal'), closeFavBtn: document.getElementById('close-fav-btn'), addFavBtn: document.getElementById('add-current-fav-btn'), favList: document.getElementById('fav-list'),
  749.             logsBtn: document.getElementById('logs-btn'), logsModal: document.getElementById('xai-logs-modal'), closeLogsBtn: document.getElementById('close-logs-btn'), copyLogsBtn: document.getElementById('copy-logs-btn'), clearLogsBtn: document.getElementById('clear-logs-btn'), logsTextarea: document.getElementById('logs-textarea'),
  750.             modal401: document.getElementById('xai-401-modal'), close401Btn: document.getElementById('close-401-btn')
  751.         };
  752.  
  753.         drawTicks('ticks-batch', MAPS.batch);
  754.  
  755.         let isHighRes = true;
  756.  
  757.         const updateGenSettingsMemory = () => {
  758.             localStorage.setItem('xai_api_gen_settings', JSON.stringify({
  759.                 action: els.action.value, isHighRes: isHighRes,
  760.                 n: MAPS.batch[els.n.value], duration: activeDurationMap[els.duration.value], loops: els.loopsNum.value
  761.             }));
  762.         };
  763.  
  764.         els.highResBtn.addEventListener('click', () => {
  765.             isHighRes = !isHighRes;
  766.             els.highResBtn.className = isHighRes ? 'toggle-btn active' : 'toggle-btn';
  767.             els.highResBtn.innerText = isHighRes ? 'ON (2K / 720p)' : 'OFF (1K / 480p)';
  768.             updateGenSettingsMemory();
  769.         });
  770.         els.n.addEventListener('input', () => { syncSlider(els.n, document.getElementById('val-batch'), MAPS.batch, v => v + (v===1?' Image':' Images')); updateGenSettingsMemory(); });
  771.         els.duration.addEventListener('input', () => { syncSlider(els.duration, document.getElementById('val-duration'), activeDurationMap, v => v + ' Seconds'); updateGenSettingsMemory(); updateWarnings(); });
  772.  
  773.         els.loopsSlider.addEventListener('input', e => { els.loopsNum.value = e.target.value; updateGenSettingsMemory(); });
  774.         els.loopsNum.addEventListener('input', e => { let v = parseInt(e.target.value) || 1; els.loopsSlider.value = Math.min(v, 20); updateGenSettingsMemory(); });
  775.  
  776.         els.logsBtn.addEventListener('click', () => { els.logsTextarea.value = formatLogsForExport(); els.logsModal.style.display = 'flex'; els.logsTextarea.scrollTop = els.logsTextarea.scrollHeight; });
  777.         els.closeLogsBtn.addEventListener('click', () => els.logsModal.style.display = 'none');
  778.         els.copyLogsBtn.addEventListener('click', () => { els.logsTextarea.select(); document.execCommand('copy'); alert('Logs copied to clipboard!'); });
  779.         els.clearLogsBtn.addEventListener('click', () => { if (confirm("Clear all logs?")) { appLogs.length = 0; els.logsTextarea.value = ''; } });
  780.  
  781.         els.close401Btn.addEventListener('click', () => els.modal401.style.display = 'none');
  782.  
  783.         function updateWarnings() {
  784.             els.warningsContainer.innerHTML = ''; let html = '';
  785.             if (/--[a-zA-Z0-9_-]+=/.test(els.prompt.value)) html += `<div class="warning-box warning-red">⚠️ A gentle reminder that --parameter=value has never been shown to do anything at all, including --seed and --sid, but you do you!</div>`;
  786.             const action = els.action.value; const duration = activeDurationMap[els.duration.value] || 0; const activeRefs = referenceMedia.filter(m => m.active).length;
  787.             if (action === 'gen_video') {
  788.                 if (activeRefs > 0 && duration > 10) html += `<div class="warning-box warning-red">⚠️ Grok seems to always refuse to generate videos by reference with length above 10s. Without references it works fine.</div>`;
  789.                 if (duration > 8 && !appSettings.suppressEditWarning) html += `<div class="warning-box warning-yellow">⚠️ Note: edit won't work on videos longer than 8s like the one you are about to generate. You can suppress this warning in options.</div>`;
  790.            }
  791.            els.warningsContainer.innerHTML = html;
  792.        }
  793.  
  794.        function syncPromptBackdrop() {
  795.            const val = els.prompt.value; updateWarnings();
  796.            let html = val.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
  797.            const activeRefs = referenceMedia.filter(m => m.active);
  798.            html = html.replace(/&lt;IMAGE_(\d+)&gt;/g, (match, idxStr) => { const idx = parseInt(idxStr); return activeRefs[idx] ? `<span class="prompt-tag" data-idx="${idx}">${match}</span>` : match; });
  799.            els.promptBackdrop.innerHTML = html.replace(/\n/g, '<br>') + '<br>';
  800.            els.promptBackdrop.scrollTop = els.prompt.scrollTop; els.promptBackdrop.scrollLeft = els.prompt.scrollLeft;
  801.        }
  802.  
  803.        function setPromptVal(val) { els.prompt.focus(); els.prompt.setSelectionRange(0, els.prompt.value.length); document.execCommand('insertText', false, val); localStorage.setItem('xai_api_prompt', val); syncPromptBackdrop(); }
  804.        if (localStorage.getItem('xai_api_prompt')) setPromptVal(localStorage.getItem('xai_api_prompt'));
  805.        els.prompt.addEventListener('input', () => { localStorage.setItem('xai_api_prompt', els.prompt.value); syncPromptBackdrop(); });
  806.        els.prompt.addEventListener('scroll', () => { els.promptBackdrop.scrollTop = els.prompt.scrollTop; els.promptBackdrop.scrollLeft = els.prompt.scrollLeft; });
  807.  
  808.        let hidePreviewTimeout;
  809.        els.prompt.addEventListener('mousemove', (e) => {
  810.            if (e.buttons !== 0) { els.previewBox.style.display = 'none'; return; }
  811.            els.prompt.style.pointerEvents = 'none'; const el = document.elementFromPoint(e.clientX, e.clientY); els.prompt.style.pointerEvents = 'auto';
  812.            if (el && el.classList.contains('prompt-tag')) {
  813.                const idx = el.getAttribute('data-idx'); const media = referenceMedia.filter(m => m.active)[parseInt(idx)];
  814.                if (media) {
  815.                    els.previewBox.style.display = 'block'; els.previewImg.src = media.thumb || media.base64;
  816.                    let tx = e.clientX + 15; let ty = e.clientY + 15;
  817.                    if (tx + 160 > window.innerWidth) tx = e.clientX - 165; if (ty + 160 > window.innerHeight) ty = e.clientY - 165;
  818.                    els.previewBox.style.left = tx + 'px'; els.previewBox.style.top = ty + 'px';
  819.                    clearTimeout(hidePreviewTimeout);
  820.                }
  821.            } else hidePreviewTimeout = setTimeout(() => els.previewBox.style.display = 'none', 50);
  822.        });
  823.        els.prompt.addEventListener('mouseleave', () => els.previewBox.style.display = 'none');
  824.  
  825.        try { const storedRefs = JSON.parse(localStorage.getItem('xai_api_refs')); if (Array.isArray(storedRefs)) referenceMedia = storedRefs; } catch(e) {}
  826.  
  827.        els.settingsBtn.addEventListener('click', () => {
  828.            document.getElementById('set-retries').value = appSettings.retries; document.getElementById('set-delay-min').value = appSettings.delayMin; document.getElementById('set-delay-max').value = appSettings.delayMax; document.getElementById('set-rate-limit-delay').value = appSettings.rateLimitDelay; document.getElementById('set-notifications').checked = appSettings.notifications; document.getElementById('set-save-png').checked = appSettings.saveAsPng; document.getElementById('set-auto-download').checked = appSettings.autoDownload; document.getElementById('set-anti-throttling').checked = appSettings.antiThrottling; document.getElementById('set-auto-retry-video').checked = appSettings.autoRetryStuckVideo; document.getElementById('set-break-loop-on-failure').checked = appSettings.breakLoopOnFailure; document.getElementById('set-video-timeout').value = appSettings.videoPollTimeout; document.getElementById('set-suppress-warning').checked = appSettings.suppressEditWarning; document.getElementById('set-video-autoplay').checked = appSettings.videoAutoplay; document.getElementById('set-video-muted').checked = appSettings.videoMuted; document.getElementById('set-api-base-url').value = appSettings.apiBaseUrl || ''; document.getElementById('set-custom-css').value = appSettings.customCSS || ''; document.getElementById('set-font-size').value = appSettings.uiFontSize;
  829.            els.settingsModal.style.display = 'flex';
  830.        });
  831.        els.saveSettingsBtn.addEventListener('click', () => {
  832.            appSettings.retries = parseInt(document.getElementById('set-retries').value) || 20; appSettings.delayMin = parseInt(document.getElementById('set-delay-min').value) || 2000; appSettings.delayMax = Math.max(appSettings.delayMin, parseInt(document.getElementById('set-delay-max').value) || 4000); appSettings.rateLimitDelay = parseInt(document.getElementById('set-rate-limit-delay').value) || 60; appSettings.notifications = document.getElementById('set-notifications').checked; appSettings.saveAsPng = document.getElementById('set-save-png').checked; appSettings.autoDownload = document.getElementById('set-auto-download').checked; appSettings.antiThrottling = document.getElementById('set-anti-throttling').checked; appSettings.autoRetryStuckVideo = document.getElementById('set-auto-retry-video').checked; appSettings.breakLoopOnFailure = document.getElementById('set-break-loop-on-failure').checked; appSettings.videoPollTimeout = parseInt(document.getElementById('set-video-timeout').value) || 300; appSettings.suppressEditWarning = document.getElementById('set-suppress-warning').checked; appSettings.videoAutoplay = document.getElementById('set-video-autoplay').checked; appSettings.videoMuted = document.getElementById('set-video-muted').checked; appSettings.apiBaseUrl = document.getElementById('set-api-base-url').value.trim(); appSettings.customCSS = document.getElementById('set-custom-css').value; appSettings.uiFontSize = parseInt(document.getElementById('set-font-size').value) || 13;
  833.            localStorage.setItem('xai_api_settings', JSON.stringify(appSettings)); document.getElementById('xai-custom-css-block').innerHTML = appSettings.customCSS; updateFontSize(appSettings.uiFontSize); updateWarnings(); els.settingsModal.style.display = 'none';
  834.        });
  835.        els.closeSettingsBtn.addEventListener('click', () => els.settingsModal.style.display = 'none');
  836.  
  837.        els.exportDataBtn.addEventListener('click', () => {
  838.            const blob = new Blob([JSON.stringify({settings: appSettings, refs: referenceMedia}, null, 2)], { type: 'application/json' });
  839.            const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `xAI_Pro_Studio_Backup_${makeTimestamp()}.json`; a.click(); URL.revokeObjectURL(url);
  840.        });
  841.        els.importDataBtn.addEventListener('click', () => els.importDataFile.click());
  842.        els.importDataFile.addEventListener('change', (e) => {
  843.            const file = e.target.files[0]; if (!file) return; const reader = new FileReader();
  844.            reader.onload = (evt) => {
  845.                try {
  846.                    const parsed = JSON.parse(evt.target.result);
  847.                    if (parsed.settings) { appSettings = { ...appSettings, ...parsed.settings }; localStorage.setItem('xai_api_settings', JSON.stringify(appSettings)); }
  848.                    if (parsed.refs && Array.isArray(parsed.refs)) { referenceMedia = parsed.refs; saveRefs(); }
  849.                    alert("Import successful! Page will reload."); location.reload();
  850.                } catch(err) { alert("Failed to parse the JSON file."); }
  851.            }; reader.readAsText(file); e.target.value = '';
  852.        });
  853.  
  854.        function renderFavList() {
  855.            els.favList.innerHTML = '';
  856.            if (favoritePrompts.length === 0) { els.favList.innerHTML = '<div style="color: #64748b; font-size: 12px; text-align: center; margin-top: 15px;">No favorite prompts yet.</div>'; return; }
  857.            favoritePrompts.forEach((promptText, idx) => {
  858.                const div = document.createElement('div'); div.className = 'fav-item'; div.innerHTML = `<div class="fav-text" title="${promptText.replace(/"/g, '&quot;')}">${promptText}</div><button class="btn-secondary" style="padding: 2px 6px; font-size: 10px; border-color: #ef4444; color: #ef4444;">Del</button>`;
  859.                div.querySelector('.fav-text').addEventListener('click', () => { setPromptVal(promptText); els.favModal.style.display = 'none'; });
  860.                div.querySelector('button').addEventListener('click', () => { favoritePrompts.splice(idx, 1); localStorage.setItem('xai_api_favs', JSON.stringify(favoritePrompts)); renderFavList(); });
  861.                els.favList.appendChild(div);
  862.            });
  863.        }
  864.        els.favBtn.addEventListener('click', () => { renderFavList(); els.favModal.style.display = 'flex'; });
  865.        els.closeFavBtn.addEventListener('click', () => els.favModal.style.display = 'none');
  866.        els.addFavBtn.addEventListener('click', () => {
  867.            const p = els.prompt.value.trim(); if (!p) return alert("Prompt is empty!"); if (favoritePrompts.includes(p)) return alert("Already in favorites!");
  868.            favoritePrompts.unshift(p); localStorage.setItem('xai_api_favs', JSON.stringify(favoritePrompts)); renderFavList();
  869.        });
  870.  
  871.        els.prompt.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); els.promptContainer.classList.add('prompt-drag-over'); });
  872.        els.prompt.addEventListener('dragleave', (e) => { e.preventDefault(); e.stopPropagation(); els.promptContainer.classList.remove('prompt-drag-over'); });
  873.        els.prompt.addEventListener('drop', async (e) => {
  874.            e.preventDefault(); e.stopPropagation(); els.promptContainer.classList.remove('prompt-drag-over');
  875.            if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
  876.                const file = e.dataTransfer.files[0]; const oldVal = els.prompt.value; setPromptVal("Extracting prompt from EXIF...");
  877.                const extractedPrompt = await extractPromptFromImage(file);
  878.                if (extractedPrompt) setPromptVal(extractedPrompt); else { setPromptVal(oldVal); alert("No prompt found in this image's EXIF data."); }
  879.            }
  880.        });
  881.  
  882.        function getMaxMedia() { const val = els.action.value; return val === 'gen_image' ? 5 : (val === 'gen_video' ? 7 : 1); }
  883.  
  884.        function updateActionUI() {
  885.            const val = els.action.value;
  886.  
  887.            if (val === 'gen_image') {
  888.                els.settingAr.style.display = 'flex'; els.settingHighRes.style.display = 'flex'; els.settingBatch.style.display = 'flex'; els.settingDuration.style.display = 'none';
  889.            } else if (val === 'gen_video') {
  890.                els.settingAr.style.display = 'flex'; els.settingHighRes.style.display = 'flex'; els.settingBatch.style.display = 'none'; els.settingDuration.style.display = 'flex';
  891.                activeDurationMap = MAPS.durGen; els.duration.max = activeDurationMap.length - 1;
  892.                drawTicks('ticks-duration', activeDurationMap, getDurationColor);
  893.                syncSlider(els.duration, document.getElementById('val-duration'), activeDurationMap, v => v + ' Seconds');
  894.            } else if (val === 'edit_video') {
  895.                els.settingAr.style.display = 'none'; els.settingHighRes.style.display = 'none'; els.settingBatch.style.display = 'none'; els.settingDuration.style.display = 'none';
  896.            } else if (val === 'extend_video') {
  897.                els.settingAr.style.display = 'none'; els.settingHighRes.style.display = 'none'; els.settingBatch.style.display = 'none'; els.settingDuration.style.display = 'flex';
  898.                activeDurationMap = MAPS.durExt; els.duration.max = activeDurationMap.length - 1;
  899.                drawTicks('ticks-duration', activeDurationMap, getDurationColor);
  900.                syncSlider(els.duration, document.getElementById('val-duration'), activeDurationMap, v => v + ' Seconds');
  901.            }
  902.  
  903.            const max = getMaxMedia(); els.refTitle.innerText = `Reference Media (Max Active: ${max})`;
  904.            let activeCount = 0;
  905.            referenceMedia.forEach(m => {
  906.                let valid = true;
  907.                if ((val === 'gen_image' || val === 'gen_video') && m.isVideo) valid = false;
  908.                if ((val === 'edit_video' || val === 'extend_video') && !m.isVideo) valid = false;
  909.                if (!valid) m.active = false; else if (m.active) { activeCount++; if (activeCount > max) m.active = false; }
  910.            });
  911.            renderRefList(); updateWarnings();
  912.        }
  913.  
  914.        els.action.addEventListener('change', () => { updateActionUI(); updateGenSettingsMemory(); });
  915.  
  916.        try {
  917.            const storedGen = JSON.parse(localStorage.getItem('xai_api_gen_settings') || '{}');
  918.            if (storedGen.action) els.action.value = storedGen.action;
  919.            updateActionUI();
  920.  
  921.            if (typeof storedGen.isHighRes !== 'undefined') isHighRes = storedGen.isHighRes;
  922.            else if (storedGen.resImg === '1k' || storedGen.resVid === '480p') isHighRes = false;
  923.  
  924.            els.highResBtn.className = isHighRes ? 'toggle-btn active' : 'toggle-btn';
  925.            els.highResBtn.innerText = isHighRes ? 'ON (2K / 720p)' : 'OFF (1K / 480p)';
  926.  
  927.            if (storedGen.n) els.n.value = findMapIndex(MAPS.batch, storedGen.n);
  928.  
  929.            if (storedGen.duration) {
  930.                let idx = activeDurationMap.indexOf(parseInt(storedGen.duration));
  931.                els.duration.value = idx !== -1 ? idx : 0;
  932.            }
  933.            if (storedGen.loops) {
  934.                els.loopsNum.value = storedGen.loops;
  935.                els.loopsSlider.value = Math.min(storedGen.loops, 20);
  936.            }
  937.  
  938.            syncSlider(els.n, document.getElementById('val-batch'), MAPS.batch, v => v + (v===1?' Image':' Images'));
  939.            syncSlider(els.duration, document.getElementById('val-duration'), activeDurationMap, v => v + ' Seconds');
  940.        } catch(e) {}
  941.  
  942.        updateUIForEditMedia = (mediaUrl, isVideo, fileName, actionOverride) => {
  943.            referenceMedia.forEach(m => m.active = false);
  944.            referenceMedia.unshift({ id: Date.now() + Math.random(), base64: mediaUrl, isVideo: isVideo, active: true, thumb: isVideo ? null : mediaUrl, fileName: fileName || (isVideo ? 'Edited_Video.mp4' : 'Edited_Image.png'), w:0, h:0 });
  945.            els.action.value = actionOverride || (isVideo ? 'edit_video' : 'gen_image');
  946.            updateActionUI(); updateGenSettingsMemory(); renderRefList(); saveRefs();
  947.        };
  948.  
  949.        function getMediaDimensions(base64, isVideo) {
  950.            return new Promise(resolve => {
  951.                if (isVideo) {
  952.                    const vid = document.createElement('video');
  953.                    vid.onloadedmetadata = () => resolve({w: vid.videoWidth, h: vid.videoHeight});
  954.                    vid.onerror = () => resolve({w: 0, h: 0});
  955.                    vid.src = base64;
  956.                } else {
  957.                    const img = new Image();
  958.                    img.onload = () => resolve({w: img.width, h: img.height});
  959.                    img.onerror = () => resolve({w: 0, h: 0});
  960.                    img.src = base64;
  961.                }
  962.            });
  963.        }
  964.  
  965.        async function buildPayloadData(dynamicPromptText) {
  966.            const action = els.action.value; const payload = { model: action === 'gen_image' ? "grok-imagine-image" : "grok-imagine-video", prompt: dynamicPromptText };
  967.            let ar = currentAr.toLowerCase(); const activeRefs = referenceMedia.filter(m => m.active);
  968.  
  969.            if (currentAr === 'By First Ref') {
  970.                if (activeRefs.length > 0) {
  971.                    let ref = activeRefs[0];
  972.                    if (!ref.w || !ref.h) {
  973.                        const dims = await getMediaDimensions(ref.base64, ref.isVideo);
  974.                        ref.w = dims.w; ref.h = dims.h;
  975.                        saveRefs();
  976.                    }
  977.  
  978.                    if (ref.w && ref.h) {
  979.                        const targetRatio = ref.w / ref.h;
  980.                        let bestMatch = '1:1'; let minDiff = Infinity;
  981.                        arData.forEach(a => {
  982.                            if (a.isAuto || a.isRef) return;
  983.                            const r = a.w / a.h; const diff = Math.abs(r - targetRatio);
  984.                            if (diff < minDiff) { minDiff = diff; bestMatch = a.label; }
  985.                        });
  986.                        ar = bestMatch.toLowerCase();
  987.                        addLog('INFO', 'Calculated AR by First Ref', { targetRatio, selectedAr: ar });
  988.                    } else ar = 'auto';
  989.                } else ar = 'auto';
  990.            }
  991.  
  992.            if (action === 'gen_image') {
  993.                payload.n = MAPS.batch[els.n.value]; payload.resolution = isHighRes ? "2k" : "1k"; payload.response_format = "b64_json";
  994.                if (ar !== 'auto') payload.aspect_ratio = ar; else if (activeRefs.length > 0) payload.aspect_ratio = 'auto';
  995.                if (activeRefs.length > 0) {
  996.                    if (activeRefs.length === 1) payload.image = { url: activeRefs[0].base64 };
  997.                    else payload.images = activeRefs.map(m => ({ type: "image_url", url: m.base64 }));
  998.                }
  999.            } else if (action === 'gen_video') {
  1000.                payload.duration = activeDurationMap[els.duration.value]; payload.resolution = isHighRes ? "720p" : "480p";
  1001.                if (ar !== 'auto') payload.aspect_ratio = ar;
  1002.                if (activeRefs.length > 0) payload.reference_images = activeRefs.map(m => ({ url: m.base64 }));
  1003.            } else if (action === 'edit_video') {
  1004.                if (activeRefs.length > 0) payload.video = { url: activeRefs[0].base64 };
  1005.            } else if (action === 'extend_video') {
  1006.                payload.duration = activeDurationMap[els.duration.value];
  1007.                if (activeRefs.length > 0) payload.video = { url: activeRefs[0].base64 };
  1008.            }
  1009.            return payload;
  1010.        }
  1011.  
  1012.        els.previewBtn.addEventListener('click', async () => {
  1013.            els.previewBtn.disabled = true;
  1014.            els.previewBtn.innerHTML = `<div class="loading-spinner" style="border-top-color:#64748b; width:12px; height:12px;"></div>`;
  1015.  
  1016.            fullPayloadMemory = await buildPayloadData(parseDynamicPrompt(els.prompt.value.trim()));
  1017.  
  1018.            const displayP = JSON.parse(JSON.stringify(fullPayloadMemory)); const trunc = "[BASE64_TRUNCATED_FOR_PREVIEW]";
  1019.            if (displayP.image) { if (Array.isArray(displayP.image)) displayP.image.forEach(img => img.url = trunc); else displayP.image.url = trunc; }
  1020.            if (displayP.images && Array.isArray(displayP.images)) displayP.images.forEach(img => img.url = trunc);
  1021.            if (displayP.reference_images && Array.isArray(displayP.reference_images)) displayP.reference_images.forEach(img => img.url = trunc);
  1022.            if (displayP.video) displayP.video.url = trunc;
  1023.            const activeRefs = referenceMedia.filter(m => m.active); let endpoint = '/v1/images/generations';
  1024.            if (els.action.value === 'gen_image' && activeRefs.length > 0) endpoint = '/v1/images/edits';
  1025.            else if (els.action.value === 'gen_video') endpoint = '/v1/videos/generations';
  1026.            else if (els.action.value === 'edit_video') endpoint = '/v1/videos/edits';
  1027.            else if (els.action.value === 'extend_video') endpoint = '/v1/videos/extensions';
  1028.            els.payloadEndpoint.value = endpoint; els.payloadCode.value = JSON.stringify(displayP, null, 2); els.payloadModal.style.display = 'flex';
  1029.  
  1030.            els.previewBtn.innerHTML = `🔍 Payload`;
  1031.            els.previewBtn.disabled = false;
  1032.        });
  1033.        els.closePayloadBtn.addEventListener('click', () => els.payloadModal.style.display = 'none');
  1034.  
  1035.        function injectBase64(editedObj, originalObj) {
  1036.            if (!editedObj || typeof editedObj !== 'object') return;
  1037.            for (let key in editedObj) {
  1038.                if (typeof editedObj[key] === 'string' && editedObj[key] === '[BASE64_TRUNCATED_FOR_PREVIEW]') { if (originalObj && originalObj[key]) editedObj[key] = originalObj[key]; }
  1039.                else if (typeof editedObj[key] === 'object') injectBase64(editedObj[key], originalObj ? originalObj[key] : null);
  1040.            }
  1041.        }
  1042.  
  1043.        els.sendCustomBtn.addEventListener('click', async () => {
  1044.            initAudio(); let customPayload; try { customPayload = JSON.parse(els.payloadCode.value); } catch(e) { return alert("Invalid JSON format in textarea!"); }
  1045.            injectBase64(customPayload, fullPayloadMemory);
  1046.            const endpoint = els.payloadEndpoint.value.trim(); const isImage = customPayload.model === "grok-imagine-image";
  1047.            stealthIdentityReset(); els.sendCustomBtn.disabled = true; els.sendCustomBtn.innerHTML = `<div class="btn-content"><div class="loading-spinner"></div> Processing...</div>`; isRequestCancelled = false;
  1048.            const ok = await executeGenerationSingle(endpoint, customPayload, isImage, els.customStatus, 1, 1);
  1049.            els.sendCustomBtn.disabled = false; els.sendCustomBtn.innerHTML = `<div class="btn-content">🚀 Send Custom Payload</div>`; if (ok) notifyUser(false); else notifyUser(true);
  1050.        });
  1051.  
  1052.        els.app.addEventListener('click', (e) => {
  1053.            if (e.target && e.target.classList.contains('zoomable') && e.target.tagName === 'IMG') {
  1054.                els.lightboxImg.src = e.target.src; els.lightboxDl.href = e.target.src; els.lightboxDl.download = e.target.dataset.filename || 'Reference_Image.jpg'; els.lightbox.style.display = 'flex';
  1055.                els.lightboxDl.onclick = () => { if(e.target.closest('.current-card')) e.target.closest('.current-card').classList.add('downloaded'); };
  1056.            }
  1057.        });
  1058.        els.lightbox.addEventListener('click', (e) => { if (!e.target.closest('#xai-lightbox-dl') && !e.target.closest('.modal-box')) els.lightbox.style.display = 'none'; });
  1059.        document.addEventListener('keydown', (e) => { if(e.key === 'Escape') Array.from(document.querySelectorAll('.modal-overlay')).forEach(m => m.style.display = 'none'); });
  1060.  
  1061.        function renderArOptions() {
  1062.            els.arOptionsCont.innerHTML = '';
  1063.            arData.forEach(ar => {
  1064.                const opt = document.createElement('div'); opt.className = 'ar-option';
  1065.                opt.innerHTML = `${createArIcon(ar.w, ar.h, ar.isAuto, ar.isRef)} <span>${ar.label}</span>`;
  1066.                opt.addEventListener('click', () => { setAr(ar); els.arOptionsCont.style.display = 'none'; });
  1067.                els.arOptionsCont.appendChild(opt);
  1068.            });
  1069.        }
  1070.        function setAr(arObj) {
  1071.            currentAr = arObj.label; localStorage.setItem('xai_api_ar', currentAr);
  1072.            els.arSelectedIcon.innerHTML = createArIcon(arObj.w, arObj.h, arObj.isAuto, arObj.isRef); els.arSelectedText.innerText = arObj.label;
  1073.        }
  1074.        renderArOptions(); const initialAr = arData.find(a => a.label === currentAr) || arData[0]; setAr(initialAr);
  1075.  
  1076.        els.arSelectBox.addEventListener('click', (e) => {
  1077.            e.stopPropagation(); if (els.arOptionsCont.style.display === 'block') { els.arOptionsCont.style.display = 'none'; return; }
  1078.            const rect = els.arSelectBox.getBoundingClientRect(); els.arOptionsCont.style.top = (rect.bottom + 5) + 'px'; els.arOptionsCont.style.left = rect.left + 'px'; els.arOptionsCont.style.width = rect.width + 'px'; els.arOptionsCont.style.display = 'block';
  1079.        });
  1080.        document.addEventListener('click', (e) => { if (!els.arOptionsCont.contains(e.target)) els.arOptionsCont.style.display = 'none'; });
  1081.  
  1082.        async function processFile(file) {
  1083.            const isVid = file.type.startsWith('video/'); const isImg = file.type.startsWith('image/');
  1084.            if (!isVid && !isImg) return;
  1085.            if (isVid && (els.action.value === 'gen_image' || els.action.value === 'gen_video')) { els.action.value = 'edit_video'; updateActionUI(); updateGenSettingsMemory(); }
  1086.            else if (isImg && (els.action.value === 'edit_video' || els.action.value === 'extend_video')) { els.action.value = 'gen_image'; updateActionUI(); updateGenSettingsMemory(); }
  1087.  
  1088.            const max = getMaxMedia(); const activeCount = referenceMedia.filter(m => m.active).length;
  1089.            let thumbBase64 = null; if (isVid) thumbBase64 = await generateVideoThumbnail(file);
  1090.  
  1091.            const reader = new FileReader();
  1092.            reader.onload = async (event) => {
  1093.                const b64 = event.target.result;
  1094.                let w = 0, h = 0;
  1095.                if (isVid && file._w && file._h) { w = file._w; h = file._h; }
  1096.                else if (isImg) {
  1097.                    try {
  1098.                        const dims = await new Promise(r => { const i = new Image(); i.onload = () => r({w: i.width, h: i.height}); i.onerror = () => r({w: 0, h: 0}); i.src = b64; });
  1099.                        w = dims.w; h = dims.h;
  1100.                    } catch(e) {}
  1101.                }
  1102.                referenceMedia.push({ id: Date.now() + Math.random(), base64: b64, isVideo: isVid, active: activeCount < max, thumb: thumbBase64, fileName: file.name, w, h });
  1103.                renderRefList();
  1104.            };
  1105.            reader.readAsDataURL(file);
  1106.        }
  1107.  
  1108.        let draggedIndex = null;
  1109.        function renderRefList() {
  1110.            els.refList.innerHTML = '';
  1111.            referenceMedia.forEach((media, index) => {
  1112.                const item = document.createElement('div'); item.className = 'ref-item'; item.draggable = true;
  1113.                const activeRefs = referenceMedia.filter(m => m.active); const dynamicIndex = media.active ? activeRefs.indexOf(media) : '?';
  1114.                const actionVal = els.action.value; const disableInsert = actionVal === 'edit_video' || actionVal === 'extend_video';
  1115.  
  1116.                let thumbHtml = '';
  1117.                if (media.isVideo) {
  1118.                    if (media.thumb) thumbHtml = `<div style="position: relative; width: 90px; height: 90px; flex-shrink: 0;"><img src="${media.thumb}" class="ref-thumb" style="width: 100%; height: 100%;" title="${media.fileName || 'Video'}"><div style="position: absolute; top: 4px; right: 4px; background: rgba(0,0,0,0.7); color: white; border-radius: 4px; padding: 2px 4px; font-size: 10px;">🎥</div></div>`;
  1119.                    else thumbHtml = `<div class="ref-thumb" style="display:flex; align-items:center; justify-content:center; flex-direction:column; background:#1e293b; color:#94a3b8; font-size:24px;" title="${media.fileName || 'Video'}">🎥<span style="font-size:9px; margin-top:4px; max-width:80px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">${media.fileName || 'Video'}</span></div>`;
  1120.                } else thumbHtml = `<img src="${media.base64}" class="ref-thumb zoomable" data-filename="${media.fileName || 'Reference_'+(index+1)+'.jpg'}" title="${media.fileName || 'Click to view'}">`;
  1121.  
  1122.                let insertBtnHtml = '';
  1123.                if (!media.isVideo) {
  1124.                    if (disableInsert) insertBtnHtml = `<button class="ref-action-btn ref-insert" disabled style="opacity: 0.4; filter: grayscale(1); cursor: not-allowed;" title="Not available in this mode">➕ &lt;IMAGE_X&gt;</button>`;
  1125.                    else if (!media.active) insertBtnHtml = `<button class="ref-action-btn ref-insert" disabled style="opacity: 0.4; cursor: not-allowed;" title="Check 'Use in Payload' first">➕ &lt;IMAGE_?&gt;</button>`;
  1126.                    else insertBtnHtml = `<button class="ref-action-btn ref-insert" title="Insert into prompt">➕ &lt;IMAGE_${dynamicIndex}&gt;</button>`;
  1127.                }
  1128.  
  1129.                item.innerHTML = `<div class="ref-handle">☰</div>${thumbHtml}<div class="ref-info"><span style="font-size: 12px; color: #e2e8f0; font-weight: 600;">Ref ${index + 1} ${media.isVideo ? '(Video)' : ''}</span><label class="ref-toggle-label"><input type="checkbox" class="ref-active-toggle" ${media.active ? 'checked' : ''}> Use in Payload</label><div class="ref-btn-group">${insertBtnHtml}<button class="ref-action-btn ref-del">Remove</button></div></div>`;
  1130.  
  1131.                const insertBtn = item.querySelector('.ref-insert');
  1132.                if (insertBtn && media.active) {
  1133.                    insertBtn.addEventListener('click', (e) => {
  1134.                        const currentAction = els.action.value; if (currentAction === 'edit_video' || currentAction === 'extend_video') { e.preventDefault(); return alert("<IMAGE_X> tags not supported here."); }
  1135.                        const promptEl = els.prompt; const tag = `<IMAGE_${dynamicIndex}>`; const startPos = promptEl.selectionStart; const endPos = promptEl.selectionEnd;
  1136.                        setPromptVal(promptEl.value.substring(0, startPos) + tag + promptEl.value.substring(endPos, promptEl.value.length));
  1137.                        promptEl.selectionStart = promptEl.selectionEnd = startPos + tag.length; promptEl.focus();
  1138.                    });
  1139.                }
  1140.                if (!media.active) item.classList.add('inactive');
  1141.  
  1142.                item.querySelector('.ref-active-toggle').addEventListener('change', (e) => {
  1143.                    const max = getMaxMedia(); const currentlyActive = referenceMedia.filter(r => r.active).length;
  1144.                    if (e.target.checked && currentlyActive >= max) { alert(`Up to ${max} active references allowed.`); e.target.checked = false; return; }
  1145.                    media.active = e.target.checked; saveRefs(); renderRefList(); syncPromptBackdrop(); updateWarnings();
  1146.                });
  1147.  
  1148.                item.addEventListener('dragstart', (e) => { draggedIndex = index; e.dataTransfer.effectAllowed = 'move'; setTimeout(() => item.classList.add('dragging'), 0); });
  1149.                item.addEventListener('dragend', () => { item.classList.remove('dragging'); draggedIndex = null; document.querySelectorAll('.ref-item').forEach(el => el.classList.remove('drag-over-top', 'drag-over-bottom')); });
  1150.                item.addEventListener('dragover', (e) => { e.preventDefault(); if (draggedIndex === null || draggedIndex === index) return; const rect = item.getBoundingClientRect(); if (e.clientY - rect.top < rect.height / 2) { item.classList.add('drag-over-top'); item.classList.remove('drag-over-bottom'); } else { item.classList.add('drag-over-bottom'); item.classList.remove('drag-over-top'); } });
  1151.                item.addEventListener('dragleave', () => item.classList.remove('drag-over-top', 'drag-over-bottom'));
  1152.                item.addEventListener('drop', (e) => { e.preventDefault(); item.classList.remove('drag-over-top', 'drag-over-bottom'); if (draggedIndex === null || draggedIndex === index) return; const rect = item.getBoundingClientRect(); let insertIndex = (e.clientY - rect.top) < rect.height / 2 ? index : index + 1; if (draggedIndex < insertIndex) insertIndex--; const [movedImage] = referenceMedia.splice(draggedIndex, 1); referenceMedia.splice(insertIndex, 0, movedImage); renderRefList(); syncPromptBackdrop(); });
  1153.                item.querySelector('.ref-del').addEventListener('click', () => { referenceMedia.splice(index, 1); renderRefList(); syncPromptBackdrop(); updateWarnings(); });
  1154.                els.refList.appendChild(item);
  1155.            });
  1156.            saveRefs();
  1157.        }
  1158.  
  1159.        els.uploadPlaceholder.addEventListener('click', () => { els.fileInput.click(); });
  1160.        els.fileInput.addEventListener('change', (e) => { Array.from(e.target.files).forEach(processFile); els.fileInput.value = ''; });
  1161.        document.addEventListener('paste', (e) => { if (e.target && (e.target.id === 'payload-code' || e.target.id === 'xai-api-prompt' || e.target.id === 'set-custom-css' || e.target.tagName === 'INPUT' && e.target.type !== 'range')) return; const items = e.clipboardData.items; for (let i = 0; i < items.length; i++) if (items[i].type.indexOf('image') !== -1) processFile(items[i].getAsFile()); });
  1162.        els.dropzone.addEventListener('dragover', (e) => { e.preventDefault(); els.dropzone.classList.add('drag-over'); });
  1163.        els.dropzone.addEventListener('dragleave', () => els.dropzone.classList.remove('drag-over'));
  1164.        els.dropzone.addEventListener('drop', (e) => { e.preventDefault(); els.dropzone.classList.remove('drag-over'); if (e.dataTransfer.files) Array.from(e.dataTransfer.files).forEach(processFile); });
  1165.  
  1166.        els.cancelBtn.addEventListener('click', () => {
  1167.            isRequestCancelled = true; if (currentAbortController) currentAbortController.abort();
  1168.            toggleKeepAwake(false);
  1169.            els.cancelBtn.style.display = 'none'; els.previewBtn.style.display = 'flex';
  1170.            els.btn.disabled = false; els.btn.innerHTML = `<div class="btn-content">Generate</div>`;
  1171.            els.status.innerText = "Request Cancelled."; els.status.className = ""; els.status.style.color = "#ef4444";
  1172.        });
  1173.  
  1174.        async function executeGenerationSingle(endpoint, payload, isImageAction, statusEl, currentLoop, totalLoops) {
  1175.            let loopPrefix = totalLoops > 1 ? `[Run ${currentLoop}/${totalLoops}] ` : ''; statusEl.innerText = `${loopPrefix}Processing request...`; statusEl.className = "status-pulsing";
  1176.            addLog('INFO', `Starting generation loop ${currentLoop}/${totalLoops}`, { endpoint, isImageAction });
  1177.            currentAbortController = new AbortController(); let rateLimitFails = 0; let attempts = 0; let success = false;
  1178.  
  1179.            try {
  1180.                while (attempts < appSettings.retries && !success && !isRequestCancelled) {
  1181.                    try {
  1182.                        statusEl.innerText = `${loopPrefix}Sending request to xAI...`;
  1183.                        const fullEndpoint = getApiUrl(endpoint);
  1184.                        addLog('NETWORK_REQ', `POST ${fullEndpoint}`, payload);
  1185.                        const response = await fetch(fullEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), signal: currentAbortController.signal });
  1186.  
  1187.                        if (response.status === 401) {
  1188.                            els.modal401.style.display = 'flex';
  1189.                            statusEl.innerText = `${loopPrefix}Failed: 401 Challenge Expired.`;
  1190.                            statusEl.className = ""; statusEl.style.color = "#ef4444";
  1191.                            isRequestCancelled = true;
  1192.                            return false;
  1193.                        }
  1194.                        if (response.status === 503 || response.status === 500 || response.status === 502) { attempts++; statusEl.innerText = `${loopPrefix}Busy (Error ${response.status}). Retrying... [${attempts}/${appSettings.retries}]`; addLog('WARN', `Gateway error ${response.status}. Retrying.`); await sleep(Math.floor(Math.random() * (appSettings.delayMax - appSettings.delayMin + 1)) + appSettings.delayMin); continue; }
  1195.                        if (!response.ok) {
  1196.                            let errMsg = `HTTP ${response.status}`; let errData = null;
  1197.                            try {
  1198.                                errData = await response.json();
  1199.                                addLog('NETWORK_ERR', `Response error JSON`, { status: response.status, data: errData });
  1200.                                if (errData.error && errData.error.message) errMsg += `\n${errData.error.message}`; else if (errData.error && typeof errData.error === 'string') errMsg += `\n${errData.error}`;
  1201.                                if (errData.code) errMsg += `\nCode: ${errData.code}`; if (errData.detail) errMsg += `\n${typeof errData.detail === 'string' ? errData.detail : JSON.stringify(errData.detail)}`;
  1202.                            } catch(e) { addLog('NETWORK_ERR', `Failed to parse error response`, { status: response.status }); }
  1203.                            throw new Error(errMsg);
  1204.                        }
  1205.  
  1206.                        const data = await response.json();
  1207.                        addLog('NETWORK_RES', `Successful response`, data);
  1208.  
  1209.                        if (isImageAction) {
  1210.                            if (data && data.data && Array.isArray(data.data)) {
  1211.                                success = true; statusEl.innerText = `${loopPrefix}Processing final images...`; statusEl.style.color = "#10b981"; statusEl.className = "";
  1212.                                await Promise.all(data.data.map((imgObj, index) => processAndRenderImage(imgObj, payload.prompt, index + 1, data.data.length, els.currentImages)));
  1213.                                statusEl.innerText = "Ready"; statusEl.style.color = "#64748b";
  1214.                            } else throw new Error("Invalid response format.");
  1215.                        } else {
  1216.                            const reqId = data.request_id; if (!reqId) throw new Error(data.error ? (data.error.message || JSON.stringify(data.error)) : "No Request ID returned.");
  1217.                            let videoReady = false; let pollCount = 0; const MAX_POLLS = Math.max(1, Math.ceil(appSettings.videoPollTimeout / 5));
  1218.                            while (!videoReady && !isRequestCancelled) {
  1219.                                pollCount++; if (pollCount > MAX_POLLS) throw new Error("Timeout: Video likely dropped by filters or stuck in queue."); await sleep(5000); if (isRequestCancelled) break;
  1220.                                addLog('POLL_REQ', `Polling video ID: ${reqId}`, { pollCount, MAX_POLLS });
  1221.                                const pollRes = await fetch(getApiUrl(`/v1/videos/${reqId}`), { signal: currentAbortController.signal });
  1222.                                if (!pollRes.ok) { if (pollRes.status >= 400 && pollRes.status < 500) { let errData; try { errData = await pollRes.json(); } catch(e) {} addLog('POLL_ERR', `Poll failed HTTP ${pollRes.status}`, errData); if (errData && errData.error) throw new Error(typeof errData.error.message === 'string' ? errData.error.message : JSON.stringify(errData.error)); } continue; }
  1223.                                const pollData = await pollRes.json(); addLog('POLL_RES', `Poll Result`, pollData);
  1224.                                if (pollData.error) throw new Error(`API Error: ${pollData.error.message || JSON.stringify(pollData.error)}`);
  1225.                                const state = (pollData.status || pollData.state || 'processing').toLowerCase(); let progressText = typeof pollData.progress === 'number' ? ` ${pollData.progress}%` : "";
  1226.                                statusEl.innerText = `${loopPrefix}Polling video (Status: ${state}${progressText})...[${pollCount}/${MAX_POLLS}]`;
  1227.  
  1228.                                if (state === 'done' || state === 'completed') {
  1229.                                    videoReady = true; success = true; statusEl.innerText = "Ready"; statusEl.className = ""; statusEl.style.color = "#94a3b8"; renderVideoToGallery(pollData.video.url, payload.prompt, els.currentImages);
  1230.                                } else if (['failed', 'expired', 'rejected', 'blocked', 'moderated', 'nsfw'].includes(state) || pollData.is_sensitive) {
  1231.                                    if (pollData.video && pollData.video.url) {
  1232.                                        videoReady = true; success = true; statusEl.innerText = `${loopPrefix}Warning: Flagged as ${state}, but recovered!`; statusEl.className = ""; statusEl.style.color = "#d97706"; renderVideoToGallery(pollData.video.url, payload.prompt, els.currentImages);
  1233.                                        addLog('WARN', `Video flagged but recovered`, pollData);
  1234.                                    } else throw new Error(`Generation halted. Reason: ${state}`);
  1235.                                }
  1236.                            }
  1237.                        }
  1238.                   } catch (err) {
  1239.                        const errStr = err.message.toLowerCase();
  1240.                        addLog('ERROR', `Generation logic exception`, { message: err.message });
  1241.                        if (err.name === 'AbortError' || isRequestCancelled) { statusEl.innerText = "Request Cancelled."; statusEl.className = ""; statusEl.style.color = "#ef4444"; return false; }
  1242.                        let isLongRateLimit = false; let isShortRateLimit = false;
  1243.                        if (errStr.includes("requests per second (actual/limit)")) {
  1244.                            const secMatch = errStr.match(/requests per second \(actual\/limit\):\s*(\d+)\s*\/\s*(\d+)/); const minMatch = errStr.match(/requests per minute \(actual\/limit\):\s*(\d+)\s*\/\s*(\d+)/);
  1245.                            if (minMatch && minMatch[1] === minMatch[2]) isLongRateLimit = true; else if (secMatch && secMatch[1] === secMatch[2]) isShortRateLimit = true; else isLongRateLimit = true;
  1246.                        } else if (errStr.includes("rate limit") || errStr.includes("resource has been exhausted") || errStr.includes("http 429")) isLongRateLimit = true;
  1247.  
  1248.                        if (isShortRateLimit) {
  1249.                            attempts++; if (attempts >= appSettings.retries) { statusEl.innerText = `${loopPrefix}Failed: Max retries reached after per-second rate limits.`; statusEl.className = ""; statusEl.style.color = "#ef4444"; break; }
  1250.                            statusEl.innerText = `${loopPrefix}Per-Second Limit Hit. Retrying... [${attempts}/${appSettings.retries}]`; await sleep(Math.floor(Math.random() * (appSettings.delayMax - appSettings.delayMin + 1)) + appSettings.delayMin); continue;
  1251.                        } else if (isLongRateLimit) {
  1252.                            attempts++; if (attempts >= appSettings.retries) { statusEl.innerText = `${loopPrefix}Failed: Max retries reached after rate limits.`; statusEl.className = ""; statusEl.style.color = "#ef4444"; break; }
  1253.                            let delaySeconds = parseInt(appSettings.rateLimitDelay) || 60; let countdown = delaySeconds;
  1254.                            if ((rateLimitFails++) < 1) { await sleep(1000); continue; }
  1255.                            rateLimitFails = 0;
  1256.  
  1257.                            // Prevent background throttle during long delays
  1258.                            toggleKeepAwake(true);
  1259.                            while(countdown > 0 && !isRequestCancelled) { statusEl.innerText = `${loopPrefix}Rate Limit Hit. Retrying in ${countdown}s... [${attempts}/${appSettings.retries}]`; await sleep(1000); countdown--; }
  1260.                            toggleKeepAwake(false);
  1261.  
  1262.                            if (isRequestCancelled) return false; continue;
  1263.                        } else if ((errStr.includes("timeout: video") || errStr.includes("generation halted.") || errStr.includes("video rejected by content moderation")) && appSettings.autoRetryStuckVideo) {
  1264.                            attempts++; if (attempts >= appSettings.retries) { statusEl.innerText = `${loopPrefix}Failed: Max retries reached for stuck video.`; statusEl.className = ""; statusEl.style.color = "#ef4444"; break; }
  1265.                            statusEl.innerText = `${loopPrefix}Video Stuck/Failed. Auto-retrying... [${attempts}/${appSettings.retries}]`; await sleep(Math.floor(Math.random() * (appSettings.delayMax - appSettings.delayMin + 1)) + appSettings.delayMin);
  1266.                            if (payload.prompt) payload.prompt += "\u200B"; continue;
  1267.                        } else { statusEl.innerText = `${loopPrefix}Error: ${err.message}`; statusEl.className = ""; statusEl.style.color = "#ef4444"; break; }
  1268.                    }
  1269.                }
  1270.            } finally {
  1271.                toggleKeepAwake(false);
  1272.            }
  1273.  
  1274.            if (!success && !isRequestCancelled) {
  1275.                if (!statusEl.innerText.includes("Failed:") && !statusEl.innerText.includes("Error:")) statusEl.innerText = `${loopPrefix}Failed after ${attempts} retries.`;
  1276.                statusEl.className = ""; statusEl.style.color = "#ef4444";
  1277.                addLog('ERROR', 'Generation loop failed completely');
  1278.                return false;
  1279.            }
  1280.            return success;
  1281.        }
  1282.  
  1283.        els.btn.addEventListener('click', async () => {
  1284.            initAudio(); const basePrompt = els.prompt.value.trim(); if (!basePrompt) return alert("Please enter a prompt.");
  1285.            addLog('UI', 'Generate button clicked', { basePrompt });
  1286.            const oldImages = Array.from(els.currentImages.children);
  1287.            if (oldImages.length > 0) {
  1288.                if (els.history.innerText.includes("No history")) els.history.innerHTML = '';
  1289.                oldImages.forEach(card => { card.className = 'history-card'; const promptEl = card.querySelector('p'); promptEl.title = "Click to copy prompt"; promptEl.onclick = () => setPromptVal(promptEl.innerText); els.history.prepend(card); });
  1290.            }
  1291.  
  1292.            const action = els.action.value; const activeRefs = referenceMedia.filter(m => m.active);
  1293.            let endpoint = '/v1/images/generations';
  1294.            if (action === 'gen_image' && activeRefs.length > 0) endpoint = '/v1/images/edits';
  1295.            else if (action === 'gen_video') endpoint = '/v1/videos/generations';
  1296.            else if (action === 'edit_video') endpoint = '/v1/videos/edits';
  1297.            else if (action === 'extend_video') endpoint = '/v1/videos/extensions';
  1298.  
  1299.            const isImage = action === 'gen_image'; const totalLoops = parseInt(els.loopsNum.value) || 1;
  1300.            stealthIdentityReset(); els.btn.disabled = true; els.cancelBtn.style.display = 'flex'; els.previewBtn.style.display = 'none'; isRequestCancelled = false;
  1301.  
  1302.            let allSuccess = true;
  1303.            for (let i = 1; i <= totalLoops; i++) {
  1304.                if (isRequestCancelled) break;
  1305.                const dynamicPrompt = parseDynamicPrompt(basePrompt);
  1306.                els.btn.innerHTML = `<div class="btn-content"><div class="loading-spinner"></div> Building...</div>`;
  1307.                const payload = await buildPayloadData(dynamicPrompt);
  1308.  
  1309.                els.btn.innerHTML = `<div class="btn-content"><div class="loading-spinner"></div> Gen ${i}/${totalLoops}...</div>`;
  1310.                const ok = await executeGenerationSingle(endpoint, payload, isImage, els.status, i, totalLoops);
  1311.                if (!ok) { allSuccess = false; if (appSettings.breakLoopOnFailure) break; }
  1312.            }
  1313.  
  1314.            if (!isRequestCancelled) {
  1315.                els.cancelBtn.style.display = 'none'; els.previewBtn.style.display = 'flex'; els.btn.disabled = false; els.btn.innerHTML = `<div class="btn-content">Generate</div>`;
  1316.                if (allSuccess) { els.status.innerText = "All runs complete."; els.status.className = ""; els.status.style.color = "#64748b"; notifyUser(false); addLog('INFO', 'All generation loops completed successfully'); }
  1317.                else { notifyUser(true); addLog('ERROR', 'Generation sequence ended with errors'); }
  1318.            }
  1319.        });
  1320.  
  1321.        els.reset.addEventListener('click', () => {
  1322.            if (!confirm("Wipe cache? Your prompt and settings will be saved.")) return;
  1323.            localStorage.setItem('xai_api_prompt', els.prompt.value);
  1324.            const patterns =[/flushAfter/i, /imagine/i, /generation/i, /grok/i, /mixpanel|mp_/i, /distinct_id/i, /_rst/i, /limit/i, /credit/i];
  1325.            for (let i = localStorage.length - 1; i >= 0; i--) { const k = localStorage.key(i); if (k && !k.startsWith('xai_api_') && patterns.some(p => p.test(k))) localStorage.removeItem(k); }
  1326.            document.cookie.split(';').forEach(c => { const name = c.split('=')[0]?.trim(); 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 : ''}`; }); }); });
  1327.            const u = new URL(location.href); ['_r', '_rst'].forEach(k => u.searchParams.set(k, Date.now())); location.replace(u.toString());
  1328.        });
  1329.    }
  1330.  
  1331.    async function processAndRenderImage(imgObj, originalPrompt, num, total, galleryEl) {
  1332.        if (!imgObj.b64_json) return;
  1333.        const mime = imgObj.mime_type || "image/png"; const b64 = imgObj.b64_json; const pngDataUri = `data:${mime};base64,${b64}`;
  1334.        const finalPrompt = imgObj.revised_prompt || originalPrompt; let finalDataUri = pngDataUri; let filename = `Grok 2K - ${makeTimestamp()} - ${num}of${total}.png`;
  1335.        if (!appSettings.saveAsPng) { finalDataUri = await convertToJpegWithExif(pngDataUri, finalPrompt); filename = `Grok 2K - ${makeTimestamp()} - ${num}of${total}.jpg`; }
  1336.        const card = document.createElement('div'); card.className = 'current-card';
  1337.        card.innerHTML = `<div class="img-wrapper"><img src="${finalDataUri}" class="zoomable" data-filename="${filename}" title="Click to view fullscreen"><a href="${finalDataUri}" download="${filename}" class="overlay-btn overlay-dl-btn" title="Download Full File">${DOWNLOAD_ICON}</a><button class="overlay-btn overlay-edit-btn" title="Edit this Image">${EDIT_ICON}</button></div><p style="padding: 10px; margin: 0; font-size: 12px; color: #cbd5e1; border-top: 1px solid #334155;">${finalPrompt}</p>`;
  1338.  
  1339.        card.querySelector('.overlay-edit-btn').addEventListener('click', (e) => { e.preventDefault(); if (typeof updateUIForEditMedia === 'function') updateUIForEditMedia(finalDataUri, false, filename, 'gen_image'); });
  1340.  
  1341.        const dlBtn = card.querySelector('.overlay-dl-btn');
  1342.        dlBtn.addEventListener('click', () => { card.classList.add('downloaded'); });
  1343.  
  1344.        galleryEl.prepend(card);
  1345.  
  1346.        if (appSettings.autoDownload) { setTimeout(() => dlBtn.click(), 500); }
  1347.    }
  1348.  
  1349.    function renderVideoToGallery(videoUrl, originalPrompt, galleryEl) {
  1350.        const filename = `Grok Video - ${makeTimestamp()}.mp4`; const auto = appSettings.videoAutoplay ? "autoplay" : ""; const muted = appSettings.videoMuted ? "muted" : "";
  1351.        const card = document.createElement('div'); card.className = 'current-card';
  1352.        card.innerHTML = `<div class="img-wrapper"><video src="${videoUrl}" controls ${auto} ${muted} loop style="width: 100%; display: block; max-height: 50vh; object-fit: contain; background: #000;"></video><button class="overlay-btn overlay-dl-btn force-dl" data-url="${videoUrl}" data-filename="${filename}" style="z-index: 50; right: 6px;" title="Download MP4">${DOWNLOAD_ICON}</button><button class="overlay-btn overlay-edit-btn" style="z-index: 50; right: 40px;" title="Edit this Video">${EDIT_ICON}</button><button class="overlay-btn overlay-extend-btn" style="z-index: 50; right: 74px;" title="Extend this Video">${EXTEND_ICON}</button></div><p style="padding: 10px; margin: 0; font-size: 12px; color: #cbd5e1; border-top: 1px solid #334155;">${originalPrompt}</p>`;
  1353.  
  1354.        card.querySelector('.overlay-edit-btn').addEventListener('click', (e) => { e.preventDefault(); if (typeof updateUIForEditMedia === 'function') updateUIForEditMedia(videoUrl, true, filename, 'edit_video'); });
  1355.        card.querySelector('.overlay-extend-btn').addEventListener('click', (e) => { e.preventDefault(); if (typeof updateUIForEditMedia === 'function') updateUIForEditMedia(videoUrl, true, filename, 'extend_video'); });
  1356.  
  1357.        const dlBtn = card.querySelector('.overlay-dl-btn');
  1358.        dlBtn.addEventListener('click', (e) => {
  1359.            e.preventDefault(); const url = dlBtn.getAttribute('data-url'); const fname = dlBtn.getAttribute('data-filename'); const originalIcon = dlBtn.innerHTML;
  1360.            dlBtn.innerHTML = '<div class="loading-spinner" style="border-top-color: #10b981;"></div>'; dlBtn.style.pointerEvents = 'none'; card.classList.add('downloaded');
  1361.            if (typeof GM_xmlhttpRequest !== "undefined") {
  1362.                GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'blob', onload: function(response) {
  1363.                        if (response.status >= 200 && response.status < 300) {
  1364.                            const blobUrl = URL.createObjectURL(response.response); const a = document.createElement('a'); a.style.display = 'none'; a.href = blobUrl; a.download = fname; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
  1365.                        } else window.open(url, '_blank');
  1366.                        dlBtn.innerHTML = originalIcon; dlBtn.style.pointerEvents = 'auto';
  1367.                    }, onerror: function() { window.open(url, '_blank'); dlBtn.innerHTML = originalIcon; dlBtn.style.pointerEvents = 'auto'; } });
  1368.            } else { window.open(url, '_blank'); dlBtn.innerHTML = originalIcon; dlBtn.style.pointerEvents = 'auto'; }
  1369.        });
  1370.  
  1371.        galleryEl.prepend(card);
  1372.  
  1373.        if (appSettings.autoDownload) { setTimeout(() => dlBtn.click(), 500); }
  1374.    }
  1375.  
  1376.    if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initApp); } else { initApp(); }
  1377. })();
Add Comment
Please, Sign In to add comment