nightly_prediction

Untitled

Apr 22nd, 2026 (edited)
568
1
Never
1
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 483.97 KB | None | 1 0
  1. // ==UserScript==
  2. // @name xAI Overkill UI (v0.02.11.1)
  3. // @version 0.02.11.1
  4. // @description xAI Overkill UI
  5. // @match https://console.x.ai/*
  6. // @grant GM_cookie
  7. // @grant GM_xmlhttpRequest
  8. // @grant GM_download
  9. // @run-at document-start
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14.  
  15. const APP_CONFIG = {
  16. MEDIA: {
  17. DEFAULT_VOLUME: 0.05,
  18. THUMB_MAX_DIM: 600,
  19. THUMB_WEBP_QUALITY: 0.65,
  20. JPEG_EXPORT_QUALITY: 0.95
  21. },
  22. VOICES:['eve', 'ara', 'rex', 'sal', 'leo'],
  23. 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'],
  24. 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'],
  25. REASONING_EFFORTS:[{ val: 'none', label: 'None' }, { val: 'low', label: 'Low' }, { val: 'medium', label: 'Medium' }, { val: 'high', label: 'High' }],
  26. TTS_QUALITIES:[
  27. { label: '64k', val: '64000', bitRate: 64000 },
  28. { label: '128k', val: '128000', bitRate: 128000 },
  29. { label: '192k', val: '192000', bitRate: 192000 }
  30. ],
  31. TTS_TAGS:{
  32. instant:['pause', 'long-pause', 'hum-tune', 'laugh', 'chuckle', 'giggle', 'cry', 'tsk', 'tongue-click', 'lip-smack', 'breath', 'inhale', 'exhale', 'sigh'],
  33. wrapping:['soft', 'whisper', 'loud', 'build-intensity', 'decrease-intensity', 'higher-pitch', 'lower-pitch', 'slow', 'fast', 'sing-song', 'singing', 'laugh-speak', 'emphasis']
  34. },
  35. 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 },
  36. TIMINGS: { API_POLL_INTERVAL: 5000, MAX_POLL_ATTEMPTS: 60, MAX_RETRY_ATTEMPTS: 60, PROMPT_COMMIT_DELAY_MS: 10000 },
  37. STORAGE: {
  38. CONFIG_KEY: 'xai_overkill_ui_config',
  39. CURRENT_PANEL_SESSION_KEY: 'xai_overkill_ui_current_panel_session',
  40. EXPORT_SCHEMA_VERSION: 1,
  41. DB_NAME: 'XAI_OVERKILL_UI',
  42. DB_VERSION: 2,
  43. STORE_META: 'history_meta',
  44. STORE_DATA: 'app_state',
  45. OPFS_DIR: 'xai_overkill_ui_media'
  46. },
  47. ASPECT_RATIOS:[
  48. { 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 },
  49. { label: '3:4', w: 3, h: 4 }, { label: '4:3', w: 4, h: 3 },
  50. { label: '9:16', w: 9, h: 16 }, { label: '16:9', w: 16, h: 9 }, { label: '2:3', w: 2, h: 3 },
  51. { 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 },
  52. { label: '9:20', w: 9, h: 20, imageOnly: true }, { label: '20:9', w: 20, h: 9, imageOnly: true },
  53. { label: '1:2', w: 1, h: 2, imageOnly: true }, { label: '2:1', w: 2, h: 1, imageOnly: true }
  54. ],
  55. PLACEHOLDER_SWATCHES: [
  56. { spot: '#223140', mid: '#17222D', bg: '#070809' },
  57. { spot: '#2A2D43', mid: '#1D2030', bg: '#070809' },
  58. { spot: '#263247', mid: '#1B2433', bg: '#070809' },
  59. { spot: '#2D2833', mid: '#201B25', bg: '#070809' },
  60. { spot: '#1F2F2A', mid: '#16221E', bg: '#070809' },
  61. { spot: '#2F2926', mid: '#221D1A', bg: '#070809' },
  62. { spot: '#242A31', mid: '#191E24', bg: '#070809' },
  63. { spot: '#1C3135', mid: '#142328', bg: '#070809' },
  64. { spot: '#24302C', mid: '#19221F', bg: '#070809' },
  65. { spot: '#243544', mid: '#192734', bg: '#070809' }
  66. ],
  67. ICONS: {
  68. refresh: '↻', settings: '⚙', star: '★', play: '▶', edit: '✄', extend: '◷', video: '🎞\uFE0E', close: '✕', check: '✔', editPrompt: '✎', imageRef: '🖼️', collapseLeft: '\\00AB', collapseRight: '\\00BB',
  69. 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>'
  70. }
  71. };
  72.  
  73. ['ImageDecoder', 'VideoFrame', 'ImageBitmap'].forEach(api => {
  74. if (typeof globalThis[api] !== 'undefined' && !globalThis[api].prototype[Symbol.dispose]) {
  75. globalThis[api].prototype[Symbol.dispose] = function() { this.close(); };
  76. }
  77. });
  78.  
  79. class DisposableURL {
  80. constructor(blob) { this.url = URL.createObjectURL(blob); }
  81. [Symbol.dispose]() { URL.revokeObjectURL(this.url); }
  82. }
  83. const NoOpDisposable = { [Symbol.dispose]() {} };
  84.  
  85. class ReactiveVar {
  86. constructor(initVal) {
  87. this.v = initVal;
  88. this.subs = new Set();
  89. }
  90. get value() { return this.v; }
  91. set value(newVal) {
  92. if (this.v === newVal) return;
  93. this.v = newVal;
  94. for (const cb of this.subs) cb(newVal);
  95. }
  96. bind(callback, signal = null) {
  97. this.subs.add(callback);
  98. callback(this.v);
  99. if (signal) {
  100. if (signal.aborted) return this.subs.delete(callback);
  101. signal.addEventListener('abort', () => this.subs.delete(callback), { once: true });
  102. }
  103. }
  104. }
  105.  
  106. function createModernTemplate(htmlString) {
  107. const tpl = document.createElement('template');
  108. tpl.innerHTML = htmlString;
  109.  
  110. const bindings = [];
  111. const getDOMPath = (targetNode, root) => {
  112. const path = [];
  113. let curr = targetNode;
  114. while (curr !== root) {
  115. let index = 0, sibling = curr.previousSibling;
  116. while (sibling) { index++; sibling = sibling.previousSibling; }
  117. path.push(index);
  118. curr = curr.parentNode;
  119. }
  120. return path.reverse();
  121. };
  122.  
  123. const walker = document.createTreeWalker(tpl.content, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT);
  124. let node;
  125. while ((node = walker.nextNode())) {
  126. if (node.nodeType === 3) {
  127. const match = node.data.match(/^\{\{\s*(\w+)\s*\}\}$/);
  128. if (match) {
  129. node.data = '';
  130. bindings.push({ key: match[1], path: getDOMPath(node, tpl.content), isAttr: false });
  131. }
  132. } else if (node.nodeType === 1) {
  133. for (const attr of node.attributes) {
  134. const match = attr.value.match(/^(.*)\{\{\s*(\w+)\s*\}\}(.*)$/);
  135. if (match) {
  136. const [, prefix, key, suffix] = match;
  137. bindings.push({ key, attrName: attr.name, path: getDOMPath(node, tpl.content), isAttr: true, prefix, suffix });
  138. attr.value = prefix + suffix;
  139. }
  140. }
  141. }
  142. }
  143.  
  144. return {
  145. createInstance(initialVals = {}) {
  146. const clone = tpl.content.cloneNode(true);
  147. const partNodes = [];
  148.  
  149. for (const b of bindings) {
  150. let target = clone;
  151. for (const idx of b.path) target = target.childNodes[idx];
  152. partNodes.push({ binding: b, target });
  153. }
  154.  
  155. const instance = {
  156. content: clone,
  157. update(vals) {
  158. for (const { binding, target } of partNodes) {
  159. if (binding.key in vals) {
  160. if (binding.isAttr) target.setAttribute(binding.attrName, binding.prefix + (vals[binding.key] ?? '') + binding.suffix);
  161. else target.data = vals[binding.key] ?? '';
  162. }
  163. }
  164. }
  165. };
  166. instance.update(initialVals);
  167. return instance;
  168. }
  169. };
  170. }
  171.  
  172. const LANG_DISPLAY_NAMES = new Intl.DisplayNames(['en'], { type: 'language' });
  173. const ASPECT_RATIO_MAP = new Map(APP_CONFIG.ASPECT_RATIOS.map(o => [o.label.trim(), o]));
  174. const TTS_QUALITY_MAP = new Map(APP_CONFIG.TTS_QUALITIES.map(q => [q.val, q]));
  175. const ASPECT_RATIO_POPOVER_LAYOUT = [
  176. ['Auto', 'Ref'],
  177. ['1:1', '1:1'],
  178. ['3:4', '4:3'],
  179. ['9:16', '16:9'],
  180. ['2:3', '3:2'],
  181. ['9:19.5', '19.5:9'],
  182. ['9:20', '20:9'],
  183. ['1:2', '2:1']
  184. ];
  185.  
  186. const createRangeOptions = (min, max) => Array.from({ length: Math.max(0, max - min + 1) }, (_, idx) => {
  187. const value = String(min + idx);
  188. return { val: value, text: value };
  189. });
  190.  
  191. const getDefaultCardAction = ({ itemType, mode = 'edit' } = {}) => {
  192. if (mode === 'extend') return 'extend_video';
  193. if (mode === 'video') return 'gen_video';
  194. return itemType === 'video' ? 'edit_video' : 'gen_image';
  195. };
  196. const BaseActionSpec = {
  197. maxRefs: () => APP_CONFIG.LIMITS.MAX_REFS_UI,
  198. uploadTransition: ({ mimeType = '', currentAction }) => mimeType.startsWith('video/') ? 'edit_video' : currentAction,
  199. cardAction: getDefaultCardAction
  200. };
  201. const ACTION_SPEC = {
  202. gen_image: {
  203. ...BaseActionSpec,
  204. label: '▨ Image',
  205. title: 'Image Generation',
  206. model: 'grok-imagine-image',
  207. mediaType: 'image',
  208. refKind: 'image',
  209. durationMax: 15,
  210. activeUI:['currentAr', 'resImg', 'batchSize', 'loops'],
  211. endpoint: ({ refCount = 0 }) => refCount ? '/v1/images/edits' : '/v1/images/generations',
  212. apiLimit: APP_CONFIG.LIMITS.API_MAX_MULTI_IMG,
  213. warningPolicy: ({ refs =[], refMode = 'multi' }) =>
  214. (refMode === 'multi' && refs.length > APP_CONFIG.LIMITS.API_MAX_MULTI_IMG)
  215. ?[`API Limit: Only the first ${APP_CONFIG.LIMITS.API_MAX_MULTI_IMG} references will be used for image generation.`]
  216. :[],
  217. buildPayload: ({ payload, currentRefs, refBase64s, aspectRatio, settings }) => buildMediaPayload('image', payload, currentRefs, refBase64s, aspectRatio, settings)
  218. },
  219. gen_video: {
  220. ...BaseActionSpec,
  221. label: '🎞\uFE0E Video',
  222. title: 'Video Generation',
  223. model: 'grok-imagine-video',
  224. mediaType: 'video',
  225. refKind: 'image',
  226. durationMax: 15,
  227. activeUI:['currentAr', 'resVid', 'duration', 'loops'],
  228. endpoint: () => '/v1/videos/generations',
  229. apiLimit: APP_CONFIG.LIMITS.API_MAX_MULTI_VID,
  230. warningPolicy: ({ refs =[], duration = 5, aspectRatio = 'Auto', refMode = 'multi' }) => {
  231. const warnings =[];
  232. if (refs.length > 0 && duration > 10) {
  233. if (aspectRatio !== 'Auto' && aspectRatio !== 'Ref') warnings.push('>10s video might stretch refs instead of resizing. Auto AR is recommended.');
  234. if (refs.length > 1 && refMode === 'multi') warnings.push('>10s video uses only the first reference.');
  235. }
  236. if (refMode === 'multi' && refs.length > APP_CONFIG.LIMITS.API_MAX_MULTI_VID && duration <= 10)
  237. warnings.push(`API Limit: Only the first ${APP_CONFIG.LIMITS.API_MAX_MULTI_VID} references will be used for video generation.`);
  238. return warnings;
  239. },
  240. buildPayload: ({ payload, currentRefs, refBase64s, aspectRatio, settings }) => buildMediaPayload('video', payload, currentRefs, refBase64s, aspectRatio, settings)
  241. },
  242. edit_video: {
  243. ...BaseActionSpec,
  244. label: '✄ Edit',
  245. title: 'Edit Video Ref',
  246. model: 'grok-imagine-video',
  247. mediaType: 'video',
  248. refKind: 'video',
  249. durationMax: 15,
  250. activeUI: ['loops'],
  251. endpoint: () => '/v1/videos/edits',
  252. apiLimit: APP_CONFIG.LIMITS.API_MAX_MULTI_EDIT,
  253. uploadTransition: ({ mimeType = '', currentAction }) => mimeType.startsWith('video/') ? currentAction : 'gen_image',
  254. warningPolicy: ({ refs =[], refMode = 'multi' }) =>
  255. (refMode === 'multi' && refs.length > APP_CONFIG.LIMITS.API_MAX_MULTI_EDIT)
  256. ?[`API Limit: Only the first ${APP_CONFIG.LIMITS.API_MAX_MULTI_EDIT} video reference will be used.`]
  257. :[],
  258. buildPayload: ({ action, payload, currentRefs, refBase64s, settings }) => buildVideoEditPayload(action, payload, currentRefs, refBase64s, settings)
  259. },
  260. extend_video: {
  261. ...BaseActionSpec,
  262. label: '◷ Extend',
  263. title: 'Extend Video Ref',
  264. model: 'grok-imagine-video',
  265. mediaType: 'video',
  266. refKind: 'video',
  267. durationMax: 10,
  268. activeUI: ['duration', 'loops'],
  269. endpoint: () => '/v1/videos/extensions',
  270. apiLimit: APP_CONFIG.LIMITS.API_MAX_MULTI_EDIT,
  271. uploadTransition: ({ mimeType = '', currentAction }) => mimeType.startsWith('video/') ? currentAction : 'gen_image',
  272. warningPolicy: ({ refs =[], refMode = 'multi' }) =>
  273. (refMode === 'multi' && refs.length > APP_CONFIG.LIMITS.API_MAX_MULTI_EDIT)
  274. ?[`API Limit: Only the first ${APP_CONFIG.LIMITS.API_MAX_MULTI_EDIT} video reference will be used.`]
  275. :[],
  276. buildPayload: ({ action, payload, currentRefs, refBase64s, settings }) => buildVideoEditPayload(action, payload, currentRefs, refBase64s, settings)
  277. },
  278. gen_audio: {
  279. ...BaseActionSpec,
  280. label: '🔊\uFE0E Audio',
  281. title: 'Text to Speech',
  282. model: 'tts-1',
  283. mediaType: 'audio',
  284. refKind: 'none',
  285. durationMax: 15,
  286. activeUI:['voice', 'language', 'ttsQuality', 'ttsEffects'],
  287. endpoint: () => '/v1/tts',
  288. maxRefs: () => 0,
  289. apiLimit: 0,
  290. warningPolicy: ({ action }) => {
  291. const textLen = getTaskPrompt(action).length;
  292. return textLen > APP_CONFIG.LIMITS.TTS_TEXT * 0.8
  293. ? [`Text is ${textLen}/${APP_CONFIG.LIMITS.TTS_TEXT} chars. Near the limit.`]
  294. : [];
  295. },
  296. buildPayload: ({ payload, settings }) => buildAudioPayload(payload, settings)
  297. },
  298. gen_text: {
  299. ...BaseActionSpec,
  300. label: '💬 Text',
  301. title: 'Text Generation',
  302. model: 'grok-4.20',
  303. mediaType: 'text',
  304. refKind: 'image',
  305. activeUI:['textModel', 'textReasoningEffort', 'textWebSearch', 'textXSearch'],
  306. endpoint: () => '/v1/responses',
  307. apiLimit: APP_CONFIG.LIMITS.API_MAX_MULTI_TEXT,
  308. warningPolicy: ({ refs =[], refMode = 'multi' }) =>
  309. (refMode === 'multi' && refs.length > APP_CONFIG.LIMITS.API_MAX_MULTI_TEXT)
  310. ?[`API Limit: Only the first ${APP_CONFIG.LIMITS.API_MAX_MULTI_TEXT} references will be used for text generation.`]
  311. :[],
  312. buildPayload: ({ payload, currentRefs, refBase64s, settings, textChatMessages = [] }) => {
  313. const model = settings.textModel || 'grok-4.20';
  314. payload.model = model;
  315. if (isMultiAgentTextModel(model)) payload.reasoning = { effort: settings.textReasoningEffort || 'high' };
  316. else delete payload.reasoning;
  317. const tools = buildEnabledTextTools(model, settings);
  318. if (tools.length > 0) payload.tools = tools;
  319. else delete payload.tools;
  320.  
  321. const promptText = payload.prompt || 'Hello';
  322.  
  323. const rawMessages =[];
  324. if (Array.isArray(textChatMessages) && textChatMessages.length > 0) {
  325. textChatMessages.forEach(message => {
  326. if (!message?.text) return;
  327. rawMessages.push({ role: message.role === 'assistant' ? 'assistant' : 'user', contentText: message.text, isCurrent: false });
  328. });
  329. }
  330. rawMessages.push({ role: 'user', contentText: promptText, isCurrent: true });
  331.  
  332. const truncateTextToBytes = (text, maxBytes) => {
  333. const encoded = new TextEncoder().encode(text);
  334. if (encoded.byteLength <= maxBytes) return text;
  335. return new TextDecoder('utf-8', { fatal: false }).decode(encoded.subarray(0, maxBytes)).replace(/\uFFFD/g, '');
  336. };
  337.  
  338. const MAX_TEXT_BYTES = 512000;
  339. let runningBytes = 0;
  340. const finalInput =[];
  341.  
  342. for (let i = rawMessages.length - 1; i >= 0; i--) {
  343. const msg = rawMessages[i];
  344. const msgBytes = new TextEncoder().encode(msg.contentText).length;
  345.  
  346. if (runningBytes + msgBytes <= MAX_TEXT_BYTES) {
  347. runningBytes += msgBytes;
  348. if (msg.isCurrent) {
  349. finalInput.unshift({ role: msg.role, content: buildResponsesUserContent(msg.contentText, refBase64s) });
  350. } else {
  351. finalInput.unshift({ role: msg.role, content: buildResponsesTextContent(msg.contentText) });
  352. }
  353. } else {
  354. const remainingBytes = MAX_TEXT_BYTES - runningBytes;
  355. if (remainingBytes > 0) {
  356. const truncatedText = truncateTextToBytes(msg.contentText, remainingBytes);
  357. if (msg.isCurrent) {
  358. finalInput.unshift({ role: msg.role, content: buildResponsesUserContent(truncatedText, refBase64s) });
  359. } else {
  360. finalInput.unshift({ role: msg.role, content: buildResponsesTextContent(truncatedText) });
  361. }
  362. }
  363. break;
  364. }
  365. }
  366.  
  367. payload.stream = true;
  368. payload.store = false;
  369. payload.input = finalInput;
  370. delete payload.prompt;
  371. }
  372. }
  373. };
  374. const PANEL_ACTION_KEYS = Object.keys(ACTION_SPEC);
  375. const PANEL_STORE_SCHEMA_VERSION = 2;
  376. const DEFAULT_PROMPTS = Object.fromEntries(PANEL_ACTION_KEYS.map(k => [k, '']));
  377. const DEFAULT_FAV_PROMPTS = Object.fromEntries(PANEL_ACTION_KEYS.map(k => [k, []]));
  378.  
  379. const ALLOWED_VIDEO_ARS = new Set(['1:1', '16:9', '9:16', '4:3', '3:4', '3:2', '2:3']);
  380. const CONFIG_SCHEMA = {
  381. action: { type: 'enum', default: 'gen_image', options: () => Object.keys(ACTION_SPEC) },
  382. currentAr: { type: 'enum', default: 'Auto', options: () => APP_CONFIG.ASPECT_RATIOS.map(({ label }) => label.trim()), normalize: value => typeof value === 'string' ? value.trim() : value },
  383. batchSize: { type: 'intString', default: '2', min: 1, max: 10 },
  384. duration: { type: 'intString', default: '5', min: 1, max: ({ out, fallback }) => (ACTION_SPEC[out.action || fallback?.action || 'gen_image'] ?? ACTION_SPEC.gen_image).durationMax || 15 },
  385. loops: { type: 'intString', default: '0', min: 0, max: 99 },
  386. loopsGenImage: { type: 'intString', default: ({ source }) => source.loops ?? '0', min: 0, max: 99 },
  387. loopsGenVideo: { type: 'intString', default: ({ source }) => source.loops ?? '0', min: 0, max: 99 },
  388. loopsEditVideo: { type: 'intString', default: ({ source }) => source.loops ?? '0', min: 0, max: 99 },
  389. loopsExtendVideo: { type: 'intString', default: ({ source }) => source.loops ?? '0', min: 0, max: 99 },
  390. resImg: { type: 'enum', default: '2k', options: ['1k', '2k'] },
  391. resVid: { type: 'enum', default: '720p', options: ['480p', '720p'] },
  392. refMode: { type: 'enum', default: 'multi', options: ['multi', 'queue'] },
  393. concurrentSessions: { type: 'int', default: 1, min: 1, max: APP_CONFIG.LIMITS.MAX_CONCURRENT_SESSIONS },
  394. rateLimitDelay: { type: 'int', default: 60, min: 0, max: 300 },
  395. retryMinMs: { type: 'int', default: 2000, min: 500, max: 60000 },
  396. retryMaxMs: { type: 'int', default: 4000, min: ({ out, fallback }) => out.retryMinMs || fallback?.retryMinMs || 500, max: 60000 },
  397. autoRetryVideo: { type: 'bool', default: true },
  398. referenceARTrick: { type: 'bool', default: false },
  399. ecoMode: { type: 'bool', default: false },
  400. autoDownload: { type: 'bool', default: false },
  401. embedImagePrompt: { type: 'bool', default: ({ source }) => source.embedImagePrompt ?? source.jpegPromptXmp ?? true },
  402. optimize2k: { type: 'bool', default: false },
  403. leftCollapsed: { type: 'bool', default: false },
  404. rightCollapsed: { type: 'bool', default: false },
  405. leftWidth: { type: 'int', default: 260, min: 260, max: 2000 },
  406. rightWidth: { type: 'int', default: 320, min: 240, max: 2000 },
  407. imgBaseMs: { type: 'int', default: 3000, min: 1500, max: 10000 },
  408. voice: { type: 'enum', default: 'eve', options: () => APP_CONFIG.VOICES },
  409. language: { type: 'enum', default: 'auto', options: () => APP_CONFIG.LANG_CODES },
  410. ttsQuality: { type: 'enum', default: '128000', options: () => APP_CONFIG.TTS_QUALITIES.map(({ val }) => val) },
  411. textModel: { type: 'enum', default: 'grok-4.20', options: () => APP_CONFIG.TEXT_MODELS },
  412. textReasoningEffort: { type: 'enum', default: 'medium', options: ['low', 'medium', 'high', 'xhigh'] },
  413. textWebSearch: { type: 'enum', default: 'on', options: ['off', 'on'] },
  414. textXSearch: { type: 'enum', default: 'on', options: ['off', 'on'] },
  415. customTextModels: { type: 'any', default:[] }
  416. };
  417. const DOM_IDS = {
  418. app: 'xai-pro-app',
  419. prompt: 'xai-api-prompt',
  420. textareaWrapper: 'textarea-wrapper',
  421. promptContainer: 'prompt-container-main',
  422. chatView: 'xai-chat-container',
  423. chatScroll: 'chat-scroll',
  424. actionDropdown: 'btn-master-action',
  425. resImgToggle: 'xai-api-res-img',
  426. resVidToggle: 'xai-api-res-vid',
  427. settingBatch: 'btn-setting-batch',
  428. settingDuration: 'btn-setting-duration',
  429. settingAr: 'btn-setting-ar',
  430. settingLoops: 'setting-loops',
  431. fileInput: 'xai-api-file',
  432. dropzone: 'col-right-dropzone',
  433. uploadPlaceholder: 'xai-upload-placeholder',
  434. refCounter: 'xai-ref-counter',
  435. refList: 'xai-ref-list',
  436. generateBtn: 'xai-api-generate',
  437. saveRefBtn: 'xai-api-save-ref',
  438. refEditLabel: 'ref-edit-label',
  439. settingsBtn: 'xai-settings-btn',
  440. favBtn: 'xai-fav-btn',
  441. settingsPopover: 'popover-settings',
  442. status: 'xai-api-status',
  443. history: 'xai-history',
  444. currentImages: 'xai-current-images',
  445. reset: 'xai-reset-btn',
  446. lightbox: 'xai-lightbox',
  447. lightboxInner: 'xai-lightbox-inner',
  448. lightboxStage: 'xai-lightbox-stage',
  449. lightboxDl: 'xai-lightbox-dl',
  450. refModeToggle: 'xai-ref-mode-toggle',
  451. favDialog: 'xai-fav-dialog',
  452. favList: 'fav-list-container',
  453. rateDelayInput: 'setting-rate-delay',
  454. sessionsInput: 'setting-sessions',
  455. delayMinInput: 'setting-delay-min',
  456. delayMaxInput: 'setting-delay-max',
  457. autoRetryVideoCheckbox: 'setting-auto-retry-video',
  458. arTrickCheckbox: 'setting-ar-trick',
  459. colLeft: 'col-left',
  460. colRight: 'col-right-dropzone',
  461. autoDownloadCheckbox: 'setting-auto-download',
  462. embedImagePromptCheckbox: 'setting-embed-image-prompt',
  463. optimize2kCheckbox: 'setting-optimize-2k',
  464. labelMasterAction: 'label-master-action',
  465. labelSettingDuration: 'label-setting-duration',
  466. popoverDuration: 'popover-duration',
  467. popoverAr: 'popover-ar',
  468. metricSaved: 'metric-saved',
  469. metricDisk: 'metric-disk',
  470. metricLoad: 'metric-load',
  471. confirmDialog: 'xai-confirm-dialog',
  472. confirmMsg: 'xai-confirm-msg',
  473. labelSettingBatch: 'label-setting-batch',
  474. ecoModeCheckbox: 'setting-eco-mode',
  475. promptUpdateLabel: 'prompt-update-label',
  476. settingVoice: 'btn-setting-voice',
  477. popoverVoice: 'popover-voice',
  478. labelSettingVoice: 'label-setting-voice',
  479. settingLanguage: 'btn-setting-language',
  480. popoverLanguage: 'popover-language',
  481. labelSettingLanguage: 'label-setting-language',
  482. settingTtsQuality: 'btn-setting-tts-quality',
  483. popoverTtsQuality: 'popover-tts-quality',
  484. labelSettingTtsQuality: 'label-setting-tts-quality',
  485. btnTtsEffects: 'btn-tts-effects',
  486. popoverTtsEffects: 'popover-tts-effects',
  487. settingTextModel: 'btn-setting-text-model',
  488. popoverTextModel: 'popover-text-model',
  489. labelSettingTextModel: 'label-setting-text-model',
  490. textWebSearchToggle: 'btn-setting-text-web-search',
  491. textXSearchToggle: 'btn-setting-text-x-search',
  492. settingReasoning: 'btn-setting-reasoning',
  493. popoverReasoning: 'popover-reasoning',
  494. labelSettingReasoning: 'label-setting-reasoning'
  495. };
  496. const SETTINGS_CONTROL_SCHEMA = [
  497. { 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' },
  498. { 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)' },
  499. { 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' },
  500. { 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' },
  501. { 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' },
  502. { 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 },
  503. { 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 },
  504. { 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 },
  505. { 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 },
  506. { 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' }
  507. ];
  508. const SETTINGS_POPOVER_LAYOUT = [
  509. ['autoDownload', 'embedImagePrompt', 'optimize2k', 'autoRetryVideo', 'referenceARTrick'],
  510. ['concurrentSessions', 'rateLimitDelay'],
  511. ['retryMinMs', 'retryMaxMs'],
  512. ['ecoMode'],
  513. ['__import_export__']
  514. ];
  515. const UI_CONTROL_SCHEMA = [
  516. {
  517. id: 'action',
  518. kind: 'dropdown',
  519. domKey: 'actionDropdown',
  520. buttonId: 'btn-master-action',
  521. popoverId: 'popover-action',
  522. labelId: 'label-master-action',
  523. stateKey: 'action',
  524. title: 'Select Generation Action',
  525. defaultValue: 'gen_image',
  526. defaultLabel: '▨ Image',
  527. options: () => Object.entries(ACTION_SPEC).map(([val, meta]) => ({ val, text: meta.label, title: meta.title }))
  528. },
  529. {
  530. id: 'currentAr',
  531. kind: 'dropdown',
  532. domKey: 'settingAr',
  533. buttonId: 'btn-setting-ar',
  534. popoverId: 'popover-ar',
  535. stateKey: 'currentAr',
  536. title: 'Select Aspect Ratio',
  537. defaultValue: 'Auto',
  538. render: 'aspect-ratio'
  539. },
  540. {
  541. id: 'voice',
  542. kind: 'dropdown',
  543. domKey: 'settingVoice',
  544. buttonId: 'btn-setting-voice',
  545. popoverId: 'popover-voice',
  546. labelId: 'label-setting-voice',
  547. stateKey: 'voice',
  548. title: 'Select Voice',
  549. defaultValue: 'eve',
  550. defaultLabel: 'Eve',
  551. options: () => APP_CONFIG.VOICES.map(val => ({
  552. val,
  553. text: val.charAt(0).toUpperCase() + val.slice(1)
  554. }))
  555. },
  556. {
  557. id: 'language',
  558. kind: 'dropdown',
  559. domKey: 'settingLanguage',
  560. buttonId: 'btn-setting-language',
  561. popoverId: 'popover-language',
  562. labelId: 'label-setting-language',
  563. stateKey: 'language',
  564. title: 'Select Language',
  565. defaultValue: 'auto',
  566. defaultLabel: 'Auto',
  567. options: () => APP_CONFIG.LANG_CODES.map(val => ({
  568. val,
  569. text: val === 'auto' ? 'Auto' : LANG_DISPLAY_NAMES.of(val)
  570. }))
  571. },
  572. {
  573. id: 'ttsQuality',
  574. kind: 'dropdown',
  575. domKey: 'settingTtsQuality',
  576. buttonId: 'btn-setting-tts-quality',
  577. popoverId: 'popover-tts-quality',
  578. labelId: 'label-setting-tts-quality',
  579. stateKey: 'ttsQuality',
  580. title: 'Audio Quality',
  581. defaultValue: '128000',
  582. defaultLabel: '128k',
  583. options: () => APP_CONFIG.TTS_QUALITIES.map(({ val, label }) => ({ val, text: label }))
  584. },
  585. {
  586. id: 'ttsEffects',
  587. kind: 'button',
  588. domKey: 'btnTtsEffects',
  589. buttonId: 'btn-tts-effects',
  590. title: 'Speech Effects & Tags',
  591. text: 'Effects'
  592. },
  593. {
  594. 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',
  595. options: () => {
  596. const models = APP_CONFIG.TEXT_MODELS.map(val => ({ val, text: val }));
  597. if (STATE.textModel && !APP_CONFIG.TEXT_MODELS.includes(STATE.textModel)) {
  598. models.push({ val: STATE.textModel, text: STATE.textModel });
  599. }
  600. return models;
  601. }
  602. },
  603. {
  604. id: 'textReasoningEffort',
  605. kind: 'toggle',
  606. domKey: 'settingReasoning',
  607. buttonId: 'btn-setting-reasoning',
  608. stateKey: 'textReasoningEffort',
  609. title: 'Reasoning effort',
  610. defaultValue: 'high',
  611. values: ['low', 'medium', 'high', 'xhigh'],
  612. labels: { 'low': 'Low', 'medium': 'Medium', 'high': 'High', 'xhigh': 'xHigh' },
  613. isVisible: () => isMultiAgentTextModel()
  614. },
  615. {
  616. id: 'textWebSearch',
  617. kind: 'toggle',
  618. domKey: 'textWebSearchToggle',
  619. buttonId: 'btn-setting-text-web-search',
  620. stateKey: 'textWebSearch',
  621. title: 'Built-in Web Search tool',
  622. defaultValue: 'on',
  623. values: ['off', 'on'],
  624. labels: { off: 'Web Off', on: 'Web On' },
  625. isVisible: () => canUseBuiltInTextTools()
  626. },
  627. {
  628. id: 'textXSearch',
  629. kind: 'toggle',
  630. domKey: 'textXSearchToggle',
  631. buttonId: 'btn-setting-text-x-search',
  632. stateKey: 'textXSearch',
  633. title: 'Built-in X Search tool',
  634. defaultValue: 'on',
  635. values: ['off', 'on'],
  636. labels: { off: 'X Off', on: 'X On' },
  637. isVisible: () => canUseBuiltInTextTools()
  638. },
  639. {
  640. id: 'resImg',
  641. kind: 'toggle',
  642. domKey: 'resImgToggle',
  643. buttonId: 'xai-api-res-img',
  644. stateKey: 'resImg',
  645. title: 'Toggle Image Resolution: 1K (JPEG), 2K (PNG)',
  646. defaultValue: '2k',
  647. values: ['2k', '1k'],
  648. labels: { '2k': '2K', '1k': '1K' }
  649. },
  650. {
  651. id: 'resVid',
  652. kind: 'toggle',
  653. domKey: 'resVidToggle',
  654. buttonId: 'xai-api-res-vid',
  655. stateKey: 'resVid',
  656. title: 'Toggle Video Resolution',
  657. defaultValue: '720p',
  658. values: ['720p', '480p'],
  659. labels: { '720p': '720p', '480p': '480p' }
  660. },
  661. {
  662. id: 'batchSize',
  663. kind: 'dropdown',
  664. domKey: 'settingBatch',
  665. buttonId: 'btn-setting-batch',
  666. popoverId: 'popover-batch',
  667. labelId: 'label-setting-batch',
  668. stateKey: 'batchSize',
  669. title: 'Images per batch',
  670. defaultValue: '2',
  671. defaultLabel: '2',
  672. options: () => createRangeOptions(1, 10)
  673. },
  674. {
  675. id: 'duration',
  676. kind: 'dropdown',
  677. domKey: 'settingDuration',
  678. buttonId: 'btn-setting-duration',
  679. popoverId: 'popover-duration',
  680. labelId: 'label-setting-duration',
  681. stateKey: 'duration',
  682. title: 'Video duration (seconds)',
  683. defaultValue: '5',
  684. defaultLabel: '5',
  685. options: () => []
  686. },
  687. {
  688. id: 'loops',
  689. kind: 'number-inline',
  690. domKey: 'settingLoops',
  691. wrapperId: 'btn-setting-loops',
  692. inputId: 'setting-loops',
  693. stateKey: 'loops',
  694. title: 'Auto repeat loops',
  695. defaultValue: '0',
  696. min: 0,
  697. max: 99,
  698. prefix: '↻'
  699. }
  700. ];
  701. const CONTROL_SCHEMA = [
  702. ...SETTINGS_CONTROL_SCHEMA.map(control => ({ scope: 'settings', ...control })),
  703. ...UI_CONTROL_SCHEMA.map(control => ({ scope: 'prompt', ...control }))
  704. ];
  705. const filterControls = (scope, kind = '') => CONTROL_SCHEMA.filter(control => control.scope === scope && (!kind || control.kind === kind));
  706. const CONTROL_BY_STATE_KEY = Object.fromEntries(CONTROL_SCHEMA.filter(({ stateKey }) => !!stateKey).map(control => [control.stateKey, control]));
  707. const getControlByStateKey = (stateKey, scope = '') => {
  708. const control = CONTROL_BY_STATE_KEY[String(stateKey || '')];
  709. return control && (!scope || control.scope === scope) ? control : null;
  710. };
  711. const SETTINGS_CONTROL_BY_ID = Object.fromEntries(filterControls('settings').map(control => [control.id, control]));
  712. const SETTINGS_TOGGLE_META = filterControls('settings', 'toggle');
  713. const SETTINGS_NUMBER_META = filterControls('settings', 'number');
  714. const UI_CONTROL_BY_ID = Object.fromEntries(filterControls('prompt').map(control => [control.id, control]));
  715. const UI_DROPDOWN_CONTROLS = filterControls('prompt', 'dropdown');
  716. const UI_TOGGLE_CONTROLS = filterControls('prompt', 'toggle');
  717.  
  718. const HTTP_STATUS_CLASS = Object.fromEntries([
  719. ...[500, 502, 503].map(s => [s, 'retry']),
  720. ...[401, 403].map(s => [s, 'auth']),
  721. ...[422, 429].map(s => [s, 'rateLimit'])
  722. ]);
  723. const classifyHttpStatus = (status) => HTTP_STATUS_CLASS[status] ?? 'unknown';
  724. const VIDEO_SUCCESS_STATES = new Set(['done', 'completed']);
  725. const VIDEO_FLAGGED_STATES = new Set(['failed', 'expired', 'rejected', 'blocked', 'moderated', 'nsfw']);
  726. const CURRENT_PANEL_STATUS_STYLE = "white-space:normal; font-size:13px; text-align:center; padding:8px;";
  727. const STUCK_VIDEO_RETRY_TOKENS = ['timeout: video', 'generation halted.', 'video rejected'];
  728. const REQUEST_MODERATION_RE = /moderation|blocked|nsfw|is_sensitive|sensitive|policy/i;
  729. const REQUEST_INVALID_RE = /invalid argument|invalid request|unsupported|text length|voice_id|language|codec|sample_rate|output_format/i;
  730. const addLog = (lvl, msg, data = null) => {
  731. try {
  732. const normalizedLevel = String(lvl || '').toUpperCase();
  733. const consoleMethod = normalizedLevel.includes('ERROR') ? 'error' : normalizedLevel.includes('WARN') ? 'warn' : 'log';
  734. console[consoleMethod](`[xAI][${lvl}] ${msg}`, data);
  735. } catch(e) { console.error('[xAI][LOGGER_PANIC]', e); }
  736. };
  737. const MEDIA_VOLUME_STORAGE_KEY = 'xai_overkill_ui_media_volume';
  738. const _range = document.createRange();
  739. const _fragment = _range.createContextualFragment.bind(_range);
  740. const createMarkupFragment = (markup = '') => _fragment(markup);
  741.  
  742. const DATA_WORKER_CODE = `
  743. 'use strict';
  744. ['ImageDecoder', 'VideoFrame', 'ImageBitmap'].forEach(api => {
  745. if (typeof self[api] !== 'undefined' && !self[api].prototype[Symbol.dispose]) {
  746. self[api].prototype[Symbol.dispose] = function() { this.close(); };
  747. }
  748. });
  749.  
  750. const UTF8_ENCODER = new TextEncoder();
  751. const UTF8_DECODER = new TextDecoder();
  752.  
  753. const DB_NAME = 'XAI_OVERKILL_UI';
  754. const DB_VERSION = 2;
  755. const STORE_META = 'history_meta';
  756. const STORE_DATA = 'app_state';
  757.  
  758. const ACTIVE_ABORTS = new Map();
  759.  
  760. const DB = {
  761. db: null,
  762. async init() {
  763. if (this.db) return this.db;
  764. return new Promise((resolve, reject) => {
  765. const req = indexedDB.open(DB_NAME, DB_VERSION);
  766. req.onupgradeneeded = e => {
  767. const db = e.target.result;
  768. if (!db.objectStoreNames.contains(STORE_META)) db.createObjectStore(STORE_META, { keyPath: 'id' });
  769. if (!db.objectStoreNames.contains(STORE_DATA)) db.createObjectStore(STORE_DATA);
  770. };
  771. req.onsuccess = () => { this.db = req.result; resolve(this.db); };
  772. req.onerror = () => reject(req.error);
  773. });
  774. },
  775. async dbOp(stores, mode, operationFn) {
  776. const db = await this.init();
  777. return new Promise((resolve, reject) => {
  778. const tx = db.transaction(stores, mode);
  779. let result;
  780. tx.oncomplete = () => resolve(result);
  781. tx.onerror = () => reject(tx.error);
  782. tx.onabort = () => reject(tx.error || new Error('IDB Transaction aborted'));
  783. try {
  784. result = operationFn(tx);
  785. } catch (err) {
  786. tx.abort();
  787. reject(err);
  788. }
  789. });
  790. }
  791. };
  792.  
  793. const OPFS = {
  794. dir: null,
  795. async init() {
  796. if (!this.dir) {
  797. const root = await navigator.storage.getDirectory();
  798. this.dir = await root.getDirectoryHandle('xai_overkill_ui_media', { create: true });
  799. }
  800. return this.dir;
  801. },
  802. async delete(id) {
  803. try {
  804. const dir = await this.init();
  805. await Promise.allSettled(['full', 'thumb', 'heavy'].map(type => dir.removeEntry(\`\${id}_\${type}\`).catch(()=>{})));
  806. } catch(e) {}
  807. }
  808. };
  809.  
  810. const TaskQueue = {
  811. queue: Promise.resolve(),
  812. enqueue(fn) {
  813. this.queue = this.queue.then(fn).catch(err => console.error('[Worker Error]', err));
  814. return this.queue;
  815. }
  816. };
  817.  
  818. const XMP = {
  819. 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">'),
  820. POST: UTF8_ENCODER.encode('</rdf:li></rdf:Alt></dc:description></rdf:Description></rdf:RDF></x:xmpmeta>'),
  821. SIG: UTF8_ENCODER.encode('http://ns.adobe.com/xap/1.0/\\0'),
  822. ESC: { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&apos;' }
  823. };
  824.  
  825. const PNG = {
  826. SIG: new Uint8Array([137,80,78,71,13,10,26,10]),
  827. ITXT_TYPE: new Uint8Array([105,84,88,116]),
  828. ITXT_PRE: new Uint8Array([112,97,114,97,109,101,116,101,114,115,0,0,0,0,0]),
  829. IHDR_END: 33,
  830. CRC_TAB: (() => {
  831. const tab = new Uint32Array(256);
  832. for (let i = 0; i < 256; i++) {
  833. let c = i;
  834. for (let k = 0; k < 8; k++) c = c & 1 ? 0xEDB88320 ^ (c >>> 1) : c >>> 1;
  835. tab[i] = c;
  836. }
  837. return tab;
  838. })(),
  839. crc32(buf, crc = 0xFFFFFFFF) {
  840. for (let i = 0, l = buf.length; i < l; i++) {
  841. crc = this.CRC_TAB[(crc ^ buf[i]) & 0xFF] ^ (crc >>> 8);
  842. }
  843. return crc;
  844. }
  845. };
  846.  
  847. const POOL = {
  848. canvas: new OffscreenCanvas(1, 1),
  849. lqipCanvas: new OffscreenCanvas(2, 2)
  850. };
  851. POOL.ctx = POOL.canvas.getContext('2d', { alpha: false, desynchronized: true, willReadFrequently: true });
  852. POOL.lqipCtx = POOL.lqipCanvas.getContext('2d', { alpha: false, willReadFrequently: true });
  853.  
  854. const extractLQIP = (videoFrameOrImage) => {
  855. POOL.lqipCtx.drawImage(videoFrameOrImage, 0, 0, 2, 2);
  856. const d = POOL.lqipCtx.getImageData(0, 0, 2, 2).data;
  857. return \`radial-gradient(ellipse at 30% 30%, rgb(\${d[0]} \${d[1]} \${d[2]}), rgb(\${d[12]} \${d[13]} \${d[14]}))\`;
  858. };
  859.  
  860. const parseBlock = (block) => {
  861. if (!block.trim()) return null;
  862. let eventType = '';
  863. const dataLines = [];
  864. for (const line of block.split(/\\r?\\n/)) {
  865. if (!line || line.startsWith(':')) continue;
  866. if (line.startsWith('event:')) { eventType = line.slice(6).trim(); continue; }
  867. if (line.startsWith('data:')) dataLines.push(line.slice(5).trimStart());
  868. }
  869. if (!dataLines.length) return null;
  870. const raw = dataLines.join('\\n');
  871. if (raw === '[DONE]') return { type: 'done', done: true };
  872. try {
  873. const parsed = JSON.parse(raw);
  874. if (eventType && !parsed.type) parsed.type = eventType;
  875. return parsed;
  876. } catch {
  877. return { type: eventType || 'message', raw };
  878. }
  879. };
  880.  
  881. async function* streamSSE(body, signal) {
  882. const textStream = body.pipeThrough(new TextDecoderStream());
  883. let buffer = '';
  884. try {
  885. for await (const chunk of textStream) {
  886. if (signal?.aborted) throw signal.reason;
  887. buffer += chunk;
  888. const blocks = buffer.split(/\\r?\\n\\r?\\n/);
  889. buffer = blocks.pop() || '';
  890. for (const block of blocks) {
  891. const event = parseBlock(block);
  892. if (event) yield event;
  893. }
  894. }
  895. if (buffer.trim()) {
  896. const event = parseBlock(buffer);
  897. if (event && !event.done) yield event;
  898. }
  899. } finally {
  900. textStream.cancel?.().catch?.(() => {});
  901. }
  902. }
  903.  
  904. self.onmessage = (e) => {
  905. const { type, id, payload, reqId } = e.data;
  906.  
  907. TaskQueue.enqueue(async () => {
  908. try {
  909. switch (type) {
  910. case 'ABORT_REQ': {
  911. const controller = ACTIVE_ABORTS.get(reqId);
  912. if (controller) {
  913. controller.abort(new DOMException('Aborted by Main Thread', 'AbortError'));
  914. ACTIVE_ABORTS.delete(reqId);
  915. }
  916. break;
  917. }
  918. case 'COMMIT_ITEMS': {
  919. const dir = await OPFS.init();
  920. const lightItems = [];
  921. await Promise.all(payload.items.map(async i => {
  922. const meta_light = {
  923. id: String(i.id),
  924. type: i.type || 'image',
  925. ratio: Number.isFinite(i.ratio) ? i.ratio : 1,
  926. lqip: i.lqip || null,
  927. timestamp: i.timestamp || Date.now(),
  928. isPending: !!i.isPending,
  929. filename: i.filename || '',
  930. isUploadedRef: !!i.isUploadedRef,
  931. shortPrompt: i.shortPrompt || '',
  932. mimeType: i.mimeType || '',
  933. downloaded: !!i.downloaded,
  934. originalUrl: i.originalUrl || null,
  935. textModel: i.textModel || null,
  936. textTurnCount: Number.isFinite(i.textTurnCount) ? i.textTurnCount : 0,
  937. reqId: i.reqId || null,
  938. taskId: i.taskId || null,
  939. refSourceId: i.refSourceId || null,
  940. _statusData: i._statusData || null
  941. };
  942. lightItems.push(meta_light);
  943. if (i.prompt || i.responseText || i.textChatMessages) {
  944. const heavyData = { id: String(i.id) };
  945. if (i.prompt) heavyData.prompt = i.prompt;
  946. if (i.responseText) heavyData.responseText = i.responseText;
  947. if (i.textChatMessages) heavyData.textChatMessages = i.textChatMessages;
  948. try {
  949. const fh = await dir.getFileHandle(\`\${i.id}_heavy\`, { create: true });
  950. const writable = await fh.createWritable({ keepExistingData: false });
  951. try {
  952. const jsonStream = new Response(JSON.stringify(heavyData)).body;
  953. await jsonStream.pipeThrough(new CompressionStream('deflate')).pipeTo(writable);
  954. } finally {
  955. await writable.close().catch(()=>{});
  956. }
  957. } catch (err) {
  958. console.error('[OPFS Heavy Write Error]', err);
  959. }
  960. }
  961. }));
  962. await DB.dbOp([STORE_META], 'readwrite', tx => {
  963. const mStore = tx.objectStore(STORE_META);
  964. lightItems.forEach(i => mStore.put(i));
  965. });
  966. self.postMessage({ reqId, status: 'ok' });
  967. break;
  968. }
  969. case 'DELETE_ITEMS': {
  970. const ids = payload.ids;
  971. await DB.dbOp([STORE_META, STORE_DATA], 'readwrite', tx => {
  972. const mStore = tx.objectStore(STORE_META);
  973. const dStore = tx.objectStore(STORE_DATA);
  974. ids.forEach(itemId => {
  975. mStore.delete(itemId);
  976. dStore.delete(itemId);
  977. dStore.delete(\`textSession:\${itemId}\`);
  978. });
  979. });
  980. for (const itemId of ids) await OPFS.delete(itemId);
  981. self.postMessage({ reqId, status: 'ok' });
  982. break;
  983. }
  984. case 'SAVE_META': {
  985. const jsonStream = new Response(JSON.stringify(payload.data)).body;
  986. const compressedStream = jsonStream.pipeThrough(new CompressionStream('deflate'));
  987. const compressedBlob = await new Response(compressedStream).blob();
  988. await DB.dbOp([STORE_DATA], 'readwrite', tx => {
  989. tx.objectStore(STORE_DATA).put(compressedBlob, payload.key);
  990. });
  991. self.postMessage({ reqId, status: 'ok' });
  992. break;
  993. }
  994. case 'LOAD_META': {
  995. const db = await DB.init();
  996. const req = db.transaction(STORE_DATA).objectStore(STORE_DATA).get(payload.key);
  997. req.onsuccess = async () => {
  998. let res = req.result ?? null;
  999. if (res instanceof Blob || res instanceof Uint8Array || res instanceof ArrayBuffer) {
  1000. try {
  1001. const stream = (res instanceof Blob ? res : new Blob([res])).stream();
  1002. const decompressed = stream.pipeThrough(new DecompressionStream('deflate'));
  1003. res = await new Response(decompressed).json();
  1004. } catch(e) {
  1005. res = null;
  1006. }
  1007. }
  1008. self.postMessage({ reqId, payload: res });
  1009. };
  1010. req.onerror = () => self.postMessage({ reqId, payload: null });
  1011. break;
  1012. }
  1013. case 'LOAD_ALL_HISTORY': {
  1014. const db = await DB.init();
  1015. const req = db.transaction(STORE_META).objectStore(STORE_META).getAll();
  1016. req.onsuccess = async () => {
  1017. const rawMetas = req.result || [];
  1018. const processed = rawMetas.map(m => ({
  1019. ...m,
  1020. prompt: undefined,
  1021. responseText: undefined,
  1022. downloaded: Boolean(m.downloaded),
  1023. lqip: m.lqip || null,
  1024. isUploadedRef: Boolean(m.isUploadedRef),
  1025. textModel: m.textModel || null,
  1026. textTurnCount: Number.isFinite(m.textTurnCount) ? m.textTurnCount : 0
  1027. })).sort((a, b) => b.timestamp - a.timestamp);
  1028. self.postMessage({ reqId, payload: processed });
  1029. };
  1030. break;
  1031. }
  1032. case 'LOAD_HEAVY': {
  1033. try {
  1034. const dir = await OPFS.init();
  1035. const fh = await dir.getFileHandle(\`\${payload.id}_heavy\`);
  1036. const file = await fh.getFile();
  1037. const decompressedStream = file.stream().pipeThrough(new DecompressionStream('deflate'));
  1038. const raw = await new Response(decompressedStream).json();
  1039. let textChatMessages = null;
  1040. if (raw.textChatMessages) {
  1041. textChatMessages = typeof raw.textChatMessages === 'string' ? JSON.parse(raw.textChatMessages) : raw.textChatMessages;
  1042. }
  1043. self.postMessage({ reqId, payload: { prompt: String(raw.prompt || ''), responseText: String(raw.responseText || ''), textChatMessages } });
  1044. } catch (err) {
  1045. self.postMessage({ reqId, payload: { prompt: '', responseText: '', textChatMessages: null } });
  1046. }
  1047. break;
  1048. }
  1049. case 'DELETE_META': {
  1050. const keys = Array.isArray(payload.keys) ? payload.keys : [payload.keys];
  1051. await DB.dbOp([STORE_DATA], 'readwrite', tx => {
  1052. const store = tx.objectStore(STORE_DATA);
  1053. keys.forEach(key => store.delete(key));
  1054. });
  1055. self.postMessage({ reqId, status: 'ok' });
  1056. break;
  1057. }
  1058. case 'CLEAR_ALL': {
  1059. const dir = await OPFS.init();
  1060. const root = await navigator.storage.getDirectory();
  1061. await root.removeEntry('xai_overkill_ui_media', { recursive: true }).catch(()=>{});
  1062. OPFS.dir = null;
  1063. await DB.dbOp([STORE_META, STORE_DATA], 'readwrite', tx => {
  1064. tx.objectStore(STORE_META).clear();
  1065. tx.objectStore(STORE_DATA).clear();
  1066. });
  1067. if (DB.db) { DB.db.close(); DB.db = null; }
  1068. self.postMessage({ reqId, status: 'ok' });
  1069. break;
  1070. }
  1071. case 'processApiResponse': {
  1072. const dir = await OPFS.init();
  1073. const { id, buffer, prompt, is2k, optimize2k, embedImagePrompt } = payload;
  1074. let finalBytes = new Uint8Array(buffer);
  1075. let mime = 'image/jpeg';
  1076.  
  1077. if (finalBytes[0] === 0x89 && finalBytes[1] === 0x50 && finalBytes[2] === 0x4E && finalBytes[3] === 0x47) {
  1078. mime = 'image/png';
  1079. }
  1080. if (!is2k) mime = 'image/jpeg';
  1081.  
  1082. const needsConversion = (is2k && optimize2k && mime === 'image/png');
  1083. let decodedBmp = null;
  1084.  
  1085. try {
  1086. if (needsConversion) {
  1087. decodedBmp = await createImageBitmap(new Blob([finalBytes], { type: mime }));
  1088. POOL.canvas.width = decodedBmp.width;
  1089. POOL.canvas.height = decodedBmp.height;
  1090. POOL.ctx.drawImage(decodedBmp, 0, 0);
  1091. const jBlob = await POOL.canvas.convertToBlob({ type: 'image/jpeg', quality: ${APP_CONFIG.MEDIA.JPEG_EXPORT_QUALITY} });
  1092. finalBytes = new Uint8Array(await jBlob.arrayBuffer());
  1093. mime = 'image/jpeg';
  1094. }
  1095.  
  1096. let outputChunks = [finalBytes];
  1097. let xmpEmbedded = false;
  1098. const needsMeta = (embedImagePrompt && !!prompt);
  1099.  
  1100. if (needsMeta) {
  1101. if (mime === 'image/jpeg') {
  1102. let txt = UTF8_ENCODER.encode(prompt.replace(/[&<>"']/g, m => XMP.ESC[m]));
  1103. const MAX_TXT = 65533 - XMP.SIG.length - XMP.PRE.length - XMP.POST.length;
  1104. if (txt.length > MAX_TXT) txt = txt.subarray(0, MAX_TXT);
  1105.  
  1106. const sLen = XMP.SIG.length + XMP.PRE.length + txt.length + XMP.POST.length + 2;
  1107. const app1Header = new Uint8Array([0xFF, 0xE1, (sLen >> 8) & 0xFF, sLen & 0xFF]);
  1108.  
  1109. outputChunks =[
  1110. finalBytes.subarray(0, 2),
  1111. app1Header,
  1112. XMP.SIG,
  1113. XMP.PRE,
  1114. txt,
  1115. XMP.POST,
  1116. finalBytes.subarray(2)
  1117. ];
  1118. xmpEmbedded = true;
  1119. } else if (mime === 'image/png') {
  1120. const textBytes = UTF8_ENCODER.encode(prompt);
  1121. const bodyLen = PNG.ITXT_PRE.length + textBytes.length;
  1122.  
  1123. const chunkHead = new Uint8Array(8);
  1124. chunkHead[0] = bodyLen >>> 24; chunkHead[1] = bodyLen >>> 16 & 255; chunkHead[2] = bodyLen >>> 8 & 255; chunkHead[3] = bodyLen & 255;
  1125. chunkHead.set(PNG.ITXT_TYPE, 4);
  1126.  
  1127. let crc = PNG.crc32(PNG.ITXT_TYPE);
  1128. crc = PNG.crc32(PNG.ITXT_PRE, crc);
  1129. crc = PNG.crc32(textBytes, crc);
  1130. crc = (crc ^ 0xFFFFFFFF) >>> 0;
  1131.  
  1132. const chunkTail = new Uint8Array([crc >>> 24, crc >>> 16 & 255, crc >>> 8 & 255, crc & 255]);
  1133.  
  1134. outputChunks =[
  1135. finalBytes.subarray(0, PNG.IHDR_END),
  1136. chunkHead,
  1137. PNG.ITXT_PRE,
  1138. textBytes,
  1139. chunkTail,
  1140. finalBytes.subarray(PNG.IHDR_END)
  1141. ];
  1142. xmpEmbedded = true;
  1143. }
  1144. }
  1145.  
  1146. const fh = await dir.getFileHandle(id + '_full', { create: true });
  1147. const writable = await fh.createWritable({ keepExistingData: false });
  1148. try { await writable.write(new Blob(outputChunks, { type: mime })); }
  1149. finally { await writable.close().catch(()=>{}); }
  1150.  
  1151. if (!decodedBmp) {
  1152. decodedBmp = await createImageBitmap(new Blob(outputChunks, { type: mime }));
  1153. }
  1154.  
  1155. const ratio = decodedBmp.width / decodedBmp.height;
  1156. const targetW = Math.min(${APP_CONFIG.MEDIA.THUMB_MAX_DIM}, decodedBmp.width);
  1157. const targetH = Math.max(1, Math.round(decodedBmp.height * (targetW / decodedBmp.width)));
  1158.  
  1159. POOL.canvas.width = targetW;
  1160. POOL.canvas.height = targetH;
  1161. POOL.ctx.drawImage(decodedBmp, 0, 0, targetW, targetH);
  1162.  
  1163. const lqip = extractLQIP(decodedBmp);
  1164.  
  1165. const tBlob = await POOL.canvas.convertToBlob({ type: 'image/webp', quality: ${APP_CONFIG.MEDIA.THUMB_WEBP_QUALITY} });
  1166.  
  1167. const tfh = await dir.getFileHandle(id + '_thumb', { create: true });
  1168. const twritable = await tfh.createWritable({ keepExistingData: false });
  1169. try { await twritable.write(tBlob); }
  1170. finally { await twritable.close().catch(()=>{}); }
  1171.  
  1172. const thumbBuffer = await tBlob.arrayBuffer();
  1173.  
  1174. self.postMessage({ reqId, payload: {
  1175. lqip, ratio, mimeType: mime,
  1176. xmpEmbedded,
  1177. thumbBuffer
  1178. }}, [thumbBuffer]);
  1179. } finally {
  1180. if (decodedBmp) {
  1181. decodedBmp.close();
  1182. decodedBmp = null;
  1183. }
  1184. }
  1185. break;
  1186. }
  1187. case 'extractImagePrompt': {
  1188. const { buffer } = payload;
  1189. const bytes = new Uint8Array(buffer);
  1190. let result = null;
  1191.  
  1192. if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4E && bytes[3] === 0x47) {
  1193. let off = 8;
  1194. while (off < bytes.length - 8) {
  1195. const len = (bytes[off] << 24) | (bytes[off+1] << 16) | (bytes[off+2] << 8) | bytes[off+3];
  1196. const type = UTF8_DECODER.decode(bytes.subarray(off+4, off+8));
  1197.  
  1198. if (type === 'iTXt') {
  1199. const chunkData = bytes.subarray(off+8, off+8+len);
  1200. if (chunkData.length >= 15 && UTF8_DECODER.decode(chunkData.subarray(0, 10)) === 'parameters') {
  1201. let textStart = 10;
  1202. for (let nulls = 0; nulls < 5 && textStart < chunkData.length; textStart++) {
  1203. if (chunkData[textStart] === 0) nulls++;
  1204. }
  1205. if (textStart < chunkData.length) {
  1206. result = {
  1207. isPromptCarrier: true,
  1208. prompt: UTF8_DECODER.decode(chunkData.subarray(textStart))
  1209. };
  1210. break;
  1211. }
  1212. }
  1213. }
  1214. if (type === 'IEND') break;
  1215. off += 12 + len;
  1216. }
  1217. } else {
  1218. let off = 2;
  1219. const limit = Math.min(bytes.length - 4, 65536);
  1220. const SIG = XMP.SIG;
  1221.  
  1222. while (off < limit) {
  1223. if (bytes[off] === 0xFF && bytes[off + 1] === 0xE1) {
  1224. const sLen = (bytes[off + 2] << 8) | bytes[off + 3];
  1225. let sigMatch = true;
  1226. for (let i = 0; i < SIG.length && sigMatch; i++) {
  1227. if (bytes[off + 4 + i] !== SIG[i]) sigMatch = false;
  1228. }
  1229. if (sigMatch) {
  1230. const xml = UTF8_DECODER.decode(bytes.subarray(off + 4 + SIG.length, off + 2 + sLen));
  1231. const s = xml.indexOf('default">') + 9;
  1232. const e = xml.indexOf('</rdf:li>');
  1233. if (s > 8 && e > s) {
  1234. result = {
  1235. isPromptCarrier: true,
  1236. prompt: xml.slice(s, e).replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/g, '&')
  1237. };
  1238. }
  1239. }
  1240. if (result) break;
  1241. off += 2 + sLen;
  1242. } else if (bytes[off] === 0xFF) {
  1243. if (bytes[off + 1] === 0xDA) break;
  1244. if (bytes[off + 1] >= 0xE0 && bytes[off + 1] <= 0xEF) {
  1245. off += 2 + ((bytes[off + 2] << 8) | bytes[off + 3]);
  1246. } else break;
  1247. } else break;
  1248. }
  1249. }
  1250. self.postMessage({ reqId, payload: result });
  1251. break;
  1252. }
  1253. case 'LOAD_OPFS_MEDIA': {
  1254. try {
  1255. const dir = await OPFS.init();
  1256. const fh = await dir.getFileHandle(\`\${payload.id}_\${payload.type}\`);
  1257. const file = await fh.getFile();
  1258. self.postMessage({ reqId, payload: { file, mimeType: file.type } });
  1259. } catch (err) {
  1260. self.postMessage({ reqId, payload: null });
  1261. }
  1262. break;
  1263. }
  1264. case 'EXTRACT_VIDEO_POSTER': {
  1265. const { id } = payload;
  1266. try {
  1267. const dir = await OPFS.init();
  1268. const file = await (await dir.getFileHandle(\`\${id}_full\`)).getFile();
  1269.  
  1270. const headerBuf = await file.slice(0, 512 * 1024).arrayBuffer();
  1271. const dv = new DataView(headerBuf);
  1272. const len = headerBuf.byteLength - 8;
  1273.  
  1274. let avcC, codecString, frameSize, frameOffset;
  1275.  
  1276. for (let i = 0; i < len; i++) {
  1277. const box = dv.getUint32(i);
  1278. if (box === 0x61766343) {
  1279. avcC = headerBuf.slice(i + 4, i - 4 + dv.getUint32(i - 4));
  1280. codecString = \`avc1.\${new Uint8Array(avcC).subarray(1, 4).toHex()}\`;
  1281. } else if (box === 0x7374737a) {
  1282. frameSize = dv.getUint32(i + 8) || dv.getUint32(i + 16);
  1283. } else if (box === 0x7374636f) {
  1284. frameOffset = dv.getUint32(i + 12);
  1285. } else if (box === 0x636f3634) {
  1286. frameOffset = Number(dv.getBigUint64(i + 12));
  1287. }
  1288. if (avcC && frameSize && frameOffset) break;
  1289. }
  1290.  
  1291. if (!avcC || !frameSize || !frameOffset) throw new Error("MP4 Demux failed");
  1292.  
  1293. const { promise, resolve, reject } = Promise.withResolvers();
  1294. let decoder = null;
  1295.  
  1296. try {
  1297. decoder = new VideoDecoder({ output: resolve, error: reject });
  1298. decoder.configure({ codec: codecString, description: avcC });
  1299.  
  1300. decoder.decode(new EncodedVideoChunk({
  1301. type: 'key', timestamp: 0,
  1302. data: await file.slice(frameOffset, frameOffset + frameSize).arrayBuffer()
  1303. }));
  1304. await decoder.flush();
  1305.  
  1306. using frame = await promise;
  1307.  
  1308. const targetW = Math.min(${APP_CONFIG.MEDIA.THUMB_MAX_DIM}, frame.displayWidth);
  1309. const targetH = Math.max(1, Math.round(frame.displayHeight * (targetW / frame.displayWidth)));
  1310.  
  1311. POOL.canvas.width = targetW;
  1312. POOL.canvas.height = targetH;
  1313. POOL.ctx.drawImage(frame, 0, 0, targetW, targetH);
  1314.  
  1315. const tBlob = await POOL.canvas.convertToBlob({ type: 'image/webp', quality: ${APP_CONFIG.MEDIA.THUMB_WEBP_QUALITY} });
  1316. const thumbBytes = await tBlob.bytes();
  1317.  
  1318. const lqip = extractLQIP(frame);
  1319.  
  1320. self.postMessage({ reqId, payload: {
  1321. thumbBuffer: thumbBytes.buffer,
  1322. lqip,
  1323. ratio: frame.displayWidth / frame.displayHeight
  1324. }},[thumbBytes.buffer]);
  1325. } finally {
  1326. if (decoder && decoder.state !== 'closed') {
  1327. try { decoder.close(); } catch (e) {}
  1328. }
  1329. }
  1330.  
  1331. } catch (err) {
  1332. self.postMessage({ reqId, payload: { fallback: true, reason: err.message } });
  1333. }
  1334. break;
  1335. }
  1336. case 'PROCESS_STREAM': {
  1337. const dir = await OPFS.init();
  1338. const { stream, meta } = e.data;
  1339. const isBinary = meta.isBinary;
  1340.  
  1341. const abortController = new AbortController();
  1342. ACTIVE_ABORTS.set(reqId, abortController);
  1343. const signal = abortController.signal;
  1344.  
  1345. try {
  1346. if (isBinary) {
  1347. const fh = await dir.getFileHandle(meta.id + '_full', { create: true });
  1348. const writable = await fh.createWritable({ keepExistingData: false });
  1349. try {
  1350. await stream.pipeTo(writable, { signal, preventClose: false, preventAbort: false });
  1351. } finally {
  1352. await writable.close().catch(()=>{});
  1353. }
  1354. self.postMessage({ reqId, payload: { written: true } });
  1355. } else {
  1356. let accumulatedText = '';
  1357. let finalResponse = null;
  1358. for await (const event of streamSSE(stream, signal)) {
  1359. if (event?.type === 'response.output_text.delta' && typeof event.delta === 'string') {
  1360. accumulatedText += event.delta;
  1361. self.postMessage({ type: 'STREAM_DELTA', reqId, delta: event.delta, accumulatedText, isDone: false });
  1362. continue;
  1363. }
  1364. if (event?.type === 'response.output_text.done' && typeof event.text === 'string' && !accumulatedText) {
  1365. accumulatedText = event.text;
  1366. self.postMessage({ type: 'STREAM_DELTA', reqId, delta: event.text, accumulatedText, isDone: false });
  1367. continue;
  1368. }
  1369. if ((event?.type === 'response.completed' || event?.type === 'response.done') && event?.response) {
  1370. finalResponse = event.response;
  1371. continue;
  1372. }
  1373. if (event?.type === 'error') {
  1374. const errMsg = typeof event.error === 'object'
  1375. ? (event.error.message || JSON.stringify(event.error))
  1376. : String(event.error || event.raw || 'Stream error');
  1377. throw new Error(errMsg);
  1378. }
  1379. if (event.done) break;
  1380. }
  1381. const data = finalResponse ? { ...finalResponse } : {};
  1382. if (typeof data.output_text !== 'string' || !data.output_text) data.output_text = accumulatedText;
  1383. self.postMessage({ type: 'STREAM_DELTA', reqId, delta: '', accumulatedText, isDone: true });
  1384. self.postMessage({ reqId, payload: data });
  1385. }
  1386. } catch (err) {
  1387. if (isBinary) {
  1388. try { await dir.removeEntry(meta.id + '_full'); } catch(e) {}
  1389. }
  1390. if (err.name === 'AbortError') {
  1391. if (isBinary) {
  1392. self.postMessage({ reqId, payload: { aborted: true } });
  1393. } else {
  1394. self.postMessage({ type: 'STREAM_DELTA', reqId, delta: '', accumulatedText: '', isDone: true, aborted: true });
  1395. self.postMessage({ reqId, error: 'Aborted', errorName: 'AbortError' });
  1396. }
  1397. } else {
  1398. self.postMessage({ reqId, error: err.message });
  1399. }
  1400. } finally {
  1401. ACTIVE_ABORTS.delete(reqId);
  1402. }
  1403. break;
  1404. }
  1405. }
  1406. } catch (err) {
  1407. self.postMessage({ reqId, error: err.message });
  1408. }
  1409. });
  1410. };
  1411. `;
  1412.  
  1413. const StorageClient = {
  1414. worker: null,
  1415. resolvers: new Map(),
  1416. streamCallbacks: new Map(),
  1417. reqCounter: 0,
  1418. init() {
  1419. if (this.worker) return;
  1420. const blob = new Blob([DATA_WORKER_CODE], { type: 'application/javascript' });
  1421. this.worker = new Worker(URL.createObjectURL(blob));
  1422. this.worker.onmessage = (e) => {
  1423. const msg = e.data;
  1424. const { reqId, payload, error } = msg;
  1425. if (msg.type === 'STREAM_DELTA') {
  1426. const cb = this.streamCallbacks.get(reqId);
  1427. if (cb) cb.onDelta(msg.delta, msg.accumulatedText, msg.isDone, msg.aborted);
  1428. return;
  1429. }
  1430. if (this.resolvers.has(reqId)) {
  1431. const { resolve, reject } = this.resolvers.get(reqId);
  1432. if (error) {
  1433. const err = new Error(error);
  1434. if (msg.errorName) err.name = msg.errorName;
  1435. reject(err);
  1436. } else {
  1437. if (payload?.thumbBuffer) {
  1438. payload.thumbBlob = new Blob([payload.thumbBuffer], { type: 'image/webp' });
  1439. delete payload.thumbBuffer;
  1440. }
  1441. resolve(payload);
  1442. }
  1443. this.resolvers.delete(reqId);
  1444. this.streamCallbacks.delete(reqId);
  1445. }
  1446. };
  1447. this.worker.onerror = (err) => {
  1448. addLog('ERROR', 'DataWorker error', err);
  1449. };
  1450. },
  1451. _dispatch(type, payload = {}, transfer = []) {
  1452. this.init();
  1453. const reqId = ++this.reqCounter;
  1454. const { promise, resolve, reject } = Promise.withResolvers();
  1455. this.resolvers.set(reqId, { resolve, reject });
  1456. this.worker.postMessage({ type, reqId, id: payload.id || String(reqId), payload }, transfer);
  1457. return promise;
  1458. },
  1459. processStream(stream, meta, onDelta, signal = null) {
  1460. this.init();
  1461. const reqId = ++this.reqCounter;
  1462. const { promise, resolve, reject } = Promise.withResolvers();
  1463. this.resolvers.set(reqId, { resolve, reject });
  1464. if (onDelta) this.streamCallbacks.set(reqId, { onDelta });
  1465.  
  1466. if (signal) {
  1467. if (signal.aborted) {
  1468. reject(new DOMException('Aborted', 'AbortError'));
  1469. return promise;
  1470. }
  1471. signal.addEventListener('abort', () => {
  1472. if (this.worker) this.worker.postMessage({ type: 'ABORT_REQ', reqId });
  1473. }, { once: true });
  1474. }
  1475.  
  1476. this.worker.postMessage({ type: 'PROCESS_STREAM', reqId, stream, meta }, [stream]);
  1477. return promise;
  1478. },
  1479. saveItemsBulk(items) { return this._dispatch('COMMIT_ITEMS', { items }); },
  1480. deleteItemsBulk(ids) {
  1481. ids.forEach(id => { RefPayloadCache.delete(id); });
  1482. return this._dispatch('DELETE_ITEMS', { ids });
  1483. },
  1484. saveMeta(key, data) { return this._dispatch('SAVE_META', { key, data }); },
  1485. loadMeta(key) { return this._dispatch('LOAD_META', { key }); },
  1486. loadAllHistory() { return this._dispatch('LOAD_ALL_HISTORY'); },
  1487. loadHeavy(id) { return this._dispatch('LOAD_HEAVY', { id }); },
  1488. deleteMeta(keys) { return this._dispatch('DELETE_META', { keys: Array.isArray(keys) ? keys : [keys] }); },
  1489. clearAll() { return this._dispatch('CLEAR_ALL'); },
  1490. processMediaResponse(id, payload, transfer = []) { return this._dispatch('processApiResponse', { id, ...payload }, transfer); },
  1491. extractImagePrompt(buffer) { return this._dispatch('extractImagePrompt', { buffer }, [buffer]); },
  1492. loadOpfsMedia(id, type) {
  1493. return this._dispatch('LOAD_OPFS_MEDIA', { id, type });
  1494. },
  1495. extractVideoPosterWorker(id, mimeType) {
  1496. return this._dispatch('EXTRACT_VIDEO_POSTER', { id, mimeType });
  1497. }
  1498. };
  1499.  
  1500. const terminateMediaWorker = () => {
  1501. if (StorageClient.worker) {
  1502. StorageClient.worker.terminate();
  1503. StorageClient.worker = null;
  1504. StorageClient.resolvers.clear();
  1505. StorageClient.streamCallbacks.clear();
  1506. }
  1507. };
  1508. const LazyTextStore = {
  1509. _cache: new Map(),
  1510. _MAX: 100,
  1511. async get(id) {
  1512. const key = String(id);
  1513. if (this._cache.has(key)) {
  1514. const v = this._cache.get(key);
  1515. this._cache.delete(key);
  1516. this._cache.set(key, v);
  1517. return v;
  1518. }
  1519. const data = await StorageClient.loadHeavy(key);
  1520. if (data) {
  1521. this._cache.set(key, data);
  1522. if (this._cache.size > this._MAX) {
  1523. const oldest = this._cache.keys().next().value;
  1524. this._cache.delete(oldest);
  1525. }
  1526. }
  1527. return data || { prompt: '', responseText: '', textChatMessages: null };
  1528. },
  1529. set(id, data) {
  1530. const key = String(id);
  1531. this._cache.delete(key);
  1532. this._cache.set(key, data);
  1533. if (this._cache.size > this._MAX) {
  1534. const oldest = this._cache.keys().next().value;
  1535. this._cache.delete(oldest);
  1536. }
  1537. },
  1538. invalidate(id) {
  1539. this._cache.delete(String(id));
  1540. }
  1541. };
  1542.  
  1543. const parseResponseJsonSafe = async (response) => {
  1544. try { return await response.json(); } catch (e) { return null; }
  1545. };
  1546. const buildHttpErrorMessage = (status, errData = null) => {
  1547. const parts = new Set([`HTTP ${status}`]);
  1548. const fields = [['error','message'], ['error','code'], ['error','detail'], ['error','type'], ['message'], ['code'], ['detail']];
  1549. for (const path of fields) {
  1550. let v = errData;
  1551. for (const k of path) v = v?.[k];
  1552. if (v != null && v !== '') {
  1553. const text = String(v).trim();
  1554. if (text) parts.add(text);
  1555. }
  1556. }
  1557. if (typeof errData?.error === 'string') {
  1558. const t = errData.error.trim();
  1559. if (t) parts.add(t);
  1560. }
  1561. return Array.from(parts).join('\n');
  1562. };
  1563. const getErrorMessage = (error, fallback = 'Unknown error') => error?.message ?? String(error ?? fallback);
  1564. const buildCurrentStatusLines = (status) => [{ text: status.longStatus, color: status.color, style: CURRENT_PANEL_STATUS_STYLE }];
  1565. const isRequestModeratedError = (message = '') => REQUEST_MODERATION_RE.test(message);
  1566. const isInvalidRequestError = (message = '') => REQUEST_INVALID_RE.test(message);
  1567. const shouldAutoRetryStuckVideo = (message = '', autoRetryEnabled = STATE.autoRetryVideo) => message.includes('video lost') || (autoRetryEnabled && STUCK_VIDEO_RETRY_TOKENS.some(token => message.includes(token)));
  1568.  
  1569. const sleep = (ms, signal) => {
  1570. const { promise, resolve, reject } = Promise.withResolvers();
  1571. if (signal?.aborted) {
  1572. reject(new DOMException('Aborted', 'AbortError'));
  1573. return promise;
  1574. }
  1575.  
  1576. let timerId;
  1577. const onAbort = () => {
  1578. clearTimeout(timerId);
  1579. reject(new DOMException('Aborted', 'AbortError'));
  1580. };
  1581.  
  1582. if (signal) signal.addEventListener('abort', onAbort, { once: true });
  1583.  
  1584. timerId = setTimeout(() => {
  1585. if (signal) signal.removeEventListener('abort', onAbort);
  1586. resolve();
  1587. }, ms);
  1588.  
  1589. return promise;
  1590. };
  1591.  
  1592. const scheduleTask = (run, delay = 0, { priority = 'background' } = {}) => {
  1593. const controller = new AbortController();
  1594. scheduler.postTask(() => {
  1595. if (controller.signal.aborted) return;
  1596. return run();
  1597. }, { priority, delay, signal: controller.signal }).catch(() => {});
  1598. return controller;
  1599. };
  1600. const runBg = (fn, opts = { priority: 'background' }) => scheduler.postTask(async () => { try { return await fn(); } catch (e) { addLog('WARN', 'runBg err', e); } }, opts);
  1601.  
  1602. const isMemoryConstrainedMode = () => STATE.ecoMode || App.isAutoEco;
  1603. const clampMediaVolume = (value, fallback = APP_CONFIG.MEDIA.DEFAULT_VOLUME) => {
  1604. if (value == null || value === '') return fallback;
  1605. const num = Number(value);
  1606. if (!Number.isFinite(num)) return fallback;
  1607. return Math.min(1, Math.max(0, num));
  1608. };
  1609. const getStoredMediaVolume = () => {
  1610. try {
  1611. return clampMediaVolume(localStorage.getItem(MEDIA_VOLUME_STORAGE_KEY));
  1612. } catch (e) {
  1613. return APP_CONFIG.MEDIA.DEFAULT_VOLUME;
  1614. }
  1615. };
  1616. const persistMediaVolume = (value) => {
  1617. const safeVolume = clampMediaVolume(value);
  1618. try { localStorage.setItem(MEDIA_VOLUME_STORAGE_KEY, String(safeVolume)); } catch (e) {}
  1619. return safeVolume;
  1620. };
  1621. const applyMemoryPressureProfile = (reason = 'manual') => {
  1622. RefPayloadCache.clear();
  1623. CardRegistry.maps.current.forEach(card => card.setLoadLevel(isMemoryConstrainedMode() ? 'thumb' : (card._isActuallyVisible ? 'full' : 'thumb')));
  1624. CardRegistry.maps.history.forEach(card => card.setLoadLevel(isMemoryConstrainedMode() ? (card._isActuallyVisible ? 'thumb' : 'none') : 'thumb'));
  1625. CardRegistry.maps.reference.forEach(card => card.setLoadLevel('thumb'));
  1626. };
  1627.  
  1628. document.addEventListener('visibilitychange', () => {
  1629. if (isMemoryConstrainedMode()) {
  1630. if (document.hidden) {
  1631. RefPayloadCache.clear();
  1632. } else {
  1633. applyMemoryPressureProfile('tab-visible');
  1634. }
  1635. }
  1636. });
  1637.  
  1638. const TimeUtils = {
  1639. formatter: new Intl.DateTimeFormat('sv-SE', {
  1640. year: 'numeric', month: '2-digit', day: '2-digit',
  1641. hour: '2-digit', minute: '2-digit', second: '2-digit'
  1642. }),
  1643. forTooltip(ts) { return this.formatter.format(ts || Date.now()); },
  1644. forFile(ts) { return this.formatter.format(ts || Date.now()).replace(/[-: ]/g, ''); }
  1645. };
  1646. const TaskChunker = {
  1647. _deadlineMs: 8,
  1648. _lastYieldTime: globalThis.performance.now(),
  1649.  
  1650. reset() {
  1651. this._lastYieldTime = globalThis.performance.now();
  1652. },
  1653.  
  1654. async yieldIfNeeded() {
  1655. const now = globalThis.performance.now();
  1656. const timeExceeded = (now - this._lastYieldTime) > this._deadlineMs;
  1657. const inputPending = globalThis.navigator?.scheduling?.isInputPending?.({ includeContinuous: true });
  1658.  
  1659. if (timeExceeded || inputPending) {
  1660. await globalThis.scheduler.yield();
  1661. this._lastYieldTime = globalThis.performance.now();
  1662. }
  1663. }
  1664. };
  1665. const makeTimestamp = (ts) => TimeUtils.forFile(ts);
  1666. const TEXT_SESSION_KEY_PREFIX = 'textSession:';
  1667. const ACTIVE_TEXT_SESSION_KEY = 'activeTextSessionId';
  1668. const MULTI_AGENT_TEXT_MODEL = 'grok-4.20-multi-agent';
  1669. const ESC_MAP = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
  1670. const SAFE_PROTO = new Set(['http:', 'https:']);
  1671.  
  1672. const escapeHtml = (s = '') => String(s).replace(/[&<>"']/g, c => ESC_MAP[c]);
  1673. const escapeHtmlAttr = (s = '') => escapeHtml(s).replace(/`/g, '&#96;');
  1674. const escMd = (s = '') => String(s).replace(/[&<>]/g, c => ESC_MAP[c]);
  1675.  
  1676. const sanitizeUrl = url => {
  1677. if (!url || typeof url !== 'string' || /[<>"'`\s]/.test(url)) return '';
  1678. if (url.startsWith('/') || url.startsWith('data:') || url.startsWith('blob:')) return url;
  1679. try { return SAFE_PROTO.has(new URL(url).protocol) ? url : ''; } catch { return ''; }
  1680. };
  1681.  
  1682. const parseList = block => {
  1683. let html = '', stack =[];
  1684. for (const line of block.trimEnd().split('\n')) {
  1685. const match = line.match(/^([ \t]*)([*+-]|\d+\.)\s+(?:\[([ xX])\]\s+)?(.+)$/);
  1686. if (!match) { html += `<br>${line.trim()}`; continue; }
  1687. const [, spaces, marker, check, text] = match;
  1688. const indent = spaces.length, tag = marker.endsWith('.') ? 'ol' : 'ul';
  1689.  
  1690. while (stack.length && stack.at(-1).i > indent) html += `</li></${stack.pop().t}>`;
  1691.  
  1692. if (!stack.length || stack.at(-1).i < indent) {
  1693. html += `<${tag}>`; stack.push({ t: tag, i: indent });
  1694. } else if (stack.at(-1).t !== tag) {
  1695. html += `</li></${stack.pop().t}><${tag}>`; stack.push({ t: tag, i: indent });
  1696. } else {
  1697. html += '</li>';
  1698. }
  1699.  
  1700. html += `<li${check !== undefined ? ' class="task"' : ''}>${check !== undefined ? `<input type="checkbox" disabled${check.trim() ? ' checked' : ''}> ` : ''}${text}`;
  1701. }
  1702. while (stack.length) html += `</li></${stack.pop().t}>`;
  1703. return html;
  1704. };
  1705.  
  1706. const parseQuotes = s => s.replace(/(?:^&gt;[ \t]?.*\n?)+/gm, m => `<blockquote><p>${parseQuotes(m.replace(/^&gt;[ \t]?/gm, '').trimEnd())}</p></blockquote>`);
  1707.  
  1708. const parseMarkdown = s => {
  1709. if (!s) return '';
  1710. s = escMd(s);
  1711. const B =[];
  1712.  
  1713. 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`))
  1714. .replace(/`([^`]+)`/g, (_, c) => (B.push(`<code>${c}</code>`), `\x00${B.length-1}\x00`))
  1715. .replace(/\\([\\`*_{}[\]()#+\-.!>|])/g, (m, c) => `&#${c.charCodeAt(0)};`)
  1716. .replace(/ {2,}\n/g, '<br>');
  1717.  
  1718. s = parseQuotes(s)
  1719. .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; })
  1720. .replace(/^(#{1,6})\s+(.+)$/gm, (_, h, t) => `<h${h.length}>${t}</h${h.length}>`)
  1721. .replace(/^\|(.+?)\|[ \t]*\n[ \t]*\|([-| :]+)\|[ \t]*\n((?:[ \t]*\|.+?\|[ \t]*(?:\n|$))*)/gm, (_, h, alignRow, r) => {
  1722. const aligns = alignRow.split('|').slice(1,-1).map(c => {
  1723. const s = c.trim(); return s.startsWith(':') && s.endsWith(':') ? 'center' : s.endsWith(':') ? 'right' : (s.startsWith(':') ? 'left' : '');
  1724. });
  1725. const th = h.split('|').map((c, i) => `<th${aligns[i] ? ` style="text-align:${aligns[i]}"` : ''}>${c.trim()}</th>`).join('');
  1726. 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('');
  1727. return `<div class="md-table"><table><thead><tr>${th}</tr></thead><tbody>${tb}</tbody></table></div>`;
  1728. })
  1729. .replace(/(?:^[ \t]*(?:[*+-]|\d+\.)\s+.*(?:\n|$))+/gm, parseList)
  1730. .replace(/^[-*_]{3,}\s*$/gm, '<hr>')
  1731. .replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>')
  1732. .replace(/(\*\*|__)(.+?)\1/g, '<strong>$2</strong>')
  1733. .replace(/([*_])(.+?)\1/g, '<em>$2</em>')
  1734. .replace(/~~(.+?)~~/g, '<del>$1</del>')
  1735. .replace(/(!?)\[([^\]]*)\]\(([^)"]+)(?:\s+"([^"]*)")?\)/g, (_, img, t, u, ti) => {
  1736. const su = sanitizeUrl(u); if (!su) return t;
  1737. 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>`;
  1738. })
  1739. .replace(/(?<!["'])(https?:\/\/[^\s<]+?)(?=[.,;!?)]*(?:\s|<|$))(?![^<]*<\/a>)/g, u => {
  1740. const su = sanitizeUrl(u); return su ? `<a href="${escapeHtmlAttr(su)}" target="_blank" rel="noopener noreferrer">${u}</a>` : u;
  1741. })
  1742. .replace(/&lt;(\/?(details|summary)[^&]*)&gt;/g, (_, t) => /^\/?(?:details|summary)$/i.test(t.trim()) ? `<${t.toLowerCase()}>` : `&lt;${t}&gt;`);
  1743.  
  1744. return `<p>${s.trim().replace(/\n{2,}/g, '</p><p>')}</p>`
  1745. .replace(/<p>\s*<(ul|ol|pre|blockquote|h[1-6]|hr|div|details|summary)/g, '<$1')
  1746. .replace(/<\/(ul|ol|pre|blockquote|h[1-6]|hr|div|details|summary)>\s*<\/p>/g, '</$1>')
  1747. .replace(/<p>\s*<\/p>/g, '')
  1748. .replace(/<\/(ul|ol|li|pre|blockquote|h[1-6]|hr|div|table|thead|tbody|tr|th|td|p|details|summary)>\s+</g, '</$1><')
  1749. .replace(/>\s+<(ul|ol|li|pre|blockquote|h[1-6]|hr|div|table|thead|tbody|tr|th|td|p|details|summary)>/g, '><$1>')
  1750. .replace(/\x00(\d+)\x00/g, (_, i) => B[i]);
  1751. };
  1752. const isMultiAgentTextModel = (model = STATE?.textModel) => String(model || '') === MULTI_AGENT_TEXT_MODEL;
  1753. const canUseBuiltInTextTools = (model = STATE?.textModel) => isMultiAgentTextModel(model);
  1754. const buildEnabledTextTools = (model = STATE?.textModel, settings = STATE) => {
  1755. if (!canUseBuiltInTextTools(model)) return [];
  1756. const tools = [];
  1757. if (settings.textWebSearch === 'on') tools.push({ type: 'web_search' });
  1758. if (settings.textXSearch === 'on') tools.push({ type: 'x_search' });
  1759. return tools;
  1760. };
  1761. const buildActionSettingsSnapshot = (action = STATE.action) => ({
  1762. action,
  1763. currentAr: STATE.currentAr,
  1764. batchSize: STATE.batchSize,
  1765. duration: STATE.duration,
  1766. loops: getLoopsValue(action),
  1767. refMode: STATE.refMode,
  1768. concurrentSessions: STATE.concurrentSessions,
  1769. rateLimitDelay: STATE.rateLimitDelay,
  1770. retryMinMs: STATE.retryMinMs,
  1771. retryMaxMs: STATE.retryMaxMs,
  1772. autoRetryVideo: STATE.autoRetryVideo,
  1773. referenceARTrick: STATE.referenceARTrick,
  1774. optimize2k: STATE.optimize2k,
  1775. embedImagePrompt: STATE.embedImagePrompt,
  1776. resImg: STATE.resImg,
  1777. resVid: STATE.resVid,
  1778. imgBaseMs: STATE.imgBaseMs,
  1779. voice: STATE.voice,
  1780. language: STATE.language,
  1781. ttsQuality: STATE.ttsQuality,
  1782. textModel: STATE.textModel,
  1783. textReasoningEffort: STATE.textReasoningEffort,
  1784. textWebSearch: STATE.textWebSearch,
  1785. textXSearch: STATE.textXSearch,
  1786. textChatMessages: sanitizeTextConversationStore(structuredClone(STATE.textChatMessages || []))
  1787. });
  1788.  
  1789. const fetchBlobCSP = (url, { signal, timeout = 30000, range } = {}) => {
  1790. const { promise, resolve, reject } = Promise.withResolvers();
  1791. if (typeof GM_xmlhttpRequest === 'undefined') {
  1792. reject(new Error('GM_xmlhttpRequest not granted'));
  1793. return promise;
  1794. }
  1795.  
  1796. let settled = false;
  1797. let req;
  1798. const combinedSignal = signal ? AbortSignal.any([signal, AbortSignal.timeout(timeout)]) : AbortSignal.timeout(timeout);
  1799.  
  1800. const onAbort = () => {
  1801. if (settled) return;
  1802. settled = true;
  1803. req?.abort();
  1804. reject(signal?.aborted ? new DOMException('Aborted', 'AbortError') : new Error('Timeout'));
  1805. };
  1806.  
  1807. if (combinedSignal.aborted) {
  1808. onAbort();
  1809. return promise;
  1810. }
  1811. combinedSignal.addEventListener('abort', onAbort, { once: true });
  1812.  
  1813. try {
  1814. req = GM_xmlhttpRequest({
  1815. method: 'GET', url,
  1816. headers: range ? { Range: `bytes=${range}` } : {},
  1817. responseType: 'blob',
  1818. onload: (res) => {
  1819. if (settled) return;
  1820. settled = true;
  1821. combinedSignal.removeEventListener('abort', onAbort);
  1822. (res.status >= 200 && res.status < 300) ? resolve(res.response) : reject(new Error(`HTTP ${res.status}`));
  1823. },
  1824. onerror: (err) => {
  1825. if (settled) return;
  1826. settled = true;
  1827. combinedSignal.removeEventListener('abort', onAbort);
  1828. reject(err);
  1829. }
  1830. });
  1831. } catch (e) {
  1832. if (!settled) {
  1833. settled = true;
  1834. combinedSignal.removeEventListener('abort', onAbort);
  1835. reject(e);
  1836. }
  1837. }
  1838. return promise;
  1839. };
  1840.  
  1841. const resetVideoElement = (video, { pause = true } = {}) => {
  1842. if (!video) return;
  1843. if (pause) video.pause?.();
  1844. if (video.src?.startsWith('blob:')) URL.revokeObjectURL(video.src);
  1845. video.removeAttribute('src');
  1846. video.srcObject = null;
  1847. video.load();
  1848. };
  1849. const clickTransientLink = ({ href = '', download = '', target = '' } = {}) => {
  1850. const a = document.createElement('a');
  1851. a.href = href;
  1852. if (download) a.download = download;
  1853. if (target) a.target = target;
  1854. document.body.appendChild(a);
  1855. a.click();
  1856. a.remove();
  1857. };
  1858.  
  1859. const insertTextAtCursor = (inputEl, text, replaceAll = false) => {
  1860. inputEl.focus();
  1861. if (replaceAll) { inputEl.select(); document.execCommand('insertText', false, text); } else document.execCommand('insertText', false, text);
  1862. inputEl.dispatchEvent(new Event('input', { bubbles: true }));
  1863. };
  1864. const isImagePromptSupported = (file) => file && file.size > 128 && (/^image\/(jpeg|png)$/.test(file.type) || /\.(jpe?g|png)$/i.test(file.name));
  1865. const inferMediaMimeType = (file) => {
  1866. const declaredType = String(file?.type || '').toLowerCase();
  1867. if (declaredType) return declaredType;
  1868. const name = String(file?.name || '');
  1869. if (/\.jpe?g$/i.test(name)) return 'image/jpeg';
  1870. if (/\.png$/i.test(name)) return 'image/png';
  1871. if (/\.webp$/i.test(name)) return 'image/webp';
  1872. if (/\.gif$/i.test(name)) return 'image/gif';
  1873. if (/\.mp4$/i.test(name)) return 'video/mp4';
  1874. if (/\.webm$/i.test(name)) return 'video/webm';
  1875. return '';
  1876. };
  1877.  
  1878. const createLimiter = (limit = 1) => {
  1879. let active = 0;
  1880. const queue = [];
  1881. const pump = () => {
  1882. if (active >= limit || queue.length === 0) return;
  1883. active++;
  1884. const { task, resolve, reject } = queue.shift();
  1885. Promise.resolve()
  1886. .then(task)
  1887. .then(resolve, reject)
  1888. .finally(() => {
  1889. active--;
  1890. pump();
  1891. });
  1892. };
  1893. return task => new Promise((resolve, reject) => {
  1894. queue.push({ task, resolve, reject });
  1895. pump();
  1896. });
  1897. };
  1898.  
  1899. const DecodeQueue = {
  1900. queue: [], active: 0, max: Math.min(12, Math.max(4, navigator.hardwareConcurrency || 4)),
  1901. async enqueue(url, signal) {
  1902. if (signal?.aborted) throw new DOMException('Aborted', 'AbortError');
  1903. const { promise, resolve, reject } = Promise.withResolvers();
  1904. this.queue.push({ url, signal, resolve, reject });
  1905. this._process();
  1906. return promise;
  1907. },
  1908. async _process() {
  1909. if (this.active >= this.max || this.queue.length === 0) return;
  1910. const task = this.queue.pop();
  1911.  
  1912. if (task.signal?.aborted) {
  1913. task.reject(new DOMException('Aborted', 'AbortError'));
  1914. return this._process();
  1915. }
  1916.  
  1917. this.active++;
  1918. const img = new Image();
  1919.  
  1920. try {
  1921. img.src = task.url;
  1922.  
  1923. if (task.signal) {
  1924. const { promise, reject } = Promise.withResolvers();
  1925. const abortHandler = () => {
  1926. img.src = '';
  1927. reject(new DOMException('Aborted', 'AbortError'));
  1928. };
  1929.  
  1930. if (task.signal.aborted) throw new DOMException('Aborted', 'AbortError');
  1931. task.signal.addEventListener('abort', abortHandler, { once: true });
  1932.  
  1933. await Promise.race([img.decode(), promise]);
  1934. task.signal.removeEventListener('abort', abortHandler);
  1935. } else {
  1936. await img.decode();
  1937. }
  1938.  
  1939. task.resolve(task.url);
  1940. } catch (err) {
  1941. img.src = '';
  1942. task.reject(err);
  1943. } finally {
  1944. this.active--;
  1945. this._process();
  1946. }
  1947. }
  1948. };
  1949.  
  1950. const MediaUtils = {
  1951. _imageQueue: createLimiter(4),
  1952. _videoQueue: createLimiter(2),
  1953. getDownloadName(item) {
  1954. const imageExt = item?.mimeType?.includes('jpeg') ? 'jpg' : item?.mimeType?.includes('webp') ? 'webp' : 'png';
  1955. return item?.filename || `dl.${item?.type === 'video' ? 'mp4' : item?.type === 'audio' ? 'mp3' : item?.type === 'text' ? 'txt' : imageExt}`;
  1956. },
  1957.  
  1958. async processSource(source, type, maxDim = 600, qual = 0.75) {
  1959. const queue = type === 'video' ? this._videoQueue : this._imageQueue;
  1960. return queue(async () => {
  1961. const isVid = type === 'video', isLocal = isVid && source instanceof Blob;
  1962. let v = null;
  1963. const cleanup = () => { resetVideoElement(v, { pause: false }); };
  1964. try {
  1965. using durl = isLocal ? new DisposableURL(source) : NoOpDisposable;
  1966. const url = isLocal ? durl.url : source;
  1967. using bmp = await (isVid ? (() => {
  1968. v = document.createElement('video');
  1969. v.muted = true; v.playsInline = true; v.disableRemotePlayback = true;
  1970. if (!isLocal) v.crossOrigin = 'anonymous';
  1971. const { promise, resolve, reject } = Promise.withResolvers();
  1972. const tmr = setTimeout(() => reject(new Error('WebP Timeout')), 20000);
  1973. v.onloadeddata = () => { v.currentTime = 0.1; };
  1974. const extractFrame = async () => {
  1975. clearTimeout(tmr);
  1976. try { resolve(await createImageBitmap(v, { resizeWidth: maxDim, resizeQuality: 'medium', premultiplyAlpha: 'none', colorSpaceConversion: 'none' })); }
  1977. catch (e) { reject(e); }
  1978. finally { resetVideoElement(v, { pause: false }); }
  1979. };
  1980. v.requestVideoFrameCallback(extractFrame);
  1981. v.onerror = () => { clearTimeout(tmr); setTimeout(() => reject(new Error('Decode err')), 100); };
  1982. v.src = url;
  1983. v.load();
  1984. return promise;
  1985. })() : createImageBitmap(source, { resizeWidth: maxDim, resizeQuality: 'medium', premultiplyAlpha: 'none', colorSpaceConversion: 'none' }));
  1986.  
  1987. if (!bmp) throw new Error("Bitmap err");
  1988. const ratio = bmp.width / bmp.height;
  1989. let tCtx = new OffscreenCanvas(2, 2).getContext('2d', { alpha: false }); tCtx.drawImage(bmp, 0, 0, 2, 2);
  1990. const d = tCtx.getImageData(0, 0, 2, 2).data;
  1991. const lqip = `radial-gradient(ellipse at 30% 30%, rgb(${d[0]} ${d[1]} ${d[2]}), rgb(${d[12]} ${d[13]} ${d[14]}))`;
  1992. let exportCanvas = new OffscreenCanvas(bmp.width, bmp.height); exportCanvas.getContext('2d', { alpha: false }).drawImage(bmp, 0, 0);
  1993. const thumbBlob = await exportCanvas.convertToBlob({ type: 'image/webp', quality: qual });
  1994. exportCanvas.width = 1; exportCanvas.height = 1;
  1995. tCtx.canvas.width = 1; tCtx.canvas.height = 1;
  1996. cleanup();
  1997. tCtx = null;
  1998. exportCanvas = null;
  1999. return { thumbBlob, lqip, ratio };
  2000. } catch (err) { addLog('WARN', 'processSource err', err); cleanup(); return { thumbBlob: null, lqip: null, ratio: 16/9 }; }
  2001. });
  2002. },
  2003.  
  2004. async downloadItem(item, silent = false, card = null) {
  2005. if (!silent) document.body.style.cursor = 'wait';
  2006. try {
  2007. const fileName = this.getDownloadName(item);
  2008. let blobUrl = item.originalUrl, isBlobUrl = item.type === 'image' || item.type === 'audio' || (item.originalUrl?.startsWith('blob:') ?? false);
  2009. if (item.type === 'image' || item.type === 'audio' || item.type === 'text') {
  2010. let blob = await readOpfsAsBlob(item.id, item.mimeType, 'full');
  2011. if (blob) {
  2012. blobUrl = URL.createObjectURL(blob);
  2013. isBlobUrl = true;
  2014. }
  2015. }
  2016. if (!blobUrl) throw new Error('No URL');
  2017. const markDl = () => {
  2018. if (item.downloaded) return;
  2019. item.downloaded = true;
  2020. DBQueue.add(item);
  2021. card?._internals.states.add('downloaded');
  2022. CardRegistry.setDownloaded(item.id, true);
  2023. };
  2024. const fallbackDl = async () => { let fetchUrl = blobUrl, needRevoke = false; if (!isBlobUrl && item.originalUrl) { fetchUrl = URL.createObjectURL(await fetchBlobCSP(item.originalUrl, {})); needRevoke = true; }
  2025. clickTransientLink({ href: fetchUrl, download: fileName }); markDl();
  2026. if (isBlobUrl || needRevoke) {
  2027. setTimeout(() => URL.revokeObjectURL(fetchUrl), 60000);
  2028. }
  2029. };
  2030. if (isBlobUrl || typeof GM_download === 'undefined') await fallbackDl(); else await DiskIOLimiter(() => new Promise((res, rej) => {
  2031. GM_download({
  2032. url: blobUrl,
  2033. name: fileName,
  2034. onload: () => { markDl(); if (isBlobUrl) URL.revokeObjectURL(blobUrl); res(); },
  2035. onerror: () => { if (isBlobUrl) URL.revokeObjectURL(blobUrl); fallbackDl().then(res).catch(rej); },
  2036. ontimeout: res
  2037. });
  2038. }));
  2039. } catch (err) { addLog('WARN', 'Download failed', err); if (!silent && item.originalUrl) clickTransientLink({ href: item.originalUrl, download: this.getDownloadName(item), target: '_blank' }); }
  2040. if (!silent) document.body.style.cursor = '';
  2041. }
  2042. };
  2043.  
  2044. const readOpfsAsBlob = async (mediaId, mimeType, variant = 'full') => {
  2045. const file = await OPFS.read(mediaId, variant);
  2046. return file ? new Blob([file], { type: mimeType }) : null;
  2047. };
  2048.  
  2049. const extractImagePromptViaWorker = async (file) => {
  2050. if (!isImagePromptSupported(file)) return null;
  2051. const buffer = await file.arrayBuffer();
  2052. const transferredBuffer = buffer.transfer();
  2053. return StorageClient.extractImagePrompt(transferredBuffer);
  2054. };
  2055.  
  2056. const UIUtils = {
  2057. getIcon: (char, size = 16, cls = '') => `<span class="icon-inline ${cls}" style="font-size:${size}px;">${char}</span>`,
  2058. getDlIcon: (size = 18) => `<div class="icon-dl flex-center" style="width:${size}px; height:${size}px;">${APP_CONFIG.ICONS.download}</div>`,
  2059. createArIcon: (w, h, isAuto, isRef) => {
  2060. if (isAuto || isRef) return `<div class="ar-icon-shell"><div class="ar-icon-letter">${isRef ? 'R' : 'A'}</div></div>`;
  2061. const scale = 14 / Math.max(w, h);
  2062. const inner = `<div class="ar-icon-frame" style="width:${w * scale}px; height:${h * scale}px;"></div>`;
  2063. return `<div class="ar-icon-shell">${inner}</div>`;
  2064. }
  2065. };
  2066.  
  2067. const _tpl = {
  2068. sep: '<div class="popover-separator"></div>',
  2069. tog: (c) => {
  2070. 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>`);
  2071. return frag;
  2072. },
  2073. num: (c) => {
  2074. 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>`);
  2075. return frag;
  2076. }
  2077. };
  2078. const _prL = (id, t) => `<span id="${id}">${t}</span><span class="ar-dropdown-icon">▼</span>`;
  2079. const renderPromptControl = (c) => c
  2080. ? c.kind === 'dropdown' && c.render === 'aspect-ratio'
  2081. ? `<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>`
  2082. : c.kind === 'dropdown'
  2083. ? `<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>`
  2084. : c.kind === 'toggle'
  2085. ? `<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>`
  2086. : c.kind === 'number-inline'
  2087. ? `<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>`
  2088. : `<button class="xai-btn" id="${c.buttonId}" title="${c.title}">${c.text || ''}</button>`
  2089. : '';
  2090. 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('');
  2091. const buildPromptPopoverShellsHtml = () => UI_DROPDOWN_CONTROLS.map(({ popoverId, stateKey }) => `<div id="${popoverId}" popover="auto" class="xai-custom-popover" data-state-key="${stateKey}"></div>`).join('');
  2092. 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>';
  2093. const renderSettingsControl = (id) => {
  2094. const c = SETTINGS_CONTROL_BY_ID[id];
  2095. return c ? c.kind === 'toggle' ? _tpl.tog(c) : _tpl.num(c) : null;
  2096. };
  2097. const buildSettingsPopoverContent = () => {
  2098. const fragment = document.createDocumentFragment();
  2099. SETTINGS_POPOVER_LAYOUT.forEach((g, gi) => {
  2100. if (gi > 0) fragment.appendChild(createMarkupFragment(_tpl.sep));
  2101. if (g[0] === '__import_export__') {
  2102. fragment.appendChild(createMarkupFragment(buildSettingsImportExportHtml()));
  2103. } else {
  2104. g.forEach(id => { const node = renderSettingsControl(id); if (node) fragment.appendChild(node); });
  2105. }
  2106. });
  2107. return fragment;
  2108. };
  2109.  
  2110. 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>';
  2111. 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>';
  2112.  
  2113. const CARD_TEMPLATE_HTML = `
  2114. <div class="top-bar">
  2115. <div class="ref-header drag-handle" title="Drag to reorder">
  2116. <input class="ref-active-toggle" type="checkbox" title="Toggle reference" data-intent="toggle-ref-active">
  2117. <div class="ref-label-text-wrapper">
  2118. <span class="ref-label-text">{{refLabelText}}</span>
  2119. </div>
  2120. <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>
  2121. </div>
  2122. </div>
  2123. <div class="text-chat-header" id="text-chat-header">
  2124. <button class="text-chat-header-btn overlay-del-btn" data-intent="delete-global" title="Delete">${APP_CONFIG.ICONS.close}</button>
  2125. <div class="text-chat-model-name" id="text-model-name">{{textModelName}}</div>
  2126. <button class="text-chat-header-btn overlay-dl-btn" data-intent="download" title="Download">${UIUtils.getDlIcon(18)}</button>
  2127. </div>
  2128. <div class="media-wrapper" data-prompt="{{mediaWrapperPrompt}}">
  2129. <div class="bg-layer" id="bg-base" style="background: {{bgBaseBg}}"></div>
  2130. <div class="play-icon" id="play" style="display: {{playDisplay}}">${APP_CONFIG.ICONS.play}</div>
  2131. <img id="img" decoding="async">
  2132. <div id="progress-bar" class="progress-bar-layer"></div>
  2133. </div>
  2134. <div class="overlay">
  2135. <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>
  2136. <div class="overlay-actions">
  2137. <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>
  2138. <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>
  2139. <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>
  2140. <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>
  2141. </div>
  2142. </div>
  2143. <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>
  2144. <div class="ref-status-overlay" id="status-overlay"></div>
  2145. <div class="bottom-bar">
  2146. <p class="card-prompt" title="{{cardPromptTooltip}}">{{cardPrompt}}</p>
  2147. </div>
  2148. `;
  2149. const CARD_TEMPLATE = createModernTemplate(CARD_TEMPLATE_HTML);
  2150.  
  2151. 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&#10;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>`;
  2152.  
  2153. const TEMPLATE_APP = document.createElement('template');
  2154. TEMPLATE_APP.innerHTML = buildAppHtml();
  2155.  
  2156. const originalTitle = document.title || "xAI Advanced UI";
  2157. const PlaceholderPalette = {
  2158. deck: [],
  2159. lastSpot: '',
  2160. _gradient({ spot, mid, bg }) {
  2161. return `radial-gradient(circle at 30% 22%, ${spot} 0%, ${mid} 46%, ${bg} 100%)`;
  2162. },
  2163. next() {
  2164. if (this.deck.length === 0) {
  2165. this.deck = [...APP_CONFIG.PLACEHOLDER_SWATCHES];
  2166. 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]]; }
  2167. if (this.deck[0]?.spot === this.lastSpot && this.deck.length > 1) [this.deck[0], this.deck[1]] = [this.deck[1], this.deck[0]];
  2168. }
  2169. const swatch = this.deck.shift() || APP_CONFIG.PLACEHOLDER_SWATCHES[0];
  2170. this.lastSpot = swatch.spot;
  2171. return { ...swatch, gradient: this._gradient(swatch) };
  2172. }
  2173. };
  2174. const TitleState = {
  2175. _lastProgress: '',
  2176. markDone() {
  2177. STATE.pendingDoneNotice = document.hidden || !document.hasFocus();
  2178. this._lastProgress = '';
  2179. this.sync();
  2180. },
  2181. clearDone() {
  2182. if (!STATE.pendingDoneNotice) return;
  2183. STATE.pendingDoneNotice = false;
  2184. this.sync();
  2185. },
  2186. sync(progressText) {
  2187. if (progressText !== undefined) {
  2188. this._lastProgress = progressText;
  2189. }
  2190. if (isAnyRuntimeBusy()) {
  2191. setDocumentTitle(this._lastProgress ? `[${this._lastProgress}] ${originalTitle}` : originalTitle);
  2192. return;
  2193. }
  2194. this._lastProgress = '';
  2195. if (STATE.pendingDoneNotice && (document.hidden || !document.hasFocus())) {
  2196. setDocumentTitle(`[Done] ${originalTitle}`);
  2197. return;
  2198. }
  2199. setDocumentTitle(originalTitle);
  2200. },
  2201. handleTabActive() { if (document.hasFocus()) this.clearDone(); }
  2202. };
  2203.  
  2204. const createQueueWakeSignal = () => {
  2205. let version = 0;
  2206. let deferred = Promise.withResolvers();
  2207. return {
  2208. get version() { return version; },
  2209. wait(seenVersion = version) {
  2210. return seenVersion === version ? deferred.promise : Promise.resolve(version);
  2211. },
  2212. notify() {
  2213. version++;
  2214. deferred.resolve(version);
  2215. deferred = Promise.withResolvers();
  2216. }
  2217. };
  2218. };
  2219. const GENERATION_LANE_BY_ACTION = {
  2220. gen_image: 'media',
  2221. gen_video: 'media',
  2222. edit_video: 'media',
  2223. extend_video: 'media',
  2224. gen_audio: 'audio',
  2225. gen_text: 'text'
  2226. };
  2227. const GENERATION_LANE_LABEL = {
  2228. media: 'Media',
  2229. audio: 'Audio',
  2230. text: 'Text'
  2231. };
  2232. const createGenerationRuntime = (lane) => ({
  2233. lane,
  2234. label: GENERATION_LANE_LABEL[lane] || lane,
  2235. isGenerating: false,
  2236. generationToken: 0,
  2237. successCount: 0,
  2238. activeAction: null,
  2239. tasks: new Map(),
  2240. taskPromises: new Set(),
  2241. progress: new Map(),
  2242. queueWake: createQueueWakeSignal(),
  2243. queueWakeTimer: 0
  2244. });
  2245. const GenerationRegistry = {
  2246. runtimes: {
  2247. media: createGenerationRuntime('media'),
  2248. audio: createGenerationRuntime('audio'),
  2249. text: createGenerationRuntime('text')
  2250. },
  2251. getLane(action = 'gen_image') {
  2252. return GENERATION_LANE_BY_ACTION[String(action || '').trim()] || 'media';
  2253. },
  2254. getRuntime(actionOrLane = 'gen_image') {
  2255. if (this.runtimes[actionOrLane]) return this.runtimes[actionOrLane];
  2256. return this.runtimes[this.getLane(actionOrLane)];
  2257. },
  2258. getAllRuntimes() {
  2259. return Object.values(this.runtimes);
  2260. },
  2261. getAllTasks() {
  2262. return this.getAllRuntimes().flatMap(runtime => [...runtime.tasks.values()]);
  2263. },
  2264. findTask(taskId) {
  2265. const key = String(taskId);
  2266. for (const runtime of this.getAllRuntimes()) {
  2267. const task = runtime.tasks.get(key);
  2268. if (task) return task;
  2269. }
  2270. return null;
  2271. },
  2272. resetRuntime(runtime) {
  2273. clearTimeout(runtime.queueWakeTimer);
  2274. runtime.queueWakeTimer = 0;
  2275. runtime.queueWake = createQueueWakeSignal();
  2276. runtime.tasks.clear();
  2277. runtime.taskPromises.clear();
  2278. runtime.progress.clear();
  2279. runtime.isGenerating = false;
  2280. runtime.generationToken = 0;
  2281. runtime.successCount = 0;
  2282. runtime.activeAction = null;
  2283. },
  2284. resetAll() {
  2285. this.getAllRuntimes().forEach(runtime => this.resetRuntime(runtime));
  2286. },
  2287. isAnyBusy() {
  2288. return this.getAllRuntimes().some(runtime => runtime.isGenerating || runtime.tasks.size > 0);
  2289. }
  2290. };
  2291. const getCurrentRuntime = (action = STATE?.action) => GenerationRegistry.getRuntime(action);
  2292. const isRuntimeBusy = (actionOrLane = STATE?.action) => GenerationRegistry.getRuntime(actionOrLane).isGenerating;
  2293. const isAnyRuntimeBusy = () => GenerationRegistry.isAnyBusy();
  2294. const wakeUpQueue = (actionOrLane = STATE?.action) => GenerationRegistry.getRuntime(actionOrLane).queueWake.notify();
  2295. const scheduleRuntimeWake = (actionOrLane = STATE?.action, delay = 300) => {
  2296. const runtime = GenerationRegistry.getRuntime(actionOrLane);
  2297. clearTimeout(runtime.queueWakeTimer);
  2298. runtime.queueWakeTimer = setTimeout(() => wakeUpQueue(runtime.lane), delay);
  2299. };
  2300. const applyRuntimeIdleCleanup = () => {
  2301. if (isAnyRuntimeBusy()) return;
  2302. RefPayloadCache.clear();
  2303. PanelManager.update(STATE.historyOrder, STATE.items, 'history');
  2304. };
  2305. const notifyRuntimeGeneratingChanged = (runtime, isGenerating) => {
  2306. if (isGenerating) STATE.pendingDoneNotice = false;
  2307. if (STATE.isMounted) {
  2308. if (runtime === getCurrentRuntime()) {
  2309. toggleGenerateButton(isGenerating);
  2310. updateDirtyStateUI();
  2311. }
  2312. updateStatusUI();
  2313. syncWipeButtonState?.();
  2314. }
  2315. if (!isGenerating) applyRuntimeIdleCleanup();
  2316. TitleState.sync();
  2317. };
  2318. const setRuntimeField = (runtime, key, value) => {
  2319. if (runtime[key] === value) return false;
  2320. runtime[key] = value;
  2321. if (key === 'isGenerating') notifyRuntimeGeneratingChanged(runtime, value);
  2322. return true;
  2323. };
  2324. let _configSaveTask = null, _chatScrollRaf = 0, DOM = {};
  2325. const configKeys = new Set(Object.keys(CONFIG_SCHEMA));
  2326. const PROMPT_KEYS = Object.keys(DEFAULT_PROMPTS);
  2327. const RETRY_PROMPT_PAD_RE = /[\u200B-\u200D\uFEFF]/g;
  2328. const resolveSchemaRule = (value, ctx) => typeof value === 'function' ? value(ctx) : value;
  2329. const clampInt = (value, min, max, fallback) => {
  2330. const parsed = Number.parseInt(value, 10);
  2331. const fallbackParsed = Number.parseInt(fallback, 10);
  2332. const safeFallback = Number.isFinite(fallbackParsed) ? fallbackParsed : min;
  2333. return Number.isFinite(parsed) ? Math.min(max, Math.max(min, parsed)) : safeFallback;
  2334. };
  2335. const parseJsonSafe = (raw, fallback = null) => {
  2336. if (typeof raw !== 'string' || raw === '') return fallback;
  2337. try { return JSON.parse(raw); } catch (e) { return fallback; }
  2338. };
  2339. const cloneSerializable = (value, fallback = null) => {
  2340. if (value === undefined) return fallback;
  2341. try { return structuredClone(value); }
  2342. catch { return fallback; }
  2343. };
  2344. const normalizePromptDraft = (value, fallback = '') => typeof value === 'string' ? value : fallback;
  2345. const sanitizePromptText = (value, fallback = '') => {
  2346. const clean = typeof value === 'string' ? value.replace(RETRY_PROMPT_PAD_RE, '').trimEnd() : '';
  2347. return (clean || (typeof fallback === 'string' ? fallback.replace(RETRY_PROMPT_PAD_RE, '').trimEnd() : '')).slice(0, 10000);
  2348. };
  2349. const hasDataTransferType = (dataTransfer, type) => !!dataTransfer && Array.from(dataTransfer.types || []).includes(type);
  2350. const normalizeConfigValue = (rawValue, rule, fallbackValue, ctx) => {
  2351. const value = rule.normalize ? rule.normalize(rawValue) : rawValue;
  2352. switch (rule.type) {
  2353. case 'bool':
  2354. return typeof value === 'boolean' ? value : !!fallbackValue;
  2355. case 'int': {
  2356. const min = resolveSchemaRule(rule.min ?? Number.MIN_SAFE_INTEGER, ctx);
  2357. const max = resolveSchemaRule(rule.max ?? Number.MAX_SAFE_INTEGER, ctx);
  2358. return clampInt(value, min, max, fallbackValue);
  2359. }
  2360. case 'intString': {
  2361. const min = resolveSchemaRule(rule.min ?? Number.MIN_SAFE_INTEGER, ctx);
  2362. const max = resolveSchemaRule(rule.max ?? Number.MAX_SAFE_INTEGER, ctx);
  2363. return String(clampInt(value, min, max, fallbackValue));
  2364. }
  2365. case 'enum': {
  2366. const options = resolveSchemaRule(rule.options || [], ctx);
  2367. const allowed = Array.isArray(options) ? options : [];
  2368. return allowed.includes(value) ? value : fallbackValue;
  2369. }
  2370. default:
  2371. return value ?? fallbackValue;
  2372. }
  2373. };
  2374. const normalizeConfig = (raw, fallback = null) => {
  2375. const source = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
  2376. const out = {};
  2377. for (const [key, rule] of Object.entries(CONFIG_SCHEMA)) {
  2378. const ctx = { source, out, fallback };
  2379. const fallbackValue = fallback?.[key] ?? resolveSchemaRule(rule.default, ctx);
  2380. out[key] = normalizeConfigValue(source[key], rule, fallbackValue, ctx);
  2381. }
  2382. if (out.retryMaxMs < out.retryMinMs) out.retryMaxMs = out.retryMinMs;
  2383. return out;
  2384. };
  2385. const CONFIG_DEFAULTS = normalizeConfig({});
  2386. const getConfigSnapshot = () => Object.fromEntries([...configKeys].map(key => [key, STATE[key]]));
  2387. const mapStore = (raw, fallback, fn) => {
  2388. const base = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
  2389. return Object.fromEntries(Object.keys(fallback).map(k => [k, fn(base[k], fallback[k])]));
  2390. };
  2391. const createSafeStore = (raw, schema) => {
  2392. if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return typeof schema === 'function' ? schema() : schema;
  2393. const out = Array.isArray(schema) ? [] : {};
  2394. for (const [key, typeRule] of Object.entries(schema)) {
  2395. if (Array.isArray(out)) {
  2396. if (!Array.isArray(raw[key])) continue;
  2397. out.push(...raw[key].map(item => typeof typeRule === 'function' ? typeRule(item) : item).filter(Boolean));
  2398. } else if (typeof typeRule === 'function') {
  2399. out[key] = typeRule(raw[key]);
  2400. } else if (typeRule && typeof typeRule === 'object' && !Array.isArray(typeRule)) {
  2401. out[key] = createSafeStore(raw[key], typeRule);
  2402. } else {
  2403. out[key] = raw[key] !== undefined && raw[key] !== null ? raw[key] : typeRule;
  2404. }
  2405. }
  2406. return out;
  2407. };
  2408. const sanitizePromptStore = (raw, fallback = DEFAULT_PROMPTS) => mapStore(raw, fallback, (v, fb) => normalizePromptDraft(v, fb || ''));
  2409. const sanitizeFavPromptStore = (raw, fallback = DEFAULT_FAV_PROMPTS) => mapStore(raw, fallback, (v, fb) => Array.isArray(v) ? v.filter(x => typeof x === 'string') : [...fb]);
  2410. const sanitizeRefPromptStore = (raw, fallback = {}) => {
  2411. if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return { ...fallback };
  2412. 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()))]));
  2413. };
  2414. const sanitizeIdListStore = (raw) => Array.isArray(raw) ? [...new Set(raw.map(String).filter(Boolean))] : [];
  2415. const sanitizeStatusLinesStore = (raw) => createSafeStore(raw, { text: '', color: '', style: '' });
  2416. const sanitizeTextChatMessage = (raw) => {
  2417. if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
  2418. const role = raw.role === 'assistant' ? 'assistant' : 'user';
  2419. const text = raw.text || '';
  2420. return text ? { role, text, timestamp: raw.timestamp || Date.now() } : null;
  2421. };
  2422. const sanitizeTextConversationStore = (raw) => Array.isArray(raw)
  2423. ? raw.map(sanitizeTextChatMessage).filter(Boolean)
  2424. : [];
  2425. const getTextSessionStoreKey = (id) => `${TEXT_SESSION_KEY_PREFIX}${String(id)}`;
  2426. const getTextSessionMessagesFromItemAsync = async (item) => {
  2427. const direct = sanitizeTextConversationStore(item?.textConversation);
  2428. if (direct.length > 0) return direct;
  2429. const heavy = await LazyTextStore.get(item?.id);
  2430. const prompt = heavy?.prompt || item?.prompt || '';
  2431. const responseText = heavy?.responseText || item?.responseText || '';
  2432. const fallback = [];
  2433. if (prompt) fallback.push({ role: 'user', text: prompt, timestamp: item.timestamp || Date.now() });
  2434. if (responseText) fallback.push({ role: 'assistant', text: responseText, timestamp: item.timestamp || Date.now() });
  2435. return fallback;
  2436. };
  2437. const getFirstUserText = (messages = []) => messages.find(message => message.role === 'user')?.text || '';
  2438. const getLastAssistantText = (messages = []) => {
  2439. for (let i = messages.length - 1; i >= 0; i--) {
  2440. if (messages[i]?.role === 'assistant') return messages[i].text || '';
  2441. }
  2442. return '';
  2443. };
  2444. const getTextSessionTurnCount = (messages = []) => messages.reduce((count, message) => count + (message.role === 'assistant' ? 1 : 0), 0);
  2445. const buildTextSessionTranscript = (messages = [], model = '') => {
  2446. const header = model ? `Model: ${model}` : '';
  2447. const body = messages.map(message => `${message.role === 'assistant' ? 'Assistant' : 'User'}:\n${message.text}`).join('\n\n');
  2448. return [header, body].filter(Boolean).join('\n\n');
  2449. };
  2450. const summarizeTextSessionItem = (item, messages = [], { updatedAt = Date.now(), model = item?.textModel || STATE.textModel } = {}) => {
  2451. const cleanMessages = sanitizeTextConversationStore(Array.isArray(messages) ? [...messages] : messages);
  2452. const prompt = Pipeline.normalizePrompt(getFirstUserText(cleanMessages));
  2453. const responseText = getLastAssistantText(cleanMessages);
  2454. item.shortPrompt = prompt.substring(0, 100);
  2455. item.textModel = typeof model === 'string' && model ? model : (item.textModel || STATE.textModel || 'grok-4.20');
  2456. item.textTurnCount = getTextSessionTurnCount(cleanMessages);
  2457. item.timestamp = updatedAt;
  2458. LazyTextStore.set(item.id, { prompt, responseText, textChatMessages: null });
  2459. return cleanMessages;
  2460. };
  2461. const sanitizeCurrentPanelSessionItem = (raw) => {
  2462. if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
  2463. const id = String(raw.id);
  2464. if (!id) return null;
  2465. const fullPrompt = raw.prompt ? Pipeline.normalizePrompt(raw.prompt) : '';
  2466. return {
  2467. id,
  2468. timestamp: raw.timestamp || Date.now(),
  2469. type: raw.type || 'image',
  2470. mimeType: raw.mimeType || '',
  2471. ratio: raw.ratio || 1,
  2472. originalUrl: raw.originalUrl || null,
  2473. filename: raw.filename || 'Pending...',
  2474. downloaded: Boolean(raw.downloaded),
  2475. prompt: fullPrompt,
  2476. shortPrompt: fullPrompt.substring(0, 100),
  2477. responseText: raw.responseText || null,
  2478. textModel: raw.textModel || null,
  2479. textTurnCount: raw.textTurnCount || 0,
  2480. lqip: raw.lqip || null,
  2481. isUploadedRef: Boolean(raw.isUploadedRef),
  2482. isPending: Boolean(raw.isPending),
  2483. reqId: raw.reqId || null,
  2484. taskId: raw.taskId || null,
  2485. refSourceId: raw.refSourceId != null ? String(raw.refSourceId) : null,
  2486. _statusData: sanitizeStatusLinesStore(raw._statusData),
  2487. persist: false
  2488. };
  2489. };
  2490. const sanitizeCurrentPanelSessionStore = (raw) => {
  2491. if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
  2492. return {
  2493. schemaVersion: 1,
  2494. order: sanitizeIdListStore(raw.order),
  2495. items: Array.isArray(raw.items) ? raw.items.map(sanitizeCurrentPanelSessionItem).filter(Boolean) : []
  2496. };
  2497. };
  2498. 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])));
  2499. const createEmptyPanelOrderStore = () => Object.fromEntries(PANEL_ACTION_KEYS.map(key => [key, []]));
  2500.  
  2501. const sanitizePanelKey = (raw, fallback = 'gen_image') => {
  2502. const key = String(raw || '').trim();
  2503. return PANEL_ACTION_KEYS.includes(key) ? key : fallback;
  2504. };
  2505. const REF_VIEW_SCHEMA_VERSION = 1;
  2506. const REF_VIEW_MODES = ['multi', 'queue'];
  2507. const ACTION_LOOP_STATE_KEY = {
  2508. gen_image: 'loopsGenImage',
  2509. gen_video: 'loopsGenVideo',
  2510. edit_video: 'loopsEditVideo',
  2511. extend_video: 'loopsExtendVideo'
  2512. };
  2513. const normalizeRefModeForAction = (action = STATE?.action || 'gen_image', refMode = STATE?.refMode || 'multi') => {
  2514. const safeAction = sanitizePanelKey(action);
  2515. if (safeAction === 'gen_text' || safeAction === 'gen_audio') return 'multi';
  2516. return String(refMode) === 'queue' ? 'queue' : 'multi';
  2517. };
  2518. const getLoopsStateKey = (action = STATE?.action || 'gen_image') => ACTION_LOOP_STATE_KEY[sanitizePanelKey(action)] || null;
  2519. const getLoopsValue = (action = STATE?.action || 'gen_image', source = STATE) => {
  2520. const key = getLoopsStateKey(action);
  2521. if (key && source && source[key] != null) return String(source[key]);
  2522. return String(source?.loops ?? '0');
  2523. };
  2524. const setLoopsValue = (action = STATE?.action || 'gen_image', value = '0') => {
  2525. const key = getLoopsStateKey(action);
  2526. const normalized = String(clampInt(value, 0, 99, 0));
  2527. if (key) STATE[key] = normalized;
  2528. STATE.loops = normalized;
  2529. return normalized;
  2530. };
  2531. const getRefPoolTypeForAction = (action = STATE?.action || 'gen_image') => ['edit_video', 'extend_video'].includes(sanitizePanelKey(action)) ? 'video' : 'image';
  2532. const getRefViewKey = (action = STATE?.action || 'gen_image', refMode = STATE?.refMode || 'multi') => `${sanitizePanelKey(action)}:${normalizeRefModeForAction(action, refMode)}`;
  2533. const REF_VIEW_KEYS = [...new Set(PANEL_ACTION_KEYS.flatMap(action => REF_VIEW_MODES.map(mode => getRefViewKey(action, mode))))];
  2534. const createEmptyRefViewEntry = () => ({ order: [], active: {}, waitUntil: {} });
  2535. const cloneRefViewEntry = (entry = createEmptyRefViewEntry()) => ({
  2536. order: sanitizeIdListStore(entry.order),
  2537. active: Object.fromEntries(Object.entries(entry.active || {}).filter(([, val]) => val === true).map(([id]) => [String(id), true])),
  2538. waitUntil: Object.fromEntries(Object.entries(entry.waitUntil || {}).filter(([, val]) => Number.isFinite(val) && val > 0).map(([id, val]) => [String(id), val]))
  2539. });
  2540. const createEmptyRefViewStore = () => Object.fromEntries(REF_VIEW_KEYS.map(key => [key, createEmptyRefViewEntry()]));
  2541. const sanitizeRefViewEntry = (raw) => {
  2542. const entry = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
  2543. const order = sanitizeIdListStore(entry.order);
  2544. const orderSet = new Set(order);
  2545. const activeKeys = Object.entries(entry.active || {}).filter(([id, val]) => val === true && orderSet.has(String(id))).map(([id]) => [String(id), true]);
  2546. const active = Object.fromEntries(activeKeys);
  2547. const waitKeys = Object.entries(entry.waitUntil || {}).filter(([id, val]) => orderSet.has(String(id)) && Number.isFinite(val) && val > 0).map(([id, val]) => [String(id), val]);
  2548. const waitUntil = Object.fromEntries(waitKeys);
  2549. return { order, active, waitUntil };
  2550. };
  2551. const sanitizeRefViewStore = (raw) => {
  2552. const source = raw && typeof raw === 'object' && !Array.isArray(raw) && raw.schemaVersion === REF_VIEW_SCHEMA_VERSION ? raw.views : null;
  2553. const next = createEmptyRefViewStore();
  2554. REF_VIEW_KEYS.forEach(key => {
  2555. next[key] = sanitizeRefViewEntry(source?.[key]);
  2556. });
  2557. return next;
  2558. };
  2559. const serializeRefViewStore = (store = STATE?.refViews || createEmptyRefViewStore()) => ({
  2560. schemaVersion: REF_VIEW_SCHEMA_VERSION,
  2561. views: Object.fromEntries(REF_VIEW_KEYS.map(key => [key, cloneRefViewEntry(store[key])]))
  2562. });
  2563. const getRefViewKeysForPoolType = (poolType = 'image') => REF_VIEW_KEYS.filter(key => getRefPoolTypeForAction(key.split(':')[0]) === poolType);
  2564. const isMp4ReferenceItem = (item) => !!item && item.type === 'video' && (!item.mimeType || /mp4/i.test(String(item.mimeType)));
  2565. const isRefCompatibleWithPoolType = (item, poolType = 'image') => poolType === 'video' ? isMp4ReferenceItem(item) : item?.type === 'image';
  2566. const createDefaultRefViewsFromItems = () => {
  2567. const next = createEmptyRefViewStore();
  2568. const imageIds = [];
  2569. const videoIds = [];
  2570. STATE.items.forEach((item, id) => {
  2571. if (!item || !item.isUploadedRef) return;
  2572. if (isRefCompatibleWithPoolType(item, 'image')) imageIds.push(String(id));
  2573. else if (isRefCompatibleWithPoolType(item, 'video')) videoIds.push(String(id));
  2574. });
  2575. getRefViewKeysForPoolType('image').forEach(key => { next[key].order = [...imageIds]; });
  2576. getRefViewKeysForPoolType('video').forEach(key => { next[key].order = [...videoIds]; });
  2577. return next;
  2578. };
  2579. const sanitizePanelOrderStore = (raw) => {
  2580. const source = raw && typeof raw === 'object' && !Array.isArray(raw) && raw.schemaVersion === PANEL_STORE_SCHEMA_VERSION ? raw.panels : null;
  2581. const next = createEmptyPanelOrderStore();
  2582. PANEL_ACTION_KEYS.forEach(key => {
  2583. next[key] = sanitizeIdListStore(source?.[key]);
  2584. });
  2585. return next;
  2586. };
  2587. const sanitizePanelSessionMapStore = (raw) => {
  2588. const source = raw && typeof raw === 'object' && !Array.isArray(raw) && raw.schemaVersion === PANEL_STORE_SCHEMA_VERSION ? raw.panels : null;
  2589. const panels = {};
  2590. PANEL_ACTION_KEYS.forEach(key => {
  2591. const safeSession = sanitizeCurrentPanelSessionStore(source?.[key]);
  2592. if (safeSession && (safeSession.order.length > 0 || safeSession.items.length > 0)) panels[key] = safeSession;
  2593. });
  2594. return { schemaVersion: PANEL_STORE_SCHEMA_VERSION, panels };
  2595. };
  2596. const parseImportedState = (raw, fallback = getConfigSnapshot()) => {
  2597. if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new Error('Invalid import payload');
  2598. if (raw.schemaVersion !== APP_CONFIG.STORAGE.EXPORT_SCHEMA_VERSION) throw new Error(`Unsupported import schema: ${raw.schemaVersion ?? 'unknown'}`);
  2599. if (!raw.config || typeof raw.config !== 'object' || Array.isArray(raw.config)) throw new Error('Invalid config payload');
  2600. const prompts = sanitizePromptStore(raw.prompts, STATE.prompts);
  2601. return {
  2602. config: normalizeConfig(raw.config, fallback),
  2603. prompts,
  2604. promptDrafts: sanitizePromptStore(raw.promptDrafts, prompts),
  2605. refPrompts: sanitizeRefPromptStore(raw.refPrompts, STATE.refPrompts),
  2606. favPrompts: sanitizeFavPromptStore(raw.favPrompts, STATE.favPrompts)
  2607. };
  2608. };
  2609.  
  2610. const triggerMediaQueueWake = () => {
  2611. if (!STATE.isMounted || !isRuntimeBusy('media')) return;
  2612. scheduleRuntimeWake('media');
  2613. updateStatusUI();
  2614. };
  2615.  
  2616. const RUNTIME_STATE_KEYS = new Set(['isGenerating', 'generationToken', 'successCount']);
  2617.  
  2618. const AppState = {
  2619. isMounted: new ReactiveVar(false),
  2620. items: new ReactiveVar(new Map()),
  2621. refViews: new ReactiveVar(createEmptyRefViewStore()),
  2622. refOrder: new ReactiveVar([]),
  2623. panelOrders: new ReactiveVar(createEmptyPanelOrderStore()),
  2624. currentOrder: new ReactiveVar([]),
  2625. textChatMessages: new ReactiveVar([]),
  2626. activeTextSessionId: new ReactiveVar(null),
  2627. activeTextSessionTmp: new ReactiveVar(undefined),
  2628. currentPanelSessionTmp: new ReactiveVar(undefined),
  2629. historyOrder: new ReactiveVar([]),
  2630. activeLightboxId: new ReactiveVar(null),
  2631. activeRefPromptId: new ReactiveVar(null),
  2632. activeRefPromptAction: new ReactiveVar(null),
  2633. activeRefPromptDraft: new ReactiveVar(''),
  2634. prompts: new ReactiveVar(sanitizePromptStore(DEFAULT_PROMPTS)),
  2635. promptDrafts: new ReactiveVar(sanitizePromptStore(DEFAULT_PROMPTS)),
  2636. favPrompts: new ReactiveVar(sanitizeFavPromptStore(DEFAULT_FAV_PROMPTS)),
  2637. refPrompts: new ReactiveVar({}),
  2638. pendingDoneNotice: new ReactiveVar(false),
  2639. timings: new ReactiveVar(undefined),
  2640. refViewsTmp: new ReactiveVar(undefined),
  2641. panelOrdersTmp: new ReactiveVar(undefined),
  2642. action: new ReactiveVar('gen_image'),
  2643. currentAr: new ReactiveVar('Auto'),
  2644. batchSize: new ReactiveVar('2'),
  2645. duration: new ReactiveVar('5'),
  2646. loops: new ReactiveVar('0'),
  2647. loopsGenImage: new ReactiveVar('0'),
  2648. loopsGenVideo: new ReactiveVar('0'),
  2649. loopsEditVideo: new ReactiveVar('0'),
  2650. loopsExtendVideo: new ReactiveVar('0'),
  2651. resImg: new ReactiveVar('2k'),
  2652. resVid: new ReactiveVar('720p'),
  2653. refMode: new ReactiveVar('multi'),
  2654. concurrentSessions: new ReactiveVar(1),
  2655. rateLimitDelay: new ReactiveVar(60),
  2656. retryMinMs: new ReactiveVar(2000),
  2657. retryMaxMs: new ReactiveVar(4000),
  2658. autoRetryVideo: new ReactiveVar(true),
  2659. referenceARTrick: new ReactiveVar(false),
  2660. ecoMode: new ReactiveVar(false),
  2661. autoDownload: new ReactiveVar(false),
  2662. embedImagePrompt: new ReactiveVar(true),
  2663. optimize2k: new ReactiveVar(false),
  2664. leftCollapsed: new ReactiveVar(false),
  2665. rightCollapsed: new ReactiveVar(false),
  2666. leftWidth: new ReactiveVar(260),
  2667. rightWidth: new ReactiveVar(320),
  2668. imgBaseMs: new ReactiveVar(3000),
  2669. voice: new ReactiveVar('eve'),
  2670. language: new ReactiveVar('auto'),
  2671. ttsQuality: new ReactiveVar('128000'),
  2672. textModel: new ReactiveVar('grok-4.20'),
  2673. textReasoningEffort: new ReactiveVar('medium'),
  2674. textWebSearch: new ReactiveVar('on'),
  2675. textXSearch: new ReactiveVar('on'),
  2676. customTextModels: new ReactiveVar([])
  2677. };
  2678.  
  2679. function scheduleConfigSave() {
  2680. _configSaveTask?.abort();
  2681. _configSaveTask = scheduleTask(() => {
  2682. _configSaveTask = null;
  2683. localStorage.setItem(APP_CONFIG.STORAGE.CONFIG_KEY, JSON.stringify(normalizeConfig(getConfigSnapshot(), CONFIG_DEFAULTS)));
  2684. }, 250);
  2685. }
  2686.  
  2687. const _appAbortController = new AbortController();
  2688.  
  2689. const STATE = new Proxy(AppState, {
  2690. get(target, prop) {
  2691. if (RUNTIME_STATE_KEYS.has(prop)) return getCurrentRuntime(target.action.value)[prop];
  2692. const rv = target[prop];
  2693. return rv instanceof ReactiveVar ? rv.value : rv;
  2694. },
  2695. set(target, prop, value) {
  2696. if (prop === 'isGenerating') {
  2697. const runtime = getCurrentRuntime(target.action.value || 'gen_image');
  2698. setRuntimeField(runtime, prop, value);
  2699. return true;
  2700. }
  2701. if (prop === 'refMode') {
  2702. const lane = JobManager.getLane(target.action.value || 'gen_image');
  2703. if (JobManager.getRuntime(lane).isGenerating) {
  2704. UI.showToast('Reference mode is locked during active generation', 'warn');
  2705. return true;
  2706. }
  2707. }
  2708. if (RUNTIME_STATE_KEYS.has(prop)) {
  2709. const runtime = getCurrentRuntime(target.action.value || 'gen_image');
  2710. const changed = setRuntimeField(runtime, prop, value);
  2711. if (changed && prop !== 'isGenerating' && runtime === getCurrentRuntime(target.action.value || 'gen_image') && target.isMounted.value) {
  2712. updateStatusUI();
  2713. }
  2714. return true;
  2715. }
  2716. if (prop in target) {
  2717. const rv = target[prop];
  2718. if (rv instanceof ReactiveVar) rv.value = value;
  2719. else target[prop] = new ReactiveVar(value);
  2720. } else {
  2721. target[prop] = new ReactiveVar(value);
  2722. }
  2723. if (target.isMounted.value) {
  2724. if (prop === 'currentOrder') {
  2725. syncActivePanelOrderState();
  2726. PanelManager.update(STATE.currentOrder, STATE.items, 'current');
  2727. persistPanelOrderStore();
  2728. scheduleCurrentPanelSessionSave();
  2729. } else if (prop === 'concurrentSessions') {
  2730. triggerMediaQueueWake();
  2731. }
  2732. }
  2733. if (configKeys.has(prop)) scheduleConfigSave();
  2734. return true;
  2735. }
  2736. });
  2737.  
  2738.  
  2739. (function bindAppStateHandlers() {
  2740. AppState.action.bind(currentAction => {
  2741. if (!AppState.isMounted.value) return;
  2742. syncVisibleRefStateFromView();
  2743. renderRefList();
  2744. updateActionUI();
  2745. toggleGenerateButton(STATE.isGenerating);
  2746. updateDirtyStateUI();
  2747. syncWipeButtonState();
  2748. updateStatusUI();
  2749. syncRefCardsPromptFlags(currentAction);
  2750. }, _appAbortController.signal);
  2751.  
  2752.  
  2753. AppState.ecoMode.bind(isEco => {
  2754. if (!AppState.isMounted.value) return;
  2755. document.body.classList.toggle('eco-mode', isEco);
  2756. applyMemoryPressureProfile(isEco ? 'eco-on' : 'eco-off');
  2757. }, _appAbortController.signal);
  2758.  
  2759. AppState.leftCollapsed.bind(collapsed => {
  2760. if (!AppState.isMounted.value) return;
  2761. DOM.colLeft.classList.toggle('is-collapsed', collapsed);
  2762. document.getElementById('btn-collapse-left').setAttribute('aria-expanded', !collapsed);
  2763. requestAnimationFrame(() => syncLayoutWidths());
  2764. }, _appAbortController.signal);
  2765.  
  2766. AppState.rightCollapsed.bind(collapsed => {
  2767. if (!AppState.isMounted.value) return;
  2768. DOM.colRight.classList.toggle('is-collapsed', collapsed);
  2769. document.getElementById('btn-collapse-right').setAttribute('aria-expanded', !collapsed);
  2770. requestAnimationFrame(() => syncLayoutWidths());
  2771. }, _appAbortController.signal);
  2772.  
  2773. AppState.refMode.bind(() => {
  2774. if (!AppState.isMounted.value) return;
  2775. syncVisibleRefStateFromView();
  2776. renderRefList();
  2777. renderRefMode();
  2778. }, _appAbortController.signal);
  2779.  
  2780. AppState.currentAr.bind(() => {
  2781. if (!AppState.isMounted.value) return;
  2782. syncDOMControls('currentAr');
  2783. PanelManager.update(STATE.currentOrder, STATE.items, 'current');
  2784. }, _appAbortController.signal);
  2785.  
  2786. AppState.loops.bind(() => { if (AppState.isMounted.value) triggerMediaQueueWake(); }, _appAbortController.signal);
  2787. AppState.loopsGenImage.bind(() => { if (AppState.isMounted.value) triggerMediaQueueWake(); }, _appAbortController.signal);
  2788. AppState.loopsGenVideo.bind(() => { if (AppState.isMounted.value) triggerMediaQueueWake(); }, _appAbortController.signal);
  2789. AppState.loopsEditVideo.bind(() => { if (AppState.isMounted.value) triggerMediaQueueWake(); }, _appAbortController.signal);
  2790. AppState.loopsExtendVideo.bind(() => { if (AppState.isMounted.value) triggerMediaQueueWake(); }, _appAbortController.signal);
  2791. })();
  2792.  
  2793. const JobManager = {
  2794. getLane(action = STATE.action) { return GenerationRegistry.getLane(action); },
  2795. getRuntime(actionOrLane = STATE.action) { return GenerationRegistry.getRuntime(actionOrLane); },
  2796. get tasks() { return this.getRuntime().tasks; },
  2797. get taskPromises() { return this.getRuntime().taskPromises; },
  2798. get progress() { return this.getRuntime().progress; },
  2799. getAllTasks() { return GenerationRegistry.getAllTasks(); },
  2800. findTask(taskId) { return GenerationRegistry.findTask(taskId); },
  2801. findTasksByRef(refId) {
  2802. const key = String(refId);
  2803. return GenerationRegistry.getAllRuntimes().flatMap(runtime => [...runtime.tasks.values()].filter(task => task.refIds.includes(key)));
  2804. },
  2805. abortAll() {
  2806. GenerationRegistry.getAllRuntimes().forEach(runtime => runtime.tasks.forEach(task => task.controller.abort()));
  2807. },
  2808. resetAll() {
  2809. GenerationRegistry.resetAll();
  2810. }
  2811. };
  2812.  
  2813. function syncChatActionBarState() {
  2814. const container = DOM.chatScroll || document.getElementById('chat-scroll');
  2815. if (!container) return;
  2816. const aiRows = [...container.querySelectorAll('.chat-msg-row.ai')];
  2817. const latestInteractiveRow = [...aiRows].reverse().find(row => !row.classList.contains('status-msg')) || null;
  2818. aiRows.forEach(row => {
  2819. const newChatBtn = row.querySelector('[data-chat-action="new-chat"]');
  2820. const copyBtn = row.querySelector('[data-chat-action="copy"]');
  2821. const repeatBtn = row.querySelector('[data-chat-action="repeat"]');
  2822. const isStatus = row.classList.contains('status-msg');
  2823. if (copyBtn) copyBtn.hidden = isStatus;
  2824. if (repeatBtn) repeatBtn.hidden = isStatus;
  2825. if (newChatBtn) newChatBtn.hidden = isStatus || row !== latestInteractiveRow;
  2826. });
  2827. }
  2828.  
  2829. function scrollTextChatToBottom({ smooth = false } = {}) {
  2830. const chatContainer = DOM.chatView || document.getElementById('xai-chat-container');
  2831. if (!chatContainer) return;
  2832. if (_chatScrollRaf) cancelAnimationFrame(_chatScrollRaf);
  2833. _chatScrollRaf = requestAnimationFrame(() => {
  2834. _chatScrollRaf = 0;
  2835. if (smooth) chatContainer.scrollTo({ top: chatContainer.scrollHeight, behavior: 'smooth' });
  2836. else chatContainer.scrollTop = chatContainer.scrollHeight;
  2837. });
  2838. }
  2839.  
  2840. function updateRenderedChatMessage(msgEl, text, isStatus = false, preParsedHtml = null) {
  2841. if (!msgEl) return;
  2842. if (msgEl._lastText === text) return;
  2843. msgEl._lastText = text;
  2844. msgEl.classList.toggle('status-msg', !!isStatus);
  2845. const bubble = msgEl.querySelector('.chat-bubble-ui');
  2846. if (bubble) {
  2847. if (isStatus) {
  2848. bubble.setHTMLUnsafe(`<span class="chat-status-text">${escapeHtml(text)}</span>`);
  2849. } else {
  2850. bubble.setHTMLUnsafe(preParsedHtml !== null ? preParsedHtml : parseMarkdown(text));
  2851. }
  2852. bubble.classList.toggle('is-status', !!isStatus);
  2853. }
  2854. const toolbar = msgEl.querySelector('.chat-msg-toolbar');
  2855. if (toolbar) {
  2856. toolbar.hidden = false;
  2857. toolbar.style.visibility = isStatus ? 'hidden' : 'visible';
  2858. toolbar.style.height = isStatus ? '30px' : '';
  2859. }
  2860. syncChatActionBarState();
  2861. }
  2862.  
  2863. function patchMessage(msgIdOrNode, text, isStatus = false, preParsedHtml = null) {
  2864. const msgEl = typeof msgIdOrNode === 'string' ? document.querySelector(`[data-msg-id="${msgIdOrNode}"]`) : msgIdOrNode;
  2865. if (msgEl) updateRenderedChatMessage(msgEl, text, isStatus, preParsedHtml);
  2866. }
  2867.  
  2868. function addMessage(role, text, isStatus = false, userPrompt = '') {
  2869. const container = DOM.chatScroll || document.getElementById('chat-scroll');
  2870. if (!container) return null;
  2871.  
  2872. const isUser = role === 'user';
  2873. const frag = document.createRange().createContextualFragment(buildChatMsgHtml({
  2874. rowClass: `${isUser ? 'user' : 'ai'}${isStatus ? ' status-msg' : ''}`,
  2875. msgId: '',
  2876. avatarDisplay: isUser ? 'none' : '',
  2877. avatarText: isUser ? '' : '\u{1D54F}',
  2878. bubbleWrapperClass: isUser ? '' : 'chat-bubble-wrapper--ai',
  2879. repeatUserPrompt: userPrompt || ''
  2880. }));
  2881. const row = frag.firstChild;
  2882. if (isUser) {
  2883. const toolbar = row.querySelector('.chat-msg-toolbar');
  2884. if (toolbar) toolbar.remove();
  2885. } else if (isStatus) {
  2886. const toolbar = row.querySelector('.chat-msg-toolbar');
  2887. if (toolbar) {
  2888. toolbar.hidden = false;
  2889. toolbar.style.visibility = 'hidden';
  2890. toolbar.style.height = '30px';
  2891. }
  2892. }
  2893.  
  2894. container.appendChild(frag);
  2895. updateRenderedChatMessage(row, text, isStatus);
  2896. return row;
  2897. }
  2898.  
  2899. function renderTextChatMessage(role, text, isStatus = false, userPrompt = '') {
  2900. const msgId = Date.now() + Math.random();
  2901. try {
  2902. const msgEl = addMessage(role, text, isStatus, userPrompt);
  2903. if (msgEl) msgEl.dataset.msgId = msgId;
  2904. } catch (e) {}
  2905. return msgId;
  2906. }
  2907.  
  2908. function safeRenderResponse(text, role) {
  2909. try {
  2910. addMessage(role === 'user' ? 'user' : 'assistant', text);
  2911. } catch (e) {
  2912. console.error('[RENDER] Chat container failed:', e);
  2913. }
  2914. }
  2915.  
  2916. function restoreChatFromSession() {
  2917. const container = DOM.chatScroll || document.getElementById('chat-scroll');
  2918. if (container) container.innerHTML = '';
  2919. if (Array.isArray(STATE.textChatMessages) && STATE.textChatMessages.length > 0) {
  2920. let lastUserText = '';
  2921. STATE.textChatMessages.forEach(message => {
  2922. if (message.role === 'user') lastUserText = message.text;
  2923. if (message?.text) renderTextChatMessage(message.role === 'assistant' ? 'assistant' : 'user', message.text, false, lastUserText);
  2924. });
  2925. }
  2926. const textRuntime = JobManager.getRuntime('text');
  2927. const liveTasks = [...textRuntime.tasks.values()]
  2928. .filter(task => sanitizePanelKey(task?.action, textRuntime.activeAction || 'gen_text') === 'gen_text')
  2929. .sort((a, b) => (a?.createdAt || 0) - (b?.createdAt || 0));
  2930. liveTasks.forEach(task => {
  2931. const promptText = parseDynamicPrompt(sanitizePromptText(task?.promptText || ''));
  2932. if (promptText) task.chatUserMsgId = renderTextChatMessage('user', promptText);
  2933. const liveText = sanitizePromptText(task?.streamText || '') || task?.longStatus || task?.shortStatus || 'Working...';
  2934. task.chatStatusMsgId = renderTextChatMessage('assistant', liveText, !task?.chatHasStreamText, promptText);
  2935. if (task?.chatHasStreamText && task.chatStatusMsgId) {
  2936. document.querySelector(`[data-msg-id="${task.chatStatusMsgId}"]`)?.querySelector('.chat-msg-toolbar')?.setAttribute('hidden', '');
  2937. }
  2938. });
  2939. syncChatActionBarState();
  2940. scrollTextChatToBottom({ smooth: true });
  2941. }
  2942. async function copyChatMessageText(msgEl) {
  2943. const text = msgEl?._lastText || msgEl?.querySelector('.chat-bubble-ui')?.textContent || '';
  2944. if (!text) return;
  2945. try {
  2946. await navigator.clipboard.writeText(text);
  2947. UI.showToast('Response copied', 'success');
  2948. } catch (e) {
  2949. UI.showToast('Copy failed', 'error');
  2950. }
  2951. }
  2952. function handleChatAction(action, msgEl) {
  2953. if (!action) return;
  2954. if (action === 'repeat') {
  2955. const repeatBtn = msgEl.querySelector('[data-chat-action="repeat"]');
  2956. const userPrompt = repeatBtn?.dataset.userPrompt || '';
  2957. if (userPrompt) {
  2958. setEditorPromptValue(userPrompt);
  2959. if (DOM.prompt) DOM.prompt.focus();
  2960. }
  2961. return;
  2962. }
  2963. if (action === 'copy') {
  2964. void copyChatMessageText(msgEl);
  2965. return;
  2966. }
  2967. if (action === 'new-chat') {
  2968. if (isRuntimeBusy('text')) {
  2969. UI.showToast('Wait until the response finishes', 'warn');
  2970. return;
  2971. }
  2972. resetTextChatSession({ persist: true });
  2973. }
  2974. }
  2975.  
  2976. const createInitialState = () => ({
  2977. isMounted: false,
  2978. items: new Map(),
  2979. refViews: createEmptyRefViewStore(),
  2980. refOrder: [],
  2981. panelOrders: createEmptyPanelOrderStore(),
  2982. currentOrder: [],
  2983. textChatMessages: [],
  2984. activeTextSessionId: null,
  2985. activeTextSessionTmp: undefined,
  2986. currentPanelSessionTmp: undefined,
  2987. historyOrder: [],
  2988. activeLightboxId: null,
  2989. activeRefPromptId: null,
  2990. activeRefPromptAction: null,
  2991. activeRefPromptDraft: '',
  2992. prompts: sanitizePromptStore(DEFAULT_PROMPTS),
  2993. promptDrafts: sanitizePromptStore(DEFAULT_PROMPTS),
  2994. favPrompts: sanitizeFavPromptStore(DEFAULT_FAV_PROMPTS),
  2995. refPrompts: {},
  2996. pendingDoneNotice: false,
  2997. timings: undefined,
  2998. refViewsTmp: undefined,
  2999. panelOrdersTmp: undefined
  3000. });
  3001.  
  3002. function resetState() {
  3003. const initialState = createInitialState();
  3004. Object.assign(initialState, normalizeConfig(parseJsonSafe(localStorage.getItem(APP_CONFIG.STORAGE.CONFIG_KEY), {}), CONFIG_DEFAULTS));
  3005. GenerationRegistry.resetAll();
  3006. for (const key in initialState) {
  3007. if (key in AppState) {
  3008. AppState[key].value = initialState[key];
  3009. }
  3010. }
  3011. Object.keys(DOM).forEach(k => delete DOM[k]);
  3012. }
  3013. resetState();
  3014.  
  3015. const MediaCache = {
  3016. _inFlight: new Map(),
  3017.  
  3018. async getFile(id, type) {
  3019. const key = `${id}_${type}`;
  3020. if (this._inFlight.has(key)) return this._inFlight.get(key);
  3021.  
  3022. const { promise, resolve } = Promise.withResolvers();
  3023. this._inFlight.set(key, promise);
  3024.  
  3025. try {
  3026. const res = await StorageClient.loadOpfsMedia(id, type);
  3027. resolve(res ? res.file : null);
  3028. return res ? res.file : null;
  3029. } catch(e) {
  3030. resolve(null); return null;
  3031. } finally {
  3032. this._inFlight.delete(key);
  3033. }
  3034. },
  3035.  
  3036. };
  3037. const RefPayloadCache = {
  3038. dataUrls: new Map(),
  3039. maxEntries() { return isMemoryConstrainedMode() ? 2 : 8; },
  3040. _touch(key) {
  3041. const entry = this.dataUrls.get(key);
  3042. if (!entry) return null;
  3043. this.dataUrls.delete(key);
  3044. this.dataUrls.set(key, entry);
  3045. return entry;
  3046. },
  3047. trim() {
  3048. const limit = this.maxEntries();
  3049. if (this.dataUrls.size <= limit) return;
  3050. for (const [key, entry] of this.dataUrls.entries()) {
  3051. if (this.dataUrls.size <= limit) break;
  3052. if (!entry?.pending) {
  3053. this.dataUrls.delete(key);
  3054. }
  3055. }
  3056. if (this.dataUrls.size > limit) {
  3057. const firstKey = this.dataUrls.keys().next().value;
  3058. if (firstKey) this.dataUrls.delete(firstKey);
  3059. }
  3060. },
  3061. async get(item) {
  3062. if (!item) return null;
  3063. if (!item.isUploadedRef && item.originalUrl) return item.originalUrl;
  3064.  
  3065. if (item.type === 'video') {
  3066. const res = await StorageClient.loadOpfsMedia(item.id, 'full');
  3067. if (!res || !res.file) return item.originalUrl || null;
  3068. const bytes = await res.file.bytes();
  3069. return `data:${item.mimeType || 'video/mp4'};base64,${bytes.toBase64()}`;
  3070. }
  3071.  
  3072. const key = String(item.id);
  3073. const cached = this._touch(key);
  3074. if (cached) return cached.promise;
  3075. const entry = {
  3076. pending: true,
  3077. promise: (async () => {
  3078. const res = await StorageClient.loadOpfsMedia(item.id, 'full');
  3079. if (!res || !res.file) return item.originalUrl || null;
  3080.  
  3081. const mimeType = item.mimeType || res.mimeType || 'image/jpeg';
  3082. const bytes = await res.file.bytes();
  3083. const base64 = bytes.toBase64();
  3084. return `data:${mimeType};base64,${base64}`;
  3085. })()
  3086. };
  3087. this.dataUrls.set(key, entry);
  3088. const result = await entry.promise;
  3089. entry.pending = false;
  3090. if (!result) this.dataUrls.delete(key);
  3091. else this.trim();
  3092. return result;
  3093. },
  3094. delete(id) { this.dataUrls.delete(String(id)); },
  3095. clear() { this.dataUrls.clear(); }
  3096. };
  3097.  
  3098. const PROMPT_DELIMITER = '\u2063';
  3099.  
  3100. const PromptPipeline = {
  3101. process(text, fallback = '', action = STATE?.action) {
  3102. if (!text) return { api: fallback, storage: fallback };
  3103. const clean = sanitizePromptText(text, fallback);
  3104.  
  3105. let originalText = clean;
  3106. if (clean.includes(PROMPT_DELIMITER)) {
  3107. originalText = clean.split(PROMPT_DELIMITER)[1];
  3108. }
  3109.  
  3110. const apiPrompt = parseDynamicPrompt(originalText);
  3111.  
  3112. if (action !== 'gen_image') {
  3113. return { api: apiPrompt, storage: apiPrompt };
  3114. }
  3115.  
  3116. const textWithoutComments = originalText.replace(/^\s*#[^\n]*\n?/gm, "").trim();
  3117.  
  3118. if (textWithoutComments === apiPrompt && !originalText.includes('{')) {
  3119. return { api: apiPrompt, storage: originalText };
  3120. }
  3121.  
  3122. const commentedApiPrompt = apiPrompt.split('\n').map(line => `# ${line}`).join('\n');
  3123. return { api: apiPrompt, storage: `${commentedApiPrompt}\n${PROMPT_DELIMITER}${originalText}` };
  3124. }
  3125. };
  3126.  
  3127. const Pipeline = {
  3128. getOriginalPrompt(text) {
  3129. if (!text) return '';
  3130. if (text.includes(PROMPT_DELIMITER)) {
  3131. return text.split(PROMPT_DELIMITER)[1].trim();
  3132. }
  3133. return text.trim();
  3134. },
  3135. getDisplayPrompt(text) {
  3136. if (!text) return '';
  3137. if (text.includes(PROMPT_DELIMITER)) {
  3138. return text.split(PROMPT_DELIMITER)[0].replace(/^# ?/gm, '').trim();
  3139. }
  3140. return text.trim();
  3141. },
  3142. normalizePrompt(value, fallback = '', action = STATE?.action) {
  3143. return PromptPipeline.process(value, fallback, action).storage;
  3144. },
  3145. isTransientItem(item) {
  3146. if (item?.isPending && item?.reqId && item?.type === 'video') return false;
  3147. return !!(item?.persist === false || item?.isPending || item?.id?.startsWith('pending-') || item?.filename === 'Pending...');
  3148. },
  3149. isPhantomMeta(meta) {
  3150. if (meta?.isPending && meta?.reqId) return false;
  3151. return String(meta?.id || '').startsWith('pending-') || meta?.filename === 'Pending...';
  3152. },
  3153. makeBatchTimestamp(base, index, total) { return base + (total - index); }
  3154. };
  3155.  
  3156. const GenerationTimings = {
  3157. get(type, res, batchSize) {
  3158. const key = `${type}_${res}_${batchSize}`;
  3159. if (STATE.timings?.[key]) return STATE.timings[key];
  3160. return (res === '2k' ? 6500 : 4000) * Math.pow(batchSize, 0.75);
  3161. },
  3162. update(type, res, batchSize, actualMs) {
  3163. if (!STATE.timings) STATE.timings = {};
  3164. const key = `${type}_${res}_${batchSize}`;
  3165. const prev = this.get(type, res, batchSize);
  3166. STATE.timings[key] = Math.max(1500, (prev * 0.2) + (actualMs * 0.8));
  3167. DBQueue.addAppWrite('timings', STATE.timings, 1000);
  3168. }
  3169. };
  3170.  
  3171. const getCurrentPanelKey = (action = STATE.action) => sanitizePanelKey(action);
  3172. const getPanelOrder = (panelKey = getCurrentPanelKey()) => sanitizeIdListStore(STATE.panelOrders?.[sanitizePanelKey(panelKey)]);
  3173. const syncActivePanelOrderState = (order = STATE.currentOrder, panelKey = getCurrentPanelKey()) => {
  3174. const key = sanitizePanelKey(panelKey);
  3175. const nextOrder = sanitizeIdListStore(order);
  3176. const prevOrder = sanitizeIdListStore(STATE.panelOrders?.[key]);
  3177. if (isSameIdList(prevOrder, nextOrder)) return nextOrder;
  3178. STATE.panelOrders = { ...STATE.panelOrders, [key]: nextOrder };
  3179. return nextOrder;
  3180. };
  3181. const persistPanelOrderStore = () => DBQueue.addAppWrite('currentOrder', {
  3182. schemaVersion: PANEL_STORE_SCHEMA_VERSION,
  3183. panels: Object.fromEntries(PANEL_ACTION_KEYS.map(key =>[key, sanitizeIdListStore(getPanelOrder(key).filter(id => STATE.items.has(String(id))))]))
  3184. }, 90);
  3185. const activatePanelState = (panelKey = getCurrentPanelKey()) => {
  3186. const key = sanitizePanelKey(panelKey);
  3187. const nextOrder = getPanelOrder(key);
  3188. if (!isSameIdList(STATE.currentOrder, nextOrder)) STATE.currentOrder = [...nextOrder];
  3189. else if (STATE.isMounted) PanelManager.update(nextOrder, STATE.items, 'current');
  3190. };
  3191. const setPanelOrderState = (panelKey = getCurrentPanelKey(), nextOrder = []) => {
  3192. const key = sanitizePanelKey(panelKey);
  3193. const safeOrder = sanitizeIdListStore(nextOrder);
  3194. if (key === getCurrentPanelKey()) {
  3195. if (!isSameIdList(STATE.currentOrder, safeOrder)) STATE.currentOrder = safeOrder;
  3196. return safeOrder;
  3197. }
  3198. const prevOrder = sanitizeIdListStore(STATE.panelOrders?.[key]);
  3199. if (isSameIdList(prevOrder, safeOrder)) return safeOrder;
  3200. STATE.panelOrders = { ...STATE.panelOrders, [key]: safeOrder };
  3201. persistPanelOrderStore();
  3202. scheduleCurrentPanelSessionSave();
  3203. return safeOrder;
  3204. };
  3205. const removeIdsFromAllPanels = (ids = []) => {
  3206. const staleIds = sanitizeIdListStore(ids);
  3207. if (!staleIds.length) return false;
  3208. const staleSet = new Set(staleIds);
  3209. let changed = false;
  3210. const nextPanels = { ...STATE.panelOrders };
  3211. PANEL_ACTION_KEYS.forEach(key => {
  3212. const prev = sanitizeIdListStore(nextPanels[key]);
  3213. const filtered = [...new Set(prev)].filter(v => !staleSet.has(String(v)));
  3214. if (!isSameIdList(prev, filtered)) {
  3215. nextPanels[key] = filtered;
  3216. changed = true;
  3217. }
  3218. });
  3219. if (!changed) return false;
  3220. STATE.panelOrders = nextPanels;
  3221. const activeOrder = sanitizeIdListStore(nextPanels[getCurrentPanelKey()]);
  3222. if (!isSameIdList(STATE.currentOrder, activeOrder)) STATE.currentOrder = activeOrder;
  3223. else {
  3224. persistPanelOrderStore();
  3225. scheduleCurrentPanelSessionSave(0);
  3226. }
  3227. return true;
  3228. };
  3229. let _currentPanelSessionTask = null;
  3230. const getPanelSessionSnapshot = (panelKey = getCurrentPanelKey()) => {
  3231. const key = sanitizePanelKey(panelKey);
  3232. const order = getPanelOrder(key).filter(id => STATE.items.has(String(id)));
  3233. const items = order
  3234. .map(id => STATE.items.get(String(id)))
  3235. .filter(item => {
  3236. if (!item) return false;
  3237. if (item.isPending) return item.type === 'video' && !!item.reqId;
  3238. return Pipeline.isTransientItem(item) || item.persist === false;
  3239. })
  3240. .map(item => ({
  3241. id: String(item.id),
  3242. timestamp: item.timestamp || Date.now(),
  3243. type: item.type,
  3244. mimeType: item.mimeType || '',
  3245. ratio: item.ratio || 1,
  3246. originalUrl: item.originalUrl || null,
  3247. filename: item.filename || '',
  3248. downloaded: Boolean(item.downloaded),
  3249. prompt: Pipeline.normalizePrompt(item.prompt),
  3250. responseText: typeof item.responseText === 'string' ? item.responseText : null,
  3251. textModel: typeof item.textModel === 'string' ? item.textModel : null,
  3252. textTurnCount: Number.isFinite(item.textTurnCount) ? item.textTurnCount : 0,
  3253. lqip: item.lqip || null,
  3254. isUploadedRef: Boolean(item.isUploadedRef),
  3255. isPending: Boolean(item.isPending),
  3256. reqId: item.reqId || null,
  3257. taskId: item.taskId || null,
  3258. refSourceId: item.refSourceId != null ? String(item.refSourceId) : null,
  3259. _statusData: sanitizeStatusLinesStore(item._statusData),
  3260. persist: false
  3261. }));
  3262. return { schemaVersion: 2, order, items };
  3263. };
  3264. const getCurrentPanelSessionSnapshot = () => ({
  3265. schemaVersion: PANEL_STORE_SCHEMA_VERSION,
  3266. panels: Object.fromEntries(PANEL_ACTION_KEYS.map(key => [key, getPanelSessionSnapshot(key)]).filter(([, panel]) => panel.order.length > 0 || panel.items.length > 0))
  3267. });
  3268. const writeCurrentPanelSessionSnapshot = (snapshot = getCurrentPanelSessionSnapshot()) => {
  3269. try {
  3270. const safeSnapshot = sanitizePanelSessionMapStore(snapshot);
  3271. if (!safeSnapshot || Object.keys(safeSnapshot.panels || {}).length === 0) {
  3272. sessionStorage.removeItem(APP_CONFIG.STORAGE.CURRENT_PANEL_SESSION_KEY);
  3273. } else {
  3274. sessionStorage.setItem(APP_CONFIG.STORAGE.CURRENT_PANEL_SESSION_KEY, JSON.stringify(safeSnapshot));
  3275. }
  3276. } catch (e) { addLog('WARN', 'Current panel session save failed', e); }
  3277. };
  3278. const scheduleCurrentPanelSessionSave = (delay = 90) => {
  3279. _currentPanelSessionTask?.abort();
  3280. _currentPanelSessionTask = scheduleTask(() => {
  3281. _currentPanelSessionTask = null;
  3282. writeCurrentPanelSessionSnapshot();
  3283. }, delay);
  3284. };
  3285. const flushCurrentPanelSessionSave = () => {
  3286. _currentPanelSessionTask?.abort();
  3287. _currentPanelSessionTask = null;
  3288. writeCurrentPanelSessionSnapshot();
  3289. };
  3290. const clearCurrentPanelSession = () => {
  3291. _currentPanelSessionTask?.abort();
  3292. _currentPanelSessionTask = null;
  3293. try {
  3294. sessionStorage.removeItem(APP_CONFIG.STORAGE.CURRENT_PANEL_SESSION_KEY);
  3295. } catch (e) { addLog('WARN', 'Current panel session clear failed', e); }
  3296. };
  3297.  
  3298. const createFlyweightItem = (rawItem) => ({
  3299. id: String(rawItem.id),
  3300. type: rawItem.type,
  3301. ratio: rawItem.ratio,
  3302. lqip: rawItem.lqip || null,
  3303. mimeType: rawItem.mimeType,
  3304. filename: rawItem.filename,
  3305. isPending: !!rawItem.isPending,
  3306. isUploadedRef: !!rawItem.isUploadedRef,
  3307. downloaded: !!rawItem.downloaded,
  3308. timestamp: rawItem.timestamp,
  3309. shortPrompt: rawItem.prompt ? Pipeline.getDisplayPrompt(rawItem.prompt).substring(0, 100) : (rawItem.shortPrompt || ''),
  3310. originalUrl: rawItem.originalUrl || null,
  3311. textModel: rawItem.textModel || null,
  3312. textTurnCount: rawItem.textTurnCount || 0,
  3313. reqId: rawItem.reqId || null,
  3314. taskId: rawItem.taskId || null,
  3315. refSourceId: rawItem.refSourceId || null,
  3316. _statusData: rawItem._statusData || null,
  3317. active: rawItem.active || false,
  3318. xmpEmbedded: !!rawItem.xmpEmbedded
  3319. });
  3320.  
  3321. const Store = {
  3322. commitNewItem(itemData, targetOrOptions = null, replacePlaceholderId = null) {
  3323. const options = targetOrOptions && typeof targetOrOptions === 'object' && !Array.isArray(targetOrOptions)
  3324. ? targetOrOptions
  3325. : { targetArrayName: targetOrOptions, replacePlaceholderId };
  3326. const item = { ...itemData, id: String(itemData.id), timestamp: itemData.timestamp || Date.now(), persist: options.persist ?? itemData.persist ?? !itemData.isPending };
  3327. if (item.prompt || item.responseText || item.textChatMessages) {
  3328. LazyTextStore.set(item.id, { prompt: item.prompt || '', responseText: item.responseText || '', textChatMessages: item.textChatMessages || null });
  3329. }
  3330. const flyweight = createFlyweightItem(item);
  3331. STATE.items.set(String(item.id), flyweight);
  3332. if (item.persist !== false) DBQueue.add(flyweight);
  3333. if (options.targetArrayName && STATE[options.targetArrayName]) {
  3334. const panelKey = sanitizePanelKey(options.panelKey || getCurrentPanelKey());
  3335. const arr = panelKey === getCurrentPanelKey() ? STATE[options.targetArrayName] : getPanelOrder(panelKey);
  3336. const idx = options.replacePlaceholderId ? arr.indexOf(options.replacePlaceholderId) : -1;
  3337. const nextOrder = idx !== -1 ? arr.with(idx, item.id) : [...arr, item.id];
  3338. if (idx !== -1) this.removeItem(options.replacePlaceholderId);
  3339. setPanelOrderState(panelKey, nextOrder);
  3340. }
  3341. return flyweight;
  3342. },
  3343. addItem(item) {
  3344. if (!item) return;
  3345. if (item.prompt || item.responseText || item.textChatMessages) {
  3346. LazyTextStore.set(item.id, { prompt: item.prompt || '', responseText: item.responseText || '', textChatMessages: item.textChatMessages || null });
  3347. }
  3348. STATE.items.set(String(item.id), createFlyweightItem(item));
  3349. },
  3350. removeItem(id) {
  3351. const key = String(id);
  3352. STATE.items.delete(key);
  3353. LazyTextStore.invalidate(key);
  3354. },
  3355. prependHistory(ids) {
  3356. if (!ids?.length) return;
  3357. const orderedIds = ids.map(String), seen = new Set(orderedIds);
  3358. STATE.historyOrder = [...orderedIds, ...STATE.historyOrder.filter(id => !seen.has(String(id)))];
  3359. if (STATE.isMounted) PanelManager.prepend(orderedIds, STATE.items, 'history');
  3360. scheduleHistoryMetricsUpdate();
  3361. },
  3362. clearCurrentTransient(ids, panelKey = getCurrentPanelKey()) {
  3363. if (!ids?.length) return;
  3364. const staleIds = ids.map(String), staleSet = new Set(staleIds);
  3365. const nextOrder = getPanelOrder(panelKey).filter(id => !staleSet.has(String(id)));
  3366. setPanelOrderState(panelKey, nextOrder);
  3367. staleIds.forEach(id => this.removeItem(id));
  3368. },
  3369. getRefItems(action = STATE.action, refMode = STATE.refMode) {
  3370. return getRefOrderForView(action, refMode).map(id => getRefItemForView(id, action, refMode)).filter(Boolean);
  3371. }
  3372. };
  3373.  
  3374. let wipePromise = null, lastWipeTime = 0, _skipPagehidePersistence = false;
  3375. async function nuclearWipe() {
  3376. if (wipePromise) return wipePromise;
  3377. if (Date.now() - lastWipeTime < 3000) { await sleep(3000 - (Date.now() - lastWipeTime)); return; }
  3378. lastWipeTime = Date.now();
  3379. addLog('INFO', 'Executing modern nuclear wipe (Tokens/Caches)...');
  3380. wipePromise = (async () => {
  3381. const clearCookiesGM = (url) => new Promise(res => {
  3382. if(typeof GM_cookie === 'undefined') return res();
  3383. GM_cookie.list({ url }, (cookies, err) => {
  3384. if (err || !cookies?.length) return res();
  3385. Promise.allSettled(cookies.map(c => new Promise(r => GM_cookie.delete({ url, name: c.name }, r)))).then(() => res());
  3386. });
  3387. });
  3388. await Promise.allSettled([clearCookiesGM(window.location.href), clearCookiesGM('https://x.ai')]);
  3389. if (window.cookieStore) {
  3390. try {
  3391. const cookies = await cookieStore.getAll();
  3392. await Promise.allSettled(cookies.map(c => cookieStore.delete({ name: c.name, domain: c.domain, path: c.path })));
  3393. } catch(e) { addLog('WARN', 'cookieStore wipe failed', e); }
  3394. }
  3395. document.cookie.split(';').forEach(c => {
  3396. const n = c.split('=')[0].trim();['.x.ai', 'console.x.ai', ''].forEach(d => {['/', '/playground/imagine', '/playground/voice', '/playground/voice/text-to-speech', ''].forEach(p => {
  3397. document.cookie = `${n}=;max-age=0;path=${p}${d ? ';domain=' + d : ''}`;
  3398. });
  3399. });
  3400. });
  3401. if ('caches' in window) {
  3402. try {
  3403. const cacheKeys = await caches.keys();
  3404. await Promise.allSettled(cacheKeys.map(k => caches.delete(k)));
  3405. } catch(e) {}
  3406. }
  3407. try { await fetch('https://console.x.ai/playground/imagine', { credentials: 'include', cache: 'reload' }); } catch(e) {}
  3408. try { await fetch('https://console.x.ai/playground/voice/text-to-speech', { credentials: 'include', cache: 'reload' }); } catch(e) {}
  3409. })();
  3410. await wipePromise; wipePromise = null;
  3411. }
  3412.  
  3413. const OPFS = {
  3414. _dirHandle: null,
  3415. async initDir() {
  3416. if (!this._dirHandle) {
  3417. const root = await navigator.storage.getDirectory();
  3418. this._dirHandle = await root.getDirectoryHandle(APP_CONFIG.STORAGE.OPFS_DIR, { create: true });
  3419. }
  3420. return this._dirHandle;
  3421. },
  3422. async write(id, type, blob) {
  3423. let w;
  3424. try {
  3425. const dir = await this.initDir();
  3426. const fh = await dir.getFileHandle(`${id}_${type}`, { create: true });
  3427. w = await fh.createWritable({ keepExistingData: false });
  3428. await w.write(blob);
  3429. } catch (e) { addLog('ERROR', 'OPFS Write', e); }
  3430. finally { if (w) await w.close().catch(()=>{}); }
  3431. },
  3432. async read(id, type) {
  3433. try {
  3434. const dir = await this.initDir();
  3435. const fh = await dir.getFileHandle(`${id}_${type}`);
  3436. return await fh.getFile();
  3437. } catch (e) { return null; }
  3438. },
  3439. async clear() {
  3440. try {
  3441. const root = await navigator.storage.getDirectory();
  3442. await root.removeEntry(APP_CONFIG.STORAGE.OPFS_DIR, { recursive: true }).catch(()=>{});
  3443. this._dirHandle = null;
  3444. } catch (e) {}
  3445. }
  3446. };
  3447.  
  3448. const DBQueue = {
  3449. _pendingItems: new Map(), _appWrites: new Map(), _appWriteData: new Map(), _isFlushing: false,
  3450. _serializeItem(item) {
  3451. if (!item || Pipeline.isTransientItem(item)) return null;
  3452. const heavy = LazyTextStore._cache.get(String(item.id));
  3453.  
  3454. const prompt = heavy ? heavy.prompt : undefined;
  3455. const responseText = heavy ? heavy.responseText : undefined;
  3456. const textChatMessages = heavy ? heavy.textChatMessages : undefined;
  3457.  
  3458. if (item.type === 'text') {
  3459. return {
  3460. id: String(item.id),
  3461. timestamp: item.timestamp,
  3462. type: item.type,
  3463. mimeType: item.mimeType || 'text/plain',
  3464. ratio: 1,
  3465. filename: item.filename,
  3466. downloaded: item.downloaded || false,
  3467. prompt: prompt !== undefined ? Pipeline.normalizePrompt(prompt) : undefined,
  3468. shortPrompt: item.shortPrompt || '',
  3469. responseText: responseText,
  3470. textChatMessages: textChatMessages,
  3471. textModel: item.textModel,
  3472. textTurnCount: Number.isFinite(item.textTurnCount) ? item.textTurnCount : 0,
  3473. lqip: item.lqip,
  3474. isUploadedRef: false,
  3475. isPending: false
  3476. };
  3477. }
  3478. return {
  3479. id: String(item.id),
  3480. timestamp: item.timestamp,
  3481. type: item.type,
  3482. mimeType: item.mimeType,
  3483. ratio: item.ratio || 1,
  3484. originalUrl: item.type === 'video' ? item.originalUrl : (item.originalUrl || null),
  3485. filename: item.filename,
  3486. downloaded: item.downloaded || false,
  3487. prompt: prompt !== undefined ? Pipeline.normalizePrompt(prompt) : undefined,
  3488. shortPrompt: item.shortPrompt || '',
  3489. lqip: item.lqip,
  3490. isUploadedRef: item.isUploadedRef || false,
  3491. isPending: item.isPending || false,
  3492. reqId: item.reqId || null,
  3493. taskId: item.taskId || null,
  3494. refSourceId: item.refSourceId || null,
  3495. _statusData: item._statusData || null,
  3496. textModel: item.textModel || null,
  3497. textTurnCount: Number.isFinite(item.textTurnCount) ? item.textTurnCount : 0,
  3498. xmpEmbedded: !!item.xmpEmbedded
  3499. };
  3500. },
  3501. add(item) {
  3502. const serialized = this._serializeItem(item);
  3503. if (!serialized) return;
  3504. this._pendingItems.set(serialized.id, serialized);
  3505. this._queueFlush();
  3506. },
  3507. forgetItem(id) { this._pendingItems.delete(String(id)); },
  3508. forgetItems(ids) { ids.forEach(id => this.forgetItem(id)); },
  3509. async _queueFlush() {
  3510. if (this._isFlushing || this._pendingItems.size === 0) return;
  3511. this._isFlushing = true;
  3512. try {
  3513. await scheduler.postTask(async () => {
  3514. const itemsToSave = [];
  3515. TaskChunker.reset();
  3516. for (const item of this._pendingItems.values()) {
  3517. if (Pipeline.isPhantomMeta(item)) continue;
  3518. itemsToSave.push(item);
  3519. await TaskChunker.yieldIfNeeded();
  3520. }
  3521. this._pendingItems.clear();
  3522. if (itemsToSave.length > 0) {
  3523. await StorageClient.saveItemsBulk(itemsToSave);
  3524. runBg(updateHistoryMetrics);
  3525. scheduleDiskUpdate();
  3526. }
  3527. }, { priority: 'background' });
  3528. } catch(e) { addLog('ERROR', 'DBQueue flush', e); } finally { this._isFlushing = false; if (this._pendingItems.size > 0) this._queueFlush(); }
  3529. },
  3530. addAppWrite(key, dataOrFn, delay = 500) {
  3531. const prev = this._appWrites.get(key);
  3532. prev?.controller?.abort();
  3533. const controller = new AbortController();
  3534. scheduler.postTask(async () => {
  3535. if (controller.signal.aborted) return;
  3536. const resolvedData = typeof dataOrFn === 'function' ? dataOrFn() : dataOrFn;
  3537. const snapshot = cloneSerializable(resolvedData, resolvedData);
  3538. this._appWrites.delete(key);
  3539. this._appWriteData.delete(key);
  3540. await StorageClient.saveMeta(key, snapshot);
  3541. }, { priority: 'background', delay, signal: controller.signal }).catch(() => {});
  3542. this._appWrites.set(key, { controller });
  3543. this._appWriteData.set(key, dataOrFn);
  3544. },
  3545. async flushAppWrites(keys = null) {
  3546. const allow = keys ? new Set((Array.isArray(keys) ? keys : [keys]).map(String)) : null;
  3547. const jobs = [];
  3548. const flushedKeys = [];
  3549. for (const [key, entry] of this._appWrites.entries()) {
  3550. if (allow && !allow.has(String(key))) continue;
  3551. entry?.controller?.abort();
  3552. const dataOrFn = this._appWriteData.get(key);
  3553. if (dataOrFn !== undefined) {
  3554. const resolvedData = typeof dataOrFn === 'function' ? dataOrFn() : dataOrFn;
  3555. const snapshot = cloneSerializable(resolvedData, resolvedData);
  3556. jobs.push(StorageClient.saveMeta(key, snapshot));
  3557. }
  3558. flushedKeys.push(key);
  3559. }
  3560. flushedKeys.forEach(key => {
  3561. this._appWrites.delete(key);
  3562. this._appWriteData.delete(key);
  3563. });
  3564. if (jobs.length > 0) await Promise.allSettled(jobs);
  3565. },
  3566. clearPending() {
  3567. this._pendingItems.clear();
  3568. this._appWrites.forEach(entry => entry?.controller?.abort());
  3569. this._appWrites.clear();
  3570. this._appWriteData.clear();
  3571. this._isFlushing = false;
  3572. },
  3573. dropAppWrites(keys = []) {
  3574. const safeKeys = [...new Set((Array.isArray(keys) ? keys : [keys]).map(key => typeof key === 'string' ? key : '').filter(Boolean))];
  3575. safeKeys.forEach(key => {
  3576. const entry = this._appWrites.get(key);
  3577. entry?.controller?.abort();
  3578. this._appWrites.delete(key);
  3579. this._appWriteData.delete(key);
  3580. });
  3581. }
  3582. };
  3583.  
  3584. const createAppWritePersistor = (key, read, delay = 500) => () => DBQueue.addAppWrite(key, read, delay);
  3585. const persistEffectivePrompts = createAppWritePersistor('prompts', () => sanitizePromptStore(STATE.prompts));
  3586. const persistPromptDrafts = createAppWritePersistor('promptDrafts', () => sanitizePromptStore(STATE.promptDrafts));
  3587. const persistRefPrompts = createAppWritePersistor('refPrompts', () => sanitizeRefPromptStore(STATE.refPrompts));
  3588. const persistFavPrompts = createAppWritePersistor('favPrompts', () => sanitizeFavPromptStore(STATE.favPrompts));
  3589. const persistActiveTextSessionId = createAppWritePersistor(ACTIVE_TEXT_SESSION_KEY, () => STATE.activeTextSessionId || null, 60);
  3590. const persistPromptStateStores = () => {
  3591. persistEffectivePrompts();
  3592. persistPromptDrafts();
  3593. persistRefPrompts();
  3594. persistFavPrompts();
  3595. };
  3596. const persistTextSessionConversation = (id, messages, model = STATE.textModel) => {
  3597. const sessionId = String(id || '');
  3598. if (!sessionId) return;
  3599. DBQueue.addAppWrite(getTextSessionStoreKey(sessionId), {
  3600. schemaVersion: 1,
  3601. textModel: typeof model === 'string' ? model : null,
  3602. updatedAt: Date.now(),
  3603. messages: sanitizeTextConversationStore(messages)
  3604. }, 90);
  3605. };
  3606. const syncTextSessionDownload = (item, messages = STATE.textChatMessages) => {
  3607. if (!item?.id) return;
  3608. const transcript = buildTextSessionTranscript(messages, item.textModel);
  3609. runBg(() => OPFS.write(item.id, 'full', new Blob([transcript], { type: 'text/plain' })));
  3610. };
  3611. const buildTextSessionItem = (id = crypto.randomUUID(), createdAt = Date.now()) => ({
  3612. id,
  3613. type: 'text',
  3614. mimeType: 'text/plain',
  3615. timestamp: createdAt,
  3616. prompt: '',
  3617. responseText: '',
  3618. textModel: STATE.textModel || 'grok-4.20',
  3619. textTurnCount: 0,
  3620. filename: `Grok Chat - ${makeTimestamp()}.txt`,
  3621. ratio: 1,
  3622. originalUrl: null,
  3623. downloaded: false,
  3624. lqip: '#1e1e2e',
  3625. isUploadedRef: false
  3626. });
  3627. const setPromptStoreValue = (storeKey, action, value = '', persist = null) => {
  3628. const nextValue = normalizePromptDraft(value);
  3629. if (STATE[storeKey][action] === nextValue) return nextValue;
  3630. STATE[storeKey] = { ...STATE[storeKey], [action]: nextValue };
  3631. persist?.();
  3632. return nextValue;
  3633. };
  3634. const getActionFavorites = (action = STATE.action) => STATE.favPrompts[action] || [];
  3635. const setActionFavorites = (list = [], action = STATE.action) => {
  3636. STATE.favPrompts = { ...STATE.favPrompts, [action]: list };
  3637. persistFavPrompts();
  3638. return list;
  3639. };
  3640. const addActionFavorite = (text, action = STATE.action) => {
  3641. const originalText = Pipeline.getOriginalPrompt(text);
  3642. const nextText = String(originalText || '').trim();
  3643. if (!nextText) return false;
  3644. const current = getActionFavorites(action);
  3645. if (current.includes(nextText)) return false;
  3646. setActionFavorites([...current, nextText], action);
  3647. return true;
  3648. };
  3649. const removeActionFavorite = (text, action = STATE.action) => {
  3650. const nextText = String(text || '').trim();
  3651. const current = getActionFavorites(action);
  3652. const next = current.filter(entry => entry !== nextText);
  3653. if (next.length === current.length) return false;
  3654. setActionFavorites(next, action);
  3655. return true;
  3656. };
  3657. const setPromptValueForAction = (action, value = '', { persist = true } = {}) => {
  3658. const nextValue = normalizePromptDraft(value);
  3659. setPromptStoreValue('promptDrafts', action, nextValue);
  3660. setPromptStoreValue('prompts', action, nextValue);
  3661. if (persist) { persistPromptDrafts(); persistEffectivePrompts(); }
  3662. return nextValue;
  3663. };
  3664. const clearTextPromptInput = ({ focus = false, syncEditor = STATE.action === 'gen_text' } = {}) => {
  3665. setPromptValueForAction('gen_text', '');
  3666. if (!syncEditor) return;
  3667. setEditorPromptValue('');
  3668. DOM.promptUpdateLabel?.classList.remove('show', 'saved');
  3669. DOM.generateBtn?.classList.remove('is-dirty', 'flash-saved');
  3670. if (focus && DOM.prompt) requestAnimationFrame(() => DOM.prompt.focus());
  3671. };
  3672. const applyTextModelSelection = (value) => {
  3673. const nextValue = String(value || '').trim();
  3674. if (!nextValue) return;
  3675. STATE.textModel = nextValue;
  3676. const control = UI_CONTROL_BY_ID.textModel;
  3677. const button = control ? document.getElementById(control.buttonId) : null;
  3678. const label = control?.labelId ? document.getElementById(control.labelId) : null;
  3679. if (button) button.dataset.value = nextValue;
  3680. if (label) label.textContent = nextValue;
  3681. if (control?.popoverId) setActivePopoverItem(document.getElementById(control.popoverId), nextValue);
  3682. updateActionUI();
  3683. };
  3684. const setActiveTextSessionState = (sessionId = null, messages = [], { persist = true } = {}) => {
  3685. STATE.activeTextSessionId = sessionId ? String(sessionId) : null;
  3686. STATE.textChatMessages = sanitizeTextConversationStore(messages);
  3687. if (persist) persistActiveTextSessionId();
  3688. };
  3689. const resetTextChatSession = ({ persist = true } = {}) => {
  3690. setActiveTextSessionState(null, [], { persist });
  3691. restoreChatFromSession();
  3692. };
  3693. const commitTextSession = (messages, { sessionId = STATE.activeTextSessionId, model = STATE.textModel, updatedAt = Date.now() } = {}) => {
  3694. const cleanMessages = sanitizeTextConversationStore(Array.isArray(messages) ? [...messages] : messages);
  3695. const id = String(sessionId || crypto.randomUUID());
  3696. const existing = STATE.items.get(id);
  3697. let item = existing || buildTextSessionItem(id, updatedAt);
  3698. if (!item.filename || /^Grok Text\b/.test(item.filename)) item.filename = `Grok Chat - ${makeTimestamp()}.txt`;
  3699. summarizeTextSessionItem(item, cleanMessages, { updatedAt, model });
  3700. if (!existing) item = Store.commitNewItem(item, null);
  3701. else DBQueue.add(item);
  3702. Store.prependHistory([id]);
  3703. setActiveTextSessionState(id, cleanMessages, { persist: true });
  3704. persistTextSessionConversation(id, cleanMessages, item.textModel);
  3705. syncTextSessionDownload(item, cleanMessages);
  3706. return item;
  3707. };
  3708. const loadTextSessionMessages = async (itemOrId) => {
  3709. const item = typeof itemOrId === 'object' ? itemOrId : STATE.items.get(String(itemOrId));
  3710. const itemId = String(item?.id ?? itemOrId ?? '');
  3711. if (!itemId) return [];
  3712. const raw = await StorageClient.loadMeta(getTextSessionStoreKey(itemId));
  3713. const messages = sanitizeTextConversationStore(raw?.messages);
  3714. return messages.length > 0 ? messages : await getTextSessionMessagesFromItemAsync(item);
  3715. };
  3716. const clearTextSessionArtifacts = async (ids = []) => {
  3717. const safeIds = sanitizeIdListStore(ids);
  3718. if (!safeIds.length) return;
  3719. const dataKeys = safeIds.map(getTextSessionStoreKey);
  3720. DBQueue.dropAppWrites(dataKeys);
  3721. if (safeIds.includes(String(STATE.activeTextSessionId || ''))) {
  3722. DBQueue.dropAppWrites(ACTIVE_TEXT_SESSION_KEY);
  3723. setActiveTextSessionState(null, [], { persist: true });
  3724. restoreChatFromSession();
  3725. dataKeys.push(ACTIVE_TEXT_SESSION_KEY);
  3726. }
  3727. await StorageClient.deleteMeta(dataKeys).catch(e => addLog('WARN', 'Delete text session data failed', { ids: safeIds, error: e?.message || e }));
  3728. };
  3729. const getStoredActionPrompt = (action = STATE.action) => normalizePromptDraft(STATE.prompts[action], '');
  3730. const getDraftActionPrompt = (action = STATE.action) => normalizePromptDraft(STATE.promptDrafts[action], getStoredActionPrompt(action));
  3731. const getEditorPromptValue = () => normalizePromptDraft(DOM.prompt?.value, '');
  3732. const setEditorPromptValue = (value = '') => {
  3733. if (DOM.prompt) DOM.prompt.value = normalizePromptDraft(value);
  3734. };
  3735. const replaceEditorPromptValue = (value = '') => {
  3736. const prompt = sanitizePromptText(value);
  3737. if (!DOM.prompt || !prompt) return false;
  3738. insertTextAtCursor(DOM.prompt, prompt, true);
  3739. return true;
  3740. };
  3741. const getActiveRefPromptBaseline = () => getDraftActionPrompt(STATE.activeRefPromptAction || STATE.action);
  3742. const hasCustomRefPromptDraft = (value = STATE.activeRefPromptDraft, baseline = getActiveRefPromptBaseline()) => {
  3743. const draft = sanitizePromptText(value);
  3744. return !!draft && draft !== sanitizePromptText(baseline);
  3745. };
  3746. const setActionPromptDraft = (action = STATE.action, value = getEditorPromptValue()) => {
  3747. return setPromptStoreValue('promptDrafts', action, value, persistPromptDrafts);
  3748. };
  3749. const commitActionPrompt = (action = STATE.action, value = getDraftActionPrompt(action)) => {
  3750. return setPromptStoreValue('prompts', action, value, persistEffectivePrompts);
  3751. };
  3752. const getTaskPrompt = (action = STATE.action) => {
  3753. const stored = sanitizePromptText(getStoredActionPrompt(action));
  3754. if (stored || STATE.isGenerating) return stored;
  3755. return sanitizePromptText(getDraftActionPrompt(action));
  3756. };
  3757. const PromptSync = {
  3758. controllers: new Map(),
  3759. captureEditor(action = STATE.action, { scheduleCommit = false } = {}) {
  3760. const draft = setActionPromptDraft(action, getEditorPromptValue());
  3761. if (scheduleCommit) this.scheduleCommit(action, draft);
  3762. return draft;
  3763. },
  3764. scheduleCommit(action = STATE.action, value = getDraftActionPrompt(action), delay = APP_CONFIG.TIMINGS.PROMPT_COMMIT_DELAY_MS) {
  3765. this.cancel(action);
  3766. const normalized = normalizePromptDraft(value);
  3767. const controller = new AbortController();
  3768. scheduler.postTask(() => {
  3769. if (controller.signal.aborted) return;
  3770. this.controllers.delete(action);
  3771. commitActionPrompt(action, normalized);
  3772. }, { priority: 'background', delay, signal: controller.signal }).catch(() => {});
  3773. this.controllers.set(action, controller);
  3774. return normalized;
  3775. },
  3776. commitNow(action = STATE.action) {
  3777. this.cancel(action);
  3778. return commitActionPrompt(action, getDraftActionPrompt(action));
  3779. },
  3780. cancel(action = STATE.action) {
  3781. this.controllers.get(action)?.abort();
  3782. this.controllers.delete(action);
  3783. },
  3784. clear() {
  3785. this.controllers.forEach(controller => controller.abort());
  3786. this.controllers.clear();
  3787. }
  3788. };
  3789. const deleteRefPrompts = (ids = []) => {
  3790. const next = { ...STATE.refPrompts };
  3791. sanitizeIdListStore(ids).forEach(id => delete next[id]);
  3792. STATE.refPrompts = next;
  3793. persistRefPrompts();
  3794. };
  3795. const setRefPrompt = (id, action, prompt) => {
  3796. const currentRefData = STATE.refPrompts[id] || {};
  3797. if (prompt) {
  3798. STATE.refPrompts = { ...STATE.refPrompts, [id]: { ...currentRefData, [action]: prompt } };
  3799. } else {
  3800. const nextData = { ...currentRefData };
  3801. delete nextData[action];
  3802. if (Object.keys(nextData).length === 0) {
  3803. const nextPrompts = { ...STATE.refPrompts };
  3804. delete nextPrompts[id];
  3805. STATE.refPrompts = nextPrompts;
  3806. } else {
  3807. STATE.refPrompts = { ...STATE.refPrompts, [id]: nextData };
  3808. }
  3809. }
  3810. persistRefPrompts();
  3811. };
  3812. const getAspectRatioOption = (label = STATE.currentAr) => ASPECT_RATIO_MAP.get(label.trim()) || ASPECT_RATIO_MAP.get('Auto');
  3813. const getAspectRatioPopoverOptions = (action = STATE.action) => {
  3814. const isVideo = action === 'gen_video';
  3815. return ASPECT_RATIO_POPOVER_LAYOUT.flatMap(pair => pair.map(label => {
  3816. const option = getAspectRatioOption(label);
  3817. return {
  3818. val: option.label,
  3819. text: option.label,
  3820. icon: UIUtils.createArIcon(option.w, option.h, option.isAuto, option.isRef),
  3821. disabled: isVideo && !option.isAuto && (option.imageOnly || !ALLOWED_VIDEO_ARS.has(option.label.trim()))
  3822. };
  3823. }));
  3824. };
  3825. const syncActionLabel = (action = STATE.action) => {
  3826. if (DOM.actionDropdown) DOM.actionDropdown.dataset.value = action;
  3827. if (DOM.labelMasterAction) DOM.labelMasterAction.innerText = (ACTION_SPEC[action] ?? ACTION_SPEC.gen_image).label;
  3828. setActivePopoverItem(document.getElementById('popover-action'), action);
  3829. };
  3830. let _dirtyStateTask = null;
  3831. const updateDirtyStateUI = () => {
  3832. if (!DOM.promptUpdateLabel || !DOM.generateBtn || STATE.activeRefPromptId) return;
  3833. if (STATE.action === 'gen_text' || !STATE.isGenerating) {
  3834. DOM.promptUpdateLabel.classList.remove('show', 'saved');
  3835. DOM.generateBtn.classList.remove('is-dirty');
  3836. return;
  3837. }
  3838.  
  3839. _dirtyStateTask?.abort();
  3840. _dirtyStateTask = new AbortController();
  3841.  
  3842. scheduler.postTask(() => {
  3843. if (_dirtyStateTask.signal.aborted) return;
  3844. const editorVal = sanitizePromptText(DOM.prompt?.value || '');
  3845. const committedVal = sanitizePromptText(getStoredActionPrompt(STATE.action));
  3846. const isDirty = editorVal !== committedVal;
  3847.  
  3848. DOM.promptUpdateLabel.classList.toggle('saved', !isDirty);
  3849. DOM.promptUpdateLabel.classList.toggle('show', isDirty);
  3850. DOM.generateBtn.classList.toggle('is-dirty', isDirty);
  3851. if (isDirty) DOM.promptUpdateLabel.innerText = 'Draft modified: Press Enter to apply';
  3852. }, { priority: 'user-visible', delay: 150, signal: _dirtyStateTask.signal }).catch(() => {});
  3853. };
  3854. const syncDOMControls = (stateKeys) => {
  3855. const keys = Array.isArray(stateKeys) ? stateKeys : [stateKeys];
  3856. keys.forEach(stateKey => {
  3857. const control = getControlByStateKey(stateKey);
  3858. if (!control) return;
  3859. const value = STATE[stateKey] ?? control.defaultValue;
  3860. const valueStr = String(value);
  3861. const elements = document.querySelectorAll(`[data-state-key="${stateKey}"]`);
  3862. if (!elements.length) return;
  3863. elements.forEach(el => {
  3864. if (control.render === 'aspect-ratio') {
  3865. el.dataset.value = valueStr;
  3866. const option = getAspectRatioOption(valueStr);
  3867. const iconContainer = el.querySelector('#ar-selected-icon');
  3868. const textNode = el.querySelector('#ar-selected-text');
  3869. if (iconContainer) {
  3870. const iconMarkup = UIUtils.createArIcon(option.w, option.h, option.isAuto, option.isRef);
  3871. iconContainer.replaceChildren(...(iconMarkup ? [createMarkupFragment(iconMarkup)] : []));
  3872. iconContainer.style.display = iconMarkup ? 'flex' : 'none';
  3873. }
  3874. if (textNode) textNode.innerText = option.label.trim();
  3875. return;
  3876. }
  3877. if (control.kind === 'toggle') {
  3878. const input = el.tagName === 'INPUT' && el.type === 'checkbox' ? el : el.querySelector('input[type="checkbox"]');
  3879. if (input) input.checked = !!value;
  3880. else {
  3881. el.dataset.value = valueStr;
  3882. el.innerText = control.labels?.[valueStr] ?? valueStr;
  3883. }
  3884. } else if (control.kind === 'dropdown') {
  3885. el.dataset.value = valueStr;
  3886. const options = typeof control.options === 'function' ? control.options() : (control.options || []);
  3887. const activeOpt = options.find(opt => String(opt.val) === valueStr) || options[0];
  3888. if (control.labelId && activeOpt) {
  3889. const labelNodes = el.querySelectorAll(`[id="${control.labelId}"]`);
  3890. labelNodes.forEach(ln => ln.textContent = activeOpt.text);
  3891. }
  3892. if (control.popoverId) setActivePopoverItem(document.getElementById(control.popoverId), valueStr);
  3893. } else if (control.kind === 'number' || control.kind === 'number-inline') {
  3894. el.dataset.value = valueStr;
  3895. const input = el.tagName === 'INPUT' ? el : el.querySelector('input');
  3896. if (input) input.value = valueStr;
  3897. } else if (control.kind === 'button') {
  3898. el.dataset.value = valueStr;
  3899. }
  3900. });
  3901. });
  3902. };
  3903. const syncLayoutWidths = () => {
  3904. if (DOM.colLeft) DOM.colLeft.style.width = Math.max(260, STATE.leftWidth) + 'px';
  3905. if (DOM.colRight) DOM.colRight.style.width = STATE.rightWidth + 'px';
  3906. };
  3907. const escapeSelectorValue = (value) => CSS.escape(String(value));
  3908. const setActivePopoverItem = (popover, value) => {
  3909. if (!popover) return;
  3910. const stringValue = String(value);
  3911. const matches = popover.querySelectorAll(`.popover-item[data-val="${escapeSelectorValue(stringValue)}"]`);
  3912. const activeItems = popover.querySelectorAll('.popover-item.active');
  3913. if (matches.length && activeItems.length === matches.length && [...matches].every(node => node.classList.contains('active'))) return;
  3914. activeItems.forEach(node => node.classList.remove('active'));
  3915. matches.forEach(node => node.classList.add('active'));
  3916. };
  3917. 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>`;
  3918. const buildPopoverOptionItem = (selectedValue, opt) => {
  3919. const optionValue = String(opt.val);
  3920. const classNames = ['popover-item', String(selectedValue) === optionValue ? 'active' : '', opt.disabled ? 'disabled' : '', opt.icon ? 'has-icon' : 'no-icon'].filter(Boolean);
  3921. const frag = document.createRange().createContextualFragment(buildPopoverOptionHtml({
  3922. val: optionValue,
  3923. className: classNames.join(' '),
  3924. title: opt.title || '',
  3925. iconHtml: '',
  3926. text: opt.text
  3927. }));
  3928. if (opt.icon) {
  3929. const iconDiv = frag.querySelector('.popover-icon');
  3930. if (iconDiv) iconDiv.append(createMarkupFragment(opt.icon));
  3931. } else {
  3932. const iconDiv = frag.querySelector('.popover-icon');
  3933. if (iconDiv) iconDiv.remove();
  3934. }
  3935. return frag;
  3936. };
  3937. const renderPopoverOptions = (popover, selectedValue, opts = []) => {
  3938. const fragment = document.createDocumentFragment();
  3939. opts.forEach(opt => fragment.appendChild(buildPopoverOptionItem(selectedValue, opt)));
  3940. popover.replaceChildren(fragment);
  3941. };
  3942. const buildExportState = () => ({
  3943. schemaVersion: APP_CONFIG.STORAGE.EXPORT_SCHEMA_VERSION,
  3944. config: normalizeConfig(getConfigSnapshot(), CONFIG_DEFAULTS),
  3945. prompts: sanitizePromptStore(STATE.prompts),
  3946. promptDrafts: sanitizePromptStore(STATE.promptDrafts, STATE.prompts),
  3947. refPrompts: sanitizeRefPromptStore(STATE.refPrompts),
  3948. favPrompts: sanitizeFavPromptStore(STATE.favPrompts)
  3949. });
  3950. const getRefViewEntry = (action = STATE.action, refMode = STATE.refMode) => {
  3951. const viewKey = getRefViewKey(action, refMode);
  3952. if (!STATE.refViews[viewKey]) STATE.refViews[viewKey] = createEmptyRefViewEntry();
  3953. return STATE.refViews[viewKey];
  3954. };
  3955. const normalizeRefViewEntryForAction = (entry, action = STATE.action) => {
  3956. const poolType = getRefPoolTypeForAction(action);
  3957. const order = sanitizeIdListStore(entry?.order).filter(id => {
  3958. const item = STATE.items.get(String(id));
  3959. return item && isRefCompatibleWithPoolType(item, poolType);
  3960. });
  3961. const orderSet = new Set(order);
  3962. const activeKeys = Object.entries(entry?.active || {}).filter(([id, val]) => val === true && orderSet.has(String(id))).map(([id]) => [String(id), true]);
  3963. const active = Object.fromEntries(activeKeys);
  3964. const waitUntilKeys = Object.entries(entry?.waitUntil || {}).filter(([id, val]) => orderSet.has(String(id)) && Number.isFinite(val) && val > 0).map(([id, val]) => [String(id), val]);
  3965. const waitUntil = Object.fromEntries(waitUntilKeys);
  3966. return { order, active, waitUntil };
  3967. };
  3968. const getMutableRefViewEntry = (action = STATE.action, refMode = STATE.refMode) => {
  3969. const viewKey = getRefViewKey(action, refMode);
  3970. const normalized = normalizeRefViewEntryForAction(getRefViewEntry(action, refMode), action);
  3971. STATE.refViews[viewKey] = normalized;
  3972. return normalized;
  3973. };
  3974. const getRefOrderForView = (action = STATE.action, refMode = STATE.refMode) => [...getMutableRefViewEntry(action, refMode).order];
  3975. const getRefItemStateForView = (id, action = STATE.action, refMode = STATE.refMode) => {
  3976. const key = String(id);
  3977. const entry = getMutableRefViewEntry(action, refMode);
  3978. return { active: entry.active[key] === true, readyAt: Number(entry.waitUntil[key]) || 0 };
  3979. };
  3980. const getRefItemForView = (id, action = STATE.action, refMode = STATE.refMode) => {
  3981. const key = String(id);
  3982. const item = STATE.items.get(key);
  3983. if (!item || !isRefCompatibleWithPoolType(item, getRefPoolTypeForAction(action))) return null;
  3984. return { ...item, ...getRefItemStateForView(key, action, refMode) };
  3985. };
  3986. const isVisibleRefView = (action = STATE.action, refMode = STATE.refMode) => getRefViewKey(action, refMode) === getRefViewKey(STATE.action, STATE.refMode);
  3987. const shouldDelayReferenceActivation = (action = STATE.action) => {
  3988. const safeAction = sanitizePanelKey(action, STATE.action);
  3989. if (JobManager.getLane(safeAction) !== 'media') return false;
  3990. const mediaRuntime = JobManager.getRuntime('media');
  3991. return mediaRuntime.isGenerating && mediaRuntime.activeAction === safeAction;
  3992. };
  3993. const syncVisibleRefStateFromView = (action = STATE.action, refMode = STATE.refMode) => {
  3994. const safeAction = sanitizePanelKey(action, STATE.action);
  3995. const entry = getMutableRefViewEntry(safeAction, refMode);
  3996. const order = [...entry.order];
  3997. const orderSet = new Set(order);
  3998. const poolType = getRefPoolTypeForAction(safeAction);
  3999. STATE.refOrder = order;
  4000. STATE.items.forEach((item, id) => {
  4001. if (!isRefCompatibleWithPoolType(item, poolType)) return;
  4002. const key = String(id);
  4003. if (!orderSet.has(key)) {
  4004. item.active = false;
  4005. item.readyAt = 0;
  4006. return;
  4007. }
  4008. item.active = entry.active[key] === true;
  4009. item.readyAt = Number(entry.waitUntil[key]) || 0;
  4010. });
  4011. return entry;
  4012. };
  4013. const persistRefViews = () => DBQueue.addAppWrite('refViews', serializeRefViewStore(STATE.refViews), 90);
  4014. const registerReferenceAcrossViews = (id, action = STATE.action) => {
  4015. const key = String(id);
  4016. const poolType = getRefPoolTypeForAction(action);
  4017. let added = false;
  4018. getRefViewKeysForPoolType(poolType).forEach(viewKey => {
  4019. const [viewAction, viewMode] = viewKey.split(':');
  4020. const entry = getMutableRefViewEntry(viewAction, viewMode);
  4021. if (!entry.order.includes(key)) {
  4022. entry.order.push(key);
  4023. added = true;
  4024. }
  4025. });
  4026. return added;
  4027. };
  4028. const activateReferenceItem = (item, { action = STATE.action, refMode = STATE.refMode, exclusive = false } = {}) => {
  4029. const refItem = item && typeof item === 'object' ? item : STATE.items.get(String(item));
  4030. if (!refItem) return false;
  4031. const key = String(refItem.id);
  4032. const entry = getMutableRefViewEntry(action, refMode);
  4033. const wasInactive = entry.active[key] !== true;
  4034. if (exclusive) {
  4035. entry.order.forEach(rid => {
  4036. delete entry.active[String(rid)];
  4037. delete entry.waitUntil[String(rid)];
  4038. });
  4039. }
  4040. entry.active[key] = true;
  4041. if (shouldDelayReferenceActivation(action) && wasInactive) entry.waitUntil[key] = Date.now() + 10000;
  4042. else if (!(Number.isFinite(entry.waitUntil[key]) && entry.waitUntil[key] > Date.now())) delete entry.waitUntil[key];
  4043. if (isVisibleRefView(action, refMode)) {
  4044. refItem.active = true;
  4045. refItem.readyAt = Number(entry.waitUntil[key]) || 0;
  4046. }
  4047. return wasInactive;
  4048. };
  4049. const abortTasksForRef = (refId, action = STATE.action) => {
  4050. const targetAction = sanitizePanelKey(action, STATE.action);
  4051. JobManager.findTasksByRef(refId).forEach(task => {
  4052. const taskAction = sanitizePanelKey(task.action || JobManager.getRuntime(task.lane).activeAction || targetAction, targetAction);
  4053. if (taskAction === targetAction) task.controller.abort();
  4054. });
  4055. };
  4056. const updateReferenceOrder = (id, { position = 'front', exclusiveActive = false, enforceLimit = false, forceRebuild = true, action = STATE.action, refMode = STATE.refMode, activate = true } = {}) => {
  4057. const key = String(id);
  4058. const targetAction = sanitizePanelKey(action, STATE.action);
  4059. const targetRefMode = normalizeRefModeForAction(targetAction, refMode);
  4060. const item = STATE.items.get(key);
  4061. if (!item || !isRefCompatibleWithPoolType(item, getRefPoolTypeForAction(targetAction))) return false;
  4062. const targetEntry = getMutableRefViewEntry(targetAction, targetRefMode);
  4063. const hadTargetRef = targetEntry.order.includes(key);
  4064. const targetMaxRefs = (ACTION_SPEC[targetAction] ?? ACTION_SPEC.gen_image).maxRefs?.({ action: targetAction, refMode: targetRefMode }) ?? 0;
  4065. if (targetMaxRefs === 0) return false;
  4066. if (enforceLimit && !hadTargetRef && targetEntry.order.length >= Math.min(APP_CONFIG.LIMITS.MAX_REFS_UI, targetMaxRefs)) return false;
  4067. const isNewReference = registerReferenceAcrossViews(key, targetAction);
  4068. const nextEntry = getMutableRefViewEntry(targetAction, targetRefMode);
  4069. const isFront = position === 'front';
  4070. const wasEdge = isFront ? nextEntry.order[0] === key : nextEntry.order[nextEntry.order.length - 1] === key;
  4071. const reorder = nextEntry.order.filter(rid => String(rid) !== key);
  4072. if (isFront) reorder.unshift(key);
  4073. else reorder.push(key);
  4074. nextEntry.order = reorder;
  4075. if (exclusiveActive) {
  4076. nextEntry.order.forEach(rid => {
  4077. delete nextEntry.active[String(rid)];
  4078. delete nextEntry.waitUntil[String(rid)];
  4079. });
  4080. }
  4081. if (activate) {
  4082. const wasInactive = nextEntry.active[key] !== true;
  4083. nextEntry.active[key] = true;
  4084. if (shouldDelayReferenceActivation(targetAction) && (wasInactive || isNewReference || exclusiveActive)) nextEntry.waitUntil[key] = Date.now() + 10000;
  4085. else if (!(Number.isFinite(nextEntry.waitUntil[key]) && nextEntry.waitUntil[key] > Date.now())) delete nextEntry.waitUntil[key];
  4086. } else {
  4087. delete nextEntry.active[key];
  4088. delete nextEntry.waitUntil[key];
  4089. }
  4090. updateRefState({ forceRebuild: forceRebuild && isVisibleRefView(targetAction, targetRefMode) && !wasEdge });
  4091. if (isVisibleRefView(targetAction, targetRefMode) && DOM.refList) DOM.refList.scrollTop = isFront ? 0 : DOM.refList.scrollHeight;
  4092. return true;
  4093. };
  4094. const moveReferenceToFront = (id, opts = {}) => updateReferenceOrder(id, { ...opts, position: 'front' });
  4095. const appendReferenceToEnd = (id, opts = {}) => updateReferenceOrder(id, { ...opts, position: 'end' });
  4096. const pruneReferenceState = (ids, { closePrompt = true } = {}) => {
  4097. const keys = sanitizeIdListStore(ids);
  4098. if (!keys.length) return false;
  4099. const removed = new Set(keys);
  4100. let changed = false;
  4101. REF_VIEW_KEYS.forEach(viewKey => {
  4102. const [viewAction, viewMode] = viewKey.split(':');
  4103. const entry = getMutableRefViewEntry(viewAction, viewMode);
  4104. const nextOrder = [...new Set(entry.order)].filter(v => !removed.has(v));
  4105. if (!isSameIdList(entry.order, nextOrder)) {
  4106. entry.order = nextOrder;
  4107. changed = true;
  4108. }
  4109. keys.forEach(key => {
  4110. if (entry.active[key] === true || entry.waitUntil[key]) changed = true;
  4111. delete entry.active[key];
  4112. delete entry.waitUntil[key];
  4113. });
  4114. });
  4115. if (!changed) return false;
  4116. deleteRefPrompts(keys);
  4117. updateRefState();
  4118. if (closePrompt && STATE.activeRefPromptId && removed.has(String(STATE.activeRefPromptId))) saveAndCloseRefPrompt(true);
  4119. return true;
  4120. };
  4121.  
  4122. const syncRefCardsPromptFlags = (action = STATE.action) => {
  4123. CardRegistry.maps.reference.forEach((card, id) => {
  4124. card.setReferencePromptFlag(!!STATE.refPrompts[id]?.[action]);
  4125. });
  4126. };
  4127. function updateRefState({ forceRebuild = false } = {}) {
  4128. REF_VIEW_KEYS.forEach(viewKey => {
  4129. const [viewAction, viewMode] = viewKey.split(':');
  4130. STATE.refViews[viewKey] = normalizeRefViewEntryForAction(getRefViewEntry(viewAction, viewMode), viewAction);
  4131. });
  4132. syncVisibleRefStateFromView();
  4133. persistRefViews();
  4134. if (forceRebuild && DOM.refList) PanelManager.clear(DOM.refList, 'reference');
  4135. renderRefList();
  4136. updateUploadPlaceholderState();
  4137. if (isRuntimeBusy('media') && JobManager.getLane(STATE.action) === 'media') wakeUpQueue('media');
  4138. }
  4139.  
  4140. const CardRegistry = {
  4141. maps: {
  4142. current: new Map(),
  4143. history: new Map(),
  4144. reference: new Map()
  4145. },
  4146. register(card, context, id) {
  4147. const store = this.maps[context];
  4148. if (store) store.set(String(id), card);
  4149. },
  4150. unregister(card, context, id) {
  4151. const store = this.maps[context];
  4152. if (store && store.get(String(id)) === card) store.delete(String(id));
  4153. },
  4154. move(card, prevContext, prevId, nextContext, nextId) {
  4155. if (prevContext && prevId !== undefined) this.unregister(card, prevContext, prevId);
  4156. this.register(card, nextContext, nextId);
  4157. },
  4158. get(context, id) { return this.maps[context]?.get(String(id)) || null; },
  4159. forEachById(id, visitor) {
  4160. const key = String(id);
  4161. for (const store of Object.values(this.maps)) {
  4162. const card = store.get(key);
  4163. if (card) visitor(card);
  4164. }
  4165. },
  4166. setDownloaded(id, downloaded = true) {
  4167. this.forEachById(id, card => { if (downloaded) card._internals.states.add('downloaded'); else card._internals.states.delete('downloaded'); });
  4168. },
  4169. find(id) {
  4170. const targetId = String(id);
  4171. return this.maps.current.get(targetId) || this.maps.history.get(targetId) || this.maps.reference.get(targetId) || null;
  4172. },
  4173. clear(context = null) {
  4174. if (context) {
  4175. this.maps[context]?.forEach(card => card.unbind?.());
  4176. this.maps[context]?.clear();
  4177. } else {
  4178. for (const map of Object.values(this.maps)) {
  4179. map.forEach(card => card.unbind?.());
  4180. map.clear();
  4181. }
  4182. }
  4183. }
  4184. };
  4185.  
  4186. const CardObserverHub = (() => {
  4187. const preloadObserver = new IntersectionObserver((entries) => {
  4188. entries.forEach(entry => {
  4189. const card = entry.target;
  4190. if (!card._bound) return;
  4191.  
  4192. if (entry.isIntersecting) {
  4193. card._isPreloadIntersecting = true;
  4194. if (card._item?.type === 'image') card.setLoadLevel('full');
  4195. else if (card._mediaState === 'none') card.setLoadLevel('thumb');
  4196. } else {
  4197. card._isPreloadIntersecting = false;
  4198. card.setLoadLevel('thumb');
  4199. }
  4200. });
  4201. }, { rootMargin: '150% 0px 150% 0px' });
  4202.  
  4203. const playbackObserver = new IntersectionObserver((entries) => {
  4204. entries.forEach(entry => {
  4205. const card = entry.target;
  4206. if (!card._bound) return;
  4207.  
  4208. card._isActuallyVisible = entry.isIntersecting;
  4209. if (!entry.isIntersecting && typeof card.forcePause === 'function') {
  4210. card._isHovering = false;
  4211. card.forcePause();
  4212. }
  4213. });
  4214. }, { rootMargin: '0px', threshold: 0 });
  4215.  
  4216. return {
  4217. observe(card) { preloadObserver.observe(card); playbackObserver.observe(card); },
  4218. unobserve(card) { preloadObserver.unobserve(card); playbackObserver.unobserve(card); }
  4219. };
  4220. })();
  4221.  
  4222. const cardSheet = new CSSStyleSheet();
  4223. 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%;}`);;
  4224.  
  4225.  
  4226.  
  4227. 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>`;
  4228.  
  4229. const CARD_CHAT_TEMPLATE = document.createElement('template');
  4230. 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>`;
  4231.  
  4232. const VideoHoverPreviewer = new class {
  4233. constructor() {
  4234. this.video = document.createElement('video');
  4235. this.video.muted = true;
  4236. this.video.loop = true;
  4237. this.video.playsInline = true;
  4238. this.video.setAttribute('disablePictureInPicture', '');
  4239. this.video.setAttribute('disableRemotePlayback', '');
  4240. 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;';
  4241.  
  4242. this.activeCard = null;
  4243. this.enterTimer = null;
  4244. this.leaveTimer = null;
  4245. this.abortController = null;
  4246. this._activeBlobUrl = null;
  4247. }
  4248.  
  4249. async enter(card) {
  4250. if (this.activeCard === card) {
  4251. clearTimeout(this.leaveTimer);
  4252. if (this.video.style.opacity === '0') this.video.style.opacity = '1';
  4253. if (card.playIcon) card.playIcon.style.opacity = '0';
  4254. return;
  4255. }
  4256. this.cancel();
  4257.  
  4258. this.enterTimer = setTimeout(async () => {
  4259. this.activeCard = card;
  4260. const item = card._item;
  4261. if (!item || item.type !== 'video' || card._context !== 'current') return;
  4262.  
  4263. card.mediaWrapper.appendChild(this.video);
  4264. this.video.style.display = 'block';
  4265.  
  4266. this.abortController = new AbortController();
  4267. const signal = this.abortController.signal;
  4268.  
  4269. try {
  4270. const fetchId = item.refSourceId || item.id;
  4271. const file = await MediaCache.getFile(fetchId, 'full');
  4272. if (signal.aborted || !file) { this.clear(); return; }
  4273.  
  4274. if (this._activeBlobUrl) {
  4275. URL.revokeObjectURL(this._activeBlobUrl);
  4276. this._activeBlobUrl = null;
  4277. }
  4278. this._activeBlobUrl = URL.createObjectURL(file);
  4279. this.video.src = this._activeBlobUrl;
  4280. this.video.load();
  4281.  
  4282. await Promise.race([
  4283. new Promise(r => this.video.addEventListener('canplay', r, { once: true })),
  4284. new Promise((_, rej) => this.video.addEventListener('error', rej, { once: true })),
  4285. new Promise((_, rej) => AbortSignal.timeout(4000).addEventListener('abort', rej, { once: true })),
  4286. new Promise((_, rej) => signal.addEventListener('abort', rej, { once: true }))
  4287. ]);
  4288.  
  4289. if (signal.aborted) return;
  4290.  
  4291. await this.video.play();
  4292.  
  4293. this.video.style.opacity = '1';
  4294. if (card.playIcon) card.playIcon.style.opacity = '0';
  4295. card._internals.states.add('playing');
  4296.  
  4297. } catch (e) {
  4298. this.clear();
  4299. }
  4300. }, 120);
  4301. }
  4302.  
  4303. leave(card) {
  4304. if (this.activeCard !== card) return;
  4305. clearTimeout(this.enterTimer);
  4306.  
  4307. if (this.video.style.opacity === '1' || !this.video.paused) {
  4308. this.video.style.opacity = '0';
  4309. if (card.playIcon) card.playIcon.style.opacity = '1';
  4310. card._internals.states.delete('playing');
  4311.  
  4312. this.leaveTimer = setTimeout(() => this.clear(), 200);
  4313. } else {
  4314. this.clear();
  4315. }
  4316. }
  4317.  
  4318. cancel() {
  4319. clearTimeout(this.enterTimer);
  4320. clearTimeout(this.leaveTimer);
  4321. this.clear();
  4322. }
  4323.  
  4324. clear() {
  4325. if (this._activeBlobUrl) {
  4326. URL.revokeObjectURL(this._activeBlobUrl);
  4327. this._activeBlobUrl = null;
  4328. }
  4329. if (this.abortController) {
  4330. this.abortController.abort();
  4331. this.abortController = null;
  4332. }
  4333. this.video.pause();
  4334. this.video.removeAttribute('src');
  4335. this.video.srcObject = null;
  4336. this.video.load();
  4337. this.video.style.display = 'none';
  4338. this.video.style.opacity = '0';
  4339. if (this.video.parentNode) this.video.parentNode.removeChild(this.video);
  4340.  
  4341. if (this.activeCard) {
  4342. this.activeCard._internals.states.delete('playing');
  4343. if (this.activeCard.playIcon) this.activeCard.playIcon.style.opacity = '1';
  4344. }
  4345. this.activeCard = null;
  4346. }
  4347. };
  4348.  
  4349. class XaiCard extends HTMLElement {
  4350. constructor() {
  4351. super(); this.attachShadow({ mode: 'open' }); this.shadowRoot.adoptedStyleSheets =[cardSheet]; this._internals = this.attachInternals();
  4352. this._partInstance = CARD_TEMPLATE.createInstance({
  4353. refLabelText: 'IMAGE_0',
  4354. textModelName: '',
  4355. mediaWrapperPrompt: '',
  4356. bgBaseBg: '',
  4357. playDisplay: 'none',
  4358. cardPrompt: '',
  4359. cardPromptTooltip: ''
  4360. });
  4361. this.shadowRoot.append(this._partInstance.content);
  4362.  
  4363. this.mediaWrapper = this.shadowRoot.querySelector('.media-wrapper');
  4364. this.playIcon = this.shadowRoot.getElementById('play');
  4365. this.imgEl = this.shadowRoot.getElementById('img');
  4366. this.statusOverlay = this.shadowRoot.getElementById('status-overlay');
  4367. this.refToggle = this.shadowRoot.querySelector('.ref-active-toggle');
  4368. this.refSideBtn = this.shadowRoot.querySelector('.ref-side-prompt-btn');
  4369. this.refLabelText = this.shadowRoot.querySelector('.ref-label-text');
  4370. this.textChatHeader = this.shadowRoot.getElementById('text-chat-header');
  4371.  
  4372. this.audioTextEl = null;
  4373. this.vidEl = null;
  4374. this.chatUserMsgEl = null;
  4375. this.chatAiMsgEl = null;
  4376. this._chatContainerEl = null;
  4377.  
  4378. this._bound = false; this._mediaState = 'none';
  4379. this._managedBlobUrls = new Set();
  4380. this._currentImgUrl = null;
  4381. this._currentVidUrl = null;
  4382. this._loadController = null; this._hoverTimer = null; this._warmTimer = null; this._isActuallyVisible = false; this._isHovering = false; this._hoverUpgraded = false; this._statusSig = ''; this._progressValue = null;
  4383. this._cardAbortController = null;
  4384. this._statusPool = Array.from({ length: 3 }, () => {
  4385. const el = document.createElement('div');
  4386. el.className = 'ref-status-line';
  4387. el.style.display = 'none';
  4388. this.statusOverlay.appendChild(el);
  4389. return el;
  4390. });
  4391. this.progressBar = this.shadowRoot.getElementById('progress-bar');
  4392.  
  4393. }
  4394.  
  4395. setProgress(p, durationMs = 300, easing = 'ease-out') {
  4396. const next = p == null ? null : Number(p);
  4397. if (this._progressValue === next) return;
  4398. this._progressValue = next;
  4399.  
  4400. if (!this.progressBar) return;
  4401.  
  4402. if (next == null) {
  4403. this.progressBar.style.display = 'none';
  4404. this.progressBar.style.transition = 'none';
  4405. this.style.removeProperty('--card-progress');
  4406. this.removeAttribute('data-progress');
  4407. return;
  4408. }
  4409.  
  4410. this.dataset.progress = String(next);
  4411. this.style.setProperty('--card-progress', `${next}%`);
  4412.  
  4413. if (this.progressBar.style.display !== 'block') {
  4414. this.progressBar.style.display = 'block';
  4415. this.progressBar.offsetWidth;
  4416. }
  4417. }
  4418. reflectClassFlags(flags = []) {
  4419. const s = this._internals.states;
  4420. flags.forEach(([name, enabled]) => { if (enabled) s.add(name); else s.delete(name); });
  4421. }
  4422. setStatus(lines) {
  4423. const normalized = Array.isArray(lines) ? lines : [];
  4424. const sig = normalized.length ? normalized.map(l => `${l.text || ''}|${l.color || ''}|${l.style || ''}`).join('\n') : '';
  4425. if (this._statusSig === sig) return;
  4426. this._statusSig = sig;
  4427.  
  4428. if (normalized.length === 0) {
  4429. this.statusOverlay.style.display = 'none';
  4430. if (this._item?.type === 'text' && this._item.isPending && this.chatAiMsgEl) {
  4431. this.chatAiMsgEl.textContent = 'Queued...';
  4432. this.chatAiMsgEl.style.opacity = '0.6';
  4433. this.chatAiMsgEl.style.fontStyle = 'italic';
  4434. }
  4435. for (let i = 0; i < this._statusPool.length; i++) {
  4436. if (this._statusPool[i].style.display !== 'none') this._statusPool[i].style.display = 'none';
  4437. }
  4438. return;
  4439. }
  4440.  
  4441. if (this._item?.type === 'text' && this._item.isPending && this.chatAiMsgEl) {
  4442. const statusText = normalized.length ? normalized[normalized.length - 1].text : '';
  4443. this.chatAiMsgEl.textContent = statusText;
  4444. this.chatAiMsgEl.style.opacity = '0.6';
  4445. this.chatAiMsgEl.style.fontStyle = 'italic';
  4446. }
  4447.  
  4448. this.statusOverlay.style.display = 'flex';
  4449.  
  4450. for (let i = 0; i < this._statusPool.length; i++) {
  4451. const node = this._statusPool[i];
  4452. const line = normalized[i];
  4453. if (line) {
  4454. const nextCss = (line.style || '') + (line.color ? `color:${line.color};` : '');
  4455. if (node._lastCss !== nextCss) {
  4456. node.style.cssText = nextCss;
  4457. node._lastCss = nextCss;
  4458. }
  4459. if (node.textContent !== line.text) node.textContent = line.text;
  4460. if (node.style.display !== 'block') node.style.display = 'block';
  4461. } else {
  4462. if (node.style.display !== 'none') node.style.display = 'none';
  4463. }
  4464. }
  4465. }
  4466. _updateParts(vals) {
  4467. this._partInstance.update(vals);
  4468. }
  4469. setLabel(text, title = '', onClick = null) {
  4470. if (!this.refLabelText) return;
  4471. this._updateParts({ refLabelText: text });
  4472. this.refLabelText.classList.toggle('insertable-tag', !!onClick);
  4473. this.refLabelText.title = title;
  4474. if (onClick) {
  4475. this.refLabelText.setAttribute('data-intent', 'insert-tag');
  4476. this._insertTagIdx = text.match(/IMAGE_(\d+)/)?.[1] ?? '0';
  4477. } else {
  4478. this.refLabelText.removeAttribute('data-intent');
  4479. this._insertTagIdx = null;
  4480. }
  4481. }
  4482. setHistoryPrompt(prompt, itemType) {
  4483. const safePrompt = Pipeline.getDisplayPrompt(prompt);
  4484. const turns = itemType === 'text' && this._item && this._item.textTurnCount > 0 ? `${this._item.textTurnCount}t ` : '';
  4485. const prefix = itemType === 'text' ? `💬 ${turns}` : '';
  4486. const preview = safePrompt ? (safePrompt.length > 120 ? safePrompt.substring(0, 120) + '...' : safePrompt) : '';
  4487. this._updateParts({
  4488. cardPromptTooltip: '',
  4489. cardPrompt: prefix + preview
  4490. });
  4491. }
  4492. setPromptTooltip(prompt, timestamp) {
  4493. const displayPrompt = Pipeline.getDisplayPrompt(prompt);
  4494. const safePrompt = displayPrompt ? (displayPrompt.length > 1500 ? displayPrompt.substring(0, 1500) + '...' : displayPrompt) : '';
  4495. const timePrefix = TimeUtils.forTooltip(timestamp);
  4496. const fullText = `${timePrefix}\n${safePrompt}`;
  4497. if (this.title !== fullText) this.title = fullText;
  4498. }
  4499. setReferenceActive(active) {
  4500. if (this.refToggle) this.refToggle.checked = !!active;
  4501. if (!active) this._internals.states.add('inactive'); else this._internals.states.delete('inactive');
  4502. }
  4503. setReferencePromptFlag(hasPrompt) {
  4504. if (this.refSideBtn) this.refSideBtn.classList.toggle('has-prompt', !!hasPrompt);
  4505. if (hasPrompt) this._internals.states.add('has-custom-prompt'); else this._internals.states.delete('has-custom-prompt');
  4506. }
  4507. refresh(item, context = this._context) {
  4508. this.draggable = ['history', 'current', 'reference'].includes(context) && !item?.isPending;
  4509. if (!this._bound || this.dataset.id !== String(item.id) || this._context !== context) {
  4510. this.bind(item, context);
  4511. return;
  4512. }
  4513. this._item = item;
  4514. this.dataset.prompt = item.shortPrompt || '';
  4515. const shortForDisplay = item.shortPrompt || '';
  4516. this.setPromptTooltip(shortForDisplay, item.timestamp);
  4517. if (this.audioTextEl) {
  4518. this.audioTextEl.textContent = item.shortPrompt || 'Audio Generation';
  4519. }
  4520. const isRefCard = !!item.refSourceId || !!item.originalUrl || !!item.isUploadedRef || context === 'reference';
  4521. this.reflectClassFlags([
  4522. ['has-ref', isRefCard],
  4523. ['downloaded', item.downloaded],
  4524. ['inactive', context === 'reference' && !item.active],
  4525. ['pending', item.isPending],
  4526. ['type-image', item.type === 'image'],
  4527. ['type-video', item.type === 'video'],
  4528. ['type-audio', item.type === 'audio'],
  4529. ['type-text', item.type === 'text']
  4530. ]);
  4531. if (item.ratio) {
  4532. const ratioValue = String(item.ratio);
  4533. if (this.style.getPropertyValue('--ratio') !== ratioValue) this.style.setProperty('--ratio', ratioValue);
  4534. const vertical = item.ratio <= 1.05 ? 'true' : 'false';
  4535. if (this.dataset.vertical !== vertical) this.dataset.vertical = vertical;
  4536. }
  4537. if (item.type === 'audio' && !this.audioTextEl) {
  4538. this.audioTextEl = document.createElement('div');
  4539. this.audioTextEl.className = 'audio-text-overlay';
  4540. this.mediaWrapper.appendChild(this.audioTextEl);
  4541. }
  4542. const bg = item.lqip || '';
  4543. const playDisplay = (item.type === 'video' || item.type === 'audio') && context !== 'reference' && !item.isPending ? 'flex' : 'none';
  4544. const modelName = item.type === 'text' ? (item.textModel || STATE.textModel || 'grok-4.20') : (this.shadowRoot.getElementById('text-model-name')?.textContent || '');
  4545. this._updateParts({
  4546. mediaWrapperPrompt: item.shortPrompt || '',
  4547. bgBaseBg: bg,
  4548. playDisplay: playDisplay,
  4549. textModelName: modelName
  4550. });
  4551. if (item.isPending) this.setStatus(item._statusData || [{ text: 'Queued...', color: '#fff' }]);
  4552. if (item.isPending && item.refSourceId && this._mediaState === 'none') {
  4553. this.setLoadLevel('thumb');
  4554. }
  4555. else if (context === 'reference') {
  4556. this.setReferenceActive(item.active);
  4557. this.setReferencePromptFlag(!!STATE.refPrompts[item.id]?.[STATE.action]);
  4558. if (STATE.activeRefPromptId === item.id) this._internals.states.add('editing'); else this._internals.states.delete('editing');
  4559. } else if (context === 'history') this.setHistoryPrompt(item.shortPrompt, item.type);
  4560. LazyTextStore.get(item.id).then(heavy => {
  4561. if (!this._bound || this.dataset.id !== String(item.id)) return;
  4562. this.dataset.prompt = heavy.prompt || '';
  4563. this._updateParts({ mediaWrapperPrompt: heavy.prompt || '' });
  4564. this.setPromptTooltip(heavy.prompt, item.timestamp);
  4565. if (context === 'history') this.setHistoryPrompt(heavy.prompt, item.type);
  4566. if (this.chatUserMsgEl) this.chatUserMsgEl.textContent = heavy.prompt || '';
  4567. if (this.chatAiMsgEl && !item.isPending) {
  4568. const resp = heavy.responseText || '';
  4569. this.chatAiMsgEl.textContent = resp.length > 120 ? resp.substring(0, 120) + '...' : resp;
  4570. this.chatAiMsgEl.style.opacity = '1';
  4571. this.chatAiMsgEl.style.fontStyle = 'normal';
  4572. }
  4573. });
  4574.  
  4575. if (item.isNewlyGenerated) {
  4576. this._isPreloadIntersecting = true;
  4577. this.setLoadLevel('full');
  4578. delete item.isNewlyGenerated;
  4579. }
  4580.  
  4581. if (this._bound && !item.isPending && this._isPreloadIntersecting) {
  4582. if (item.type === 'image' && this._context === 'current' && this._mediaState !== 'full' && !isMemoryConstrainedMode()) {
  4583. this.setLoadLevel('full');
  4584. }
  4585. }
  4586. }
  4587. connectedCallback() { if (this._bound) CardObserverHub.observe(this); }
  4588. disconnectedCallback() {
  4589. CardObserverHub.unobserve(this);
  4590. if (!this.isConnected && this._bound) {
  4591. this.unbind();
  4592. }
  4593. }
  4594. forcePause() {
  4595. if (this._item?.type === 'video') {
  4596. VideoHoverPreviewer.leave(this);
  4597. }
  4598. }
  4599. hydrateMedia() {
  4600. if (!this._bound || !this._item) return false;
  4601. const type = this._item.type;
  4602. if (isMemoryConstrainedMode() && (type === 'video' || type === 'audio')) return false;
  4603. if (type === 'audio') {
  4604. if (!this.vidEl) {
  4605. this.vidEl = document.createElement('video');
  4606. this.vidEl.id = 'vid';
  4607. this.vidEl.preload = 'none';
  4608. this.vidEl.muted = true;
  4609. this.vidEl.loop = true;
  4610. this.vidEl.playsInline = true;
  4611. this.vidEl.setAttribute('playsinline', '');
  4612. this.vidEl.setAttribute('disablePictureInPicture', '');
  4613. this.vidEl.setAttribute('disableRemotePlayback', '');
  4614. this.mediaWrapper.appendChild(this.vidEl);
  4615. }
  4616. }
  4617. if (type === 'text') {
  4618. if (!this._chatContainerEl) {
  4619. const clone = CARD_CHAT_TEMPLATE.content.cloneNode(true);
  4620. const container = clone.querySelector('#chat-container');
  4621. this.mediaWrapper.appendChild(container);
  4622. this._chatContainerEl = container;
  4623. this.chatUserMsgEl = clone.querySelector('#chat-user-msg');
  4624. this.chatAiMsgEl = clone.querySelector('#text-content');
  4625. const itemId = String(this._item.id);
  4626. LazyTextStore.get(itemId).then(heavy => {
  4627. if (!this._bound || String(this._item?.id) !== itemId) return;
  4628. if (this.chatUserMsgEl) this.chatUserMsgEl.textContent = heavy.prompt || '';
  4629. if (this.chatAiMsgEl && !this._item.isPending) {
  4630. this.chatAiMsgEl.textContent = heavy.responseText || '';
  4631. this.chatAiMsgEl.style.opacity = '1';
  4632. this.chatAiMsgEl.style.fontStyle = 'normal';
  4633. }
  4634. });
  4635. }
  4636. }
  4637. return true;
  4638. }
  4639. dehydrateMedia() {
  4640. if (this.vidEl) {
  4641. resetVideoElement(this.vidEl);
  4642. this.vidEl.remove();
  4643. this.vidEl = null;
  4644. }
  4645. if (this._chatContainerEl) {
  4646. this._chatContainerEl.remove();
  4647. this._chatContainerEl = null;
  4648. this.chatUserMsgEl = null;
  4649. this.chatAiMsgEl = null;
  4650. }
  4651. if (this.audioTextEl) {
  4652. this.audioTextEl.remove();
  4653. this.audioTextEl = null;
  4654. }
  4655. }
  4656.  
  4657. [Symbol.dispose]() {
  4658. this.unbind();
  4659. }
  4660.  
  4661. setHoverState(isHover) {
  4662. if (!this._bound || this._item?.type !== 'video' || this._context !== 'current') return;
  4663. if (this._isHovering === isHover) return;
  4664. this._isHovering = isHover;
  4665.  
  4666. if (this._item.type === 'video') {
  4667. if (STATE.ecoMode || App.isAutoEco || DOM.lightbox?.open) return;
  4668. if (isHover) VideoHoverPreviewer.enter(this);
  4669. else VideoHoverPreviewer.leave(this);
  4670. return;
  4671. }
  4672.  
  4673. clearTimeout(this._hoverTimer);
  4674. this._hoverTimer = null;
  4675. if (!isHover) {
  4676. this.forcePause();
  4677. this._hoverTimer = setTimeout(() => {
  4678. this._hoverTimer = null;
  4679. if (this._isHovering) return;
  4680. if (this._hoverUpgraded) { this._hoverUpgraded = false; this.setLoadLevel('thumb'); }
  4681. }, 300);
  4682. return;
  4683. }
  4684. if (STATE.ecoMode || App.isAutoEco || DOM.lightbox?.open) return;
  4685. if (!this.vidEl && !this.hydrateMedia()) return;
  4686. this._hoverTimer = setTimeout(async () => {
  4687. this._hoverTimer = null;
  4688. if (!this._isHovering || !this._bound) return;
  4689. try {
  4690. if (this._mediaState !== 'full') {
  4691. this._hoverUpgraded = true;
  4692. await this.setLoadLevel('full');
  4693. }
  4694. if (!this._isHovering || DOM.lightbox?.open || !this.vidEl) return;
  4695.  
  4696. if (!this.vidEl.src || this.vidEl.src === location.href) {
  4697. const fetchId = this._item.refSourceId || this._item.id;
  4698. const file = await MediaCache.getFile(fetchId, 'full');
  4699. 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; }
  4700. if (!this.vidEl.src) return;
  4701. }
  4702. this.vidEl.style.display = 'block';
  4703. this.vidEl.muted = true;
  4704.  
  4705. if (this.vidEl.readyState < 3) {
  4706. await Promise.race([
  4707. new Promise(r => this.vidEl.addEventListener('canplay', r, { once: true })),
  4708. new Promise((_, rej) => this.vidEl.addEventListener('error', e => rej(e.error ?? new Error('video load error')), { once: true })),
  4709. new Promise(r => AbortSignal.timeout(5000).addEventListener('abort', r, { once: true }))
  4710. ]);
  4711. }
  4712. if (!this._isHovering) return;
  4713.  
  4714. await this.vidEl.play();
  4715.  
  4716. if (this._isHovering && !this.vidEl.paused) {
  4717. if (this.playIcon) this.playIcon.style.opacity = '0';
  4718. this._internals.states.add('playing');
  4719. }
  4720. } catch(e) {
  4721. if (e?.name !== 'AbortError') this._internals.states.delete('playing');
  4722. if (this.playIcon) this.playIcon.style.opacity = '1';
  4723. }
  4724. }, 80);
  4725. }
  4726. _abortLoad() { if (this._loadController) { this._loadController.abort(); this._loadController = null; } }
  4727.  
  4728. async setLoadLevel(level) {
  4729. if (this._mediaState === level || !this._bound) return;
  4730. if (this._item.isPending && !this._item.refSourceId) return;
  4731. const isImageOrVideo = this._item.type === 'image' || this._item.type === 'video';
  4732. const isHistoryOrRef = this._context === 'history' || this._context === 'reference';
  4733. const isCurrentVideoNotHovered = this._context === 'current' && this._item.type === 'video' && !this._isHovering;
  4734.  
  4735. if (level === 'full' && (this._item.isPending || STATE.ecoMode || (isImageOrVideo && isHistoryOrRef) || isCurrentVideoNotHovered)) {
  4736. level = 'thumb'; if (this._mediaState === level) return;
  4737. }
  4738.  
  4739. this._abortLoad(); this._loadController = new AbortController(); const signal = this._loadController.signal;
  4740.  
  4741. if (level === 'none') {
  4742. this._clearSrc();
  4743. this.dehydrateMedia();
  4744. return;
  4745. }
  4746.  
  4747. if (level === 'thumb') {
  4748. if (level === 'thumb' && this._mediaState === 'none' && !this._item.isPending) {
  4749. this._clearSrc();
  4750. }
  4751. this.imgEl.style.display = 'block';
  4752. const isDehydratable = this._item.type !== 'video' && this._item.type !== 'audio';
  4753. if (isDehydratable) this.dehydrateMedia();
  4754. }
  4755.  
  4756. if (level === 'full') {
  4757. this.hydrateMedia();
  4758. }
  4759.  
  4760. try {
  4761. const { thumbFile, fullFile } = await this._resolveUrls(level, signal);
  4762. if (signal.aborted) return;
  4763. await this._applySrc(level, thumbFile, fullFile, signal, String(this._item?.id || ''));
  4764. } catch(e) { if (e.name !== 'AbortError') addLog('WARN', 'setLoadLevel error', e); }
  4765. }
  4766.  
  4767. async _resolveUrls(level, signal) {
  4768. const fetchId = this._item.refSourceId || this._item.id;
  4769. const isAudio = this._item.type === "audio";
  4770. const needsThumb = !isAudio && (this._item.type === "video" || this._item.type === "image" || level !== "full" || this._context !== "current" || isMemoryConstrainedMode());
  4771.  
  4772. const thumbPromise = needsThumb ? MediaCache.getFile(fetchId, "thumb") : Promise.resolve(null);
  4773. const fullPromise = level === "full"
  4774. ? MediaCache.getFile(fetchId, "full").then(f => f || (this._item.originalUrl || null))
  4775. : Promise.resolve(null);
  4776.  
  4777. let [thumbFile, fullFile] = await Promise.all([thumbPromise, fullPromise]);
  4778.  
  4779. if (!thumbFile && !fullFile && level === "full" && this._item.type === "image") {
  4780. thumbFile = await MediaCache.getFile(fetchId, "thumb");
  4781. }
  4782. if (signal.aborted) return {};
  4783.  
  4784. return { thumbFile, fullFile };
  4785. }
  4786.  
  4787. async _applySrc(level, thumbFile, fullFile, signal = this._loadController?.signal) {
  4788. if (!this._bound) return;
  4789. this._mediaState = level;
  4790. const expectedId = this.dataset.id;
  4791.  
  4792. if (this._item.type === 'image') {
  4793. if (this.vidEl) this.vidEl.style.display = 'none';
  4794. this.imgEl.style.display = 'block';
  4795. this.imgEl.loading = this._context === 'history' ? 'lazy' : 'eager';
  4796. this.imgEl.fetchPriority = (this._context === 'current' && level === 'full') ? 'high' : 'auto';
  4797.  
  4798. if (level === 'full' && thumbFile && !this.imgEl.src) {
  4799. if (this._currentImgUrl) this._revokeTrackedUrl(this._currentImgUrl);
  4800. this._currentImgUrl = this._trackUrl(URL.createObjectURL(thumbFile));
  4801. const tempUrl = this._currentImgUrl;
  4802.  
  4803. this.imgEl.onload = this.imgEl.onerror = () => {
  4804. this._revokeTrackedUrl(tempUrl);
  4805. if (this._currentImgUrl === tempUrl) this._currentImgUrl = null;
  4806. };
  4807. this.imgEl.src = this._currentImgUrl;
  4808. this._internals.states.add('loaded');
  4809. }
  4810.  
  4811. const targetFile = fullFile || thumbFile;
  4812. if (targetFile) {
  4813. const tempUrl = URL.createObjectURL(targetFile);
  4814. if (this.imgEl.src !== tempUrl) {
  4815. try {
  4816. const preloader = new Image();
  4817. preloader.src = tempUrl;
  4818. await preloader.decode();
  4819.  
  4820. if (signal.aborted || !this.imgEl) {
  4821. URL.revokeObjectURL(tempUrl);
  4822. return;
  4823. }
  4824.  
  4825. const oldUrl = this._currentImgUrl;
  4826. this._currentImgUrl = this._trackUrl(tempUrl);
  4827.  
  4828. this.imgEl.src = tempUrl;
  4829. this.imgEl.style.display = 'block';
  4830. this._internals.states.add('loaded');
  4831. this._mediaState = level;
  4832.  
  4833. if (oldUrl) URL.revokeObjectURL(oldUrl);
  4834.  
  4835. } catch (err) {
  4836. URL.revokeObjectURL(tempUrl);
  4837. if (err.name !== 'AbortError' && err.name !== 'EncodingError') console.warn('[xAI] Decode failed', err);
  4838. }
  4839. }
  4840. } else {
  4841. this.imgEl.removeAttribute('src');
  4842. this._internals.states.delete('loaded');
  4843. }
  4844. } else if (this._item.type === 'audio') {
  4845. this.imgEl.style.display = 'none';
  4846. if (this.vidEl) {
  4847. this.vidEl.removeAttribute('crossorigin');
  4848. if (level === 'full' && (fullFile || thumbFile)) {
  4849. this.vidEl.style.display = 'block';
  4850. const targetFile = fullFile || thumbFile;
  4851. if (targetFile) {
  4852. if (this._currentVidUrl) this._revokeTrackedUrl(this._currentVidUrl);
  4853. this._currentVidUrl = this._trackUrl(URL.createObjectURL(targetFile));
  4854. this.vidEl.src = this._currentVidUrl;
  4855. this._internals.states.add('loaded');
  4856. }
  4857. } else {
  4858. resetVideoElement(this.vidEl, { pause: false });
  4859. }
  4860. }
  4861. } else if (this._item.type === 'text') {
  4862. this.imgEl.style.display = 'none';
  4863. if (this.vidEl) this.vidEl.style.display = 'none';
  4864. if (level === 'full' && fullFile) {
  4865. fullFile.text().then(txt => {
  4866. if (signal.aborted || !this._bound || this.dataset.id !== expectedId) return;
  4867. if (this.chatAiMsgEl) this.chatAiMsgEl.textContent = txt;
  4868. this._internals.states.add('loaded');
  4869. }).catch(err => {
  4870. if (err.name !== 'AbortError') addLog('WARN', 'text card load failed', err);
  4871. });
  4872. }
  4873. } else {
  4874. this.imgEl.style.display = 'block';
  4875.  
  4876. if (thumbFile) {
  4877. if (this._currentImgUrl) this._revokeTrackedUrl(this._currentImgUrl);
  4878. this._currentImgUrl = this._trackUrl(URL.createObjectURL(thumbFile));
  4879. const tempUrl = this._currentImgUrl;
  4880. try {
  4881. await DecodeQueue.enqueue(tempUrl, signal);
  4882. if (!signal.aborted && this.imgEl) {
  4883. this.imgEl.onload = this.imgEl.onerror = () => {
  4884. this._revokeTrackedUrl(tempUrl);
  4885. if (this._currentImgUrl === tempUrl) this._currentImgUrl = null;
  4886. };
  4887. this.imgEl.src = tempUrl;
  4888. this.imgEl.style.display = 'block';
  4889. this._internals.states.add('loaded');
  4890. this._mediaState = level;
  4891. } else {
  4892. this._revokeTrackedUrl(tempUrl);
  4893. if (this._currentImgUrl === tempUrl) this._currentImgUrl = null;
  4894. }
  4895. } catch (err) {
  4896. this._revokeTrackedUrl(tempUrl);
  4897. if (this._currentImgUrl === tempUrl) this._currentImgUrl = null;
  4898. if (err.name !== 'AbortError' && err.name !== 'EncodingError') console.warn('[xAI] Decode failed', err);
  4899. }
  4900. } else {
  4901. this.imgEl.style.display = 'none';
  4902. }
  4903.  
  4904. if (this.vidEl) {
  4905. this.vidEl.removeAttribute('crossorigin');
  4906. if (level === 'full' && fullFile) {
  4907. this.vidEl.style.display = 'block';
  4908. if (this._currentVidUrl) this._revokeTrackedUrl(this._currentVidUrl);
  4909. this._currentVidUrl = this._trackUrl(URL.createObjectURL(fullFile));
  4910. this.vidEl.src = this._currentVidUrl;
  4911. const onLoaded = () => { if (this._bound) this._internals.states.add('loaded'); };
  4912. if (this.vidEl.readyState >= 2) onLoaded(); else this.vidEl.addEventListener('loadeddata', onLoaded, { once: true });
  4913. } else resetVideoElement(this.vidEl, { pause: false });
  4914. }
  4915. }
  4916. }
  4917.  
  4918. _trackUrl(url) {
  4919. if (url && url.startsWith('blob:')) this._managedBlobUrls.add(url);
  4920. return url;
  4921. }
  4922.  
  4923. _revokeTrackedUrl(url) {
  4924. if (this._managedBlobUrls.has(url)) {
  4925. URL.revokeObjectURL(url);
  4926. this._managedBlobUrls.delete(url);
  4927. }
  4928. }
  4929.  
  4930. _revokeAllUrls() {
  4931. this._managedBlobUrls.forEach(url => URL.revokeObjectURL(url));
  4932. this._managedBlobUrls.clear();
  4933. this._currentImgUrl = null;
  4934. this._currentVidUrl = null;
  4935. }
  4936.  
  4937. _clearSrc() {
  4938. this._revokeAllUrls();
  4939. this.forcePause();
  4940.  
  4941. if (this.imgEl) {
  4942. this.imgEl.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
  4943. this.imgEl.removeAttribute('src');
  4944. this.imgEl.style.display = 'none';
  4945. }
  4946.  
  4947. if (this.vidEl) {
  4948. resetVideoElement(this.vidEl);
  4949. this.vidEl.style.display = 'none';
  4950. }
  4951. if (this.chatAiMsgEl) this.chatAiMsgEl.textContent = '';
  4952.  
  4953. this._internals.states.delete('loaded'); this._internals.states.delete('playing');
  4954. this._mediaState = 'none';
  4955. }
  4956.  
  4957. bind(item, context) {
  4958. const prevContext = this._context, prevId = this.dataset.id;
  4959. this.unbind(); this._item = item; this._context = context; this.dataset.id = item.id;
  4960. this.className = 'xai-card';
  4961. this._internals.states.clear();
  4962. this._internals.states.add(context);
  4963. this._internals.states.add('type-' + item.type);
  4964.  
  4965. CardRegistry.move(this, prevContext, prevId, context, item.id);
  4966. if (this.vidEl) this.vidEl.style.display = 'none'; if (this.imgEl) this.imgEl.style.display = 'none';
  4967. this._internals.states.delete('loaded'); this._internals.states.delete('playing'); this._internals.states.delete('processing');
  4968. this._statusSig = '';
  4969. this._progressValue = null;
  4970. this._bound = true;
  4971. this.refresh(item, context);
  4972.  
  4973. this._cardAbortController = new AbortController();
  4974. const cardSignal = this._cardAbortController.signal;
  4975.  
  4976. if (context === 'current' && (item.type === 'video' || item.type === 'audio')) {
  4977. this.addEventListener('pointerenter', () => this.setHoverState(true), { signal: cardSignal });
  4978. this.addEventListener('pointerleave', () => this.setHoverState(false), { signal: cardSignal });
  4979. }
  4980.  
  4981. AppState.action.bind(currentAction => {
  4982. this._partInstance.update({ textModelName: currentAction === 'gen_text' ? (AppState.textModel.value || '') : '' });
  4983. }, cardSignal);
  4984.  
  4985. CardObserverHub.observe(this);
  4986.  
  4987. }
  4988. unbind() {
  4989. if (this._item?.type === 'video') VideoHoverPreviewer.leave(this);
  4990. if (this._cardAbortController) { this._cardAbortController.abort(); this._cardAbortController = null; }
  4991. if (this.dataset.id) {
  4992. RefPayloadCache.delete(this.dataset.id);
  4993. }
  4994. CardRegistry.unregister(this, this._context, this.dataset.id);
  4995. clearTimeout(this._hoverTimer); this._hoverTimer = null;
  4996. clearTimeout(this._warmTimer); this._warmTimer = null;
  4997. this._abortLoad();
  4998. if (this.vidEl) {
  4999. this.vidEl.pause();
  5000. this.vidEl.removeAttribute('src');
  5001. this.vidEl.load();
  5002. }
  5003. this._clearSrc();
  5004. this.dehydrateMedia();
  5005. CardObserverHub.unobserve(this);
  5006. this._mediaState = 'none';
  5007. this._isActuallyVisible = false;
  5008. this._isHovering = false;
  5009. this._hoverUpgraded = false;
  5010. this._isPreloadIntersecting = false;
  5011. this._statusSig = '';
  5012. this._progressValue = null;
  5013. this.removeAttribute('data-id');
  5014. this.removeAttribute('data-progress');
  5015. this.removeAttribute('data-prompt');
  5016. this.removeAttribute('data-vertical');
  5017. if (!this._isPoolCard) this.removeAttribute('style');
  5018. this.className = 'xai-card';
  5019. this._internals.states.clear();
  5020. if (this._partInstance) this._updateParts({ bgBaseBg: '' });
  5021. if (this.imgEl) this.imgEl.src = '';
  5022. this._item = null;
  5023. this._context = null;
  5024. this._bound = false;
  5025. }
  5026. }
  5027. if (!customElements.get('xai-card')) customElements.define('xai-card', XaiCard);
  5028.  
  5029. class HybridVirtualGrid {
  5030. constructor(container) {
  5031. this.container = container;
  5032. this.nodesMap = new Map();
  5033. this._pool = [];
  5034. this.itemsData = [];
  5035. this.rafId = 0;
  5036. this.colWidth = 0;
  5037. this.yPositions = [];
  5038. this.heights = [];
  5039. this.totalHeight = 0;
  5040.  
  5041.  
  5042. this.spacer = document.createElement('div');
  5043. this.spacer.className = 'scroll-spacer';
  5044. this.container.appendChild(this.spacer);
  5045.  
  5046. this.resizeObs = new ResizeObserver((entries) => {
  5047. const cw = entries[0].contentRect.width;
  5048. if (!cw || this.colWidth === cw) return;
  5049. this.colWidth = cw;
  5050. this._recalcLayout();
  5051. this._scheduleRender();
  5052. });
  5053. this.resizeObs.observe(this.container);
  5054. this.container.addEventListener('scroll', () => this._scheduleRender(), { passive: true });
  5055. }
  5056.  
  5057. _itemHeight(meta) {
  5058. if (meta.type === 'text') return 70;
  5059. if (meta.type === 'audio') return 120;
  5060. const ratio = meta.ratio || 1;
  5061. const targetRatio = ratio < 1 ? ratio / 0.95 : ratio;
  5062. return Math.max(68, Math.round(this.colWidth / Math.max(0.5, targetRatio)));
  5063. }
  5064.  
  5065. _recalcLayout() {
  5066. const cw = this.colWidth || this.container.clientWidth;
  5067. if (!cw) return;
  5068. this.colWidth = cw;
  5069. let y = 0;
  5070. this.yPositions = [];
  5071. this.heights = [];
  5072. for (let i = 0; i < this.itemsData.length; i++) {
  5073. this.yPositions.push(y);
  5074. const h = this._itemHeight(this.itemsData[i].meta);
  5075. this.heights.push(h);
  5076. y += h + 4;
  5077. }
  5078. this.totalHeight = y;
  5079. this.spacer.style.height = this.totalHeight + 'px';
  5080. }
  5081.  
  5082. _findStartIndex(scrollTop) {
  5083. let lo = 0, hi = this.yPositions.length - 1;
  5084. while (lo < hi) {
  5085. const mid = (lo + hi + 1) >> 1;
  5086. if (this.yPositions[mid] <= scrollTop) lo = mid; else hi = mid - 1;
  5087. }
  5088. return Math.max(0, lo - 1);
  5089. }
  5090.  
  5091. _scheduleRender() {
  5092. if (!this.rafId) this.rafId = requestAnimationFrame(() => { this.rafId = 0; this.render(); });
  5093. }
  5094.  
  5095. updateData(ids, itemDataMap) {
  5096. this.itemsData = ids.map(id => ({ id: String(id), meta: itemDataMap.get(String(id)) })).filter(d => d.meta);
  5097. this._recalcLayout();
  5098. this.render();
  5099. }
  5100.  
  5101. render() {
  5102. if (!this.itemsData.length) {
  5103. this.clear();
  5104. return;
  5105. }
  5106. const scrollTop = this.container.scrollTop;
  5107. const viewH = this.container.clientHeight;
  5108. const startIndex = this._findStartIndex(scrollTop);
  5109. const itemsPerScreen = Math.ceil(viewH / 150);
  5110. const dynamicBuffer = itemsPerScreen * 2;
  5111.  
  5112. const endIndex = Math.min(this.itemsData.length, startIndex + itemsPerScreen + dynamicBuffer);
  5113. const safeStartIndex = Math.max(0, startIndex - dynamicBuffer);
  5114.  
  5115. const visibleIds = new Set();
  5116. for (let i = safeStartIndex; i < endIndex; i++) {
  5117. visibleIds.add(this.itemsData[i].id);
  5118. }
  5119.  
  5120. for (const [id, card] of this.nodesMap.entries()) {
  5121. if (!visibleIds.has(id)) {
  5122. card.unbind();
  5123. card.style.display = 'none';
  5124. this._pool.push(card);
  5125. this.nodesMap.delete(id);
  5126. }
  5127. }
  5128.  
  5129. for (let i = safeStartIndex; i < endIndex; i++) {
  5130. const data = this.itemsData[i];
  5131. let card = this.nodesMap.get(data.id);
  5132.  
  5133. if (!card) {
  5134. card = this._pool.pop();
  5135. if (!card) {
  5136. card = document.createElement('xai-card');
  5137. card.style.position = 'absolute';
  5138. this.container.appendChild(card);
  5139. }
  5140. card.style.display = '';
  5141. this.nodesMap.set(data.id, card);
  5142. card.bind(data.meta, 'history');
  5143. }
  5144.  
  5145. card.style.transform = `translate3d(0, ${this.yPositions[i]}px, 0)`;
  5146. card.style.width = this.colWidth + 'px';
  5147. card.style.height = this.heights[i] + 'px';
  5148. }
  5149. }
  5150.  
  5151. clear() {
  5152. for (const [id, card] of this.nodesMap.entries()) {
  5153. card.unbind();
  5154. card.style.display = 'none';
  5155. this._pool.push(card);
  5156. }
  5157. this.nodesMap.clear();
  5158. this.itemsData = [];
  5159. this.yPositions = [];
  5160. this.heights = [];
  5161. this.totalHeight = 0;
  5162. this.spacer.style.height = '0px';
  5163. }
  5164.  
  5165. destroy() {
  5166. this.resizeObs.disconnect();
  5167. if (this.rafId) cancelAnimationFrame(this.rafId);
  5168. for (const [id, card] of this.nodesMap.entries()) { card.unbind(); card.remove(); }
  5169. for (const card of this._pool) { card.remove(); }
  5170. this.nodesMap.clear();
  5171. this._pool = [];
  5172. this.spacer.remove();
  5173. }
  5174. }
  5175.  
  5176. let historyGrid = null;
  5177.  
  5178. const PanelManager = {
  5179. init() {
  5180. if (DOM.currentImages) DOM.currentImages.dataset.renderContext = 'current';
  5181. if (DOM.refList) DOM.refList.dataset.renderContext = 'reference';
  5182. if (DOM.history) DOM.history.dataset.renderContext = 'history';
  5183. if (DOM.history && !historyGrid) historyGrid = new HybridVirtualGrid(DOM.history);
  5184. },
  5185. prepend(order, itemDataMap = STATE.items, context = 'history') {
  5186. if (context === 'history') {
  5187. if (!historyGrid) return;
  5188. historyGrid.updateData(STATE.historyOrder, itemDataMap);
  5189. return;
  5190. }
  5191. const container = null;
  5192. if (!container || !order || order.length === 0) return;
  5193. const nodes =[];
  5194. for (let i = 0; i < order.length; i++) {
  5195. const id = order[i], meta = itemDataMap.get(id);
  5196. if (!meta) continue;
  5197. let card = CardRegistry.get(context, id) || document.createElement('xai-card');
  5198. if (card._bound && card.dataset.id === String(id) && card._context === context) {
  5199. card.refresh(meta, context);
  5200. } else {
  5201. card.bind(meta, context);
  5202. }
  5203. nodes.push(card);
  5204. }
  5205. container.prepend(...nodes);
  5206. },
  5207. async update(order, itemDataMap = STATE.items, context = 'current') {
  5208. if (context === 'history') {
  5209. if (historyGrid) historyGrid.updateData(order, itemDataMap);
  5210. return;
  5211. }
  5212. const container = context === 'reference' ? DOM.refList : DOM.currentImages;
  5213. if (!container) return;
  5214. if (!order || order.length === 0) return this.clear(container);
  5215.  
  5216. const newSet = new Set(order.map(String));
  5217.  
  5218. Array.from(container.children).forEach(child => {
  5219. if (child.tagName === 'XAI-CARD' && !newSet.has(child.dataset.id)) {
  5220. if (typeof child.unbind === 'function') child.unbind();
  5221. child.remove();
  5222. }
  5223. });
  5224.  
  5225. for (let i = 0; i < order.length; i++) {
  5226. const id = String(order[i]), meta = itemDataMap.get(id);
  5227. if (!meta) continue;
  5228.  
  5229. let card = CardRegistry.get(context, id);
  5230. if (!card) {
  5231. card = document.createElement('xai-card');
  5232. card.bind(meta, context);
  5233. } else if (card._bound && card.dataset.id === id && card._context === context) {
  5234. card.refresh(meta, context);
  5235. } else {
  5236. card.bind(meta, context);
  5237. }
  5238.  
  5239. const expectedSibling = container.children[i];
  5240. if (expectedSibling !== card) {
  5241. try {
  5242. container.moveBefore(card, expectedSibling || null);
  5243. } catch {
  5244. container.insertBefore(card, expectedSibling || null);
  5245. }
  5246. }
  5247. }
  5248. },
  5249. clear(container = DOM.currentImages, contextOverride = null) {
  5250. if (!container) return;
  5251. const context = contextOverride || container.dataset.renderContext || (container === DOM.history ? 'history' : container === DOM.refList ? 'reference' : 'current');
  5252. if (context === 'history' && historyGrid) { historyGrid.clear(); CardRegistry.clear(context); return; }
  5253. Array.from(container.children).forEach(n => n.tagName === 'XAI-CARD' && n[Symbol.dispose] ? n[Symbol.dispose]() : null);
  5254. container.replaceChildren();
  5255. if (container === DOM.history || container === DOM.refList || container === DOM.currentImages) container.scrollTop = 0;
  5256. CardRegistry.clear(context);
  5257. }
  5258. };
  5259.  
  5260.  
  5261. const App = {
  5262. ac: new AbortController(), perfObserver: null, recentLagMs: 0, smoothedLoad: 0, isAutoEco: false, ecoHighTicks: 0, ecoLowTicks: 0,
  5263. on(target, event, handler, opts = false) { target.addEventListener(event, handler, { ...(typeof opts === 'boolean' ? { capture: opts } : opts), signal: this.ac.signal }); },
  5264. async mount() {
  5265. if (STATE.isMounted) return; resetState(); STATE.isMounted = true; this.ac = new AbortController();
  5266. _statusViewCacheKey = '';
  5267. document.documentElement.style.overflow = document.body.style.overflow = 'hidden'; document.body.classList.toggle('eco-mode', STATE.ecoMode);
  5268. this.ac.signal.addEventListener('abort', () => { document.documentElement.style.overflow = document.body.style.overflow = ''; _cleanupGlobalTimers(); VideoHoverPreviewer.cancel(); });
  5269. this.on(window, 'dragover', e => e.preventDefault()); this.on(window, 'drop', e => e.preventDefault());
  5270. this.on(window, 'focus', () => TitleState.handleTabActive());
  5271. this.on(window, 'pagehide', () => {
  5272. if (_skipPagehidePersistence) return;
  5273. flushCurrentPanelSessionSave();
  5274. void DBQueue.flushAppWrites();
  5275. if (_configSaveTask) {
  5276. _configSaveTask.abort();
  5277. _configSaveTask = null;
  5278. localStorage.setItem(APP_CONFIG.STORAGE.CONFIG_KEY, JSON.stringify(normalizeConfig(getConfigSnapshot(), CONFIG_DEFAULTS)));
  5279. }
  5280. });
  5281. this.on(document, 'visibilitychange', () => document.hidden ? TitleState.sync() : TitleState.handleTabActive());
  5282. const loafType = (Array.isArray(PerformanceObserver.supportedEntryTypes) && PerformanceObserver.supportedEntryTypes.includes('long-animation-frame')) ? 'long-animation-frame' : (PerformanceObserver.supportedEntryTypes?.includes('longtask') ? 'longtask' : null);
  5283. if (loafType) {
  5284. 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; });
  5285. this.ac.signal.addEventListener('abort', () => this.perfObserver?.disconnect());
  5286. } else this.perfObserver = null;
  5287.  
  5288. let metInterval = null;
  5289. const startTelemetry = () => {
  5290. if (!metInterval) metInterval = setInterval(() => runBg(updateHistoryMetrics), 2500);
  5291. if (this.perfObserver && loafType) this.perfObserver.observe({ entryTypes: [loafType] });
  5292. };
  5293. const stopTelemetry = () => {
  5294. if (metInterval) { clearInterval(metInterval); metInterval = null; }
  5295. if (this.perfObserver) this.perfObserver.disconnect();
  5296. };
  5297.  
  5298. if (!document.hidden) startTelemetry();
  5299. this.on(document, 'visibilitychange', () => document.hidden ? stopTelemetry() : startTelemetry());
  5300. this.ac.signal.addEventListener('abort', stopTelemetry);
  5301. const sheet = new CSSStyleSheet(); sheet.replaceSync(UI_CSS); document.adoptedStyleSheets =[...document.adoptedStyleSheets, sheet];
  5302. const appDiv = document.createElement('div'); appDiv.id = 'xai-pro-app-wrapper'; appDiv.append(TEMPLATE_APP.content.cloneNode(true)); document.body.appendChild(appDiv);
  5303. this.ac.signal.addEventListener('abort', () => { document.adoptedStyleSheets = document.adoptedStyleSheets.filter(s => s !== sheet); appDiv.remove(); });
  5304.  
  5305. StorageClient.init(); await loadDataFromDB(); bindLogic(); await loadHistoryFromDB();
  5306. runBg(updateDiskMetric);
  5307. },
  5308. 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(); }
  5309. };
  5310.  
  5311. 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; };
  5312. if ('navigation' in window && typeof navigation?.addEventListener === 'function') {
  5313. 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); });
  5314. }
  5315.  
  5316. const UI = {
  5317. showToast(msg, type = 'info') {
  5318. const c = document.getElementById('xai-toast-container'); if(!c) return;
  5319. const icon = type === 'error' ? '❌' : type === 'warn' ? '⚠️' : type === 'success' ? '✅' : 'ℹ️';
  5320. const toast = document.createElement('div'); toast.className = `xai-toast ${type}`;
  5321. const iconEl = document.createElement('span'); iconEl.textContent = icon;
  5322. const msgEl = document.createElement('span'); msgEl.textContent = msg;
  5323. toast.append(iconEl, msgEl);
  5324. c.appendChild(toast); requestAnimationFrame(() => toast.classList.add('show')); setTimeout(() => { toast.classList.remove('show'); setTimeout(() => toast.remove(), 400); }, 4000);
  5325. },
  5326. 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(); }); }
  5327. };
  5328.  
  5329. let _cachedDiskUsage = '--';
  5330. let _diskMetricTimer = null;
  5331.  
  5332. function scheduleDiskUpdate() {
  5333. if (_diskMetricTimer) clearTimeout(_diskMetricTimer);
  5334. _diskMetricTimer = setTimeout(() => {
  5335. _diskMetricTimer = null;
  5336. runBg(updateDiskMetric);
  5337. }, 800);
  5338. }
  5339.  
  5340. async function updateDiskMetric() {
  5341. try { _cachedDiskUsage = ((await navigator.storage.estimate()).usage / 1048576).toFixed(0) + 'M'; } catch(e){}
  5342. if (DOM.metricDisk) DOM.metricDisk.innerText = '⛁\uFE0E ' + _cachedDiskUsage;
  5343. }
  5344.  
  5345. let _metricsUpdateTimer = 0;
  5346. function scheduleHistoryMetricsUpdate() {
  5347. if (_metricsUpdateTimer) clearTimeout(_metricsUpdateTimer);
  5348. _metricsUpdateTimer = setTimeout(() => {
  5349. _metricsUpdateTimer = 0;
  5350. runBg(updateHistoryMetrics);
  5351. }, 300);
  5352. }
  5353.  
  5354. async function updateHistoryMetrics() {
  5355. const lagFactor = Math.min(App.recentLagMs / 1000, 1);
  5356. const memoryFactor = performance.memory ? Math.max(0, Math.min((performance.memory.usedJSHeapSize / (performance.memory.jsHeapSizeLimit || 1) - 0.6) / 0.3, 1)) : 0;
  5357. const currentLoad = Math.max(lagFactor, memoryFactor);
  5358. App.smoothedLoad = (App.smoothedLoad * 0.4) + (currentLoad * 0.6);
  5359. App.recentLagMs = 0;
  5360. const loadPercent = Math.round(App.smoothedLoad * 100);
  5361.  
  5362. if (loadPercent > 75) {
  5363. App.ecoHighTicks++;
  5364. App.ecoLowTicks = 0;
  5365. if (!App.isAutoEco && App.ecoHighTicks >= 4) {
  5366. App.isAutoEco = true; document.body.classList.add('auto-eco');
  5367. applyMemoryPressureProfile('auto-eco-on');
  5368. }
  5369. } else if (loadPercent < 45) {
  5370. App.ecoLowTicks++;
  5371. App.ecoHighTicks = 0;
  5372. if (App.isAutoEco && App.ecoLowTicks >= 8) {
  5373. App.isAutoEco = false; document.body.classList.remove('auto-eco');
  5374. applyMemoryPressureProfile('auto-eco-off');
  5375. }
  5376. } else {
  5377. App.ecoHighTicks = 0;
  5378. App.ecoLowTicks = 0;
  5379. }
  5380.  
  5381. if (DOM.metricSaved) DOM.metricSaved.innerText = STATE.historyOrder.length;
  5382. if (DOM.metricDisk && DOM.metricDisk.innerText === '--') DOM.metricDisk.innerText = '⛁\uFE0E ' + _cachedDiskUsage;
  5383. if (DOM.metricLoad) { DOM.metricLoad.innerText = loadPercent + '%'; DOM.metricLoad.classList.remove('d-none'); }
  5384. }
  5385.  
  5386. function getEligibleRefs(action = STATE.action, { refMode = STATE.refMode } = {}) {
  5387. const acceptedType = (ACTION_SPEC[action] ?? ACTION_SPEC.gen_image).refKind;
  5388. const effectiveRefMode = normalizeRefModeForAction(action, refMode);
  5389. const maxRefs = (ACTION_SPEC[action] ?? ACTION_SPEC.gen_image).maxRefs?.({ action, refMode: effectiveRefMode }) ?? 0;
  5390. const eligible = [];
  5391. for (const item of Store.getRefItems(action, effectiveRefMode)) {
  5392. if (item && item.active && (acceptedType === 'any' || item.type === acceptedType) && eligible.length < maxRefs) eligible.push(item);
  5393. }
  5394. return { eligible, acceptedType, maxRefs };
  5395. }
  5396.  
  5397. function updateUploadPlaceholderState() {
  5398. if (!DOM.uploadPlaceholder) return;
  5399. const count = getRefOrderForView().length;
  5400. const maxRefs = (ACTION_SPEC[STATE.action] ?? ACTION_SPEC.gen_image).maxRefs?.({ action: STATE.action, refMode: normalizeRefModeForAction(STATE.action, STATE.refMode) }) ?? 0;
  5401. const effectiveLimit = maxRefs === 0 ? 0 : Math.min(APP_CONFIG.LIMITS.MAX_REFS_UI, maxRefs);
  5402. const disabled = effectiveLimit === 0 || count >= effectiveLimit;
  5403. DOM.uploadPlaceholder.style.opacity = disabled ? '0.4' : '1';
  5404. DOM.uploadPlaceholder.style.pointerEvents = disabled ? 'none' : 'auto';
  5405. DOM.uploadPlaceholder.querySelector('.upload-label-main').innerText = effectiveLimit === 0 ? 'N/A' : (count >= effectiveLimit ? 'Limit' : 'Add Media');
  5406. if (DOM.refCounter) DOM.refCounter.innerText = count;
  5407. }
  5408.  
  5409. function updateActionUI() { _updateActionUIDom(); }
  5410. function _updateActionUIDom() {
  5411. const action = STATE.action;
  5412. if (DOM.app) DOM.app.dataset.activeAction = action;
  5413.  
  5414. const dOpts = createRangeOptions(1, (ACTION_SPEC[action] ?? ACTION_SPEC.gen_image).durationMax || 15);
  5415. if (!dOpts.some(o => o.val === STATE.duration)) STATE.duration = dOpts[0].val;
  5416. if (DOM.labelSettingDuration) DOM.labelSettingDuration.innerText = dOpts.find(o => o.val === STATE.duration).text;
  5417. if (DOM.settingDuration) DOM.settingDuration.dataset.value = STATE.duration;
  5418. DOM.popoverDuration?._updateOptions?.(dOpts);
  5419.  
  5420. if (DOM.labelSettingBatch) DOM.labelSettingBatch.innerText = STATE.batchSize;
  5421. if (DOM.settingBatch) DOM.settingBatch.dataset.value = STATE.batchSize;
  5422.  
  5423. const isVideo = action === 'gen_video';
  5424. if (isVideo && !getAspectRatioOption().isAuto && !ALLOWED_VIDEO_ARS.has(STATE.currentAr)) STATE.currentAr = 'Auto';
  5425. syncDOMControls('currentAr');
  5426. DOM.popoverAr?._updateOptions?.(getAspectRatioPopoverOptions(action));
  5427.  
  5428. const activeUI = (ACTION_SPEC[action] ?? ACTION_SPEC.gen_image).activeUI ||[];
  5429. if (DOM.settingLoops) DOM.settingLoops.value = getLoopsValue(action);
  5430.  
  5431. UI_CONTROL_SCHEMA.forEach(control => {
  5432. if (control.id === 'action') return;
  5433.  
  5434. const targetId = control.wrapperId || control.buttonId || DOM_IDS[control.domKey];
  5435. const el = document.getElementById(targetId);
  5436.  
  5437. if (el) {
  5438. const shouldShow = activeUI.includes(control.id) && (typeof control.isVisible === 'function' ? control.isVisible({ action, state: STATE }) : true);
  5439. el.classList.toggle('d-none', !shouldShow);
  5440. }
  5441. });
  5442.  
  5443. updateStatusUI();
  5444. updateUploadPlaceholderState();
  5445. const isTextMode = action === 'gen_text' || action === 'gen_audio';
  5446. const isMediaRefModeLocked = JobManager.getLane(action) === 'media' && JobManager.getRuntime('media').isGenerating;
  5447. const isRefModeDisabled = isTextMode || isMediaRefModeLocked;
  5448. if (DOM.refModeToggle) {
  5449. DOM.refModeToggle.innerText = isTextMode ? 'Multi' : (STATE.refMode === 'multi' ? 'Multi' : 'Queue');
  5450. DOM.refModeToggle.disabled = isRefModeDisabled;
  5451. DOM.refModeToggle.style.opacity = isRefModeDisabled ? '0.4' : '1';
  5452. DOM.refModeToggle.style.pointerEvents = isRefModeDisabled ? 'none' : 'auto';
  5453. 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');
  5454. }
  5455. if (DOM.currentImages) DOM.currentImages.dataset.viewMode = action === 'gen_text' ? 'chat' : 'gallery';
  5456.  
  5457. const chatEl = document.getElementById('xai-chat-container');
  5458. if (action === 'gen_text') {
  5459. if (chatEl) chatEl.classList.add('is-active');
  5460. if (DOM.currentImages) DOM.currentImages.style.display = 'none';
  5461. restoreChatFromSession();
  5462. } else {
  5463. if (chatEl) chatEl.classList.remove('is-active');
  5464. if (DOM.currentImages) DOM.currentImages.style.display = '';
  5465. }
  5466.  
  5467. toggleGenerateButton(STATE.isGenerating);
  5468. syncWipeButtonState();
  5469. updateDirtyStateUI();
  5470.  
  5471. }
  5472. const renderRefMode = () => { if (DOM.refModeToggle) DOM.refModeToggle.innerText = STATE.refMode === 'multi' ? 'Multi' : 'Queue'; updateActionUI(); };
  5473.  
  5474. 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;}`;
  5475. const UI_HTML = buildAppHtml();
  5476.  
  5477.  
  5478. const getActiveRefIdsSet = () => {
  5479. const set = new Set();
  5480. for (let i = 0; i < REF_VIEW_KEYS.length; i++) {
  5481. const order = STATE.refViews[REF_VIEW_KEYS[i]]?.order;
  5482. if (order) for (let j = 0; j < order.length; j++) set.add(order[j]);
  5483. }
  5484. return set;
  5485. };
  5486.  
  5487. const processDeletions = (targetIds) => {
  5488. const refIds = getActiveRefIdsSet();
  5489. const targetSet = new Set(targetIds.map(String));
  5490. const toVirtualize =[...targetSet.intersection(refIds)];
  5491. const toDelete = [...targetSet.difference(refIds)];
  5492. return { toDelete, toVirtualize };
  5493. };
  5494.  
  5495. const virtualizeRefs = (ids) => {
  5496. if (!ids?.length) return;
  5497. const idSet = new Set(ids.map(String));
  5498. STATE.historyOrder = STATE.historyOrder.filter(id => !idSet.has(String(id)));
  5499. removeIdsFromAllPanels(ids);
  5500. ids.forEach(id => {
  5501. const item = STATE.items.get(String(id));
  5502. if (item && !item.isUploadedRef) {
  5503. item.isUploadedRef = true;
  5504. DBQueue.add(item);
  5505. }
  5506. });
  5507. PanelManager.update(STATE.historyOrder, STATE.items, 'history');
  5508. scheduleHistoryMetricsUpdate();
  5509. };
  5510.  
  5511. async function enforceHistoryLimit() {
  5512. if (STATE.historyOrder.length > APP_CONFIG.LIMITS.MAX_HISTORY) {
  5513. const overflowIds = STATE.historyOrder.slice(APP_CONFIG.LIMITS.MAX_HISTORY);
  5514. STATE.historyOrder = STATE.historyOrder.slice(0, APP_CONFIG.LIMITS.MAX_HISTORY);
  5515.  
  5516. const { toDelete, toVirtualize } = processDeletions(overflowIds);
  5517.  
  5518. if (toVirtualize.length > 0) virtualizeRefs(toVirtualize);
  5519.  
  5520. if (toDelete.length > 0) {
  5521. DBQueue.forgetItems(toDelete);
  5522. removeIdsFromAllPanels(toDelete);
  5523. pruneReferenceState(toDelete, { closePrompt: false });
  5524. await StorageClient.deleteItemsBulk(toDelete);
  5525. await clearTextSessionArtifacts(toDelete);
  5526. toDelete.forEach(id => Store.removeItem(id));
  5527. }
  5528. }
  5529. }
  5530.  
  5531. function removeCurrentPanelItems(ids, { purgeStorage = true } = {}) {
  5532. const keys = sanitizeIdListStore(ids);
  5533. if (!keys.length) { scheduleCurrentPanelSessionSave(0); return; }
  5534. const removed = new Set(keys);
  5535. DBQueue.forgetItems(keys);
  5536. STATE.historyOrder = STATE.historyOrder.filter(id => !removed.has(String(id)));
  5537. removeIdsFromAllPanels(keys);
  5538. pruneReferenceState(keys, { closePrompt: false });
  5539. if (STATE.activeLightboxId && removed.has(String(STATE.activeLightboxId))) closeLightbox();
  5540. keys.forEach(id => Store.removeItem(id));
  5541. keys.forEach(id => {
  5542. document.querySelectorAll(`xai-card[data-id="${id}"]`).forEach(node => {
  5543. if (node.closest('#xai-history') && historyGrid) { node.unbind(); } else { node.remove(); }
  5544. });
  5545. });
  5546. if (historyGrid) historyGrid.updateData(STATE.historyOrder, STATE.items);
  5547. scheduleCurrentPanelSessionSave(0);
  5548. scheduleDiskUpdate();
  5549. if (purgeStorage) runBg(async () => {
  5550. await StorageClient.deleteItemsBulk(keys).catch(e => addLog('WARN', 'Remove failed', e));
  5551. await clearTextSessionArtifacts(keys);
  5552. });
  5553. }
  5554.  
  5555. async function wipeCardHistoryData() {
  5556. const targetIds =[];
  5557. STATE.items.forEach((item, id) => {
  5558. if (item && !item.isUploadedRef) targetIds.push(String(id));
  5559. });
  5560.  
  5561. if (!targetIds.length) return 0;
  5562.  
  5563. const { toDelete, toVirtualize } = processDeletions(targetIds);
  5564.  
  5565. if (toVirtualize.length > 0) virtualizeRefs(toVirtualize);
  5566.  
  5567. if (toDelete.length > 0) {
  5568. removeCurrentPanelItems(toDelete, { purgeStorage: false });
  5569. await DBQueue.flushAppWrites(['currentOrder', ACTIVE_TEXT_SESSION_KEY]);
  5570. await StorageClient.deleteItemsBulk(toDelete);
  5571. flushCurrentPanelSessionSave();
  5572. }
  5573. return toDelete.length + toVirtualize.length;
  5574. }
  5575.  
  5576. async function loadDataFromDB() {
  5577. const [
  5578. promptsRaw,
  5579. promptDraftsRaw,
  5580. favPromptsRaw,
  5581. refPromptsRaw,
  5582. refViewsRaw,
  5583. currentOrderRaw,
  5584. activeTextSessionRaw,
  5585. currentPanelSessionRaw,
  5586. timingsRaw
  5587. ] = await Promise.all([
  5588. StorageClient.loadMeta('prompts'),
  5589. StorageClient.loadMeta('promptDrafts'),
  5590. StorageClient.loadMeta('favPrompts'),
  5591. StorageClient.loadMeta('refPrompts'),
  5592. StorageClient.loadMeta('refViews'),
  5593. StorageClient.loadMeta('currentOrder'),
  5594. StorageClient.loadMeta(ACTIVE_TEXT_SESSION_KEY),
  5595. StorageClient.loadMeta('currentPanelSession'),
  5596. StorageClient.loadMeta('timings')
  5597. ]);
  5598. STATE.prompts = sanitizePromptStore(promptsRaw, STATE.prompts);
  5599. STATE.promptDrafts = sanitizePromptStore(promptDraftsRaw, STATE.prompts);
  5600. STATE.favPrompts = sanitizeFavPromptStore(favPromptsRaw, STATE.favPrompts);
  5601. STATE.refPrompts = sanitizeRefPromptStore(refPromptsRaw, STATE.refPrompts);
  5602. STATE.refViewsTmp = refViewsRaw ? sanitizeRefViewStore(refViewsRaw) : null;
  5603. STATE.panelOrdersTmp = sanitizePanelOrderStore(currentOrderRaw);
  5604. STATE.activeTextSessionTmp = typeof activeTextSessionRaw === 'string' ? activeTextSessionRaw : null;
  5605. let localSessionRaw = null;
  5606. try {
  5607. const stored = sessionStorage.getItem(APP_CONFIG.STORAGE.CURRENT_PANEL_SESSION_KEY);
  5608. if (stored) localSessionRaw = JSON.parse(stored);
  5609. } catch(e) {}
  5610.  
  5611. STATE.currentPanelSessionTmp = sanitizePanelSessionMapStore(localSessionRaw || currentPanelSessionRaw);
  5612. STATE.timings = timingsRaw || {};
  5613. }
  5614. async function extractVideoPosterData(videoUrl, pItem = null) {
  5615. if (pItem?.id) {
  5616. try {
  5617. const workerResult = await StorageClient.extractVideoPosterWorker(pItem.id, pItem.mimeType || 'video/mp4');
  5618. if (!workerResult.fallback) {
  5619. return {
  5620. thumbBlob: workerResult.thumbBlob,
  5621. lqip: workerResult.lqip,
  5622. ratio: workerResult.ratio
  5623. };
  5624. }
  5625. addLog('WARN', `Worker Media Pipeline failed: ${workerResult.reason}. Falling back to main-thread <video> element.`);
  5626. } catch (err) {
  5627. addLog('WARN', 'Worker poster extraction threw error. Falling back to <video> element.', err);
  5628. }
  5629. }
  5630.  
  5631. const { promise, resolve, reject } = Promise.withResolvers();
  5632. const video = document.createElement('video');
  5633. Object.assign(video, { muted: true, playsInline: true, preload: 'auto' });
  5634.  
  5635. const signal = AbortSignal.timeout(15000);
  5636. const onAbort = () => {
  5637. cleanup();
  5638. reject(new Error('Timeout: Failed to load video poster'));
  5639. };
  5640. signal.addEventListener('abort', onAbort, { once: true });
  5641.  
  5642. const cleanup = () => {
  5643. signal.removeEventListener('abort', onAbort);
  5644. video.onerror = null;
  5645. resetVideoElement(video, { pause: false });
  5646. };
  5647.  
  5648. const onFrame = async () => {
  5649. if (signal.aborted) return;
  5650. try {
  5651. using bmp = await createImageBitmap(video, { resizeWidth: APP_CONFIG.MEDIA.THUMB_MAX_DIM, resizeQuality: 'medium' });
  5652. const oc = new OffscreenCanvas(bmp.width, bmp.height), ctx = oc.getContext('2d', { alpha: false });
  5653. ctx.drawImage(bmp, 0, 0);
  5654. const thumbBlob = await oc.convertToBlob({ type: 'image/webp', quality: APP_CONFIG.MEDIA.THUMB_WEBP_QUALITY });
  5655.  
  5656. const lqipOc = new OffscreenCanvas(2, 2), lqCtx = lqipOc.getContext('2d', { alpha: false });
  5657. lqCtx.drawImage(bmp, 0, 0, 2, 2);
  5658. const d = lqCtx.getImageData(0, 0, 2, 2).data;
  5659. const lqip = `radial-gradient(ellipse at 30% 30%, rgb(${d[0]} ${d[1]} ${d[2]}), rgb(${d[12]} ${d[13]} ${d[14]}))`;
  5660.  
  5661. oc.width = 1; oc.height = 1;
  5662. lqipOc.width = 1; lqipOc.height = 1;
  5663. cleanup();
  5664. resolve({ thumbBlob, lqip, ratio: bmp.width / bmp.height });
  5665. } catch (err) { cleanup(); reject(err); }
  5666. };
  5667.  
  5668. video.addEventListener('loadeddata', () => {
  5669. if (signal.aborted) return;
  5670. video.currentTime = 0.1;
  5671. video.requestVideoFrameCallback(onFrame);
  5672. }, { once: true });
  5673.  
  5674. video.onerror = () => { cleanup(); reject(new Error('Video load error for poster')); };
  5675. video.src = videoUrl;
  5676.  
  5677. try { return await promise; }
  5678. catch (e) {
  5679. addLog('WARN', '[Poster_Extract] Failed to extract poster', e);
  5680. if (pItem?.refSourceId) {
  5681. try {
  5682. const refThumb = await OPFS.read(pItem.refSourceId, 'thumb');
  5683. if (refThumb) return { thumbBlob: refThumb, lqip: pItem.lqip, ratio: pItem.ratio };
  5684. } catch (err) {}
  5685. }
  5686. return null;
  5687. }
  5688. }
  5689. const DiskIOLimiter = createLimiter(2);
  5690. const scheduleAutoDownloads = (items = [], baseDelay = 500, stepDelay = 0) => {
  5691. if (!STATE.autoDownload) return;
  5692. items.forEach((item, idx) => setTimeout(() => MediaUtils.downloadItem(item, true), baseDelay + (idx * stepDelay)));
  5693. };
  5694. const commitItemsToHistory = async (items = [], ids = items.map(item => item?.id).filter(Boolean)) => {
  5695. if (!ids.length) return;
  5696. Store.prependHistory(ids);
  5697. await enforceHistoryLimit();
  5698. };
  5699.  
  5700. function recoverPendingVideos() {
  5701. const pendingItems = [...STATE.items.values()].filter(i => i.isPending && i.reqId);
  5702. if (!pendingItems.length) return;
  5703. const now = Date.now();
  5704. addLog('INFO', `Recovering ${pendingItems.length} background video tasks`);
  5705.  
  5706. for (const item of pendingItems) {
  5707. if (now - (item.timestamp || 0) > 1000 * 60 * 60 * 2) {
  5708. UI.showToast("Background video task expired and was removed", "warn");
  5709. removeCurrentPanelItems([item.id], { purgeStorage: true });
  5710. continue;
  5711. }
  5712.  
  5713. const ac = new AbortController();
  5714.  
  5715. const onProgress = (val) => {
  5716. item._statusData = buildCurrentStatusLines(val);
  5717. const card = CardRegistry.get('current', item.id) || CardRegistry.get('history', item.id);
  5718. if (card) { card.setStatus(item._statusData); card.setProgress(val.progress); }
  5719. };
  5720.  
  5721. runBg(async () => {
  5722. try {
  5723. const videoUrl = await pollVideoTask(item.reqId, ac.signal, -1, onProgress);
  5724. if (!videoUrl || !STATE.items.has(item.id)) return;
  5725.  
  5726. onProgress({ shortStatus: 'Saving video...', longStatus: 'Downloading full video...', color: '#3b82f6', progress: 95 });
  5727. const videoBlob = await fetchBlobCSP(videoUrl, { signal: ac.signal, timeout: 30000 });
  5728. await OPFS.write(item.id, 'full', videoBlob);
  5729.  
  5730. onProgress({ shortStatus: 'Extracting poster...', longStatus: 'Extracting poster image...', color: '#3b82f6', progress: 100 });
  5731. using localVideoUrl = new DisposableURL(videoBlob);
  5732. let posterData = await extractVideoPosterData(localVideoUrl.url, item);
  5733.  
  5734. if (posterData?.thumbBlob) await OPFS.write(item.id, 'thumb', posterData.thumbBlob);
  5735.  
  5736. finalizePendingVideoItem(item, videoUrl, posterData);
  5737. DBQueue.add(item);
  5738. scheduleCurrentPanelSessionSave();
  5739. await commitItemsToHistory([item]);
  5740.  
  5741. const card = CardRegistry.get('current', item.id) || CardRegistry.get('history', item.id);
  5742. if (card) await refreshFinalizedVideoCard(card, item);
  5743.  
  5744. UI.showToast("Background video task completed!", "success");
  5745. } catch (e) {
  5746. if (!STATE.items.has(item.id)) return;
  5747. UI.showToast(`Video failed and was removed: ${getErrorMessage(e).substring(0, 40)}`, "error");
  5748. removeCurrentPanelItems([item.id], { purgeStorage: true });
  5749. }
  5750. });
  5751. }
  5752. }
  5753. async function cleanupOrphanMediaFiles() {
  5754. try {
  5755. const validIds = new Set(STATE.items.keys()), dir = await OPFS.initDir(); let deletedCount = 0;
  5756. for await (const [name] of dir.entries()) {
  5757. if (!validIds.has(name.split('_')[0])) {
  5758. await dir.removeEntry(name).catch(()=>{});
  5759. deletedCount++;
  5760. }
  5761. }
  5762. if (deletedCount > 0) addLog('INFO', `GC removed ${deletedCount} orphan OPFS files`);
  5763. } catch(e) {}
  5764. }
  5765.  
  5766. async function loadHistoryFromDB() {
  5767. const allMeta = await StorageClient.loadAllHistory();
  5768. const historyIds = [];
  5769. const phantomIds = [];
  5770. TaskChunker.reset();
  5771. for (let i = 0; i < allMeta.length; i++) {
  5772. const m = allMeta[i];
  5773. m.id = String(m.id);
  5774. if (Pipeline.isPhantomMeta(m)) { phantomIds.push(m.id); continue; }
  5775. Store.addItem(m);
  5776. if (!m.isUploadedRef && !m.isPending) historyIds.push(m.id);
  5777. await TaskChunker.yieldIfNeeded();
  5778. }
  5779. if (phantomIds.length > 0) await StorageClient.deleteItemsBulk(phantomIds);
  5780. STATE.historyOrder = historyIds;
  5781. const historyIdSet = new Set(historyIds);
  5782.  
  5783. STATE.refViews = STATE.refViewsTmp ? sanitizeRefViewStore(serializeRefViewStore(STATE.refViewsTmp)) : createDefaultRefViewsFromItems();
  5784. STATE.refViewsTmp = undefined;
  5785. updateRefState({ forceRebuild: true });
  5786. const sessionPanels = STATE.currentPanelSessionTmp?.panels || {};
  5787. const sessionItemIds = new Set();
  5788. Object.values(sessionPanels).forEach(panel => {
  5789. panel?.items?.forEach(item => {
  5790. sessionItemIds.add(item.id);
  5791. const existing = STATE.items.get(item.id);
  5792. if (existing) {
  5793. Object.assign(existing, item);
  5794. if (item.prompt || item.responseText || item.textChatMessages) {
  5795. LazyTextStore.set(item.id, { prompt: item.prompt || '', responseText: item.responseText || '', textChatMessages: item.textChatMessages || null });
  5796. }
  5797. }
  5798. else {
  5799. const revived = { ...item, persist: !item.isPending };
  5800. Store.addItem(revived);
  5801. if (revived.persist !== false) DBQueue.add(revived);
  5802. }
  5803. if (!item.isPending && !item.isUploadedRef && !historyIdSet.has(item.id)) {
  5804. const orphan = STATE.items.get(item.id);
  5805. if (orphan && orphan.persist === false) {
  5806. orphan.persist = true;
  5807. DBQueue.add(orphan);
  5808. }
  5809. }
  5810. });
  5811. });
  5812. const restoredPanelOrders = createEmptyPanelOrderStore();
  5813. PANEL_ACTION_KEYS.forEach(panelKey => {
  5814. const sessionPanel = sessionPanels[panelKey];
  5815. const storedOrder = sanitizeIdListStore(STATE.panelOrdersTmp?.[panelKey]);
  5816. const sourceOrder = sessionPanel?.order?.length ? sessionPanel.order : storedOrder;
  5817. restoredPanelOrders[panelKey] = sanitizeIdListStore(sourceOrder).filter(id => {
  5818. const item = STATE.items.get(String(id));
  5819. return item && (sessionItemIds.has(String(id)) || !Pipeline.isTransientItem(item));
  5820. });
  5821. });
  5822. STATE.panelOrders = restoredPanelOrders;
  5823. STATE.currentOrder = getPanelOrder(getCurrentPanelKey());
  5824. STATE.panelOrdersTmp = undefined;
  5825. STATE.currentPanelSessionTmp = undefined;
  5826. if (STATE.activeTextSessionTmp) {
  5827. const activeItem = STATE.items.get(String(STATE.activeTextSessionTmp));
  5828. if (activeItem?.type === 'text') {
  5829. setActiveTextSessionState(activeItem.id, await loadTextSessionMessages(activeItem), { persist: false });
  5830. summarizeTextSessionItem(activeItem, STATE.textChatMessages, { updatedAt: activeItem.timestamp || Date.now(), model: activeItem.textModel });
  5831. DBQueue.add(activeItem);
  5832. if (activeItem.textModel && STATE.action === 'gen_text') applyTextModelSelection(activeItem.textModel);
  5833. } else {
  5834. setActiveTextSessionState(null, [], { persist: false });
  5835. persistActiveTextSessionId();
  5836. }
  5837. }
  5838. STATE.activeTextSessionTmp = undefined;
  5839. if (STATE.action === 'gen_text') restoreChatFromSession();
  5840. PanelManager.update(STATE.historyOrder, STATE.items, 'history');
  5841. await enforceHistoryLimit();
  5842. recoverPendingVideos();
  5843.  
  5844. const zombieIds = STATE.items.values()
  5845. .filter(item => item.isPending && (!item.reqId || item.type !== 'video'))
  5846. .map(item => String(item.id))
  5847. .toArray();
  5848.  
  5849. if (zombieIds.length > 0) {
  5850. removeCurrentPanelItems(zombieIds, { purgeStorage: true });
  5851. }
  5852.  
  5853. runBg(cleanupOrphanMediaFiles);
  5854. }
  5855.  
  5856. function renderRefList() {
  5857. syncVisibleRefStateFromView();
  5858. PanelManager.update(STATE.refOrder, STATE.items, 'reference');
  5859. syncRefPromptEditingUI();
  5860. updateStatusUI();
  5861. }
  5862.  
  5863. function syncRefPromptEditingUI({ restoreEditor = false } = {}) {
  5864. const activeId = STATE.activeRefPromptId != null ? String(STATE.activeRefPromptId) : null;
  5865. const isValidActiveRef = !!activeId && STATE.refOrder.includes(activeId) && STATE.items.has(activeId);
  5866.  
  5867. CardRegistry.maps.reference.forEach((card, cardId) => {
  5868. if (isValidActiveRef && String(cardId) === activeId) card._internals.states.add('editing'); else card._internals.states.delete('editing');
  5869. });
  5870.  
  5871. if (!isValidActiveRef) {
  5872. STATE.activeRefPromptId = null;
  5873. STATE.activeRefPromptAction = null;
  5874. STATE.activeRefPromptDraft = '';
  5875. DOM.promptContainer?.classList.remove('editing-ref');
  5876. DOM.refEditLabel?.classList.remove('show');
  5877. DOM.generateBtn?.classList.remove('d-none');
  5878. DOM.saveRefBtn?.classList.add('d-none');
  5879. if (restoreEditor) setEditorPromptValue(getDraftActionPrompt());
  5880. return false;
  5881. }
  5882.  
  5883. if (DOM.promptContainer) DOM.promptContainer.classList.add('editing-ref');
  5884. if (DOM.refEditLabel) {
  5885. DOM.refEditLabel.innerText = `Editing Individual Prompt: IMAGE_${STATE.refOrder.indexOf(activeId)}`;
  5886. DOM.refEditLabel.classList.add('show');
  5887. }
  5888. DOM.generateBtn?.classList.add('d-none');
  5889. DOM.saveRefBtn?.classList.remove('d-none');
  5890. return true;
  5891. }
  5892.  
  5893. function toggleRefPrompt(id) {
  5894. const refId = String(id);
  5895. if (STATE.activeRefPromptId === refId) return saveAndCloseRefPrompt(true);
  5896. if (STATE.activeRefPromptId) saveAndCloseRefPrompt(true);
  5897. else if (DOM.prompt) setActionPromptDraft(STATE.action, getEditorPromptValue());
  5898. STATE.activeRefPromptId = refId;
  5899. STATE.activeRefPromptAction = STATE.action;
  5900. STATE.activeRefPromptDraft = STATE.refPrompts[refId]?.[STATE.action] ?? getDraftActionPrompt(STATE.action);
  5901. syncRefPromptEditingUI();
  5902. setEditorPromptValue(STATE.activeRefPromptDraft);
  5903. DOM.prompt?.focus();
  5904. }
  5905.  
  5906. function saveAndCloseRefPrompt(cancel = false) {
  5907. if (!STATE.activeRefPromptId) return; const id = STATE.activeRefPromptId;
  5908. STATE.activeRefPromptDraft = getEditorPromptValue();
  5909. if (!cancel) {
  5910. const val = sanitizePromptText(STATE.activeRefPromptDraft);
  5911. if (val && val !== sanitizePromptText(getActiveRefPromptBaseline())) setRefPrompt(id, STATE.activeRefPromptAction, val);
  5912. else setRefPrompt(id, STATE.activeRefPromptAction, null);
  5913. }
  5914. const hasAnyPrompt = !!STATE.refPrompts[id]?.[STATE.activeRefPromptAction];
  5915. const card = CardRegistry.get('reference', id); if (card) card.setReferencePromptFlag(hasAnyPrompt);
  5916. STATE.activeRefPromptId = null;
  5917. STATE.activeRefPromptAction = null;
  5918. STATE.activeRefPromptDraft = '';
  5919. syncRefPromptEditingUI({ restoreEditor: true });
  5920. }
  5921.  
  5922. const TRANSPARENT_1X1 = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
  5923.  
  5924. const createLightboxImageLayer = (item, className, src = TRANSPARENT_1X1, fallbackBg = 'transparent') => {
  5925. const img = document.createElement('img');
  5926. img.className = className;
  5927. img.decoding = 'async';
  5928. img.fetchPriority = 'high';
  5929. img.src = src || TRANSPARENT_1X1;
  5930. img.style.background = fallbackBg;
  5931. if (item?.ratio) img.style.aspectRatio = String(item.ratio);
  5932. return img;
  5933. };
  5934. const createLightboxVideoElement = (item, src = '', poster = '', fallbackBg = 'transparent') => {
  5935. const video = document.createElement('video');
  5936. video.className = 'lightbox-media lightbox-media--video';
  5937. video.controls = true;
  5938. video.loop = true;
  5939. video.playsInline = true;
  5940. video.preload = 'metadata';
  5941. video.volume = getStoredMediaVolume();
  5942. video.addEventListener('volumechange', () => { persistMediaVolume(video.volume); });
  5943. video.style.background = fallbackBg;
  5944. if (item?.ratio) video.style.aspectRatio = String(item.ratio);
  5945. if (poster) video.poster = poster;
  5946. if (src) video.src = src;
  5947. return video;
  5948. };
  5949. const clearLightboxStage = () => {
  5950. DOM.lightboxInner?.classList.remove('is-loaded');
  5951. DOM.lightboxStage?.querySelectorAll('.lightbox-media, .lightbox-audio-card').forEach(node => {
  5952. const imgEl = node.tagName === 'IMG' ? node : node.querySelector('img');
  5953. if (imgEl?.src?.startsWith('blob:')) URL.revokeObjectURL(imgEl.src);
  5954. const videoEl = node.tagName === 'VIDEO' ? node : node.querySelector('video');
  5955. resetVideoElement(videoEl);
  5956. node.remove();
  5957. });
  5958. if (DOM.lightboxStage && DOM.lightboxDl && !DOM.lightboxStage.contains(DOM.lightboxDl)) {
  5959. DOM.lightboxStage.appendChild(DOM.lightboxDl);
  5960. }
  5961. if (DOM.lightboxStage) DOM.lightboxStage.style.removeProperty('--lightbox-ratio');
  5962. };
  5963. async function openLightbox(item, sourceCard) {
  5964. document.querySelectorAll('xai-card').forEach(card => {
  5965. card._isHovering = false;
  5966. if (typeof card.forcePause === 'function') card.forcePause();
  5967. });
  5968. const inner = DOM.lightboxInner;
  5969. clearLightboxStage();
  5970.  
  5971. if (DOM.lightbox._activeUrl) {
  5972. URL.revokeObjectURL(DOM.lightbox._activeUrl);
  5973. DOM.lightbox._activeUrl = null;
  5974. }
  5975.  
  5976. const openToken = (DOM.lightbox._openToken = (DOM.lightbox._openToken || 0) + 1);
  5977. const fetchId = String(item.refSourceId || item.id);
  5978. const fallbackBg = (item.lqip && item.lqip.includes('gradient')) ? item.lqip : 'transparent';
  5979. const isStale = () => STATE.activeLightboxId !== item.id || DOM.lightbox?._openToken !== openToken;
  5980.  
  5981. if (DOM.lightboxStage) DOM.lightboxStage.style.setProperty('--lightbox-ratio', String(item.ratio || (item.type === 'video' ? (16 / 9) : 1)));
  5982. DOM.lightboxDl.href = item.originalUrl || '#';
  5983. DOM.lightboxDl.download = item.filename || 'download';
  5984. STATE.activeLightboxId = item.id;
  5985.  
  5986. if (item.type === 'image') {
  5987. const previewEl = createLightboxImageLayer(item, 'lightbox-media lightbox-media--preview', TRANSPARENT_1X1, fallbackBg);
  5988. DOM.lightboxStage.insertBefore(previewEl, DOM.lightboxDl);
  5989.  
  5990. const fullEl = createLightboxImageLayer(item, 'lightbox-media lightbox-media--full', TRANSPARENT_1X1, 'transparent');
  5991. fullEl.style.opacity = '0';
  5992. fullEl.style.position = 'absolute';
  5993. fullEl.style.inset = '0';
  5994. DOM.lightboxStage.insertBefore(fullEl, DOM.lightboxDl);
  5995.  
  5996. MediaCache.getFile(fetchId, 'full').then(file => {
  5997. if (isStale() || !file) return;
  5998.  
  5999. if (DOM.lightbox._activeUrl) {
  6000. URL.revokeObjectURL(DOM.lightbox._activeUrl);
  6001. }
  6002.  
  6003. const tempUrl = URL.createObjectURL(file);
  6004. DOM.lightbox._activeUrl = tempUrl;
  6005.  
  6006. fullEl.onload = () => {
  6007. if (isStale()) return;
  6008. fullEl.style.opacity = '1';
  6009. inner.classList.add('is-loaded');
  6010. setTimeout(() => { if (previewEl.parentNode) previewEl.remove(); }, 150);
  6011. };
  6012. fullEl.src = tempUrl;
  6013. });
  6014. } else if (item.type === 'audio') {
  6015. const audioWrapper = document.createElement('div');
  6016. audioWrapper.className = 'lightbox-audio-card lightbox-media';
  6017. const promptArea = document.createElement('div');
  6018. promptArea.className = 'lightbox-audio-text';
  6019. promptArea.textContent = item.shortPrompt || item.prompt || 'Audio Generation';
  6020. LazyTextStore.get(item.id).then(heavy => { if (heavy?.prompt && !isStale()) promptArea.textContent = heavy.prompt || item.shortPrompt || 'Audio Generation'; });
  6021.  
  6022. const mediaEl = createLightboxVideoElement(item, '', '', fallbackBg);
  6023. mediaEl.className = 'lightbox-audio-player lightbox-media--video';
  6024. mediaEl.setAttribute('controlslist', 'nodownload noplaybackrate');
  6025. mediaEl.style.opacity = '1';
  6026. mediaEl.style.aspectRatio = 'auto';
  6027.  
  6028. audioWrapper.appendChild(DOM.lightboxDl);
  6029. audioWrapper.appendChild(promptArea);
  6030. audioWrapper.appendChild(mediaEl);
  6031. DOM.lightboxStage.appendChild(audioWrapper);
  6032. requestAnimationFrame(() => audioWrapper.classList.add('is-visible'));
  6033. } else {
  6034. const mediaEl = createLightboxVideoElement(item, '', '', fallbackBg);
  6035. DOM.lightboxStage.insertBefore(mediaEl, DOM.lightboxDl);
  6036. requestAnimationFrame(() => mediaEl.classList.add('is-visible'));
  6037. }
  6038.  
  6039. if (!DOM.lightbox.open) DOM.lightbox.showModal();
  6040.  
  6041. if (item.type !== 'image') {
  6042. MediaCache.getFile(fetchId, 'full').then(file => {
  6043. if (isStale()) return;
  6044.  
  6045. const videoEl = DOM.lightboxStage.querySelector('.lightbox-media--video');
  6046. if (!videoEl) return;
  6047.  
  6048. if (file) {
  6049. if (DOM.lightbox._activeUrl) {
  6050. URL.revokeObjectURL(DOM.lightbox._activeUrl);
  6051. }
  6052.  
  6053. const tempUrl = URL.createObjectURL(file);
  6054. DOM.lightbox._activeUrl = tempUrl;
  6055. DOM.lightboxDl.href = tempUrl;
  6056.  
  6057. videoEl.src = tempUrl;
  6058. } else if (item.originalUrl) {
  6059. DOM.lightboxDl.href = item.originalUrl;
  6060. videoEl.src = item.originalUrl;
  6061. } else {
  6062. return;
  6063. }
  6064.  
  6065. inner.classList.add('is-loaded');
  6066. videoEl.play().catch(()=>{});
  6067. }).catch(err => addLog('WARN', 'Failed to load media file for lightbox', err));
  6068. }
  6069. }
  6070.  
  6071. const closeLightbox = () => {
  6072. DOM.lightbox._openToken = (DOM.lightbox._openToken || 0) + 1;
  6073. DOM.lightbox.close();
  6074. clearLightboxStage();
  6075.  
  6076. if (DOM.lightbox._activeUrl) {
  6077. URL.revokeObjectURL(DOM.lightbox._activeUrl);
  6078. DOM.lightbox._activeUrl = null;
  6079. }
  6080.  
  6081. STATE.activeLightboxId = null;
  6082. }
  6083.  
  6084. function initMediaInteractions() {
  6085. const getEventElement = (event) => event.composedPath?.().find(node => node?.nodeType === Node.ELEMENT_NODE) || event.target;
  6086. const getEventCard = (event, targetEl = getEventElement(event)) => targetEl?.closest?.('xai-card') || targetEl?.getRootNode?.().host || null;
  6087. const handleRefDelete = (id) => {
  6088. const existsInAnyPanel = PANEL_ACTION_KEYS.some(panelKey => getPanelOrder(panelKey).includes(String(id)));
  6089. if (STATE.historyOrder.includes(id) || existsInAnyPanel) {
  6090. pruneReferenceState([id]);
  6091. } else void handleGlobalDelete(id, true);
  6092. };
  6093. const handleGlobalDelete = async (id, isRefDel = false) => {
  6094. const key = String(id);
  6095. const isText = STATE.items.get(key)?.type === 'text';
  6096. if (isText && isRuntimeBusy('text')) {
  6097. UI.showToast('Wait until the response finishes', 'warn');
  6098. return;
  6099. }
  6100.  
  6101. if (!isRefDel) {
  6102. const { toDelete, toVirtualize } = processDeletions([key]);
  6103. if (toVirtualize.length > 0) {
  6104. virtualizeRefs(toVirtualize);
  6105. return;
  6106. }
  6107. }
  6108.  
  6109. DBQueue.forgetItem(key);
  6110. STATE.historyOrder = STATE.historyOrder.filter(i => i !== key);
  6111. removeIdsFromAllPanels([key]);
  6112. document.querySelectorAll(`xai-card[data-id="${key}"]`).forEach(node => {
  6113. if (node.closest('#xai-history') && historyGrid) { node.unbind(); } else { node.remove(); }
  6114. });
  6115. if (historyGrid) historyGrid.updateData(STATE.historyOrder, STATE.items);
  6116. pruneReferenceState([key]);
  6117. Store.removeItem(key);
  6118. await StorageClient.deleteItemsBulk([key]).catch(e => addLog('WARN', 'Delete item failed', { id: key, error: e?.message || e }));
  6119. if (isText) await clearTextSessionArtifacts([key]);
  6120. if (isText && STATE.action === 'gen_text') restoreChatFromSession();
  6121. if (!isRefDel) scheduleHistoryMetricsUpdate();
  6122. scheduleDiskUpdate();
  6123. };
  6124.  
  6125. const handleGlobalClick = async (e) => {
  6126. const targetEl = getEventElement(e);
  6127. const card = getEventCard(e, targetEl);
  6128. if (!card) return;
  6129.  
  6130. const id = String(card.dataset.id), item = STATE.items.get(id);
  6131. if (!item) return;
  6132.  
  6133. const intentEl = targetEl.closest('[data-intent]');
  6134. const intent = intentEl?.dataset?.intent;
  6135.  
  6136. if (intent) {
  6137. if (intentEl.tagName !== 'INPUT' || intentEl.type !== 'checkbox') e.preventDefault();
  6138. e.stopPropagation();
  6139.  
  6140. switch (intent) {
  6141. case 'toggle-ref-active': {
  6142. const entry = getMutableRefViewEntry();
  6143. if (intentEl.checked) {
  6144. activateReferenceItem(item);
  6145. } else {
  6146. delete entry.active[id];
  6147. delete entry.waitUntil[id];
  6148. item.active = false;
  6149. item.readyAt = 0;
  6150. abortTasksForRef(item.id, STATE.action);
  6151. }
  6152. if (!intentEl.checked) card._internals.states.add('inactive'); else card._internals.states.delete('inactive');
  6153. persistRefViews();
  6154. updateUploadPlaceholderState();
  6155. if (isRuntimeBusy('media') && JobManager.getLane(STATE.action) === 'media') wakeUpQueue('media');
  6156. return;
  6157. }
  6158. case 'ref-prompt': return toggleRefPrompt(id);
  6159. case 'delete-ref': return handleRefDelete(id);
  6160. case 'download': return MediaUtils.downloadItem(item, false, card);
  6161. case 'insert-tag': {
  6162. const idx = card._insertTagIdx ?? targetEl.textContent.match(/IMAGE_(\d+)/)?.[1] ?? '0';
  6163. return insertTextAtCursor(DOM.prompt, ` <IMAGE_${idx}> `);
  6164. }
  6165. case 'delete-global': {
  6166. if (card._context === 'current' && !item.isPending) {
  6167. removeIdsFromAllPanels([id]);
  6168. return;
  6169. }
  6170. if (item.isPending && item.taskId) {
  6171. JobManager.findTask(item.taskId)?.controller.abort();
  6172. }
  6173. return void handleGlobalDelete(id);
  6174. }
  6175. case 'action-edit':
  6176. case 'action-extend':
  6177. case 'action-video': {
  6178. const modeMap = { 'action-extend': 'extend', 'action-video': 'video', 'action-edit': 'edit' };
  6179. const reqMode = modeMap[intent];
  6180. 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 });
  6181. if (newAction !== STATE.action) {
  6182. changeActionState(newAction);
  6183. if (STATE.action !== newAction) return;
  6184. }
  6185. moveReferenceToFront(id, { action: newAction, exclusiveActive: true, forceRebuild: true });
  6186. return;
  6187. }
  6188. }
  6189. }
  6190.  
  6191. if (targetEl.closest('.card-prompt') && item.type !== 'text') {
  6192. LazyTextStore.get(id).then(heavy => { if (heavy?.prompt) replaceEditorPromptValue(heavy.prompt); });
  6193. return;
  6194. }
  6195. if (!item.isPending && !targetEl.closest('.ref-header') && !targetEl.closest('.ref-side-prompt-btn')) {
  6196. if (item.type === 'text') void loadTextFromHistory(item);
  6197. else openLightbox(item, card);
  6198. }
  6199. };
  6200. App.on(document.body, 'click', handleGlobalClick);
  6201.  
  6202.  
  6203. App.on(DOM.saveRefBtn, 'click', () => saveAndCloseRefPrompt(false));
  6204. }
  6205.  
  6206. function initHotkeys() {
  6207. let tgt = DOM.currentImages; const setTgt = el => () => tgt = el;
  6208. App.on(DOM.history, 'mouseenter', setTgt(DOM.history)); App.on(DOM.currentImages, 'mouseenter', setTgt(DOM.currentImages)); App.on(DOM.refList, 'mouseenter', setTgt(DOM.refList));
  6209. App.on(document, 'keydown', e => {
  6210. const isInput = document.activeElement &&['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName);
  6211. if (isInput && document.activeElement === DOM.prompt &&['Home', 'End', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'PageUp', 'PageDown'].includes(e.key)) return;
  6212. if (isInput && document.activeElement !== DOM.prompt) return;
  6213. if (['PageUp', 'PageDown', 'Home', 'End'].includes(e.key)) {
  6214. if (!tgt) return;
  6215. e.preventDefault();
  6216. if (e.key === 'PageUp') tgt.scrollBy({ top: -tgt.clientHeight * 0.8, behavior: 'smooth' });
  6217. else if (e.key === 'PageDown') tgt.scrollBy({ top: tgt.clientHeight * 0.8, behavior: 'smooth' });
  6218. else if (e.key === 'Home') tgt.scrollTo({ top: 0, behavior: 'smooth' });
  6219. else if (e.key === 'End') tgt.scrollTo({ top: tgt.scrollHeight, behavior: 'smooth' });
  6220. }
  6221. });
  6222. }
  6223.  
  6224. function changeActionState(actionValue) {
  6225. const nextAction = sanitizePanelKey(actionValue, STATE.action);
  6226. if (STATE.action === nextAction) return;
  6227. const currentLane = JobManager.getLane(STATE.action);
  6228. const nextLane = JobManager.getLane(nextAction);
  6229. const nextRuntime = JobManager.getRuntime(nextLane);
  6230. if (nextRuntime.isGenerating && nextRuntime.activeAction && nextRuntime.activeAction !== nextAction) {
  6231. UI.showToast(`Active ${nextRuntime.label.toLowerCase()} work is locked to ${ACTION_SPEC[nextRuntime.activeAction]?.label || nextRuntime.activeAction}`, 'warn');
  6232. return;
  6233. }
  6234. if (currentLane === nextLane && isRuntimeBusy(currentLane)) {
  6235. UI.showToast('Stop current generation before switching within the same server group', 'warn');
  6236. return;
  6237. }
  6238. if (STATE.activeRefPromptId) saveAndCloseRefPrompt(true);
  6239. else if (DOM.prompt) PromptSync.captureEditor(STATE.action, { scheduleCommit: false });
  6240. flushCurrentPanelSessionSave();
  6241. STATE.action = nextAction; syncActionLabel(nextAction);
  6242. activatePanelState(nextAction);
  6243. setEditorPromptValue(getDraftActionPrompt());
  6244. updateDirtyStateUI();
  6245. }
  6246.  
  6247. function bindLogic() {
  6248. Object.entries(DOM_IDS).forEach(([k, id]) => DOM[k] = document.getElementById(id));
  6249. if (DOM.settingsPopover) DOM.settingsPopover.appendChild(buildSettingsPopoverContent());
  6250. PanelManager.init();
  6251.  
  6252. if (STATE.leftCollapsed) { DOM.colLeft.classList.add('is-collapsed'); document.getElementById('btn-collapse-left').setAttribute('aria-expanded', 'false'); }
  6253. if (STATE.rightCollapsed) { DOM.colRight.classList.add('is-collapsed'); document.getElementById('btn-collapse-right').setAttribute('aria-expanded', 'false'); }
  6254.  
  6255. initUIControls(); initLayoutManager(); initMediaManager(); initPromptManager(); initModals(); initMediaInteractions(); initAPIAndGeneration(); initHotkeys();
  6256. }
  6257.  
  6258. function initUIControls() {
  6259. const PRIMARY_DROPDOWN_IDS = ['action', 'textModel', 'batchSize', 'voice', 'language', 'ttsQuality', 'duration'];
  6260. const IMPORT_SYNC_DROPDOWN_IDS = ['batchSize', 'voice', 'language', 'ttsQuality'];
  6261. const getControlOptions = (control) => typeof control?.options === 'function' ? control.options() : (control?.options || []);
  6262. const setDropdownValue = (control, value, text = null) => {
  6263. const btn = DOM[control.domKey];
  6264. const popover = document.getElementById(control.popoverId);
  6265. if (btn) btn.dataset.value = String(value);
  6266. if (control.labelId && text !== null) {
  6267. const label = document.getElementById(control.labelId);
  6268. if (label) label.textContent = text;
  6269. }
  6270. setActivePopoverItem(popover, value);
  6271. };
  6272. const applyDropdownValue = (id, value) => {
  6273. if (id === 'action') return changeActionState(value);
  6274. if (id === 'currentAr') {
  6275. STATE.currentAr = getAspectRatioOption(value).label.trim();
  6276. syncDOMControls('currentAr');
  6277. return;
  6278. }
  6279. if (id === 'textModel') {
  6280. applyTextModelSelection(value);
  6281. return;
  6282. }
  6283. if (id === 'duration') {
  6284. STATE.duration = value;
  6285. return;
  6286. }
  6287. STATE[UI_CONTROL_BY_ID[id].stateKey] = value;
  6288. };
  6289. const syncImportedControlState = () => {
  6290. syncDOMControls(IMPORT_SYNC_DROPDOWN_IDS.map(id => UI_CONTROL_BY_ID[id].stateKey));
  6291. syncDOMControls(UI_TOGGLE_CONTROLS.map(c => c.stateKey));
  6292. syncDOMControls(SETTINGS_TOGGLE_META.map(c => c.stateKey));
  6293. syncDOMControls(SETTINGS_NUMBER_META.map(c => c.stateKey));
  6294. syncDOMControls('currentAr');
  6295. if (DOM.settingLoops) DOM.settingLoops.value = getLoopsValue(STATE.action);
  6296. syncLayoutWidths();
  6297. };
  6298.  
  6299. UI_DROPDOWN_CONTROLS.forEach(control => {
  6300. const btn = DOM[control.domKey], popover = document.getElementById(control.popoverId);
  6301. if (!btn || !popover) return;
  6302. btn.setAttribute('popovertarget', control.popoverId);
  6303. const renderOpts = (opts = []) => renderPopoverOptions(popover, btn.dataset.value || control.defaultValue, opts);
  6304. renderOpts(getControlOptions(control));
  6305. popover._updateOptions = (opts = []) => renderOpts(opts);
  6306. });
  6307. syncDOMControls(PRIMARY_DROPDOWN_IDS.map(id => UI_CONTROL_BY_ID[id].stateKey));
  6308. syncDOMControls('currentAr');
  6309. const toggleControlValue = (control, el) => {
  6310. if (!control || control.kind !== 'toggle') return;
  6311. const values = control.values || [];
  6312. const currentIndex = Math.max(0, values.indexOf(el.dataset.value));
  6313. const nextValue = values[(currentIndex + 1) % values.length] || values[0];
  6314. el.dataset.value = nextValue;
  6315. el.innerText = control.labels?.[nextValue] ?? nextValue;
  6316. STATE[control.stateKey] = nextValue;
  6317. if (control.id === 'textReasoningEffort' || control.id === 'textWebSearch' || control.id === 'textXSearch') updateActionUI();
  6318. };
  6319. const applyNumericControlValue = (control, el) => {
  6320. if (!control) return;
  6321. const input = el?.querySelector?.('input') || el;
  6322. if (control.id === 'loops') {
  6323. setLoopsValue(STATE.action, input?.value);
  6324. return;
  6325. }
  6326. const nextValue = parseInt(input?.value, 10) || 0;
  6327. const max = control.max;
  6328. STATE[control.stateKey] = max ? Math.min(max, nextValue) : nextValue;
  6329. };
  6330.  
  6331. const textModelPopover = document.getElementById('popover-text-model');
  6332. if (textModelPopover) {
  6333. const inputWrapper = document.createElement('div');
  6334. inputWrapper.className = 'custom-model-wrapper';
  6335. inputWrapper.style.cssText = 'padding: 6px 8px; border-bottom: 1px solid var(--border); margin-bottom: 4px;';
  6336.  
  6337. const input = document.createElement('input');
  6338. input.type = 'text';
  6339. input.placeholder = 'Add custom model... (Enter)';
  6340. 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;';
  6341.  
  6342. inputWrapper.appendChild(input);
  6343.  
  6344. inputWrapper.addEventListener('click', e => e.stopPropagation());
  6345.  
  6346. input.addEventListener('keydown', e => {
  6347. if (e.key === 'Enter') {
  6348. e.preventDefault();
  6349. const val = input.value.trim();
  6350. if (val) {
  6351. applyTextModelSelection(val);
  6352. textModelPopover.hidePopover();
  6353. }
  6354. input.value = '';
  6355. }
  6356. });
  6357.  
  6358. const origUpdate = textModelPopover._updateOptions;
  6359. textModelPopover._updateOptions = (opts) => {
  6360. if (origUpdate) origUpdate(opts);
  6361. textModelPopover.prepend(inputWrapper);
  6362. };
  6363. textModelPopover._updateOptions(UI_CONTROL_BY_ID.textModel.options());
  6364. }
  6365. syncDOMControls(UI_TOGGLE_CONTROLS.map(c => c.stateKey));
  6366.  
  6367. const setupTtsEffects = () => {
  6368. const btn = DOM.btnTtsEffects, popover = DOM.popoverTtsEffects;
  6369. if (!btn || !popover) return;
  6370. btn.setAttribute('popovertarget', 'popover-tts-effects');
  6371. const renderEffects = () => {
  6372. popover.innerHTML = '';
  6373. const appendGroup = (label, tags, onClick, getTitle) => {
  6374. const group = document.createElement('div');
  6375. group.className = 'tts-tag-group';
  6376. group.textContent = label;
  6377. popover.appendChild(group);
  6378. tags.forEach(tag => {
  6379. const item = document.createElement('div');
  6380. item.className = 'tts-tag-item';
  6381. item.textContent = tag;
  6382. item.title = getTitle(tag);
  6383. item.addEventListener('click', () => onClick(tag));
  6384. popover.appendChild(item);
  6385. });
  6386. };
  6387. appendGroup('Instant', APP_CONFIG.TTS_TAGS.instant.map(t => `[${t}]`), tag => {
  6388. insertTextAtCursor(DOM.prompt, tag);
  6389. }, tag => `Insert ${tag}`);
  6390. appendGroup('Wrapping', APP_CONFIG.TTS_TAGS.wrapping.map(t => `<${t}>`), tag => {
  6391. const selectedText = DOM.prompt.selectionStart !== DOM.prompt.selectionEnd
  6392. ? DOM.prompt.value.substring(DOM.prompt.selectionStart, DOM.prompt.selectionEnd)
  6393. : '';
  6394. const closing = tag.replace('<', '</');
  6395. const textToInsert = selectedText
  6396. ? `${tag}${selectedText}${closing}`
  6397. : `${tag}${closing}`;
  6398. insertTextAtCursor(DOM.prompt, textToInsert);
  6399. }, tag => `Insert ${tag}...${tag.replace('<', '</')}`);
  6400. };
  6401. renderEffects();
  6402. };
  6403. setupTtsEffects();
  6404.  
  6405. if (DOM.settingLoops) {
  6406. DOM.settingLoops.value = getLoopsValue(STATE.action);
  6407. App.on(DOM.settingLoops, 'click', e => e.stopPropagation());
  6408. }
  6409. syncDOMControls(SETTINGS_TOGGLE_META.map(c => c.stateKey));
  6410. syncDOMControls(SETTINGS_NUMBER_META.map(c => c.stateKey));
  6411. if (DOM.settingLoops) DOM.settingLoops.value = getLoopsValue(STATE.action);
  6412.  
  6413. DOM.settingsBtn.setAttribute('popovertarget', 'popover-settings');
  6414.  
  6415. const importInput = document.getElementById('file-import-state');
  6416. App.on(document, 'click', e => {
  6417. if (e.target.tagName === 'DIALOG' && e.target.open) return e.target.close();
  6418. const actionEl = e.target.closest('[data-action]');
  6419. if (!actionEl) return;
  6420. const action = actionEl.dataset.action;
  6421. if (action === 'select-popover-option') {
  6422. if (actionEl.classList.contains('disabled')) return;
  6423. const popover = actionEl.closest('.xai-custom-popover');
  6424. const control = getControlByStateKey(popover?.dataset.stateKey, 'prompt');
  6425. if (!popover || !control || control.kind !== 'dropdown') return;
  6426. const text = actionEl.querySelector('span')?.textContent || '';
  6427. setDropdownValue(control, actionEl.dataset.val, text);
  6428. popover.hidePopover();
  6429. applyDropdownValue(control.id, actionEl.dataset.val);
  6430. return;
  6431. }
  6432. if (action === 'toggle-ui-control') {
  6433. const control = getControlByStateKey(actionEl.dataset.stateKey, 'prompt');
  6434. toggleControlValue(control, actionEl);
  6435. return;
  6436. }
  6437. if (action === 'toggle-setting') {
  6438. const control = getControlByStateKey(actionEl.dataset.stateKey, 'settings');
  6439. if (!control || control.kind !== 'toggle') return;
  6440. STATE[control.stateKey] = !STATE[control.stateKey];
  6441. const checkbox = actionEl.querySelector('input[type="checkbox"]');
  6442. if (checkbox) checkbox.checked = !!STATE[control.stateKey];
  6443. if (DOM[control.domKey]) DOM[control.domKey].checked = !!STATE[control.stateKey];
  6444. e.stopPropagation();
  6445. return;
  6446. }
  6447. if (action === 'export-state') {
  6448. if (!STATE.activeRefPromptId && DOM.prompt) PromptSync.captureEditor(STATE.action, { scheduleCommit: false });
  6449. const stateToExport = buildExportState();
  6450. const u = URL.createObjectURL(new Blob([JSON.stringify(stateToExport)], {type: 'application/json'}));
  6451. clickTransientLink({ href: u, download: `xAI_Pro_Settings_${makeTimestamp()}.json` });
  6452. setTimeout(() => URL.revokeObjectURL(u), 10000);
  6453. return;
  6454. }
  6455. if (action === 'import-state') {
  6456. importInput?.click();
  6457. return;
  6458. }
  6459. if (action === 'toggle-ref-mode') {
  6460. STATE.refMode = STATE.refMode === 'multi' ? 'queue' : 'multi';
  6461. return;
  6462. }
  6463. if (action === 'toggle-left-panel') {
  6464. STATE.leftCollapsed = !STATE.leftCollapsed;
  6465. return;
  6466. }
  6467. if (action === 'toggle-right-panel') {
  6468. STATE.rightCollapsed = !STATE.rightCollapsed;
  6469. return;
  6470. }
  6471. if (action === 'open-favs') {
  6472. e.preventDefault();
  6473. renderFavs();
  6474. DOM.favDialog.showModal();
  6475. return;
  6476. }
  6477. if (action === 'close-lightbox') {
  6478. e.preventDefault();
  6479. closeLightbox();
  6480. return;
  6481. }
  6482. if (action === 'lightbox-dl') {
  6483. e.preventDefault();
  6484. e.stopPropagation();
  6485. if (STATE.activeLightboxId) {
  6486. const item = STATE.items.get(STATE.activeLightboxId);
  6487. if (item) MediaUtils.downloadItem(item, true);
  6488. }
  6489. return;
  6490. }
  6491. if (action === 'chat-action') {
  6492. e.preventDefault();
  6493. e.stopPropagation();
  6494. handleChatAction(actionEl.dataset.chatAction, actionEl.closest('.chat-msg-row'));
  6495. return;
  6496. }
  6497. });
  6498. App.on(document, 'input', e => {
  6499. const actionEl = e.target.closest('[data-action]');
  6500. if (!actionEl) return;
  6501. if (actionEl.dataset.action === 'input-ui-number') {
  6502. const control = getControlByStateKey(actionEl.dataset.stateKey, 'prompt');
  6503. if (control?.kind === 'number-inline') applyNumericControlValue(control, actionEl);
  6504. return;
  6505. }
  6506. if (actionEl.dataset.action === 'input-setting-number') {
  6507. const control = getControlByStateKey(actionEl.dataset.stateKey, 'settings');
  6508. if (control?.kind === 'number') applyNumericControlValue(control, actionEl);
  6509. }
  6510. });
  6511. App.on(importInput, 'change', e => {
  6512. const file = e.target.files[0]; if (!file) return;
  6513. const reader = new FileReader();
  6514. reader.onload = evt => {
  6515. try {
  6516. const p = parseImportedState(JSON.parse(evt.target.result));
  6517. Object.entries(p.config).forEach(([key, value]) => STATE[key] = value);
  6518. STATE.prompts = p.prompts;
  6519. STATE.promptDrafts = p.promptDrafts;
  6520. STATE.refPrompts = p.refPrompts;
  6521. STATE.favPrompts = p.favPrompts;
  6522. persistPromptStateStores();
  6523. syncActionLabel(); _updateActionUIDom();
  6524. syncImportedControlState();
  6525. if (!STATE.activeRefPromptId) setEditorPromptValue(getDraftActionPrompt());
  6526. renderRefMode(); UI.showToast("Settings & Prompts imported successfully!", "success");
  6527. } catch(err) { UI.showToast(err?.message || 'Failed to parse JSON', "error"); }
  6528. }; reader.readAsText(file); e.target.value = '';
  6529. });
  6530.  
  6531. renderRefMode(); updateUploadPlaceholderState(); _updateActionUIDom();
  6532. }
  6533.  
  6534. function initLayoutManager() {
  6535. syncLayoutWidths();
  6536. const saveWidthsOnIdle = () => {
  6537. if (DOM.colLeft && !STATE.leftCollapsed) STATE.leftWidth = Math.max(260, Math.round(getComputedStyle(DOM.colLeft).width) || STATE.leftWidth);
  6538. if (DOM.colRight && !STATE.rightCollapsed) STATE.rightWidth = Math.max(240, Math.round(getComputedStyle(DOM.colRight).width) || STATE.rightWidth);
  6539. };
  6540. App.on(DOM.colLeft, 'mouseup', saveWidthsOnIdle);
  6541. App.on(DOM.colRight, 'mouseup', saveWidthsOnIdle);
  6542. }
  6543.  
  6544. let processMediaFileLocal = null;
  6545. function initMediaManager() {
  6546. let dragged = null, pendingRefTarget = null, pendingRefAfter = false, refReorderRaf = 0;
  6547. const flushRefReorder = () => {
  6548. refReorderRaf = 0;
  6549. if (!dragged || !pendingRefTarget || pendingRefTarget === dragged) return;
  6550. DOM.refList.moveBefore(dragged, pendingRefAfter ? pendingRefTarget.nextSibling : pendingRefTarget);
  6551. };
  6552. const finishRefDrag = () => {
  6553. if (refReorderRaf) {
  6554. cancelAnimationFrame(refReorderRaf);
  6555. flushRefReorder();
  6556. }
  6557. if (!dragged) return;
  6558. dragged.classList.remove('dragging');
  6559. dragged = null;
  6560. pendingRefTarget = null;
  6561. pendingRefAfter = false;
  6562. getMutableRefViewEntry().order = [...DOM.refList.querySelectorAll('xai-card:state(reference)')].map(c => String(c.dataset.id));
  6563. updateRefState();
  6564. };
  6565. App.on(DOM.refList, 'dragstart', e => {
  6566. const card = e.target.closest('xai-card:state(reference)');
  6567. if (!card) return;
  6568. dragged = card;
  6569. pendingRefTarget = null;
  6570. pendingRefAfter = false;
  6571. e.dataTransfer.effectAllowed = 'move';
  6572. e.dataTransfer.setData('text/plain', card.dataset.id);
  6573. requestAnimationFrame(() => card.classList.add('dragging'));
  6574. });
  6575. App.on(DOM.refList, 'dragover', e => {
  6576. if (!dragged) return;
  6577. e.preventDefault();
  6578. e.dataTransfer.dropEffect = 'move';
  6579. const targetCard = e.target.closest('xai-card:state(reference):not(.dragging)');
  6580. if (!targetCard || targetCard === dragged) return;
  6581. const rect = targetCard.getBoundingClientRect(), next = (e.clientY - rect.top) / (rect.bottom - rect.top) > 0.5;
  6582. if (targetCard === pendingRefTarget && next === pendingRefAfter) return;
  6583. pendingRefTarget = targetCard;
  6584. pendingRefAfter = next;
  6585. if (refReorderRaf) cancelAnimationFrame(refReorderRaf);
  6586. refReorderRaf = requestAnimationFrame(flushRefReorder);
  6587. });
  6588. App.on(DOM.refList, 'drop', e => { if (!dragged) return; e.preventDefault(); finishRefDrag(); });
  6589. App.on(DOM.refList, 'dragend', finishRefDrag);
  6590. App.ac.signal.addEventListener('abort', () => {
  6591. if (refReorderRaf) cancelAnimationFrame(refReorderRaf);
  6592. document.body.classList.remove('is-dragging');
  6593. dragged = null;
  6594. pendingRefTarget = null;
  6595. pendingRefAfter = false;
  6596. refReorderRaf = 0;
  6597. });
  6598.  
  6599. const mediaDragKey = 'application/x-xai-media-id';
  6600. const handleMediaDragStart = (e) => {
  6601. const card = e.target.closest('xai-card');
  6602. if (!card || (!card._internals.states.has('history') && !card._internals.states.has('current'))) return;
  6603. document.body.classList.add('is-dragging');
  6604. e.dataTransfer.effectAllowed = 'copy';
  6605. e.dataTransfer.setData(mediaDragKey, card.dataset.id);
  6606. };
  6607. [DOM.history, DOM.currentImages].forEach(el => {
  6608. if (!el) return;
  6609. App.on(el, 'dragstart', handleMediaDragStart);
  6610. App.on(el, 'dragend', () => document.body.classList.remove('is-dragging'));
  6611. });
  6612.  
  6613. processMediaFileLocal = async (file) => {
  6614. const mimeType = inferMediaMimeType(file), isVid = mimeType.startsWith('video/'), isImg = mimeType.startsWith('image/'); if (!isVid && !isImg) return;
  6615. const nextAction = (ACTION_SPEC[STATE.action] ?? ACTION_SPEC.gen_image).uploadTransition?.({ mimeType, currentAction: STATE.action }) ?? STATE.action;
  6616. if (isVid && !/mp4/i.test(mimeType)) {
  6617. UI.showToast('Only MP4 video refs are supported for video edit/extend', 'warn');
  6618. return;
  6619. }
  6620. if (nextAction !== STATE.action) {
  6621. changeActionState(nextAction);
  6622. if (STATE.action !== nextAction) return;
  6623. }
  6624. const maxRefs = (ACTION_SPEC[STATE.action] ?? ACTION_SPEC.gen_image).maxRefs?.({ action: STATE.action, refMode: normalizeRefModeForAction(STATE.action, STATE.refMode) }) ?? 0;
  6625. if (maxRefs === 0) {
  6626. UI.showToast('This mode does not use references', 'warn');
  6627. return;
  6628. }
  6629. if (getRefOrderForView().length >= maxRefs) return;
  6630. const mediaData = await MediaUtils.processSource(file, isVid ? 'video' : 'image', APP_CONFIG.MEDIA.THUMB_MAX_DIM, APP_CONFIG.MEDIA.THUMB_WEBP_QUALITY), id = crypto.randomUUID();
  6631. await OPFS.write(id, 'full', file); if (mediaData.thumbBlob) await OPFS.write(id, 'thumb', mediaData.thumbBlob);
  6632. Store.commitNewItem({ id, type: isVid ? 'video' : 'image', mimeType, filename: file.name, ratio: mediaData.ratio, lqip: mediaData.lqip, isUploadedRef: true });
  6633. appendReferenceToEnd(id, { enforceLimit: true, forceRebuild: true });
  6634. };
  6635. let mediaImportQueue = Promise.resolve();
  6636. const queueMediaFileLocal = (file) => {
  6637. mediaImportQueue = mediaImportQueue.then(() => processMediaFileLocal(file)).catch(err => addLog('WARN', 'Local media import failed', err));
  6638. return mediaImportQueue;
  6639. };
  6640.  
  6641. const supportsDropPayload = (dataTransfer, { allowFiles = true, allowInternalMedia = true } = {}) => !!dataTransfer && ((allowFiles && hasDataTransferType(dataTransfer, 'Files')) || (allowInternalMedia && hasDataTransferType(dataTransfer, mediaDragKey)));
  6642. const bindDropSurface = (el, { onHover, canHandle = () => true, onDrop }) => {
  6643. if (!el) return;
  6644. App.on(el, 'dragenter', e => {
  6645. if (!canHandle(e.dataTransfer)) return;
  6646. e.preventDefault();
  6647. e.stopPropagation();
  6648. onHover?.(true);
  6649. });
  6650. App.on(el, 'dragover', e => {
  6651. if (!canHandle(e.dataTransfer)) return;
  6652. e.preventDefault();
  6653. e.stopPropagation();
  6654. e.dataTransfer.dropEffect = 'copy';
  6655. });
  6656. App.on(el, 'dragleave', e => {
  6657. if (!canHandle(e.dataTransfer)) return;
  6658. e.preventDefault();
  6659. e.stopPropagation();
  6660. onHover?.(false);
  6661. });
  6662. App.on(el, 'drop', async e => {
  6663. if (!canHandle(e.dataTransfer)) return;
  6664. e.preventDefault();
  6665. e.stopPropagation();
  6666. onHover?.(false);
  6667. await onDrop?.(e.dataTransfer);
  6668. });
  6669. };
  6670. const handleReferenceDrop = async (dataTransfer) => {
  6671. const mediaId = dataTransfer.getData(mediaDragKey);
  6672. if (mediaId && STATE.items.has(mediaId)) {
  6673. const item = STATE.items.get(mediaId);
  6674. const mimeType = item?.mimeType || `${item?.type || 'image'}/`;
  6675. const nextAction = (ACTION_SPEC[STATE.action] ?? ACTION_SPEC.gen_image).uploadTransition?.({ mimeType, currentAction: STATE.action }) ?? STATE.action;
  6676. if (item?.type === 'video' && !isMp4ReferenceItem(item)) {
  6677. UI.showToast('Only MP4 video refs are supported for video edit/extend', 'warn');
  6678. return;
  6679. }
  6680. if (nextAction !== STATE.action) {
  6681. changeActionState(nextAction);
  6682. if (STATE.action !== nextAction) return;
  6683. }
  6684. const maxRefs = (ACTION_SPEC[nextAction] ?? ACTION_SPEC.gen_image).maxRefs?.({ action: nextAction, refMode: normalizeRefModeForAction(nextAction, STATE.refMode) }) ?? 0;
  6685. if (maxRefs === 0) {
  6686. UI.showToast('This mode does not use references', 'warn');
  6687. return;
  6688. }
  6689. moveReferenceToFront(mediaId, { action: nextAction, enforceLimit: true, forceRebuild: true });
  6690. return;
  6691. }
  6692. Array.from(dataTransfer.files || []).forEach(queueMediaFileLocal);
  6693. };
  6694. const handlePromptDrop = async (dataTransfer) => {
  6695. const mediaId = dataTransfer.getData(mediaDragKey);
  6696. if (mediaId && STATE.items.has(mediaId)) {
  6697. const heavy = await LazyTextStore.get(mediaId);
  6698. if (heavy && heavy.prompt) {
  6699. if (replaceEditorPromptValue(heavy.prompt)) return;
  6700. }
  6701. UI.showToast('This media has no saved prompt to insert.', 'warn');
  6702. return;
  6703. }
  6704. const files = Array.from(dataTransfer.files || []);
  6705. if (files.length === 1) {
  6706. const payload = await extractImagePromptViaWorker(files[0]);
  6707. if (payload?.isPromptCarrier) {
  6708. if (replaceEditorPromptValue(payload.prompt)) return;
  6709. UI.showToast('This image has no embedded prompt.', 'warn');
  6710. return;
  6711. }
  6712. }
  6713. files.forEach(queueMediaFileLocal);
  6714. };
  6715. App.on(DOM.uploadPlaceholder, 'click', () => { if (getRefOrderForView().length < APP_CONFIG.LIMITS.MAX_REFS_UI) DOM.fileInput.click(); });
  6716. App.on(DOM.fileInput, 'change', e => { Array.from(e.target.files).forEach(queueMediaFileLocal); DOM.fileInput.value = ''; });
  6717. bindDropSurface(DOM.dropzone, { onHover: hover => DOM.uploadPlaceholder.classList.toggle('drag-over', hover), canHandle: dataTransfer => supportsDropPayload(dataTransfer), onDrop: handleReferenceDrop });
  6718. bindDropSurface(DOM.prompt, { onHover: hover => DOM.promptContainer.classList.toggle('drag-over', hover), canHandle: dataTransfer => supportsDropPayload(dataTransfer), onDrop: handlePromptDrop });
  6719.  
  6720. const handleFavDrop = async (dataTransfer) => {
  6721. const files = Array.from(dataTransfer.files ||[]);
  6722. for (const file of files) {
  6723. if (isImagePromptSupported(file)) {
  6724. const payload = await extractImagePromptViaWorker(file);
  6725. if (payload?.isPromptCarrier && payload.prompt) {
  6726. if (addActionFavorite(payload.prompt)) {
  6727. renderFavs();
  6728. UI.showToast('Prompt added to favorites from image!', 'success');
  6729. } else {
  6730. UI.showToast('Prompt is already in favorites.', 'info');
  6731. }
  6732. } else {
  6733. UI.showToast('No embedded prompt found in this image.', 'warn');
  6734. }
  6735. }
  6736. }
  6737. };
  6738.  
  6739. bindDropSurface(DOM.favBtn, {
  6740. onHover: hover => DOM.favBtn.style.transform = hover ? 'scale(1.1)' : '',
  6741. canHandle: dt => supportsDropPayload(dt, { allowInternalMedia: false }),
  6742. onDrop: handleFavDrop
  6743. });
  6744. bindDropSurface(DOM.favDialog, {
  6745. onHover: hover => DOM.favDialog.style.boxShadow = hover ? 'inset 0 0 0 2px var(--success)' : '',
  6746. canHandle: dt => supportsDropPayload(dt, { allowInternalMedia: false }),
  6747. onDrop: handleFavDrop
  6748. });
  6749.  
  6750. App.on(document, 'paste', e => {
  6751. const tag = document.activeElement?.tagName; if (tag &&['INPUT', 'TEXTAREA'].includes(tag) && document.activeElement !== DOM.prompt) return;
  6752. let hasMedia = false;
  6753. if (e.clipboardData?.items) {
  6754. for (let i = 0; i < e.clipboardData.items.length; i++) {
  6755. const item = e.clipboardData.items[i];
  6756. 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); }
  6757. }
  6758. }
  6759. });
  6760. }
  6761.  
  6762. function initPromptManager() {
  6763. setEditorPromptValue(getDraftActionPrompt());
  6764. App.on(DOM.promptContainer, 'pointerdown', e => {
  6765. if (e.target === DOM.promptContainer || e.target === DOM.textareaWrapper) DOM.prompt.focus();
  6766. });
  6767. App.on(DOM.prompt, 'input', () => {
  6768. if (STATE.activeRefPromptId) {
  6769. STATE.activeRefPromptDraft = DOM.prompt.value;
  6770. CardRegistry.get('reference', STATE.activeRefPromptId)?.setReferencePromptFlag(hasCustomRefPromptDraft(STATE.activeRefPromptDraft));
  6771. return;
  6772. }
  6773. PromptSync.captureEditor(STATE.action, { scheduleCommit: false });
  6774. updateDirtyStateUI();
  6775. });
  6776. App.on(DOM.prompt, 'keydown', e => {
  6777. if (STATE.activeRefPromptId) {
  6778. if (e.key === 'Escape') { e.preventDefault(); saveAndCloseRefPrompt(true); }
  6779. if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); saveAndCloseRefPrompt(false); }
  6780. return;
  6781. }
  6782. if (e.key === 'Enter' && !e.shiftKey) {
  6783. e.preventDefault();
  6784. if (STATE.action !== 'gen_text') {
  6785. const editorVal = sanitizePromptText(getEditorPromptValue());
  6786. const committedVal = sanitizePromptText(getStoredActionPrompt(STATE.action));
  6787. if (editorVal !== committedVal) {
  6788. PromptSync.commitNow(STATE.action);
  6789. DOM.promptUpdateLabel.innerText = 'Saved to queue';
  6790. DOM.promptUpdateLabel.classList.add('saved');
  6791. DOM.generateBtn.classList.remove('is-dirty');
  6792. DOM.generateBtn.classList.remove('flash-saved');
  6793. void DOM.generateBtn.offsetWidth;
  6794. DOM.generateBtn.classList.add('flash-saved');
  6795. setTimeout(() => {
  6796. DOM.promptUpdateLabel.classList.remove('show');
  6797. DOM.promptUpdateLabel.classList.remove('saved');
  6798. DOM.generateBtn.classList.remove('flash-saved');
  6799. }, 1500);
  6800. }
  6801. }
  6802. if (!DOM.generateBtn.disabled && !STATE.isGenerating) DOM.generateBtn.click();
  6803. }
  6804. });
  6805. }
  6806.  
  6807. const renderFavs = () => {
  6808. const favs = getActionFavorites();
  6809. const pv = Pipeline.getOriginalPrompt(DOM.prompt.value);
  6810. const hasPending = pv && !favs.includes(pv);
  6811. const fragment = document.createDocumentFragment();
  6812. const buildFavItem = (text, pending = false) => {
  6813. const row = document.createElement('div'); row.className = `fav-item${pending ? ' pending' : ''}`; row.dataset.text = text;
  6814. const textEl = document.createElement('div'); textEl.className = 'fav-text'; textEl.title = text; textEl.textContent = text;
  6815. 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;
  6816. row.append(textEl, btn);
  6817. return row;
  6818. };
  6819.  
  6820. if (favs.length === 0 && !hasPending) {
  6821. const empty = document.createElement('div');
  6822. empty.className = 'empty-favs text-muted text-center';
  6823. empty.style.cssText = 'padding:16px;font-size:14px;';
  6824. empty.textContent = 'No favorites yet.';
  6825. fragment.appendChild(empty);
  6826. } else {
  6827. favs.forEach(f => fragment.appendChild(buildFavItem(f)));
  6828. if (hasPending) fragment.appendChild(buildFavItem(pv, true));
  6829. }
  6830.  
  6831. DOM.favList.replaceChildren(fragment);
  6832. };
  6833.  
  6834. function initModals() {
  6835. App.on(DOM.favDialog, 'click', e => {
  6836. const itemEl = e.target.closest('.fav-item'); if (!itemEl) return;
  6837. const text = itemEl.dataset.text;
  6838. if (e.target.closest('.fav-text')) { if (!itemEl.classList.contains('pending')) { DOM.favDialog.close(); requestAnimationFrame(() => insertTextAtCursor(DOM.prompt, text, true)); } }
  6839. else if (e.target.closest('.fav-add-btn')) {
  6840. addActionFavorite(text);
  6841. renderFavs();
  6842. }
  6843. else if (e.target.closest('.fav-del-btn')) {
  6844. removeActionFavorite(text);
  6845. renderFavs();
  6846. }
  6847. });
  6848. App.on(DOM.lightbox, 'cancel', e => { e.preventDefault(); closeLightbox(); });
  6849. }
  6850.  
  6851. async function loadTextFromHistory(item) {
  6852. if (isRuntimeBusy('text')) {
  6853. UI.showToast('Wait until the response finishes', 'warn');
  6854. return;
  6855. }
  6856. const messages = await loadTextSessionMessages(item);
  6857. summarizeTextSessionItem(item, messages, { updatedAt: item.timestamp || Date.now(), model: item.textModel });
  6858. DBQueue.add(item);
  6859. setActiveTextSessionState(item.id, messages, { persist: true });
  6860. changeActionState('gen_text');
  6861. if (item.textModel) applyTextModelSelection(item.textModel);
  6862. restoreChatFromSession();
  6863. }
  6864.  
  6865. function parseDynamicPrompt(text) {
  6866. if (typeof text !== "string" || !text) return text || "";
  6867.  
  6868. let out = text.replace(/^\s*#[^\n]*/gm, "");
  6869. const vars = new Map();
  6870.  
  6871. const expandVars = (input, depth, stack = new Set()) => {
  6872. if (depth <= 0 || !input.includes("${")) return input;
  6873. let res = "", i = 0;
  6874.  
  6875. while (i < input.length) {
  6876. if (input[i] === "$" && input[i + 1] === "{") {
  6877. let j = i + 2, braces = 0;
  6878. while (j < input.length) {
  6879. if (input[j] === "{") braces++;
  6880. else if (input[j] === "}") {
  6881. if (!braces) break;
  6882. braces--;
  6883. }
  6884. j++;
  6885. }
  6886. if (j >= input.length) { res += input.slice(i); break; }
  6887.  
  6888. const body = input.slice(i + 2, j);
  6889. const eq = body.indexOf("=");
  6890.  
  6891. if (eq > 0) {
  6892. const name = body.slice(0, eq).trim();
  6893. let expr = body.slice(eq + 1);
  6894. const imm = expr.startsWith("!");
  6895. if (imm) expr = expr.slice(1);
  6896. if (name) vars.set(name, { expr, imm, val: imm ? resolve(expr, depth - 1, new Set(stack)) : "" });
  6897. } else {
  6898. const col = body.indexOf(":");
  6899. const hasDefault = col > -1;
  6900. const name = (hasDefault ? body.slice(0, col) : body).trim();
  6901. const fb = hasDefault ? body.slice(col + 1) : "";
  6902. const ref = vars.get(name);
  6903.  
  6904. if (ref) {
  6905. if (ref.imm) res += ref.val;
  6906. else if (!stack.has(name)) res += resolve(ref.expr, depth - 1, new Set(stack).add(name));
  6907. } else if (hasDefault) {
  6908. res += resolve(fb, depth - 1, new Set(stack));
  6909. } else {
  6910. res += `\${${body}}`;
  6911. }
  6912. }
  6913. i = j + 1;
  6914. } else {
  6915. res += input[i++];
  6916. }
  6917. }
  6918. return res;
  6919. };
  6920.  
  6921. const expandBlocks = (input) => input.replace(/\{([^{}]+)\}/g, (_, inner) => {
  6922. const p = inner.split("$$"), hasCfg = p.length > 1;
  6923. const rRaw = hasCfg ? (p[0] || "1") : "1";
  6924. const sep = p.length > 2 ? p[1] : ", ";
  6925. const oRaw = hasCfg ? (p.length > 2 ? p.slice(2).join("$$") : p[1]) : inner;
  6926.  
  6927. const pool = oRaw.split("|").flatMap(opt => {
  6928. const i = opt.indexOf("::"), w = i < 0 ? 1 : +opt.slice(0, i), v = (i < 0 ? opt : opt.slice(i + 2)).trim();
  6929. return Number.isFinite(w) && w > 0 && v ? [{ w, v }] : [];
  6930. });
  6931. if (!pool.length) return "";
  6932.  
  6933. const [a, b] = rRaw.split("-");
  6934. const min = Math.max(0, +a || 1);
  6935. const max = Math.max(min, b === undefined ? min : +b || pool.length);
  6936. let c = Math.min(pool.length, (Math.random() * (max - min + 1) + min) | 0);
  6937. if (c <= 0) return "";
  6938.  
  6939. const picked = [];
  6940. let total = pool.reduce((sum, o) => sum + o.w, 0);
  6941. while (c--) {
  6942. let r = Math.random() * total, i = pool.findIndex(o => (r -= o.w) <= 0);
  6943. if (i < 0) i = pool.length - 1;
  6944. const [hit] = pool.splice(i, 1);
  6945. picked.push(hit.v);
  6946. total -= hit.w;
  6947. }
  6948. return picked.join(sep);
  6949. });
  6950.  
  6951. const resolve = (input, depth, stack = new Set()) => {
  6952. let current = input;
  6953. for (let i = 0; i < depth && (current.includes("${") || /\{[^{}]+\}/.test(current)); i++) {
  6954. const prev = current;
  6955. current = expandBlocks(expandVars(current, depth - i, stack));
  6956. if (current === prev) break;
  6957. }
  6958. return current;
  6959. };
  6960.  
  6961. return resolve(out, 50).replace(/[ \t]{2,}/g, " ").replace(/\n\s*\n/g, "\n").trim();
  6962. }
  6963. function resolveAspectRatio(currentRefs = null, settings = STATE) {
  6964. let ar = settings.currentAr;
  6965. if (ar !== 'Ref') return ar.toLowerCase().trim();
  6966. if (!currentRefs?.length || !currentRefs[0]?.ratio) return 'auto';
  6967.  
  6968. let bestMatch = '1:1', minDiff = Infinity;
  6969. APP_CONFIG.ASPECT_RATIOS.forEach(option => {
  6970. if (option.isAuto || option.isRef) return;
  6971. const ratio = option.w / option.h, diff = Math.abs(ratio - currentRefs[0].ratio);
  6972. if (diff < minDiff) {
  6973. minDiff = diff;
  6974. bestMatch = option.label;
  6975. }
  6976. });
  6977. return bestMatch.toLowerCase().trim();
  6978. }
  6979. function applySingleRefTrick(singleRefUrl) {
  6980. return[
  6981. { url: singleRefUrl },
  6982. { url: singleRefUrl }
  6983. ];
  6984. }
  6985.  
  6986. function buildMediaPayload(type, payload, currentRefs, refBase64s, ar, settings = STATE) {
  6987. const isImage = type === 'image';
  6988. payload.resolution = isImage ? settings.resImg : settings.resVid;
  6989. if (isImage) payload.response_format = 'b64_json';
  6990. if (!isImage) payload.duration = parseInt(settings.duration);
  6991.  
  6992. const canUseAr = ar !== 'auto' && (isImage || ALLOWED_VIDEO_ARS.has(ar.toUpperCase?.() ? ar.toUpperCase() : ar));
  6993. if (canUseAr) payload.aspect_ratio = ar;
  6994.  
  6995. if (!currentRefs?.length || !refBase64s[0]) return;
  6996.  
  6997. if (!isImage && payload.duration > 10) {
  6998. payload.image = { url: refBase64s[0] };
  6999. return;
  7000. }
  7001.  
  7002. if (currentRefs.length === 1) {
  7003. if (isImage && settings.referenceARTrick && ar !== 'auto') {
  7004. payload.images = applySingleRefTrick(refBase64s[0]);
  7005. } else {
  7006. payload.image = { url: refBase64s[0] };
  7007. }
  7008. return;
  7009. }
  7010.  
  7011. const limit = isImage ? APP_CONFIG.LIMITS.API_MAX_MULTI_IMG : APP_CONFIG.LIMITS.API_MAX_MULTI_VID;
  7012. const limitedBase64s = refBase64s.filter(Boolean).slice(0, limit);
  7013.  
  7014. payload[isImage ? 'images' : 'reference_images'] = limitedBase64s.map(url => ({ url }));
  7015. }
  7016.  
  7017. function buildVideoEditPayload(actionType, payload, currentRefs, refBase64s, settings = STATE) {
  7018. if (actionType === 'extend_video') payload.duration = parseInt(settings.duration);
  7019. if (currentRefs?.length && refBase64s[0]) payload.video = { url: refBase64s[0] };
  7020. }
  7021. function buildAudioPayload(payload, settings = STATE) {
  7022. const text = payload.prompt;
  7023. delete payload.model;
  7024. delete payload.prompt;
  7025. payload.text = text;
  7026. payload.voice_id = settings.voice || 'eve';
  7027. payload.language = settings.language || 'auto';
  7028. const quality = TTS_QUALITY_MAP.get(settings.ttsQuality) || APP_CONFIG.TTS_QUALITIES[1];
  7029. payload.output_format = {
  7030. codec: 'mp3',
  7031. sample_rate: 48000,
  7032. bit_rate: quality.bitRate
  7033. };
  7034. }
  7035. function buildResponsesTextContent(text) {
  7036. return [{ type: 'input_text', text: String(text ?? '') }];
  7037. }
  7038. function buildResponsesUserContent(text, imageUrls = []) {
  7039. const content = buildResponsesTextContent(text);
  7040. imageUrls.filter(Boolean).slice(0, APP_CONFIG.LIMITS.API_MAX_MULTI_TEXT).forEach(url => content.push({ type: 'input_image', image_url: url }));
  7041. return content;
  7042. }
  7043. function extractResponsesOutputText(responseData) {
  7044. if (typeof responseData?.output_text === 'string' && responseData.output_text) return responseData.output_text;
  7045. const parts = [];
  7046. const output = Array.isArray(responseData?.output) ? responseData.output : [];
  7047. output.forEach(item => {
  7048. if (!item || item.type !== 'message' || item.role !== 'assistant' || !Array.isArray(item.content)) return;
  7049. item.content.forEach(contentItem => {
  7050. if (contentItem?.type === 'output_text' && typeof contentItem.text === 'string' && contentItem.text) {
  7051. parts.push(contentItem.text);
  7052. }
  7053. });
  7054. });
  7055. return parts.join('');
  7056. }
  7057. function getLastResponsesUserText(payload) {
  7058. const input = Array.isArray(payload?.input) ? payload.input : [];
  7059. const message = input.findLast(m => m?.role === 'user');
  7060. if (!message) return '';
  7061. const content = Array.isArray(message.content) ? message.content : buildResponsesTextContent(message.content);
  7062. const textParts = content
  7063. .filter(part => part?.type === 'input_text' && typeof part.text === 'string')
  7064. .map(part => part.text);
  7065. return textParts.length > 0 ? textParts.join('\n').trim() : '';
  7066. }
  7067.  
  7068. async function getApiPayload(actionType = STATE.action, currentRefs = null, refPromptOverride = null, apiPromptOverride = null, settings = buildActionSettingsSnapshot(actionType)) {
  7069. const livePrompt = apiPromptOverride != null
  7070. ? sanitizePromptText(apiPromptOverride)
  7071. : PromptPipeline.process(STATE.activeRefPromptId ? getStoredActionPrompt(actionType) : getTaskPrompt(actionType), '', actionType).api;
  7072. const parsedRefPrompt = refPromptOverride != null ? PromptPipeline.process(refPromptOverride, '', actionType).api : null;
  7073. const endpoint = (ACTION_SPEC[actionType] ?? ACTION_SPEC.gen_image).endpoint?.({ action: actionType, refCount: currentRefs?.length || 0 }) || '';
  7074. const payload = { model: (ACTION_SPEC[actionType] ?? ACTION_SPEC.gen_image).model, prompt: sanitizePromptText(parsedRefPrompt ?? livePrompt) };
  7075. const aspectRatio = resolveAspectRatio(currentRefs, settings);
  7076. const refBase64s = currentRefs ? await Promise.all(currentRefs.map(m => RefPayloadCache.get(m))) : [];
  7077. (ACTION_SPEC[actionType] ?? ACTION_SPEC.gen_image).buildPayload?.({ action: actionType, payload, currentRefs, refBase64s, aspectRatio, settings, textChatMessages: settings.textChatMessages });
  7078. addLog('INFO', 'Built payload', { endpoint, payload });
  7079. return { endpoint, payload };
  7080. }
  7081.  
  7082. async function fetchWithRetries(endpoint, payload, signal, runtime, token, settings, taskId, onProgress, streamHandlers = null, binaryOpfsId = null) {
  7083. let attempts = 0, rateLimitFails = 0, authRetries = 0; const getDelay = () => Math.floor(Math.random() * (settings.retryMaxMs - settings.retryMinMs + 1)) + settings.retryMinMs;
  7084. const expectsBinary = endpoint === '/v1/tts';
  7085. while (attempts < APP_CONFIG.TIMINGS.MAX_RETRY_ATTEMPTS && runtime.generationToken === token) {
  7086. try {
  7087. streamHandlers?.onAttemptStart?.(attempts + 1);
  7088. const attemptNo = attempts + 1;
  7089. const requestStartedAt = performance.now();
  7090. addLog('NETWORK_REQ', `POST ${endpoint}`, { taskId, attempt: attemptNo, payload });
  7091. const res = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Parallel-Task': taskId }, body: JSON.stringify(payload), signal });
  7092. const elapsedMs = Math.max(0, Math.round(performance.now() - requestStartedAt));
  7093. if (classifyHttpStatus(res.status) === 'retry') {
  7094. attempts++;
  7095. addLog('NETWORK_WARN', `Gateway error ${res.status}`, { taskId, attempt: attemptNo, elapsedMs });
  7096. onProgress({ shortStatus: `Busy(${res.status}). Rty[${attempts}]`, longStatus: `Server Busy (${res.status}). Retrying (Attempt ${attempts})...`, color: '#eab308' });
  7097. await sleep(getDelay(), signal);
  7098. continue;
  7099. }
  7100. if (classifyHttpStatus(res.status) === 'auth') {
  7101. attempts++; authRetries++;
  7102. addLog('NETWORK_WARN', `Authentication error ${res.status}`, { taskId, attempt: attemptNo, elapsedMs, authRetries });
  7103. 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 });
  7104. onProgress({ shortStatus: `Auth(${res.status}). Wiping...[${attempts}]`, longStatus: `Session Expired (${res.status}). Resetting (Attempt ${attempts})...`, color: 'orange' });
  7105. await nuclearWipe(); await sleep(Math.max(getDelay(), authRetries * 2000), signal); continue;
  7106. }
  7107. authRetries = 0;
  7108. if (!res.ok) {
  7109. const errData = await parseResponseJsonSafe(res);
  7110. addLog('NETWORK_ERR', 'Response error JSON', { taskId, attempt: attemptNo, status: res.status, elapsedMs, data: errData });
  7111. const errMsg = buildHttpErrorMessage(res.status, errData);
  7112. if (res.status === 400 || isInvalidRequestError(errMsg)) {
  7113. addLog('NETWORK_WARN', 'Invalid request payload (400)', { taskId, attempt: attemptNo, status: res.status, elapsedMs, data: errData });
  7114. onProgress({ shortStatus: `Bad Req(${res.status})`, longStatus: `Payload Error (${res.status}): Check text length, voice, or format.`, color: '#f59e0b' });
  7115. throw Object.assign(new Error(errMsg || 'Bad Request (400)'), { status: res.status, data: errData, noRetry: true, invalidRequest: true });
  7116. }
  7117. if (isRequestModeratedError(errMsg)) {
  7118. addLog('NETWORK_WARN', 'Moderation or policy failure', { taskId, attempt: attemptNo, status: res.status, elapsedMs, data: errData });
  7119. onProgress({ shortStatus: `Blocked(${res.status})`, longStatus: `Request blocked by moderation (${res.status}).`, color: '#a1a1aa' });
  7120. throw Object.assign(new Error(errMsg), { status: res.status, data: errData, noRetry: true, moderation: true });
  7121. }
  7122. throw Object.assign(new Error(errMsg), { status: res.status, data: errData });
  7123. }
  7124. if (expectsBinary) {
  7125. const stream = res.body;
  7126. addLog('NETWORK_RES', 'Transferring binary stream to worker', { taskId, attempt: attemptNo, status: res.status, elapsedMs });
  7127. const opfsId = binaryOpfsId || taskId;
  7128. await StorageClient.processStream(stream, { isBinary: true, id: opfsId }, null, signal);
  7129. return { _streamedBinary: true, opfsId };
  7130. }
  7131. if (streamHandlers && payload.stream === true) {
  7132. const stream = res.body;
  7133. addLog('NETWORK_RES', 'Transferring SSE stream to worker', { taskId, attempt: attemptNo, status: res.status, elapsedMs });
  7134. const data = await StorageClient.processStream(stream, { isBinary: false }, (delta, accumulatedText, isDone) => {
  7135. if (isDone) {
  7136. streamHandlers.onStreamDone?.();
  7137. return;
  7138. }
  7139. if (delta) streamHandlers.onTextDelta?.(delta, accumulatedText);
  7140. }, signal);
  7141. addLog('NETWORK_RES', 'Successful streaming response', { taskId, attempt: attemptNo, status: res.status, elapsedMs, data });
  7142. return data;
  7143. }
  7144. const data = await res.json();
  7145. addLog('NETWORK_RES', 'Successful response', { taskId, attempt: attemptNo, status: res.status, elapsedMs, data });
  7146. return data;
  7147. } catch(err) {
  7148. if (err.name === 'AbortError' || err.noRetry) throw err;
  7149. const errMsg = getErrorMessage(err);
  7150. const errStr = errMsg.toLowerCase();
  7151. const isShortRateLimit = errStr.match(/requests per second \(actual\/limit\):\s*(\d+)\s*\/\s*\1/);
  7152. const isLongRateLimit = classifyHttpStatus(err.status) === 'rateLimit' || /rate limit|exhausted/.test(errStr) || errStr.match(/requests per minute \(actual\/limit\):\s*(\d+)\s*\/\s*\1/);
  7153. if (isShortRateLimit || isLongRateLimit) {
  7154. const nextAttempt = attempts + 1;
  7155. addLog('NETWORK_WARN', 'Rate limit encountered', { taskId, attempt: attempts + 1, status: err.status || 429, message: errMsg });
  7156. onProgress({ shortStatus: `Lmt(${err.status || 429}). Wiping...[${nextAttempt}]`, longStatus: `Rate Limit Hit (${err.status || 429}). Resetting (Attempt ${nextAttempt})...`, color: 'orange' }); await nuclearWipe(); attempts++;
  7157. if (attempts >= APP_CONFIG.TIMINGS.MAX_RETRY_ATTEMPTS) throw new Error("Max retries on rate limits");
  7158. 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;
  7159. }
  7160. if (shouldAutoRetryStuckVideo(errStr, settings.autoRetryVideo)) {
  7161. attempts++;
  7162. addLog('POLL_WARN', 'Video task stalled or rejected', { taskId, attempt: attempts, message: errMsg });
  7163. if (attempts >= APP_CONFIG.TIMINGS.MAX_RETRY_ATTEMPTS) throw new Error("Max retries for stuck video");
  7164. onProgress({ shortStatus: `Stuck Vid. Rty[${attempts}]`, longStatus: `Video Generation Stuck. Auto-retrying (Attempt ${attempts})...`, color: 'orange' });
  7165. await sleep(getDelay(), signal);
  7166. if (payload.prompt) payload.prompt += '\u200B';
  7167. continue;
  7168. }
  7169. attempts++; if (attempts >= APP_CONFIG.TIMINGS.MAX_RETRY_ATTEMPTS) throw new Error(`Err: ${errMsg.split('\n')[0].substring(0, 15)}`);
  7170. 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);
  7171. }
  7172. } throw new Error("Max retry reached");
  7173. }
  7174.  
  7175. async function pollVideoTask(reqId, signal, runtime, token, onProgress) {
  7176. let pollCount = 0;
  7177. let fatalGraceCount = 0;
  7178. while (token === -1 || runtime.generationToken === token) {
  7179. if (++pollCount > APP_CONFIG.TIMINGS.MAX_POLL_ATTEMPTS) throw new Error("Timeout: Video likely dropped or stuck.");
  7180. await sleep(APP_CONFIG.TIMINGS.API_POLL_INTERVAL, signal);
  7181. if (token !== -1 && runtime.generationToken !== token) break;
  7182. try {
  7183. const pollStartedAt = performance.now();
  7184. addLog('POLL_REQ', `Polling video ${reqId}`, { reqId, pollCount, maxPolls: APP_CONFIG.TIMINGS.MAX_POLL_ATTEMPTS });
  7185. const res = await fetch(`/v1/videos/${reqId}`, { signal });
  7186. const elapsedMs = Math.max(0, Math.round(performance.now() - pollStartedAt));
  7187. 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; }
  7188.  
  7189. let isFatal = false;
  7190. let fatalReason = '';
  7191. let data = null;
  7192.  
  7193. if (!res.ok) {
  7194. const errData = await parseResponseJsonSafe(res);
  7195. if (res.status === 404) { isFatal = true; fatalReason = "video lost (404). Session wiped."; }
  7196. 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 });
  7197. } else {
  7198. data = await res.json();
  7199. if (data?.error) throw Object.assign(new Error(data.error.message || JSON.stringify(data.error)), { data });
  7200. const st = (data?.status || data?.state || 'processing').toLowerCase();
  7201. if (VIDEO_SUCCESS_STATES.has(st)) return data.video.url;
  7202. else if (VIDEO_FLAGGED_STATES.has(st) || data.is_sensitive) {
  7203. if (data?.video?.url) return data.video.url;
  7204. isFatal = true; fatalReason = `Generation halted. Reason: ${st}`;
  7205. }
  7206. }
  7207.  
  7208. if (isFatal) {
  7209. if (fatalGraceCount < 4) {
  7210. fatalGraceCount++;
  7211. pollCount--;
  7212. addLog('POLL_WARN', `Fatal condition met, entering grace retry ${fatalGraceCount}/4`);
  7213. continue;
  7214. }
  7215. throw new Error(fatalReason);
  7216. }
  7217.  
  7218. if (data) {
  7219. const st = (data?.status || data?.state || 'processing').toLowerCase(), pNum = typeof data?.progress === 'number' ? data.progress : null, progStr = pNum !== null ? ` ${pNum}%` : '';
  7220. const isDone = VIDEO_SUCCESS_STATES.has(st);
  7221. onProgress({
  7222. shortStatus: `[${pollCount}/${APP_CONFIG.TIMINGS.MAX_POLL_ATTEMPTS}] ${st.charAt(0).toUpperCase() + st.slice(1)}${progStr}...`,
  7223. longStatus: `Rendering Video: ${st.charAt(0).toUpperCase() + st.slice(1)}${progStr} (Attempt ${pollCount}/${APP_CONFIG.TIMINGS.MAX_POLL_ATTEMPTS})`,
  7224. color: '#fff',
  7225. progress: pNum,
  7226. durationMs: isDone || pNum === 100 ? 400 : 2500,
  7227. easing: isDone || pNum === 100 ? 'ease-out' : 'cubic-bezier(0.2, 1, 0.3, 1)',
  7228. forceUIUpdate: true
  7229. });
  7230. }
  7231. } catch (e) { throw e; }
  7232. }
  7233. }
  7234.  
  7235. function createPlaceholders(taskId, batchCount, actionType, currentRefs, panelKey = actionType, draftPrompt = '') {
  7236. let targetRatio = actionType.includes('video') ? (16 / 9) : (actionType === 'gen_audio' ? 5 : 1); const ar = STATE.currentAr.trim();
  7237. 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;
  7238. const refItem = currentRefs.length > 0 ? currentRefs[0] : null, theme = refItem ? null : PlaceholderPalette.next(), bgPreview = refItem?.lqip || theme?.gradient || '#070809', placeholderIds =[];
  7239. const initialStatus = refItem
  7240. ? [{ text: `${(ACTION_SPEC[actionType] ?? ACTION_SPEC.gen_image).title} • ${refItem.filename || 'Reference'}`, color: '#fff', style: CURRENT_PANEL_STATUS_STYLE }]
  7241. : [
  7242. { text: (ACTION_SPEC[actionType] ?? ACTION_SPEC.gen_image).title, color: '#fff', style: CURRENT_PANEL_STATUS_STYLE },
  7243. { text: 'Queued...', color: '#d4d4d8' }
  7244. ];
  7245. for (let i = 0; i < batchCount; i++) {
  7246. const pid = crypto.randomUUID(); placeholderIds.push(pid);
  7247. const promptText = draftPrompt || (actionType === 'gen_text' ? getTaskPrompt('gen_text') : (actionType === 'gen_audio' ? getTaskPrompt('gen_audio') : ''));
  7248. 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 });
  7249. }
  7250. return placeholderIds;
  7251. }
  7252.  
  7253. function finalizePendingVideoItem(item, videoUrl, posterData = null) {
  7254. if (!item) return null;
  7255. item.type = 'video';
  7256. item.mimeType = 'video/mp4';
  7257. item.isPending = false;
  7258. item.originalUrl = videoUrl;
  7259. item.filename = `Grok Video - ${makeTimestamp()}.mp4`;
  7260. item.ratio = posterData?.ratio || item.ratio || (16 / 9);
  7261. item.lqip = posterData?.lqip || item.lqip || null;
  7262. item.refSourceId = null;
  7263. item.reqId = null;
  7264. item.taskId = null;
  7265. item.persist = true;
  7266. item._statusData = [];
  7267. return item;
  7268. }
  7269.  
  7270. async function refreshFinalizedVideoCard(card, item) {
  7271. if (!card || !item) return;
  7272. card.refresh(item);
  7273. card.setStatus([]);
  7274. card.setProgress(null);
  7275. const targetLevel = card._isActuallyVisible
  7276. ? (card._context === 'current' ? (isMemoryConstrainedMode() ? 'thumb' : 'full') : 'thumb')
  7277. : (card._context === 'current' ? (isMemoryConstrainedMode() ? 'none' : 'thumb') : 'none');
  7278. await card.setLoadLevel('none');
  7279. if (targetLevel !== 'none') await card.setLoadLevel(targetLevel);
  7280. }
  7281.  
  7282. const getPendingItemIdsForRuntime = (runtime) => {
  7283. const taskIds = new Set([...runtime.tasks.keys()].map(String));
  7284. return STATE.items.values()
  7285. .filter(item => item?.isPending && item.taskId && taskIds.has(String(item.taskId)))
  7286. .map(item => String(item.id))
  7287. .filter(Boolean);
  7288. };
  7289.  
  7290. async function executeSingleTask(runtime, taskId, refIds, isMulti, token) {
  7291. const taskMeta = runtime.tasks.get(taskId); if (!taskMeta) return;
  7292. const actionType = sanitizePanelKey(taskMeta.action, runtime.activeAction || taskMeta.action || STATE.action);
  7293. const settings = taskMeta.settingsSnapshot || buildActionSettingsSnapshot(actionType);
  7294. const currentRefs = refIds.map(id => getRefItemForView(id, actionType, settings.refMode)).filter(r => r && r.active);
  7295. if (currentRefs.length === 0 && !['MULTI', 'NO_REF'].includes(refIds[0])) { taskMeta.longStatus = 'Skipped'; return; }
  7296. const isImage = actionType === 'gen_image';
  7297. const batchCount = isImage ? parseInt(settings.batchSize || '1') : 1;
  7298. const { api: apiPrompt, storage: storagePrompt } = PromptPipeline.process(taskMeta.promptText, '', actionType);
  7299. const placeholderIds = createPlaceholders(taskId, batchCount, actionType, currentRefs, taskMeta.panelKey || actionType, storagePrompt);
  7300. let attempts = 0, success = false, reqStartTime = Date.now();
  7301. let failureCleanupDelayMs = 0;
  7302. let progressTimer = 0;
  7303.  
  7304. const batchCards = placeholderIds.map(pid => CardRegistry.get('current', pid)).filter(Boolean);
  7305. const updateTask = (val, dur, ease) => {
  7306. taskMeta.shortStatus = val.shortStatus; taskMeta.longStatus = val.longStatus; taskMeta.color = val.color;
  7307. taskMeta.progress = val.progress !== undefined ? val.progress : null;
  7308. if (val.forceUIUpdate) {
  7309. updateStatusUI(true);
  7310. } else {
  7311. updateStatusUI(false);
  7312. }
  7313. if (actionType === 'gen_text' && taskMeta.chatStatusMsgId && !taskMeta.chatHasStreamText) {
  7314. patchMessage(taskMeta.chatStatusMsgNode || taskMeta.chatStatusMsgId, val.longStatus || val.shortStatus || 'Working...', true);
  7315. }
  7316. const statusLines = buildCurrentStatusLines(val);
  7317. for (let i = 0; i < batchCards.length; i++) {
  7318. const card = batchCards[i];
  7319. if (!card._isActuallyVisible && val.progress !== 100 && val.progress !== 0) continue;
  7320. card.setStatus(statusLines);
  7321. card.setProgress(taskMeta.progress, dur ?? val.durationMs, ease ?? val.easing);
  7322. }
  7323. placeholderIds.forEach(pid => {
  7324. const pItem = STATE.items.get(pid);
  7325. if (pItem) pItem._statusData = statusLines;
  7326. });
  7327. if (runtime.lane !== 'media') return;
  7328. const refProgress = new Map();
  7329. runtime.tasks.values().forEach(task => {
  7330. task.refIds.forEach(rid => {
  7331. const key = String(rid);
  7332. const entry = refProgress.get(key) || { total: 0, count: 0 };
  7333. entry.total += task.progress || 0;
  7334. entry.count++;
  7335. refProgress.set(key, entry);
  7336. });
  7337. });
  7338. refIds.forEach(rid => {
  7339. const refCard = CardRegistry.get('reference', rid);
  7340. if (refCard?.setProgress) {
  7341. const entry = refProgress.get(String(rid));
  7342. refCard.setProgress(entry ? entry.total / entry.count : taskMeta.progress, dur ?? val.durationMs, ease ?? val.easing);
  7343. }
  7344. });
  7345. };
  7346.  
  7347. let expectedMs = 5000;
  7348. if (isImage) {
  7349. expectedMs = GenerationTimings.get('img', settings.resImg, batchCount);
  7350. updateTask({ shortStatus: 'Req...', longStatus: 'Requesting...', color: '#fff', progress: 0, forceUIUpdate: true }, 0, 'linear');
  7351.  
  7352. const tickProgress = () => {
  7353. if (success || taskMeta.controller.signal.aborted) return;
  7354. const elapsed = Date.now() - reqStartTime;
  7355. const percent = Math.min(95, Math.floor((elapsed / expectedMs) * 100));
  7356. updateTask({ shortStatus: `[${percent}%] Rendering...`, longStatus: 'Rendering image...', color: '#fff', progress: percent, durationMs: 250, easing: 'linear', forceUIUpdate: true });
  7357. progressTimer = setTimeout(tickProgress, 250);
  7358. };
  7359. progressTimer = setTimeout(tickProgress, 250);
  7360. taskMeta.controller.signal.addEventListener('abort', () => clearTimeout(progressTimer), { once: true });
  7361. }
  7362.  
  7363. try {
  7364. while (attempts < APP_CONFIG.TIMINGS.MAX_RETRY_ATTEMPTS && !success && runtime.generationToken === token) {
  7365. try {
  7366. const liveRefPrompt = (!isMulti && currentRefs.length === 1) ? (STATE.refPrompts[currentRefs[0].id]?.[actionType] ?? null) : null;
  7367. const config = await getApiPayload(actionType, currentRefs.length > 0 ? currentRefs : null, liveRefPrompt, apiPrompt, settings);
  7368. const payload = config.payload;
  7369. if (actionType === 'gen_text') taskMeta.textModel = taskMeta.textModel || payload.model || settings.textModel || 'grok-4.20';
  7370.  
  7371. if (actionType === 'gen_image') payload.n = parseInt(settings.batchSize || '1');
  7372.  
  7373. if (actionType === 'gen_text' && attempts === 0) {
  7374. const userPrompt = storagePrompt || getLastResponsesUserText(payload);
  7375. if (userPrompt) {
  7376. const promptText = typeof userPrompt === 'string' ? userPrompt : JSON.stringify(userPrompt);
  7377. taskMeta.chatUserMsgId = renderTextChatMessage('user', promptText);
  7378. taskMeta.chatStatusMsgId = renderTextChatMessage('ai', 'Working...', true, promptText);
  7379. taskMeta.chatStatusMsgNode = document.querySelector(`[data-msg-id="${taskMeta.chatStatusMsgId}"]`);
  7380. }
  7381. }
  7382. if (actionType === 'gen_text') {
  7383. taskMeta.chatHasStreamText = false;
  7384. taskMeta.streamText = '';
  7385. }
  7386.  
  7387. reqStartTime = Date.now();
  7388. taskMeta.reqStartTime = reqStartTime;
  7389. taskMeta.expectedMs = expectedMs;
  7390.  
  7391. const streamHandlers = actionType === 'gen_text' && payload.stream === true ? {
  7392. onAttemptStart: () => {
  7393. taskMeta.chatHasStreamText = false;
  7394. taskMeta.streamText = '';
  7395. taskMeta._mdDirty = false;
  7396. taskMeta._mdFlushId = 0;
  7397. taskMeta._mdCache = null;
  7398. taskMeta._lastRenderTime = 0;
  7399. if (taskMeta.chatStatusMsgId) patchMessage(taskMeta.chatStatusMsgNode || taskMeta.chatStatusMsgId, 'Working...', true);
  7400. },
  7401. onTextDelta: (delta, fullText) => {
  7402. taskMeta.chatHasStreamText = true;
  7403. taskMeta.streamText = fullText;
  7404. taskMeta._mdDirty = true;
  7405. if (!taskMeta._mdFlushId) {
  7406. const delay = Math.max(0, 50 - (Date.now() - (taskMeta._lastRenderTime || 0)));
  7407. taskMeta._mdFlushId = setTimeout(() => {
  7408. requestAnimationFrame(() => {
  7409. taskMeta._mdFlushId = 0;
  7410. taskMeta._lastRenderTime = Date.now();
  7411. if (taskMeta.chatStatusMsgNode && taskMeta._mdDirty) {
  7412. const lastSafeCut = taskMeta.streamText.lastIndexOf('\n\n');
  7413. if (lastSafeCut > 0) {
  7414. if (taskMeta._mdCache && taskMeta.streamText.startsWith(taskMeta._mdCache.text)) {
  7415. const cachedLen = taskMeta._mdCache.text.length;
  7416. if (lastSafeCut > cachedLen) {
  7417. const newBlocks = taskMeta.streamText.slice(cachedLen, lastSafeCut);
  7418. taskMeta._mdCache.text = taskMeta.streamText.slice(0, lastSafeCut);
  7419. taskMeta._mdCache.html += parseMarkdown(newBlocks);
  7420. }
  7421. const tail = taskMeta.streamText.slice(lastSafeCut);
  7422. patchMessage(taskMeta.chatStatusMsgNode || taskMeta.chatStatusMsgId, taskMeta.streamText, false, taskMeta._mdCache.html + parseMarkdown(tail));
  7423. } else {
  7424. const safeText = taskMeta.streamText.slice(0, lastSafeCut);
  7425. taskMeta._mdCache = { text: safeText, html: parseMarkdown(safeText) };
  7426. const tail = taskMeta.streamText.slice(lastSafeCut);
  7427. patchMessage(taskMeta.chatStatusMsgNode || taskMeta.chatStatusMsgId, taskMeta.streamText, false, taskMeta._mdCache.html + parseMarkdown(tail));
  7428. }
  7429. } else {
  7430. patchMessage(taskMeta.chatStatusMsgNode || taskMeta.chatStatusMsgId, taskMeta.streamText, false, parseMarkdown(taskMeta.streamText));
  7431. }
  7432. taskMeta._mdDirty = false;
  7433. }
  7434. });
  7435. }, delay);
  7436. }
  7437. if (actionType === 'gen_text' && STATE.action === 'gen_text') {
  7438. if (!taskMeta._smoothScrollTimer) {
  7439. taskMeta._smoothScrollTimer = setTimeout(() => {
  7440. taskMeta._smoothScrollTimer = 0;
  7441. const chatView = document.getElementById('xai-chat-container');
  7442. if (chatView) {
  7443. const isNearBottom = chatView.scrollHeight - chatView.scrollTop - chatView.clientHeight < 350;
  7444. if (isNearBottom) chatView.scrollTo({ top: chatView.scrollHeight, behavior: 'smooth' });
  7445. }
  7446. }, 300);
  7447. }
  7448. }
  7449. },
  7450. onStreamDone: () => {
  7451. if (taskMeta._mdFlushId) { clearTimeout(taskMeta._mdFlushId); taskMeta._mdFlushId = 0; }
  7452. if (taskMeta.chatStatusMsgId && taskMeta._mdDirty) {
  7453. patchMessage(taskMeta.chatStatusMsgNode || taskMeta.chatStatusMsgId, taskMeta.streamText, false, parseMarkdown(taskMeta.streamText));
  7454. taskMeta._mdDirty = false;
  7455. taskMeta._mdCache = null;
  7456. }
  7457. },
  7458. onTextDelta: (delta, fullText) => {
  7459. taskMeta.chatHasStreamText = true;
  7460. taskMeta.streamText = fullText;
  7461. taskMeta._mdDirty = true;
  7462. if (!taskMeta._mdFlushId) {
  7463. const delay = Math.max(0, 50 - (Date.now() - (taskMeta._lastRenderTime || 0)));
  7464. taskMeta._mdFlushId = setTimeout(() => {
  7465. requestAnimationFrame(() => {
  7466. taskMeta._mdFlushId = 0;
  7467. taskMeta._lastRenderTime = Date.now();
  7468. if (taskMeta.chatStatusMsgNode && taskMeta._mdDirty) {
  7469. patchMessage(taskMeta.chatStatusMsgNode, taskMeta.streamText, false);
  7470. taskMeta._mdDirty = false;
  7471. }
  7472. });
  7473. }, delay);
  7474. }
  7475. if (actionType === 'gen_text' && STATE.action === 'gen_text') {
  7476. if (!taskMeta._smoothScrollTimer) {
  7477. taskMeta._smoothScrollTimer = setTimeout(() => {
  7478. taskMeta._smoothScrollTimer = 0;
  7479. const chatView = document.getElementById('xai-chat-container');
  7480. if (chatView) {
  7481. const isNearBottom = chatView.scrollHeight - chatView.scrollTop - chatView.clientHeight < 350;
  7482. if (isNearBottom) chatView.scrollTo({ top: chatView.scrollHeight, behavior: 'smooth' });
  7483. }
  7484. }, 300);
  7485. }
  7486. }
  7487. },
  7488. onStreamDone: () => {
  7489. if (taskMeta._mdFlushId) { clearTimeout(taskMeta._mdFlushId); taskMeta._mdFlushId = 0; }
  7490. if (taskMeta.chatStatusMsgId && taskMeta._mdDirty) {
  7491. patchMessage(taskMeta.chatStatusMsgNode || taskMeta.chatStatusMsgId, taskMeta.streamText, false);
  7492. taskMeta._mdDirty = false;
  7493. }
  7494. }
  7495. } : null;
  7496.  
  7497. const audioOpfsId = actionType === 'gen_audio' ? crypto.randomUUID() : null;
  7498. const resultData = await fetchWithRetries(config.endpoint, payload, taskMeta.controller.signal, runtime, token, settings, taskId, updateTask, streamHandlers, audioOpfsId);
  7499. if (runtime.generationToken !== token || !resultData) break;
  7500.  
  7501. if (isImage) {
  7502. if (resultData?.data && Array.isArray(resultData.data)) {
  7503. updateTask({ shortStatus: 'Saving...', longStatus: 'Rendering Complete. Saving...', color: '#10b981', progress: 100, forceUIUpdate: true }, 400, 'ease-out');
  7504.  
  7505. if (attempts === 0) {
  7506. GenerationTimings.update('img', settings.resImg, batchCount, Date.now() - reqStartTime);
  7507. }
  7508.  
  7509. const committedIds = [], committedItems = [], batchTimestampBase = Date.now();
  7510. for (let i = 0; i < resultData.data.length; i++) {
  7511. if (runtime.generationToken !== token) break;
  7512. const imgObj = resultData.data[i];
  7513. const is2k = settings.resImg === '2k';
  7514. const prompt = imgObj.revised_prompt ? PromptPipeline.process(imgObj.revised_prompt).storage : storagePrompt;
  7515. const finalId = crypto.randomUUID();
  7516.  
  7517. const b64Str = imgObj.b64_json;
  7518. delete imgObj.b64_json;
  7519. imgObj.b64_json = null;
  7520.  
  7521. const finalBytes = Uint8Array.fromBase64(b64Str);
  7522. const buffer = finalBytes.buffer;
  7523.  
  7524. const result = await StorageClient.processMediaResponse(finalId, {
  7525. buffer: buffer,
  7526. prompt,
  7527. is2k,
  7528. optimize2k: settings.optimize2k,
  7529. embedImagePrompt: settings.embedImagePrompt
  7530. }, [buffer]);
  7531.  
  7532. if (result.thumbBlob) {
  7533. const cacheKey = `${finalId}_thumb`;
  7534. const thumbFile = new File([result.thumbBlob], cacheKey, { type: result.mimeType || 'image/webp' });
  7535. const cachedPromise = Promise.resolve(thumbFile);
  7536. MediaCache._inFlight.set(cacheKey, cachedPromise);
  7537. cachedPromise.finally(() => MediaCache._inFlight.delete(cacheKey));
  7538. }
  7539.  
  7540. const item = Store.commitNewItem({
  7541. id: finalId,
  7542. type: 'image',
  7543. mimeType: result.mimeType,
  7544. ratio: result.ratio,
  7545. lqip: result.lqip,
  7546. xmpEmbedded: result.xmpEmbedded,
  7547. prompt,
  7548. timestamp: Pipeline.makeBatchTimestamp(batchTimestampBase, i, resultData.data.length),
  7549. filename: `Grok Img - ${makeTimestamp()} - ${i + 1}of${resultData.data.length}.${result.mimeType === 'image/jpeg' ? 'jpg' : 'png'}`,
  7550. isUploadedRef: false,
  7551. isNewlyGenerated: true
  7552. }, { targetArrayName: 'currentOrder', replacePlaceholderId: placeholderIds[i], panelKey: taskMeta.panelKey });
  7553.  
  7554. committedIds.push(finalId);
  7555. committedItems.push(item);
  7556. }
  7557. if (committedIds.length > 0) {
  7558. await commitItemsToHistory(committedItems, committedIds);
  7559. scheduleAutoDownloads(committedItems, 500, 50);
  7560. }
  7561. const unused = placeholderIds.slice(resultData.data.length);
  7562. if (unused.length > 0) Store.clearCurrentTransient(unused, taskMeta.panelKey);
  7563. success = committedIds.length > 0;
  7564. } else throw new Error('Invalid response format');
  7565. } else if (actionType === 'gen_audio') {
  7566. updateTask({ shortStatus: 'Saving...', longStatus: 'Audio Generated. Saving...', color: '#10b981', progress: 100 });
  7567. const id = resultData.opfsId;
  7568. const item = Store.commitNewItem({
  7569. id, type: 'audio', mimeType: 'audio/mpeg', timestamp: Date.now(), prompt: Pipeline.normalizePrompt(payload.text || ''),
  7570. filename: `Grok Audio - ${makeTimestamp()}.mp3`,
  7571. ratio: 5, originalUrl: null, downloaded: false, lqip: '#0f172a', isUploadedRef: false, isNewlyGenerated: true
  7572. }, { targetArrayName: 'currentOrder', replacePlaceholderId: placeholderIds[0], panelKey: taskMeta.panelKey });
  7573. await commitItemsToHistory([item], [id]);
  7574. scheduleAutoDownloads([item]);
  7575. success = true;
  7576. } else if (actionType === 'gen_text') {
  7577. const textContent = extractResponsesOutputText(resultData);
  7578. if (!textContent) {
  7579. throw new Error(resultData.error?.message || 'Invalid API response: No text returned.');
  7580. }
  7581. updateTask({ shortStatus: 'Saving...', longStatus: 'Text Generated. Saving...', color: '#10b981', progress: 100 });
  7582.  
  7583. const userPrompt = storagePrompt || getLastResponsesUserText(payload);
  7584. const baseMessages = sanitizeTextConversationStore(taskMeta.textChatSnapshot || []);
  7585.  
  7586. if (taskMeta.chatHasStreamText && taskMeta.chatStatusMsgId) {
  7587. patchMessage(taskMeta.chatStatusMsgNode || taskMeta.chatStatusMsgId, textContent, false);
  7588. } else {
  7589. const statusEl = document.querySelector(`[data-msg-id="${taskMeta.chatStatusMsgId}"]`);
  7590. if (statusEl) statusEl.remove();
  7591. safeRenderResponse(textContent, 'assistant');
  7592. }
  7593.  
  7594. Store.removeItem(placeholderIds[0]);
  7595. const updatedAt = Date.now();
  7596. const item = commitTextSession([
  7597. ...baseMessages,
  7598. { role: 'user', text: Pipeline.normalizePrompt(typeof userPrompt === 'string' ? userPrompt : JSON.stringify(userPrompt)), timestamp: updatedAt - 1 },
  7599. { role: 'assistant', text: textContent, timestamp: updatedAt }
  7600. ], { model: taskMeta.textModel || settings.textModel, updatedAt });
  7601. await commitItemsToHistory([item], [item.id]);
  7602. success = true;
  7603. if (actionType === 'gen_text' && STATE.action === 'gen_text') scrollTextChatToBottom();
  7604. } else {
  7605. const reqId = resultData.request_id; if (!reqId) throw new Error('No Request ID returned');
  7606. const pItem = STATE.items.get(placeholderIds[0]);
  7607. if (pItem) {
  7608. pItem.reqId = reqId;
  7609. pItem.prompt = Pipeline.normalizePrompt(payload.prompt);
  7610. LazyTextStore.set(pItem.id, { prompt: pItem.prompt, responseText: '', textChatMessages: null });
  7611. flushCurrentPanelSessionSave();
  7612. }
  7613. const videoUrl = await pollVideoTask(reqId, taskMeta.controller.signal, runtime, token, updateTask);
  7614. if (videoUrl && runtime.generationToken === token) {
  7615. updateTask({ shortStatus: 'Saving video...', longStatus: 'Downloading full video...', color: '#3b82f6', progress: 95 });
  7616. const videoBlob = await fetchBlobCSP(videoUrl, { signal: taskMeta.controller.signal, timeout: 30000 });
  7617. await OPFS.write(placeholderIds[0], 'full', videoBlob);
  7618.  
  7619. updateTask({ shortStatus: 'Extracting poster...', longStatus: 'Extracting poster image...', color: '#3b82f6', progress: 100 });
  7620. using localVideoUrl = new DisposableURL(videoBlob);
  7621. let posterData = await extractVideoPosterData(localVideoUrl.url, pItem);
  7622.  
  7623. if (posterData?.thumbBlob) await OPFS.write(placeholderIds[0], 'thumb', posterData.thumbBlob);
  7624.  
  7625. finalizePendingVideoItem(pItem, videoUrl, posterData);
  7626. pItem.isNewlyGenerated = true;
  7627. DBQueue.add(pItem);
  7628. scheduleCurrentPanelSessionSave();
  7629.  
  7630. const card = CardRegistry.get('current', pItem.id);
  7631. if (card) await refreshFinalizedVideoCard(card, pItem);
  7632.  
  7633. await commitItemsToHistory([pItem], [pItem.id]);
  7634. scheduleAutoDownloads([pItem]);
  7635. success = true;
  7636. }
  7637. }
  7638. if (success) {
  7639. runtime.successCount++;
  7640. if (runtime.successCount >= 3) {
  7641. runtime.successCount = 0;
  7642. nuclearWipe();
  7643. }
  7644. }
  7645. } catch (err) {
  7646. if (err.name === 'AbortError' || runtime.generationToken !== token) {
  7647. taskMeta.shortStatus = 'Cancelled'; taskMeta.longStatus = 'Cancelled';
  7648. updateTask({ shortStatus: 'Cancelled', longStatus: 'Cancelled', color: 'var(--danger)' });
  7649. } else {
  7650. const errMsg = getErrorMessage(err);
  7651. if (actionType === 'gen_text' && taskMeta.chatStatusMsgId) {
  7652. if (taskMeta.chatHasStreamText && taskMeta.streamText) patchMessage(taskMeta.chatStatusMsgNode || taskMeta.chatStatusMsgId, `${taskMeta.streamText}\n\n[Failed: ${errMsg}]`, false);
  7653. else patchMessage(taskMeta.chatStatusMsgNode || taskMeta.chatStatusMsgId, `Failed: ${errMsg}`, false);
  7654. if (storagePrompt) {
  7655. const failText = taskMeta.chatHasStreamText && taskMeta.streamText ? `${taskMeta.streamText}\n\n[Failed: ${errMsg}]` : `Failed: ${errMsg}`;
  7656. const updatedAt = Date.now();
  7657. const baseMessages = sanitizeTextConversationStore(taskMeta.textChatSnapshot || []);
  7658. const item = commitTextSession([
  7659. ...baseMessages,
  7660. { role: 'user', text: storagePrompt, timestamp: updatedAt - 1 },
  7661. { role: 'assistant', text: failText, timestamp: updatedAt }
  7662. ], { model: taskMeta.textModel || settings.textModel, updatedAt });
  7663. void commitItemsToHistory([item], [item.id]);
  7664. }
  7665. }
  7666. if (err.moderation) {
  7667. taskMeta.shortStatus = 'Blocked';
  7668. taskMeta.longStatus = `Moderation Blocked (${errMsg})`;
  7669. updateTask({ shortStatus: taskMeta.shortStatus, longStatus: taskMeta.longStatus, color: '#a1a1aa' });
  7670. addLog('WARN', `Task Blocked: ${errMsg}`, err);
  7671. failureCleanupDelayMs = 1200;
  7672. } else if (err.invalidRequest) {
  7673. taskMeta.shortStatus = 'Invalid';
  7674. taskMeta.longStatus = `Invalid Request (${errMsg})`;
  7675. updateTask({ shortStatus: taskMeta.shortStatus, longStatus: taskMeta.longStatus, color: '#f59e0b' });
  7676. addLog('WARN', `Task Invalid: ${errMsg}`, err);
  7677. failureCleanupDelayMs = 1200;
  7678. } else {
  7679. taskMeta.shortStatus = `Failed (${errMsg.substring(0, 12)})`;
  7680. taskMeta.longStatus = `Failed (${errMsg})`;
  7681. updateTask({ shortStatus: taskMeta.shortStatus, longStatus: taskMeta.longStatus, color: 'var(--danger)' });
  7682. addLog('ERROR', `Task Failed: ${errMsg}`, err);
  7683. failureCleanupDelayMs = 1200;
  7684. }
  7685. }
  7686. break;
  7687. }
  7688. }
  7689. } finally {
  7690. clearTimeout(progressTimer);
  7691. if (!success) {
  7692. if (failureCleanupDelayMs > 0) await sleep(failureCleanupDelayMs, taskMeta.controller.signal).catch(() => {});
  7693. removeCurrentPanelItems(placeholderIds, { purgeStorage: true });
  7694. }
  7695. }
  7696. }
  7697.  
  7698. function spawnTask(runtime, jobKey, refIds, isMulti, token, taskContext = {}) {
  7699. let pObj = runtime.progress.get(jobKey);
  7700. if (pObj) pObj.active++;
  7701. const tId = `task-${Date.now()}-${Math.random()}`;
  7702. runtime.tasks.set(tId, { controller: new AbortController(), refIds, shortStatus: 'Starting...', longStatus: 'Starting...', color: '#fff', isMulti, progress: 0, lane: runtime.lane, createdAt: Date.now(), ...taskContext });
  7703. updateStatusUI();
  7704. let tProm = executeSingleTask(runtime, tId, refIds, isMulti, token).finally(() => {
  7705. if (pObj) { pObj.active--; pObj.done++; }
  7706. runtime.tasks.delete(tId);
  7707. runtime.taskPromises.delete(tProm);
  7708. updateStatusUI();
  7709. });
  7710. runtime.taskPromises.add(tProm);
  7711. }
  7712.  
  7713. async function processQueue(runtime, token, taskContext = {}) {
  7714. const generationAction = sanitizePanelKey(taskContext.action, runtime.activeAction || taskContext.action || STATE.action);
  7715. runtime.successCount = 0;
  7716. runtime.activeAction = generationAction;
  7717. const getActiveCount = () => runtime.tasks.size;
  7718. let endedNaturally = false;
  7719. while (runtime.isGenerating && runtime.generationToken === token) {
  7720. const isTextOrAudio = runtime.lane !== 'media';
  7721. const limit = isTextOrAudio ? 1 : (parseInt(taskContext.settingsSnapshot?.concurrentSessions ?? STATE.concurrentSessions) || 1);
  7722. const liveLoops = isTextOrAudio ? 1 : ((parseInt(taskContext.settingsSnapshot?.loops ?? getLoopsValue(generationAction)) || 0) + 1);
  7723. const effectiveRefMode = isTextOrAudio ? 'multi' : normalizeRefModeForAction(generationAction, taskContext.settingsSnapshot?.refMode ?? STATE.refMode);
  7724. const refs = getEligibleRefs(generationAction, { refMode: effectiveRefMode }).eligible.filter(m => m.active), now = Date.now(); let spawned = false;
  7725. const hasWaitingRefs = !isTextOrAudio && refs.some(ref => (ref.readyAt || 0) > now);
  7726. const spawnTaskIfAllowed = (jobKey, nextRefIds, nextIsMulti) => {
  7727. if (getActiveCount() >= limit) return false;
  7728. let p = runtime.progress.get(jobKey);
  7729. if (!p) {
  7730. p = { done: 0, active: 0, needed: liveLoops };
  7731. runtime.progress.set(jobKey, p);
  7732. } else p.needed = liveLoops;
  7733. if (p.done + p.active < p.needed) {
  7734. spawnTask(runtime, jobKey, nextRefIds, nextIsMulti, token, taskContext);
  7735. return true;
  7736. }
  7737. return false;
  7738. };
  7739. if (refs.length === 0) { while (spawnTaskIfAllowed('NO_REF', ['NO_REF'], false)) spawned = true; }
  7740. else if (effectiveRefMode === 'queue') {
  7741. const pendingRefs = refs.filter(r => (r.readyAt || 0) <= now).sort((a, b) => {
  7742. const pA = runtime.progress.get(a.id) || { done: 0, active: 0 };
  7743. const pB = runtime.progress.get(b.id) || { done: 0, active: 0 };
  7744. return (pA.done + pA.active) - (pB.done + pB.active);
  7745. });
  7746. for (const ref of pendingRefs) {
  7747. if (!spawnTaskIfAllowed(ref.id, [ref.id], false)) break;
  7748. spawned = true;
  7749. }
  7750. } else if (!hasWaitingRefs) {
  7751. while (spawnTaskIfAllowed('MULTI', refs.map(r => r.id), true)) spawned = true;
  7752. }
  7753. const isDone = runtime.progress.entries().every(([key, progress]) => {
  7754. if (key === 'MULTI' || key === 'NO_REF' || refs.some(ref => ref.id === key)) return progress.done >= progress.needed;
  7755. return true;
  7756. });
  7757. if (isDone && !hasWaitingRefs && getActiveCount() === 0) { endedNaturally = true; break; }
  7758. if (!spawned) {
  7759. const wakeVersion = runtime.queueWake.version;
  7760. const promises = [...runtime.taskPromises, runtime.queueWake.wait(wakeVersion)];
  7761. if (promises.length === 1) promises.push(sleep(1000));
  7762. await Promise.race(promises);
  7763. } else {
  7764. await TaskChunker.yieldIfNeeded();
  7765. }
  7766. }
  7767. if (runtime.generationToken === token) {
  7768. if (endedNaturally) TitleState.markDone();
  7769. runtime.progress.clear();
  7770. setRuntimeField(runtime, 'isGenerating', false);
  7771. runtime.activeAction = null;
  7772. updateStatusUI(true);
  7773. }
  7774. }
  7775.  
  7776. let _statusRenderHandle = 0;
  7777. let _lastStatusUpdateTime = 0;
  7778. let _statusDeferredTimer = 0;
  7779. let _statusViewCacheKey = '';
  7780. const _cleanupGlobalTimers = () => {
  7781. clearTimeout(_statusDeferredTimer); _statusDeferredTimer = 0;
  7782. cancelAnimationFrame(_statusRenderHandle); _statusRenderHandle = 0;
  7783. cancelAnimationFrame(_chatScrollRaf); _chatScrollRaf = 0;
  7784. cancelAnimationFrame(refReorderRaf); refReorderRaf = 0;
  7785. };
  7786. const UI_SCHEDULER = {
  7787. _writeTask: null,
  7788. write(fn) {
  7789. if (this._writeTask) return;
  7790. this._writeTask = requestAnimationFrame(() => {
  7791. fn();
  7792. this._writeTask = null;
  7793. });
  7794. }
  7795. };
  7796. const createStatusShell = () => {
  7797. const row = document.createElement('div');
  7798. row.className = 'w-100 d-flex justify-end';
  7799. return row;
  7800. };
  7801. const GENERATION_LANE_SHORT_LABEL = {
  7802. media: 'M',
  7803. audio: 'A',
  7804. text: 'T'
  7805. };
  7806. const getTaskLaneLabel = (task) => {
  7807. if (task.lane === 'media') return (task.action && task.action.includes('video')) ? 'MV' : 'MI';
  7808. return GENERATION_LANE_SHORT_LABEL[task.lane] || task.lane || 'Task';
  7809. };
  7810.  
  7811. const getCompactTaskStatus = (task) => {
  7812. let progress = Number(task?.progress);
  7813.  
  7814. if (task?.action === 'gen_image' && (progress === 0 || Number.isNaN(progress)) && task.reqStartTime && task.expectedMs) {
  7815. const elapsed = Date.now() - task.reqStartTime;
  7816. progress = Math.min(95, (elapsed / task.expectedMs) * 95);
  7817. }
  7818.  
  7819. if (Number.isFinite(progress) && progress > 0 && progress < 100) return `${Math.round(progress)}%`;
  7820. const short = String(task?.shortStatus || task?.longStatus || '').trim();
  7821. if (!short) return '...';
  7822. const compact = short.replace(/\s+/g, '');
  7823. const statusCode = short.match(/\((\d{3})\)/)?.[1] || '';
  7824. const retryNo = short.match(/\[(\d+)\]/)?.[1] || '';
  7825. const retryPair = short.match(/\[(\d+)\/(\d+)\]/);
  7826. const seconds = short.match(/(\d+)s/i)?.[1] || '';
  7827. if (retryPair) return `P${retryPair[1]}/${retryPair[2]}`;
  7828. if (/^starting/i.test(short)) return 'In1';
  7829. if (/^req/i.test(short)) return 'Req';
  7830. if (/^saving/i.test(short)) return 'Sav';
  7831. if (/extracting/i.test(short)) return 'Pst';
  7832. if (/^cancel/i.test(short)) return 'Stop';
  7833. if (/^blocked/i.test(short)) return statusCode ? `B${statusCode}` : 'Blk';
  7834. if (/bad req|^invalid/i.test(short)) return statusCode ? `E${statusCode}` : 'Bad';
  7835. if (/^failed/i.test(short)) return retryNo ? `F${retryNo}` : 'Fail';
  7836. if (/^busy/i.test(short)) return `${statusCode ? `B${statusCode}` : 'Busy'}${retryNo ? `/${retryNo}` : ''}`;
  7837. if (/^auth/i.test(short)) return `${statusCode ? `A${statusCode}` : 'Auth'}${retryNo ? `/${retryNo}` : ''}`;
  7838. if (/^limit|^lmt/i.test(short)) {
  7839. if (seconds) return `L${seconds}s`;
  7840. return `${statusCode ? `L${statusCode}` : 'Lmt'}${retryNo ? `/${retryNo}` : ''}`;
  7841. }
  7842. if (/^wait/i.test(short)) return `${seconds ? `W${seconds}s` : 'Wait'}${retryNo ? `/${retryNo}` : ''}`;
  7843. return compact.slice(0, 4);
  7844. };
  7845. const createStatusSummaryText = ({ doneT, totalT }) => `${Math.max(0, totalT - doneT)}`;
  7846. const createStatusSummaryKey = ({ taskList, doneT, totalT, activeT, showTaskList }) =>[
  7847. doneT,
  7848. totalT,
  7849. activeT,
  7850. showTaskList ? 1 : 0,
  7851. ...(showTaskList ? taskList.map(task => `${task.lane || ''}|${task.color || ''}|${getCompactTaskStatus(task)}|${Math.round(Number(task?.progress) || 0)}`) : [])
  7852. ].join('\n');
  7853. const createTaskSummaryChip = (task) => {
  7854. const chip = document.createElement('span');
  7855. chip.style.padding = '0 4px';
  7856. chip.style.borderLeft = '1px solid rgba(255,255,255,0.1)';
  7857. chip.style.whiteSpace = 'nowrap';
  7858. if (task.color) chip.style.color = task.color;
  7859. chip.textContent = `${getTaskLaneLabel(task)} ${getCompactTaskStatus(task)}`;
  7860. return chip;
  7861. };
  7862. const StatusSingleton = { ready: null, summary: null, summaryText: null, list: null };
  7863. const buildStatusReadyView = () => {
  7864. if (!StatusSingleton.ready) {
  7865. const row = createStatusShell();
  7866. const text = document.createElement('span');
  7867. text.className = 'text-muted'; text.textContent = 'Ready';
  7868. row.appendChild(text);
  7869. StatusSingleton.ready = row;
  7870. }
  7871. return { key: 'ready', node: StatusSingleton.ready };
  7872. };
  7873. const buildStatusSummaryView = ({ taskList, doneT, totalT, activeT, showTaskList }) => {
  7874. const summaryText = createStatusSummaryText({ doneT, totalT, activeT, taskList });
  7875. const key = createStatusSummaryKey({ taskList, doneT, totalT, activeT, showTaskList });
  7876. if (!StatusSingleton.summary) {
  7877. StatusSingleton.summary = createStatusShell();
  7878. StatusSingleton.list = document.createElement('div');
  7879. StatusSingleton.list.className = 'd-flex justify-end';
  7880. StatusSingleton.list.style.cssText = 'flex:1; overflow:hidden; mask-image:linear-gradient(to right, transparent, black 15px);';
  7881. StatusSingleton.summaryText = document.createElement('span');
  7882. StatusSingleton.summaryText.className = 'text-success';
  7883. StatusSingleton.summaryText.style.cssText = 'font-weight:600; white-space:nowrap; flex-shrink:0;';
  7884. StatusSingleton.summaryText.title = 'Tasks across all runtimes: remaining (active)';
  7885. StatusSingleton.summary.appendChild(StatusSingleton.list);
  7886. StatusSingleton.summary.appendChild(StatusSingleton.summaryText);
  7887. }
  7888. const row = StatusSingleton.summary;
  7889. const tasksSig = showTaskList ? taskList.map(t => `${t.lane}|${t.color}|${getCompactTaskStatus(t)}`).join(',') : '';
  7890. return {
  7891. key, node: row,
  7892. update: () => {
  7893. row.classList.toggle('gap-2', showTaskList);
  7894. if (StatusSingleton.summaryText.textContent !== summaryText) StatusSingleton.summaryText.textContent = summaryText;
  7895. if (StatusSingleton.list._lastSig !== tasksSig) {
  7896. StatusSingleton.list._lastSig = tasksSig;
  7897. if (showTaskList) {
  7898. StatusSingleton.list.style.display = 'flex';
  7899. StatusSingleton.list.replaceChildren(...taskList.map(createTaskSummaryChip));
  7900. } else StatusSingleton.list.style.display = 'none';
  7901. }
  7902. }
  7903. };
  7904. };
  7905. const setStatusView = (view) => {
  7906. if (!DOM.status || !view || _statusViewCacheKey === view.key) return;
  7907. _statusViewCacheKey = view.key;
  7908. UI_SCHEDULER.write(() => {
  7909. if (DOM.status.firstChild !== view.node) DOM.status.replaceChildren(view.node);
  7910. if (view.update) view.update();
  7911. });
  7912. };
  7913. const setDocumentTitle = (title) => {
  7914. if (document.title !== title) document.title = title;
  7915. };
  7916. const getQueuePlanState = (action = STATE.action, { refMode = STATE.refMode, loops = getLoopsValue(action) } = {}) => {
  7917. const effectiveRefMode = normalizeRefModeForAction(action, refMode);
  7918. const refs = Store.getRefItems(action, effectiveRefMode);
  7919. const now = Date.now();
  7920. const { eligible, acceptedType } = getEligibleRefs(action, { refMode: effectiveRefMode });
  7921. const eligibleIds = new Set(eligible.map(e => e.id));
  7922. const isTextOrAudio = action === 'gen_text' || action === 'gen_audio';
  7923. const liveLoops = isTextOrAudio ? 1 : ((parseInt(loops) || 0) + 1);
  7924. const plannedJobs = eligible.length === 0 ? liveLoops : (effectiveRefMode === 'queue' ? eligible.length * liveLoops : liveLoops);
  7925. return { refs, now, eligible, eligibleIds, acceptedType, liveLoops, plannedJobs, effectiveRefMode };
  7926. };
  7927. const pruneJobProgress = (runtime, eligibleIds, taskList = []) => {
  7928. const valid = new Set(['MULTI', 'NO_REF', ...eligibleIds]);
  7929. taskList.forEach(task => {
  7930. if (task.isMulti) valid.add('MULTI');
  7931. else if (task.refIds[0] === 'NO_REF') valid.add('NO_REF');
  7932. task.refIds.forEach(refId => valid.add(String(refId)));
  7933. });
  7934. runtime.progress.keys().forEach(key => {
  7935. if (!valid.has(String(key))) runtime.progress.delete(key);
  7936. });
  7937. };
  7938. const getProgressKey = (refId, refMode = STATE.refMode) => refMode === 'queue' ? refId : 'MULTI';
  7939. const clearReferenceCardRuntime = (card) => {
  7940. card?.setStatus?.([]);
  7941. card?.setProgress?.(null);
  7942. };
  7943. const getReferenceLabelState = (ref, idx, acceptedType, eligibleIds, progress, now, { isGenerating = false, refMode = STATE.refMode, action = STATE.action } = {}) => {
  7944. const baseLabel = `IMAGE_${Math.max(0, idx)}`;
  7945. if (acceptedType === 'none') return { text: baseLabel, title: 'This mode does not use references' };
  7946. if (isGenerating && ref.active && eligibleIds.has(ref.id)) {
  7947. if ((ref.readyAt || 0) > now) return { text: `${baseLabel} (Wait)` };
  7948. if (progress) return { text: `${baseLabel} (${progress.done}/${progress.needed})` };
  7949. return { text: baseLabel };
  7950. }
  7951. if (eligibleIds.has(ref.id) && ref.active) {
  7952. const apiLimit = (ACTION_SPEC[action] ?? ACTION_SPEC.gen_image).apiLimit ?? 99;
  7953. const withinApiLimit = refMode !== 'multi' || idx < apiLimit;
  7954.  
  7955. if (refMode === 'multi' && withinApiLimit) {
  7956. return {
  7957. text: `<IMAGE_${idx}>`,
  7958. title: 'Insert tag',
  7959. onClick: (e) => {
  7960. e.preventDefault();
  7961. e.stopPropagation();
  7962. insertTextAtCursor(DOM.prompt, ` <IMAGE_${idx}> `);
  7963. }
  7964. };
  7965. }
  7966. return { text: baseLabel, title: withinApiLimit ? 'Active Reference' : 'API Limit Exceeded (Ignored)' };
  7967. }
  7968. return { text: baseLabel, title: ref.type !== acceptedType ? `Need ${acceptedType}` : 'Limit Reached' };
  7969. };
  7970. const getRefWarmupState = (ref, now = Date.now()) => {
  7971. const readyAt = Number(ref?.readyAt) || 0;
  7972. if (readyAt <= now) return null;
  7973. const totalMs = 10000;
  7974. const remainingMs = readyAt - now;
  7975. const elapsedMs = Math.max(0, totalMs - Math.min(totalMs, remainingMs));
  7976. return { remainingMs, progress: Math.min(100, Math.max(0, (elapsedMs / totalMs) * 100)) };
  7977. };
  7978. const getReferenceStatusLines = (ref, tasks, progress, now) => {
  7979. if (!ref.active) return [];
  7980. const warmup = getRefWarmupState(ref, now);
  7981. if (!progress && !warmup && tasks.length === 0) return [];
  7982. const lines = [];
  7983. if (warmup) lines.push({ text: `Warmup ${Math.max(0.1, warmup.remainingMs / 1000).toFixed(1)}s`, color: '#9e9e9e' });
  7984. tasks.forEach((task, index) => lines.push({ text: `[T${index + 1}] ${task.shortStatus}`, color: task.color }));
  7985. return lines;
  7986. };
  7987. const getReferenceStatusAction = () => {
  7988. if (JobManager.getLane(STATE.action) !== 'media') return STATE.action;
  7989. const mediaRuntime = JobManager.getRuntime('media');
  7990. if (mediaRuntime.isGenerating && mediaRuntime.activeAction) return mediaRuntime.activeAction;
  7991. return STATE.action;
  7992. };
  7993. const getRuntimeSummaryCounts = (runtime) => {
  7994. if (runtime.lane === 'media') {
  7995. const action = runtime.activeAction || (JobManager.getLane(STATE.action) === 'media' ? STATE.action : 'gen_image');
  7996. const { eligibleIds, plannedJobs } = getQueuePlanState(action);
  7997. const taskList = [...runtime.tasks.values()];
  7998. pruneJobProgress(runtime, eligibleIds, taskList);
  7999. let queuedT = 0, doneT = 0, totalT = 0;
  8000. for (const [key, p] of runtime.progress.entries()) {
  8001. if (!(key === 'MULTI' || key === 'NO_REF' || eligibleIds.has(key))) continue;
  8002. const activeForKey = Math.max(0, p.active || 0);
  8003. const totalForKey = Math.max(p.needed || 0, (p.done || 0) + activeForKey);
  8004. const doneForKey = Math.min(Math.max(0, p.done || 0), totalForKey);
  8005. queuedT += Math.max(totalForKey - doneForKey - activeForKey, 0);
  8006. doneT += doneForKey;
  8007. totalT += totalForKey;
  8008. }
  8009. totalT = Math.max(totalT, runtime.isGenerating ? plannedJobs : 0, doneT + queuedT + taskList.length);
  8010. return { doneT, totalT, queuedT };
  8011. }
  8012. const activeT = runtime.tasks.size;
  8013. const totalT = runtime.isGenerating ? Math.max(1, activeT) : activeT;
  8014. return { doneT: 0, totalT, queuedT: Math.max(0, totalT - activeT) };
  8015. };
  8016. function seedQueueProgress(runtime, action = STATE.action) {
  8017. runtime.progress.clear();
  8018. if (runtime.lane !== 'media') return;
  8019. const { eligible, liveLoops, effectiveRefMode } = getQueuePlanState(action);
  8020. if (eligible.length === 0) runtime.progress.set('NO_REF', { done: 0, active: 0, needed: liveLoops });
  8021. else if (effectiveRefMode === 'queue') eligible.forEach(ref => runtime.progress.set(ref.id, { done: 0, active: 0, needed: liveLoops }));
  8022. else runtime.progress.set('MULTI', { done: 0, active: 0, needed: liveLoops });
  8023. }
  8024. function updateStatusUI(forceUpdate = false) {
  8025. if (!DOM.status) return;
  8026.  
  8027. const now = performance.now();
  8028. if (!forceUpdate && (now - _lastStatusUpdateTime < 200)) {
  8029. if (!_statusDeferredTimer) {
  8030. _statusDeferredTimer = setTimeout(() => {
  8031. _statusDeferredTimer = 0;
  8032. updateStatusUI(true);
  8033. }, 200 - (now - _lastStatusUpdateTime));
  8034. }
  8035. return;
  8036. }
  8037.  
  8038. if (_statusDeferredTimer) {
  8039. clearTimeout(_statusDeferredTimer);
  8040. _statusDeferredTimer = 0;
  8041. }
  8042. _lastStatusUpdateTime = now;
  8043.  
  8044. if (_statusRenderHandle) return;
  8045. const renderFn = () => {
  8046. _statusRenderHandle = 0;
  8047. if (!DOM.status) return;
  8048.  
  8049. const mediaRuntime = JobManager.getRuntime('media');
  8050. const refStatusAction = getReferenceStatusAction();
  8051. const refStatusPlan = getQueuePlanState(refStatusAction);
  8052. const { refs, now, eligibleIds, acceptedType, effectiveRefMode } = refStatusPlan;
  8053. const refIndexMap = new Map(getRefOrderForView(refStatusAction, effectiveRefMode).map((id, index) => [id, index]));
  8054. const taskList = JobManager.getAllTasks();
  8055. const activeT = taskList.length;
  8056. const mediaTaskList = [...mediaRuntime.tasks.values()];
  8057. pruneJobProgress(mediaRuntime, eligibleIds, mediaTaskList);
  8058. const tasksByRef = Object.groupBy(
  8059. mediaTaskList.flatMap(task => task.refIds.map(rid => ({ rid, task }))),
  8060. ({ rid }) => rid
  8061. );
  8062.  
  8063. let queuedT = 0, done = 0, totalPlanned = 0;
  8064. GenerationRegistry.getAllRuntimes().forEach(runtime => {
  8065. const runtimeCounts = getRuntimeSummaryCounts(runtime);
  8066. queuedT += runtimeCounts.queuedT;
  8067. done += runtimeCounts.doneT;
  8068. totalPlanned += runtimeCounts.totalT;
  8069. });
  8070. totalPlanned = Math.max(totalPlanned, done + activeT + queuedT);
  8071. const doneT = Math.min(done, totalPlanned);
  8072. const totalT = Math.max(totalPlanned, doneT + activeT);
  8073. let activeRefCount = 0;
  8074. const showRefProgress = JobManager.getLane(STATE.action) === 'media';
  8075.  
  8076. let activeTagIndex = 0;
  8077.  
  8078. refs.forEach(r => {
  8079. const card = CardRegistry.get('reference', r.id); if (!card) return;
  8080. if (r.active) activeRefCount++;
  8081. const refTasks = tasksByRef[r.id]?.map(entry => entry.task) ?? [];
  8082. const progress = mediaRuntime.progress.get(getProgressKey(r.id, effectiveRefMode));
  8083. let idx = refIndexMap.get(r.id) ?? -1;
  8084. if (r.active && eligibleIds.has(r.id)) {
  8085. idx = activeTagIndex++;
  8086. }
  8087. const label = getReferenceLabelState(r, idx, acceptedType, eligibleIds, progress, now, { isGenerating: mediaRuntime.isGenerating, refMode: effectiveRefMode, action: refStatusAction });
  8088. const statusLines = showRefProgress ? getReferenceStatusLines(r, refTasks, progress, now) : [];
  8089. const warmup = getRefWarmupState(r, now);
  8090. const isProcessing = showRefProgress && (refTasks.length > 0 || (mediaRuntime.isGenerating && r.active && eligibleIds.has(r.id) && !!warmup));
  8091.  
  8092. if (isProcessing) card._internals.states.add('processing'); else card._internals.states.delete('processing');
  8093. if (label.onClick) card._internals.states.add('active-multi'); else card._internals.states.delete('active-multi');
  8094. card.setLabel(label.text, label.title || '', label.onClick || null);
  8095.  
  8096. if (statusLines.length > 0) card.setStatus(statusLines);
  8097. else clearReferenceCardRuntime(card);
  8098. if (refTasks.length === 0 && warmup) card.setProgress(warmup.progress);
  8099. else if (statusLines.length === 0) card.setProgress(null);
  8100. });
  8101.  
  8102. if (!isAnyRuntimeBusy()) {
  8103. setStatusView(buildStatusReadyView());
  8104. TitleState.sync();
  8105. return;
  8106. }
  8107.  
  8108. setStatusView(buildStatusSummaryView({ taskList, doneT, totalT, activeT, showTaskList: true }));
  8109. TitleState.sync(`${totalPlanned > 0 ? Math.floor((done / totalPlanned) * 100) : 0}%`);
  8110.  
  8111. if (isAnyRuntimeBusy()) {
  8112. if (!DOM.status._busyTimer) {
  8113. DOM.status._busyTimer = setInterval(() => updateStatusUI(), 250);
  8114. }
  8115. } else {
  8116. if (DOM.status._busyTimer) {
  8117. clearInterval(DOM.status._busyTimer);
  8118. DOM.status._busyTimer = null;
  8119. }
  8120. }
  8121. };
  8122.  
  8123. _statusRenderHandle = 1;
  8124. if (document.hidden) {
  8125. scheduler.postTask(renderFn, { priority: 'background', delay: 500 }).catch(() => setTimeout(renderFn, 1000));
  8126. } else {
  8127. _statusRenderHandle = requestAnimationFrame(renderFn);
  8128. }
  8129. }
  8130.  
  8131. function syncWipeButtonState() {
  8132. if (!DOM.reset) return;
  8133. const isBusy = isAnyRuntimeBusy();
  8134. DOM.reset.disabled = isBusy;
  8135. DOM.reset.setAttribute('aria-disabled', String(isBusy));
  8136. DOM.reset.title = isBusy ? 'Wipe is locked while work is active in any mode' : 'Wipe Menu (Cookie / Data / Full)';
  8137. }
  8138.  
  8139. function toggleGenerateButton(isRunning) {
  8140. if (!DOM.generateBtn) return;
  8141. let c = DOM.generateBtn.querySelector('.btn-content');
  8142. if (!c) {
  8143. c = document.createElement('div');
  8144. c.className = 'btn-content flex-center';
  8145. DOM.generateBtn.replaceChildren(c);
  8146. }
  8147. DOM.generateBtn.classList.toggle('xai-btn--stop', isRunning);
  8148. DOM.generateBtn.classList.toggle('xai-btn--run', !isRunning);
  8149. DOM.generateBtn.classList.toggle('is-running', isRunning);
  8150. }
  8151.  
  8152. function handleGeneration() {
  8153. if (STATE.activeRefPromptId) saveAndCloseRefPrompt(true);
  8154. PromptSync.captureEditor(STATE.action, { scheduleCommit: false });
  8155. PromptSync.commitNow(STATE.action);
  8156. const actionType = STATE.action;
  8157. const runtime = JobManager.getRuntime(actionType);
  8158. if (runtime.isGenerating) {
  8159. const pendingIds = getPendingItemIdsForRuntime(runtime);
  8160. runtime.generationToken++;
  8161. runtime.tasks.forEach(task => task.controller.abort());
  8162. runtime.tasks.clear();
  8163. runtime.taskPromises.clear();
  8164. runtime.progress.clear();
  8165. if (pendingIds.length > 0) removeCurrentPanelItems(pendingIds, { purgeStorage: true });
  8166. setRuntimeField(runtime, 'isGenerating', false);
  8167. runtime.activeAction = null;
  8168. return;
  8169. }
  8170. const isTextMode = actionType === 'gen_text';
  8171. const submittedPrompt = isTextMode ? sanitizePromptText(getEditorPromptValue()) : sanitizePromptText(getTaskPrompt(actionType));
  8172. const usableRefs = getEligibleRefs(actionType).eligible;
  8173. const hasCurrentRefPrompt = Store.getRefItems(actionType).some(ref => sanitizePromptText(STATE.refPrompts[ref.id]?.[actionType] || '').length > 0);
  8174. if (!submittedPrompt && !hasCurrentRefPrompt && usableRefs.length === 0) return UI.showToast("Empty prompt", "error");
  8175. 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");
  8176. const aRefs = usableRefs.filter(m => m.active), dur = parseInt(STATE.duration) || 5;
  8177. const effectiveRefMode = normalizeRefModeForAction(actionType, STATE.refMode);
  8178. ((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"));
  8179. const staleTransientIds = STATE.currentOrder.filter(id => Pipeline.isTransientItem(STATE.items.get(id)));
  8180. if (staleTransientIds.length > 0) {
  8181. removeCurrentPanelItems(staleTransientIds, { purgeStorage: false });
  8182. }
  8183. runtime.generationToken++;
  8184. runtime.activeAction = actionType;
  8185. seedQueueProgress(runtime, actionType);
  8186. if (!isTextMode && DOM.currentImages) {
  8187. DOM.currentImages.scrollTop = 0;
  8188. }
  8189. PanelManager.clear();
  8190. if (actionType !== 'gen_text') STATE.currentOrder = [];
  8191. const taskContext = {
  8192. lane: runtime.lane,
  8193. action: actionType,
  8194. panelKey: actionType,
  8195. promptText: submittedPrompt,
  8196. settingsSnapshot: buildActionSettingsSnapshot(actionType),
  8197. textChatSnapshot: actionType === 'gen_text' ? sanitizeTextConversationStore(STATE.textChatMessages || []) : []
  8198. };
  8199. setRuntimeField(runtime, 'isGenerating', true);
  8200. if (isTextMode) clearTextPromptInput({ focus: true });
  8201. processQueue(runtime, runtime.generationToken, taskContext).catch(err => {
  8202. addLog('ERROR', 'Queue Error', err);
  8203. setRuntimeField(runtime, 'isGenerating', false);
  8204. runtime.activeAction = null;
  8205. });
  8206. }
  8207.  
  8208. function initAPIAndGeneration() {
  8209. App.on(DOM.generateBtn, 'click', handleGeneration);
  8210. App.on(DOM.reset, 'click', async () => {
  8211. if (isAnyRuntimeBusy()) return UI.showToast("Stop generation first", "error");
  8212. const choice = await UI.confirm(buildWipeConfirmMarkup());
  8213. if (!choice || choice === 'cancel') return;
  8214. if (choice === 'data') {
  8215. const removedCount = await wipeCardHistoryData();
  8216. return UI.showToast(removedCount > 0 ? `Card history cleared (${removedCount})` : "No card history to clear", "success");
  8217. }
  8218. if (choice === 'full') {
  8219. _skipPagehidePersistence = true;
  8220. if (STATE.activeLightboxId) closeLightbox();
  8221. STATE.items.forEach((_, id) => Store.removeItem(id));
  8222. PromptSync.clear();
  8223. DBQueue.clearPending();
  8224. clearCurrentPanelSession();
  8225. _configSaveTask?.abort();
  8226. _configSaveTask = null;
  8227. await StorageClient.clearAll();
  8228. localStorage.removeItem(APP_CONFIG.STORAGE.CONFIG_KEY);
  8229. localStorage.removeItem(MEDIA_VOLUME_STORAGE_KEY);
  8230. }
  8231. lastWipeTime = 0; wipePromise = null; await nuclearWipe(); location.reload();
  8232. });
  8233. }
  8234.  
  8235. if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', checkRoute); else checkRoute();
  8236. })();
Comments
  • dimadoma
    116 days
    # text 0.10 KB | 0 0
    1. Hey! Used this script and it worked for 2 days but yesterday it stuck again on 95%/// any fix please?
Add Comment
Please, Sign In to add comment