Kriso1

Grok скрипт

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