Guest User

Untitled

a guest
Apr 21st, 2026
164
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
JavaScript 143.07 KB | Software | 0 0
  1. // ==UserScript==
  2. // @name         xAI Pro Studio Frontend (v39.4 - Configurable Memory Limit)
  3. // @version      39.4
  4. // @description  Full-Screen UI, Exif Fix, Multi-Image, Auto-Retry, Export Settings, Video Warnings, Quick-Action Sliders, Adaptive AR, Auto-Download, Extend Support, Anti-Throttling, Auto-Resume + MEMORY LEAK PROTECTION (настраиваемый лимит батчей)
  5. // @match        https://console.x.ai/playground/imagine*
  6. // @match        https://console.x.ai/team/*/imagine*
  7. // @grant        GM_xmlhttpRequest
  8. // @grant        GM_cookie
  9. // @grant        GM_download
  10. // @grant        GM_info
  11. // @connect      vidgen.x.ai
  12. // @run-at       document-start
  13. // ==/UserScript==
  14.  
  15. (function() {
  16.     'use strict';
  17.  
  18.     // ─── LOGGING & DEBUG ENGINE ───────────────────────────────────────────────
  19.     const appLogs = [];
  20.     const MAX_LOGS = 500;
  21.  
  22.     function stripHeavyData(val) {
  23.         if (val === null || val === undefined) return val;
  24.         if (typeof val === 'string') {
  25.             if (val.startsWith('data:image') || val.startsWith('data:video') || val.length > 2000) {
  26.                 return `[TRUNCATED_DATA_LEN_${val.length}]`;
  27.             }
  28.             return val;
  29.         }
  30.         if (typeof val !== 'object') return val;
  31.         if (Array.isArray(val)) return val.map(stripHeavyData);
  32.         const copy = {};
  33.         for (const k in val) {
  34.             if (Object.prototype.hasOwnProperty.call(val, k)) {
  35.                 copy[k] = stripHeavyData(val[k]);
  36.             }
  37.         }
  38.         return copy;
  39.     }
  40.  
  41.     function addLog(level, msg, data = null) {
  42.         try {
  43.             const ts = new Date().toISOString().replace('T', ' ').substring(0, 23);
  44.             const safeData = data ? stripHeavyData(data) : null;
  45.             appLogs.push({ ts, level, msg, data: safeData });
  46.             if (appLogs.length > MAX_LOGS) appLogs.shift();
  47.  
  48.             const consoleMsg = `[xAI Studio] [${level}] ${msg}`;
  49.             if (level === 'ERROR') console.error(consoleMsg, safeData || '');
  50.             else if (level === 'WARN') console.warn(consoleMsg, safeData || '');
  51.             else console.log(consoleMsg, safeData || '');
  52.         } catch(e) {}
  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 friendly format: YYYY-MM-DD HH:MM:SS
  74.         return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
  75.     }
  76.     function base64ToBlobUrl(b64) {
  77.         if (!b64 || !b64.startsWith('data:')) return b64;
  78.         try {
  79.             const parts = b64.split(',');
  80.             const mime = parts[0].match(/:(.*?);/)[1];
  81.             const bstr = atob(parts[1]);
  82.             let n = bstr.length;
  83.             const u8arr = new Uint8Array(n);
  84.             while (n--) u8arr[n] = bstr.charCodeAt(n);
  85.             return URL.createObjectURL(new Blob([u8arr], { type: mime }));
  86.         } catch(e) { return b64; }
  87.     }
  88.     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>`;
  89.     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>`;
  90.     const EXTEND_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>`;
  91.     const VIDEO_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="m22 8-6 4 6 4V8Z"/><rect width="14" height="12" x="2" y="6" rx="2" ry="2"/></svg>`;
  92.  
  93.     // ─── SETTINGS SCHEMA & MANAGER ───────────────────────────────────────────
  94.     const SETTINGS_SCHEMA = {
  95.         retries:             { id: 'set-retries',               type: 'number',   default: 20 },
  96.         delayMin:            { id: 'set-delay-min',             type: 'number',   default: 2000 },
  97.         delayMax:            { id: 'set-delay-max',             type: 'number',   default: 4000 },
  98.         rateLimitDelay:      { id: 'set-rate-limit-delay',      type: 'number',   default: 20 },
  99.         notifications:       { id: 'set-notifications',         type: 'checkbox', default: true },
  100.         customCSS:           { id: 'set-custom-css',            type: 'text',     default: "" },
  101.         saveAsPng:           { id: 'set-save-png',              type: 'checkbox', default: false },
  102.         apiBaseUrl:          { id: 'set-api-base-url',          type: 'text',     default: "" },
  103.         videoPollTimeout:    { id: 'set-video-timeout',         type: 'number',   default: 300 },
  104.         autoRetryStuckVideo: { id: 'set-auto-retry-video',      type: 'checkbox', default: false },
  105.         breakLoopOnFailure:  { id: 'set-break-loop-on-failure', type:'checkbox', default: true },
  106.         referenceARTrick:    { id: 'set-reference-ar-trick',    type: 'checkbox', default: true },
  107.         maximumGreedMode:    { id: 'set-maximum-greed-mode',    type: 'checkbox', default: false },
  108.         suppressEditWarning: { id: 'set-suppress-warning',      type: 'checkbox', default: false },
  109.         videoAutoplay:       { id: 'set-video-autoplay',        type: 'checkbox', default: true },
  110.         videoMuted:          { id: 'set-video-muted',           type: 'checkbox', default: false },
  111.         uiFontSize:          { id: 'set-font-size',             type: 'number',   default: 13 },
  112.         autoDownload:        { id: 'set-auto-download',         type: 'checkbox', default: false },
  113.         antiThrottling:      { id: 'set-anti-throttling',       type: 'checkbox', default: true },
  114.         maxGalleryBatches:   { id: 'set-max-batches',           type: 'number',   default: 8 }
  115.     };
  116.  
  117.     const SettingsManager = {
  118.         load: function() {
  119.             let stored = JSON.parse(localStorage.getItem('xai_api_settings') || '{}');
  120.             let parsed = {};
  121.             for (const [key, config] of Object.entries(SETTINGS_SCHEMA)) {
  122.                 let val = stored[key] !== undefined ? stored[key] : config.default;
  123.                 if (config.type === 'number') val = parseInt(val) || config.default;
  124.                 parsed[key] = val;
  125.             }
  126.             parsed.delayMax = Math.max(parsed.delayMin, parsed.delayMax);
  127.             return parsed;
  128.         },
  129.         populateUI: function(currentSettings) {
  130.             for (const [key, config] of Object.entries(SETTINGS_SCHEMA)) {
  131.                 const el = document.getElementById(config.id);
  132.                 if (!el) continue;
  133.                 if (config.type === 'checkbox') el.checked = currentSettings[key];
  134.                 else el.value = currentSettings[key];
  135.             }
  136.         },
  137.         saveFromUI: function() {
  138.             let newSettings = {};
  139.             for (const [key, config] of Object.entries(SETTINGS_SCHEMA)) {
  140.                 const el = document.getElementById(config.id);
  141.                 if (!el) { newSettings[key] = config.default; continue; }
  142.  
  143.                 if (config.type === 'checkbox') {
  144.                     newSettings[key] = el.checked;
  145.                 } else if (config.type === 'number') {
  146.                     newSettings[key] = parseInt(el.value) || config.default;
  147.                 } else {
  148.                     newSettings[key] = el.value.trim();
  149.                 }
  150.             }
  151.             newSettings.delayMax = Math.max(newSettings.delayMin, newSettings.delayMax);
  152.             localStorage.setItem('xai_api_settings', JSON.stringify(newSettings));
  153.             return newSettings;
  154.         }
  155.     };
  156.  
  157.     let appSettings = SettingsManager.load();
  158.     let favoritePrompts = JSON.parse(localStorage.getItem('xai_api_favs') || '[]');
  159.  
  160.     function getApiUrl(path) {
  161.         let base = appSettings.apiBaseUrl || '';
  162.         if (base.endsWith('/')) base = base.slice(0, -1);
  163.         if (!path.startsWith('/') && base) path = '/' + path;
  164.         return base + path;
  165.     }
  166.  
  167.     function parseDynamicPrompt(text) {
  168.         if (!text) return text;
  169.         let parsed = text; let prev;
  170.         do {
  171.             prev = parsed;
  172.             parsed = parsed.replace(/\{([^{}]+)\}/g, (m, c) => { const o = c.split('|'); return o[Math.floor(Math.random() * o.length)]; });
  173.         } while (parsed !== prev);
  174.         parsed = parsed.replace(/@randomseed/gi, () => Math.floor(1000000000 + Math.random() * 9000000000).toString());
  175.         return parsed.trim();
  176.     }
  177.  
  178.     let sharedAudioCtx = null;
  179.     let awakeOsc = null;
  180.     let titleBlinkInterval = null;
  181.     const originalTitle = document.title || "xAI Pro Studio";
  182.  
  183.     function initAudio() {
  184.         if (!sharedAudioCtx) {
  185.             try { sharedAudioCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch(e) {}
  186.         }
  187.         if (sharedAudioCtx && sharedAudioCtx.state === 'suspended') sharedAudioCtx.resume();
  188.     }
  189.  
  190.     function toggleKeepAwake(enable) {
  191.         if (enable && !appSettings.antiThrottling) return;
  192.         if (enable) {
  193.             initAudio();
  194.             if (sharedAudioCtx) {
  195.                 if (!awakeOsc) {
  196.                     try {
  197.                         //Сделай звук активности вкладки типа 21 кГц -90 дБ.
  198.                         awakeOsc = sharedAudioCtx.createOscillator();
  199.                         awakeOsc.frequency.value = 21000;
  200.                         awakeOsc.type = 'sine';
  201.                         const gain = sharedAudioCtx.createGain();
  202.                         //gain.gain.value = 0.001;
  203.                         gain.gain.value = 0.000031622776601683795; //Сделай звук активности вкладки типа 21 кГц -90 дБ.
  204.                         awakeOsc.connect(gain);
  205.                         gain.connect(sharedAudioCtx.destination);
  206.                         awakeOsc.start();
  207.                     } catch(e) {}
  208.                 }
  209.             }
  210.         } else {
  211.             if (awakeOsc) {
  212.                 try { awakeOsc.stop(); awakeOsc.disconnect(); } catch(e) {}
  213.                 awakeOsc = null;
  214.             }
  215.         }
  216.     }
  217.  
  218.     function notifyUser(isError = false) {
  219.         if (!appSettings.notifications) return;
  220.         initAudio();
  221.         if (sharedAudioCtx) {
  222.             try {
  223.                 const osc = sharedAudioCtx.createOscillator(); const gain = sharedAudioCtx.createGain();
  224.                 osc.connect(gain); gain.connect(sharedAudioCtx.destination);
  225.                 if (isError) {
  226.                     osc.type = 'sawtooth'; osc.frequency.setValueAtTime(300, sharedAudioCtx.currentTime); osc.frequency.exponentialRampToValueAtTime(100, sharedAudioCtx.currentTime + 0.3);
  227.                 } else {
  228.                     osc.type = 'sine'; osc.frequency.setValueAtTime(500, sharedAudioCtx.currentTime); osc.frequency.exponentialRampToValueAtTime(1000, sharedAudioCtx.currentTime + 0.2);
  229.                 }
  230.                 gain.gain.setValueAtTime(0.1, sharedAudioCtx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, sharedAudioCtx.currentTime + 0.5);
  231.                 osc.start(sharedAudioCtx.currentTime); osc.stop(sharedAudioCtx.currentTime + 0.5);
  232.             } catch(e) {}
  233.         }
  234.         if (!document.hasFocus()) {
  235.             if (titleBlinkInterval) clearInterval(titleBlinkInterval);
  236.             let toggle = true;
  237.             titleBlinkInterval = setInterval(() => { document.title = toggle ? (isError ? "❌ FAILED" : "✅ DONE") : originalTitle; toggle = !toggle; }, 1000);
  238.             const clearBlink = () => { clearInterval(titleBlinkInterval); document.title = originalTitle; window.removeEventListener('focus', clearBlink); };
  239.             window.addEventListener('focus', clearBlink);
  240.         }
  241.     }
  242.  
  243.     async function nuclearSessionReset(remainingLoops = 0) {
  244.         addLog('INFO', `Nuclear Reset triggered.`);
  245.  
  246.         if (typeof GM_cookie !== 'undefined') {
  247.             await new Promise(resolve => {
  248.                 GM_cookie.list({ url: window.location.href }, (cookies, error) => {
  249.                     if (!error && cookies && cookies.length > 0) {
  250.                         let deletedCount = 0;
  251.                         cookies.forEach(c => {
  252.                             GM_cookie.delete({ url: window.location.href, name: c.name }, () => {
  253.                                 deletedCount++;
  254.                                 if (deletedCount === cookies.length) resolve();
  255.                             });
  256.                         });
  257.                     } else resolve();
  258.                 });
  259.             });
  260.             await new Promise(resolve => {
  261.                 GM_cookie.list({ url: 'https://x.ai' }, (cookies, error) => {
  262.                     if (!error && cookies && cookies.length > 0) {
  263.                         let deletedCount = 0;
  264.                         cookies.forEach(c => {
  265.                             GM_cookie.delete({ url: 'https://x.ai', name: c.name }, () => {
  266.                                 deletedCount++;
  267.                                 if (deletedCount === cookies.length) resolve();
  268.                             });
  269.                         });
  270.                     } else resolve();
  271.                 });
  272.             });
  273.         }
  274.  
  275.         for (let i = localStorage.length - 1; i >= 0; i--) {
  276.             const k = localStorage.key(i);
  277.             if (k && !k.startsWith('xai_api_')) localStorage.removeItem(k);
  278.         }
  279.         sessionStorage.clear();
  280.  
  281.         if ( appSettings.maximumGreedMode && remainingLoops > 0) {
  282.             sessionStorage.setItem('xai_resume_loops', remainingLoops.toString());
  283.         } else {
  284.             await fetch('https://console.x.ai/playground/imagine');
  285.         }
  286.     }
  287.  
  288.     let piexifPromise = null;
  289.     function loadPiexif() {
  290.         if (piexifPromise) return piexifPromise;
  291.         piexifPromise = new Promise((resolve, reject) => {
  292.             const getPiexif = () => window.piexif || (typeof unsafeWindow !== 'undefined' ? unsafeWindow.piexif : null);
  293.             let px = getPiexif();
  294.             if (px) { resolve(px); return; }
  295.             const script = document.createElement('script');
  296.             script.src = 'https://cdnjs.cloudflare.com/ajax/libs/piexifjs/1.0.6/piexif.min.js';
  297.             script.onload = () => { px = getPiexif(); if (px) resolve(px); else { piexifPromise = null; reject(new Error("piexif missing")); } };
  298.             script.onerror = () => { piexifPromise = null; reject(new Error('Failed to load piexifjs')); };
  299.             document.head.appendChild(script);
  300.         });
  301.         return piexifPromise;
  302.     }
  303.  
  304.     function convertToJpegWithExif(base64PngUri, prompt) {
  305.         return new Promise((resolve, reject) => {
  306.             const img = new Image();
  307.             img.onload = async () => {
  308.                 const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height;
  309.                 const ctx = canvas.getContext('2d'); ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.drawImage(img, 0, 0);
  310.                 let jpegUri = canvas.toDataURL('image/jpeg', 0.95);
  311.                 if (prompt) {
  312.                     try {
  313.                         const piexif = await loadPiexif();
  314.                         const exifObj = {"0th": {}, "Exif": {}, "GPS": {}, "1st": {}, "Interop": {}};
  315.                         exifObj['0th'][piexif.ImageIFD.ImageDescription] = unescape(encodeURIComponent(prompt));
  316.                         jpegUri = piexif.insert(piexif.dump(exifObj), jpegUri);
  317.                     } catch (e) { addLog('ERROR', 'EXIF Injection Failed', e); }
  318.                 }
  319.                 resolve(jpegUri);
  320.             };
  321.             img.onerror = reject; img.src = base64PngUri;
  322.         });
  323.     }
  324.  
  325.     async function extractPromptFromImage(file) {
  326.         if (!file || !file.type.includes('image')) return null;
  327.         try {
  328.             const piexif = await loadPiexif();
  329.             return new Promise((resolve) => {
  330.                 const reader = new FileReader();
  331.                 reader.onload = (e) => {
  332.                     try {
  333.                         const exifData = piexif.load(e.target.result);
  334.                         let prompt = exifData['0th'] && exifData['0th'][piexif.ImageIFD.ImageDescription];
  335.                         if (Array.isArray(prompt)) prompt = String.fromCharCode.apply(null, prompt).replace(/\0/g, '');
  336.                         if (prompt) { try { prompt = decodeURIComponent(escape(prompt)); } catch(err) {} }
  337.                         if (!prompt && exifData['0th'] && exifData['0th'][40091]) {
  338.                             const xpTitleArr = exifData['0th'][40091]; let str = '';
  339.                             for (let i = 0; i < xpTitleArr.length; i += 2) {
  340.                                 const charCode = xpTitleArr[i] | (xpTitleArr[i+1] << 8);
  341.                                 if (charCode === 0) break;
  342.                                 str += String.fromCharCode(charCode);
  343.                             }
  344.                             prompt = str;
  345.                         }
  346.                         resolve(prompt ? prompt.trim() : null);
  347.                     } catch (err) { resolve(null); }
  348.                 };
  349.                 reader.onerror = () => resolve(null);
  350.                 reader.readAsDataURL(file);
  351.             });
  352.         } catch (e) { return null; }
  353.     }
  354.  
  355.     function generateVideoThumbnail(file) {
  356.         return new Promise((resolve) => {
  357.             const video = document.createElement('video'); video.preload = 'metadata'; video.muted = true; video.playsInline = true;
  358.             const url = URL.createObjectURL(file); video.src = url; let isSeeked = false;
  359.             video.onloadeddata = () => { video.currentTime = Math.min(0.5, video.duration / 2 || 0); file._w = video.videoWidth; file._h = video.videoHeight; };
  360.             video.onseeked = () => {
  361.                 if (isSeeked) return; isSeeked = true;
  362.                 try {
  363.                     const canvas = document.createElement('canvas'); canvas.width = video.videoWidth || 140; canvas.height = video.videoHeight || 140;
  364.                     const ctx = canvas.getContext('2d'); ctx.drawImage(video, 0, 0, canvas.width, canvas.height); URL.revokeObjectURL(url); resolve(canvas.toDataURL('image/jpeg', 0.8));
  365.                 } catch(e) { URL.revokeObjectURL(url); resolve(null); }
  366.             };
  367.             video.onerror = () => { URL.revokeObjectURL(url); resolve(null); };
  368.             setTimeout(() => { if (!isSeeked) { URL.revokeObjectURL(url); resolve(null); } }, 2000);
  369.         });
  370.     }
  371.  
  372.     function updateFontSize(size) {
  373.         let el = document.getElementById('xai-custom-font-size');
  374.         if (!el) { el = document.createElement('style'); el.id = 'xai-custom-font-size'; document.head.appendChild(el); }
  375.         if (size === 13) { el.innerHTML = ''; return; }
  376.  
  377.         el.innerHTML = `
  378.             #xai-pro-app textarea, #xai-pro-app #prompt-backdrop, #xai-pro-app select, #xai-pro-app input[type="text"], #xai-pro-app input[type="number"], #xai-pro-app .btn-primary, #xai-pro-app .btn-secondary, #xai-pro-app .ar-select-box, #xai-pro-app .action-select { font-size: ${size}px !important; }
  379.             #xai-pro-app .ar-option, #xai-pro-app .fav-text, #xai-pro-app #xai-api-status { font-size: ${size - 1}px !important; }
  380.             #xai-pro-app .panel-title, #xai-pro-app label, #xai-pro-app .toggle-btn, #xai-pro-app .slider-header, #xai-pro-app .warning-box, #xai-pro-app .ref-info span { font-size: ${size - 2}px !important; }
  381.             #xai-pro-app .slider-ticks, #xai-pro-app .slider-ticks span, #xai-pro-app .ref-action-btn, #xai-pro-app .ref-toggle-label, #xai-pro-app .ref-thumb span, #xai-pro-app button[style*="font-size: 10px"] { font-size: ${size - 3}px !important; }
  382.             #xai-pro-app .ref-handle { font-size: ${size + 1}px !important; }
  383.             #xai-pro-app h3 { font-size: ${size + 2}px !important; }
  384.         `;
  385.     }
  386.  
  387.     const MAPS = {
  388.         batch: [1, 2, 3, 4, 5, 8, 10],
  389.         durGen: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
  390.         durExt: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  391.     };
  392.  
  393.     const arData =[
  394.         { label: 'Auto', w: 1, h: 1, isAuto: true }, { label: 'By First Ref', w: 1, h: 1, isAuto: true, isRef: true },
  395.         { label: '1:1', w: 1, h: 1, imageOnly: true }, { label: '1:1', w: 1, h: 1, imageOnly: true },
  396.         { label: '3:4', w: 3, h: 4 }, { label: '4:3', w: 4, h: 3 },
  397.         { label: '9:16', w: 9, h: 16 }, { label: '16:9', w: 16, h: 9 },
  398.         { label: '2:3', w: 2, h: 3 }, { label: '3:2', w: 3, h: 2 },
  399.         { label: '9:19.5', w: 9, h: 19.5, imageOnly: true }, { label: '19.5:9', w: 19.5, h: 9, imageOnly: true },
  400.         { label: '9:20', w: 9, h: 20, imageOnly: true }, { label: '20:9', w: 20, h: 9, imageOnly: true },
  401.         { label: '1:2', w: 1, h: 2, imageOnly: true }, { label: '2:1', w: 2, h: 1, imageOnly: true }
  402.     ];
  403.  
  404.     function createArIcon(w, h, isAuto, isRef) {
  405.         if (isRef) return `<div style="width: 12px; height: 12px; border: 1px dashed #10b981; border-radius: 2px; display: flex; align-items: center; justify-content: center; font-size: 8px; color: #10b981; font-weight:bold;">R</div>`;
  406.         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>`;
  407.         const scale = 12 / Math.max(w, h);
  408.         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>`;
  409.     }
  410.  
  411.     // ─── UI INJECTION ─────────────────────────────────────────────────────────
  412.     const UI_CSS = `
  413.         ::-webkit-scrollbar { width: 6px; }
  414.         ::-webkit-scrollbar-track { background: transparent; }
  415.         ::-webkit-scrollbar-thumb { background: #475569; border-radius: 6px; }
  416.  
  417.         @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
  418.         @keyframes pulse { 0% { opacity: 0.5; } 50% { opacity: 1; } 100% { opacity: 0.5; } }
  419.  
  420.         .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; }
  421.         .btn-content { display: flex; align-items: center; justify-content: center; gap: 6px; }
  422.         .status-pulsing { animation: pulse 2s ease-in-out infinite; color: #3b82f6 !important; font-weight: 600; }
  423.  
  424.         #xai-pro-app { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 9999999; background: #0f172a; font-family: 'Inter', -apple-system, sans-serif; color: #e2e8f0; display: flex; box-sizing: border-box; overflow: hidden; }
  425.         #xai-pro-app * { box-sizing: border-box; }
  426.  
  427.         .panel {
  428.             background: #1e293b;
  429.             border-radius: 8px;
  430.             border: 1px solid rgba(255, 255, 255, 0.08);
  431.             box-shadow: none; /* Removed shadow */
  432.             display: flex; flex-direction: column; overflow: hidden;
  433.         }
  434.         .panel-title { font-size: 11px; font-weight: 700; color: #94a3b8; letter-spacing: 0.5px; padding: 10px; border-bottom: 1px solid rgba(255, 255, 255, 0.05);  text-transform: uppercase; flex-shrink: 0; }
  435.  
  436.         .col-left {
  437.             width: 310px; min-width: 320px; display: flex; flex-direction: column; flex-shrink: 0;
  438.             transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1), min-width 0.3s, opacity 0.2s;
  439.             overflow: hidden; opacity: 1;
  440.             padding: 10px 0 10px 10px;
  441.         }
  442.         .col-left.collapsed { width: 0; min-width: 0; opacity: 0; pointer-events: none; padding: 10px 0; }
  443.         .sidebar-inner { width: 310px; display: flex; flex-direction: column; gap: 10px; height: 100%; overflow-y: hidden; }
  444.  
  445.         #toggle-sidebar-btn {
  446.             display: flex; align-items: center; justify-content: center; width: 22px; height: 50px;
  447.             background: #1e293b; border: 1px solid #334155; border-left: none; border-radius: 0 6px 6px 0;
  448.             cursor: pointer; z-index: 101; color: #94a3b8; font-size: 10px; transition: 0.2s; margin-top: 15px; box-shadow: 2px 0 5px rgba(0,0,0,0.2);
  449.         }
  450.         #toggle-sidebar-btn:hover { background: #334155; color: #f8fafc; }
  451.  
  452.         .col-center { position: relative; flex: 1; display: flex; flex-direction: column; overflow: hidden; padding: 10px; gap: 10px; }
  453.  
  454.         .scrollable-main { flex: 1; overflow-y: auto; padding-bottom: 220px; display: flex; flex-direction: column; gap: 20px; }
  455.  
  456.         .frosted-panel {
  457.             position: absolute; bottom: 10px; left: 10px; right: 10px; z-index: 100;
  458.             margin: 0 auto; max-width: 1024px; border-radius: 10px;
  459.             background: rgba(15, 23, 42, 0.6) !important;
  460.             backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
  461.             border: 1px solid rgba(255, 255, 255, 0.05);
  462.             padding: 12px; flex-shrink: 0;
  463.             box-shadow: 0 20px 40px rgba(0,0,0,0.3);
  464.         }
  465.  
  466.          textarea, select, input[type="text"], input[type="number"], input[type="checkbox"] {
  467.             background: rgba(0, 0, 0, 0.2);
  468.             border: 1px solid transparent;
  469.             border-radius: 6px;
  470.             padding: 8px; color: #f8fafc; font-size: 13px; outline: none; transition: 0.2s;
  471.         }
  472.         textarea, select, input[type="text"], input[type="number"] {
  473.             width: 100%; /* Restores full width in modals */
  474.             box-sizing: border-box;
  475.         }
  476.         textarea:focus, select:focus, input[type="text"]:focus, input[type="number"]:focus {
  477.             border-color: rgba(99, 102, 241, 0.5);
  478.             background: rgba(0, 0, 0, 0.3);
  479.             box-shadow: none;
  480.         }
  481.         label { font-size: 11px; font-weight: 600; color: #94a3b8; margin-bottom: 4px; display: block; }
  482.  
  483.         .btn-primary { background: #4f46e5; 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; }
  484.         .btn-primary:hover:not(:disabled) { background: #4338ca; }
  485.         .btn-primary:disabled { opacity: 0.8; cursor: not-allowed; }
  486.         .btn-secondary { background: transparent; color: #94a3b8; border: 1px solid transparent; 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; }
  487.         .btn-secondary:hover:not(:disabled) { background: rgba(255, 255, 255, 0.05);  }
  488.  
  489.         .action-select { width: 100%; font-weight: 600; font-size: 13px; color: #f8fafc; background: transparent; border: none; outline: none; overflow-y: hidden; }
  490.         .action-select option { padding: 8px 10px; margin-bottom: cursor: pointer; transition: 0.1s; background: #0f172a; }
  491.         .action-select option:checked { background: linear-gradient(180deg, #6366f1 0%, #4f46e5 100%); border-color: #4f46e5; color: #fff; box-shadow: 0 2px 4px rgba(0,0,0,0.3); }
  492.         .action-select option:hover:not(:checked) { background: #334155; color: #f8fafc; }
  493.  
  494.         .slider-block { display: flex; flex-direction: column; margin-bottom: 12px; }
  495.         .toggle-btn {
  496.             background: rgba(0, 0, 0, 0.2);
  497.             border: 1px solid transparent;
  498.             color: #94a3b8;
  499.             box-shadow: none;
  500.         }
  501.         .toggle-btn.active {
  502.             background: #10b981; /* Solid flat green */
  503.             color: #fff; border-color: transparent;
  504.             box-shadow: none;
  505.         }
  506.         .slider-header { display: flex; justify-content: space-between; font-size: 11px; margin-bottom: 6px; font-weight: 600; color: #94a3b8; }
  507.         .slider-header span { color: #f8fafc; }
  508.         .slider-ticks { position: relative; height: 14px; width: 100%; font-size: 10px; color: #64748b; margin-top: 4px; padding: 0; }
  509.         .slider-ticks span { position: absolute; transform: translateX(-50%); text-align: center; white-space: nowrap; line-height: 1; }
  510.  
  511.         input[type=range] { -webkit-appearance: none; width: 100%; background: transparent; margin: 4px 0; outline: none; transform: scaleY(1.6); }
  512.         input[type=range]::-webkit-slider-runnable-track {
  513.             background: rgba(255, 255, 255, 0.1);
  514.             box-shadow: none; /* Remove inset shadow */
  515.         }
  516.         input[type=range]::-moz-range-track { width: 100%; height: 6px; cursor: pointer; background: #334155; border-radius: 3px; box-shadow: inset 0 1px 3px rgba(0,0,0,0.6); }
  517.         input[type=range]::-webkit-slider-thumb {
  518.             background: #10b981;
  519.             box-shadow: none; /* Remove drop shadow */
  520.         }
  521.         input[type=range]::-moz-range-thumb { height: 16px; width: 16px; border-radius: 50%; background: #10b981; cursor: pointer; border: none; box-shadow: 0 2px 5px rgba(0,0,0,0.6); }
  522.         input[type=range]:focus::-webkit-slider-thumb { box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.4); }
  523.         input[type=range]:focus::-moz-range-thumb { box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.4); }
  524.  
  525.         .ar-select-box { background: #1e293b; border: 1px solid #475569; border-radius: 6px; padding: 8px; display: flex; align-items: center; gap: 8px; cursor: pointer; color: #f8fafc; font-size: 13px; }
  526.         #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: 265px; overflow-y: auto; display: none; z-index: 99999999; grid-template-columns: 1fr 1fr; }
  527.         .ar-option { display: flex; align-items: center; gap: 8px; padding: 6px 8px; cursor: pointer; font-size: 12px; color: #cbd5e1; }
  528.         .ar-option:hover { background: #334155; color: #f8fafc; }
  529.  
  530.         .img-wrapper { position: relative; display: block; width: 100%; background: #0f172a; border-radius: 7px; overflow: hidden; }
  531.         .zoomable { cursor: zoom-in; transition: transform 0.2s; width: 100%; display: block; }
  532.         .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: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); opacity: 0.75; z-index: 50; cursor: pointer; border: none; padding: 0; }
  533.         .overlay-btn:hover { background: rgba(15, 23, 42, 0.9); opacity: 1; transform: scale(1.05); }
  534.         .overlay-dl-btn { right: 6px; }
  535.         .overlay-edit-btn { right: 40px; color: #10b981; }
  536.         .overlay-extend-btn { right: 74px; color: #3b82f6; }
  537.         .overlay-video-btn { right: 74px; color: #3b82f6; }
  538.         .current-card .overlay-btn { opacity: 0; visibility: hidden; transform: translateY(-5px); pointer-events: none; }
  539.         .current-card:hover .overlay-btn { opacity: 0.75; visibility: visible; transform: translateY(0); pointer-events: auto; }
  540.         .current-card .overlay-btn:hover { opacity: 1; transform: scale(1.1); background: rgba(15, 23, 42, 0.95); }
  541.  
  542.         .ref-item { position: relative; display: flex; align-items: center; gap: 10px; padding: 2px; background: #1e293b; border: 1px solid rgba(255,255,255,0.05); border-radius: 6px; margin-bottom: 6px; cursor: grab; transition: 0.1s; }
  543.         .ref-item.inactive { opacity: 0.6; filter: grayscale(0.8); }
  544.         .ref-item:active { cursor: grabbing; }
  545.         .ref-item.dragging { opacity: 0.4; }
  546.         .ref-close-btn { position: absolute; top: 6px; right: 6px; background: transparent; border: none; color: #94a3b8; font-size: 16px; width: 22px; height: 22px; display: flex; align-items: center; justify-content: center; cursor: pointer; border-radius: 4px; transition: 0.2s; line-height: 1; padding: 0; z-index: 10; }
  547.         .ref-close-btn:hover { background: rgba(239, 68, 68, 0.15); color: #ef4444; }
  548.         .ref-handle { font-size: 14px; color: #475569; cursor: grab; padding: 0 4px; }
  549.         .ref-thumb { width: 90px; height: 90px; border-radius: 4px; object-fit: cover; border: 1px solid #334155; background: #0f172a; transition: 0.2s; }
  550.         .ref-info { flex: 1; display: flex; flex-direction: column; justify-content: center; gap: 6px; min-width: 0; }
  551.         .ref-toggle-label { font-size: 11px; color: #cbd5e1; display: flex; align-items: center; gap: 4px; cursor: pointer; user-select: none; }
  552.         .ref-toggle-label input { width: 12px; height: 12px; cursor: pointer; accent-color: #10b981; margin: 0; padding: 0; }
  553.         .ref-btn-group { display: flex; gap: 4px; width: 100%; margin-top: auto; }
  554.         .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; }
  555.         .ref-insert { background: #0f766e; color: #ccfbf1; border: 1px solid #115e59; }
  556.         .ref-insert:hover { background: #115e59; color: #f0fdfa; }
  557.         .ref-del { background: #450a0a; color: #fca5a5; border: 1px solid #7f1d1d; }
  558.         .ref-del:hover { background: #7f1d1d; color: #fee2e2; }
  559.         .drag-over-top { border-top: 2px solid #22c55e !important; }
  560.         .drag-over-bottom { border-bottom: 2px solid #22c55e !important; }
  561.         .drop-panel.drag-over { background: #064e3b; border-color: #10b981; box-shadow: 0 0 0 4px rgba(16,185,129,0.15); }
  562.  
  563.         .batch-container { display: flex; flex-direction: column; background: rgba(15, 23, 42, 0.4); border: 1px solid #334155; border-radius: 10px; padding: 10px; }
  564.         .batch-header { font-size: 12px; color: #cbd5e1; font-weight: 600; padding-bottom: 8px; border-bottom: 1px solid #334155; margin-bottom: 10px; gap: 20px; display: flex;  align-items: center; cursor: pointer; transition: 0.2s;}
  565.         .batch-header:hover { color: #f8fafc; border-bottom-color: #64748b; }
  566.         .batch-grid { display: flex; flex-wrap: wrap; gap: 12px; align-content: start; justify-content: flex-start; }
  567.  
  568.         .current-card { flex: 0 1 380px; max-width: 100%; position: relative; border-radius: 8px; box-shadow: 0 4px 10px rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.05); background: #1e293b; transition: all 0.3s ease; z-index: 1; }
  569.         .current-card:hover { z-index: 10; }
  570.         .current-card.downloaded { border-color: #10b981 !important; box-shadow: 0 0 10px rgba(16,185,129,0.3) !important; }
  571.         .current-card img, .current-card video { display: block; width: 100%; max-height: 50vh; object-fit: contain; background: #0f172a; }
  572.  
  573.         .prompt-dropdown { position: absolute; top: calc(100% + 6px); left: 0; right: 0; background: rgba(30, 41, 59, 0.95); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; box-shadow: 0 15px 35px rgba(0,0,0,0.5); padding: 12px; font-size: 11px; color: #94a3b8; cursor: pointer; opacity: 0; visibility: hidden; transform: translateY(-10px); transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); max-height: 250px; overflow-y: auto; line-height: 1.4; word-wrap: break-word; }
  574.         .current-card:hover .prompt-dropdown { opacity: 1; visibility: visible; transform: translateY(0); }
  575.         .prompt-dropdown:hover { color: #f8fafc; background: rgba(40, 53, 72, 0.95); }
  576.  
  577.         .prompt-drawer { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 0.3s cubic-bezier(0.4, 0, 0.2, 1); background: rgba(0, 0, 0, 0.2); }
  578.         .current-card:hover .prompt-drawer { grid-template-rows: 1fr; }
  579.         .prompt-drawer-inner { overflow: hidden; }
  580.         .prompt-drawer-text { padding: 12px; font-size: 11px; color: #94a3b8; cursor: pointer; border-top: 1px solid rgba(255,255,255,0.05); transition: background 0.2s, color 0.2s; margin: 0; line-height: 1.4; word-wrap: break-word; }
  581.         .prompt-drawer-text:hover { color: #f8fafc; background: rgba(255,255,255,0.05); }
  582.         .prompt-overlay:hover { background: rgba(30, 41, 59, 0.95); }
  583.         .prompt-ref-thumb { width: 32px; height: 32px; border-radius: 4px; object-fit: cover; border: 1px solid rgba(255,255,255,0.1); opacity: 0.5; transition: all 0.2s; user-select: none; }
  584.         .prompt-ref-thumb.glow { opacity: 1; border-color: #10b981; box-shadow: 0 0 10px rgba(16,185,129,0.8); transform: scale(1.15) translateY(-2px); z-index: 10; }
  585.  
  586.         .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); }
  587.         .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; overflow-y: auto; }
  588.         .fav-item { display: flex; justify-content: space-between; align-items: center; background: #0f172a; padding: 8px; border-radius: 6px; border: 1px solid #334155; }
  589.         .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; }
  590.         .fav-text:hover { color: #10b981; }
  591.  
  592.         .top-shortcuts { position: fixed; top: 10px; right: 15px; z-index: 100; display: flex; gap: 10px; background: rgba(0,0,0,0.5); padding: 5px; corner-radius: 5px; }
  593.         .shortcut-btn { font-size: 20px; cursor: pointer; transition: transform 0.2s; opacity: 0.8; }
  594.         .shortcut-btn:hover { transform: scale(1.15); opacity: 1; }
  595.  
  596.         .faq-item { background: #0f172a; border: 1px solid #334155; border-radius: 6px; overflow: hidden; margin-bottom: 8px; flex-shrink: 0; }
  597.         .faq-question { width: 100%; background: #1e293b; color: #f8fafc; border: none; padding: 12px 15px; text-align: left; font-size: 13px; font-weight: 600; cursor: pointer; display: flex; justify-content: space-between; align-items: center; transition: background 0.2s; outline: none; }
  598.         .faq-question:hover { background: #334155; }
  599.         .faq-question::after { content: '▼'; font-size: 10px; color: #94a3b8; transition: transform 0.2s; }
  600.         .faq-question.active::after { transform: rotate(180deg); }
  601.         .faq-answer { max-height: 0; overflow: hidden; transition: max-height 0.3s ease-out; background: #0f172a; }
  602.         .faq-answer p { margin: 0; padding: 12px 15px; font-size: 12px; color: #cbd5e1; line-height: 1.5; border-top: 1px solid #334155; }
  603.  
  604.         #xai-lightbox { cursor: zoom-out; }
  605.         #xai-lightbox-wrapper { position: relative; display: inline-block; max-width: 95vw; max-height: 95vh; }
  606.         #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); }
  607.  
  608.         .prompt-container { position: relative; width: 100%; height: 100px; min-height: 70px; margin-bottom: 10px; resize: vertical; overflow: hidden; border: 1px solid transparent; border-radius: 6px; background: rgba(0, 0, 0, 0.15); transition: 0.2s; box-sizing: border-box; font-family: 'Inter', -apple-system, sans-serif; }
  609.         .prompt-container:focus-within { border-color: rgba(255,255,255,0.1); background: rgba(0,0,0,0.25); box-shadow: none; }
  610.         .prompt-container.prompt-drag-over { border-color: #10b981 !important; background: rgba(6,78,59,0.9) !important; box-shadow: 0 0 0 2px rgba(16,185,129,0.15) !important; }
  611.         #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; }
  612.         .prompt-tag { pointer-events: auto; }
  613.         #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; }
  614.         #xai-api-prompt:focus { background: transparent; border: none; box-shadow: none; }
  615.  
  616.         .warning-box { border-radius: 6px; padding: 6px 10px; font-size: 11px; margin-bottom: 8px; font-weight: 500; display: block; }
  617.         .warning-red { background: #450a0a; color: #fca5a5; border: 1px solid #7f1d1d; }
  618.         .warning-yellow { background: #422006; color: #fcd34d; border: 1px solid #78350f; }
  619.  
  620.         #xai-version-tag { position: fixed; bottom: 12px; left: 14px; font-size: 10px; font-weight: 600; color: #475569; z-index: 100; pointer-events: none; user-select: none; background: rgba(0, 0, 0, 0.2); padding: 2px }
  621.     `;
  622.  
  623.     const UI_HTML = `
  624.         <div id="xai-pro-app">
  625.             <div class="top-shortcuts">
  626.                 <div id="xai-reset-btn" class="shortcut-btn" title="Wipe Cache & Reset Session">🗑️</div>
  627.                 <div id="help-btn" class="shortcut-btn" title="Help / FAQ">❓</div>
  628.                 <div id="logs-btn" class="shortcut-btn" title="Debug Logs">🐛</div>
  629.                 <div id="settings-btn" class="shortcut-btn" title="Studio Settings" style="transition: transform 0.2s;">⚙️</div>
  630.             </div>
  631.  
  632.             <div id="xai-version-tag">v${typeof GM_info != 'undefined' ? GM_info.script.version : "???"}</div>
  633.  
  634.             <!-- LEFT COLUMN (Collapsible) -->
  635.             <div class="col-left" id="sidebar-left">
  636.                 <div class="sidebar-inner">
  637.                     <div class="panel" style="flex-shrink: 0;">
  638.                         <div style="display: flex; background: transparent; border-radius: 0 0 6px 6px; margin-bottom: 6px; padding: 10px; padding-bottom: 0;">
  639.                             <select id="master-action" size="4" class="action-select">
  640.                                 <option value="gen_image" selected>🖼️ Generate Image</option>
  641.                                 <option value="gen_video">🎥 Generate Video</option>
  642.                                 <option value="edit_video">✂️ Edit Video</option>
  643.                                 <option value="extend_video">➡️ Extend Video</option>
  644.                             </select>
  645.                         </div>
  646.  
  647.                         <div style="padding: 10px; display: flex; flex-direction: column;">
  648.                             <div id="setting-ar" class="slider-block">
  649.                                 <div class="slider-header"><label style="margin:0;">Aspect Ratio</label></div>
  650.                                 <div class="ar-select-box" id="ar-select-box">
  651.                                     <div id="ar-selected-icon"></div>
  652.                                     <span id="ar-selected-text">Auto</span>
  653.                                 </div>
  654.                             </div>
  655.  
  656.                             <div id="setting-high-res" class="slider-block" style="flex-direction: row; justify-content: space-between; align-items: center;">
  657.                                 <div class="slider-header" style="margin-bottom:0;"><label style="margin:0;">High Resolution</label></div>
  658.                                 <button id="high-res-btn" class="toggle-btn active">ON (2K / 720p)</button>
  659.                             </div>
  660.  
  661.                             <div id="setting-batch" class="slider-block">
  662.                                 <div class="slider-header"><label style="margin:0;">Batch Size</label> <span id="val-batch">1 Image</span></div>
  663.                                 <input type="range" id="xai-api-n" min="0" max="6" value="0">
  664.                                 <div class="slider-ticks" id="ticks-batch"></div>
  665.                             </div>
  666.  
  667.                             <div id="setting-duration" class="slider-block" style="display: none;">
  668.                                 <div class="slider-header"><label style="margin:0;">Duration</label> <span id="val-duration">5 Seconds</span></div>
  669.                                 <input type="range" id="xai-api-duration" min="0" max="14" value="4">
  670.                                 <div class="slider-ticks" id="ticks-duration"></div>
  671.                             </div>
  672.  
  673.                             <div id="setting-loops" class="slider-block" style="margin-bottom:0;">
  674.                                 <div class="slider-header" style="margin-bottom:0;"><label style="margin:0;" title="How many consecutive API calls to perform in a row">Loops</label></div>
  675.                                 <div style="display: flex; gap: 10px; align-items: center; margin-top: 4px;">
  676.                                     <input type="range" id="slider-loops" min="1" max="20" value="1" style="flex: 1; margin:0;">
  677.                                     <input type="number" id="xai-api-loops" value="1" min="1" max="100" style="width: 50px; height: 26px; padding: 2px 4px; text-align: center; border-radius: 4px; border: 1px solid #334155; background: #1e293b; color: #f8fafc;">
  678.                                 </div>
  679.                             </div>
  680.                         </div>
  681.                     </div>
  682.  
  683.                     <div class="panel drop-panel" id="ref-dropzone" style="flex: 1; display: flex; flex-direction: column; min-height: 0;">
  684.                         <div class="panel-title" id="ref-panel-title">Reference Media</div>
  685.                         <div style="padding: 10px; flex: 1; display: flex; flex-direction: column; overflow: hidden;">
  686.                             <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);">
  687.                                 <div style="font-size: 20px; margin-bottom: 4px;">📥</div>
  688.                                 <div style="font-size: 12px; font-weight: 600;">Drag & Drop / Ctrl+V</div>
  689.                             </div>
  690.                             <input type="file" id="xai-api-file" accept="image/*, video/mp4, video/webm" multiple style="display: none;">
  691.                             <div style="font-size: 10px; color: #94a3b8; margin-bottom: 4px; text-transform: uppercase; font-weight: bold; flex-shrink: 0;">Upload Order (Top = First)</div>
  692.                             <div id="xai-ref-list" style="flex: 1; overflow-y: auto; padding-right: 4px;"></div>
  693.                         </div>
  694.                     </div>
  695.                 </div>
  696.             </div>
  697.  
  698.             <!-- SIDEBAR TOGGLE -->
  699.             <div id="toggle-sidebar-btn" title="Toggle Sidebar">◀</div>
  700.  
  701.             <!-- CENTER COLUMN (Scrollable outputs + Floating Prompt) -->
  702.             <div class="col-center">
  703.  
  704.                 <div id="xai-current-images" class="scrollable-main">
  705.                     <div class="ready-placeholder" style="color: #94a3b8; font-size: 13px; text-align: center; margin-top: 40px; font-weight: 500;">Ready to generate. Results will appear here.</div>
  706.                 </div>
  707.  
  708.                 <div id="floating-prompt-panel" class="panel frosted-panel">
  709.                     <label style="font-size: 13px; color: #e2e8f0; display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
  710.                         <span style="display: flex; align-items: center; gap: 8px;">
  711.                             <span>Prompt</span>
  712.                             <button id="fav-prompts-btn" class="btn-secondary" style="padding: 2px 8px; font-size: 10px; height: 22px; background: rgba(30,41,59,0.8);">⭐ Favorites</button>
  713.                         </span>
  714.                         <span style="font-size: 10px; font-weight: 400; color: #94a3b8;">(Drop image here for EXIF)</span>
  715.                     </label>
  716.                     <div id="warnings-container" style="display: flex; flex-direction: column; gap: 0px;"></div>
  717.                     <div id="prompt-ref-previews" style="display: flex; gap: 8px; margin-bottom: 8px; min-height: 32px; display: none;"></div>
  718.                     <div class="prompt-container">
  719.                         <div id="prompt-backdrop"></div>
  720.                         <textarea id="xai-api-prompt" placeholder="Describe what you want to see... Supports Spintax {cat|dog} and @randomseed"></textarea>
  721.                     </div>
  722.                     <div style="display: flex; align-items: center; justify-content: space-between;">
  723.                         <div id="xai-api-status" style="font-size: 12px; color: #cbd5e1; font-weight: 500;">Ready</div>
  724.                         <div style="display: flex; gap: 8px;">
  725.                             <button id="xai-preview-btn" class="btn-secondary" style="background: rgba(30,41,59,0.8);">🔍 Payload</button>
  726.                             <button id="xai-cancel-btn" class="btn-secondary" style="display: none; color: #ef4444; border-color: #ef4444; background: rgba(30,41,59,0.8);">🛑 Cancel</button>
  727.                             <button id="xai-api-generate" class="btn-primary" style="width: 120px;"><div class="btn-content">Generate</div></button>
  728.                         </div>
  729.                     </div>
  730.                 </div>
  731.             </div>
  732.  
  733.             <div id="ar-options-container"></div>
  734.  
  735.             <!-- MODALS -->
  736.             <div id="xai-lightbox" class="modal-overlay">
  737.                 <div id="xai-lightbox-wrapper">
  738.                     <img id="xai-lightbox-img" src="">
  739.                     <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>
  740.                 </div>
  741.             </div>
  742.  
  743.             <!-- LOGS MODAL -->
  744.             <div id="xai-logs-modal" class="modal-overlay">
  745.                 <div class="modal-box" style="width: 800px; max-width: 95vw; height: 80vh;">
  746.                     <div style="display: flex; justify-content: space-between; align-items: center;">
  747.                         <h3 style="margin:0; font-size: 15px; color: #f8fafc;">🐛 Debug Logs</h3>
  748.                         <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>
  749.                     </div>
  750.                     <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>
  751.                     <div style="display: flex; gap: 8px;">
  752.                         <button id="copy-logs-btn" class="btn-primary" style="flex: 1;">📋 Copy Logs to Clipboard</button>
  753.                         <button id="clear-logs-btn" class="btn-secondary" style="flex: 1; color: #ef4444; border-color: #ef4444;">🗑️ Clear Logs</button>
  754.                     </div>
  755.                 </div>
  756.             </div>
  757.  
  758.             <!-- SETTINGS MODAL -->
  759.             <div id="xai-settings-modal" class="modal-overlay">
  760.                 <div class="modal-box" style="width: 500px;">
  761.                     <h3 style="margin:0; color: #f8fafc; font-size: 15px;">⚙️ Studio Settings</h3>
  762.                     <div style="display:flex; gap:10px;">
  763.                         <div style="flex:1;"><label>Max Retries</label><input type="number" id="set-retries" min="1" max="100"></div>
  764.                         <div style="flex:1;"><label>Delay Min (ms)</label><input type="number" id="set-delay-min" min="500" step="500"></div>
  765.                         <div style="flex:1;"><label>Delay Max (ms)</label><input type="number" id="set-delay-max" min="1000" step="500"></div>
  766.                     </div>
  767.                     <div><label>API Base URL (Leave empty for default console.x.ai)</label><input type="text" id="set-api-base-url" placeholder="e.g., https://api.proxy.com"></div>
  768.                     <div style="display:flex; align-items:center; gap: 8px; margin-top: 4px;"><input type="checkbox" id="set-notifications" style="width: 16px; height: 16px;"><label for="set-notifications" style="margin: 0; cursor: pointer;">Enable Notifications (Sound & Tab Blink)</label></div>
  769.                     <div style="display:flex; align-items:center; gap: 8px; margin-top: 4px;"><input type="checkbox" id="set-save-png" style="width: 16px; height: 16px;"><label for="set-save-png" style="margin: 0; cursor: pointer;">Save as Original PNG (Disable JPEG / EXIF Prompt)</label></div>
  770.                     <div style="display:flex; align-items:center; gap: 8px; margin-top: 4px;"><input type="checkbox" id="set-auto-download" style="width: 16px; height: 16px;"><label for="set-auto-download" style="margin: 0; cursor: pointer;">Auto-Download Generated Media</label></div>
  771.                     <div style="display:flex; align-items:center; gap: 8px; margin-top: 4px;"><input type="checkbox" id="set-anti-throttling" style="width: 16px; height: 16px;"><label for="set-anti-throttling" style="margin: 0; cursor: pointer;">Prevent Background Throttling (Plays silent audio)</label></div>
  772.                     <div style="display:flex; align-items:center; gap: 8px; margin-top: 4px;"><input type="checkbox" id="set-auto-retry-video" style="width: 16px; height: 16px;"><label for="set-auto-retry-video" style="margin: 0; cursor: pointer;">Auto-Retry Stuck Videos (Timeout / Moderated)</label></div>
  773.                     <div style="display:flex; align-items:center; gap: 8px; margin-top: 4px;"><input type="checkbox" id="set-break-loop-on-failure" style="width: 16px; height: 16px;"><label for="set-break-loop-on-failure" style="margin: 0; cursor: pointer;">Break loops if one iteration fails entirely.</label></div>
  774.                     <div style="display:flex; align-items:center; gap: 8px; margin-top: 4px;"><input type="checkbox" id="set-reference-ar-trick" style="width: 16px; height: 16px;"><label for="set-reference-ar-trick" style="margin: 0; cursor: pointer;">Single reference aspect ratio fix hack.</label></div>
  775.                     <div style="display:flex; align-items:center; gap: 8px; margin-top: 4px;"><input type="checkbox" id="set-maximum-greed-mode" style="width: 16px; height: 16px;"><label for="set-maximum-greed-mode" style="margin: 0; cursor: pointer;">MAXIUMUM GREED mode: reload tab on rate limit instead of waiting</label></div>
  776.                     <div style="display:flex; gap:10px; margin-top: 4px;">
  777.                         <div style="flex:1;"><label>Video Poll Timeout (s)</label><input type="number" id="set-video-timeout" min="10" step="10"></div>
  778.                         <div style="flex:1;"><label title="When 'Rate limit exceeded' occurs">Rate Limit Delay (s)</label><input type="number" id="set-rate-limit-delay" min="1" step="1"></div>
  779.                         <div style="flex:1;"><label>Base Font Size (px)</label><input type="number" id="set-font-size" min="10" max="24"></div>
  780.                         <div style="flex:1;"><label>Max Gallery Batches (memory)</label><input type="number" id="set-max-batches" min="3" max="50" value="8"></div>
  781.                     </div>
  782.                     <div style="border-top: 1px solid #334155; margin-top: 6px; padding-top: 6px;"></div>
  783.                     <div style="display:flex; align-items:center; gap: 8px; margin-top: 4px;"><input type="checkbox" id="set-suppress-warning" style="width: 16px; height: 16px;"><label for="set-suppress-warning" style="margin: 0; cursor: pointer; color: #fcd34d;">Suppress yellow ">8s video edit" warning</label></div>
  784.                     <div style="display:flex; align-items:center; gap: 8px; margin-top: 4px;"><input type="checkbox" id="set-video-autoplay" style="width: 16px; height: 16px;"><label for="set-video-autoplay" style="margin: 0; cursor: pointer;">Autoplay generated videos in gallery</label></div>
  785.                     <div style="display:flex; align-items:center; gap: 8px; margin-top: 4px;"><input type="checkbox" id="set-video-muted" style="width: 16px; height: 16px;"><label for="set-video-muted" style="margin: 0; cursor: pointer;">Start generated videos muted</label></div>
  786.                     <div style="border-top: 1px solid #334155; margin-top: 6px; padding-top: 6px;">
  787.                         <label>Custom CSS Overrides</label>
  788.                         <textarea id="set-custom-css" style="font-family: monospace; height: 60px; font-size: 11px;" placeholder="/* e.g., #xai-pro-app { background: red; } */"></textarea>
  789.                     </div>
  790.                     <div style="display: flex; gap: 8px; margin-top: 5px; border-top: 1px solid #334155; padding-top: 10px;">
  791.                         <button id="export-data-btn" class="btn-secondary" style="flex:1;">📤 Export Data</button>
  792.                         <button id="import-data-btn" class="btn-secondary" style="flex:1;">📥 Import Data</button>
  793.                         <input type="file" id="import-data-file" accept=".json" style="display: none;">
  794.                     </div>
  795.                     <div style="display: flex; gap: 8px; margin-top: 5px;">
  796.                         <button id="save-settings-btn" class="btn-primary" style="flex:1;">Save Settings</button>
  797.                         <button id="close-settings-btn" class="btn-secondary" style="flex:1;">Cancel</button>
  798.                     </div>
  799.                 </div>
  800.             </div>
  801.  
  802.             <!-- FAVORITES MODAL -->
  803.             <div id="xai-fav-modal" class="modal-overlay">
  804.                 <div class="modal-box" style="width: 550px;">
  805.                     <div style="display: flex; justify-content: space-between; align-items: center;">
  806.                         <h3 style="margin:0; color: #f8fafc; font-size: 15px;">⭐ Favorite Prompts</h3>
  807.                         <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>
  808.                     </div>
  809.                     <button id="add-current-fav-btn" class="btn-primary" style="background: linear-gradient(180deg, #10b981 0%, #059669 100%);">➕ Save Current Prompt to Favorites</button>
  810.                     <div id="fav-list" style="flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 6px; padding-right: 4px;"></div>
  811.                 </div>
  812.             </div>
  813.  
  814.             <!-- PAYLOAD MODAL -->
  815.             <div id="xai-payload-modal" class="modal-overlay">
  816.                 <div class="modal-box" style="width: 550px;">
  817.                     <div style="display: flex; justify-content: space-between; align-items: center;">
  818.                         <h3 style="margin:0; font-size: 15px; color: #f8fafc;">API Payload Debugger</h3>
  819.                         <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>
  820.                     </div>
  821.                     <div><label>Endpoint URL:</label><input type="text" id="payload-endpoint" style="font-family: monospace;" value="/v1/images/generations"></div>
  822.                     <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>
  823.                     <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>
  824.                     <div id="custom-payload-status" style="font-size: 11px; color: #64748b; text-align: center;">Ready</div>
  825.                 </div>
  826.             </div>
  827.             <!-- FAQ MODAL -->
  828.             <div id="xai-help-modal" class="modal-overlay">
  829.             <div class="modal-box" style="width: 600px; max-height: 85vh;">
  830.                 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;">
  831.                     <h3 style="margin:0; font-size: 15px; color: #f8fafc;">❓ Help & FAQ</h3>
  832.                     <button id="close-help-btn" style="background: #ef4444; color: #fff; border: none; border-radius: 6px; padding: 4px 8px; cursor: pointer; font-weight: bold; font-size: 11px;">Close</button>
  833.                 </div>
  834.                 <div id="faq-container" style="display: flex; flex-direction: column; overflow-y: auto; padding-right: 4px;">
  835.                     <div class="faq-item">
  836.                         <button class="faq-question">I am stuck in a loop refreshing cookies. How do I fix this?</button>
  837.                         <div class="faq-answer">
  838.                             <p>This usually happens if Tampermonkey lacks permission to access and modify protected HttpOnly cookies. Without this permission, the script cannot properly wipe your session cookies.</p>
  839.                             <p>Or maybe you are in the future and the exploits are fixed.</p>
  840.                         </div>
  841.                     </div>
  842.  
  843.                     <div class="faq-item">
  844.                         <button class="faq-question">How do I use specific reference images in my prompt?</button>
  845.                             <div class="faq-answer">
  846.                             <p>First, check the "Use in Payload" box on your desired reference. A green button (e.g., <code>➕ &lt;IMAGE_0&gt;</code>) will become active next to the "Use in Payload" toggle. You can click this green button to instantly insert the correct tag into your prompt.</p>
  847.                             <p>Do note that numbering can change if more references are enabled or disabled. To verify which image a tag belongs to, look at the small preview thumbnails just above the prompt text box. Hovering your mouse over the <code>&lt;IMAGE_X&gt;</code> tag inside the prompt will highlight the matching thumbnail!</p>
  848.                         </div>
  849.                     </div>
  850.  
  851.                     <div class="faq-item">
  852.                         <button class="faq-question">Why do my generation loops stop if a single result fails?</button>
  853.                         <div class="faq-answer">
  854.                             <p>This is an intentional safeguard. If a prompt triggers a filter or is otherwise blocked, the script assumes you don't want to keep doing API calls on a prompt that will continually fail.</p>
  855.                            <p>You can disable it by unchecking the "Break loops if one iteration fails entirely" option in the settings.</p>
  856.                        </div>
  857.                    </div>
  858.                    </div>
  859.                </div>
  860.            </div>
  861.        </div>
  862.    `;
  863.  
  864.    // ─── DOM ELEMENT CACHE ───────────────────────────────────────────────────
  865.    const els = {};
  866.  
  867.    function initializeDOM() {
  868.        Object.assign(els, {
  869.            app: document.getElementById('xai-pro-app'),
  870.            prompt: document.getElementById('xai-api-prompt'),
  871.            promptBackdrop: document.getElementById('prompt-backdrop'),
  872.            warningsContainer: document.getElementById('warnings-container'),
  873.            promptContainer: document.querySelector('.prompt-container'),
  874.            action: document.getElementById('master-action'),
  875.            highResBtn: document.getElementById('high-res-btn'),
  876.            n: document.getElementById('xai-api-n'),
  877.            duration: document.getElementById('xai-api-duration'),
  878.            loopsSlider: document.getElementById('slider-loops'),
  879.            loopsNum: document.getElementById('xai-api-loops'),
  880.            settingAr: document.getElementById('setting-ar'),
  881.            settingHighRes: document.getElementById('setting-high-res'),
  882.            settingBatch: document.getElementById('setting-batch'),
  883.            settingDuration: document.getElementById('setting-duration'),
  884.            fileInput: document.getElementById('xai-api-file'),
  885.            dropzone: document.getElementById('ref-dropzone'),
  886.            uploadPlaceholder: document.getElementById('xai-upload-placeholder'),
  887.            refList: document.getElementById('xai-ref-list'),
  888.            refTitle: document.getElementById('ref-panel-title'),
  889.            btn: document.getElementById('xai-api-generate'),
  890.            previewBtn: document.getElementById('xai-preview-btn'),
  891.            cancelBtn: document.getElementById('xai-cancel-btn'),
  892.            status: document.getElementById('xai-api-status'),
  893.            currentImages: document.getElementById('xai-current-images'), // Main scroll area
  894.            reset: document.getElementById('xai-reset-btn'),
  895.            sidebar: document.getElementById('sidebar-left'),
  896.            toggleSidebarBtn: document.getElementById('toggle-sidebar-btn'),
  897.            arSelectBox: document.getElementById('ar-select-box'),
  898.            arOptionsCont: document.getElementById('ar-options-container'),
  899.            arSelectedIcon: document.getElementById('ar-selected-icon'),
  900.            arSelectedText: document.getElementById('ar-selected-text'),
  901.            lightbox: document.getElementById('xai-lightbox'),
  902.            lightboxImg: document.getElementById('xai-lightbox-img'),
  903.            lightboxDl: document.getElementById('xai-lightbox-dl'),
  904.            promptRefPreviews: document.getElementById('prompt-ref-previews'),
  905.            payloadModal: document.getElementById('xai-payload-modal'),
  906.            payloadEndpoint: document.getElementById('payload-endpoint'),
  907.            payloadCode: document.getElementById('payload-code'),
  908.            closePayloadBtn: document.getElementById('close-payload-btn'),
  909.            sendCustomBtn: document.getElementById('send-custom-payload-btn'),
  910.            customStatus: document.getElementById('custom-payload-status'),
  911.            settingsBtn: document.getElementById('settings-btn'),
  912.            settingsModal: document.getElementById('xai-settings-modal'),
  913.            closeSettingsBtn: document.getElementById('close-settings-btn'),
  914.            saveSettingsBtn: document.getElementById('save-settings-btn'),
  915.            exportDataBtn: document.getElementById('export-data-btn'),
  916.            importDataBtn: document.getElementById('import-data-btn'),
  917.            importDataFile: document.getElementById('import-data-file'),
  918.            favBtn: document.getElementById('fav-prompts-btn'),
  919.            favModal: document.getElementById('xai-fav-modal'),
  920.            closeFavBtn: document.getElementById('close-fav-btn'),
  921.            addFavBtn: document.getElementById('add-current-fav-btn'),
  922.            favList: document.getElementById('fav-list'),
  923.            logsBtn: document.getElementById('logs-btn'),
  924.            logsModal: document.getElementById('xai-logs-modal'),
  925.            closeLogsBtn: document.getElementById('close-logs-btn'),
  926.            copyLogsBtn: document.getElementById('copy-logs-btn'),
  927.            clearLogsBtn: document.getElementById('clear-logs-btn'),
  928.            logsTextarea: document.getElementById('logs-textarea'),
  929.            helpBtn: document.getElementById('help-btn'),
  930.            helpModal: document.getElementById('xai-help-modal'),
  931.            closeHelpBtn: document.getElementById('close-help-btn')
  932.        });
  933.    }
  934.  
  935.    // ─── INITIALIZATION ───────────────────────────────────────────────────────
  936.    let updateUIForEditMedia = null;
  937.    let activeDurationMap = MAPS.durGen;
  938.  
  939.    const initApp = () => {
  940.        if (!document.body) return setTimeout(initApp, 50);
  941.        const hideNode = (node) => {
  942.            if (node.nodeType === 1 && node.id !== 'xai-pro-app' && !['SCRIPT', 'STYLE', 'LINK'].includes(node.tagName)) {
  943.                node.style.display = 'none';
  944.            }
  945.        };
  946.        Array.from(document.body.children).forEach(hideNode);
  947.        new MutationObserver((mutations) => {
  948.            mutations.forEach(m => m.addedNodes.forEach(hideNode));
  949.        }).observe(document.body, { childList: true });
  950.  
  951.        if (!document.getElementById('xai-pro-app')) {
  952.            const style = document.createElement('style'); style.innerHTML = UI_CSS; document.head.appendChild(style);
  953.            const customStyle = document.createElement('style'); customStyle.id = 'xai-custom-css-block'; customStyle.innerHTML = appSettings.customCSS; document.head.appendChild(customStyle);
  954.            document.body.insertAdjacentHTML('beforeend', UI_HTML);
  955.            updateFontSize(appSettings.uiFontSize);
  956.            initializeDOM();
  957.            bindLogic();
  958.        }
  959.    };
  960.  
  961.    let referenceMedia =[];
  962.    let currentAr = localStorage.getItem('xai_api_ar') || 'Auto';
  963.    let fullPayloadMemory = {};
  964.    let currentAbortController = null;
  965.    let isRequestCancelled = false;
  966.  
  967.    // ─── INDEXEDDB MANAGER ───────────────────────────────────────────────────
  968.    const DB_NAME = 'xAIProStudioDB';
  969.    const STORE_NAME = 'refsStore';
  970.  
  971.    const DBManager = {
  972.        init: function() {
  973.            return new Promise((resolve, reject) => {
  974.                const request = indexedDB.open(DB_NAME, 1);
  975.                request.onupgradeneeded = (e) => {
  976.                    const db = e.target.result;
  977.                    if (!db.objectStoreNames.contains(STORE_NAME)) db.createObjectStore(STORE_NAME);
  978.                };
  979.                request.onsuccess = () => resolve(request.result);
  980.                request.onerror = () => reject(request.error);
  981.            });
  982.        },
  983.        setRefs: async function(data) {
  984.            const db = await this.init();
  985.            return new Promise((resolve, reject) => {
  986.                const transaction = db.transaction([STORE_NAME], 'readwrite');
  987.                const store = transaction.objectStore(STORE_NAME);
  988.                const request = store.put(data, 'refs');
  989.                request.onsuccess = () => resolve();
  990.                request.onerror = () => reject(request.error);
  991.            });
  992.        },
  993.        getRefs: async function() {
  994.            const db = await this.init();
  995.            return new Promise((resolve, reject) => {
  996.                const transaction = db.transaction([STORE_NAME], 'readonly');
  997.                const store = transaction.objectStore(STORE_NAME);
  998.                const request = store.get('refs');
  999.                request.onsuccess = () => resolve(request.result || null);
  1000.                request.onerror = () => reject(request.error);
  1001.            });
  1002.        }
  1003.    };
  1004.  
  1005.    let saveRefsTimeout = null;
  1006.    function saveRefs(immediate = false) {
  1007.        const executeSave = async () => {
  1008.            try {
  1009.                const cleanRefs = referenceMedia.map(m => {
  1010.                    const copy = { ...m };
  1011.                    delete copy.blobUrl;
  1012.                    return copy;
  1013.                });
  1014.                await DBManager.setRefs(cleanRefs);
  1015.            } catch(e) { addLog('ERROR', 'Failed to save refs to IDB', e); }
  1016.        };
  1017.  
  1018.        if (saveRefsTimeout) clearTimeout(saveRefsTimeout);
  1019.        if (immediate) return executeSave();
  1020.        else saveRefsTimeout = setTimeout(executeSave, 400);
  1021.    }
  1022.  
  1023.    function findMapIndex(arr, val) {
  1024.        let idx = arr.findIndex(item => (item.v || item).toString() === val.toString());
  1025.        return idx !== -1 ? idx : 0;
  1026.    }
  1027.  
  1028.    function drawTicks(containerId, map, colorFn) {
  1029.        const cont = document.getElementById(containerId);
  1030.        if (!cont) return;
  1031.        cont.innerHTML = map.map((item, index) => {
  1032.            let val = item.v || item; let label = item.t || val;
  1033.            let color = colorFn ? colorFn(val) : '#64748b';
  1034.            let percent = map.length > 1 ? (index / (map.length - 1)) * 100 : 50;
  1035.            let offset = 8 - (percent / 100) * 16;
  1036.            return `<span style="color:${color}; left:calc(${percent}% + ${offset}px);">${label}</span>`;
  1037.        }).join('');
  1038.    }
  1039.  
  1040.    function getDurationColor(val) {
  1041.        const v = parseInt(val);
  1042.        if (v <= 8) return '#f8fafc';
  1043.        if (v <= 10) return '#94a3b8';
  1044.        return '#ef4444';
  1045.    }
  1046.  
  1047.    function syncSlider(sliderEl, textEl, mapArr, formatFn) {
  1048.        const val = mapArr[sliderEl.value];
  1049.        textEl.innerText = formatFn ? formatFn(val) : (val.l || val);
  1050.    }
  1051.  
  1052.    let isHighRes = true;
  1053.    let genSettingsTimeout = null;
  1054.    function updateGenSettingsMemory() {
  1055.        if (genSettingsTimeout) clearTimeout(genSettingsTimeout);
  1056.        genSettingsTimeout = setTimeout(() => {
  1057.            localStorage.setItem('xai_api_gen_settings', JSON.stringify({
  1058.                action: els.action.value, isHighRes: isHighRes,
  1059.                n: MAPS.batch[els.n.value], duration: activeDurationMap[els.duration.value], loops: els.loopsNum.value
  1060.            }));
  1061.        }, 300);
  1062.    }
  1063.  
  1064.    function updateWarnings() {
  1065.        els.warningsContainer.innerHTML = ''; let html = '';
  1066.        if (/--[a-zA-Z0-9_-]+=/.test(els.prompt.value)) html += `<div class="warning-box warning-red">⚠️ A gentle reminder that --parameter=value has never been shown to do anything at all, including --seed and --sid, but you do you!</div>`;
  1067.        const action = els.action.value; const duration = activeDurationMap[els.duration.value] || 0; const activeRefs = referenceMedia.filter(m => m.active).length;
  1068.        if (action === 'gen_video') {
  1069.            if (activeRefs > 0 && duration > 10) {
  1070.                const currentAr = localStorage.getItem('xai_api_ar');
  1071.                if ( currentAr != 'Auto' && currentAr != 'By First Ref' ) {
  1072.                    html += `<div class="warning-box warning-red">⚠️ Warning: videos longer than 10 seconds seem to stretch the reference to the desired aspect ratio instead of properly resizing them. You might get better results with auto.</div>`;
  1073.                }
  1074.                if ( activeRefs > 1 ) {
  1075.                    html += `<div class="warning-box warning-red">⚠️ Warning: only the first reference is going to be used for videos longer than 10 seconds.</div>`;
  1076.                }
  1077.            }
  1078.            if (duration > 8 && !appSettings.suppressEditWarning) html += `<div class="warning-box warning-yellow">⚠️ Note: edit won't work on videos longer than 8s like the one you are about to generate. You can suppress this warning in options.</div>`;
  1079.         }
  1080.         els.warningsContainer.innerHTML = html;
  1081.     }
  1082.  
  1083.         // ─── MEMORY LEAK PROTECTION v39.4 ───────────────────────────────────────
  1084.     function pruneOldResults() {
  1085.         const MAX_BATCHES = appSettings.maxGalleryBatches || 8;
  1086.         const MAX_CARDS_PER_BATCH = 12;
  1087.  
  1088.         const batches = Array.from(els.currentImages.querySelectorAll('.batch-container'));
  1089.  
  1090.         if (batches.length > MAX_BATCHES) {
  1091.             batches.slice(0, batches.length - MAX_BATCHES).forEach(batch => {
  1092.                 batch.querySelectorAll('img.zoomable, video').forEach(el => {
  1093.                     if (el.src && (el.src.startsWith('data:') || el.src.startsWith('blob:'))) {
  1094.                         try { URL.revokeObjectURL(el.src); } catch(e) {}
  1095.                     }
  1096.                 });
  1097.                 batch.remove();
  1098.             });
  1099.         }
  1100.  
  1101.         batches.forEach(batch => {
  1102.             const grid = batch.querySelector('.batch-grid');
  1103.             if (!grid) return;
  1104.             const cards = Array.from(grid.children);
  1105.             if (cards.length > MAX_CARDS_PER_BATCH) {
  1106.                 cards.slice(0, cards.length - MAX_CARDS_PER_BATCH).forEach(card => {
  1107.                     card.querySelectorAll('img, video').forEach(el => {
  1108.                         if (el.src && (el.src.startsWith('data:') || el.src.startsWith('blob:'))) {
  1109.                             try { URL.revokeObjectURL(el.src); } catch(e) {}
  1110.                         }
  1111.                     });
  1112.                     card.remove();
  1113.                 });
  1114.             }
  1115.         });
  1116.     }
  1117.  
  1118.         // ─── CLEAR ALL REFERENCES ───────────────────────────────────────────────
  1119.     function clearAllReferences() {
  1120.         if (referenceMedia.length === 0) return;
  1121.         if (!confirm(`Удалить ВСЕ ${referenceMedia.length} референсы из панели?\n\n(это действие нельзя отменить)`)) return;
  1122.  
  1123.         // Отзываем все blob URL чтобы освободить память
  1124.         referenceMedia.forEach(m => {
  1125.             if (m.blobUrl) {
  1126.                 try { URL.revokeObjectURL(m.blobUrl); } catch(e) {}
  1127.             }
  1128.         });
  1129.  
  1130.         referenceMedia = [];
  1131.         saveRefs(true);           // сохраняем в IndexedDB
  1132.         renderRefList();
  1133.         updateWarnings();
  1134.         syncPromptBackdrop();
  1135.         addLog('INFO', 'All reference media cleared by user');
  1136.     }
  1137.  
  1138.     function syncPromptBackdrop() {
  1139.         const val = els.prompt.value; updateWarnings();
  1140.         let html = val.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
  1141.         const activeRefs = referenceMedia.filter(m => m.active);
  1142.         html = html.replace(/&lt;IMAGE_(\d+)&gt;/g, (match, idxStr) => { const idx = parseInt(idxStr); return activeRefs[idx] ? `<span class="prompt-tag" data-idx="${idx}">${match}</span>` : match; });
  1143.         els.promptBackdrop.innerHTML = html.replace(/\n/g, '<br>') + '<br>';
  1144.         els.promptBackdrop.scrollTop = els.prompt.scrollTop; els.promptBackdrop.scrollLeft = els.prompt.scrollLeft;
  1145.     }
  1146.  
  1147.     function setPromptVal(val) {
  1148.         els.prompt.focus();
  1149.         els.prompt.setSelectionRange(0, els.prompt.value.length);
  1150.         document.execCommand('insertText', false, val);
  1151.         localStorage.setItem('xai_api_prompt', val);
  1152.         syncPromptBackdrop();
  1153.     }
  1154.  
  1155.     // ─── MODALS & AUXILIARY UI SETUP ─────────────────────────────────────────
  1156.     function renderFavList() {
  1157.         els.favList.innerHTML = '';
  1158.         if (favoritePrompts.length === 0) { els.favList.innerHTML = '<div style="color: #64748b; font-size: 12px; text-align: center; margin-top: 15px;">No favorite prompts yet.</div>'; return; }
  1159.         favoritePrompts.forEach((promptText, idx) => {
  1160.             const div = document.createElement('div'); div.className = 'fav-item'; div.innerHTML = `<div class="fav-text" title="${promptText.replace(/"/g, '&quot;')}">${promptText}</div><button class="btn-secondary" style="padding: 2px 6px; font-size: 10px; border-color: #ef4444; color: #ef4444;">Del</button>`;
  1161.            div.querySelector('.fav-text').addEventListener('click', () => { setPromptVal(promptText); els.favModal.style.display = 'none'; });
  1162.            div.querySelector('button').addEventListener('click', () => { favoritePrompts.splice(idx, 1); localStorage.setItem('xai_api_favs', JSON.stringify(favoritePrompts)); renderFavList(); });
  1163.            els.favList.appendChild(div);
  1164.        });
  1165.    }
  1166.  
  1167.    function setupModalsAndAuxUI() {
  1168.        els.logsBtn.addEventListener('click', () => { els.logsTextarea.value = formatLogsForExport(); els.logsModal.style.display = 'flex'; els.logsTextarea.scrollTop = els.logsTextarea.scrollHeight; });
  1169.        els.closeLogsBtn.addEventListener('click', () => els.logsModal.style.display = 'none');
  1170.        els.copyLogsBtn.addEventListener('click', () => { els.logsTextarea.select(); document.execCommand('copy'); alert('Logs copied to clipboard!'); });
  1171.        els.clearLogsBtn.addEventListener('click', () => { if (confirm("Clear all logs?")) { appLogs.length = 0; els.logsTextarea.value = ''; } });
  1172.  
  1173.        els.settingsBtn.addEventListener('click', () => { SettingsManager.populateUI(appSettings); els.settingsModal.style.display = 'flex'; });
  1174.        els.saveSettingsBtn.addEventListener('click', () => { appSettings = SettingsManager.saveFromUI(); document.getElementById('xai-custom-css-block').innerHTML = appSettings.customCSS; updateFontSize(appSettings.uiFontSize); updateWarnings(); els.settingsModal.style.display = 'none'; });
  1175.        els.closeSettingsBtn.addEventListener('click', () => els.settingsModal.style.display = 'none');
  1176.  
  1177.        els.exportDataBtn.addEventListener('click', () => {
  1178.            const blob = new Blob([JSON.stringify({settings: appSettings, refs: referenceMedia}, null, 2)], { type: 'application/json' });
  1179.            const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `xAI_Pro_Studio_Backup_${makeTimestamp().replace(/:/g, '-')}.json`; a.click(); URL.revokeObjectURL(url);
  1180.        });
  1181.        els.importDataBtn.addEventListener('click', () => els.importDataFile.click());
  1182.        els.importDataFile.addEventListener('change', (e) => {
  1183.            const file = e.target.files[0]; if (!file) return; const reader = new FileReader();
  1184.            reader.onload = async (evt) => {
  1185.                try {
  1186.                    const parsed = JSON.parse(evt.target.result);
  1187.                    if (parsed.settings) { appSettings = { ...appSettings, ...parsed.settings }; localStorage.setItem('xai_api_settings', JSON.stringify(appSettings)); }
  1188.                    if (parsed.refs && Array.isArray(parsed.refs)) {
  1189.                        referenceMedia = parsed.refs;
  1190.                        await saveRefs(true);
  1191.                    }
  1192.                    alert("Import successful! Page will reload."); location.reload();
  1193.                } catch(err) { alert("Failed to parse the JSON file."); }
  1194.            }; reader.readAsText(file); e.target.value = '';
  1195.        });
  1196.  
  1197.        els.favBtn.addEventListener('click', () => { renderFavList(); els.favModal.style.display = 'flex'; });
  1198.        els.closeFavBtn.addEventListener('click', () => els.favModal.style.display = 'none');
  1199.        els.addFavBtn.addEventListener('click', () => {
  1200.            const p = els.prompt.value.trim(); if (!p) return alert("Prompt is empty!"); if (favoritePrompts.includes(p)) return alert("Already in favorites!");
  1201.            favoritePrompts.unshift(p); localStorage.setItem('xai_api_favs', JSON.stringify(favoritePrompts)); renderFavList();
  1202.        });
  1203.  
  1204.        els.app.addEventListener('click', (e) => {
  1205.            if (e.target && e.target.classList.contains('zoomable') && e.target.tagName === 'IMG') {
  1206.                els.lightboxImg.src = e.target.src; els.lightboxDl.href = e.target.src; els.lightboxDl.download = e.target.dataset.filename || 'Reference_Image.jpg'; els.lightbox.style.display = 'flex';
  1207.                els.lightboxDl.onclick = () => { if(e.target.closest('.current-card')) e.target.closest('.current-card').classList.add('downloaded'); };
  1208.            }
  1209.        });
  1210.        els.lightbox.addEventListener('click', (e) => { if (!e.target.closest('#xai-lightbox-dl') && !e.target.closest('.modal-box')) els.lightbox.style.display = 'none'; });
  1211.        document.addEventListener('keydown', (e) => { if(e.key === 'Escape') Array.from(document.querySelectorAll('.modal-overlay')).forEach(m => m.style.display = 'none'); });
  1212.  
  1213.        els.helpBtn.addEventListener('click', () => { els.helpModal.style.display = 'flex'; });
  1214.        els.closeHelpBtn.addEventListener('click', () => els.helpModal.style.display = 'none');
  1215.        document.querySelectorAll('.faq-question').forEach(button => {
  1216.            button.addEventListener('click', () => {
  1217.                const isActive = button.classList.contains('active');
  1218.                if (!isActive) {
  1219.                    button.classList.add('active');
  1220.                    button.nextElementSibling.style.maxHeight = button.nextElementSibling.scrollHeight + "px";
  1221.                } else {
  1222.                    button.classList.remove('active');
  1223.                    button.nextElementSibling.style.maxHeight = null;
  1224.                }
  1225.            });
  1226.        });
  1227.  
  1228.        els.toggleSidebarBtn.addEventListener('click', () => {
  1229.            els.sidebar.classList.toggle('collapsed');
  1230.            els.toggleSidebarBtn.innerText = els.sidebar.classList.contains('collapsed') ? '▶' : '◀';
  1231.        });
  1232.  
  1233.        els.reset.addEventListener('click', () => {
  1234.            if (!confirm("Wipe cache? Your prompt and settings will be saved.")) return;
  1235.            localStorage.setItem('xai_api_prompt', els.prompt.value); nuclearSessionReset();
  1236.        });
  1237.    }
  1238.  
  1239.    function renderPromptRefPreviews() {
  1240.        if (!els.promptRefPreviews) return;
  1241.        els.promptRefPreviews.innerHTML = '';
  1242.        const activeRefs = referenceMedia.filter(m => m.active);
  1243.        if (activeRefs.length === 0) {
  1244.            els.promptRefPreviews.style.display = 'none';
  1245.            return;
  1246.        }
  1247.        els.promptRefPreviews.style.display = 'flex';
  1248.        activeRefs.forEach((media, idx) => {
  1249.            const thumbSrc = media.thumb || media.blobUrl || media.base64;
  1250.            let el;
  1251.            if (thumbSrc) {
  1252.                el = document.createElement('img');
  1253.                el.src = thumbSrc;
  1254.                if (!media.isVideo) {
  1255.                    el.style.cursor = 'zoom-in';
  1256.                    el.addEventListener('click', () => {
  1257.                        els.lightboxImg.src = media.blobUrl || media.base64;
  1258.                        els.lightboxDl.href = media.blobUrl || media.base64;
  1259.                        els.lightboxDl.download = media.fileName || `Reference_${idx+1}.jpg`;
  1260.                        els.lightbox.style.display = 'flex';
  1261.                    });
  1262.                }
  1263.            } else {
  1264.                el = document.createElement('div');
  1265.                el.innerText = '🎥';
  1266.                el.style.display = 'flex'; el.style.alignItems = 'center'; el.style.justifyContent = 'center'; el.style.background = 'rgba(0,0,0,0.3)';
  1267.            }
  1268.            el.classList.add('prompt-ref-thumb');
  1269.            el.setAttribute('data-idx', idx);
  1270.            el.title = media.isVideo ? `Video (Matches <IMAGE_${idx}>)` : `Click to view (Matches <IMAGE_${idx}>)`;
  1271.            els.promptRefPreviews.appendChild(el);
  1272.        });
  1273.    }
  1274.  
  1275.    function setupPromptListeners() {
  1276.        if (localStorage.getItem('xai_api_prompt')) setPromptVal(localStorage.getItem('xai_api_prompt'));
  1277.        let promptSaveTimeout = null;
  1278.        els.prompt.addEventListener('input', () => {
  1279.            if (promptSaveTimeout) clearTimeout(promptSaveTimeout);
  1280.            promptSaveTimeout = setTimeout(() => localStorage.setItem('xai_api_prompt', els.prompt.value), 400);
  1281.            syncPromptBackdrop();
  1282.        });
  1283.        els.prompt.addEventListener('scroll', () => { els.promptBackdrop.scrollTop = els.prompt.scrollTop; els.promptBackdrop.scrollLeft = els.prompt.scrollLeft; });
  1284.  
  1285.        els.prompt.addEventListener('mousemove', (e) => {
  1286.            if (e.buttons !== 0) return;
  1287.            els.prompt.style.pointerEvents = 'none'; const el = document.elementFromPoint(e.clientX, e.clientY); els.prompt.style.pointerEvents = 'auto';
  1288.  
  1289.            document.querySelectorAll('.prompt-ref-thumb').forEach(t => t.classList.remove('glow'));
  1290.  
  1291.            if (el && el.classList.contains('prompt-tag')) {
  1292.                const idx = el.getAttribute('data-idx');
  1293.                const thumb = els.promptRefPreviews.querySelector(`.prompt-ref-thumb[data-idx="${idx}"]`);
  1294.                if (thumb) thumb.classList.add('glow');
  1295.            }
  1296.        });
  1297.        els.prompt.addEventListener('mouseleave', () => {
  1298.            document.querySelectorAll('.prompt-ref-thumb').forEach(t => t.classList.remove('glow'));
  1299.        });
  1300.        els.prompt.addEventListener('keydown', (e) => {
  1301.            if (e.key === 'Enter' && !e.shiftKey) {
  1302.                e.preventDefault();
  1303.                if (!els.btn.disabled) {
  1304.                    els.btn.click();
  1305.                }
  1306.            }
  1307.        });
  1308.  
  1309.        els.prompt.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); els.promptContainer.classList.add('prompt-drag-over'); });
  1310.        els.prompt.addEventListener('dragleave', (e) => { e.preventDefault(); e.stopPropagation(); els.promptContainer.classList.remove('prompt-drag-over'); });
  1311.        els.prompt.addEventListener('drop', async (e) => {
  1312.            e.preventDefault(); e.stopPropagation(); els.promptContainer.classList.remove('prompt-drag-over');
  1313.            if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
  1314.                const file = e.dataTransfer.files[0]; const oldVal = els.prompt.value; setPromptVal("Extracting prompt from EXIF...");
  1315.                const extractedPrompt = await extractPromptFromImage(file);
  1316.                if (extractedPrompt) setPromptVal(extractedPrompt); else { setPromptVal(oldVal); alert("No prompt found in this image's EXIF data."); }
  1317.            }
  1318.        });
  1319.    }
  1320.  
  1321.    function setupAspectRatioUI() {
  1322.        function renderArOptions() {
  1323.            els.arOptionsCont.innerHTML = '';
  1324.            arData.forEach(ar => {
  1325.                const opt = document.createElement('div'); opt.className = `ar-option${ar.imageOnly ? ' image-only-ar' : ''}`;
  1326.                opt.innerHTML = `${createArIcon(ar.w, ar.h, ar.isAuto, ar.isRef)} <span>${ar.label}</span>`;
  1327.                opt.addEventListener('click', () => { setAr(ar); els.arOptionsCont.style.display = 'none'; });
  1328.                els.arOptionsCont.appendChild(opt);
  1329.            });
  1330.        }
  1331.        function setAr(arObj) {
  1332.            currentAr = arObj.label; localStorage.setItem('xai_api_ar', currentAr);
  1333.            els.arSelectedIcon.innerHTML = createArIcon(arObj.w, arObj.h, arObj.isAuto, arObj.isRef); els.arSelectedText.innerText = arObj.label;
  1334.            updateWarnings();
  1335.        }
  1336.        renderArOptions();
  1337.        const initialAr = arData.find(a => a.label === currentAr) || arData[0];
  1338.        setAr(initialAr);
  1339.  
  1340.        els.arSelectBox.addEventListener('click', (e) => {
  1341.            e.stopPropagation(); if (els.arOptionsCont.style.display === 'grid') { els.arOptionsCont.style.display = 'none'; return; }
  1342.            const rect = els.arSelectBox.getBoundingClientRect(); els.arOptionsCont.style.top = (rect.bottom + 5) + 'px';
  1343.            els.arOptionsCont.style.left = rect.left + 'px'; els.arOptionsCont.style.width = rect.width + 'px'; els.arOptionsCont.style.display = 'grid';
  1344.            const currentAction = els.action.value;
  1345.            for ( const el of els.arOptionsCont.getElementsByClassName('image-only-ar') ) { el.style.display = currentAction === 'gen_image' ? 'flex' : 'none' }
  1346.        });
  1347.        document.addEventListener('click', (e) => { if (!els.arOptionsCont.contains(e.target)) els.arOptionsCont.style.display = 'none'; });
  1348.    }
  1349.  
  1350.    function getMaxMedia() {
  1351.        const val = els.action.value;
  1352.        return val === 'gen_image' ? 5 : (val === 'gen_video' ? 7 : 1);
  1353.    }
  1354.  
  1355.    function updateActionUI() {
  1356.        const val = els.action.value;
  1357.        if (val === 'gen_image') {
  1358.            els.settingAr.style.display = 'flex'; els.settingHighRes.style.display = 'flex'; els.settingBatch.style.display = 'flex'; els.settingDuration.style.display = 'none';
  1359.        } else if (val === 'gen_video') {
  1360.            els.settingAr.style.display = 'flex'; els.settingHighRes.style.display = 'flex'; els.settingBatch.style.display = 'none'; els.settingDuration.style.display = 'flex';
  1361.            activeDurationMap = MAPS.durGen; els.duration.max = activeDurationMap.length - 1;
  1362.            drawTicks('ticks-duration', activeDurationMap, getDurationColor);
  1363.            syncSlider(els.duration, document.getElementById('val-duration'), activeDurationMap, v => v + ' Seconds');
  1364.        } else if (val === 'edit_video') {
  1365.            els.settingAr.style.display = 'none'; els.settingHighRes.style.display = 'none'; els.settingBatch.style.display = 'none'; els.settingDuration.style.display = 'none';
  1366.        } else if (val === 'extend_video') {
  1367.            els.settingAr.style.display = 'none'; els.settingHighRes.style.display = 'none'; els.settingBatch.style.display = 'none'; els.settingDuration.style.display = 'flex';
  1368.            activeDurationMap = MAPS.durExt; els.duration.max = activeDurationMap.length - 1;
  1369.            drawTicks('ticks-duration', activeDurationMap, getDurationColor);
  1370.            syncSlider(els.duration, document.getElementById('val-duration'), activeDurationMap, v => v + ' Seconds');
  1371.        }
  1372.  
  1373.        const max = getMaxMedia(); els.refTitle.innerText = `Reference Media (Max Active: ${max})`;
  1374.        let activeCount = 0;
  1375.        referenceMedia.forEach(m => {
  1376.            let valid = true;
  1377.            if ((val === 'gen_image' || val === 'gen_video') && m.isVideo) valid = false;
  1378.            if ((val === 'edit_video' || val === 'extend_video') && !m.isVideo) valid = false;
  1379.            if (!valid) m.active = false; else if (m.active) { activeCount++; if (activeCount > max) m.active = false; }
  1380.        });
  1381.        renderRefList(); updateWarnings();
  1382.    }
  1383.  
  1384.    let draggedIndex = null;
  1385.    function updateRefActiveStates() {
  1386.        const actionVal = els.action.value;
  1387.        const disableInsert = actionVal === 'edit_video' || actionVal === 'extend_video';
  1388.        const activeRefs = referenceMedia.filter(m => m.active);
  1389.        const refNodes = Array.from(els.refList.children);
  1390.  
  1391.        referenceMedia.forEach((media, index) => {
  1392.            const item = refNodes[index];
  1393.            if (!item) return;
  1394.  
  1395.            if (media.active) item.classList.remove('inactive');
  1396.            else item.classList.add('inactive');
  1397.  
  1398.            if (!media.isVideo) {
  1399.                const insertBtn = item.querySelector('.ref-insert');
  1400.                if (insertBtn) {
  1401.                    if (disableInsert) {
  1402.                        insertBtn.disabled = true; insertBtn.style.opacity = '0.4'; insertBtn.style.filter = 'grayscale(1)'; insertBtn.style.cursor = 'not-allowed'; insertBtn.title = "Not available in this mode"; insertBtn.innerHTML = `➕ &lt;IMAGE_X&gt;`;
  1403.                    } else if (!media.active) {
  1404.                        insertBtn.disabled = true; insertBtn.style.opacity = '0.4'; insertBtn.style.filter = 'none'; insertBtn.style.cursor = 'not-allowed'; insertBtn.title = "Check 'Use in Payload' first"; insertBtn.innerHTML = `➕ &lt;IMAGE_?&gt;`;
  1405.                    } else {
  1406.                        const dynamicIndex = activeRefs.indexOf(media);
  1407.                        insertBtn.disabled = false; insertBtn.style.opacity = '1'; insertBtn.style.filter = 'none'; insertBtn.style.cursor = 'pointer'; insertBtn.title = "Insert into prompt"; insertBtn.innerHTML = `➕ &lt;IMAGE_${dynamicIndex}&gt;`;
  1408.                    }
  1409.                }
  1410.            }
  1411.        });
  1412.        updateWarnings();
  1413.    }
  1414.  
  1415.    function renderRefList() {
  1416.        els.refList.innerHTML = '';
  1417.        referenceMedia.forEach((media, index) => {
  1418.            const item = document.createElement('div'); item.className = 'ref-item'; item.draggable = true;
  1419.            const activeRefs = referenceMedia.filter(m => m.active); const dynamicIndex = media.active ? activeRefs.indexOf(media) : '?';
  1420.            const actionVal = els.action.value; const disableInsert = actionVal === 'edit_video' || actionVal === 'extend_video';
  1421.  
  1422.            let thumbHtml = '';
  1423.            if (media.isVideo) {
  1424.                if (media.thumb) thumbHtml = `<div style="position: relative; width: 90px; height: 90px; flex-shrink: 0;"><img src="${media.thumb}" class="ref-thumb" style="width: 100%; height: 100%;" title="${media.fileName || 'Video'}"><div style="position: absolute; top: 4px; right: 4px; background: rgba(0,0,0,0.7); color: white; border-radius: 4px; padding: 2px 4px; font-size: 10px;">🎥</div></div>`;
  1425.                else thumbHtml = `<div class="ref-thumb" style="display:flex; align-items:center; justify-content:center; flex-direction:column; background:#1e293b; color:#94a3b8; font-size:24px;" title="${media.fileName || 'Video'}">🎥<span style="font-size:9px; margin-top:4px; max-width:80px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">${media.fileName || 'Video'}</span></div>`;
  1426.            } else thumbHtml = `<img src="${media.blobUrl || media.base64}" class="ref-thumb zoomable" data-filename="${media.fileName || 'Reference_'+(index+1)+'.jpg'}" title="${media.fileName || 'Click to view'}">`;
  1427.  
  1428.            let insertBtnHtml = '';
  1429.            if (!media.isVideo) {
  1430.                if (disableInsert) insertBtnHtml = `<button class="ref-action-btn ref-insert" disabled style="opacity: 0.4; filter: grayscale(1); cursor: not-allowed;" title="Not available in this mode">➕ &lt;IMAGE_X&gt;</button>`;
  1431.                else if (!media.active) insertBtnHtml = `<button class="ref-action-btn ref-insert" disabled style="opacity: 0.4; cursor: not-allowed;" title="Check 'Use in Payload' first">➕ &lt;IMAGE_?&gt;</button>`;
  1432.                else insertBtnHtml = `<button class="ref-action-btn ref-insert" title="Insert into prompt">➕ &lt;IMAGE_${dynamicIndex}&gt;</button>`;
  1433.            }
  1434.  
  1435.            item.innerHTML = `<button class="ref-close-btn" title="Remove Media">&times;</button><div class="ref-handle">☰</div>${thumbHtml}<div class="ref-info"><span style="font-size: 12px; color: #e2e8f0; font-weight: 600;">Ref ${index + 1} ${media.isVideo ? '(Video)' : ''}</span><label class="ref-toggle-label"><input type="checkbox" class="ref-active-toggle" ${media.active ? 'checked' : ''}> Use in Payload</label><div class="ref-btn-group">${insertBtnHtml}</div></div>`;
  1436.  
  1437.            const insertBtn = item.querySelector('.ref-insert');
  1438.            if (insertBtn) {
  1439.                insertBtn.addEventListener('click', (e) => {
  1440.                    if (!media.active || insertBtn.disabled) return;
  1441.                    const currentAction = els.action.value; if (currentAction === 'edit_video' || currentAction === 'extend_video') { e.preventDefault(); return alert("<IMAGE_X> tags not supported here."); }
  1442.  
  1443.                    const dynIdx = referenceMedia.filter(m => m.active).indexOf(media);
  1444.                    const promptEl = els.prompt; const tag = `<IMAGE_${dynIdx}>`; const startPos = promptEl.selectionStart; const endPos = promptEl.selectionEnd;
  1445.  
  1446.                    setPromptVal(promptEl.value.substring(0, startPos) + tag + promptEl.value.substring(endPos, promptEl.value.length));
  1447.                    promptEl.selectionStart = promptEl.selectionEnd = startPos + tag.length; promptEl.focus();
  1448.                });
  1449.            }
  1450.            if (!media.active) item.classList.add('inactive');
  1451.  
  1452.            item.querySelector('.ref-active-toggle').addEventListener('change', (e) => {
  1453.                const max = getMaxMedia(); const currentlyActive = referenceMedia.filter(r => r.active).length;
  1454.                if (e.target.checked && currentlyActive >= max) { alert(`Up to ${max} active references allowed.`); e.target.checked = false; return; }
  1455.                media.active = e.target.checked;
  1456.                saveRefs();
  1457.                updateRefActiveStates();
  1458.                syncPromptBackdrop();
  1459.                updateWarnings();
  1460.                renderPromptRefPreviews();
  1461.            });
  1462.  
  1463.            item.addEventListener('dragstart', (e) => { draggedIndex = index; e.dataTransfer.effectAllowed = 'move'; setTimeout(() => item.classList.add('dragging'), 0); });
  1464.            item.addEventListener('dragend', () => { item.classList.remove('dragging'); draggedIndex = null; document.querySelectorAll('.ref-item').forEach(el => el.classList.remove('drag-over-top', 'drag-over-bottom')); });
  1465.            item.addEventListener('dragover', (e) => { e.preventDefault(); if (draggedIndex === null || draggedIndex === index) return; const rect = item.getBoundingClientRect(); if (e.clientY - rect.top < rect.height / 2) { item.classList.add('drag-over-top'); item.classList.remove('drag-over-bottom'); } else { item.classList.add('drag-over-bottom'); item.classList.remove('drag-over-top'); } });
  1466.            item.addEventListener('dragleave', () => item.classList.remove('drag-over-top', 'drag-over-bottom'));
  1467.            item.addEventListener('drop', (e) => { e.preventDefault(); item.classList.remove('drag-over-top', 'drag-over-bottom'); if (draggedIndex === null || draggedIndex === index) return; const rect = item.getBoundingClientRect(); let insertIndex = (e.clientY - rect.top) < rect.height / 2 ? index : index + 1; if (draggedIndex < insertIndex) insertIndex--; const [movedImage] = referenceMedia.splice(draggedIndex, 1); referenceMedia.splice(insertIndex, 0, movedImage); renderRefList(); syncPromptBackdrop(); });
  1468.            item.querySelector('.ref-close-btn').addEventListener('click', () => {
  1469.                const removed = referenceMedia.splice(index, 1)[0];
  1470.                if (removed && removed.blobUrl) URL.revokeObjectURL(removed.blobUrl);
  1471.                renderRefList(); syncPromptBackdrop(); updateWarnings();
  1472.            });
  1473.            els.refList.appendChild(item);
  1474.        });
  1475.        saveRefs();
  1476.        renderPromptRefPreviews();
  1477.    }
  1478.  
  1479.    async function processFile(file) {
  1480.        const isVid = file.type.startsWith('video/'); const isImg = file.type.startsWith('image/');
  1481.        if (!isVid && !isImg) return;
  1482.        if (isVid && (els.action.value === 'gen_image' || els.action.value === 'gen_video')) { els.action.value = 'edit_video'; updateActionUI(); updateGenSettingsMemory(); }
  1483.        else if (isImg && (els.action.value === 'edit_video' || els.action.value === 'extend_video')) { els.action.value = 'gen_image'; updateActionUI(); updateGenSettingsMemory(); }
  1484.  
  1485.        const max = getMaxMedia(); const activeCount = referenceMedia.filter(m => m.active).length;
  1486.        let thumbBase64 = null; if (isVid) thumbBase64 = await generateVideoThumbnail(file);
  1487.  
  1488.        const blobUrl = URL.createObjectURL(file);
  1489.        const reader = new FileReader();
  1490.        reader.onload = async (event) => {
  1491.            const b64 = event.target.result;
  1492.            let w = 0, h = 0;
  1493.            if (isVid && file._w && file._h) { w = file._w; h = file._h; }
  1494.            else if (isImg) {
  1495.                try {
  1496.                    const dims = await new Promise(r => { const i = new Image(); i.onload = () => r({w: i.width, h: i.height}); i.onerror = () => r({w: 0, h: 0}); i.src = blobUrl; });
  1497.                    w = dims.w; h = dims.h;
  1498.                } catch(e) {}
  1499.            }
  1500.            referenceMedia.unshift({ id: Date.now() + Math.random(), base64: b64, blobUrl: blobUrl, isVideo: isVid, active: activeCount < max, thumb: thumbBase64, fileName: file.name, w, h });
  1501.            renderRefList();
  1502.        };
  1503.        reader.readAsDataURL(file);
  1504.    }
  1505.  
  1506.    function setupMediaAndActionUI() {
  1507.        drawTicks('ticks-batch', MAPS.batch);
  1508.  
  1509.        (async () => {
  1510.            try {
  1511.                let storedRefs = await DBManager.getRefs();
  1512.                if (!storedRefs) {
  1513.                    const legacyRefs = localStorage.getItem('xai_api_refs');
  1514.                    if (legacyRefs) {
  1515.                        storedRefs = JSON.parse(legacyRefs);
  1516.                        localStorage.removeItem('xai_api_refs');
  1517.                    }
  1518.                }
  1519.                if (Array.isArray(storedRefs)) {
  1520.                    referenceMedia = storedRefs;
  1521.                    referenceMedia.forEach(m => {
  1522.                        if (m.base64) m.blobUrl = base64ToBlobUrl(m.base64);
  1523.                    });
  1524.                    updateActionUI();
  1525.                    renderRefList();
  1526.                }
  1527.            } catch(e) {}
  1528.        })();
  1529.  
  1530.        els.highResBtn.addEventListener('click', () => {
  1531.            isHighRes = !isHighRes;
  1532.            els.highResBtn.className = isHighRes ? 'toggle-btn active' : 'toggle-btn';
  1533.            els.highResBtn.innerText = isHighRes ? 'ON (2K / 720p)' : 'OFF (1K / 480p)';
  1534.            updateGenSettingsMemory();
  1535.        });
  1536.        els.n.addEventListener('input', () => { syncSlider(els.n, document.getElementById('val-batch'), MAPS.batch, v => v + (v===1?' Image':' Images')); updateGenSettingsMemory(); });
  1537.        els.duration.addEventListener('input', () => { syncSlider(els.duration, document.getElementById('val-duration'), activeDurationMap, v => v + ' Seconds'); updateGenSettingsMemory(); updateWarnings(); });
  1538.        els.loopsSlider.addEventListener('input', e => { els.loopsNum.value = e.target.value; updateGenSettingsMemory(); });
  1539.        els.loopsNum.addEventListener('input', e => { let v = parseInt(e.target.value) || 1; els.loopsSlider.value = Math.min(v, 20); updateGenSettingsMemory(); });
  1540.  
  1541.        els.action.addEventListener('change', () => { updateActionUI(); updateGenSettingsMemory(); });
  1542.  
  1543.        try {
  1544.            const storedGen = JSON.parse(localStorage.getItem('xai_api_gen_settings') || '{}');
  1545.            if (storedGen.action) els.action.value = storedGen.action;
  1546.            updateActionUI();
  1547.  
  1548.            if (typeof storedGen.isHighRes !== 'undefined') isHighRes = storedGen.isHighRes;
  1549.            else if (storedGen.resImg === '1k' || storedGen.resVid === '480p') isHighRes = false;
  1550.  
  1551.            els.highResBtn.className = isHighRes ? 'toggle-btn active' : 'toggle-btn';
  1552.            els.highResBtn.innerText = isHighRes ? 'ON (2K / 720p)' : 'OFF (1K / 480p)';
  1553.  
  1554.            if (storedGen.n) els.n.value = findMapIndex(MAPS.batch, storedGen.n);
  1555.  
  1556.            if (storedGen.duration) { let idx = activeDurationMap.indexOf(parseInt(storedGen.duration)); els.duration.value = idx !== -1 ? idx : 0; }
  1557.            if (storedGen.loops) { els.loopsNum.value = storedGen.loops; els.loopsSlider.value = Math.min(storedGen.loops, 20); }
  1558.  
  1559.            syncSlider(els.n, document.getElementById('val-batch'), MAPS.batch, v => v + (v===1?' Image':' Images'));
  1560.            syncSlider(els.duration, document.getElementById('val-duration'), activeDurationMap, v => v + ' Seconds');
  1561.        } catch(e) {}
  1562.  
  1563.        updateUIForEditMedia = (mediaUrl, isVideo, fileName, actionOverride) => {
  1564.            referenceMedia.forEach(m => m.active = false);
  1565.            const blobUrl = base64ToBlobUrl(mediaUrl);
  1566.            referenceMedia.unshift({ id: Date.now() + Math.random(), base64: mediaUrl, blobUrl: blobUrl, isVideo: isVideo, active: true, thumb: isVideo ? null : blobUrl, fileName: fileName || (isVideo ? 'Edited_Video.mp4' : 'Edited_Image.png'), w:0, h:0 });
  1567.            els.action.value = actionOverride || (isVideo ? 'edit_video' : 'gen_image');
  1568.            updateActionUI(); updateGenSettingsMemory(); renderRefList(); saveRefs();
  1569.        };
  1570.  
  1571.        els.uploadPlaceholder.addEventListener('click', () => { els.fileInput.click(); });
  1572.        els.fileInput.addEventListener('change', (e) => { Array.from(e.target.files).forEach(processFile); els.fileInput.value = ''; });
  1573.        document.addEventListener('paste', (e) => { if (e.target && (e.target.id === 'payload-code' || e.target.id === 'xai-api-prompt' || e.target.id === 'set-custom-css' || e.target.tagName === 'INPUT' && e.target.type !== 'range')) return; const items = e.clipboardData.items; for (let i = 0; i < items.length; i++) if (items[i].type.indexOf('image') !== -1) processFile(items[i].getAsFile()); });
  1574.        els.dropzone.addEventListener('dragover', (e) => { e.preventDefault(); els.dropzone.classList.add('drag-over'); });
  1575.        els.dropzone.addEventListener('dragleave', () => els.dropzone.classList.remove('drag-over'));
  1576.        els.dropzone.addEventListener('drop', (e) => { e.preventDefault(); els.dropzone.classList.remove('drag-over'); if (e.dataTransfer.files) Array.from(e.dataTransfer.files).forEach(processFile); });
  1577.  
  1578.                // ─── Кнопка очистки всех референсов (v39.2 patch) ─────────────────
  1579.        const refPanelHeader = els.refTitle.parentElement; // div.panel
  1580.        refPanelHeader.style.display = 'flex';
  1581.        refPanelHeader.style.alignItems = 'center';
  1582.        refPanelHeader.style.justifyContent = 'space-between';
  1583.  
  1584.        const clearRefsBtn = document.createElement('button');
  1585.        clearRefsBtn.style.cssText = `
  1586.            background: #ef4444;
  1587.            color: #fff;
  1588.            border: none;
  1589.            border-radius: 4px;
  1590.            padding: 3px 10px;
  1591.            font-size: 11px;
  1592.            font-weight: 600;
  1593.            cursor: pointer;
  1594.            display: flex;
  1595.            align-items: center;
  1596.            gap: 4px;
  1597.            margin-left: auto;
  1598.        `;
  1599.        clearRefsBtn.innerHTML = `🗑️ <span>Clear All</span>`;
  1600.        clearRefsBtn.title = 'Очистить все Reference Media';
  1601.        clearRefsBtn.addEventListener('click', clearAllReferences);
  1602.  
  1603.        // Добавляем кнопку в заголовок панели
  1604.        els.refTitle.parentElement.appendChild(clearRefsBtn);
  1605.  
  1606.        // Обновляем заголовок чтобы он не растягивался
  1607.        els.refTitle.style.marginRight = 'auto';
  1608.    }
  1609.  
  1610.    // ─── GENERATION & API CORE LOGIC ─────────────────────────────────────────
  1611.    function getMediaDimensions(base64, isVideo) {
  1612.        return new Promise(resolve => {
  1613.            if (isVideo) {
  1614.                const vid = document.createElement('video'); vid.onloadedmetadata = () => resolve({w: vid.videoWidth, h: vid.videoHeight}); vid.onerror = () => resolve({w: 0, h: 0}); vid.src = base64;
  1615.            } else {
  1616.                const img = new Image(); img.onload = () => resolve({w: img.width, h: img.height}); img.onerror = () => resolve({w: 0, h: 0}); img.src = base64;
  1617.            }
  1618.        });
  1619.    }
  1620.  
  1621.    async function buildPayloadData(dynamicPromptText) {
  1622.        const action = els.action.value; const payload = { model: action === 'gen_image' ? "grok-imagine-image" : "grok-imagine-video", prompt: dynamicPromptText };
  1623.        let ar = currentAr.toLowerCase(); const activeRefs = referenceMedia.filter(m => m.active);
  1624.  
  1625.        if (currentAr === 'By First Ref') {
  1626.            if (activeRefs.length > 0) {
  1627.                let ref = activeRefs[0];
  1628.                if (!ref.w || !ref.h) { const dims = await getMediaDimensions(ref.base64, ref.isVideo); ref.w = dims.w; ref.h = dims.h; saveRefs(); }
  1629.                if (ref.w && ref.h) {
  1630.                    const targetRatio = ref.w / ref.h; let bestMatch = '1:1'; let minDiff = Infinity;
  1631.                    arData.forEach(a => {
  1632.                        if (a.isAuto || a.isRef) return;
  1633.                        if (action === 'gen_video' && a.imageOnly ) return;
  1634.                        const r = a.w / a.h; const diff = Math.abs(r - targetRatio);
  1635.                        if (diff < minDiff) { minDiff = diff; bestMatch = a.label; }
  1636.                    });
  1637.                    ar = bestMatch.toLowerCase(); addLog('INFO', 'Calculated AR by First Ref', { targetRatio, selectedAr: ar });
  1638.                } else ar = 'auto';
  1639.            } else ar = 'auto';
  1640.        }
  1641.  
  1642.        if (action === 'gen_image') {
  1643.            payload.n = MAPS.batch[els.n.value]; payload.resolution = isHighRes ? "2k" : "1k"; payload.response_format = "b64_json";
  1644.            if (ar !== 'auto') payload.aspect_ratio = ar;
  1645.            if (activeRefs.length > 0) {
  1646.                if (activeRefs.length === 1) {
  1647.                    if (appSettings.referenceARTrick && ar != 'auto') payload.images = [{ type: "image_url", url: activeRefs[0].base64 }, { type: "image_url", url: activeRefs[0].base64 }];
  1648.                    else payload.image = { url: activeRefs[0].base64 };
  1649.                }
  1650.                else payload.images = activeRefs.map(m => ({ type: "image_url", url: m.base64 }));
  1651.            }
  1652.        } else if (action === 'gen_video') {
  1653.            payload.duration = activeDurationMap[els.duration.value]; payload.resolution = isHighRes ? "720p" : "480p";
  1654.            if (['3:4', '4:3', '9:16', '16:9', '2:3', '3:2'].includes(ar)) payload.aspect_ratio = ar;
  1655.            if ( activeRefs.length > 0 ) {
  1656.                if ( payload.duration > 10 ) {
  1657.                    payload.image = { url: activeRefs[0].base64 };
  1658.                } else {
  1659.                    if (activeRefs.length === 1) {
  1660.                        if (appSettings.referenceARTrick && ar != 'auto') {
  1661.                            payload.reference_images = [{ type: "image_url", url: activeRefs[0].base64 }, { type: "image_url", url: activeRefs[0].base64 }];
  1662.                        }
  1663.                        else {
  1664.                            payload.image = { url: activeRefs[0].base64 };
  1665.                        }
  1666.                    }
  1667.                    else {
  1668.                        payload.reference_images = activeRefs.map(m => ({ url: m.base64 }));
  1669.                    }
  1670.                }
  1671.            }
  1672.        } else if (action === 'edit_video') {
  1673.            if (activeRefs.length > 0) payload.video = { url: activeRefs[0].base64 };
  1674.        } else if (action === 'extend_video') {
  1675.            payload.duration = activeDurationMap[els.duration.value];
  1676.            if (activeRefs.length > 0) payload.video = { url: activeRefs[0].base64 };
  1677.        }
  1678.        return payload;
  1679.    }
  1680.  
  1681.    function injectBase64(editedObj, originalObj) {
  1682.        if (!editedObj || typeof editedObj !== 'object') return;
  1683.        for (let key in editedObj) {
  1684.            if (typeof editedObj[key] === 'string' && editedObj[key] === '[BASE64_TRUNCATED_FOR_PREVIEW]') { if (originalObj && originalObj[key]) editedObj[key] = originalObj[key]; }
  1685.            else if (typeof editedObj[key] === 'object') injectBase64(editedObj[key], originalObj ? originalObj[key] : null);
  1686.        }
  1687.    }
  1688.  
  1689.    function updateStatusText( text, color, className ) {
  1690.        els.status.innerText = text;
  1691.        els.status.style.color = color || "#cbd5e1";
  1692.        if (className !== undefined) els.status.className = className;
  1693.    }
  1694.  
  1695.    async function executeGenerationSingle(endpoint, payload, isImageAction, statusEl, currentLoop, totalLoops, galleryEl) {
  1696.        let loopPrefix = totalLoops > 1 ? `[Run ${currentLoop}/${totalLoops}] ` : '';
  1697.        updateStatusText(`${loopPrefix}Processing request...`, "", 'status-pulsing');
  1698.        addLog('INFO', `Starting generation loop ${currentLoop}/${totalLoops}`, { endpoint, isImageAction });
  1699.        currentAbortController = new AbortController(); let attempts = 0; let success = false; let rateLimitFails = 0;
  1700.  
  1701.        try {
  1702.            while (attempts < appSettings.retries && !success && !isRequestCancelled) {
  1703.                try {
  1704.                    updateStatusText(`${loopPrefix}Sending request to xAI...`);
  1705.                    const fullEndpoint = getApiUrl(endpoint);
  1706.                    addLog('NETWORK_REQ', `POST ${fullEndpoint}`, payload);
  1707.                    const response = await fetch(fullEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), signal: currentAbortController.signal });
  1708.  
  1709.                    if (response.status === 503 || response.status === 500 || response.status === 502) {
  1710.                        attempts++;
  1711.                        updateStatusText(`${loopPrefix}Busy (Error ${response.status}). Retrying... [${attempts}/${appSettings.retries}]`);
  1712.                        addLog('WARN', `Gateway error ${response.status}. Retrying.`);
  1713.                        await sleep(Math.floor(Math.random() * (appSettings.delayMax - appSettings.delayMin + 1)) + appSettings.delayMin); continue; }
  1714.                    if (response.status === 401 || response.status === 403) {
  1715.                        updateStatusText(`${loopPrefix}Session Expired (${response.status}). Requesting new cookies...`, "", "#d97706");
  1716.                        await nuclearSessionReset();
  1717.                        await sleep(Math.floor(Math.random() * (appSettings.delayMax - appSettings.delayMin + 1)) + appSettings.delayMin); continue;
  1718.                    }
  1719.                    if (!response.ok) {
  1720.                        let errMsg = `HTTP ${response.status}`; let errData = null;
  1721.                        try { errData = await response.json(); addLog('NETWORK_ERR', `Response error JSON`, { status: response.status, data: errData });
  1722.                            if (errData.error && errData.error.message) errMsg += `\n${errData.error.message}`; else if (errData.error && typeof errData.error === 'string') errMsg += `\n${errData.error}`;
  1723.                            if (errData.code) errMsg += `\nCode: ${errData.code}`; if (errData.detail) errMsg += `\n${typeof errData.detail === 'string' ? errData.detail : JSON.stringify(errData.detail)}`;
  1724.                        } catch(e) { addLog('NETWORK_ERR', `Failed to parse error response`, { status: response.status }); }
  1725.                        throw new Error(errMsg);
  1726.                    }
  1727.  
  1728.                    const data = await response.json(); addLog('NETWORK_RES', `Successful response`, data);
  1729.  
  1730.                    if (isImageAction) {
  1731.                        if (data && data.data && Array.isArray(data.data)) {
  1732.                            success = true;
  1733.                            updateStatusText( `${loopPrefix}Processing final images...`, "#10b981", "" );
  1734.                            await Promise.all(data.data.map((imgObj, index) => processAndRenderImage(imgObj, payload.prompt, index + 1, data.data.length, galleryEl)));
  1735.                            updateStatusText( "Ready", "#cbd5e1" );
  1736.                        } else throw new Error("Invalid response format.");
  1737.                    } else {
  1738.                        const reqId = data.request_id; if (!reqId) throw new Error(data.error ? (data.error.message || JSON.stringify(data.error)) : "No Request ID returned.");
  1739.                        let videoReady = false; let pollCount = 0; const MAX_POLLS = Math.max(1, Math.ceil(appSettings.videoPollTimeout / 5));
  1740.                        while (!videoReady && !isRequestCancelled) {
  1741.                            pollCount++; if (pollCount > MAX_POLLS) throw new Error("Timeout: Video likely dropped by filters or stuck in queue."); await sleep(5000); if (isRequestCancelled) break;
  1742.                            addLog('POLL_REQ', `Polling video ID: ${reqId}`, { pollCount, MAX_POLLS });
  1743.                            const pollRes = await fetch(getApiUrl(`/v1/videos/${reqId}`), { signal: currentAbortController.signal });
  1744.                            if (!pollRes.ok) {
  1745.                                if (pollRes.status === 401 || pollRes.status === 403) {
  1746.                                    addLog('POLL_WARN', `Poll got 401/403. Cross-tab wipe likely. Restoring...`);
  1747.                                    updateStatusText(`${loopPrefix}Polling interrupted (401). Restoring session...`);
  1748.                                    try { await fetch('https://console.x.ai/playground/imagine', { credentials: 'include', cache: 'no-store' }); } catch(e) {} await sleep(3000); continue;
  1749.                                 }
  1750.                                 if (pollRes.status >= 400 && pollRes.status < 500) {
  1751.                                     let errData; try { errData = await pollRes.json(); } catch(e) {} addLog('POLL_ERR', `Poll failed HTTP ${pollRes.status}`, errData);
  1752.                                     if (pollRes.status === 404) throw new Error("video lost (404). Session wiped by another tab.");
  1753.                                     if (errData && errData.error) throw new Error(typeof errData.error.message === 'string' ? errData.error.message : JSON.stringify(errData.error));
  1754.                                 }
  1755.                                 continue;
  1756.                             }
  1757.                             const pollData = await pollRes.json(); addLog('POLL_RES', `Poll Result`, pollData);
  1758.                             if (pollData.error) throw new Error(`API Error: ${pollData.error.message || JSON.stringify(pollData.error)}`);
  1759.                             const state = (pollData.status || pollData.state || 'processing').toLowerCase(); let progressText = typeof pollData.progress === 'number' ? ` ${pollData.progress}%` : "";
  1760.                             updateStatusText(`${loopPrefix}Polling video (Status: ${state}${progressText})...[${pollCount}/${MAX_POLLS}]`);
  1761.  
  1762.                             if (state === 'done' || state === 'completed') {
  1763.                                 videoReady = true;
  1764.                                 success = true;
  1765.                                 updateStatusText("Ready", "#cbd5e1", "")
  1766.                                 renderVideoToGallery(pollData.video.url, payload.prompt, galleryEl);
  1767.                             } else if (['failed', 'expired', 'rejected', 'blocked', 'moderated', 'nsfw'].includes(state) || pollData.is_sensitive) {
  1768.                                 if (pollData.video && pollData.video.url) {
  1769.                                     videoReady = true;
  1770.                                     success = true;
  1771.                                     updateStatusText(`${loopPrefix}Warning: Flagged as ${state}, but recovered!`, "#d97706", "")
  1772.                                     renderVideoToGallery(pollData.video.url, payload.prompt, galleryEl);
  1773.                                     addLog('WARN', `Video flagged but recovered`, pollData);
  1774.                                 } else throw new Error(`Generation halted. Reason: ${state}`);
  1775.                             }
  1776.                         }
  1777.                     }
  1778.                } catch (err) {
  1779.                     const errStr = err.message.toLowerCase(); addLog('ERROR', `Generation logic exception`, { message: err.message });
  1780.                     if (err.name === 'AbortError' || isRequestCancelled) {
  1781.                         updateStatusText("Request Cancelled.", "#ef4444", "");
  1782.                         return false;
  1783.                     }
  1784.                     let isLongRateLimit = false; let isShortRateLimit = false;
  1785.                     if (errStr.includes("requests per second (actual/limit)")) {
  1786.                         const secMatch = errStr.match(/requests per second \(actual\/limit\):\s*(\d+)\s*\/\s*(\d+)/); const minMatch = errStr.match(/requests per minute \(actual\/limit\):\s*(\d+)\s*\/\s*(\d+)/);
  1787.                         if (minMatch && minMatch[1] === minMatch[2]) isLongRateLimit = true; else if (secMatch && secMatch[1] === secMatch[2]) isShortRateLimit = true; else isLongRateLimit = true;
  1788.                     } else if (errStr.includes("rate limit") || errStr.includes("resource has been exhausted") || errStr.includes("http 429") || errStr.includes("http 422")) isLongRateLimit = true;
  1789.  
  1790.                     if (isShortRateLimit || isLongRateLimit) {
  1791.                         if (appSettings.maximumGreedMode) {
  1792.                             updateStatusText(`${loopPrefix}Rate Limit Hit (429). Refreshing tab...`, "#ef4444", "" );
  1793.                             await nuclearSessionReset(totalLoops - currentLoop + 1); const u = new URL(window.location.href.split('?')[0]); u.searchParams.set('_rst', Date.now()); window.location.replace(u.toString()); await sleep(10000);
  1794.                         } else {
  1795.                             updateStatusText(`${loopPrefix}Rate Limit Hit (429). Resetting cookies...`);
  1796.                             await nuclearSessionReset();
  1797.                             if (isShortRateLimit) {
  1798.                                 attempts++; if (attempts >= appSettings.retries) {
  1799.                                     updateStatusText(`${loopPrefix}Failed: Max retries reached after per-second rate limits.`, "#ef4444", "");
  1800.                                     break;
  1801.                                 }
  1802.                                 await sleep(Math.floor(Math.random() * (appSettings.delayMax - appSettings.delayMin + 1)) + appSettings.delayMin);
  1803.                             } else {
  1804.                                 attempts++;
  1805.                                 if (attempts >= appSettings.retries) {
  1806.                                     updateStatusText(`${loopPrefix}Failed: Max retries reached after rate limits.`, "#ef4444", "");
  1807.                                     break;
  1808.                                 }
  1809.                                 let delaySeconds = parseInt(appSettings.rateLimitDelay) || 60; let countdown = delaySeconds;
  1810.                                 if ((rateLimitFails++) < 1) { await sleep(1000); continue; }
  1811.                                 rateLimitFails = 0; toggleKeepAwake(true);
  1812.                                 while(countdown > 0 && !isRequestCancelled) {
  1813.                                     updateStatusText(`${loopPrefix}Rate Limit Hit. Retrying in ${countdown}s... [${attempts}/${appSettings.retries}]`);
  1814.                                     await sleep(1000); countdown--;
  1815.                                 }
  1816.                                 toggleKeepAwake(false);
  1817.                             }
  1818.                             if (isRequestCancelled) return false; continue;
  1819.                         }
  1820.                     } else if (errStr.includes("video lost") || ((errStr.includes("timeout: video") || errStr.includes("generation halted.") || errStr.includes("video rejected")) && appSettings.autoRetryStuckVideo)) {
  1821.                         attempts++;
  1822.                         if (attempts >= appSettings.retries) {
  1823.                             updateStatusText(`${loopPrefix}Failed: Max retries reached for stuck video.`, "#ef4444", "");
  1824.                             break;
  1825.                         }
  1826.                         updateStatusText(`${loopPrefix}Video Stuck/Failed. Auto-retrying... [${attempts}/${appSettings.retries}]`);
  1827.                         await sleep(Math.floor(Math.random() * (appSettings.delayMax - appSettings.delayMin + 1)) + appSettings.delayMin);
  1828.                         if (payload.prompt) payload.prompt += "\u200B"; continue;
  1829.                     } else {
  1830.                         updateStatusText(`${loopPrefix}Error: ${err.message}`, "#ef4444", "");
  1831.                         break;
  1832.                     }
  1833.                 }
  1834.             }
  1835.         } finally { toggleKeepAwake(false); }
  1836.  
  1837.         if (!success && !isRequestCancelled) {
  1838.             if (!statusEl.innerText.includes("Failed:") && !statusEl.innerText.includes("Error:") && !statusEl.innerText.includes("Reloading")) updateStatusText( `${loopPrefix}Failed after ${attempts} retries.` );
  1839.             updateStatusText( statusEl.innerText, "#ef4444", "" );
  1840.             return false;
  1841.         }
  1842.         return success;
  1843.     }
  1844.  
  1845.     function createBatchContainer(promptStr) {
  1846.         const batchContainer = document.createElement('div');
  1847.         batchContainer.className = 'batch-container';
  1848.         const ts = makeTimestamp();
  1849.         batchContainer.innerHTML = `
  1850.             <div class="batch-header" title="Click to copy prompt to input">
  1851.                 <span>${ts}</span>
  1852.                 <span style="font-weight: normal; opacity: 0.8; font-style: italic; max-width: 65%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${promptStr.replace(/"/g, '&quot;')}">${promptStr}</span>
  1853.            </div>
  1854.            <div class="batch-grid"></div>
  1855.        `;
  1856.        batchContainer.querySelector('.batch-header').addEventListener('click', () => {
  1857.            setPromptVal(promptStr);
  1858.        });
  1859.  
  1860.        const placeholder = els.currentImages.querySelector('.ready-placeholder');
  1861.        if (placeholder) placeholder.remove();
  1862.  
  1863.        els.currentImages.prepend(batchContainer);
  1864.        return batchContainer.querySelector('.batch-grid');
  1865.    }
  1866.  
  1867.    function setupGenerationUI() {
  1868.        els.previewBtn.addEventListener('click', async () => {
  1869.            els.previewBtn.disabled = true; els.previewBtn.innerHTML = `<div class="loading-spinner" style="border-top-color:#64748b; width:12px; height:12px;"></div>`;
  1870.            fullPayloadMemory = await buildPayloadData(parseDynamicPrompt(els.prompt.value.trim()));
  1871.            const displayP = JSON.parse(JSON.stringify(fullPayloadMemory)); const trunc = "[BASE64_TRUNCATED_FOR_PREVIEW]";
  1872.            if (displayP.image) { if (Array.isArray(displayP.image)) displayP.image.forEach(img => (img.url = trunc)); else displayP.image.url = trunc; }
  1873.            if (displayP.images && Array.isArray(displayP.images)) displayP.images.forEach(img => (img.url = trunc));
  1874.            if (displayP.reference_images && Array.isArray(displayP.reference_images)) displayP.reference_images.forEach(img => (img.url = trunc));
  1875.            if (displayP.video) displayP.video.url = trunc;
  1876.            const activeRefs = referenceMedia.filter(m => m.active); let endpoint = '/v1/images/generations';
  1877.            if (els.action.value === 'gen_image' && activeRefs.length > 0) endpoint = '/v1/images/edits';
  1878.            else if (els.action.value === 'gen_video') endpoint = '/v1/videos/generations';
  1879.            else if (els.action.value === 'edit_video') endpoint = '/v1/videos/edits';
  1880.            else if (els.action.value === 'extend_video') endpoint = '/v1/videos/extensions';
  1881.            els.payloadEndpoint.value = endpoint; els.payloadCode.value = JSON.stringify(displayP, null, 2); els.payloadModal.style.display = 'flex';
  1882.            els.previewBtn.innerHTML = `🔍 Payload`; els.previewBtn.disabled = false;
  1883.        });
  1884.  
  1885.        els.closePayloadBtn.addEventListener('click', () => els.payloadModal.style.display = 'none');
  1886.  
  1887.        els.sendCustomBtn.addEventListener('click', async () => {
  1888.            initAudio(); let customPayload; try { customPayload = JSON.parse(els.payloadCode.value); } catch(e) { return alert("Invalid JSON format in textarea!"); }
  1889.            injectBase64(customPayload, fullPayloadMemory);
  1890.            const endpoint = els.payloadEndpoint.value.trim(); const isImage = customPayload.model === "grok-imagine-image";
  1891.  
  1892.            const currentBatchGrid = createBatchContainer("Custom Payload Execution");
  1893.  
  1894.            els.sendCustomBtn.disabled = true; els.sendCustomBtn.innerHTML = `<div class="btn-content"><div class="loading-spinner"></div> Processing...</div>`; isRequestCancelled = false;
  1895.            const ok = await executeGenerationSingle(endpoint, customPayload, isImage, els.customStatus, 1, 1, currentBatchGrid);
  1896.  
  1897.            if (currentBatchGrid.children.length === 0) {
  1898.                currentBatchGrid.parentElement.remove();
  1899.                if (els.currentImages.children.length === 0) els.currentImages.innerHTML = `<div class="ready-placeholder" style="color: #94a3b8; font-size: 13px; text-align: center; margin-top: 40px; font-weight: 500;">Ready to generate. Results will appear here.</div>`;
  1900.            }
  1901.  
  1902.            els.sendCustomBtn.disabled = false; els.sendCustomBtn.innerHTML = `<div class="btn-content">🚀 Send Custom Payload</div>`; if (ok) notifyUser(false); else notifyUser(true);
  1903.        });
  1904.  
  1905.        els.cancelBtn.addEventListener('click', () => {
  1906.            isRequestCancelled = true; if (currentAbortController) currentAbortController.abort(); toggleKeepAwake(false);
  1907.            els.cancelBtn.style.display = 'none'; els.previewBtn.style.display = 'flex'; els.btn.disabled = false; els.btn.innerHTML = `<div class="btn-content">Generate</div>`;
  1908.            els.status.innerText = "Request Cancelled."; els.status.className = ""; els.status.style.color = "#ef4444";
  1909.        });
  1910.  
  1911.        els.btn.addEventListener('click', async () => {
  1912.            initAudio(); const basePrompt = els.prompt.value.trim(); if (!basePrompt) return alert("Please enter a prompt.");
  1913.            addLog('UI', 'Generate button clicked', { basePrompt });
  1914.  
  1915.            const action = els.action.value; const activeRefs = referenceMedia.filter(m => m.active);
  1916.            let endpoint = '/v1/images/generations';
  1917.            if (action === 'gen_image' && activeRefs.length > 0) endpoint = '/v1/images/edits';
  1918.            else if (action === 'gen_video') endpoint = '/v1/videos/generations';
  1919.            else if (action === 'edit_video') endpoint = '/v1/videos/edits';
  1920.            else if (action === 'extend_video') endpoint = '/v1/videos/extensions';
  1921.  
  1922.            const isImage = action === 'gen_image'; const totalLoops = parseInt(els.loopsNum.value) || 1;
  1923.            els.btn.disabled = true; els.cancelBtn.style.display = 'flex'; els.previewBtn.style.display = 'none'; isRequestCancelled = false;
  1924.  
  1925.            const currentBatchGrid = createBatchContainer(basePrompt);
  1926.  
  1927.            let allSuccess = true;
  1928.            for (let i = 1; i <= totalLoops; i++) {
  1929.                if (isRequestCancelled) break; const dynamicPrompt = parseDynamicPrompt(basePrompt);
  1930.                els.btn.innerHTML = `<div class="btn-content"><div class="loading-spinner"></div> Building...</div>`;
  1931.                const payload = await buildPayloadData(dynamicPrompt);
  1932.                els.btn.innerHTML = `<div class="btn-content"><div class="loading-spinner"></div> Gen ${i}/${totalLoops}...</div>`;
  1933.                const ok = await executeGenerationSingle(endpoint, payload, isImage, els.status, i, totalLoops, currentBatchGrid);
  1934.                if (!ok) { allSuccess = false; if (appSettings.breakLoopOnFailure) break; }
  1935.            }
  1936.  
  1937.            if (currentBatchGrid.children.length === 0) {
  1938.                currentBatchGrid.parentElement.remove();
  1939.                if (els.currentImages.children.length === 0) els.currentImages.innerHTML = `<div class="ready-placeholder" style="color: #94a3b8; font-size: 13px; text-align: center; margin-top: 40px; font-weight: 500;">Ready to generate. Results will appear here.</div>`;
  1940.            }
  1941.  
  1942.            if (!isRequestCancelled) {
  1943.                els.cancelBtn.style.display = 'none'; els.previewBtn.style.display = 'flex'; els.btn.disabled = false; els.btn.innerHTML = `<div class="btn-content">Generate</div>`;
  1944.                if (allSuccess) { els.status.innerText = "All runs complete."; els.status.className = ""; els.status.style.color = "#cbd5e1"; notifyUser(false); addLog('INFO', 'All generation loops completed successfully'); }
  1945.                else { notifyUser(true); addLog('ERROR', 'Generation sequence ended with errors'); }
  1946.            }
  1947.        });
  1948.    }
  1949.  
  1950.    function checkAutoResume() {
  1951.        const resumeLoops = sessionStorage.getItem('xai_resume_loops');
  1952.        if (resumeLoops) {
  1953.            sessionStorage.removeItem('xai_resume_loops'); const loops = parseInt(resumeLoops);
  1954.            if (loops > 0) {
  1955.                els.loopsNum.value = loops; els.loopsSlider.value = Math.min(loops, 20); updateGenSettingsMemory();
  1956.                els.status.innerText = `Auto-resuming ${loops} runs...`; els.status.className = "status-pulsing";
  1957.                setTimeout(() => { if (!els.btn.disabled) els.btn.click(); }, 2500);
  1958.            }
  1959.        }
  1960.    }
  1961.  
  1962.    function bindLogic() {
  1963.        setupModalsAndAuxUI();
  1964.        setupPromptListeners();
  1965.        setupAspectRatioUI();
  1966.        setupMediaAndActionUI();
  1967.        setupGenerationUI();
  1968.        checkAutoResume();
  1969.  
  1970.        // ─── MEMORY LEAK FIX v39.4 (настраиваемый лимит) ─────────────────
  1971.        const originalCreateBatchContainer = createBatchContainer;
  1972.        createBatchContainer = function(promptStr) {
  1973.            const grid = originalCreateBatchContainer(promptStr);
  1974.            setTimeout(pruneOldResults, 1200);
  1975.            return grid;
  1976.        };
  1977.  
  1978.        // Кнопка быстрой очистки галереи
  1979.        const clearBtn = document.createElement('div');
  1980.        clearBtn.className = 'shortcut-btn';
  1981.        clearBtn.innerHTML = '🧹';
  1982.        clearBtn.title = 'Clear Gallery + Free Memory';
  1983.        clearBtn.style.fontSize = '22px';
  1984.        clearBtn.addEventListener('click', () => {
  1985.            if (confirm('Очистить всю галерею и освободить память?')) {
  1986.                els.currentImages.innerHTML = `<div class="ready-placeholder" style="color: #94a3b8; font-size: 13px; text-align: center; margin-top: 40px; font-weight: 500;">Ready to generate. Results will appear here.</div>`;
  1987.                pruneOldResults();
  1988.                addLog('INFO', 'Gallery manually cleared');
  1989.            }
  1990.        });
  1991.        document.querySelector('.top-shortcuts').appendChild(clearBtn);
  1992.  
  1993.        addLog('INFO', `Memory leak protection v39.4 activated (max ${appSettings.maxGalleryBatches} batches)`);
  1994.  
  1995.        // Дублирующая кнопка в топ-шорткатах
  1996.        const topClearRefs = document.createElement('div');
  1997.        topClearRefs.className = 'shortcut-btn';
  1998.        topClearRefs.innerHTML = '🗑️';
  1999.        topClearRefs.title = 'Clear ALL Reference Media';
  2000.        topClearRefs.style.fontSize = '20px';
  2001.        topClearRefs.addEventListener('click', () => {
  2002.            clearAllReferences();
  2003.        });
  2004.        document.querySelector('.top-shortcuts').appendChild(topClearRefs);
  2005.    }
  2006.  
  2007.    async function processAndRenderImage(imgObj, originalPrompt, num, total, galleryEl) {
  2008.        if (!imgObj.b64_json) return;
  2009.        const mime = imgObj.mime_type || "image/png"; const b64 = imgObj.b64_json; const pngDataUri = `data:${mime};base64,${b64}`;
  2010.        const finalPrompt = imgObj.revised_prompt || originalPrompt; let finalDataUri = pngDataUri; let filename = `Grok 2K - ${makeTimestamp().replace(/:/g, '-')} - ${num}of${total}.png`;
  2011.        if (!appSettings.saveAsPng) { finalDataUri = await convertToJpegWithExif(pngDataUri, finalPrompt); filename = `Grok 2K - ${makeTimestamp().replace(/:/g, '-')} - ${num}of${total}.jpg`; }
  2012.        const card = document.createElement('div'); card.className = 'current-card';
  2013.        card.innerHTML = `
  2014.            <div class="img-wrapper">
  2015.                <img src="${finalDataUri}" class="zoomable" data-filename="${filename}" title="Click to view fullscreen">
  2016.                <a href="${finalDataUri}" download="${filename}" class="overlay-btn overlay-dl-btn" title="Download Full File">${DOWNLOAD_ICON}</a>
  2017.                <button class="overlay-btn overlay-edit-btn" title="Edit this Image">${EDIT_ICON}</button>
  2018.                <button class="overlay-btn overlay-video-btn" title="Make Video">${VIDEO_ICON}</button>
  2019.            </div>
  2020.            <div class="prompt-dropdown" title="Click to copy prompt">${finalPrompt}</div>`;
  2021.  
  2022.        card.querySelector('.prompt-dropdown').addEventListener('click', () => setPromptVal(finalPrompt));
  2023.        card.querySelector('.overlay-edit-btn').addEventListener('click', (e) => { e.preventDefault(); if (typeof updateUIForEditMedia === 'function') updateUIForEditMedia(finalDataUri, false, filename, 'gen_image'); });
  2024.        card.querySelector('.overlay-video-btn').addEventListener('click', (e) => { e.preventDefault(); if (typeof updateUIForEditMedia === 'function') updateUIForEditMedia(finalDataUri, false, filename, 'gen_video'); });
  2025.  
  2026.        const dlBtn = card.querySelector('.overlay-dl-btn');
  2027.        dlBtn.addEventListener('click', () => { card.classList.add('downloaded'); });
  2028.  
  2029.        galleryEl.prepend(card);
  2030.  
  2031.        if (appSettings.autoDownload) {
  2032.            setTimeout(() => {
  2033.                GM_download({ url: finalDataUri, name: filename, saveAs: false });
  2034.                card.classList.add('downloaded');
  2035.            }, 500);
  2036.        }
  2037.    }
  2038.  
  2039.    let lastVideoVolume = 1.0;
  2040.    let lastVideoMuted = null;
  2041.    function renderVideoToGallery(videoUrl, originalPrompt, galleryEl) {
  2042.        if (lastVideoMuted === null) lastVideoMuted = appSettings.videoMuted;
  2043.        const filename = `Grok Video - ${makeTimestamp().replace(/:/g, '-')}.mp4`; const auto = appSettings.videoAutoplay ? "autoplay" : ""; const muted = appSettings.videoMuted ? "muted" : "";
  2044.        const card = document.createElement('div'); card.className = 'current-card';
  2045.        card.innerHTML = `
  2046.            <div class="img-wrapper">
  2047.                <video src="${videoUrl}" controls ${auto} loop style="width: 100%; max-height: 50vh; object-fit: contain; background: #000;"></video>
  2048.                <button class="overlay-btn overlay-dl-btn force-dl" data-url="${videoUrl}" data-filename="${filename}" title="Download MP4">${DOWNLOAD_ICON}</button>
  2049.                <button class="overlay-btn overlay-edit-btn" title="Edit this Video">${EDIT_ICON}</button>
  2050.                <button class="overlay-btn overlay-extend-btn" title="Extend this Video">${EXTEND_ICON}</button>
  2051.            </div>
  2052.            <div class="prompt-dropdown" title="Click to copy prompt">${originalPrompt}</div>`;
  2053.  
  2054.        card.querySelector('.prompt-dropdown').addEventListener('click', () => setPromptVal(originalPrompt));
  2055.  
  2056.        const vidEl = card.querySelector('video');
  2057.        vidEl.volume = lastVideoVolume;
  2058.        vidEl.muted = lastVideoMuted;
  2059.        vidEl.addEventListener('volumechange', (e) => {
  2060.            lastVideoVolume = e.target.volume;
  2061.            lastVideoMuted = e.target.muted;
  2062.        });
  2063.  
  2064.        card.querySelector('.overlay-edit-btn').addEventListener('click', (e) => { e.preventDefault(); if (typeof updateUIForEditMedia === 'function') updateUIForEditMedia(videoUrl, true, filename, 'edit_video'); });
  2065.        card.querySelector('.overlay-extend-btn').addEventListener('click', (e) => { e.preventDefault(); if (typeof updateUIForEditMedia === 'function') updateUIForEditMedia(videoUrl, true, filename, 'extend_video'); });
  2066.  
  2067.        const dlBtn = card.querySelector('.overlay-dl-btn');
  2068.        dlBtn.addEventListener('click', (e) => {
  2069.            e.preventDefault(); const url = dlBtn.getAttribute('data-url'); const fname = dlBtn.getAttribute('data-filename'); const originalIcon = dlBtn.innerHTML;
  2070.            dlBtn.innerHTML = '<div class="loading-spinner" style="border-top-color: #10b981;"></div>'; dlBtn.style.pointerEvents = 'none'; card.classList.add('downloaded');
  2071.            if (typeof GM_xmlhttpRequest !== "undefined") {
  2072.                GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'blob', onload: function(response) {
  2073.                        if (response.status >= 200 && response.status < 300) {
  2074.                            const blobUrl = URL.createObjectURL(response.response); const a = document.createElement('a'); a.style.display = 'none'; a.href = blobUrl; a.download = fname; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
  2075.                        } else window.open(url, '_blank');
  2076.                        dlBtn.innerHTML = originalIcon; dlBtn.style.pointerEvents = 'auto';
  2077.                    }, onerror: function() { window.open(url, '_blank'); dlBtn.innerHTML = originalIcon; dlBtn.style.pointerEvents = 'auto'; } });
  2078.            } else { window.open(url, '_blank'); dlBtn.innerHTML = originalIcon; dlBtn.style.pointerEvents = 'auto'; }
  2079.        });
  2080.  
  2081.        galleryEl.prepend(card);
  2082.  
  2083.        if (appSettings.autoDownload) {
  2084.            setTimeout(() => {
  2085.                GM_download({ url: videoUrl, name: filename, saveAs: false });
  2086.                card.classList.add('downloaded');
  2087.            }, 500);
  2088.        }
  2089.    }
  2090.  
  2091.    if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initApp); } else { initApp(); }
  2092. })();
Add Comment
Please, Sign In to add comment