Guest User

Untitled

a guest
Apr 2nd, 2026
328
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. // ==UserScript==
  2. // @name         xAI Pro Studio Frontend (v31 - Debug Logging Edition)
  3. // @namespace    http://tampermonkey.net/
  4. // @version      31
  5. // @description  Full-Screen UI, Exif Fix, Multi-Image, Auto-Retry, Export Settings, Video Warnings, Rate Limit Backoff, Debug Logs
  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.             console.error("[xAI Studio] Logger error", e);
  52.         }
  53.     }
  54.  
  55.     function formatLogsForExport() {
  56.         return appLogs.map(l => {
  57.             let text = `[${l.ts}] [${l.level}] ${l.msg}`;
  58.             if (l.data) {
  59.                 try { text += `\n${JSON.stringify(l.data, null, 2)}`; }
  60.                 catch(e) { text += `\n[Unserializable Data]`; }
  61.             }
  62.             return text;
  63.         }).join('\n\n----------------------------------------\n\n');
  64.     }
  65.  
  66.     addLog('INFO', 'Script started executing');
  67.  
  68.     // ─── UTILITIES & DATA STORAGE ─────────────────────────────────────────────
  69.     const sleep = (ms) => new Promise(r => setTimeout(r, ms));
  70.     function pad2(n) { return String(n).padStart(2, '0'); }
  71.     function makeTimestamp() {
  72.         const d = new Date();
  73.         return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}-${pad2(d.getMinutes())}-${pad2(d.getSeconds())}`;
  74.     }
  75.     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>`;
  76.     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>`;
  77.  
  78.     // Load Settings
  79.     let appSettings = JSON.parse(localStorage.getItem('xai_api_settings') || '{}');
  80.     appSettings.retries = parseInt(appSettings.retries) || 20;
  81.     appSettings.delayMin = parseInt(appSettings.delayMin) || 2000;
  82.     appSettings.delayMax = parseInt(appSettings.delayMax) || 4000;
  83.     if (typeof appSettings.rateLimitDelay === 'undefined') appSettings.rateLimitDelay = 60;
  84.     if (typeof appSettings.notifications === 'undefined') appSettings.notifications = true;
  85.     if (typeof appSettings.customCSS === 'undefined') appSettings.customCSS = "";
  86.     if (typeof appSettings.saveAsPng === 'undefined') appSettings.saveAsPng = false;
  87.     if (typeof appSettings.apiBaseUrl === 'undefined') appSettings.apiBaseUrl = "";
  88.     if (typeof appSettings.videoPollTimeout === 'undefined') appSettings.videoPollTimeout = 300;
  89.     if (typeof appSettings.autoRetryStuckVideo === 'undefined') appSettings.autoRetryStuckVideo = false;
  90.     if (typeof appSettings.breakLoopOnFailure === 'undefined') appSettings.breakLoopOnFailure = true;
  91.  
  92.     if (typeof appSettings.suppressEditWarning === 'undefined') appSettings.suppressEditWarning = false;
  93.     if (typeof appSettings.videoAutoplay === 'undefined') appSettings.videoAutoplay = true;
  94.     if (typeof appSettings.videoMuted === 'undefined') appSettings.videoMuted = false;
  95.  
  96.     addLog('INFO', 'Settings loaded', appSettings);
  97.  
  98.     let favoritePrompts = JSON.parse(localStorage.getItem('xai_api_favs') || '[]');
  99.  
  100.     // API URL Helper
  101.     function getApiUrl(path) {
  102.         let base = appSettings.apiBaseUrl || '';
  103.         if (base.endsWith('/')) base = base.slice(0, -1);
  104.         if (!path.startsWith('/') && base) path = '/' + path;
  105.         return base + path;
  106.     }
  107.  
  108.     // ─── DYNAMIC PROMPT PARSER (SPINTAX & RANDOM SEED) ────────────────────────
  109.     function parseDynamicPrompt(text) {
  110.         if (!text) return text;
  111.         let parsed = text;
  112.         let prev;
  113.         do {
  114.             prev = parsed;
  115.             parsed = parsed.replace(/\{([^{}]+)\}/g, (match, contents) => {
  116.                 if (contents.includes('|')) {
  117.                     const options = contents.split('|');
  118.                     return options[Math.floor(Math.random() * options.length)];
  119.                 }
  120.                 return match;
  121.             });
  122.         } while (parsed !== prev);
  123.  
  124.         parsed = parsed.replace(/@randomseed/gi, () => {
  125.             return Math.floor(1000000000 + Math.random() * 9000000000).toString();
  126.         });
  127.  
  128.         if (parsed !== text) addLog('INFO', 'Parsed dynamic prompt', { original: text, parsed: parsed });
  129.         return parsed.trim();
  130.     }
  131.  
  132.     // ─── BACKGROUND ALERT SYSTEM (AUDIO + TAB BLINK) ──────────────────────────
  133.     let sharedAudioCtx = null;
  134.     let titleBlinkInterval = null;
  135.     const originalTitle = document.title || "xAI Pro Studio";
  136.  
  137.     function initAudio() {
  138.         if (!sharedAudioCtx && appSettings.notifications) {
  139.             try { sharedAudioCtx = new (window.AudioContext || window.webkitAudioContext)(); }
  140.             catch(e) { console.warn("AudioContext not supported"); }
  141.         }
  142.         if (sharedAudioCtx && sharedAudioCtx.state === 'suspended') sharedAudioCtx.resume();
  143.     }
  144.  
  145.     function notifyUser(isError = false) {
  146.         if (!appSettings.notifications) return;
  147.         addLog('INFO', 'Triggering user notification', { isError });
  148.  
  149.         if (sharedAudioCtx) {
  150.             try {
  151.                 const osc = sharedAudioCtx.createOscillator();
  152.                 const gain = sharedAudioCtx.createGain();
  153.                 osc.connect(gain);
  154.                 gain.connect(sharedAudioCtx.destination);
  155.  
  156.                 if (isError) {
  157.                     osc.type = 'sawtooth';
  158.                     osc.frequency.setValueAtTime(300, sharedAudioCtx.currentTime);
  159.                     osc.frequency.exponentialRampToValueAtTime(100, sharedAudioCtx.currentTime + 0.3);
  160.                 } else {
  161.                     osc.type = 'sine';
  162.                     osc.frequency.setValueAtTime(500, sharedAudioCtx.currentTime);
  163.                     osc.frequency.exponentialRampToValueAtTime(1000, sharedAudioCtx.currentTime + 0.2);
  164.                 }
  165.  
  166.                 gain.gain.setValueAtTime(0.1, sharedAudioCtx.currentTime);
  167.                 gain.gain.exponentialRampToValueAtTime(0.01, sharedAudioCtx.currentTime + 0.5);
  168.  
  169.                 osc.start(sharedAudioCtx.currentTime);
  170.                 osc.stop(sharedAudioCtx.currentTime + 0.5);
  171.             } catch(e) {}
  172.         }
  173.  
  174.         if (!document.hasFocus()) {
  175.             if (titleBlinkInterval) clearInterval(titleBlinkInterval);
  176.             let toggle = true;
  177.             titleBlinkInterval = setInterval(() => {
  178.                 document.title = toggle ? (isError ? "❌ FAILED" : "✅ DONE") : originalTitle;
  179.                 toggle = !toggle;
  180.             }, 1000);
  181.  
  182.             const clearBlink = () => {
  183.                 clearInterval(titleBlinkInterval);
  184.                 document.title = originalTitle;
  185.                 window.removeEventListener('focus', clearBlink);
  186.             };
  187.             window.addEventListener('focus', clearBlink);
  188.         }
  189.     }
  190.  
  191.     // ─── STEALTH IDENTITY RESETTER ────────────────────────────────────────────
  192.     function stealthIdentityReset() {
  193.         addLog('INFO', 'Executing stealth identity reset');
  194.         const trackingCookies =['x-anonuserid', 'mixpanel', 'mp_', 'distinct_id'];
  195.         document.cookie.split(';').forEach(c => {
  196.             const name = c.split('=')[0].trim();
  197.             if (trackingCookies.some(tc => name.includes(tc))) {['.x.ai', 'console.x.ai', ''].forEach(d => {['/', '/playground/imagine', ''].forEach(p => {
  198.                         document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=${p}${d ? ';domain=' + d : ''}`;
  199.                     });
  200.                 });
  201.             }
  202.         });
  203.  
  204.         for (let i = localStorage.length - 1; i >= 0; i--) {
  205.             const k = localStorage.key(i);
  206.             if (k && !k.startsWith('xai_api_') && trackingCookies.some(tc => k.includes(tc))) {
  207.                 localStorage.removeItem(k);
  208.             }
  209.         }
  210.     }
  211.  
  212.     // ─── EXIF LIBRARY LOADER & WINDOWS METADATA ENCODER ───────────────────────
  213.     let piexifLoaded = false;
  214.     function loadPiexif() {
  215.         if (piexifLoaded) return Promise.resolve(window.piexif);
  216.         addLog('INFO', 'Loading piexif library');
  217.         return new Promise(resolve => {
  218.             const script = document.createElement('script');
  219.             script.src = 'https://cdn.jsdelivr.net/npm/piexifjs';
  220.             script.onload = () => { piexifLoaded = true; addLog('INFO', 'piexif loaded'); resolve(window.piexif); };
  221.             document.head.appendChild(script);
  222.         });
  223.     }
  224.  
  225.     function toUTF16LE(str) {
  226.         const arr =[];
  227.         for (let i = 0; i < str.length; i++) {
  228.             const code = str.charCodeAt(i);
  229.             arr.push(code & 0xFF); arr.push((code >> 8) & 0xFF);
  230.         }
  231.         arr.push(0, 0); return arr;
  232.     }
  233.  
  234.     function convertToJpegWithExif(base64PngUri, prompt) {
  235.         return new Promise((resolve, reject) => {
  236.             const img = new Image();
  237.             img.onload = async () => {
  238.                 const canvas = document.createElement('canvas');
  239.                 canvas.width = img.width; canvas.height = img.height;
  240.                 const ctx = canvas.getContext('2d');
  241.                 ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, canvas.width, canvas.height);
  242.                 ctx.drawImage(img, 0, 0);
  243.  
  244.                 let jpegUri = canvas.toDataURL('image/jpeg', 0.95);
  245.                 if (prompt) {
  246.                     try {
  247.                         const piexif = await loadPiexif();
  248.                         const exifObj = {"0th": {}, "Exif": {}, "GPS": {}, "1st": {}, "Interop": {}};
  249.                         const safePrompt = unescape(encodeURIComponent(prompt));
  250.                         exifObj['0th'][piexif.ImageIFD.ImageDescription] = safePrompt;
  251.  
  252.                         const utf16Prompt = toUTF16LE(prompt);
  253.                         exifObj['0th'][40091] = utf16Prompt;
  254.                         exifObj['0th'][40092] = utf16Prompt;
  255.                         exifObj['0th'][40093] = utf16Prompt;
  256.  
  257.                         jpegUri = piexif.insert(piexif.dump(exifObj), jpegUri);
  258.                         addLog('INFO', 'Successfully attached EXIF to JPEG', { promptLength: prompt.length });
  259.                     } catch (e) {
  260.                         addLog('ERROR', 'Failed to attach EXIF', e.message);
  261.                     }
  262.                 }
  263.                 resolve(jpegUri);
  264.             };
  265.             img.onerror = (e) => { addLog('ERROR', 'Failed to convert to JPEG', e); reject(e); };
  266.             img.src = base64PngUri;
  267.         });
  268.     }
  269.  
  270.     async function extractPromptFromImage(file) {
  271.         addLog('INFO', 'Extracting prompt from image EXIF', file.name);
  272.         if (!file.type.includes('image')) return null;
  273.         try {
  274.             const piexif = await loadPiexif();
  275.             return new Promise((resolve) => {
  276.                 const reader = new FileReader();
  277.                 reader.onload = (e) => {
  278.                     try {
  279.                         const exifData = piexif.load(e.target.result);
  280.                         let prompt = exifData['0th'] && exifData['0th'][piexif.ImageIFD.ImageDescription];
  281.                         if (Array.isArray(prompt)) prompt = String.fromCharCode.apply(null, prompt).replace(/\0/g, '');
  282.                         if (prompt) { try { prompt = decodeURIComponent(escape(prompt)); } catch(e) {} }
  283.  
  284.                         if (!prompt && exifData['0th'] && exifData['0th'][40091]) {
  285.                             const xpTitleArr = exifData['0th'][40091];
  286.                             let str = '';
  287.                             for (let i = 0; i < xpTitleArr.length; i += 2) {
  288.                                 const charCode = xpTitleArr[i] | (xpTitleArr[i+1] << 8);
  289.                                 if (charCode === 0) break;
  290.                                 str += String.fromCharCode(charCode);
  291.                             }
  292.                             prompt = str;
  293.                         }
  294.                         const result = prompt ? prompt.trim() : null;
  295.                         addLog('INFO', 'EXIF extraction complete', { found: !!result });
  296.                         resolve(result);
  297.                     } catch (err) {
  298.                         addLog('WARN', 'EXIF parsing failed', err.message);
  299.                         resolve(null);
  300.                     }
  301.                 };
  302.                 reader.readAsDataURL(file);
  303.             });
  304.         } catch (e) {
  305.             addLog('ERROR', 'EXIF loader failed', e.message);
  306.             return null;
  307.         }
  308.     }
  309.  
  310.     // ─── VIDEO THUMBNAIL EXTRACTOR ────────────────────────────────────────────
  311.     function generateVideoThumbnail(file) {
  312.         addLog('INFO', 'Generating thumbnail for video', file.name);
  313.         return new Promise((resolve) => {
  314.             const video = document.createElement('video');
  315.             video.preload = 'metadata';
  316.             video.muted = true;
  317.             video.playsInline = true;
  318.  
  319.             const url = URL.createObjectURL(file);
  320.             video.src = url;
  321.  
  322.             let isSeeked = false;
  323.  
  324.             video.onloadeddata = () => {
  325.                 video.currentTime = Math.min(0.5, video.duration / 2 || 0);
  326.             };
  327.  
  328.             video.onseeked = () => {
  329.                 if (isSeeked) return;
  330.                 isSeeked = true;
  331.                 try {
  332.                     const canvas = document.createElement('canvas');
  333.                     canvas.width = video.videoWidth || 140;
  334.                     canvas.height = video.videoHeight || 140;
  335.                     const ctx = canvas.getContext('2d');
  336.                     ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
  337.                     URL.revokeObjectURL(url);
  338.                     addLog('INFO', 'Video thumbnail generated');
  339.                     resolve(canvas.toDataURL('image/jpeg', 0.8));
  340.                 } catch(e) {
  341.                     URL.revokeObjectURL(url);
  342.                     addLog('WARN', 'Failed generating video thumbnail', e.message);
  343.                     resolve(null);
  344.                 }
  345.             };
  346.  
  347.             video.onerror = () => {
  348.                 URL.revokeObjectURL(url);
  349.                 addLog('WARN', 'Video object error during thumbnail generation');
  350.                 resolve(null);
  351.             };
  352.  
  353.             setTimeout(() => { if (!isSeeked) { URL.revokeObjectURL(url); addLog('WARN', 'Video thumbnail timed out'); resolve(null); } }, 2000);
  354.         });
  355.     }
  356.  
  357.     // ─── ASPECT RATIO DATA ────────────────────────────────────────────────────
  358.     const arData =[
  359.         { label: 'Auto', w: 1, h: 1, isAuto: true },
  360.         { label: '1:1', w: 1, h: 1 }, { label: '3:4', w: 3, h: 4 }, { label: '4:3', w: 4, h: 3 },
  361.         { label: '9:16', w: 9, h: 16 }, { label: '16:9', w: 16, h: 9 }, { label: '2:3', w: 2, h: 3 },
  362.         { 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 },
  363.         { 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 }
  364.     ];
  365.  
  366.     function createArIcon(w, h, isAuto) {
  367.         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>`;
  368.         const scale = 12 / Math.max(w, h);
  369.         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>`;
  370.     }
  371.  
  372.     // ─── UI INJECTION (COMPACT CSS) ───────────────────────────────────────────
  373.     const UI_CSS = `
  374.         ::-webkit-scrollbar { width: 6px; }
  375.         ::-webkit-scrollbar-track { background: #0f172a; }
  376.         ::-webkit-scrollbar-thumb { background: #475569; border-radius: 6px; }
  377.  
  378.         @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
  379.         @keyframes pulse { 0% { opacity: 0.5; } 50% { opacity: 1; } 100% { opacity: 0.5; } }
  380.  
  381.         .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; }
  382.         .btn-content { display: flex; align-items: center; justify-content: center; gap: 6px; }
  383.         .status-pulsing { animation: pulse 2s ease-in-out infinite; color: #3b82f6 !important; font-weight: 600; }
  384.  
  385.         #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; }
  386.         #xai-pro-app * { box-sizing: border-box; }
  387.  
  388.         .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; }
  389.         .panel-title { font-size: 11px; font-weight: 700; color: #94a3b8; letter-spacing: 0.5px; padding: 10px; border-bottom: 1px solid #334155; text-transform: uppercase; }
  390.  
  391.         .col-left { width: 310px; display: flex; flex-direction: column; gap: 10px; flex-shrink: 0; }
  392.         .col-center { flex: 1; display: flex; flex-direction: column; gap: 10px; min-width: 0; min-height: 0; }
  393.         .col-right { width: 310px; display: flex; flex-direction: column; gap: 10px; flex-shrink: 0; transition: 0.2s; }
  394.         .col-right.drag-over { background: #064e3b; border-color: #10b981; box-shadow: 0 0 0 4px rgba(16,185,129,0.15); border-radius: 10px; }
  395.  
  396.         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; }
  397.         textarea, select, input[type="text"], input[type="number"] { width: 100%; }
  398.         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); }
  399.         label { font-size: 11px; font-weight: 600; color: #94a3b8; margin-bottom: 4px; display: block; }
  400.  
  401.         .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; }
  402.         .btn-primary:hover:not(:disabled) { background: linear-gradient(180deg, #4f46e5 0%, #4338ca 100%); transform: translateY(-1px); }
  403.         .btn-primary:disabled { opacity: 0.8; cursor: not-allowed; }
  404.  
  405.         .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; }
  406.         .btn-secondary:hover:not(:disabled) { background: #334155; border-color: #64748b; }
  407.  
  408.         .ar-select-box { background: #0f172a; border: 1px solid #334155; border-radius: 6px; padding: 8px; display: flex; align-items: center; gap: 8px; cursor: pointer; color: #f8fafc; font-size: 13px; }
  409.         #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; }
  410.         .ar-option { display: flex; align-items: center; gap: 8px; padding: 6px 8px; cursor: pointer; font-size: 12px; color: #cbd5e1; }
  411.         .ar-option:hover { background: #334155; color: #f8fafc; }
  412.  
  413.         .img-wrapper { position: relative; display: block; }
  414.         .zoomable { cursor: zoom-in; transition: transform 0.2s; width: 100%; display: block; }
  415.  
  416.         .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; }
  417.         .overlay-btn:hover { background: rgba(15, 23, 42, 0.9); opacity: 1; transform: scale(1.05); }
  418.         .overlay-dl-btn { right: 6px; }
  419.         .overlay-edit-btn { right: 40px; color: #10b981; }
  420.  
  421.         .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; }
  422.         .ref-item.inactive { opacity: 0.6; filter: grayscale(0.8); }
  423.         .ref-item:active { cursor: grabbing; }
  424.         .ref-item.dragging { opacity: 0.4; }
  425.         .ref-handle { font-size: 14px; color: #475569; cursor: grab; padding: 0 4px; }
  426.         .ref-thumb { width: 90px; height: 90px; border-radius: 4px; object-fit: cover; border: 1px solid #334155; background: #0f172a; transition: 0.2s; }
  427.         .ref-info { flex: 1; display: flex; flex-direction: column; justify-content: center; gap: 6px; min-width: 0; }
  428.         .ref-toggle-label { font-size: 11px; color: #cbd5e1; display: flex; align-items: center; gap: 4px; cursor: pointer; user-select: none; }
  429.         .ref-toggle-label input { width: 12px; height: 12px; cursor: pointer; accent-color: #10b981; margin: 0; padding: 0; }
  430.         .ref-btn-group { display: flex; gap: 4px; width: 100%; margin-top: auto; }
  431.         .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; }
  432.         .ref-insert { background: #0f766e; color: #ccfbf1; border: 1px solid #115e59; }
  433.         .ref-insert:hover { background: #115e59; color: #f0fdfa; }
  434.         .ref-del { background: #450a0a; color: #fca5a5; border: 1px solid #7f1d1d; }
  435.         .ref-del:hover { background: #7f1d1d; color: #fee2e2; }
  436.         .drag-over-top { border-top: 2px solid #22c55e !important; }
  437.         .drag-over-bottom { border-bottom: 2px solid #22c55e !important; }
  438.  
  439.         .history-card { background: #0f172a; border: 1px solid #334155; border-radius: 6px; overflow: hidden; margin-bottom: 10px; }
  440.         .history-card .overlay-btn { width: 24px; height: 24px; top: 4px; border-radius: 4px; }
  441.         .history-card .overlay-dl-btn { right: 4px; }
  442.         .history-card .overlay-edit-btn { right: 32px; }
  443.         .history-card p { padding: 8px; margin: 0; font-size: 11px; color: #94a3b8; cursor: pointer; transition: 0.2s; }
  444.         .history-card p:hover { background: #1e293b; color: #e2e8f0; }
  445.  
  446.         .current-card { border-radius: 8px; overflow: hidden; box-shadow: 0 4px 10px rgba(0,0,0,0.3); border: 1px solid #334155; background: #1e293b; }
  447.         .current-card img, .current-card video { max-height: 50vh; object-fit: contain; background: #0f172a; }
  448.  
  449.         /* Modals Generic Classes */
  450.         .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); }
  451.         .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; }
  452.         .fav-item { display: flex; justify-content: space-between; align-items: center; background: #0f172a; padding: 8px; border-radius: 6px; border: 1px solid #334155; }
  453.         .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; }
  454.         .fav-text:hover { color: #10b981; }
  455.  
  456.         #settings-btn { position: fixed; top: 10px; right: 15px; font-size: 20px; cursor: pointer; z-index: 100; transition: transform 0.2s; opacity: 0.8;}
  457.         #settings-btn:hover { transform: rotate(45deg); opacity: 1; }
  458.  
  459.         #logs-btn { position: fixed; top: 10px; right: 45px; font-size: 20px; cursor: pointer; z-index: 100; transition: transform 0.2s; opacity: 0.8;}
  460.         #logs-btn:hover { transform: scale(1.15); opacity: 1; }
  461.  
  462.         #xai-lightbox { cursor: zoom-out; }
  463.         #xai-lightbox-wrapper { position: relative; display: inline-block; max-width: 95vw; max-height: 95vh; }
  464.         #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); }
  465.  
  466.         /* PROMPT CONTAINER CSS (For hover previews) */
  467.         .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; }
  468.         .prompt-container:focus-within { border-color: #64748b; background: #1e293b; box-shadow: 0 0 0 2px rgba(100,116,139,0.2); }
  469.         .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; }
  470.  
  471.         #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; }
  472.         .prompt-tag { pointer-events: auto; }
  473.  
  474.         #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; }
  475.         #xai-api-prompt:focus { background: transparent; border: none; box-shadow: none; }
  476.  
  477.         /* Warnings CSS */
  478.         .warning-box { border-radius: 6px; padding: 6px 10px; font-size: 11px; margin-bottom: 8px; font-weight: 500; display: block; }
  479.         .warning-red { background: #450a0a; color: #fca5a5; border: 1px solid #7f1d1d; }
  480.         .warning-yellow { background: #422006; color: #fcd34d; border: 1px solid #78350f; }
  481.     `;
  482.  
  483.     const UI_HTML = `
  484.         <div id="xai-pro-app">
  485.             <div id="settings-btn" title="Studio Settings">⚙️</div>
  486.             <div id="logs-btn" title="Debug Logs">🐛</div>
  487.  
  488.             <!-- LEFT COLUMN -->
  489.             <div class="col-left">
  490.                 <div class="panel" style="flex-shrink: 0;">
  491.                     <div style="display: flex; background: rgba(0,0,0,0.2); border-radius: 0 0 6px 6px; margin-bottom: 10px; border-bottom: 1px solid #334155; padding: 10px;">
  492.                         <div style="flex: 1;">
  493.                             <label>Master Action</label>
  494.                             <select id="master-action" style="font-weight: bold; color: #f8fafc; border-color: #475569; box-shadow: 0 2px 4px rgba(0,0,0,0.3);">
  495.                                 <option value="gen_image">🖼️ Generate Image</option>
  496.                                 <option value="gen_video">🎥 Generate Video</option>
  497.                                 <option value="edit_video">✂️ Edit Video</option>
  498.                                 <option value="extend_video">➡️ Extend Video</option>
  499.                             </select>
  500.                         </div>
  501.                     </div>
  502.  
  503.                     <div style="padding: 10px; display: flex; flex-direction: column; gap: 10px;">
  504.                         <div id="setting-ar">
  505.                             <label>Aspect Ratio</label>
  506.                             <div class="ar-select-box" id="ar-select-box">
  507.                                 <div id="ar-selected-icon"></div>
  508.                                 <span id="ar-selected-text">Auto</span>
  509.                             </div>
  510.                         </div>
  511.  
  512.                         <div style="display: flex; gap: 8px;">
  513.                             <div id="setting-res-img" style="flex: 1;">
  514.                                 <label>Resolution</label>
  515.                                 <select id="xai-api-res-img">
  516.                                     <option value="2k" selected>2K (Pro)</option>
  517.                                     <option value="1k">1K</option>
  518.                                 </select>
  519.                             </div>
  520.                             <div id="setting-res-vid" style="flex: 1; display: none;">
  521.                                 <label>Resolution</label>
  522.                                 <select id="xai-api-res-vid">
  523.                                     <option value="720p" selected>720p (HD)</option>
  524.                                     <option value="480p">480p (SD)</option>
  525.                                 </select>
  526.                             </div>
  527.  
  528.                             <div id="setting-batch" style="flex: 1;">
  529.                                 <label>Batch Size</label>
  530.                                 <select id="xai-api-n">
  531.                                     <option value="1" selected>1 Image</option>
  532.                                     <option value="2">2 Images</option>
  533.                                     <option value="3">3 Images</option>
  534.                                     <option value="4">4 Images</option>
  535.                                     <option value="5">5 Images</option>
  536.                                     <option value="8">8 Images</option>
  537.                                     <option value="10">10 Images</option>
  538.                                 </select>
  539.                             </div>
  540.                             <div id="setting-duration" style="flex: 1; display: none;">
  541.                                 <label>Duration</label>
  542.                                 <select id="xai-api-duration">
  543.                                     <option value="5" selected>5 Seconds</option>
  544.                                     <option value="8">8 Seconds</option>
  545.                                     <option value="10">10 Seconds</option>
  546.                                     <option value="12">12 Seconds</option>
  547.                                     <option value="15">15 Seconds</option>
  548.                                     <option value="2">2s Extension</option>
  549.                                     <option value="4">4s Extension</option>
  550.                                     <option value="6">6s Extension</option>
  551.                                 </select>
  552.                             </div>
  553.  
  554.                             <div id="setting-loops" style="flex: 1;">
  555.                                 <label title="How many consecutive API calls to perform in a row">Loops</label>
  556.                                 <input type="number" id="xai-api-loops" value="1" min="1" max="100">
  557.                             </div>
  558.                         </div>
  559.                     </div>
  560.                 </div>
  561.  
  562.                 <div class="panel" style="flex: 1; min-height: 0;">
  563.                     <div class="panel-title" style="display:flex; justify-content: space-between; align-items: center;">
  564.                         <span>Previous Results</span>
  565.                         <span id="xai-reset-btn" style="cursor: pointer; color: #ef4444; text-transform: none; text-decoration: underline;">Wipe Cache</span>
  566.                     </div>
  567.                     <div id="xai-history" style="flex: 1; overflow-y: auto; padding: 10px;">
  568.                         <div style="color: #94a3b8; font-size: 11px; text-align: center; margin-top: 20px;">No history yet.</div>
  569.                     </div>
  570.                 </div>
  571.             </div>
  572.  
  573.             <!-- CENTER COLUMN -->
  574.             <div class="col-center">
  575.                 <div class="panel" style="flex: 1; background: transparent; border: none; box-shadow: none; min-height: 0;">
  576.                     <div class="panel-title" style="background: #1e293b; border-radius: 8px; border: 1px solid #334155; margin-bottom: 10px; flex-shrink: 0;">Current Generation</div>
  577.                     <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>
  578.                 </div>
  579.  
  580.                 <div class="panel" style="flex-shrink: 0; padding: 12px;">
  581.                     <label style="font-size: 13px; color: #e2e8f0; display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
  582.                         <span style="display: flex; align-items: center; gap: 8px;">
  583.                             <span>Prompt</span>
  584.                             <button id="fav-prompts-btn" class="btn-secondary" style="padding: 2px 8px; font-size: 10px; height: 22px;">⭐ Favorites</button>
  585.                         </span>
  586.                         <span style="font-size: 10px; font-weight: 400; color: #94a3b8;">(Drop image here for EXIF)</span>
  587.                     </label>
  588.  
  589.                     <div id="warnings-container" style="display: flex; flex-direction: column; gap: 0px;"></div>
  590.  
  591.                     <div class="prompt-container">
  592.                         <div id="prompt-backdrop"></div>
  593.                         <textarea id="xai-api-prompt" placeholder="Describe what you want to see... Supports Spintax {cat|dog} and @randomseed"></textarea>
  594.                     </div>
  595.  
  596.                     <div style="display: flex; align-items: center; justify-content: space-between;">
  597.                         <div id="xai-api-status" style="font-size: 12px; color: #64748b; font-weight: 500;">Ready</div>
  598.                         <div style="display: flex; gap: 8px;">
  599.                             <button id="xai-preview-btn" class="btn-secondary">🔍 Payload</button>
  600.                             <button id="xai-cancel-btn" class="btn-secondary" style="display: none; color: #ef4444; border-color: #ef4444;">🛑 Cancel</button>
  601.                             <button id="xai-api-generate" class="btn-primary" style="width: 120px;"><div class="btn-content">Generate</div></button>
  602.                         </div>
  603.                     </div>
  604.                 </div>
  605.             </div>
  606.  
  607.             <!-- RIGHT COLUMN -->
  608.             <div class="col-right" id="col-right-dropzone">
  609.                 <div class="panel" style="flex: 1; display: flex; flex-direction: column; min-height: 0;">
  610.                     <div class="panel-title" id="ref-panel-title">Reference Media</div>
  611.                     <div style="padding: 10px; flex: 1; display: flex; flex-direction: column; overflow: hidden;">
  612.                         <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);">
  613.                             <div style="font-size: 20px; margin-bottom: 4px;">📥</div>
  614.                             <div style="font-size: 12px; font-weight: 600;">Drag & Drop / Ctrl+V</div>
  615.                         </div>
  616.                         <input type="file" id="xai-api-file" accept="image/*, video/mp4, video/webm" multiple style="display: none;">
  617.                         <div style="font-size: 10px; color: #94a3b8; margin-bottom: 4px; text-transform: uppercase; font-weight: bold; flex-shrink: 0;">Upload Order (Top = First)</div>
  618.                         <div id="xai-ref-list" style="flex: 1; overflow-y: auto; padding-right: 4px;"></div>
  619.                     </div>
  620.                 </div>
  621.             </div>
  622.  
  623.             <!-- FIXED FLOATING DROPDOWN -->
  624.             <div id="ar-options-container"></div>
  625.  
  626.             <!-- MODALS -->
  627.             <div id="xai-lightbox" class="modal-overlay">
  628.                 <div id="xai-lightbox-wrapper">
  629.                     <img id="xai-lightbox-img" src="">
  630.                     <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>
  631.                 </div>
  632.             </div>
  633.  
  634.             <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;">
  635.                 <img id="tag-preview-img" style="max-width: 150px; max-height: 150px; border-radius: 4px; display: block; object-fit: cover;">
  636.             </div>
  637.  
  638.             <!-- LOGS MODAL -->
  639.             <div id="xai-logs-modal" class="modal-overlay">
  640.                 <div class="modal-box" style="width: 800px; max-width: 95vw; height: 80vh;">
  641.                     <div style="display: flex; justify-content: space-between; align-items: center;">
  642.                         <h3 style="margin:0; font-size: 15px; color: #f8fafc;">🐛 Debug Logs</h3>
  643.                         <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>
  644.                     </div>
  645.                     <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>
  646.                     <div style="display: flex; gap: 8px;">
  647.                         <button id="copy-logs-btn" class="btn-primary" style="flex: 1;">📋 Copy Logs to Clipboard</button>
  648.                         <button id="clear-logs-btn" class="btn-secondary" style="flex: 1; color: #ef4444; border-color: #ef4444;">🗑️ Clear Logs</button>
  649.                     </div>
  650.                 </div>
  651.             </div>
  652.  
  653.             <!-- SETTINGS MODAL -->
  654.             <div id="xai-settings-modal" class="modal-overlay">
  655.                 <div class="modal-box" style="width: 500px;">
  656.                     <h3 style="margin:0; color: #f8fafc; font-size: 15px;">⚙️ Studio Settings</h3>
  657.                     <div style="display:flex; gap:10px;">
  658.                         <div style="flex:1;">
  659.                             <label>Max Retries</label>
  660.                             <input type="number" id="set-retries" min="1" max="100">
  661.                         </div>
  662.                         <div style="flex:1;">
  663.                             <label>Delay Min (ms)</label>
  664.                             <input type="number" id="set-delay-min" min="500" step="500">
  665.                         </div>
  666.                         <div style="flex:1;">
  667.                             <label>Delay Max (ms)</label>
  668.                             <input type="number" id="set-delay-max" min="1000" step="500">
  669.                         </div>
  670.                     </div>
  671.                     <div>
  672.                         <label>API Base URL (Leave empty for default console.x.ai)</label>
  673.                         <input type="text" id="set-api-base-url" placeholder="e.g., https://api.proxy.com">
  674.                     </div>
  675.                     <div style="display:flex; align-items:center; gap: 8px; margin-top: 4px;">
  676.                         <input type="checkbox" id="set-notifications" style="width: 16px; height: 16px;">
  677.                         <label for="set-notifications" style="margin: 0; cursor: pointer;">Enable Notifications (Sound & Tab Blink)</label>
  678.                     </div>
  679.                     <div style="display:flex; align-items:center; gap: 8px; margin-top: 4px;">
  680.                         <input type="checkbox" id="set-save-png" style="width: 16px; height: 16px;">
  681.                         <label for="set-save-png" style="margin: 0; cursor: pointer;">Save as Original PNG (Disable JPEG / EXIF Prompt)</label>
  682.                     </div>
  683.                     <div style="display:flex; align-items:center; gap: 8px; margin-top: 4px;">
  684.                         <input type="checkbox" id="set-auto-retry-video" style="width: 16px; height: 16px;">
  685.                         <label for="set-auto-retry-video" style="margin: 0; cursor: pointer;">Auto-Retry Stuck Videos (Timeout / Moderated)</label>
  686.                     </div>
  687.                     <div style="display:flex; align-items:center; gap: 8px; margin-top: 4px;">
  688.                         <input type="checkbox" id="set-break-loop-on-failure" style="width: 16px; height: 16px;">
  689.                         <label for="set-break-loop-on-failure" style="margin: 0; cursor: pointer;">Break loops if one iteration fails entirely.</label>
  690.                     </div>
  691.                     <div style="display:flex; gap:10px; margin-top: 4px;">
  692.                         <div style="flex:1;">
  693.                             <label>Video Poll Timeout (s)</label>
  694.                             <input type="number" id="set-video-timeout" min="10" step="10">
  695.                         </div>
  696.                         <div style="flex:1;">
  697.                             <label title="When 'Rate limit exceeded' occurs">Rate Limit Delay (s)</label>
  698.                             <input type="number" id="set-rate-limit-delay" min="1" step="1">
  699.                         </div>
  700.                     </div>
  701.  
  702.                     <div style="border-top: 1px solid #334155; margin-top: 6px; padding-top: 6px;"></div>
  703.  
  704.                     <div style="display:flex; align-items:center; gap: 8px; margin-top: 4px;">
  705.                         <input type="checkbox" id="set-suppress-warning" style="width: 16px; height: 16px;">
  706.                         <label for="set-suppress-warning" style="margin: 0; cursor: pointer; color: #fcd34d;">Suppress yellow ">8s video edit" warning</label>
  707.                     </div>
  708.                     <div style="display:flex; align-items:center; gap: 8px; margin-top: 4px;">
  709.                         <input type="checkbox" id="set-video-autoplay" style="width: 16px; height: 16px;">
  710.                         <label for="set-video-autoplay" style="margin: 0; cursor: pointer;">Autoplay generated videos in gallery</label>
  711.                     </div>
  712.                     <div style="display:flex; align-items:center; gap: 8px; margin-top: 4px;">
  713.                         <input type="checkbox" id="set-video-muted" style="width: 16px; height: 16px;">
  714.                         <label for="set-video-muted" style="margin: 0; cursor: pointer;">Start generated videos muted</label>
  715.                     </div>
  716.  
  717.                     <div style="border-top: 1px solid #334155; margin-top: 6px; padding-top: 6px;">
  718.                         <label>Custom CSS Overrides</label>
  719.                         <textarea id="set-custom-css" style="font-family: monospace; height: 60px; font-size: 11px;" placeholder="/* e.g., #xai-pro-app { background: red; } */"></textarea>
  720.                     </div>
  721.  
  722.                     <div style="display: flex; gap: 8px; margin-top: 5px; border-top: 1px solid #334155; padding-top: 10px;">
  723.                         <button id="export-data-btn" class="btn-secondary" style="flex:1;">📤 Export Data</button>
  724.                         <button id="import-data-btn" class="btn-secondary" style="flex:1;">📥 Import Data</button>
  725.                         <input type="file" id="import-data-file" accept=".json" style="display: none;">
  726.                     </div>
  727.  
  728.                     <div style="display: flex; gap: 8px; margin-top: 5px;">
  729.                         <button id="save-settings-btn" class="btn-primary" style="flex:1;">Save Settings</button>
  730.                         <button id="close-settings-btn" class="btn-secondary" style="flex:1;">Cancel</button>
  731.                     </div>
  732.                 </div>
  733.             </div>
  734.  
  735.             <!-- FAVORITES MODAL -->
  736.             <div id="xai-fav-modal" class="modal-overlay">
  737.                 <div class="modal-box" style="width: 550px;">
  738.                     <div style="display: flex; justify-content: space-between; align-items: center;">
  739.                         <h3 style="margin:0; color: #f8fafc; font-size: 15px;">⭐ Favorite Prompts</h3>
  740.                         <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>
  741.                     </div>
  742.                     <button id="add-current-fav-btn" class="btn-primary" style="background: linear-gradient(180deg, #10b981 0%, #059669 100%);">➕ Save Current Prompt to Favorites</button>
  743.                     <div id="fav-list" style="flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 6px; padding-right: 4px;"></div>
  744.                 </div>
  745.             </div>
  746.  
  747.             <!-- CUSTOM PAYLOAD DEBUGGER MODAL -->
  748.             <div id="xai-payload-modal" class="modal-overlay">
  749.                 <div class="modal-box" style="width: 550px;">
  750.                     <div style="display: flex; justify-content: space-between; align-items: center;">
  751.                         <h3 style="margin:0; font-size: 15px; color: #f8fafc;">API Payload Debugger</h3>
  752.                         <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>
  753.                     </div>
  754.                     <div><label>Endpoint URL:</label><input type="text" id="payload-endpoint" style="font-family: monospace;" value="/v1/images/generations"></div>
  755.                     <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>
  756.                     <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>
  757.                     <div id="custom-payload-status" style="font-size: 11px; color: #64748b; text-align: center;">Ready</div>
  758.                 </div>
  759.             </div>
  760.         </div>
  761.     `;
  762.  
  763.     // ─── INITIALIZATION ───────────────────────────────────────────────────────
  764.     let updateUIForEditMedia = null;
  765.  
  766.     const initApp = () => {
  767.         addLog('INFO', 'Initializing UI');
  768.         if (!document.body) return setTimeout(initApp, 50);
  769.  
  770.         const hideOriginals = () => {
  771.             Array.from(document.body.children).forEach(child => {
  772.                 if (child.id !== 'xai-pro-app' && !['SCRIPT', 'STYLE', 'LINK'].includes(child.tagName)) {
  773.                     child.style.display = 'none';
  774.                 }
  775.             });
  776.         };
  777.         hideOriginals();
  778.         new MutationObserver(hideOriginals).observe(document.body, { childList: true });
  779.  
  780.         if (!document.getElementById('xai-pro-app')) {
  781.             const style = document.createElement('style');
  782.             style.innerHTML = UI_CSS;
  783.             document.head.appendChild(style);
  784.  
  785.             const customStyle = document.createElement('style');
  786.             customStyle.id = 'xai-custom-css-block';
  787.             customStyle.innerHTML = appSettings.customCSS;
  788.             document.head.appendChild(customStyle);
  789.  
  790.             document.body.insertAdjacentHTML('beforeend', UI_HTML);
  791.             bindLogic();
  792.             addLog('INFO', 'UI Injected successfully');
  793.         }
  794.     };
  795.  
  796.     let referenceMedia =[];
  797.     let currentAr = localStorage.getItem('xai_api_ar') || 'Auto';
  798.     let fullPayloadMemory = {};
  799.     let currentAbortController = null;
  800.     let isRequestCancelled = false;
  801.  
  802.     function saveRefs() {
  803.         try {
  804.             localStorage.setItem('xai_api_refs', JSON.stringify(referenceMedia));
  805.         } catch (e) {
  806.             addLog('WARN', 'Could not save references to localStorage (quota exceeded).', e.message);
  807.         }
  808.     }
  809.  
  810.     function bindLogic() {
  811.         addLog('INFO', 'Binding UI logic');
  812.         const els = {
  813.             app: document.getElementById('xai-pro-app'),
  814.             prompt: document.getElementById('xai-api-prompt'),
  815.             promptBackdrop: document.getElementById('prompt-backdrop'),
  816.             warningsContainer: document.getElementById('warnings-container'),
  817.             promptContainer: document.querySelector('.prompt-container'),
  818.             action: document.getElementById('master-action'),
  819.             resImg: document.getElementById('xai-api-res-img'),
  820.             resVid: document.getElementById('xai-api-res-vid'),
  821.             n: document.getElementById('xai-api-n'),
  822.             duration: document.getElementById('xai-api-duration'),
  823.             loops: document.getElementById('xai-api-loops'),
  824.  
  825.             settingAr: document.getElementById('setting-ar'),
  826.             settingResImg: document.getElementById('setting-res-img'),
  827.             settingResVid: document.getElementById('setting-res-vid'),
  828.             settingBatch: document.getElementById('setting-batch'),
  829.             settingDuration: document.getElementById('setting-duration'),
  830.             settingLoops: document.getElementById('setting-loops'),
  831.  
  832.             fileInput: document.getElementById('xai-api-file'),
  833.             dropzone: document.getElementById('col-right-dropzone'),
  834.             uploadPlaceholder: document.getElementById('xai-upload-placeholder'),
  835.             refList: document.getElementById('xai-ref-list'),
  836.             refTitle: document.getElementById('ref-panel-title'),
  837.  
  838.             btn: document.getElementById('xai-api-generate'),
  839.             previewBtn: document.getElementById('xai-preview-btn'),
  840.             cancelBtn: document.getElementById('xai-cancel-btn'),
  841.             status: document.getElementById('xai-api-status'),
  842.             history: document.getElementById('xai-history'),
  843.             currentImages: document.getElementById('xai-current-images'),
  844.             reset: document.getElementById('xai-reset-btn'),
  845.  
  846.             arSelectBox: document.getElementById('ar-select-box'),
  847.             arOptionsCont: document.getElementById('ar-options-container'),
  848.             arSelectedIcon: document.getElementById('ar-selected-icon'),
  849.             arSelectedText: document.getElementById('ar-selected-text'),
  850.  
  851.             lightbox: document.getElementById('xai-lightbox'),
  852.             lightboxImg: document.getElementById('xai-lightbox-img'),
  853.             lightboxDl: document.getElementById('xai-lightbox-dl'),
  854.  
  855.             previewBox: document.getElementById('tag-preview-box'),
  856.             previewImg: document.getElementById('tag-preview-img'),
  857.  
  858.             payloadModal: document.getElementById('xai-payload-modal'),
  859.             payloadEndpoint: document.getElementById('payload-endpoint'),
  860.             payloadCode: document.getElementById('payload-code'),
  861.             closePayloadBtn: document.getElementById('close-payload-btn'),
  862.             sendCustomBtn: document.getElementById('send-custom-payload-btn'),
  863.             customStatus: document.getElementById('custom-payload-status'),
  864.  
  865.             settingsBtn: document.getElementById('settings-btn'),
  866.             settingsModal: document.getElementById('xai-settings-modal'),
  867.             closeSettingsBtn: document.getElementById('close-settings-btn'),
  868.             saveSettingsBtn: document.getElementById('save-settings-btn'),
  869.  
  870.             exportDataBtn: document.getElementById('export-data-btn'),
  871.             importDataBtn: document.getElementById('import-data-btn'),
  872.             importDataFile: document.getElementById('import-data-file'),
  873.  
  874.             favBtn: document.getElementById('fav-prompts-btn'),
  875.             favModal: document.getElementById('xai-fav-modal'),
  876.             closeFavBtn: document.getElementById('close-fav-btn'),
  877.             addFavBtn: document.getElementById('add-current-fav-btn'),
  878.             favList: document.getElementById('fav-list'),
  879.  
  880.             logsBtn: document.getElementById('logs-btn'),
  881.             logsModal: document.getElementById('xai-logs-modal'),
  882.             closeLogsBtn: document.getElementById('close-logs-btn'),
  883.             copyLogsBtn: document.getElementById('copy-logs-btn'),
  884.             clearLogsBtn: document.getElementById('clear-logs-btn'),
  885.             logsTextarea: document.getElementById('logs-textarea')
  886.         };
  887.  
  888.         // ─── LOGS UI BINDINGS ──────────────────────────────────────────────────────
  889.         els.logsBtn.addEventListener('click', () => {
  890.             els.logsTextarea.value = formatLogsForExport();
  891.             els.logsModal.style.display = 'flex';
  892.             els.logsTextarea.scrollTop = els.logsTextarea.scrollHeight;
  893.             addLog('UI', 'Opened Debug Logs modal');
  894.         });
  895.         els.closeLogsBtn.addEventListener('click', () => els.logsModal.style.display = 'none');
  896.         els.copyLogsBtn.addEventListener('click', () => {
  897.             els.logsTextarea.select();
  898.             document.execCommand('copy');
  899.             alert('Logs copied to clipboard!');
  900.             addLog('UI', 'Copied logs to clipboard');
  901.         });
  902.         els.clearLogsBtn.addEventListener('click', () => {
  903.             if (confirm("Clear all logs?")) {
  904.                 appLogs.length = 0;
  905.                 els.logsTextarea.value = '';
  906.                 addLog('INFO', 'Logs cleared by user');
  907.             }
  908.         });
  909.  
  910.         // ─── DYNAMIC WARNINGS MANAGER ───────────────────────────────────────────────
  911.         function updateWarnings() {
  912.             els.warningsContainer.innerHTML = '';
  913.             let html = '';
  914.  
  915.             if (/--[a-zA-Z0-9_-]+=/.test(els.prompt.value)) {
  916.                 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>`;
  917.             }
  918.  
  919.             const action = els.action.value;
  920.             const duration = parseInt(els.duration.value) || 0;
  921.             const activeRefs = referenceMedia.filter(m => m.active).length;
  922.  
  923.             if (action === 'gen_video') {
  924.                 if (activeRefs > 0 && duration > 10) {
  925.                     html += `<div class="warning-box warning-red">⚠️ Grok seems to always refuse to generate videos by reference with length above 10s, script has nothing to do with it. Without references it works fine.</div>`;
  926.                 }
  927.                 if (duration > 8 && !appSettings.suppressEditWarning) {
  928.                     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>`;
  929.                }
  930.            }
  931.  
  932.            els.warningsContainer.innerHTML = html;
  933.        }
  934.  
  935.        // ─── PROMPT HELPER & BACKDROP (HOVER) SYSTEM ────────────────────────────
  936.        function syncPromptBackdrop() {
  937.            const val = els.prompt.value;
  938.            updateWarnings();
  939.  
  940.            let html = val.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
  941.            const activeRefs = referenceMedia.filter(m => m.active);
  942.  
  943.            html = html.replace(/&lt;IMAGE_(\d+)&gt;/g, (match, idxStr) => {
  944.                const idx = parseInt(idxStr);
  945.                if (activeRefs[idx]) return `<span class="prompt-tag" data-idx="${idx}">${match}</span>`;
  946.                return match;
  947.            });
  948.  
  949.            html = html.replace(/\n/g, '<br>') + '<br>';
  950.            els.promptBackdrop.innerHTML = html;
  951.            els.promptBackdrop.scrollTop = els.prompt.scrollTop;
  952.            els.promptBackdrop.scrollLeft = els.prompt.scrollLeft;
  953.        }
  954.  
  955.        function setPromptVal(val) {
  956.            els.prompt.value = val;
  957.            localStorage.setItem('xai_api_prompt', val);
  958.            syncPromptBackdrop();
  959.        }
  960.  
  961.        if (localStorage.getItem('xai_api_prompt')) setPromptVal(localStorage.getItem('xai_api_prompt'));
  962.  
  963.        els.prompt.addEventListener('input', () => setPromptVal(els.prompt.value));
  964.        els.prompt.addEventListener('scroll', () => { els.promptBackdrop.scrollTop = els.prompt.scrollTop; els.promptBackdrop.scrollLeft = els.prompt.scrollLeft; });
  965.  
  966.        let hidePreviewTimeout;
  967.        els.prompt.addEventListener('mousemove', (e) => {
  968.            if (e.buttons !== 0) { els.previewBox.style.display = 'none'; return; }
  969.  
  970.            els.prompt.style.pointerEvents = 'none';
  971.            const el = document.elementFromPoint(e.clientX, e.clientY);
  972.            els.prompt.style.pointerEvents = 'auto';
  973.  
  974.            if (el && el.classList.contains('prompt-tag')) {
  975.                const idx = el.getAttribute('data-idx');
  976.                const activeRefs = referenceMedia.filter(m => m.active);
  977.                const media = activeRefs[parseInt(idx)];
  978.                if (media) {
  979.                    els.previewBox.style.display = 'block';
  980.                    els.previewImg.src = media.thumb || media.base64;
  981.  
  982.                    let targetX = e.clientX + 15; let targetY = e.clientY + 15;
  983.                    if (targetX + 160 > window.innerWidth) targetX = e.clientX - 165;
  984.                    if (targetY + 160 > window.innerHeight) targetY = e.clientY - 165;
  985.  
  986.                    els.previewBox.style.left = targetX + 'px';
  987.                    els.previewBox.style.top = targetY + 'px';
  988.                    clearTimeout(hidePreviewTimeout);
  989.                }
  990.            } else {
  991.                hidePreviewTimeout = setTimeout(() => els.previewBox.style.display = 'none', 50);
  992.            }
  993.        });
  994.        els.prompt.addEventListener('mouseleave', () => els.previewBox.style.display = 'none');
  995.  
  996.  
  997.        try {
  998.            const storedGen = JSON.parse(localStorage.getItem('xai_api_gen_settings') || '{}');
  999.            if (storedGen.action) els.action.value = storedGen.action;
  1000.            if (storedGen.resImg) els.resImg.value = storedGen.resImg;
  1001.            if (storedGen.resVid) els.resVid.value = storedGen.resVid;
  1002.            if (storedGen.n) els.n.value = storedGen.n;
  1003.            if (storedGen.duration) els.duration.value = storedGen.duration;
  1004.            if (storedGen.loops) els.loops.value = storedGen.loops;
  1005.        } catch(e) {}
  1006.  
  1007.        const updateGenSettingsMemory = () => {
  1008.            localStorage.setItem('xai_api_gen_settings', JSON.stringify({
  1009.                action: els.action.value, resImg: els.resImg.value, resVid: els.resVid.value,
  1010.                n: els.n.value, duration: els.duration.value, loops: els.loops.value
  1011.            }));
  1012.        };
  1013.        [els.action, els.resImg, els.resVid, els.n, els.duration, els.loops].forEach(el => el.addEventListener('change', updateGenSettingsMemory));
  1014.        els.duration.addEventListener('change', updateWarnings);
  1015.  
  1016.        try {
  1017.            const storedRefs = JSON.parse(localStorage.getItem('xai_api_refs'));
  1018.            if (Array.isArray(storedRefs)) referenceMedia = storedRefs;
  1019.        } catch(e) {}
  1020.  
  1021.        // ─── SETTINGS & EXPORT/IMPORT BINDINGS ───────────────────────────────────
  1022.        els.settingsBtn.addEventListener('click', () => {
  1023.            document.getElementById('set-retries').value = appSettings.retries;
  1024.            document.getElementById('set-delay-min').value = appSettings.delayMin;
  1025.            document.getElementById('set-delay-max').value = appSettings.delayMax;
  1026.            document.getElementById('set-rate-limit-delay').value = appSettings.rateLimitDelay;
  1027.            document.getElementById('set-notifications').checked = appSettings.notifications;
  1028.            document.getElementById('set-save-png').checked = appSettings.saveAsPng;
  1029.            document.getElementById('set-auto-retry-video').checked = appSettings.autoRetryStuckVideo;
  1030.            document.getElementById('set-break-loop-on-failure').checked = appSettings.breakLoopOnFailure;
  1031.            document.getElementById('set-video-timeout').value = appSettings.videoPollTimeout;
  1032.  
  1033.            document.getElementById('set-suppress-warning').checked = appSettings.suppressEditWarning;
  1034.            document.getElementById('set-video-autoplay').checked = appSettings.videoAutoplay;
  1035.            document.getElementById('set-video-muted').checked = appSettings.videoMuted;
  1036.  
  1037.            document.getElementById('set-api-base-url').value = appSettings.apiBaseUrl || '';
  1038.            document.getElementById('set-custom-css').value = appSettings.customCSS || '';
  1039.            els.settingsModal.style.display = 'flex';
  1040.            addLog('UI', 'Opened Settings modal');
  1041.        });
  1042.  
  1043.        els.saveSettingsBtn.addEventListener('click', () => {
  1044.            appSettings.retries = parseInt(document.getElementById('set-retries').value) || 20;
  1045.            appSettings.delayMin = parseInt(document.getElementById('set-delay-min').value) || 2000;
  1046.            appSettings.delayMax = Math.max(appSettings.delayMin, parseInt(document.getElementById('set-delay-max').value) || 4000);
  1047.            appSettings.rateLimitDelay = parseInt(document.getElementById('set-rate-limit-delay').value) || 60;
  1048.            appSettings.notifications = document.getElementById('set-notifications').checked;
  1049.            appSettings.saveAsPng = document.getElementById('set-save-png').checked;
  1050.            appSettings.autoRetryStuckVideo = document.getElementById('set-auto-retry-video').checked;
  1051.            appSettings.breakLoopOnFailure = document.getElementById('set-break-loop-on-failure').checked;
  1052.            appSettings.videoPollTimeout = parseInt(document.getElementById('set-video-timeout').value) || 300;
  1053.  
  1054.            appSettings.suppressEditWarning = document.getElementById('set-suppress-warning').checked;
  1055.            appSettings.videoAutoplay = document.getElementById('set-video-autoplay').checked;
  1056.            appSettings.videoMuted = document.getElementById('set-video-muted').checked;
  1057.  
  1058.            appSettings.apiBaseUrl = document.getElementById('set-api-base-url').value.trim();
  1059.            appSettings.customCSS = document.getElementById('set-custom-css').value;
  1060.  
  1061.            localStorage.setItem('xai_api_settings', JSON.stringify(appSettings));
  1062.            document.getElementById('xai-custom-css-block').innerHTML = appSettings.customCSS;
  1063.  
  1064.            updateWarnings();
  1065.            els.settingsModal.style.display = 'none';
  1066.            addLog('UI', 'Saved new settings', appSettings);
  1067.        });
  1068.  
  1069.        els.closeSettingsBtn.addEventListener('click', () => els.settingsModal.style.display = 'none');
  1070.  
  1071.        // Export/Import Logic
  1072.        els.exportDataBtn.addEventListener('click', () => {
  1073.            addLog('UI', 'Exporting Data');
  1074.            const exportObj = {
  1075.                settings: appSettings,
  1076.                refs: referenceMedia
  1077.            };
  1078.            const blob = new Blob([JSON.stringify(exportObj, null, 2)], { type: 'application/json' });
  1079.            const url = URL.createObjectURL(blob);
  1080.            const a = document.createElement('a');
  1081.            a.href = url;
  1082.            a.download = `xAI_Pro_Studio_Backup_${makeTimestamp()}.json`;
  1083.            a.click();
  1084.            URL.revokeObjectURL(url);
  1085.        });
  1086.  
  1087.        els.importDataBtn.addEventListener('click', () => els.importDataFile.click());
  1088.        els.importDataFile.addEventListener('change', (e) => {
  1089.            const file = e.target.files[0];
  1090.            if (!file) return;
  1091.            addLog('UI', 'Importing Data', file.name);
  1092.            const reader = new FileReader();
  1093.            reader.onload = (evt) => {
  1094.                try {
  1095.                    const parsed = JSON.parse(evt.target.result);
  1096.                    if (parsed.settings) {
  1097.                        appSettings = { ...appSettings, ...parsed.settings };
  1098.                        localStorage.setItem('xai_api_settings', JSON.stringify(appSettings));
  1099.                    }
  1100.                    if (parsed.refs && Array.isArray(parsed.refs)) {
  1101.                        referenceMedia = parsed.refs;
  1102.                        saveRefs();
  1103.                    }
  1104.                    alert("Import successful! The page will now reload to apply settings.");
  1105.                    location.reload();
  1106.                } catch(err) {
  1107.                    addLog('ERROR', 'Import failed', err.message);
  1108.                    alert("Failed to parse the uploaded JSON file. Ensure it is a valid backup.");
  1109.                }
  1110.            };
  1111.            reader.readAsText(file);
  1112.            e.target.value = '';
  1113.        });
  1114.  
  1115.        // ─── FAVORITES BINDINGS ──────────────────────────────────────────────────
  1116.        function renderFavList() {
  1117.            els.favList.innerHTML = '';
  1118.            if (favoritePrompts.length === 0) {
  1119.                els.favList.innerHTML = '<div style="color: #64748b; font-size: 12px; text-align: center; margin-top: 15px;">No favorite prompts yet.</div>';
  1120.                return;
  1121.            }
  1122.            favoritePrompts.forEach((promptText, idx) => {
  1123.                const div = document.createElement('div');
  1124.                div.className = 'fav-item';
  1125.                div.innerHTML = `
  1126.                    <div class="fav-text" title="${promptText.replace(/"/g, '&quot;')}">${promptText}</div>
  1127.                    <button class="btn-secondary" style="padding: 2px 6px; font-size: 10px; border-color: #ef4444; color: #ef4444;">Del</button>
  1128.                `;
  1129.                div.querySelector('.fav-text').addEventListener('click', () => {
  1130.                    setPromptVal(promptText);
  1131.                    els.favModal.style.display = 'none';
  1132.                    addLog('UI', 'Loaded favorite prompt');
  1133.                });
  1134.                div.querySelector('button').addEventListener('click', () => {
  1135.                    favoritePrompts.splice(idx, 1);
  1136.                    localStorage.setItem('xai_api_favs', JSON.stringify(favoritePrompts));
  1137.                    renderFavList();
  1138.                });
  1139.                els.favList.appendChild(div);
  1140.            });
  1141.        }
  1142.  
  1143.        els.favBtn.addEventListener('click', () => {
  1144.            renderFavList();
  1145.            els.favModal.style.display = 'flex';
  1146.        });
  1147.        els.closeFavBtn.addEventListener('click', () => els.favModal.style.display = 'none');
  1148.        els.addFavBtn.addEventListener('click', () => {
  1149.            const p = els.prompt.value.trim();
  1150.            if (!p) return alert("Prompt is empty!");
  1151.            if (favoritePrompts.includes(p)) return alert("Already in favorites!");
  1152.            favoritePrompts.unshift(p);
  1153.            localStorage.setItem('xai_api_favs', JSON.stringify(favoritePrompts));
  1154.            renderFavList();
  1155.            addLog('UI', 'Added prompt to favorites');
  1156.        });
  1157.  
  1158.        // ─── PROMPT BOX EXIF DRAG & DROP ──────────────────────────────────────────
  1159.        els.prompt.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); els.promptContainer.classList.add('prompt-drag-over'); });
  1160.        els.prompt.addEventListener('dragleave', (e) => { e.preventDefault(); e.stopPropagation(); els.promptContainer.classList.remove('prompt-drag-over'); });
  1161.        els.prompt.addEventListener('drop', async (e) => {
  1162.            e.preventDefault(); e.stopPropagation();
  1163.            els.promptContainer.classList.remove('prompt-drag-over');
  1164.            if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
  1165.                const file = e.dataTransfer.files[0];
  1166.                const oldVal = els.prompt.value;
  1167.                setPromptVal("Extracting prompt from EXIF...");
  1168.                addLog('UI', 'Dropped file into prompt box', file.name);
  1169.                const extractedPrompt = await extractPromptFromImage(file);
  1170.                if (extractedPrompt) {
  1171.                    setPromptVal(extractedPrompt);
  1172.                } else {
  1173.                    setPromptVal(oldVal);
  1174.                    alert("No prompt found in this image's EXIF data (or not a valid JPEG).");
  1175.                }
  1176.            }
  1177.        });
  1178.  
  1179.        // ─── MASTER ACTION UI LOGIC ───────────────────────────────────────────────
  1180.        function getMaxMedia() {
  1181.            const val = els.action.value;
  1182.            if (val === 'gen_image') return 5;
  1183.            if (val === 'gen_video') return 7;
  1184.            return 1;
  1185.        }
  1186.  
  1187.        function updateActionUI() {
  1188.            const val = els.action.value;
  1189.            addLog('UI', 'Action changed to ' + val);
  1190.  
  1191.            if (val === 'gen_image') {
  1192.                els.settingAr.style.display = 'block'; els.settingResImg.style.display = 'block'; els.settingResVid.style.display = 'none'; els.settingBatch.style.display = 'block'; els.settingDuration.style.display = 'none';
  1193.            } else if (val === 'gen_video') {
  1194.                els.settingAr.style.display = 'block'; els.settingResImg.style.display = 'none'; els.settingResVid.style.display = 'block'; els.settingBatch.style.display = 'none'; els.settingDuration.style.display = 'block';
  1195.                els.duration.innerHTML = `<option value="5">5 Seconds</option><option value="8">8 Seconds</option><option value="10">10 Seconds</option><option value="12">12 Seconds</option><option value="15">15 Seconds</option>`;
  1196.                if (localStorage.getItem('xai_api_gen_settings') && JSON.parse(localStorage.getItem('xai_api_gen_settings')).duration <= 15) {
  1197.                    els.duration.value = JSON.parse(localStorage.getItem('xai_api_gen_settings')).duration;
  1198.                } else els.duration.value = "5";
  1199.            } else if (val === 'edit_video') {
  1200.                els.settingAr.style.display = 'none'; els.settingResImg.style.display = 'none'; els.settingResVid.style.display = 'none'; els.settingBatch.style.display = 'none'; els.settingDuration.style.display = 'none';
  1201.            } else if (val === 'extend_video') {
  1202.                els.settingAr.style.display = 'none'; els.settingResImg.style.display = 'none'; els.settingResVid.style.display = 'none'; els.settingBatch.style.display = 'none'; els.settingDuration.style.display = 'block';
  1203.                els.duration.innerHTML = `<option value="2">2s Extension</option><option value="4">4s Extension</option><option value="6">6s Extension</option><option value="8">8s Extension</option><option value="10">10s Extension</option>`;
  1204.                if (localStorage.getItem('xai_api_gen_settings') && JSON.parse(localStorage.getItem('xai_api_gen_settings')).duration <= 10) {
  1205.                    els.duration.value = JSON.parse(localStorage.getItem('xai_api_gen_settings')).duration;
  1206.                } else els.duration.value = "6";
  1207.            }
  1208.  
  1209.            const max = getMaxMedia();
  1210.            els.refTitle.innerText = `Reference Media (Max Active: ${max})`;
  1211.  
  1212.            let activeCount = 0;
  1213.            referenceMedia.forEach(m => {
  1214.                let valid = true;
  1215.                if ((val === 'gen_image' || val === 'gen_video') && m.isVideo) valid = false;
  1216.                if ((val === 'edit_video' || val === 'extend_video') && !m.isVideo) valid = false;
  1217.  
  1218.                if (!valid) m.active = false;
  1219.                else if (m.active) {
  1220.                    activeCount++;
  1221.                    if (activeCount > max) m.active = false;
  1222.                }
  1223.            });
  1224.            renderRefList();
  1225.            updateWarnings();
  1226.        }
  1227.  
  1228.        els.action.addEventListener('change', updateActionUI);
  1229.        updateActionUI();
  1230.  
  1231.        // ─── INSTANT EDIT BINDING HANDLER ─────────────────────────────────────────
  1232.        updateUIForEditMedia = (mediaUrl, isVideo, fileName) => {
  1233.            addLog('UI', 'Instant edit triggered', { fileName, isVideo });
  1234.            referenceMedia.forEach(m => m.active = false);
  1235.  
  1236.            referenceMedia.unshift({
  1237.                id: Date.now() + Math.random(),
  1238.                base64: mediaUrl,
  1239.                isVideo: isVideo,
  1240.                active: true,
  1241.                thumb: isVideo ? null : mediaUrl,
  1242.                fileName: fileName || (isVideo ? 'Edited_Video.mp4' : 'Edited_Image.png')
  1243.            });
  1244.  
  1245.            els.action.value = isVideo ? 'edit_video' : 'gen_image';
  1246.            updateActionUI();
  1247.            updateGenSettingsMemory();
  1248.            renderRefList();
  1249.            saveRefs();
  1250.        };
  1251.  
  1252.        // ─── BUILD PAYLOAD LOGIC ──────────────────────────────────────────────────
  1253.        function buildPayloadData(dynamicPromptText) {
  1254.            const action = els.action.value;
  1255.            const payload = {
  1256.                model: action === 'gen_image' ? "grok-imagine-image" : "grok-imagine-video",
  1257.                prompt: dynamicPromptText,
  1258.            };
  1259.  
  1260.            const ar = currentAr.toLowerCase();
  1261.            const activeRefs = referenceMedia.filter(m => m.active);
  1262.  
  1263.            if (action === 'gen_image') {
  1264.                payload.n = parseInt(els.n.value); payload.resolution = els.resImg.value; payload.response_format = "b64_json";
  1265.                if (ar !== 'auto') payload.aspect_ratio = ar; else if (activeRefs.length > 0) payload.aspect_ratio = 'auto';
  1266.  
  1267.                if (activeRefs.length > 0) {
  1268.                    if (activeRefs.length === 1) payload.image = { url: activeRefs[0].base64 };
  1269.                    else payload.images = activeRefs.map(m => ({ type: "image_url", url: m.base64 }));
  1270.                }
  1271.            } else if (action === 'gen_video') {
  1272.                payload.duration = parseInt(els.duration.value); payload.resolution = els.resVid.value;
  1273.                if (ar !== 'auto') payload.aspect_ratio = ar;
  1274.                if (activeRefs.length > 0) payload.reference_images = activeRefs.map(m => ({ url: m.base64 }));
  1275.            } else if (action === 'edit_video') {
  1276.                if (activeRefs.length > 0) payload.video = { url: activeRefs[0].base64 };
  1277.            } else if (action === 'extend_video') {
  1278.                payload.duration = parseInt(els.duration.value);
  1279.                if (activeRefs.length > 0) payload.video = { url: activeRefs[0].base64 };
  1280.            }
  1281.  
  1282.            addLog('INFO', 'Payload built', payload);
  1283.            return payload;
  1284.        }
  1285.  
  1286.        // ─── CUSTOM PAYLOAD DEBUGGER LOGIC ────────────────────────────────────────
  1287.        els.previewBtn.addEventListener('click', () => {
  1288.            const dynamicPrompt = parseDynamicPrompt(els.prompt.value.trim());
  1289.            fullPayloadMemory = buildPayloadData(dynamicPrompt);
  1290.            const displayP = JSON.parse(JSON.stringify(fullPayloadMemory));
  1291.            const trunc = "[BASE64_TRUNCATED_FOR_PREVIEW]";
  1292.  
  1293.            if (displayP.image) { if (Array.isArray(displayP.image)) displayP.image.forEach(img => img.url = trunc); else displayP.image.url = trunc; }
  1294.            if (displayP.images && Array.isArray(displayP.images)) displayP.images.forEach(img => img.url = trunc);
  1295.            if (displayP.reference_images && Array.isArray(displayP.reference_images)) displayP.reference_images.forEach(img => img.url = trunc);
  1296.            if (displayP.video) displayP.video.url = trunc;
  1297.  
  1298.            const activeRefs = referenceMedia.filter(m => m.active);
  1299.            let endpoint = '/v1/images/generations';
  1300.            if (els.action.value === 'gen_image' && activeRefs.length > 0) endpoint = '/v1/images/edits';
  1301.            else if (els.action.value === 'gen_video') endpoint = '/v1/videos/generations';
  1302.            else if (els.action.value === 'edit_video') endpoint = '/v1/videos/edits';
  1303.            else if (els.action.value === 'extend_video') endpoint = '/v1/videos/extensions';
  1304.  
  1305.            els.payloadEndpoint.value = endpoint;
  1306.            els.payloadCode.value = JSON.stringify(displayP, null, 2);
  1307.            els.payloadModal.style.display = 'flex';
  1308.            addLog('UI', 'Opened custom payload preview');
  1309.        });
  1310.  
  1311.        els.closePayloadBtn.addEventListener('click', () => els.payloadModal.style.display = 'none');
  1312.  
  1313.        function injectBase64(editedObj, originalObj) {
  1314.            if (!editedObj || typeof editedObj !== 'object') return;
  1315.            for (let key in editedObj) {
  1316.                if (typeof editedObj[key] === 'string' && editedObj[key] === '[BASE64_TRUNCATED_FOR_PREVIEW]') {
  1317.                    if (originalObj && originalObj[key]) editedObj[key] = originalObj[key];
  1318.                } else if (typeof editedObj[key] === 'object') { injectBase64(editedObj[key], originalObj ? originalObj[key] : null); }
  1319.            }
  1320.        }
  1321.  
  1322.        els.sendCustomBtn.addEventListener('click', async () => {
  1323.            initAudio();
  1324.            let customPayload;
  1325.            try { customPayload = JSON.parse(els.payloadCode.value); }
  1326.            catch(e) { return alert("Invalid JSON format in textarea!"); }
  1327.  
  1328.            injectBase64(customPayload, fullPayloadMemory);
  1329.            const endpoint = els.payloadEndpoint.value.trim();
  1330.            const isImage = customPayload.model === "grok-imagine-image";
  1331.  
  1332.            stealthIdentityReset();
  1333.  
  1334.            els.sendCustomBtn.disabled = true;
  1335.            els.sendCustomBtn.innerHTML = `<div class="btn-content"><div class="loading-spinner"></div> Processing...</div>`;
  1336.            isRequestCancelled = false;
  1337.  
  1338.            addLog('UI', 'Sending custom payload', { endpoint, isImage });
  1339.            const ok = await executeGenerationSingle(endpoint, customPayload, isImage, els.customStatus, 1, 1);
  1340.  
  1341.            els.sendCustomBtn.disabled = false;
  1342.            els.sendCustomBtn.innerHTML = `<div class="btn-content">🚀 Send Custom Payload</div>`;
  1343.            if (ok) notifyUser(false); else notifyUser(true);
  1344.        });
  1345.  
  1346.        // ─── CSP-SAFE DELEGATED LIGHTBOX LOGIC ────────────────────────────────────
  1347.        els.app.addEventListener('click', (e) => {
  1348.            if (e.target && e.target.classList.contains('zoomable') && e.target.tagName === 'IMG') {
  1349.                els.lightboxImg.src = e.target.src; els.lightboxDl.href = e.target.src; els.lightboxDl.download = e.target.dataset.filename || 'Reference_Image.jpg';
  1350.                els.lightbox.style.display = 'flex';
  1351.            }
  1352.        });
  1353.        els.lightbox.addEventListener('click', (e) => {
  1354.            if (e.target.closest('#xai-lightbox-dl')) return;
  1355.            if (e.target.closest('.modal-box')) return;
  1356.            els.lightbox.style.display = 'none';
  1357.        });
  1358.        document.addEventListener('keydown', (e) => { if(e.key === 'Escape') { Array.from(document.querySelectorAll('.modal-overlay')).forEach(m => m.style.display = 'none'); } });
  1359.  
  1360.        // ─── FIXED FLOATING AR DROPDOWN LOGIC ─────────────────────────────────────
  1361.        function renderArOptions() {
  1362.            els.arOptionsCont.innerHTML = '';
  1363.            arData.forEach(ar => {
  1364.                const opt = document.createElement('div'); opt.className = 'ar-option';
  1365.                opt.innerHTML = `${createArIcon(ar.w, ar.h, ar.isAuto)} <span>${ar.label}</span>`;
  1366.                opt.addEventListener('click', () => { setAr(ar); els.arOptionsCont.style.display = 'none'; });
  1367.                els.arOptionsCont.appendChild(opt);
  1368.            });
  1369.        }
  1370.        function setAr(arObj) {
  1371.            currentAr = arObj.label; localStorage.setItem('xai_api_ar', currentAr);
  1372.            els.arSelectedIcon.innerHTML = createArIcon(arObj.w, arObj.h, arObj.isAuto); els.arSelectedText.innerText = arObj.label;
  1373.            addLog('UI', 'Set Aspect Ratio', currentAr);
  1374.        }
  1375.        renderArOptions();
  1376.        const initialAr = arData.find(a => a.label === currentAr) || arData[0]; setAr(initialAr);
  1377.  
  1378.        els.arSelectBox.addEventListener('click', (e) => {
  1379.            e.stopPropagation();
  1380.            if (els.arOptionsCont.style.display === 'block') { els.arOptionsCont.style.display = 'none'; return; }
  1381.            const rect = els.arSelectBox.getBoundingClientRect();
  1382.            els.arOptionsCont.style.top = (rect.bottom + 5) + 'px'; els.arOptionsCont.style.left = rect.left + 'px'; els.arOptionsCont.style.width = rect.width + 'px';
  1383.            els.arOptionsCont.style.display = 'block';
  1384.        });
  1385.        document.addEventListener('click', (e) => { if (!els.arOptionsCont.contains(e.target)) els.arOptionsCont.style.display = 'none'; });
  1386.  
  1387.        // ─── UNLIMITED MULTI-IMAGE DRAG/DROP WITH VIDEO THUMBNAILS ────────────────
  1388.        async function processFile(file) {
  1389.            const isVid = file.type.startsWith('video/'); const isImg = file.type.startsWith('image/');
  1390.            if (!isVid && !isImg) {
  1391.                addLog('WARN', 'Attempted to process unsupported file type', file.type);
  1392.                return;
  1393.            }
  1394.  
  1395.            addLog('INFO', 'Processing uploaded file', { name: file.name, type: file.type, size: file.size });
  1396.  
  1397.            if (isVid && (els.action.value === 'gen_image' || els.action.value === 'gen_video')) { els.action.value = 'edit_video'; updateActionUI(); updateGenSettingsMemory(); }
  1398.            else if (isImg && (els.action.value === 'edit_video' || els.action.value === 'extend_video')) { els.action.value = 'gen_image'; updateActionUI(); updateGenSettingsMemory(); }
  1399.  
  1400.            const max = getMaxMedia();
  1401.            const activeCount = referenceMedia.filter(m => m.active).length;
  1402.  
  1403.            let thumbBase64 = null;
  1404.            if (isVid) thumbBase64 = await generateVideoThumbnail(file);
  1405.  
  1406.            const reader = new FileReader();
  1407.            reader.onload = (event) => {
  1408.                referenceMedia.push({
  1409.                    id: Date.now() + Math.random(),
  1410.                    base64: event.target.result,
  1411.                    isVideo: isVid,
  1412.                    active: activeCount < max,
  1413.                    thumb: thumbBase64,
  1414.                    fileName: file.name
  1415.                });
  1416.                renderRefList();
  1417.                if (isImg) setAr(arData[0]);
  1418.                addLog('INFO', 'File processed and added to reference media');
  1419.            };
  1420.            reader.readAsDataURL(file);
  1421.        }
  1422.  
  1423.        let draggedIndex = null;
  1424.        function renderRefList() {
  1425.            els.refList.innerHTML = '';
  1426.            referenceMedia.forEach((media, index) => {
  1427.                const item = document.createElement('div');
  1428.                item.className = 'ref-item';
  1429.                item.draggable = true;
  1430.  
  1431.                const activeRefs = referenceMedia.filter(m => m.active);
  1432.                const dynamicIndex = media.active ? activeRefs.indexOf(media) : '?';
  1433.                const actionVal = els.action.value;
  1434.                const disableInsert = actionVal === 'edit_video' || actionVal === 'extend_video';
  1435.  
  1436.                let thumbHtml = '';
  1437.                if (media.isVideo) {
  1438.                    if (media.thumb) {
  1439.                        thumbHtml = `
  1440.                            <div style="position: relative; width: 90px; height: 90px; flex-shrink: 0;">
  1441.                                <img src="${media.thumb}" class="ref-thumb" style="width: 100%; height: 100%;" title="${media.fileName || 'Video'}">
  1442.                                <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>
  1443.                            </div>
  1444.                        `;
  1445.                    } else {
  1446.                        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>`;
  1447.                    }
  1448.                } else {
  1449.                    thumbHtml = `<img src="${media.base64}" class="ref-thumb zoomable" data-filename="${media.fileName || 'Reference_'+(index+1)+'.jpg'}" title="${media.fileName || 'Click to view'}">`;
  1450.                }
  1451.  
  1452.                let insertBtnHtml = '';
  1453.                if (!media.isVideo) {
  1454.                    if (disableInsert) {
  1455.                        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>`;
  1456.                    } else if (!media.active) {
  1457.                        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>`;
  1458.                    } else {
  1459.                        insertBtnHtml = `<button class="ref-action-btn ref-insert" title="Insert into prompt">➕ &lt;IMAGE_${dynamicIndex}&gt;</button>`;
  1460.                    }
  1461.                }
  1462.  
  1463.                item.innerHTML = `
  1464.                    <div class="ref-handle">☰</div>
  1465.                    ${thumbHtml}
  1466.                    <div class="ref-info">
  1467.                        <span style="font-size: 12px; color: #e2e8f0; font-weight: 600;">Ref ${index + 1} ${media.isVideo ? '(Video)' : ''}</span>
  1468.                        <label class="ref-toggle-label">
  1469.                            <input type="checkbox" class="ref-active-toggle" ${media.active ? 'checked' : ''}> Use in Payload
  1470.                        </label>
  1471.                        <div class="ref-btn-group">
  1472.                            ${insertBtnHtml}
  1473.                            <button class="ref-action-btn ref-del">Remove</button>
  1474.                        </div>
  1475.                    </div>
  1476.                `;
  1477.  
  1478.                const insertBtn = item.querySelector('.ref-insert');
  1479.                if (insertBtn && media.active) {
  1480.                    insertBtn.addEventListener('click', (e) => {
  1481.                        const currentAction = els.action.value;
  1482.                        if (currentAction === 'edit_video' || currentAction === 'extend_video') {
  1483.                            e.preventDefault();
  1484.                            return alert("<IMAGE_X> tags are not supported in Edit Video and Extend Video modes.");
  1485.                        }
  1486.  
  1487.                        const promptEl = els.prompt;
  1488.                        const tag = `<IMAGE_${dynamicIndex}>`;
  1489.                        const startPos = promptEl.selectionStart;
  1490.                        const endPos = promptEl.selectionEnd;
  1491.  
  1492.                        setPromptVal(promptEl.value.substring(0, startPos) + tag + promptEl.value.substring(endPos, promptEl.value.length));
  1493.                        promptEl.selectionStart = promptEl.selectionEnd = startPos + tag.length;
  1494.                        promptEl.focus();
  1495.                        addLog('UI', 'Inserted image tag to prompt', tag);
  1496.                    });
  1497.                }
  1498.  
  1499.                if (!media.active) item.classList.add('inactive');
  1500.  
  1501.                item.querySelector('.ref-active-toggle').addEventListener('change', (e) => {
  1502.                    const max = getMaxMedia();
  1503.                    const currentlyActive = referenceMedia.filter(r => r.active).length;
  1504.  
  1505.                    if (e.target.checked && currentlyActive >= max) {
  1506.                        alert(`You can only have up to ${max} active references for this mode.`);
  1507.                        e.target.checked = false;
  1508.                        return;
  1509.                    }
  1510.                    media.active = e.target.checked;
  1511.                    saveRefs();
  1512.                    renderRefList();
  1513.                    syncPromptBackdrop();
  1514.                    updateWarnings();
  1515.                });
  1516.  
  1517.                item.addEventListener('dragstart', (e) => { draggedIndex = index; e.dataTransfer.effectAllowed = 'move'; setTimeout(() => item.classList.add('dragging'), 0); });
  1518.                item.addEventListener('dragend', () => { item.classList.remove('dragging'); draggedIndex = null; document.querySelectorAll('.ref-item').forEach(el => el.classList.remove('drag-over-top', 'drag-over-bottom')); });
  1519.                item.addEventListener('dragover', (e) => {
  1520.                    e.preventDefault(); if (draggedIndex === null || draggedIndex === index) return;
  1521.                    const rect = item.getBoundingClientRect();
  1522.                    if (e.clientY - rect.top < rect.height / 2) { item.classList.add('drag-over-top'); item.classList.remove('drag-over-bottom'); }
  1523.                    else { item.classList.add('drag-over-bottom'); item.classList.remove('drag-over-top'); }
  1524.                });
  1525.                item.addEventListener('dragleave', () => item.classList.remove('drag-over-top', 'drag-over-bottom'));
  1526.                item.addEventListener('drop', (e) => {
  1527.                    e.preventDefault(); item.classList.remove('drag-over-top', 'drag-over-bottom');
  1528.                    if (draggedIndex === null || draggedIndex === index) return;
  1529.                    const rect = item.getBoundingClientRect();
  1530.                    let insertIndex = (e.clientY - rect.top) < rect.height / 2 ? index : index + 1;
  1531.                    if (draggedIndex < insertIndex) insertIndex--;
  1532.                    const [movedImage] = referenceMedia.splice(draggedIndex, 1);
  1533.                    referenceMedia.splice(insertIndex, 0, movedImage); renderRefList(); syncPromptBackdrop();
  1534.                });
  1535.  
  1536.                item.querySelector('.ref-del').addEventListener('click', () => { referenceMedia.splice(index, 1); renderRefList(); syncPromptBackdrop(); updateWarnings(); });
  1537.                els.refList.appendChild(item);
  1538.            });
  1539.            saveRefs();
  1540.        }
  1541.  
  1542.        els.uploadPlaceholder.addEventListener('click', () => { els.fileInput.click(); });
  1543.        els.fileInput.addEventListener('change', (e) => { Array.from(e.target.files).forEach(processFile); els.fileInput.value = ''; });
  1544.        document.addEventListener('paste', (e) => {
  1545.            if (e.target && (e.target.id === 'payload-code' || e.target.id === 'xai-api-prompt' || e.target.id === 'set-custom-css' || e.target.tagName === 'INPUT')) return;
  1546.            const items = e.clipboardData.items; for (let i = 0; i < items.length; i++) if (items[i].type.indexOf('image') !== -1) processFile(items[i].getAsFile());
  1547.        });
  1548.  
  1549.        els.dropzone.addEventListener('dragover', (e) => { e.preventDefault(); els.dropzone.classList.add('drag-over'); });
  1550.        els.dropzone.addEventListener('dragleave', () => els.dropzone.classList.remove('drag-over'));
  1551.        els.dropzone.addEventListener('drop', (e) => { e.preventDefault(); els.dropzone.classList.remove('drag-over'); if (e.dataTransfer.files) Array.from(e.dataTransfer.files).forEach(processFile); });
  1552.  
  1553.        // ─── CANCEL BUTTON LOGIC ─────────────────────────────────────────────────
  1554.        els.cancelBtn.addEventListener('click', () => {
  1555.            addLog('UI', 'Request cancelled by user');
  1556.            isRequestCancelled = true;
  1557.            if (currentAbortController) currentAbortController.abort();
  1558.            els.cancelBtn.style.display = 'none'; els.previewBtn.style.display = 'flex';
  1559.            els.btn.disabled = false; els.btn.innerHTML = `<div class="btn-content">Generate</div>`;
  1560.            els.status.innerText = "Request Cancelled."; els.status.className = ""; els.status.style.color = "#ef4444";
  1561.        });
  1562.  
  1563.        // ─── MASTER GENERATION LOGIC ──────────────────────────────────────────────
  1564.        async function executeGenerationSingle(endpoint, payload, isImageAction, statusEl, currentLoop, totalLoops) {
  1565.            let loopPrefix = totalLoops > 1 ? `[Run ${currentLoop}/${totalLoops}] ` : '';
  1566.            statusEl.innerText = `${loopPrefix}Processing request...`;
  1567.            statusEl.className = "status-pulsing";
  1568.  
  1569.            addLog('INFO', `Starting generation loop ${currentLoop}/${totalLoops}`, { endpoint, isImageAction });
  1570.  
  1571.            currentAbortController = new AbortController();
  1572.            let rateLimitFails = 0;
  1573.  
  1574.            let attempts = 0; let success = false;
  1575.            while (attempts < appSettings.retries && !success && !isRequestCancelled) {
  1576.                try {
  1577.                    statusEl.innerText = `${loopPrefix}Sending request to xAI...`;
  1578.                    const fullEndpoint = getApiUrl(endpoint);
  1579.  
  1580.                    addLog('NETWORK_REQ', `POST ${fullEndpoint}`, payload);
  1581.  
  1582.                    const response = await fetch(fullEndpoint, {
  1583.                        method: 'POST',
  1584.                        headers: { 'Content-Type': 'application/json' },
  1585.                        body: JSON.stringify(payload),
  1586.                        signal: currentAbortController.signal
  1587.                    });
  1588.  
  1589.                    // We do not blind-retry 429 here anymore so it falls into JSON parser for specific rate limit handling
  1590.                    if (response.status === 503 || response.status === 500 || response.status === 502) {
  1591.                        attempts++;
  1592.                        statusEl.innerText = `${loopPrefix}Busy (Error ${response.status}). Retrying... [${attempts}/${appSettings.retries}]`;
  1593.                        const delay = Math.floor(Math.random() * (appSettings.delayMax - appSettings.delayMin + 1)) + appSettings.delayMin;
  1594.                        addLog('WARN', `Gateway error ${response.status}. Retrying in ${delay}ms`);
  1595.                        await sleep(delay);
  1596.                        continue;
  1597.                    }
  1598.  
  1599.                    if (!response.ok) {
  1600.                        let errMsg = `HTTP ${response.status}`;
  1601.                        let errData = null;
  1602.                        try {
  1603.                            errData = await response.json();
  1604.                            addLog('NETWORK_ERR', `Response error JSON`, { status: response.status, data: errData });
  1605.  
  1606.                            if (errData.error && errData.error.message) errMsg += `\n${errData.error.message}`;
  1607.                            else if (errData.error && typeof errData.error === 'string') errMsg += `\n${errData.error}`;
  1608.  
  1609.                            if (errData.code) errMsg += `\nCode: ${errData.code}`;
  1610.                            if (errData.detail) errMsg += `\n${typeof errData.detail === 'string' ? errData.detail : JSON.stringify(errData.detail)}`;
  1611.                        } catch(e) {
  1612.                            addLog('NETWORK_ERR', `Failed to parse error response body`, { status: response.status });
  1613.                        }
  1614.                        throw new Error(errMsg);
  1615.                    }
  1616.  
  1617.                    const data = await response.json();
  1618.                    addLog('NETWORK_RES', `Successful response`, data);
  1619.  
  1620.                    if (isImageAction) {
  1621.                        if (data && data.data && Array.isArray(data.data)) {
  1622.                            success = true;
  1623.                            statusEl.innerText = `${loopPrefix}Processing final images...`;
  1624.                            statusEl.style.color = "#10b981";
  1625.                            statusEl.className = "";
  1626.  
  1627.                            await Promise.all(data.data.map((imgObj, index) =>
  1628.                                processAndRenderImage(imgObj, payload.prompt, index + 1, data.data.length, els.currentImages)
  1629.                            ));
  1630.  
  1631.                            statusEl.innerText = "Ready";
  1632.                            statusEl.style.color = "#64748b";
  1633.                        } else throw new Error("Invalid response format.");
  1634.                    } else {
  1635.                        const reqId = data.request_id;
  1636.                        if (!reqId) {
  1637.                            if (data.error) throw new Error(data.error.message || JSON.stringify(data.error));
  1638.                            throw new Error("No Request ID returned.");
  1639.                        }
  1640.  
  1641.                        let videoReady = false;
  1642.                        let pollCount = 0;
  1643.                        const MAX_POLLS = Math.max(1, Math.ceil(appSettings.videoPollTimeout / 5));
  1644.  
  1645.                        while (!videoReady && !isRequestCancelled) {
  1646.                            pollCount++;
  1647.                            if (pollCount > MAX_POLLS) throw new Error("Timeout: Video likely dropped by filters or stuck in queue.");
  1648.                            await sleep(5000);
  1649.                            if (isRequestCancelled) break;
  1650.  
  1651.                            addLog('POLL_REQ', `Polling video ID: ${reqId}`, { pollCount, MAX_POLLS });
  1652.                            const pollRes = await fetch(getApiUrl(`/v1/videos/${reqId}`), { signal: currentAbortController.signal });
  1653.  
  1654.                            if (!pollRes.ok) {
  1655.                                if (pollRes.status >= 400 && pollRes.status < 500) {
  1656.                                    let errData;
  1657.                                    try { errData = await pollRes.json(); } catch(e) {}
  1658.                                    addLog('POLL_ERR', `Poll failed HTTP ${pollRes.status}`, errData);
  1659.                                    if (errData && errData.error) {
  1660.                                        const errMsg = errData.error.message || errData.error;
  1661.                                        throw new Error(typeof errMsg === 'string' ? errMsg : JSON.stringify(errData.error));
  1662.                                    }
  1663.                                }
  1664.                                addLog('WARN', `Poll returned ${pollRes.status}, continuing to next poll`);
  1665.                                continue;
  1666.                            }
  1667.  
  1668.                            const pollData = await pollRes.json();
  1669.                            addLog('POLL_RES', `Poll Result`, pollData);
  1670.  
  1671.                            if (pollData.error) throw new Error(`API Error: ${pollData.error.message || JSON.stringify(pollData.error)}`);
  1672.  
  1673.                            const state = (pollData.status || pollData.state || 'processing').toLowerCase();
  1674.  
  1675.                            let progressText = "";
  1676.                            if (typeof pollData.progress === 'number') {
  1677.                                progressText = ` ${pollData.progress}%`;
  1678.                            }
  1679.  
  1680.                            statusEl.innerText = `${loopPrefix}Polling video (Status: ${state}${progressText})...[${pollCount}/${MAX_POLLS}]`;
  1681.  
  1682.                            if (state === 'done' || state === 'completed') {
  1683.                                videoReady = true;
  1684.                                success = true;
  1685.                                statusEl.innerText = "Ready"; statusEl.className = ""; statusEl.style.color = "#94a3b8";
  1686.                                renderVideoToGallery(pollData.video.url, payload.prompt, els.currentImages);
  1687.                            } else if (['failed', 'expired', 'rejected', 'blocked', 'moderated', 'nsfw'].includes(state) || pollData.is_sensitive) {
  1688.                                if (pollData.video && pollData.video.url) {
  1689.                                    videoReady = true;
  1690.                                    success = true;
  1691.                                    statusEl.innerText = `${loopPrefix}Warning: Flagged as ${state}, but recovered!`;
  1692.                                    statusEl.className = ""; statusEl.style.color = "#d97706";
  1693.                                    renderVideoToGallery(pollData.video.url, payload.prompt, els.currentImages);
  1694.                                    addLog('WARN', `Video flagged as ${state} but recovered url`, pollData);
  1695.                                } else {
  1696.                                    throw new Error(`Generation halted. Reason: ${state}`);
  1697.                                }
  1698.                            }
  1699.                        }
  1700.                    }
  1701.  
  1702.               } catch (err) {
  1703.                    const errStr = err.message.toLowerCase();
  1704.                    addLog('ERROR', `Generation logic exception`, { message: err.message, stack: err.stack });
  1705.  
  1706.                    if (err.name === 'AbortError' || isRequestCancelled) {
  1707.                        statusEl.innerText = "Request Cancelled."; statusEl.className = ""; statusEl.style.color = "#ef4444";
  1708.                        return false;
  1709.                    }
  1710.  
  1711.                    let isLongRateLimit = false;
  1712.                    let isShortRateLimit = false;
  1713.  
  1714.                    // Smart detection: differentiate between per-second vs per-minute/global limits
  1715.                    if (errStr.includes("requests per second (actual/limit)")) {
  1716.                        const secMatch = errStr.match(/requests per second \(actual\/limit\):\s*(\d+)\s*\/\s*(\d+)/);
  1717.                        const minMatch = errStr.match(/requests per minute \(actual\/limit\):\s*(\d+)\s*\/\s*(\d+)/);
  1718.  
  1719.                        const secExhausted = secMatch && secMatch[1] === secMatch[2];
  1720.                        const minExhausted = minMatch && minMatch[1] === minMatch[2];
  1721.  
  1722.                        if (minExhausted) {
  1723.                            isLongRateLimit = true;
  1724.                        } else if (secExhausted) {
  1725.                            isShortRateLimit = true;
  1726.                        } else {
  1727.                            isLongRateLimit = true;
  1728.                        }
  1729.                    } else if (errStr.includes("rate limit") || errStr.includes("resource has been exhausted") || errStr.includes("http 429")) {
  1730.                        isLongRateLimit = true;
  1731.                    }
  1732.  
  1733.                    if (isShortRateLimit) {
  1734.                        attempts++;
  1735.                        if (attempts >= appSettings.retries) {
  1736.                            statusEl.innerText = `${loopPrefix}Failed: Max retries reached after per-second rate limits.`;
  1737.                            statusEl.className = ""; statusEl.style.color = "#ef4444";
  1738.                            break;
  1739.                        }
  1740.                        statusEl.innerText = `${loopPrefix}Per-Second Limit Hit. Retrying... [${attempts}/${appSettings.retries}]`;
  1741.                        const delay = Math.floor(Math.random() * (appSettings.delayMax - appSettings.delayMin + 1)) + appSettings.delayMin;
  1742.                        await sleep(delay);
  1743.                        continue;
  1744.                    } else if (isLongRateLimit) {
  1745.                        attempts++;
  1746.                        if (attempts >= appSettings.retries) {
  1747.                            statusEl.innerText = `${loopPrefix}Failed: Max retries reached after rate limits.`;
  1748.                            statusEl.className = ""; statusEl.style.color = "#ef4444";
  1749.                            break;
  1750.                        }
  1751.  
  1752.                        let delaySeconds = parseInt(appSettings.rateLimitDelay) || 60;
  1753.                        let countdown = delaySeconds;
  1754.  
  1755.                        //TODO: Другие значения кроме 1, может быть?
  1756.                        if ( (rateLimitFails++) < 1 ) {
  1757.                            await sleep(1000);
  1758.                            continue;
  1759.                        }
  1760.                        rateLimitFails = 0;
  1761.  
  1762.                        while( countdown > 0 && !isRequestCancelled) {
  1763.                            rateLimitFails = 0;
  1764.                            statusEl.innerText = `${loopPrefix}Rate Limit Hit. Retrying in ${countdown}s... [${attempts}/${appSettings.retries}]`;
  1765.                            await sleep(1000);
  1766.                            countdown--;
  1767.                        }
  1768.                        if (isRequestCancelled) return false;
  1769.  
  1770.                        continue;
  1771.                    } else if ((errStr.includes("timeout: video") || errStr.includes("generation halted.") || errStr.includes("video rejected by content moderation")) && appSettings.autoRetryStuckVideo) {
  1772.                        attempts++;
  1773.                        if (attempts >= appSettings.retries) {
  1774.                            statusEl.innerText = `${loopPrefix}Failed: Max retries reached for stuck video.`;
  1775.                            statusEl.className = ""; statusEl.style.color = "#ef4444";
  1776.                            break;
  1777.                        }
  1778.                        statusEl.innerText = `${loopPrefix}Video Stuck/Failed. Auto-retrying... [${attempts}/${appSettings.retries}]`;
  1779.                        const delay = Math.floor(Math.random() * (appSettings.delayMax - appSettings.delayMin + 1)) + appSettings.delayMin;
  1780.                        await sleep(delay);
  1781.  
  1782.                        // Anti-caching injection: add a zero-width space to the prompt to force the server to evaluate a fresh generation.
  1783.                        if (payload.prompt) payload.prompt += "\u200B";
  1784.  
  1785.                        continue;
  1786.                    } else {
  1787.                        statusEl.innerText = `${loopPrefix}Error: ${err.message}`; statusEl.className = ""; statusEl.style.color = "#ef4444";
  1788.                        break;
  1789.                    }
  1790.                }
  1791.            }
  1792.  
  1793.            if (!success && !isRequestCancelled) {
  1794.                if (!statusEl.innerText.includes("Failed:") && !statusEl.innerText.includes("Error:")) {
  1795.                    statusEl.innerText = `${loopPrefix}Failed after ${attempts} retries.`;
  1796.                }
  1797.                statusEl.className = ""; statusEl.style.color = "#ef4444";
  1798.                addLog('ERROR', 'Generation loop failed completely');
  1799.                return false;
  1800.            }
  1801.  
  1802.            return success;
  1803.        }
  1804.  
  1805.        els.btn.addEventListener('click', async () => {
  1806.            initAudio();
  1807.            const basePrompt = els.prompt.value.trim();
  1808.            if (!basePrompt) return alert("Please enter a prompt.");
  1809.  
  1810.            addLog('UI', 'Generate button clicked', { basePrompt });
  1811.  
  1812.            const oldImages = Array.from(els.currentImages.children);
  1813.            if (oldImages.length > 0) {
  1814.                if (els.history.innerText.includes("No history")) els.history.innerHTML = '';
  1815.                oldImages.forEach(card => {
  1816.                    card.className = 'history-card';
  1817.                    const promptEl = card.querySelector('p');
  1818.                    promptEl.title = "Click to copy prompt";
  1819.                    promptEl.onclick = () => { setPromptVal(promptEl.innerText); };
  1820.                    els.history.prepend(card);
  1821.                });
  1822.            }
  1823.  
  1824.            const action = els.action.value;
  1825.            const activeRefs = referenceMedia.filter(m => m.active);
  1826.            let endpoint = '/v1/images/generations';
  1827.            if (action === 'gen_image' && activeRefs.length > 0) endpoint = '/v1/images/edits';
  1828.            else if (action === 'gen_video') endpoint = '/v1/videos/generations';
  1829.            else if (action === 'edit_video') endpoint = '/v1/videos/edits';
  1830.            else if (action === 'extend_video') endpoint = '/v1/videos/extensions';
  1831.  
  1832.            const isImage = action === 'gen_image';
  1833.            const totalLoops = parseInt(els.loops.value) || 1;
  1834.  
  1835.            stealthIdentityReset();
  1836.  
  1837.            els.btn.disabled = true;
  1838.            els.cancelBtn.style.display = 'flex';
  1839.            els.previewBtn.style.display = 'none';
  1840.            isRequestCancelled = false;
  1841.  
  1842.            let allSuccess = true;
  1843.            for (let i = 1; i <= totalLoops; i++) {
  1844.                if (isRequestCancelled) break;
  1845.  
  1846.                const dynamicPrompt = parseDynamicPrompt(basePrompt);
  1847.                const payload = buildPayloadData(dynamicPrompt);
  1848.  
  1849.                els.btn.innerHTML = `<div class="btn-content"><div class="loading-spinner"></div> Gen ${i}/${totalLoops}...</div>`;
  1850.  
  1851.                const ok = await executeGenerationSingle(endpoint, payload, isImage, els.status, i, totalLoops);
  1852.                if (!ok) {
  1853.                    allSuccess = false;
  1854.                    if (appSettings.breakLoopOnFailure) {
  1855.                        break;
  1856.                    } else {
  1857.                        continue;
  1858.                    }
  1859.                }
  1860.            }
  1861.  
  1862.            if (!isRequestCancelled) {
  1863.                els.cancelBtn.style.display = 'none';
  1864.                els.previewBtn.style.display = 'flex';
  1865.                els.btn.disabled = false;
  1866.                els.btn.innerHTML = `<div class="btn-content">Generate</div>`;
  1867.  
  1868.                if (allSuccess) {
  1869.                    els.status.innerText = "All runs complete.";
  1870.                    els.status.className = "";
  1871.                    els.status.style.color = "#64748b";
  1872.                    notifyUser(false);
  1873.                    addLog('INFO', 'All generation loops completed successfully');
  1874.                } else {
  1875.                    notifyUser(true);
  1876.                    addLog('ERROR', 'Generation sequence ended with errors');
  1877.                }
  1878.            }
  1879.        });
  1880.  
  1881.        // ─── SESSION RESETTER ─────────────────────────────────────────────────────
  1882.        els.reset.addEventListener('click', () => {
  1883.            if (!confirm("Wipe cache? (This forces a page reload to clear front-end tokens). Your prompt and settings will be saved.")) return;
  1884.            addLog('UI', 'Wipe cache confirmed, wiping session memory');
  1885.            localStorage.setItem('xai_api_prompt', els.prompt.value);
  1886.            const patterns =[/flushAfter/i, /imagine/i, /generation/i, /grok/i, /mixpanel|mp_/i, /distinct_id/i, /_rst/i, /limit/i, /credit/i];
  1887.            for (let i = localStorage.length - 1; i >= 0; i--) {
  1888.                const k = localStorage.key(i);
  1889.                if (k && !k.startsWith('xai_api_') && patterns.some(p => p.test(k))) localStorage.removeItem(k);
  1890.            }
  1891.            document.cookie.split(';').forEach(c => {
  1892.                const name = c.split('=')[0]?.trim();
  1893.                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 : ''}`; });
  1894.                });
  1895.            });
  1896.            const u = new URL(location.href);
  1897.            ['_r', '_rst'].forEach(k => u.searchParams.set(k, Date.now()));
  1898.            location.replace(u.toString());
  1899.        });
  1900.    }
  1901.  
  1902.    // ─── ASYNC PROCESSORS & RENDERERS ─────────────────────────────────────────
  1903.    async function processAndRenderImage(imgObj, originalPrompt, num, total, galleryEl) {
  1904.        if (!imgObj.b64_json) {
  1905.            addLog('WARN', 'Image object missing b64_json');
  1906.            return;
  1907.        }
  1908.        const mime = imgObj.mime_type || "image/png";
  1909.        const b64 = imgObj.b64_json;
  1910.        const pngDataUri = `data:${mime};base64,${b64}`;
  1911.  
  1912.        const finalPrompt = imgObj.revised_prompt || originalPrompt;
  1913.        let finalDataUri = pngDataUri;
  1914.        let filename = `Grok 2K - ${makeTimestamp()} - ${num}of${total}.png`;
  1915.  
  1916.        if (!appSettings.saveAsPng) {
  1917.            finalDataUri = await convertToJpegWithExif(pngDataUri, finalPrompt);
  1918.            filename = `Grok 2K - ${makeTimestamp()} - ${num}of${total}.jpg`;
  1919.        }
  1920.  
  1921.        const card = document.createElement('div');
  1922.        card.className = 'current-card';
  1923.        card.innerHTML = `
  1924.            <div class="img-wrapper">
  1925.                <img src="${finalDataUri}" class="zoomable" data-filename="${filename}" title="Click to view fullscreen">
  1926.                <a href="${finalDataUri}" download="${filename}" class="overlay-btn overlay-dl-btn" title="Download Full File">${DOWNLOAD_ICON}</a>
  1927.                <button class="overlay-btn overlay-edit-btn" title="Edit this Image">${EDIT_ICON}</button>
  1928.            </div>
  1929.            <p style="padding: 10px; margin: 0; font-size: 12px; color: #cbd5e1; border-top: 1px solid #334155;">${finalPrompt}</p>
  1930.        `;
  1931.  
  1932.        const editBtn = card.querySelector('.overlay-edit-btn');
  1933.        editBtn.addEventListener('click', (e) => {
  1934.            e.preventDefault();
  1935.            if (typeof updateUIForEditMedia === 'function') updateUIForEditMedia(finalDataUri, false, filename);
  1936.        });
  1937.  
  1938.        galleryEl.prepend(card);
  1939.        addLog('INFO', `Rendered image ${num}/${total}`, { filename, finalPrompt });
  1940.    }
  1941.  
  1942.    function renderVideoToGallery(videoUrl, originalPrompt, galleryEl) {
  1943.        addLog('INFO', 'Rendering video to gallery', { url: videoUrl });
  1944.        const filename = `Grok Video - ${makeTimestamp()}.mp4`;
  1945.        const auto = appSettings.videoAutoplay ? "autoplay" : "";
  1946.        const muted = appSettings.videoMuted ? "muted" : "";
  1947.  
  1948.        const card = document.createElement('div');
  1949.        card.className = 'current-card';
  1950.        card.innerHTML = `
  1951.            <div class="img-wrapper">
  1952.                <video src="${videoUrl}" controls ${auto} ${muted} loop style="width: 100%; display: block; max-height: 50vh; object-fit: contain; background: #000;"></video>
  1953.                <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>
  1954.                <button class="overlay-btn overlay-edit-btn" style="z-index: 50; right: 40px;" title="Edit this Video">${EDIT_ICON}</button>
  1955.            </div>
  1956.            <p style="padding: 10px; margin: 0; font-size: 12px; color: #cbd5e1; border-top: 1px solid #334155;">${originalPrompt}</p>
  1957.        `;
  1958.  
  1959.        const editBtn = card.querySelector('.overlay-edit-btn');
  1960.        editBtn.addEventListener('click', (e) => {
  1961.            e.preventDefault();
  1962.            if (typeof updateUIForEditMedia === 'function') updateUIForEditMedia(videoUrl, true, filename);
  1963.        });
  1964.  
  1965.        const dlBtn = card.querySelector('.overlay-dl-btn');
  1966.        dlBtn.addEventListener('click', (e) => {
  1967.            e.preventDefault();
  1968.            const url = dlBtn.getAttribute('data-url');
  1969.            const fname = dlBtn.getAttribute('data-filename');
  1970.            const originalIcon = dlBtn.innerHTML;
  1971.  
  1972.            dlBtn.innerHTML = '<div class="loading-spinner" style="border-top-color: #10b981;"></div>';
  1973.            dlBtn.style.pointerEvents = 'none';
  1974.            addLog('UI', 'Downloading video MP4', url);
  1975.  
  1976.            if (typeof GM_xmlhttpRequest !== "undefined") {
  1977.                GM_xmlhttpRequest({
  1978.                    method: 'GET',
  1979.                    url: url,
  1980.                    responseType: 'blob',
  1981.                    onload: function(response) {
  1982.                        if (response.status >= 200 && response.status < 300) {
  1983.                            const blobUrl = URL.createObjectURL(response.response);
  1984.                            const a = document.createElement('a');
  1985.                            a.style.display = 'none';
  1986.                            a.href = blobUrl;
  1987.                            a.download = fname;
  1988.                            document.body.appendChild(a);
  1989.                            a.click();
  1990.                            document.body.removeChild(a);
  1991.                            setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
  1992.                            addLog('INFO', 'Video downloaded via GM_xmlhttpRequest');
  1993.                        } else {
  1994.                            window.open(url, '_blank');
  1995.                            addLog('WARN', 'GM_xmlhttpRequest returned non-200, opening in new tab', response.status);
  1996.                        }
  1997.                        dlBtn.innerHTML = originalIcon;
  1998.                        dlBtn.style.pointerEvents = 'auto';
  1999.                    },
  2000.                    onerror: function(err) {
  2001.                        window.open(url, '_blank');
  2002.                        addLog('ERROR', 'GM_xmlhttpRequest failed, opening in new tab', err);
  2003.                        dlBtn.innerHTML = originalIcon;
  2004.                        dlBtn.style.pointerEvents = 'auto';
  2005.                    }
  2006.                });
  2007.            } else {
  2008.                window.open(url, '_blank');
  2009.                dlBtn.innerHTML = originalIcon;
  2010.                dlBtn.style.pointerEvents = 'auto';
  2011.            }
  2012.        });
  2013.  
  2014.        galleryEl.prepend(card);
  2015.    }
  2016.  
  2017.    if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initApp); }
  2018.    else { initApp(); }
  2019.  
  2020. })();
Add Comment
Please, Sign In to add comment