Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // ==UserScript==
- // @name xAI Overkill UI (v0.01.24.9)
- // @namespace http://tampermonkey.net/
- // @version 0.01.24.9
- // @description PromptAndPray
- // @match https://console.x.ai/*
- // @grant GM_cookie
- // @grant GM_xmlhttpRequest
- // @grant GM_download
- // @run-at document-start
- // ==/UserScript==
- (function() {
- 'use strict';
- const APP_CONFIG = {
- USER_DEFAULTS: {
- action: 'gen_image', currentAr: 'Auto', batchSize: '2', duration: '8', loops: '0', resImg: '2k', resVid: '720p',
- refMode: 'multi', concurrentSessions: 1, rateLimitDelay: 60, retryMinMs: 2000, retryMaxMs: 4000,
- autoRetryVideo: true, referenceARTrick: true, ecoMode: false, autoDownload: false,
- leftCollapsed: false, rightCollapsed: false, leftWidth: 320, rightWidth: 320, imgBaseMs: 3000
- },
- LIMITS: { MAX_CONCURRENT_SESSIONS: 2, MAX_REFS_IMG: 5, MAX_REFS_VID: 7, MAX_REFS_EDIT: 1, MAX_REFS_QUEUE: 100, MAX_HISTORY: 2000 },
- TIMINGS: { API_POLL_INTERVAL: 5000, MAX_POLL_ATTEMPTS: 60, MAX_RETRY_ATTEMPTS: 60, PROMPT_COMMIT_DELAY_MS: 10000 },
- STORAGE: {
- CONFIG_KEY: 'xai_overkill_ui_config_2026',
- DB_NAME: 'XAI_OVERKILL_UI_2026',
- DB_VERSION: 1,
- STORE_META: 'history_meta_v1',
- STORE_DATA: 'app_state_v1',
- OPFS_DIR: 'xai_overkill_ui_media_2026'
- },
- ASPECT_RATIOS:[
- { label: 'Auto', w: 1, h: 1, isAuto: true }, { label: 'Ref', w: 1, h: 1, isAuto: true, isRef: true },
- { 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 },
- { 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, imageOnly: true }, { label: '19.5:9', w: 19.5, h: 9, imageOnly: true },
- { label: '9:20', w: 9, h: 20, imageOnly: true }, { label: '20:9', w: 20, h: 9, imageOnly: true },
- { label: '1:2', w: 1, h: 2, imageOnly: true }, { label: '2:1', w: 2, h: 1, imageOnly: true }
- ],
- PLACEHOLDER_SWATCHES: [
- { name: 'Arctic Slate', spot: '#223140', mid: '#17222D', bg: '#070809' },
- { name: 'Dusty Indigo', spot: '#2A2D43', mid: '#1D2030', bg: '#070809' },
- { name: 'Storm Blue', spot: '#263247', mid: '#1B2433', bg: '#070809' },
- { name: 'Quiet Plum', spot: '#2D2833', mid: '#201B25', bg: '#070809' },
- { name: 'Pine Smoke', spot: '#1F2F2A', mid: '#16221E', bg: '#070809' },
- { name: 'Coal Mocha', spot: '#2F2926', mid: '#221D1A', bg: '#070809' },
- { name: 'Midnight Alloy', spot: '#242A31', mid: '#191E24', bg: '#070809' },
- { name: 'Deep Petrol', spot: '#1C3135', mid: '#142328', bg: '#070809' },
- { name: 'Smoked Sage', spot: '#24302C', mid: '#19221F', bg: '#070809' },
- { name: 'Blue Ash', spot: '#243544', mid: '#192734', bg: '#070809' }
- ],
- ICONS: {
- refresh: '↻', settings: '⚙', star: '★', play: '▶', edit: '✄', extend: '◷', video: '🎞\uFE0E', close: '✕', check: '✔', editPrompt: '✎', imageRef: '🖼️', collapseLeft: '\\00AB', collapseRight: '\\00BB',
- download: `<svg viewBox="0 0 24 24" width="100%" height="100%" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>`
- }
- };
- const DEFAULT_PROMPTS = Object.freeze({ gen_image: '', gen_video: '', edit_video: '', extend_video: '' });
- const DEFAULT_FAV_PROMPTS = Object.freeze({ gen_image: [], gen_video: [], edit_video: [], extend_video: [] });
- const ACTION_META = Object.freeze({
- gen_image: { label: '▨ Image', title: 'Image Generation', model: 'grok-imagine-image', refType: 'image' },
- gen_video: { label: '🎞\uFE0E Video', title: 'Video Generation', model: 'grok-imagine-video', refType: 'image' },
- edit_video: { label: '✄ Edit', title: 'Edit Video Ref', model: 'grok-imagine-video', refType: 'video' },
- extend_video: { label: '◷ Extend', title: 'Extend Video Ref', model: 'grok-imagine-video', refType: 'video' }
- });
- const ALLOWED_VIDEO_ARS = new Set(['1:1', '16:9', '9:16', '4:3', '3:4', '3:2', '2:3']);
- const DOM_IDS = {
- app: 'xai-pro-app',
- prompt: 'xai-api-prompt',
- textareaWrapper: 'textarea-wrapper',
- promptContainer: 'prompt-container-main',
- actionDropdown: 'btn-master-action',
- resImgToggle: 'xai-api-res-img',
- resVidToggle: 'xai-api-res-vid',
- settingBatch: 'btn-setting-batch',
- settingDuration: 'btn-setting-duration',
- settingAr: 'btn-setting-ar',
- settingLoops: 'setting-loops',
- fileInput: 'xai-api-file',
- dropzone: 'col-right-dropzone',
- uploadPlaceholder: 'xai-upload-placeholder',
- refCounter: 'xai-ref-counter',
- refList: 'xai-ref-list',
- generateBtn: 'xai-api-generate',
- saveRefBtn: 'xai-api-save-ref',
- refEditLabel: 'ref-edit-label',
- settingsBtn: 'xai-settings-btn',
- favBtn: 'xai-fav-btn',
- settingsPopover: 'popover-settings',
- status: 'xai-api-status',
- history: 'xai-history',
- currentImages: 'xai-current-images',
- reset: 'xai-reset-btn',
- lightbox: 'xai-lightbox',
- lightboxInner: 'xai-lightbox-inner',
- lightboxStage: 'xai-lightbox-stage',
- lightboxDl: 'xai-lightbox-dl',
- refModeToggle: 'xai-ref-mode-toggle',
- favDialog: 'xai-fav-dialog',
- favList: 'fav-list-container',
- rateDelayInput: 'setting-rate-delay',
- sessionsInput: 'setting-sessions',
- delayMinInput: 'setting-delay-min',
- delayMaxInput: 'setting-delay-max',
- autoRetryVideoCheckbox: 'setting-auto-retry-video',
- arTrickCheckbox: 'setting-ar-trick',
- colLeft: 'col-left',
- colRight: 'col-right-dropzone',
- autoDownloadCheckbox: 'setting-auto-download',
- labelMasterAction: 'label-master-action',
- labelSettingDuration: 'label-setting-duration',
- popoverDuration: 'popover-duration',
- popoverAr: 'popover-ar',
- arSelectedIcon: 'ar-selected-icon',
- arSelectedText: 'ar-selected-text',
- metricSaved: 'metric-saved',
- metricDisk: 'metric-disk',
- metricLoad: 'metric-load',
- confirmDialog: 'xai-confirm-dialog',
- confirmMsg: 'xai-confirm-msg',
- labelSettingBatch: 'label-setting-batch',
- ecoModeCheckbox: 'setting-eco-mode'
- };
- const SETTINGS_TOGGLE_META = Object.freeze([
- { rowId: 'settings-auto-download-toggle', inputId: 'setting-auto-download', domKey: 'autoDownloadCheckbox', stateKey: 'autoDownload', label: 'Auto-Download Media', title: 'Save completed media to disk automatically' },
- { rowId: 'settings-auto-retry-video-toggle', inputId: 'setting-auto-retry-video', domKey: 'autoRetryVideoCheckbox', stateKey: 'autoRetryVideo', label: 'Auto-Retry Stuck Videos', title: 'Automatically restart generation if a video gets stuck' },
- { rowId: 'settings-ar-trick-toggle', inputId: 'setting-ar-trick', domKey: 'arTrickCheckbox', stateKey: 'referenceARTrick', label: 'Single Ref AR Trick', title: 'Send the single reference image twice to bypass API crop issues' },
- { rowId: 'settings-eco-mode-toggle', inputId: 'setting-eco-mode', domKey: 'ecoModeCheckbox', stateKey: 'ecoMode', label: 'Eco Mode (Low Mem)', title: 'Unload invisible images from RAM to prevent browser crashes' }
- ]);
- const SETTINGS_NUMBER_META = Object.freeze([
- { inputId: 'setting-sessions', domKey: 'sessionsInput', stateKey: 'concurrentSessions', label: 'Concurrent Sessions', title: 'Number of simultaneous API requests (higher = faster, but hits rate limits sooner)', min: 1, max: APP_CONFIG.LIMITS.MAX_CONCURRENT_SESSIONS },
- { inputId: 'setting-rate-delay', domKey: 'rateDelayInput', stateKey: 'rateLimitDelay', label: 'Rate Limit Delay (s)', title: 'Pause duration when strict rate limit (429) is hit', min: 0, max: 300 },
- { inputId: 'setting-delay-min', domKey: 'delayMinInput', stateKey: 'retryMinMs', label: 'Delay Min (ms)', title: 'Minimum random delay range between API retries to avoid spamming', min: 500, step: 500, wide: true },
- { inputId: 'setting-delay-max', domKey: 'delayMaxInput', stateKey: 'retryMaxMs', label: 'Delay Max (ms)', title: 'Maximum random delay range between API retries', min: 1000, step: 500, wide: true }
- ]);
- const EXPORT_STATE_KEYS = Object.freeze([
- 'action', 'currentAr', 'batchSize', 'duration', 'loops', 'resImg', 'resVid',
- 'concurrentSessions', 'rateLimitDelay', 'retryMinMs', 'retryMaxMs',
- 'autoRetryVideo', 'referenceARTrick', 'refMode', 'ecoMode', 'autoDownload',
- 'leftCollapsed', 'rightCollapsed', 'prompts', 'promptDrafts', 'refPrompts', 'favPrompts'
- ]);
- const ActionUtils = {
- get(action = STATE.action) { return ACTION_META[action] || ACTION_META.gen_image; },
- getLabel(action = STATE.action) { return this.get(action).label; },
- getTitle(action = STATE.action) { return this.get(action).title; },
- getModel(action = STATE.action) { return this.get(action).model; },
- isImage(action = STATE.action) { return action === 'gen_image'; },
- isVideo(action = STATE.action) { return action !== 'gen_image'; },
- isVideoGeneration(action = STATE.action) { return action === 'gen_video'; },
- isVideoEdit(action = STATE.action) { return action === 'edit_video' || action === 'extend_video'; },
- usesImageRefs(action = STATE.action) { return !this.isVideoEdit(action); },
- getRefType(action = STATE.action) { return this.get(action).refType; },
- getMaxRefs(action = STATE.action, refMode = STATE.refMode) {
- if (this.isVideoEdit(action)) return APP_CONFIG.LIMITS.MAX_REFS_EDIT;
- if (refMode === 'queue') return APP_CONFIG.LIMITS.MAX_REFS_QUEUE;
- return this.isVideoGeneration(action) ? APP_CONFIG.LIMITS.MAX_REFS_VID : APP_CONFIG.LIMITS.MAX_REFS_IMG;
- },
- getDurationOptions(action = STATE.action) {
- const max = action === 'extend_video' ? 10 : 15;
- return Array.from({ length: max }, (_, idx) => {
- const value = String(idx + 1);
- return { val: value, text: action === 'extend_video' ? `${value}s Ext` : `${value}s` };
- });
- },
- getUploadAction(currentAction, mimeType) {
- const isVideo = mimeType.startsWith('video/');
- if (isVideo && this.usesImageRefs(currentAction)) return 'edit_video';
- if (!isVideo && this.isVideoEdit(currentAction)) return 'gen_image';
- return currentAction;
- },
- getCardAction(itemType, mode = 'edit') {
- if (mode === 'extend') return 'extend_video';
- if (mode === 'video') return 'gen_video';
- return itemType === 'video' ? 'edit_video' : 'gen_image';
- },
- getEndpoint(action = STATE.action, refCount = 0) {
- if (action === 'gen_image') return refCount ? '/v1/images/edits' : '/v1/images/generations';
- if (action === 'gen_video') return '/v1/videos/generations';
- if (action === 'edit_video') return '/v1/videos/edits';
- return '/v1/videos/extensions';
- },
- getGenerationWarnings(action = STATE.action, refs = [], duration = parseInt(STATE.duration) || 5, aspectRatio = STATE.currentAr) {
- if (!this.isVideoGeneration(action)) return [];
- const warnings = [];
- if (refs.length > 0 && duration > 10) {
- if (aspectRatio !== 'Auto' && aspectRatio !== 'Ref') warnings.push(">10s video might stretch refs instead of resizing. Auto AR is recommended.");
- if (refs.length > 1) warnings.push(">10s video uses only the first reference.");
- }
- if (duration > 8) warnings.push("Videos >8s cannot be used for edits later.");
- return warnings;
- }
- };
- const LOG_HISTORY =[];
- const LOG_HISTORY_LIMIT = 2000;
- const RETRYABLE_GATEWAY_STATUSES = new Set([500, 502, 503]);
- const AUTH_FAILURE_STATUSES = new Set([401, 403]);
- const RATE_LIMIT_FAILURE_STATUSES = new Set([422, 429]);
- const VIDEO_SUCCESS_STATES = new Set(['done', 'completed']);
- const VIDEO_FLAGGED_STATES = new Set(['failed', 'expired', 'rejected', 'blocked', 'moderated', 'nsfw']);
- const CURRENT_PANEL_STATUS_STYLE = "white-space:normal; font-size:13px; text-align:center; padding:8px;";
- const STUCK_VIDEO_RETRY_TOKENS = ['timeout: video', 'generation halted.', 'video rejected'];
- const REQUEST_MODERATION_RE = /moderation|blocked|nsfw|is_sensitive|sensitive|policy/i;
- const REQUEST_INVALID_RE = /invalid argument|invalid request|unsupported/i;
- const stripHeavyData = (val) => { if (val === null || val === undefined) return val; if (typeof val === 'string') return (val.startsWith('data:') || val.length > 2000) ? `[TRUNCATED_LEN_${val.length}]` : val; if (typeof val !== 'object') return val; if (Array.isArray(val)) return val.map(stripHeavyData); const copy = {}; for (let k in val) copy[k] = stripHeavyData(val[k]); return copy; };
- const addLog = (lvl, msg, data = null) => {
- try {
- const outData = data ? stripHeavyData(data) : '';
- const normalizedLevel = String(lvl || '').toUpperCase();
- const consoleMethod = normalizedLevel.includes('ERROR') ? 'error' : normalizedLevel.includes('WARN') ? 'warn' : 'log';
- console[consoleMethod](`[xAI][${lvl}] ${msg}`, outData);
- LOG_HISTORY.push({ lvl, msg, timestamp: Date.now(), data: outData });
- if (LOG_HISTORY.length > LOG_HISTORY_LIMIT) LOG_HISTORY.shift();
- } catch(e) { console.error('[xAI][LOGGER_PANIC]', e); }
- };
- const HISTORY_MEASURE_EPSILON = 4;
- const createMarkupFragment = (markup = '') => {
- const template = document.createElement('template');
- template.innerHTML = markup;
- return template.content.cloneNode(true);
- };
- const scheduleObjectUrlRevoke = (url, delayMs = 10000) => {
- if (typeof url !== 'string' || !url.startsWith('blob:')) return;
- setTimeout(() => {
- try { URL.revokeObjectURL(url); }
- catch (e) { addLog('WARN', 'revokeObjectURL failed', { url, error: e?.message || e }); }
- }, delayMs);
- };
- const parseResponseJsonSafe = async (response) => {
- try { return await response.json(); } catch (e) { return null; }
- };
- const addErrorDetail = (parts, value, label = '') => {
- if (value === null || value === undefined || value === '') return;
- let text = '';
- if (typeof value === 'string') text = value.trim();
- else {
- try { text = JSON.stringify(value); } catch (e) { text = String(value); }
- }
- if (!text) return;
- const line = label ? `${label}: ${text}` : text;
- if (!parts.includes(line)) parts.push(line);
- };
- const buildHttpErrorMessage = (status, errData = null) => {
- const parts = [`HTTP ${status}`];
- const err = errData?.error;
- if (typeof err === 'string') addErrorDetail(parts, err);
- else if (err && typeof err === 'object') {
- addErrorDetail(parts, err.message);
- addErrorDetail(parts, err.code, 'Code');
- addErrorDetail(parts, err.detail, 'Detail');
- addErrorDetail(parts, err.type, 'Type');
- }
- addErrorDetail(parts, errData?.message);
- addErrorDetail(parts, errData?.code, 'Code');
- addErrorDetail(parts, errData?.detail, 'Detail');
- return parts.join('\n');
- };
- const buildCurrentStatusLines = (status) => [{ text: status.longStatus, color: status.color, style: CURRENT_PANEL_STATUS_STYLE }];
- const getTaskSummaryText = (task) => task?.shortStatus || task?.longStatus || '...';
- const isRequestModeratedError = (message = '') => REQUEST_MODERATION_RE.test(message);
- const isInvalidRequestError = (message = '') => REQUEST_INVALID_RE.test(message);
- const shouldAutoRetryStuckVideo = (message = '', autoRetryEnabled = STATE.autoRetryVideo) => message.includes('video lost') || (autoRetryEnabled && STUCK_VIDEO_RETRY_TOKENS.some(token => message.includes(token)));
- const sleep = (ms, signal) => new Promise(res => { if (signal?.aborted) return res(); const t = setTimeout(res, ms); signal?.addEventListener('abort', () => { clearTimeout(t); res(); }, { once: true }); });
- const postTaskSafe = (fn, opts = {}) => {
- if (typeof scheduler !== 'undefined' && typeof scheduler.postTask === 'function') return scheduler.postTask(fn, opts);
- return new Promise((resolve, reject) => {
- const run = () => { try { Promise.resolve(fn()).then(resolve, reject); } catch (e) { reject(e); } };
- if (opts?.priority === 'user-blocking' && typeof queueMicrotask === 'function') queueMicrotask(run);
- else setTimeout(run, 0);
- });
- };
- const runBg = (fn, opts = { priority: 'background' }) => postTaskSafe(async () => { try { return await fn(); } catch (e) { addLog('WARN', 'runBg err', e); } }, opts);
- const yieldToMain = () => (typeof scheduler !== 'undefined' && scheduler.yield) ? scheduler.yield() : new Promise(r => setTimeout(r, 0));
- const runTransition = (fn) => { fn(); return null; };
- const isAutoEcoEnabled = () => !!document.body?.classList.contains('auto-eco');
- const isMemoryConstrainedMode = () => STATE.ecoMode || isAutoEcoEnabled();
- const getVisibleCurrentCardCount = () => {
- const map = typeof CardRegistry !== 'undefined' ? CardRegistry.maps?.current : null;
- if (!map || map.size === 0) return 0;
- let count = 0;
- map.forEach(card => { if (card?._isActuallyVisible) count++; });
- return count;
- };
- const getThumbCacheLimit = () => isMemoryConstrainedMode() ? 36 : 120;
- const getFullCacheLimit = () => {
- if (isMemoryConstrainedMode()) return 2;
- const visibleCount = getVisibleCurrentCardCount();
- return Math.min(18, Math.max(6, (visibleCount * 3) + 4));
- };
- const getThumbMaxDim = () => isMemoryConstrainedMode() ? 420 : 600;
- const getThumbQuality = () => isMemoryConstrainedMode() ? 0.68 : 0.75;
- const getHistoryOverscanPx = () => isMemoryConstrainedMode() ? 640 : 1400;
- const getHistoryPrerenderBudget = () => isMemoryConstrainedMode() ? 12 : 20;
- const applyMemoryPressureProfile = (reason = 'manual') => {
- MediaWarmPool.releaseAll();
- RefPayloadCache.clear();
- CardRegistry.maps.current.forEach(card => card.setLoadLevel(isMemoryConstrainedMode() ? (card._isActuallyVisible ? 'thumb' : 'none') : (card._isActuallyVisible ? 'full' : 'thumb')));
- CardRegistry.maps.history.forEach(card => card.setLoadLevel(isMemoryConstrainedMode() ? (card._isActuallyVisible ? 'thumb' : 'none') : 'thumb'));
- CardRegistry.maps.reference.forEach(card => card.setLoadLevel('thumb'));
- BlobCache.trim({ thumb: getThumbCacheLimit(), full: getFullCacheLimit() });
- DOM.historyScroller?.refreshMemoryPressure?.(reason);
- };
- const dateFormatter = new Intl.DateTimeFormat('sv-SE', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false });
- const makeTimestamp = () => dateFormatter.format(new Date()).replace(/:/g, '-');
- const sanitizeUrl = (url) => {
- if (typeof url !== 'string' || !url) return '';
- if (url.startsWith('data:') || url.startsWith('blob:') || url.startsWith('/')) return url;
- try { return ['http:', 'https:'].includes(new URL(url).protocol) ? url : ''; } catch(e) { return ''; }
- };
- const fetchBlobCSP = (url, range = null) => new Promise((resolve, reject) => {
- if (typeof GM_xmlhttpRequest === 'undefined') return reject(new Error('GM_xmlhttpRequest not granted'));
- GM_xmlhttpRequest({ method: 'GET', url, headers: range ? { 'Range': `bytes=${range}` } : {}, responseType: 'blob',
- onload: (res) => (res.status >= 200 && res.status < 300) ? resolve(res.response) : reject(new Error(`HTTP ${res.status}`)), onerror: reject, ontimeout: () => reject(new Error('Timeout'))
- });
- });
- const DomUtils = {
- insertTextAtCursor(inputEl, text, replaceAll = false) {
- inputEl.focus();
- if (replaceAll) { inputEl.select(); document.execCommand('insertText', false, text); } else document.execCommand('insertText', false, text);
- inputEl.dispatchEvent(new Event('input', { bubbles: true }));
- },
- base64ToBlob(b64, mimeType) {
- const byteChars = atob(b64); const byteArrays =[];
- for (let offset = 0; offset < byteChars.length; offset += 512) {
- const slice = byteChars.slice(offset, offset + 512), byteNums = new Array(slice.length);
- for (let i = 0; i < slice.length; i++) byteNums[i] = slice.charCodeAt(i);
- byteArrays.push(new Uint8Array(byteNums));
- }
- return new Blob(byteArrays, { type: mimeType });
- }
- };
- const MediaUtils = {
- _queue: {
- active: { video: 0, image: 0 }, limits: { video: 2, image: 4 }, tasks:[],
- enqueue(type, taskFn) { const { promise, resolve, reject } = Promise.withResolvers(); this.tasks.push({ type, taskFn, resolve, reject }); this._next(); return promise; },
- _next() {
- const idx = this.tasks.findIndex(t => this.active[t.type] < this.limits[t.type]); if (idx === -1) return;
- const { type, taskFn, resolve, reject } = this.tasks.splice(idx, 1)[0]; this.active[type]++;
- Promise.resolve().then(taskFn).then(resolve).catch(reject).finally(() => { this.active[type]--; this._next(); });
- }
- },
- _lqip: new OffscreenCanvas(2, 2),
- _getCtx() { return this._lqip.getContext('2d', { alpha: false, willReadFrequently: true }); },
- async fileToBase64(file, mimeType = 'image/jpeg') {
- if (!file) return null;
- const targetBlob = file.type === mimeType ? file : new Blob([file], { type: mimeType }), { promise, resolve } = Promise.withResolvers(), r = new FileReader();
- r.onload = () => resolve(r.result); r.onerror = () => resolve(null); r.readAsDataURL(targetBlob); return promise;
- },
- async processSource(source, type, maxDim = 600, qual = 0.75) {
- return this._queue.enqueue(type, async () => {
- const isVid = type === 'video', isLocal = isVid && source instanceof Blob, url = isVid ? (isLocal ? URL.createObjectURL(source) : source) : null;
- let bmp = null, v = null, cleanup = () => { if (isLocal && url) URL.revokeObjectURL(url); if (v) { v.removeAttribute('src'); v.load(); v = null; } if (bmp) bmp.close(); };
- try {
- if (isVid) {
- v = document.createElement('video'); v.muted = v.playsInline = true; if (!isLocal) v.crossOrigin = 'anonymous';
- const { promise, resolve, reject } = Promise.withResolvers();
- v.onloadeddata = () => { v.currentTime = 0.1; };
- v.onseeked = async () => { try { resolve(await createImageBitmap(v, { resizeWidth: maxDim, resizeQuality: 'medium' })); } catch (e) { reject(e); } };
- v.onerror = () => reject(new Error('Decode err')); v.src = url; v.load(); bmp = await promise;
- } else bmp = await createImageBitmap(source, { resizeWidth: maxDim, resizeQuality: 'medium' });
- if (!bmp) throw new Error("Bitmap err");
- const ratio = bmp.width / bmp.height, ctx = this._getCtx(); ctx.drawImage(bmp, 0, 0, 2, 2);
- const d = ctx.getImageData(0, 0, 2, 2).data, rgb = (i) => `${d[i]},${d[i+1]},${d[i+2]}`;
- const avg = [
- Math.round((d[0] + d[4] + d[8] + d[12]) / 4),
- Math.round((d[1] + d[5] + d[9] + d[13]) / 4),
- Math.round((d[2] + d[6] + d[10] + d[14]) / 4)
- ];
- const dark = avg.map(v => Math.max(0, Math.round(v * 0.32))).join(',');
- const base = avg.map(v => Math.max(0, Math.round(v * 0.55))).join(',');
- const lqip = `radial-gradient(circle at 22% 20%, rgb(${rgb(0)}) 0, transparent 52%), radial-gradient(circle at 78% 28%, rgb(${rgb(4)}) 0, transparent 58%), linear-gradient(180deg, rgb(${base}) 0%, rgb(${dark}) 100%)`;
- const exportCanvas = new OffscreenCanvas(bmp.width, bmp.height); exportCanvas.getContext('2d', { alpha: false }).drawImage(bmp, 0, 0);
- const thumbBlob = await exportCanvas.convertToBlob({ type: 'image/webp', quality: qual });
- cleanup(); return { thumbBlob, lqip, ratio };
- } catch (err) { addLog('WARN', 'processSource err', err); cleanup(); return { thumbBlob: null, lqip: null, ratio: 16/9 }; }
- });
- },
- async downloadItem(item, silent = false, card = null) {
- if (!silent) document.body.style.cursor = 'wait';
- try {
- let blobUrl = item.originalUrl, isBlobUrl = item.type === 'image' || (item.originalUrl?.startsWith('blob:') ?? false);
- if (item.type === 'image') { const blob = await OPFS.read(item.id, 'full'); if (blob) { blobUrl = URL.createObjectURL(blob); isBlobUrl = true; } }
- if (!blobUrl) throw new Error('No URL');
- const markDl = () => {
- if (item.downloaded) return;
- item.downloaded = true;
- DBQueue.add(item);
- card?.classList.add('downloaded');
- CardRegistry.setDownloaded(item.id, true);
- };
- const fallbackDl = async () => { let fetchUrl = blobUrl, needRevoke = false; if (!isBlobUrl && item.originalUrl) { fetchUrl = URL.createObjectURL(await fetchBlobCSP(item.originalUrl)); needRevoke = true; }
- const a = document.createElement('a'); a.href = fetchUrl; a.download = item.filename || `dl.${item.type === 'video' ? 'mp4' : 'png'}`; document.body.appendChild(a); a.click(); a.remove(); markDl();
- if (isBlobUrl || needRevoke) scheduleObjectUrlRevoke(fetchUrl);
- };
- if (isBlobUrl || typeof GM_download === 'undefined') await fallbackDl(); else GM_download({ url: blobUrl, name: item.filename || `dl.${item.type === 'video' ? 'mp4' : 'png'}`, onload: markDl, onerror: fallbackDl });
- } catch (err) { addLog('WARN', 'Download failed', err); if (!silent && item.originalUrl) { const a = document.createElement('a'); a.href = item.originalUrl; a.download = item.filename || 'dl'; a.target = '_blank'; document.body.appendChild(a); a.click(); a.remove(); } }
- if (!silent) document.body.style.cursor = '';
- }
- };
- const UIUtils = {
- getIcon: (char, size = 16, cls = '') => `<span class="icon-inline ${cls}" style="font-size:${size}px;">${char}</span>`,
- getDlIcon: (size = 18) => `<div class="icon-dl flex-center" style="width:${size}px; height:${size}px;">${APP_CONFIG.ICONS.download}</div>`,
- createArIcon: (w, h, isAuto, isRef) => {
- if (isAuto || isRef) return `<div class="ar-icon-shell"><div class="ar-icon-letter">${isRef ? 'R' : 'A'}</div></div>`;
- const scale = 14 / Math.max(w, h);
- const inner = `<div class="ar-icon-frame" style="width:${w * scale}px; height:${h * scale}px;"></div>`;
- return `<div class="ar-icon-shell">${inner}</div>`;
- }
- };
- const TemplateUtils = {
- separator: () => `<div style="border-top: 1px solid var(--border); margin: 4px 0;"></div>`,
- settingsToggle: ({ rowId, inputId, label, title }) => `<div class="popover-item flex-between" id="${rowId}" title="${title}"><span title="${title}">${label}</span><input type="checkbox" id="${inputId}" style="pointer-events:none; margin:0;"></div>`,
- settingsNumber: ({ inputId, label, title, min, max, step, wide }) => `<div class="popover-item flex-between" style="cursor:default;" title="${title}"><span title="${title}">${label}</span><input type="number" id="${inputId}" min="${min}"${max != null ? ` max="${max}"` : ''}${step != null ? ` step="${step}"` : ''} class="input-popover${wide ? ' input-popover-lg' : ''}"></div>`
- };
- const buildSettingsPopoverHtml = () => [
- ...SETTINGS_TOGGLE_META.slice(0, 3).map(TemplateUtils.settingsToggle),
- TemplateUtils.separator(),
- ...SETTINGS_NUMBER_META.slice(0, 2).map(TemplateUtils.settingsNumber),
- TemplateUtils.separator(),
- ...SETTINGS_NUMBER_META.slice(2).map(TemplateUtils.settingsNumber),
- TemplateUtils.separator(),
- TemplateUtils.settingsToggle(SETTINGS_TOGGLE_META[3]),
- TemplateUtils.separator(),
- `<div class="gap-2 d-flex" style="padding:4px 8px;"><button id="btn-export-state" class="xai-btn" style="flex:1; font-size:12px; height:28px;" title="Export all settings and prompts to a file">Export</button><button id="btn-import-state" class="xai-btn" style="flex:1; font-size:12px; height:28px;" title="Import settings and prompts from a file">Import</button><input type="file" id="file-import-state" accept=".json" class="d-none"></div>`
- ].join('');
- const buildConfirmDialogHtml = () => `<dialog id="xai-confirm-dialog" class="modal-box"><div class="dialog-inner text-center" style="min-width:250px;"><div id="xai-confirm-msg" style="white-space:pre-wrap; margin-bottom:15px; text-align:left;"></div><form method="dialog" style="display:flex; gap:10px; justify-content:center;"><button value="cancel" class="xai-btn">Cancel</button><button value="cookie" class="xai-btn xai-btn--warning">Cookie</button><button value="data" class="xai-btn xai-btn--orange">Data</button><button value="full" class="xai-btn xai-btn--danger">Full</button></form></div></dialog>`;
- const buildCardTemplateHtml = () => `<div class="top-bar"><div class="ref-header drag-handle" title="Drag to reorder"><input class="ref-active-toggle" type="checkbox" title="Toggle reference"><div class="ref-label-text-wrapper"><span class="ref-label-text">IMAGE_0</span></div><button class="xai-btn xai-btn--square xai-btn--glass xai-btn--xs hover-danger ref-del" title="Delete">${APP_CONFIG.ICONS.close}</button></div></div><div class="media-wrapper"><div class="bg-layer" id="bg-base"></div><div class="play-icon" id="play">${APP_CONFIG.ICONS.play}</div><img id="img" decoding="async"><video id="vid" preload="none" muted loop playsinline disablePictureInPicture disableRemotePlayback></video><div class="overlay"><button class="xai-btn xai-btn--square xai-btn--glass xai-btn--sm-square hover-danger overlay-del-btn" title="Delete Media / Cancel Task">${APP_CONFIG.ICONS.close}</button><div class="overlay-actions"><button class="xai-btn xai-btn--square xai-btn--glass xai-btn--sm-square overlay-edit-btn" title="Edit Media">${APP_CONFIG.ICONS.edit}</button><button class="xai-btn xai-btn--square xai-btn--glass xai-btn--sm-square overlay-extend-btn" title="Extend Video">${APP_CONFIG.ICONS.extend}</button><button class="xai-btn xai-btn--square xai-btn--glass xai-btn--sm-square overlay-video-btn" title="Make Video">${APP_CONFIG.ICONS.video}</button><a class="xai-btn xai-btn--square xai-btn--glass xai-btn--dl-square overlay-dl-btn ml-auto" title="Download">${UIUtils.getDlIcon(18)}</a></div></div></div><button class="xai-btn xai-btn--square xai-btn--glass ref-side-prompt-btn" title="Edit Prompt">${APP_CONFIG.ICONS.editPrompt}</button><div class="ref-status-overlay" id="status-overlay"></div><div class="bottom-bar"><p class="card-prompt" title=""></p></div>`;
- const buildAppHtml = () => `<div id="xai-pro-app" data-active-action="gen_image"><div class="col-left" id="col-left"><div class="panel"><div class="panel-header"><div id="history-metrics" class="metrics-container" title="History Stats"><span id="metric-saved" class="metric-pill" title="History Items Saved">--</span><span id="metric-load" class="metric-pill d-none" title="System CPU/RAM Load">--</span><button id="xai-reset-btn" class="xai-btn xai-btn--icon-sm xai-btn--danger" title="Wipe Session Cache & Reload">${UIUtils.getIcon(APP_CONFIG.ICONS.refresh, 14)}</button><span id="metric-disk" class="metric-pill" title="History Storage Used">--</span></div><button id="btn-collapse-left" class="btn-collapse" aria-expanded="true" title="Toggle Left Panel"></button></div><div id="xai-history"></div></div></div><div class="col-center" id="col-center"><div id="xai-current-images"></div><div class="prompt-panel-wrapper" id="prompt-panel-wrapper"><div id="xai-toast-container"></div><div id="ref-edit-label" class="floating-edit-label">Editing Prompt</div><div class="prompt-container" id="prompt-container-main"><div class="textarea-wrapper" id="textarea-wrapper"><textarea id="xai-api-prompt" placeholder="Shift+Enter: line break Dynamic nesting: {cat|dog|{red|blue} car}"></textarea></div><div class="prompt-actions"><div id="action-row-global-left" class="action-row-left"><button id="xai-settings-btn" class="xai-btn" title="Open Settings Menu">${UIUtils.getIcon(APP_CONFIG.ICONS.settings, 18)}</button><button id="xai-fav-btn" class="xai-btn" title="Favorite Prompts (Saved per selected mode)">${UIUtils.getIcon(APP_CONFIG.ICONS.star, 18)}</button><button class="xai-btn" id="btn-master-action" data-value="gen_image" title="Select Generation Action"><span id="label-master-action">▨ Image</span><span class="ar-dropdown-icon">▼</span></button><button id="btn-setting-ar" class="xai-btn" title="Select Aspect Ratio"><div id="ar-selected-icon"></div><span id="ar-selected-text">Auto</span><span class="ar-dropdown-icon">▼</span></button><button id="xai-api-res-img" class="xai-btn" data-value="2k" title="Toggle Image Resolution">2K Pro</button><button id="xai-api-res-vid" class="xai-btn" data-value="720p" title="Toggle Video Resolution">720p</button><button class="xai-btn" id="btn-setting-batch" data-value="2" title="Images per batch"><span id="label-setting-batch">2</span><span class="ar-dropdown-icon">▼</span></button><button class="xai-btn" id="btn-setting-duration" data-value="5" title="Video duration"><span id="label-setting-duration">5s</span><span class="ar-dropdown-icon">▼</span></button><div class="xai-btn" id="btn-setting-loops" title="Auto repeat loops"><span style="opacity:0.7;margin-right:4px;">↻</span><input type="number" id="setting-loops" value="0" min="0" max="999" class="input-loop"></div></div><div class="action-row-right"><div id="xai-api-status" class="w-100 d-flex justify-end" style="font-size:14px; font-weight:500; padding-right:8px;" title="Current API Status"><span class="text-muted">Ready</span></div><button id="xai-api-generate" class="xai-btn xai-btn--circle-lg xai-btn--run" title="Start Generation"><div class="btn-content flex-center">↑</div></button><button id="xai-api-save-ref" class="xai-btn xai-btn--success d-none" title="Save Ref Prompt and Exit">Save & Exit</button></div></div></div></div></div><div class="col-right" id="col-right-dropzone"><div class="col-right-inner"><div class="panel"><div class="panel-header"><button id="btn-collapse-right" class="btn-collapse" aria-expanded="true" title="Toggle Right Panel"></button><button id="xai-upload-placeholder" class="xai-btn" title="Click to upload media or Drag & Drop"><span class="upload-label-main">Add Media</span></button><span id="xai-ref-counter" class="metric-pill" title="Active reference count">0</span><button id="xai-ref-mode-toggle" class="xai-btn" style="width:70px; padding:0;" title="Toggle Queue/Multi mode">Multi</button></div><input type="file" id="xai-api-file" accept="image/*, video/mp4, video/webm" multiple class="d-none"><div id="xai-ref-list"></div></div></div></div><dialog id="xai-lightbox" class="xai-lightbox-dialog"><div id="xai-lightbox-inner" class="lightbox-frame"><div id="xai-lightbox-stage" class="lightbox-stage"><a id="xai-lightbox-dl" href="#" download="" target="_blank" class="dl-abs-btn" title="Download Lightbox Image/Video">${UIUtils.getDlIcon(20)}</a></div></div></dialog><dialog id="xai-fav-dialog" class="xai-custom-dialog"><div class="dialog-inner" style="max-width:1600px; width:95vw; border-radius:24px; padding:4px;"><div id="fav-list-container" class="dialog-content"></div></div></dialog><div id="popover-action" popover="auto" class="xai-custom-popover"></div><div id="popover-ar" popover="auto" class="xai-custom-popover"></div><div id="popover-batch" popover="auto" class="xai-custom-popover"></div><div id="popover-duration" popover="auto" class="xai-custom-popover"></div><div id="popover-settings" popover="auto" class="xai-custom-popover">${buildSettingsPopoverHtml()}</div>${buildConfirmDialogHtml()}</div>`;
- const originalTitle = document.title || "xAI Advanced UI";
- const shuffleInPlace = (list) => {
- for (let i = list.length - 1; i > 0; i--) {
- const j = Math.floor(Math.random() * (i + 1));
- [list[i], list[j]] = [list[j], list[i]];
- }
- return list;
- };
- const PlaceholderPalette = {
- deck: [],
- lastName: '',
- _gradient({ spot, mid, bg }) {
- return `radial-gradient(circle at 30% 22%, ${spot} 0%, ${mid} 46%, ${bg} 100%)`;
- },
- next() {
- if (this.deck.length === 0) {
- this.deck = shuffleInPlace(APP_CONFIG.PLACEHOLDER_SWATCHES.map(swatch => ({ ...swatch })));
- if (this.deck[0]?.name === this.lastName && this.deck.length > 1) [this.deck[0], this.deck[1]] = [this.deck[1], this.deck[0]];
- }
- const swatch = this.deck.shift() || APP_CONFIG.PLACEHOLDER_SWATCHES[0];
- this.lastName = swatch.name;
- return { ...swatch, gradient: this._gradient(swatch) };
- }
- };
- const TitleState = {
- markDone() {
- STATE.pendingDoneNotice = document.hidden || !document.hasFocus();
- this.sync();
- },
- clearDone() {
- if (!STATE.pendingDoneNotice) return;
- STATE.pendingDoneNotice = false;
- this.sync();
- },
- sync(progressText = '') {
- if (STATE.isGenerating) {
- setDocumentTitle(progressText ? `[${progressText}] ${originalTitle}` : originalTitle);
- return;
- }
- if (STATE.pendingDoneNotice && (document.hidden || !document.hasFocus())) {
- setDocumentTitle(`[Done] ${originalTitle}`);
- return;
- }
- setDocumentTitle(originalTitle);
- },
- handleTabActive() {
- if (document.hidden || !document.hasFocus()) return;
- this.clearDone();
- }
- };
- const createQueueWakeSignal = () => {
- let version = 0;
- let deferred = Promise.withResolvers();
- return {
- get version() { return version; },
- wait(seenVersion = version) {
- return seenVersion === version ? deferred.promise : Promise.resolve(version);
- },
- notify() {
- version++;
- deferred.resolve(version);
- deferred = Promise.withResolvers();
- }
- };
- };
- let queueWake = createQueueWakeSignal(), _configSaveTimer = null, _queueWakeTimer = null, DOM = {};
- const wakeUpQueue = () => queueWake.notify();
- const configKeys = new Set(Object.keys(APP_CONFIG.USER_DEFAULTS));
- const getConfigSnapshot = () => Object.fromEntries([...configKeys].map(key => [key, STATE[key]]));
- const IMPORT_PROMPT_KEYS = Object.keys(DEFAULT_PROMPTS);
- const IMPORT_ACTIONS = new Set(IMPORT_PROMPT_KEYS);
- const IMPORT_REF_MODES = new Set(['multi', 'queue']);
- const IMPORT_RES_IMG = new Set(['1k', '2k']);
- const IMPORT_RES_VID = new Set(['480p', '720p']);
- const IMPORT_ARS = new Set(APP_CONFIG.ASPECT_RATIOS.map(a => a.label.trim()));
- const RETRY_PROMPT_PAD_RE = /[\u200B-\u200D\uFEFF]/g;
- const clampInt = (value, min, max, fallback) => {
- const parsed = Number.parseInt(value, 10);
- return Number.isFinite(parsed) ? Math.min(max, Math.max(min, parsed)) : fallback;
- };
- const pickAllowed = (value, allowed, fallback) => allowed.has(value) ? value : fallback;
- const parseJsonSafe = (raw, fallback = null) => {
- if (typeof raw !== 'string' || raw === '') return fallback;
- try { return JSON.parse(raw); } catch (e) { return fallback; }
- };
- const cloneSerializable = (value, fallback = null) => {
- if (value === undefined) return fallback;
- if (typeof structuredClone === 'function') {
- try { return structuredClone(value); } catch (e) {}
- }
- try { return JSON.parse(JSON.stringify(value)); } catch (e) { return fallback; }
- };
- const normalizePromptDraft = (value, fallback = '') => typeof value === 'string' ? value : fallback;
- const sanitizePromptText = (value, fallback = '') => {
- const clean = typeof value === 'string' ? value.replace(RETRY_PROMPT_PAD_RE, '').trim() : '';
- if (clean) return clean;
- return typeof fallback === 'string' ? fallback.replace(RETRY_PROMPT_PAD_RE, '').trim() : '';
- };
- const sanitizePromptStore = (raw, fallback = DEFAULT_PROMPTS) => {
- const base = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
- return Object.fromEntries(IMPORT_PROMPT_KEYS.map(key => [key, normalizePromptDraft(base[key], fallback[key] || '')]));
- };
- const sanitizeFavPromptStore = (raw, fallback = DEFAULT_FAV_PROMPTS) => {
- const base = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
- return Object.fromEntries(IMPORT_PROMPT_KEYS.map(key => [key, Array.isArray(base[key]) ? base[key].filter(v => typeof v === 'string') : [...(fallback[key] || [])]]));
- };
- const sanitizeRefPromptStore = (raw, fallback = {}) => {
- if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return { ...fallback };
- return Object.fromEntries(Object.entries(raw).filter(([, prompt]) => typeof prompt === 'string').map(([id, prompt]) => [String(id), prompt]));
- };
- const sanitizeIdListStore = (raw) => Array.isArray(raw) ? [...new Set(raw.map(id => String(id)).filter(Boolean))] : [];
- const sanitizeRefDataStore = (raw) => Array.isArray(raw)
- ? raw.filter(entry => entry && typeof entry === 'object').map(entry => ({ id: String(entry.id), active: entry.active !== false }))
- : null;
- const sanitizePendingVideosStore = (raw) => {
- if (!Array.isArray(raw)) return [];
- const deduped = new Map();
- raw.filter(entry => entry && typeof entry === 'object' && typeof entry.reqId === 'string').forEach(entry => {
- deduped.set(entry.reqId, {
- reqId: entry.reqId,
- prompt: sanitizePromptText(entry.prompt),
- timestamp: clampInt(entry.timestamp, 0, Number.MAX_SAFE_INTEGER, Date.now())
- });
- });
- return [...deduped.values()];
- };
- const sanitizeStoredConfig = (raw) => {
- const defaults = APP_CONFIG.USER_DEFAULTS;
- const stored = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
- const retryMinMs = clampInt(stored.retryMinMs, 500, 60000, defaults.retryMinMs);
- const retryMaxMs = clampInt(stored.retryMaxMs, retryMinMs, 60000, Math.max(defaults.retryMaxMs, retryMinMs));
- return {
- action: pickAllowed(stored.action, IMPORT_ACTIONS, defaults.action),
- currentAr: pickAllowed(typeof stored.currentAr === 'string' ? stored.currentAr.trim() : '', IMPORT_ARS, defaults.currentAr),
- batchSize: String(clampInt(stored.batchSize, 1, 10, clampInt(defaults.batchSize, 1, 10, 2))),
- duration: String(clampInt(stored.duration, 1, 15, clampInt(defaults.duration, 1, 15, 5))),
- loops: String(clampInt(stored.loops, 0, 999, clampInt(defaults.loops, 0, 999, 0))),
- resImg: pickAllowed(stored.resImg, IMPORT_RES_IMG, defaults.resImg),
- resVid: pickAllowed(stored.resVid, IMPORT_RES_VID, defaults.resVid),
- refMode: pickAllowed(stored.refMode, IMPORT_REF_MODES, defaults.refMode),
- concurrentSessions: clampInt(stored.concurrentSessions, 1, APP_CONFIG.LIMITS.MAX_CONCURRENT_SESSIONS, defaults.concurrentSessions),
- rateLimitDelay: clampInt(stored.rateLimitDelay, 0, 300, defaults.rateLimitDelay),
- retryMinMs,
- retryMaxMs,
- autoRetryVideo: typeof stored.autoRetryVideo === 'boolean' ? stored.autoRetryVideo : defaults.autoRetryVideo,
- referenceARTrick: typeof stored.referenceARTrick === 'boolean' ? stored.referenceARTrick : defaults.referenceARTrick,
- ecoMode: typeof stored.ecoMode === 'boolean' ? stored.ecoMode : defaults.ecoMode,
- autoDownload: typeof stored.autoDownload === 'boolean' ? stored.autoDownload : defaults.autoDownload,
- leftCollapsed: typeof stored.leftCollapsed === 'boolean' ? stored.leftCollapsed : defaults.leftCollapsed,
- rightCollapsed: typeof stored.rightCollapsed === 'boolean' ? stored.rightCollapsed : defaults.rightCollapsed,
- leftWidth: clampInt(stored.leftWidth, 320, 2000, defaults.leftWidth),
- rightWidth: clampInt(stored.rightWidth, 240, 2000, defaults.rightWidth),
- imgBaseMs: clampInt(stored.imgBaseMs, 1500, 10000, defaults.imgBaseMs)
- };
- };
- const STATE_EFFECTS = {
- action: () => { if (STATE.isMounted) updateActionUI(); },
- isGenerating: (val) => {
- if (!STATE.isMounted) return;
- toggleGenerateButton(val);
- updateStatusUI();
- if (val) STATE.pendingDoneNotice = false;
- if (!val) {
- RefPayloadCache.clear();
- BlobCache.trim({ thumb: getThumbCacheLimit(), full: getFullCacheLimit() });
- DOM.historyScroller?.scheduleRender?.();
- }
- },
- refMode: () => { if (STATE.isMounted) renderRefMode(); },
- currentOrder: () => {
- if (!STATE.isMounted) return;
- NativeGridManager.update(STATE.currentOrder, STATE.items, 'current');
- DBQueue.addAppWrite('currentOrder', sanitizeIdListStore(STATE.currentOrder.filter(id => {
- const item = STATE.items.get(String(id));
- return item && !Pipeline.isTransientItem(item);
- })));
- },
- historyOrder: () => {
- if (!STATE.isMounted) return;
- DOM.historyScroller?.update(STATE.historyOrder, STATE.items);
- runBg(updateHistoryMetrics);
- },
- currentAr: () => { if (STATE.isMounted) NativeGridManager.update(STATE.currentOrder, STATE.items, 'current'); },
- ecoMode: (val) => {
- if (!STATE.isMounted) return;
- document.body.classList.toggle('eco-mode', val);
- applyMemoryPressureProfile(val ? 'eco-on' : 'eco-off');
- },
- loops: () => {
- if (!STATE.isMounted || !STATE.isGenerating) return;
- clearTimeout(_queueWakeTimer);
- _queueWakeTimer = setTimeout(wakeUpQueue, 300);
- updateStatusUI();
- },
- concurrentSessions: () => STATE_EFFECTS.loops(),
- leftCollapsed: (val) => { if (STATE.isMounted) { DOM.colLeft.classList.toggle('is-collapsed', val); document.getElementById('btn-collapse-left').setAttribute('aria-expanded', !val); requestAnimationFrame(() => { syncLayoutWidths(); NativeGridManager.syncCurrentLayoutMetrics?.(); }); } },
- rightCollapsed: (val) => { if (STATE.isMounted) { DOM.colRight.classList.toggle('is-collapsed', val); document.getElementById('btn-collapse-right').setAttribute('aria-expanded', !val); requestAnimationFrame(() => { syncLayoutWidths(); NativeGridManager.syncCurrentLayoutMetrics?.(); }); } }
- };
- const STATE = new Proxy({}, {
- set(target, prop, value) {
- if (target[prop] === value) return true; target[prop] = value; STATE_EFFECTS[prop]?.(value);
- if (configKeys.has(prop)) { clearTimeout(_configSaveTimer); _configSaveTimer = setTimeout(() => localStorage.setItem(APP_CONFIG.STORAGE.CONFIG_KEY, JSON.stringify(getConfigSnapshot())), 250); }
- return true;
- }
- });
- const JobManager = { tasks: new Map(), taskPromises: new Set(), progress: new Map() };
- const createInitialState = () => ({
- isMounted: false,
- items: new Map(),
- refOrder: [],
- currentOrder: [],
- historyOrder: [],
- isGenerating: false,
- activeLightboxId: null,
- generationToken: 0,
- activeRefPromptId: null,
- activeRefPromptAction: null,
- activeRefPromptDraft: '',
- successCount: 0,
- prompts: sanitizePromptStore(DEFAULT_PROMPTS),
- promptDrafts: sanitizePromptStore(DEFAULT_PROMPTS),
- favPrompts: sanitizeFavPromptStore(DEFAULT_FAV_PROMPTS),
- refPrompts: {},
- pendingVideos: [],
- pendingDoneNotice: false
- });
- function resetState() {
- const initialState = createInitialState();
- Object.assign(initialState, sanitizeStoredConfig(parseJsonSafe(localStorage.getItem(APP_CONFIG.STORAGE.CONFIG_KEY), {})));
- Object.assign(STATE, initialState); Object.keys(DOM).forEach(k => delete DOM[k]); JobManager.tasks.clear(); JobManager.taskPromises.clear(); JobManager.progress.clear();
- queueWake = createQueueWakeSignal();
- }
- resetState();
- function sanitizeImportedState(raw) {
- if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new Error('Invalid import payload');
- const prompts = { ...STATE.prompts };
- const promptDrafts = { ...STATE.promptDrafts };
- if (raw.prompts && typeof raw.prompts === 'object') IMPORT_PROMPT_KEYS.forEach(key => {
- if (typeof raw.prompts[key] === 'string') {
- prompts[key] = raw.prompts[key];
- promptDrafts[key] = raw.prompts[key];
- }
- });
- const refPrompts = raw.refPrompts && typeof raw.refPrompts === 'object' ? {} : { ...STATE.refPrompts };
- if (raw.refPrompts && typeof raw.refPrompts === 'object') Object.entries(raw.refPrompts).forEach(([id, prompt]) => { if (typeof prompt === 'string') refPrompts[String(id)] = prompt; });
- const favPrompts = Object.fromEntries(IMPORT_PROMPT_KEYS.map(key => [key, Array.isArray(STATE.favPrompts[key]) ? [...STATE.favPrompts[key]] : []]));
- if (raw.favPrompts && typeof raw.favPrompts === 'object') {
- IMPORT_PROMPT_KEYS.forEach(key => {
- if (Array.isArray(raw.favPrompts[key])) favPrompts[key] = raw.favPrompts[key].filter(v => typeof v === 'string');
- });
- }
- const retryMinMs = clampInt(raw.retryMinMs, 500, 60000, STATE.retryMinMs);
- const retryMaxMs = clampInt(raw.retryMaxMs, retryMinMs, 60000, Math.max(STATE.retryMaxMs, retryMinMs));
- return {
- action: pickAllowed(raw.action, IMPORT_ACTIONS, STATE.action),
- currentAr: pickAllowed(typeof raw.currentAr === 'string' ? raw.currentAr.trim() : '', IMPORT_ARS, STATE.currentAr),
- batchSize: String(clampInt(raw.batchSize, 1, 10, clampInt(STATE.batchSize, 1, 10, 2))),
- duration: String(clampInt(raw.duration, 1, 15, clampInt(STATE.duration, 1, 15, 5))),
- loops: String(clampInt(raw.loops, 0, 999, clampInt(STATE.loops, 0, 999, 0))),
- resImg: pickAllowed(raw.resImg, IMPORT_RES_IMG, STATE.resImg),
- resVid: pickAllowed(raw.resVid, IMPORT_RES_VID, STATE.resVid),
- concurrentSessions: clampInt(raw.concurrentSessions, 1, APP_CONFIG.LIMITS.MAX_CONCURRENT_SESSIONS, STATE.concurrentSessions),
- rateLimitDelay: clampInt(raw.rateLimitDelay, 0, 300, STATE.rateLimitDelay),
- retryMinMs,
- retryMaxMs,
- autoRetryVideo: typeof raw.autoRetryVideo === 'boolean' ? raw.autoRetryVideo : STATE.autoRetryVideo,
- referenceARTrick: typeof raw.referenceARTrick === 'boolean' ? raw.referenceARTrick : STATE.referenceARTrick,
- refMode: pickAllowed(raw.refMode, IMPORT_REF_MODES, STATE.refMode),
- ecoMode: typeof raw.ecoMode === 'boolean' ? raw.ecoMode : STATE.ecoMode,
- autoDownload: typeof raw.autoDownload === 'boolean' ? raw.autoDownload : STATE.autoDownload,
- leftCollapsed: typeof raw.leftCollapsed === 'boolean' ? raw.leftCollapsed : STATE.leftCollapsed,
- rightCollapsed: typeof raw.rightCollapsed === 'boolean' ? raw.rightCollapsed : STATE.rightCollapsed,
- leftWidth: clampInt(raw.leftWidth, 320, 2000, STATE.leftWidth),
- rightWidth: clampInt(raw.rightWidth, 240, 2000, STATE.rightWidth),
- prompts,
- promptDrafts,
- refPrompts,
- favPrompts
- };
- }
- class BlobLRUCache {
- constructor(maxFn) { this.getMax = maxFn; this.cache = new Map(); this.inFlight = new Map(); }
- _touch(key) {
- const entry = this.cache.get(key);
- if (!entry) return null;
- this.cache.delete(key);
- this.cache.set(key, entry);
- return entry;
- }
- async getUrl(id, type) {
- const key = `${id}_${type}`;
- const cached = this._touch(key);
- if (cached) return cached.url;
- if (this.inFlight.has(key)) return this.inFlight.get(key);
- const { promise, resolve } = Promise.withResolvers();
- this.inFlight.set(key, promise);
- (async () => {
- try {
- const blob = await OPFS.read(id, type);
- if (blob) { const url = URL.createObjectURL(blob); this.cache.set(key, { url, pins: 0 }); this.trim(); resolve(url); }
- else resolve(null);
- } catch(e) { resolve(null); } finally { this.inFlight.delete(key); }
- })();
- return promise;
- }
- async retain(id, type) {
- const key = `${id}_${type}`;
- const url = await this.getUrl(id, type);
- const entry = this._touch(key);
- if (entry) entry.pins = (entry.pins || 0) + 1;
- return url;
- }
- release(id, type) {
- const entry = this.cache.get(`${id}_${type}`);
- if (entry) entry.pins = Math.max(0, (entry.pins || 0) - 1);
- }
- delete(id) {
- [`${id}_thumb`, `${id}_full`].forEach(k => {
- if (this.cache.has(k)) { URL.revokeObjectURL(this.cache.get(k).url); this.cache.delete(k); }
- });
- }
- trim(forceLimit = null) {
- const limit = forceLimit !== null ? forceLimit : this.getMax();
- while (this.cache.size > limit) {
- const firstKey = [...this.cache.entries()].find(([, entry]) => !(entry.pins > 0))?.[0];
- if (!firstKey) break;
- URL.revokeObjectURL(this.cache.get(firstKey).url);
- this.cache.delete(firstKey);
- }
- }
- clear() { this.cache.forEach(v => URL.revokeObjectURL(v.url)); this.cache.clear(); this.inFlight.clear(); }
- }
- const BlobCache = {
- thumb: new BlobLRUCache(() => getThumbCacheLimit()),
- full: new BlobLRUCache(() => getFullCacheLimit()),
- delete(id) { this.thumb.delete(id); this.full.delete(id); },
- retain(id, type) { return this[type]?.retain(id, type); },
- release(id, type) { this[type]?.release(id, type); },
- trim(limit = null) {
- if (typeof limit === 'number') {
- this.thumb.trim(limit);
- this.full.trim(limit);
- return;
- }
- this.thumb.trim(limit?.thumb ?? null);
- this.full.trim(limit?.full ?? null);
- },
- clear() { this.thumb.clear(); this.full.clear(); }
- };
- const MediaWarmPool = {
- limit: 2,
- order: [],
- pending: new Map(),
- _splitKey(key) {
- const idx = key.lastIndexOf('_');
- return idx === -1 ? [key, 'full'] : [key.slice(0, idx), key.slice(idx + 1)];
- },
- _touch(key) {
- const idx = this.order.indexOf(key);
- if (idx !== -1) this.order.splice(idx, 1);
- this.order.push(key);
- },
- _trim() {
- while (this.order.length > this.limit) {
- const key = this.order.shift();
- if (!key) continue;
- const [id, type] = this._splitKey(key);
- BlobCache.release(id, type);
- }
- },
- async warm(item) {
- if (!item || item.type !== 'image' || item.isPending || STATE.ecoMode || document.body.classList.contains('auto-eco')) return null;
- const id = String(item.refSourceId || item.id);
- const key = `${id}_full`;
- if (this.pending.has(key)) return this.pending.get(key);
- if (this.order.includes(key)) {
- this._touch(key);
- return BlobCache.full.getUrl(id, 'full');
- }
- const promise = BlobCache.full.retain(id, 'full').then(url => {
- if (url) { this._touch(key); this._trim(); }
- return url;
- }).finally(() => this.pending.delete(key));
- this.pending.set(key, promise);
- return promise;
- },
- releaseAll() {
- while (this.order.length > 0) {
- const key = this.order.pop();
- if (!key) continue;
- const [id, type] = this._splitKey(key);
- BlobCache.release(id, type);
- }
- this.pending.clear();
- }
- };
- const RefPayloadCache = {
- dataUrls: new Map(),
- maxEntries() { return isMemoryConstrainedMode() ? 2 : 8; },
- _touch(key) {
- const entry = this.dataUrls.get(key);
- if (!entry) return null;
- this.dataUrls.delete(key);
- this.dataUrls.set(key, entry);
- return entry;
- },
- trim() {
- const limit = this.maxEntries();
- while (this.dataUrls.size > limit) {
- const firstKey = [...this.dataUrls.entries()].find(([, entry]) => !entry?.pending)?.[0] || this.dataUrls.keys().next().value;
- if (!firstKey) break;
- this.dataUrls.delete(firstKey);
- }
- },
- async get(item) {
- if (!item) return null;
- if (!item.isUploadedRef && item.originalUrl) return item.originalUrl;
- const key = String(item.id);
- const cached = this._touch(key);
- if (cached) return cached.promise;
- const entry = {
- pending: true,
- promise: (async () => {
- const blob = await OPFS.read(item.id, 'full');
- if (!blob) return item.originalUrl || null;
- return MediaUtils.fileToBase64(blob, item.mimeType || (item.type === 'video' ? 'video/mp4' : 'image/jpeg'));
- })()
- };
- this.dataUrls.set(key, entry);
- const result = await entry.promise;
- entry.pending = false;
- if (!result) this.dataUrls.delete(key);
- else this.trim();
- return result;
- },
- delete(id) { this.dataUrls.delete(String(id)); },
- clear() { this.dataUrls.clear(); }
- };
- const Pipeline = {
- normalizePrompt(value, fallback = '') { return sanitizePromptText(value, fallback); },
- isTransientItem(item) { return !!(item?.persist === false || item?.isPending || String(item?.id || '').startsWith('pending-') || item?.filename === 'Pending...'); },
- isPhantomMeta(meta) { return String(meta?.id || '').startsWith('pending-') || meta?.filename === 'Pending...'; },
- makeBatchTimestamp(base, index, total) { return base + (total - index); }
- };
- const Store = {
- commitNewItem(itemData, targetOrOptions = null, replacePlaceholderId = null) {
- const options = targetOrOptions && typeof targetOrOptions === 'object' && !Array.isArray(targetOrOptions)
- ? targetOrOptions
- : { targetArrayName: targetOrOptions, replacePlaceholderId };
- const item = { ...itemData, id: String(itemData.id), timestamp: itemData.timestamp || Date.now(), persist: options.persist ?? itemData.persist ?? !itemData.isPending };
- STATE.items.set(String(item.id), item);
- if (options.targetArrayName && STATE[options.targetArrayName]) {
- const arr = STATE[options.targetArrayName];
- const idx = options.replacePlaceholderId ? arr.indexOf(options.replacePlaceholderId) : -1;
- if (idx !== -1) { arr[idx] = item.id; this.removeItem(options.replacePlaceholderId); } else arr.push(item.id);
- STATE[options.targetArrayName] =[...arr];
- }
- if (item.persist !== false) DBQueue.add(item);
- return item;
- },
- addItem(item) { STATE.items.set(String(item.id), item); },
- removeItem(id) {
- const key = String(id);
- STATE.items.delete(key);
- DOM.historyScroller?.measurements?.delete?.(key);
- },
- prependHistory(ids) {
- if (!ids?.length) return;
- const orderedIds = ids.map(String), seen = new Set(orderedIds);
- STATE.historyOrder = [...orderedIds, ...STATE.historyOrder.filter(id => !seen.has(String(id)))];
- },
- clearCurrentTransient(ids) {
- if (!ids?.length) return;
- const staleIds = ids.map(String), staleSet = new Set(staleIds);
- STATE.currentOrder = STATE.currentOrder.filter(id => !staleSet.has(String(id)));
- staleIds.forEach(id => this.removeItem(id));
- },
- getRefItems() { return STATE.refOrder.map(id => STATE.items.get(id)).filter(Boolean); }
- };
- let wipePromise = null, lastWipeTime = 0;
- async function nuclearWipe() {
- if (wipePromise) return wipePromise;
- if (Date.now() - lastWipeTime < 3000) { await sleep(3000 - (Date.now() - lastWipeTime)); return; }
- lastWipeTime = Date.now();
- addLog('INFO', 'Executing nuclear cookie wipe...');
- wipePromise = (async () => {
- const clearCookies = (url) => new Promise(res => { GM_cookie.list({ url }, (cookies, err) => { if (err || !cookies?.length) return res(); Promise.allSettled(cookies.map(c => new Promise(r => GM_cookie.delete({ url, name: c.name }, r)))).then(res); }); });
- await Promise.allSettled([clearCookies(window.location.href), clearCookies('https://x.ai')]);
- document.cookie.split(';').forEach(c => { const n = c.split('=')[0].trim();['.x.ai', 'console.x.ai', ''].forEach(d => {['/', '/playground/imagine', ''].forEach(p => { document.cookie = `${n}=;max-age=0;path=${p}${d ? ';domain=' + d : ''}`; }); }); });
- try { await fetch('https://console.x.ai/playground/imagine', { credentials: 'include', cache: 'no-store' }); } catch(e) {}
- })();
- await wipePromise; wipePromise = null;
- }
- const OPFS = {
- _dirHandle: null,
- _fileHandles: new Map(),
- async initDir() {
- if (!this._dirHandle) {
- const root = await navigator.storage.getDirectory();
- this._dirHandle = await root.getDirectoryHandle(APP_CONFIG.STORAGE.OPFS_DIR, { create: true });
- }
- return this._dirHandle;
- },
- async getFileHandle(name, create = false) {
- if (this._fileHandles.has(name)) return this._fileHandles.get(name);
- const handle = await (await this.initDir()).getFileHandle(name, { create });
- this._fileHandles.set(name, handle);
- return handle;
- },
- async write(id, type, blob) {
- let w;
- try {
- const fileHandle = await this.getFileHandle(`${id}_${type}`, true);
- w = await fileHandle.createWritable({ keepExistingData: false });
- await w.write(blob);
- } catch (e) { addLog('ERROR', 'OPFS Write', e); } finally { if (w) await w.close().catch(()=>{}); }
- },
- async read(id, type) { try { return await (await this.getFileHandle(`${id}_${type}`)).getFile(); } catch (e) { return null; } },
- async del(id) {
- try {
- const dir = await this.initDir();
- await Promise.allSettled(['full', 'thumb'].map(async type => {
- const name = `${id}_${type}`;
- this._fileHandles.delete(name);
- await dir.removeEntry(name).catch(()=>{});
- }));
- } catch (e) { addLog('WARN', 'OPFS Delete', { id, error: e?.message || e }); }
- },
- async clear() {
- try {
- this._fileHandles.clear();
- await (await navigator.storage.getDirectory()).removeEntry(APP_CONFIG.STORAGE.OPFS_DIR, { recursive: true }).catch(()=>{});
- this._dirHandle = null;
- } catch (e) { addLog('WARN', 'OPFS Clear', { error: e?.message || e }); }
- }
- };
- const StorageDB = {
- db: null,
- async init() {
- if (this.db) return this.db;
- return new Promise((resolve, reject) => {
- const req = indexedDB.open(APP_CONFIG.STORAGE.DB_NAME, APP_CONFIG.STORAGE.DB_VERSION);
- req.onupgradeneeded = e => {
- const db = e.target.result;
- if (!db.objectStoreNames.contains(APP_CONFIG.STORAGE.STORE_META)) db.createObjectStore(APP_CONFIG.STORAGE.STORE_META, { keyPath: 'id' });
- if (!db.objectStoreNames.contains(APP_CONFIG.STORAGE.STORE_DATA)) db.createObjectStore(APP_CONFIG.STORAGE.STORE_DATA);
- };
- req.onsuccess = () => { this.db = req.result; resolve(this.db); }; req.onerror = () => reject(req.error);
- });
- },
- async _tx(stores, mode, cb) { const db = await this.init(); return new Promise((resolve, reject) => { const tx = db.transaction(stores, mode); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); cb(tx); }); },
- async loadData(key) {
- const db = await this.init();
- return new Promise(r => {
- const req = db.transaction(APP_CONFIG.STORAGE.STORE_DATA).objectStore(APP_CONFIG.STORAGE.STORE_DATA).get(key);
- req.onsuccess = () => r(req.result ?? null);
- req.onerror = () => r(null);
- });
- },
- async saveHistoryItemsBulk(items) { return this._tx([APP_CONFIG.STORAGE.STORE_META], 'readwrite', tx => { const store = tx.objectStore(APP_CONFIG.STORAGE.STORE_META); items.forEach(i => store.put(i)); }); },
- async loadAllMeta() {
- const metas = await new Promise(async r => { const db = await this.init(); const req = db.transaction(APP_CONFIG.STORAGE.STORE_META).objectStore(APP_CONFIG.STORAGE.STORE_META).getAll(); req.onsuccess = () => r(req.result ||[]); });
- return metas.map(m => ({ ...m, downloaded: Boolean(m.downloaded), lqip: m.lqip || null, isUploadedRef: Boolean(m.isUploadedRef), prompt: Pipeline.normalizePrompt(m.prompt) })).sort((a, b) => b.timestamp - a.timestamp);
- },
- async deleteItem(id) { BlobCache.delete(id); RefPayloadCache.delete(id); await OPFS.del(id); return this._tx([APP_CONFIG.STORAGE.STORE_META], 'readwrite', tx => tx.objectStore(APP_CONFIG.STORAGE.STORE_META).delete(id)); },
- async deleteItemsBulk(ids) { if (!ids?.length) return; await Promise.allSettled(ids.map(id => { BlobCache.delete(id); RefPayloadCache.delete(id); return OPFS.del(id); })); return this._tx([APP_CONFIG.STORAGE.STORE_META], 'readwrite', tx => { const store = tx.objectStore(APP_CONFIG.STORAGE.STORE_META); ids.forEach(id => store.delete(id)); }); },
- async clear() {
- BlobCache.clear();
- RefPayloadCache.clear();
- await OPFS.clear();
- await this._tx([APP_CONFIG.STORAGE.STORE_META, APP_CONFIG.STORAGE.STORE_DATA], 'readwrite', tx => {
- tx.objectStore(APP_CONFIG.STORAGE.STORE_META).clear();
- tx.objectStore(APP_CONFIG.STORAGE.STORE_DATA).clear();
- });
- this.db?.close?.();
- this.db = null;
- }
- };
- const DBQueue = {
- _appWrites: new Map(), _appWriteVersions: new Map(), _pendingItems: new Map(), _deletedIds: new Set(), _isFlushing: false, _flushGeneration: 0,
- _serializeItem(item) {
- if (!item || Pipeline.isTransientItem(item)) return null;
- return {
- id: String(item.id),
- timestamp: item.timestamp,
- type: item.type,
- mimeType: item.mimeType,
- ratio: item.ratio || 1,
- originalUrl: item.type === 'video' ? item.originalUrl : null,
- filename: item.filename,
- downloaded: item.downloaded || false,
- prompt: Pipeline.normalizePrompt(item.prompt),
- lqip: item.lqip,
- isUploadedRef: item.isUploadedRef || false
- };
- },
- add(item) {
- const serialized = this._serializeItem(item);
- if (!serialized) return;
- this._deletedIds.delete(serialized.id);
- this._pendingItems.set(serialized.id, serialized);
- this._queueFlush();
- },
- forgetItem(id) {
- const key = String(id);
- this._pendingItems.delete(key);
- this._deletedIds.add(key);
- },
- forgetItems(ids = []) {
- ids.forEach(id => this.forgetItem(id));
- },
- async _queueFlush() {
- if (this._isFlushing || this._pendingItems.size === 0) return;
- this._isFlushing = true;
- const flushGeneration = this._flushGeneration;
- await yieldToMain();
- try {
- if (flushGeneration !== this._flushGeneration) return;
- const itemsToSave =[];
- for (const item of this._pendingItems.values()) {
- if (this._deletedIds.has(item.id)) continue;
- if (!STATE.items.has(item.id)) continue;
- if (Pipeline.isPhantomMeta(item)) continue;
- itemsToSave.push(item);
- }
- this._pendingItems.clear();
- if (flushGeneration !== this._flushGeneration) return;
- if (itemsToSave.length > 0) { await StorageDB.saveHistoryItemsBulk(itemsToSave); runBg(updateHistoryMetrics); }
- } catch(e) { addLog('ERROR', 'DBQueue flush', e); } finally { this._isFlushing = false; if (this._pendingItems.size > 0) this._queueFlush(); }
- },
- addAppWrite(key, dataObj, delay = 500) {
- const snapshot = cloneSerializable(dataObj, dataObj);
- const nextVersion = (this._appWriteVersions.get(key) || 0) + 1;
- this._appWriteVersions.set(key, nextVersion);
- const prev = this._appWrites.get(key);
- if (prev?.timer) clearTimeout(prev.timer);
- const timer = setTimeout(async () => {
- if (this._appWriteVersions.get(key) !== nextVersion) return;
- this._appWrites.delete(key);
- try { await StorageDB._tx([APP_CONFIG.STORAGE.STORE_DATA], 'readwrite', tx => tx.objectStore(APP_CONFIG.STORAGE.STORE_DATA).put(snapshot, key)); }
- catch (e) { addLog('WARN', 'DBQueue app write failed', { key, error: e?.message || e }); }
- }, delay);
- this._appWrites.set(key, { timer, version: nextVersion });
- },
- clearPending() {
- this._flushGeneration++;
- this._pendingItems.clear();
- this._deletedIds.clear();
- this._appWrites.forEach(entry => clearTimeout(entry?.timer));
- this._appWrites.clear();
- this._appWriteVersions.clear();
- this._isFlushing = false;
- }
- };
- const persistEffectivePrompts = () => DBQueue.addAppWrite('prompts', sanitizePromptStore(STATE.prompts));
- const persistPromptDrafts = () => DBQueue.addAppWrite('promptDrafts', sanitizePromptStore(STATE.promptDrafts));
- const persistRefPrompts = () => DBQueue.addAppWrite('refPrompts', sanitizeRefPromptStore(STATE.refPrompts));
- const persistFavPrompts = () => DBQueue.addAppWrite('favPrompts', sanitizeFavPromptStore(STATE.favPrompts));
- const persistPendingVideos = () => DBQueue.addAppWrite('pendingVideos', sanitizePendingVideosStore(STATE.pendingVideos));
- const getStoredActionPrompt = (action = STATE.action) => normalizePromptDraft(STATE.prompts[action], '');
- const getDraftActionPrompt = (action = STATE.action) => normalizePromptDraft(STATE.promptDrafts[action], getStoredActionPrompt(action));
- const getEditorPromptValue = () => normalizePromptDraft(DOM.prompt?.value, '');
- const setEditorPromptValue = (value = '') => {
- if (DOM.prompt) DOM.prompt.value = normalizePromptDraft(value);
- };
- const getActiveRefPromptBaseline = () => getStoredActionPrompt(STATE.activeRefPromptAction || STATE.action);
- const hasCustomRefPromptDraft = (value = STATE.activeRefPromptDraft, baseline = getActiveRefPromptBaseline()) => {
- const draft = sanitizePromptText(value);
- return !!draft && draft !== sanitizePromptText(baseline);
- };
- const setActionPromptDraft = (action = STATE.action, value = getEditorPromptValue()) => {
- const nextValue = normalizePromptDraft(value);
- if (STATE.promptDrafts[action] === nextValue) return nextValue;
- STATE.promptDrafts = { ...STATE.promptDrafts, [action]: nextValue };
- persistPromptDrafts();
- return nextValue;
- };
- const commitActionPrompt = (action = STATE.action, value = getDraftActionPrompt(action)) => {
- const nextValue = normalizePromptDraft(value);
- if (STATE.prompts[action] === nextValue) return nextValue;
- STATE.prompts = { ...STATE.prompts, [action]: nextValue };
- persistEffectivePrompts();
- return nextValue;
- };
- const getTaskPrompt = (action = STATE.action) => {
- const stored = sanitizePromptText(getStoredActionPrompt(action));
- if (stored || STATE.isGenerating) return stored;
- return sanitizePromptText(getDraftActionPrompt(action));
- };
- const PromptSync = {
- timers: new Map(),
- captureEditor(action = STATE.action, { scheduleCommit = true } = {}) {
- const draft = setActionPromptDraft(action, getEditorPromptValue());
- if (scheduleCommit) this.scheduleCommit(action, draft);
- return draft;
- },
- scheduleCommit(action = STATE.action, value = getDraftActionPrompt(action), delay = APP_CONFIG.TIMINGS.PROMPT_COMMIT_DELAY_MS) {
- this.cancel(action);
- const normalized = normalizePromptDraft(value);
- const timer = setTimeout(() => {
- this.timers.delete(action);
- commitActionPrompt(action, normalized);
- }, delay);
- this.timers.set(action, timer);
- return normalized;
- },
- commitNow(action = STATE.action) {
- this.cancel(action);
- return commitActionPrompt(action, getDraftActionPrompt(action));
- },
- cancel(action = STATE.action) {
- const timer = this.timers.get(action);
- if (timer) clearTimeout(timer);
- this.timers.delete(action);
- },
- clear() {
- this.timers.forEach(timer => clearTimeout(timer));
- this.timers.clear();
- }
- };
- const deleteRefPrompt = (id) => {
- const next = { ...STATE.refPrompts };
- delete next[id];
- STATE.refPrompts = next;
- persistRefPrompts();
- };
- const setRefPrompt = (id, prompt) => {
- STATE.refPrompts = { ...STATE.refPrompts, [id]: prompt };
- persistRefPrompts();
- };
- const getAspectRatioOption = (label = STATE.currentAr) => APP_CONFIG.ASPECT_RATIOS.find(option => option.label.trim() === label) || APP_CONFIG.ASPECT_RATIOS[0];
- const syncActionLabel = (action = STATE.action) => {
- if (DOM.actionDropdown) DOM.actionDropdown.dataset.value = action;
- if (DOM.labelMasterAction) DOM.labelMasterAction.innerText = ActionUtils.getLabel(action);
- };
- const syncAspectRatioUI = (label = STATE.currentAr) => {
- const option = getAspectRatioOption(label);
- if (DOM.settingAr) DOM.settingAr.dataset.value = option.label.trim();
- if (DOM.arSelectedIcon) {
- const iconMarkup = UIUtils.createArIcon(option.w, option.h, option.isAuto, option.isRef);
- DOM.arSelectedIcon.replaceChildren(...(iconMarkup ? [createMarkupFragment(iconMarkup)] : []));
- DOM.arSelectedIcon.style.display = iconMarkup ? 'flex' : 'none';
- }
- if (DOM.arSelectedText) DOM.arSelectedText.innerText = option.label.trim();
- };
- const syncResolutionButtons = () => {
- if (DOM.resImgToggle) {
- DOM.resImgToggle.dataset.value = STATE.resImg;
- DOM.resImgToggle.innerText = STATE.resImg === '2k' ? '2K Pro' : '1K Std';
- }
- if (DOM.resVidToggle) {
- DOM.resVidToggle.dataset.value = STATE.resVid;
- DOM.resVidToggle.innerText = STATE.resVid === '720p' ? '720p' : '480p';
- }
- };
- const syncSettingsInputs = () => {
- SETTINGS_NUMBER_META.forEach(({ domKey, stateKey }) => {
- if (DOM[domKey]) DOM[domKey].value = STATE[stateKey];
- });
- SETTINGS_TOGGLE_META.forEach(({ domKey, stateKey }) => {
- if (DOM[domKey]) DOM[domKey].checked = !!STATE[stateKey];
- });
- if (DOM.settingLoops) DOM.settingLoops.value = STATE.loops;
- if (DOM.settingBatch) DOM.settingBatch.dataset.value = STATE.batchSize;
- if (DOM.labelSettingBatch) DOM.labelSettingBatch.innerText = STATE.batchSize;
- };
- const syncLayoutWidths = () => {
- if (DOM.colLeft) DOM.colLeft.style.width = Math.max(320, STATE.leftWidth) + 'px';
- if (DOM.colRight) DOM.colRight.style.width = STATE.rightWidth + 'px';
- };
- const escapeSelectorValue = (value) => (globalThis.CSS && typeof CSS.escape === 'function') ? CSS.escape(String(value)) : String(value).replace(/["\\]/g, '\\$&');
- const setActivePopoverItem = (popover, value) => {
- if (!popover) return;
- const stringValue = String(value);
- const current = popover.querySelector('.popover-item.active');
- if (current?.dataset.val === stringValue) return;
- current?.classList.remove('active');
- popover.querySelector(`.popover-item[data-val="${escapeSelectorValue(stringValue)}"]`)?.classList.add('active');
- };
- const buildPopoverOptionItem = (selectedValue, opt) => {
- const optionValue = String(opt.val);
- const item = document.createElement('div');
- const classNames = ['popover-item', String(selectedValue) === optionValue ? 'active' : '', opt.disabled ? 'disabled' : '', opt.icon ? 'has-icon' : 'no-icon'].filter(Boolean);
- item.className = classNames.join(' ');
- item.dataset.val = optionValue;
- item.title = opt.title || '';
- if (opt.icon) {
- const icon = document.createElement('div');
- icon.className = 'popover-icon';
- icon.append(createMarkupFragment(opt.icon));
- item.appendChild(icon);
- }
- const text = document.createElement('span');
- text.textContent = opt.text;
- item.appendChild(text);
- return item;
- };
- const renderPopoverOptions = (popover, selectedValue, opts = []) => {
- const fragment = document.createDocumentFragment();
- opts.forEach(opt => fragment.appendChild(buildPopoverOptionItem(selectedValue, opt)));
- popover.replaceChildren(fragment);
- };
- const buildExportState = () => Object.fromEntries(EXPORT_STATE_KEYS.map(key => [key, STATE[key]]));
- const activateReferenceItem = (item) => {
- if (!item) return false;
- const wasInactive = item.active !== true;
- item.active = true;
- if (STATE.isGenerating && wasInactive) item.readyAt = Date.now() + 10000;
- else if (!Number.isFinite(item.readyAt)) item.readyAt = 0;
- return wasInactive;
- };
- const moveReferenceToFront = (id, { exclusiveActive = false, enforceLimit = false, forceRebuild = true } = {}) => {
- const key = String(id);
- const item = STATE.items.get(key);
- if (!item) return false;
- if (enforceLimit && !STATE.refOrder.includes(key) && STATE.refOrder.length >= APP_CONFIG.LIMITS.MAX_REFS_QUEUE) return false;
- if (exclusiveActive) {
- STATE.refOrder.forEach(rid => {
- const refItem = STATE.items.get(String(rid));
- if (refItem) refItem.active = false;
- });
- }
- activateReferenceItem(item);
- const wasFirst = STATE.refOrder[0] === key;
- STATE.refOrder = [key, ...STATE.refOrder.filter(rid => String(rid) !== key)];
- updateRefState({ forceRebuild: forceRebuild && !wasFirst });
- if (DOM.refList) DOM.refList.scrollTop = 0;
- return true;
- };
- function updateRefState({ forceRebuild = false } = {}) {
- DBQueue.addAppWrite('refData', STATE.refOrder.map(id => ({ id, active: STATE.items.get(id)?.active ?? true })));
- if (forceRebuild && DOM.refList) NativeGridManager.clear(DOM.refList, 'reference');
- renderRefList();
- updateUploadPlaceholderState();
- }
- const CardRegistry = {
- maps: {
- current: new Map(),
- history: new Map(),
- reference: new Map()
- },
- register(card, context, id) {
- const store = this.maps[context];
- if (store) store.set(String(id), card);
- },
- unregister(card, context, id) {
- const store = this.maps[context];
- if (store && store.get(String(id)) === card) store.delete(String(id));
- },
- move(card, prevContext, prevId, nextContext, nextId) {
- if (prevContext && prevId !== undefined) this.unregister(card, prevContext, prevId);
- this.register(card, nextContext, nextId);
- },
- get(context, id) { return this.maps[context]?.get(String(id)) || null; },
- forEachById(id, visitor) {
- const key = String(id);
- Object.values(this.maps).forEach(store => {
- const card = store.get(key);
- if (card) visitor(card);
- });
- },
- setDownloaded(id, downloaded = true) {
- this.forEachById(id, card => card.classList.toggle('downloaded', !!downloaded));
- },
- find(id) {
- const targetId = String(id);
- return this.maps.current.get(targetId) || this.maps.history.get(targetId) || this.maps.reference.get(targetId) || null;
- },
- clear(context = null) {
- if (context) this.maps[context]?.clear();
- else Object.values(this.maps).forEach(map => map.clear());
- }
- };
- const ViewportObserver = new IntersectionObserver((entries) => {
- entries.forEach(e => {
- const card = e.target; card._isActuallyVisible = e.isIntersecting;
- if (e.isIntersecting) {
- if (card._context === 'current') card.setLoadLevel(isMemoryConstrainedMode() ? 'thumb' : 'full');
- else card.setLoadLevel('thumb');
- } else {
- if (typeof card.forcePause === 'function') card.forcePause();
- card.setLoadLevel(card._context === 'current' ? (isMemoryConstrainedMode() ? 'none' : 'thumb') : 'none');
- if (card._context === 'current') BlobCache.trim({ full: getFullCacheLimit() });
- }
- });
- }, { rootMargin: '320px 0px 320px 0px' });
- const cardSheet = new CSSStyleSheet();
- cardSheet.replaceSync(`
- :host{position:relative;display:flex;flex-direction:column;background:var(--panel);border:1px solid var(--border);border-radius:8px;overflow:hidden;contain:strict;flex-shrink:0;user-select:none;will-change:transform;transition:box-shadow .2s,border-color .2s;content-visibility:auto;contain-intrinsic-size:auto 300px;container-type:inline-size;}
- :host(.downloaded){outline:2px solid var(--success);outline-offset:-2px;}
- :host(.is-history){contain:layout paint style;width:100%;aspect-ratio:var(--ratio)/1;margin-bottom:6px;}
- :host(.is-current){contain:layout paint style;--target-h:var(--gallery-target-h, 40vh);height:var(--target-h);aspect-ratio:var(--ratio)/1;flex:0 0 auto;max-width:100%;border:none;background:0 0;margin:0;border-radius:0;transition:height .4s cubic-bezier(0.2,0.8,0.2,1);}
- :host(.is-current:not(.is-pending)) .media-wrapper{border-radius:0;inset:0;border:none;}
- :host(.is-reference){flex:0 0 auto;width:100%;height:calc(100cqw / var(--ratio,1));min-height:100px;max-height:100cqh;transition:opacity .15s;}
- :host(.is-reference.inactive){opacity:.4;filter:grayscale(1);}
- :host(.is-editing){outline:2px solid var(--success);outline-offset:-2px;border-color:var(--success);}
- :host(.is-editing) .ref-header{background:rgb(from var(--success) r g b/80%);border-bottom-color:transparent;}
- :host(.is-editing) .ref-side-prompt-btn{opacity:1;visibility:visible;background:var(--success)!important;border-color:var(--success)!important;color:#000!important;scale:1.05;}
- .media-wrapper{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background-color:#000;border-radius:6px;overflow:hidden;transition:transform .2s;}
- .media-wrapper::before{content:'';position:absolute;inset:0;background:rgb(8 10 14 / 0);opacity:var(--media-dim-opacity,0);transition:opacity .15s;z-index:4;pointer-events:none;}
- .bg-layer{position:absolute;inset:0;background-size:100% 100%;z-index:0;}
- :host(.is-pending) .media-wrapper{inset:8px;border-radius:12px;box-shadow:0 4px 16px rgba(0,0,0,.4);transform:translateZ(0);overflow:hidden;}
- :host(.is-pending) .bg-layer::after,:host(.is-processing) .bg-layer::after{content:'';position:absolute;top:0;left:-150%;width:150%;height:100%;background:linear-gradient(90deg,transparent,rgba(255,255,255,.08),transparent);transform:skewX(-20deg);animation:xai-shimmer 2s infinite;z-index:1;pointer-events:none;}
- :host(.is-pending) .media-wrapper::after,:host(.is-processing) .media-wrapper::after{content:'';position:absolute;inset:0;background:rgba(0,0,0,.62);transform-origin:right;transform:scaleX(calc(1 - (var(--p-raw,0) / 100)));transition:transform .24s linear;z-index:5;pointer-events:none;border-left:2px solid rgba(255,255,255,.2);}
- @keyframes xai-shimmer{100%{transform:translateX(250%) skewX(-20deg);}}
- #img, #vid { position:absolute; inset:0; width:100%; height:100%; border-radius:0; z-index:3; transition:opacity .2s ease-out; object-fit:contain; opacity:0; display:none; will-change:transform; }
- #vid { z-index:2; }
- :host(.is-loaded) #img, :host(.is-loaded) #vid { opacity:1; }
- :host(.is-playing) #img { opacity:0; pointer-events:none; }
- :host(.is-pending) #img, :host(.is-pending) #vid { inset:6px; width:calc(100% - 12px); height:calc(100% - 12px); border-radius:6px; }
- :host(.is-reference:not(.is-highlighted)){--media-dim-opacity:.38;}
- :host(.is-reference.inactive){--media-dim-opacity:.58;}
- .ref-header,.ref-side-prompt-btn,.bottom-bar{display:none;}
- :host(.is-reference) .ref-header{display:flex;}
- :host(.is-reference) .ref-side-prompt-btn{display:inline-flex;}
- :host(.is-history) .bottom-bar{display:block;}
- :host(.is-history) .media-wrapper{border-bottom:1px solid var(--border);}
- :host(.is-history) .media-wrapper,:host(.is-reference) .media-wrapper{position:relative;flex:1 1 auto;min-height:0;background:var(--bg);}
- .overlay{position:absolute;inset:0;display:flex;justify-content:space-between;align-items:flex-start;padding:8px;pointer-events:none;opacity:0;transition:opacity .15s;z-index:10;}
- :host(:hover) .overlay{opacity:1;}
- @media (hover:none){.overlay{opacity:1;background:linear-gradient(to top,rgba(0,0,0,.6) 0,transparent 60px);}}
- :host(.is-reference) .overlay{display:none!important;}
- :host(.is-pending) .overlay{pointer-events:none;}
- :host(.is-pending) .overlay>*:not(.overlay-del-btn){display:none!important;}
- .overlay>*{pointer-events:auto;}
- :host(.type-image) .overlay-extend-btn{display:none!important;}
- :host(.type-video) .overlay-video-btn{display:none!important;}
- .play-icon{position:absolute;inset:50% auto auto 50%;translate:-50% -50%;width:48px;height:48px;background:rgb(8 10 14 / 72%);border-radius:50%;color:#fff;font-size:24px;padding-left:4px;border:1px solid rgb(from var(--accent) r g b/20%);z-index:6;display:none;align-items:center;justify-content:center;transition:opacity .15s;pointer-events:none;}
- .top-bar,.bottom-bar{position:relative;z-index:15;}
- .ref-status-overlay{position:absolute;inset:0;display:none;flex-direction:column;justify-content:center;align-items:center;z-index:20;pointer-events:none;padding:0 10px;gap:4px;}
- .ref-status-line{background:rgb(0 0 0/80%); padding:clamp(4px,2cqi,8px) clamp(10px,3cqi,16px); border-radius:6px; font-size:clamp(11px,4cqi,14px); font-weight:600; color:#fff; border:1px solid rgb(from var(--accent) r g b/15%); max-width:100%; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; box-shadow:0 2px 8px rgb(0 0 0/70%); line-height:1.3;}
- :host(.is-reference) .ref-status-line{font-size:11px;padding:4px 8px;}
- .xai-btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;height:36px;padding:0 16px;white-space:nowrap;flex-shrink:0;border-radius:20px;border:none;outline:none;text-decoration:none;font-family:inherit;font-size:13px;font-weight:600;cursor:pointer;background:rgb(from var(--accent) r g b/12%);color:var(--text-main);transition:.15s ease;}
- .xai-btn:hover{background:rgb(from var(--accent) r g b/20%);box-shadow:inset 0 0 0 1px rgb(from var(--accent) r g b/25%);}
- .xai-btn--square{width:32px;height:32px;padding:0;border-radius:8px;font-size:16px;}
- .xai-btn--xs{width:24px;height:24px;font-size:12px;}
- .xai-btn--sm-square{width:28px;height:28px;font-size:12px;}
- .xai-btn--dl-square{width:32px;height:32px;}
- .xai-btn--glass{background:rgb(0 0 0/50%)!important;border:1px solid rgb(from var(--accent) r g b/20%)!important;color:#fff!important;}
- .xai-btn--glass:hover{background:rgb(0 0 0/80%)!important;}
- .hover-danger:hover{background:var(--danger)!important;border-color:var(--danger)!important;color:#fff!important;}
- .overlay-actions{display:flex;gap:4px;align-items:center;}
- .overlay-dl-btn{margin-left:8px;}
- .card-prompt{padding:6px 8px;margin:0;font-size:11px;line-height:1.3;color:var(--text-main);cursor:pointer;display:-webkit-box;line-clamp:2;box-orient:vertical;overflow:hidden;height:34px;box-sizing:border-box;background:rgb(from var(--accent) r g b/2%);}
- .card-prompt:hover{background:rgb(from var(--accent) r g b/10%);}
- .ref-header{position:absolute;inset:0 0 auto 0;justify-content:space-between;align-items:center;padding:6px 10px;background:rgb(0 0 0/58%);border-bottom:1px solid rgb(from var(--accent) r g b/10%);z-index:var(--z-local-header);gap:6px;pointer-events:auto;cursor:grab;transition:background-color .15s;}
- .ref-active-toggle{cursor:pointer;margin:0;pointer-events:auto;width:14px;height:14px;flex-shrink:0;}
- .ref-label-text-wrapper{flex:1;display:flex;align-items:center;pointer-events:none;overflow:hidden;}
- .ref-label-text{font-family:monospace;font-size:13px;font-weight:600;color:var(--text-main);pointer-events:none;transition:color .15s;}
- .ref-label-text.insertable-tag{pointer-events:auto;cursor:pointer;text-decoration:underline;color:var(--success);}
- .ref-side-prompt-btn{position:absolute;left:8px;top:50%;translate:0 -50%;z-index:25;pointer-events:auto;opacity:0;visibility:hidden;transition:opacity .2s,visibility .2s;margin:0;}
- .ref-side-prompt-btn.has-prompt{opacity:1;visibility:visible;color:var(--success)!important;border-color:var(--success)!important;background:rgb(from var(--success) r g b/15%)!important;}
- :host(:not(.is-reference)) .ref-side-prompt-btn{display:none!important;}
- :host(:hover) .ref-side-prompt-btn{opacity:1;visibility:visible;}
- .drag-handle{cursor:grab;} .ml-auto{margin-left:auto;} .mr-auto{margin-right:auto;} .dragging{opacity:.5;box-shadow:0 0 0 2px var(--success);}
- `);
- const xaiCardTemplate = document.createElement('template');
- xaiCardTemplate.innerHTML = buildCardTemplateHtml();
- class XaiCard extends HTMLElement {
- constructor() {
- super(); this.attachShadow({ mode: 'open' }); this.shadowRoot.adoptedStyleSheets =[cardSheet];
- this._bound = false; this._mediaState = 'none';
- this._loadController = null; this._hoverTimer = null; this._warmTimer = null; this._isActuallyVisible = false; this._statusSig = ''; this._progressValue = null;
- this.shadowRoot.appendChild(xaiCardTemplate.content.cloneNode(true));
- this.mediaWrapper = this.shadowRoot.querySelector('.media-wrapper'); this.overlay = this.shadowRoot.querySelector('.overlay');
- this.bgBase = this.shadowRoot.getElementById('bg-base'); this.playIcon = this.shadowRoot.getElementById('play');
- this.imgEl = this.shadowRoot.getElementById('img'); this.vidEl = this.shadowRoot.getElementById('vid');
- this.statusOverlay = this.shadowRoot.getElementById('status-overlay'); this.refToggle = this.shadowRoot.querySelector('.ref-active-toggle');
- this.refSideBtn = this.shadowRoot.querySelector('.ref-side-prompt-btn'); this.promptText = this.shadowRoot.querySelector('.card-prompt'); this.refLabelText = this.shadowRoot.querySelector('.ref-label-text');
- }
- setProgress(p) {
- const next = p == null ? null : Number(p);
- if (this._progressValue === next) return;
- this._progressValue = next;
- if (next == null) { this.removeAttribute('data-progress'); this.style.setProperty('--p-raw', '0'); }
- else { this.dataset.progress = String(next); this.style.setProperty('--p-raw', String(next)); }
- }
- setStatus(lines) {
- const normalized = Array.isArray(lines) ? lines : [];
- const sig = normalized.length ? normalized.map(l => `${l.text || ''}|${l.color || ''}|${l.style || ''}`).join('\n') : '';
- if (this._statusSig === sig) return;
- this._statusSig = sig;
- if (normalized.length === 0) {
- this.statusOverlay.style.display = 'none';
- this.statusOverlay.replaceChildren();
- return;
- }
- const fragment = document.createDocumentFragment();
- normalized.forEach(l => {
- const d = document.createElement('div');
- d.className = 'ref-status-line';
- if (l.color) d.style.color = l.color;
- if (l.style) d.style.cssText += l.style;
- d.textContent = l.text;
- fragment.appendChild(d);
- });
- this.statusOverlay.style.display = 'flex';
- this.statusOverlay.replaceChildren(fragment);
- }
- setLabel(text, title = '', onClick = null) {
- if (!this.refLabelText) return;
- if (this.refLabelText.textContent !== text) this.refLabelText.textContent = text;
- this.refLabelText.classList.toggle('insertable-tag', typeof onClick === 'function');
- this.refLabelText.title = title;
- this.refLabelText.onclick = onClick;
- }
- setHistoryPrompt(prompt) {
- if (!this.promptText) return;
- const safePrompt = Pipeline.normalizePrompt(prompt);
- const preview = safePrompt ? (safePrompt.length > 120 ? safePrompt.substring(0, 120) + '...' : safePrompt) : 'No prompt';
- if (this.promptText.title !== safePrompt) this.promptText.title = safePrompt;
- if (this.promptText.textContent !== preview) this.promptText.textContent = preview;
- }
- setReferenceActive(active) {
- if (this.refToggle) this.refToggle.checked = !!active;
- this.classList.toggle('inactive', !active);
- }
- setReferencePromptFlag(hasPrompt) {
- if (this.refSideBtn) this.refSideBtn.classList.toggle('has-prompt', !!hasPrompt);
- }
- refresh(item, context = this._context) {
- if (!this._bound || this.dataset.id !== String(item.id) || this._context !== context) {
- this.bind(item, context);
- return;
- }
- this._item = item;
- this.classList.toggle('downloaded', !!item.downloaded);
- this.classList.toggle('inactive', context === 'reference' && !item.active);
- this.classList.toggle('is-pending', !!item.isPending);
- this.classList.toggle('type-image', item.type === 'image');
- this.classList.toggle('type-video', item.type === 'video');
- if (item.ratio) {
- const ratioValue = String(item.ratio);
- if (this.style.getPropertyValue('--ratio') !== ratioValue) this.style.setProperty('--ratio', ratioValue);
- const vertical = item.ratio <= 1.05 ? 'true' : 'false';
- if (this.dataset.vertical !== vertical) this.dataset.vertical = vertical;
- }
- const bg = item.lqip || '';
- if (this.bgBase.style.background !== bg) this.bgBase.style.background = bg;
- const playDisplay = item.type === 'video' && context !== 'reference' && !item.isPending ? 'flex' : 'none';
- if (this.playIcon.style.display !== playDisplay) this.playIcon.style.display = playDisplay;
- if (item.isPending) this.setStatus(item._statusData || [{ text: 'Queued...', color: '#fff' }]);
- else if (context === 'reference') {
- this.setReferenceActive(item.active);
- this.setReferencePromptFlag(!!STATE.refPrompts[item.id]);
- } else if (context === 'history') this.setHistoryPrompt(item.prompt);
- }
- connectedCallback() { if (this._bound) ViewportObserver.observe(this); }
- disconnectedCallback() {
- ViewportObserver.unobserve(this);
- queueMicrotask(() => { if (!this.isConnected) this.unbind(); });
- }
- forcePause() { if (this._item?.type === 'video') { this.playIcon.style.opacity = '1'; this.vidEl.pause(); this.classList.remove('is-playing'); } }
- async _handleHover(isHover) {
- if (!this._bound || this._item?.type !== 'video' || this._context !== 'current') return;
- const eco = STATE.ecoMode || document.body.classList.contains('auto-eco');
- if (eco) {
- this.forcePause();
- return;
- }
- if (isHover && this._isActuallyVisible) {
- if (this._mediaState !== 'full') { this._hoverUpgraded = true; await this.setLoadLevel('full'); }
- if (!this.vidEl || !this.vidEl.getAttribute('src')) return;
- this.playIcon.style.opacity = '0'; this.classList.add('is-playing');
- try { await this.vidEl.play(); } catch(e) { this.classList.remove('is-playing'); this.playIcon.style.opacity = '1'; }
- } else {
- this.forcePause();
- if (this._hoverUpgraded) { this._hoverUpgraded = false; this.setLoadLevel('thumb'); }
- }
- }
- _abortLoad() { if (this._loadController) { this._loadController.abort(); this._loadController = null; } }
- async setLoadLevel(level) {
- if (this._mediaState === level || !this._bound) return;
- if (this._item.isPending && !this._item.refSourceId) return;
- if (level === 'full' && (this._item.isPending || (this._item.type === 'image' && (this._context === 'history' || this._context === 'reference' || STATE.ecoMode)))) { level = 'thumb'; if (this._mediaState === level) return; }
- this._abortLoad(); this._loadController = new AbortController(); const signal = this._loadController.signal;
- if (level === 'none') return this._clearSrc();
- try {
- const { thumbUrl, fullUrl } = await this._resolveUrls(level, signal);
- if (signal.aborted) return;
- this._applySrc(level, thumbUrl, fullUrl);
- } catch(e) { if (e.name !== 'AbortError') addLog('WARN', 'setLoadLevel error', e); }
- }
- async _resolveUrls(level, signal) {
- const fetchId = this._item.refSourceId || this._item.id;
- const needsThumb = this._item.type === 'video' || level !== 'full' || this._context !== 'current' || isMemoryConstrainedMode();
- const thumbPromise = needsThumb ? BlobCache.thumb.getUrl(fetchId, 'thumb') : Promise.resolve(null);
- const fullPromise = level === 'full'
- ? (this._item.type === 'video' && this._item.originalUrl ? Promise.resolve(this._item.originalUrl) : BlobCache.full.getUrl(fetchId, 'full'))
- : Promise.resolve(null);
- let [thumbUrl, fullUrl] = await Promise.all([thumbPromise, fullPromise]);
- if (!thumbUrl && !fullUrl && level === 'full' && this._item.type === 'image') thumbUrl = await BlobCache.thumb.getUrl(fetchId, 'thumb');
- if (signal.aborted) return {};
- return { thumbUrl, fullUrl };
- }
- _applySrc(level, thumbUrl, fullUrl) {
- if (!this._bound) return;
- this._mediaState = level;
- if (this._item.type === 'image') {
- this.vidEl.style.display = 'none'; this.imgEl.style.display = 'block';
- const targetUrl = fullUrl || thumbUrl;
- if (targetUrl && this.imgEl.src !== targetUrl) {
- this.imgEl.src = targetUrl;
- this._decodeVisibleImage();
- } else if (!targetUrl) {
- this.imgEl.removeAttribute('src');
- this.classList.remove('is-loaded');
- }
- } else {
- if (thumbUrl && this.imgEl.src !== thumbUrl) {
- this.imgEl.src = thumbUrl; this.imgEl.style.display = 'block';
- this._decodeVisibleImage();
- } else if (!thumbUrl) this.imgEl.style.display = 'none';
- this.vidEl.style.display = 'block'; this.vidEl.removeAttribute('crossorigin');
- if (level === 'full' && fullUrl) {
- if (this.vidEl.src !== fullUrl) {
- this.vidEl.src = fullUrl;
- const onLoaded = () => { if (this._bound) { this.classList.add('is-loaded'); if (this.vidEl.paused && this.vidEl.currentTime === 0) this.vidEl.currentTime = 0.1; } };
- if (this.vidEl.readyState >= 2) onLoaded(); else this.vidEl.addEventListener('loadeddata', onLoaded, { once: true });
- }
- } else { this.vidEl.removeAttribute('src'); this.vidEl.load(); }
- }
- }
- _clearSrc() {
- this.forcePause();
- this.imgEl.removeAttribute('src'); this.imgEl.style.display = 'none';
- this.vidEl.removeAttribute('src'); this.vidEl.load(); this.vidEl.style.display = 'none';
- this.classList.remove('is-loaded', 'is-playing');
- this._mediaState = 'none';
- }
- _decodeVisibleImage() {
- this.imgEl.decode().then(() => { if (this._bound) this.classList.add('is-loaded'); }).catch(() => {});
- }
- bind(item, context) {
- const prevContext = this._context, prevId = this.dataset.id;
- this.unbind(); this._item = item; this._context = context; this.dataset.id = item.id;
- this.className = '';
- this.classList.add('xai-card', `is-${context}`, `type-${item.type}`);
- this.draggable =['history', 'current', 'reference'].includes(context);
- CardRegistry.move(this, prevContext, prevId, context, item.id);
- this.vidEl.style.display = 'none'; this.imgEl.style.display = 'none';
- this.classList.remove('is-loaded', 'is-playing', 'is-processing');
- this._statusSig = '';
- this._progressValue = null;
- this._bound = true;
- this.refresh(item, context);
- ViewportObserver.observe(this);
- }
- unbind() {
- CardRegistry.unregister(this, this._context, this.dataset.id);
- clearTimeout(this._hoverTimer); this._hoverTimer = null;
- clearTimeout(this._warmTimer); this._warmTimer = null;
- this._abortLoad();
- ViewportObserver.unobserve(this); this.setLoadLevel('none'); this.setStatus([]); this.setProgress(null); this.removeAttribute('data-id'); this.classList.remove('is-pending', 'is-processing', 'inactive', 'downloaded'); this._item = null; this._bound = false;
- }
- }
- if (!customElements.get('xai-card')) customElements.define('xai-card', XaiCard);
- const NativeGridManager = {
- container: null, _renderVersions: new Map(),
- init() {
- this.container = DOM.currentImages;
- if (this.container) this.container.dataset.renderContext = 'current';
- if (DOM.refList) DOM.refList.dataset.renderContext = 'reference';
- if (DOM.history) DOM.history.dataset.renderContext = 'history';
- },
- syncCurrentLayoutMetrics(container = this.container, order = STATE.currentOrder, map = STATE.items) {
- if (!container) return;
- if (!order || order.length === 0) {
- container.style.removeProperty('--gallery-target-h');
- return;
- }
- let W = container.clientWidth || window.innerWidth * 0.5, H = (container.clientHeight || window.innerHeight) - 130, W_u = W * 0.98, cAR = 0, tH = H;
- for (let i = 0; i < order.length; i++) {
- let ar = map.get(order[i])?.ratio || 1;
- cAR += ar;
- let pW = cAR * H;
- if (pW > W_u) {
- let perfH = W_u / cAR, prevH = (cAR - ar) > 0 ? (W_u / (cAR - ar)) : H * 2;
- tH = prevH > H ? perfH : prevH;
- break;
- }
- }
- if (tH < 200) tH = 200;
- container.style.setProperty('--gallery-target-h', `${tH}px`);
- },
- update(order, itemDataMap = STATE.items, context = 'current', containerOverride = null) {
- const container = containerOverride || (context === 'history' ? DOM.history : context === 'reference' ? DOM.refList : this.container); if (!container) return;
- const version = Symbol(); this._renderVersions.set(container, version);
- this._processQueue(context, order, itemDataMap, container, version);
- },
- async _processQueue(context, order, map, container, version) {
- if (!order || order.length === 0) return this.clear(container);
- if (context === 'current') this.syncCurrentLayoutMetrics(container, order, map);
- const existingNodes = Array.from(container.children), nodeMap = new Map(existingNodes.map(n =>[n.dataset.id, n])), orderSet = new Set(order);
- existingNodes.forEach(n => { if (!orderSet.has(n.dataset.id)) { n.unbind(); n.remove(); } });
- let t = performance.now(), fragment = document.createDocumentFragment();
- for (let i = 0; i < order.length; i++) {
- if (this._renderVersions.get(container) !== version) return;
- if (performance.now() - t > 12) {
- if (fragment.children.length > 0) container.insertBefore(fragment, container.children[i] || null);
- await yieldToMain();
- if (this._renderVersions.get(container) !== version) return;
- t = performance.now();
- }
- const id = order[i], meta = map.get(id); if (!meta) continue;
- let node = nodeMap.get(id);
- if (!node) { node = document.createElement('xai-card'); node.bind(meta, context); fragment.appendChild(node); }
- else {
- node.refresh(meta, context);
- if (fragment.children.length > 0) container.insertBefore(fragment, container.children[i] || null);
- if (container.children[i] !== node) container.insertBefore(node, container.children[i] || null);
- }
- }
- if (fragment.children.length > 0) container.appendChild(fragment);
- },
- clear(container = this.container, contextOverride = null) {
- if (!container) return;
- const context = contextOverride || container.dataset.renderContext || (container === DOM.history ? 'history' : container === DOM.refList ? 'reference' : 'current');
- Array.from(container.children).forEach(n => n.unbind());
- container.replaceChildren();
- if (container === DOM.history || container === DOM.refList || container === this.container) container.scrollTop = 0;
- CardRegistry.clear(context);
- if (container === this.container) container.style.removeProperty('--gallery-target-h');
- }
- };
- const HistoryManager = {
- container: null,
- topSpacer: null,
- bottomSpacer: null,
- windowEl: null,
- itemMap: STATE.items,
- order: [],
- measurements: new Map(),
- renderRaf: 0,
- measureRaf: 0,
- resizeObserver: null,
- init(container) {
- this.container = container;
- this.order = [];
- this.measurements.clear();
- this.topSpacer = document.createElement('div');
- this.bottomSpacer = document.createElement('div');
- this.windowEl = document.createElement('div');
- this.topSpacer.className = 'history-spacer';
- this.bottomSpacer.className = 'history-spacer';
- this.windowEl.className = 'history-window';
- this.windowEl.dataset.renderContext = 'history';
- this.container.classList.add('history-virtualized');
- this.container.replaceChildren(this.topSpacer, this.windowEl, this.bottomSpacer);
- App.on(this.container, 'scroll', () => this.scheduleRender(), { passive: true });
- this.resizeObserver = new ResizeObserver(() => {
- this.measurements.clear();
- this.scheduleRender();
- });
- this.resizeObserver.observe(this.container);
- App.ac.signal.addEventListener('abort', () => this.clear());
- },
- _getContentWidth() {
- return Math.max(120, (this.container?.clientWidth || 0) - 8);
- },
- _estimateHeight(id) {
- const key = String(id);
- if (this.measurements.has(key)) return this.measurements.get(key);
- const item = this.itemMap?.get(key);
- const ratio = Math.max(0.2, Number(item?.ratio) || 1);
- return Math.ceil(this._getContentWidth() / ratio) + 6;
- },
- _measureVisible() {
- if (!this.windowEl) return;
- let changed = false;
- Array.from(this.windowEl.children).forEach(node => {
- if (node.tagName !== 'XAI-CARD') return;
- const id = String(node.dataset.id);
- const nextHeight = Math.ceil(node.getBoundingClientRect().height + 6);
- if (!Number.isFinite(nextHeight) || nextHeight <= 0) return;
- if (Math.abs((this.measurements.get(id) || 0) - nextHeight) >= HISTORY_MEASURE_EPSILON) {
- this.measurements.set(id, nextHeight);
- changed = true;
- }
- });
- if (changed) this.scheduleRender();
- },
- _computeWindow() {
- const viewportHeight = Math.max(1, this.container?.clientHeight || 0);
- const scrollTop = Math.max(0, this.container?.scrollTop || 0);
- const overscan = getHistoryOverscanPx();
- const targetStart = Math.max(0, scrollTop - overscan);
- const targetEnd = scrollTop + viewportHeight + overscan;
- let top = 0, bottom = 0, start = 0, end = this.order.length, offset = 0;
- let foundStart = false;
- for (let i = 0; i < this.order.length; i++) {
- const id = this.order[i];
- const itemHeight = this._estimateHeight(id);
- const nextOffset = offset + itemHeight;
- if (!foundStart && nextOffset >= targetStart) {
- start = Math.max(0, i - getHistoryPrerenderBudget());
- foundStart = true;
- }
- if (foundStart && nextOffset >= targetEnd) {
- end = Math.min(this.order.length, i + 1 + getHistoryPrerenderBudget());
- break;
- }
- offset = nextOffset;
- }
- for (let i = 0; i < start; i++) top += this._estimateHeight(this.order[i]);
- for (let i = end; i < this.order.length; i++) bottom += this._estimateHeight(this.order[i]);
- return { start, end, top, bottom };
- },
- render() {
- if (!this.container || !this.windowEl) return;
- if (!this.order.length) {
- if (this.topSpacer) this.topSpacer.style.height = '0px';
- if (this.bottomSpacer) this.bottomSpacer.style.height = '0px';
- NativeGridManager.clear(this.windowEl, 'history');
- return;
- }
- const { start, end, top, bottom } = this._computeWindow();
- if (this.topSpacer) this.topSpacer.style.height = `${top}px`;
- if (this.bottomSpacer) this.bottomSpacer.style.height = `${bottom}px`;
- NativeGridManager.update(this.order.slice(start, end), this.itemMap, 'history', this.windowEl);
- if (this.measureRaf) cancelAnimationFrame(this.measureRaf);
- this.measureRaf = requestAnimationFrame(() => {
- this.measureRaf = 0;
- this._measureVisible();
- });
- },
- scheduleRender() {
- if (this.renderRaf) cancelAnimationFrame(this.renderRaf);
- this.renderRaf = requestAnimationFrame(() => {
- this.renderRaf = 0;
- this.render();
- });
- },
- refreshMemoryPressure() {
- this.scheduleRender();
- },
- update(order, itemMap) {
- this.order = Array.isArray(order) ? order.map(String) : [];
- this.itemMap = itemMap || STATE.items;
- this.scheduleRender();
- },
- clear() {
- if (this.renderRaf) cancelAnimationFrame(this.renderRaf);
- if (this.measureRaf) cancelAnimationFrame(this.measureRaf);
- this.renderRaf = 0;
- this.measureRaf = 0;
- this.resizeObserver?.disconnect();
- this.resizeObserver = null;
- this.measurements.clear();
- if (this.windowEl) NativeGridManager.clear(this.windowEl, 'history');
- if (this.container) {
- this.container.replaceChildren();
- this.container.classList.remove('history-virtualized');
- }
- this.topSpacer = null;
- this.bottomSpacer = null;
- this.windowEl = null;
- this.container = null;
- this.order = [];
- }
- };
- const App = {
- ac: new AbortController(), perfObserver: null, recentLongTaskMs: 0, smoothedLoad: 0,
- on(target, event, handler, opts = false) { target.addEventListener(event, handler, { ...(typeof opts === 'boolean' ? { capture: opts } : opts), signal: this.ac.signal }); },
- async mount() {
- if (STATE.isMounted) return; resetState(); STATE.isMounted = true; this.ac = new AbortController();
- _statusViewCacheKey = '';
- document.documentElement.style.overflow = document.body.style.overflow = 'hidden'; document.body.classList.toggle('eco-mode', STATE.ecoMode);
- this.ac.signal.addEventListener('abort', () => document.documentElement.style.overflow = document.body.style.overflow = '');
- this.on(window, 'dragover', e => e.preventDefault()); this.on(window, 'drop', e => e.preventDefault());
- this.on(window, 'focus', () => TitleState.handleTabActive());
- this.on(document, 'visibilitychange', () => document.hidden ? TitleState.sync() : TitleState.handleTabActive());
- if ('PerformanceObserver' in window) { this.perfObserver = new PerformanceObserver((list) => { for (const e of list.getEntries()) this.recentLongTaskMs += e.duration; }); this.perfObserver.observe({ entryTypes:['longtask'] }); this.ac.signal.addEventListener('abort', () => this.perfObserver.disconnect()); }
- const metInterval = setInterval(() => runBg(updateHistoryMetrics), 5000); this.ac.signal.addEventListener('abort', () => clearInterval(metInterval));
- const sheet = new CSSStyleSheet(); sheet.replaceSync(UI_CSS); document.adoptedStyleSheets =[...document.adoptedStyleSheets, sheet];
- const appDiv = document.createElement('div'); appDiv.id = 'xai-pro-app-wrapper'; appDiv.append(createMarkupFragment(UI_HTML)); document.body.appendChild(appDiv);
- this.ac.signal.addEventListener('abort', () => { document.adoptedStyleSheets = document.adoptedStyleSheets.filter(s => s !== sheet); appDiv.remove(); });
- await StorageDB.init(); await loadDataFromDB(); bindLogic(); await loadHistoryFromDB();
- },
- unmount() { if (!STATE.isMounted) return; JobManager.tasks.forEach(t => t.controller.abort()); this.ac.abort(); PromptSync.clear(); DBQueue.clearPending(); MediaWarmPool.releaseAll(); STATE.items.forEach((_, id) => Store.removeItem(id)); NativeGridManager.clear(); DOM.historyScroller?.clear(); CardRegistry.clear(); RefPayloadCache.clear(); _statusViewCacheKey = ''; setDocumentTitle(originalTitle); resetState(); }
- };
- const checkRoute = (path) => { const p = typeof path === 'string' ? path : location.pathname; const ok = p.startsWith('/playground/imagine') || p.match(/^\/team\/[^\/]+\/imagine/); ok && !STATE.isMounted ? App.mount() : !ok && STATE.isMounted ? App.unmount() : null; };
- navigation.addEventListener('navigate', (e) => { if (e.downloadRequest !== undefined) return; const url = new URL(e.destination.url); if (['blob:', 'data:', 'javascript:'].includes(url.protocol)) return; if (url.pathname !== location.pathname) checkRoute(url.pathname); });
- const UI = {
- showToast(msg, type = 'info') {
- const c = document.getElementById('xai-toast-container'); if(!c) return;
- const icon = type === 'error' ? '❌' : type === 'warn' ? '⚠️' : type === 'success' ? '✅' : 'ℹ️';
- const toast = document.createElement('div'); toast.className = `xai-toast ${type}`;
- const iconEl = document.createElement('span'); iconEl.textContent = icon;
- const msgEl = document.createElement('span'); msgEl.textContent = msg;
- toast.append(iconEl, msgEl);
- c.appendChild(toast); requestAnimationFrame(() => toast.classList.add('show')); setTimeout(() => { toast.classList.remove('show'); setTimeout(() => toast.remove(), 400); }, 4000);
- },
- confirm(msg) { return new Promise(resolve => { let dialog = DOM.confirmDialog; DOM.confirmMsg.replaceChildren(createMarkupFragment(msg)); const handler = () => { dialog.removeEventListener('close', handler); resolve(dialog.returnValue); }; dialog.addEventListener('close', handler); dialog.returnValue = ''; dialog.showModal(); }); }
- };
- async function updateHistoryMetrics() {
- let disk = '--'; try { disk = ((await navigator.storage.estimate()).usage / 1048576).toFixed(0) + 'M'; } catch(e){}
- App.smoothedLoad = (App.smoothedLoad * 0.4) + (Math.max(Math.min(App.recentLongTaskMs / 1500, 1), performance.memory ? Math.max(0, Math.min((performance.memory.usedJSHeapSize / (performance.memory.jsHeapSizeLimit || 1) - 0.6) / 0.3, 1)) : 0) * 0.6);
- App.recentLongTaskMs = 0; const loadPercent = Math.round(App.smoothedLoad * 100), isAutoEco = isAutoEcoEnabled();
- if (!isAutoEco && loadPercent > 80) {
- document.body.classList.add('auto-eco');
- applyMemoryPressureProfile('auto-eco-on');
- } else if (isAutoEco && loadPercent < 45) {
- document.body.classList.remove('auto-eco');
- applyMemoryPressureProfile('auto-eco-off');
- }
- if (DOM.metricSaved) DOM.metricSaved.innerText = STATE.historyOrder.length; if (DOM.metricDisk) DOM.metricDisk.innerText = '⛁\uFE0E ' + disk;
- if (DOM.metricLoad) { DOM.metricLoad.innerText = loadPercent + '%'; DOM.metricLoad.classList.remove('d-none'); }
- }
- function getEligibleRefs() {
- const acceptedType = ActionUtils.getRefType(), maxRefs = ActionUtils.getMaxRefs();
- const eligible = [];
- for (const id of STATE.refOrder) {
- const item = STATE.items.get(id);
- if (item && item.active && item.type === acceptedType && eligible.length < maxRefs) eligible.push(item);
- }
- return { eligible, acceptedType, maxRefs };
- }
- function updateUploadPlaceholderState() {
- if (!DOM.uploadPlaceholder) return; const count = STATE.refOrder.length, max = APP_CONFIG.LIMITS.MAX_REFS_QUEUE;
- DOM.uploadPlaceholder.style.opacity = count >= max ? '0.4' : '1'; DOM.uploadPlaceholder.style.pointerEvents = count >= max ? 'none' : 'auto';
- DOM.uploadPlaceholder.querySelector('.upload-label-main').innerText = count >= max ? 'Limit' : 'Add Media'; if (DOM.refCounter) DOM.refCounter.innerText = count;
- }
- function updateActionUI() { runTransition(_updateActionUIDom); }
- function _updateActionUIDom() {
- const action = STATE.action; DOM.app.dataset.activeAction = action;
- const dOpts = ActionUtils.getDurationOptions(action);
- if (!dOpts.some(o => o.val === STATE.duration)) STATE.duration = dOpts[0].val;
- if (DOM.labelSettingDuration) DOM.labelSettingDuration.innerText = dOpts.find(o => o.val === STATE.duration).text; DOM.settingDuration.dataset.value = STATE.duration; DOM.popoverDuration?._updateOptions?.(dOpts);
- if (DOM.labelSettingBatch) DOM.labelSettingBatch.innerText = STATE.batchSize; if (DOM.settingBatch) DOM.settingBatch.dataset.value = STATE.batchSize;
- const isVideo = ActionUtils.isVideoGeneration(action);
- DOM.popoverAr?._updateOptions?.(APP_CONFIG.ASPECT_RATIOS.map(ar => ({ val: ar.label, text: ar.label, icon: UIUtils.createArIcon(ar.w, ar.h, ar.isAuto, ar.isRef), disabled: isVideo && !ar.isAuto && (ar.imageOnly || !ALLOWED_VIDEO_ARS.has(ar.label.trim())) })));
- if (isVideo && !getAspectRatioOption().isAuto && !ALLOWED_VIDEO_ARS.has(STATE.currentAr)) STATE.currentAr = 'Auto';
- syncAspectRatioUI();
- updateStatusUI(); updateUploadPlaceholderState();
- }
- const renderRefMode = () => { if (DOM.refModeToggle) DOM.refModeToggle.innerText = STATE.refMode === 'multi' ? 'Multi' : 'Queue'; updateActionUI(); };
- const UI_CSS = `
- :root {
- --bg:#2b2d31;
- --bg-center:#1a1b1e;
- --panel:#313338;
- --text-main:#f3f3f3;
- --text-muted:#9e9e9e;
- --danger:#ef4444;
- --warning:#eab308;
- --success:#10b981;
- --accent:#ffffff;
- --border:rgb(from var(--accent) r g b / 8%);
- --border-hover:rgb(from var(--accent) r g b / 20%);
- --bg-hover:rgb(from var(--accent) r g b / 15%);
- --bg-active:rgb(from var(--accent) r g b / 25%);
- --z-app:9999999;
- --z-local-header:20;
- --z-prompt:100;
- }
- #xai-pro-app {
- position:fixed;
- inset:0;
- z-index:var(--z-app);
- background:var(--bg);
- color:var(--text-main);
- font-family:ui-sans-serif,system-ui,sans-serif;
- display:flex;
- gap:4px;
- padding:4px;
- box-sizing:border-box;
- overflow:hidden;
- }
- #xai-pro-app * {
- box-sizing:border-box;
- scrollbar-color:var(--bg-hover) transparent;
- }
- .col-left,
- .col-right {
- overflow:hidden;
- display:flex;
- flex-direction:column;
- gap:4px;
- flex-shrink:0;
- }
- .col-left {
- width:clamp(24px,var(--left-panel-width,320px),50vw);
- min-width:200px;
- max-width:50vw;
- resize:horizontal;
- }
- .col-right {
- width:clamp(24px,var(--right-panel-width,320px),50vw);
- min-width:240px;
- max-width:50vw;
- direction:rtl;
- resize:horizontal;
- }
- .col-right-inner {
- direction:ltr;
- display:flex;
- flex-direction:column;
- gap:4px;
- height:100%;
- width:100%;
- flex:1;
- }
- .col-left::-webkit-scrollbar,
- .col-right::-webkit-scrollbar {
- display:none;
- }
- ::-webkit-resizer {
- background:transparent;
- border-width:8px;
- border-style:solid;
- border-color:transparent rgb(from var(--accent) r g b/25%) rgb(from var(--accent) r g b/25%) transparent;
- }
- .col-right::-webkit-resizer {
- border-color:transparent transparent rgb(from var(--accent) r g b/25%) rgb(from var(--accent) r g b/25%);
- }
- .col-center {
- flex:1;
- display:flex;
- flex-direction:column;
- min-width:300px;
- position:relative;
- overflow:hidden;
- background:var(--bg-center);
- border-radius:12px;
- box-shadow:inset 0 0 0 1px var(--border);
- }
- #xai-current-images {
- position:absolute;
- inset:0;
- z-index:1;
- container-type:size;
- overflow-y:auto;
- overflow-x:hidden;
- display:flex;
- flex-wrap:wrap;
- align-content:flex-start;
- justify-content:center;
- gap:0;
- padding:0 0 300px 0;
- scroll-behavior:smooth;
- overscroll-behavior:contain;
- -webkit-overflow-scrolling:touch;
- }
- .prompt-panel-wrapper {
- position:absolute;
- bottom:0;
- left:0;
- right:0;
- width:100%;
- max-width:none;
- padding:0 16px 16px;
- z-index:var(--z-prompt);
- display:flex;
- flex-direction:column;
- pointer-events:none;
- }
- .panel {
- background:var(--panel);
- border:1px solid var(--border);
- border-radius:12px;
- display:flex;
- flex-direction:column;
- overflow:hidden;
- height:100%;
- }
- .panel-header {
- padding:4px 6px;
- display:flex;
- align-items:center;
- border-bottom:1px solid var(--border);
- height:42px;
- flex-shrink:0;
- gap:6px;
- justify-content:space-between;
- }
- #xai-history,
- #xai-ref-list {
- position:relative;
- overflow-y:auto;
- overflow-x:hidden;
- padding:4px;
- flex:1;
- min-height:0;
- container-type:size;
- display:flex;
- flex-direction:column;
- gap:4px;
- }
- #xai-history.history-virtualized {
- display:block;
- }
- .history-spacer,
- .history-window {
- width:100%;
- }
- .history-spacer {
- flex:0 0 auto;
- pointer-events:none;
- }
- .history-window {
- display:block;
- }
- .history-window > xai-card.is-history:last-child {
- margin-bottom:0;
- }
- #xai-ref-list {
- padding-right:2px;
- }
- .flex-center { display:flex; align-items:center; justify-content:center; }
- .d-none { display:none !important; }
- .d-flex { display:flex !important; }
- .text-muted { color:var(--text-muted); }
- .text-success { color:var(--success); }
- .text-center { text-align:center; }
- .w-100 { width:100%; }
- .justify-end { justify-content:flex-end; }
- .gap-2 { gap:8px; }
- #xai-ref-list:has(> xai-card:nth-child(2)) xai-card.is-reference { max-height:calc(100cqh / 2 - 4px); }
- #xai-ref-list:has(> xai-card:nth-child(3)) xai-card.is-reference { max-height:calc(100cqh / 3 - 4px); }
- #xai-ref-list:has(> xai-card:nth-child(4)) xai-card.is-reference { max-height:calc(100cqh / 4 - 4px); }
- #xai-ref-list:has(> xai-card:nth-child(5)) xai-card.is-reference { max-height:calc(100cqh / 5 - 4px); }
- .xai-btn {
- display:inline-flex;
- align-items:center;
- justify-content:center;
- gap:6px;
- height:36px;
- padding:0 16px;
- white-space:nowrap;
- flex-shrink:0;
- border-radius:20px;
- border:none;
- outline:none;
- text-decoration:none;
- font-family:inherit;
- font-size:13px;
- font-weight:600;
- cursor:pointer;
- background:rgb(from var(--accent) r g b/12%);
- color:var(--text-main);
- transition:.15s ease;
- box-shadow:inset 0 0 0 1px transparent;
- }
- .xai-btn:hover:not(:disabled) { background:rgb(from var(--accent) r g b/20%); box-shadow:inset 0 0 0 1px rgb(from var(--accent) r g b/25%); }
- .xai-btn:active { scale:.96; }
- .xai-btn--square { width:32px; height:32px; padding:0; border-radius:8px; font-size:16px; }
- .xai-btn--icon-sm { width:28px; height:28px; padding:0; border-radius:6px; font-size:14px; }
- .xai-btn--danger { background:rgb(from var(--danger) r g b/15%); color:var(--danger); box-shadow:inset 0 0 0 1px rgb(from var(--danger) r g b/20%); }
- .xai-btn--danger:hover:not(:disabled) { background:var(--danger); color:#fff; box-shadow:inset 0 0 0 1px var(--danger); }
- .xai-btn--warning { background:rgb(from var(--warning) r g b/15%); color:var(--warning); box-shadow:inset 0 0 0 1px rgb(from var(--warning) r g b/20%); }
- .xai-btn--warning:hover:not(:disabled) { background:var(--warning); color:#fff; box-shadow:inset 0 0 0 1px var(--warning); }
- .xai-btn--orange { background:rgb(249 115 22 / 15%); color:#f97316; box-shadow:inset 0 0 0 1px rgb(249 115 22 / 20%); }
- .xai-btn--orange:hover:not(:disabled) { background:#f97316; color:#fff; box-shadow:inset 0 0 0 1px #f97316; }
- .xai-btn--success { background:var(--success); color:#fff; box-shadow:0 2px 10px rgb(0 0 0/20%); }
- .xai-btn--run { background:#ffffff; color:#0f1419; box-shadow:none; }
- .xai-btn--run:hover:not(:disabled) { background:#d7dbdc; box-shadow:none; }
- .xai-btn--stop { background:var(--danger); color:#fff; }
- .xai-btn--circle-lg {
- width:42px;
- height:42px;
- padding:0;
- border-radius:50%;
- font-size:22px;
- margin:0 -4px -6px 0;
- box-shadow:0 4px 12px rgb(0 0 0/20%);
- }
- .xai-btn--circle-lg:hover:not(:disabled) { scale:1.05; box-shadow:0 6px 16px rgb(0 0 0/30%); }
- .xai-btn--circle-lg.is-running { animation:xai-pulse-stop 2s infinite; }
- @keyframes xai-pulse-stop {
- 0% { box-shadow:0 0 0 0 rgb(from var(--danger) r g b/40%); }
- 70% { box-shadow:0 0 0 8px rgb(from var(--danger) r g b/0%); }
- 100% { box-shadow:0 0 0 0 rgb(from var(--danger) r g b/0%); }
- }
- .metrics-container {
- display:flex;
- gap:6px;
- align-items:center;
- overflow:hidden;
- flex:1;
- }
- .metric-pill {
- display:inline-flex;
- justify-content:center;
- align-items:center;
- background:rgb(0 0 0/45%);
- height:36px;
- box-sizing:border-box;
- border-radius:20px;
- font-family:monospace;
- font-size:13px;
- font-variant-numeric:tabular-nums;
- box-shadow:inset 0 1px 4px rgb(0 0 0/50%),0 1px 0 rgb(255 255 255/4%);
- color:#a1a1aa;
- border:1px solid transparent;
- white-space:nowrap;
- flex-shrink:1;
- overflow:hidden;
- text-overflow:ellipsis;
- transition:color .3s;
- }
- #xai-ref-counter { width:40px; min-width:40px; padding:0; justify-content:center; }
- #metric-saved { width:50px; min-width:50px; padding:0; justify-content:center; }
- #metric-load { width:48px; min-width:48px; padding:0; justify-content:center; }
- #metric-disk { width:56px; min-width:56px; padding:0; justify-content:center; }
- .prompt-container {
- pointer-events:auto;
- max-width:1600px;
- width:100%;
- margin:0 auto;
- padding:12px 16px;
- display:flex;
- flex-direction:column;
- border-radius:24px;
- position:relative;
- background:rgb(28 28 28 / 75%);
- border:1px solid rgb(255 255 255/8%);
- box-shadow:0 4px 32px rgb(0 0 0/40%);
- transition:background .4s ease .5s,border-color .4s ease .5s;
- }
- .prompt-container:hover,
- .prompt-container:focus-within {
- background:rgba(43,45,49,1);
- border-color:rgb(255 255 255/15%);
- transition:background .15s ease 0s,border-color .15s ease 0s;
- }
- .prompt-container.drag-over {
- background:color-mix(in srgb,var(--success) 40%,rgba(43,45,49,1));
- border-color:var(--success);
- }
- .prompt-container.editing-ref {
- background:rgb(17 40 28) !important;
- border-color:var(--success) !important;
- box-shadow:none !important;
- transition:none !important;
- }
- .textarea-wrapper {
- margin-bottom:8px;
- display:flex;
- flex-direction:column;
- transition:mask-image .2s,-webkit-mask-image .2s;
- }
- .textarea-wrapper.can-scroll {
- -webkit-mask-image:linear-gradient(to bottom, #000 calc(100% - 20px), transparent);
- mask-image:linear-gradient(to bottom, #000 calc(100% - 20px), transparent);
- }
- #xai-api-prompt {
- width:100%;
- background:transparent;
- border:none;
- color:var(--text-main);
- font-size:16px;
- outline:none;
- resize:none;
- overflow-y:auto;
- line-height:1.5;
- field-sizing:content;
- min-height:50px;
- max-height:50px;
- padding:0 4px;
- scrollbar-width:thin;
- transition:max-height .4s cubic-bezier(0.4,0,0.2,1) .5s;
- }
- .prompt-container:hover #xai-api-prompt,
- .prompt-container:focus-within #xai-api-prompt { max-height:270px; transition:max-height .2s cubic-bezier(0.4,0,0.2,1) 0s; }
- .prompt-actions {
- display:flex;
- flex-wrap:wrap-reverse;
- min-height:36px;
- gap:12px;
- justify-content:space-between;
- align-items:center;
- }
- .action-row-left {
- display:flex;
- gap:8px;
- align-items:center;
- flex-wrap:wrap;
- flex:1 1 50%;
- min-width:260px;
- }
- .action-row-right {
- display:flex;
- gap:12px;
- align-items:center;
- flex:1 1 auto;
- justify-content:flex-end;
- min-width:220px;
- }
- #xai-upload-placeholder {
- flex:1;
- margin:0;
- gap:4px;
- font-weight:500;
- padding:0 8px;
- font-size:13px;
- }
- body.is-dragging #xai-upload-placeholder {
- background:rgb(from var(--success) r g b/10%);
- box-shadow:inset 0 0 0 1px rgb(from var(--success) r g b/50%);
- color:var(--success);
- }
- #xai-upload-placeholder.drag-over {
- background:rgb(from var(--success) r g b/20%) !important;
- box-shadow:inset 0 0 0 2px var(--success) !important;
- color:#fff;
- }
- #xai-api-res-img,
- #xai-api-res-vid,
- #btn-setting-batch,
- #btn-setting-duration,
- #btn-setting-ar,
- #btn-setting-loops { display:none; }
- [data-active-action="gen_image"] #xai-api-res-img,
- [data-active-action="gen_image"] #btn-setting-batch,
- [data-active-action="gen_image"] #btn-setting-ar,
- [data-active-action="gen_image"] #btn-setting-loops { display:inline-flex; }
- [data-active-action="gen_video"] #xai-api-res-vid,
- [data-active-action="gen_video"] #btn-setting-duration,
- [data-active-action="gen_video"] #btn-setting-ar,
- [data-active-action="gen_video"] #btn-setting-loops { display:inline-flex; }
- [data-active-action="extend_video"] #btn-setting-duration,
- [data-active-action="extend_video"] #btn-setting-loops { display:inline-flex; }
- [data-active-action="edit_video"] #btn-setting-loops { display:inline-flex; }
- :is(.xai-custom-dialog,.modal-box) {
- margin:auto;
- padding:0;
- outline:none;
- border:none;
- background:transparent;
- transition:display .12s allow-discrete,overlay .12s allow-discrete;
- align-items:center;
- justify-content:center;
- }
- :is(.xai-custom-dialog,.modal-box)::backdrop {
- background:rgba(0,0,0,.5);
- transition:display .12s allow-discrete,overlay .12s allow-discrete,background-color .12s;
- }
- :is(.xai-custom-dialog,.modal-box)[open] { display:flex; }
- .dialog-inner {
- background:var(--panel);
- border:1px solid var(--border);
- border-radius:20px;
- color:var(--text-main);
- padding:16px;
- contain:layout paint style;
- transition:opacity .12s;
- opacity:0;
- }
- :is(.xai-custom-dialog,.modal-box)[open] .dialog-inner { opacity:1; }
- @starting-style {
- :is(.xai-custom-dialog,.modal-box)[open] .dialog-inner { opacity:0; }
- :is(.xai-custom-dialog,.modal-box)[open]::backdrop { background-color:rgba(0,0,0,0); }
- }
- .xai-lightbox-dialog {
- margin:auto;
- outline:none;
- border:none;
- padding:0;
- background:transparent;
- max-width:100vw;
- max-height:100dvh;
- width:100vw;
- height:100dvh;
- overflow:hidden;
- }
- .xai-lightbox-dialog::backdrop { background:rgba(0,0,0,.82); backdrop-filter:blur(8px); }
- .xai-lightbox-dialog[open] {
- display:flex;
- align-items:center;
- justify-content:center;
- }
- .lightbox-frame {
- position:relative;
- display:grid;
- place-items:center;
- width:100%;
- height:100%;
- padding:24px;
- max-width:100%;
- max-height:100%;
- contain:layout paint style;
- isolation:isolate;
- }
- .lightbox-stage {
- position:relative;
- display:grid;
- place-items:center;
- width:min(calc(100vw - 48px), calc((100dvh - 48px) * var(--lightbox-ratio, 16 / 9)));
- max-width:calc(100vw - 48px);
- max-height:calc(100dvh - 48px);
- aspect-ratio:var(--lightbox-ratio, 16 / 9);
- min-width:160px;
- min-height:160px;
- isolation:isolate;
- }
- .lightbox-frame::before {
- content:'';
- position:absolute;
- inset:24px;
- border-radius:28px;
- background:radial-gradient(circle at 50% 20%, rgb(255 255 255 / 10%), transparent 55%), linear-gradient(180deg, rgb(255 255 255 / 5%), rgb(255 255 255 / 0%));
- opacity:.9;
- pointer-events:none;
- z-index:0;
- }
- .lightbox-frame.is-loaded .lightbox-media--preview {
- opacity:0;
- filter:blur(14px);
- transform:scale(1.02);
- }
- .xai-custom-popover {
- position:fixed;
- inset:auto;
- margin:0;
- padding:6px;
- border:1px solid rgb(from var(--accent) r g b/15%);
- background:rgb(35 35 35/98%);
- border-radius:20px;
- max-height:90dvh;
- overflow-y:auto;
- scrollbar-gutter:stable;
- contain:layout paint style;
- color:var(--text-main);
- transition:display .1s allow-discrete,overlay .1s allow-discrete,opacity .1s;
- opacity:0;
- flex-direction:column;
- gap:2px;
- }
- .xai-custom-popover:popover-open {
- display:flex;
- opacity:1;
- }
- @starting-style {
- .xai-custom-popover:popover-open { opacity:0; }
- }
- .popover-item {
- padding:8px 14px;
- font-size:14px;
- font-weight:500;
- border-radius:12px;
- cursor:pointer;
- display:flex;
- align-items:center;
- gap:10px;
- transition:background-color .1s,color .1s,opacity .1s;
- }
- .popover-item:hover { background:var(--bg-hover); }
- .popover-item.active { background:var(--bg-active); color:#fff; }
- .popover-item.disabled { opacity:.4; pointer-events:none; }
- .ar-icon-shell { width:18px; height:18px; display:inline-flex; align-items:center; justify-content:center; flex-shrink:0; line-height:0; }
- .ar-icon-frame { border:1.5px solid currentColor; border-radius:2px; opacity:.72; }
- .ar-icon-letter { width:12px; height:12px; display:flex; align-items:center; justify-content:center; border:1px dashed currentColor; border-radius:2px; font-size:8px; line-height:1; font-weight:700; opacity:.82; }
- #btn-master-action { anchor-name:--a-act; }
- #popover-action { position-anchor:--a-act; bottom:anchor(top); left:anchor(left); margin-bottom:8px; min-width:max(130px,anchor-size(width)); position-try-fallbacks:flip-block; }
- #btn-setting-ar { anchor-name:--a-ar; gap:3px; padding:0 10px 0 8px; align-items:center; }
- #ar-selected-icon { display:flex; align-items:center; justify-content:center; width:18px; height:18px; flex:0 0 18px; }
- #ar-selected-text { display:inline-flex; align-items:center; line-height:1; }
- #popover-ar { position-anchor:--a-ar; bottom:anchor(top); left:anchor(left); margin-bottom:8px; min-width:max-content; position-try-fallbacks:flip-block; grid-template-columns:1fr 1fr; grid-auto-flow:row; gap:4px; padding:6px; justify-items:stretch; align-items:stretch; }
- #popover-ar:popover-open { display:grid; }
- #popover-ar .popover-item { padding:5px 8px; display:flex; align-items:center; justify-content:flex-start; gap:6px; min-width:85px; }
- #popover-ar .popover-item span { text-align:left; line-height:1; }
- #popover-ar .popover-item.no-icon { gap:0; }
- #popover-ar .popover-icon { display:flex; align-items:center; justify-content:center; width:18px; height:18px; margin:0; flex:0 0 18px; }
- #btn-setting-batch { anchor-name:--a-b; }
- #popover-batch { position-anchor:--a-b; bottom:anchor(top); left:anchor(left); margin-bottom:8px; position-try-fallbacks:flip-block; min-width:60px; padding:4px; max-height:450px; overflow-y:auto; }
- #popover-batch .popover-item { padding:6px 12px; justify-content:center; }
- #btn-setting-duration { anchor-name:--a-d; }
- #popover-duration { position-anchor:--a-d; bottom:anchor(top); left:anchor(left); margin-bottom:8px; min-width:max(130px,anchor-size(width)); position-try-fallbacks:flip-block; }
- #xai-settings-btn { anchor-name:--a-set; }
- #popover-settings { position-anchor:--a-set; bottom:anchor(top); left:anchor(left); margin-bottom:8px; position-try-fallbacks:flip-block; min-width:240px; }
- .ar-dropdown-icon { font-size:10px; opacity:.6; pointer-events:none; display:inline-flex; align-items:center; justify-content:center; line-height:1; }
- .fav-item {
- display:flex;
- align-items:center;
- justify-content:space-between;
- padding:6px 10px;
- border-radius:8px;
- gap:10px;
- cursor:pointer;
- }
- .fav-item:hover { background:var(--bg-hover); }
- .fav-item.pending {
- outline:1px dashed var(--success);
- outline-offset:-1px;
- background:rgb(from var(--success) r g b/10%);
- }
- .fav-text {
- flex:1;
- font-size:14px;
- overflow:hidden;
- text-overflow:ellipsis;
- white-space:nowrap;
- }
- #xai-toast-container {
- position:absolute;
- inset:auto 0 100% 0;
- display:flex;
- flex-direction:column;
- justify-content:flex-end;
- align-items:center;
- gap:8px;
- pointer-events:none;
- width:100%;
- z-index:var(--z-prompt);
- margin-bottom:12px;
- }
- .xai-toast {
- background:rgb(30 30 30/95%);
- border:1px solid var(--border);
- border-radius:12px;
- padding:8px 16px;
- color:var(--text-main);
- font-size:13px;
- font-weight:500;
- pointer-events:auto;
- display:flex;
- align-items:center;
- gap:8px;
- opacity:0;
- transition:opacity .18s ease;
- }
- .xai-toast.show { opacity:1; }
- .floating-edit-label {
- position:absolute;
- top:-36px;
- left:50%;
- translate:-50% 0;
- background:rgb(16 185 129);
- color:#fff;
- padding:6px 16px;
- border-radius:20px;
- font-size:14px;
- font-weight:700;
- pointer-events:none;
- opacity:0;
- transition:opacity .12s ease,top .12s ease;
- z-index:var(--z-prompt);
- border:1px solid rgb(from var(--accent) r g b/20%);
- }
- .floating-edit-label.show {
- opacity:1;
- top:-44px;
- }
- .btn-collapse {
- width:18px;
- min-width:18px;
- height:32px;
- padding:0;
- display:flex;
- align-items:center;
- justify-content:center;
- font-size:16px;
- color:var(--text-muted);
- background:transparent;
- border-radius:4px;
- transition:background-color .15s,color .15s;
- cursor:pointer;
- border:none;
- outline:none;
- }
- .btn-collapse:hover {
- background:var(--bg-hover);
- color:var(--text-main);
- }
- .col-left .btn-collapse { margin-right:-4px; }
- .col-right .btn-collapse { margin-left:-4px; }
- #btn-collapse-left::before { content:"${APP_CONFIG.ICONS.collapseLeft}"; }
- #btn-collapse-left[aria-expanded="false"]::before { content:"${APP_CONFIG.ICONS.collapseRight}"; }
- #btn-collapse-right::before { content:"${APP_CONFIG.ICONS.collapseRight}"; }
- #btn-collapse-right[aria-expanded="false"]::before { content:"${APP_CONFIG.ICONS.collapseLeft}"; }
- :is(.col-left,.col-right).is-collapsed {
- width:20px !important;
- min-width:20px !important;
- resize:none;
- }
- :is(.col-left,.col-right).is-collapsed :is(#history-metrics,#xai-history,#xai-upload-placeholder,#xai-ref-counter,#xai-ref-mode-toggle,#xai-ref-list,#xai-api-file) {
- display:none !important;
- }
- :is(.col-left,.col-right).is-collapsed .panel-header {
- padding:0;
- height:100%;
- border-bottom:none;
- flex-direction:column;
- align-items:stretch;
- justify-content:stretch;
- }
- :is(.col-left,.col-right).is-collapsed .btn-collapse {
- width:100%;
- min-width:100%;
- height:100%;
- margin:0;
- border-radius:0;
- font-size:20px;
- }
- .input-loop {
- width:44px;
- background:transparent;
- border:none;
- color:#fff;
- font-size:14px;
- font-weight:600;
- text-align:center;
- outline:none;
- padding:0;
- }
- .input-popover {
- width:45px;
- height:24px;
- background:rgb(0 0 0/30%);
- border:1px solid var(--border);
- color:#fff;
- font-size:13px;
- border-radius:4px;
- text-align:center;
- outline:none;
- }
- .input-popover-lg { width:55px; }
- .flex-between { display:flex; justify-content:space-between; }
- .dialog-content {
- display:flex;
- flex-direction:column;
- gap:2px;
- max-height:85vh;
- overflow-y:auto;
- padding:8px;
- scrollbar-width:thin;
- scrollbar-gutter:stable;
- }
- .lightbox-media {
- grid-area:1 / 1;
- width:100%;
- height:100%;
- max-width:100%;
- max-height:100%;
- object-fit:contain;
- display:block;
- user-select:none;
- outline:none;
- border-radius:12px;
- box-shadow:0 24px 64px rgb(0 0 0 / 45%);
- transition:opacity .18s ease,filter .28s ease,transform .28s ease;
- opacity:0;
- position:relative;
- z-index:1;
- background:rgb(0 0 0 / 12%);
- }
- .lightbox-media.is-visible { opacity:1; }
- .lightbox-media--preview { filter:blur(10px); transform:scale(1.015); }
- .lightbox-media--full { z-index:2; }
- .lightbox-media--video { background:rgb(0 0 0 / 35%); }
- .dl-abs-btn {
- position:absolute;
- top:12px;
- right:12px;
- pointer-events:auto;
- background:var(--panel) !important;
- color:var(--text-main) !important;
- border:1px solid var(--border) !important;
- border-radius:8px;
- width:36px;
- height:36px;
- display:flex;
- align-items:center;
- justify-content:center;
- z-index:1000;
- box-shadow:0 4px 16px rgba(0,0,0,.8) !important;
- transition:background-color .15s,transform .15s;
- }
- .dl-abs-btn:hover {
- background:var(--bg-hover) !important;
- scale:1.05;
- }
- :is(.eco-mode,.auto-eco) * {
- backdrop-filter:none !important;
- box-shadow:none !important;
- }
- @media (prefers-reduced-motion: reduce) {
- .xai-btn,
- .xai-toast,
- .floating-edit-label,
- .dialog-inner,
- .xai-custom-popover,
- #xai-api-prompt,
- .textarea-wrapper {
- transition:none !important;
- animation:none !important;
- }
- }
- `;
- const UI_HTML = buildAppHtml();
- async function enforceHistoryLimit() {
- if (STATE.historyOrder.length > APP_CONFIG.LIMITS.MAX_HISTORY) {
- const ids = STATE.historyOrder.slice(APP_CONFIG.LIMITS.MAX_HISTORY);
- const removed = new Set(ids.map(String));
- DBQueue.forgetItems(ids);
- STATE.historyOrder = STATE.historyOrder.slice(0, APP_CONFIG.LIMITS.MAX_HISTORY);
- STATE.currentOrder = STATE.currentOrder.filter(id => !removed.has(String(id)));
- if (STATE.refOrder.some(id => removed.has(String(id)))) {
- STATE.refOrder = STATE.refOrder.filter(id => !removed.has(String(id)));
- ids.forEach(id => deleteRefPrompt(String(id)));
- updateRefState();
- }
- await StorageDB.deleteItemsBulk(ids);
- ids.forEach(id => Store.removeItem(id));
- }
- }
- async function loadDataFromDB() {
- STATE.prompts = sanitizePromptStore(await StorageDB.loadData('prompts'), STATE.prompts);
- STATE.promptDrafts = sanitizePromptStore(await StorageDB.loadData('promptDrafts'), STATE.prompts);
- STATE.favPrompts = sanitizeFavPromptStore(await StorageDB.loadData('favPrompts'), STATE.favPrompts);
- STATE.refPrompts = sanitizeRefPromptStore(await StorageDB.loadData('refPrompts'), STATE.refPrompts);
- const refData = sanitizeRefDataStore(await StorageDB.loadData('refData')); if (refData) STATE.refDataTmp = refData;
- STATE.currentOrderTmp = sanitizeIdListStore(await StorageDB.loadData('currentOrder'));
- STATE.pendingVideos = sanitizePendingVideosStore(await StorageDB.loadData('pendingVideos'));
- }
- function recoverPendingVideos() {
- if (!STATE.pendingVideos || !STATE.pendingVideos.length) return; const now = Date.now();
- STATE.pendingVideos.forEach(v => {
- if (now - v.timestamp > 1000 * 60 * 60 * 2) return;
- const ac = new AbortController();
- pollVideoTask(v.reqId, ac.signal, -1, () => {}).then(async videoUrl => {
- if (videoUrl) {
- let posterData = null;
- try { posterData = await MediaUtils.processSource(await fetchBlobCSP(videoUrl, '0-500000'), 'video', getThumbMaxDim(), getThumbQuality()); if (!posterData.thumbBlob) throw new Error('Partial failed'); }
- catch (e) { try { posterData = await MediaUtils.processSource(await fetchBlobCSP(videoUrl, null), 'video', getThumbMaxDim(), getThumbQuality()); } catch (err) {} }
- const id = crypto.randomUUID();
- Store.commitNewItem({ id, type: 'video', mimeType: 'video/mp4', prompt: Pipeline.normalizePrompt(v.prompt), filename: `Grok Video (Recovered) - ${makeTimestamp()}.mp4`, ratio: posterData?.ratio || 16/9, originalUrl: videoUrl, downloaded: false, lqip: posterData?.lqip || null, isUploadedRef: false }, 'currentOrder');
- Store.prependHistory([id]); await enforceHistoryLimit();
- UI.showToast("Recovered a background video generation!", "success");
- }
- }).catch(e => {}).finally(() => { STATE.pendingVideos = STATE.pendingVideos.filter(pv => pv.reqId !== v.reqId); persistPendingVideos(); });
- });
- }
- async function cleanupOrphanMediaFiles() {
- try {
- const validIds = new Set(STATE.items.keys()), dir = await OPFS.initDir(); let deletedCount = 0;
- for await (const [name] of dir.entries()) {
- if (!validIds.has(name.split('_')[0])) {
- await dir.removeEntry(name).catch(()=>{});
- deletedCount++;
- }
- }
- if (deletedCount > 0) addLog('INFO', `GC removed ${deletedCount} orphan OPFS files`);
- } catch(e) {}
- }
- async function loadHistoryFromDB() {
- const allMeta = await StorageDB.loadAllMeta();
- const historyIds = [];
- const phantomIds = [];
- for (let i = 0; i < allMeta.length; i++) {
- const m = allMeta[i];
- m.id = String(m.id);
- if (Pipeline.isPhantomMeta(m)) { phantomIds.push(m.id); continue; }
- Store.addItem(m);
- if (!m.isUploadedRef) historyIds.push(m.id);
- if (i > 0 && i % 200 === 0) await yieldToMain();
- }
- if (phantomIds.length > 0) await StorageDB.deleteItemsBulk(phantomIds);
- STATE.historyOrder = historyIds;
- if (STATE.refDataTmp) { STATE.refOrder = STATE.refDataTmp.map(r => String(r.id)).filter(id => STATE.items.has(id)); STATE.refDataTmp.forEach(r => { const item = STATE.items.get(String(r.id)); if (item) item.active = r.active; }); delete STATE.refDataTmp; updateRefState(); }
- STATE.currentOrder = sanitizeIdListStore(STATE.currentOrderTmp).filter(id => {
- const item = STATE.items.get(String(id));
- return item && !Pipeline.isTransientItem(item);
- });
- delete STATE.currentOrderTmp;
- await enforceHistoryLimit();
- recoverPendingVideos();
- runBg(cleanupOrphanMediaFiles);
- }
- function renderRefList() {
- STATE.refOrder.forEach(id => {
- const item = STATE.items.get(id);
- if (item && item.active === undefined) item.active = true;
- });
- NativeGridManager.update(STATE.refOrder, STATE.items, 'reference');
- updateStatusUI();
- }
- function toggleRefPrompt(id) {
- if (STATE.activeRefPromptId === id) return saveAndCloseRefPrompt(true);
- if (STATE.activeRefPromptId) saveAndCloseRefPrompt(true);
- else if (DOM.prompt) setActionPromptDraft(STATE.action, getEditorPromptValue());
- STATE.activeRefPromptId = id; CardRegistry.maps.reference.forEach(card => card.classList.remove('is-editing'));
- STATE.activeRefPromptAction = STATE.action;
- STATE.activeRefPromptDraft = STATE.refPrompts[id] ?? getStoredActionPrompt(STATE.action);
- const activeCard = CardRegistry.get('reference', id); if (activeCard) activeCard.classList.add('is-editing');
- DOM.promptContainer.classList.add('editing-ref'); DOM.refEditLabel.innerText = `Editing Individual Prompt: IMAGE_${STATE.refOrder.indexOf(id)}`; DOM.refEditLabel.classList.add('show');
- DOM.generateBtn.classList.add('d-none'); DOM.saveRefBtn.classList.remove('d-none'); setEditorPromptValue(STATE.activeRefPromptDraft); DOM.prompt.focus();
- }
- function saveAndCloseRefPrompt(cancel = false) {
- if (!STATE.activeRefPromptId) return; const id = STATE.activeRefPromptId;
- STATE.activeRefPromptDraft = getEditorPromptValue();
- if (!cancel) {
- const val = sanitizePromptText(STATE.activeRefPromptDraft);
- if (val && val !== sanitizePromptText(getActiveRefPromptBaseline())) setRefPrompt(id, val);
- else deleteRefPrompt(id);
- }
- const card = CardRegistry.get('reference', id); if (card) { card.classList.remove('is-editing'); card.setReferencePromptFlag(!!STATE.refPrompts[id]); }
- STATE.activeRefPromptId = null;
- STATE.activeRefPromptAction = null;
- STATE.activeRefPromptDraft = '';
- DOM.promptContainer.classList.remove('editing-ref'); DOM.refEditLabel.classList.remove('show'); DOM.generateBtn.classList.remove('d-none'); DOM.saveRefBtn.classList.add('d-none'); setEditorPromptValue(getDraftActionPrompt());
- }
- const TRANSPARENT_1X1 = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
- const decodeImageElement = (img) => {
- if (!img) return Promise.resolve();
- if (typeof img.decode === 'function') return img.decode().catch(()=>{});
- return new Promise(resolve => {
- if (img.complete) return resolve();
- img.onload = img.onerror = () => resolve();
- });
- };
- const getLightboxSourceMedia = (sourceCard) => {
- if (!sourceCard?.shadowRoot) return null;
- const sourceVid = sourceCard.shadowRoot.querySelector('#vid');
- const sourceImg = sourceCard.shadowRoot.querySelector('#img');
- if (sourceVid?.getAttribute('src')) return sourceVid;
- if (sourceImg?.getAttribute('src')) return sourceImg;
- return null;
- };
- const createLightboxImageLayer = (item, className, src = TRANSPARENT_1X1, fallbackBg = 'transparent') => {
- const img = document.createElement('img');
- img.className = className;
- img.decoding = 'async';
- img.fetchPriority = 'high';
- img.src = src || TRANSPARENT_1X1;
- img.style.background = fallbackBg;
- if (item?.ratio) img.style.aspectRatio = String(item.ratio);
- return img;
- };
- const createLightboxVideoElement = (item, src = TRANSPARENT_1X1, poster = '', fallbackBg = 'transparent') => {
- const video = document.createElement('video');
- video.className = 'lightbox-media lightbox-media--video';
- video.controls = true;
- video.autoplay = true;
- video.loop = true;
- video.playsInline = true;
- video.preload = 'metadata';
- video.volume = 0.05;
- video.style.background = fallbackBg;
- if (item?.ratio) video.style.aspectRatio = String(item.ratio);
- if (poster) video.poster = poster;
- video.src = src || TRANSPARENT_1X1;
- return video;
- };
- const clearLightboxStage = () => {
- DOM.lightboxInner?.classList.remove('is-loaded');
- DOM.lightboxStage?.querySelectorAll('.lightbox-media').forEach(node => {
- if (node.tagName === 'VIDEO') node.pause();
- node.remove();
- });
- if (DOM.lightboxStage) DOM.lightboxStage.style.removeProperty('--lightbox-ratio');
- };
- const releaseLightboxPinnedMedia = () => {
- const pin = DOM.lightbox?._cachePin;
- if (pin) BlobCache.release(pin.id, pin.type);
- if (DOM.lightbox) DOM.lightbox._cachePin = null;
- };
- function openLightbox(item, sourceCard) {
- const inner = DOM.lightboxInner;
- clearLightboxStage();
- releaseLightboxPinnedMedia();
- const openToken = (DOM.lightbox._openToken = (DOM.lightbox._openToken || 0) + 1);
- const sourceMedia = getLightboxSourceMedia(sourceCard);
- const previewSrc = sourceMedia ? (sourceMedia.tagName === 'VIDEO' ? (sourceMedia.poster || sourceMedia.currentSrc || sourceMedia.src) : (sourceMedia.currentSrc || sourceMedia.src)) : null;
- const fallbackBg = (item.lqip && item.lqip.includes('gradient')) ? item.lqip : 'transparent';
- const fetchId = String(item.refSourceId || item.id);
- const isStale = () => STATE.activeLightboxId !== item.id || DOM.lightbox?._openToken !== openToken;
- if (DOM.lightboxStage) DOM.lightboxStage.style.setProperty('--lightbox-ratio', String(item.ratio || (item.type === 'video' ? (16 / 9) : 1)));
- DOM.lightboxDl.href = item.originalUrl || '#';
- DOM.lightboxDl.download = item.filename || 'download';
- STATE.activeLightboxId = item.id;
- if (item.type === 'image') {
- const previewEl = createLightboxImageLayer(item, 'lightbox-media lightbox-media--preview', previewSrc || TRANSPARENT_1X1, fallbackBg);
- const fullEl = createLightboxImageLayer(item, 'lightbox-media lightbox-media--full', previewSrc || TRANSPARENT_1X1, fallbackBg);
- DOM.lightboxStage.insertBefore(previewEl, DOM.lightboxDl);
- DOM.lightboxStage.insertBefore(fullEl, DOM.lightboxDl);
- requestAnimationFrame(() => previewEl.classList.add('is-visible'));
- } else {
- const videoEl = createLightboxVideoElement(item, item.originalUrl || TRANSPARENT_1X1, previewSrc || '', fallbackBg);
- DOM.lightboxStage.insertBefore(videoEl, DOM.lightboxDl);
- requestAnimationFrame(() => videoEl.classList.add('is-visible'));
- }
- if (!DOM.lightbox.open) DOM.lightbox.showModal();
- if (item.type === 'image') MediaWarmPool.warm(item).catch(()=>{});
- const fullUrlPromise = item.type === 'video' && item.originalUrl ? Promise.resolve(item.originalUrl) : BlobCache.full.retain(fetchId, 'full');
- fullUrlPromise.then(async targetUrl => {
- if (isStale()) {
- if (!(item.type === 'video' && item.originalUrl)) BlobCache.release(fetchId, 'full');
- return;
- }
- const safeUrl = sanitizeUrl(targetUrl);
- if (!safeUrl) {
- if (!(item.type === 'video' && item.originalUrl)) BlobCache.release(fetchId, 'full');
- return;
- }
- if (!(item.type === 'video' && item.originalUrl)) DOM.lightbox._cachePin = { id: fetchId, type: 'full' };
- if (item.type === 'image') {
- const fullEl = DOM.lightboxStage.querySelector('.lightbox-media--full');
- if (!fullEl) return;
- fullEl.src = safeUrl;
- await decodeImageElement(fullEl);
- if (isStale()) return;
- DOM.lightboxDl.href = safeUrl;
- fullEl.classList.add('is-visible');
- requestAnimationFrame(() => inner.classList.add('is-loaded'));
- setTimeout(() => { if (!isStale()) DOM.lightboxStage.querySelector('.lightbox-media--preview')?.remove(); }, 260);
- } else {
- const videoEl = DOM.lightboxStage.querySelector('.lightbox-media--video');
- if (!videoEl) return;
- if (!item.originalUrl) {
- const currentTime = videoEl.currentTime || 0;
- videoEl.src = safeUrl;
- if (currentTime > 0) videoEl.currentTime = currentTime;
- }
- DOM.lightboxDl.href = safeUrl;
- videoEl.play().catch(()=>{});
- }
- }).catch(err => addLog('WARN', 'Failed to load full blob for lightbox', err));
- }
- const closeLightbox = () => {
- DOM.lightbox._openToken = (DOM.lightbox._openToken || 0) + 1;
- DOM.lightbox.close();
- clearLightboxStage();
- releaseLightboxPinnedMedia();
- STATE.activeLightboxId = null;
- }
- function initMediaInteractions() {
- const handleRefDelete = (id) => {
- if (STATE.historyOrder.includes(id) || STATE.currentOrder.includes(id)) {
- STATE.refOrder = STATE.refOrder.filter(i => i !== id);
- deleteRefPrompt(id);
- updateRefState();
- if (STATE.activeRefPromptId === id) saveAndCloseRefPrompt(true);
- } else void handleGlobalDelete(id, true);
- };
- const handleGlobalDelete = async (id, isRefDel = false) => {
- const key = String(id);
- DBQueue.forgetItem(key);
- STATE.historyOrder = STATE.historyOrder.filter(i => i !== key);
- STATE.currentOrder = STATE.currentOrder.filter(i => i !== key);
- if (STATE.refOrder.includes(id)) {
- STATE.refOrder = STATE.refOrder.filter(i => i !== key);
- deleteRefPrompt(key);
- updateRefState();
- if (STATE.activeRefPromptId === key) saveAndCloseRefPrompt(true);
- }
- Store.removeItem(key);
- await StorageDB.deleteItem(key).catch(e => addLog('WARN', 'Delete item failed', { id: key, error: e?.message || e }));
- if (!isRefDel) runBg(updateHistoryMetrics);
- };
- const handleCardHover = (e, isHover) => {
- const card = e.composedPath ? e.composedPath().find(el => el.tagName === 'XAI-CARD') : e.target.closest('xai-card'); if (!card) return; if (e.relatedTarget && card.contains(e.relatedTarget)) return;
- if (isHover) { clearTimeout(card._hoverTimer); card._hoverTimer = setTimeout(() => card._handleHover(true), 150); } else { clearTimeout(card._hoverTimer); card._handleHover(false); }
- };
- const warmLightboxCandidate = (card, delay = 90) => {
- if (!card || card.classList.contains('is-reference')) return;
- clearTimeout(card._warmTimer);
- card._warmTimer = setTimeout(() => {
- const item = STATE.items.get(String(card.dataset.id));
- MediaWarmPool.warm(item).catch(()=>{});
- }, delay);
- };
- const clearWarmLightboxCandidate = (card) => {
- if (!card) return;
- clearTimeout(card._warmTimer);
- card._warmTimer = null;
- };
- [DOM.currentImages].forEach(container => {
- if (!container) return;
- App.on(container, 'mouseover', e => {
- handleCardHover(e, true);
- const card = e.composedPath ? e.composedPath().find(el => el.tagName === 'XAI-CARD') : e.target.closest('xai-card');
- if (card) warmLightboxCandidate(card);
- });
- App.on(container, 'mouseout', e => {
- handleCardHover(e, false);
- const card = e.composedPath ? e.composedPath().find(el => el.tagName === 'XAI-CARD') : e.target.closest('xai-card');
- if (card) clearWarmLightboxCandidate(card);
- });
- App.on(container, 'pointerdown', e => {
- const card = e.composedPath ? e.composedPath().find(el => el.tagName === 'XAI-CARD') : e.target.closest('xai-card');
- if (card) warmLightboxCandidate(card, 0);
- }, { passive: true });
- });
- const handleClick = async (e) => {
- let targetEl = e.composedPath ? e.composedPath()[0] : e.target; const card = targetEl.closest('xai-card') || targetEl.getRootNode().host; if (!card) return;
- const id = String(card.dataset.id), item = STATE.items.get(id); if (!item) return;
- if (targetEl.closest('.ref-side-prompt-btn')) return e.preventDefault() & e.stopPropagation() & toggleRefPrompt(id);
- const isRefDel = targetEl.closest('.ref-del'), isGlobalDel = targetEl.closest('.overlay-del-btn'), isEdit = targetEl.closest('.overlay-edit-btn'), isExtend = targetEl.closest('.overlay-extend-btn'), isVideoBtn = targetEl.closest('.overlay-video-btn');
- if (isRefDel) { e.preventDefault(); e.stopPropagation(); return handleRefDelete(id); }
- if (isGlobalDel) { e.preventDefault(); e.stopPropagation(); if (item.isPending && item.taskId) { const task = JobManager.tasks.get(item.taskId); if (task && task.controller) task.controller.abort(); } return void handleGlobalDelete(id); }
- if (isEdit || isExtend || isVideoBtn) {
- e.preventDefault(); e.stopPropagation();
- const newAction = ActionUtils.getCardAction(item.type, isExtend ? 'extend' : (isVideoBtn ? 'video' : 'edit'));
- moveReferenceToFront(id, { exclusiveActive: true, forceRebuild: true });
- changeActionState(newAction);
- return;
- }
- if (targetEl.closest('.card-prompt')) { e.preventDefault(); e.stopPropagation(); if (STATE.activeRefPromptId) saveAndCloseRefPrompt(true); DomUtils.insertTextAtCursor(DOM.prompt, item.prompt, true); PromptSync.captureEditor(); return; }
- const isDlBtn = targetEl.closest('.overlay-dl-btn'); if (isDlBtn) { e.preventDefault(); e.stopPropagation(); return MediaUtils.downloadItem(item, false, card); }
- if (!item.isPending && !isDlBtn && !isGlobalDel && !isRefDel && !targetEl.closest('.ref-header') && !targetEl.closest('.ref-side-prompt-btn')) openLightbox(item, card);
- };[DOM.history, DOM.currentImages].forEach(el => App.on(el, 'click', handleClick));
- App.on(DOM.refList, 'click', (e) => {
- let targetEl = e.composedPath ? e.composedPath()[0] : e.target, t = targetEl.closest('.ref-active-toggle');
- if (t) { const c = targetEl.getRootNode().host || targetEl.closest('xai-card'), i = STATE.items.get(String(c.dataset.id)); if (i) { i.active = t.checked; c.classList.toggle('inactive', !i.active); if (i.active && STATE.isGenerating) i.readyAt = Date.now() + 10000; else if (!i.active)[...JobManager.tasks.values()].filter(ts => ts.refIds.includes(i.id)).forEach(ts => ts.controller.abort()); updateRefState(); } return; }
- handleClick(e);
- });
- App.on(DOM.saveRefBtn, 'click', () => saveAndCloseRefPrompt(false));
- }
- function initHotkeys() {
- let tgt = DOM.currentImages; const setTgt = el => () => tgt = el;
- App.on(DOM.history, 'mouseenter', setTgt(DOM.history)); App.on(DOM.currentImages, 'mouseenter', setTgt(DOM.currentImages)); App.on(DOM.refList, 'mouseenter', setTgt(DOM.refList));
- App.on(document, 'keydown', e => {
- const isInput = document.activeElement &&['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName);
- if (isInput && document.activeElement === DOM.prompt &&['Home', 'End', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'PageUp', 'PageDown'].includes(e.key)) return;
- if (isInput && document.activeElement !== DOM.prompt) return;
- if (['PageUp', 'PageDown', 'Home', 'End'].includes(e.key)) {
- if (!tgt) return;
- e.preventDefault();
- if (e.key === 'PageUp') tgt.scrollBy({ top: -tgt.clientHeight * 0.8, behavior: 'smooth' });
- else if (e.key === 'PageDown') tgt.scrollBy({ top: tgt.clientHeight * 0.8, behavior: 'smooth' });
- else if (e.key === 'Home') tgt.scrollTo({ top: 0, behavior: 'smooth' });
- else if (e.key === 'End') tgt.scrollTo({ top: tgt.scrollHeight, behavior: 'smooth' });
- }
- });
- }
- function changeActionState(actionValue) {
- if (STATE.action === actionValue) return;
- runTransition(() => {
- if (STATE.activeRefPromptId) saveAndCloseRefPrompt(true);
- else if (DOM.prompt) PromptSync.captureEditor(STATE.action, { scheduleCommit: false });
- STATE.action = actionValue; syncActionLabel(actionValue);
- setActivePopoverItem(document.getElementById('popover-action'), actionValue);
- setEditorPromptValue(getDraftActionPrompt());
- });
- }
- function bindLogic() {
- Object.entries(DOM_IDS).forEach(([k, id]) => DOM[k] = document.getElementById(id));
- NativeGridManager.init(); DOM.historyScroller = HistoryManager; DOM.historyScroller.init(DOM.history);
- if (STATE.leftCollapsed) { DOM.colLeft.classList.add('is-collapsed'); document.getElementById('btn-collapse-left').setAttribute('aria-expanded', 'false'); }
- if (STATE.rightCollapsed) { DOM.colRight.classList.add('is-collapsed'); document.getElementById('btn-collapse-right').setAttribute('aria-expanded', 'false'); }
- initUIControls(); initLayoutManager(); initMediaManager(); initPromptManager(); initModals(); initMediaInteractions(); initAPIAndGeneration(); initHotkeys();
- }
- function initUIControls() {
- const setupDropdown = (btnId, popoverId, labelId, options, onChange) => {
- const btn = document.getElementById(btnId), popover = document.getElementById(popoverId), label = document.getElementById(labelId);
- if (!btn || !popover) return; btn.setAttribute('popovertarget', popoverId);
- const _renderOpts = (opts) => renderPopoverOptions(popover, btn.dataset.value, opts);
- _renderOpts(options);
- App.on(popover, 'click', e => {
- const item = e.target.closest('.popover-item');
- if (!item || item.classList.contains('disabled')) return;
- setActivePopoverItem(popover, item.dataset.val);
- btn.dataset.value = item.dataset.val;
- const text = item.querySelector('span')?.textContent || '';
- if (label) label.textContent = text;
- popover.hidePopover();
- onChange?.(item.dataset.val, text);
- });
- popover._updateOptions = _renderOpts;
- };
- const setupToggle = (elId, valA, valB, textA, textB, onChange) => { const el = document.getElementById(elId); if (!el) return; App.on(el, 'click', () => { const isA = el.dataset.value === valA; el.dataset.value = isA ? valB : valA; el.innerText = isA ? textB : textA; onChange?.(el.dataset.value); }); };
- const bindNum = (id, key, max) => { const el = document.getElementById(id); if (!el) return; el.value = STATE[key]; App.on(el, 'input', e => STATE[key] = max ? Math.min(max, parseInt(e.target.value) || 0) : (parseInt(e.target.value) || 0)); App.on(el, 'click', e => e.stopPropagation()); };
- const findToggleMeta = (target) => SETTINGS_TOGGLE_META.find(meta => target.closest(`#${meta.rowId}`));
- syncActionLabel();
- setupDropdown('btn-master-action', 'popover-action', 'label-master-action', Object.entries(ACTION_META).map(([val, meta]) => ({ val, text: meta.label, title: meta.title })), (v) => changeActionState(v));
- DOM.settingBatch.dataset.value = STATE.batchSize; if (DOM.labelSettingBatch) DOM.labelSettingBatch.innerText = STATE.batchSize;
- setupDropdown('btn-setting-batch', 'popover-batch', 'label-setting-batch', Array.from({ length: 10 }, (_, i) => ({ val: String(i + 1), text: String(i + 1) })), v => STATE.batchSize = v);
- setupDropdown('btn-setting-ar', 'popover-ar', null,[], v => { STATE.currentAr = getAspectRatioOption(v).label.trim(); syncAspectRatioUI(); });
- syncAspectRatioUI();
- setupDropdown('btn-setting-duration', 'popover-duration', 'label-setting-duration',[], v => STATE.duration = v);
- DOM.settingLoops.value = STATE.loops; App.on(DOM.settingLoops, 'input', () => STATE.loops = DOM.settingLoops.value);
- setupToggle('xai-api-res-img', '2k', '1k', '2K Pro', '1K Std', v => STATE.resImg = v); setupToggle('xai-api-res-vid', '720p', '480p', '720p', '480p', v => STATE.resVid = v);
- syncResolutionButtons();
- SETTINGS_NUMBER_META.forEach(({ inputId, stateKey, max }) => bindNum(inputId, stateKey, max));
- syncSettingsInputs();
- DOM.settingsBtn.setAttribute('popovertarget', 'popover-settings');
- App.on(DOM.settingsPopover, 'click', e => {
- const meta = findToggleMeta(e.target);
- if (!meta) return;
- STATE[meta.stateKey] = !STATE[meta.stateKey];
- if (DOM[meta.domKey]) DOM[meta.domKey].checked = !!STATE[meta.stateKey];
- e.stopPropagation();
- });
- App.on(document.getElementById('btn-export-state'), 'click', () => {
- if (!STATE.activeRefPromptId && DOM.prompt) PromptSync.captureEditor(STATE.action, { scheduleCommit: false });
- const stateToExport = buildExportState();
- const u = URL.createObjectURL(new Blob([JSON.stringify(stateToExport)], {type: 'application/json'})), a = document.createElement('a'); a.href = u; a.download = `xAI_Pro_Settings_${makeTimestamp()}.json`; a.click(); scheduleObjectUrlRevoke(u, 5000);
- });
- const importInput = document.getElementById('file-import-state');
- App.on(document.getElementById('btn-import-state'), 'click', () => importInput.click());
- App.on(importInput, 'change', e => {
- const file = e.target.files[0]; if (!file) return;
- const reader = new FileReader();
- reader.onload = evt => {
- try {
- const p = sanitizeImportedState(JSON.parse(evt.target.result));
- const { prompts, promptDrafts, refPrompts, favPrompts, ...nextState } = p;
- Object.entries(nextState).forEach(([key, value]) => STATE[key] = value);
- STATE.prompts = sanitizePromptStore(prompts, STATE.prompts);
- STATE.promptDrafts = sanitizePromptStore(promptDrafts, STATE.prompts);
- STATE.refPrompts = sanitizeRefPromptStore(refPrompts, STATE.refPrompts);
- STATE.favPrompts = sanitizeFavPromptStore(favPrompts, STATE.favPrompts);
- persistEffectivePrompts(); persistPromptDrafts(); persistRefPrompts(); persistFavPrompts();
- syncActionLabel(); _updateActionUIDom();
- syncSettingsInputs(); syncResolutionButtons(); syncAspectRatioUI(); syncLayoutWidths();
- if (!STATE.activeRefPromptId) setEditorPromptValue(getDraftActionPrompt());
- renderRefMode(); UI.showToast("Settings & Prompts imported successfully!", "success");
- } catch(err) { UI.showToast("Failed to parse JSON", "error"); }
- }; reader.readAsText(file); e.target.value = '';
- });
- renderRefMode(); App.on(DOM.refModeToggle, 'click', () => STATE.refMode = STATE.refMode === 'multi' ? 'queue' : 'multi'); updateUploadPlaceholderState(); _updateActionUIDom();
- }
- function initLayoutManager() {
- let layoutRaf = 0;
- const scheduleCurrentLayout = () => {
- if (layoutRaf) cancelAnimationFrame(layoutRaf);
- layoutRaf = requestAnimationFrame(() => {
- layoutRaf = 0;
- NativeGridManager.syncCurrentLayoutMetrics?.();
- });
- };
- const syncObservedWidth = (target, width) => {
- const next = Math.round(width || 0);
- if (!Number.isFinite(next) || next <= 0) return;
- if (target === DOM.colLeft && !STATE.leftCollapsed && Math.abs(next - STATE.leftWidth) >= 2) STATE.leftWidth = Math.max(320, next);
- if (target === DOM.colRight && !STATE.rightCollapsed && Math.abs(next - STATE.rightWidth) >= 2) STATE.rightWidth = Math.max(240, next);
- };
- App.on(window, 'resize', scheduleCurrentLayout);
- const ro = new ResizeObserver(entries => {
- for (let e of entries) {
- const borderBox = Array.isArray(e.borderBoxSize) ? e.borderBoxSize[0] : e.borderBoxSize;
- syncObservedWidth(e.target, borderBox?.inlineSize || e.contentRect?.width || 0);
- }
- scheduleCurrentLayout();
- });
- syncLayoutWidths();
- scheduleCurrentLayout();
- if (DOM.colLeft) ro.observe(DOM.colLeft); if (DOM.colRight) ro.observe(DOM.colRight);
- App.ac.signal.addEventListener('abort', () => {
- if (layoutRaf) cancelAnimationFrame(layoutRaf);
- ro.disconnect();
- });
- App.on(document.getElementById('btn-collapse-left'), 'click', () => STATE.leftCollapsed = !STATE.leftCollapsed); App.on(document.getElementById('btn-collapse-right'), 'click', () => STATE.rightCollapsed = !STATE.rightCollapsed);
- }
- let processMediaFileLocal = null;
- function initMediaManager() {
- let dragged = null, pendingRefTarget = null, pendingRefAfter = false, refReorderRaf = 0;
- const flushRefReorder = () => {
- refReorderRaf = 0;
- if (!dragged || !pendingRefTarget || pendingRefTarget === dragged) return;
- DOM.refList.insertBefore(dragged, pendingRefAfter ? pendingRefTarget.nextSibling : pendingRefTarget);
- };
- const finishRefDrag = () => {
- if (refReorderRaf) {
- cancelAnimationFrame(refReorderRaf);
- flushRefReorder();
- }
- if (!dragged) return;
- dragged.classList.remove('dragging');
- dragged = null;
- pendingRefTarget = null;
- pendingRefAfter = false;
- STATE.refOrder = Array.from(DOM.refList.querySelectorAll('xai-card.is-reference')).map(c => String(c.dataset.id));
- updateRefState();
- };
- App.on(DOM.refList, 'dragstart', e => {
- const card = e.target.closest('xai-card.is-reference');
- if (!card) return;
- dragged = card;
- pendingRefTarget = null;
- pendingRefAfter = false;
- e.dataTransfer.effectAllowed = 'move';
- e.dataTransfer.setData('text/plain', card.dataset.id);
- requestAnimationFrame(() => card.classList.add('dragging'));
- });
- App.on(DOM.refList, 'dragover', e => {
- if (!dragged) return;
- e.preventDefault();
- e.dataTransfer.dropEffect = 'move';
- const targetCard = e.target.closest('xai-card.is-reference:not(.dragging)');
- if (!targetCard || targetCard === dragged) return;
- const rect = targetCard.getBoundingClientRect(), next = (e.clientY - rect.top) / (rect.bottom - rect.top) > 0.5;
- if (targetCard === pendingRefTarget && next === pendingRefAfter) return;
- pendingRefTarget = targetCard;
- pendingRefAfter = next;
- if (refReorderRaf) cancelAnimationFrame(refReorderRaf);
- refReorderRaf = requestAnimationFrame(flushRefReorder);
- });
- App.on(DOM.refList, 'drop', e => { if (!dragged) return; e.preventDefault(); finishRefDrag(); });
- App.on(DOM.refList, 'dragend', finishRefDrag);
- App.ac.signal.addEventListener('abort', () => {
- if (refReorderRaf) cancelAnimationFrame(refReorderRaf);
- document.body.classList.remove('is-dragging');
- dragged = null;
- pendingRefTarget = null;
- pendingRefAfter = false;
- refReorderRaf = 0;
- });
- const handleMediaDragStart = (e) => {
- const card = e.target.closest('xai-card');
- if (!card || (!card.classList.contains('is-history') && !card.classList.contains('is-current'))) return;
- document.body.classList.add('is-dragging');
- e.dataTransfer.effectAllowed = 'copy';
- e.dataTransfer.setData('application/x-xai-media-id', card.dataset.id);
- };
- [DOM.history, DOM.currentImages].forEach(el => {
- if (!el) return;
- App.on(el, 'dragstart', handleMediaDragStart);
- App.on(el, 'dragend', () => document.body.classList.remove('is-dragging'));
- });
- processMediaFileLocal = async (file) => {
- const isVid = file.type.startsWith('video/'), isImg = file.type.startsWith('image/'); if (!isVid && !isImg) return;
- const nextAction = ActionUtils.getUploadAction(STATE.action, file.type);
- if (nextAction !== STATE.action) changeActionState(nextAction);
- const maxRefs = ActionUtils.getMaxRefs(STATE.action, 'queue'); if (STATE.refOrder.length >= maxRefs) return;
- const mediaData = await MediaUtils.processSource(file, isVid ? 'video' : 'image', getThumbMaxDim(), getThumbQuality()), id = crypto.randomUUID();
- await OPFS.write(id, 'full', file); if (mediaData.thumbBlob) await OPFS.write(id, 'thumb', mediaData.thumbBlob);
- Store.commitNewItem({ id, type: isVid ? 'video' : 'image', mimeType: file.type, active: true, filename: file.name, ratio: mediaData.ratio, lqip: mediaData.lqip, readyAt: STATE.isGenerating ? Date.now() + 10000 : 0, isUploadedRef: true });
- moveReferenceToFront(id, { enforceLimit: true, forceRebuild: true });
- };
- let mediaImportQueue = Promise.resolve();
- const queueMediaFileLocal = (file) => {
- mediaImportQueue = mediaImportQueue.then(() => processMediaFileLocal(file)).catch(err => addLog('WARN', 'Local media import failed', err));
- return mediaImportQueue;
- };
- const supportsDropPayload = (dataTransfer) => !!dataTransfer && Array.from(dataTransfer.types || []).some(type => type === 'Files' || type === 'application/x-xai-media-id');
- const createHoverTracker = (onHover) => {
- let depth = 0;
- return {
- enter() { depth++; onHover?.(true); },
- leave() { depth = Math.max(0, depth - 1); if (depth === 0) onHover?.(false); },
- reset() { depth = 0; onHover?.(false); }
- };
- };
- const setupDrop = (el, onHover) => {
- if (!el) return;
- const hoverTracker = createHoverTracker(onHover);
- App.on(el, 'dragenter', e => {
- if (!supportsDropPayload(e.dataTransfer)) return;
- e.preventDefault();
- e.stopPropagation();
- hoverTracker.enter();
- });
- App.on(el, 'dragover', e => {
- if (!supportsDropPayload(e.dataTransfer)) return;
- e.preventDefault();
- e.stopPropagation();
- e.dataTransfer.dropEffect = 'copy';
- });
- App.on(el, 'dragleave', e => {
- if (!supportsDropPayload(e.dataTransfer)) return;
- e.preventDefault();
- e.stopPropagation();
- hoverTracker.leave();
- });
- App.on(el, 'drop', async e => {
- if (!supportsDropPayload(e.dataTransfer)) return;
- e.preventDefault();
- e.stopPropagation();
- hoverTracker.reset();
- const mediaId = e.dataTransfer.getData('application/x-xai-media-id');
- if (mediaId && STATE.items.has(mediaId)) {
- const item = STATE.items.get(mediaId);
- const mimeType = item?.mimeType || `${item?.type || 'image'}/`;
- const nextAction = ActionUtils.getUploadAction(STATE.action, mimeType);
- if (nextAction !== STATE.action) changeActionState(nextAction);
- moveReferenceToFront(mediaId, { enforceLimit: true, forceRebuild: true });
- } else if (e.dataTransfer.files?.length) {
- Array.from(e.dataTransfer.files).forEach(queueMediaFileLocal);
- }
- });
- };
- App.on(DOM.uploadPlaceholder, 'click', () => { if (STATE.refOrder.length < APP_CONFIG.LIMITS.MAX_REFS_QUEUE) DOM.fileInput.click(); });
- App.on(DOM.fileInput, 'change', e => { Array.from(e.target.files).forEach(queueMediaFileLocal); DOM.fileInput.value = ''; });
- setupDrop(DOM.dropzone, hover => DOM.uploadPlaceholder.classList.toggle('drag-over', hover)); setupDrop(DOM.prompt, hover => DOM.promptContainer.classList.toggle('drag-over', hover));
- App.on(document, 'paste', e => {
- const tag = document.activeElement?.tagName; if (tag &&['INPUT', 'TEXTAREA'].includes(tag) && document.activeElement !== DOM.prompt) return;
- let hasMedia = false;
- if (e.clipboardData?.items) {
- for (let i = 0; i < e.clipboardData.items.length; i++) {
- const item = e.clipboardData.items[i];
- if (item.type.startsWith('image/') || item.type.startsWith('video/')) { if (!hasMedia && document.activeElement === DOM.prompt) e.preventDefault(); hasMedia = true; const file = item.getAsFile(); if (file) queueMediaFileLocal(file); }
- }
- }
- });
- }
- function initPromptManager() {
- setEditorPromptValue(getDraftActionPrompt());
- App.on(DOM.promptContainer, 'pointerdown', e => {
- if (e.target === DOM.promptContainer || e.target === DOM.textareaWrapper) DOM.prompt.focus();
- });
- App.on(DOM.prompt, 'input', () => {
- if (STATE.activeRefPromptId) {
- STATE.activeRefPromptDraft = getEditorPromptValue();
- CardRegistry.get('reference', STATE.activeRefPromptId)?.setReferencePromptFlag(hasCustomRefPromptDraft(STATE.activeRefPromptDraft));
- return;
- }
- PromptSync.captureEditor();
- });
- App.on(DOM.prompt, 'keydown', e => {
- if (STATE.activeRefPromptId) {
- if (e.key === 'Escape') { e.preventDefault(); saveAndCloseRefPrompt(true); }
- if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); saveAndCloseRefPrompt(false); }
- return;
- }
- if (e.key === 'Enter' && !e.shiftKey) {
- e.preventDefault();
- if (!DOM.generateBtn.disabled && !STATE.isGenerating) DOM.generateBtn.click();
- }
- });
- const updateScrollIndicator = () => {
- if (!DOM.prompt || !DOM.textareaWrapper) return;
- const isScrollable = DOM.prompt.scrollHeight > DOM.prompt.clientHeight;
- const isNotAtBottom = Math.ceil(DOM.prompt.scrollTop + DOM.prompt.clientHeight) < DOM.prompt.scrollHeight - 4;
- DOM.textareaWrapper.classList.toggle('can-scroll', isScrollable && isNotAtBottom);
- };
- App.on(DOM.prompt, 'scroll', updateScrollIndicator, { passive: true });
- App.on(DOM.prompt, 'input', updateScrollIndicator);
- if (window.ResizeObserver) {
- const ro = new ResizeObserver(updateScrollIndicator);
- ro.observe(DOM.prompt);
- App.ac.signal.addEventListener('abort', () => ro.disconnect());
- }
- updateScrollIndicator();
- }
- const renderFavs = () => {
- const favs = STATE.favPrompts[STATE.action] ||[], pv = DOM.prompt.value.trim(), hasPending = pv && !favs.includes(pv);
- const fragment = document.createDocumentFragment();
- const buildFavItem = (text, pending = false) => {
- const row = document.createElement('div'); row.className = `fav-item${pending ? ' pending' : ''}`; row.dataset.text = text;
- const textEl = document.createElement('div'); textEl.className = 'fav-text'; textEl.title = text; textEl.textContent = text;
- const btn = document.createElement('button'); btn.type = 'button'; btn.className = `xai-btn xai-btn--icon-sm ${pending ? 'xai-btn--success fav-add-btn' : 'xai-btn--danger fav-del-btn'}`; btn.title = pending ? 'Confirm Save Favorite' : 'Delete Favorite'; btn.textContent = pending ? APP_CONFIG.ICONS.check : APP_CONFIG.ICONS.close;
- row.append(textEl, btn);
- return row;
- };
- if (favs.length === 0 && !hasPending) {
- const empty = document.createElement('div');
- empty.className = 'empty-favs text-muted text-center';
- empty.style.cssText = 'padding:16px;font-size:14px;';
- empty.textContent = 'No favorites yet.';
- fragment.appendChild(empty);
- } else {
- favs.forEach(f => fragment.appendChild(buildFavItem(f)));
- if (hasPending) fragment.appendChild(buildFavItem(pv, true));
- }
- DOM.favList.replaceChildren(fragment);
- };
- function initModals() {
- App.on(DOM.favBtn, 'click', () => { renderFavs(); DOM.favDialog.showModal(); });
- App.on(DOM.favDialog, 'click', e => {
- if (e.target === DOM.favDialog) return DOM.favDialog.close();
- const itemEl = e.target.closest('.fav-item'); if (!itemEl) return;
- const text = itemEl.dataset.text;
- if (e.target.closest('.fav-text')) { if (!itemEl.classList.contains('pending')) { DOM.favDialog.close(); requestAnimationFrame(() => DomUtils.insertTextAtCursor(DOM.prompt, text, true)); } }
- else if (e.target.closest('.fav-add-btn')) {
- const currentFavs = [...(STATE.favPrompts[STATE.action] || [])];
- currentFavs.push(text);
- STATE.favPrompts = { ...STATE.favPrompts, [STATE.action]: currentFavs };
- persistFavPrompts();
- renderFavs();
- }
- else if (e.target.closest('.fav-del-btn')) {
- const currentFavs = [...(STATE.favPrompts[STATE.action] || [])], idx = currentFavs.indexOf(text);
- if (idx > -1) {
- currentFavs.splice(idx, 1);
- STATE.favPrompts = { ...STATE.favPrompts, [STATE.action]: currentFavs };
- persistFavPrompts();
- }
- renderFavs();
- }
- });
- App.on(DOM.lightbox, 'click', e => { const dlBtn = e.target.closest('.dl-abs-btn'); if (dlBtn && STATE.activeLightboxId) { e.preventDefault(); e.stopPropagation(); const item = STATE.items.get(STATE.activeLightboxId); if (item) MediaUtils.downloadItem(item, true); return; } closeLightbox(); });
- App.on(DOM.lightbox, 'cancel', e => { e.preventDefault(); closeLightbox(); });
- }
- function parseDynamicPrompt(text) { if (!text) return text; let p = text, pr; do { pr = p; p = p.replace(/\{([^{}]+)\}/g, (_, contents) => { const opts = contents.split('|'); return opts[Math.floor(Math.random() * opts.length)]; }); } while (p !== pr); return p.trim(); }
- function resolveAspectRatio(currentRefs = null) {
- let ar = STATE.currentAr;
- if (ar !== 'Ref') return ar.toLowerCase().trim();
- if (!currentRefs?.length || !currentRefs[0]?.ratio) return 'auto';
- let bestMatch = '1:1', minDiff = Infinity;
- APP_CONFIG.ASPECT_RATIOS.forEach(option => {
- if (option.isAuto || option.isRef) return;
- const ratio = option.w / option.h, diff = Math.abs(ratio - currentRefs[0].ratio);
- if (diff < minDiff) {
- minDiff = diff;
- bestMatch = option.label;
- }
- });
- return bestMatch.toLowerCase().trim();
- }
- function applySingleRefTrick(targetKey, singleRefUrl) {
- return [
- { type: 'image_url', url: singleRefUrl },
- { type: 'image_url', url: singleRefUrl }
- ];
- }
- function buildImagePayload(payload, currentRefs, refBase64s, ar) {
- payload.resolution = STATE.resImg;
- payload.response_format = 'b64_json';
- if (ar !== 'auto') payload.aspect_ratio = ar;
- if (!currentRefs?.length || !refBase64s[0]) return;
- if (currentRefs.length === 1) {
- if (STATE.referenceARTrick && ar !== 'auto') payload.images = applySingleRefTrick('images', refBase64s[0]);
- else payload.image = { url: refBase64s[0] };
- return;
- }
- payload.images = refBase64s.filter(Boolean).map(url => ({ type: 'image_url', url }));
- }
- function buildVideoPayload(payload, currentRefs, refBase64s, ar) {
- payload.duration = parseInt(STATE.duration);
- payload.resolution = STATE.resVid;
- const canUseVideoAr = ar !== 'auto' && ALLOWED_VIDEO_ARS.has(ar.toUpperCase?.() ? ar.toUpperCase() : ar);
- if (canUseVideoAr) payload.aspect_ratio = ar;
- if (!currentRefs?.length || !refBase64s[0]) return;
- if (payload.duration > 10) {
- payload.image = { url: refBase64s[0] };
- return;
- }
- if (currentRefs.length === 1) {
- if (STATE.referenceARTrick && ar !== 'auto') payload.reference_images = applySingleRefTrick('reference_images', refBase64s[0]);
- else payload.image = { url: refBase64s[0] };
- return;
- }
- payload.reference_images = refBase64s.filter(Boolean).map(url => ({ url }));
- }
- function buildVideoEditPayload(actionType, payload, currentRefs, refBase64s) {
- if (actionType === 'extend_video') payload.duration = parseInt(STATE.duration);
- if (currentRefs?.length && refBase64s[0]) payload.video = { url: refBase64s[0] };
- }
- async function getApiPayload(currentRefs = null, refPromptOverride = null) {
- const actionType = STATE.action;
- const livePrompt = STATE.activeRefPromptId ? getStoredActionPrompt(actionType) : getTaskPrompt(actionType);
- const endpoint = ActionUtils.getEndpoint(actionType, currentRefs?.length || 0);
- const payload = { model: ActionUtils.getModel(actionType), prompt: sanitizePromptText(refPromptOverride ?? livePrompt) };
- const ar = resolveAspectRatio(currentRefs);
- const refBase64s = currentRefs ? await Promise.all(currentRefs.map(m => RefPayloadCache.get(m))) :[];
- if (actionType === 'gen_image') buildImagePayload(payload, currentRefs, refBase64s, ar);
- else if (actionType === 'gen_video') buildVideoPayload(payload, currentRefs, refBase64s, ar);
- else buildVideoEditPayload(actionType, payload, currentRefs, refBase64s);
- addLog('INFO', 'Built payload', { endpoint, payload });
- return { endpoint, payload };
- }
- async function fetchWithRetries(endpoint, payload, signal, token, taskId, onProgress) {
- let attempts = 0, rateLimitFails = 0, authRetries = 0; const getDelay = () => Math.floor(Math.random() * (STATE.retryMaxMs - STATE.retryMinMs + 1)) + STATE.retryMinMs, isImage = payload.model === "grok-imagine-image";
- while (attempts < APP_CONFIG.TIMINGS.MAX_RETRY_ATTEMPTS && STATE.generationToken === token) {
- try {
- const attemptNo = attempts + 1;
- const requestStartedAt = performance.now();
- addLog('NETWORK_REQ', `POST ${endpoint}`, { taskId, attempt: attemptNo, payload });
- const res = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Parallel-Task': taskId }, body: JSON.stringify(payload), signal });
- const elapsedMs = Math.max(0, Math.round(performance.now() - requestStartedAt));
- if (RETRYABLE_GATEWAY_STATUSES.has(res.status)) {
- attempts++;
- addLog('NETWORK_WARN', `Gateway error ${res.status}`, { taskId, attempt: attemptNo, elapsedMs });
- onProgress({ shortStatus: `Busy(${res.status}). Rty[${attempts}]`, longStatus: `Server Busy (${res.status}). Retrying (Attempt ${attempts})...`, color: '#eab308' });
- await sleep(getDelay(), signal);
- continue;
- }
- if (AUTH_FAILURE_STATUSES.has(res.status)) {
- attempts++; authRetries++;
- addLog('NETWORK_WARN', `Authentication error ${res.status}`, { taskId, attempt: attemptNo, elapsedMs, authRetries });
- if (attempts >= APP_CONFIG.TIMINGS.MAX_RETRY_ATTEMPTS || authRetries >= 3) throw Object.assign(new Error(`Auth retry limit reached (${res.status})`), { status: res.status, noRetry: true });
- onProgress({ shortStatus: `Auth(${res.status}). Wiping...[${attempts}]`, longStatus: `Session Expired (${res.status}). Resetting (Attempt ${attempts})...`, color: 'orange' });
- await nuclearWipe(); await sleep(Math.max(getDelay(), authRetries * 2000), signal); continue;
- }
- authRetries = 0;
- if (!res.ok) {
- const errData = await parseResponseJsonSafe(res);
- addLog('NETWORK_ERR', 'Response error JSON', { taskId, attempt: attemptNo, status: res.status, elapsedMs, data: errData });
- const errMsg = buildHttpErrorMessage(res.status, errData);
- if (isRequestModeratedError(errMsg)) {
- addLog('NETWORK_WARN', 'Moderation or policy failure', { taskId, attempt: attemptNo, status: res.status, elapsedMs, data: errData });
- onProgress({ shortStatus: `Blocked(${res.status})`, longStatus: `Request blocked by moderation (${res.status}).`, color: '#a1a1aa' });
- throw Object.assign(new Error(errMsg), { status: res.status, data: errData, noRetry: true, moderation: true });
- }
- if (isInvalidRequestError(errMsg)) {
- addLog('NETWORK_WARN', 'Invalid request payload', { taskId, attempt: attemptNo, status: res.status, elapsedMs, data: errData });
- onProgress({ shortStatus: `Invalid(${res.status})`, longStatus: `Server rejected the payload (${res.status}).`, color: '#f59e0b' });
- throw Object.assign(new Error(errMsg), { status: res.status, data: errData, noRetry: true, invalidRequest: true });
- }
- throw Object.assign(new Error(errMsg), { status: res.status, data: errData });
- }
- const data = await res.json();
- addLog('NETWORK_RES', 'Successful response', { taskId, attempt: attemptNo, status: res.status, elapsedMs, data });
- return data;
- } catch(err) {
- if (err.name === 'AbortError' || err.noRetry) throw err;
- const errStr = err.message.toLowerCase(), isShortRateLimit = errStr.match(/requests per second \(actual\/limit\):\s*(\d+)\s*\/\s*\1/), isLongRateLimit = RATE_LIMIT_FAILURE_STATUSES.has(err.status) || /rate limit|exhausted/.test(errStr) || errStr.match(/requests per minute \(actual\/limit\):\s*(\d+)\s*\/\s*\1/);
- if (isShortRateLimit || isLongRateLimit) {
- const nextAttempt = attempts + 1;
- addLog('NETWORK_WARN', 'Rate limit encountered', { taskId, attempt: attempts + 1, status: err.status || 429, message: err.message });
- onProgress({ shortStatus: `Lmt(${err.status || 429}). Wiping...[${nextAttempt}]`, longStatus: `Rate Limit Hit (${err.status || 429}). Resetting (Attempt ${nextAttempt})...`, color: 'orange' }); await nuclearWipe(); attempts++;
- if (attempts >= APP_CONFIG.TIMINGS.MAX_RETRY_ATTEMPTS) throw new Error("Max retries on rate limits");
- if (isShortRateLimit) await sleep(getDelay(), signal); else { if (++rateLimitFails <= 1) await sleep(1000, signal); else { rateLimitFails = 0; let cd = parseInt(STATE.rateLimitDelay) || 60; while (cd > 0 && STATE.generationToken === token && !signal.aborted) { onProgress({ shortStatus: `Limit(${cd}s)...`, longStatus: `Rate Limit Active. Waiting ${cd} seconds...`, color: 'orange' }); await sleep(1000, signal); cd--; } } } continue;
- }
- if (shouldAutoRetryStuckVideo(errStr)) {
- attempts++;
- addLog('POLL_WARN', 'Video task stalled or rejected', { taskId, attempt: attempts, message: err.message });
- if (attempts >= APP_CONFIG.TIMINGS.MAX_RETRY_ATTEMPTS) throw new Error("Max retries for stuck video");
- onProgress({ shortStatus: `Stuck Vid. Rty[${attempts}]`, longStatus: `Video Generation Stuck. Auto-retrying (Attempt ${attempts})...`, color: 'orange' });
- await sleep(getDelay(), signal);
- if (payload.prompt) payload.prompt += '\u200B';
- continue;
- }
- attempts++; if (attempts >= APP_CONFIG.TIMINGS.MAX_RETRY_ATTEMPTS) throw new Error(`Err: ${err.message.split('\n')[0].substring(0, 15)}`);
- onProgress({ shortStatus: `Wait 10s[${attempts}]`, longStatus: `Generation Failed. Waiting 10s to retry (Attempt ${attempts})...`, color: '#ef4444' }); if (payload.prompt) payload.prompt += '\u200B'; await sleep(10000, signal);
- }
- } throw new Error("Max retry reached");
- }
- async function pollVideoTask(reqId, signal, token, onProgress) {
- let pollCount = 0;
- while (token === -1 || STATE.generationToken === token) {
- if (++pollCount > APP_CONFIG.TIMINGS.MAX_POLL_ATTEMPTS) throw new Error("Timeout: Video likely dropped or stuck."); await sleep(APP_CONFIG.TIMINGS.API_POLL_INTERVAL, signal);
- if (token !== -1 && STATE.generationToken !== token) break;
- try {
- const pollStartedAt = performance.now();
- addLog('POLL_REQ', `Polling video ${reqId}`, { reqId, pollCount, maxPolls: APP_CONFIG.TIMINGS.MAX_POLL_ATTEMPTS });
- const res = await fetch(`/v1/videos/${reqId}`, { signal });
- const elapsedMs = Math.max(0, Math.round(performance.now() - pollStartedAt));
- if (AUTH_FAILURE_STATUSES.has(res.status)) { addLog('POLL_WARN', 'Polling auth failure', { reqId, pollCount, status: res.status, elapsedMs }); onProgress({ shortStatus: 'Poll 401. Wiping...!', longStatus: 'Authentication Error during poll. Resetting...', color: 'orange' }); await nuclearWipe(); await sleep(3000, signal); continue; }
- if (!res.ok) {
- const errData = await parseResponseJsonSafe(res);
- addLog('POLL_ERR', `Poll failed HTTP ${res.status}`, { reqId, pollCount, status: res.status, elapsedMs, data: errData });
- if (res.status === 404) throw new Error("video lost (404). Session wiped.");
- if (errData?.error) throw Object.assign(new Error(typeof errData.error.message === 'string' ? errData.error.message : JSON.stringify(errData.error)), { status: res.status, data: errData });
- continue;
- }
- const data = await res.json();
- addLog('POLL_RES', 'Poll result', { reqId, pollCount, status: res.status, elapsedMs, data });
- if (data?.error) throw Object.assign(new Error(data.error.message || JSON.stringify(data.error)), { data });
- const st = (data?.status || data?.state || 'processing').toLowerCase(), pNum = typeof data?.progress === 'number' ? data.progress : null, progStr = pNum !== null ? ` ${pNum}%` : '';
- onProgress({ shortStatus: `[${pollCount}/${APP_CONFIG.TIMINGS.MAX_POLL_ATTEMPTS}] ${st.charAt(0).toUpperCase() + st.slice(1)}${progStr}...`, longStatus: `Rendering Video: ${st.charAt(0).toUpperCase() + st.slice(1)}${progStr} (Attempt ${pollCount}/${APP_CONFIG.TIMINGS.MAX_POLL_ATTEMPTS})`, color: '#fff', progress: pNum });
- if (VIDEO_SUCCESS_STATES.has(st)) return data.video.url;
- else if (VIDEO_FLAGGED_STATES.has(st) || data.is_sensitive) {
- if (data?.video?.url) {
- addLog('POLL_WARN', 'Video flagged but recovered', { reqId, pollCount, state: st, elapsedMs, data });
- onProgress({ shortStatus: `Flagged(${st})`, longStatus: `Video flagged as ${st}, but media was recovered. Saving...`, color: '#d97706', progress: pNum });
- return data.video.url;
- }
- throw new Error(`Generation halted. Reason: ${st}`);
- }
- } catch (e) { throw e; }
- }
- }
- function createPlaceholders(taskId, batchCount, actionType, currentRefs) {
- let targetRatio = actionType.includes('video') ? (16 / 9) : 1; const ar = STATE.currentAr.trim();
- if (ar !== 'Auto' && ar !== 'Ref') { const parts = ar.split(':'); if (parts.length === 2) targetRatio = parseFloat(parts[0]) / parseFloat(parts[1]); } else if (currentRefs.length > 0 && currentRefs[0]) targetRatio = currentRefs[0].ratio || targetRatio;
- const refItem = currentRefs.length > 0 ? currentRefs[0] : null, theme = refItem ? null : PlaceholderPalette.next(), bgPreview = refItem?.lqip || theme?.gradient || '#070809', placeholderIds =[];
- const initialStatus = refItem
- ? [{ text: `${ActionUtils.getTitle(actionType)} • ${refItem.filename || 'Reference'}`, color: '#fff', style: CURRENT_PANEL_STATUS_STYLE }]
- : [
- { text: ActionUtils.getTitle(actionType), color: '#fff', style: CURRENT_PANEL_STATUS_STYLE },
- { text: 'Queued...', color: '#d4d4d8' }
- ];
- for (let i = 0; i < batchCount; i++) {
- const pid = `pending-${taskId}-${i}`; placeholderIds.push(pid);
- Store.commitNewItem({ id: pid, type: refItem ? refItem.type : (actionType === 'gen_image' ? 'image' : 'video'), ratio: targetRatio, lqip: bgPreview, isPending: true, persist: false, taskId: taskId, filename: 'Pending...', downloaded: false, refSourceId: refItem?.id, originalUrl: refItem?.originalUrl, _statusData: initialStatus }, { targetArrayName: 'currentOrder', persist: false });
- }
- return placeholderIds;
- }
- async function executeSingleTask(taskId, refIds, isMulti, token) {
- const taskMeta = JobManager.tasks.get(taskId); if (!taskMeta) return;
- const currentRefs = refIds.map(id => STATE.items.get(id)).filter(r => r && r.active);
- if (currentRefs.length === 0 && !['MULTI', 'NO_REF'].includes(refIds[0])) { taskMeta.longStatus = 'Skipped'; return; }
- const actionType = STATE.action, isImage = actionType === 'gen_image', batchCount = isImage ? parseInt(STATE.batchSize || "1") : 1, placeholderIds = createPlaceholders(taskId, batchCount, actionType, currentRefs);
- let attempts = 0, success = false, reqStartTime = Date.now(), fakeProgressTimer = null;
- const updateTask = (val) => {
- taskMeta.shortStatus = val.shortStatus; taskMeta.longStatus = val.longStatus; taskMeta.color = val.color;
- taskMeta.progress = val.progress !== undefined ? val.progress : null;
- updateStatusUI();
- const statusLines = buildCurrentStatusLines(val);
- placeholderIds.forEach(pid => {
- const pItem = STATE.items.get(pid), card = CardRegistry.get('current', pid);
- if (pItem) pItem._statusData = statusLines;
- if (card && card.setStatus) card.setStatus(statusLines);
- if (card && card.setProgress) card.setProgress(taskMeta.progress);
- });
- refIds.forEach(rid => {
- const refCard = CardRegistry.get('reference', rid);
- if (refCard && refCard.setProgress) {
- const tasksForRef =[...JobManager.tasks.values()].filter(t => t.refIds.includes(rid));
- if (tasksForRef.length > 0) refCard.setProgress(tasksForRef.reduce((s, t) => s + (t.progress || 0), 0) / tasksForRef.length);
- else refCard.setProgress(taskMeta.progress);
- }
- });
- };
- if (isImage) {
- const timeMultiplier = STATE.resImg === '2k' ? 1.5 : 1.0;
- const expectedMs = Math.max(3000, STATE.imgBaseMs * batchCount * timeMultiplier);
- fakeProgressTimer = setInterval(() => {
- if (taskMeta.longStatus && /Req|Render|Busy/.test(taskMeta.longStatus)) {
- let elapsed = Date.now() - reqStartTime;
- updateTask({ shortStatus: taskMeta.shortStatus, longStatus: taskMeta.longStatus, color: taskMeta.color, progress: Math.min(95, Math.max(0, (elapsed / expectedMs) * 100)) });
- }
- }, 300);
- updateTask({ shortStatus: 'Req...', longStatus: 'Requesting...', color: '#fff', progress: 0 });
- }
- try {
- while (attempts < APP_CONFIG.TIMINGS.MAX_RETRY_ATTEMPTS && !success && STATE.generationToken === token) {
- try {
- const liveRefPrompt = (!isMulti && currentRefs.length === 1) ? (STATE.refPrompts[currentRefs[0].id] ?? null) : null, config = await getApiPayload(currentRefs.length > 0 ? currentRefs : null, liveRefPrompt), payload = config.payload;
- if (STATE.action === 'gen_image') payload.n = parseInt(STATE.batchSize || "1"); payload.prompt = parseDynamicPrompt(payload.prompt);
- reqStartTime = Date.now();
- const resultData = await fetchWithRetries(config.endpoint, payload, taskMeta.controller.signal, token, taskId, updateTask);
- if (STATE.generationToken !== token || !resultData) break;
- if (isImage) {
- if (resultData?.data && Array.isArray(resultData.data)) {
- updateTask({ shortStatus: 'Saving...', longStatus: 'Rendering Complete. Saving...', color: '#10b981', progress: 100 });
- const committedIds =[], committedItems =[], batchTimestampBase = Date.now();
- for (let i = 0; i < resultData.data.length; i++) {
- if (STATE.generationToken !== token) break;
- const imgObj = resultData.data[i], id = crypto.randomUUID(), blob = DomUtils.base64ToBlob(imgObj.b64_json.replace(/\s/g, ''), imgObj.mime_type || 'image/png');
- await OPFS.write(id, 'full', blob);
- const mediaData = await MediaUtils.processSource(blob, 'image', getThumbMaxDim(), getThumbQuality());
- if (mediaData.thumbBlob) await OPFS.write(id, 'thumb', mediaData.thumbBlob);
- const item = Store.commitNewItem({
- id, type: 'image', mimeType: blob.type, timestamp: Pipeline.makeBatchTimestamp(batchTimestampBase, i, resultData.data.length), prompt: Pipeline.normalizePrompt(imgObj.revised_prompt, Pipeline.normalizePrompt(payload.prompt)),
- filename: `Grok Img - ${makeTimestamp()} - ${i + 1}of${resultData.data.length}.png`,
- ratio: mediaData.ratio, originalUrl: null, downloaded: false, lqip: mediaData.lqip, isUploadedRef: false
- }, 'currentOrder', placeholderIds[i]);
- committedIds.push(id); committedItems.push(item);
- }
- if (committedIds.length > 0) { Store.prependHistory(committedIds); await enforceHistoryLimit(); if (STATE.autoDownload) committedItems.forEach((item, idx) => setTimeout(() => MediaUtils.downloadItem(item, true), 500 + (idx * 50))); }
- const unused = placeholderIds.slice(resultData.data.length); if (unused.length > 0) Store.clearCurrentTransient(unused);
- success = committedIds.length > 0;
- if (attempts === 0) {
- const timeMultiplier = STATE.resImg === '2k' ? 1.5 : 1.0;
- const actualBaseMs = (Date.now() - reqStartTime) / (batchCount * timeMultiplier);
- STATE.imgBaseMs = Math.max(1500, Math.min(10000, (STATE.imgBaseMs * 0.7) + (actualBaseMs * 0.3)));
- }
- } else throw new Error("Invalid response format");
- } else {
- const reqId = resultData.request_id; if (!reqId) throw new Error("No Request ID returned");
- STATE.pendingVideos = [...(STATE.pendingVideos || []).filter(v => v.reqId !== reqId), { reqId, prompt: Pipeline.normalizePrompt(payload.prompt), timestamp: Date.now() }]; persistPendingVideos();
- const videoUrl = await pollVideoTask(reqId, taskMeta.controller.signal, token, updateTask);
- STATE.pendingVideos = STATE.pendingVideos.filter(v => v.reqId !== reqId); persistPendingVideos();
- if (videoUrl && STATE.generationToken === token) {
- updateTask({ shortStatus: `Extracting poster...`, longStatus: `Extracting poster image...`, color: '#3b82f6', progress: 100 });
- let posterData = null; try { posterData = await MediaUtils.processSource(await fetchBlobCSP(videoUrl, '0-500000'), 'video', getThumbMaxDim(), getThumbQuality()); if (!posterData.thumbBlob) throw new Error('Partial extraction failed'); } catch (e) { try { posterData = await MediaUtils.processSource(await fetchBlobCSP(videoUrl, null), 'video', getThumbMaxDim(), getThumbQuality()); } catch (err) {} }
- const id = crypto.randomUUID(); if (posterData && posterData.thumbBlob) await OPFS.write(id, 'thumb', posterData.thumbBlob);
- const item = Store.commitNewItem({
- id, type: 'video', mimeType: 'video/mp4', prompt: Pipeline.normalizePrompt(payload.prompt), filename: `Grok Video - ${makeTimestamp()}.mp4`, ratio: posterData?.ratio || 16/9, originalUrl: videoUrl, downloaded: false, lqip: posterData?.lqip || null, isUploadedRef: false
- }, 'currentOrder', placeholderIds[0]);
- Store.prependHistory([id]); await enforceHistoryLimit(); if (STATE.autoDownload) setTimeout(() => MediaUtils.downloadItem(item, true), 500);
- success = true;
- }
- }
- if (success) { STATE.successCount++; if (STATE.successCount >= 3) { STATE.successCount = 0; nuclearWipe(); } }
- } catch (err) {
- if (err.name === 'AbortError' || STATE.generationToken !== token) {
- taskMeta.shortStatus = "Cancelled"; taskMeta.longStatus = "Cancelled";
- updateTask({ shortStatus: "Cancelled", longStatus: "Cancelled", color: 'var(--danger)' });
- } else {
- let errMsg = err.message || "Unknown error";
- if (err.moderation) {
- taskMeta.shortStatus = 'Blocked';
- taskMeta.longStatus = `Moderation Blocked (${errMsg})`;
- updateTask({ shortStatus: taskMeta.shortStatus, longStatus: taskMeta.longStatus, color: '#a1a1aa' });
- addLog('WARN', `Task Blocked: ${errMsg}`, err);
- } else if (err.invalidRequest) {
- taskMeta.shortStatus = 'Invalid';
- taskMeta.longStatus = `Invalid Request (${errMsg})`;
- updateTask({ shortStatus: taskMeta.shortStatus, longStatus: taskMeta.longStatus, color: '#f59e0b' });
- addLog('WARN', `Task Invalid: ${errMsg}`, err);
- } else {
- taskMeta.shortStatus = `Failed (${errMsg.substring(0, 12)})`;
- taskMeta.longStatus = `Failed (${errMsg})`;
- updateTask({ shortStatus: taskMeta.shortStatus, longStatus: taskMeta.longStatus, color: 'var(--danger)' });
- addLog('ERROR', `Task Failed: ${errMsg}`, err);
- }
- }
- break;
- }
- }
- } finally {
- if (fakeProgressTimer) clearInterval(fakeProgressTimer);
- if (!success) {
- setTimeout(() => {
- Store.clearCurrentTransient(placeholderIds);
- }, 5000);
- }
- }
- }
- function spawnTask(jobKey, refIds, isMulti, token) {
- let pObj = JobManager.progress.get(jobKey); if (pObj) pObj.active++; let tId = `task-${Date.now()}-${Math.random()}`;
- JobManager.tasks.set(tId, { controller: new AbortController(), refIds, shortStatus: 'Starting...', longStatus: 'Starting...', color: '#fff', isMulti, progress: 0 });
- updateStatusUI();
- let tProm = executeSingleTask(tId, refIds, isMulti, token).finally(() => { if (pObj) { pObj.active--; pObj.done++; } JobManager.tasks.delete(tId); JobManager.taskPromises.delete(tProm); updateStatusUI(); });
- JobManager.taskPromises.add(tProm);
- }
- async function processQueue(token) {
- STATE.successCount = 0; const getActiveCount = () => JobManager.tasks.size; let endedNaturally = false;
- while (STATE.isGenerating && STATE.generationToken === token) {
- const limit = parseInt(STATE.concurrentSessions) || 1, liveLoops = (parseInt(STATE.loops) || 0) + 1, refs = getEligibleRefs().eligible.filter(m => m.active), now = Date.now(); let spawned = false;
- const spawnTaskIfAllowed = (jobKey, refIds, isMulti) => { if (getActiveCount() >= limit) return false; let p = JobManager.progress.get(jobKey); if (!p) { p = { done: 0, active: 0, needed: liveLoops }; JobManager.progress.set(jobKey, p); } else p.needed = liveLoops; if (p.done + p.active < p.needed) { spawnTask(jobKey, refIds, isMulti, token); return true; } return false; };
- if (refs.length === 0) { while (spawnTaskIfAllowed('NO_REF',['NO_REF'], false)) spawned = true; }
- else if (STATE.refMode === 'queue') {
- const pendingRefs = refs.filter(r => (r.readyAt || 0) <= now).sort((a, b) => { const pA = JobManager.progress.get(a.id) || {done:0, active:0}, pB = JobManager.progress.get(b.id) || {done:0, active:0}; return (pA.done + pA.active) - (pB.done + pB.active); });
- for (let r of pendingRefs) { if (!spawnTaskIfAllowed(r.id,[r.id], false)) break; spawned = true; }
- } else { if (!refs.some(r => (r.readyAt || 0) > now)) while (spawnTaskIfAllowed('MULTI', refs.map(r => r.id), true)) spawned = true; }
- const isDone =[...JobManager.progress.entries()].every(([k, p]) => { if (k === 'MULTI' || k === 'NO_REF' || refs.some(r => r.id === k)) return p.done >= p.needed; return true; });
- if (isDone && !refs.some(r => (r.readyAt || 0) > now) && getActiveCount() === 0) { endedNaturally = true; break; }
- if (!spawned) {
- const wakeVersion = queueWake.version;
- const promises = [...JobManager.taskPromises, queueWake.wait(wakeVersion)];
- if (promises.length === 1) promises.push(sleep(1000));
- await Promise.race(promises);
- } else await yieldToMain();
- }
- if (STATE.generationToken === token) {
- if (endedNaturally) TitleState.markDone();
- STATE.isGenerating = false;
- }
- }
- let _statusRenderHandle = 0;
- let _statusViewCacheKey = '';
- const createStatusShell = () => {
- const row = document.createElement('div');
- row.className = 'w-100 d-flex justify-end';
- return row;
- };
- const createStatusSummaryText = ({ doneT, totalT, activeT }) => `Tasks: ${Math.max(0, totalT - doneT)} (${activeT})`;
- const createStatusSummaryKey = ({ taskList, doneT, totalT, activeT, showTaskList }) =>[
- doneT,
- totalT,
- activeT,
- showTaskList ? 1 : 0,
- ...(showTaskList ? taskList.map(task => `${task.color || ''}|${getTaskSummaryText(task)}`) : [])
- ].join('\n');
- const createTaskSummaryChip = (task, index) => {
- const chip = document.createElement('span');
- chip.style.padding = '0 4px';
- chip.style.borderLeft = '1px solid rgba(255,255,255,0.1)';
- chip.style.whiteSpace = 'nowrap';
- if (task.color) chip.style.color = task.color;
- chip.textContent = `T${index + 1}: ${getTaskSummaryText(task)}`;
- return chip;
- };
- const buildStatusReadyView = () => {
- const row = createStatusShell();
- const text = document.createElement('span');
- text.className = 'text-muted';
- text.textContent = 'Ready';
- row.appendChild(text);
- return { key: 'ready', node: row };
- };
- const buildStatusSummaryView = ({ taskList, doneT, totalT, activeT, showTaskList }) => {
- const summaryTitle = 'Tasks: remaining (active)';
- const summaryText = createStatusSummaryText({ doneT, totalT, activeT });
- const row = createStatusShell();
- if (showTaskList) {
- row.classList.add('gap-2');
- const list = document.createElement('div');
- list.className = 'd-flex justify-end';
- list.style.cssText = 'flex:1; overflow:hidden; mask-image:linear-gradient(to right, transparent, black 15px);';
- taskList.forEach((task, index) => list.appendChild(createTaskSummaryChip(task, index)));
- row.appendChild(list);
- }
- const summary = document.createElement('span');
- summary.className = 'text-success';
- summary.style.cssText = 'font-weight:600; white-space:nowrap; flex-shrink:0;';
- summary.title = summaryTitle;
- summary.textContent = summaryText;
- row.appendChild(summary);
- return { key: createStatusSummaryKey({ taskList, doneT, totalT, activeT, showTaskList }), node: row };
- };
- const setStatusView = (view) => {
- if (!DOM.status || !view || _statusViewCacheKey === view.key) return;
- _statusViewCacheKey = view.key;
- DOM.status.replaceChildren(view.node);
- };
- const setDocumentTitle = (title) => {
- if (document.title !== title) document.title = title;
- };
- const getQueuePlanState = () => {
- const refs = Store.getRefItems(), now = Date.now(), { eligible, acceptedType } = getEligibleRefs(), eligibleIds = new Set(eligible.map(e => e.id)), liveLoops = (parseInt(STATE.loops) || 0) + 1;
- const plannedJobs = eligible.length === 0 ? liveLoops : (STATE.refMode === 'queue' ? eligible.length * liveLoops : liveLoops);
- return { refs, now, eligible, eligibleIds, acceptedType, liveLoops, plannedJobs };
- };
- const pruneJobProgress = (eligibleIds, taskList = []) => {
- const valid = new Set(['MULTI', 'NO_REF', ...eligibleIds]);
- taskList.forEach(task => {
- if (task.isMulti) valid.add('MULTI');
- else if (task.refIds[0] === 'NO_REF') valid.add('NO_REF');
- task.refIds.forEach(refId => valid.add(String(refId)));
- });
- [...JobManager.progress.keys()].forEach(key => {
- if (!valid.has(String(key))) JobManager.progress.delete(key);
- });
- };
- const getProgressKey = (refId) => STATE.refMode === 'queue' ? refId : 'MULTI';
- const clearReferenceCardRuntime = (card) => {
- card?.setStatus?.([]);
- card?.setProgress?.(null);
- };
- const getReferenceLabelState = (ref, idx, acceptedType, eligibleIds, progress, now) => {
- const baseLabel = `IMAGE_${idx}`;
- if (STATE.isGenerating && ref.active && eligibleIds.has(ref.id)) {
- if ((ref.readyAt || 0) > now) return { text: `${baseLabel} (Wait)` };
- if (progress) return { text: `${baseLabel} (${progress.done}/${progress.needed})` };
- return { text: baseLabel };
- }
- if (eligibleIds.has(ref.id) && ref.active) {
- if (STATE.refMode === 'multi') {
- return {
- text: `<IMAGE_${idx}>`,
- title: 'Insert tag',
- onClick: (e) => {
- e.preventDefault();
- e.stopPropagation();
- DomUtils.insertTextAtCursor(DOM.prompt, ` <IMAGE_${idx}> `);
- }
- };
- }
- return { text: baseLabel, title: 'Active Reference' };
- }
- return { text: baseLabel, title: ref.type !== acceptedType ? `Need ${acceptedType}` : 'Limit Reached' };
- };
- const getReferenceStatusLines = (ref, tasks, progress, now) => {
- if (!ref.active) return [];
- const isWait = (ref.readyAt || 0) > now;
- if (!progress && !isWait && tasks.length === 0) return [];
- const lines = [];
- if (isWait) lines.push({ text: 'Waiting...', color: '#9e9e9e' });
- tasks.forEach((task, index) => lines.push({ text: `[T${index + 1}] ${task.shortStatus}`, color: task.color }));
- return lines;
- };
- function seedQueueProgress() {
- const { eligible, liveLoops } = getQueuePlanState();
- JobManager.progress.clear();
- if (eligible.length === 0) JobManager.progress.set('NO_REF', { done: 0, active: 0, needed: liveLoops });
- else if (STATE.refMode === 'queue') eligible.forEach(ref => JobManager.progress.set(ref.id, { done: 0, active: 0, needed: liveLoops }));
- else JobManager.progress.set('MULTI', { done: 0, active: 0, needed: liveLoops });
- }
- function updateStatusUI() {
- if (!DOM.status) return;
- if (_statusRenderHandle) return;
- _statusRenderHandle = requestAnimationFrame(() => {
- _statusRenderHandle = 0;
- if (!DOM.status) return;
- const { refs, now, eligibleIds, acceptedType, plannedJobs } = getQueuePlanState();
- const refIndexMap = new Map(STATE.refOrder.map((id, index) => [id, index]));
- const activeT = JobManager.tasks.size, taskList = [...JobManager.tasks.values()], tasksByRef = new Map();
- pruneJobProgress([...eligibleIds], taskList);
- taskList.forEach(task => task.refIds.forEach(rid => {
- if (!tasksByRef.has(rid)) tasksByRef.set(rid, []);
- tasksByRef.get(rid).push(task);
- }));
- let queuedT = 0, done = 0, totalPlanned = 0;
- for (const [key, p] of JobManager.progress.entries()) {
- if (!(key === 'MULTI' || key === 'NO_REF' || eligibleIds.has(key))) continue;
- const activeForKey = Math.max(0, p.active || 0);
- const totalForKey = Math.max(p.needed || 0, (p.done || 0) + activeForKey);
- const doneForKey = Math.min(Math.max(0, p.done || 0), totalForKey);
- queuedT += Math.max(totalForKey - doneForKey - activeForKey, 0);
- done += doneForKey;
- totalPlanned += totalForKey;
- }
- totalPlanned = Math.max(totalPlanned, STATE.isGenerating ? plannedJobs : 0, done + activeT + queuedT);
- const doneT = Math.min(done, totalPlanned);
- const totalT = Math.max(totalPlanned, doneT + activeT);
- let activeRefCount = 0;
- refs.forEach(r => {
- const card = CardRegistry.get('reference', r.id); if (!card) return;
- if (r.active) activeRefCount++;
- const refTasks = tasksByRef.get(r.id) || [];
- const progress = JobManager.progress.get(getProgressKey(r.id));
- const idx = refIndexMap.get(r.id) ?? -1;
- const label = getReferenceLabelState(r, idx, acceptedType, eligibleIds, progress, now);
- const statusLines = getReferenceStatusLines(r, refTasks, progress, now);
- const isProcessing = refTasks.length > 0 || (STATE.isGenerating && r.active && eligibleIds.has(r.id) && (r.readyAt || 0) > now);
- card.classList.toggle('is-processing', isProcessing);
- card.setLabel(label.text, label.title || '', label.onClick || null);
- if (statusLines.length > 0) card.setStatus(statusLines);
- else clearReferenceCardRuntime(card);
- });
- if (!STATE.isGenerating) {
- setStatusView(buildStatusReadyView());
- TitleState.sync();
- return;
- }
- setStatusView(buildStatusSummaryView({ taskList, doneT, totalT, activeT, showTaskList: activeRefCount === 0 }));
- TitleState.sync(`${totalPlanned > 0 ? Math.floor((done / totalPlanned) * 100) : 0}%`);
- });
- }
- function toggleGenerateButton(isRunning) {
- const c = document.createElement('div');
- c.className = 'btn-content flex-center';
- if (isRunning) {
- DOM.generateBtn.classList.replace('xai-btn--run', 'xai-btn--stop');
- DOM.generateBtn.classList.add('is-running');
- c.textContent = '■';
- c.style.fontSize = '16px';
- } else {
- DOM.generateBtn.classList.replace('xai-btn--stop', 'xai-btn--run');
- if (!DOM.generateBtn.classList.contains('xai-btn--run')) DOM.generateBtn.classList.add('xai-btn--run');
- DOM.generateBtn.classList.remove('is-running');
- c.textContent = '↑';
- c.style.fontSize = '22px';
- }
- DOM.generateBtn.replaceChildren(c);
- }
- function handleGeneration() {
- if (STATE.activeRefPromptId) saveAndCloseRefPrompt(true);
- if (STATE.isGenerating) { STATE.generationToken++; JobManager.tasks.forEach(t => t.controller.abort()); JobManager.tasks.clear(); JobManager.taskPromises.clear(); JobManager.progress.clear(); STATE.isGenerating = false; return; }
- if (!sanitizePromptText(getStoredActionPrompt()) && sanitizePromptText(getDraftActionPrompt())) commitActionPrompt(STATE.action, getDraftActionPrompt());
- if (!sanitizePromptText(getTaskPrompt()) && Object.keys(STATE.refPrompts).length === 0 && Store.getRefItems().length === 0) return UI.showToast("Empty prompt", "error");
- const aRefs = Store.getRefItems().filter(m => m.active), dur = parseInt(STATE.duration) || 5;
- ActionUtils.getGenerationWarnings(STATE.action, aRefs, dur, STATE.currentAr).forEach(message => UI.showToast(message, "warn"));
- const staleTransientIds = STATE.currentOrder.filter(id => Pipeline.isTransientItem(STATE.items.get(id)));
- staleTransientIds.forEach(id => Store.removeItem(id));
- STATE.generationToken++;
- seedQueueProgress();
- NativeGridManager.clear();
- STATE.currentOrder =[];
- STATE.isGenerating = true;
- processQueue(STATE.generationToken).catch(err => { addLog('ERROR', 'Queue Error', err); STATE.isGenerating = false; });
- }
- function initAPIAndGeneration() {
- App.on(DOM.generateBtn, 'click', handleGeneration);
- App.on(DOM.reset, 'click', async () => {
- if (STATE.isGenerating) return UI.showToast("Stop generation first", "error");
- const choice = await UI.confirm("<div style='text-align:center; font-weight:bold; margin-bottom:8px; font-size:15px;'>Wipe Options:</div><div style='font-size:13px; line-height:1.5;'><b>[Cookie]</b> - Clears tokens/rate limits.<br><b>[Data]</b> - Clears history, keeps references.<br><b>[Full]</b> - Completely wipes all cache and DB.</div>");
- if (!choice || choice === 'cancel') return;
- if (choice === 'data') {
- const keepIds = new Set(STATE.refOrder), idsToDelete =[];
- STATE.items.forEach((_, id) => { if (!keepIds.has(id)) idsToDelete.push(id); });
- await StorageDB.deleteItemsBulk(idsToDelete); idsToDelete.forEach(id => Store.removeItem(id));
- STATE.historyOrder =[]; STATE.currentOrder = STATE.currentOrder.filter(id => keepIds.has(id));
- NativeGridManager.update(STATE.currentOrder, STATE.items, 'current'); DOM.historyScroller.update(STATE.historyOrder, STATE.items);
- runBg(updateHistoryMetrics); return UI.showToast("History data cleared (Refs kept)", "success");
- }
- if (choice === 'full') {
- STATE.items.forEach((_, id) => Store.removeItem(id));
- PromptSync.clear();
- STATE.refPrompts = {};
- persistRefPrompts();
- DBQueue.clearPending();
- clearTimeout(_configSaveTimer);
- await StorageDB.clear();
- localStorage.removeItem(APP_CONFIG.STORAGE.CONFIG_KEY);
- }
- lastWipeTime = 0; wipePromise = null; await nuclearWipe(); location.reload();
- });
- }
- if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', checkRoute); else checkRoute();
- })();
Add Comment
Please, Sign In to add comment