Guest User

Untitled

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