Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // ==UserScript==
- // @name xAI Overkill UI (v0.02.11.1)
- // @version 0.02.11.1
- // @description xAI Overkill UI
- // @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 = {
- MEDIA: {
- DEFAULT_VOLUME: 0.05,
- THUMB_MAX_DIM: 600,
- THUMB_WEBP_QUALITY: 0.65,
- JPEG_EXPORT_QUALITY: 0.95
- },
- VOICES:['eve', 'ara', 'rex', 'sal', 'leo'],
- LANG_CODES:['auto', 'en', 'ru', 'zh', 'fr', 'de', 'es-MX', 'es-ES', 'pt-BR', 'pt-PT', 'ja', 'ko', 'hi', 'ar-EG', 'ar-SA', 'ar-AE', 'bn', 'id', 'it', 'tr', 'vi'],
- TEXT_MODELS:['grok-4.20', 'grok-4.20-multi-agent', 'grok-4.20-non-reasoning', 'grok-4-1-fast', 'grok-4-1-fast-non-reasoning'],
- REASONING_EFFORTS:[{ val: 'none', label: 'None' }, { val: 'low', label: 'Low' }, { val: 'medium', label: 'Medium' }, { val: 'high', label: 'High' }],
- TTS_QUALITIES:[
- { label: '64k', val: '64000', bitRate: 64000 },
- { label: '128k', val: '128000', bitRate: 128000 },
- { label: '192k', val: '192000', bitRate: 192000 }
- ],
- TTS_TAGS:{
- instant:['pause', 'long-pause', 'hum-tune', 'laugh', 'chuckle', 'giggle', 'cry', 'tsk', 'tongue-click', 'lip-smack', 'breath', 'inhale', 'exhale', 'sigh'],
- wrapping:['soft', 'whisper', 'loud', 'build-intensity', 'decrease-intensity', 'higher-pitch', 'lower-pitch', 'slow', 'fast', 'sing-song', 'singing', 'laugh-speak', 'emphasis']
- },
- LIMITS: { TTS_TEXT: 15000, HISTORY_MEASURE_EPSILON: 4, MAX_CONCURRENT_SESSIONS: 2, MAX_REFS_UI: 99, MAX_HISTORY: 2000, API_MAX_MULTI_IMG: 5, API_MAX_MULTI_VID: 7, API_MAX_MULTI_EDIT: 1, API_MAX_MULTI_TEXT: 5 },
- 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',
- CURRENT_PANEL_SESSION_KEY: 'xai_overkill_ui_current_panel_session',
- EXPORT_SCHEMA_VERSION: 1,
- DB_NAME: 'XAI_OVERKILL_UI',
- DB_VERSION: 2,
- STORE_META: 'history_meta',
- STORE_DATA: 'app_state',
- OPFS_DIR: 'xai_overkill_ui_media'
- },
- 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: '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: [
- { spot: '#223140', mid: '#17222D', bg: '#070809' },
- { spot: '#2A2D43', mid: '#1D2030', bg: '#070809' },
- { spot: '#263247', mid: '#1B2433', bg: '#070809' },
- { spot: '#2D2833', mid: '#201B25', bg: '#070809' },
- { spot: '#1F2F2A', mid: '#16221E', bg: '#070809' },
- { spot: '#2F2926', mid: '#221D1A', bg: '#070809' },
- { spot: '#242A31', mid: '#191E24', bg: '#070809' },
- { spot: '#1C3135', mid: '#142328', bg: '#070809' },
- { spot: '#24302C', mid: '#19221F', bg: '#070809' },
- { 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%" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/><path d="M12 15V3"/></svg>'
- }
- };
- ['ImageDecoder', 'VideoFrame', 'ImageBitmap'].forEach(api => {
- if (typeof globalThis[api] !== 'undefined' && !globalThis[api].prototype[Symbol.dispose]) {
- globalThis[api].prototype[Symbol.dispose] = function() { this.close(); };
- }
- });
- class DisposableURL {
- constructor(blob) { this.url = URL.createObjectURL(blob); }
- [Symbol.dispose]() { URL.revokeObjectURL(this.url); }
- }
- const NoOpDisposable = { [Symbol.dispose]() {} };
- class ReactiveVar {
- constructor(initVal) {
- this.v = initVal;
- this.subs = new Set();
- }
- get value() { return this.v; }
- set value(newVal) {
- if (this.v === newVal) return;
- this.v = newVal;
- for (const cb of this.subs) cb(newVal);
- }
- bind(callback, signal = null) {
- this.subs.add(callback);
- callback(this.v);
- if (signal) {
- if (signal.aborted) return this.subs.delete(callback);
- signal.addEventListener('abort', () => this.subs.delete(callback), { once: true });
- }
- }
- }
- function createModernTemplate(htmlString) {
- const tpl = document.createElement('template');
- tpl.innerHTML = htmlString;
- const bindings = [];
- const getDOMPath = (targetNode, root) => {
- const path = [];
- let curr = targetNode;
- while (curr !== root) {
- let index = 0, sibling = curr.previousSibling;
- while (sibling) { index++; sibling = sibling.previousSibling; }
- path.push(index);
- curr = curr.parentNode;
- }
- return path.reverse();
- };
- const walker = document.createTreeWalker(tpl.content, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT);
- let node;
- while ((node = walker.nextNode())) {
- if (node.nodeType === 3) {
- const match = node.data.match(/^\{\{\s*(\w+)\s*\}\}$/);
- if (match) {
- node.data = '';
- bindings.push({ key: match[1], path: getDOMPath(node, tpl.content), isAttr: false });
- }
- } else if (node.nodeType === 1) {
- for (const attr of node.attributes) {
- const match = attr.value.match(/^(.*)\{\{\s*(\w+)\s*\}\}(.*)$/);
- if (match) {
- const [, prefix, key, suffix] = match;
- bindings.push({ key, attrName: attr.name, path: getDOMPath(node, tpl.content), isAttr: true, prefix, suffix });
- attr.value = prefix + suffix;
- }
- }
- }
- }
- return {
- createInstance(initialVals = {}) {
- const clone = tpl.content.cloneNode(true);
- const partNodes = [];
- for (const b of bindings) {
- let target = clone;
- for (const idx of b.path) target = target.childNodes[idx];
- partNodes.push({ binding: b, target });
- }
- const instance = {
- content: clone,
- update(vals) {
- for (const { binding, target } of partNodes) {
- if (binding.key in vals) {
- if (binding.isAttr) target.setAttribute(binding.attrName, binding.prefix + (vals[binding.key] ?? '') + binding.suffix);
- else target.data = vals[binding.key] ?? '';
- }
- }
- }
- };
- instance.update(initialVals);
- return instance;
- }
- };
- }
- const LANG_DISPLAY_NAMES = new Intl.DisplayNames(['en'], { type: 'language' });
- const ASPECT_RATIO_MAP = new Map(APP_CONFIG.ASPECT_RATIOS.map(o => [o.label.trim(), o]));
- const TTS_QUALITY_MAP = new Map(APP_CONFIG.TTS_QUALITIES.map(q => [q.val, q]));
- const ASPECT_RATIO_POPOVER_LAYOUT = [
- ['Auto', 'Ref'],
- ['1:1', '1:1'],
- ['3:4', '4:3'],
- ['9:16', '16:9'],
- ['2:3', '3:2'],
- ['9:19.5', '19.5:9'],
- ['9:20', '20:9'],
- ['1:2', '2:1']
- ];
- const createRangeOptions = (min, max) => Array.from({ length: Math.max(0, max - min + 1) }, (_, idx) => {
- const value = String(min + idx);
- return { val: value, text: value };
- });
- const getDefaultCardAction = ({ itemType, mode = 'edit' } = {}) => {
- if (mode === 'extend') return 'extend_video';
- if (mode === 'video') return 'gen_video';
- return itemType === 'video' ? 'edit_video' : 'gen_image';
- };
- const BaseActionSpec = {
- maxRefs: () => APP_CONFIG.LIMITS.MAX_REFS_UI,
- uploadTransition: ({ mimeType = '', currentAction }) => mimeType.startsWith('video/') ? 'edit_video' : currentAction,
- cardAction: getDefaultCardAction
- };
- const ACTION_SPEC = {
- gen_image: {
- ...BaseActionSpec,
- label: '▨ Image',
- title: 'Image Generation',
- model: 'grok-imagine-image',
- mediaType: 'image',
- refKind: 'image',
- durationMax: 15,
- activeUI:['currentAr', 'resImg', 'batchSize', 'loops'],
- endpoint: ({ refCount = 0 }) => refCount ? '/v1/images/edits' : '/v1/images/generations',
- apiLimit: APP_CONFIG.LIMITS.API_MAX_MULTI_IMG,
- warningPolicy: ({ refs =[], refMode = 'multi' }) =>
- (refMode === 'multi' && refs.length > APP_CONFIG.LIMITS.API_MAX_MULTI_IMG)
- ?[`API Limit: Only the first ${APP_CONFIG.LIMITS.API_MAX_MULTI_IMG} references will be used for image generation.`]
- :[],
- buildPayload: ({ payload, currentRefs, refBase64s, aspectRatio, settings }) => buildMediaPayload('image', payload, currentRefs, refBase64s, aspectRatio, settings)
- },
- gen_video: {
- ...BaseActionSpec,
- label: '🎞\uFE0E Video',
- title: 'Video Generation',
- model: 'grok-imagine-video',
- mediaType: 'video',
- refKind: 'image',
- durationMax: 15,
- activeUI:['currentAr', 'resVid', 'duration', 'loops'],
- endpoint: () => '/v1/videos/generations',
- apiLimit: APP_CONFIG.LIMITS.API_MAX_MULTI_VID,
- warningPolicy: ({ refs =[], duration = 5, aspectRatio = 'Auto', refMode = 'multi' }) => {
- 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 && refMode === 'multi') warnings.push('>10s video uses only the first reference.');
- }
- if (refMode === 'multi' && refs.length > APP_CONFIG.LIMITS.API_MAX_MULTI_VID && duration <= 10)
- warnings.push(`API Limit: Only the first ${APP_CONFIG.LIMITS.API_MAX_MULTI_VID} references will be used for video generation.`);
- return warnings;
- },
- buildPayload: ({ payload, currentRefs, refBase64s, aspectRatio, settings }) => buildMediaPayload('video', payload, currentRefs, refBase64s, aspectRatio, settings)
- },
- edit_video: {
- ...BaseActionSpec,
- label: '✄ Edit',
- title: 'Edit Video Ref',
- model: 'grok-imagine-video',
- mediaType: 'video',
- refKind: 'video',
- durationMax: 15,
- activeUI: ['loops'],
- endpoint: () => '/v1/videos/edits',
- apiLimit: APP_CONFIG.LIMITS.API_MAX_MULTI_EDIT,
- uploadTransition: ({ mimeType = '', currentAction }) => mimeType.startsWith('video/') ? currentAction : 'gen_image',
- warningPolicy: ({ refs =[], refMode = 'multi' }) =>
- (refMode === 'multi' && refs.length > APP_CONFIG.LIMITS.API_MAX_MULTI_EDIT)
- ?[`API Limit: Only the first ${APP_CONFIG.LIMITS.API_MAX_MULTI_EDIT} video reference will be used.`]
- :[],
- buildPayload: ({ action, payload, currentRefs, refBase64s, settings }) => buildVideoEditPayload(action, payload, currentRefs, refBase64s, settings)
- },
- extend_video: {
- ...BaseActionSpec,
- label: '◷ Extend',
- title: 'Extend Video Ref',
- model: 'grok-imagine-video',
- mediaType: 'video',
- refKind: 'video',
- durationMax: 10,
- activeUI: ['duration', 'loops'],
- endpoint: () => '/v1/videos/extensions',
- apiLimit: APP_CONFIG.LIMITS.API_MAX_MULTI_EDIT,
- uploadTransition: ({ mimeType = '', currentAction }) => mimeType.startsWith('video/') ? currentAction : 'gen_image',
- warningPolicy: ({ refs =[], refMode = 'multi' }) =>
- (refMode === 'multi' && refs.length > APP_CONFIG.LIMITS.API_MAX_MULTI_EDIT)
- ?[`API Limit: Only the first ${APP_CONFIG.LIMITS.API_MAX_MULTI_EDIT} video reference will be used.`]
- :[],
- buildPayload: ({ action, payload, currentRefs, refBase64s, settings }) => buildVideoEditPayload(action, payload, currentRefs, refBase64s, settings)
- },
- gen_audio: {
- ...BaseActionSpec,
- label: '🔊\uFE0E Audio',
- title: 'Text to Speech',
- model: 'tts-1',
- mediaType: 'audio',
- refKind: 'none',
- durationMax: 15,
- activeUI:['voice', 'language', 'ttsQuality', 'ttsEffects'],
- endpoint: () => '/v1/tts',
- maxRefs: () => 0,
- apiLimit: 0,
- warningPolicy: ({ action }) => {
- const textLen = getTaskPrompt(action).length;
- return textLen > APP_CONFIG.LIMITS.TTS_TEXT * 0.8
- ? [`Text is ${textLen}/${APP_CONFIG.LIMITS.TTS_TEXT} chars. Near the limit.`]
- : [];
- },
- buildPayload: ({ payload, settings }) => buildAudioPayload(payload, settings)
- },
- gen_text: {
- ...BaseActionSpec,
- label: '💬 Text',
- title: 'Text Generation',
- model: 'grok-4.20',
- mediaType: 'text',
- refKind: 'image',
- activeUI:['textModel', 'textReasoningEffort', 'textWebSearch', 'textXSearch'],
- endpoint: () => '/v1/responses',
- apiLimit: APP_CONFIG.LIMITS.API_MAX_MULTI_TEXT,
- warningPolicy: ({ refs =[], refMode = 'multi' }) =>
- (refMode === 'multi' && refs.length > APP_CONFIG.LIMITS.API_MAX_MULTI_TEXT)
- ?[`API Limit: Only the first ${APP_CONFIG.LIMITS.API_MAX_MULTI_TEXT} references will be used for text generation.`]
- :[],
- buildPayload: ({ payload, currentRefs, refBase64s, settings, textChatMessages = [] }) => {
- const model = settings.textModel || 'grok-4.20';
- payload.model = model;
- if (isMultiAgentTextModel(model)) payload.reasoning = { effort: settings.textReasoningEffort || 'high' };
- else delete payload.reasoning;
- const tools = buildEnabledTextTools(model, settings);
- if (tools.length > 0) payload.tools = tools;
- else delete payload.tools;
- const promptText = payload.prompt || 'Hello';
- const rawMessages =[];
- if (Array.isArray(textChatMessages) && textChatMessages.length > 0) {
- textChatMessages.forEach(message => {
- if (!message?.text) return;
- rawMessages.push({ role: message.role === 'assistant' ? 'assistant' : 'user', contentText: message.text, isCurrent: false });
- });
- }
- rawMessages.push({ role: 'user', contentText: promptText, isCurrent: true });
- const truncateTextToBytes = (text, maxBytes) => {
- const encoded = new TextEncoder().encode(text);
- if (encoded.byteLength <= maxBytes) return text;
- return new TextDecoder('utf-8', { fatal: false }).decode(encoded.subarray(0, maxBytes)).replace(/\uFFFD/g, '');
- };
- const MAX_TEXT_BYTES = 512000;
- let runningBytes = 0;
- const finalInput =[];
- for (let i = rawMessages.length - 1; i >= 0; i--) {
- const msg = rawMessages[i];
- const msgBytes = new TextEncoder().encode(msg.contentText).length;
- if (runningBytes + msgBytes <= MAX_TEXT_BYTES) {
- runningBytes += msgBytes;
- if (msg.isCurrent) {
- finalInput.unshift({ role: msg.role, content: buildResponsesUserContent(msg.contentText, refBase64s) });
- } else {
- finalInput.unshift({ role: msg.role, content: buildResponsesTextContent(msg.contentText) });
- }
- } else {
- const remainingBytes = MAX_TEXT_BYTES - runningBytes;
- if (remainingBytes > 0) {
- const truncatedText = truncateTextToBytes(msg.contentText, remainingBytes);
- if (msg.isCurrent) {
- finalInput.unshift({ role: msg.role, content: buildResponsesUserContent(truncatedText, refBase64s) });
- } else {
- finalInput.unshift({ role: msg.role, content: buildResponsesTextContent(truncatedText) });
- }
- }
- break;
- }
- }
- payload.stream = true;
- payload.store = false;
- payload.input = finalInput;
- delete payload.prompt;
- }
- }
- };
- const PANEL_ACTION_KEYS = Object.keys(ACTION_SPEC);
- const PANEL_STORE_SCHEMA_VERSION = 2;
- const DEFAULT_PROMPTS = Object.fromEntries(PANEL_ACTION_KEYS.map(k => [k, '']));
- const DEFAULT_FAV_PROMPTS = Object.fromEntries(PANEL_ACTION_KEYS.map(k => [k, []]));
- const ALLOWED_VIDEO_ARS = new Set(['1:1', '16:9', '9:16', '4:3', '3:4', '3:2', '2:3']);
- const CONFIG_SCHEMA = {
- action: { type: 'enum', default: 'gen_image', options: () => Object.keys(ACTION_SPEC) },
- currentAr: { type: 'enum', default: 'Auto', options: () => APP_CONFIG.ASPECT_RATIOS.map(({ label }) => label.trim()), normalize: value => typeof value === 'string' ? value.trim() : value },
- batchSize: { type: 'intString', default: '2', min: 1, max: 10 },
- duration: { type: 'intString', default: '5', min: 1, max: ({ out, fallback }) => (ACTION_SPEC[out.action || fallback?.action || 'gen_image'] ?? ACTION_SPEC.gen_image).durationMax || 15 },
- loops: { type: 'intString', default: '0', min: 0, max: 99 },
- loopsGenImage: { type: 'intString', default: ({ source }) => source.loops ?? '0', min: 0, max: 99 },
- loopsGenVideo: { type: 'intString', default: ({ source }) => source.loops ?? '0', min: 0, max: 99 },
- loopsEditVideo: { type: 'intString', default: ({ source }) => source.loops ?? '0', min: 0, max: 99 },
- loopsExtendVideo: { type: 'intString', default: ({ source }) => source.loops ?? '0', min: 0, max: 99 },
- resImg: { type: 'enum', default: '2k', options: ['1k', '2k'] },
- resVid: { type: 'enum', default: '720p', options: ['480p', '720p'] },
- refMode: { type: 'enum', default: 'multi', options: ['multi', 'queue'] },
- concurrentSessions: { type: 'int', default: 1, min: 1, max: APP_CONFIG.LIMITS.MAX_CONCURRENT_SESSIONS },
- rateLimitDelay: { type: 'int', default: 60, min: 0, max: 300 },
- retryMinMs: { type: 'int', default: 2000, min: 500, max: 60000 },
- retryMaxMs: { type: 'int', default: 4000, min: ({ out, fallback }) => out.retryMinMs || fallback?.retryMinMs || 500, max: 60000 },
- autoRetryVideo: { type: 'bool', default: true },
- referenceARTrick: { type: 'bool', default: false },
- ecoMode: { type: 'bool', default: false },
- autoDownload: { type: 'bool', default: false },
- embedImagePrompt: { type: 'bool', default: ({ source }) => source.embedImagePrompt ?? source.jpegPromptXmp ?? true },
- optimize2k: { type: 'bool', default: false },
- leftCollapsed: { type: 'bool', default: false },
- rightCollapsed: { type: 'bool', default: false },
- leftWidth: { type: 'int', default: 260, min: 260, max: 2000 },
- rightWidth: { type: 'int', default: 320, min: 240, max: 2000 },
- imgBaseMs: { type: 'int', default: 3000, min: 1500, max: 10000 },
- voice: { type: 'enum', default: 'eve', options: () => APP_CONFIG.VOICES },
- language: { type: 'enum', default: 'auto', options: () => APP_CONFIG.LANG_CODES },
- ttsQuality: { type: 'enum', default: '128000', options: () => APP_CONFIG.TTS_QUALITIES.map(({ val }) => val) },
- textModel: { type: 'enum', default: 'grok-4.20', options: () => APP_CONFIG.TEXT_MODELS },
- textReasoningEffort: { type: 'enum', default: 'medium', options: ['low', 'medium', 'high', 'xhigh'] },
- textWebSearch: { type: 'enum', default: 'on', options: ['off', 'on'] },
- textXSearch: { type: 'enum', default: 'on', options: ['off', 'on'] },
- customTextModels: { type: 'any', default:[] }
- };
- const DOM_IDS = {
- app: 'xai-pro-app',
- prompt: 'xai-api-prompt',
- textareaWrapper: 'textarea-wrapper',
- promptContainer: 'prompt-container-main',
- chatView: 'xai-chat-container',
- chatScroll: 'chat-scroll',
- 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',
- embedImagePromptCheckbox: 'setting-embed-image-prompt',
- optimize2kCheckbox: 'setting-optimize-2k',
- labelMasterAction: 'label-master-action',
- labelSettingDuration: 'label-setting-duration',
- popoverDuration: 'popover-duration',
- popoverAr: 'popover-ar',
- 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',
- promptUpdateLabel: 'prompt-update-label',
- settingVoice: 'btn-setting-voice',
- popoverVoice: 'popover-voice',
- labelSettingVoice: 'label-setting-voice',
- settingLanguage: 'btn-setting-language',
- popoverLanguage: 'popover-language',
- labelSettingLanguage: 'label-setting-language',
- settingTtsQuality: 'btn-setting-tts-quality',
- popoverTtsQuality: 'popover-tts-quality',
- labelSettingTtsQuality: 'label-setting-tts-quality',
- btnTtsEffects: 'btn-tts-effects',
- popoverTtsEffects: 'popover-tts-effects',
- settingTextModel: 'btn-setting-text-model',
- popoverTextModel: 'popover-text-model',
- labelSettingTextModel: 'label-setting-text-model',
- textWebSearchToggle: 'btn-setting-text-web-search',
- textXSearchToggle: 'btn-setting-text-x-search',
- settingReasoning: 'btn-setting-reasoning',
- popoverReasoning: 'popover-reasoning',
- labelSettingReasoning: 'label-setting-reasoning'
- };
- const SETTINGS_CONTROL_SCHEMA = [
- { id: 'autoDownload', kind: 'toggle', rowId: 'settings-auto-download-toggle', inputId: 'setting-auto-download', domKey: 'autoDownloadCheckbox', stateKey: 'autoDownload', label: 'Auto-Download Media', title: 'Save completed media to disk automatically' },
- { id: 'embedImagePrompt', kind: 'toggle', rowId: 'settings-embed-image-prompt-toggle', inputId: 'setting-embed-image-prompt', domKey: 'embedImagePromptCheckbox', stateKey: 'embedImagePrompt', label: 'Embed Prompt in Image', title: 'Write the prompt into metadata for all downloaded images (JPEG XMP or PNG iTXt)' },
- { id: 'optimize2k', kind: 'toggle', rowId: 'settings-optimize-2k-toggle', inputId: 'setting-optimize-2k', domKey: 'optimize2kCheckbox', stateKey: 'optimize2k', label: 'Convert 2K Images to JPEG', title: 'Convert 2K PNG to high-quality JPEG (95%) to save space and enable XMP prompts' },
- { id: 'autoRetryVideo', kind: 'toggle', 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' },
- { id: 'referenceARTrick', kind: 'toggle', 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' },
- { id: 'concurrentSessions', kind: 'number', 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 },
- { id: 'rateLimitDelay', kind: 'number', 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 },
- { id: 'retryMinMs', kind: 'number', 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 },
- { id: 'retryMaxMs', kind: 'number', 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 },
- { id: 'ecoMode', kind: 'toggle', 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_POPOVER_LAYOUT = [
- ['autoDownload', 'embedImagePrompt', 'optimize2k', 'autoRetryVideo', 'referenceARTrick'],
- ['concurrentSessions', 'rateLimitDelay'],
- ['retryMinMs', 'retryMaxMs'],
- ['ecoMode'],
- ['__import_export__']
- ];
- const UI_CONTROL_SCHEMA = [
- {
- id: 'action',
- kind: 'dropdown',
- domKey: 'actionDropdown',
- buttonId: 'btn-master-action',
- popoverId: 'popover-action',
- labelId: 'label-master-action',
- stateKey: 'action',
- title: 'Select Generation Action',
- defaultValue: 'gen_image',
- defaultLabel: '▨ Image',
- options: () => Object.entries(ACTION_SPEC).map(([val, meta]) => ({ val, text: meta.label, title: meta.title }))
- },
- {
- id: 'currentAr',
- kind: 'dropdown',
- domKey: 'settingAr',
- buttonId: 'btn-setting-ar',
- popoverId: 'popover-ar',
- stateKey: 'currentAr',
- title: 'Select Aspect Ratio',
- defaultValue: 'Auto',
- render: 'aspect-ratio'
- },
- {
- id: 'voice',
- kind: 'dropdown',
- domKey: 'settingVoice',
- buttonId: 'btn-setting-voice',
- popoverId: 'popover-voice',
- labelId: 'label-setting-voice',
- stateKey: 'voice',
- title: 'Select Voice',
- defaultValue: 'eve',
- defaultLabel: 'Eve',
- options: () => APP_CONFIG.VOICES.map(val => ({
- val,
- text: val.charAt(0).toUpperCase() + val.slice(1)
- }))
- },
- {
- id: 'language',
- kind: 'dropdown',
- domKey: 'settingLanguage',
- buttonId: 'btn-setting-language',
- popoverId: 'popover-language',
- labelId: 'label-setting-language',
- stateKey: 'language',
- title: 'Select Language',
- defaultValue: 'auto',
- defaultLabel: 'Auto',
- options: () => APP_CONFIG.LANG_CODES.map(val => ({
- val,
- text: val === 'auto' ? 'Auto' : LANG_DISPLAY_NAMES.of(val)
- }))
- },
- {
- id: 'ttsQuality',
- kind: 'dropdown',
- domKey: 'settingTtsQuality',
- buttonId: 'btn-setting-tts-quality',
- popoverId: 'popover-tts-quality',
- labelId: 'label-setting-tts-quality',
- stateKey: 'ttsQuality',
- title: 'Audio Quality',
- defaultValue: '128000',
- defaultLabel: '128k',
- options: () => APP_CONFIG.TTS_QUALITIES.map(({ val, label }) => ({ val, text: label }))
- },
- {
- id: 'ttsEffects',
- kind: 'button',
- domKey: 'btnTtsEffects',
- buttonId: 'btn-tts-effects',
- title: 'Speech Effects & Tags',
- text: 'Effects'
- },
- {
- id: 'textModel', kind: 'dropdown', domKey: 'settingTextModel', buttonId: 'btn-setting-text-model', popoverId: 'popover-text-model', labelId: 'label-setting-text-model', stateKey: 'textModel', title: 'Text Model', defaultValue: 'grok-4.20',
- options: () => {
- const models = APP_CONFIG.TEXT_MODELS.map(val => ({ val, text: val }));
- if (STATE.textModel && !APP_CONFIG.TEXT_MODELS.includes(STATE.textModel)) {
- models.push({ val: STATE.textModel, text: STATE.textModel });
- }
- return models;
- }
- },
- {
- id: 'textReasoningEffort',
- kind: 'toggle',
- domKey: 'settingReasoning',
- buttonId: 'btn-setting-reasoning',
- stateKey: 'textReasoningEffort',
- title: 'Reasoning effort',
- defaultValue: 'high',
- values: ['low', 'medium', 'high', 'xhigh'],
- labels: { 'low': 'Low', 'medium': 'Medium', 'high': 'High', 'xhigh': 'xHigh' },
- isVisible: () => isMultiAgentTextModel()
- },
- {
- id: 'textWebSearch',
- kind: 'toggle',
- domKey: 'textWebSearchToggle',
- buttonId: 'btn-setting-text-web-search',
- stateKey: 'textWebSearch',
- title: 'Built-in Web Search tool',
- defaultValue: 'on',
- values: ['off', 'on'],
- labels: { off: 'Web Off', on: 'Web On' },
- isVisible: () => canUseBuiltInTextTools()
- },
- {
- id: 'textXSearch',
- kind: 'toggle',
- domKey: 'textXSearchToggle',
- buttonId: 'btn-setting-text-x-search',
- stateKey: 'textXSearch',
- title: 'Built-in X Search tool',
- defaultValue: 'on',
- values: ['off', 'on'],
- labels: { off: 'X Off', on: 'X On' },
- isVisible: () => canUseBuiltInTextTools()
- },
- {
- id: 'resImg',
- kind: 'toggle',
- domKey: 'resImgToggle',
- buttonId: 'xai-api-res-img',
- stateKey: 'resImg',
- title: 'Toggle Image Resolution: 1K (JPEG), 2K (PNG)',
- defaultValue: '2k',
- values: ['2k', '1k'],
- labels: { '2k': '2K', '1k': '1K' }
- },
- {
- id: 'resVid',
- kind: 'toggle',
- domKey: 'resVidToggle',
- buttonId: 'xai-api-res-vid',
- stateKey: 'resVid',
- title: 'Toggle Video Resolution',
- defaultValue: '720p',
- values: ['720p', '480p'],
- labels: { '720p': '720p', '480p': '480p' }
- },
- {
- id: 'batchSize',
- kind: 'dropdown',
- domKey: 'settingBatch',
- buttonId: 'btn-setting-batch',
- popoverId: 'popover-batch',
- labelId: 'label-setting-batch',
- stateKey: 'batchSize',
- title: 'Images per batch',
- defaultValue: '2',
- defaultLabel: '2',
- options: () => createRangeOptions(1, 10)
- },
- {
- id: 'duration',
- kind: 'dropdown',
- domKey: 'settingDuration',
- buttonId: 'btn-setting-duration',
- popoverId: 'popover-duration',
- labelId: 'label-setting-duration',
- stateKey: 'duration',
- title: 'Video duration (seconds)',
- defaultValue: '5',
- defaultLabel: '5',
- options: () => []
- },
- {
- id: 'loops',
- kind: 'number-inline',
- domKey: 'settingLoops',
- wrapperId: 'btn-setting-loops',
- inputId: 'setting-loops',
- stateKey: 'loops',
- title: 'Auto repeat loops',
- defaultValue: '0',
- min: 0,
- max: 99,
- prefix: '↻'
- }
- ];
- const CONTROL_SCHEMA = [
- ...SETTINGS_CONTROL_SCHEMA.map(control => ({ scope: 'settings', ...control })),
- ...UI_CONTROL_SCHEMA.map(control => ({ scope: 'prompt', ...control }))
- ];
- const filterControls = (scope, kind = '') => CONTROL_SCHEMA.filter(control => control.scope === scope && (!kind || control.kind === kind));
- const CONTROL_BY_STATE_KEY = Object.fromEntries(CONTROL_SCHEMA.filter(({ stateKey }) => !!stateKey).map(control => [control.stateKey, control]));
- const getControlByStateKey = (stateKey, scope = '') => {
- const control = CONTROL_BY_STATE_KEY[String(stateKey || '')];
- return control && (!scope || control.scope === scope) ? control : null;
- };
- const SETTINGS_CONTROL_BY_ID = Object.fromEntries(filterControls('settings').map(control => [control.id, control]));
- const SETTINGS_TOGGLE_META = filterControls('settings', 'toggle');
- const SETTINGS_NUMBER_META = filterControls('settings', 'number');
- const UI_CONTROL_BY_ID = Object.fromEntries(filterControls('prompt').map(control => [control.id, control]));
- const UI_DROPDOWN_CONTROLS = filterControls('prompt', 'dropdown');
- const UI_TOGGLE_CONTROLS = filterControls('prompt', 'toggle');
- const HTTP_STATUS_CLASS = Object.fromEntries([
- ...[500, 502, 503].map(s => [s, 'retry']),
- ...[401, 403].map(s => [s, 'auth']),
- ...[422, 429].map(s => [s, 'rateLimit'])
- ]);
- const classifyHttpStatus = (status) => HTTP_STATUS_CLASS[status] ?? 'unknown';
- 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|text length|voice_id|language|codec|sample_rate|output_format/i;
- const addLog = (lvl, msg, data = null) => {
- try {
- const normalizedLevel = String(lvl || '').toUpperCase();
- const consoleMethod = normalizedLevel.includes('ERROR') ? 'error' : normalizedLevel.includes('WARN') ? 'warn' : 'log';
- console[consoleMethod](`[xAI][${lvl}] ${msg}`, data);
- } catch(e) { console.error('[xAI][LOGGER_PANIC]', e); }
- };
- const MEDIA_VOLUME_STORAGE_KEY = 'xai_overkill_ui_media_volume';
- const _range = document.createRange();
- const _fragment = _range.createContextualFragment.bind(_range);
- const createMarkupFragment = (markup = '') => _fragment(markup);
- const DATA_WORKER_CODE = `
- 'use strict';
- ['ImageDecoder', 'VideoFrame', 'ImageBitmap'].forEach(api => {
- if (typeof self[api] !== 'undefined' && !self[api].prototype[Symbol.dispose]) {
- self[api].prototype[Symbol.dispose] = function() { this.close(); };
- }
- });
- const UTF8_ENCODER = new TextEncoder();
- const UTF8_DECODER = new TextDecoder();
- const DB_NAME = 'XAI_OVERKILL_UI';
- const DB_VERSION = 2;
- const STORE_META = 'history_meta';
- const STORE_DATA = 'app_state';
- const ACTIVE_ABORTS = new Map();
- const DB = {
- db: null,
- async init() {
- if (this.db) return this.db;
- return new Promise((resolve, reject) => {
- const req = indexedDB.open(DB_NAME, DB_VERSION);
- req.onupgradeneeded = e => {
- const db = e.target.result;
- if (!db.objectStoreNames.contains(STORE_META)) db.createObjectStore(STORE_META, { keyPath: 'id' });
- if (!db.objectStoreNames.contains(STORE_DATA)) db.createObjectStore(STORE_DATA);
- };
- req.onsuccess = () => { this.db = req.result; resolve(this.db); };
- req.onerror = () => reject(req.error);
- });
- },
- async dbOp(stores, mode, operationFn) {
- const db = await this.init();
- return new Promise((resolve, reject) => {
- const tx = db.transaction(stores, mode);
- let result;
- tx.oncomplete = () => resolve(result);
- tx.onerror = () => reject(tx.error);
- tx.onabort = () => reject(tx.error || new Error('IDB Transaction aborted'));
- try {
- result = operationFn(tx);
- } catch (err) {
- tx.abort();
- reject(err);
- }
- });
- }
- };
- const OPFS = {
- dir: null,
- async init() {
- if (!this.dir) {
- const root = await navigator.storage.getDirectory();
- this.dir = await root.getDirectoryHandle('xai_overkill_ui_media', { create: true });
- }
- return this.dir;
- },
- async delete(id) {
- try {
- const dir = await this.init();
- await Promise.allSettled(['full', 'thumb', 'heavy'].map(type => dir.removeEntry(\`\${id}_\${type}\`).catch(()=>{})));
- } catch(e) {}
- }
- };
- const TaskQueue = {
- queue: Promise.resolve(),
- enqueue(fn) {
- this.queue = this.queue.then(fn).catch(err => console.error('[Worker Error]', err));
- return this.queue;
- }
- };
- const XMP = {
- PRE: UTF8_ENCODER.encode('<x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:description><rdf:Alt><rdf:li xml:lang="x-default">'),
- POST: UTF8_ENCODER.encode('</rdf:li></rdf:Alt></dc:description></rdf:Description></rdf:RDF></x:xmpmeta>'),
- SIG: UTF8_ENCODER.encode('http://ns.adobe.com/xap/1.0/\\0'),
- ESC: { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }
- };
- const PNG = {
- SIG: new Uint8Array([137,80,78,71,13,10,26,10]),
- ITXT_TYPE: new Uint8Array([105,84,88,116]),
- ITXT_PRE: new Uint8Array([112,97,114,97,109,101,116,101,114,115,0,0,0,0,0]),
- IHDR_END: 33,
- CRC_TAB: (() => {
- const tab = new Uint32Array(256);
- for (let i = 0; i < 256; i++) {
- let c = i;
- for (let k = 0; k < 8; k++) c = c & 1 ? 0xEDB88320 ^ (c >>> 1) : c >>> 1;
- tab[i] = c;
- }
- return tab;
- })(),
- crc32(buf, crc = 0xFFFFFFFF) {
- for (let i = 0, l = buf.length; i < l; i++) {
- crc = this.CRC_TAB[(crc ^ buf[i]) & 0xFF] ^ (crc >>> 8);
- }
- return crc;
- }
- };
- const POOL = {
- canvas: new OffscreenCanvas(1, 1),
- lqipCanvas: new OffscreenCanvas(2, 2)
- };
- POOL.ctx = POOL.canvas.getContext('2d', { alpha: false, desynchronized: true, willReadFrequently: true });
- POOL.lqipCtx = POOL.lqipCanvas.getContext('2d', { alpha: false, willReadFrequently: true });
- const extractLQIP = (videoFrameOrImage) => {
- POOL.lqipCtx.drawImage(videoFrameOrImage, 0, 0, 2, 2);
- const d = POOL.lqipCtx.getImageData(0, 0, 2, 2).data;
- return \`radial-gradient(ellipse at 30% 30%, rgb(\${d[0]} \${d[1]} \${d[2]}), rgb(\${d[12]} \${d[13]} \${d[14]}))\`;
- };
- const parseBlock = (block) => {
- if (!block.trim()) return null;
- let eventType = '';
- const dataLines = [];
- for (const line of block.split(/\\r?\\n/)) {
- if (!line || line.startsWith(':')) continue;
- if (line.startsWith('event:')) { eventType = line.slice(6).trim(); continue; }
- if (line.startsWith('data:')) dataLines.push(line.slice(5).trimStart());
- }
- if (!dataLines.length) return null;
- const raw = dataLines.join('\\n');
- if (raw === '[DONE]') return { type: 'done', done: true };
- try {
- const parsed = JSON.parse(raw);
- if (eventType && !parsed.type) parsed.type = eventType;
- return parsed;
- } catch {
- return { type: eventType || 'message', raw };
- }
- };
- async function* streamSSE(body, signal) {
- const textStream = body.pipeThrough(new TextDecoderStream());
- let buffer = '';
- try {
- for await (const chunk of textStream) {
- if (signal?.aborted) throw signal.reason;
- buffer += chunk;
- const blocks = buffer.split(/\\r?\\n\\r?\\n/);
- buffer = blocks.pop() || '';
- for (const block of blocks) {
- const event = parseBlock(block);
- if (event) yield event;
- }
- }
- if (buffer.trim()) {
- const event = parseBlock(buffer);
- if (event && !event.done) yield event;
- }
- } finally {
- textStream.cancel?.().catch?.(() => {});
- }
- }
- self.onmessage = (e) => {
- const { type, id, payload, reqId } = e.data;
- TaskQueue.enqueue(async () => {
- try {
- switch (type) {
- case 'ABORT_REQ': {
- const controller = ACTIVE_ABORTS.get(reqId);
- if (controller) {
- controller.abort(new DOMException('Aborted by Main Thread', 'AbortError'));
- ACTIVE_ABORTS.delete(reqId);
- }
- break;
- }
- case 'COMMIT_ITEMS': {
- const dir = await OPFS.init();
- const lightItems = [];
- await Promise.all(payload.items.map(async i => {
- const meta_light = {
- id: String(i.id),
- type: i.type || 'image',
- ratio: Number.isFinite(i.ratio) ? i.ratio : 1,
- lqip: i.lqip || null,
- timestamp: i.timestamp || Date.now(),
- isPending: !!i.isPending,
- filename: i.filename || '',
- isUploadedRef: !!i.isUploadedRef,
- shortPrompt: i.shortPrompt || '',
- mimeType: i.mimeType || '',
- downloaded: !!i.downloaded,
- originalUrl: i.originalUrl || null,
- textModel: i.textModel || null,
- textTurnCount: Number.isFinite(i.textTurnCount) ? i.textTurnCount : 0,
- reqId: i.reqId || null,
- taskId: i.taskId || null,
- refSourceId: i.refSourceId || null,
- _statusData: i._statusData || null
- };
- lightItems.push(meta_light);
- if (i.prompt || i.responseText || i.textChatMessages) {
- const heavyData = { id: String(i.id) };
- if (i.prompt) heavyData.prompt = i.prompt;
- if (i.responseText) heavyData.responseText = i.responseText;
- if (i.textChatMessages) heavyData.textChatMessages = i.textChatMessages;
- try {
- const fh = await dir.getFileHandle(\`\${i.id}_heavy\`, { create: true });
- const writable = await fh.createWritable({ keepExistingData: false });
- try {
- const jsonStream = new Response(JSON.stringify(heavyData)).body;
- await jsonStream.pipeThrough(new CompressionStream('deflate')).pipeTo(writable);
- } finally {
- await writable.close().catch(()=>{});
- }
- } catch (err) {
- console.error('[OPFS Heavy Write Error]', err);
- }
- }
- }));
- await DB.dbOp([STORE_META], 'readwrite', tx => {
- const mStore = tx.objectStore(STORE_META);
- lightItems.forEach(i => mStore.put(i));
- });
- self.postMessage({ reqId, status: 'ok' });
- break;
- }
- case 'DELETE_ITEMS': {
- const ids = payload.ids;
- await DB.dbOp([STORE_META, STORE_DATA], 'readwrite', tx => {
- const mStore = tx.objectStore(STORE_META);
- const dStore = tx.objectStore(STORE_DATA);
- ids.forEach(itemId => {
- mStore.delete(itemId);
- dStore.delete(itemId);
- dStore.delete(\`textSession:\${itemId}\`);
- });
- });
- for (const itemId of ids) await OPFS.delete(itemId);
- self.postMessage({ reqId, status: 'ok' });
- break;
- }
- case 'SAVE_META': {
- const jsonStream = new Response(JSON.stringify(payload.data)).body;
- const compressedStream = jsonStream.pipeThrough(new CompressionStream('deflate'));
- const compressedBlob = await new Response(compressedStream).blob();
- await DB.dbOp([STORE_DATA], 'readwrite', tx => {
- tx.objectStore(STORE_DATA).put(compressedBlob, payload.key);
- });
- self.postMessage({ reqId, status: 'ok' });
- break;
- }
- case 'LOAD_META': {
- const db = await DB.init();
- const req = db.transaction(STORE_DATA).objectStore(STORE_DATA).get(payload.key);
- req.onsuccess = async () => {
- let res = req.result ?? null;
- if (res instanceof Blob || res instanceof Uint8Array || res instanceof ArrayBuffer) {
- try {
- const stream = (res instanceof Blob ? res : new Blob([res])).stream();
- const decompressed = stream.pipeThrough(new DecompressionStream('deflate'));
- res = await new Response(decompressed).json();
- } catch(e) {
- res = null;
- }
- }
- self.postMessage({ reqId, payload: res });
- };
- req.onerror = () => self.postMessage({ reqId, payload: null });
- break;
- }
- case 'LOAD_ALL_HISTORY': {
- const db = await DB.init();
- const req = db.transaction(STORE_META).objectStore(STORE_META).getAll();
- req.onsuccess = async () => {
- const rawMetas = req.result || [];
- const processed = rawMetas.map(m => ({
- ...m,
- prompt: undefined,
- responseText: undefined,
- downloaded: Boolean(m.downloaded),
- lqip: m.lqip || null,
- isUploadedRef: Boolean(m.isUploadedRef),
- textModel: m.textModel || null,
- textTurnCount: Number.isFinite(m.textTurnCount) ? m.textTurnCount : 0
- })).sort((a, b) => b.timestamp - a.timestamp);
- self.postMessage({ reqId, payload: processed });
- };
- break;
- }
- case 'LOAD_HEAVY': {
- try {
- const dir = await OPFS.init();
- const fh = await dir.getFileHandle(\`\${payload.id}_heavy\`);
- const file = await fh.getFile();
- const decompressedStream = file.stream().pipeThrough(new DecompressionStream('deflate'));
- const raw = await new Response(decompressedStream).json();
- let textChatMessages = null;
- if (raw.textChatMessages) {
- textChatMessages = typeof raw.textChatMessages === 'string' ? JSON.parse(raw.textChatMessages) : raw.textChatMessages;
- }
- self.postMessage({ reqId, payload: { prompt: String(raw.prompt || ''), responseText: String(raw.responseText || ''), textChatMessages } });
- } catch (err) {
- self.postMessage({ reqId, payload: { prompt: '', responseText: '', textChatMessages: null } });
- }
- break;
- }
- case 'DELETE_META': {
- const keys = Array.isArray(payload.keys) ? payload.keys : [payload.keys];
- await DB.dbOp([STORE_DATA], 'readwrite', tx => {
- const store = tx.objectStore(STORE_DATA);
- keys.forEach(key => store.delete(key));
- });
- self.postMessage({ reqId, status: 'ok' });
- break;
- }
- case 'CLEAR_ALL': {
- const dir = await OPFS.init();
- const root = await navigator.storage.getDirectory();
- await root.removeEntry('xai_overkill_ui_media', { recursive: true }).catch(()=>{});
- OPFS.dir = null;
- await DB.dbOp([STORE_META, STORE_DATA], 'readwrite', tx => {
- tx.objectStore(STORE_META).clear();
- tx.objectStore(STORE_DATA).clear();
- });
- if (DB.db) { DB.db.close(); DB.db = null; }
- self.postMessage({ reqId, status: 'ok' });
- break;
- }
- case 'processApiResponse': {
- const dir = await OPFS.init();
- const { id, buffer, prompt, is2k, optimize2k, embedImagePrompt } = payload;
- let finalBytes = new Uint8Array(buffer);
- let mime = 'image/jpeg';
- if (finalBytes[0] === 0x89 && finalBytes[1] === 0x50 && finalBytes[2] === 0x4E && finalBytes[3] === 0x47) {
- mime = 'image/png';
- }
- if (!is2k) mime = 'image/jpeg';
- const needsConversion = (is2k && optimize2k && mime === 'image/png');
- let decodedBmp = null;
- try {
- if (needsConversion) {
- decodedBmp = await createImageBitmap(new Blob([finalBytes], { type: mime }));
- POOL.canvas.width = decodedBmp.width;
- POOL.canvas.height = decodedBmp.height;
- POOL.ctx.drawImage(decodedBmp, 0, 0);
- const jBlob = await POOL.canvas.convertToBlob({ type: 'image/jpeg', quality: ${APP_CONFIG.MEDIA.JPEG_EXPORT_QUALITY} });
- finalBytes = new Uint8Array(await jBlob.arrayBuffer());
- mime = 'image/jpeg';
- }
- let outputChunks = [finalBytes];
- let xmpEmbedded = false;
- const needsMeta = (embedImagePrompt && !!prompt);
- if (needsMeta) {
- if (mime === 'image/jpeg') {
- let txt = UTF8_ENCODER.encode(prompt.replace(/[&<>"']/g, m => XMP.ESC[m]));
- const MAX_TXT = 65533 - XMP.SIG.length - XMP.PRE.length - XMP.POST.length;
- if (txt.length > MAX_TXT) txt = txt.subarray(0, MAX_TXT);
- const sLen = XMP.SIG.length + XMP.PRE.length + txt.length + XMP.POST.length + 2;
- const app1Header = new Uint8Array([0xFF, 0xE1, (sLen >> 8) & 0xFF, sLen & 0xFF]);
- outputChunks =[
- finalBytes.subarray(0, 2),
- app1Header,
- XMP.SIG,
- XMP.PRE,
- txt,
- XMP.POST,
- finalBytes.subarray(2)
- ];
- xmpEmbedded = true;
- } else if (mime === 'image/png') {
- const textBytes = UTF8_ENCODER.encode(prompt);
- const bodyLen = PNG.ITXT_PRE.length + textBytes.length;
- const chunkHead = new Uint8Array(8);
- chunkHead[0] = bodyLen >>> 24; chunkHead[1] = bodyLen >>> 16 & 255; chunkHead[2] = bodyLen >>> 8 & 255; chunkHead[3] = bodyLen & 255;
- chunkHead.set(PNG.ITXT_TYPE, 4);
- let crc = PNG.crc32(PNG.ITXT_TYPE);
- crc = PNG.crc32(PNG.ITXT_PRE, crc);
- crc = PNG.crc32(textBytes, crc);
- crc = (crc ^ 0xFFFFFFFF) >>> 0;
- const chunkTail = new Uint8Array([crc >>> 24, crc >>> 16 & 255, crc >>> 8 & 255, crc & 255]);
- outputChunks =[
- finalBytes.subarray(0, PNG.IHDR_END),
- chunkHead,
- PNG.ITXT_PRE,
- textBytes,
- chunkTail,
- finalBytes.subarray(PNG.IHDR_END)
- ];
- xmpEmbedded = true;
- }
- }
- const fh = await dir.getFileHandle(id + '_full', { create: true });
- const writable = await fh.createWritable({ keepExistingData: false });
- try { await writable.write(new Blob(outputChunks, { type: mime })); }
- finally { await writable.close().catch(()=>{}); }
- if (!decodedBmp) {
- decodedBmp = await createImageBitmap(new Blob(outputChunks, { type: mime }));
- }
- const ratio = decodedBmp.width / decodedBmp.height;
- const targetW = Math.min(${APP_CONFIG.MEDIA.THUMB_MAX_DIM}, decodedBmp.width);
- const targetH = Math.max(1, Math.round(decodedBmp.height * (targetW / decodedBmp.width)));
- POOL.canvas.width = targetW;
- POOL.canvas.height = targetH;
- POOL.ctx.drawImage(decodedBmp, 0, 0, targetW, targetH);
- const lqip = extractLQIP(decodedBmp);
- const tBlob = await POOL.canvas.convertToBlob({ type: 'image/webp', quality: ${APP_CONFIG.MEDIA.THUMB_WEBP_QUALITY} });
- const tfh = await dir.getFileHandle(id + '_thumb', { create: true });
- const twritable = await tfh.createWritable({ keepExistingData: false });
- try { await twritable.write(tBlob); }
- finally { await twritable.close().catch(()=>{}); }
- const thumbBuffer = await tBlob.arrayBuffer();
- self.postMessage({ reqId, payload: {
- lqip, ratio, mimeType: mime,
- xmpEmbedded,
- thumbBuffer
- }}, [thumbBuffer]);
- } finally {
- if (decodedBmp) {
- decodedBmp.close();
- decodedBmp = null;
- }
- }
- break;
- }
- case 'extractImagePrompt': {
- const { buffer } = payload;
- const bytes = new Uint8Array(buffer);
- let result = null;
- if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4E && bytes[3] === 0x47) {
- let off = 8;
- while (off < bytes.length - 8) {
- const len = (bytes[off] << 24) | (bytes[off+1] << 16) | (bytes[off+2] << 8) | bytes[off+3];
- const type = UTF8_DECODER.decode(bytes.subarray(off+4, off+8));
- if (type === 'iTXt') {
- const chunkData = bytes.subarray(off+8, off+8+len);
- if (chunkData.length >= 15 && UTF8_DECODER.decode(chunkData.subarray(0, 10)) === 'parameters') {
- let textStart = 10;
- for (let nulls = 0; nulls < 5 && textStart < chunkData.length; textStart++) {
- if (chunkData[textStart] === 0) nulls++;
- }
- if (textStart < chunkData.length) {
- result = {
- isPromptCarrier: true,
- prompt: UTF8_DECODER.decode(chunkData.subarray(textStart))
- };
- break;
- }
- }
- }
- if (type === 'IEND') break;
- off += 12 + len;
- }
- } else {
- let off = 2;
- const limit = Math.min(bytes.length - 4, 65536);
- const SIG = XMP.SIG;
- while (off < limit) {
- if (bytes[off] === 0xFF && bytes[off + 1] === 0xE1) {
- const sLen = (bytes[off + 2] << 8) | bytes[off + 3];
- let sigMatch = true;
- for (let i = 0; i < SIG.length && sigMatch; i++) {
- if (bytes[off + 4 + i] !== SIG[i]) sigMatch = false;
- }
- if (sigMatch) {
- const xml = UTF8_DECODER.decode(bytes.subarray(off + 4 + SIG.length, off + 2 + sLen));
- const s = xml.indexOf('default">') + 9;
- const e = xml.indexOf('</rdf:li>');
- if (s > 8 && e > s) {
- result = {
- isPromptCarrier: true,
- prompt: xml.slice(s, e).replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, '&')
- };
- }
- }
- if (result) break;
- off += 2 + sLen;
- } else if (bytes[off] === 0xFF) {
- if (bytes[off + 1] === 0xDA) break;
- if (bytes[off + 1] >= 0xE0 && bytes[off + 1] <= 0xEF) {
- off += 2 + ((bytes[off + 2] << 8) | bytes[off + 3]);
- } else break;
- } else break;
- }
- }
- self.postMessage({ reqId, payload: result });
- break;
- }
- case 'LOAD_OPFS_MEDIA': {
- try {
- const dir = await OPFS.init();
- const fh = await dir.getFileHandle(\`\${payload.id}_\${payload.type}\`);
- const file = await fh.getFile();
- self.postMessage({ reqId, payload: { file, mimeType: file.type } });
- } catch (err) {
- self.postMessage({ reqId, payload: null });
- }
- break;
- }
- case 'EXTRACT_VIDEO_POSTER': {
- const { id } = payload;
- try {
- const dir = await OPFS.init();
- const file = await (await dir.getFileHandle(\`\${id}_full\`)).getFile();
- const headerBuf = await file.slice(0, 512 * 1024).arrayBuffer();
- const dv = new DataView(headerBuf);
- const len = headerBuf.byteLength - 8;
- let avcC, codecString, frameSize, frameOffset;
- for (let i = 0; i < len; i++) {
- const box = dv.getUint32(i);
- if (box === 0x61766343) {
- avcC = headerBuf.slice(i + 4, i - 4 + dv.getUint32(i - 4));
- codecString = \`avc1.\${new Uint8Array(avcC).subarray(1, 4).toHex()}\`;
- } else if (box === 0x7374737a) {
- frameSize = dv.getUint32(i + 8) || dv.getUint32(i + 16);
- } else if (box === 0x7374636f) {
- frameOffset = dv.getUint32(i + 12);
- } else if (box === 0x636f3634) {
- frameOffset = Number(dv.getBigUint64(i + 12));
- }
- if (avcC && frameSize && frameOffset) break;
- }
- if (!avcC || !frameSize || !frameOffset) throw new Error("MP4 Demux failed");
- const { promise, resolve, reject } = Promise.withResolvers();
- let decoder = null;
- try {
- decoder = new VideoDecoder({ output: resolve, error: reject });
- decoder.configure({ codec: codecString, description: avcC });
- decoder.decode(new EncodedVideoChunk({
- type: 'key', timestamp: 0,
- data: await file.slice(frameOffset, frameOffset + frameSize).arrayBuffer()
- }));
- await decoder.flush();
- using frame = await promise;
- const targetW = Math.min(${APP_CONFIG.MEDIA.THUMB_MAX_DIM}, frame.displayWidth);
- const targetH = Math.max(1, Math.round(frame.displayHeight * (targetW / frame.displayWidth)));
- POOL.canvas.width = targetW;
- POOL.canvas.height = targetH;
- POOL.ctx.drawImage(frame, 0, 0, targetW, targetH);
- const tBlob = await POOL.canvas.convertToBlob({ type: 'image/webp', quality: ${APP_CONFIG.MEDIA.THUMB_WEBP_QUALITY} });
- const thumbBytes = await tBlob.bytes();
- const lqip = extractLQIP(frame);
- self.postMessage({ reqId, payload: {
- thumbBuffer: thumbBytes.buffer,
- lqip,
- ratio: frame.displayWidth / frame.displayHeight
- }},[thumbBytes.buffer]);
- } finally {
- if (decoder && decoder.state !== 'closed') {
- try { decoder.close(); } catch (e) {}
- }
- }
- } catch (err) {
- self.postMessage({ reqId, payload: { fallback: true, reason: err.message } });
- }
- break;
- }
- case 'PROCESS_STREAM': {
- const dir = await OPFS.init();
- const { stream, meta } = e.data;
- const isBinary = meta.isBinary;
- const abortController = new AbortController();
- ACTIVE_ABORTS.set(reqId, abortController);
- const signal = abortController.signal;
- try {
- if (isBinary) {
- const fh = await dir.getFileHandle(meta.id + '_full', { create: true });
- const writable = await fh.createWritable({ keepExistingData: false });
- try {
- await stream.pipeTo(writable, { signal, preventClose: false, preventAbort: false });
- } finally {
- await writable.close().catch(()=>{});
- }
- self.postMessage({ reqId, payload: { written: true } });
- } else {
- let accumulatedText = '';
- let finalResponse = null;
- for await (const event of streamSSE(stream, signal)) {
- if (event?.type === 'response.output_text.delta' && typeof event.delta === 'string') {
- accumulatedText += event.delta;
- self.postMessage({ type: 'STREAM_DELTA', reqId, delta: event.delta, accumulatedText, isDone: false });
- continue;
- }
- if (event?.type === 'response.output_text.done' && typeof event.text === 'string' && !accumulatedText) {
- accumulatedText = event.text;
- self.postMessage({ type: 'STREAM_DELTA', reqId, delta: event.text, accumulatedText, isDone: false });
- continue;
- }
- if ((event?.type === 'response.completed' || event?.type === 'response.done') && event?.response) {
- finalResponse = event.response;
- continue;
- }
- if (event?.type === 'error') {
- const errMsg = typeof event.error === 'object'
- ? (event.error.message || JSON.stringify(event.error))
- : String(event.error || event.raw || 'Stream error');
- throw new Error(errMsg);
- }
- if (event.done) break;
- }
- const data = finalResponse ? { ...finalResponse } : {};
- if (typeof data.output_text !== 'string' || !data.output_text) data.output_text = accumulatedText;
- self.postMessage({ type: 'STREAM_DELTA', reqId, delta: '', accumulatedText, isDone: true });
- self.postMessage({ reqId, payload: data });
- }
- } catch (err) {
- if (isBinary) {
- try { await dir.removeEntry(meta.id + '_full'); } catch(e) {}
- }
- if (err.name === 'AbortError') {
- if (isBinary) {
- self.postMessage({ reqId, payload: { aborted: true } });
- } else {
- self.postMessage({ type: 'STREAM_DELTA', reqId, delta: '', accumulatedText: '', isDone: true, aborted: true });
- self.postMessage({ reqId, error: 'Aborted', errorName: 'AbortError' });
- }
- } else {
- self.postMessage({ reqId, error: err.message });
- }
- } finally {
- ACTIVE_ABORTS.delete(reqId);
- }
- break;
- }
- }
- } catch (err) {
- self.postMessage({ reqId, error: err.message });
- }
- });
- };
- `;
- const StorageClient = {
- worker: null,
- resolvers: new Map(),
- streamCallbacks: new Map(),
- reqCounter: 0,
- init() {
- if (this.worker) return;
- const blob = new Blob([DATA_WORKER_CODE], { type: 'application/javascript' });
- this.worker = new Worker(URL.createObjectURL(blob));
- this.worker.onmessage = (e) => {
- const msg = e.data;
- const { reqId, payload, error } = msg;
- if (msg.type === 'STREAM_DELTA') {
- const cb = this.streamCallbacks.get(reqId);
- if (cb) cb.onDelta(msg.delta, msg.accumulatedText, msg.isDone, msg.aborted);
- return;
- }
- if (this.resolvers.has(reqId)) {
- const { resolve, reject } = this.resolvers.get(reqId);
- if (error) {
- const err = new Error(error);
- if (msg.errorName) err.name = msg.errorName;
- reject(err);
- } else {
- if (payload?.thumbBuffer) {
- payload.thumbBlob = new Blob([payload.thumbBuffer], { type: 'image/webp' });
- delete payload.thumbBuffer;
- }
- resolve(payload);
- }
- this.resolvers.delete(reqId);
- this.streamCallbacks.delete(reqId);
- }
- };
- this.worker.onerror = (err) => {
- addLog('ERROR', 'DataWorker error', err);
- };
- },
- _dispatch(type, payload = {}, transfer = []) {
- this.init();
- const reqId = ++this.reqCounter;
- const { promise, resolve, reject } = Promise.withResolvers();
- this.resolvers.set(reqId, { resolve, reject });
- this.worker.postMessage({ type, reqId, id: payload.id || String(reqId), payload }, transfer);
- return promise;
- },
- processStream(stream, meta, onDelta, signal = null) {
- this.init();
- const reqId = ++this.reqCounter;
- const { promise, resolve, reject } = Promise.withResolvers();
- this.resolvers.set(reqId, { resolve, reject });
- if (onDelta) this.streamCallbacks.set(reqId, { onDelta });
- if (signal) {
- if (signal.aborted) {
- reject(new DOMException('Aborted', 'AbortError'));
- return promise;
- }
- signal.addEventListener('abort', () => {
- if (this.worker) this.worker.postMessage({ type: 'ABORT_REQ', reqId });
- }, { once: true });
- }
- this.worker.postMessage({ type: 'PROCESS_STREAM', reqId, stream, meta }, [stream]);
- return promise;
- },
- saveItemsBulk(items) { return this._dispatch('COMMIT_ITEMS', { items }); },
- deleteItemsBulk(ids) {
- ids.forEach(id => { RefPayloadCache.delete(id); });
- return this._dispatch('DELETE_ITEMS', { ids });
- },
- saveMeta(key, data) { return this._dispatch('SAVE_META', { key, data }); },
- loadMeta(key) { return this._dispatch('LOAD_META', { key }); },
- loadAllHistory() { return this._dispatch('LOAD_ALL_HISTORY'); },
- loadHeavy(id) { return this._dispatch('LOAD_HEAVY', { id }); },
- deleteMeta(keys) { return this._dispatch('DELETE_META', { keys: Array.isArray(keys) ? keys : [keys] }); },
- clearAll() { return this._dispatch('CLEAR_ALL'); },
- processMediaResponse(id, payload, transfer = []) { return this._dispatch('processApiResponse', { id, ...payload }, transfer); },
- extractImagePrompt(buffer) { return this._dispatch('extractImagePrompt', { buffer }, [buffer]); },
- loadOpfsMedia(id, type) {
- return this._dispatch('LOAD_OPFS_MEDIA', { id, type });
- },
- extractVideoPosterWorker(id, mimeType) {
- return this._dispatch('EXTRACT_VIDEO_POSTER', { id, mimeType });
- }
- };
- const terminateMediaWorker = () => {
- if (StorageClient.worker) {
- StorageClient.worker.terminate();
- StorageClient.worker = null;
- StorageClient.resolvers.clear();
- StorageClient.streamCallbacks.clear();
- }
- };
- const LazyTextStore = {
- _cache: new Map(),
- _MAX: 100,
- async get(id) {
- const key = String(id);
- if (this._cache.has(key)) {
- const v = this._cache.get(key);
- this._cache.delete(key);
- this._cache.set(key, v);
- return v;
- }
- const data = await StorageClient.loadHeavy(key);
- if (data) {
- this._cache.set(key, data);
- if (this._cache.size > this._MAX) {
- const oldest = this._cache.keys().next().value;
- this._cache.delete(oldest);
- }
- }
- return data || { prompt: '', responseText: '', textChatMessages: null };
- },
- set(id, data) {
- const key = String(id);
- this._cache.delete(key);
- this._cache.set(key, data);
- if (this._cache.size > this._MAX) {
- const oldest = this._cache.keys().next().value;
- this._cache.delete(oldest);
- }
- },
- invalidate(id) {
- this._cache.delete(String(id));
- }
- };
- const parseResponseJsonSafe = async (response) => {
- try { return await response.json(); } catch (e) { return null; }
- };
- const buildHttpErrorMessage = (status, errData = null) => {
- const parts = new Set([`HTTP ${status}`]);
- const fields = [['error','message'], ['error','code'], ['error','detail'], ['error','type'], ['message'], ['code'], ['detail']];
- for (const path of fields) {
- let v = errData;
- for (const k of path) v = v?.[k];
- if (v != null && v !== '') {
- const text = String(v).trim();
- if (text) parts.add(text);
- }
- }
- if (typeof errData?.error === 'string') {
- const t = errData.error.trim();
- if (t) parts.add(t);
- }
- return Array.from(parts).join('\n');
- };
- const getErrorMessage = (error, fallback = 'Unknown error') => error?.message ?? String(error ?? fallback);
- const buildCurrentStatusLines = (status) => [{ text: status.longStatus, color: status.color, style: CURRENT_PANEL_STATUS_STYLE }];
- 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) => {
- const { promise, resolve, reject } = Promise.withResolvers();
- if (signal?.aborted) {
- reject(new DOMException('Aborted', 'AbortError'));
- return promise;
- }
- let timerId;
- const onAbort = () => {
- clearTimeout(timerId);
- reject(new DOMException('Aborted', 'AbortError'));
- };
- if (signal) signal.addEventListener('abort', onAbort, { once: true });
- timerId = setTimeout(() => {
- if (signal) signal.removeEventListener('abort', onAbort);
- resolve();
- }, ms);
- return promise;
- };
- const scheduleTask = (run, delay = 0, { priority = 'background' } = {}) => {
- const controller = new AbortController();
- scheduler.postTask(() => {
- if (controller.signal.aborted) return;
- return run();
- }, { priority, delay, signal: controller.signal }).catch(() => {});
- return controller;
- };
- const runBg = (fn, opts = { priority: 'background' }) => scheduler.postTask(async () => { try { return await fn(); } catch (e) { addLog('WARN', 'runBg err', e); } }, opts);
- const isMemoryConstrainedMode = () => STATE.ecoMode || App.isAutoEco;
- const clampMediaVolume = (value, fallback = APP_CONFIG.MEDIA.DEFAULT_VOLUME) => {
- if (value == null || value === '') return fallback;
- const num = Number(value);
- if (!Number.isFinite(num)) return fallback;
- return Math.min(1, Math.max(0, num));
- };
- const getStoredMediaVolume = () => {
- try {
- return clampMediaVolume(localStorage.getItem(MEDIA_VOLUME_STORAGE_KEY));
- } catch (e) {
- return APP_CONFIG.MEDIA.DEFAULT_VOLUME;
- }
- };
- const persistMediaVolume = (value) => {
- const safeVolume = clampMediaVolume(value);
- try { localStorage.setItem(MEDIA_VOLUME_STORAGE_KEY, String(safeVolume)); } catch (e) {}
- return safeVolume;
- };
- const applyMemoryPressureProfile = (reason = 'manual') => {
- RefPayloadCache.clear();
- CardRegistry.maps.current.forEach(card => card.setLoadLevel(isMemoryConstrainedMode() ? 'thumb' : (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'));
- };
- document.addEventListener('visibilitychange', () => {
- if (isMemoryConstrainedMode()) {
- if (document.hidden) {
- RefPayloadCache.clear();
- } else {
- applyMemoryPressureProfile('tab-visible');
- }
- }
- });
- const TimeUtils = {
- formatter: new Intl.DateTimeFormat('sv-SE', {
- year: 'numeric', month: '2-digit', day: '2-digit',
- hour: '2-digit', minute: '2-digit', second: '2-digit'
- }),
- forTooltip(ts) { return this.formatter.format(ts || Date.now()); },
- forFile(ts) { return this.formatter.format(ts || Date.now()).replace(/[-: ]/g, ''); }
- };
- const TaskChunker = {
- _deadlineMs: 8,
- _lastYieldTime: globalThis.performance.now(),
- reset() {
- this._lastYieldTime = globalThis.performance.now();
- },
- async yieldIfNeeded() {
- const now = globalThis.performance.now();
- const timeExceeded = (now - this._lastYieldTime) > this._deadlineMs;
- const inputPending = globalThis.navigator?.scheduling?.isInputPending?.({ includeContinuous: true });
- if (timeExceeded || inputPending) {
- await globalThis.scheduler.yield();
- this._lastYieldTime = globalThis.performance.now();
- }
- }
- };
- const makeTimestamp = (ts) => TimeUtils.forFile(ts);
- const TEXT_SESSION_KEY_PREFIX = 'textSession:';
- const ACTIVE_TEXT_SESSION_KEY = 'activeTextSessionId';
- const MULTI_AGENT_TEXT_MODEL = 'grok-4.20-multi-agent';
- const ESC_MAP = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
- const SAFE_PROTO = new Set(['http:', 'https:']);
- const escapeHtml = (s = '') => String(s).replace(/[&<>"']/g, c => ESC_MAP[c]);
- const escapeHtmlAttr = (s = '') => escapeHtml(s).replace(/`/g, '`');
- const escMd = (s = '') => String(s).replace(/[&<>]/g, c => ESC_MAP[c]);
- const sanitizeUrl = url => {
- if (!url || typeof url !== 'string' || /[<>"'`\s]/.test(url)) return '';
- if (url.startsWith('/') || url.startsWith('data:') || url.startsWith('blob:')) return url;
- try { return SAFE_PROTO.has(new URL(url).protocol) ? url : ''; } catch { return ''; }
- };
- const parseList = block => {
- let html = '', stack =[];
- for (const line of block.trimEnd().split('\n')) {
- const match = line.match(/^([ \t]*)([*+-]|\d+\.)\s+(?:\[([ xX])\]\s+)?(.+)$/);
- if (!match) { html += `<br>${line.trim()}`; continue; }
- const [, spaces, marker, check, text] = match;
- const indent = spaces.length, tag = marker.endsWith('.') ? 'ol' : 'ul';
- while (stack.length && stack.at(-1).i > indent) html += `</li></${stack.pop().t}>`;
- if (!stack.length || stack.at(-1).i < indent) {
- html += `<${tag}>`; stack.push({ t: tag, i: indent });
- } else if (stack.at(-1).t !== tag) {
- html += `</li></${stack.pop().t}><${tag}>`; stack.push({ t: tag, i: indent });
- } else {
- html += '</li>';
- }
- html += `<li${check !== undefined ? ' class="task"' : ''}>${check !== undefined ? `<input type="checkbox" disabled${check.trim() ? ' checked' : ''}> ` : ''}${text}`;
- }
- while (stack.length) html += `</li></${stack.pop().t}>`;
- return html;
- };
- const parseQuotes = s => s.replace(/(?:^>[ \t]?.*\n?)+/gm, m => `<blockquote><p>${parseQuotes(m.replace(/^>[ \t]?/gm, '').trimEnd())}</p></blockquote>`);
- const parseMarkdown = s => {
- if (!s) return '';
- s = escMd(s);
- const B =[];
- s = s.replace(/```(\w*)\n([\s\S]*?)```/g, (_, l, c) => (B.push(`<pre><code>${c.trimEnd()}</code>${l ? `<span class="md-lang">${l}</span>` : ''}</pre>`), `\x00${B.length-1}\x00`))
- .replace(/`([^`]+)`/g, (_, c) => (B.push(`<code>${c}</code>`), `\x00${B.length-1}\x00`))
- .replace(/\\([\\`*_{}[\]()#+\-.!>|])/g, (m, c) => `&#${c.charCodeAt(0)};`)
- .replace(/ {2,}\n/g, '<br>');
- s = parseQuotes(s)
- .replace(/\[\[?(\d+)\]\]?\(([^)]+)\)/g, (m, n, u) => { const su = sanitizeUrl(u); return su ? `<a href="${escapeHtmlAttr(su)}" target="_blank" rel="noopener noreferrer" class="xai-cite-inline" title="${escapeHtmlAttr(su)}">[${n}]</a>` : m; })
- .replace(/^(#{1,6})\s+(.+)$/gm, (_, h, t) => `<h${h.length}>${t}</h${h.length}>`)
- .replace(/^\|(.+?)\|[ \t]*\n[ \t]*\|([-| :]+)\|[ \t]*\n((?:[ \t]*\|.+?\|[ \t]*(?:\n|$))*)/gm, (_, h, alignRow, r) => {
- const aligns = alignRow.split('|').slice(1,-1).map(c => {
- const s = c.trim(); return s.startsWith(':') && s.endsWith(':') ? 'center' : s.endsWith(':') ? 'right' : (s.startsWith(':') ? 'left' : '');
- });
- const th = h.split('|').map((c, i) => `<th${aligns[i] ? ` style="text-align:${aligns[i]}"` : ''}>${c.trim()}</th>`).join('');
- const tb = r.trim().split('\n').map(row => `<tr>${row.split('|').slice(1,-1).map((c, i) => `<td${aligns[i] ? ` style="text-align:${aligns[i]}"` : ''}>${c.trim()}</td>`).join('')}</tr>`).join('');
- return `<div class="md-table"><table><thead><tr>${th}</tr></thead><tbody>${tb}</tbody></table></div>`;
- })
- .replace(/(?:^[ \t]*(?:[*+-]|\d+\.)\s+.*(?:\n|$))+/gm, parseList)
- .replace(/^[-*_]{3,}\s*$/gm, '<hr>')
- .replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>')
- .replace(/(\*\*|__)(.+?)\1/g, '<strong>$2</strong>')
- .replace(/([*_])(.+?)\1/g, '<em>$2</em>')
- .replace(/~~(.+?)~~/g, '<del>$1</del>')
- .replace(/(!?)\[([^\]]*)\]\(([^)"]+)(?:\s+"([^"]*)")?\)/g, (_, img, t, u, ti) => {
- const su = sanitizeUrl(u); if (!su) return t;
- return img ? `<img src="${escapeHtmlAttr(su)}" alt="${escapeHtmlAttr(t)}"${ti ? ` title="${escapeHtmlAttr(ti)}"` : ''}>` : `<a href="${escapeHtmlAttr(su)}" target="_blank" rel="noopener noreferrer"${ti ? ` title="${escapeHtmlAttr(ti)}"` : ''}>${t}</a>`;
- })
- .replace(/(?<!["'])(https?:\/\/[^\s<]+?)(?=[.,;!?)]*(?:\s|<|$))(?![^<]*<\/a>)/g, u => {
- const su = sanitizeUrl(u); return su ? `<a href="${escapeHtmlAttr(su)}" target="_blank" rel="noopener noreferrer">${u}</a>` : u;
- })
- .replace(/<(\/?(details|summary)[^&]*)>/g, (_, t) => /^\/?(?:details|summary)$/i.test(t.trim()) ? `<${t.toLowerCase()}>` : `<${t}>`);
- return `<p>${s.trim().replace(/\n{2,}/g, '</p><p>')}</p>`
- .replace(/<p>\s*<(ul|ol|pre|blockquote|h[1-6]|hr|div|details|summary)/g, '<$1')
- .replace(/<\/(ul|ol|pre|blockquote|h[1-6]|hr|div|details|summary)>\s*<\/p>/g, '</$1>')
- .replace(/<p>\s*<\/p>/g, '')
- .replace(/<\/(ul|ol|li|pre|blockquote|h[1-6]|hr|div|table|thead|tbody|tr|th|td|p|details|summary)>\s+</g, '</$1><')
- .replace(/>\s+<(ul|ol|li|pre|blockquote|h[1-6]|hr|div|table|thead|tbody|tr|th|td|p|details|summary)>/g, '><$1>')
- .replace(/\x00(\d+)\x00/g, (_, i) => B[i]);
- };
- const isMultiAgentTextModel = (model = STATE?.textModel) => String(model || '') === MULTI_AGENT_TEXT_MODEL;
- const canUseBuiltInTextTools = (model = STATE?.textModel) => isMultiAgentTextModel(model);
- const buildEnabledTextTools = (model = STATE?.textModel, settings = STATE) => {
- if (!canUseBuiltInTextTools(model)) return [];
- const tools = [];
- if (settings.textWebSearch === 'on') tools.push({ type: 'web_search' });
- if (settings.textXSearch === 'on') tools.push({ type: 'x_search' });
- return tools;
- };
- const buildActionSettingsSnapshot = (action = STATE.action) => ({
- action,
- currentAr: STATE.currentAr,
- batchSize: STATE.batchSize,
- duration: STATE.duration,
- loops: getLoopsValue(action),
- refMode: STATE.refMode,
- concurrentSessions: STATE.concurrentSessions,
- rateLimitDelay: STATE.rateLimitDelay,
- retryMinMs: STATE.retryMinMs,
- retryMaxMs: STATE.retryMaxMs,
- autoRetryVideo: STATE.autoRetryVideo,
- referenceARTrick: STATE.referenceARTrick,
- optimize2k: STATE.optimize2k,
- embedImagePrompt: STATE.embedImagePrompt,
- resImg: STATE.resImg,
- resVid: STATE.resVid,
- imgBaseMs: STATE.imgBaseMs,
- voice: STATE.voice,
- language: STATE.language,
- ttsQuality: STATE.ttsQuality,
- textModel: STATE.textModel,
- textReasoningEffort: STATE.textReasoningEffort,
- textWebSearch: STATE.textWebSearch,
- textXSearch: STATE.textXSearch,
- textChatMessages: sanitizeTextConversationStore(structuredClone(STATE.textChatMessages || []))
- });
- const fetchBlobCSP = (url, { signal, timeout = 30000, range } = {}) => {
- const { promise, resolve, reject } = Promise.withResolvers();
- if (typeof GM_xmlhttpRequest === 'undefined') {
- reject(new Error('GM_xmlhttpRequest not granted'));
- return promise;
- }
- let settled = false;
- let req;
- const combinedSignal = signal ? AbortSignal.any([signal, AbortSignal.timeout(timeout)]) : AbortSignal.timeout(timeout);
- const onAbort = () => {
- if (settled) return;
- settled = true;
- req?.abort();
- reject(signal?.aborted ? new DOMException('Aborted', 'AbortError') : new Error('Timeout'));
- };
- if (combinedSignal.aborted) {
- onAbort();
- return promise;
- }
- combinedSignal.addEventListener('abort', onAbort, { once: true });
- try {
- req = GM_xmlhttpRequest({
- method: 'GET', url,
- headers: range ? { Range: `bytes=${range}` } : {},
- responseType: 'blob',
- onload: (res) => {
- if (settled) return;
- settled = true;
- combinedSignal.removeEventListener('abort', onAbort);
- (res.status >= 200 && res.status < 300) ? resolve(res.response) : reject(new Error(`HTTP ${res.status}`));
- },
- onerror: (err) => {
- if (settled) return;
- settled = true;
- combinedSignal.removeEventListener('abort', onAbort);
- reject(err);
- }
- });
- } catch (e) {
- if (!settled) {
- settled = true;
- combinedSignal.removeEventListener('abort', onAbort);
- reject(e);
- }
- }
- return promise;
- };
- const resetVideoElement = (video, { pause = true } = {}) => {
- if (!video) return;
- if (pause) video.pause?.();
- if (video.src?.startsWith('blob:')) URL.revokeObjectURL(video.src);
- video.removeAttribute('src');
- video.srcObject = null;
- video.load();
- };
- const clickTransientLink = ({ href = '', download = '', target = '' } = {}) => {
- const a = document.createElement('a');
- a.href = href;
- if (download) a.download = download;
- if (target) a.target = target;
- document.body.appendChild(a);
- a.click();
- a.remove();
- };
- const 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 }));
- };
- const isImagePromptSupported = (file) => file && file.size > 128 && (/^image\/(jpeg|png)$/.test(file.type) || /\.(jpe?g|png)$/i.test(file.name));
- const inferMediaMimeType = (file) => {
- const declaredType = String(file?.type || '').toLowerCase();
- if (declaredType) return declaredType;
- const name = String(file?.name || '');
- if (/\.jpe?g$/i.test(name)) return 'image/jpeg';
- if (/\.png$/i.test(name)) return 'image/png';
- if (/\.webp$/i.test(name)) return 'image/webp';
- if (/\.gif$/i.test(name)) return 'image/gif';
- if (/\.mp4$/i.test(name)) return 'video/mp4';
- if (/\.webm$/i.test(name)) return 'video/webm';
- return '';
- };
- const createLimiter = (limit = 1) => {
- let active = 0;
- const queue = [];
- const pump = () => {
- if (active >= limit || queue.length === 0) return;
- active++;
- const { task, resolve, reject } = queue.shift();
- Promise.resolve()
- .then(task)
- .then(resolve, reject)
- .finally(() => {
- active--;
- pump();
- });
- };
- return task => new Promise((resolve, reject) => {
- queue.push({ task, resolve, reject });
- pump();
- });
- };
- const DecodeQueue = {
- queue: [], active: 0, max: Math.min(12, Math.max(4, navigator.hardwareConcurrency || 4)),
- async enqueue(url, signal) {
- if (signal?.aborted) throw new DOMException('Aborted', 'AbortError');
- const { promise, resolve, reject } = Promise.withResolvers();
- this.queue.push({ url, signal, resolve, reject });
- this._process();
- return promise;
- },
- async _process() {
- if (this.active >= this.max || this.queue.length === 0) return;
- const task = this.queue.pop();
- if (task.signal?.aborted) {
- task.reject(new DOMException('Aborted', 'AbortError'));
- return this._process();
- }
- this.active++;
- const img = new Image();
- try {
- img.src = task.url;
- if (task.signal) {
- const { promise, reject } = Promise.withResolvers();
- const abortHandler = () => {
- img.src = '';
- reject(new DOMException('Aborted', 'AbortError'));
- };
- if (task.signal.aborted) throw new DOMException('Aborted', 'AbortError');
- task.signal.addEventListener('abort', abortHandler, { once: true });
- await Promise.race([img.decode(), promise]);
- task.signal.removeEventListener('abort', abortHandler);
- } else {
- await img.decode();
- }
- task.resolve(task.url);
- } catch (err) {
- img.src = '';
- task.reject(err);
- } finally {
- this.active--;
- this._process();
- }
- }
- };
- const MediaUtils = {
- _imageQueue: createLimiter(4),
- _videoQueue: createLimiter(2),
- getDownloadName(item) {
- const imageExt = item?.mimeType?.includes('jpeg') ? 'jpg' : item?.mimeType?.includes('webp') ? 'webp' : 'png';
- return item?.filename || `dl.${item?.type === 'video' ? 'mp4' : item?.type === 'audio' ? 'mp3' : item?.type === 'text' ? 'txt' : imageExt}`;
- },
- async processSource(source, type, maxDim = 600, qual = 0.75) {
- const queue = type === 'video' ? this._videoQueue : this._imageQueue;
- return queue(async () => {
- const isVid = type === 'video', isLocal = isVid && source instanceof Blob;
- let v = null;
- const cleanup = () => { resetVideoElement(v, { pause: false }); };
- try {
- using durl = isLocal ? new DisposableURL(source) : NoOpDisposable;
- const url = isLocal ? durl.url : source;
- using bmp = await (isVid ? (() => {
- v = document.createElement('video');
- v.muted = true; v.playsInline = true; v.disableRemotePlayback = true;
- if (!isLocal) v.crossOrigin = 'anonymous';
- const { promise, resolve, reject } = Promise.withResolvers();
- const tmr = setTimeout(() => reject(new Error('WebP Timeout')), 20000);
- v.onloadeddata = () => { v.currentTime = 0.1; };
- const extractFrame = async () => {
- clearTimeout(tmr);
- try { resolve(await createImageBitmap(v, { resizeWidth: maxDim, resizeQuality: 'medium', premultiplyAlpha: 'none', colorSpaceConversion: 'none' })); }
- catch (e) { reject(e); }
- finally { resetVideoElement(v, { pause: false }); }
- };
- v.requestVideoFrameCallback(extractFrame);
- v.onerror = () => { clearTimeout(tmr); setTimeout(() => reject(new Error('Decode err')), 100); };
- v.src = url;
- v.load();
- return promise;
- })() : createImageBitmap(source, { resizeWidth: maxDim, resizeQuality: 'medium', premultiplyAlpha: 'none', colorSpaceConversion: 'none' }));
- if (!bmp) throw new Error("Bitmap err");
- const ratio = bmp.width / bmp.height;
- let tCtx = new OffscreenCanvas(2, 2).getContext('2d', { alpha: false }); tCtx.drawImage(bmp, 0, 0, 2, 2);
- const d = tCtx.getImageData(0, 0, 2, 2).data;
- const lqip = `radial-gradient(ellipse at 30% 30%, rgb(${d[0]} ${d[1]} ${d[2]}), rgb(${d[12]} ${d[13]} ${d[14]}))`;
- let 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 });
- exportCanvas.width = 1; exportCanvas.height = 1;
- tCtx.canvas.width = 1; tCtx.canvas.height = 1;
- cleanup();
- tCtx = null;
- exportCanvas = null;
- 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 {
- const fileName = this.getDownloadName(item);
- let blobUrl = item.originalUrl, isBlobUrl = item.type === 'image' || item.type === 'audio' || (item.originalUrl?.startsWith('blob:') ?? false);
- if (item.type === 'image' || item.type === 'audio' || item.type === 'text') {
- let blob = await readOpfsAsBlob(item.id, item.mimeType, '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?._internals.states.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; }
- clickTransientLink({ href: fetchUrl, download: fileName }); markDl();
- if (isBlobUrl || needRevoke) {
- setTimeout(() => URL.revokeObjectURL(fetchUrl), 60000);
- }
- };
- if (isBlobUrl || typeof GM_download === 'undefined') await fallbackDl(); else await DiskIOLimiter(() => new Promise((res, rej) => {
- GM_download({
- url: blobUrl,
- name: fileName,
- onload: () => { markDl(); if (isBlobUrl) URL.revokeObjectURL(blobUrl); res(); },
- onerror: () => { if (isBlobUrl) URL.revokeObjectURL(blobUrl); fallbackDl().then(res).catch(rej); },
- ontimeout: res
- });
- }));
- } catch (err) { addLog('WARN', 'Download failed', err); if (!silent && item.originalUrl) clickTransientLink({ href: item.originalUrl, download: this.getDownloadName(item), target: '_blank' }); }
- if (!silent) document.body.style.cursor = '';
- }
- };
- const readOpfsAsBlob = async (mediaId, mimeType, variant = 'full') => {
- const file = await OPFS.read(mediaId, variant);
- return file ? new Blob([file], { type: mimeType }) : null;
- };
- const extractImagePromptViaWorker = async (file) => {
- if (!isImagePromptSupported(file)) return null;
- const buffer = await file.arrayBuffer();
- const transferredBuffer = buffer.transfer();
- return StorageClient.extractImagePrompt(transferredBuffer);
- };
- 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 _tpl = {
- sep: '<div class="popover-separator"></div>',
- tog: (c) => {
- const frag = document.createRange().createContextualFragment(`<div class="popover-item flex-between" id="${c.rowId}" data-action="toggle-setting" data-state-key="${c.stateKey}" title="${c.title || ''}"><span title="${c.title || ''}">${c.label}</span><input type="checkbox" id="${c.inputId}" class="popover-toggle-input"></div>`);
- return frag;
- },
- num: (c) => {
- const frag = document.createRange().createContextualFragment(`<div class="popover-item popover-item--static flex-between" data-action="input-setting-number" data-state-key="${c.stateKey}" title="${c.title || ''}"><span title="${c.title || ''}">${c.label}</span><input type="number" id="${c.inputId}" min="${c.min ?? ''}"${c.max != null ? ` max="${c.max}"` : ''}${c.step != null ? ` step="${c.step}"` : ''} class="input-popover${c.wide ? ' input-popover-lg' : ''}"></div>`);
- return frag;
- }
- };
- const _prL = (id, t) => `<span id="${id}">${t}</span><span class="ar-dropdown-icon">▼</span>`;
- const renderPromptControl = (c) => c
- ? c.kind === 'dropdown' && c.render === 'aspect-ratio'
- ? `<button id="${c.buttonId}" class="xai-btn" data-state-key="${c.stateKey}" title="${c.title}"><div id="ar-selected-icon"></div><span id="ar-selected-text">${c.defaultValue}</span><span class="ar-dropdown-icon">▼</span></button>`
- : c.kind === 'dropdown'
- ? `<button class="xai-btn" id="${c.buttonId}" data-state-key="${c.stateKey}" data-value="${c.defaultValue}" title="${c.title}">${_prL(c.labelId, c.defaultLabel)}</button>`
- : c.kind === 'toggle'
- ? `<button class="xai-btn" id="${c.buttonId}" data-action="toggle-ui-control" data-state-key="${c.stateKey}" data-value="${c.defaultValue}" title="${c.title}">${c.labels[c.defaultValue]}</button>`
- : c.kind === 'number-inline'
- ? `<div class="xai-btn xai-btn--inline-number" id="${c.wrapperId}" data-action="input-ui-number" data-state-key="${c.stateKey}" title="${c.title}"><span class="inline-control-prefix">${c.prefix}</span><input type="number" id="${c.inputId}" value="${c.defaultValue}" min="${c.min}" max="${c.max}" class="input-loop"></div>`
- : `<button class="xai-btn" id="${c.buttonId}" title="${c.title}">${c.text || ''}</button>`
- : '';
- const buildPromptControlsHtml = () => [`<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" data-action="open-favs" title="Favorite Prompts (Saved per selected mode)">${UIUtils.getIcon(APP_CONFIG.ICONS.star, 18)}</button>`,...filterControls('prompt').map(renderPromptControl)].join('');
- const buildPromptPopoverShellsHtml = () => UI_DROPDOWN_CONTROLS.map(({ popoverId, stateKey }) => `<div id="${popoverId}" popover="auto" class="xai-custom-popover" data-state-key="${stateKey}"></div>`).join('');
- const buildSettingsImportExportHtml = () => '<div class="gap-2 d-flex settings-actions"><button id="btn-export-state" class="xai-btn xai-btn--compact" data-action="export-state" title="Export all settings and prompts to a file">Export</button><button id="btn-import-state" class="xai-btn xai-btn--compact" data-action="import-state" title="Import settings and prompts from a file">Import</button><input type="file" id="file-import-state" accept=".json" class="d-none"></div>';
- const renderSettingsControl = (id) => {
- const c = SETTINGS_CONTROL_BY_ID[id];
- return c ? c.kind === 'toggle' ? _tpl.tog(c) : _tpl.num(c) : null;
- };
- const buildSettingsPopoverContent = () => {
- const fragment = document.createDocumentFragment();
- SETTINGS_POPOVER_LAYOUT.forEach((g, gi) => {
- if (gi > 0) fragment.appendChild(createMarkupFragment(_tpl.sep));
- if (g[0] === '__import_export__') {
- fragment.appendChild(createMarkupFragment(buildSettingsImportExportHtml()));
- } else {
- g.forEach(id => { const node = renderSettingsControl(id); if (node) fragment.appendChild(node); });
- }
- });
- return fragment;
- };
- const buildConfirmDialogHtml = () => '<dialog id="xai-confirm-dialog" class="modal-box"><div class="dialog-inner dialog-inner--confirm text-center"><div id="xai-confirm-msg" class="confirm-message"></div><form method="dialog" class="dialog-actions"><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 buildWipeConfirmMarkup = () => '<div class="wipe-confirm-title">Wipe Options:</div><div class="wipe-confirm-body"><b>[Cookie]</b> - Clears tokens/rate limits.<br><b>[Data]</b> - Clears history from every mode. Keeps prompts, favorites, refs and settings.<br><b>[Full]</b> - Factory reset. Wipes cache, prompts, refs, settings, DB and restores first-run defaults.</div>';
- const CARD_TEMPLATE_HTML = `
- <div class="top-bar">
- <div class="ref-header drag-handle" title="Drag to reorder">
- <input class="ref-active-toggle" type="checkbox" title="Toggle reference" data-intent="toggle-ref-active">
- <div class="ref-label-text-wrapper">
- <span class="ref-label-text">{{refLabelText}}</span>
- </div>
- <button class="xai-btn xai-btn--square xai-btn--glass xai-btn--xs hover-danger ref-del" data-intent="delete-ref" title="Delete">${APP_CONFIG.ICONS.close}</button>
- </div>
- </div>
- <div class="text-chat-header" id="text-chat-header">
- <button class="text-chat-header-btn overlay-del-btn" data-intent="delete-global" title="Delete">${APP_CONFIG.ICONS.close}</button>
- <div class="text-chat-model-name" id="text-model-name">{{textModelName}}</div>
- <button class="text-chat-header-btn overlay-dl-btn" data-intent="download" title="Download">${UIUtils.getDlIcon(18)}</button>
- </div>
- <div class="media-wrapper" data-prompt="{{mediaWrapperPrompt}}">
- <div class="bg-layer" id="bg-base" style="background: {{bgBaseBg}}"></div>
- <div class="play-icon" id="play" style="display: {{playDisplay}}">${APP_CONFIG.ICONS.play}</div>
- <img id="img" decoding="async">
- <div id="progress-bar" class="progress-bar-layer"></div>
- </div>
- <div class="overlay">
- <button class="xai-btn xai-btn--square xai-btn--glass xai-btn--sm-square hover-danger overlay-del-btn" data-intent="delete-global" 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" data-intent="action-edit" 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" data-intent="action-extend" 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" data-intent="action-video" 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" data-intent="download" title="Download">${UIUtils.getDlIcon(18)}</a>
- </div>
- </div>
- <button class="xai-btn xai-btn--square xai-btn--glass ref-side-prompt-btn" data-intent="ref-prompt" 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="{{cardPromptTooltip}}">{{cardPrompt}}</p>
- </div>
- `;
- const CARD_TEMPLATE = createModernTemplate(CARD_TEMPLATE_HTML);
- 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 Menu (Cookie / Data / Full)">${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" data-action="toggle-left-panel" 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 id="xai-chat-container" class="xai-chat-view"><div id="chat-scroll" class="chat-scroll-content"></div></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 id="prompt-update-label" class="floating-update-label">Draft modified: Press Enter to apply</div><div class="prompt-container" id="prompt-container-main"><div class="textarea-wrapper" id="textarea-wrapper"><textarea id="xai-api-prompt" maxlength="10000" placeholder="Shift+Enter: line break Dynamic Prompts: {cat|dog|{red|blue} car}"></textarea></div><div class="prompt-actions"><div id="action-row-global-left" class="action-row-left">${buildPromptControlsHtml()}</div><div class="action-row-right"><div id="xai-api-status" class="prompt-status w-100 d-flex justify-end" 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" data-action="toggle-right-panel" 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 xai-btn--ref-mode" data-action="toggle-ref-mode" title="Toggle Queue/Multi mode">Multi</button></div><input type="file" id="xai-api-file" accept="image/*, video/mp4" multiple class="d-none"><div id="xai-ref-list"></div></div></div></div><dialog id="xai-lightbox" class="xai-lightbox-dialog" data-action="close-lightbox"><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" data-action="lightbox-dl" 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 dialog-inner--fav"><div id="fav-list-container" class="dialog-content"></div></div></dialog>${buildPromptPopoverShellsHtml()}<div id="popover-tts-effects" popover="auto" class="xai-custom-popover"></div><div id="popover-settings" popover="auto" class="xai-custom-popover"></div>${buildConfirmDialogHtml()}</div>`;
- const TEMPLATE_APP = document.createElement('template');
- TEMPLATE_APP.innerHTML = buildAppHtml();
- const originalTitle = document.title || "xAI Advanced UI";
- const PlaceholderPalette = {
- deck: [],
- lastSpot: '',
- _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 = [...APP_CONFIG.PLACEHOLDER_SWATCHES];
- for (let i = this.deck.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [this.deck[i], this.deck[j]] = [this.deck[j], this.deck[i]]; }
- if (this.deck[0]?.spot === this.lastSpot && 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.lastSpot = swatch.spot;
- return { ...swatch, gradient: this._gradient(swatch) };
- }
- };
- const TitleState = {
- _lastProgress: '',
- markDone() {
- STATE.pendingDoneNotice = document.hidden || !document.hasFocus();
- this._lastProgress = '';
- this.sync();
- },
- clearDone() {
- if (!STATE.pendingDoneNotice) return;
- STATE.pendingDoneNotice = false;
- this.sync();
- },
- sync(progressText) {
- if (progressText !== undefined) {
- this._lastProgress = progressText;
- }
- if (isAnyRuntimeBusy()) {
- setDocumentTitle(this._lastProgress ? `[${this._lastProgress}] ${originalTitle}` : originalTitle);
- return;
- }
- this._lastProgress = '';
- if (STATE.pendingDoneNotice && (document.hidden || !document.hasFocus())) {
- setDocumentTitle(`[Done] ${originalTitle}`);
- return;
- }
- setDocumentTitle(originalTitle);
- },
- handleTabActive() { if (document.hasFocus()) 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();
- }
- };
- };
- const GENERATION_LANE_BY_ACTION = {
- gen_image: 'media',
- gen_video: 'media',
- edit_video: 'media',
- extend_video: 'media',
- gen_audio: 'audio',
- gen_text: 'text'
- };
- const GENERATION_LANE_LABEL = {
- media: 'Media',
- audio: 'Audio',
- text: 'Text'
- };
- const createGenerationRuntime = (lane) => ({
- lane,
- label: GENERATION_LANE_LABEL[lane] || lane,
- isGenerating: false,
- generationToken: 0,
- successCount: 0,
- activeAction: null,
- tasks: new Map(),
- taskPromises: new Set(),
- progress: new Map(),
- queueWake: createQueueWakeSignal(),
- queueWakeTimer: 0
- });
- const GenerationRegistry = {
- runtimes: {
- media: createGenerationRuntime('media'),
- audio: createGenerationRuntime('audio'),
- text: createGenerationRuntime('text')
- },
- getLane(action = 'gen_image') {
- return GENERATION_LANE_BY_ACTION[String(action || '').trim()] || 'media';
- },
- getRuntime(actionOrLane = 'gen_image') {
- if (this.runtimes[actionOrLane]) return this.runtimes[actionOrLane];
- return this.runtimes[this.getLane(actionOrLane)];
- },
- getAllRuntimes() {
- return Object.values(this.runtimes);
- },
- getAllTasks() {
- return this.getAllRuntimes().flatMap(runtime => [...runtime.tasks.values()]);
- },
- findTask(taskId) {
- const key = String(taskId);
- for (const runtime of this.getAllRuntimes()) {
- const task = runtime.tasks.get(key);
- if (task) return task;
- }
- return null;
- },
- resetRuntime(runtime) {
- clearTimeout(runtime.queueWakeTimer);
- runtime.queueWakeTimer = 0;
- runtime.queueWake = createQueueWakeSignal();
- runtime.tasks.clear();
- runtime.taskPromises.clear();
- runtime.progress.clear();
- runtime.isGenerating = false;
- runtime.generationToken = 0;
- runtime.successCount = 0;
- runtime.activeAction = null;
- },
- resetAll() {
- this.getAllRuntimes().forEach(runtime => this.resetRuntime(runtime));
- },
- isAnyBusy() {
- return this.getAllRuntimes().some(runtime => runtime.isGenerating || runtime.tasks.size > 0);
- }
- };
- const getCurrentRuntime = (action = STATE?.action) => GenerationRegistry.getRuntime(action);
- const isRuntimeBusy = (actionOrLane = STATE?.action) => GenerationRegistry.getRuntime(actionOrLane).isGenerating;
- const isAnyRuntimeBusy = () => GenerationRegistry.isAnyBusy();
- const wakeUpQueue = (actionOrLane = STATE?.action) => GenerationRegistry.getRuntime(actionOrLane).queueWake.notify();
- const scheduleRuntimeWake = (actionOrLane = STATE?.action, delay = 300) => {
- const runtime = GenerationRegistry.getRuntime(actionOrLane);
- clearTimeout(runtime.queueWakeTimer);
- runtime.queueWakeTimer = setTimeout(() => wakeUpQueue(runtime.lane), delay);
- };
- const applyRuntimeIdleCleanup = () => {
- if (isAnyRuntimeBusy()) return;
- RefPayloadCache.clear();
- PanelManager.update(STATE.historyOrder, STATE.items, 'history');
- };
- const notifyRuntimeGeneratingChanged = (runtime, isGenerating) => {
- if (isGenerating) STATE.pendingDoneNotice = false;
- if (STATE.isMounted) {
- if (runtime === getCurrentRuntime()) {
- toggleGenerateButton(isGenerating);
- updateDirtyStateUI();
- }
- updateStatusUI();
- syncWipeButtonState?.();
- }
- if (!isGenerating) applyRuntimeIdleCleanup();
- TitleState.sync();
- };
- const setRuntimeField = (runtime, key, value) => {
- if (runtime[key] === value) return false;
- runtime[key] = value;
- if (key === 'isGenerating') notifyRuntimeGeneratingChanged(runtime, value);
- return true;
- };
- let _configSaveTask = null, _chatScrollRaf = 0, DOM = {};
- const configKeys = new Set(Object.keys(CONFIG_SCHEMA));
- const PROMPT_KEYS = Object.keys(DEFAULT_PROMPTS);
- const RETRY_PROMPT_PAD_RE = /[\u200B-\u200D\uFEFF]/g;
- const resolveSchemaRule = (value, ctx) => typeof value === 'function' ? value(ctx) : value;
- const clampInt = (value, min, max, fallback) => {
- const parsed = Number.parseInt(value, 10);
- const fallbackParsed = Number.parseInt(fallback, 10);
- const safeFallback = Number.isFinite(fallbackParsed) ? fallbackParsed : min;
- return Number.isFinite(parsed) ? Math.min(max, Math.max(min, parsed)) : safeFallback;
- };
- 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;
- try { return structuredClone(value); }
- catch { 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, '').trimEnd() : '';
- return (clean || (typeof fallback === 'string' ? fallback.replace(RETRY_PROMPT_PAD_RE, '').trimEnd() : '')).slice(0, 10000);
- };
- const hasDataTransferType = (dataTransfer, type) => !!dataTransfer && Array.from(dataTransfer.types || []).includes(type);
- const normalizeConfigValue = (rawValue, rule, fallbackValue, ctx) => {
- const value = rule.normalize ? rule.normalize(rawValue) : rawValue;
- switch (rule.type) {
- case 'bool':
- return typeof value === 'boolean' ? value : !!fallbackValue;
- case 'int': {
- const min = resolveSchemaRule(rule.min ?? Number.MIN_SAFE_INTEGER, ctx);
- const max = resolveSchemaRule(rule.max ?? Number.MAX_SAFE_INTEGER, ctx);
- return clampInt(value, min, max, fallbackValue);
- }
- case 'intString': {
- const min = resolveSchemaRule(rule.min ?? Number.MIN_SAFE_INTEGER, ctx);
- const max = resolveSchemaRule(rule.max ?? Number.MAX_SAFE_INTEGER, ctx);
- return String(clampInt(value, min, max, fallbackValue));
- }
- case 'enum': {
- const options = resolveSchemaRule(rule.options || [], ctx);
- const allowed = Array.isArray(options) ? options : [];
- return allowed.includes(value) ? value : fallbackValue;
- }
- default:
- return value ?? fallbackValue;
- }
- };
- const normalizeConfig = (raw, fallback = null) => {
- const source = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
- const out = {};
- for (const [key, rule] of Object.entries(CONFIG_SCHEMA)) {
- const ctx = { source, out, fallback };
- const fallbackValue = fallback?.[key] ?? resolveSchemaRule(rule.default, ctx);
- out[key] = normalizeConfigValue(source[key], rule, fallbackValue, ctx);
- }
- if (out.retryMaxMs < out.retryMinMs) out.retryMaxMs = out.retryMinMs;
- return out;
- };
- const CONFIG_DEFAULTS = normalizeConfig({});
- const getConfigSnapshot = () => Object.fromEntries([...configKeys].map(key => [key, STATE[key]]));
- const mapStore = (raw, fallback, fn) => {
- const base = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
- return Object.fromEntries(Object.keys(fallback).map(k => [k, fn(base[k], fallback[k])]));
- };
- const createSafeStore = (raw, schema) => {
- if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return typeof schema === 'function' ? schema() : schema;
- const out = Array.isArray(schema) ? [] : {};
- for (const [key, typeRule] of Object.entries(schema)) {
- if (Array.isArray(out)) {
- if (!Array.isArray(raw[key])) continue;
- out.push(...raw[key].map(item => typeof typeRule === 'function' ? typeRule(item) : item).filter(Boolean));
- } else if (typeof typeRule === 'function') {
- out[key] = typeRule(raw[key]);
- } else if (typeRule && typeof typeRule === 'object' && !Array.isArray(typeRule)) {
- out[key] = createSafeStore(raw[key], typeRule);
- } else {
- out[key] = raw[key] !== undefined && raw[key] !== null ? raw[key] : typeRule;
- }
- }
- return out;
- };
- const sanitizePromptStore = (raw, fallback = DEFAULT_PROMPTS) => mapStore(raw, fallback, (v, fb) => normalizePromptDraft(v, fb || ''));
- const sanitizeFavPromptStore = (raw, fallback = DEFAULT_FAV_PROMPTS) => mapStore(raw, fallback, (v, fb) => Array.isArray(v) ? v.filter(x => typeof x === 'string') : [...fb]);
- const sanitizeRefPromptStore = (raw, fallback = {}) => {
- if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return { ...fallback };
- return Object.fromEntries(Object.entries(raw).filter(([, val]) => val && typeof val === 'object' && !Array.isArray(val)).map(([id, val]) => [id, Object.fromEntries(Object.entries(val).filter(([, p]) => typeof p === 'string' && p.trim()))]));
- };
- const sanitizeIdListStore = (raw) => Array.isArray(raw) ? [...new Set(raw.map(String).filter(Boolean))] : [];
- const sanitizeStatusLinesStore = (raw) => createSafeStore(raw, { text: '', color: '', style: '' });
- const sanitizeTextChatMessage = (raw) => {
- if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
- const role = raw.role === 'assistant' ? 'assistant' : 'user';
- const text = raw.text || '';
- return text ? { role, text, timestamp: raw.timestamp || Date.now() } : null;
- };
- const sanitizeTextConversationStore = (raw) => Array.isArray(raw)
- ? raw.map(sanitizeTextChatMessage).filter(Boolean)
- : [];
- const getTextSessionStoreKey = (id) => `${TEXT_SESSION_KEY_PREFIX}${String(id)}`;
- const getTextSessionMessagesFromItemAsync = async (item) => {
- const direct = sanitizeTextConversationStore(item?.textConversation);
- if (direct.length > 0) return direct;
- const heavy = await LazyTextStore.get(item?.id);
- const prompt = heavy?.prompt || item?.prompt || '';
- const responseText = heavy?.responseText || item?.responseText || '';
- const fallback = [];
- if (prompt) fallback.push({ role: 'user', text: prompt, timestamp: item.timestamp || Date.now() });
- if (responseText) fallback.push({ role: 'assistant', text: responseText, timestamp: item.timestamp || Date.now() });
- return fallback;
- };
- const getFirstUserText = (messages = []) => messages.find(message => message.role === 'user')?.text || '';
- const getLastAssistantText = (messages = []) => {
- for (let i = messages.length - 1; i >= 0; i--) {
- if (messages[i]?.role === 'assistant') return messages[i].text || '';
- }
- return '';
- };
- const getTextSessionTurnCount = (messages = []) => messages.reduce((count, message) => count + (message.role === 'assistant' ? 1 : 0), 0);
- const buildTextSessionTranscript = (messages = [], model = '') => {
- const header = model ? `Model: ${model}` : '';
- const body = messages.map(message => `${message.role === 'assistant' ? 'Assistant' : 'User'}:\n${message.text}`).join('\n\n');
- return [header, body].filter(Boolean).join('\n\n');
- };
- const summarizeTextSessionItem = (item, messages = [], { updatedAt = Date.now(), model = item?.textModel || STATE.textModel } = {}) => {
- const cleanMessages = sanitizeTextConversationStore(Array.isArray(messages) ? [...messages] : messages);
- const prompt = Pipeline.normalizePrompt(getFirstUserText(cleanMessages));
- const responseText = getLastAssistantText(cleanMessages);
- item.shortPrompt = prompt.substring(0, 100);
- item.textModel = typeof model === 'string' && model ? model : (item.textModel || STATE.textModel || 'grok-4.20');
- item.textTurnCount = getTextSessionTurnCount(cleanMessages);
- item.timestamp = updatedAt;
- LazyTextStore.set(item.id, { prompt, responseText, textChatMessages: null });
- return cleanMessages;
- };
- const sanitizeCurrentPanelSessionItem = (raw) => {
- if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
- const id = String(raw.id);
- if (!id) return null;
- const fullPrompt = raw.prompt ? Pipeline.normalizePrompt(raw.prompt) : '';
- return {
- id,
- timestamp: raw.timestamp || Date.now(),
- type: raw.type || 'image',
- mimeType: raw.mimeType || '',
- ratio: raw.ratio || 1,
- originalUrl: raw.originalUrl || null,
- filename: raw.filename || 'Pending...',
- downloaded: Boolean(raw.downloaded),
- prompt: fullPrompt,
- shortPrompt: fullPrompt.substring(0, 100),
- responseText: raw.responseText || null,
- textModel: raw.textModel || null,
- textTurnCount: raw.textTurnCount || 0,
- lqip: raw.lqip || null,
- isUploadedRef: Boolean(raw.isUploadedRef),
- isPending: Boolean(raw.isPending),
- reqId: raw.reqId || null,
- taskId: raw.taskId || null,
- refSourceId: raw.refSourceId != null ? String(raw.refSourceId) : null,
- _statusData: sanitizeStatusLinesStore(raw._statusData),
- persist: false
- };
- };
- const sanitizeCurrentPanelSessionStore = (raw) => {
- if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
- return {
- schemaVersion: 1,
- order: sanitizeIdListStore(raw.order),
- items: Array.isArray(raw.items) ? raw.items.map(sanitizeCurrentPanelSessionItem).filter(Boolean) : []
- };
- };
- const isSameIdList = (a = [], b = []) => a === b || (Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((id, i) => String(id) === String(b[i])));
- const createEmptyPanelOrderStore = () => Object.fromEntries(PANEL_ACTION_KEYS.map(key => [key, []]));
- const sanitizePanelKey = (raw, fallback = 'gen_image') => {
- const key = String(raw || '').trim();
- return PANEL_ACTION_KEYS.includes(key) ? key : fallback;
- };
- const REF_VIEW_SCHEMA_VERSION = 1;
- const REF_VIEW_MODES = ['multi', 'queue'];
- const ACTION_LOOP_STATE_KEY = {
- gen_image: 'loopsGenImage',
- gen_video: 'loopsGenVideo',
- edit_video: 'loopsEditVideo',
- extend_video: 'loopsExtendVideo'
- };
- const normalizeRefModeForAction = (action = STATE?.action || 'gen_image', refMode = STATE?.refMode || 'multi') => {
- const safeAction = sanitizePanelKey(action);
- if (safeAction === 'gen_text' || safeAction === 'gen_audio') return 'multi';
- return String(refMode) === 'queue' ? 'queue' : 'multi';
- };
- const getLoopsStateKey = (action = STATE?.action || 'gen_image') => ACTION_LOOP_STATE_KEY[sanitizePanelKey(action)] || null;
- const getLoopsValue = (action = STATE?.action || 'gen_image', source = STATE) => {
- const key = getLoopsStateKey(action);
- if (key && source && source[key] != null) return String(source[key]);
- return String(source?.loops ?? '0');
- };
- const setLoopsValue = (action = STATE?.action || 'gen_image', value = '0') => {
- const key = getLoopsStateKey(action);
- const normalized = String(clampInt(value, 0, 99, 0));
- if (key) STATE[key] = normalized;
- STATE.loops = normalized;
- return normalized;
- };
- const getRefPoolTypeForAction = (action = STATE?.action || 'gen_image') => ['edit_video', 'extend_video'].includes(sanitizePanelKey(action)) ? 'video' : 'image';
- const getRefViewKey = (action = STATE?.action || 'gen_image', refMode = STATE?.refMode || 'multi') => `${sanitizePanelKey(action)}:${normalizeRefModeForAction(action, refMode)}`;
- const REF_VIEW_KEYS = [...new Set(PANEL_ACTION_KEYS.flatMap(action => REF_VIEW_MODES.map(mode => getRefViewKey(action, mode))))];
- const createEmptyRefViewEntry = () => ({ order: [], active: {}, waitUntil: {} });
- const cloneRefViewEntry = (entry = createEmptyRefViewEntry()) => ({
- order: sanitizeIdListStore(entry.order),
- active: Object.fromEntries(Object.entries(entry.active || {}).filter(([, val]) => val === true).map(([id]) => [String(id), true])),
- waitUntil: Object.fromEntries(Object.entries(entry.waitUntil || {}).filter(([, val]) => Number.isFinite(val) && val > 0).map(([id, val]) => [String(id), val]))
- });
- const createEmptyRefViewStore = () => Object.fromEntries(REF_VIEW_KEYS.map(key => [key, createEmptyRefViewEntry()]));
- const sanitizeRefViewEntry = (raw) => {
- const entry = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
- const order = sanitizeIdListStore(entry.order);
- const orderSet = new Set(order);
- const activeKeys = Object.entries(entry.active || {}).filter(([id, val]) => val === true && orderSet.has(String(id))).map(([id]) => [String(id), true]);
- const active = Object.fromEntries(activeKeys);
- const waitKeys = Object.entries(entry.waitUntil || {}).filter(([id, val]) => orderSet.has(String(id)) && Number.isFinite(val) && val > 0).map(([id, val]) => [String(id), val]);
- const waitUntil = Object.fromEntries(waitKeys);
- return { order, active, waitUntil };
- };
- const sanitizeRefViewStore = (raw) => {
- const source = raw && typeof raw === 'object' && !Array.isArray(raw) && raw.schemaVersion === REF_VIEW_SCHEMA_VERSION ? raw.views : null;
- const next = createEmptyRefViewStore();
- REF_VIEW_KEYS.forEach(key => {
- next[key] = sanitizeRefViewEntry(source?.[key]);
- });
- return next;
- };
- const serializeRefViewStore = (store = STATE?.refViews || createEmptyRefViewStore()) => ({
- schemaVersion: REF_VIEW_SCHEMA_VERSION,
- views: Object.fromEntries(REF_VIEW_KEYS.map(key => [key, cloneRefViewEntry(store[key])]))
- });
- const getRefViewKeysForPoolType = (poolType = 'image') => REF_VIEW_KEYS.filter(key => getRefPoolTypeForAction(key.split(':')[0]) === poolType);
- const isMp4ReferenceItem = (item) => !!item && item.type === 'video' && (!item.mimeType || /mp4/i.test(String(item.mimeType)));
- const isRefCompatibleWithPoolType = (item, poolType = 'image') => poolType === 'video' ? isMp4ReferenceItem(item) : item?.type === 'image';
- const createDefaultRefViewsFromItems = () => {
- const next = createEmptyRefViewStore();
- const imageIds = [];
- const videoIds = [];
- STATE.items.forEach((item, id) => {
- if (!item || !item.isUploadedRef) return;
- if (isRefCompatibleWithPoolType(item, 'image')) imageIds.push(String(id));
- else if (isRefCompatibleWithPoolType(item, 'video')) videoIds.push(String(id));
- });
- getRefViewKeysForPoolType('image').forEach(key => { next[key].order = [...imageIds]; });
- getRefViewKeysForPoolType('video').forEach(key => { next[key].order = [...videoIds]; });
- return next;
- };
- const sanitizePanelOrderStore = (raw) => {
- const source = raw && typeof raw === 'object' && !Array.isArray(raw) && raw.schemaVersion === PANEL_STORE_SCHEMA_VERSION ? raw.panels : null;
- const next = createEmptyPanelOrderStore();
- PANEL_ACTION_KEYS.forEach(key => {
- next[key] = sanitizeIdListStore(source?.[key]);
- });
- return next;
- };
- const sanitizePanelSessionMapStore = (raw) => {
- const source = raw && typeof raw === 'object' && !Array.isArray(raw) && raw.schemaVersion === PANEL_STORE_SCHEMA_VERSION ? raw.panels : null;
- const panels = {};
- PANEL_ACTION_KEYS.forEach(key => {
- const safeSession = sanitizeCurrentPanelSessionStore(source?.[key]);
- if (safeSession && (safeSession.order.length > 0 || safeSession.items.length > 0)) panels[key] = safeSession;
- });
- return { schemaVersion: PANEL_STORE_SCHEMA_VERSION, panels };
- };
- const parseImportedState = (raw, fallback = getConfigSnapshot()) => {
- if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new Error('Invalid import payload');
- if (raw.schemaVersion !== APP_CONFIG.STORAGE.EXPORT_SCHEMA_VERSION) throw new Error(`Unsupported import schema: ${raw.schemaVersion ?? 'unknown'}`);
- if (!raw.config || typeof raw.config !== 'object' || Array.isArray(raw.config)) throw new Error('Invalid config payload');
- const prompts = sanitizePromptStore(raw.prompts, STATE.prompts);
- return {
- config: normalizeConfig(raw.config, fallback),
- prompts,
- promptDrafts: sanitizePromptStore(raw.promptDrafts, prompts),
- refPrompts: sanitizeRefPromptStore(raw.refPrompts, STATE.refPrompts),
- favPrompts: sanitizeFavPromptStore(raw.favPrompts, STATE.favPrompts)
- };
- };
- const triggerMediaQueueWake = () => {
- if (!STATE.isMounted || !isRuntimeBusy('media')) return;
- scheduleRuntimeWake('media');
- updateStatusUI();
- };
- const RUNTIME_STATE_KEYS = new Set(['isGenerating', 'generationToken', 'successCount']);
- const AppState = {
- isMounted: new ReactiveVar(false),
- items: new ReactiveVar(new Map()),
- refViews: new ReactiveVar(createEmptyRefViewStore()),
- refOrder: new ReactiveVar([]),
- panelOrders: new ReactiveVar(createEmptyPanelOrderStore()),
- currentOrder: new ReactiveVar([]),
- textChatMessages: new ReactiveVar([]),
- activeTextSessionId: new ReactiveVar(null),
- activeTextSessionTmp: new ReactiveVar(undefined),
- currentPanelSessionTmp: new ReactiveVar(undefined),
- historyOrder: new ReactiveVar([]),
- activeLightboxId: new ReactiveVar(null),
- activeRefPromptId: new ReactiveVar(null),
- activeRefPromptAction: new ReactiveVar(null),
- activeRefPromptDraft: new ReactiveVar(''),
- prompts: new ReactiveVar(sanitizePromptStore(DEFAULT_PROMPTS)),
- promptDrafts: new ReactiveVar(sanitizePromptStore(DEFAULT_PROMPTS)),
- favPrompts: new ReactiveVar(sanitizeFavPromptStore(DEFAULT_FAV_PROMPTS)),
- refPrompts: new ReactiveVar({}),
- pendingDoneNotice: new ReactiveVar(false),
- timings: new ReactiveVar(undefined),
- refViewsTmp: new ReactiveVar(undefined),
- panelOrdersTmp: new ReactiveVar(undefined),
- action: new ReactiveVar('gen_image'),
- currentAr: new ReactiveVar('Auto'),
- batchSize: new ReactiveVar('2'),
- duration: new ReactiveVar('5'),
- loops: new ReactiveVar('0'),
- loopsGenImage: new ReactiveVar('0'),
- loopsGenVideo: new ReactiveVar('0'),
- loopsEditVideo: new ReactiveVar('0'),
- loopsExtendVideo: new ReactiveVar('0'),
- resImg: new ReactiveVar('2k'),
- resVid: new ReactiveVar('720p'),
- refMode: new ReactiveVar('multi'),
- concurrentSessions: new ReactiveVar(1),
- rateLimitDelay: new ReactiveVar(60),
- retryMinMs: new ReactiveVar(2000),
- retryMaxMs: new ReactiveVar(4000),
- autoRetryVideo: new ReactiveVar(true),
- referenceARTrick: new ReactiveVar(false),
- ecoMode: new ReactiveVar(false),
- autoDownload: new ReactiveVar(false),
- embedImagePrompt: new ReactiveVar(true),
- optimize2k: new ReactiveVar(false),
- leftCollapsed: new ReactiveVar(false),
- rightCollapsed: new ReactiveVar(false),
- leftWidth: new ReactiveVar(260),
- rightWidth: new ReactiveVar(320),
- imgBaseMs: new ReactiveVar(3000),
- voice: new ReactiveVar('eve'),
- language: new ReactiveVar('auto'),
- ttsQuality: new ReactiveVar('128000'),
- textModel: new ReactiveVar('grok-4.20'),
- textReasoningEffort: new ReactiveVar('medium'),
- textWebSearch: new ReactiveVar('on'),
- textXSearch: new ReactiveVar('on'),
- customTextModels: new ReactiveVar([])
- };
- function scheduleConfigSave() {
- _configSaveTask?.abort();
- _configSaveTask = scheduleTask(() => {
- _configSaveTask = null;
- localStorage.setItem(APP_CONFIG.STORAGE.CONFIG_KEY, JSON.stringify(normalizeConfig(getConfigSnapshot(), CONFIG_DEFAULTS)));
- }, 250);
- }
- const _appAbortController = new AbortController();
- const STATE = new Proxy(AppState, {
- get(target, prop) {
- if (RUNTIME_STATE_KEYS.has(prop)) return getCurrentRuntime(target.action.value)[prop];
- const rv = target[prop];
- return rv instanceof ReactiveVar ? rv.value : rv;
- },
- set(target, prop, value) {
- if (prop === 'isGenerating') {
- const runtime = getCurrentRuntime(target.action.value || 'gen_image');
- setRuntimeField(runtime, prop, value);
- return true;
- }
- if (prop === 'refMode') {
- const lane = JobManager.getLane(target.action.value || 'gen_image');
- if (JobManager.getRuntime(lane).isGenerating) {
- UI.showToast('Reference mode is locked during active generation', 'warn');
- return true;
- }
- }
- if (RUNTIME_STATE_KEYS.has(prop)) {
- const runtime = getCurrentRuntime(target.action.value || 'gen_image');
- const changed = setRuntimeField(runtime, prop, value);
- if (changed && prop !== 'isGenerating' && runtime === getCurrentRuntime(target.action.value || 'gen_image') && target.isMounted.value) {
- updateStatusUI();
- }
- return true;
- }
- if (prop in target) {
- const rv = target[prop];
- if (rv instanceof ReactiveVar) rv.value = value;
- else target[prop] = new ReactiveVar(value);
- } else {
- target[prop] = new ReactiveVar(value);
- }
- if (target.isMounted.value) {
- if (prop === 'currentOrder') {
- syncActivePanelOrderState();
- PanelManager.update(STATE.currentOrder, STATE.items, 'current');
- persistPanelOrderStore();
- scheduleCurrentPanelSessionSave();
- } else if (prop === 'concurrentSessions') {
- triggerMediaQueueWake();
- }
- }
- if (configKeys.has(prop)) scheduleConfigSave();
- return true;
- }
- });
- (function bindAppStateHandlers() {
- AppState.action.bind(currentAction => {
- if (!AppState.isMounted.value) return;
- syncVisibleRefStateFromView();
- renderRefList();
- updateActionUI();
- toggleGenerateButton(STATE.isGenerating);
- updateDirtyStateUI();
- syncWipeButtonState();
- updateStatusUI();
- syncRefCardsPromptFlags(currentAction);
- }, _appAbortController.signal);
- AppState.ecoMode.bind(isEco => {
- if (!AppState.isMounted.value) return;
- document.body.classList.toggle('eco-mode', isEco);
- applyMemoryPressureProfile(isEco ? 'eco-on' : 'eco-off');
- }, _appAbortController.signal);
- AppState.leftCollapsed.bind(collapsed => {
- if (!AppState.isMounted.value) return;
- DOM.colLeft.classList.toggle('is-collapsed', collapsed);
- document.getElementById('btn-collapse-left').setAttribute('aria-expanded', !collapsed);
- requestAnimationFrame(() => syncLayoutWidths());
- }, _appAbortController.signal);
- AppState.rightCollapsed.bind(collapsed => {
- if (!AppState.isMounted.value) return;
- DOM.colRight.classList.toggle('is-collapsed', collapsed);
- document.getElementById('btn-collapse-right').setAttribute('aria-expanded', !collapsed);
- requestAnimationFrame(() => syncLayoutWidths());
- }, _appAbortController.signal);
- AppState.refMode.bind(() => {
- if (!AppState.isMounted.value) return;
- syncVisibleRefStateFromView();
- renderRefList();
- renderRefMode();
- }, _appAbortController.signal);
- AppState.currentAr.bind(() => {
- if (!AppState.isMounted.value) return;
- syncDOMControls('currentAr');
- PanelManager.update(STATE.currentOrder, STATE.items, 'current');
- }, _appAbortController.signal);
- AppState.loops.bind(() => { if (AppState.isMounted.value) triggerMediaQueueWake(); }, _appAbortController.signal);
- AppState.loopsGenImage.bind(() => { if (AppState.isMounted.value) triggerMediaQueueWake(); }, _appAbortController.signal);
- AppState.loopsGenVideo.bind(() => { if (AppState.isMounted.value) triggerMediaQueueWake(); }, _appAbortController.signal);
- AppState.loopsEditVideo.bind(() => { if (AppState.isMounted.value) triggerMediaQueueWake(); }, _appAbortController.signal);
- AppState.loopsExtendVideo.bind(() => { if (AppState.isMounted.value) triggerMediaQueueWake(); }, _appAbortController.signal);
- })();
- const JobManager = {
- getLane(action = STATE.action) { return GenerationRegistry.getLane(action); },
- getRuntime(actionOrLane = STATE.action) { return GenerationRegistry.getRuntime(actionOrLane); },
- get tasks() { return this.getRuntime().tasks; },
- get taskPromises() { return this.getRuntime().taskPromises; },
- get progress() { return this.getRuntime().progress; },
- getAllTasks() { return GenerationRegistry.getAllTasks(); },
- findTask(taskId) { return GenerationRegistry.findTask(taskId); },
- findTasksByRef(refId) {
- const key = String(refId);
- return GenerationRegistry.getAllRuntimes().flatMap(runtime => [...runtime.tasks.values()].filter(task => task.refIds.includes(key)));
- },
- abortAll() {
- GenerationRegistry.getAllRuntimes().forEach(runtime => runtime.tasks.forEach(task => task.controller.abort()));
- },
- resetAll() {
- GenerationRegistry.resetAll();
- }
- };
- function syncChatActionBarState() {
- const container = DOM.chatScroll || document.getElementById('chat-scroll');
- if (!container) return;
- const aiRows = [...container.querySelectorAll('.chat-msg-row.ai')];
- const latestInteractiveRow = [...aiRows].reverse().find(row => !row.classList.contains('status-msg')) || null;
- aiRows.forEach(row => {
- const newChatBtn = row.querySelector('[data-chat-action="new-chat"]');
- const copyBtn = row.querySelector('[data-chat-action="copy"]');
- const repeatBtn = row.querySelector('[data-chat-action="repeat"]');
- const isStatus = row.classList.contains('status-msg');
- if (copyBtn) copyBtn.hidden = isStatus;
- if (repeatBtn) repeatBtn.hidden = isStatus;
- if (newChatBtn) newChatBtn.hidden = isStatus || row !== latestInteractiveRow;
- });
- }
- function scrollTextChatToBottom({ smooth = false } = {}) {
- const chatContainer = DOM.chatView || document.getElementById('xai-chat-container');
- if (!chatContainer) return;
- if (_chatScrollRaf) cancelAnimationFrame(_chatScrollRaf);
- _chatScrollRaf = requestAnimationFrame(() => {
- _chatScrollRaf = 0;
- if (smooth) chatContainer.scrollTo({ top: chatContainer.scrollHeight, behavior: 'smooth' });
- else chatContainer.scrollTop = chatContainer.scrollHeight;
- });
- }
- function updateRenderedChatMessage(msgEl, text, isStatus = false, preParsedHtml = null) {
- if (!msgEl) return;
- if (msgEl._lastText === text) return;
- msgEl._lastText = text;
- msgEl.classList.toggle('status-msg', !!isStatus);
- const bubble = msgEl.querySelector('.chat-bubble-ui');
- if (bubble) {
- if (isStatus) {
- bubble.setHTMLUnsafe(`<span class="chat-status-text">${escapeHtml(text)}</span>`);
- } else {
- bubble.setHTMLUnsafe(preParsedHtml !== null ? preParsedHtml : parseMarkdown(text));
- }
- bubble.classList.toggle('is-status', !!isStatus);
- }
- const toolbar = msgEl.querySelector('.chat-msg-toolbar');
- if (toolbar) {
- toolbar.hidden = false;
- toolbar.style.visibility = isStatus ? 'hidden' : 'visible';
- toolbar.style.height = isStatus ? '30px' : '';
- }
- syncChatActionBarState();
- }
- function patchMessage(msgIdOrNode, text, isStatus = false, preParsedHtml = null) {
- const msgEl = typeof msgIdOrNode === 'string' ? document.querySelector(`[data-msg-id="${msgIdOrNode}"]`) : msgIdOrNode;
- if (msgEl) updateRenderedChatMessage(msgEl, text, isStatus, preParsedHtml);
- }
- function addMessage(role, text, isStatus = false, userPrompt = '') {
- const container = DOM.chatScroll || document.getElementById('chat-scroll');
- if (!container) return null;
- const isUser = role === 'user';
- const frag = document.createRange().createContextualFragment(buildChatMsgHtml({
- rowClass: `${isUser ? 'user' : 'ai'}${isStatus ? ' status-msg' : ''}`,
- msgId: '',
- avatarDisplay: isUser ? 'none' : '',
- avatarText: isUser ? '' : '\u{1D54F}',
- bubbleWrapperClass: isUser ? '' : 'chat-bubble-wrapper--ai',
- repeatUserPrompt: userPrompt || ''
- }));
- const row = frag.firstChild;
- if (isUser) {
- const toolbar = row.querySelector('.chat-msg-toolbar');
- if (toolbar) toolbar.remove();
- } else if (isStatus) {
- const toolbar = row.querySelector('.chat-msg-toolbar');
- if (toolbar) {
- toolbar.hidden = false;
- toolbar.style.visibility = 'hidden';
- toolbar.style.height = '30px';
- }
- }
- container.appendChild(frag);
- updateRenderedChatMessage(row, text, isStatus);
- return row;
- }
- function renderTextChatMessage(role, text, isStatus = false, userPrompt = '') {
- const msgId = Date.now() + Math.random();
- try {
- const msgEl = addMessage(role, text, isStatus, userPrompt);
- if (msgEl) msgEl.dataset.msgId = msgId;
- } catch (e) {}
- return msgId;
- }
- function safeRenderResponse(text, role) {
- try {
- addMessage(role === 'user' ? 'user' : 'assistant', text);
- } catch (e) {
- console.error('[RENDER] Chat container failed:', e);
- }
- }
- function restoreChatFromSession() {
- const container = DOM.chatScroll || document.getElementById('chat-scroll');
- if (container) container.innerHTML = '';
- if (Array.isArray(STATE.textChatMessages) && STATE.textChatMessages.length > 0) {
- let lastUserText = '';
- STATE.textChatMessages.forEach(message => {
- if (message.role === 'user') lastUserText = message.text;
- if (message?.text) renderTextChatMessage(message.role === 'assistant' ? 'assistant' : 'user', message.text, false, lastUserText);
- });
- }
- const textRuntime = JobManager.getRuntime('text');
- const liveTasks = [...textRuntime.tasks.values()]
- .filter(task => sanitizePanelKey(task?.action, textRuntime.activeAction || 'gen_text') === 'gen_text')
- .sort((a, b) => (a?.createdAt || 0) - (b?.createdAt || 0));
- liveTasks.forEach(task => {
- const promptText = parseDynamicPrompt(sanitizePromptText(task?.promptText || ''));
- if (promptText) task.chatUserMsgId = renderTextChatMessage('user', promptText);
- const liveText = sanitizePromptText(task?.streamText || '') || task?.longStatus || task?.shortStatus || 'Working...';
- task.chatStatusMsgId = renderTextChatMessage('assistant', liveText, !task?.chatHasStreamText, promptText);
- if (task?.chatHasStreamText && task.chatStatusMsgId) {
- document.querySelector(`[data-msg-id="${task.chatStatusMsgId}"]`)?.querySelector('.chat-msg-toolbar')?.setAttribute('hidden', '');
- }
- });
- syncChatActionBarState();
- scrollTextChatToBottom({ smooth: true });
- }
- async function copyChatMessageText(msgEl) {
- const text = msgEl?._lastText || msgEl?.querySelector('.chat-bubble-ui')?.textContent || '';
- if (!text) return;
- try {
- await navigator.clipboard.writeText(text);
- UI.showToast('Response copied', 'success');
- } catch (e) {
- UI.showToast('Copy failed', 'error');
- }
- }
- function handleChatAction(action, msgEl) {
- if (!action) return;
- if (action === 'repeat') {
- const repeatBtn = msgEl.querySelector('[data-chat-action="repeat"]');
- const userPrompt = repeatBtn?.dataset.userPrompt || '';
- if (userPrompt) {
- setEditorPromptValue(userPrompt);
- if (DOM.prompt) DOM.prompt.focus();
- }
- return;
- }
- if (action === 'copy') {
- void copyChatMessageText(msgEl);
- return;
- }
- if (action === 'new-chat') {
- if (isRuntimeBusy('text')) {
- UI.showToast('Wait until the response finishes', 'warn');
- return;
- }
- resetTextChatSession({ persist: true });
- }
- }
- const createInitialState = () => ({
- isMounted: false,
- items: new Map(),
- refViews: createEmptyRefViewStore(),
- refOrder: [],
- panelOrders: createEmptyPanelOrderStore(),
- currentOrder: [],
- textChatMessages: [],
- activeTextSessionId: null,
- activeTextSessionTmp: undefined,
- currentPanelSessionTmp: undefined,
- historyOrder: [],
- activeLightboxId: null,
- activeRefPromptId: null,
- activeRefPromptAction: null,
- activeRefPromptDraft: '',
- prompts: sanitizePromptStore(DEFAULT_PROMPTS),
- promptDrafts: sanitizePromptStore(DEFAULT_PROMPTS),
- favPrompts: sanitizeFavPromptStore(DEFAULT_FAV_PROMPTS),
- refPrompts: {},
- pendingDoneNotice: false,
- timings: undefined,
- refViewsTmp: undefined,
- panelOrdersTmp: undefined
- });
- function resetState() {
- const initialState = createInitialState();
- Object.assign(initialState, normalizeConfig(parseJsonSafe(localStorage.getItem(APP_CONFIG.STORAGE.CONFIG_KEY), {}), CONFIG_DEFAULTS));
- GenerationRegistry.resetAll();
- for (const key in initialState) {
- if (key in AppState) {
- AppState[key].value = initialState[key];
- }
- }
- Object.keys(DOM).forEach(k => delete DOM[k]);
- }
- resetState();
- const MediaCache = {
- _inFlight: new Map(),
- async getFile(id, type) {
- const key = `${id}_${type}`;
- if (this._inFlight.has(key)) return this._inFlight.get(key);
- const { promise, resolve } = Promise.withResolvers();
- this._inFlight.set(key, promise);
- try {
- const res = await StorageClient.loadOpfsMedia(id, type);
- resolve(res ? res.file : null);
- return res ? res.file : null;
- } catch(e) {
- resolve(null); return null;
- } finally {
- this._inFlight.delete(key);
- }
- },
- };
- 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();
- if (this.dataUrls.size <= limit) return;
- for (const [key, entry] of this.dataUrls.entries()) {
- if (this.dataUrls.size <= limit) break;
- if (!entry?.pending) {
- this.dataUrls.delete(key);
- }
- }
- if (this.dataUrls.size > limit) {
- const firstKey = this.dataUrls.keys().next().value;
- if (firstKey) this.dataUrls.delete(firstKey);
- }
- },
- async get(item) {
- if (!item) return null;
- if (!item.isUploadedRef && item.originalUrl) return item.originalUrl;
- if (item.type === 'video') {
- const res = await StorageClient.loadOpfsMedia(item.id, 'full');
- if (!res || !res.file) return item.originalUrl || null;
- const bytes = await res.file.bytes();
- return `data:${item.mimeType || 'video/mp4'};base64,${bytes.toBase64()}`;
- }
- const key = String(item.id);
- const cached = this._touch(key);
- if (cached) return cached.promise;
- const entry = {
- pending: true,
- promise: (async () => {
- const res = await StorageClient.loadOpfsMedia(item.id, 'full');
- if (!res || !res.file) return item.originalUrl || null;
- const mimeType = item.mimeType || res.mimeType || 'image/jpeg';
- const bytes = await res.file.bytes();
- const base64 = bytes.toBase64();
- return `data:${mimeType};base64,${base64}`;
- })()
- };
- 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 PROMPT_DELIMITER = '\u2063';
- const PromptPipeline = {
- process(text, fallback = '', action = STATE?.action) {
- if (!text) return { api: fallback, storage: fallback };
- const clean = sanitizePromptText(text, fallback);
- let originalText = clean;
- if (clean.includes(PROMPT_DELIMITER)) {
- originalText = clean.split(PROMPT_DELIMITER)[1];
- }
- const apiPrompt = parseDynamicPrompt(originalText);
- if (action !== 'gen_image') {
- return { api: apiPrompt, storage: apiPrompt };
- }
- const textWithoutComments = originalText.replace(/^\s*#[^\n]*\n?/gm, "").trim();
- if (textWithoutComments === apiPrompt && !originalText.includes('{')) {
- return { api: apiPrompt, storage: originalText };
- }
- const commentedApiPrompt = apiPrompt.split('\n').map(line => `# ${line}`).join('\n');
- return { api: apiPrompt, storage: `${commentedApiPrompt}\n${PROMPT_DELIMITER}${originalText}` };
- }
- };
- const Pipeline = {
- getOriginalPrompt(text) {
- if (!text) return '';
- if (text.includes(PROMPT_DELIMITER)) {
- return text.split(PROMPT_DELIMITER)[1].trim();
- }
- return text.trim();
- },
- getDisplayPrompt(text) {
- if (!text) return '';
- if (text.includes(PROMPT_DELIMITER)) {
- return text.split(PROMPT_DELIMITER)[0].replace(/^# ?/gm, '').trim();
- }
- return text.trim();
- },
- normalizePrompt(value, fallback = '', action = STATE?.action) {
- return PromptPipeline.process(value, fallback, action).storage;
- },
- isTransientItem(item) {
- if (item?.isPending && item?.reqId && item?.type === 'video') return false;
- return !!(item?.persist === false || item?.isPending || item?.id?.startsWith('pending-') || item?.filename === 'Pending...');
- },
- isPhantomMeta(meta) {
- if (meta?.isPending && meta?.reqId) return false;
- return String(meta?.id || '').startsWith('pending-') || meta?.filename === 'Pending...';
- },
- makeBatchTimestamp(base, index, total) { return base + (total - index); }
- };
- const GenerationTimings = {
- get(type, res, batchSize) {
- const key = `${type}_${res}_${batchSize}`;
- if (STATE.timings?.[key]) return STATE.timings[key];
- return (res === '2k' ? 6500 : 4000) * Math.pow(batchSize, 0.75);
- },
- update(type, res, batchSize, actualMs) {
- if (!STATE.timings) STATE.timings = {};
- const key = `${type}_${res}_${batchSize}`;
- const prev = this.get(type, res, batchSize);
- STATE.timings[key] = Math.max(1500, (prev * 0.2) + (actualMs * 0.8));
- DBQueue.addAppWrite('timings', STATE.timings, 1000);
- }
- };
- const getCurrentPanelKey = (action = STATE.action) => sanitizePanelKey(action);
- const getPanelOrder = (panelKey = getCurrentPanelKey()) => sanitizeIdListStore(STATE.panelOrders?.[sanitizePanelKey(panelKey)]);
- const syncActivePanelOrderState = (order = STATE.currentOrder, panelKey = getCurrentPanelKey()) => {
- const key = sanitizePanelKey(panelKey);
- const nextOrder = sanitizeIdListStore(order);
- const prevOrder = sanitizeIdListStore(STATE.panelOrders?.[key]);
- if (isSameIdList(prevOrder, nextOrder)) return nextOrder;
- STATE.panelOrders = { ...STATE.panelOrders, [key]: nextOrder };
- return nextOrder;
- };
- const persistPanelOrderStore = () => DBQueue.addAppWrite('currentOrder', {
- schemaVersion: PANEL_STORE_SCHEMA_VERSION,
- panels: Object.fromEntries(PANEL_ACTION_KEYS.map(key =>[key, sanitizeIdListStore(getPanelOrder(key).filter(id => STATE.items.has(String(id))))]))
- }, 90);
- const activatePanelState = (panelKey = getCurrentPanelKey()) => {
- const key = sanitizePanelKey(panelKey);
- const nextOrder = getPanelOrder(key);
- if (!isSameIdList(STATE.currentOrder, nextOrder)) STATE.currentOrder = [...nextOrder];
- else if (STATE.isMounted) PanelManager.update(nextOrder, STATE.items, 'current');
- };
- const setPanelOrderState = (panelKey = getCurrentPanelKey(), nextOrder = []) => {
- const key = sanitizePanelKey(panelKey);
- const safeOrder = sanitizeIdListStore(nextOrder);
- if (key === getCurrentPanelKey()) {
- if (!isSameIdList(STATE.currentOrder, safeOrder)) STATE.currentOrder = safeOrder;
- return safeOrder;
- }
- const prevOrder = sanitizeIdListStore(STATE.panelOrders?.[key]);
- if (isSameIdList(prevOrder, safeOrder)) return safeOrder;
- STATE.panelOrders = { ...STATE.panelOrders, [key]: safeOrder };
- persistPanelOrderStore();
- scheduleCurrentPanelSessionSave();
- return safeOrder;
- };
- const removeIdsFromAllPanels = (ids = []) => {
- const staleIds = sanitizeIdListStore(ids);
- if (!staleIds.length) return false;
- const staleSet = new Set(staleIds);
- let changed = false;
- const nextPanels = { ...STATE.panelOrders };
- PANEL_ACTION_KEYS.forEach(key => {
- const prev = sanitizeIdListStore(nextPanels[key]);
- const filtered = [...new Set(prev)].filter(v => !staleSet.has(String(v)));
- if (!isSameIdList(prev, filtered)) {
- nextPanels[key] = filtered;
- changed = true;
- }
- });
- if (!changed) return false;
- STATE.panelOrders = nextPanels;
- const activeOrder = sanitizeIdListStore(nextPanels[getCurrentPanelKey()]);
- if (!isSameIdList(STATE.currentOrder, activeOrder)) STATE.currentOrder = activeOrder;
- else {
- persistPanelOrderStore();
- scheduleCurrentPanelSessionSave(0);
- }
- return true;
- };
- let _currentPanelSessionTask = null;
- const getPanelSessionSnapshot = (panelKey = getCurrentPanelKey()) => {
- const key = sanitizePanelKey(panelKey);
- const order = getPanelOrder(key).filter(id => STATE.items.has(String(id)));
- const items = order
- .map(id => STATE.items.get(String(id)))
- .filter(item => {
- if (!item) return false;
- if (item.isPending) return item.type === 'video' && !!item.reqId;
- return Pipeline.isTransientItem(item) || item.persist === false;
- })
- .map(item => ({
- id: String(item.id),
- timestamp: item.timestamp || Date.now(),
- type: item.type,
- mimeType: item.mimeType || '',
- ratio: item.ratio || 1,
- originalUrl: item.originalUrl || null,
- filename: item.filename || '',
- downloaded: Boolean(item.downloaded),
- prompt: Pipeline.normalizePrompt(item.prompt),
- responseText: typeof item.responseText === 'string' ? item.responseText : null,
- textModel: typeof item.textModel === 'string' ? item.textModel : null,
- textTurnCount: Number.isFinite(item.textTurnCount) ? item.textTurnCount : 0,
- lqip: item.lqip || null,
- isUploadedRef: Boolean(item.isUploadedRef),
- isPending: Boolean(item.isPending),
- reqId: item.reqId || null,
- taskId: item.taskId || null,
- refSourceId: item.refSourceId != null ? String(item.refSourceId) : null,
- _statusData: sanitizeStatusLinesStore(item._statusData),
- persist: false
- }));
- return { schemaVersion: 2, order, items };
- };
- const getCurrentPanelSessionSnapshot = () => ({
- schemaVersion: PANEL_STORE_SCHEMA_VERSION,
- panels: Object.fromEntries(PANEL_ACTION_KEYS.map(key => [key, getPanelSessionSnapshot(key)]).filter(([, panel]) => panel.order.length > 0 || panel.items.length > 0))
- });
- const writeCurrentPanelSessionSnapshot = (snapshot = getCurrentPanelSessionSnapshot()) => {
- try {
- const safeSnapshot = sanitizePanelSessionMapStore(snapshot);
- if (!safeSnapshot || Object.keys(safeSnapshot.panels || {}).length === 0) {
- sessionStorage.removeItem(APP_CONFIG.STORAGE.CURRENT_PANEL_SESSION_KEY);
- } else {
- sessionStorage.setItem(APP_CONFIG.STORAGE.CURRENT_PANEL_SESSION_KEY, JSON.stringify(safeSnapshot));
- }
- } catch (e) { addLog('WARN', 'Current panel session save failed', e); }
- };
- const scheduleCurrentPanelSessionSave = (delay = 90) => {
- _currentPanelSessionTask?.abort();
- _currentPanelSessionTask = scheduleTask(() => {
- _currentPanelSessionTask = null;
- writeCurrentPanelSessionSnapshot();
- }, delay);
- };
- const flushCurrentPanelSessionSave = () => {
- _currentPanelSessionTask?.abort();
- _currentPanelSessionTask = null;
- writeCurrentPanelSessionSnapshot();
- };
- const clearCurrentPanelSession = () => {
- _currentPanelSessionTask?.abort();
- _currentPanelSessionTask = null;
- try {
- sessionStorage.removeItem(APP_CONFIG.STORAGE.CURRENT_PANEL_SESSION_KEY);
- } catch (e) { addLog('WARN', 'Current panel session clear failed', e); }
- };
- const createFlyweightItem = (rawItem) => ({
- id: String(rawItem.id),
- type: rawItem.type,
- ratio: rawItem.ratio,
- lqip: rawItem.lqip || null,
- mimeType: rawItem.mimeType,
- filename: rawItem.filename,
- isPending: !!rawItem.isPending,
- isUploadedRef: !!rawItem.isUploadedRef,
- downloaded: !!rawItem.downloaded,
- timestamp: rawItem.timestamp,
- shortPrompt: rawItem.prompt ? Pipeline.getDisplayPrompt(rawItem.prompt).substring(0, 100) : (rawItem.shortPrompt || ''),
- originalUrl: rawItem.originalUrl || null,
- textModel: rawItem.textModel || null,
- textTurnCount: rawItem.textTurnCount || 0,
- reqId: rawItem.reqId || null,
- taskId: rawItem.taskId || null,
- refSourceId: rawItem.refSourceId || null,
- _statusData: rawItem._statusData || null,
- active: rawItem.active || false,
- xmpEmbedded: !!rawItem.xmpEmbedded
- });
- 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 };
- if (item.prompt || item.responseText || item.textChatMessages) {
- LazyTextStore.set(item.id, { prompt: item.prompt || '', responseText: item.responseText || '', textChatMessages: item.textChatMessages || null });
- }
- const flyweight = createFlyweightItem(item);
- STATE.items.set(String(item.id), flyweight);
- if (item.persist !== false) DBQueue.add(flyweight);
- if (options.targetArrayName && STATE[options.targetArrayName]) {
- const panelKey = sanitizePanelKey(options.panelKey || getCurrentPanelKey());
- const arr = panelKey === getCurrentPanelKey() ? STATE[options.targetArrayName] : getPanelOrder(panelKey);
- const idx = options.replacePlaceholderId ? arr.indexOf(options.replacePlaceholderId) : -1;
- const nextOrder = idx !== -1 ? arr.with(idx, item.id) : [...arr, item.id];
- if (idx !== -1) this.removeItem(options.replacePlaceholderId);
- setPanelOrderState(panelKey, nextOrder);
- }
- return flyweight;
- },
- addItem(item) {
- if (!item) return;
- if (item.prompt || item.responseText || item.textChatMessages) {
- LazyTextStore.set(item.id, { prompt: item.prompt || '', responseText: item.responseText || '', textChatMessages: item.textChatMessages || null });
- }
- STATE.items.set(String(item.id), createFlyweightItem(item));
- },
- removeItem(id) {
- const key = String(id);
- STATE.items.delete(key);
- LazyTextStore.invalidate(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)))];
- if (STATE.isMounted) PanelManager.prepend(orderedIds, STATE.items, 'history');
- scheduleHistoryMetricsUpdate();
- },
- clearCurrentTransient(ids, panelKey = getCurrentPanelKey()) {
- if (!ids?.length) return;
- const staleIds = ids.map(String), staleSet = new Set(staleIds);
- const nextOrder = getPanelOrder(panelKey).filter(id => !staleSet.has(String(id)));
- setPanelOrderState(panelKey, nextOrder);
- staleIds.forEach(id => this.removeItem(id));
- },
- getRefItems(action = STATE.action, refMode = STATE.refMode) {
- return getRefOrderForView(action, refMode).map(id => getRefItemForView(id, action, refMode)).filter(Boolean);
- }
- };
- let wipePromise = null, lastWipeTime = 0, _skipPagehidePersistence = false;
- 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 modern nuclear wipe (Tokens/Caches)...');
- wipePromise = (async () => {
- const clearCookiesGM = (url) => new Promise(res => {
- if(typeof GM_cookie === 'undefined') return 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([clearCookiesGM(window.location.href), clearCookiesGM('https://x.ai')]);
- if (window.cookieStore) {
- try {
- const cookies = await cookieStore.getAll();
- await Promise.allSettled(cookies.map(c => cookieStore.delete({ name: c.name, domain: c.domain, path: c.path })));
- } catch(e) { addLog('WARN', 'cookieStore wipe failed', e); }
- }
- document.cookie.split(';').forEach(c => {
- const n = c.split('=')[0].trim();['.x.ai', 'console.x.ai', ''].forEach(d => {['/', '/playground/imagine', '/playground/voice', '/playground/voice/text-to-speech', ''].forEach(p => {
- document.cookie = `${n}=;max-age=0;path=${p}${d ? ';domain=' + d : ''}`;
- });
- });
- });
- if ('caches' in window) {
- try {
- const cacheKeys = await caches.keys();
- await Promise.allSettled(cacheKeys.map(k => caches.delete(k)));
- } catch(e) {}
- }
- try { await fetch('https://console.x.ai/playground/imagine', { credentials: 'include', cache: 'reload' }); } catch(e) {}
- try { await fetch('https://console.x.ai/playground/voice/text-to-speech', { credentials: 'include', cache: 'reload' }); } catch(e) {}
- })();
- await wipePromise; wipePromise = null;
- }
- const OPFS = {
- _dirHandle: null,
- 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 write(id, type, blob) {
- let w;
- try {
- const dir = await this.initDir();
- const fh = await dir.getFileHandle(`${id}_${type}`, { create: true });
- w = await fh.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 {
- const dir = await this.initDir();
- const fh = await dir.getFileHandle(`${id}_${type}`);
- return await fh.getFile();
- } catch (e) { return null; }
- },
- async clear() {
- try {
- const root = await navigator.storage.getDirectory();
- await root.removeEntry(APP_CONFIG.STORAGE.OPFS_DIR, { recursive: true }).catch(()=>{});
- this._dirHandle = null;
- } catch (e) {}
- }
- };
- const DBQueue = {
- _pendingItems: new Map(), _appWrites: new Map(), _appWriteData: new Map(), _isFlushing: false,
- _serializeItem(item) {
- if (!item || Pipeline.isTransientItem(item)) return null;
- const heavy = LazyTextStore._cache.get(String(item.id));
- const prompt = heavy ? heavy.prompt : undefined;
- const responseText = heavy ? heavy.responseText : undefined;
- const textChatMessages = heavy ? heavy.textChatMessages : undefined;
- if (item.type === 'text') {
- return {
- id: String(item.id),
- timestamp: item.timestamp,
- type: item.type,
- mimeType: item.mimeType || 'text/plain',
- ratio: 1,
- filename: item.filename,
- downloaded: item.downloaded || false,
- prompt: prompt !== undefined ? Pipeline.normalizePrompt(prompt) : undefined,
- shortPrompt: item.shortPrompt || '',
- responseText: responseText,
- textChatMessages: textChatMessages,
- textModel: item.textModel,
- textTurnCount: Number.isFinite(item.textTurnCount) ? item.textTurnCount : 0,
- lqip: item.lqip,
- isUploadedRef: false,
- isPending: false
- };
- }
- return {
- id: String(item.id),
- timestamp: item.timestamp,
- type: item.type,
- mimeType: item.mimeType,
- ratio: item.ratio || 1,
- originalUrl: item.type === 'video' ? item.originalUrl : (item.originalUrl || null),
- filename: item.filename,
- downloaded: item.downloaded || false,
- prompt: prompt !== undefined ? Pipeline.normalizePrompt(prompt) : undefined,
- shortPrompt: item.shortPrompt || '',
- lqip: item.lqip,
- isUploadedRef: item.isUploadedRef || false,
- isPending: item.isPending || false,
- reqId: item.reqId || null,
- taskId: item.taskId || null,
- refSourceId: item.refSourceId || null,
- _statusData: item._statusData || null,
- textModel: item.textModel || null,
- textTurnCount: Number.isFinite(item.textTurnCount) ? item.textTurnCount : 0,
- xmpEmbedded: !!item.xmpEmbedded
- };
- },
- add(item) {
- const serialized = this._serializeItem(item);
- if (!serialized) return;
- this._pendingItems.set(serialized.id, serialized);
- this._queueFlush();
- },
- forgetItem(id) { this._pendingItems.delete(String(id)); },
- forgetItems(ids) { ids.forEach(id => this.forgetItem(id)); },
- async _queueFlush() {
- if (this._isFlushing || this._pendingItems.size === 0) return;
- this._isFlushing = true;
- try {
- await scheduler.postTask(async () => {
- const itemsToSave = [];
- TaskChunker.reset();
- for (const item of this._pendingItems.values()) {
- if (Pipeline.isPhantomMeta(item)) continue;
- itemsToSave.push(item);
- await TaskChunker.yieldIfNeeded();
- }
- this._pendingItems.clear();
- if (itemsToSave.length > 0) {
- await StorageClient.saveItemsBulk(itemsToSave);
- runBg(updateHistoryMetrics);
- scheduleDiskUpdate();
- }
- }, { priority: 'background' });
- } catch(e) { addLog('ERROR', 'DBQueue flush', e); } finally { this._isFlushing = false; if (this._pendingItems.size > 0) this._queueFlush(); }
- },
- addAppWrite(key, dataOrFn, delay = 500) {
- const prev = this._appWrites.get(key);
- prev?.controller?.abort();
- const controller = new AbortController();
- scheduler.postTask(async () => {
- if (controller.signal.aborted) return;
- const resolvedData = typeof dataOrFn === 'function' ? dataOrFn() : dataOrFn;
- const snapshot = cloneSerializable(resolvedData, resolvedData);
- this._appWrites.delete(key);
- this._appWriteData.delete(key);
- await StorageClient.saveMeta(key, snapshot);
- }, { priority: 'background', delay, signal: controller.signal }).catch(() => {});
- this._appWrites.set(key, { controller });
- this._appWriteData.set(key, dataOrFn);
- },
- async flushAppWrites(keys = null) {
- const allow = keys ? new Set((Array.isArray(keys) ? keys : [keys]).map(String)) : null;
- const jobs = [];
- const flushedKeys = [];
- for (const [key, entry] of this._appWrites.entries()) {
- if (allow && !allow.has(String(key))) continue;
- entry?.controller?.abort();
- const dataOrFn = this._appWriteData.get(key);
- if (dataOrFn !== undefined) {
- const resolvedData = typeof dataOrFn === 'function' ? dataOrFn() : dataOrFn;
- const snapshot = cloneSerializable(resolvedData, resolvedData);
- jobs.push(StorageClient.saveMeta(key, snapshot));
- }
- flushedKeys.push(key);
- }
- flushedKeys.forEach(key => {
- this._appWrites.delete(key);
- this._appWriteData.delete(key);
- });
- if (jobs.length > 0) await Promise.allSettled(jobs);
- },
- clearPending() {
- this._pendingItems.clear();
- this._appWrites.forEach(entry => entry?.controller?.abort());
- this._appWrites.clear();
- this._appWriteData.clear();
- this._isFlushing = false;
- },
- dropAppWrites(keys = []) {
- const safeKeys = [...new Set((Array.isArray(keys) ? keys : [keys]).map(key => typeof key === 'string' ? key : '').filter(Boolean))];
- safeKeys.forEach(key => {
- const entry = this._appWrites.get(key);
- entry?.controller?.abort();
- this._appWrites.delete(key);
- this._appWriteData.delete(key);
- });
- }
- };
- const createAppWritePersistor = (key, read, delay = 500) => () => DBQueue.addAppWrite(key, read, delay);
- const persistEffectivePrompts = createAppWritePersistor('prompts', () => sanitizePromptStore(STATE.prompts));
- const persistPromptDrafts = createAppWritePersistor('promptDrafts', () => sanitizePromptStore(STATE.promptDrafts));
- const persistRefPrompts = createAppWritePersistor('refPrompts', () => sanitizeRefPromptStore(STATE.refPrompts));
- const persistFavPrompts = createAppWritePersistor('favPrompts', () => sanitizeFavPromptStore(STATE.favPrompts));
- const persistActiveTextSessionId = createAppWritePersistor(ACTIVE_TEXT_SESSION_KEY, () => STATE.activeTextSessionId || null, 60);
- const persistPromptStateStores = () => {
- persistEffectivePrompts();
- persistPromptDrafts();
- persistRefPrompts();
- persistFavPrompts();
- };
- const persistTextSessionConversation = (id, messages, model = STATE.textModel) => {
- const sessionId = String(id || '');
- if (!sessionId) return;
- DBQueue.addAppWrite(getTextSessionStoreKey(sessionId), {
- schemaVersion: 1,
- textModel: typeof model === 'string' ? model : null,
- updatedAt: Date.now(),
- messages: sanitizeTextConversationStore(messages)
- }, 90);
- };
- const syncTextSessionDownload = (item, messages = STATE.textChatMessages) => {
- if (!item?.id) return;
- const transcript = buildTextSessionTranscript(messages, item.textModel);
- runBg(() => OPFS.write(item.id, 'full', new Blob([transcript], { type: 'text/plain' })));
- };
- const buildTextSessionItem = (id = crypto.randomUUID(), createdAt = Date.now()) => ({
- id,
- type: 'text',
- mimeType: 'text/plain',
- timestamp: createdAt,
- prompt: '',
- responseText: '',
- textModel: STATE.textModel || 'grok-4.20',
- textTurnCount: 0,
- filename: `Grok Chat - ${makeTimestamp()}.txt`,
- ratio: 1,
- originalUrl: null,
- downloaded: false,
- lqip: '#1e1e2e',
- isUploadedRef: false
- });
- const setPromptStoreValue = (storeKey, action, value = '', persist = null) => {
- const nextValue = normalizePromptDraft(value);
- if (STATE[storeKey][action] === nextValue) return nextValue;
- STATE[storeKey] = { ...STATE[storeKey], [action]: nextValue };
- persist?.();
- return nextValue;
- };
- const getActionFavorites = (action = STATE.action) => STATE.favPrompts[action] || [];
- const setActionFavorites = (list = [], action = STATE.action) => {
- STATE.favPrompts = { ...STATE.favPrompts, [action]: list };
- persistFavPrompts();
- return list;
- };
- const addActionFavorite = (text, action = STATE.action) => {
- const originalText = Pipeline.getOriginalPrompt(text);
- const nextText = String(originalText || '').trim();
- if (!nextText) return false;
- const current = getActionFavorites(action);
- if (current.includes(nextText)) return false;
- setActionFavorites([...current, nextText], action);
- return true;
- };
- const removeActionFavorite = (text, action = STATE.action) => {
- const nextText = String(text || '').trim();
- const current = getActionFavorites(action);
- const next = current.filter(entry => entry !== nextText);
- if (next.length === current.length) return false;
- setActionFavorites(next, action);
- return true;
- };
- const setPromptValueForAction = (action, value = '', { persist = true } = {}) => {
- const nextValue = normalizePromptDraft(value);
- setPromptStoreValue('promptDrafts', action, nextValue);
- setPromptStoreValue('prompts', action, nextValue);
- if (persist) { persistPromptDrafts(); persistEffectivePrompts(); }
- return nextValue;
- };
- const clearTextPromptInput = ({ focus = false, syncEditor = STATE.action === 'gen_text' } = {}) => {
- setPromptValueForAction('gen_text', '');
- if (!syncEditor) return;
- setEditorPromptValue('');
- DOM.promptUpdateLabel?.classList.remove('show', 'saved');
- DOM.generateBtn?.classList.remove('is-dirty', 'flash-saved');
- if (focus && DOM.prompt) requestAnimationFrame(() => DOM.prompt.focus());
- };
- const applyTextModelSelection = (value) => {
- const nextValue = String(value || '').trim();
- if (!nextValue) return;
- STATE.textModel = nextValue;
- const control = UI_CONTROL_BY_ID.textModel;
- const button = control ? document.getElementById(control.buttonId) : null;
- const label = control?.labelId ? document.getElementById(control.labelId) : null;
- if (button) button.dataset.value = nextValue;
- if (label) label.textContent = nextValue;
- if (control?.popoverId) setActivePopoverItem(document.getElementById(control.popoverId), nextValue);
- updateActionUI();
- };
- const setActiveTextSessionState = (sessionId = null, messages = [], { persist = true } = {}) => {
- STATE.activeTextSessionId = sessionId ? String(sessionId) : null;
- STATE.textChatMessages = sanitizeTextConversationStore(messages);
- if (persist) persistActiveTextSessionId();
- };
- const resetTextChatSession = ({ persist = true } = {}) => {
- setActiveTextSessionState(null, [], { persist });
- restoreChatFromSession();
- };
- const commitTextSession = (messages, { sessionId = STATE.activeTextSessionId, model = STATE.textModel, updatedAt = Date.now() } = {}) => {
- const cleanMessages = sanitizeTextConversationStore(Array.isArray(messages) ? [...messages] : messages);
- const id = String(sessionId || crypto.randomUUID());
- const existing = STATE.items.get(id);
- let item = existing || buildTextSessionItem(id, updatedAt);
- if (!item.filename || /^Grok Text\b/.test(item.filename)) item.filename = `Grok Chat - ${makeTimestamp()}.txt`;
- summarizeTextSessionItem(item, cleanMessages, { updatedAt, model });
- if (!existing) item = Store.commitNewItem(item, null);
- else DBQueue.add(item);
- Store.prependHistory([id]);
- setActiveTextSessionState(id, cleanMessages, { persist: true });
- persistTextSessionConversation(id, cleanMessages, item.textModel);
- syncTextSessionDownload(item, cleanMessages);
- return item;
- };
- const loadTextSessionMessages = async (itemOrId) => {
- const item = typeof itemOrId === 'object' ? itemOrId : STATE.items.get(String(itemOrId));
- const itemId = String(item?.id ?? itemOrId ?? '');
- if (!itemId) return [];
- const raw = await StorageClient.loadMeta(getTextSessionStoreKey(itemId));
- const messages = sanitizeTextConversationStore(raw?.messages);
- return messages.length > 0 ? messages : await getTextSessionMessagesFromItemAsync(item);
- };
- const clearTextSessionArtifacts = async (ids = []) => {
- const safeIds = sanitizeIdListStore(ids);
- if (!safeIds.length) return;
- const dataKeys = safeIds.map(getTextSessionStoreKey);
- DBQueue.dropAppWrites(dataKeys);
- if (safeIds.includes(String(STATE.activeTextSessionId || ''))) {
- DBQueue.dropAppWrites(ACTIVE_TEXT_SESSION_KEY);
- setActiveTextSessionState(null, [], { persist: true });
- restoreChatFromSession();
- dataKeys.push(ACTIVE_TEXT_SESSION_KEY);
- }
- await StorageClient.deleteMeta(dataKeys).catch(e => addLog('WARN', 'Delete text session data failed', { ids: safeIds, error: e?.message || e }));
- };
- 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 replaceEditorPromptValue = (value = '') => {
- const prompt = sanitizePromptText(value);
- if (!DOM.prompt || !prompt) return false;
- insertTextAtCursor(DOM.prompt, prompt, true);
- return true;
- };
- const getActiveRefPromptBaseline = () => getDraftActionPrompt(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()) => {
- return setPromptStoreValue('promptDrafts', action, value, persistPromptDrafts);
- };
- const commitActionPrompt = (action = STATE.action, value = getDraftActionPrompt(action)) => {
- return setPromptStoreValue('prompts', action, value, persistEffectivePrompts);
- };
- const getTaskPrompt = (action = STATE.action) => {
- const stored = sanitizePromptText(getStoredActionPrompt(action));
- if (stored || STATE.isGenerating) return stored;
- return sanitizePromptText(getDraftActionPrompt(action));
- };
- const PromptSync = {
- controllers: new Map(),
- captureEditor(action = STATE.action, { scheduleCommit = false } = {}) {
- 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 controller = new AbortController();
- scheduler.postTask(() => {
- if (controller.signal.aborted) return;
- this.controllers.delete(action);
- commitActionPrompt(action, normalized);
- }, { priority: 'background', delay, signal: controller.signal }).catch(() => {});
- this.controllers.set(action, controller);
- return normalized;
- },
- commitNow(action = STATE.action) {
- this.cancel(action);
- return commitActionPrompt(action, getDraftActionPrompt(action));
- },
- cancel(action = STATE.action) {
- this.controllers.get(action)?.abort();
- this.controllers.delete(action);
- },
- clear() {
- this.controllers.forEach(controller => controller.abort());
- this.controllers.clear();
- }
- };
- const deleteRefPrompts = (ids = []) => {
- const next = { ...STATE.refPrompts };
- sanitizeIdListStore(ids).forEach(id => delete next[id]);
- STATE.refPrompts = next;
- persistRefPrompts();
- };
- const setRefPrompt = (id, action, prompt) => {
- const currentRefData = STATE.refPrompts[id] || {};
- if (prompt) {
- STATE.refPrompts = { ...STATE.refPrompts, [id]: { ...currentRefData, [action]: prompt } };
- } else {
- const nextData = { ...currentRefData };
- delete nextData[action];
- if (Object.keys(nextData).length === 0) {
- const nextPrompts = { ...STATE.refPrompts };
- delete nextPrompts[id];
- STATE.refPrompts = nextPrompts;
- } else {
- STATE.refPrompts = { ...STATE.refPrompts, [id]: nextData };
- }
- }
- persistRefPrompts();
- };
- const getAspectRatioOption = (label = STATE.currentAr) => ASPECT_RATIO_MAP.get(label.trim()) || ASPECT_RATIO_MAP.get('Auto');
- const getAspectRatioPopoverOptions = (action = STATE.action) => {
- const isVideo = action === 'gen_video';
- return ASPECT_RATIO_POPOVER_LAYOUT.flatMap(pair => pair.map(label => {
- const option = getAspectRatioOption(label);
- return {
- val: option.label,
- text: option.label,
- icon: UIUtils.createArIcon(option.w, option.h, option.isAuto, option.isRef),
- disabled: isVideo && !option.isAuto && (option.imageOnly || !ALLOWED_VIDEO_ARS.has(option.label.trim()))
- };
- }));
- };
- const syncActionLabel = (action = STATE.action) => {
- if (DOM.actionDropdown) DOM.actionDropdown.dataset.value = action;
- if (DOM.labelMasterAction) DOM.labelMasterAction.innerText = (ACTION_SPEC[action] ?? ACTION_SPEC.gen_image).label;
- setActivePopoverItem(document.getElementById('popover-action'), action);
- };
- let _dirtyStateTask = null;
- const updateDirtyStateUI = () => {
- if (!DOM.promptUpdateLabel || !DOM.generateBtn || STATE.activeRefPromptId) return;
- if (STATE.action === 'gen_text' || !STATE.isGenerating) {
- DOM.promptUpdateLabel.classList.remove('show', 'saved');
- DOM.generateBtn.classList.remove('is-dirty');
- return;
- }
- _dirtyStateTask?.abort();
- _dirtyStateTask = new AbortController();
- scheduler.postTask(() => {
- if (_dirtyStateTask.signal.aborted) return;
- const editorVal = sanitizePromptText(DOM.prompt?.value || '');
- const committedVal = sanitizePromptText(getStoredActionPrompt(STATE.action));
- const isDirty = editorVal !== committedVal;
- DOM.promptUpdateLabel.classList.toggle('saved', !isDirty);
- DOM.promptUpdateLabel.classList.toggle('show', isDirty);
- DOM.generateBtn.classList.toggle('is-dirty', isDirty);
- if (isDirty) DOM.promptUpdateLabel.innerText = 'Draft modified: Press Enter to apply';
- }, { priority: 'user-visible', delay: 150, signal: _dirtyStateTask.signal }).catch(() => {});
- };
- const syncDOMControls = (stateKeys) => {
- const keys = Array.isArray(stateKeys) ? stateKeys : [stateKeys];
- keys.forEach(stateKey => {
- const control = getControlByStateKey(stateKey);
- if (!control) return;
- const value = STATE[stateKey] ?? control.defaultValue;
- const valueStr = String(value);
- const elements = document.querySelectorAll(`[data-state-key="${stateKey}"]`);
- if (!elements.length) return;
- elements.forEach(el => {
- if (control.render === 'aspect-ratio') {
- el.dataset.value = valueStr;
- const option = getAspectRatioOption(valueStr);
- const iconContainer = el.querySelector('#ar-selected-icon');
- const textNode = el.querySelector('#ar-selected-text');
- if (iconContainer) {
- const iconMarkup = UIUtils.createArIcon(option.w, option.h, option.isAuto, option.isRef);
- iconContainer.replaceChildren(...(iconMarkup ? [createMarkupFragment(iconMarkup)] : []));
- iconContainer.style.display = iconMarkup ? 'flex' : 'none';
- }
- if (textNode) textNode.innerText = option.label.trim();
- return;
- }
- if (control.kind === 'toggle') {
- const input = el.tagName === 'INPUT' && el.type === 'checkbox' ? el : el.querySelector('input[type="checkbox"]');
- if (input) input.checked = !!value;
- else {
- el.dataset.value = valueStr;
- el.innerText = control.labels?.[valueStr] ?? valueStr;
- }
- } else if (control.kind === 'dropdown') {
- el.dataset.value = valueStr;
- const options = typeof control.options === 'function' ? control.options() : (control.options || []);
- const activeOpt = options.find(opt => String(opt.val) === valueStr) || options[0];
- if (control.labelId && activeOpt) {
- const labelNodes = el.querySelectorAll(`[id="${control.labelId}"]`);
- labelNodes.forEach(ln => ln.textContent = activeOpt.text);
- }
- if (control.popoverId) setActivePopoverItem(document.getElementById(control.popoverId), valueStr);
- } else if (control.kind === 'number' || control.kind === 'number-inline') {
- el.dataset.value = valueStr;
- const input = el.tagName === 'INPUT' ? el : el.querySelector('input');
- if (input) input.value = valueStr;
- } else if (control.kind === 'button') {
- el.dataset.value = valueStr;
- }
- });
- });
- };
- const syncLayoutWidths = () => {
- if (DOM.colLeft) DOM.colLeft.style.width = Math.max(260, STATE.leftWidth) + 'px';
- if (DOM.colRight) DOM.colRight.style.width = STATE.rightWidth + 'px';
- };
- const escapeSelectorValue = (value) => CSS.escape(String(value));
- const setActivePopoverItem = (popover, value) => {
- if (!popover) return;
- const stringValue = String(value);
- const matches = popover.querySelectorAll(`.popover-item[data-val="${escapeSelectorValue(stringValue)}"]`);
- const activeItems = popover.querySelectorAll('.popover-item.active');
- if (matches.length && activeItems.length === matches.length && [...matches].every(node => node.classList.contains('active'))) return;
- activeItems.forEach(node => node.classList.remove('active'));
- matches.forEach(node => node.classList.add('active'));
- };
- const buildPopoverOptionHtml = (d = {}) => `<div class="${d.className ?? ''}" data-action="select-popover-option" data-val="${d.val ?? ''}" title="${d.title ?? ''}"><div class="popover-icon">${d.iconHtml ?? ''}</div><span>${d.text ?? ''}</span></div>`;
- const buildPopoverOptionItem = (selectedValue, opt) => {
- const optionValue = String(opt.val);
- const classNames = ['popover-item', String(selectedValue) === optionValue ? 'active' : '', opt.disabled ? 'disabled' : '', opt.icon ? 'has-icon' : 'no-icon'].filter(Boolean);
- const frag = document.createRange().createContextualFragment(buildPopoverOptionHtml({
- val: optionValue,
- className: classNames.join(' '),
- title: opt.title || '',
- iconHtml: '',
- text: opt.text
- }));
- if (opt.icon) {
- const iconDiv = frag.querySelector('.popover-icon');
- if (iconDiv) iconDiv.append(createMarkupFragment(opt.icon));
- } else {
- const iconDiv = frag.querySelector('.popover-icon');
- if (iconDiv) iconDiv.remove();
- }
- return frag;
- };
- const renderPopoverOptions = (popover, selectedValue, opts = []) => {
- const fragment = document.createDocumentFragment();
- opts.forEach(opt => fragment.appendChild(buildPopoverOptionItem(selectedValue, opt)));
- popover.replaceChildren(fragment);
- };
- const buildExportState = () => ({
- schemaVersion: APP_CONFIG.STORAGE.EXPORT_SCHEMA_VERSION,
- config: normalizeConfig(getConfigSnapshot(), CONFIG_DEFAULTS),
- prompts: sanitizePromptStore(STATE.prompts),
- promptDrafts: sanitizePromptStore(STATE.promptDrafts, STATE.prompts),
- refPrompts: sanitizeRefPromptStore(STATE.refPrompts),
- favPrompts: sanitizeFavPromptStore(STATE.favPrompts)
- });
- const getRefViewEntry = (action = STATE.action, refMode = STATE.refMode) => {
- const viewKey = getRefViewKey(action, refMode);
- if (!STATE.refViews[viewKey]) STATE.refViews[viewKey] = createEmptyRefViewEntry();
- return STATE.refViews[viewKey];
- };
- const normalizeRefViewEntryForAction = (entry, action = STATE.action) => {
- const poolType = getRefPoolTypeForAction(action);
- const order = sanitizeIdListStore(entry?.order).filter(id => {
- const item = STATE.items.get(String(id));
- return item && isRefCompatibleWithPoolType(item, poolType);
- });
- const orderSet = new Set(order);
- const activeKeys = Object.entries(entry?.active || {}).filter(([id, val]) => val === true && orderSet.has(String(id))).map(([id]) => [String(id), true]);
- const active = Object.fromEntries(activeKeys);
- const waitUntilKeys = Object.entries(entry?.waitUntil || {}).filter(([id, val]) => orderSet.has(String(id)) && Number.isFinite(val) && val > 0).map(([id, val]) => [String(id), val]);
- const waitUntil = Object.fromEntries(waitUntilKeys);
- return { order, active, waitUntil };
- };
- const getMutableRefViewEntry = (action = STATE.action, refMode = STATE.refMode) => {
- const viewKey = getRefViewKey(action, refMode);
- const normalized = normalizeRefViewEntryForAction(getRefViewEntry(action, refMode), action);
- STATE.refViews[viewKey] = normalized;
- return normalized;
- };
- const getRefOrderForView = (action = STATE.action, refMode = STATE.refMode) => [...getMutableRefViewEntry(action, refMode).order];
- const getRefItemStateForView = (id, action = STATE.action, refMode = STATE.refMode) => {
- const key = String(id);
- const entry = getMutableRefViewEntry(action, refMode);
- return { active: entry.active[key] === true, readyAt: Number(entry.waitUntil[key]) || 0 };
- };
- const getRefItemForView = (id, action = STATE.action, refMode = STATE.refMode) => {
- const key = String(id);
- const item = STATE.items.get(key);
- if (!item || !isRefCompatibleWithPoolType(item, getRefPoolTypeForAction(action))) return null;
- return { ...item, ...getRefItemStateForView(key, action, refMode) };
- };
- const isVisibleRefView = (action = STATE.action, refMode = STATE.refMode) => getRefViewKey(action, refMode) === getRefViewKey(STATE.action, STATE.refMode);
- const shouldDelayReferenceActivation = (action = STATE.action) => {
- const safeAction = sanitizePanelKey(action, STATE.action);
- if (JobManager.getLane(safeAction) !== 'media') return false;
- const mediaRuntime = JobManager.getRuntime('media');
- return mediaRuntime.isGenerating && mediaRuntime.activeAction === safeAction;
- };
- const syncVisibleRefStateFromView = (action = STATE.action, refMode = STATE.refMode) => {
- const safeAction = sanitizePanelKey(action, STATE.action);
- const entry = getMutableRefViewEntry(safeAction, refMode);
- const order = [...entry.order];
- const orderSet = new Set(order);
- const poolType = getRefPoolTypeForAction(safeAction);
- STATE.refOrder = order;
- STATE.items.forEach((item, id) => {
- if (!isRefCompatibleWithPoolType(item, poolType)) return;
- const key = String(id);
- if (!orderSet.has(key)) {
- item.active = false;
- item.readyAt = 0;
- return;
- }
- item.active = entry.active[key] === true;
- item.readyAt = Number(entry.waitUntil[key]) || 0;
- });
- return entry;
- };
- const persistRefViews = () => DBQueue.addAppWrite('refViews', serializeRefViewStore(STATE.refViews), 90);
- const registerReferenceAcrossViews = (id, action = STATE.action) => {
- const key = String(id);
- const poolType = getRefPoolTypeForAction(action);
- let added = false;
- getRefViewKeysForPoolType(poolType).forEach(viewKey => {
- const [viewAction, viewMode] = viewKey.split(':');
- const entry = getMutableRefViewEntry(viewAction, viewMode);
- if (!entry.order.includes(key)) {
- entry.order.push(key);
- added = true;
- }
- });
- return added;
- };
- const activateReferenceItem = (item, { action = STATE.action, refMode = STATE.refMode, exclusive = false } = {}) => {
- const refItem = item && typeof item === 'object' ? item : STATE.items.get(String(item));
- if (!refItem) return false;
- const key = String(refItem.id);
- const entry = getMutableRefViewEntry(action, refMode);
- const wasInactive = entry.active[key] !== true;
- if (exclusive) {
- entry.order.forEach(rid => {
- delete entry.active[String(rid)];
- delete entry.waitUntil[String(rid)];
- });
- }
- entry.active[key] = true;
- if (shouldDelayReferenceActivation(action) && wasInactive) entry.waitUntil[key] = Date.now() + 10000;
- else if (!(Number.isFinite(entry.waitUntil[key]) && entry.waitUntil[key] > Date.now())) delete entry.waitUntil[key];
- if (isVisibleRefView(action, refMode)) {
- refItem.active = true;
- refItem.readyAt = Number(entry.waitUntil[key]) || 0;
- }
- return wasInactive;
- };
- const abortTasksForRef = (refId, action = STATE.action) => {
- const targetAction = sanitizePanelKey(action, STATE.action);
- JobManager.findTasksByRef(refId).forEach(task => {
- const taskAction = sanitizePanelKey(task.action || JobManager.getRuntime(task.lane).activeAction || targetAction, targetAction);
- if (taskAction === targetAction) task.controller.abort();
- });
- };
- const updateReferenceOrder = (id, { position = 'front', exclusiveActive = false, enforceLimit = false, forceRebuild = true, action = STATE.action, refMode = STATE.refMode, activate = true } = {}) => {
- const key = String(id);
- const targetAction = sanitizePanelKey(action, STATE.action);
- const targetRefMode = normalizeRefModeForAction(targetAction, refMode);
- const item = STATE.items.get(key);
- if (!item || !isRefCompatibleWithPoolType(item, getRefPoolTypeForAction(targetAction))) return false;
- const targetEntry = getMutableRefViewEntry(targetAction, targetRefMode);
- const hadTargetRef = targetEntry.order.includes(key);
- const targetMaxRefs = (ACTION_SPEC[targetAction] ?? ACTION_SPEC.gen_image).maxRefs?.({ action: targetAction, refMode: targetRefMode }) ?? 0;
- if (targetMaxRefs === 0) return false;
- if (enforceLimit && !hadTargetRef && targetEntry.order.length >= Math.min(APP_CONFIG.LIMITS.MAX_REFS_UI, targetMaxRefs)) return false;
- const isNewReference = registerReferenceAcrossViews(key, targetAction);
- const nextEntry = getMutableRefViewEntry(targetAction, targetRefMode);
- const isFront = position === 'front';
- const wasEdge = isFront ? nextEntry.order[0] === key : nextEntry.order[nextEntry.order.length - 1] === key;
- const reorder = nextEntry.order.filter(rid => String(rid) !== key);
- if (isFront) reorder.unshift(key);
- else reorder.push(key);
- nextEntry.order = reorder;
- if (exclusiveActive) {
- nextEntry.order.forEach(rid => {
- delete nextEntry.active[String(rid)];
- delete nextEntry.waitUntil[String(rid)];
- });
- }
- if (activate) {
- const wasInactive = nextEntry.active[key] !== true;
- nextEntry.active[key] = true;
- if (shouldDelayReferenceActivation(targetAction) && (wasInactive || isNewReference || exclusiveActive)) nextEntry.waitUntil[key] = Date.now() + 10000;
- else if (!(Number.isFinite(nextEntry.waitUntil[key]) && nextEntry.waitUntil[key] > Date.now())) delete nextEntry.waitUntil[key];
- } else {
- delete nextEntry.active[key];
- delete nextEntry.waitUntil[key];
- }
- updateRefState({ forceRebuild: forceRebuild && isVisibleRefView(targetAction, targetRefMode) && !wasEdge });
- if (isVisibleRefView(targetAction, targetRefMode) && DOM.refList) DOM.refList.scrollTop = isFront ? 0 : DOM.refList.scrollHeight;
- return true;
- };
- const moveReferenceToFront = (id, opts = {}) => updateReferenceOrder(id, { ...opts, position: 'front' });
- const appendReferenceToEnd = (id, opts = {}) => updateReferenceOrder(id, { ...opts, position: 'end' });
- const pruneReferenceState = (ids, { closePrompt = true } = {}) => {
- const keys = sanitizeIdListStore(ids);
- if (!keys.length) return false;
- const removed = new Set(keys);
- let changed = false;
- REF_VIEW_KEYS.forEach(viewKey => {
- const [viewAction, viewMode] = viewKey.split(':');
- const entry = getMutableRefViewEntry(viewAction, viewMode);
- const nextOrder = [...new Set(entry.order)].filter(v => !removed.has(v));
- if (!isSameIdList(entry.order, nextOrder)) {
- entry.order = nextOrder;
- changed = true;
- }
- keys.forEach(key => {
- if (entry.active[key] === true || entry.waitUntil[key]) changed = true;
- delete entry.active[key];
- delete entry.waitUntil[key];
- });
- });
- if (!changed) return false;
- deleteRefPrompts(keys);
- updateRefState();
- if (closePrompt && STATE.activeRefPromptId && removed.has(String(STATE.activeRefPromptId))) saveAndCloseRefPrompt(true);
- return true;
- };
- const syncRefCardsPromptFlags = (action = STATE.action) => {
- CardRegistry.maps.reference.forEach((card, id) => {
- card.setReferencePromptFlag(!!STATE.refPrompts[id]?.[action]);
- });
- };
- function updateRefState({ forceRebuild = false } = {}) {
- REF_VIEW_KEYS.forEach(viewKey => {
- const [viewAction, viewMode] = viewKey.split(':');
- STATE.refViews[viewKey] = normalizeRefViewEntryForAction(getRefViewEntry(viewAction, viewMode), viewAction);
- });
- syncVisibleRefStateFromView();
- persistRefViews();
- if (forceRebuild && DOM.refList) PanelManager.clear(DOM.refList, 'reference');
- renderRefList();
- updateUploadPlaceholderState();
- if (isRuntimeBusy('media') && JobManager.getLane(STATE.action) === 'media') wakeUpQueue('media');
- }
- 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);
- for (const store of Object.values(this.maps)) {
- const card = store.get(key);
- if (card) visitor(card);
- }
- },
- setDownloaded(id, downloaded = true) {
- this.forEachById(id, card => { if (downloaded) card._internals.states.add('downloaded'); else card._internals.states.delete('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]?.forEach(card => card.unbind?.());
- this.maps[context]?.clear();
- } else {
- for (const map of Object.values(this.maps)) {
- map.forEach(card => card.unbind?.());
- map.clear();
- }
- }
- }
- };
- const CardObserverHub = (() => {
- const preloadObserver = new IntersectionObserver((entries) => {
- entries.forEach(entry => {
- const card = entry.target;
- if (!card._bound) return;
- if (entry.isIntersecting) {
- card._isPreloadIntersecting = true;
- if (card._item?.type === 'image') card.setLoadLevel('full');
- else if (card._mediaState === 'none') card.setLoadLevel('thumb');
- } else {
- card._isPreloadIntersecting = false;
- card.setLoadLevel('thumb');
- }
- });
- }, { rootMargin: '150% 0px 150% 0px' });
- const playbackObserver = new IntersectionObserver((entries) => {
- entries.forEach(entry => {
- const card = entry.target;
- if (!card._bound) return;
- card._isActuallyVisible = entry.isIntersecting;
- if (!entry.isIntersecting && typeof card.forcePause === 'function') {
- card._isHovering = false;
- card.forcePause();
- }
- });
- }, { rootMargin: '0px', threshold: 0 });
- return {
- observe(card) { preloadObserver.observe(card); playbackObserver.observe(card); },
- unobserve(card) { preloadObserver.unobserve(card); playbackObserver.unobserve(card); }
- };
- })();
- 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;flex-shrink:0;user-select:none;contain:strict;content-visibility:auto;contain-intrinsic-size:auto 300px;transition:none;}:host(:state(downloaded)){outline:none;}:host(:state(downloaded))::after{content:'';position:absolute;inset:0;outline:3px solid var(--success);outline-offset:-3px;border-radius:inherit;pointer-events:none;z-index:50;}:host(:state(history)){contain:layout paint;width:100%;height:auto;aspect-ratio:calc(var(--ratio, 1) / (1 - 0.05 * clamp(0, (1 - var(--ratio, 1)) * 100, 1)));margin:0 !important;border-radius:4px !important;border-inline:none;box-shadow:0 4px 12px rgba(0,0,0,0.2);z-index:1;}:host(:state(history):state(type-text)){min-height:70px;height:70px;aspect-ratio:auto !important;}:host(:state(history):state(type-audio)){min-height:120px;height:120px;aspect-ratio:auto !important;}:host(:state(history)) #img,:host(:state(history)) #vid{object-fit:cover !important;width:100% !important;height:100% !important;border-radius:4px !important;transition:scale 0.2s cubic-bezier(0.2,0.8,0.2,1);}:host(:state(history):hover) #img,:host(:state(history):hover) #vid{transform:scale(1.03);transition:transform 0.2s cubic-bezier(0.2,0.8,0.2,1);}:host(:state(history):hover){z-index:2;filter:brightness(1.05);}:host(:state(history):state(type-audio)){}:host(:state(history):state(type-audio)) .media-wrapper{display:flex !important;background:#3b3e45 !important;box-shadow:none !important;border-top:1px solid rgba(255,255,255,0.04) !important;}:host(:state(history):state(type-text)) .media-wrapper{display:none !important;}:host(:state(history):state(type-audio)) .media-wrapper::before{content:"🔊 Audio Generation";position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:600;color:var(--text-muted);opacity:0.7;z-index:1;pointer-events:none;}:host(:state(history):state(type-audio)) .audio-text-overlay,:host(:state(history):state(type-audio)) #vid,:host(:state(history):state(type-text)) .chat-container{display:none !important;}:host(:state(history)) .overlay{opacity:0 !important;transition:opacity 0.2s;align-items:flex-start;justify-content:space-between;}:host(:state(history):hover) .overlay{opacity:1 !important;}:host(:state(history)) .overlay-actions > *:not(.overlay-dl-btn){display:none !important;}:host(:state(history)) .overlay-del-btn{width:26px !important;height:26px !important;}:host(:state(history)) .overlay-dl-btn{width:28px !important;height:28px !important;margin-left:8px !important;display:inline-flex !important;}:host(:state(current)){--max-avail-h:calc(100svh - 140px);--trimmed-ratio:calc(var(--ratio, 1) / (1 - 0.05 * clamp(0, (1 - var(--ratio, 1)) * 100, 1)));aspect-ratio:var(--trimmed-ratio) !important;flex:calc(var(--trimmed-ratio) * 100) 1 auto;flex-basis:min(calc(50% - 3px), calc(var(--max-avail-h) * var(--trimmed-ratio)));max-width:calc(var(--max-avail-h) * var(--trimmed-ratio));max-height:var(--max-avail-h);min-width:min(100%, 340px, calc(var(--max-avail-h) * var(--trimmed-ratio)));border:none !important;background:transparent !important;box-shadow:none !important;margin:0;border-radius:0;}:host(:state(current)) #img,:host(:state(current)) #vid{object-fit:cover !important;width:100% !important;height:100% !important;inset:0 !important;}:host(:hover){transition:scale .2s cubic-bezier(0.2,0.8,0.2,1),border-color .2s,opacity .2s,filter .2s;}:host(:state(current):not(:state(pending))) #img,:host(:state(current):not(:state(pending))) #vid{inset:0 !important;width:100% !important;height:100% !important;object-fit:cover !important;border-radius:0 !important;}:host(:state(pending):state(has-ref)) #img,:host(:state(pending):state(has-ref)) #vid,:host(:state(processing):state(has-ref)) #img,:host(:state(processing):state(has-ref)) #vid{display:block !important;opacity:1 !important;object-fit:cover !important;}.progress-bar-layer{position:absolute;inset:0;z-index:5;pointer-events:none;display:none;transform:translateX(-100%);will-change:transform;}:host(:state(pending):not(:state(has-ref))) .progress-bar-layer,:host(:state(processing):not(:state(has-ref))) .progress-bar-layer{display:block;background:rgba(255,255,255,0.15);transform:translateX(calc(-100% + var(--card-progress,0%)));transition:transform 0.3s ease-out;}:host(:state(pending):state(has-ref)) .progress-bar-layer,:host(:state(processing):state(has-ref)) .progress-bar-layer{display:block;background:rgba(0,0,0,0.75);border-left:2px solid rgba(255,255,255,0.3);transform:translate3d(var(--card-progress,0%),0,0) !important;transition:transform 0.3s ease-out;will-change:transform;}:host(:state(pending):state(has-ref)) #img,:host(:state(pending):state(has-ref)) #vid,:host(:state(processing):state(has-ref)) #img,:host(:state(processing):state(has-ref)) #vid{inset:14px !important;width:calc(100% - 28px) !important;height:calc(100% - 28px) !important;border-radius:8px !important;}:host(:state(reference)){contain:layout paint;flex:0 0 auto;align-self:center;width:100%;height:auto;aspect-ratio:var(--ratio,1);margin:0 !important;border-radius:4px;border-inline:none;box-shadow:0 4px 12px rgba(0,0,0,0.2);z-index:1;}:host(:state(reference)) #img,:host(:state(reference)) #vid{object-fit:cover !important;border-radius:4px !important;}:host(:state(reference):hover){z-index:2;filter:brightness(1.05);}:host(:state(reference):hover) #img,:host(:state(reference):hover) #vid{transform:scale(1.02);transition:transform 0.2s cubic-bezier(0.2,0.8,0.2,1);}:host(:state(reference):state(inactive)){opacity:.4;filter:grayscale(1);--media-dim-opacity:.58;}:host(:state(editing)){outline:none !important;border-color:transparent !important;box-shadow:0 0 24px rgba(16,185,129,0.3) !important;transition:none !important;}:host(:state(editing))::after{content:'';position:absolute;inset:0;box-shadow:inset 0 0 0 3px var(--success) !important;border-radius:inherit;pointer-events:none;z-index:50;}:host(:state(editing)) .media-wrapper{}:host(:state(editing)) .ref-header{background:#065f46 !important;border-bottom-color:transparent;}:host(:state(reference):not(:state(highlighted))){--media-dim-opacity:.38;}.media-wrapper{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background-color:#000;border-radius:6px;overflow:hidden;}.media-wrapper::before{content:'';position:absolute;inset:0;background:rgb(8 10 14);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(:state(history)) .media-wrapper{position:relative;flex:1 1 auto;min-height:0;background:#3b3e45;border-bottom:none;border-top:1px solid rgba(255,255,255,0.04);}:host(:state(reference)) .media-wrapper{position:relative;flex:1 1 auto;min-height:0;background:var(--bg);}:host(:state(current):not(:state(pending))) .media-wrapper{border-radius:0;inset:0;border:none;}:host(:state(pending)) .media-wrapper{inset:8px;border-radius:12px;box-shadow:0 4px 16px rgba(0,0,0,.4);transform:translateZ(0);overflow:hidden;}:host(:state(pending)) .media-wrapper::before,:host(:state(processing)) .media-wrapper::before{content:'';position:absolute;inset:0;background:#ffffff;mix-blend-mode:soft-light;opacity:0.15;z-index:4;pointer-events:none;}:host-context(body.eco-mode):host(:state(pending)) .media-wrapper::before,:host-context(body.eco-mode):host(:state(processing)) .media-wrapper::before{display:none !important;}#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;}#vid{z-index:2;}:host(:state(loaded)) #img,:host(:state(loaded)) #vid{opacity:1;}:host(:state(pending)) #img,:host(:state(pending)) #vid{inset:6px;width:calc(100% - 12px);height:calc(100% - 12px);border-radius:6px;}.overlay{position:absolute;inset:0;display:flex;justify-content:space-between;align-items:flex-start;padding:10px !important;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(:state(reference)) .overlay{display:none !important;}:host(:state(pending)) .overlay{pointer-events:none;}:host(:state(pending)) .overlay > *:not(.overlay-del-btn){display:none !important;}.overlay > *{pointer-events:auto;}.overlay-actions{display:flex;gap:6px;align-items:center;}.overlay-edit-btn,.overlay-extend-btn,.overlay-video-btn{width:26px;height:26px;font-size:12px;}.overlay-dl-btn{margin-inline-start:20px;}:host(:state(type-image)) .overlay-extend-btn{display:none !important;}:host(:state(type-video)) .overlay-video-btn{display:none !important;}:host(:state(type-audio)){--ratio:2.33 !important;background:var(--bg-center);}.audio-text-overlay{position:absolute;inset:12px;z-index:5;pointer-events:none;font-size:clamp(11px,4cqi,16px);line-height:1.4;font-family:monospace;color:rgba(255,255,255,0.6);text-align:center;overflow:hidden;text-shadow:0 2px 8px rgba(0,0,0,0.8);word-break:break-word;white-space:pre-wrap;display:none;flex-direction:column;}:host(:state(type-audio)) .audio-text-overlay{display:flex;}.audio-text-overlay::before,.audio-text-overlay::after{content:'';margin:auto;}:host(:state(type-audio)) #img{display:none !important;}:host(:state(type-audio)) #vid{display:block !important;z-index:2;opacity:0;}:host(:state(type-audio)) .overlay-actions button{display:none !important;}:host(:state(type-text)){background:#212121 !important;border:1px solid #3a3a3a !important;border-radius:16px !important;box-shadow:0 4px 24px rgba(0,0,0,0.4) !important;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;overflow:hidden;}:host(:state(current):state(type-text)){width:100% !important;max-width:800px !important;height:auto !important;max-height:65vh !important;min-height:100px;aspect-ratio:auto !important;flex:1 1 100%;margin:15px auto;}:host(:state(current):state(type-text)) .media-wrapper{position:relative !important;inset:auto !important;width:100%;height:100%;align-items:flex-start;background:transparent !important;border:none !important;border-radius:0 !important;overflow-y:auto;}:host(:state(type-text)) .overlay{display:none !important;}:host(:state(history):state(type-text)) .text-chat-header{display:flex !important;position:relative !important;inset:auto !important;flex:1;align-items:center;justify-content:center;background:transparent;border:none;padding:0;pointer-events:none;z-index:10;}:host(:state(history):state(type-text)) .text-chat-header-btn{pointer-events:auto;z-index:20;}:host(:state(type-text)) .text-chat-header{display:flex;align-items:center;justify-content:space-between;padding:12px 18px;background:#2f2f2f;border-bottom:1px solid #3a3a3a;height:50px;box-sizing:border-box;flex-shrink:0;}:host(:state(type-text)) .text-chat-model-name{color:#e8e8e8;font-weight:600;font-size:14px;text-align:center;flex-grow:1;pointer-events:none;}:host(:state(type-text)) .text-chat-header-btn{background:none;border:none;color:#b4b4b4;cursor:pointer;width:34px;height:34px;display:flex;align-items:center;justify-content:center;border-radius:8px;transition:background 0.2s,color 0.2s;padding:0;}:host(:state(type-text)) .text-chat-header-btn:hover{background:#4a4a4a;color:#ffffff;}:host(:state(type-text)) .text-chat-header-btn svg{width:18px;height:18px;}:host(:state(type-text)) #img,:host(:state(type-text)) #vid,:host(:state(type-text)) .play-icon{display:none !important;}:host(:state(type-text)) .overlay-video-btn,:host(:state(type-text)) .overlay-extend-btn,:host(:state(type-text)) .overlay-edit-btn{display:none !important;}:host(:not(:state(type-text))) .text-chat-header{display:none !important;}:host(:state(type-text)) .top-bar{display:none !important;}:host(:state(current):state(type-text)){width:100% !important;max-width:1080px !important;height:auto !important;max-height:none !important;min-height:min-content;aspect-ratio:auto !important;margin:0 auto;background:transparent !important;border:none !important;box-shadow:none !important;flex:0 0 auto;}:host(:state(current):state(type-text)) .text-chat-header{display:none !important;}:host(:state(type-text)) .media-wrapper{position:relative !important;inset:auto !important;background:transparent !important;overflow:visible !important;}.chat-container{display:none;flex-direction:column;gap:24px;width:100%;padding:16px 24px;box-sizing:border-box;}:host(:state(type-text)) .chat-container{display:flex;}.chat-message{display:flex;gap:16px;align-items:flex-start;width:100%;}.chat-avatar{width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:bold;font-size:14px;flex-shrink:0;font-family:system-ui,sans-serif;}.chat-user .chat-avatar{background:#3b82f6;color:#fff;}.chat-ai .chat-avatar{background:#1a1b1e;border:1px solid #333;color:#fff;}.chat-bubble{flex:1;font-size:16px;line-height:1.6;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;color:#ececec;white-space:pre-wrap;user-select:text;cursor:text;pointer-events:auto;}.chat-user .chat-bubble{background:rgba(255,255,255,0.05);padding:12px 18px;border-radius:16px;border-top-left-radius:4px;width:fit-content;max-width:100%;}.chat-ai .chat-bubble{padding-top:4px;}.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-header,.ref-side-prompt-btn,.bottom-bar{display:none;}:host(:state(reference)) .ref-header{display:flex;}:host(:state(reference)) .ref-side-prompt-btn{display:inline-flex;}:host(:state(history)) .bottom-bar{display:block;}:host(:state(editing)) .ref-header{background:#065f46 !important;border-bottom-color:transparent;}:host(:state(active-multi):not(:state(editing))) .ref-header{background:oklch(0.35 0.1 240 / 0.75) !important;border-bottom-color:oklch(0.6 0.15 240 / 0.4) !important;box-shadow:inset 0 -1px 0 oklch(0.6 0.15 240 / 0.2);}:host(:state(active-multi):not(:state(editing))) .ref-label-text.insertable-tag{color:oklch(0.75 0.15 150) !important;}.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(:state(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:26px;height:26px;font-size:12px;}.xai-btn--dl-square{width:28px;height:28px;}.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-dl-btn:hover{background:var(--text-main) !important;border-color:var(--text-main) !important;color:var(--bg) !important;transform:scale(1.05);}.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:4px 10px !important;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:16px;height:16px;flex-shrink:0;accent-color:#555;filter:grayscale(1);}.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;margin:0;}.ref-side-prompt-btn.has-prompt{opacity:1;visibility:visible;color:var(--success) !important;border-color:rgb(from var(--success) r g b / 40%) !important;background:rgb(0 0 0 / 60%) !important;box-shadow:none !important;}.ref-side-prompt-btn:hover{background:rgb(0 0 0 / 90%) !important;border-color:var(--text-main) !important;color:#fff !important;}.ref-side-prompt-btn.has-prompt:hover{border-color:var(--success) !important;background:rgb(0 0 0 / 90%) !important;}:host(:state(editing)) .ref-side-prompt-btn{opacity:1;visibility:visible;background:var(--success) !important;border-color:var(--success) !important;color:#fff !important;box-shadow:0 0 15px rgb(from var(--success) r g b / 40%) !important;scale:1.05;transition:none !important;}:host(:not(:state(reference))) .ref-side-prompt-btn{display:none !important;}:host(:hover) .ref-side-prompt-btn{opacity:1;visibility:visible;}:host(:state(history)) .overlay-actions > *:not(.overlay-dl-btn){display:none !important;}:host(:state(history):state(type-text)){display:flex;justify-content:center;background:#3b3e45 !important;border:none !important;border-top:1px solid rgba(255,255,255,0.04) !important;border-radius:0 !important;}:host(:state(history):state(type-text)) .text-chat-model-name{position:relative !important;inset:auto !important;display:block;font-size:15px;font-weight:600;color:var(--text-muted);opacity:0.9;pointer-events:none;text-align:center;}.drag-handle{cursor:grab;}.ml-auto{margin-left:auto;}.dragging{opacity:.5;box-shadow:0 0 0 2px var(--success);}:host(:state(type-text):state(pending)) .chat-bubble{display:block !important;white-space:nowrap !important;text-wrap:nowrap !important;overflow:hidden !important;text-overflow:ellipsis !important;word-break:keep-all !important;width:100%;}`);;
- const buildChatMsgHtml = (d = {}) => `<div class="chat-msg-row ${d.rowClass ?? ''}" data-msg-id="${d.msgId ?? ''}"><div class="chat-avatar-ui" style="display:${d.avatarDisplay ?? ''}">${d.avatarText ?? ''}</div><div class="chat-bubble-wrapper ${d.bubbleWrapperClass ?? ''}"><div class="chat-bubble-ui"></div><div class="chat-msg-toolbar"><button type="button" class="chat-msg-action chat-msg-action--icon" data-chat-action="repeat" data-action="chat-action" title="Repeat prompt" data-user-prompt="${d.repeatUserPrompt ?? ''}">↻</button><button type="button" class="chat-msg-action chat-msg-action--icon" data-chat-action="copy" data-action="chat-action" title="Copy response">⧉</button><button type="button" class="chat-msg-action chat-msg-action--new-chat" data-chat-action="new-chat" data-action="chat-action" title="Start a new chat">New Chat</button></div></div></div>`;
- const CARD_CHAT_TEMPLATE = document.createElement('template');
- CARD_CHAT_TEMPLATE.innerHTML = `<div class="chat-container" id="chat-container"><div class="chat-message chat-user"><div class="chat-avatar">U</div><div class="chat-bubble" id="chat-user-msg"></div></div><div class="chat-message chat-ai"><div class="chat-avatar">\u{1D54F}</div><div class="chat-bubble-wrapper" style="position:relative; flex:1; display:flex; flex-direction:column; align-items:flex-start;"><div class="chat-bubble" id="text-content"></div></div></div></div>`;
- const VideoHoverPreviewer = new class {
- constructor() {
- this.video = document.createElement('video');
- this.video.muted = true;
- this.video.loop = true;
- this.video.playsInline = true;
- this.video.setAttribute('disablePictureInPicture', '');
- this.video.setAttribute('disableRemotePlayback', '');
- this.video.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;object-fit:cover;z-index:4;opacity:0;transition:opacity 0.2s ease-in-out;pointer-events:none;display:none;';
- this.activeCard = null;
- this.enterTimer = null;
- this.leaveTimer = null;
- this.abortController = null;
- this._activeBlobUrl = null;
- }
- async enter(card) {
- if (this.activeCard === card) {
- clearTimeout(this.leaveTimer);
- if (this.video.style.opacity === '0') this.video.style.opacity = '1';
- if (card.playIcon) card.playIcon.style.opacity = '0';
- return;
- }
- this.cancel();
- this.enterTimer = setTimeout(async () => {
- this.activeCard = card;
- const item = card._item;
- if (!item || item.type !== 'video' || card._context !== 'current') return;
- card.mediaWrapper.appendChild(this.video);
- this.video.style.display = 'block';
- this.abortController = new AbortController();
- const signal = this.abortController.signal;
- try {
- const fetchId = item.refSourceId || item.id;
- const file = await MediaCache.getFile(fetchId, 'full');
- if (signal.aborted || !file) { this.clear(); return; }
- if (this._activeBlobUrl) {
- URL.revokeObjectURL(this._activeBlobUrl);
- this._activeBlobUrl = null;
- }
- this._activeBlobUrl = URL.createObjectURL(file);
- this.video.src = this._activeBlobUrl;
- this.video.load();
- await Promise.race([
- new Promise(r => this.video.addEventListener('canplay', r, { once: true })),
- new Promise((_, rej) => this.video.addEventListener('error', rej, { once: true })),
- new Promise((_, rej) => AbortSignal.timeout(4000).addEventListener('abort', rej, { once: true })),
- new Promise((_, rej) => signal.addEventListener('abort', rej, { once: true }))
- ]);
- if (signal.aborted) return;
- await this.video.play();
- this.video.style.opacity = '1';
- if (card.playIcon) card.playIcon.style.opacity = '0';
- card._internals.states.add('playing');
- } catch (e) {
- this.clear();
- }
- }, 120);
- }
- leave(card) {
- if (this.activeCard !== card) return;
- clearTimeout(this.enterTimer);
- if (this.video.style.opacity === '1' || !this.video.paused) {
- this.video.style.opacity = '0';
- if (card.playIcon) card.playIcon.style.opacity = '1';
- card._internals.states.delete('playing');
- this.leaveTimer = setTimeout(() => this.clear(), 200);
- } else {
- this.clear();
- }
- }
- cancel() {
- clearTimeout(this.enterTimer);
- clearTimeout(this.leaveTimer);
- this.clear();
- }
- clear() {
- if (this._activeBlobUrl) {
- URL.revokeObjectURL(this._activeBlobUrl);
- this._activeBlobUrl = null;
- }
- if (this.abortController) {
- this.abortController.abort();
- this.abortController = null;
- }
- this.video.pause();
- this.video.removeAttribute('src');
- this.video.srcObject = null;
- this.video.load();
- this.video.style.display = 'none';
- this.video.style.opacity = '0';
- if (this.video.parentNode) this.video.parentNode.removeChild(this.video);
- if (this.activeCard) {
- this.activeCard._internals.states.delete('playing');
- if (this.activeCard.playIcon) this.activeCard.playIcon.style.opacity = '1';
- }
- this.activeCard = null;
- }
- };
- class XaiCard extends HTMLElement {
- constructor() {
- super(); this.attachShadow({ mode: 'open' }); this.shadowRoot.adoptedStyleSheets =[cardSheet]; this._internals = this.attachInternals();
- this._partInstance = CARD_TEMPLATE.createInstance({
- refLabelText: 'IMAGE_0',
- textModelName: '',
- mediaWrapperPrompt: '',
- bgBaseBg: '',
- playDisplay: 'none',
- cardPrompt: '',
- cardPromptTooltip: ''
- });
- this.shadowRoot.append(this._partInstance.content);
- this.mediaWrapper = this.shadowRoot.querySelector('.media-wrapper');
- this.playIcon = this.shadowRoot.getElementById('play');
- this.imgEl = this.shadowRoot.getElementById('img');
- 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.refLabelText = this.shadowRoot.querySelector('.ref-label-text');
- this.textChatHeader = this.shadowRoot.getElementById('text-chat-header');
- this.audioTextEl = null;
- this.vidEl = null;
- this.chatUserMsgEl = null;
- this.chatAiMsgEl = null;
- this._chatContainerEl = null;
- this._bound = false; this._mediaState = 'none';
- this._managedBlobUrls = new Set();
- this._currentImgUrl = null;
- this._currentVidUrl = null;
- this._loadController = null; this._hoverTimer = null; this._warmTimer = null; this._isActuallyVisible = false; this._isHovering = false; this._hoverUpgraded = false; this._statusSig = ''; this._progressValue = null;
- this._cardAbortController = null;
- this._statusPool = Array.from({ length: 3 }, () => {
- const el = document.createElement('div');
- el.className = 'ref-status-line';
- el.style.display = 'none';
- this.statusOverlay.appendChild(el);
- return el;
- });
- this.progressBar = this.shadowRoot.getElementById('progress-bar');
- }
- setProgress(p, durationMs = 300, easing = 'ease-out') {
- const next = p == null ? null : Number(p);
- if (this._progressValue === next) return;
- this._progressValue = next;
- if (!this.progressBar) return;
- if (next == null) {
- this.progressBar.style.display = 'none';
- this.progressBar.style.transition = 'none';
- this.style.removeProperty('--card-progress');
- this.removeAttribute('data-progress');
- return;
- }
- this.dataset.progress = String(next);
- this.style.setProperty('--card-progress', `${next}%`);
- if (this.progressBar.style.display !== 'block') {
- this.progressBar.style.display = 'block';
- this.progressBar.offsetWidth;
- }
- }
- reflectClassFlags(flags = []) {
- const s = this._internals.states;
- flags.forEach(([name, enabled]) => { if (enabled) s.add(name); else s.delete(name); });
- }
- 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';
- if (this._item?.type === 'text' && this._item.isPending && this.chatAiMsgEl) {
- this.chatAiMsgEl.textContent = 'Queued...';
- this.chatAiMsgEl.style.opacity = '0.6';
- this.chatAiMsgEl.style.fontStyle = 'italic';
- }
- for (let i = 0; i < this._statusPool.length; i++) {
- if (this._statusPool[i].style.display !== 'none') this._statusPool[i].style.display = 'none';
- }
- return;
- }
- if (this._item?.type === 'text' && this._item.isPending && this.chatAiMsgEl) {
- const statusText = normalized.length ? normalized[normalized.length - 1].text : '';
- this.chatAiMsgEl.textContent = statusText;
- this.chatAiMsgEl.style.opacity = '0.6';
- this.chatAiMsgEl.style.fontStyle = 'italic';
- }
- this.statusOverlay.style.display = 'flex';
- for (let i = 0; i < this._statusPool.length; i++) {
- const node = this._statusPool[i];
- const line = normalized[i];
- if (line) {
- const nextCss = (line.style || '') + (line.color ? `color:${line.color};` : '');
- if (node._lastCss !== nextCss) {
- node.style.cssText = nextCss;
- node._lastCss = nextCss;
- }
- if (node.textContent !== line.text) node.textContent = line.text;
- if (node.style.display !== 'block') node.style.display = 'block';
- } else {
- if (node.style.display !== 'none') node.style.display = 'none';
- }
- }
- }
- _updateParts(vals) {
- this._partInstance.update(vals);
- }
- setLabel(text, title = '', onClick = null) {
- if (!this.refLabelText) return;
- this._updateParts({ refLabelText: text });
- this.refLabelText.classList.toggle('insertable-tag', !!onClick);
- this.refLabelText.title = title;
- if (onClick) {
- this.refLabelText.setAttribute('data-intent', 'insert-tag');
- this._insertTagIdx = text.match(/IMAGE_(\d+)/)?.[1] ?? '0';
- } else {
- this.refLabelText.removeAttribute('data-intent');
- this._insertTagIdx = null;
- }
- }
- setHistoryPrompt(prompt, itemType) {
- const safePrompt = Pipeline.getDisplayPrompt(prompt);
- const turns = itemType === 'text' && this._item && this._item.textTurnCount > 0 ? `${this._item.textTurnCount}t ` : '';
- const prefix = itemType === 'text' ? `💬 ${turns}` : '';
- const preview = safePrompt ? (safePrompt.length > 120 ? safePrompt.substring(0, 120) + '...' : safePrompt) : '';
- this._updateParts({
- cardPromptTooltip: '',
- cardPrompt: prefix + preview
- });
- }
- setPromptTooltip(prompt, timestamp) {
- const displayPrompt = Pipeline.getDisplayPrompt(prompt);
- const safePrompt = displayPrompt ? (displayPrompt.length > 1500 ? displayPrompt.substring(0, 1500) + '...' : displayPrompt) : '';
- const timePrefix = TimeUtils.forTooltip(timestamp);
- const fullText = `${timePrefix}\n${safePrompt}`;
- if (this.title !== fullText) this.title = fullText;
- }
- setReferenceActive(active) {
- if (this.refToggle) this.refToggle.checked = !!active;
- if (!active) this._internals.states.add('inactive'); else this._internals.states.delete('inactive');
- }
- setReferencePromptFlag(hasPrompt) {
- if (this.refSideBtn) this.refSideBtn.classList.toggle('has-prompt', !!hasPrompt);
- if (hasPrompt) this._internals.states.add('has-custom-prompt'); else this._internals.states.delete('has-custom-prompt');
- }
- refresh(item, context = this._context) {
- this.draggable = ['history', 'current', 'reference'].includes(context) && !item?.isPending;
- if (!this._bound || this.dataset.id !== String(item.id) || this._context !== context) {
- this.bind(item, context);
- return;
- }
- this._item = item;
- this.dataset.prompt = item.shortPrompt || '';
- const shortForDisplay = item.shortPrompt || '';
- this.setPromptTooltip(shortForDisplay, item.timestamp);
- if (this.audioTextEl) {
- this.audioTextEl.textContent = item.shortPrompt || 'Audio Generation';
- }
- const isRefCard = !!item.refSourceId || !!item.originalUrl || !!item.isUploadedRef || context === 'reference';
- this.reflectClassFlags([
- ['has-ref', isRefCard],
- ['downloaded', item.downloaded],
- ['inactive', context === 'reference' && !item.active],
- ['pending', item.isPending],
- ['type-image', item.type === 'image'],
- ['type-video', item.type === 'video'],
- ['type-audio', item.type === 'audio'],
- ['type-text', item.type === 'text']
- ]);
- 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;
- }
- if (item.type === 'audio' && !this.audioTextEl) {
- this.audioTextEl = document.createElement('div');
- this.audioTextEl.className = 'audio-text-overlay';
- this.mediaWrapper.appendChild(this.audioTextEl);
- }
- const bg = item.lqip || '';
- const playDisplay = (item.type === 'video' || item.type === 'audio') && context !== 'reference' && !item.isPending ? 'flex' : 'none';
- const modelName = item.type === 'text' ? (item.textModel || STATE.textModel || 'grok-4.20') : (this.shadowRoot.getElementById('text-model-name')?.textContent || '');
- this._updateParts({
- mediaWrapperPrompt: item.shortPrompt || '',
- bgBaseBg: bg,
- playDisplay: playDisplay,
- textModelName: modelName
- });
- if (item.isPending) this.setStatus(item._statusData || [{ text: 'Queued...', color: '#fff' }]);
- if (item.isPending && item.refSourceId && this._mediaState === 'none') {
- this.setLoadLevel('thumb');
- }
- else if (context === 'reference') {
- this.setReferenceActive(item.active);
- this.setReferencePromptFlag(!!STATE.refPrompts[item.id]?.[STATE.action]);
- if (STATE.activeRefPromptId === item.id) this._internals.states.add('editing'); else this._internals.states.delete('editing');
- } else if (context === 'history') this.setHistoryPrompt(item.shortPrompt, item.type);
- LazyTextStore.get(item.id).then(heavy => {
- if (!this._bound || this.dataset.id !== String(item.id)) return;
- this.dataset.prompt = heavy.prompt || '';
- this._updateParts({ mediaWrapperPrompt: heavy.prompt || '' });
- this.setPromptTooltip(heavy.prompt, item.timestamp);
- if (context === 'history') this.setHistoryPrompt(heavy.prompt, item.type);
- if (this.chatUserMsgEl) this.chatUserMsgEl.textContent = heavy.prompt || '';
- if (this.chatAiMsgEl && !item.isPending) {
- const resp = heavy.responseText || '';
- this.chatAiMsgEl.textContent = resp.length > 120 ? resp.substring(0, 120) + '...' : resp;
- this.chatAiMsgEl.style.opacity = '1';
- this.chatAiMsgEl.style.fontStyle = 'normal';
- }
- });
- if (item.isNewlyGenerated) {
- this._isPreloadIntersecting = true;
- this.setLoadLevel('full');
- delete item.isNewlyGenerated;
- }
- if (this._bound && !item.isPending && this._isPreloadIntersecting) {
- if (item.type === 'image' && this._context === 'current' && this._mediaState !== 'full' && !isMemoryConstrainedMode()) {
- this.setLoadLevel('full');
- }
- }
- }
- connectedCallback() { if (this._bound) CardObserverHub.observe(this); }
- disconnectedCallback() {
- CardObserverHub.unobserve(this);
- if (!this.isConnected && this._bound) {
- this.unbind();
- }
- }
- forcePause() {
- if (this._item?.type === 'video') {
- VideoHoverPreviewer.leave(this);
- }
- }
- hydrateMedia() {
- if (!this._bound || !this._item) return false;
- const type = this._item.type;
- if (isMemoryConstrainedMode() && (type === 'video' || type === 'audio')) return false;
- if (type === 'audio') {
- if (!this.vidEl) {
- this.vidEl = document.createElement('video');
- this.vidEl.id = 'vid';
- this.vidEl.preload = 'none';
- this.vidEl.muted = true;
- this.vidEl.loop = true;
- this.vidEl.playsInline = true;
- this.vidEl.setAttribute('playsinline', '');
- this.vidEl.setAttribute('disablePictureInPicture', '');
- this.vidEl.setAttribute('disableRemotePlayback', '');
- this.mediaWrapper.appendChild(this.vidEl);
- }
- }
- if (type === 'text') {
- if (!this._chatContainerEl) {
- const clone = CARD_CHAT_TEMPLATE.content.cloneNode(true);
- const container = clone.querySelector('#chat-container');
- this.mediaWrapper.appendChild(container);
- this._chatContainerEl = container;
- this.chatUserMsgEl = clone.querySelector('#chat-user-msg');
- this.chatAiMsgEl = clone.querySelector('#text-content');
- const itemId = String(this._item.id);
- LazyTextStore.get(itemId).then(heavy => {
- if (!this._bound || String(this._item?.id) !== itemId) return;
- if (this.chatUserMsgEl) this.chatUserMsgEl.textContent = heavy.prompt || '';
- if (this.chatAiMsgEl && !this._item.isPending) {
- this.chatAiMsgEl.textContent = heavy.responseText || '';
- this.chatAiMsgEl.style.opacity = '1';
- this.chatAiMsgEl.style.fontStyle = 'normal';
- }
- });
- }
- }
- return true;
- }
- dehydrateMedia() {
- if (this.vidEl) {
- resetVideoElement(this.vidEl);
- this.vidEl.remove();
- this.vidEl = null;
- }
- if (this._chatContainerEl) {
- this._chatContainerEl.remove();
- this._chatContainerEl = null;
- this.chatUserMsgEl = null;
- this.chatAiMsgEl = null;
- }
- if (this.audioTextEl) {
- this.audioTextEl.remove();
- this.audioTextEl = null;
- }
- }
- [Symbol.dispose]() {
- this.unbind();
- }
- setHoverState(isHover) {
- if (!this._bound || this._item?.type !== 'video' || this._context !== 'current') return;
- if (this._isHovering === isHover) return;
- this._isHovering = isHover;
- if (this._item.type === 'video') {
- if (STATE.ecoMode || App.isAutoEco || DOM.lightbox?.open) return;
- if (isHover) VideoHoverPreviewer.enter(this);
- else VideoHoverPreviewer.leave(this);
- return;
- }
- clearTimeout(this._hoverTimer);
- this._hoverTimer = null;
- if (!isHover) {
- this.forcePause();
- this._hoverTimer = setTimeout(() => {
- this._hoverTimer = null;
- if (this._isHovering) return;
- if (this._hoverUpgraded) { this._hoverUpgraded = false; this.setLoadLevel('thumb'); }
- }, 300);
- return;
- }
- if (STATE.ecoMode || App.isAutoEco || DOM.lightbox?.open) return;
- if (!this.vidEl && !this.hydrateMedia()) return;
- this._hoverTimer = setTimeout(async () => {
- this._hoverTimer = null;
- if (!this._isHovering || !this._bound) return;
- try {
- if (this._mediaState !== 'full') {
- this._hoverUpgraded = true;
- await this.setLoadLevel('full');
- }
- if (!this._isHovering || DOM.lightbox?.open || !this.vidEl) return;
- if (!this.vidEl.src || this.vidEl.src === location.href) {
- const fetchId = this._item.refSourceId || this._item.id;
- const file = await MediaCache.getFile(fetchId, 'full');
- if (file) { if (this.vidEl.src?.startsWith('blob:')) URL.revokeObjectURL(this.vidEl.src); this.vidEl.src = URL.createObjectURL(file); } else if (this._item?.originalUrl) { this.vidEl.src = this._item.originalUrl; }
- if (!this.vidEl.src) return;
- }
- this.vidEl.style.display = 'block';
- this.vidEl.muted = true;
- if (this.vidEl.readyState < 3) {
- await Promise.race([
- new Promise(r => this.vidEl.addEventListener('canplay', r, { once: true })),
- new Promise((_, rej) => this.vidEl.addEventListener('error', e => rej(e.error ?? new Error('video load error')), { once: true })),
- new Promise(r => AbortSignal.timeout(5000).addEventListener('abort', r, { once: true }))
- ]);
- }
- if (!this._isHovering) return;
- await this.vidEl.play();
- if (this._isHovering && !this.vidEl.paused) {
- if (this.playIcon) this.playIcon.style.opacity = '0';
- this._internals.states.add('playing');
- }
- } catch(e) {
- if (e?.name !== 'AbortError') this._internals.states.delete('playing');
- if (this.playIcon) this.playIcon.style.opacity = '1';
- }
- }, 80);
- }
- _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;
- const isImageOrVideo = this._item.type === 'image' || this._item.type === 'video';
- const isHistoryOrRef = this._context === 'history' || this._context === 'reference';
- const isCurrentVideoNotHovered = this._context === 'current' && this._item.type === 'video' && !this._isHovering;
- if (level === 'full' && (this._item.isPending || STATE.ecoMode || (isImageOrVideo && isHistoryOrRef) || isCurrentVideoNotHovered)) {
- level = 'thumb'; if (this._mediaState === level) return;
- }
- this._abortLoad(); this._loadController = new AbortController(); const signal = this._loadController.signal;
- if (level === 'none') {
- this._clearSrc();
- this.dehydrateMedia();
- return;
- }
- if (level === 'thumb') {
- if (level === 'thumb' && this._mediaState === 'none' && !this._item.isPending) {
- this._clearSrc();
- }
- this.imgEl.style.display = 'block';
- const isDehydratable = this._item.type !== 'video' && this._item.type !== 'audio';
- if (isDehydratable) this.dehydrateMedia();
- }
- if (level === 'full') {
- this.hydrateMedia();
- }
- try {
- const { thumbFile, fullFile } = await this._resolveUrls(level, signal);
- if (signal.aborted) return;
- await this._applySrc(level, thumbFile, fullFile, signal, String(this._item?.id || ''));
- } catch(e) { if (e.name !== 'AbortError') addLog('WARN', 'setLoadLevel error', e); }
- }
- async _resolveUrls(level, signal) {
- const fetchId = this._item.refSourceId || this._item.id;
- const isAudio = this._item.type === "audio";
- const needsThumb = !isAudio && (this._item.type === "video" || this._item.type === "image" || level !== "full" || this._context !== "current" || isMemoryConstrainedMode());
- const thumbPromise = needsThumb ? MediaCache.getFile(fetchId, "thumb") : Promise.resolve(null);
- const fullPromise = level === "full"
- ? MediaCache.getFile(fetchId, "full").then(f => f || (this._item.originalUrl || null))
- : Promise.resolve(null);
- let [thumbFile, fullFile] = await Promise.all([thumbPromise, fullPromise]);
- if (!thumbFile && !fullFile && level === "full" && this._item.type === "image") {
- thumbFile = await MediaCache.getFile(fetchId, "thumb");
- }
- if (signal.aborted) return {};
- return { thumbFile, fullFile };
- }
- async _applySrc(level, thumbFile, fullFile, signal = this._loadController?.signal) {
- if (!this._bound) return;
- this._mediaState = level;
- const expectedId = this.dataset.id;
- if (this._item.type === 'image') {
- if (this.vidEl) this.vidEl.style.display = 'none';
- this.imgEl.style.display = 'block';
- this.imgEl.loading = this._context === 'history' ? 'lazy' : 'eager';
- this.imgEl.fetchPriority = (this._context === 'current' && level === 'full') ? 'high' : 'auto';
- if (level === 'full' && thumbFile && !this.imgEl.src) {
- if (this._currentImgUrl) this._revokeTrackedUrl(this._currentImgUrl);
- this._currentImgUrl = this._trackUrl(URL.createObjectURL(thumbFile));
- const tempUrl = this._currentImgUrl;
- this.imgEl.onload = this.imgEl.onerror = () => {
- this._revokeTrackedUrl(tempUrl);
- if (this._currentImgUrl === tempUrl) this._currentImgUrl = null;
- };
- this.imgEl.src = this._currentImgUrl;
- this._internals.states.add('loaded');
- }
- const targetFile = fullFile || thumbFile;
- if (targetFile) {
- const tempUrl = URL.createObjectURL(targetFile);
- if (this.imgEl.src !== tempUrl) {
- try {
- const preloader = new Image();
- preloader.src = tempUrl;
- await preloader.decode();
- if (signal.aborted || !this.imgEl) {
- URL.revokeObjectURL(tempUrl);
- return;
- }
- const oldUrl = this._currentImgUrl;
- this._currentImgUrl = this._trackUrl(tempUrl);
- this.imgEl.src = tempUrl;
- this.imgEl.style.display = 'block';
- this._internals.states.add('loaded');
- this._mediaState = level;
- if (oldUrl) URL.revokeObjectURL(oldUrl);
- } catch (err) {
- URL.revokeObjectURL(tempUrl);
- if (err.name !== 'AbortError' && err.name !== 'EncodingError') console.warn('[xAI] Decode failed', err);
- }
- }
- } else {
- this.imgEl.removeAttribute('src');
- this._internals.states.delete('loaded');
- }
- } else if (this._item.type === 'audio') {
- this.imgEl.style.display = 'none';
- if (this.vidEl) {
- this.vidEl.removeAttribute('crossorigin');
- if (level === 'full' && (fullFile || thumbFile)) {
- this.vidEl.style.display = 'block';
- const targetFile = fullFile || thumbFile;
- if (targetFile) {
- if (this._currentVidUrl) this._revokeTrackedUrl(this._currentVidUrl);
- this._currentVidUrl = this._trackUrl(URL.createObjectURL(targetFile));
- this.vidEl.src = this._currentVidUrl;
- this._internals.states.add('loaded');
- }
- } else {
- resetVideoElement(this.vidEl, { pause: false });
- }
- }
- } else if (this._item.type === 'text') {
- this.imgEl.style.display = 'none';
- if (this.vidEl) this.vidEl.style.display = 'none';
- if (level === 'full' && fullFile) {
- fullFile.text().then(txt => {
- if (signal.aborted || !this._bound || this.dataset.id !== expectedId) return;
- if (this.chatAiMsgEl) this.chatAiMsgEl.textContent = txt;
- this._internals.states.add('loaded');
- }).catch(err => {
- if (err.name !== 'AbortError') addLog('WARN', 'text card load failed', err);
- });
- }
- } else {
- this.imgEl.style.display = 'block';
- if (thumbFile) {
- if (this._currentImgUrl) this._revokeTrackedUrl(this._currentImgUrl);
- this._currentImgUrl = this._trackUrl(URL.createObjectURL(thumbFile));
- const tempUrl = this._currentImgUrl;
- try {
- await DecodeQueue.enqueue(tempUrl, signal);
- if (!signal.aborted && this.imgEl) {
- this.imgEl.onload = this.imgEl.onerror = () => {
- this._revokeTrackedUrl(tempUrl);
- if (this._currentImgUrl === tempUrl) this._currentImgUrl = null;
- };
- this.imgEl.src = tempUrl;
- this.imgEl.style.display = 'block';
- this._internals.states.add('loaded');
- this._mediaState = level;
- } else {
- this._revokeTrackedUrl(tempUrl);
- if (this._currentImgUrl === tempUrl) this._currentImgUrl = null;
- }
- } catch (err) {
- this._revokeTrackedUrl(tempUrl);
- if (this._currentImgUrl === tempUrl) this._currentImgUrl = null;
- if (err.name !== 'AbortError' && err.name !== 'EncodingError') console.warn('[xAI] Decode failed', err);
- }
- } else {
- this.imgEl.style.display = 'none';
- }
- if (this.vidEl) {
- this.vidEl.removeAttribute('crossorigin');
- if (level === 'full' && fullFile) {
- this.vidEl.style.display = 'block';
- if (this._currentVidUrl) this._revokeTrackedUrl(this._currentVidUrl);
- this._currentVidUrl = this._trackUrl(URL.createObjectURL(fullFile));
- this.vidEl.src = this._currentVidUrl;
- const onLoaded = () => { if (this._bound) this._internals.states.add('loaded'); };
- if (this.vidEl.readyState >= 2) onLoaded(); else this.vidEl.addEventListener('loadeddata', onLoaded, { once: true });
- } else resetVideoElement(this.vidEl, { pause: false });
- }
- }
- }
- _trackUrl(url) {
- if (url && url.startsWith('blob:')) this._managedBlobUrls.add(url);
- return url;
- }
- _revokeTrackedUrl(url) {
- if (this._managedBlobUrls.has(url)) {
- URL.revokeObjectURL(url);
- this._managedBlobUrls.delete(url);
- }
- }
- _revokeAllUrls() {
- this._managedBlobUrls.forEach(url => URL.revokeObjectURL(url));
- this._managedBlobUrls.clear();
- this._currentImgUrl = null;
- this._currentVidUrl = null;
- }
- _clearSrc() {
- this._revokeAllUrls();
- this.forcePause();
- if (this.imgEl) {
- this.imgEl.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
- this.imgEl.removeAttribute('src');
- this.imgEl.style.display = 'none';
- }
- if (this.vidEl) {
- resetVideoElement(this.vidEl);
- this.vidEl.style.display = 'none';
- }
- if (this.chatAiMsgEl) this.chatAiMsgEl.textContent = '';
- this._internals.states.delete('loaded'); this._internals.states.delete('playing');
- this._mediaState = 'none';
- }
- 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 = 'xai-card';
- this._internals.states.clear();
- this._internals.states.add(context);
- this._internals.states.add('type-' + item.type);
- CardRegistry.move(this, prevContext, prevId, context, item.id);
- if (this.vidEl) this.vidEl.style.display = 'none'; if (this.imgEl) this.imgEl.style.display = 'none';
- this._internals.states.delete('loaded'); this._internals.states.delete('playing'); this._internals.states.delete('processing');
- this._statusSig = '';
- this._progressValue = null;
- this._bound = true;
- this.refresh(item, context);
- this._cardAbortController = new AbortController();
- const cardSignal = this._cardAbortController.signal;
- if (context === 'current' && (item.type === 'video' || item.type === 'audio')) {
- this.addEventListener('pointerenter', () => this.setHoverState(true), { signal: cardSignal });
- this.addEventListener('pointerleave', () => this.setHoverState(false), { signal: cardSignal });
- }
- AppState.action.bind(currentAction => {
- this._partInstance.update({ textModelName: currentAction === 'gen_text' ? (AppState.textModel.value || '') : '' });
- }, cardSignal);
- CardObserverHub.observe(this);
- }
- unbind() {
- if (this._item?.type === 'video') VideoHoverPreviewer.leave(this);
- if (this._cardAbortController) { this._cardAbortController.abort(); this._cardAbortController = null; }
- if (this.dataset.id) {
- RefPayloadCache.delete(this.dataset.id);
- }
- CardRegistry.unregister(this, this._context, this.dataset.id);
- clearTimeout(this._hoverTimer); this._hoverTimer = null;
- clearTimeout(this._warmTimer); this._warmTimer = null;
- this._abortLoad();
- if (this.vidEl) {
- this.vidEl.pause();
- this.vidEl.removeAttribute('src');
- this.vidEl.load();
- }
- this._clearSrc();
- this.dehydrateMedia();
- CardObserverHub.unobserve(this);
- this._mediaState = 'none';
- this._isActuallyVisible = false;
- this._isHovering = false;
- this._hoverUpgraded = false;
- this._isPreloadIntersecting = false;
- this._statusSig = '';
- this._progressValue = null;
- this.removeAttribute('data-id');
- this.removeAttribute('data-progress');
- this.removeAttribute('data-prompt');
- this.removeAttribute('data-vertical');
- if (!this._isPoolCard) this.removeAttribute('style');
- this.className = 'xai-card';
- this._internals.states.clear();
- if (this._partInstance) this._updateParts({ bgBaseBg: '' });
- if (this.imgEl) this.imgEl.src = '';
- this._item = null;
- this._context = null;
- this._bound = false;
- }
- }
- if (!customElements.get('xai-card')) customElements.define('xai-card', XaiCard);
- class HybridVirtualGrid {
- constructor(container) {
- this.container = container;
- this.nodesMap = new Map();
- this._pool = [];
- this.itemsData = [];
- this.rafId = 0;
- this.colWidth = 0;
- this.yPositions = [];
- this.heights = [];
- this.totalHeight = 0;
- this.spacer = document.createElement('div');
- this.spacer.className = 'scroll-spacer';
- this.container.appendChild(this.spacer);
- this.resizeObs = new ResizeObserver((entries) => {
- const cw = entries[0].contentRect.width;
- if (!cw || this.colWidth === cw) return;
- this.colWidth = cw;
- this._recalcLayout();
- this._scheduleRender();
- });
- this.resizeObs.observe(this.container);
- this.container.addEventListener('scroll', () => this._scheduleRender(), { passive: true });
- }
- _itemHeight(meta) {
- if (meta.type === 'text') return 70;
- if (meta.type === 'audio') return 120;
- const ratio = meta.ratio || 1;
- const targetRatio = ratio < 1 ? ratio / 0.95 : ratio;
- return Math.max(68, Math.round(this.colWidth / Math.max(0.5, targetRatio)));
- }
- _recalcLayout() {
- const cw = this.colWidth || this.container.clientWidth;
- if (!cw) return;
- this.colWidth = cw;
- let y = 0;
- this.yPositions = [];
- this.heights = [];
- for (let i = 0; i < this.itemsData.length; i++) {
- this.yPositions.push(y);
- const h = this._itemHeight(this.itemsData[i].meta);
- this.heights.push(h);
- y += h + 4;
- }
- this.totalHeight = y;
- this.spacer.style.height = this.totalHeight + 'px';
- }
- _findStartIndex(scrollTop) {
- let lo = 0, hi = this.yPositions.length - 1;
- while (lo < hi) {
- const mid = (lo + hi + 1) >> 1;
- if (this.yPositions[mid] <= scrollTop) lo = mid; else hi = mid - 1;
- }
- return Math.max(0, lo - 1);
- }
- _scheduleRender() {
- if (!this.rafId) this.rafId = requestAnimationFrame(() => { this.rafId = 0; this.render(); });
- }
- updateData(ids, itemDataMap) {
- this.itemsData = ids.map(id => ({ id: String(id), meta: itemDataMap.get(String(id)) })).filter(d => d.meta);
- this._recalcLayout();
- this.render();
- }
- render() {
- if (!this.itemsData.length) {
- this.clear();
- return;
- }
- const scrollTop = this.container.scrollTop;
- const viewH = this.container.clientHeight;
- const startIndex = this._findStartIndex(scrollTop);
- const itemsPerScreen = Math.ceil(viewH / 150);
- const dynamicBuffer = itemsPerScreen * 2;
- const endIndex = Math.min(this.itemsData.length, startIndex + itemsPerScreen + dynamicBuffer);
- const safeStartIndex = Math.max(0, startIndex - dynamicBuffer);
- const visibleIds = new Set();
- for (let i = safeStartIndex; i < endIndex; i++) {
- visibleIds.add(this.itemsData[i].id);
- }
- for (const [id, card] of this.nodesMap.entries()) {
- if (!visibleIds.has(id)) {
- card.unbind();
- card.style.display = 'none';
- this._pool.push(card);
- this.nodesMap.delete(id);
- }
- }
- for (let i = safeStartIndex; i < endIndex; i++) {
- const data = this.itemsData[i];
- let card = this.nodesMap.get(data.id);
- if (!card) {
- card = this._pool.pop();
- if (!card) {
- card = document.createElement('xai-card');
- card.style.position = 'absolute';
- this.container.appendChild(card);
- }
- card.style.display = '';
- this.nodesMap.set(data.id, card);
- card.bind(data.meta, 'history');
- }
- card.style.transform = `translate3d(0, ${this.yPositions[i]}px, 0)`;
- card.style.width = this.colWidth + 'px';
- card.style.height = this.heights[i] + 'px';
- }
- }
- clear() {
- for (const [id, card] of this.nodesMap.entries()) {
- card.unbind();
- card.style.display = 'none';
- this._pool.push(card);
- }
- this.nodesMap.clear();
- this.itemsData = [];
- this.yPositions = [];
- this.heights = [];
- this.totalHeight = 0;
- this.spacer.style.height = '0px';
- }
- destroy() {
- this.resizeObs.disconnect();
- if (this.rafId) cancelAnimationFrame(this.rafId);
- for (const [id, card] of this.nodesMap.entries()) { card.unbind(); card.remove(); }
- for (const card of this._pool) { card.remove(); }
- this.nodesMap.clear();
- this._pool = [];
- this.spacer.remove();
- }
- }
- let historyGrid = null;
- const PanelManager = {
- init() {
- if (DOM.currentImages) DOM.currentImages.dataset.renderContext = 'current';
- if (DOM.refList) DOM.refList.dataset.renderContext = 'reference';
- if (DOM.history) DOM.history.dataset.renderContext = 'history';
- if (DOM.history && !historyGrid) historyGrid = new HybridVirtualGrid(DOM.history);
- },
- prepend(order, itemDataMap = STATE.items, context = 'history') {
- if (context === 'history') {
- if (!historyGrid) return;
- historyGrid.updateData(STATE.historyOrder, itemDataMap);
- return;
- }
- const container = null;
- if (!container || !order || order.length === 0) return;
- const nodes =[];
- for (let i = 0; i < order.length; i++) {
- const id = order[i], meta = itemDataMap.get(id);
- if (!meta) continue;
- let card = CardRegistry.get(context, id) || document.createElement('xai-card');
- if (card._bound && card.dataset.id === String(id) && card._context === context) {
- card.refresh(meta, context);
- } else {
- card.bind(meta, context);
- }
- nodes.push(card);
- }
- container.prepend(...nodes);
- },
- async update(order, itemDataMap = STATE.items, context = 'current') {
- if (context === 'history') {
- if (historyGrid) historyGrid.updateData(order, itemDataMap);
- return;
- }
- const container = context === 'reference' ? DOM.refList : DOM.currentImages;
- if (!container) return;
- if (!order || order.length === 0) return this.clear(container);
- const newSet = new Set(order.map(String));
- Array.from(container.children).forEach(child => {
- if (child.tagName === 'XAI-CARD' && !newSet.has(child.dataset.id)) {
- if (typeof child.unbind === 'function') child.unbind();
- child.remove();
- }
- });
- for (let i = 0; i < order.length; i++) {
- const id = String(order[i]), meta = itemDataMap.get(id);
- if (!meta) continue;
- let card = CardRegistry.get(context, id);
- if (!card) {
- card = document.createElement('xai-card');
- card.bind(meta, context);
- } else if (card._bound && card.dataset.id === id && card._context === context) {
- card.refresh(meta, context);
- } else {
- card.bind(meta, context);
- }
- const expectedSibling = container.children[i];
- if (expectedSibling !== card) {
- try {
- container.moveBefore(card, expectedSibling || null);
- } catch {
- container.insertBefore(card, expectedSibling || null);
- }
- }
- }
- },
- clear(container = DOM.currentImages, contextOverride = null) {
- if (!container) return;
- const context = contextOverride || container.dataset.renderContext || (container === DOM.history ? 'history' : container === DOM.refList ? 'reference' : 'current');
- if (context === 'history' && historyGrid) { historyGrid.clear(); CardRegistry.clear(context); return; }
- Array.from(container.children).forEach(n => n.tagName === 'XAI-CARD' && n[Symbol.dispose] ? n[Symbol.dispose]() : null);
- container.replaceChildren();
- if (container === DOM.history || container === DOM.refList || container === DOM.currentImages) container.scrollTop = 0;
- CardRegistry.clear(context);
- }
- };
- const App = {
- ac: new AbortController(), perfObserver: null, recentLagMs: 0, smoothedLoad: 0, isAutoEco: false, ecoHighTicks: 0, ecoLowTicks: 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 = ''; _cleanupGlobalTimers(); VideoHoverPreviewer.cancel(); });
- this.on(window, 'dragover', e => e.preventDefault()); this.on(window, 'drop', e => e.preventDefault());
- this.on(window, 'focus', () => TitleState.handleTabActive());
- this.on(window, 'pagehide', () => {
- if (_skipPagehidePersistence) return;
- flushCurrentPanelSessionSave();
- void DBQueue.flushAppWrites();
- if (_configSaveTask) {
- _configSaveTask.abort();
- _configSaveTask = null;
- localStorage.setItem(APP_CONFIG.STORAGE.CONFIG_KEY, JSON.stringify(normalizeConfig(getConfigSnapshot(), CONFIG_DEFAULTS)));
- }
- });
- this.on(document, 'visibilitychange', () => document.hidden ? TitleState.sync() : TitleState.handleTabActive());
- const loafType = (Array.isArray(PerformanceObserver.supportedEntryTypes) && PerformanceObserver.supportedEntryTypes.includes('long-animation-frame')) ? 'long-animation-frame' : (PerformanceObserver.supportedEntryTypes?.includes('longtask') ? 'longtask' : null);
- if (loafType) {
- this.perfObserver = new PerformanceObserver((list) => { for (const e of list.getEntries()) this.recentLagMs += loafType === 'long-animation-frame' ? Math.max(0, e.duration - 50) : e.duration; });
- this.ac.signal.addEventListener('abort', () => this.perfObserver?.disconnect());
- } else this.perfObserver = null;
- let metInterval = null;
- const startTelemetry = () => {
- if (!metInterval) metInterval = setInterval(() => runBg(updateHistoryMetrics), 2500);
- if (this.perfObserver && loafType) this.perfObserver.observe({ entryTypes: [loafType] });
- };
- const stopTelemetry = () => {
- if (metInterval) { clearInterval(metInterval); metInterval = null; }
- if (this.perfObserver) this.perfObserver.disconnect();
- };
- if (!document.hidden) startTelemetry();
- this.on(document, 'visibilitychange', () => document.hidden ? stopTelemetry() : startTelemetry());
- this.ac.signal.addEventListener('abort', stopTelemetry);
- 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(TEMPLATE_APP.content.cloneNode(true)); document.body.appendChild(appDiv);
- this.ac.signal.addEventListener('abort', () => { document.adoptedStyleSheets = document.adoptedStyleSheets.filter(s => s !== sheet); appDiv.remove(); });
- StorageClient.init(); await loadDataFromDB(); bindLogic(); await loadHistoryFromDB();
- runBg(updateDiskMetric);
- },
- unmount() { if (!STATE.isMounted) return; flushCurrentPanelSessionSave(); void DBQueue.flushAppWrites(); JobManager.abortAll(); this.ac.abort(); PromptSync.clear(); DBQueue.clearPending(); STATE.items.forEach((_, id) => Store.removeItem(id)); PanelManager.clear(); CardRegistry.clear(); RefPayloadCache.clear(); terminateMediaWorker(); _statusViewCacheKey = ''; if (historyGrid) { historyGrid.destroy(); historyGrid = null; } setDocumentTitle(originalTitle); resetState(); }
- };
- const checkRoute = (path) => { const p = typeof path === 'string' ? path : location.pathname; const ok = p.match(/^\/playground\/(imagine|voice)/) || p.match(/^\/team\/[^\/]+\/(imagine|voice)/); ok && !STATE.isMounted ? App.mount() : !ok && STATE.isMounted ? App.unmount() : null; };
- if ('navigation' in window && typeof navigation?.addEventListener === 'function') {
- 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(); }); }
- };
- let _cachedDiskUsage = '--';
- let _diskMetricTimer = null;
- function scheduleDiskUpdate() {
- if (_diskMetricTimer) clearTimeout(_diskMetricTimer);
- _diskMetricTimer = setTimeout(() => {
- _diskMetricTimer = null;
- runBg(updateDiskMetric);
- }, 800);
- }
- async function updateDiskMetric() {
- try { _cachedDiskUsage = ((await navigator.storage.estimate()).usage / 1048576).toFixed(0) + 'M'; } catch(e){}
- if (DOM.metricDisk) DOM.metricDisk.innerText = '⛁\uFE0E ' + _cachedDiskUsage;
- }
- let _metricsUpdateTimer = 0;
- function scheduleHistoryMetricsUpdate() {
- if (_metricsUpdateTimer) clearTimeout(_metricsUpdateTimer);
- _metricsUpdateTimer = setTimeout(() => {
- _metricsUpdateTimer = 0;
- runBg(updateHistoryMetrics);
- }, 300);
- }
- async function updateHistoryMetrics() {
- const lagFactor = Math.min(App.recentLagMs / 1000, 1);
- const memoryFactor = performance.memory ? Math.max(0, Math.min((performance.memory.usedJSHeapSize / (performance.memory.jsHeapSizeLimit || 1) - 0.6) / 0.3, 1)) : 0;
- const currentLoad = Math.max(lagFactor, memoryFactor);
- App.smoothedLoad = (App.smoothedLoad * 0.4) + (currentLoad * 0.6);
- App.recentLagMs = 0;
- const loadPercent = Math.round(App.smoothedLoad * 100);
- if (loadPercent > 75) {
- App.ecoHighTicks++;
- App.ecoLowTicks = 0;
- if (!App.isAutoEco && App.ecoHighTicks >= 4) {
- App.isAutoEco = true; document.body.classList.add('auto-eco');
- applyMemoryPressureProfile('auto-eco-on');
- }
- } else if (loadPercent < 45) {
- App.ecoLowTicks++;
- App.ecoHighTicks = 0;
- if (App.isAutoEco && App.ecoLowTicks >= 8) {
- App.isAutoEco = false; document.body.classList.remove('auto-eco');
- applyMemoryPressureProfile('auto-eco-off');
- }
- } else {
- App.ecoHighTicks = 0;
- App.ecoLowTicks = 0;
- }
- if (DOM.metricSaved) DOM.metricSaved.innerText = STATE.historyOrder.length;
- if (DOM.metricDisk && DOM.metricDisk.innerText === '--') DOM.metricDisk.innerText = '⛁\uFE0E ' + _cachedDiskUsage;
- if (DOM.metricLoad) { DOM.metricLoad.innerText = loadPercent + '%'; DOM.metricLoad.classList.remove('d-none'); }
- }
- function getEligibleRefs(action = STATE.action, { refMode = STATE.refMode } = {}) {
- const acceptedType = (ACTION_SPEC[action] ?? ACTION_SPEC.gen_image).refKind;
- const effectiveRefMode = normalizeRefModeForAction(action, refMode);
- const maxRefs = (ACTION_SPEC[action] ?? ACTION_SPEC.gen_image).maxRefs?.({ action, refMode: effectiveRefMode }) ?? 0;
- const eligible = [];
- for (const item of Store.getRefItems(action, effectiveRefMode)) {
- if (item && item.active && (acceptedType === 'any' || item.type === acceptedType) && eligible.length < maxRefs) eligible.push(item);
- }
- return { eligible, acceptedType, maxRefs };
- }
- function updateUploadPlaceholderState() {
- if (!DOM.uploadPlaceholder) return;
- const count = getRefOrderForView().length;
- const maxRefs = (ACTION_SPEC[STATE.action] ?? ACTION_SPEC.gen_image).maxRefs?.({ action: STATE.action, refMode: normalizeRefModeForAction(STATE.action, STATE.refMode) }) ?? 0;
- const effectiveLimit = maxRefs === 0 ? 0 : Math.min(APP_CONFIG.LIMITS.MAX_REFS_UI, maxRefs);
- const disabled = effectiveLimit === 0 || count >= effectiveLimit;
- DOM.uploadPlaceholder.style.opacity = disabled ? '0.4' : '1';
- DOM.uploadPlaceholder.style.pointerEvents = disabled ? 'none' : 'auto';
- DOM.uploadPlaceholder.querySelector('.upload-label-main').innerText = effectiveLimit === 0 ? 'N/A' : (count >= effectiveLimit ? 'Limit' : 'Add Media');
- if (DOM.refCounter) DOM.refCounter.innerText = count;
- }
- function updateActionUI() { _updateActionUIDom(); }
- function _updateActionUIDom() {
- const action = STATE.action;
- if (DOM.app) DOM.app.dataset.activeAction = action;
- const dOpts = createRangeOptions(1, (ACTION_SPEC[action] ?? ACTION_SPEC.gen_image).durationMax || 15);
- 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;
- if (DOM.settingDuration) 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 = action === 'gen_video';
- if (isVideo && !getAspectRatioOption().isAuto && !ALLOWED_VIDEO_ARS.has(STATE.currentAr)) STATE.currentAr = 'Auto';
- syncDOMControls('currentAr');
- DOM.popoverAr?._updateOptions?.(getAspectRatioPopoverOptions(action));
- const activeUI = (ACTION_SPEC[action] ?? ACTION_SPEC.gen_image).activeUI ||[];
- if (DOM.settingLoops) DOM.settingLoops.value = getLoopsValue(action);
- UI_CONTROL_SCHEMA.forEach(control => {
- if (control.id === 'action') return;
- const targetId = control.wrapperId || control.buttonId || DOM_IDS[control.domKey];
- const el = document.getElementById(targetId);
- if (el) {
- const shouldShow = activeUI.includes(control.id) && (typeof control.isVisible === 'function' ? control.isVisible({ action, state: STATE }) : true);
- el.classList.toggle('d-none', !shouldShow);
- }
- });
- updateStatusUI();
- updateUploadPlaceholderState();
- const isTextMode = action === 'gen_text' || action === 'gen_audio';
- const isMediaRefModeLocked = JobManager.getLane(action) === 'media' && JobManager.getRuntime('media').isGenerating;
- const isRefModeDisabled = isTextMode || isMediaRefModeLocked;
- if (DOM.refModeToggle) {
- DOM.refModeToggle.innerText = isTextMode ? 'Multi' : (STATE.refMode === 'multi' ? 'Multi' : 'Queue');
- DOM.refModeToggle.disabled = isRefModeDisabled;
- DOM.refModeToggle.style.opacity = isRefModeDisabled ? '0.4' : '1';
- DOM.refModeToggle.style.pointerEvents = isRefModeDisabled ? 'none' : 'auto';
- DOM.refModeToggle.title = isTextMode ? 'Queue mode not available for text/audio' : (isMediaRefModeLocked ? 'Queue/Multi mode is locked while media generation is active' : 'Toggle Queue/Multi mode');
- }
- if (DOM.currentImages) DOM.currentImages.dataset.viewMode = action === 'gen_text' ? 'chat' : 'gallery';
- const chatEl = document.getElementById('xai-chat-container');
- if (action === 'gen_text') {
- if (chatEl) chatEl.classList.add('is-active');
- if (DOM.currentImages) DOM.currentImages.style.display = 'none';
- restoreChatFromSession();
- } else {
- if (chatEl) chatEl.classList.remove('is-active');
- if (DOM.currentImages) DOM.currentImages.style.display = '';
- }
- toggleGenerateButton(STATE.isGenerating);
- syncWipeButtonState();
- updateDirtyStateUI();
- }
- 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;--ovk-history-gap:4px;--ovk-card-min-h:68px;--ovk-card-max-h:55vh;}@property --spin-angle{syntax:'<angle>';inherits:false;initial-value:0deg;}@keyframes xai-spin-angle{to{--spin-angle:360deg;}}#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 0;box-sizing:border-box;overflow:hidden;user-select:none;-webkit-user-select:none;}#xai-pro-app input,#xai-pro-app textarea,#xai-pro-app .chat-bubble,#xai-pro-app .chat-bubble-ui{user-select:text;-webkit-user-select:text;cursor:auto;}.col-left .panel{border-left:none;border-radius:0;box-shadow:4px 0 24px rgba(0,0,0,0.15);}.col-right .panel{border-right:none;border-radius:0;box-shadow:-4px 0 24px rgba(0,0,0,0.15);}#xai-pro-app *{box-sizing:border-box;}*{scrollbar-width:thin;scrollbar-color:rgb(from var(--text-muted) r g b / 30%) transparent;}::-webkit-scrollbar{width:3px;height:3px;}::-webkit-scrollbar-track,::-webkit-scrollbar-corner,::-webkit-resizer{background:transparent;display:none !important;}::-webkit-scrollbar-thumb{background:rgb(from var(--text-muted) r g b / 30%);border-radius:3px;}::-webkit-scrollbar-thumb:hover{background:rgb(from var(--text-muted) r g b / 60%);}::-webkit-scrollbar-button{display:none !important;width:0 !important;height:0 !important;}.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,260px),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-center{flex:1;display:flex;flex-direction:column;min-width:300px;position:relative;overflow:hidden;background:var(--bg-center);border-radius:8px;border:1px solid var(--border);}#xai-current-images{position:absolute;inset:0;z-index:1;overflow-y:auto;overflow-x:hidden;display:flex;flex-wrap:wrap;align-content:flex-start;justify-content:center;gap:3px;padding:14px 14px 320px 14px;scroll-behavior:smooth;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;scrollbar-gutter:stable;overflow-anchor:auto;}.xai-chat-view{position:absolute;inset:0;z-index:1;overflow-y:auto;overflow-x:hidden;padding:24px 0 300px 0;display:none;flex-direction:column;scroll-behavior:smooth;scrollbar-gutter:stable;overflow-anchor:auto;}.xai-chat-view.is-active{display:flex;}.chat-scroll-content{display:flex;flex-direction:column;gap:24px;width:100%;max-width:1080px;margin:0 auto;padding:0 24px;}.chat-msg-row{display:flex;gap:16px;width:100%;align-items:flex-start;content-visibility:auto;contain-intrinsic-size:auto 150px;}.chat-msg-row.user{justify-content:flex-end;flex-direction:row-reverse;}.chat-msg-row.ai{justify-content:flex-start;}.chat-avatar-ui{width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-weight:bold;font-size:14px;background:var(--panel);border:1px solid var(--border);color:var(--text-main);}.chat-msg-row.user .chat-avatar-ui{background:rgb(from var(--accent) r g b / 15%);border:none;color:var(--accent);}.chat-bubble-ui{padding:12px 18px;border-radius:16px;font-size:15px;line-height:1.6;color:var(--text-main);white-space:pre-wrap;word-break:break-word;max-width:100%;}.chat-msg-row.user .chat-bubble-ui{background:rgb(from var(--accent) r g b / 10%);border-top-right-radius:4px;}.chat-msg-row.ai .chat-bubble-ui{background:transparent;border:1px solid rgb(from var(--accent) r g b / 15%);border-top-left-radius:4px;padding-top:12px;}.chat-msg-row .chat-bubble-wrapper{position:relative;display:flex;flex-direction:column;align-items:flex-start;max-width:85%;}.chat-msg-row.user .chat-bubble-wrapper{align-items:flex-end;}.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;contain:layout style;}.panel{background:var(--panel);border:1px solid var(--border);border-radius:12px;display:flex;flex-direction:column;overflow:hidden;height:100%;}.panel-header{padding:2px 6px;display:flex;align-items:center;border-bottom:1px solid var(--border);height:34px;flex-shrink:0;gap:6px;justify-content:space-between;}#xai-history{position:relative;overflow-y:auto;overflow-x:hidden;padding:0;flex:1;min-height:0;scrollbar-gutter:stable;overflow-anchor:auto;}#xai-history .scroll-spacer{}#xai-history xai-card:state(history){position:absolute;top:0;left:0;will-change:transform;}#xai-ref-list{position:relative;overflow-y:auto;overflow-x:hidden;padding:6px 0;flex:1;min-height:0;display:flex;flex-direction:column;gap:4px;scrollbar-gutter:stable;overflow-anchor:auto;}.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;}.icon-inline{display:inline-flex;align-items:center;justify-content:center;line-height:1;}.icon-dl{line-height:0;}.prompt-status{font-size:14px;font-weight:500;padding-right:8px;}.popover-separator{border-top:1px solid var(--border);margin:4px 0;}.popover-item--static{cursor:default;}.popover-toggle-input{pointer-events:none;margin:0;accent-color:#555;filter:grayscale(1);}.inline-control-prefix{opacity:.7;margin-right:4px;}.settings-actions{padding:4px 8px;}.wipe-confirm-title{text-align:center;font-weight:700;margin-bottom:8px;font-size:15px;}.wipe-confirm-body{font-size:13px;line-height:1.5;text-align:left;}#xai-ref-list:has(>xai-card:nth-child(2)) xai-card:state(reference){max-height:calc(100cqh / 2 - 4px);}#xai-ref-list:has(>xai-card:nth-child(3)) xai-card:state(reference){max-height:calc(100cqh / 3 - 4px);}#xai-ref-list:has(>xai-card:nth-child(4)) xai-card:state(reference){max-height:calc(100cqh / 4 - 4px);}#xai-ref-list:has(>xai-card:nth-child(5)) xai-card:state(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:transparent;color:var(--text-main);position:relative;z-index:1;}.xai-btn::before{content:'';position:absolute;inset:0;border-radius:inherit;background:rgb(from var(--accent)r g b/12%);z-index:-1;pointer-events:none;}.xai-btn::after{content:'';position:absolute;inset:0;border-radius:inherit;background:rgb(from var(--accent)r g b/20%);border:1px solid rgb(from var(--accent)r g b/25%);opacity:0;z-index:-1;transition:opacity .15s ease;pointer-events:none;}.xai-btn:hover:not(:disabled)::after{opacity:1;}.xai-btn:active:not(:disabled){transform:none;}.xai-btn--compact{flex:1;height:28px;padding:0 12px;font-size:12px;}.xai-btn--inline-number{gap:0;}.xai-btn--ref-mode{width:68px;height:32px;font-size:12px;padding:0;}.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::before{background:rgb(from var(--danger)r g b/15%);}.xai-btn--danger::after{background:var(--danger);border:1px solid var(--danger);}.xai-btn--danger{color:var(--danger);}.xai-btn--danger:hover:not(:disabled)::after{opacity:1;}.xai-btn--danger:hover:not(:disabled){color:#fff;}.xai-btn--warning::before{background:rgb(from var(--warning)r g b/15%);}.xai-btn--warning::after{background:var(--warning);border:1px solid var(--warning);}.xai-btn--warning{color:var(--warning);}.xai-btn--warning:hover:not(:disabled)::after{opacity:1;}.xai-btn--warning:hover:not(:disabled){color:#fff;}.xai-btn--orange::before{background:rgb(249 115 22 / 15%);}.xai-btn--orange::after{background:#f97316;border:1px solid #f97316;}.xai-btn--orange{color:#f97316;}.xai-btn--orange:hover:not(:disabled)::after{opacity:1;}.xai-btn--orange:hover:not(:disabled){color:#fff;}.xai-btn--success::before{background:var(--success);box-shadow:0 2px 10px rgb(0 0 0/20%);}.xai-btn--success{color:#fff;}.xai-btn--run::before{background:#ffffff;}.xai-btn--run{color:#0f1419;}.xai-btn--run:hover:not(:disabled)::before{background:#d7dbdc;}.xai-btn--run.is-dirty::before{background:var(--warning);}.xai-btn--run.is-dirty{color:#000;box-shadow:0 0 15px rgb(from var(--warning) r g b / 25%);}.xai-btn--stop::before{background:var(--danger);}.xai-btn--stop{color:#fff;}.xai-btn--circle-lg{width:42px;height:42px;padding:0;border-radius:50%;font-size:22px;margin:0 -4px -6px 0;contain:layout size;}.xai-btn--circle-lg::before{box-shadow:0 4px 12px rgb(0 0 0/20%);}.xai-btn--circle-lg::after{background:transparent;border:none;box-shadow:0 6px 16px rgb(0 0 0/30%);}.xai-btn--circle-lg:hover:not(:disabled){transform:scale(1.05);}.xai-btn--circle-lg:hover:not(:disabled)::after{opacity:1;}.xai-btn--circle-lg.is-running::after{content:"";position:absolute;inset:0;border-radius:50%;border:2px solid var(--danger);box-shadow:none;background:transparent;animation:xai-pulse-stop-optimized 2s cubic-bezier(0.1,0.7,1,0.1) infinite;pointer-events:none;opacity:0;will-change:transform,opacity;}@keyframes xai-pulse-stop-optimized{0%{transform:scale(1);opacity:0.6}70%{transform:scale(1.4);opacity:0}100%{transform:scale(1);opacity: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:32px;box-sizing:border-box;border-radius:16px;font-family:monospace;font-size:12px;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:26px;min-width:26px;padding:0;justify-content:center;}#metric-saved{width:48px;min-width:48px;padding:0;justify-content:center;}#metric-load{width:46px;min-width:46px;padding:0;justify-content:center;}#metric-disk{width:max-content;min-width:64px;padding:0 8px;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%);anchor-name:--prompt-main-box;contain:layout style;interpolate-size:allow-keywords;}.prompt-container::before{content:'';position:absolute;inset:-2px;border-radius:inherit;pointer-events:none;z-index:0;opacity:0;}.prompt-container:hover,.prompt-container:focus-within{background:rgba(43,45,49,1);border-color:rgb(255 255 255/15%);}.prompt-container.drag-over::before{border:2px solid var(--success);opacity:1;}.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::before{border:2px solid var(--success);opacity:1;}.prompt-container.editing-ref{background:rgb(17 40 28)!important;border-color:var(--success)!important;box-shadow:none !important;}.textarea-wrapper{margin-bottom:8px;display:flex;flex-direction:column;transition:mask-image .2s,-webkit-mask-image .2s;animation:textarea-scroll-shadow linear both;animation-timeline:scroll(self block);}@keyframes textarea-scroll-shadow{from{mask-image:linear-gradient(to bottom,#000 100%,transparent)}to{mask-image:linear-gradient(to bottom,#000 calc(100% - 20px),transparent)}}.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;padding:0 4px;scrollbar-width:thin;field-sizing:content;min-height:50px;max-height:50px;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:12px;height:32px;}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);}body.is-dragging .dropzone-children{pointer-events:none;}.prompt-container:has(:drop-target){border:2px solid var(--success);background:color-mix(in srgb,var(--success)40%,rgba(43,45,49,1));}#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;}:is(.xai-custom-dialog,.modal-box){margin:auto;padding:0;outline:none;border:none;background:transparent;align-items:center;justify-content:center;}:is(.xai-custom-dialog,.modal-box)::backdrop{background:rgba(0,0,0,.6);backdrop-filter:blur(3px);}:is(.xai-custom-dialog,.modal-box)[open]{display:flex;}.dialog-inner{user-select:none;background:var(--panel);border:1px solid var(--border);border-radius:20px;color:var(--text-main);padding:16px;opacity:1;}.dialog-inner--confirm{min-width:250px;}.dialog-inner--fav{max-width:1600px;width:95vw;border-radius:24px;padding:4px;}.confirm-message{white-space:pre-wrap;margin-bottom:15px;text-align:left;}.dialog-actions{display:flex;gap:10px;justify-content:center;}.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-color:oklch(0 0 0 / 0.92);}.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%;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.is-loaded .lightbox-media--preview{z-index:2;transform:none!important;filter:none!important;opacity:1;transition:opacity 0.25s ease-out;}.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;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:6px 10px;font-size:14px;font-weight:500;border-radius:12px;cursor:pointer;display:flex;align-items:center;gap:8px;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:anchor-size(width);max-width:max-content;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:60px;padding:4px;position-try-fallbacks:flip-block;}#popover-duration .popover-item{padding:6px 12px;justify-content:center;}#btn-setting-voice{anchor-name:--a-v;gap:3px;padding:0 10px 0 8px;align-items:center;}#popover-voice{position-anchor:--a-v;bottom:anchor(top);left:anchor(left);margin-bottom:8px;min-width:anchor-size(width);max-width:max-content;position-try-fallbacks:flip-block;}#btn-setting-language{anchor-name:--a-lang;gap:3px;padding:0 10px 0 8px;align-items:center;}#popover-language{position-anchor:--a-lang;bottom:anchor(top);left:anchor(left);margin-bottom:8px;min-width:anchor-size(width);max-width:max-content;position-try-fallbacks:flip-block;max-height:320px;overflow-y:auto;}#btn-setting-tts-quality{anchor-name:--a-tts-q;gap:3px;padding:0 10px 0 8px;align-items:center;}#popover-tts-quality{position-anchor:--a-tts-q;bottom:anchor(top);left:anchor(left);margin-bottom:8px;min-width:anchor-size(width);max-width:max-content;position-try-fallbacks:flip-block;}#btn-tts-effects{anchor-name:--a-tts-e;}#popover-tts-effects{position-anchor:--a-tts-e;bottom:anchor(top);left:anchor(left);margin-bottom:8px;min-width:200px;max-height:400px;overflow-y:auto;position-try-fallbacks:flip-block;}#popover-tts-effects .tts-tag-group{padding:6px 10px;font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.5px;border-bottom:1px solid var(--border);}#popover-tts-effects .tts-tag-item{padding:6px 12px;cursor:pointer;font-family:monospace;font-size:12px;}#popover-tts-effects .tts-tag-item:hover{background:var(--bg-hover);}#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;bottom:100%;left:50%;translate:-50% 0;margin-bottom:56px;display:flex;flex-direction:column;align-items:center;gap:10px;width:max-content;max-width:90vw;pointer-events:none;z-index:calc(var(--z-prompt)+ 1);}.xai-toast{background:rgba(20,20,22,0.95);border:1px solid rgba(255,255,255,0.12);box-shadow:0 8px 32px rgba(0,0,0,0.3);border-radius:24px;padding:10px 20px;color:#fff;font-size:13px;font-weight:500;pointer-events:auto;display:flex;align-items:center;gap:10px;opacity:0;transform:translateY(16px)scale(0.95);transition:opacity .3s ease,transform .4s cubic-bezier(0.34,1.56,0.64,1);will-change:transform,opacity;}.xai-toast.show{opacity:1;transform:translateY(0)scale(1);}.xai-toast.error{border-left:3px solid var(--danger);padding-left:18px;}.xai-toast.warn{border-left:3px solid var(--warning);padding-left:18px;}.xai-toast.success{border-left:3px solid var(--success);padding-left:18px;}.floating-edit-label,.floating-update-label{position:absolute;position-anchor:--prompt-main-box;bottom:100%;bottom:anchor(top);margin-bottom:8px;left:50%;left:anchor(center);translate:-50% 0;background:rgba(20,20,22,0.95);border:1px solid rgba(255,255,255,0.12);box-shadow:0 8px 32px rgba(0,0,0,0.3);border-radius:20px;padding:8px 18px;font-size:13px;font-weight:600;letter-spacing:0.3px;color:#fff;pointer-events:none;z-index:var(--z-prompt);white-space:nowrap;contain:layout style;opacity:0;transform:translateY(16px) scale(0.95);transition:opacity 0.3s ease,transform 0.4s cubic-bezier(0.34,1.56,0.64,1),bottom 0.2s linear;will-change:transform,opacity;}.floating-edit-label.show,.floating-update-label.show{opacity:1;transform:translateY(0) scale(1);}.floating-update-label{color:#fcd34d;border-color:rgba(245,158,11,0.3);box-shadow:0 8px 32px rgba(0,0,0,0.3),0 0 16px rgba(245,158,11,0.15);}.floating-update-label.saved{color:#6ee7b7;border-color:rgba(16,185,129,0.3);box-shadow:0 8px 32px rgba(0,0,0,0.3),0 0 16px rgba(16,185,129,0.15);}.floating-edit-label{background:rgba(16,185,129,0.95);color:#fff;border-color:rgba(255,255,255,0.3);box-shadow:0 8px 32px rgba(0,0,0,0.3),0 0 20px rgba(16,185,129,0.4);}.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:34px;font-variant-numeric:tabular-nums;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{z-index:2;transform:none!important;filter:none!important;opacity:1;transition:opacity 0.25s ease-out;}.lightbox-media--full{z-index:3;transition:opacity 0.15s ease-out;}.lightbox-media--video{background:rgb(0 0 0 / 35%);}.dl-abs-btn{position:absolute;top:12px;right:12px;pointer-events:auto;background:rgb(0 0 0 / 50%) !important;color:#fff !important;border:1px solid rgb(from var(--accent) r g b / 20%) !important;border-radius:8px;width:40px;height:40px;display:flex;align-items:center;justify-content:center;z-index:1000;box-shadow:0 8px 24px rgba(0,0,0,0.6) !important;transition:background-color 0.15s,color 0.15s,transform 0.15s;}.dl-abs-btn:hover{background:var(--text-main) !important;border-color:var(--text-main) !important;color:var(--bg) !important;transform: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};}.xai-btn--circle-lg{position:relative;}.xai-btn--circle-lg.is-running.is-dirty::after{content:'';position:absolute;inset:-4px;border-radius:50%;padding:4px;background:conic-gradient(from var(--spin-angle),transparent 0%,transparent 60%,var(--warning)100%);-webkit-mask:linear-gradient(#fff 0 0)content-box,linear-gradient(#fff 0 0);-webkit-mask-composite:xor;mask-composite:exclude;animation:xai-spin-angle 1s linear infinite;pointer-events:none;opacity:1;border:none;box-shadow:none;transform:translateZ(0);will-change:--spin-angle;}.xai-btn--circle-lg.flash-saved::after{content:'';position:absolute;inset:0;border-radius:50%;background:var(--success);opacity:0;animation:xai-flash-glow .6s ease-out;pointer-events:none;}@keyframes xai-flash-glow{0%{opacity:.8;transform:scale(1)}100%{opacity:0;transform:scale(1.3)};}#xai-api-res-img,#xai-api-res-vid,#btn-setting-batch,#btn-setting-duration,#btn-setting-loops,#btn-setting-tts-quality{font-variant-numeric:tabular-nums;}#btn-setting-loops{justify-content:center;}#btn-setting-voice{width:62px;justify-content:space-between;padding:0 10px 0 12px;}#btn-setting-tts-quality{width:66px;justify-content:space-between;padding:0 8px 0 12px;}#btn-setting-duration{width:48px;justify-content:space-between;padding:0 8px 0 12px;}#btn-setting-batch{width:48px;justify-content:space-between;padding:0 8px 0 12px;}.lightbox-audio-card{position:relative;width:100%;height:100%;display:flex;flex-direction:column;background:radial-gradient(circle at top,#1f2937 0%,#030712 100%);border-radius:16px;box-shadow:0 24px 64px rgba(0,0,0,0.6);overflow:hidden;border:1px solid rgba(255,255,255,0.05);}.lightbox-audio-text{flex:1;display:flex;align-items:center;justify-content:center;padding:32px 48px;font-size:clamp(16px,4vw,32px);color:rgba(255,255,255,0.85);text-align:center;font-family:monospace;overflow-y:auto;text-wrap:balance;}.lightbox-audio-player{width:100%;height:54px;background:rgba(0,0,0,0.5);outline:none;}#btn-setting-text-model{anchor-name:--a-tm;gap:3px;padding:0 10px 0 8px;align-items:center;}#popover-text-model{position-anchor:--a-tm;bottom:anchor(top);left:anchor(left);margin-bottom:8px;min-width:anchor-size(width);max-width:max-content;position-try-fallbacks:flip-block;}#btn-setting-reasoning{anchor-name:--a-re;gap:3px;padding:0 10px 0 8px;align-items:center;}#popover-reasoning{position-anchor:--a-re;bottom:anchor(top);left:anchor(left);margin-bottom:8px;min-width:anchor-size(width);max-width:max-content;position-try-fallbacks:flip-block;}#xai-pro-app[data-active-action="gen_text"] .prompt-container{background:rgba(43,45,49,1)!important;border-color:rgb(255 255 255/15%)!important;}.xai-cite-inline{vertical-align:super;font-size:0.8em;font-weight:700;margin:0 2px;color:#3b82f6;text-decoration:none;}.xai-cite-inline:hover{text-decoration:underline;}.xai-lightbox-dialog,.xai-lightbox-dialog::backdrop{transition:background-color .1s ease!important;}.xai-lightbox-dialog:not([open]),.xai-lightbox-dialog:not([open])::backdrop{transition:none!important;}.lightbox-media{border-radius:0!important;box-shadow:none!important;transition:opacity .15s ease-out!important;transform:none!important;filter:none!important;}.lightbox-audio-card{border-radius:16px!important;}.prompt-container{z-index:1;}.chat-msg-row.ai .chat-bubble-wrapper--ai{background:rgb(from var(--accent) r g b / 2%);border:1px solid rgb(from var(--accent) r g b / 15%);border-radius:16px;border-top-left-radius:4px;overflow:hidden;box-shadow:0 10px 30px rgb(0 0 0 / 10%);}.chat-msg-row.ai .chat-bubble-wrapper--ai .chat-bubble-ui{border:none!important;padding:12px 18px 10px;}.chat-msg-row.ai.status-msg .chat-bubble-wrapper--ai{border-color:rgb(from var(--accent) r g b / 12%);background:rgb(from var(--accent) r g b / 3%);}.chat-msg-row.ai.status-msg .chat-bubble-ui{padding:8px 16px !important;min-height:0 !important;display:inline-flex !important;align-items:center;white-space:nowrap !important;text-wrap:nowrap !important;overflow:hidden !important;text-overflow:ellipsis !important;word-break:keep-all !important;line-height:1.2 !important;max-width:none !important;}.chat-bubble-ui.is-status{padding-bottom:12px;color:#dbe1ea;}.chat-status-text{white-space:nowrap !important;overflow:hidden !important;line-height:1.2 !important;font-style:italic;font-weight:600;background:linear-gradient(110deg,#8a939e 0%,#ffffff 30%,#8a939e 60%);background-size:200% auto;color:transparent;-webkit-background-clip:text;background-clip:text;animation:xai-text-glow 3s linear infinite;margin:0;}@keyframes xai-text-glow{0%{background-position:200% center}100%{background-position:-200% center}}.chat-msg-toolbar{display:flex;align-items:center;justify-content:flex-start;gap:10px;width:100%;padding:8px 12px 10px;border-top:1px solid rgb(from var(--accent) r g b / 10%);}.chat-msg-action{display:inline-flex;align-items:center;appearance:none;border:none;background:transparent;color:var(--text-muted);font:inherit;font-size:12px;font-weight:600;line-height:1;min-height:30px;padding:0 10px;border-radius:999px;cursor:pointer;transition:background-color .15s ease,color .15s ease;}.chat-msg-action:hover{background:rgb(from var(--accent) r g b / 10%);color:var(--text-main);}.chat-msg-action[hidden]{display:none!important;}.chat-msg-action--icon{padding:0 8px;min-width:34px;font-size:15px;justify-content:center;}.chat-msg-action--new-chat{margin-left:auto;background:rgb(from var(--accent) r g b / 8%);box-shadow:inset 0 0 0 1px rgb(from var(--accent) r g b / 10%);}.chat-bubble-ui pre{background:rgb(0 0 0/30%);padding:12px;border-radius:8px;overflow-x:auto;margin:8px 0;position:relative;}.chat-bubble-ui code{font-family:monospace;background:rgb(0 0 0/20%);padding:2px 4px;border-radius:4px;}.chat-bubble-ui pre code{padding:0;background:transparent;}.chat-bubble-ui p{margin:0 0 8px 0;}.chat-bubble-ui p:last-child{margin:0;}.chat-bubble-ui ul,.chat-bubble-ui ol{margin:8px 0;padding-left:24px;}.chat-bubble-ui blockquote{border-left:3px solid var(--accent);margin:8px 0;padding-left:12px;opacity:0.8;}.md-table{overflow-x:auto;}.chat-bubble-ui table{border-collapse:collapse;width:100%;margin:8px 0;}.chat-bubble-ui th,.chat-bubble-ui td{border:1px solid var(--border);padding:6px 12px;text-align:left;}.chat-bubble-ui th{background:rgb(0 0 0/20%);}.md-lang{position:absolute;right:8px;top:8px;font-size:10px;opacity:0.5;}.chat-msg-row.ai .chat-bubble-wrapper--ai{background:#313338!important;border:1px solid #1e1f22!important;}.chat-msg-row.ai .chat-bubble-ui{color:#dbdee1!important;}.chat-msg-row.ai .chat-bubble-ui strong,.chat-msg-row.ai .chat-bubble-ui h1,.chat-msg-row.ai .chat-bubble-ui h2,.chat-msg-row.ai .chat-bubble-ui h3,.chat-msg-row.ai .chat-bubble-ui h4,.chat-msg-row.ai .chat-bubble-ui h5,.chat-msg-row.ai .chat-bubble-ui h6{color:#f2f3f5!important;}.chat-msg-row.ai .chat-bubble-ui pre,.chat-msg-row.ai .chat-bubble-ui code{background:#2b2d31!important;color:#dbdee1!important;border-color:#1e1f22!important;}.chat-msg-row.ai .chat-bubble-ui table th{background:#2b2d31!important;}.chat-msg-row.ai .chat-bubble-ui table th,.chat-msg-row.ai .chat-bubble-ui table td{border-color:#1e1f22!important;}.chat-msg-row.ai .chat-bubble-ui a{color:#00a8fc!important;}.chat-msg-row.user .chat-bubble-ui{background:#1a1d21!important;color:#e6edf3!important;border:1px solid #30363d!important;border-radius:20px 20px 4px 20px!important;}.chat-msg-action--new-chat{color:#fff!important;}.chat-bubble-ui h1,.chat-bubble-ui h2,.chat-bubble-ui h3,.chat-bubble-ui h4,.chat-bubble-ui h5,.chat-bubble-ui h6{margin:16px 0 8px;font-weight:700;line-height:1.3;color:#f2f3f5;}.chat-bubble-ui h1{font-size:1.6em;}.chat-bubble-ui h2{font-size:1.4em;}.chat-bubble-ui h3{font-size:1.2em;}.chat-bubble-ui h4{font-size:1.1em;}.chat-bubble-ui hr{border:none;border-top:1px solid var(--border);margin:16px 0;display:block;height:1px;}.chat-bubble-ui ul{list-style:disc outside;padding-left:24px;margin:8px 0;}.chat-bubble-ui ol{list-style:decimal outside;padding-left:24px;margin:8px 0;}.chat-bubble-ui li{display:list-item;margin-bottom:4px;}.chat-bubble-ui li.task{list-style:none;margin-left:-24px;}.chat-bubble-ui li.task input[type="checkbox"]{margin-right:8px;vertical-align:middle;}.chat-msg-row.ai.status-msg .chat-bubble-wrapper--ai{width:fit-content;max-width:100%;}.chat-msg-row.ai.status-msg .chat-msg-toolbar{display:none !important;}.chat-status-text{display:inline !important;white-space:nowrap !important;text-wrap:nowrap !important;word-break:keep-all !important;overflow:visible !important;}.xai-btn--run .btn-content::before{content:'↑';font-size:22px;}.xai-btn--stop .btn-content::before{content:'■';font-size:16px;}`;
- const UI_HTML = buildAppHtml();
- const getActiveRefIdsSet = () => {
- const set = new Set();
- for (let i = 0; i < REF_VIEW_KEYS.length; i++) {
- const order = STATE.refViews[REF_VIEW_KEYS[i]]?.order;
- if (order) for (let j = 0; j < order.length; j++) set.add(order[j]);
- }
- return set;
- };
- const processDeletions = (targetIds) => {
- const refIds = getActiveRefIdsSet();
- const targetSet = new Set(targetIds.map(String));
- const toVirtualize =[...targetSet.intersection(refIds)];
- const toDelete = [...targetSet.difference(refIds)];
- return { toDelete, toVirtualize };
- };
- const virtualizeRefs = (ids) => {
- if (!ids?.length) return;
- const idSet = new Set(ids.map(String));
- STATE.historyOrder = STATE.historyOrder.filter(id => !idSet.has(String(id)));
- removeIdsFromAllPanels(ids);
- ids.forEach(id => {
- const item = STATE.items.get(String(id));
- if (item && !item.isUploadedRef) {
- item.isUploadedRef = true;
- DBQueue.add(item);
- }
- });
- PanelManager.update(STATE.historyOrder, STATE.items, 'history');
- scheduleHistoryMetricsUpdate();
- };
- async function enforceHistoryLimit() {
- if (STATE.historyOrder.length > APP_CONFIG.LIMITS.MAX_HISTORY) {
- const overflowIds = STATE.historyOrder.slice(APP_CONFIG.LIMITS.MAX_HISTORY);
- STATE.historyOrder = STATE.historyOrder.slice(0, APP_CONFIG.LIMITS.MAX_HISTORY);
- const { toDelete, toVirtualize } = processDeletions(overflowIds);
- if (toVirtualize.length > 0) virtualizeRefs(toVirtualize);
- if (toDelete.length > 0) {
- DBQueue.forgetItems(toDelete);
- removeIdsFromAllPanels(toDelete);
- pruneReferenceState(toDelete, { closePrompt: false });
- await StorageClient.deleteItemsBulk(toDelete);
- await clearTextSessionArtifacts(toDelete);
- toDelete.forEach(id => Store.removeItem(id));
- }
- }
- }
- function removeCurrentPanelItems(ids, { purgeStorage = true } = {}) {
- const keys = sanitizeIdListStore(ids);
- if (!keys.length) { scheduleCurrentPanelSessionSave(0); return; }
- const removed = new Set(keys);
- DBQueue.forgetItems(keys);
- STATE.historyOrder = STATE.historyOrder.filter(id => !removed.has(String(id)));
- removeIdsFromAllPanels(keys);
- pruneReferenceState(keys, { closePrompt: false });
- if (STATE.activeLightboxId && removed.has(String(STATE.activeLightboxId))) closeLightbox();
- keys.forEach(id => Store.removeItem(id));
- keys.forEach(id => {
- document.querySelectorAll(`xai-card[data-id="${id}"]`).forEach(node => {
- if (node.closest('#xai-history') && historyGrid) { node.unbind(); } else { node.remove(); }
- });
- });
- if (historyGrid) historyGrid.updateData(STATE.historyOrder, STATE.items);
- scheduleCurrentPanelSessionSave(0);
- scheduleDiskUpdate();
- if (purgeStorage) runBg(async () => {
- await StorageClient.deleteItemsBulk(keys).catch(e => addLog('WARN', 'Remove failed', e));
- await clearTextSessionArtifacts(keys);
- });
- }
- async function wipeCardHistoryData() {
- const targetIds =[];
- STATE.items.forEach((item, id) => {
- if (item && !item.isUploadedRef) targetIds.push(String(id));
- });
- if (!targetIds.length) return 0;
- const { toDelete, toVirtualize } = processDeletions(targetIds);
- if (toVirtualize.length > 0) virtualizeRefs(toVirtualize);
- if (toDelete.length > 0) {
- removeCurrentPanelItems(toDelete, { purgeStorage: false });
- await DBQueue.flushAppWrites(['currentOrder', ACTIVE_TEXT_SESSION_KEY]);
- await StorageClient.deleteItemsBulk(toDelete);
- flushCurrentPanelSessionSave();
- }
- return toDelete.length + toVirtualize.length;
- }
- async function loadDataFromDB() {
- const [
- promptsRaw,
- promptDraftsRaw,
- favPromptsRaw,
- refPromptsRaw,
- refViewsRaw,
- currentOrderRaw,
- activeTextSessionRaw,
- currentPanelSessionRaw,
- timingsRaw
- ] = await Promise.all([
- StorageClient.loadMeta('prompts'),
- StorageClient.loadMeta('promptDrafts'),
- StorageClient.loadMeta('favPrompts'),
- StorageClient.loadMeta('refPrompts'),
- StorageClient.loadMeta('refViews'),
- StorageClient.loadMeta('currentOrder'),
- StorageClient.loadMeta(ACTIVE_TEXT_SESSION_KEY),
- StorageClient.loadMeta('currentPanelSession'),
- StorageClient.loadMeta('timings')
- ]);
- STATE.prompts = sanitizePromptStore(promptsRaw, STATE.prompts);
- STATE.promptDrafts = sanitizePromptStore(promptDraftsRaw, STATE.prompts);
- STATE.favPrompts = sanitizeFavPromptStore(favPromptsRaw, STATE.favPrompts);
- STATE.refPrompts = sanitizeRefPromptStore(refPromptsRaw, STATE.refPrompts);
- STATE.refViewsTmp = refViewsRaw ? sanitizeRefViewStore(refViewsRaw) : null;
- STATE.panelOrdersTmp = sanitizePanelOrderStore(currentOrderRaw);
- STATE.activeTextSessionTmp = typeof activeTextSessionRaw === 'string' ? activeTextSessionRaw : null;
- let localSessionRaw = null;
- try {
- const stored = sessionStorage.getItem(APP_CONFIG.STORAGE.CURRENT_PANEL_SESSION_KEY);
- if (stored) localSessionRaw = JSON.parse(stored);
- } catch(e) {}
- STATE.currentPanelSessionTmp = sanitizePanelSessionMapStore(localSessionRaw || currentPanelSessionRaw);
- STATE.timings = timingsRaw || {};
- }
- async function extractVideoPosterData(videoUrl, pItem = null) {
- if (pItem?.id) {
- try {
- const workerResult = await StorageClient.extractVideoPosterWorker(pItem.id, pItem.mimeType || 'video/mp4');
- if (!workerResult.fallback) {
- return {
- thumbBlob: workerResult.thumbBlob,
- lqip: workerResult.lqip,
- ratio: workerResult.ratio
- };
- }
- addLog('WARN', `Worker Media Pipeline failed: ${workerResult.reason}. Falling back to main-thread <video> element.`);
- } catch (err) {
- addLog('WARN', 'Worker poster extraction threw error. Falling back to <video> element.', err);
- }
- }
- const { promise, resolve, reject } = Promise.withResolvers();
- const video = document.createElement('video');
- Object.assign(video, { muted: true, playsInline: true, preload: 'auto' });
- const signal = AbortSignal.timeout(15000);
- const onAbort = () => {
- cleanup();
- reject(new Error('Timeout: Failed to load video poster'));
- };
- signal.addEventListener('abort', onAbort, { once: true });
- const cleanup = () => {
- signal.removeEventListener('abort', onAbort);
- video.onerror = null;
- resetVideoElement(video, { pause: false });
- };
- const onFrame = async () => {
- if (signal.aborted) return;
- try {
- using bmp = await createImageBitmap(video, { resizeWidth: APP_CONFIG.MEDIA.THUMB_MAX_DIM, resizeQuality: 'medium' });
- const oc = new OffscreenCanvas(bmp.width, bmp.height), ctx = oc.getContext('2d', { alpha: false });
- ctx.drawImage(bmp, 0, 0);
- const thumbBlob = await oc.convertToBlob({ type: 'image/webp', quality: APP_CONFIG.MEDIA.THUMB_WEBP_QUALITY });
- const lqipOc = new OffscreenCanvas(2, 2), lqCtx = lqipOc.getContext('2d', { alpha: false });
- lqCtx.drawImage(bmp, 0, 0, 2, 2);
- const d = lqCtx.getImageData(0, 0, 2, 2).data;
- const lqip = `radial-gradient(ellipse at 30% 30%, rgb(${d[0]} ${d[1]} ${d[2]}), rgb(${d[12]} ${d[13]} ${d[14]}))`;
- oc.width = 1; oc.height = 1;
- lqipOc.width = 1; lqipOc.height = 1;
- cleanup();
- resolve({ thumbBlob, lqip, ratio: bmp.width / bmp.height });
- } catch (err) { cleanup(); reject(err); }
- };
- video.addEventListener('loadeddata', () => {
- if (signal.aborted) return;
- video.currentTime = 0.1;
- video.requestVideoFrameCallback(onFrame);
- }, { once: true });
- video.onerror = () => { cleanup(); reject(new Error('Video load error for poster')); };
- video.src = videoUrl;
- try { return await promise; }
- catch (e) {
- addLog('WARN', '[Poster_Extract] Failed to extract poster', e);
- if (pItem?.refSourceId) {
- try {
- const refThumb = await OPFS.read(pItem.refSourceId, 'thumb');
- if (refThumb) return { thumbBlob: refThumb, lqip: pItem.lqip, ratio: pItem.ratio };
- } catch (err) {}
- }
- return null;
- }
- }
- const DiskIOLimiter = createLimiter(2);
- const scheduleAutoDownloads = (items = [], baseDelay = 500, stepDelay = 0) => {
- if (!STATE.autoDownload) return;
- items.forEach((item, idx) => setTimeout(() => MediaUtils.downloadItem(item, true), baseDelay + (idx * stepDelay)));
- };
- const commitItemsToHistory = async (items = [], ids = items.map(item => item?.id).filter(Boolean)) => {
- if (!ids.length) return;
- Store.prependHistory(ids);
- await enforceHistoryLimit();
- };
- function recoverPendingVideos() {
- const pendingItems = [...STATE.items.values()].filter(i => i.isPending && i.reqId);
- if (!pendingItems.length) return;
- const now = Date.now();
- addLog('INFO', `Recovering ${pendingItems.length} background video tasks`);
- for (const item of pendingItems) {
- if (now - (item.timestamp || 0) > 1000 * 60 * 60 * 2) {
- UI.showToast("Background video task expired and was removed", "warn");
- removeCurrentPanelItems([item.id], { purgeStorage: true });
- continue;
- }
- const ac = new AbortController();
- const onProgress = (val) => {
- item._statusData = buildCurrentStatusLines(val);
- const card = CardRegistry.get('current', item.id) || CardRegistry.get('history', item.id);
- if (card) { card.setStatus(item._statusData); card.setProgress(val.progress); }
- };
- runBg(async () => {
- try {
- const videoUrl = await pollVideoTask(item.reqId, ac.signal, -1, onProgress);
- if (!videoUrl || !STATE.items.has(item.id)) return;
- onProgress({ shortStatus: 'Saving video...', longStatus: 'Downloading full video...', color: '#3b82f6', progress: 95 });
- const videoBlob = await fetchBlobCSP(videoUrl, { signal: ac.signal, timeout: 30000 });
- await OPFS.write(item.id, 'full', videoBlob);
- onProgress({ shortStatus: 'Extracting poster...', longStatus: 'Extracting poster image...', color: '#3b82f6', progress: 100 });
- using localVideoUrl = new DisposableURL(videoBlob);
- let posterData = await extractVideoPosterData(localVideoUrl.url, item);
- if (posterData?.thumbBlob) await OPFS.write(item.id, 'thumb', posterData.thumbBlob);
- finalizePendingVideoItem(item, videoUrl, posterData);
- DBQueue.add(item);
- scheduleCurrentPanelSessionSave();
- await commitItemsToHistory([item]);
- const card = CardRegistry.get('current', item.id) || CardRegistry.get('history', item.id);
- if (card) await refreshFinalizedVideoCard(card, item);
- UI.showToast("Background video task completed!", "success");
- } catch (e) {
- if (!STATE.items.has(item.id)) return;
- UI.showToast(`Video failed and was removed: ${getErrorMessage(e).substring(0, 40)}`, "error");
- removeCurrentPanelItems([item.id], { purgeStorage: true });
- }
- });
- }
- }
- 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 StorageClient.loadAllHistory();
- const historyIds = [];
- const phantomIds = [];
- TaskChunker.reset();
- 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 && !m.isPending) historyIds.push(m.id);
- await TaskChunker.yieldIfNeeded();
- }
- if (phantomIds.length > 0) await StorageClient.deleteItemsBulk(phantomIds);
- STATE.historyOrder = historyIds;
- const historyIdSet = new Set(historyIds);
- STATE.refViews = STATE.refViewsTmp ? sanitizeRefViewStore(serializeRefViewStore(STATE.refViewsTmp)) : createDefaultRefViewsFromItems();
- STATE.refViewsTmp = undefined;
- updateRefState({ forceRebuild: true });
- const sessionPanels = STATE.currentPanelSessionTmp?.panels || {};
- const sessionItemIds = new Set();
- Object.values(sessionPanels).forEach(panel => {
- panel?.items?.forEach(item => {
- sessionItemIds.add(item.id);
- const existing = STATE.items.get(item.id);
- if (existing) {
- Object.assign(existing, item);
- if (item.prompt || item.responseText || item.textChatMessages) {
- LazyTextStore.set(item.id, { prompt: item.prompt || '', responseText: item.responseText || '', textChatMessages: item.textChatMessages || null });
- }
- }
- else {
- const revived = { ...item, persist: !item.isPending };
- Store.addItem(revived);
- if (revived.persist !== false) DBQueue.add(revived);
- }
- if (!item.isPending && !item.isUploadedRef && !historyIdSet.has(item.id)) {
- const orphan = STATE.items.get(item.id);
- if (orphan && orphan.persist === false) {
- orphan.persist = true;
- DBQueue.add(orphan);
- }
- }
- });
- });
- const restoredPanelOrders = createEmptyPanelOrderStore();
- PANEL_ACTION_KEYS.forEach(panelKey => {
- const sessionPanel = sessionPanels[panelKey];
- const storedOrder = sanitizeIdListStore(STATE.panelOrdersTmp?.[panelKey]);
- const sourceOrder = sessionPanel?.order?.length ? sessionPanel.order : storedOrder;
- restoredPanelOrders[panelKey] = sanitizeIdListStore(sourceOrder).filter(id => {
- const item = STATE.items.get(String(id));
- return item && (sessionItemIds.has(String(id)) || !Pipeline.isTransientItem(item));
- });
- });
- STATE.panelOrders = restoredPanelOrders;
- STATE.currentOrder = getPanelOrder(getCurrentPanelKey());
- STATE.panelOrdersTmp = undefined;
- STATE.currentPanelSessionTmp = undefined;
- if (STATE.activeTextSessionTmp) {
- const activeItem = STATE.items.get(String(STATE.activeTextSessionTmp));
- if (activeItem?.type === 'text') {
- setActiveTextSessionState(activeItem.id, await loadTextSessionMessages(activeItem), { persist: false });
- summarizeTextSessionItem(activeItem, STATE.textChatMessages, { updatedAt: activeItem.timestamp || Date.now(), model: activeItem.textModel });
- DBQueue.add(activeItem);
- if (activeItem.textModel && STATE.action === 'gen_text') applyTextModelSelection(activeItem.textModel);
- } else {
- setActiveTextSessionState(null, [], { persist: false });
- persistActiveTextSessionId();
- }
- }
- STATE.activeTextSessionTmp = undefined;
- if (STATE.action === 'gen_text') restoreChatFromSession();
- PanelManager.update(STATE.historyOrder, STATE.items, 'history');
- await enforceHistoryLimit();
- recoverPendingVideos();
- const zombieIds = STATE.items.values()
- .filter(item => item.isPending && (!item.reqId || item.type !== 'video'))
- .map(item => String(item.id))
- .toArray();
- if (zombieIds.length > 0) {
- removeCurrentPanelItems(zombieIds, { purgeStorage: true });
- }
- runBg(cleanupOrphanMediaFiles);
- }
- function renderRefList() {
- syncVisibleRefStateFromView();
- PanelManager.update(STATE.refOrder, STATE.items, 'reference');
- syncRefPromptEditingUI();
- updateStatusUI();
- }
- function syncRefPromptEditingUI({ restoreEditor = false } = {}) {
- const activeId = STATE.activeRefPromptId != null ? String(STATE.activeRefPromptId) : null;
- const isValidActiveRef = !!activeId && STATE.refOrder.includes(activeId) && STATE.items.has(activeId);
- CardRegistry.maps.reference.forEach((card, cardId) => {
- if (isValidActiveRef && String(cardId) === activeId) card._internals.states.add('editing'); else card._internals.states.delete('editing');
- });
- if (!isValidActiveRef) {
- 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');
- if (restoreEditor) setEditorPromptValue(getDraftActionPrompt());
- return false;
- }
- if (DOM.promptContainer) DOM.promptContainer.classList.add('editing-ref');
- if (DOM.refEditLabel) {
- DOM.refEditLabel.innerText = `Editing Individual Prompt: IMAGE_${STATE.refOrder.indexOf(activeId)}`;
- DOM.refEditLabel.classList.add('show');
- }
- DOM.generateBtn?.classList.add('d-none');
- DOM.saveRefBtn?.classList.remove('d-none');
- return true;
- }
- function toggleRefPrompt(id) {
- const refId = String(id);
- if (STATE.activeRefPromptId === refId) return saveAndCloseRefPrompt(true);
- if (STATE.activeRefPromptId) saveAndCloseRefPrompt(true);
- else if (DOM.prompt) setActionPromptDraft(STATE.action, getEditorPromptValue());
- STATE.activeRefPromptId = refId;
- STATE.activeRefPromptAction = STATE.action;
- STATE.activeRefPromptDraft = STATE.refPrompts[refId]?.[STATE.action] ?? getDraftActionPrompt(STATE.action);
- syncRefPromptEditingUI();
- 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, STATE.activeRefPromptAction, val);
- else setRefPrompt(id, STATE.activeRefPromptAction, null);
- }
- const hasAnyPrompt = !!STATE.refPrompts[id]?.[STATE.activeRefPromptAction];
- const card = CardRegistry.get('reference', id); if (card) card.setReferencePromptFlag(hasAnyPrompt);
- STATE.activeRefPromptId = null;
- STATE.activeRefPromptAction = null;
- STATE.activeRefPromptDraft = '';
- syncRefPromptEditingUI({ restoreEditor: true });
- }
- const TRANSPARENT_1X1 = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
- 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 = '', poster = '', fallbackBg = 'transparent') => {
- const video = document.createElement('video');
- video.className = 'lightbox-media lightbox-media--video';
- video.controls = true;
- video.loop = true;
- video.playsInline = true;
- video.preload = 'metadata';
- video.volume = getStoredMediaVolume();
- video.addEventListener('volumechange', () => { persistMediaVolume(video.volume); });
- video.style.background = fallbackBg;
- if (item?.ratio) video.style.aspectRatio = String(item.ratio);
- if (poster) video.poster = poster;
- if (src) video.src = src;
- return video;
- };
- const clearLightboxStage = () => {
- DOM.lightboxInner?.classList.remove('is-loaded');
- DOM.lightboxStage?.querySelectorAll('.lightbox-media, .lightbox-audio-card').forEach(node => {
- const imgEl = node.tagName === 'IMG' ? node : node.querySelector('img');
- if (imgEl?.src?.startsWith('blob:')) URL.revokeObjectURL(imgEl.src);
- const videoEl = node.tagName === 'VIDEO' ? node : node.querySelector('video');
- resetVideoElement(videoEl);
- node.remove();
- });
- if (DOM.lightboxStage && DOM.lightboxDl && !DOM.lightboxStage.contains(DOM.lightboxDl)) {
- DOM.lightboxStage.appendChild(DOM.lightboxDl);
- }
- if (DOM.lightboxStage) DOM.lightboxStage.style.removeProperty('--lightbox-ratio');
- };
- async function openLightbox(item, sourceCard) {
- document.querySelectorAll('xai-card').forEach(card => {
- card._isHovering = false;
- if (typeof card.forcePause === 'function') card.forcePause();
- });
- const inner = DOM.lightboxInner;
- clearLightboxStage();
- if (DOM.lightbox._activeUrl) {
- URL.revokeObjectURL(DOM.lightbox._activeUrl);
- DOM.lightbox._activeUrl = null;
- }
- const openToken = (DOM.lightbox._openToken = (DOM.lightbox._openToken || 0) + 1);
- const fetchId = String(item.refSourceId || item.id);
- const fallbackBg = (item.lqip && item.lqip.includes('gradient')) ? item.lqip : 'transparent';
- 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', TRANSPARENT_1X1, fallbackBg);
- DOM.lightboxStage.insertBefore(previewEl, DOM.lightboxDl);
- const fullEl = createLightboxImageLayer(item, 'lightbox-media lightbox-media--full', TRANSPARENT_1X1, 'transparent');
- fullEl.style.opacity = '0';
- fullEl.style.position = 'absolute';
- fullEl.style.inset = '0';
- DOM.lightboxStage.insertBefore(fullEl, DOM.lightboxDl);
- MediaCache.getFile(fetchId, 'full').then(file => {
- if (isStale() || !file) return;
- if (DOM.lightbox._activeUrl) {
- URL.revokeObjectURL(DOM.lightbox._activeUrl);
- }
- const tempUrl = URL.createObjectURL(file);
- DOM.lightbox._activeUrl = tempUrl;
- fullEl.onload = () => {
- if (isStale()) return;
- fullEl.style.opacity = '1';
- inner.classList.add('is-loaded');
- setTimeout(() => { if (previewEl.parentNode) previewEl.remove(); }, 150);
- };
- fullEl.src = tempUrl;
- });
- } else if (item.type === 'audio') {
- const audioWrapper = document.createElement('div');
- audioWrapper.className = 'lightbox-audio-card lightbox-media';
- const promptArea = document.createElement('div');
- promptArea.className = 'lightbox-audio-text';
- promptArea.textContent = item.shortPrompt || item.prompt || 'Audio Generation';
- LazyTextStore.get(item.id).then(heavy => { if (heavy?.prompt && !isStale()) promptArea.textContent = heavy.prompt || item.shortPrompt || 'Audio Generation'; });
- const mediaEl = createLightboxVideoElement(item, '', '', fallbackBg);
- mediaEl.className = 'lightbox-audio-player lightbox-media--video';
- mediaEl.setAttribute('controlslist', 'nodownload noplaybackrate');
- mediaEl.style.opacity = '1';
- mediaEl.style.aspectRatio = 'auto';
- audioWrapper.appendChild(DOM.lightboxDl);
- audioWrapper.appendChild(promptArea);
- audioWrapper.appendChild(mediaEl);
- DOM.lightboxStage.appendChild(audioWrapper);
- requestAnimationFrame(() => audioWrapper.classList.add('is-visible'));
- } else {
- const mediaEl = createLightboxVideoElement(item, '', '', fallbackBg);
- DOM.lightboxStage.insertBefore(mediaEl, DOM.lightboxDl);
- requestAnimationFrame(() => mediaEl.classList.add('is-visible'));
- }
- if (!DOM.lightbox.open) DOM.lightbox.showModal();
- if (item.type !== 'image') {
- MediaCache.getFile(fetchId, 'full').then(file => {
- if (isStale()) return;
- const videoEl = DOM.lightboxStage.querySelector('.lightbox-media--video');
- if (!videoEl) return;
- if (file) {
- if (DOM.lightbox._activeUrl) {
- URL.revokeObjectURL(DOM.lightbox._activeUrl);
- }
- const tempUrl = URL.createObjectURL(file);
- DOM.lightbox._activeUrl = tempUrl;
- DOM.lightboxDl.href = tempUrl;
- videoEl.src = tempUrl;
- } else if (item.originalUrl) {
- DOM.lightboxDl.href = item.originalUrl;
- videoEl.src = item.originalUrl;
- } else {
- return;
- }
- inner.classList.add('is-loaded');
- videoEl.play().catch(()=>{});
- }).catch(err => addLog('WARN', 'Failed to load media file for lightbox', err));
- }
- }
- const closeLightbox = () => {
- DOM.lightbox._openToken = (DOM.lightbox._openToken || 0) + 1;
- DOM.lightbox.close();
- clearLightboxStage();
- if (DOM.lightbox._activeUrl) {
- URL.revokeObjectURL(DOM.lightbox._activeUrl);
- DOM.lightbox._activeUrl = null;
- }
- STATE.activeLightboxId = null;
- }
- function initMediaInteractions() {
- const getEventElement = (event) => event.composedPath?.().find(node => node?.nodeType === Node.ELEMENT_NODE) || event.target;
- const getEventCard = (event, targetEl = getEventElement(event)) => targetEl?.closest?.('xai-card') || targetEl?.getRootNode?.().host || null;
- const handleRefDelete = (id) => {
- const existsInAnyPanel = PANEL_ACTION_KEYS.some(panelKey => getPanelOrder(panelKey).includes(String(id)));
- if (STATE.historyOrder.includes(id) || existsInAnyPanel) {
- pruneReferenceState([id]);
- } else void handleGlobalDelete(id, true);
- };
- const handleGlobalDelete = async (id, isRefDel = false) => {
- const key = String(id);
- const isText = STATE.items.get(key)?.type === 'text';
- if (isText && isRuntimeBusy('text')) {
- UI.showToast('Wait until the response finishes', 'warn');
- return;
- }
- if (!isRefDel) {
- const { toDelete, toVirtualize } = processDeletions([key]);
- if (toVirtualize.length > 0) {
- virtualizeRefs(toVirtualize);
- return;
- }
- }
- DBQueue.forgetItem(key);
- STATE.historyOrder = STATE.historyOrder.filter(i => i !== key);
- removeIdsFromAllPanels([key]);
- document.querySelectorAll(`xai-card[data-id="${key}"]`).forEach(node => {
- if (node.closest('#xai-history') && historyGrid) { node.unbind(); } else { node.remove(); }
- });
- if (historyGrid) historyGrid.updateData(STATE.historyOrder, STATE.items);
- pruneReferenceState([key]);
- Store.removeItem(key);
- await StorageClient.deleteItemsBulk([key]).catch(e => addLog('WARN', 'Delete item failed', { id: key, error: e?.message || e }));
- if (isText) await clearTextSessionArtifacts([key]);
- if (isText && STATE.action === 'gen_text') restoreChatFromSession();
- if (!isRefDel) scheduleHistoryMetricsUpdate();
- scheduleDiskUpdate();
- };
- const handleGlobalClick = async (e) => {
- const targetEl = getEventElement(e);
- const card = getEventCard(e, targetEl);
- if (!card) return;
- const id = String(card.dataset.id), item = STATE.items.get(id);
- if (!item) return;
- const intentEl = targetEl.closest('[data-intent]');
- const intent = intentEl?.dataset?.intent;
- if (intent) {
- if (intentEl.tagName !== 'INPUT' || intentEl.type !== 'checkbox') e.preventDefault();
- e.stopPropagation();
- switch (intent) {
- case 'toggle-ref-active': {
- const entry = getMutableRefViewEntry();
- if (intentEl.checked) {
- activateReferenceItem(item);
- } else {
- delete entry.active[id];
- delete entry.waitUntil[id];
- item.active = false;
- item.readyAt = 0;
- abortTasksForRef(item.id, STATE.action);
- }
- if (!intentEl.checked) card._internals.states.add('inactive'); else card._internals.states.delete('inactive');
- persistRefViews();
- updateUploadPlaceholderState();
- if (isRuntimeBusy('media') && JobManager.getLane(STATE.action) === 'media') wakeUpQueue('media');
- return;
- }
- case 'ref-prompt': return toggleRefPrompt(id);
- case 'delete-ref': return handleRefDelete(id);
- case 'download': return MediaUtils.downloadItem(item, false, card);
- case 'insert-tag': {
- const idx = card._insertTagIdx ?? targetEl.textContent.match(/IMAGE_(\d+)/)?.[1] ?? '0';
- return insertTextAtCursor(DOM.prompt, ` <IMAGE_${idx}> `);
- }
- case 'delete-global': {
- if (card._context === 'current' && !item.isPending) {
- removeIdsFromAllPanels([id]);
- return;
- }
- if (item.isPending && item.taskId) {
- JobManager.findTask(item.taskId)?.controller.abort();
- }
- return void handleGlobalDelete(id);
- }
- case 'action-edit':
- case 'action-extend':
- case 'action-video': {
- const modeMap = { 'action-extend': 'extend', 'action-video': 'video', 'action-edit': 'edit' };
- const reqMode = modeMap[intent];
- const newAction = (ACTION_SPEC[item.type === 'video' ? 'edit_video' : 'gen_image'] ?? ACTION_SPEC.gen_image).cardAction?.({ itemType: item.type, mode: reqMode }) ?? getDefaultCardAction({ itemType: item.type, mode: reqMode });
- if (newAction !== STATE.action) {
- changeActionState(newAction);
- if (STATE.action !== newAction) return;
- }
- moveReferenceToFront(id, { action: newAction, exclusiveActive: true, forceRebuild: true });
- return;
- }
- }
- }
- if (targetEl.closest('.card-prompt') && item.type !== 'text') {
- LazyTextStore.get(id).then(heavy => { if (heavy?.prompt) replaceEditorPromptValue(heavy.prompt); });
- return;
- }
- if (!item.isPending && !targetEl.closest('.ref-header') && !targetEl.closest('.ref-side-prompt-btn')) {
- if (item.type === 'text') void loadTextFromHistory(item);
- else openLightbox(item, card);
- }
- };
- App.on(document.body, 'click', handleGlobalClick);
- 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) {
- const nextAction = sanitizePanelKey(actionValue, STATE.action);
- if (STATE.action === nextAction) return;
- const currentLane = JobManager.getLane(STATE.action);
- const nextLane = JobManager.getLane(nextAction);
- const nextRuntime = JobManager.getRuntime(nextLane);
- if (nextRuntime.isGenerating && nextRuntime.activeAction && nextRuntime.activeAction !== nextAction) {
- UI.showToast(`Active ${nextRuntime.label.toLowerCase()} work is locked to ${ACTION_SPEC[nextRuntime.activeAction]?.label || nextRuntime.activeAction}`, 'warn');
- return;
- }
- if (currentLane === nextLane && isRuntimeBusy(currentLane)) {
- UI.showToast('Stop current generation before switching within the same server group', 'warn');
- return;
- }
- if (STATE.activeRefPromptId) saveAndCloseRefPrompt(true);
- else if (DOM.prompt) PromptSync.captureEditor(STATE.action, { scheduleCommit: false });
- flushCurrentPanelSessionSave();
- STATE.action = nextAction; syncActionLabel(nextAction);
- activatePanelState(nextAction);
- setEditorPromptValue(getDraftActionPrompt());
- updateDirtyStateUI();
- }
- function bindLogic() {
- Object.entries(DOM_IDS).forEach(([k, id]) => DOM[k] = document.getElementById(id));
- if (DOM.settingsPopover) DOM.settingsPopover.appendChild(buildSettingsPopoverContent());
- PanelManager.init();
- 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 PRIMARY_DROPDOWN_IDS = ['action', 'textModel', 'batchSize', 'voice', 'language', 'ttsQuality', 'duration'];
- const IMPORT_SYNC_DROPDOWN_IDS = ['batchSize', 'voice', 'language', 'ttsQuality'];
- const getControlOptions = (control) => typeof control?.options === 'function' ? control.options() : (control?.options || []);
- const setDropdownValue = (control, value, text = null) => {
- const btn = DOM[control.domKey];
- const popover = document.getElementById(control.popoverId);
- if (btn) btn.dataset.value = String(value);
- if (control.labelId && text !== null) {
- const label = document.getElementById(control.labelId);
- if (label) label.textContent = text;
- }
- setActivePopoverItem(popover, value);
- };
- const applyDropdownValue = (id, value) => {
- if (id === 'action') return changeActionState(value);
- if (id === 'currentAr') {
- STATE.currentAr = getAspectRatioOption(value).label.trim();
- syncDOMControls('currentAr');
- return;
- }
- if (id === 'textModel') {
- applyTextModelSelection(value);
- return;
- }
- if (id === 'duration') {
- STATE.duration = value;
- return;
- }
- STATE[UI_CONTROL_BY_ID[id].stateKey] = value;
- };
- const syncImportedControlState = () => {
- syncDOMControls(IMPORT_SYNC_DROPDOWN_IDS.map(id => UI_CONTROL_BY_ID[id].stateKey));
- syncDOMControls(UI_TOGGLE_CONTROLS.map(c => c.stateKey));
- syncDOMControls(SETTINGS_TOGGLE_META.map(c => c.stateKey));
- syncDOMControls(SETTINGS_NUMBER_META.map(c => c.stateKey));
- syncDOMControls('currentAr');
- if (DOM.settingLoops) DOM.settingLoops.value = getLoopsValue(STATE.action);
- syncLayoutWidths();
- };
- UI_DROPDOWN_CONTROLS.forEach(control => {
- const btn = DOM[control.domKey], popover = document.getElementById(control.popoverId);
- if (!btn || !popover) return;
- btn.setAttribute('popovertarget', control.popoverId);
- const renderOpts = (opts = []) => renderPopoverOptions(popover, btn.dataset.value || control.defaultValue, opts);
- renderOpts(getControlOptions(control));
- popover._updateOptions = (opts = []) => renderOpts(opts);
- });
- syncDOMControls(PRIMARY_DROPDOWN_IDS.map(id => UI_CONTROL_BY_ID[id].stateKey));
- syncDOMControls('currentAr');
- const toggleControlValue = (control, el) => {
- if (!control || control.kind !== 'toggle') return;
- const values = control.values || [];
- const currentIndex = Math.max(0, values.indexOf(el.dataset.value));
- const nextValue = values[(currentIndex + 1) % values.length] || values[0];
- el.dataset.value = nextValue;
- el.innerText = control.labels?.[nextValue] ?? nextValue;
- STATE[control.stateKey] = nextValue;
- if (control.id === 'textReasoningEffort' || control.id === 'textWebSearch' || control.id === 'textXSearch') updateActionUI();
- };
- const applyNumericControlValue = (control, el) => {
- if (!control) return;
- const input = el?.querySelector?.('input') || el;
- if (control.id === 'loops') {
- setLoopsValue(STATE.action, input?.value);
- return;
- }
- const nextValue = parseInt(input?.value, 10) || 0;
- const max = control.max;
- STATE[control.stateKey] = max ? Math.min(max, nextValue) : nextValue;
- };
- const textModelPopover = document.getElementById('popover-text-model');
- if (textModelPopover) {
- const inputWrapper = document.createElement('div');
- inputWrapper.className = 'custom-model-wrapper';
- inputWrapper.style.cssText = 'padding: 6px 8px; border-bottom: 1px solid var(--border); margin-bottom: 4px;';
- const input = document.createElement('input');
- input.type = 'text';
- input.placeholder = 'Add custom model... (Enter)';
- input.style.cssText = 'width: 100%; background: rgb(0 0 0 / 30%); border: 1px solid var(--border); color: #fff; border-radius: 4px; padding: 6px; font-size: 13px; outline: none;';
- inputWrapper.appendChild(input);
- inputWrapper.addEventListener('click', e => e.stopPropagation());
- input.addEventListener('keydown', e => {
- if (e.key === 'Enter') {
- e.preventDefault();
- const val = input.value.trim();
- if (val) {
- applyTextModelSelection(val);
- textModelPopover.hidePopover();
- }
- input.value = '';
- }
- });
- const origUpdate = textModelPopover._updateOptions;
- textModelPopover._updateOptions = (opts) => {
- if (origUpdate) origUpdate(opts);
- textModelPopover.prepend(inputWrapper);
- };
- textModelPopover._updateOptions(UI_CONTROL_BY_ID.textModel.options());
- }
- syncDOMControls(UI_TOGGLE_CONTROLS.map(c => c.stateKey));
- const setupTtsEffects = () => {
- const btn = DOM.btnTtsEffects, popover = DOM.popoverTtsEffects;
- if (!btn || !popover) return;
- btn.setAttribute('popovertarget', 'popover-tts-effects');
- const renderEffects = () => {
- popover.innerHTML = '';
- const appendGroup = (label, tags, onClick, getTitle) => {
- const group = document.createElement('div');
- group.className = 'tts-tag-group';
- group.textContent = label;
- popover.appendChild(group);
- tags.forEach(tag => {
- const item = document.createElement('div');
- item.className = 'tts-tag-item';
- item.textContent = tag;
- item.title = getTitle(tag);
- item.addEventListener('click', () => onClick(tag));
- popover.appendChild(item);
- });
- };
- appendGroup('Instant', APP_CONFIG.TTS_TAGS.instant.map(t => `[${t}]`), tag => {
- insertTextAtCursor(DOM.prompt, tag);
- }, tag => `Insert ${tag}`);
- appendGroup('Wrapping', APP_CONFIG.TTS_TAGS.wrapping.map(t => `<${t}>`), tag => {
- const selectedText = DOM.prompt.selectionStart !== DOM.prompt.selectionEnd
- ? DOM.prompt.value.substring(DOM.prompt.selectionStart, DOM.prompt.selectionEnd)
- : '';
- const closing = tag.replace('<', '</');
- const textToInsert = selectedText
- ? `${tag}${selectedText}${closing}`
- : `${tag}${closing}`;
- insertTextAtCursor(DOM.prompt, textToInsert);
- }, tag => `Insert ${tag}...${tag.replace('<', '</')}`);
- };
- renderEffects();
- };
- setupTtsEffects();
- if (DOM.settingLoops) {
- DOM.settingLoops.value = getLoopsValue(STATE.action);
- App.on(DOM.settingLoops, 'click', e => e.stopPropagation());
- }
- syncDOMControls(SETTINGS_TOGGLE_META.map(c => c.stateKey));
- syncDOMControls(SETTINGS_NUMBER_META.map(c => c.stateKey));
- if (DOM.settingLoops) DOM.settingLoops.value = getLoopsValue(STATE.action);
- DOM.settingsBtn.setAttribute('popovertarget', 'popover-settings');
- const importInput = document.getElementById('file-import-state');
- App.on(document, 'click', e => {
- if (e.target.tagName === 'DIALOG' && e.target.open) return e.target.close();
- const actionEl = e.target.closest('[data-action]');
- if (!actionEl) return;
- const action = actionEl.dataset.action;
- if (action === 'select-popover-option') {
- if (actionEl.classList.contains('disabled')) return;
- const popover = actionEl.closest('.xai-custom-popover');
- const control = getControlByStateKey(popover?.dataset.stateKey, 'prompt');
- if (!popover || !control || control.kind !== 'dropdown') return;
- const text = actionEl.querySelector('span')?.textContent || '';
- setDropdownValue(control, actionEl.dataset.val, text);
- popover.hidePopover();
- applyDropdownValue(control.id, actionEl.dataset.val);
- return;
- }
- if (action === 'toggle-ui-control') {
- const control = getControlByStateKey(actionEl.dataset.stateKey, 'prompt');
- toggleControlValue(control, actionEl);
- return;
- }
- if (action === 'toggle-setting') {
- const control = getControlByStateKey(actionEl.dataset.stateKey, 'settings');
- if (!control || control.kind !== 'toggle') return;
- STATE[control.stateKey] = !STATE[control.stateKey];
- const checkbox = actionEl.querySelector('input[type="checkbox"]');
- if (checkbox) checkbox.checked = !!STATE[control.stateKey];
- if (DOM[control.domKey]) DOM[control.domKey].checked = !!STATE[control.stateKey];
- e.stopPropagation();
- return;
- }
- if (action === 'export-state') {
- 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'}));
- clickTransientLink({ href: u, download: `xAI_Pro_Settings_${makeTimestamp()}.json` });
- setTimeout(() => URL.revokeObjectURL(u), 10000);
- return;
- }
- if (action === 'import-state') {
- importInput?.click();
- return;
- }
- if (action === 'toggle-ref-mode') {
- STATE.refMode = STATE.refMode === 'multi' ? 'queue' : 'multi';
- return;
- }
- if (action === 'toggle-left-panel') {
- STATE.leftCollapsed = !STATE.leftCollapsed;
- return;
- }
- if (action === 'toggle-right-panel') {
- STATE.rightCollapsed = !STATE.rightCollapsed;
- return;
- }
- if (action === 'open-favs') {
- e.preventDefault();
- renderFavs();
- DOM.favDialog.showModal();
- return;
- }
- if (action === 'close-lightbox') {
- e.preventDefault();
- closeLightbox();
- return;
- }
- if (action === 'lightbox-dl') {
- e.preventDefault();
- e.stopPropagation();
- if (STATE.activeLightboxId) {
- const item = STATE.items.get(STATE.activeLightboxId);
- if (item) MediaUtils.downloadItem(item, true);
- }
- return;
- }
- if (action === 'chat-action') {
- e.preventDefault();
- e.stopPropagation();
- handleChatAction(actionEl.dataset.chatAction, actionEl.closest('.chat-msg-row'));
- return;
- }
- });
- App.on(document, 'input', e => {
- const actionEl = e.target.closest('[data-action]');
- if (!actionEl) return;
- if (actionEl.dataset.action === 'input-ui-number') {
- const control = getControlByStateKey(actionEl.dataset.stateKey, 'prompt');
- if (control?.kind === 'number-inline') applyNumericControlValue(control, actionEl);
- return;
- }
- if (actionEl.dataset.action === 'input-setting-number') {
- const control = getControlByStateKey(actionEl.dataset.stateKey, 'settings');
- if (control?.kind === 'number') applyNumericControlValue(control, actionEl);
- }
- });
- App.on(importInput, 'change', e => {
- const file = e.target.files[0]; if (!file) return;
- const reader = new FileReader();
- reader.onload = evt => {
- try {
- const p = parseImportedState(JSON.parse(evt.target.result));
- Object.entries(p.config).forEach(([key, value]) => STATE[key] = value);
- STATE.prompts = p.prompts;
- STATE.promptDrafts = p.promptDrafts;
- STATE.refPrompts = p.refPrompts;
- STATE.favPrompts = p.favPrompts;
- persistPromptStateStores();
- syncActionLabel(); _updateActionUIDom();
- syncImportedControlState();
- if (!STATE.activeRefPromptId) setEditorPromptValue(getDraftActionPrompt());
- renderRefMode(); UI.showToast("Settings & Prompts imported successfully!", "success");
- } catch(err) { UI.showToast(err?.message || 'Failed to parse JSON', "error"); }
- }; reader.readAsText(file); e.target.value = '';
- });
- renderRefMode(); updateUploadPlaceholderState(); _updateActionUIDom();
- }
- function initLayoutManager() {
- syncLayoutWidths();
- const saveWidthsOnIdle = () => {
- if (DOM.colLeft && !STATE.leftCollapsed) STATE.leftWidth = Math.max(260, Math.round(getComputedStyle(DOM.colLeft).width) || STATE.leftWidth);
- if (DOM.colRight && !STATE.rightCollapsed) STATE.rightWidth = Math.max(240, Math.round(getComputedStyle(DOM.colRight).width) || STATE.rightWidth);
- };
- App.on(DOM.colLeft, 'mouseup', saveWidthsOnIdle);
- App.on(DOM.colRight, 'mouseup', saveWidthsOnIdle);
- }
- 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.moveBefore(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;
- getMutableRefViewEntry().order = [...DOM.refList.querySelectorAll('xai-card:state(reference)')].map(c => String(c.dataset.id));
- updateRefState();
- };
- App.on(DOM.refList, 'dragstart', e => {
- const card = e.target.closest('xai-card:state(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:state(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 mediaDragKey = 'application/x-xai-media-id';
- const handleMediaDragStart = (e) => {
- const card = e.target.closest('xai-card');
- if (!card || (!card._internals.states.has('history') && !card._internals.states.has('current'))) return;
- document.body.classList.add('is-dragging');
- e.dataTransfer.effectAllowed = 'copy';
- e.dataTransfer.setData(mediaDragKey, 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 mimeType = inferMediaMimeType(file), isVid = mimeType.startsWith('video/'), isImg = mimeType.startsWith('image/'); if (!isVid && !isImg) return;
- const nextAction = (ACTION_SPEC[STATE.action] ?? ACTION_SPEC.gen_image).uploadTransition?.({ mimeType, currentAction: STATE.action }) ?? STATE.action;
- if (isVid && !/mp4/i.test(mimeType)) {
- UI.showToast('Only MP4 video refs are supported for video edit/extend', 'warn');
- return;
- }
- if (nextAction !== STATE.action) {
- changeActionState(nextAction);
- if (STATE.action !== nextAction) return;
- }
- const maxRefs = (ACTION_SPEC[STATE.action] ?? ACTION_SPEC.gen_image).maxRefs?.({ action: STATE.action, refMode: normalizeRefModeForAction(STATE.action, STATE.refMode) }) ?? 0;
- if (maxRefs === 0) {
- UI.showToast('This mode does not use references', 'warn');
- return;
- }
- if (getRefOrderForView().length >= maxRefs) return;
- const mediaData = await MediaUtils.processSource(file, isVid ? 'video' : 'image', APP_CONFIG.MEDIA.THUMB_MAX_DIM, APP_CONFIG.MEDIA.THUMB_WEBP_QUALITY), 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, filename: file.name, ratio: mediaData.ratio, lqip: mediaData.lqip, isUploadedRef: true });
- appendReferenceToEnd(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, { allowFiles = true, allowInternalMedia = true } = {}) => !!dataTransfer && ((allowFiles && hasDataTransferType(dataTransfer, 'Files')) || (allowInternalMedia && hasDataTransferType(dataTransfer, mediaDragKey)));
- const bindDropSurface = (el, { onHover, canHandle = () => true, onDrop }) => {
- if (!el) return;
- App.on(el, 'dragenter', e => {
- if (!canHandle(e.dataTransfer)) return;
- e.preventDefault();
- e.stopPropagation();
- onHover?.(true);
- });
- App.on(el, 'dragover', e => {
- if (!canHandle(e.dataTransfer)) return;
- e.preventDefault();
- e.stopPropagation();
- e.dataTransfer.dropEffect = 'copy';
- });
- App.on(el, 'dragleave', e => {
- if (!canHandle(e.dataTransfer)) return;
- e.preventDefault();
- e.stopPropagation();
- onHover?.(false);
- });
- App.on(el, 'drop', async e => {
- if (!canHandle(e.dataTransfer)) return;
- e.preventDefault();
- e.stopPropagation();
- onHover?.(false);
- await onDrop?.(e.dataTransfer);
- });
- };
- const handleReferenceDrop = async (dataTransfer) => {
- const mediaId = dataTransfer.getData(mediaDragKey);
- if (mediaId && STATE.items.has(mediaId)) {
- const item = STATE.items.get(mediaId);
- const mimeType = item?.mimeType || `${item?.type || 'image'}/`;
- const nextAction = (ACTION_SPEC[STATE.action] ?? ACTION_SPEC.gen_image).uploadTransition?.({ mimeType, currentAction: STATE.action }) ?? STATE.action;
- if (item?.type === 'video' && !isMp4ReferenceItem(item)) {
- UI.showToast('Only MP4 video refs are supported for video edit/extend', 'warn');
- return;
- }
- if (nextAction !== STATE.action) {
- changeActionState(nextAction);
- if (STATE.action !== nextAction) return;
- }
- const maxRefs = (ACTION_SPEC[nextAction] ?? ACTION_SPEC.gen_image).maxRefs?.({ action: nextAction, refMode: normalizeRefModeForAction(nextAction, STATE.refMode) }) ?? 0;
- if (maxRefs === 0) {
- UI.showToast('This mode does not use references', 'warn');
- return;
- }
- moveReferenceToFront(mediaId, { action: nextAction, enforceLimit: true, forceRebuild: true });
- return;
- }
- Array.from(dataTransfer.files || []).forEach(queueMediaFileLocal);
- };
- const handlePromptDrop = async (dataTransfer) => {
- const mediaId = dataTransfer.getData(mediaDragKey);
- if (mediaId && STATE.items.has(mediaId)) {
- const heavy = await LazyTextStore.get(mediaId);
- if (heavy && heavy.prompt) {
- if (replaceEditorPromptValue(heavy.prompt)) return;
- }
- UI.showToast('This media has no saved prompt to insert.', 'warn');
- return;
- }
- const files = Array.from(dataTransfer.files || []);
- if (files.length === 1) {
- const payload = await extractImagePromptViaWorker(files[0]);
- if (payload?.isPromptCarrier) {
- if (replaceEditorPromptValue(payload.prompt)) return;
- UI.showToast('This image has no embedded prompt.', 'warn');
- return;
- }
- }
- files.forEach(queueMediaFileLocal);
- };
- App.on(DOM.uploadPlaceholder, 'click', () => { if (getRefOrderForView().length < APP_CONFIG.LIMITS.MAX_REFS_UI) DOM.fileInput.click(); });
- App.on(DOM.fileInput, 'change', e => { Array.from(e.target.files).forEach(queueMediaFileLocal); DOM.fileInput.value = ''; });
- bindDropSurface(DOM.dropzone, { onHover: hover => DOM.uploadPlaceholder.classList.toggle('drag-over', hover), canHandle: dataTransfer => supportsDropPayload(dataTransfer), onDrop: handleReferenceDrop });
- bindDropSurface(DOM.prompt, { onHover: hover => DOM.promptContainer.classList.toggle('drag-over', hover), canHandle: dataTransfer => supportsDropPayload(dataTransfer), onDrop: handlePromptDrop });
- const handleFavDrop = async (dataTransfer) => {
- const files = Array.from(dataTransfer.files ||[]);
- for (const file of files) {
- if (isImagePromptSupported(file)) {
- const payload = await extractImagePromptViaWorker(file);
- if (payload?.isPromptCarrier && payload.prompt) {
- if (addActionFavorite(payload.prompt)) {
- renderFavs();
- UI.showToast('Prompt added to favorites from image!', 'success');
- } else {
- UI.showToast('Prompt is already in favorites.', 'info');
- }
- } else {
- UI.showToast('No embedded prompt found in this image.', 'warn');
- }
- }
- }
- };
- bindDropSurface(DOM.favBtn, {
- onHover: hover => DOM.favBtn.style.transform = hover ? 'scale(1.1)' : '',
- canHandle: dt => supportsDropPayload(dt, { allowInternalMedia: false }),
- onDrop: handleFavDrop
- });
- bindDropSurface(DOM.favDialog, {
- onHover: hover => DOM.favDialog.style.boxShadow = hover ? 'inset 0 0 0 2px var(--success)' : '',
- canHandle: dt => supportsDropPayload(dt, { allowInternalMedia: false }),
- onDrop: handleFavDrop
- });
- 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 = DOM.prompt.value;
- CardRegistry.get('reference', STATE.activeRefPromptId)?.setReferencePromptFlag(hasCustomRefPromptDraft(STATE.activeRefPromptDraft));
- return;
- }
- PromptSync.captureEditor(STATE.action, { scheduleCommit: false });
- updateDirtyStateUI();
- });
- 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 (STATE.action !== 'gen_text') {
- const editorVal = sanitizePromptText(getEditorPromptValue());
- const committedVal = sanitizePromptText(getStoredActionPrompt(STATE.action));
- if (editorVal !== committedVal) {
- PromptSync.commitNow(STATE.action);
- DOM.promptUpdateLabel.innerText = 'Saved to queue';
- DOM.promptUpdateLabel.classList.add('saved');
- DOM.generateBtn.classList.remove('is-dirty');
- DOM.generateBtn.classList.remove('flash-saved');
- void DOM.generateBtn.offsetWidth;
- DOM.generateBtn.classList.add('flash-saved');
- setTimeout(() => {
- DOM.promptUpdateLabel.classList.remove('show');
- DOM.promptUpdateLabel.classList.remove('saved');
- DOM.generateBtn.classList.remove('flash-saved');
- }, 1500);
- }
- }
- if (!DOM.generateBtn.disabled && !STATE.isGenerating) DOM.generateBtn.click();
- }
- });
- }
- const renderFavs = () => {
- const favs = getActionFavorites();
- const pv = Pipeline.getOriginalPrompt(DOM.prompt.value);
- const 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.favDialog, 'click', e => {
- 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(() => insertTextAtCursor(DOM.prompt, text, true)); } }
- else if (e.target.closest('.fav-add-btn')) {
- addActionFavorite(text);
- renderFavs();
- }
- else if (e.target.closest('.fav-del-btn')) {
- removeActionFavorite(text);
- renderFavs();
- }
- });
- App.on(DOM.lightbox, 'cancel', e => { e.preventDefault(); closeLightbox(); });
- }
- async function loadTextFromHistory(item) {
- if (isRuntimeBusy('text')) {
- UI.showToast('Wait until the response finishes', 'warn');
- return;
- }
- const messages = await loadTextSessionMessages(item);
- summarizeTextSessionItem(item, messages, { updatedAt: item.timestamp || Date.now(), model: item.textModel });
- DBQueue.add(item);
- setActiveTextSessionState(item.id, messages, { persist: true });
- changeActionState('gen_text');
- if (item.textModel) applyTextModelSelection(item.textModel);
- restoreChatFromSession();
- }
- function parseDynamicPrompt(text) {
- if (typeof text !== "string" || !text) return text || "";
- let out = text.replace(/^\s*#[^\n]*/gm, "");
- const vars = new Map();
- const expandVars = (input, depth, stack = new Set()) => {
- if (depth <= 0 || !input.includes("${")) return input;
- let res = "", i = 0;
- while (i < input.length) {
- if (input[i] === "$" && input[i + 1] === "{") {
- let j = i + 2, braces = 0;
- while (j < input.length) {
- if (input[j] === "{") braces++;
- else if (input[j] === "}") {
- if (!braces) break;
- braces--;
- }
- j++;
- }
- if (j >= input.length) { res += input.slice(i); break; }
- const body = input.slice(i + 2, j);
- const eq = body.indexOf("=");
- if (eq > 0) {
- const name = body.slice(0, eq).trim();
- let expr = body.slice(eq + 1);
- const imm = expr.startsWith("!");
- if (imm) expr = expr.slice(1);
- if (name) vars.set(name, { expr, imm, val: imm ? resolve(expr, depth - 1, new Set(stack)) : "" });
- } else {
- const col = body.indexOf(":");
- const hasDefault = col > -1;
- const name = (hasDefault ? body.slice(0, col) : body).trim();
- const fb = hasDefault ? body.slice(col + 1) : "";
- const ref = vars.get(name);
- if (ref) {
- if (ref.imm) res += ref.val;
- else if (!stack.has(name)) res += resolve(ref.expr, depth - 1, new Set(stack).add(name));
- } else if (hasDefault) {
- res += resolve(fb, depth - 1, new Set(stack));
- } else {
- res += `\${${body}}`;
- }
- }
- i = j + 1;
- } else {
- res += input[i++];
- }
- }
- return res;
- };
- const expandBlocks = (input) => input.replace(/\{([^{}]+)\}/g, (_, inner) => {
- const p = inner.split("$$"), hasCfg = p.length > 1;
- const rRaw = hasCfg ? (p[0] || "1") : "1";
- const sep = p.length > 2 ? p[1] : ", ";
- const oRaw = hasCfg ? (p.length > 2 ? p.slice(2).join("$$") : p[1]) : inner;
- const pool = oRaw.split("|").flatMap(opt => {
- const i = opt.indexOf("::"), w = i < 0 ? 1 : +opt.slice(0, i), v = (i < 0 ? opt : opt.slice(i + 2)).trim();
- return Number.isFinite(w) && w > 0 && v ? [{ w, v }] : [];
- });
- if (!pool.length) return "";
- const [a, b] = rRaw.split("-");
- const min = Math.max(0, +a || 1);
- const max = Math.max(min, b === undefined ? min : +b || pool.length);
- let c = Math.min(pool.length, (Math.random() * (max - min + 1) + min) | 0);
- if (c <= 0) return "";
- const picked = [];
- let total = pool.reduce((sum, o) => sum + o.w, 0);
- while (c--) {
- let r = Math.random() * total, i = pool.findIndex(o => (r -= o.w) <= 0);
- if (i < 0) i = pool.length - 1;
- const [hit] = pool.splice(i, 1);
- picked.push(hit.v);
- total -= hit.w;
- }
- return picked.join(sep);
- });
- const resolve = (input, depth, stack = new Set()) => {
- let current = input;
- for (let i = 0; i < depth && (current.includes("${") || /\{[^{}]+\}/.test(current)); i++) {
- const prev = current;
- current = expandBlocks(expandVars(current, depth - i, stack));
- if (current === prev) break;
- }
- return current;
- };
- return resolve(out, 50).replace(/[ \t]{2,}/g, " ").replace(/\n\s*\n/g, "\n").trim();
- }
- function resolveAspectRatio(currentRefs = null, settings = STATE) {
- let ar = settings.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(singleRefUrl) {
- return[
- { url: singleRefUrl },
- { url: singleRefUrl }
- ];
- }
- function buildMediaPayload(type, payload, currentRefs, refBase64s, ar, settings = STATE) {
- const isImage = type === 'image';
- payload.resolution = isImage ? settings.resImg : settings.resVid;
- if (isImage) payload.response_format = 'b64_json';
- if (!isImage) payload.duration = parseInt(settings.duration);
- const canUseAr = ar !== 'auto' && (isImage || ALLOWED_VIDEO_ARS.has(ar.toUpperCase?.() ? ar.toUpperCase() : ar));
- if (canUseAr) payload.aspect_ratio = ar;
- if (!currentRefs?.length || !refBase64s[0]) return;
- if (!isImage && payload.duration > 10) {
- payload.image = { url: refBase64s[0] };
- return;
- }
- if (currentRefs.length === 1) {
- if (isImage && settings.referenceARTrick && ar !== 'auto') {
- payload.images = applySingleRefTrick(refBase64s[0]);
- } else {
- payload.image = { url: refBase64s[0] };
- }
- return;
- }
- const limit = isImage ? APP_CONFIG.LIMITS.API_MAX_MULTI_IMG : APP_CONFIG.LIMITS.API_MAX_MULTI_VID;
- const limitedBase64s = refBase64s.filter(Boolean).slice(0, limit);
- payload[isImage ? 'images' : 'reference_images'] = limitedBase64s.map(url => ({ url }));
- }
- function buildVideoEditPayload(actionType, payload, currentRefs, refBase64s, settings = STATE) {
- if (actionType === 'extend_video') payload.duration = parseInt(settings.duration);
- if (currentRefs?.length && refBase64s[0]) payload.video = { url: refBase64s[0] };
- }
- function buildAudioPayload(payload, settings = STATE) {
- const text = payload.prompt;
- delete payload.model;
- delete payload.prompt;
- payload.text = text;
- payload.voice_id = settings.voice || 'eve';
- payload.language = settings.language || 'auto';
- const quality = TTS_QUALITY_MAP.get(settings.ttsQuality) || APP_CONFIG.TTS_QUALITIES[1];
- payload.output_format = {
- codec: 'mp3',
- sample_rate: 48000,
- bit_rate: quality.bitRate
- };
- }
- function buildResponsesTextContent(text) {
- return [{ type: 'input_text', text: String(text ?? '') }];
- }
- function buildResponsesUserContent(text, imageUrls = []) {
- const content = buildResponsesTextContent(text);
- imageUrls.filter(Boolean).slice(0, APP_CONFIG.LIMITS.API_MAX_MULTI_TEXT).forEach(url => content.push({ type: 'input_image', image_url: url }));
- return content;
- }
- function extractResponsesOutputText(responseData) {
- if (typeof responseData?.output_text === 'string' && responseData.output_text) return responseData.output_text;
- const parts = [];
- const output = Array.isArray(responseData?.output) ? responseData.output : [];
- output.forEach(item => {
- if (!item || item.type !== 'message' || item.role !== 'assistant' || !Array.isArray(item.content)) return;
- item.content.forEach(contentItem => {
- if (contentItem?.type === 'output_text' && typeof contentItem.text === 'string' && contentItem.text) {
- parts.push(contentItem.text);
- }
- });
- });
- return parts.join('');
- }
- function getLastResponsesUserText(payload) {
- const input = Array.isArray(payload?.input) ? payload.input : [];
- const message = input.findLast(m => m?.role === 'user');
- if (!message) return '';
- const content = Array.isArray(message.content) ? message.content : buildResponsesTextContent(message.content);
- const textParts = content
- .filter(part => part?.type === 'input_text' && typeof part.text === 'string')
- .map(part => part.text);
- return textParts.length > 0 ? textParts.join('\n').trim() : '';
- }
- async function getApiPayload(actionType = STATE.action, currentRefs = null, refPromptOverride = null, apiPromptOverride = null, settings = buildActionSettingsSnapshot(actionType)) {
- const livePrompt = apiPromptOverride != null
- ? sanitizePromptText(apiPromptOverride)
- : PromptPipeline.process(STATE.activeRefPromptId ? getStoredActionPrompt(actionType) : getTaskPrompt(actionType), '', actionType).api;
- const parsedRefPrompt = refPromptOverride != null ? PromptPipeline.process(refPromptOverride, '', actionType).api : null;
- const endpoint = (ACTION_SPEC[actionType] ?? ACTION_SPEC.gen_image).endpoint?.({ action: actionType, refCount: currentRefs?.length || 0 }) || '';
- const payload = { model: (ACTION_SPEC[actionType] ?? ACTION_SPEC.gen_image).model, prompt: sanitizePromptText(parsedRefPrompt ?? livePrompt) };
- const aspectRatio = resolveAspectRatio(currentRefs, settings);
- const refBase64s = currentRefs ? await Promise.all(currentRefs.map(m => RefPayloadCache.get(m))) : [];
- (ACTION_SPEC[actionType] ?? ACTION_SPEC.gen_image).buildPayload?.({ action: actionType, payload, currentRefs, refBase64s, aspectRatio, settings, textChatMessages: settings.textChatMessages });
- addLog('INFO', 'Built payload', { endpoint, payload });
- return { endpoint, payload };
- }
- async function fetchWithRetries(endpoint, payload, signal, runtime, token, settings, taskId, onProgress, streamHandlers = null, binaryOpfsId = null) {
- let attempts = 0, rateLimitFails = 0, authRetries = 0; const getDelay = () => Math.floor(Math.random() * (settings.retryMaxMs - settings.retryMinMs + 1)) + settings.retryMinMs;
- const expectsBinary = endpoint === '/v1/tts';
- while (attempts < APP_CONFIG.TIMINGS.MAX_RETRY_ATTEMPTS && runtime.generationToken === token) {
- try {
- streamHandlers?.onAttemptStart?.(attempts + 1);
- 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 (classifyHttpStatus(res.status) === 'retry') {
- 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 (classifyHttpStatus(res.status) === 'auth') {
- 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 (res.status === 400 || isInvalidRequestError(errMsg)) {
- addLog('NETWORK_WARN', 'Invalid request payload (400)', { taskId, attempt: attemptNo, status: res.status, elapsedMs, data: errData });
- onProgress({ shortStatus: `Bad Req(${res.status})`, longStatus: `Payload Error (${res.status}): Check text length, voice, or format.`, color: '#f59e0b' });
- throw Object.assign(new Error(errMsg || 'Bad Request (400)'), { status: res.status, data: errData, noRetry: true, invalidRequest: true });
- }
- 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 });
- }
- throw Object.assign(new Error(errMsg), { status: res.status, data: errData });
- }
- if (expectsBinary) {
- const stream = res.body;
- addLog('NETWORK_RES', 'Transferring binary stream to worker', { taskId, attempt: attemptNo, status: res.status, elapsedMs });
- const opfsId = binaryOpfsId || taskId;
- await StorageClient.processStream(stream, { isBinary: true, id: opfsId }, null, signal);
- return { _streamedBinary: true, opfsId };
- }
- if (streamHandlers && payload.stream === true) {
- const stream = res.body;
- addLog('NETWORK_RES', 'Transferring SSE stream to worker', { taskId, attempt: attemptNo, status: res.status, elapsedMs });
- const data = await StorageClient.processStream(stream, { isBinary: false }, (delta, accumulatedText, isDone) => {
- if (isDone) {
- streamHandlers.onStreamDone?.();
- return;
- }
- if (delta) streamHandlers.onTextDelta?.(delta, accumulatedText);
- }, signal);
- addLog('NETWORK_RES', 'Successful streaming response', { taskId, attempt: attemptNo, status: res.status, elapsedMs, data });
- return data;
- }
- 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 errMsg = getErrorMessage(err);
- const errStr = errMsg.toLowerCase();
- const isShortRateLimit = errStr.match(/requests per second \(actual\/limit\):\s*(\d+)\s*\/\s*\1/);
- const isLongRateLimit = classifyHttpStatus(err.status) === 'rateLimit' || /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: errMsg });
- 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(settings.rateLimitDelay) || 60; while (cd > 0 && runtime.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, settings.autoRetryVideo)) {
- attempts++;
- addLog('POLL_WARN', 'Video task stalled or rejected', { taskId, attempt: attempts, message: errMsg });
- 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: ${errMsg.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, runtime, token, onProgress) {
- let pollCount = 0;
- let fatalGraceCount = 0;
- while (token === -1 || runtime.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 && runtime.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 (classifyHttpStatus(res.status) === 'auth') { 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; }
- let isFatal = false;
- let fatalReason = '';
- let data = null;
- if (!res.ok) {
- const errData = await parseResponseJsonSafe(res);
- if (res.status === 404) { isFatal = true; fatalReason = "video lost (404). Session wiped."; }
- else 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 });
- } else {
- data = await res.json();
- if (data?.error) throw Object.assign(new Error(data.error.message || JSON.stringify(data.error)), { data });
- const st = (data?.status || data?.state || 'processing').toLowerCase();
- if (VIDEO_SUCCESS_STATES.has(st)) return data.video.url;
- else if (VIDEO_FLAGGED_STATES.has(st) || data.is_sensitive) {
- if (data?.video?.url) return data.video.url;
- isFatal = true; fatalReason = `Generation halted. Reason: ${st}`;
- }
- }
- if (isFatal) {
- if (fatalGraceCount < 4) {
- fatalGraceCount++;
- pollCount--;
- addLog('POLL_WARN', `Fatal condition met, entering grace retry ${fatalGraceCount}/4`);
- continue;
- }
- throw new Error(fatalReason);
- }
- if (data) {
- const st = (data?.status || data?.state || 'processing').toLowerCase(), pNum = typeof data?.progress === 'number' ? data.progress : null, progStr = pNum !== null ? ` ${pNum}%` : '';
- const isDone = VIDEO_SUCCESS_STATES.has(st);
- 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,
- durationMs: isDone || pNum === 100 ? 400 : 2500,
- easing: isDone || pNum === 100 ? 'ease-out' : 'cubic-bezier(0.2, 1, 0.3, 1)',
- forceUIUpdate: true
- });
- }
- } catch (e) { throw e; }
- }
- }
- function createPlaceholders(taskId, batchCount, actionType, currentRefs, panelKey = actionType, draftPrompt = '') {
- let targetRatio = actionType.includes('video') ? (16 / 9) : (actionType === 'gen_audio' ? 5 : 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: `${(ACTION_SPEC[actionType] ?? ACTION_SPEC.gen_image).title} • ${refItem.filename || 'Reference'}`, color: '#fff', style: CURRENT_PANEL_STATUS_STYLE }]
- : [
- { text: (ACTION_SPEC[actionType] ?? ACTION_SPEC.gen_image).title, color: '#fff', style: CURRENT_PANEL_STATUS_STYLE },
- { text: 'Queued...', color: '#d4d4d8' }
- ];
- for (let i = 0; i < batchCount; i++) {
- const pid = crypto.randomUUID(); placeholderIds.push(pid);
- const promptText = draftPrompt || (actionType === 'gen_text' ? getTaskPrompt('gen_text') : (actionType === 'gen_audio' ? getTaskPrompt('gen_audio') : ''));
- Store.commitNewItem({ id: pid, type: refItem ? refItem.type : (actionType === 'gen_image' ? 'image' : (actionType === 'gen_audio' ? 'audio' : (actionType === 'gen_text' ? 'text' : 'video'))), prompt: promptText, responseText: '', ratio: targetRatio, lqip: bgPreview, isPending: true, persist: false, taskId: taskId, filename: 'Pending...', downloaded: false, refSourceId: refItem?.id, originalUrl: refItem?.originalUrl, _statusData: initialStatus }, { targetArrayName: actionType === 'gen_text' ? null : 'currentOrder', panelKey, persist: false });
- }
- return placeholderIds;
- }
- function finalizePendingVideoItem(item, videoUrl, posterData = null) {
- if (!item) return null;
- item.type = 'video';
- item.mimeType = 'video/mp4';
- item.isPending = false;
- item.originalUrl = videoUrl;
- item.filename = `Grok Video - ${makeTimestamp()}.mp4`;
- item.ratio = posterData?.ratio || item.ratio || (16 / 9);
- item.lqip = posterData?.lqip || item.lqip || null;
- item.refSourceId = null;
- item.reqId = null;
- item.taskId = null;
- item.persist = true;
- item._statusData = [];
- return item;
- }
- async function refreshFinalizedVideoCard(card, item) {
- if (!card || !item) return;
- card.refresh(item);
- card.setStatus([]);
- card.setProgress(null);
- const targetLevel = card._isActuallyVisible
- ? (card._context === 'current' ? (isMemoryConstrainedMode() ? 'thumb' : 'full') : 'thumb')
- : (card._context === 'current' ? (isMemoryConstrainedMode() ? 'none' : 'thumb') : 'none');
- await card.setLoadLevel('none');
- if (targetLevel !== 'none') await card.setLoadLevel(targetLevel);
- }
- const getPendingItemIdsForRuntime = (runtime) => {
- const taskIds = new Set([...runtime.tasks.keys()].map(String));
- return STATE.items.values()
- .filter(item => item?.isPending && item.taskId && taskIds.has(String(item.taskId)))
- .map(item => String(item.id))
- .filter(Boolean);
- };
- async function executeSingleTask(runtime, taskId, refIds, isMulti, token) {
- const taskMeta = runtime.tasks.get(taskId); if (!taskMeta) return;
- const actionType = sanitizePanelKey(taskMeta.action, runtime.activeAction || taskMeta.action || STATE.action);
- const settings = taskMeta.settingsSnapshot || buildActionSettingsSnapshot(actionType);
- const currentRefs = refIds.map(id => getRefItemForView(id, actionType, settings.refMode)).filter(r => r && r.active);
- if (currentRefs.length === 0 && !['MULTI', 'NO_REF'].includes(refIds[0])) { taskMeta.longStatus = 'Skipped'; return; }
- const isImage = actionType === 'gen_image';
- const batchCount = isImage ? parseInt(settings.batchSize || '1') : 1;
- const { api: apiPrompt, storage: storagePrompt } = PromptPipeline.process(taskMeta.promptText, '', actionType);
- const placeholderIds = createPlaceholders(taskId, batchCount, actionType, currentRefs, taskMeta.panelKey || actionType, storagePrompt);
- let attempts = 0, success = false, reqStartTime = Date.now();
- let failureCleanupDelayMs = 0;
- let progressTimer = 0;
- const batchCards = placeholderIds.map(pid => CardRegistry.get('current', pid)).filter(Boolean);
- const updateTask = (val, dur, ease) => {
- taskMeta.shortStatus = val.shortStatus; taskMeta.longStatus = val.longStatus; taskMeta.color = val.color;
- taskMeta.progress = val.progress !== undefined ? val.progress : null;
- if (val.forceUIUpdate) {
- updateStatusUI(true);
- } else {
- updateStatusUI(false);
- }
- if (actionType === 'gen_text' && taskMeta.chatStatusMsgId && !taskMeta.chatHasStreamText) {
- patchMessage(taskMeta.chatStatusMsgNode || taskMeta.chatStatusMsgId, val.longStatus || val.shortStatus || 'Working...', true);
- }
- const statusLines = buildCurrentStatusLines(val);
- for (let i = 0; i < batchCards.length; i++) {
- const card = batchCards[i];
- if (!card._isActuallyVisible && val.progress !== 100 && val.progress !== 0) continue;
- card.setStatus(statusLines);
- card.setProgress(taskMeta.progress, dur ?? val.durationMs, ease ?? val.easing);
- }
- placeholderIds.forEach(pid => {
- const pItem = STATE.items.get(pid);
- if (pItem) pItem._statusData = statusLines;
- });
- if (runtime.lane !== 'media') return;
- const refProgress = new Map();
- runtime.tasks.values().forEach(task => {
- task.refIds.forEach(rid => {
- const key = String(rid);
- const entry = refProgress.get(key) || { total: 0, count: 0 };
- entry.total += task.progress || 0;
- entry.count++;
- refProgress.set(key, entry);
- });
- });
- refIds.forEach(rid => {
- const refCard = CardRegistry.get('reference', rid);
- if (refCard?.setProgress) {
- const entry = refProgress.get(String(rid));
- refCard.setProgress(entry ? entry.total / entry.count : taskMeta.progress, dur ?? val.durationMs, ease ?? val.easing);
- }
- });
- };
- let expectedMs = 5000;
- if (isImage) {
- expectedMs = GenerationTimings.get('img', settings.resImg, batchCount);
- updateTask({ shortStatus: 'Req...', longStatus: 'Requesting...', color: '#fff', progress: 0, forceUIUpdate: true }, 0, 'linear');
- const tickProgress = () => {
- if (success || taskMeta.controller.signal.aborted) return;
- const elapsed = Date.now() - reqStartTime;
- const percent = Math.min(95, Math.floor((elapsed / expectedMs) * 100));
- updateTask({ shortStatus: `[${percent}%] Rendering...`, longStatus: 'Rendering image...', color: '#fff', progress: percent, durationMs: 250, easing: 'linear', forceUIUpdate: true });
- progressTimer = setTimeout(tickProgress, 250);
- };
- progressTimer = setTimeout(tickProgress, 250);
- taskMeta.controller.signal.addEventListener('abort', () => clearTimeout(progressTimer), { once: true });
- }
- try {
- while (attempts < APP_CONFIG.TIMINGS.MAX_RETRY_ATTEMPTS && !success && runtime.generationToken === token) {
- try {
- const liveRefPrompt = (!isMulti && currentRefs.length === 1) ? (STATE.refPrompts[currentRefs[0].id]?.[actionType] ?? null) : null;
- const config = await getApiPayload(actionType, currentRefs.length > 0 ? currentRefs : null, liveRefPrompt, apiPrompt, settings);
- const payload = config.payload;
- if (actionType === 'gen_text') taskMeta.textModel = taskMeta.textModel || payload.model || settings.textModel || 'grok-4.20';
- if (actionType === 'gen_image') payload.n = parseInt(settings.batchSize || '1');
- if (actionType === 'gen_text' && attempts === 0) {
- const userPrompt = storagePrompt || getLastResponsesUserText(payload);
- if (userPrompt) {
- const promptText = typeof userPrompt === 'string' ? userPrompt : JSON.stringify(userPrompt);
- taskMeta.chatUserMsgId = renderTextChatMessage('user', promptText);
- taskMeta.chatStatusMsgId = renderTextChatMessage('ai', 'Working...', true, promptText);
- taskMeta.chatStatusMsgNode = document.querySelector(`[data-msg-id="${taskMeta.chatStatusMsgId}"]`);
- }
- }
- if (actionType === 'gen_text') {
- taskMeta.chatHasStreamText = false;
- taskMeta.streamText = '';
- }
- reqStartTime = Date.now();
- taskMeta.reqStartTime = reqStartTime;
- taskMeta.expectedMs = expectedMs;
- const streamHandlers = actionType === 'gen_text' && payload.stream === true ? {
- onAttemptStart: () => {
- taskMeta.chatHasStreamText = false;
- taskMeta.streamText = '';
- taskMeta._mdDirty = false;
- taskMeta._mdFlushId = 0;
- taskMeta._mdCache = null;
- taskMeta._lastRenderTime = 0;
- if (taskMeta.chatStatusMsgId) patchMessage(taskMeta.chatStatusMsgNode || taskMeta.chatStatusMsgId, 'Working...', true);
- },
- onTextDelta: (delta, fullText) => {
- taskMeta.chatHasStreamText = true;
- taskMeta.streamText = fullText;
- taskMeta._mdDirty = true;
- if (!taskMeta._mdFlushId) {
- const delay = Math.max(0, 50 - (Date.now() - (taskMeta._lastRenderTime || 0)));
- taskMeta._mdFlushId = setTimeout(() => {
- requestAnimationFrame(() => {
- taskMeta._mdFlushId = 0;
- taskMeta._lastRenderTime = Date.now();
- if (taskMeta.chatStatusMsgNode && taskMeta._mdDirty) {
- const lastSafeCut = taskMeta.streamText.lastIndexOf('\n\n');
- if (lastSafeCut > 0) {
- if (taskMeta._mdCache && taskMeta.streamText.startsWith(taskMeta._mdCache.text)) {
- const cachedLen = taskMeta._mdCache.text.length;
- if (lastSafeCut > cachedLen) {
- const newBlocks = taskMeta.streamText.slice(cachedLen, lastSafeCut);
- taskMeta._mdCache.text = taskMeta.streamText.slice(0, lastSafeCut);
- taskMeta._mdCache.html += parseMarkdown(newBlocks);
- }
- const tail = taskMeta.streamText.slice(lastSafeCut);
- patchMessage(taskMeta.chatStatusMsgNode || taskMeta.chatStatusMsgId, taskMeta.streamText, false, taskMeta._mdCache.html + parseMarkdown(tail));
- } else {
- const safeText = taskMeta.streamText.slice(0, lastSafeCut);
- taskMeta._mdCache = { text: safeText, html: parseMarkdown(safeText) };
- const tail = taskMeta.streamText.slice(lastSafeCut);
- patchMessage(taskMeta.chatStatusMsgNode || taskMeta.chatStatusMsgId, taskMeta.streamText, false, taskMeta._mdCache.html + parseMarkdown(tail));
- }
- } else {
- patchMessage(taskMeta.chatStatusMsgNode || taskMeta.chatStatusMsgId, taskMeta.streamText, false, parseMarkdown(taskMeta.streamText));
- }
- taskMeta._mdDirty = false;
- }
- });
- }, delay);
- }
- if (actionType === 'gen_text' && STATE.action === 'gen_text') {
- if (!taskMeta._smoothScrollTimer) {
- taskMeta._smoothScrollTimer = setTimeout(() => {
- taskMeta._smoothScrollTimer = 0;
- const chatView = document.getElementById('xai-chat-container');
- if (chatView) {
- const isNearBottom = chatView.scrollHeight - chatView.scrollTop - chatView.clientHeight < 350;
- if (isNearBottom) chatView.scrollTo({ top: chatView.scrollHeight, behavior: 'smooth' });
- }
- }, 300);
- }
- }
- },
- onStreamDone: () => {
- if (taskMeta._mdFlushId) { clearTimeout(taskMeta._mdFlushId); taskMeta._mdFlushId = 0; }
- if (taskMeta.chatStatusMsgId && taskMeta._mdDirty) {
- patchMessage(taskMeta.chatStatusMsgNode || taskMeta.chatStatusMsgId, taskMeta.streamText, false, parseMarkdown(taskMeta.streamText));
- taskMeta._mdDirty = false;
- taskMeta._mdCache = null;
- }
- },
- onTextDelta: (delta, fullText) => {
- taskMeta.chatHasStreamText = true;
- taskMeta.streamText = fullText;
- taskMeta._mdDirty = true;
- if (!taskMeta._mdFlushId) {
- const delay = Math.max(0, 50 - (Date.now() - (taskMeta._lastRenderTime || 0)));
- taskMeta._mdFlushId = setTimeout(() => {
- requestAnimationFrame(() => {
- taskMeta._mdFlushId = 0;
- taskMeta._lastRenderTime = Date.now();
- if (taskMeta.chatStatusMsgNode && taskMeta._mdDirty) {
- patchMessage(taskMeta.chatStatusMsgNode, taskMeta.streamText, false);
- taskMeta._mdDirty = false;
- }
- });
- }, delay);
- }
- if (actionType === 'gen_text' && STATE.action === 'gen_text') {
- if (!taskMeta._smoothScrollTimer) {
- taskMeta._smoothScrollTimer = setTimeout(() => {
- taskMeta._smoothScrollTimer = 0;
- const chatView = document.getElementById('xai-chat-container');
- if (chatView) {
- const isNearBottom = chatView.scrollHeight - chatView.scrollTop - chatView.clientHeight < 350;
- if (isNearBottom) chatView.scrollTo({ top: chatView.scrollHeight, behavior: 'smooth' });
- }
- }, 300);
- }
- }
- },
- onStreamDone: () => {
- if (taskMeta._mdFlushId) { clearTimeout(taskMeta._mdFlushId); taskMeta._mdFlushId = 0; }
- if (taskMeta.chatStatusMsgId && taskMeta._mdDirty) {
- patchMessage(taskMeta.chatStatusMsgNode || taskMeta.chatStatusMsgId, taskMeta.streamText, false);
- taskMeta._mdDirty = false;
- }
- }
- } : null;
- const audioOpfsId = actionType === 'gen_audio' ? crypto.randomUUID() : null;
- const resultData = await fetchWithRetries(config.endpoint, payload, taskMeta.controller.signal, runtime, token, settings, taskId, updateTask, streamHandlers, audioOpfsId);
- if (runtime.generationToken !== token || !resultData) break;
- if (isImage) {
- if (resultData?.data && Array.isArray(resultData.data)) {
- updateTask({ shortStatus: 'Saving...', longStatus: 'Rendering Complete. Saving...', color: '#10b981', progress: 100, forceUIUpdate: true }, 400, 'ease-out');
- if (attempts === 0) {
- GenerationTimings.update('img', settings.resImg, batchCount, Date.now() - reqStartTime);
- }
- const committedIds = [], committedItems = [], batchTimestampBase = Date.now();
- for (let i = 0; i < resultData.data.length; i++) {
- if (runtime.generationToken !== token) break;
- const imgObj = resultData.data[i];
- const is2k = settings.resImg === '2k';
- const prompt = imgObj.revised_prompt ? PromptPipeline.process(imgObj.revised_prompt).storage : storagePrompt;
- const finalId = crypto.randomUUID();
- const b64Str = imgObj.b64_json;
- delete imgObj.b64_json;
- imgObj.b64_json = null;
- const finalBytes = Uint8Array.fromBase64(b64Str);
- const buffer = finalBytes.buffer;
- const result = await StorageClient.processMediaResponse(finalId, {
- buffer: buffer,
- prompt,
- is2k,
- optimize2k: settings.optimize2k,
- embedImagePrompt: settings.embedImagePrompt
- }, [buffer]);
- if (result.thumbBlob) {
- const cacheKey = `${finalId}_thumb`;
- const thumbFile = new File([result.thumbBlob], cacheKey, { type: result.mimeType || 'image/webp' });
- const cachedPromise = Promise.resolve(thumbFile);
- MediaCache._inFlight.set(cacheKey, cachedPromise);
- cachedPromise.finally(() => MediaCache._inFlight.delete(cacheKey));
- }
- const item = Store.commitNewItem({
- id: finalId,
- type: 'image',
- mimeType: result.mimeType,
- ratio: result.ratio,
- lqip: result.lqip,
- xmpEmbedded: result.xmpEmbedded,
- prompt,
- timestamp: Pipeline.makeBatchTimestamp(batchTimestampBase, i, resultData.data.length),
- filename: `Grok Img - ${makeTimestamp()} - ${i + 1}of${resultData.data.length}.${result.mimeType === 'image/jpeg' ? 'jpg' : 'png'}`,
- isUploadedRef: false,
- isNewlyGenerated: true
- }, { targetArrayName: 'currentOrder', replacePlaceholderId: placeholderIds[i], panelKey: taskMeta.panelKey });
- committedIds.push(finalId);
- committedItems.push(item);
- }
- if (committedIds.length > 0) {
- await commitItemsToHistory(committedItems, committedIds);
- scheduleAutoDownloads(committedItems, 500, 50);
- }
- const unused = placeholderIds.slice(resultData.data.length);
- if (unused.length > 0) Store.clearCurrentTransient(unused, taskMeta.panelKey);
- success = committedIds.length > 0;
- } else throw new Error('Invalid response format');
- } else if (actionType === 'gen_audio') {
- updateTask({ shortStatus: 'Saving...', longStatus: 'Audio Generated. Saving...', color: '#10b981', progress: 100 });
- const id = resultData.opfsId;
- const item = Store.commitNewItem({
- id, type: 'audio', mimeType: 'audio/mpeg', timestamp: Date.now(), prompt: Pipeline.normalizePrompt(payload.text || ''),
- filename: `Grok Audio - ${makeTimestamp()}.mp3`,
- ratio: 5, originalUrl: null, downloaded: false, lqip: '#0f172a', isUploadedRef: false, isNewlyGenerated: true
- }, { targetArrayName: 'currentOrder', replacePlaceholderId: placeholderIds[0], panelKey: taskMeta.panelKey });
- await commitItemsToHistory([item], [id]);
- scheduleAutoDownloads([item]);
- success = true;
- } else if (actionType === 'gen_text') {
- const textContent = extractResponsesOutputText(resultData);
- if (!textContent) {
- throw new Error(resultData.error?.message || 'Invalid API response: No text returned.');
- }
- updateTask({ shortStatus: 'Saving...', longStatus: 'Text Generated. Saving...', color: '#10b981', progress: 100 });
- const userPrompt = storagePrompt || getLastResponsesUserText(payload);
- const baseMessages = sanitizeTextConversationStore(taskMeta.textChatSnapshot || []);
- if (taskMeta.chatHasStreamText && taskMeta.chatStatusMsgId) {
- patchMessage(taskMeta.chatStatusMsgNode || taskMeta.chatStatusMsgId, textContent, false);
- } else {
- const statusEl = document.querySelector(`[data-msg-id="${taskMeta.chatStatusMsgId}"]`);
- if (statusEl) statusEl.remove();
- safeRenderResponse(textContent, 'assistant');
- }
- Store.removeItem(placeholderIds[0]);
- const updatedAt = Date.now();
- const item = commitTextSession([
- ...baseMessages,
- { role: 'user', text: Pipeline.normalizePrompt(typeof userPrompt === 'string' ? userPrompt : JSON.stringify(userPrompt)), timestamp: updatedAt - 1 },
- { role: 'assistant', text: textContent, timestamp: updatedAt }
- ], { model: taskMeta.textModel || settings.textModel, updatedAt });
- await commitItemsToHistory([item], [item.id]);
- success = true;
- if (actionType === 'gen_text' && STATE.action === 'gen_text') scrollTextChatToBottom();
- } else {
- const reqId = resultData.request_id; if (!reqId) throw new Error('No Request ID returned');
- const pItem = STATE.items.get(placeholderIds[0]);
- if (pItem) {
- pItem.reqId = reqId;
- pItem.prompt = Pipeline.normalizePrompt(payload.prompt);
- LazyTextStore.set(pItem.id, { prompt: pItem.prompt, responseText: '', textChatMessages: null });
- flushCurrentPanelSessionSave();
- }
- const videoUrl = await pollVideoTask(reqId, taskMeta.controller.signal, runtime, token, updateTask);
- if (videoUrl && runtime.generationToken === token) {
- updateTask({ shortStatus: 'Saving video...', longStatus: 'Downloading full video...', color: '#3b82f6', progress: 95 });
- const videoBlob = await fetchBlobCSP(videoUrl, { signal: taskMeta.controller.signal, timeout: 30000 });
- await OPFS.write(placeholderIds[0], 'full', videoBlob);
- updateTask({ shortStatus: 'Extracting poster...', longStatus: 'Extracting poster image...', color: '#3b82f6', progress: 100 });
- using localVideoUrl = new DisposableURL(videoBlob);
- let posterData = await extractVideoPosterData(localVideoUrl.url, pItem);
- if (posterData?.thumbBlob) await OPFS.write(placeholderIds[0], 'thumb', posterData.thumbBlob);
- finalizePendingVideoItem(pItem, videoUrl, posterData);
- pItem.isNewlyGenerated = true;
- DBQueue.add(pItem);
- scheduleCurrentPanelSessionSave();
- const card = CardRegistry.get('current', pItem.id);
- if (card) await refreshFinalizedVideoCard(card, pItem);
- await commitItemsToHistory([pItem], [pItem.id]);
- scheduleAutoDownloads([pItem]);
- success = true;
- }
- }
- if (success) {
- runtime.successCount++;
- if (runtime.successCount >= 3) {
- runtime.successCount = 0;
- nuclearWipe();
- }
- }
- } catch (err) {
- if (err.name === 'AbortError' || runtime.generationToken !== token) {
- taskMeta.shortStatus = 'Cancelled'; taskMeta.longStatus = 'Cancelled';
- updateTask({ shortStatus: 'Cancelled', longStatus: 'Cancelled', color: 'var(--danger)' });
- } else {
- const errMsg = getErrorMessage(err);
- if (actionType === 'gen_text' && taskMeta.chatStatusMsgId) {
- if (taskMeta.chatHasStreamText && taskMeta.streamText) patchMessage(taskMeta.chatStatusMsgNode || taskMeta.chatStatusMsgId, `${taskMeta.streamText}\n\n[Failed: ${errMsg}]`, false);
- else patchMessage(taskMeta.chatStatusMsgNode || taskMeta.chatStatusMsgId, `Failed: ${errMsg}`, false);
- if (storagePrompt) {
- const failText = taskMeta.chatHasStreamText && taskMeta.streamText ? `${taskMeta.streamText}\n\n[Failed: ${errMsg}]` : `Failed: ${errMsg}`;
- const updatedAt = Date.now();
- const baseMessages = sanitizeTextConversationStore(taskMeta.textChatSnapshot || []);
- const item = commitTextSession([
- ...baseMessages,
- { role: 'user', text: storagePrompt, timestamp: updatedAt - 1 },
- { role: 'assistant', text: failText, timestamp: updatedAt }
- ], { model: taskMeta.textModel || settings.textModel, updatedAt });
- void commitItemsToHistory([item], [item.id]);
- }
- }
- 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);
- failureCleanupDelayMs = 1200;
- } 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);
- failureCleanupDelayMs = 1200;
- } 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);
- failureCleanupDelayMs = 1200;
- }
- }
- break;
- }
- }
- } finally {
- clearTimeout(progressTimer);
- if (!success) {
- if (failureCleanupDelayMs > 0) await sleep(failureCleanupDelayMs, taskMeta.controller.signal).catch(() => {});
- removeCurrentPanelItems(placeholderIds, { purgeStorage: true });
- }
- }
- }
- function spawnTask(runtime, jobKey, refIds, isMulti, token, taskContext = {}) {
- let pObj = runtime.progress.get(jobKey);
- if (pObj) pObj.active++;
- const tId = `task-${Date.now()}-${Math.random()}`;
- runtime.tasks.set(tId, { controller: new AbortController(), refIds, shortStatus: 'Starting...', longStatus: 'Starting...', color: '#fff', isMulti, progress: 0, lane: runtime.lane, createdAt: Date.now(), ...taskContext });
- updateStatusUI();
- let tProm = executeSingleTask(runtime, tId, refIds, isMulti, token).finally(() => {
- if (pObj) { pObj.active--; pObj.done++; }
- runtime.tasks.delete(tId);
- runtime.taskPromises.delete(tProm);
- updateStatusUI();
- });
- runtime.taskPromises.add(tProm);
- }
- async function processQueue(runtime, token, taskContext = {}) {
- const generationAction = sanitizePanelKey(taskContext.action, runtime.activeAction || taskContext.action || STATE.action);
- runtime.successCount = 0;
- runtime.activeAction = generationAction;
- const getActiveCount = () => runtime.tasks.size;
- let endedNaturally = false;
- while (runtime.isGenerating && runtime.generationToken === token) {
- const isTextOrAudio = runtime.lane !== 'media';
- const limit = isTextOrAudio ? 1 : (parseInt(taskContext.settingsSnapshot?.concurrentSessions ?? STATE.concurrentSessions) || 1);
- const liveLoops = isTextOrAudio ? 1 : ((parseInt(taskContext.settingsSnapshot?.loops ?? getLoopsValue(generationAction)) || 0) + 1);
- const effectiveRefMode = isTextOrAudio ? 'multi' : normalizeRefModeForAction(generationAction, taskContext.settingsSnapshot?.refMode ?? STATE.refMode);
- const refs = getEligibleRefs(generationAction, { refMode: effectiveRefMode }).eligible.filter(m => m.active), now = Date.now(); let spawned = false;
- const hasWaitingRefs = !isTextOrAudio && refs.some(ref => (ref.readyAt || 0) > now);
- const spawnTaskIfAllowed = (jobKey, nextRefIds, nextIsMulti) => {
- if (getActiveCount() >= limit) return false;
- let p = runtime.progress.get(jobKey);
- if (!p) {
- p = { done: 0, active: 0, needed: liveLoops };
- runtime.progress.set(jobKey, p);
- } else p.needed = liveLoops;
- if (p.done + p.active < p.needed) {
- spawnTask(runtime, jobKey, nextRefIds, nextIsMulti, token, taskContext);
- return true;
- }
- return false;
- };
- if (refs.length === 0) { while (spawnTaskIfAllowed('NO_REF', ['NO_REF'], false)) spawned = true; }
- else if (effectiveRefMode === 'queue') {
- const pendingRefs = refs.filter(r => (r.readyAt || 0) <= now).sort((a, b) => {
- const pA = runtime.progress.get(a.id) || { done: 0, active: 0 };
- const pB = runtime.progress.get(b.id) || { done: 0, active: 0 };
- return (pA.done + pA.active) - (pB.done + pB.active);
- });
- for (const ref of pendingRefs) {
- if (!spawnTaskIfAllowed(ref.id, [ref.id], false)) break;
- spawned = true;
- }
- } else if (!hasWaitingRefs) {
- while (spawnTaskIfAllowed('MULTI', refs.map(r => r.id), true)) spawned = true;
- }
- const isDone = runtime.progress.entries().every(([key, progress]) => {
- if (key === 'MULTI' || key === 'NO_REF' || refs.some(ref => ref.id === key)) return progress.done >= progress.needed;
- return true;
- });
- if (isDone && !hasWaitingRefs && getActiveCount() === 0) { endedNaturally = true; break; }
- if (!spawned) {
- const wakeVersion = runtime.queueWake.version;
- const promises = [...runtime.taskPromises, runtime.queueWake.wait(wakeVersion)];
- if (promises.length === 1) promises.push(sleep(1000));
- await Promise.race(promises);
- } else {
- await TaskChunker.yieldIfNeeded();
- }
- }
- if (runtime.generationToken === token) {
- if (endedNaturally) TitleState.markDone();
- runtime.progress.clear();
- setRuntimeField(runtime, 'isGenerating', false);
- runtime.activeAction = null;
- updateStatusUI(true);
- }
- }
- let _statusRenderHandle = 0;
- let _lastStatusUpdateTime = 0;
- let _statusDeferredTimer = 0;
- let _statusViewCacheKey = '';
- const _cleanupGlobalTimers = () => {
- clearTimeout(_statusDeferredTimer); _statusDeferredTimer = 0;
- cancelAnimationFrame(_statusRenderHandle); _statusRenderHandle = 0;
- cancelAnimationFrame(_chatScrollRaf); _chatScrollRaf = 0;
- cancelAnimationFrame(refReorderRaf); refReorderRaf = 0;
- };
- const UI_SCHEDULER = {
- _writeTask: null,
- write(fn) {
- if (this._writeTask) return;
- this._writeTask = requestAnimationFrame(() => {
- fn();
- this._writeTask = null;
- });
- }
- };
- const createStatusShell = () => {
- const row = document.createElement('div');
- row.className = 'w-100 d-flex justify-end';
- return row;
- };
- const GENERATION_LANE_SHORT_LABEL = {
- media: 'M',
- audio: 'A',
- text: 'T'
- };
- const getTaskLaneLabel = (task) => {
- if (task.lane === 'media') return (task.action && task.action.includes('video')) ? 'MV' : 'MI';
- return GENERATION_LANE_SHORT_LABEL[task.lane] || task.lane || 'Task';
- };
- const getCompactTaskStatus = (task) => {
- let progress = Number(task?.progress);
- if (task?.action === 'gen_image' && (progress === 0 || Number.isNaN(progress)) && task.reqStartTime && task.expectedMs) {
- const elapsed = Date.now() - task.reqStartTime;
- progress = Math.min(95, (elapsed / task.expectedMs) * 95);
- }
- if (Number.isFinite(progress) && progress > 0 && progress < 100) return `${Math.round(progress)}%`;
- const short = String(task?.shortStatus || task?.longStatus || '').trim();
- if (!short) return '...';
- const compact = short.replace(/\s+/g, '');
- const statusCode = short.match(/\((\d{3})\)/)?.[1] || '';
- const retryNo = short.match(/\[(\d+)\]/)?.[1] || '';
- const retryPair = short.match(/\[(\d+)\/(\d+)\]/);
- const seconds = short.match(/(\d+)s/i)?.[1] || '';
- if (retryPair) return `P${retryPair[1]}/${retryPair[2]}`;
- if (/^starting/i.test(short)) return 'In1';
- if (/^req/i.test(short)) return 'Req';
- if (/^saving/i.test(short)) return 'Sav';
- if (/extracting/i.test(short)) return 'Pst';
- if (/^cancel/i.test(short)) return 'Stop';
- if (/^blocked/i.test(short)) return statusCode ? `B${statusCode}` : 'Blk';
- if (/bad req|^invalid/i.test(short)) return statusCode ? `E${statusCode}` : 'Bad';
- if (/^failed/i.test(short)) return retryNo ? `F${retryNo}` : 'Fail';
- if (/^busy/i.test(short)) return `${statusCode ? `B${statusCode}` : 'Busy'}${retryNo ? `/${retryNo}` : ''}`;
- if (/^auth/i.test(short)) return `${statusCode ? `A${statusCode}` : 'Auth'}${retryNo ? `/${retryNo}` : ''}`;
- if (/^limit|^lmt/i.test(short)) {
- if (seconds) return `L${seconds}s`;
- return `${statusCode ? `L${statusCode}` : 'Lmt'}${retryNo ? `/${retryNo}` : ''}`;
- }
- if (/^wait/i.test(short)) return `${seconds ? `W${seconds}s` : 'Wait'}${retryNo ? `/${retryNo}` : ''}`;
- return compact.slice(0, 4);
- };
- const createStatusSummaryText = ({ doneT, totalT }) => `${Math.max(0, totalT - doneT)}`;
- const createStatusSummaryKey = ({ taskList, doneT, totalT, activeT, showTaskList }) =>[
- doneT,
- totalT,
- activeT,
- showTaskList ? 1 : 0,
- ...(showTaskList ? taskList.map(task => `${task.lane || ''}|${task.color || ''}|${getCompactTaskStatus(task)}|${Math.round(Number(task?.progress) || 0)}`) : [])
- ].join('\n');
- const createTaskSummaryChip = (task) => {
- 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 = `${getTaskLaneLabel(task)} ${getCompactTaskStatus(task)}`;
- return chip;
- };
- const StatusSingleton = { ready: null, summary: null, summaryText: null, list: null };
- const buildStatusReadyView = () => {
- if (!StatusSingleton.ready) {
- const row = createStatusShell();
- const text = document.createElement('span');
- text.className = 'text-muted'; text.textContent = 'Ready';
- row.appendChild(text);
- StatusSingleton.ready = row;
- }
- return { key: 'ready', node: StatusSingleton.ready };
- };
- const buildStatusSummaryView = ({ taskList, doneT, totalT, activeT, showTaskList }) => {
- const summaryText = createStatusSummaryText({ doneT, totalT, activeT, taskList });
- const key = createStatusSummaryKey({ taskList, doneT, totalT, activeT, showTaskList });
- if (!StatusSingleton.summary) {
- StatusSingleton.summary = createStatusShell();
- StatusSingleton.list = document.createElement('div');
- StatusSingleton.list.className = 'd-flex justify-end';
- StatusSingleton.list.style.cssText = 'flex:1; overflow:hidden; mask-image:linear-gradient(to right, transparent, black 15px);';
- StatusSingleton.summaryText = document.createElement('span');
- StatusSingleton.summaryText.className = 'text-success';
- StatusSingleton.summaryText.style.cssText = 'font-weight:600; white-space:nowrap; flex-shrink:0;';
- StatusSingleton.summaryText.title = 'Tasks across all runtimes: remaining (active)';
- StatusSingleton.summary.appendChild(StatusSingleton.list);
- StatusSingleton.summary.appendChild(StatusSingleton.summaryText);
- }
- const row = StatusSingleton.summary;
- const tasksSig = showTaskList ? taskList.map(t => `${t.lane}|${t.color}|${getCompactTaskStatus(t)}`).join(',') : '';
- return {
- key, node: row,
- update: () => {
- row.classList.toggle('gap-2', showTaskList);
- if (StatusSingleton.summaryText.textContent !== summaryText) StatusSingleton.summaryText.textContent = summaryText;
- if (StatusSingleton.list._lastSig !== tasksSig) {
- StatusSingleton.list._lastSig = tasksSig;
- if (showTaskList) {
- StatusSingleton.list.style.display = 'flex';
- StatusSingleton.list.replaceChildren(...taskList.map(createTaskSummaryChip));
- } else StatusSingleton.list.style.display = 'none';
- }
- }
- };
- };
- const setStatusView = (view) => {
- if (!DOM.status || !view || _statusViewCacheKey === view.key) return;
- _statusViewCacheKey = view.key;
- UI_SCHEDULER.write(() => {
- if (DOM.status.firstChild !== view.node) DOM.status.replaceChildren(view.node);
- if (view.update) view.update();
- });
- };
- const setDocumentTitle = (title) => {
- if (document.title !== title) document.title = title;
- };
- const getQueuePlanState = (action = STATE.action, { refMode = STATE.refMode, loops = getLoopsValue(action) } = {}) => {
- const effectiveRefMode = normalizeRefModeForAction(action, refMode);
- const refs = Store.getRefItems(action, effectiveRefMode);
- const now = Date.now();
- const { eligible, acceptedType } = getEligibleRefs(action, { refMode: effectiveRefMode });
- const eligibleIds = new Set(eligible.map(e => e.id));
- const isTextOrAudio = action === 'gen_text' || action === 'gen_audio';
- const liveLoops = isTextOrAudio ? 1 : ((parseInt(loops) || 0) + 1);
- const plannedJobs = eligible.length === 0 ? liveLoops : (effectiveRefMode === 'queue' ? eligible.length * liveLoops : liveLoops);
- return { refs, now, eligible, eligibleIds, acceptedType, liveLoops, plannedJobs, effectiveRefMode };
- };
- const pruneJobProgress = (runtime, 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)));
- });
- runtime.progress.keys().forEach(key => {
- if (!valid.has(String(key))) runtime.progress.delete(key);
- });
- };
- const getProgressKey = (refId, refMode = STATE.refMode) => refMode === 'queue' ? refId : 'MULTI';
- const clearReferenceCardRuntime = (card) => {
- card?.setStatus?.([]);
- card?.setProgress?.(null);
- };
- const getReferenceLabelState = (ref, idx, acceptedType, eligibleIds, progress, now, { isGenerating = false, refMode = STATE.refMode, action = STATE.action } = {}) => {
- const baseLabel = `IMAGE_${Math.max(0, idx)}`;
- if (acceptedType === 'none') return { text: baseLabel, title: 'This mode does not use references' };
- if (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) {
- const apiLimit = (ACTION_SPEC[action] ?? ACTION_SPEC.gen_image).apiLimit ?? 99;
- const withinApiLimit = refMode !== 'multi' || idx < apiLimit;
- if (refMode === 'multi' && withinApiLimit) {
- return {
- text: `<IMAGE_${idx}>`,
- title: 'Insert tag',
- onClick: (e) => {
- e.preventDefault();
- e.stopPropagation();
- insertTextAtCursor(DOM.prompt, ` <IMAGE_${idx}> `);
- }
- };
- }
- return { text: baseLabel, title: withinApiLimit ? 'Active Reference' : 'API Limit Exceeded (Ignored)' };
- }
- return { text: baseLabel, title: ref.type !== acceptedType ? `Need ${acceptedType}` : 'Limit Reached' };
- };
- const getRefWarmupState = (ref, now = Date.now()) => {
- const readyAt = Number(ref?.readyAt) || 0;
- if (readyAt <= now) return null;
- const totalMs = 10000;
- const remainingMs = readyAt - now;
- const elapsedMs = Math.max(0, totalMs - Math.min(totalMs, remainingMs));
- return { remainingMs, progress: Math.min(100, Math.max(0, (elapsedMs / totalMs) * 100)) };
- };
- const getReferenceStatusLines = (ref, tasks, progress, now) => {
- if (!ref.active) return [];
- const warmup = getRefWarmupState(ref, now);
- if (!progress && !warmup && tasks.length === 0) return [];
- const lines = [];
- if (warmup) lines.push({ text: `Warmup ${Math.max(0.1, warmup.remainingMs / 1000).toFixed(1)}s`, color: '#9e9e9e' });
- tasks.forEach((task, index) => lines.push({ text: `[T${index + 1}] ${task.shortStatus}`, color: task.color }));
- return lines;
- };
- const getReferenceStatusAction = () => {
- if (JobManager.getLane(STATE.action) !== 'media') return STATE.action;
- const mediaRuntime = JobManager.getRuntime('media');
- if (mediaRuntime.isGenerating && mediaRuntime.activeAction) return mediaRuntime.activeAction;
- return STATE.action;
- };
- const getRuntimeSummaryCounts = (runtime) => {
- if (runtime.lane === 'media') {
- const action = runtime.activeAction || (JobManager.getLane(STATE.action) === 'media' ? STATE.action : 'gen_image');
- const { eligibleIds, plannedJobs } = getQueuePlanState(action);
- const taskList = [...runtime.tasks.values()];
- pruneJobProgress(runtime, eligibleIds, taskList);
- let queuedT = 0, doneT = 0, totalT = 0;
- for (const [key, p] of runtime.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);
- doneT += doneForKey;
- totalT += totalForKey;
- }
- totalT = Math.max(totalT, runtime.isGenerating ? plannedJobs : 0, doneT + queuedT + taskList.length);
- return { doneT, totalT, queuedT };
- }
- const activeT = runtime.tasks.size;
- const totalT = runtime.isGenerating ? Math.max(1, activeT) : activeT;
- return { doneT: 0, totalT, queuedT: Math.max(0, totalT - activeT) };
- };
- function seedQueueProgress(runtime, action = STATE.action) {
- runtime.progress.clear();
- if (runtime.lane !== 'media') return;
- const { eligible, liveLoops, effectiveRefMode } = getQueuePlanState(action);
- if (eligible.length === 0) runtime.progress.set('NO_REF', { done: 0, active: 0, needed: liveLoops });
- else if (effectiveRefMode === 'queue') eligible.forEach(ref => runtime.progress.set(ref.id, { done: 0, active: 0, needed: liveLoops }));
- else runtime.progress.set('MULTI', { done: 0, active: 0, needed: liveLoops });
- }
- function updateStatusUI(forceUpdate = false) {
- if (!DOM.status) return;
- const now = performance.now();
- if (!forceUpdate && (now - _lastStatusUpdateTime < 200)) {
- if (!_statusDeferredTimer) {
- _statusDeferredTimer = setTimeout(() => {
- _statusDeferredTimer = 0;
- updateStatusUI(true);
- }, 200 - (now - _lastStatusUpdateTime));
- }
- return;
- }
- if (_statusDeferredTimer) {
- clearTimeout(_statusDeferredTimer);
- _statusDeferredTimer = 0;
- }
- _lastStatusUpdateTime = now;
- if (_statusRenderHandle) return;
- const renderFn = () => {
- _statusRenderHandle = 0;
- if (!DOM.status) return;
- const mediaRuntime = JobManager.getRuntime('media');
- const refStatusAction = getReferenceStatusAction();
- const refStatusPlan = getQueuePlanState(refStatusAction);
- const { refs, now, eligibleIds, acceptedType, effectiveRefMode } = refStatusPlan;
- const refIndexMap = new Map(getRefOrderForView(refStatusAction, effectiveRefMode).map((id, index) => [id, index]));
- const taskList = JobManager.getAllTasks();
- const activeT = taskList.length;
- const mediaTaskList = [...mediaRuntime.tasks.values()];
- pruneJobProgress(mediaRuntime, eligibleIds, mediaTaskList);
- const tasksByRef = Object.groupBy(
- mediaTaskList.flatMap(task => task.refIds.map(rid => ({ rid, task }))),
- ({ rid }) => rid
- );
- let queuedT = 0, done = 0, totalPlanned = 0;
- GenerationRegistry.getAllRuntimes().forEach(runtime => {
- const runtimeCounts = getRuntimeSummaryCounts(runtime);
- queuedT += runtimeCounts.queuedT;
- done += runtimeCounts.doneT;
- totalPlanned += runtimeCounts.totalT;
- });
- totalPlanned = Math.max(totalPlanned, done + activeT + queuedT);
- const doneT = Math.min(done, totalPlanned);
- const totalT = Math.max(totalPlanned, doneT + activeT);
- let activeRefCount = 0;
- const showRefProgress = JobManager.getLane(STATE.action) === 'media';
- let activeTagIndex = 0;
- refs.forEach(r => {
- const card = CardRegistry.get('reference', r.id); if (!card) return;
- if (r.active) activeRefCount++;
- const refTasks = tasksByRef[r.id]?.map(entry => entry.task) ?? [];
- const progress = mediaRuntime.progress.get(getProgressKey(r.id, effectiveRefMode));
- let idx = refIndexMap.get(r.id) ?? -1;
- if (r.active && eligibleIds.has(r.id)) {
- idx = activeTagIndex++;
- }
- const label = getReferenceLabelState(r, idx, acceptedType, eligibleIds, progress, now, { isGenerating: mediaRuntime.isGenerating, refMode: effectiveRefMode, action: refStatusAction });
- const statusLines = showRefProgress ? getReferenceStatusLines(r, refTasks, progress, now) : [];
- const warmup = getRefWarmupState(r, now);
- const isProcessing = showRefProgress && (refTasks.length > 0 || (mediaRuntime.isGenerating && r.active && eligibleIds.has(r.id) && !!warmup));
- if (isProcessing) card._internals.states.add('processing'); else card._internals.states.delete('processing');
- if (label.onClick) card._internals.states.add('active-multi'); else card._internals.states.delete('active-multi');
- card.setLabel(label.text, label.title || '', label.onClick || null);
- if (statusLines.length > 0) card.setStatus(statusLines);
- else clearReferenceCardRuntime(card);
- if (refTasks.length === 0 && warmup) card.setProgress(warmup.progress);
- else if (statusLines.length === 0) card.setProgress(null);
- });
- if (!isAnyRuntimeBusy()) {
- setStatusView(buildStatusReadyView());
- TitleState.sync();
- return;
- }
- setStatusView(buildStatusSummaryView({ taskList, doneT, totalT, activeT, showTaskList: true }));
- TitleState.sync(`${totalPlanned > 0 ? Math.floor((done / totalPlanned) * 100) : 0}%`);
- if (isAnyRuntimeBusy()) {
- if (!DOM.status._busyTimer) {
- DOM.status._busyTimer = setInterval(() => updateStatusUI(), 250);
- }
- } else {
- if (DOM.status._busyTimer) {
- clearInterval(DOM.status._busyTimer);
- DOM.status._busyTimer = null;
- }
- }
- };
- _statusRenderHandle = 1;
- if (document.hidden) {
- scheduler.postTask(renderFn, { priority: 'background', delay: 500 }).catch(() => setTimeout(renderFn, 1000));
- } else {
- _statusRenderHandle = requestAnimationFrame(renderFn);
- }
- }
- function syncWipeButtonState() {
- if (!DOM.reset) return;
- const isBusy = isAnyRuntimeBusy();
- DOM.reset.disabled = isBusy;
- DOM.reset.setAttribute('aria-disabled', String(isBusy));
- DOM.reset.title = isBusy ? 'Wipe is locked while work is active in any mode' : 'Wipe Menu (Cookie / Data / Full)';
- }
- function toggleGenerateButton(isRunning) {
- if (!DOM.generateBtn) return;
- let c = DOM.generateBtn.querySelector('.btn-content');
- if (!c) {
- c = document.createElement('div');
- c.className = 'btn-content flex-center';
- DOM.generateBtn.replaceChildren(c);
- }
- DOM.generateBtn.classList.toggle('xai-btn--stop', isRunning);
- DOM.generateBtn.classList.toggle('xai-btn--run', !isRunning);
- DOM.generateBtn.classList.toggle('is-running', isRunning);
- }
- function handleGeneration() {
- if (STATE.activeRefPromptId) saveAndCloseRefPrompt(true);
- PromptSync.captureEditor(STATE.action, { scheduleCommit: false });
- PromptSync.commitNow(STATE.action);
- const actionType = STATE.action;
- const runtime = JobManager.getRuntime(actionType);
- if (runtime.isGenerating) {
- const pendingIds = getPendingItemIdsForRuntime(runtime);
- runtime.generationToken++;
- runtime.tasks.forEach(task => task.controller.abort());
- runtime.tasks.clear();
- runtime.taskPromises.clear();
- runtime.progress.clear();
- if (pendingIds.length > 0) removeCurrentPanelItems(pendingIds, { purgeStorage: true });
- setRuntimeField(runtime, 'isGenerating', false);
- runtime.activeAction = null;
- return;
- }
- const isTextMode = actionType === 'gen_text';
- const submittedPrompt = isTextMode ? sanitizePromptText(getEditorPromptValue()) : sanitizePromptText(getTaskPrompt(actionType));
- const usableRefs = getEligibleRefs(actionType).eligible;
- const hasCurrentRefPrompt = Store.getRefItems(actionType).some(ref => sanitizePromptText(STATE.refPrompts[ref.id]?.[actionType] || '').length > 0);
- if (!submittedPrompt && !hasCurrentRefPrompt && usableRefs.length === 0) return UI.showToast("Empty prompt", "error");
- if (actionType === 'gen_audio' && submittedPrompt.length > APP_CONFIG.LIMITS.TTS_TEXT) return UI.showToast(`Text too long (${submittedPrompt.length}/${APP_CONFIG.LIMITS.TTS_TEXT} chars)`, "error");
- const aRefs = usableRefs.filter(m => m.active), dur = parseInt(STATE.duration) || 5;
- const effectiveRefMode = normalizeRefModeForAction(actionType, STATE.refMode);
- ((ACTION_SPEC[actionType] ?? ACTION_SPEC.gen_image).warningPolicy?.({ action: actionType, refs: aRefs, duration: dur, aspectRatio: STATE.currentAr, refMode: effectiveRefMode }) ||[]).forEach(message => UI.showToast(message, "warn"));
- const staleTransientIds = STATE.currentOrder.filter(id => Pipeline.isTransientItem(STATE.items.get(id)));
- if (staleTransientIds.length > 0) {
- removeCurrentPanelItems(staleTransientIds, { purgeStorage: false });
- }
- runtime.generationToken++;
- runtime.activeAction = actionType;
- seedQueueProgress(runtime, actionType);
- if (!isTextMode && DOM.currentImages) {
- DOM.currentImages.scrollTop = 0;
- }
- PanelManager.clear();
- if (actionType !== 'gen_text') STATE.currentOrder = [];
- const taskContext = {
- lane: runtime.lane,
- action: actionType,
- panelKey: actionType,
- promptText: submittedPrompt,
- settingsSnapshot: buildActionSettingsSnapshot(actionType),
- textChatSnapshot: actionType === 'gen_text' ? sanitizeTextConversationStore(STATE.textChatMessages || []) : []
- };
- setRuntimeField(runtime, 'isGenerating', true);
- if (isTextMode) clearTextPromptInput({ focus: true });
- processQueue(runtime, runtime.generationToken, taskContext).catch(err => {
- addLog('ERROR', 'Queue Error', err);
- setRuntimeField(runtime, 'isGenerating', false);
- runtime.activeAction = null;
- });
- }
- function initAPIAndGeneration() {
- App.on(DOM.generateBtn, 'click', handleGeneration);
- App.on(DOM.reset, 'click', async () => {
- if (isAnyRuntimeBusy()) return UI.showToast("Stop generation first", "error");
- const choice = await UI.confirm(buildWipeConfirmMarkup());
- if (!choice || choice === 'cancel') return;
- if (choice === 'data') {
- const removedCount = await wipeCardHistoryData();
- return UI.showToast(removedCount > 0 ? `Card history cleared (${removedCount})` : "No card history to clear", "success");
- }
- if (choice === 'full') {
- _skipPagehidePersistence = true;
- if (STATE.activeLightboxId) closeLightbox();
- STATE.items.forEach((_, id) => Store.removeItem(id));
- PromptSync.clear();
- DBQueue.clearPending();
- clearCurrentPanelSession();
- _configSaveTask?.abort();
- _configSaveTask = null;
- await StorageClient.clearAll();
- localStorage.removeItem(APP_CONFIG.STORAGE.CONFIG_KEY);
- localStorage.removeItem(MEDIA_VOLUME_STORAGE_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