Guest User

Untitled

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