Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // ==UserScript==
- // @name xAI Pro Studio Frontend (v28 - Auto-Retry Fix & Edit Flow)
- // @namespace http://tampermonkey.net/
- // @version 28
- // @description Full-Screen UI, Exif Fix, Multi-Image Routing, Dynamic Prompts, Raw PNG Mode, Auto-Retry Moderation, Instant Edit Button
- // @match https://console.x.ai/playground/imagine*
- // @match https://console.x.ai/team/*/imagine*
- // @grant none
- // @run-at document-start
- // ==/UserScript==
- (function() {
- 'use strict';
- // ─── UTILITIES & DATA STORAGE ─────────────────────────────────────────────
- const sleep = (ms) => new Promise(r => setTimeout(r, ms));
- function pad2(n) { return String(n).padStart(2, '0'); }
- function makeTimestamp() {
- const d = new Date();
- return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}-${pad2(d.getMinutes())}-${pad2(d.getSeconds())}`;
- }
- 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>`;
- 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>`;
- // Load Settings
- let appSettings = JSON.parse(localStorage.getItem('xai_api_settings') || '{}');
- appSettings.retries = parseInt(appSettings.retries) || 20;
- appSettings.delayMin = parseInt(appSettings.delayMin) || 2000;
- appSettings.delayMax = parseInt(appSettings.delayMax) || 4000;
- if (typeof appSettings.notifications === 'undefined') appSettings.notifications = true;
- if (typeof appSettings.customCSS === 'undefined') appSettings.customCSS = "";
- if (typeof appSettings.saveAsPng === 'undefined') appSettings.saveAsPng = false;
- if (typeof appSettings.apiBaseUrl === 'undefined') appSettings.apiBaseUrl = "";
- if (typeof appSettings.videoPollTimeout === 'undefined') appSettings.videoPollTimeout = 300;
- if (typeof appSettings.autoRetryStuckVideo === 'undefined') appSettings.autoRetryStuckVideo = false;
- let favoritePrompts = JSON.parse(localStorage.getItem('xai_api_favs') || '[]');
- // API URL Helper
- function getApiUrl(path) {
- let base = appSettings.apiBaseUrl || '';
- if (base.endsWith('/')) base = base.slice(0, -1);
- if (!path.startsWith('/') && base) path = '/' + path;
- return base + path;
- }
- // ─── DYNAMIC PROMPT PARSER (SPINTAX & RANDOM SEED) ────────────────────────
- function parseDynamicPrompt(text) {
- if (!text) return text;
- let parsed = text;
- let prev;
- do {
- prev = parsed;
- parsed = parsed.replace(/\{([^{}]+)\}/g, (match, contents) => {
- if (contents.includes('|')) {
- const options = contents.split('|');
- return options[Math.floor(Math.random() * options.length)];
- }
- return match;
- });
- } while (parsed !== prev);
- parsed = parsed.replace(/@randomseed/gi, () => {
- return Math.floor(1000000000 + Math.random() * 9000000000).toString();
- });
- return parsed.trim();
- }
- // ─── BACKGROUND ALERT SYSTEM (AUDIO + TAB BLINK) ──────────────────────────
- let sharedAudioCtx = null;
- let titleBlinkInterval = null;
- const originalTitle = document.title || "xAI Pro Studio";
- function initAudio() {
- if (!sharedAudioCtx && appSettings.notifications) {
- try { sharedAudioCtx = new (window.AudioContext || window.webkitAudioContext)(); }
- catch(e) { console.warn("AudioContext not supported"); }
- }
- if (sharedAudioCtx && sharedAudioCtx.state === 'suspended') sharedAudioCtx.resume();
- }
- function notifyUser(isError = false) {
- if (!appSettings.notifications) return;
- if (sharedAudioCtx) {
- try {
- const osc = sharedAudioCtx.createOscillator();
- const gain = sharedAudioCtx.createGain();
- osc.connect(gain);
- gain.connect(sharedAudioCtx.destination);
- if (isError) {
- osc.type = 'sawtooth';
- osc.frequency.setValueAtTime(300, sharedAudioCtx.currentTime);
- osc.frequency.exponentialRampToValueAtTime(100, sharedAudioCtx.currentTime + 0.3);
- } else {
- osc.type = 'sine';
- osc.frequency.setValueAtTime(500, sharedAudioCtx.currentTime);
- osc.frequency.exponentialRampToValueAtTime(1000, sharedAudioCtx.currentTime + 0.2);
- }
- gain.gain.setValueAtTime(0.1, sharedAudioCtx.currentTime);
- gain.gain.exponentialRampToValueAtTime(0.01, sharedAudioCtx.currentTime + 0.5);
- osc.start(sharedAudioCtx.currentTime);
- osc.stop(sharedAudioCtx.currentTime + 0.5);
- } catch(e) {}
- }
- if (!document.hasFocus()) {
- if (titleBlinkInterval) clearInterval(titleBlinkInterval);
- let toggle = true;
- titleBlinkInterval = setInterval(() => {
- document.title = toggle ? (isError ? "❌ FAILED" : "✅ DONE") : originalTitle;
- toggle = !toggle;
- }, 1000);
- const clearBlink = () => {
- clearInterval(titleBlinkInterval);
- document.title = originalTitle;
- window.removeEventListener('focus', clearBlink);
- };
- window.addEventListener('focus', clearBlink);
- }
- }
- // ─── STEALTH IDENTITY RESETTER ────────────────────────────────────────────
- function stealthIdentityReset() {
- const trackingCookies =['x-anonuserid', 'mixpanel', 'mp_', 'distinct_id'];
- document.cookie.split(';').forEach(c => {
- const name = c.split('=')[0].trim();
- if (trackingCookies.some(tc => name.includes(tc))) {['.x.ai', 'console.x.ai', ''].forEach(d => {['/', '/playground/imagine', ''].forEach(p => {
- document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=${p}${d ? ';domain=' + d : ''}`;
- });
- });
- }
- });
- for (let i = localStorage.length - 1; i >= 0; i--) {
- const k = localStorage.key(i);
- if (k && !k.startsWith('xai_api_') && trackingCookies.some(tc => k.includes(tc))) {
- localStorage.removeItem(k);
- }
- }
- }
- // ─── EXIF LIBRARY LOADER & WINDOWS METADATA ENCODER ───────────────────────
- let piexifLoaded = false;
- function loadPiexif() {
- if (piexifLoaded) return Promise.resolve(window.piexif);
- return new Promise(resolve => {
- const script = document.createElement('script');
- script.src = 'https://cdn.jsdelivr.net/npm/piexifjs';
- script.onload = () => { piexifLoaded = true; resolve(window.piexif); };
- document.head.appendChild(script);
- });
- }
- function toUTF16LE(str) {
- const arr =[];
- for (let i = 0; i < str.length; i++) {
- const code = str.charCodeAt(i);
- arr.push(code & 0xFF); arr.push((code >> 8) & 0xFF);
- }
- arr.push(0, 0); return arr;
- }
- function convertToJpegWithExif(base64PngUri, prompt) {
- return new Promise((resolve, reject) => {
- const img = new Image();
- img.onload = async () => {
- const canvas = document.createElement('canvas');
- canvas.width = img.width; canvas.height = img.height;
- const ctx = canvas.getContext('2d');
- ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, canvas.width, canvas.height);
- ctx.drawImage(img, 0, 0);
- let jpegUri = canvas.toDataURL('image/jpeg', 0.95);
- if (prompt) {
- try {
- const piexif = await loadPiexif();
- const exifObj = {"0th": {}, "Exif": {}, "GPS": {}, "1st": {}, "Interop": {}};
- const safePrompt = unescape(encodeURIComponent(prompt));
- exifObj['0th'][piexif.ImageIFD.ImageDescription] = safePrompt;
- const utf16Prompt = toUTF16LE(prompt);
- exifObj['0th'][40091] = utf16Prompt;
- exifObj['0th'][40092] = utf16Prompt;
- exifObj['0th'][40093] = utf16Prompt;
- jpegUri = piexif.insert(piexif.dump(exifObj), jpegUri);
- } catch (e) {}
- }
- resolve(jpegUri);
- };
- img.onerror = reject; img.src = base64PngUri;
- });
- }
- async function extractPromptFromImage(file) {
- if (!file.type.includes('image')) return null;
- try {
- const piexif = await loadPiexif();
- return new Promise((resolve) => {
- const reader = new FileReader();
- reader.onload = (e) => {
- try {
- const exifData = piexif.load(e.target.result);
- let prompt = exifData['0th'] && exifData['0th'][piexif.ImageIFD.ImageDescription];
- if (Array.isArray(prompt)) prompt = String.fromCharCode.apply(null, prompt).replace(/\0/g, '');
- if (prompt) { try { prompt = decodeURIComponent(escape(prompt)); } catch(e) {} }
- if (!prompt && exifData['0th'] && exifData['0th'][40091]) {
- const xpTitleArr = exifData['0th'][40091];
- let str = '';
- for (let i = 0; i < xpTitleArr.length; i += 2) {
- const charCode = xpTitleArr[i] | (xpTitleArr[i+1] << 8);
- if (charCode === 0) break;
- str += String.fromCharCode(charCode);
- }
- prompt = str;
- }
- resolve(prompt ? prompt.trim() : null);
- } catch (err) { resolve(null); }
- };
- reader.readAsDataURL(file);
- });
- } catch (e) { return null; }
- }
- // ─── VIDEO THUMBNAIL EXTRACTOR ────────────────────────────────────────────
- function generateVideoThumbnail(file) {
- return new Promise((resolve) => {
- const video = document.createElement('video');
- video.preload = 'metadata';
- video.muted = true;
- video.playsInline = true;
- const url = URL.createObjectURL(file);
- video.src = url;
- let isSeeked = false;
- video.onloadeddata = () => {
- video.currentTime = Math.min(0.5, video.duration / 2 || 0);
- };
- video.onseeked = () => {
- if (isSeeked) return;
- isSeeked = true;
- try {
- const canvas = document.createElement('canvas');
- canvas.width = video.videoWidth || 140;
- canvas.height = video.videoHeight || 140;
- const ctx = canvas.getContext('2d');
- ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
- URL.revokeObjectURL(url);
- resolve(canvas.toDataURL('image/jpeg', 0.8));
- } catch(e) {
- URL.revokeObjectURL(url);
- resolve(null);
- }
- };
- video.onerror = () => {
- URL.revokeObjectURL(url);
- resolve(null);
- };
- setTimeout(() => { if (!isSeeked) { URL.revokeObjectURL(url); resolve(null); } }, 2000);
- });
- }
- // ─── ASPECT RATIO DATA ────────────────────────────────────────────────────
- const arData =[
- { label: 'Auto', w: 1, h: 1, isAuto: true },
- { label: '1:1', w: 1, h: 1 }, { label: '3:4', w: 3, h: 4 }, { label: '4:3', w: 4, h: 3 },
- { label: '9:16', w: 9, h: 16 }, { label: '16:9', w: 16, h: 9 }, { label: '2:3', w: 2, h: 3 },
- { 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 },
- { 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 }
- ];
- function createArIcon(w, h, isAuto) {
- 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>`;
- const scale = 12 / Math.max(w, h);
- 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>`;
- }
- // ─── UI INJECTION (COMPACT CSS) ───────────────────────────────────────────
- const UI_CSS = `
- ::-webkit-scrollbar { width: 6px; }
- ::-webkit-scrollbar-track { background: #0f172a; }
- ::-webkit-scrollbar-thumb { background: #475569; border-radius: 6px; }
- @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
- @keyframes pulse { 0% { opacity: 0.5; } 50% { opacity: 1; } 100% { opacity: 0.5; } }
- .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; }
- .btn-content { display: flex; align-items: center; justify-content: center; gap: 6px; }
- .status-pulsing { animation: pulse 2s ease-in-out infinite; color: #3b82f6 !important; font-weight: 600; }
- #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; }
- #xai-pro-app * { box-sizing: border-box; }
- .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; }
- .panel-title { font-size: 11px; font-weight: 700; color: #94a3b8; letter-spacing: 0.5px; padding: 10px; border-bottom: 1px solid #334155; text-transform: uppercase; }
- .col-left { width: 310px; display: flex; flex-direction: column; gap: 10px; flex-shrink: 0; }
- .col-center { flex: 1; display: flex; flex-direction: column; gap: 10px; min-width: 0; min-height: 0; }
- .col-right { width: 310px; display: flex; flex-direction: column; gap: 10px; flex-shrink: 0; transition: 0.2s; }
- .col-right.drag-over { background: #064e3b; border-color: #10b981; box-shadow: 0 0 0 4px rgba(16,185,129,0.15); border-radius: 10px; }
- 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; }
- textarea, select, input[type="text"], input[type="number"] { width: 100%; }
- 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); }
- .prompt-drag-over { border-color: #10b981 !important; background: #064e3b !important; box-shadow: 0 0 0 2px rgba(16,185,129,0.15) !important; }
- label { font-size: 11px; font-weight: 600; color: #94a3b8; margin-bottom: 4px; display: block; }
- .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; }
- .btn-primary:hover:not(:disabled) { background: linear-gradient(180deg, #4f46e5 0%, #4338ca 100%); transform: translateY(-1px); }
- .btn-primary:disabled { opacity: 0.8; cursor: not-allowed; }
- .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; }
- .btn-secondary:hover:not(:disabled) { background: #334155; border-color: #64748b; }
- .ar-select-box { background: #0f172a; border: 1px solid #334155; border-radius: 6px; padding: 8px; display: flex; align-items: center; gap: 8px; cursor: pointer; color: #f8fafc; font-size: 13px; }
- #ar-options-container { position: fixed; background: #1e293b; border: 1px solid #334155; border-radius: 6px; box-shadow: 0 10px 25px rgba(0,0,0,0.4); max-height: 250px; overflow-y: auto; display: none; z-index: 99999999; }
- .ar-option { display: flex; align-items: center; gap: 8px; padding: 6px 8px; cursor: pointer; font-size: 12px; color: #cbd5e1; }
- .ar-option:hover { background: #334155; color: #f8fafc; }
- .img-wrapper { position: relative; display: block; }
- .zoomable { cursor: zoom-in; transition: transform 0.2s; width: 100%; display: block; }
- .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; }
- .overlay-btn:hover { background: rgba(15, 23, 42, 0.9); opacity: 1; transform: scale(1.05); }
- .overlay-dl-btn { right: 6px; }
- .overlay-edit-btn { right: 40px; color: #10b981; }
- .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; }
- .ref-item.inactive { opacity: 0.6; filter: grayscale(0.8); }
- .ref-item:active { cursor: grabbing; }
- .ref-item.dragging { opacity: 0.4; }
- .ref-handle { font-size: 14px; color: #475569; cursor: grab; padding: 0 4px; }
- .ref-thumb { width: 90px; height: 90px; border-radius: 4px; object-fit: cover; border: 1px solid #334155; background: #0f172a; transition: 0.2s; }
- .ref-info { flex: 1; display: flex; flex-direction: column; justify-content: center; gap: 6px; min-width: 0; }
- .ref-toggle-label { font-size: 11px; color: #cbd5e1; display: flex; align-items: center; gap: 4px; cursor: pointer; user-select: none; }
- .ref-toggle-label input { width: 12px; height: 12px; cursor: pointer; accent-color: #10b981; margin: 0; padding: 0; }
- .ref-del { background: #450a0a; color: #fca5a5; border: 1px solid #7f1d1d; border-radius: 4px; padding: 6px; cursor: pointer; font-size: 11px; transition: 0.2s; font-weight: bold; width: 100%; text-align: center; }
- .ref-del:hover { background: #7f1d1d; color: #fee2e2; }
- .drag-over-top { border-top: 2px solid #22c55e !important; }
- .drag-over-bottom { border-bottom: 2px solid #22c55e !important; }
- .history-card { background: #0f172a; border: 1px solid #334155; border-radius: 6px; overflow: hidden; margin-bottom: 10px; }
- .history-card .overlay-btn { width: 24px; height: 24px; top: 4px; border-radius: 4px; }
- .history-card .overlay-dl-btn { right: 4px; }
- .history-card .overlay-edit-btn { right: 32px; }
- .history-card p { padding: 8px; margin: 0; font-size: 11px; color: #94a3b8; cursor: pointer; transition: 0.2s; }
- .history-card p:hover { background: #1e293b; color: #e2e8f0; }
- .current-card { border-radius: 8px; overflow: hidden; box-shadow: 0 4px 10px rgba(0,0,0,0.3); border: 1px solid #334155; background: #1e293b; }
- .current-card img, .current-card video { max-height: 50vh; object-fit: contain; background: #0f172a; }
- /* Modals Generic Classes */
- .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); }
- .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; }
- .fav-item { display: flex; justify-content: space-between; align-items: center; background: #0f172a; padding: 8px; border-radius: 6px; border: 1px solid #334155; }
- .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; }
- .fav-text:hover { color: #10b981; }
- #settings-btn { position: fixed; top: 10px; right: 15px; font-size: 20px; cursor: pointer; z-index: 100; transition: transform 0.2s; opacity: 0.8;}
- #settings-btn:hover { transform: rotate(45deg); opacity: 1; }
- #xai-lightbox { cursor: zoom-out; }
- #xai-lightbox-wrapper { position: relative; display: inline-block; max-width: 95vw; max-height: 95vh; }
- #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); }
- `;
- const UI_HTML = `
- <div id="xai-pro-app">
- <div id="settings-btn" title="Studio Settings">⚙️</div>
- <!-- LEFT COLUMN -->
- <div class="col-left">
- <div class="panel" style="flex-shrink: 0;">
- <div style="display: flex; background: rgba(0,0,0,0.2); border-radius: 0 0 6px 6px; margin-bottom: 10px; border-bottom: 1px solid #334155; padding: 10px;">
- <div style="flex: 1;">
- <label>Master Action</label>
- <select id="master-action" style="font-weight: bold; color: #f8fafc; border-color: #475569; box-shadow: 0 2px 4px rgba(0,0,0,0.3);">
- <option value="gen_image">🖼️ Generate Image</option>
- <option value="gen_video">🎥 Generate Video</option>
- <option value="edit_video">✂️ Edit Video</option>
- <option value="extend_video">➡️ Extend Video</option>
- </select>
- </div>
- </div>
- <div style="padding: 10px; display: flex; flex-direction: column; gap: 10px;">
- <div id="setting-ar">
- <label>Aspect Ratio</label>
- <div class="ar-select-box" id="ar-select-box">
- <div id="ar-selected-icon"></div>
- <span id="ar-selected-text">Auto</span>
- </div>
- </div>
- <div style="display: flex; gap: 8px;">
- <div id="setting-res-img" style="flex: 1;">
- <label>Resolution</label>
- <select id="xai-api-res-img">
- <option value="2k" selected>2K (Pro)</option>
- <option value="1k">1K</option>
- </select>
- </div>
- <div id="setting-res-vid" style="flex: 1; display: none;">
- <label>Resolution</label>
- <select id="xai-api-res-vid">
- <option value="720p" selected>720p (HD)</option>
- <option value="480p">480p (SD)</option>
- </select>
- </div>
- <div id="setting-batch" style="flex: 1;">
- <label>Batch Size</label>
- <select id="xai-api-n">
- <option value="1" selected>1 Image</option>
- <option value="2">2 Images</option>
- <option value="3">3 Images</option>
- <option value="4">4 Images</option>
- <option value="5">5 Images</option>
- <option value="8">8 Images</option>
- <option value="10">10 Images</option>
- </select>
- </div>
- <div id="setting-duration" style="flex: 1; display: none;">
- <label>Duration</label>
- <select id="xai-api-duration">
- <option value="5" selected>5 Seconds</option>
- <option value="8">8 Seconds</option>
- <option value="10">10 Seconds</option>
- <option value="12">12 Seconds</option>
- <option value="15">15 Seconds</option>
- <option value="2">2s Extension</option>
- <option value="4">4s Extension</option>
- <option value="6">6s Extension</option>
- </select>
- </div>
- <div id="setting-loops" style="flex: 1;">
- <label title="How many consecutive API calls to perform in a row">Loops</label>
- <input type="number" id="xai-api-loops" value="1" min="1" max="100">
- </div>
- </div>
- </div>
- </div>
- <div class="panel" style="flex: 1; min-height: 0;">
- <div class="panel-title" style="display:flex; justify-content: space-between; align-items: center;">
- <span>Previous Results</span>
- <span id="xai-reset-btn" style="cursor: pointer; color: #ef4444; text-transform: none; text-decoration: underline;">Wipe Cache</span>
- </div>
- <div id="xai-history" style="flex: 1; overflow-y: auto; padding: 10px;">
- <div style="color: #94a3b8; font-size: 11px; text-align: center; margin-top: 20px;">No history yet.</div>
- </div>
- </div>
- </div>
- <!-- CENTER COLUMN -->
- <div class="col-center">
- <div class="panel" style="flex: 1; background: transparent; border: none; box-shadow: none; min-height: 0;">
- <div class="panel-title" style="background: #1e293b; border-radius: 8px; border: 1px solid #334155; margin-bottom: 10px; flex-shrink: 0;">Current Generation</div>
- <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>
- </div>
- <div class="panel" style="flex-shrink: 0; padding: 12px;">
- <label style="font-size: 13px; color: #e2e8f0; display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
- <span style="display: flex; align-items: center; gap: 8px;">
- <span>Prompt</span>
- <button id="fav-prompts-btn" class="btn-secondary" style="padding: 2px 8px; font-size: 10px; height: 22px;">⭐ Favorites</button>
- </span>
- <span style="font-size: 10px; font-weight: 400; color: #94a3b8;">(Drop image here for EXIF)</span>
- </label>
- <textarea id="xai-api-prompt" placeholder="Describe what you want to see... Supports Spintax {cat|dog} and @randomseed" style="height: 100px; min-height: 70px; margin-bottom: 10px; font-size: 13px;"></textarea>
- <div style="display: flex; align-items: center; justify-content: space-between;">
- <div id="xai-api-status" style="font-size: 12px; color: #64748b; font-weight: 500;">Ready</div>
- <div style="display: flex; gap: 8px;">
- <button id="xai-preview-btn" class="btn-secondary">🔍 Payload</button>
- <button id="xai-cancel-btn" class="btn-secondary" style="display: none; color: #ef4444; border-color: #ef4444;">🛑 Cancel</button>
- <button id="xai-api-generate" class="btn-primary" style="width: 120px;"><div class="btn-content">Generate</div></button>
- </div>
- </div>
- </div>
- </div>
- <!-- RIGHT COLUMN -->
- <div class="col-right" id="col-right-dropzone">
- <div class="panel" style="flex: 1; display: flex; flex-direction: column; min-height: 0;">
- <div class="panel-title" id="ref-panel-title">Reference Media</div>
- <div style="padding: 10px; flex: 1; display: flex; flex-direction: column; overflow: hidden;">
- <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);">
- <div style="font-size: 20px; margin-bottom: 4px;">📥</div>
- <div style="font-size: 12px; font-weight: 600;">Drag & Drop / Ctrl+V</div>
- </div>
- <input type="file" id="xai-api-file" accept="image/*, video/mp4, video/webm" multiple style="display: none;">
- <div style="font-size: 10px; color: #94a3b8; margin-bottom: 4px; text-transform: uppercase; font-weight: bold; flex-shrink: 0;">Upload Order (Top = First)</div>
- <div id="xai-ref-list" style="flex: 1; overflow-y: auto; padding-right: 4px;"></div>
- </div>
- </div>
- </div>
- <!-- FIXED FLOATING DROPDOWN -->
- <div id="ar-options-container"></div>
- <!-- MODALS -->
- <div id="xai-lightbox" class="modal-overlay">
- <div id="xai-lightbox-wrapper">
- <img id="xai-lightbox-img" src="">
- <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>
- </div>
- </div>
- <!-- SETTINGS MODAL -->
- <div id="xai-settings-modal" class="modal-overlay">
- <div class="modal-box" style="width: 500px;">
- <h3 style="margin:0; color: #f8fafc; font-size: 15px;">⚙️ Studio Settings</h3>
- <div style="display:flex; gap:10px;">
- <div style="flex:1;">
- <label>Max Retries</label>
- <input type="number" id="set-retries" min="1" max="100">
- </div>
- <div style="flex:1;">
- <label>Delay Min (ms)</label>
- <input type="number" id="set-delay-min" min="500" step="500">
- </div>
- <div style="flex:1;">
- <label>Delay Max (ms)</label>
- <input type="number" id="set-delay-max" min="1000" step="500">
- </div>
- </div>
- <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>
- <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>
- <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>
- <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>
- <div style="margin-top: 4px;">
- <label>Video Polling Timeout (Seconds)</label>
- <input type="number" id="set-video-timeout" min="10" step="10">
- </div>
- <div>
- <label>Custom CSS Overrides</label>
- <textarea id="set-custom-css" style="font-family: monospace; height: 100px; font-size: 11px;" placeholder="/* e.g., #xai-pro-app { background: red; } */"></textarea>
- </div>
- <div style="display: flex; gap: 8px; margin-top: 5px;">
- <button id="save-settings-btn" class="btn-primary" style="flex:1;">Save Settings</button>
- <button id="close-settings-btn" class="btn-secondary" style="flex:1;">Cancel</button>
- </div>
- </div>
- </div>
- <!-- FAVORITES MODAL -->
- <div id="xai-fav-modal" class="modal-overlay">
- <div class="modal-box" style="width: 550px;">
- <div style="display: flex; justify-content: space-between; align-items: center;">
- <h3 style="margin:0; color: #f8fafc; font-size: 15px;">⭐ Favorite Prompts</h3>
- <button id="close-fav-btn" style="background: none; border: none; color: #ef4444; font-size: 20px; cursor: pointer; font-weight: bold; line-height: 1;">×</button>
- </div>
- <button id="add-current-fav-btn" class="btn-primary" style="background: linear-gradient(180deg, #10b981 0%, #059669 100%);">➕ Save Current Prompt to Favorites</button>
- <div id="fav-list" style="flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 6px; padding-right: 4px;"></div>
- </div>
- </div>
- <!-- CUSTOM PAYLOAD DEBUGGER MODAL -->
- <div id="xai-payload-modal" class="modal-overlay">
- <div class="modal-box" style="width: 550px;">
- <div style="display: flex; justify-content: space-between; align-items: center;">
- <h3 style="margin:0; font-size: 15px; color: #f8fafc;">API Payload Debugger</h3>
- <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>
- </div>
- <div><label>Endpoint URL:</label><input type="text" id="payload-endpoint" style="font-family: monospace;" value="/v1/images/generations"></div>
- <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>
- <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>
- <div id="custom-payload-status" style="font-size: 11px; color: #64748b; text-align: center;">Ready</div>
- </div>
- </div>
- </div>
- `;
- // ─── INITIALIZATION ───────────────────────────────────────────────────────
- let updateUIForEditMedia = null;
- const initApp = () => {
- if (!document.body) return setTimeout(initApp, 50);
- const hideOriginals = () => {
- Array.from(document.body.children).forEach(child => {
- if (child.id !== 'xai-pro-app' && !['SCRIPT', 'STYLE', 'LINK'].includes(child.tagName)) {
- child.style.display = 'none';
- }
- });
- };
- hideOriginals();
- new MutationObserver(hideOriginals).observe(document.body, { childList: true });
- if (!document.getElementById('xai-pro-app')) {
- const style = document.createElement('style');
- style.innerHTML = UI_CSS;
- document.head.appendChild(style);
- const customStyle = document.createElement('style');
- customStyle.id = 'xai-custom-css-block';
- customStyle.innerHTML = appSettings.customCSS;
- document.head.appendChild(customStyle);
- document.body.insertAdjacentHTML('beforeend', UI_HTML);
- bindLogic();
- }
- };
- let referenceMedia =[];
- let currentAr = localStorage.getItem('xai_api_ar') || 'Auto';
- let fullPayloadMemory = {};
- let currentAbortController = null;
- let isRequestCancelled = false;
- function saveRefs() {
- try {
- localStorage.setItem('xai_api_refs', JSON.stringify(referenceMedia));
- } catch (e) {
- console.warn("[xAI Studio] Could not save references to localStorage (quota exceeded). Images will not persist after refresh.");
- }
- }
- function bindLogic() {
- const els = {
- app: document.getElementById('xai-pro-app'),
- prompt: document.getElementById('xai-api-prompt'),
- action: document.getElementById('master-action'),
- resImg: document.getElementById('xai-api-res-img'),
- resVid: document.getElementById('xai-api-res-vid'),
- n: document.getElementById('xai-api-n'),
- duration: document.getElementById('xai-api-duration'),
- loops: document.getElementById('xai-api-loops'),
- settingAr: document.getElementById('setting-ar'),
- settingResImg: document.getElementById('setting-res-img'),
- settingResVid: document.getElementById('setting-res-vid'),
- settingBatch: document.getElementById('setting-batch'),
- settingDuration: document.getElementById('setting-duration'),
- settingLoops: document.getElementById('setting-loops'),
- fileInput: document.getElementById('xai-api-file'),
- dropzone: document.getElementById('col-right-dropzone'),
- uploadPlaceholder: document.getElementById('xai-upload-placeholder'),
- refList: document.getElementById('xai-ref-list'),
- refTitle: document.getElementById('ref-panel-title'),
- btn: document.getElementById('xai-api-generate'),
- previewBtn: document.getElementById('xai-preview-btn'),
- cancelBtn: document.getElementById('xai-cancel-btn'),
- status: document.getElementById('xai-api-status'),
- history: document.getElementById('xai-history'),
- currentImages: document.getElementById('xai-current-images'),
- reset: document.getElementById('xai-reset-btn'),
- arSelectBox: document.getElementById('ar-select-box'),
- arOptionsCont: document.getElementById('ar-options-container'),
- arSelectedIcon: document.getElementById('ar-selected-icon'),
- arSelectedText: document.getElementById('ar-selected-text'),
- lightbox: document.getElementById('xai-lightbox'),
- lightboxImg: document.getElementById('xai-lightbox-img'),
- lightboxDl: document.getElementById('xai-lightbox-dl'),
- payloadModal: document.getElementById('xai-payload-modal'),
- payloadEndpoint: document.getElementById('payload-endpoint'),
- payloadCode: document.getElementById('payload-code'),
- closePayloadBtn: document.getElementById('close-payload-btn'),
- sendCustomBtn: document.getElementById('send-custom-payload-btn'),
- customStatus: document.getElementById('custom-payload-status'),
- settingsBtn: document.getElementById('settings-btn'),
- settingsModal: document.getElementById('xai-settings-modal'),
- closeSettingsBtn: document.getElementById('close-settings-btn'),
- saveSettingsBtn: document.getElementById('save-settings-btn'),
- favBtn: document.getElementById('fav-prompts-btn'),
- favModal: document.getElementById('xai-fav-modal'),
- closeFavBtn: document.getElementById('close-fav-btn'),
- addFavBtn: document.getElementById('add-current-fav-btn'),
- favList: document.getElementById('fav-list')
- };
- if (localStorage.getItem('xai_api_prompt')) els.prompt.value = localStorage.getItem('xai_api_prompt');
- els.prompt.addEventListener('input', () => localStorage.setItem('xai_api_prompt', els.prompt.value));
- try {
- const storedGen = JSON.parse(localStorage.getItem('xai_api_gen_settings') || '{}');
- if (storedGen.action) els.action.value = storedGen.action;
- if (storedGen.resImg) els.resImg.value = storedGen.resImg;
- if (storedGen.resVid) els.resVid.value = storedGen.resVid;
- if (storedGen.n) els.n.value = storedGen.n;
- if (storedGen.duration) els.duration.value = storedGen.duration;
- if (storedGen.loops) els.loops.value = storedGen.loops;
- } catch(e) {}
- const updateGenSettingsMemory = () => {
- localStorage.setItem('xai_api_gen_settings', JSON.stringify({
- action: els.action.value, resImg: els.resImg.value, resVid: els.resVid.value,
- n: els.n.value, duration: els.duration.value, loops: els.loops.value
- }));
- };
- [els.action, els.resImg, els.resVid, els.n, els.duration, els.loops].forEach(el => el.addEventListener('change', updateGenSettingsMemory));
- try {
- const storedRefs = JSON.parse(localStorage.getItem('xai_api_refs'));
- if (Array.isArray(storedRefs)) referenceMedia = storedRefs;
- } catch(e) {}
- // ─── SETTINGS BINDINGS ───────────────────────────────────────────────────
- els.settingsBtn.addEventListener('click', () => {
- document.getElementById('set-retries').value = appSettings.retries;
- document.getElementById('set-delay-min').value = appSettings.delayMin;
- document.getElementById('set-delay-max').value = appSettings.delayMax;
- document.getElementById('set-notifications').checked = appSettings.notifications;
- document.getElementById('set-save-png').checked = appSettings.saveAsPng;
- document.getElementById('set-auto-retry-video').checked = appSettings.autoRetryStuckVideo;
- document.getElementById('set-video-timeout').value = appSettings.videoPollTimeout;
- document.getElementById('set-api-base-url').value = appSettings.apiBaseUrl || '';
- document.getElementById('set-custom-css').value = appSettings.customCSS || '';
- els.settingsModal.style.display = 'flex';
- });
- els.saveSettingsBtn.addEventListener('click', () => {
- appSettings.retries = parseInt(document.getElementById('set-retries').value) || 20;
- appSettings.delayMin = parseInt(document.getElementById('set-delay-min').value) || 2000;
- appSettings.delayMax = Math.max(appSettings.delayMin, parseInt(document.getElementById('set-delay-max').value) || 4000);
- appSettings.notifications = document.getElementById('set-notifications').checked;
- appSettings.saveAsPng = document.getElementById('set-save-png').checked;
- appSettings.autoRetryStuckVideo = document.getElementById('set-auto-retry-video').checked;
- appSettings.videoPollTimeout = parseInt(document.getElementById('set-video-timeout').value) || 300;
- appSettings.apiBaseUrl = document.getElementById('set-api-base-url').value.trim();
- appSettings.customCSS = document.getElementById('set-custom-css').value;
- localStorage.setItem('xai_api_settings', JSON.stringify(appSettings));
- document.getElementById('xai-custom-css-block').innerHTML = appSettings.customCSS;
- els.settingsModal.style.display = 'none';
- });
- els.closeSettingsBtn.addEventListener('click', () => els.settingsModal.style.display = 'none');
- // ─── FAVORITES BINDINGS ──────────────────────────────────────────────────
- function renderFavList() {
- els.favList.innerHTML = '';
- 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;
- }
- favoritePrompts.forEach((promptText, idx) => {
- const div = document.createElement('div');
- div.className = 'fav-item';
- div.innerHTML = `
- <div class="fav-text" title="${promptText.replace(/"/g, '"')}">${promptText}</div>
- <button class="btn-secondary" style="padding: 2px 6px; font-size: 10px; border-color: #ef4444; color: #ef4444;">Del</button>
- `;
- div.querySelector('.fav-text').addEventListener('click', () => {
- els.prompt.value = promptText;
- localStorage.setItem('xai_api_prompt', promptText);
- els.favModal.style.display = 'none';
- });
- div.querySelector('button').addEventListener('click', () => {
- favoritePrompts.splice(idx, 1);
- localStorage.setItem('xai_api_favs', JSON.stringify(favoritePrompts));
- renderFavList();
- });
- els.favList.appendChild(div);
- });
- }
- els.favBtn.addEventListener('click', () => {
- renderFavList();
- els.favModal.style.display = 'flex';
- });
- els.closeFavBtn.addEventListener('click', () => els.favModal.style.display = 'none');
- els.addFavBtn.addEventListener('click', () => {
- const p = els.prompt.value.trim();
- if (!p) return alert("Prompt is empty!");
- if (favoritePrompts.includes(p)) return alert("Already in favorites!");
- favoritePrompts.unshift(p);
- localStorage.setItem('xai_api_favs', JSON.stringify(favoritePrompts));
- renderFavList();
- });
- // ─── PROMPT BOX EXIF DRAG & DROP ──────────────────────────────────────────
- els.prompt.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); els.prompt.classList.add('prompt-drag-over'); });
- els.prompt.addEventListener('dragleave', (e) => { e.preventDefault(); e.stopPropagation(); els.prompt.classList.remove('prompt-drag-over'); });
- els.prompt.addEventListener('drop', async (e) => {
- e.preventDefault(); e.stopPropagation();
- els.prompt.classList.remove('prompt-drag-over');
- if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
- const file = e.dataTransfer.files[0];
- const oldVal = els.prompt.value;
- els.prompt.value = "Extracting prompt from EXIF...";
- const extractedPrompt = await extractPromptFromImage(file);
- if (extractedPrompt) {
- els.prompt.value = extractedPrompt;
- localStorage.setItem('xai_api_prompt', extractedPrompt);
- } else {
- els.prompt.value = oldVal;
- alert("No prompt found in this image's EXIF data (or not a valid JPEG).");
- }
- }
- });
- // ─── MASTER ACTION UI LOGIC ───────────────────────────────────────────────
- function getMaxMedia() {
- const val = els.action.value;
- if (val === 'gen_image') return 5;
- if (val === 'gen_video') return 7;
- return 1;
- }
- function updateActionUI() {
- const val = els.action.value;
- if (val === 'gen_image') {
- els.settingAr.style.display = 'block'; els.settingResImg.style.display = 'block'; els.settingResVid.style.display = 'none'; els.settingBatch.style.display = 'block'; els.settingDuration.style.display = 'none';
- } else if (val === 'gen_video') {
- els.settingAr.style.display = 'block'; els.settingResImg.style.display = 'none'; els.settingResVid.style.display = 'block'; els.settingBatch.style.display = 'none'; els.settingDuration.style.display = 'block';
- els.duration.innerHTML = `<option value="5">5 Seconds</option><option value="8">8 Seconds</option><option value="10">10 Seconds</option><option value="12">12 Seconds</option><option value="15">15 Seconds</option>`;
- if (localStorage.getItem('xai_api_gen_settings') && JSON.parse(localStorage.getItem('xai_api_gen_settings')).duration <= 15) {
- els.duration.value = JSON.parse(localStorage.getItem('xai_api_gen_settings')).duration;
- } else els.duration.value = "5";
- } else if (val === 'edit_video') {
- els.settingAr.style.display = 'none'; els.settingResImg.style.display = 'none'; els.settingResVid.style.display = 'none'; els.settingBatch.style.display = 'none'; els.settingDuration.style.display = 'none';
- } else if (val === 'extend_video') {
- els.settingAr.style.display = 'none'; els.settingResImg.style.display = 'none'; els.settingResVid.style.display = 'none'; els.settingBatch.style.display = 'none'; els.settingDuration.style.display = 'block';
- els.duration.innerHTML = `<option value="2">2s Extension</option><option value="4">4s Extension</option><option value="6">6s Extension</option><option value="8">8s Extension</option><option value="10">10s Extension</option>`;
- if (localStorage.getItem('xai_api_gen_settings') && JSON.parse(localStorage.getItem('xai_api_gen_settings')).duration <= 10) {
- els.duration.value = JSON.parse(localStorage.getItem('xai_api_gen_settings')).duration;
- } else els.duration.value = "6";
- }
- const max = getMaxMedia();
- els.refTitle.innerText = `Reference Media (Max Active: ${max})`;
- let activeCount = 0;
- referenceMedia.forEach(m => {
- let valid = true;
- if ((val === 'gen_image' || val === 'gen_video') && m.isVideo) valid = false;
- if ((val === 'edit_video' || val === 'extend_video') && !m.isVideo) valid = false;
- if (!valid) m.active = false;
- else if (m.active) {
- activeCount++;
- if (activeCount > max) m.active = false;
- }
- });
- renderRefList();
- }
- els.action.addEventListener('change', updateActionUI);
- updateActionUI();
- // ─── INSTANT EDIT BINDING HANDLER ─────────────────────────────────────────
- updateUIForEditMedia = (mediaUrl, isVideo, fileName) => {
- referenceMedia.forEach(m => m.active = false);
- referenceMedia.unshift({
- id: Date.now() + Math.random(),
- base64: mediaUrl,
- isVideo: isVideo,
- active: true,
- thumb: isVideo ? null : mediaUrl,
- fileName: fileName || (isVideo ? 'Edited_Video.mp4' : 'Edited_Image.png')
- });
- els.action.value = isVideo ? 'edit_video' : 'gen_image';
- updateActionUI();
- updateGenSettingsMemory();
- renderRefList();
- saveRefs();
- };
- // ─── BUILD PAYLOAD LOGIC ──────────────────────────────────────────────────
- function buildPayloadData(dynamicPromptText) {
- const action = els.action.value;
- const payload = {
- model: action === 'gen_image' ? "grok-imagine-image" : "grok-imagine-video",
- prompt: dynamicPromptText,
- };
- const ar = currentAr.toLowerCase();
- const activeRefs = referenceMedia.filter(m => m.active);
- if (action === 'gen_image') {
- payload.n = parseInt(els.n.value); payload.resolution = els.resImg.value; payload.response_format = "b64_json";
- if (ar !== 'auto') payload.aspect_ratio = ar; else if (activeRefs.length > 0) payload.aspect_ratio = 'auto';
- if (activeRefs.length > 0) {
- if (activeRefs.length === 1) payload.image = { url: activeRefs[0].base64 };
- else payload.images = activeRefs.map(m => ({ type: "image_url", url: m.base64 }));
- }
- } else if (action === 'gen_video') {
- payload.duration = parseInt(els.duration.value); payload.resolution = els.resVid.value;
- if (ar !== 'auto') payload.aspect_ratio = ar;
- if (activeRefs.length > 0) payload.reference_images = activeRefs.map(m => ({ url: m.base64 }));
- } else if (action === 'edit_video') {
- if (activeRefs.length > 0) payload.video = { url: activeRefs[0].base64 };
- } else if (action === 'extend_video') {
- payload.duration = parseInt(els.duration.value);
- if (activeRefs.length > 0) payload.video = { url: activeRefs[0].base64 };
- }
- return payload;
- }
- // ─── CUSTOM PAYLOAD DEBUGGER LOGIC ────────────────────────────────────────
- els.previewBtn.addEventListener('click', () => {
- const dynamicPrompt = parseDynamicPrompt(els.prompt.value.trim());
- fullPayloadMemory = buildPayloadData(dynamicPrompt);
- const displayP = JSON.parse(JSON.stringify(fullPayloadMemory));
- const trunc = "[BASE64_TRUNCATED_FOR_PREVIEW]";
- if (displayP.image) { if (Array.isArray(displayP.image)) displayP.image.forEach(img => img.url = trunc); else displayP.image.url = trunc; }
- if (displayP.images && Array.isArray(displayP.images)) displayP.images.forEach(img => img.url = trunc);
- if (displayP.reference_images && Array.isArray(displayP.reference_images)) displayP.reference_images.forEach(img => img.url = trunc);
- if (displayP.video) displayP.video.url = trunc;
- const activeRefs = referenceMedia.filter(m => m.active);
- let endpoint = '/v1/images/generations';
- if (els.action.value === 'gen_image' && activeRefs.length > 0) endpoint = '/v1/images/edits';
- else if (els.action.value === 'gen_video') endpoint = '/v1/videos/generations';
- else if (els.action.value === 'edit_video') endpoint = '/v1/videos/edits';
- else if (els.action.value === 'extend_video') endpoint = '/v1/videos/extensions';
- els.payloadEndpoint.value = endpoint;
- els.payloadCode.value = JSON.stringify(displayP, null, 2);
- els.payloadModal.style.display = 'flex';
- });
- els.closePayloadBtn.addEventListener('click', () => els.payloadModal.style.display = 'none');
- function injectBase64(editedObj, originalObj) {
- if (!editedObj || typeof editedObj !== 'object') return;
- for (let key in editedObj) {
- if (typeof editedObj[key] === 'string' && editedObj[key] === '[BASE64_TRUNCATED_FOR_PREVIEW]') {
- if (originalObj && originalObj[key]) editedObj[key] = originalObj[key];
- } else if (typeof editedObj[key] === 'object') { injectBase64(editedObj[key], originalObj ? originalObj[key] : null); }
- }
- }
- els.sendCustomBtn.addEventListener('click', async () => {
- initAudio();
- let customPayload;
- try { customPayload = JSON.parse(els.payloadCode.value); }
- catch(e) { return alert("Invalid JSON format in textarea!"); }
- injectBase64(customPayload, fullPayloadMemory);
- const endpoint = els.payloadEndpoint.value.trim();
- const isImage = customPayload.model === "grok-imagine-image";
- stealthIdentityReset();
- els.sendCustomBtn.disabled = true;
- els.sendCustomBtn.innerHTML = `<div class="btn-content"><div class="loading-spinner"></div> Processing...</div>`;
- isRequestCancelled = false;
- const ok = await executeGenerationSingle(endpoint, customPayload, isImage, els.customStatus, 1, 1);
- els.sendCustomBtn.disabled = false;
- els.sendCustomBtn.innerHTML = `<div class="btn-content">🚀 Send Custom Payload</div>`;
- if (ok) notifyUser(false); else notifyUser(true);
- });
- // ─── CSP-SAFE DELEGATED LIGHTBOX LOGIC ────────────────────────────────────
- els.app.addEventListener('click', (e) => {
- if (e.target && e.target.classList.contains('zoomable') && e.target.tagName === 'IMG') {
- 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';
- }
- });
- els.lightbox.addEventListener('click', (e) => {
- if (e.target.closest('#xai-lightbox-dl')) return;
- if (e.target.closest('.modal-box')) return;
- els.lightbox.style.display = 'none';
- });
- document.addEventListener('keydown', (e) => { if(e.key === 'Escape') { Array.from(document.querySelectorAll('.modal-overlay')).forEach(m => m.style.display = 'none'); } });
- // ─── FIXED FLOATING AR DROPDOWN LOGIC ─────────────────────────────────────
- function renderArOptions() {
- els.arOptionsCont.innerHTML = '';
- arData.forEach(ar => {
- const opt = document.createElement('div'); opt.className = 'ar-option';
- opt.innerHTML = `${createArIcon(ar.w, ar.h, ar.isAuto)} <span>${ar.label}</span>`;
- opt.addEventListener('click', () => { setAr(ar); els.arOptionsCont.style.display = 'none'; });
- els.arOptionsCont.appendChild(opt);
- });
- }
- function setAr(arObj) {
- currentAr = arObj.label; localStorage.setItem('xai_api_ar', currentAr);
- els.arSelectedIcon.innerHTML = createArIcon(arObj.w, arObj.h, arObj.isAuto); els.arSelectedText.innerText = arObj.label;
- }
- renderArOptions();
- const initialAr = arData.find(a => a.label === currentAr) || arData[0]; setAr(initialAr);
- els.arSelectBox.addEventListener('click', (e) => {
- e.stopPropagation();
- if (els.arOptionsCont.style.display === 'block') { els.arOptionsCont.style.display = 'none'; return; }
- 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 = 'block';
- });
- document.addEventListener('click', (e) => { if (!els.arOptionsCont.contains(e.target)) els.arOptionsCont.style.display = 'none'; });
- // ─── UNLIMITED MULTI-IMAGE DRAG/DROP WITH VIDEO THUMBNAILS ────────────────
- async function processFile(file) {
- const isVid = file.type.startsWith('video/'); const isImg = file.type.startsWith('image/');
- if (!isVid && !isImg) return;
- if (isVid && (els.action.value === 'gen_image' || els.action.value === 'gen_video')) { els.action.value = 'edit_video'; updateActionUI(); updateGenSettingsMemory(); }
- else if (isImg && (els.action.value === 'edit_video' || els.action.value === 'extend_video')) { els.action.value = 'gen_image'; updateActionUI(); updateGenSettingsMemory(); }
- const max = getMaxMedia();
- const activeCount = referenceMedia.filter(m => m.active).length;
- let thumbBase64 = null;
- if (isVid) thumbBase64 = await generateVideoThumbnail(file);
- const reader = new FileReader();
- reader.onload = (event) => {
- referenceMedia.push({
- id: Date.now() + Math.random(),
- base64: event.target.result,
- isVideo: isVid,
- active: activeCount < max,
- thumb: thumbBase64,
- fileName: file.name
- });
- renderRefList();
- if (isImg) setAr(arData[0]);
- };
- reader.readAsDataURL(file);
- }
- let draggedIndex = null;
- function renderRefList() {
- els.refList.innerHTML = '';
- referenceMedia.forEach((media, index) => {
- const item = document.createElement('div');
- item.className = 'ref-item';
- item.draggable = true;
- let thumbHtml = '';
- if (media.isVideo) {
- 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>
- `;
- } 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>`;
- }
- } else {
- thumbHtml = `<img src="${media.base64}" class="ref-thumb zoomable" data-filename="${media.fileName || 'Reference_'+(index+1)+'.jpg'}" title="${media.fileName || 'Click to view'}">`;
- }
- item.innerHTML = `
- <div class="ref-handle">☰</div>
- ${thumbHtml}
- <div class="ref-info">
- <span style="font-size: 12px; color: #e2e8f0; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${media.fileName || ''}">
- Ref ${index + 1} <span style="font-size:10px; color:#64748b; font-weight:normal;">${media.fileName ? '- ' + media.fileName : ''}</span>
- </span>
- <label class="ref-toggle-label">
- <input type="checkbox" class="ref-active-toggle" ${media.active ? 'checked' : ''}> Use in Payload
- </label>
- <button class="ref-del">Remove</button>
- </div>
- `;
- if (!media.active) item.classList.add('inactive');
- item.querySelector('.ref-active-toggle').addEventListener('change', (e) => {
- const max = getMaxMedia();
- const currentlyActive = referenceMedia.filter(r => r.active).length;
- if (e.target.checked && currentlyActive >= max) {
- alert(`You can only have up to ${max} active references for this mode.`);
- e.target.checked = false;
- return;
- }
- media.active = e.target.checked;
- if (media.active) item.classList.remove('inactive'); else item.classList.add('inactive');
- saveRefs();
- });
- item.addEventListener('dragstart', (e) => { draggedIndex = index; e.dataTransfer.effectAllowed = 'move'; setTimeout(() => item.classList.add('dragging'), 0); });
- item.addEventListener('dragend', () => { item.classList.remove('dragging'); draggedIndex = null; document.querySelectorAll('.ref-item').forEach(el => el.classList.remove('drag-over-top', 'drag-over-bottom')); });
- 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'); }
- });
- item.addEventListener('dragleave', () => item.classList.remove('drag-over-top', 'drag-over-bottom'));
- 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();
- });
- item.querySelector('.ref-del').addEventListener('click', () => { referenceMedia.splice(index, 1); renderRefList(); });
- els.refList.appendChild(item);
- });
- saveRefs();
- }
- els.uploadPlaceholder.addEventListener('click', () => { els.fileInput.click(); });
- els.fileInput.addEventListener('change', (e) => { Array.from(e.target.files).forEach(processFile); els.fileInput.value = ''; });
- 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')) 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());
- });
- els.dropzone.addEventListener('dragover', (e) => { e.preventDefault(); els.dropzone.classList.add('drag-over'); });
- els.dropzone.addEventListener('dragleave', () => els.dropzone.classList.remove('drag-over'));
- els.dropzone.addEventListener('drop', (e) => { e.preventDefault(); els.dropzone.classList.remove('drag-over'); if (e.dataTransfer.files) Array.from(e.dataTransfer.files).forEach(processFile); });
- // ─── CANCEL BUTTON LOGIC ─────────────────────────────────────────────────
- els.cancelBtn.addEventListener('click', () => {
- isRequestCancelled = true;
- if (currentAbortController) currentAbortController.abort();
- els.cancelBtn.style.display = 'none'; els.previewBtn.style.display = 'flex';
- els.btn.disabled = false; els.btn.innerHTML = `<div class="btn-content">Generate</div>`;
- els.status.innerText = "Request Cancelled."; els.status.className = ""; els.status.style.color = "#ef4444";
- });
- // ─── MASTER GENERATION LOGIC ──────────────────────────────────────────────
- async function executeGenerationSingle(endpoint, payload, isImageAction, statusEl, currentLoop, totalLoops) {
- let loopPrefix = totalLoops > 1 ? `[Run ${currentLoop}/${totalLoops}] ` : '';
- statusEl.innerText = `${loopPrefix}Processing request...`;
- statusEl.className = "status-pulsing";
- currentAbortController = new AbortController();
- let attempts = 0; let success = false;
- while (attempts < appSettings.retries && !success && !isRequestCancelled) {
- try {
- statusEl.innerText = `${loopPrefix}Sending request to xAI...`;
- const fullEndpoint = getApiUrl(endpoint);
- const response = await fetch(fullEndpoint, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(payload),
- signal: currentAbortController.signal
- });
- if (response.status === 429 || response.status === 503 || response.status === 500 || response.status === 502) {
- attempts++;
- statusEl.innerText = `${loopPrefix}Busy (Error ${response.status}). Retrying... [${attempts}/${appSettings.retries}]`;
- const delay = Math.floor(Math.random() * (appSettings.delayMax - appSettings.delayMin + 1)) + appSettings.delayMin;
- await sleep(delay);
- continue;
- }
- if (!response.ok) {
- let errMsg = `HTTP ${response.status}`;
- try {
- const errData = await response.json();
- if (errData.error && errData.error.message) errMsg += `\n${errData.error.message}`;
- else if (errData.detail) errMsg += `\n${JSON.stringify(errData.detail)}`;
- } catch(e) {}
- throw new Error(errMsg);
- }
- const data = await response.json();
- if (isImageAction) {
- if (data && data.data && Array.isArray(data.data)) {
- success = true;
- statusEl.innerText = `${loopPrefix}Processing final images...`;
- statusEl.style.color = "#10b981";
- statusEl.className = "";
- await Promise.all(data.data.map((imgObj, index) =>
- processAndRenderImage(imgObj, payload.prompt, index + 1, data.data.length, els.currentImages)
- ));
- statusEl.innerText = "Ready";
- statusEl.style.color = "#64748b";
- } else throw new Error("Invalid response format.");
- } else {
- const reqId = data.request_id;
- if (!reqId) throw new Error("No Request ID returned.");
- let videoReady = false;
- let pollCount = 0;
- const MAX_POLLS = Math.max(1, Math.ceil(appSettings.videoPollTimeout / 5));
- while (!videoReady && !isRequestCancelled) {
- pollCount++;
- if (pollCount > MAX_POLLS) throw new Error("Timeout: Video likely dropped by filters or stuck in queue.");
- await sleep(5000);
- if (isRequestCancelled) break;
- const pollRes = await fetch(getApiUrl(`/v1/videos/${reqId}`), { signal: currentAbortController.signal });
- if (!pollRes.ok) continue;
- const pollData = await pollRes.json();
- if (pollData.error) throw new Error(`API Error: ${pollData.error.message || JSON.stringify(pollData.error)}`);
- const state = (pollData.status || pollData.state || 'processing').toLowerCase();
- statusEl.innerText = `${loopPrefix}Polling video (Status: ${state})...[${pollCount}/${MAX_POLLS}]`;
- if (state === 'done' || state === 'completed') {
- videoReady = true;
- success = true;
- statusEl.innerText = "Ready"; statusEl.className = ""; statusEl.style.color = "#94a3b8";
- renderVideoToGallery(pollData.video.url, payload.prompt, els.currentImages);
- } else if (['failed', 'expired', 'rejected', 'blocked', 'moderated', 'nsfw'].includes(state) || pollData.is_sensitive) {
- if (pollData.video && pollData.video.url) {
- 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);
- } else {
- throw new Error(`Generation halted. Reason: ${state}`);
- }
- }
- }
- }
- } catch (err) {
- if (err.name === 'AbortError' || isRequestCancelled) {
- statusEl.innerText = "Request Cancelled."; statusEl.className = ""; statusEl.style.color = "#ef4444";
- return false;
- } else if ((err.message.includes("Timeout: Video") || err.message.includes("Generation halted.")) && appSettings.autoRetryStuckVideo) {
- attempts++;
- if (attempts >= appSettings.retries) {
- statusEl.innerText = `${loopPrefix}Failed: Max retries reached for stuck video.`;
- statusEl.className = ""; statusEl.style.color = "#ef4444";
- break;
- }
- statusEl.innerText = `${loopPrefix}Video Stuck/Failed. Auto-retrying... [${attempts}/${appSettings.retries}]`;
- const delay = Math.floor(Math.random() * (appSettings.delayMax - appSettings.delayMin + 1)) + appSettings.delayMin;
- await sleep(delay);
- // Anti-caching injection: add a zero-width space to the prompt to force the server to evaluate a fresh generation.
- if (payload.prompt) payload.prompt += "\u200B";
- continue;
- } else {
- statusEl.innerText = `${loopPrefix}Error: ${err.message}`; statusEl.className = ""; statusEl.style.color = "#ef4444";
- break;
- }
- }
- }
- if (!success && !isRequestCancelled) {
- if (!statusEl.innerText.includes("Failed:") && !statusEl.innerText.includes("Error:")) {
- statusEl.innerText = `${loopPrefix}Failed after ${attempts} retries.`;
- }
- statusEl.className = ""; statusEl.style.color = "#ef4444";
- return false;
- }
- return success;
- }
- els.btn.addEventListener('click', async () => {
- initAudio();
- const basePrompt = els.prompt.value.trim();
- if (!basePrompt) return alert("Please enter a prompt.");
- const oldImages = Array.from(els.currentImages.children);
- if (oldImages.length > 0) {
- if (els.history.innerText.includes("No history")) els.history.innerHTML = '';
- oldImages.forEach(card => {
- card.className = 'history-card';
- const promptEl = card.querySelector('p');
- promptEl.title = "Click to copy prompt";
- promptEl.onclick = () => { els.prompt.value = promptEl.innerText; localStorage.setItem('xai_api_prompt', els.prompt.value); };
- els.history.prepend(card);
- });
- }
- const action = els.action.value;
- const activeRefs = referenceMedia.filter(m => m.active);
- let endpoint = '/v1/images/generations';
- if (action === 'gen_image' && activeRefs.length > 0) endpoint = '/v1/images/edits';
- else if (action === 'gen_video') endpoint = '/v1/videos/generations';
- else if (action === 'edit_video') endpoint = '/v1/videos/edits';
- else if (action === 'extend_video') endpoint = '/v1/videos/extensions';
- const isImage = action === 'gen_image';
- const totalLoops = parseInt(els.loops.value) || 1;
- stealthIdentityReset();
- els.btn.disabled = true;
- els.cancelBtn.style.display = 'flex';
- els.previewBtn.style.display = 'none';
- isRequestCancelled = false;
- let allSuccess = true;
- for (let i = 1; i <= totalLoops; i++) {
- if (isRequestCancelled) break;
- const dynamicPrompt = parseDynamicPrompt(basePrompt);
- const payload = buildPayloadData(dynamicPrompt);
- els.btn.innerHTML = `<div class="btn-content"><div class="loading-spinner"></div> Gen ${i}/${totalLoops}...</div>`;
- const ok = await executeGenerationSingle(endpoint, payload, isImage, els.status, i, totalLoops);
- if (!ok) {
- allSuccess = false;
- break;
- }
- }
- if (!isRequestCancelled) {
- els.cancelBtn.style.display = 'none';
- els.previewBtn.style.display = 'flex';
- els.btn.disabled = false;
- els.btn.innerHTML = `<div class="btn-content">Generate</div>`;
- if (allSuccess) {
- els.status.innerText = "All runs complete.";
- els.status.className = "";
- els.status.style.color = "#64748b";
- notifyUser(false);
- } else {
- notifyUser(true);
- }
- }
- });
- // ─── SESSION RESETTER ─────────────────────────────────────────────────────
- els.reset.addEventListener('click', () => {
- if (!confirm("Wipe cache? (This forces a page reload to clear front-end tokens). Your prompt and settings will be saved.")) return;
- localStorage.setItem('xai_api_prompt', els.prompt.value);
- const patterns =[/flushAfter/i, /imagine/i, /generation/i, /grok/i, /mixpanel|mp_/i, /distinct_id/i, /_rst/i, /limit/i, /credit/i];
- for (let i = localStorage.length - 1; i >= 0; i--) {
- const k = localStorage.key(i);
- if (k && !k.startsWith('xai_api_') && patterns.some(p => p.test(k))) localStorage.removeItem(k);
- }
- document.cookie.split(';').forEach(c => {
- const name = c.split('=')[0]?.trim();
- if (!name || !patterns.some(p => name.toLowerCase().includes(p.toLowerCase()))) return;['.x.ai', 'console.x.ai', ''].forEach(d => {['/', '/playground/imagine', ''].forEach(p => { document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=${p}${d ? ';domain=' + d : ''}`; });
- });
- });
- const u = new URL(location.href);
- ['_r', '_rst'].forEach(k => u.searchParams.set(k, Date.now()));
- location.replace(u.toString());
- });
- }
- // ─── ASYNC PROCESSORS & RENDERERS ─────────────────────────────────────────
- async function processAndRenderImage(imgObj, originalPrompt, num, total, galleryEl) {
- if (!imgObj.b64_json) return;
- const mime = imgObj.mime_type || "image/png";
- const b64 = imgObj.b64_json;
- const pngDataUri = `data:${mime};base64,${b64}`;
- const finalPrompt = imgObj.revised_prompt || originalPrompt;
- let finalDataUri = pngDataUri;
- let filename = `Grok 2K - ${makeTimestamp()} - ${num}of${total}.png`;
- if (!appSettings.saveAsPng) {
- finalDataUri = await convertToJpegWithExif(pngDataUri, finalPrompt);
- filename = `Grok 2K - ${makeTimestamp()} - ${num}of${total}.jpg`;
- }
- const card = document.createElement('div');
- card.className = 'current-card';
- 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>
- `;
- const editBtn = card.querySelector('.overlay-edit-btn');
- editBtn.addEventListener('click', (e) => {
- e.preventDefault();
- if (typeof updateUIForEditMedia === 'function') updateUIForEditMedia(finalDataUri, false, filename);
- });
- galleryEl.prepend(card);
- }
- function renderVideoToGallery(videoUrl, originalPrompt, galleryEl) {
- const filename = `Grok Video - ${makeTimestamp()}.mp4`;
- const card = document.createElement('div');
- card.className = 'current-card';
- card.innerHTML = `
- <div class="img-wrapper">
- <video src="${videoUrl}" controls autoplay loop style="width: 100%; display: block; max-height: 50vh; object-fit: contain; background: #000;"></video>
- <a href="${videoUrl}" target="_blank" download="${filename}" class="overlay-btn overlay-dl-btn" style="z-index: 50;" title="Download MP4">${DOWNLOAD_ICON}</a>
- <button class="overlay-btn overlay-edit-btn" style="z-index: 50;" title="Edit this Video">${EDIT_ICON}</button>
- </div>
- <p style="padding: 10px; margin: 0; font-size: 12px; color: #cbd5e1; border-top: 1px solid #334155;">${originalPrompt}</p>
- `;
- const editBtn = card.querySelector('.overlay-edit-btn');
- editBtn.addEventListener('click', (e) => {
- e.preventDefault();
- if (typeof updateUIForEditMedia === 'function') updateUIForEditMedia(videoUrl, true, filename);
- });
- galleryEl.prepend(card);
- }
- if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initApp); }
- else { initApp(); }
- })();
Add Comment
Please, Sign In to add comment