Guest User

xAI Console temp

a guest
Mar 22nd, 2026
331
1
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 95.15 KB | None | 1 0
  1. // ==UserScript==
  2. // @name xAI Console Tools
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0
  5. // @description Safe retry/edit/reset helpers, optional 2K forcing, JPEG download
  6. // @match https://console.x.ai/playground/imagine*
  7. // @match https://console.x.ai/team/*/imagine*
  8. // @grant none
  9. // @run-at document-start
  10. // ==/UserScript==
  11.  
  12. (function () {
  13. 'use strict';
  14.  
  15. if (window.__XAI_TOOLS_SINGLETON__) {
  16. console.warn('[xAI Tools] duplicate instance blocked');
  17. return;
  18. }
  19. window.__XAI_TOOLS_SINGLETON__ = '1.0';
  20.  
  21. const TAG = '[xAI Tools]';
  22. const PANEL_ID = 'xai-tools-panel';
  23. const BTN_2K_ID = 'xai-2k-btn';
  24. const BTN_COUNT_ID = 'xai-count-btn';
  25. const BTN_RETRY_ID = 'xai-retry-btn';
  26. const BTN_RESET_ID = 'xai-reset-btn';
  27. const BTN_LOG_ID = 'xai-log-btn';
  28.  
  29. const GAP_PX = 8;
  30. const JPEG_QUALITY = 0.95;
  31. const FORCE_2K_KEY = 'gc_force2k_v1';
  32. const FORCE_COUNT_KEY = 'gc_force_image_count_v1';
  33. const PANEL_IMAGE_COUNT_VALUES = [1, 2, 4];
  34. const DEFAULT_IMAGE_COUNT = 4;
  35. const IMAGE_COUNT_MIN = 1;
  36. const IMAGE_COUNT_MAX = 10;
  37. const AUTO_REHYDRATE_KEY = 'gc_auto_rehydrate_v1';
  38. const DEBUG_LOG_KEY = '__xt_debug_log_v4';
  39. const SHOW_LOG_BTN_KEY = 'gc_show_log_btn_v1';
  40. const RESTORE_STATE_KEY = '__xt_restore_state_v4';
  41. const RESTORE_DB_NAME = '__xt_restore_db_v4';
  42. const RESTORE_DB_STORE = 'kv';
  43. const RESTORE_IMAGE_KEY = 'restore-image';
  44. const BYPASS_ATTR = 'data-xai-jpeg-bypass';
  45. const DEFAULT_UI_SYNC_KEY = '__xt_default_ui_sync_v1';
  46.  
  47. let postStartRetryInFlight = false;
  48. let postStartRetryJobId = 0;
  49. let countUiSyncInFlight = null;
  50. let countUiStartupSyncDone = false;
  51. let countUiSyncQueuedReason = '';
  52. let startupDefaultSyncTimer = 0;
  53. let lastBannerSignature = '';
  54. let lastBannerSignatureAt = 0;
  55. let pendingManualCountPick = 0;
  56. let pendingManualCountPickedAt = 0;
  57. let highDemandRecoveryInFlight = 0;
  58. let highDemandWatchRaf = 0;
  59. let preferredSubmitCountUntil = 0;
  60. let preferredSubmitCountValue = 0;
  61. let glitchSubmitProxyBusy = false;
  62. let glitchProxyButton = null;
  63. let glitchProxyState = null;
  64.  
  65. function pad2(n) {
  66. return String(n).padStart(2, '0');
  67. }
  68.  
  69. function timeStamp() {
  70. const d = new Date();
  71. return `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
  72. }
  73.  
  74. function fileStamp() {
  75. const d = new Date();
  76. return `${pad2(d.getDate())}.${pad2(d.getMonth() + 1)}.${d.getFullYear()} ${pad2(d.getHours())}-${pad2(d.getMinutes())}-${pad2(d.getSeconds())}`;
  77. }
  78.  
  79. function appendPersistentLog(line) {
  80. try {
  81. const prev = localStorage.getItem(DEBUG_LOG_KEY) || '';
  82. const next = `${prev}${prev ? '\n' : ''}${line}`;
  83. localStorage.setItem(DEBUG_LOG_KEY, next.slice(-50000));
  84. } catch {}
  85. }
  86.  
  87. function getPersistentLog() {
  88. try {
  89. return localStorage.getItem(DEBUG_LOG_KEY) || '';
  90. } catch {
  91. return '';
  92. }
  93. }
  94.  
  95. function flushPersistentLogToConsole() {
  96. const text = getPersistentLog();
  97. if (!text) return;
  98. console.groupCollapsed(`${TAG} persisted debug log`);
  99. console.log(text);
  100. console.groupEnd();
  101. }
  102.  
  103. function log(msg, type = 'log') {
  104. const line = `${TAG} ${timeStamp()} ${msg}`;
  105. const fn = type === 'error' ? console.error : type === 'warn' ? console.warn : console.log;
  106. fn(line);
  107. appendPersistentLog(line);
  108. }
  109.  
  110. function sleep(ms) {
  111. return new Promise(resolve => setTimeout(resolve, ms));
  112. }
  113.  
  114. function isVisible(el) {
  115. if (!(el instanceof Element)) return false;
  116. const style = getComputedStyle(el);
  117. if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
  118. const rect = el.getBoundingClientRect();
  119. return rect.width > 0 && rect.height > 0;
  120. }
  121.  
  122. function getText(el) {
  123. if (!(el instanceof Element)) return '';
  124. return [el.textContent || '', el.getAttribute('aria-label') || '', el.getAttribute('title') || '']
  125. .join(' ')
  126. .replace(/\s+/g, ' ')
  127. .trim()
  128. .toLowerCase();
  129. }
  130.  
  131. function isForce2kEnabled() {
  132. try {
  133. return localStorage.getItem(FORCE_2K_KEY) === '1';
  134. } catch {
  135. return false;
  136. }
  137. }
  138.  
  139. function setForce2kEnabled(enabled) {
  140. try {
  141. localStorage.setItem(FORCE_2K_KEY, enabled ? '1' : '0');
  142. } catch {}
  143. }
  144.  
  145. function sanitizeImageCount(value, fallback = 1) {
  146. const num = Math.round(Number(value));
  147. if (!Number.isFinite(num)) return fallback;
  148. if (num < IMAGE_COUNT_MIN || num > IMAGE_COUNT_MAX) return fallback;
  149. return num;
  150. }
  151.  
  152. function getForcedImageCount() {
  153. try {
  154. const raw = Number(localStorage.getItem(FORCE_COUNT_KEY) || String(DEFAULT_IMAGE_COUNT));
  155. return sanitizeImageCount(raw, DEFAULT_IMAGE_COUNT);
  156. } catch {
  157. return DEFAULT_IMAGE_COUNT;
  158. }
  159. }
  160.  
  161. function setForcedImageCount(value) {
  162. const next = sanitizeImageCount(value, DEFAULT_IMAGE_COUNT);
  163. try {
  164. localStorage.setItem(FORCE_COUNT_KEY, String(next));
  165. } catch {}
  166. return next;
  167. }
  168.  
  169. function syncStoredImageCount(value, reason = 'ui') {
  170. const next = sanitizeImageCount(value, 0);
  171. if (!next) return false;
  172. const prev = getForcedImageCount();
  173. if (prev !== next) {
  174. setForcedImageCount(next);
  175. log(`image count observed from ${reason} -> x${next}`, 'warn');
  176. }
  177. updateCountButton();
  178. positionPanel();
  179. return true;
  180. }
  181.  
  182. function cycleForcedImageCount() {
  183. const current = getForcedImageCount();
  184. const index = PANEL_IMAGE_COUNT_VALUES.indexOf(current);
  185. const next = PANEL_IMAGE_COUNT_VALUES[(index >= 0 ? index + 1 : 0) % PANEL_IMAGE_COUNT_VALUES.length];
  186. setForcedImageCount(next);
  187. return next;
  188. }
  189.  
  190.  
  191. function preferStoredImageCount(value, ttlMs = 12000, reason = 'panel') {
  192. preferredSubmitCountValue = sanitizeImageCount(value, getForcedImageCount());
  193. preferredSubmitCountUntil = Date.now() + Math.max(1000, Math.round(Number(ttlMs) || 0));
  194. log(`count preference locked to x${preferredSubmitCountValue} (${reason})`, 'warn');
  195. }
  196.  
  197. function hasPreferredStoredImageCount() {
  198. return Date.now() < preferredSubmitCountUntil && preferredSubmitCountValue === getForcedImageCount();
  199. }
  200.  
  201. function clearPreferredStoredImageCount() {
  202. preferredSubmitCountUntil = 0;
  203. preferredSubmitCountValue = 0;
  204. }
  205.  
  206. function normText(value) {
  207. return String(value || '').replace(/\s+/g, ' ').trim();
  208. }
  209.  
  210. function exactCountFromText(value) {
  211. const text = normText(value);
  212. if (!/^(10|[1-9])$/.test(text)) return null;
  213. const num = Number(text);
  214. return Number.isFinite(num) ? num : null;
  215. }
  216.  
  217. function rememberManualCountPick(value, reason = 'ui-manual') {
  218. const next = sanitizeImageCount(value, 0);
  219. if (!next) return false;
  220. pendingManualCountPick = next;
  221. pendingManualCountPickedAt = Date.now();
  222. clearPreferredStoredImageCount();
  223. syncStoredImageCount(next, reason);
  224. return true;
  225. }
  226.  
  227. async function waitForValue(factory, timeoutMs = 1400, intervalMs = 80) {
  228. const started = Date.now();
  229. while (Date.now() - started < timeoutMs) {
  230. const value = factory();
  231. if (value) return value;
  232. await sleep(intervalMs);
  233. }
  234. return null;
  235. }
  236.  
  237. function clickElement(el) {
  238. if (!(el instanceof HTMLElement)) return false;
  239. try { if (typeof el.focus === 'function') el.focus(); } catch {}
  240. try { el.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, cancelable: true, pointerType: 'mouse', isPrimary: true, button: 0, buttons: 1 })); } catch {}
  241. try { el.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, button: 0, buttons: 1, view: window })); } catch {}
  242. try { el.dispatchEvent(new PointerEvent('pointerup', { bubbles: true, cancelable: true, pointerType: 'mouse', isPrimary: true, button: 0, buttons: 0 })); } catch {}
  243. try { el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, button: 0, buttons: 0, view: window })); } catch {}
  244. try { el.click(); return true; } catch { return false; }
  245. }
  246.  
  247. function sendOpenKeys(el) {
  248. if (!(el instanceof HTMLElement)) return false;
  249. try { if (typeof el.focus === 'function') el.focus(); } catch {}
  250. const events = [
  251. new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', bubbles: true, cancelable: true }),
  252. new KeyboardEvent('keyup', { key: 'Enter', code: 'Enter', bubbles: true, cancelable: true }),
  253. new KeyboardEvent('keydown', { key: ' ', code: 'Space', bubbles: true, cancelable: true }),
  254. new KeyboardEvent('keyup', { key: ' ', code: 'Space', bubbles: true, cancelable: true })
  255. ];
  256. let fired = false;
  257. for (const evt of events) {
  258. try { el.dispatchEvent(evt); fired = true; } catch {}
  259. }
  260. return fired;
  261. }
  262.  
  263. async function tryOpenImageSettingsMenu(imageBtn, reason = 'manual') {
  264. if (!(imageBtn instanceof HTMLElement)) return null;
  265. let menu = findOpenImageSettingsMenu(imageBtn);
  266. if (menu) return menu;
  267.  
  268. for (let pass = 1; pass <= 4; pass++) {
  269. if (pass > 1) await closeImageMenus(imageBtn).catch(() => {});
  270. clickElement(imageBtn);
  271. menu = await waitForValue(() => findOpenImageSettingsMenu(imageBtn), 700, 70);
  272. if (menu) return menu;
  273.  
  274. sendOpenKeys(imageBtn);
  275. menu = await waitForValue(() => findOpenImageSettingsMenu(imageBtn), 700, 70);
  276. if (menu) return menu;
  277.  
  278. try { imageBtn.click(); } catch {}
  279. menu = await waitForValue(() => findOpenImageSettingsMenu(imageBtn), 700, 70);
  280. if (menu) return menu;
  281. }
  282.  
  283. log(`count UI sync failed (${reason}): Image menu did not open`, 'error');
  284. return null;
  285. }
  286.  
  287. function getPopupForTrigger(trigger, roles = []) {
  288. if (!(trigger instanceof HTMLElement)) return null;
  289. const popupId = trigger.getAttribute('aria-controls') || '';
  290. if (popupId) {
  291. const popup = document.getElementById(popupId);
  292. if (popup instanceof HTMLElement && isVisible(popup)) {
  293. if (!roles.length || roles.includes((popup.getAttribute('role') || '').toLowerCase())) return popup;
  294. }
  295. }
  296. return null;
  297. }
  298.  
  299. function clickOutsideForMenuDismiss() {
  300. const textarea = getPromptTextarea();
  301. const target = textarea || document.body;
  302. if (!(target instanceof HTMLElement)) return false;
  303. try { if (typeof target.focus === 'function') target.focus(); } catch {}
  304. try { target.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, cancelable: true, pointerType: 'mouse', isPrimary: true, button: 0, buttons: 1 })); } catch {}
  305. try { target.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, button: 0, buttons: 1, view: window })); } catch {}
  306. try { target.dispatchEvent(new PointerEvent('pointerup', { bubbles: true, cancelable: true, pointerType: 'mouse', isPrimary: true, button: 0, buttons: 0 })); } catch {}
  307. try { target.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, button: 0, buttons: 0, view: window })); } catch {}
  308. try { target.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, button: 0, buttons: 0, view: window })); } catch {}
  309. return true;
  310. }
  311.  
  312. function findOpenImageSettingsMenu(imageBtn = null) {
  313. const byTrigger = getPopupForTrigger(imageBtn, ['menu']);
  314. if (byTrigger) return byTrigger;
  315.  
  316. const menus = Array.from(document.querySelectorAll('[role="menu"], [data-radix-menu-content]')).filter(el => {
  317. if (!(el instanceof HTMLElement)) return false;
  318. if (!isVisible(el)) return false;
  319. const text = normText(el.textContent);
  320. return /number of images/i.test(text) && (/(image resolution|aspect ratio|model)/i.test(text));
  321. });
  322. return menus[menus.length - 1] || null;
  323. }
  324.  
  325. function findImageCountCombobox(menu) {
  326. if (!(menu instanceof Element)) return null;
  327. const buttons = Array.from(menu.querySelectorAll('button[role="combobox"], button'));
  328. return buttons.find(el => /number of images/i.test(normText(el.textContent))) || null;
  329. }
  330.  
  331. function getDisplayedUiCountFromCombobox(btn) {
  332. if (!(btn instanceof Element)) return null;
  333. const matches = normText(btn.textContent).match(/\b(10|[1-9])\b/g);
  334. if (!matches || !matches.length) return null;
  335. const value = Number(matches[matches.length - 1]);
  336. return Number.isFinite(value) ? value : null;
  337. }
  338.  
  339. function findOpenCountListboxGeneric() {
  340. const listboxes = Array.from(document.querySelectorAll('[role="listbox"]')).filter(el => {
  341. if (!(el instanceof HTMLElement)) return false;
  342. if (!isVisible(el)) return false;
  343. const numericValues = new Set();
  344. for (const node of Array.from(el.querySelectorAll('*'))) {
  345. if (!(node instanceof HTMLElement)) continue;
  346. const value = exactCountFromText(node.textContent);
  347. if (value) numericValues.add(value);
  348. }
  349. return numericValues.size >= 3;
  350. });
  351. return listboxes[listboxes.length - 1] || null;
  352. }
  353.  
  354. function collectCountOptionCandidates(listbox, value) {
  355. if (!(listbox instanceof Element)) return [];
  356. const targetText = String(value);
  357. const nodes = Array.from(listbox.querySelectorAll('*')).filter(el => {
  358. if (!(el instanceof HTMLElement)) return false;
  359. if (!isVisible(el)) return false;
  360. return normText(el.textContent) === targetText;
  361. });
  362. const unique = [];
  363. const seen = new Set();
  364. const push = (el) => {
  365. if (!(el instanceof HTMLElement)) return;
  366. if (el === listbox) return;
  367. if (!listbox.contains(el)) return;
  368. if (!isVisible(el)) return;
  369. if (seen.has(el)) return;
  370. seen.add(el);
  371. unique.push(el);
  372. };
  373.  
  374. for (const node of nodes) {
  375. push(node.closest('[role="option"], [role="menuitemradio"], [data-radix-collection-item]'));
  376.  
  377. let cur = node;
  378. while (cur && cur !== listbox) {
  379. if (cur instanceof HTMLElement && normText(cur.textContent) === targetText) push(cur);
  380. cur = cur.parentElement;
  381. }
  382. }
  383.  
  384. unique.sort((a, b) => {
  385. const score = (el) => {
  386. const rect = el.getBoundingClientRect();
  387. let s = (rect.width || 0) * (rect.height || 0);
  388. if (el.getAttribute('role') === 'option') s += 1000000;
  389. if (el.hasAttribute('data-radix-collection-item')) s += 500000;
  390. return s;
  391. };
  392. return score(b) - score(a);
  393. });
  394.  
  395. return unique;
  396. }
  397.  
  398. async function chooseCountOptionFromListbox(listbox, value, countBtn) {
  399. const candidates = collectCountOptionCandidates(listbox, value);
  400. if (!candidates.length) {
  401. log(`count UI sync: option x${value} not found in listbox`, 'error');
  402. return false;
  403. }
  404.  
  405. for (const candidate of candidates) {
  406. const label = candidate.getAttribute('role') || candidate.tagName.toLowerCase();
  407. clickElement(candidate);
  408. await sleep(140);
  409.  
  410. const menuNow = findOpenImageSettingsMenu();
  411. const countBtnNow = findImageCountCombobox(menuNow) || countBtn;
  412. const uiValue = getDisplayedUiCountFromCombobox(countBtnNow);
  413. const listboxStillOpen = document.contains(listbox) && isVisible(listbox);
  414.  
  415. if (uiValue === value) {
  416. log(`count UI sync: option x${value} chosen via ${label}`, 'warn');
  417. return true;
  418. }
  419. if (!listboxStillOpen) {
  420. log(`count UI sync: listbox closed after clicking ${label}`, 'warn');
  421. return true;
  422. }
  423. }
  424.  
  425. return false;
  426. }
  427.  
  428. function sendEscape(target = document.activeElement || document.body) {
  429. const host = target instanceof EventTarget ? target : document;
  430. try { host.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', code: 'Escape', bubbles: true })); } catch {}
  431. try { host.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape', code: 'Escape', bubbles: true })); } catch {}
  432. }
  433.  
  434. async function closeImageMenus(imageBtn) {
  435. const btn = imageBtn || findToolbarAnchor();
  436. for (let pass = 1; pass <= 6; pass++) {
  437. const menuOpen = !!findOpenImageSettingsMenu(btn);
  438. const listboxOpen = !!findOpenCountListboxGeneric();
  439. if (!menuOpen && !listboxOpen) return true;
  440.  
  441. if (listboxOpen) {
  442. sendEscape(document.activeElement || document.body);
  443. await sleep(80);
  444. }
  445.  
  446. if (btn instanceof HTMLElement && btn.getAttribute('aria-expanded') === 'true') {
  447. clickElement(btn);
  448. await sleep(100);
  449. }
  450.  
  451. if (findOpenImageSettingsMenu(btn) || findOpenCountListboxGeneric()) {
  452. clickOutsideForMenuDismiss();
  453. await sleep(90);
  454. sendEscape(document.activeElement || document.body);
  455. await sleep(90);
  456. }
  457. }
  458. return !findOpenImageSettingsMenu(btn) && !findOpenCountListboxGeneric();
  459. }
  460.  
  461. async function applyForcedImageCountToUi(wantedCount, reason = 'manual') {
  462. const targetCount = sanitizeImageCount(wantedCount, getForcedImageCount());
  463. const imageBtn = findToolbarAnchor();
  464. if (!imageBtn) {
  465. log(`count UI sync skipped (${reason}): Image button not found`, 'warn');
  466. return false;
  467. }
  468.  
  469. let menu = await tryOpenImageSettingsMenu(imageBtn, reason);
  470. if (!menu) return false;
  471.  
  472. const countBtn = findImageCountCombobox(menu);
  473. if (!countBtn) {
  474. log(`count UI sync failed (${reason}): Number of images control not found`, 'error');
  475. await closeImageMenus(imageBtn);
  476. return false;
  477. }
  478.  
  479. const currentUiValue = getDisplayedUiCountFromCombobox(countBtn);
  480. if (currentUiValue === targetCount) {
  481. await closeImageMenus(imageBtn);
  482. syncStoredImageCount(targetCount, `ui-confirmed:${reason}`);
  483. log(`count UI sync (${reason}): already x${targetCount}`, 'warn');
  484. return true;
  485. }
  486.  
  487. const popupId = countBtn.getAttribute('aria-controls') || '';
  488. clickElement(countBtn);
  489. const listbox = await waitForValue(() => {
  490. const byId = popupId ? document.getElementById(popupId) : null;
  491. if (byId instanceof HTMLElement && isVisible(byId)) return byId;
  492. return findOpenCountListboxGeneric();
  493. }, 1800, 90);
  494.  
  495. if (!listbox) {
  496. log(`count UI sync failed (${reason}): listbox did not open`, 'error');
  497. await closeImageMenus(imageBtn);
  498. return false;
  499. }
  500.  
  501. const picked = await chooseCountOptionFromListbox(listbox, targetCount, countBtn);
  502. await sleep(140);
  503.  
  504. const menuAfter = findOpenImageSettingsMenu(imageBtn) || menu;
  505. const countBtnAfter = findImageCountCombobox(menuAfter) || countBtn;
  506. const finalUiValue = getDisplayedUiCountFromCombobox(countBtnAfter);
  507. const success = picked && (finalUiValue === targetCount || finalUiValue === null);
  508.  
  509. const closed = await closeImageMenus(imageBtn);
  510. if (success) syncStoredImageCount(finalUiValue || targetCount, `ui-apply:${reason}`);
  511.  
  512. log(
  513. success
  514. ? `count UI sync applied (${reason}): x${targetCount}${closed ? '' : ' (menu-close-uncertain)'}`
  515. : `count UI sync uncertain (${reason}): wanted x${targetCount}, ui=${finalUiValue ?? 'unknown'}`,
  516. success ? 'warn' : 'error'
  517. );
  518. return success;
  519. }
  520.  
  521. async function syncForcedImageCountUi(reason = 'manual') {
  522. if (countUiSyncInFlight) {
  523. countUiSyncQueuedReason = reason || countUiSyncQueuedReason || 'manual';
  524. return countUiSyncInFlight;
  525. }
  526. countUiSyncInFlight = (async () => {
  527. let loop = 0;
  528. let ok = false;
  529. let nextReason = reason || 'manual';
  530. do {
  531. countUiSyncQueuedReason = '';
  532. const wanted = getForcedImageCount();
  533. ok = await applyForcedImageCountToUi(wanted, `${nextReason}#${++loop}`);
  534. await sleep(80);
  535. nextReason = countUiSyncQueuedReason || nextReason;
  536. } while (countUiSyncQueuedReason && loop < 6);
  537. return ok;
  538. })().finally(() => {
  539. countUiSyncInFlight = null;
  540. });
  541. return countUiSyncInFlight;
  542. }
  543.  
  544. function getVisibleUiImageCount() {
  545. const menu = findOpenImageSettingsMenu();
  546. const countBtn = findImageCountCombobox(menu);
  547. return getDisplayedUiCountFromCombobox(countBtn);
  548. }
  549.  
  550. function extractCountOptionValueFromTarget(target, event = null) {
  551. const listbox = findOpenCountListboxGeneric();
  552.  
  553. const candidates = [];
  554. const pushCandidate = (node) => {
  555. if (!(node instanceof HTMLElement)) return;
  556. if (candidates.includes(node)) return;
  557. candidates.push(node);
  558. };
  559.  
  560. if (event && typeof event.composedPath === 'function') {
  561. for (const node of event.composedPath()) pushCandidate(node);
  562. }
  563.  
  564. if (target instanceof HTMLElement) {
  565. let cur = target;
  566. while (cur) {
  567. pushCandidate(cur);
  568. if (cur === listbox) break;
  569. cur = cur.parentElement;
  570. }
  571. }
  572.  
  573. for (const el of candidates) {
  574. const value = exactCountFromText(el.textContent);
  575. if (!value) continue;
  576.  
  577. if (listbox instanceof HTMLElement && listbox.contains(el)) return value;
  578.  
  579. const parentListbox = el.closest('[role="listbox"]');
  580. if (parentListbox instanceof HTMLElement && isVisible(parentListbox)) return value;
  581. }
  582.  
  583. return null;
  584. }
  585.  
  586. function installCountUiObserver() {
  587. const handleMaybeCountOptionPick = (event) => {
  588. if (countUiSyncInFlight) return;
  589. const clickedValue = extractCountOptionValueFromTarget(event.target, event);
  590. if (!clickedValue) return;
  591.  
  592. rememberManualCountPick(clickedValue, 'ui-option-pick');
  593.  
  594. setTimeout(() => {
  595. if (countUiSyncInFlight) return;
  596. const uiValue = getVisibleUiImageCount() || pendingManualCountPick || clickedValue;
  597. syncStoredImageCount(uiValue, 'ui-option-confirm');
  598. }, 180);
  599. };
  600.  
  601. document.addEventListener('pointerdown', handleMaybeCountOptionPick, true);
  602. document.addEventListener('mousedown', handleMaybeCountOptionPick, true);
  603. document.addEventListener('click', handleMaybeCountOptionPick, true);
  604. }
  605.  
  606. function scheduleCountUiStartupSync() {
  607. if (countUiStartupSyncDone) return;
  608. countUiStartupSyncDone = true;
  609.  
  610. const rawRestore = (() => { try { return sessionStorage.getItem(RESTORE_STATE_KEY); } catch { return null; } })();
  611. const needsDefaultSync = !rawRestore;
  612. if (!needsDefaultSync) return;
  613.  
  614. clearTimeout(startupDefaultSyncTimer);
  615. startupDefaultSyncTimer = setTimeout(async () => {
  616. try {
  617. await syncForcedImageCountUi('startup-default');
  618. } catch (e) {
  619. log(`startup default count sync failed: ${e.message}`, 'error');
  620. } finally {
  621. try { sessionStorage.removeItem(DEFAULT_UI_SYNC_KEY); } catch {}
  622. focusPromptTextarea();
  623. }
  624. }, 1200);
  625. }
  626.  
  627. function shouldPatchImagePayload() {
  628. return true;
  629. }
  630.  
  631. function isAutoRehydrateEnabled() {
  632. try {
  633. return localStorage.getItem(AUTO_REHYDRATE_KEY) !== '0';
  634. } catch {
  635. return true;
  636. }
  637. }
  638.  
  639. function setAutoRehydrateEnabled(enabled) {
  640. try {
  641. localStorage.setItem(AUTO_REHYDRATE_KEY, enabled ? '1' : '0');
  642. } catch {}
  643. return !!enabled;
  644. }
  645.  
  646. function modifyPayload(originalBody) {
  647. try {
  648. const body = JSON.parse(originalBody);
  649. let changed = false;
  650.  
  651. if (isForce2kEnabled() && body.resolution !== '2k') {
  652. body.resolution = '2k';
  653. changed = true;
  654. log('resolution -> 2k', 'warn');
  655. }
  656.  
  657. const storedCount = getForcedImageCount();
  658. const bodyCount = sanitizeImageCount(body.n, 0);
  659. const recentManualPick = pendingManualCountPick && (Date.now() - pendingManualCountPickedAt) < 5000;
  660.  
  661. if (recentManualPick && bodyCount && bodyCount === pendingManualCountPick) {
  662. syncStoredImageCount(bodyCount, 'payload-manual-confirm');
  663. clearPreferredStoredImageCount();
  664. } else if (bodyCount !== storedCount) {
  665. body.n = storedCount;
  666. changed = true;
  667. log(`n -> ${storedCount}`, 'warn');
  668. }
  669.  
  670. return changed ? JSON.stringify(body) : originalBody;
  671. } catch (e) {
  672. log(`modify payload error: ${e.message}`, 'error');
  673. return originalBody;
  674. }
  675. }
  676.  
  677. function isImageApiUrl(url) {
  678. try {
  679. const u = new URL(url, location.href);
  680. return /^\/v1\/images(?:\/|$)/.test(u.pathname);
  681. } catch {
  682. return typeof url === 'string' && url.includes('/v1/images');
  683. }
  684. }
  685.  
  686. const nativeFetch = window.fetch.bind(window);
  687.  
  688. window.fetch = async function (input, init) {
  689. let nextInput = input;
  690. let nextInit = init;
  691.  
  692. try {
  693. if (!shouldPatchImagePayload()) {
  694. return nativeFetch(input, init);
  695. }
  696.  
  697. const isReq = input instanceof Request;
  698. const url = isReq ? input.url : String(input || '');
  699. const method = ((init && init.method) || (isReq ? input.method : 'GET') || 'GET').toUpperCase();
  700.  
  701. if (method === 'POST' && isImageApiUrl(url)) {
  702. if (typeof init?.body === 'string') {
  703. nextInit = { ...init, body: modifyPayload(init.body) };
  704. } else if (isReq && !init) {
  705. const cloned = input.clone();
  706. const textBody = await cloned.text();
  707. const patchedBody = modifyPayload(textBody);
  708. if (patchedBody !== textBody) {
  709. nextInput = new Request(input, { body: patchedBody });
  710. }
  711. }
  712. noteImageSubmitDispatched('fetch');
  713. }
  714. } catch (e) {
  715. log(`fetch interception error: ${e.message}`, 'error');
  716. }
  717.  
  718. return nativeFetch(nextInput, nextInit);
  719. };
  720.  
  721. function openRestoreDb() {
  722. return new Promise((resolve, reject) => {
  723. const req = indexedDB.open(RESTORE_DB_NAME, 1);
  724. req.onupgradeneeded = () => {
  725. const db = req.result;
  726. if (!db.objectStoreNames.contains(RESTORE_DB_STORE)) db.createObjectStore(RESTORE_DB_STORE);
  727. };
  728. req.onsuccess = () => resolve(req.result);
  729. req.onerror = () => reject(req.error || new Error('indexedDB open failed'));
  730. });
  731. }
  732.  
  733. async function idbSet(key, value) {
  734. const db = await openRestoreDb();
  735. return new Promise((resolve, reject) => {
  736. const tx = db.transaction(RESTORE_DB_STORE, 'readwrite');
  737. tx.objectStore(RESTORE_DB_STORE).put(value, key);
  738. tx.oncomplete = () => { db.close(); resolve(); };
  739. tx.onerror = () => { db.close(); reject(tx.error || new Error('indexedDB put failed')); };
  740. });
  741. }
  742.  
  743. async function idbGet(key) {
  744. const db = await openRestoreDb();
  745. return new Promise((resolve, reject) => {
  746. const tx = db.transaction(RESTORE_DB_STORE, 'readonly');
  747. const req = tx.objectStore(RESTORE_DB_STORE).get(key);
  748. req.onsuccess = () => { db.close(); resolve(req.result); };
  749. req.onerror = () => { db.close(); reject(req.error || new Error('indexedDB get failed')); };
  750. });
  751. }
  752.  
  753. async function idbDelete(key) {
  754. const db = await openRestoreDb();
  755. return new Promise((resolve, reject) => {
  756. const tx = db.transaction(RESTORE_DB_STORE, 'readwrite');
  757. tx.objectStore(RESTORE_DB_STORE).delete(key);
  758. tx.oncomplete = () => { db.close(); resolve(); };
  759. tx.onerror = () => { db.close(); reject(tx.error || new Error('indexedDB delete failed')); };
  760. });
  761. }
  762.  
  763. function getPromptTextarea() {
  764. const textareas = Array.from(document.querySelectorAll('textarea')).filter(isVisible);
  765. return textareas.find(el => /what would you like to edit\?|what image would you like to generate\?/i.test(el.getAttribute('placeholder') || '')) || textareas[0] || null;
  766. }
  767.  
  768. function getComposerRoot() {
  769. const textarea = getPromptTextarea();
  770. if (!textarea) return null;
  771. return textarea.closest('form') || textarea.parentElement || textarea;
  772. }
  773.  
  774. function getComposerImagePreviews() {
  775. const root = getComposerRoot();
  776. if (!root) return [];
  777.  
  778. const imgs = Array.from(root.querySelectorAll('img')).filter(img => {
  779. if (!(img instanceof HTMLImageElement)) return false;
  780. if (!isVisible(img)) return false;
  781. const alt = (img.alt || '').toLowerCase();
  782. const src = img.currentSrc || img.src || '';
  783. return alt.includes('image to edit') && /^(data:image\/|blob:|https?:)/i.test(src);
  784. });
  785.  
  786. const unique = [];
  787. const seen = new Set();
  788. for (const img of imgs) {
  789. const key = `${img.currentSrc || img.src || ''}|${img.alt || ''}`;
  790. if (!key || seen.has(key)) continue;
  791. seen.add(key);
  792. unique.push(img);
  793. }
  794. return unique;
  795. }
  796.  
  797. function getComposerImagePreview() {
  798. return getComposerImagePreviews()[0] || null;
  799. }
  800.  
  801. function findNativeRetryButton() {
  802. const buttons = Array.from(document.querySelectorAll('button, [role="button"]'));
  803. return buttons.find(el => {
  804. if (!(el instanceof Element)) return false;
  805. if (!isVisible(el)) return false;
  806. if (el.closest(`#${PANEL_ID}`)) return false;
  807. return /\bretry\b/.test(getText(el));
  808. }) || null;
  809. }
  810.  
  811. function focusPromptTextarea() {
  812. const textarea = getPromptTextarea();
  813. if (!(textarea instanceof HTMLTextAreaElement)) return false;
  814. try {
  815. textarea.focus();
  816. const len = textarea.value.length;
  817. textarea.setSelectionRange(len, len);
  818. return true;
  819. } catch {
  820. return false;
  821. }
  822. }
  823.  
  824. function findStartButton() {
  825. const textarea = getPromptTextarea();
  826. const form = textarea?.closest('form') || null;
  827. const root = getComposerRoot();
  828. const candidates = Array.from(document.querySelectorAll('button[type="submit"]')).filter(el => {
  829. if (!(el instanceof HTMLButtonElement)) return false;
  830. if (!isVisible(el)) return false;
  831. if (el.closest(`#${PANEL_ID}`)) return false;
  832. return !!el.querySelector('svg.lucide-arrow-up') || el.type === 'submit';
  833. });
  834. if (!candidates.length) return null;
  835. const ranked = candidates.map((btn, index) => {
  836. let score = 0;
  837. if (form && btn.closest('form') === form) score += 1000;
  838. if (root && root.contains(btn)) score += 700;
  839. if (!btn.disabled && btn.getAttribute('aria-disabled') !== 'true') score += 300;
  840. score += index;
  841. return { btn, score };
  842. }).sort((a, b) => b.score - a.score);
  843. return ranked[0]?.btn || null;
  844. }
  845.  
  846. function getAlertBannerLines(card) {
  847. if (!(card instanceof Element)) return [];
  848. const seen = new Set();
  849. const lines = [];
  850. for (const el of Array.from(card.querySelectorAll('p, h1, h2, h3, span'))) {
  851. if (!(el instanceof HTMLElement)) continue;
  852. if (!isVisible(el)) continue;
  853. if (el.closest('button, a[href]')) continue;
  854. const text = normText(el.textContent);
  855. if (!text || seen.has(text)) continue;
  856. seen.add(text);
  857. lines.push(text);
  858. }
  859. return lines;
  860. }
  861.  
  862. function reportBannerMatchOnce(kind, lines) {
  863. const signature = `${kind}|${lines.join(' | ')}`;
  864. const now = Date.now();
  865. if (signature === lastBannerSignature && (now - lastBannerSignatureAt) < 2500) return;
  866. lastBannerSignature = signature;
  867. lastBannerSignatureAt = now;
  868. log(`${kind} banner matched: ${lines.join(' / ') || 'no-text'}`, 'warn');
  869. }
  870.  
  871. function analyzeAlertBanner(card) {
  872. if (!(card instanceof HTMLElement)) return null;
  873. if (!isVisible(card)) return null;
  874. if (card.closest(`#${PANEL_ID}`)) return null;
  875.  
  876. const hasAlert = !!card.querySelector('svg.lucide-circle-alert');
  877. if (!hasAlert) return null;
  878.  
  879. const rect = card.getBoundingClientRect();
  880. if (rect.width < 220 || rect.height < 60) return null;
  881. if (card.querySelector('textarea, input[type="text"], input[type="file"]')) return null;
  882.  
  883. const actionEls = Array.from(card.querySelectorAll('a[href], button, [role="button"]')).filter(isVisible);
  884. const hasSignupHref = actionEls.some(el => /\/sign-up/i.test(el.getAttribute?.('href') || ''));
  885. const actionTexts = actionEls.map(el => getText(el));
  886. const hasAction = hasSignupHref || actionTexts.some(t => t.includes('help') || t.includes('sign up') || t.includes('try again'));
  887. if (!hasAction) return null;
  888.  
  889. const lines = getAlertBannerLines(card);
  890. const combined = lines.join(' | ');
  891. const isHighDemand = /\bhigh demand\b/i.test(combined) || /please try again soon/i.test(combined);
  892. const kind = isHighDemand ? 'high-demand' : 'glitch';
  893.  
  894. return { card, kind, lines, combined, actionTexts };
  895. }
  896.  
  897. function findBannerInfo(kind = '') {
  898. const cards = Array.from(document.querySelectorAll('div.rounded-3xl.border, div.border.rounded-3xl'));
  899. for (const card of cards) {
  900. const info = analyzeAlertBanner(card);
  901. if (!info) continue;
  902. if (kind && info.kind !== kind) continue;
  903. reportBannerMatchOnce(info.kind, info.lines);
  904. return info;
  905. }
  906. return null;
  907. }
  908.  
  909. function findGlitchBanner() {
  910. return findBannerInfo('glitch')?.card || null;
  911. }
  912.  
  913. function findHighDemandBanner() {
  914. return findBannerInfo('high-demand')?.card || null;
  915. }
  916.  
  917. function hasGlitchBanner() {
  918. return !!findGlitchBanner();
  919. }
  920.  
  921. function hasHighDemandBanner() {
  922. return !!findHighDemandBanner();
  923. }
  924.  
  925. async function clickNativeRetryAlways() {
  926. const retryBtn = findNativeRetryButton();
  927. if (!retryBtn) {
  928. log('native Retry button not found', 'warn');
  929. return false;
  930. }
  931. log('clicking native Retry', 'warn');
  932. try {
  933. retryBtn.click();
  934. } catch (e) {
  935. log(`native Retry click failed: ${e.message}`, 'error');
  936. return false;
  937. }
  938.  
  939. const started = Date.now();
  940. while (Date.now() - started < 5000) {
  941. await sleep(120);
  942. const promptNow = (getPromptTextarea()?.value || '').trim();
  943. const imageNow = !!getComposerImagePreview();
  944. if (promptNow || imageNow) {
  945. log(`native Retry hydrated composer: prompt=${promptNow ? 'yes' : 'no'}, image=${imageNow ? 'yes' : 'no'}`, 'warn');
  946. await sleep(220);
  947. return true;
  948. }
  949. }
  950.  
  951. log('native Retry timeout: composer did not hydrate', 'warn');
  952. return false;
  953. }
  954.  
  955. async function waitForStartReadyOrGlitch(timeoutMs = 12000) {
  956. const started = Date.now();
  957. let sawDisabledSubmit = false;
  958. while (Date.now() - started < timeoutMs) {
  959. if (hasGlitchBanner()) return { kind: 'glitch', button: null };
  960. const btn = findStartButton();
  961. if (btn) {
  962. const disabled = btn.disabled || btn.getAttribute('aria-disabled') === 'true';
  963. if (!disabled) return { kind: 'start', button: btn };
  964. sawDisabledSubmit = true;
  965. }
  966. await sleep(120);
  967. }
  968. if (hasGlitchBanner()) return { kind: 'glitch', button: null };
  969. if (sawDisabledSubmit) log('start button stayed disabled until timeout', 'warn');
  970. return { kind: 'timeout', button: null };
  971. }
  972.  
  973. async function triggerStartButton(btn, options = {}) {
  974. if (!btn) return false;
  975. const shouldSyncCountUi = options.syncCountUi === true;
  976. try {
  977. const promptBeforeSync = getPromptTextarea()?.value || '';
  978. const previewCountBeforeSync = getComposerImagePreviews().length;
  979.  
  980. if (shouldSyncCountUi) {
  981. try {
  982. await syncForcedImageCountUi('before-submit');
  983. } catch (e) {
  984. log(`pre-submit count UI sync failed: ${e.message}`, 'error');
  985. }
  986. }
  987.  
  988. if (promptBeforeSync) {
  989. const textareaNow = getPromptTextarea();
  990. if (textareaNow instanceof HTMLTextAreaElement) {
  991. const shouldRefreshPrompt = !previewCountBeforeSync;
  992. if (textareaNow.value !== promptBeforeSync || shouldRefreshPrompt) {
  993. const restored = restorePromptIntoTextarea(textareaNow, promptBeforeSync, shouldRefreshPrompt);
  994. log(`pre-submit prompt restore after count sync = ${restored ? 'yes' : 'no'}`, restored ? 'warn' : 'error');
  995. await sleep(90);
  996. }
  997. }
  998. }
  999.  
  1000. const form = btn.closest('form');
  1001. if (form && typeof form.requestSubmit === 'function') {
  1002. form.requestSubmit(btn);
  1003. log('start button requestSubmit sent', 'warn');
  1004. } else {
  1005. btn.click();
  1006. log('start button clicked', 'warn');
  1007. }
  1008. return true;
  1009. } catch (e) {
  1010. log(`start button trigger failed: ${e.message}`, 'error');
  1011. return false;
  1012. }
  1013. }
  1014.  
  1015. async function waitForEnabledStartButton(timeoutMs = 7000) {
  1016. const started = Date.now();
  1017. while (Date.now() - started < timeoutMs) {
  1018. const btn = findStartButton();
  1019. if (btn && !btn.disabled && btn.getAttribute('aria-disabled') !== 'true') return btn;
  1020. await sleep(120);
  1021. }
  1022. return null;
  1023. }
  1024.  
  1025. async function waitForPostSubmitOutcome(timeoutMs = 3200) {
  1026. const started = Date.now();
  1027. while (Date.now() - started < timeoutMs) {
  1028. if (hasGlitchBanner()) return { kind: 'glitch' };
  1029. if (hasHighDemandBanner()) return { kind: 'high-demand' };
  1030. await sleep(120);
  1031. }
  1032. return { kind: 'submitted' };
  1033. }
  1034.  
  1035. function getHighDemandRetryDelay(attempt = 0) {
  1036. return 1000 + Math.floor(Math.random() * 1400) + (attempt * 260);
  1037. }
  1038.  
  1039. async function submitWithHighDemandRetry(initialBtn, reason = 'submit', options = {}) {
  1040. const opts = typeof options === 'number' ? { maxExtraRetries: options } : (options || {});
  1041. const maxExtraRetries = Math.max(0, Math.floor(Number(opts.maxExtraRetries ?? 7)));
  1042. const syncCountUiFirstAttempt = opts.syncCountUiFirstAttempt === true;
  1043.  
  1044. let btn = initialBtn || findStartButton();
  1045. if (!btn) {
  1046. log(`submit skipped (${reason}): start button not found`, 'warn');
  1047. return { kind: 'no-start' };
  1048. }
  1049.  
  1050. highDemandRecoveryInFlight++;
  1051. try {
  1052. for (let attempt = 0; attempt <= maxExtraRetries; attempt++) {
  1053. const clicked = await triggerStartButton(btn, { syncCountUi: syncCountUiFirstAttempt && attempt === 0 });
  1054. if (!clicked) return { kind: 'submit-failed' };
  1055.  
  1056. const outcome = await waitForPostSubmitOutcome(3800);
  1057. if (outcome.kind === 'high-demand' && attempt < maxExtraRetries) {
  1058. const delay = getHighDemandRetryDelay(attempt);
  1059. log(`high-demand after submit (${reason}) -> retry #${attempt + 1} in ${delay}ms`, 'warn');
  1060. await sleep(delay);
  1061. btn = await waitForEnabledStartButton(9000);
  1062. if (!btn) {
  1063. log(`high-demand retry stopped (${reason}): start button did not recover`, 'warn');
  1064. return { kind: 'high-demand-timeout' };
  1065. }
  1066. continue;
  1067. }
  1068.  
  1069. if (outcome.kind === 'high-demand') {
  1070. log(`high-demand persisted after ${attempt + 1} submit attempts (${reason})`, 'warn');
  1071. }
  1072.  
  1073. return outcome;
  1074. }
  1075.  
  1076. return { kind: 'submitted' };
  1077. } finally {
  1078. highDemandRecoveryInFlight = Math.max(0, highDemandRecoveryInFlight - 1);
  1079. }
  1080. }
  1081.  
  1082. async function recoverHighDemandGlobally(reason = 'observer') {
  1083. if (highDemandRecoveryInFlight > 0) return false;
  1084. if (!hasHighDemandBanner()) return false;
  1085.  
  1086. highDemandRecoveryInFlight++;
  1087. try {
  1088. for (let attempt = 0; attempt < 8; attempt++) {
  1089. if (!hasHighDemandBanner()) return true;
  1090.  
  1091. const delay = getHighDemandRetryDelay(attempt);
  1092. log(`high-demand watcher (${reason}) -> retry #${attempt + 1} in ${delay}ms`, 'warn');
  1093. await sleep(delay);
  1094.  
  1095. if (!hasHighDemandBanner()) return true;
  1096.  
  1097. const btn = await waitForEnabledStartButton(9000);
  1098. if (!btn) {
  1099. log(`high-demand watcher stopped (${reason}): start button did not recover`, 'warn');
  1100. return false;
  1101. }
  1102.  
  1103. const clicked = await triggerStartButton(btn, { syncCountUi: false });
  1104. if (!clicked) return false;
  1105.  
  1106. const outcome = await waitForPostSubmitOutcome(3800);
  1107. if (outcome.kind === 'glitch') {
  1108. log(`high-demand watcher saw glitch banner (${reason}), stopping`, 'warn');
  1109. return false;
  1110. }
  1111. if (outcome.kind !== 'high-demand') {
  1112. log(`high-demand watcher recovered (${reason}) after ${attempt + 1} retries`, 'warn');
  1113. return true;
  1114. }
  1115. }
  1116.  
  1117. log(`high-demand watcher gave up after 8 retries (${reason})`, 'warn');
  1118. return !hasHighDemandBanner();
  1119. } finally {
  1120. highDemandRecoveryInFlight = Math.max(0, highDemandRecoveryInFlight - 1);
  1121. }
  1122. }
  1123.  
  1124. function scheduleHighDemandWatch(reason = 'observer', delayMs = 140) {
  1125. if (highDemandRecoveryInFlight > 0) return;
  1126. cancelAnimationFrame(highDemandWatchRaf);
  1127. highDemandWatchRaf = requestAnimationFrame(() => {
  1128. setTimeout(() => {
  1129. if (highDemandRecoveryInFlight > 0) return;
  1130. if (!hasHighDemandBanner()) return;
  1131. recoverHighDemandGlobally(reason).catch(e => log(`high-demand watcher failed: ${e.message}`, 'error'));
  1132. }, delayMs);
  1133. });
  1134. }
  1135.  
  1136. function dataUrlToBlob(dataUrl) {
  1137. const match = dataUrl.match(/^data:([^;,]+)?(;base64)?,(.*)$/s);
  1138. if (!match) throw new Error('invalid data URL');
  1139. const mime = match[1] || 'application/octet-stream';
  1140. const isBase64 = !!match[2];
  1141. const data = match[3] || '';
  1142. if (isBase64) {
  1143. const binary = atob(data);
  1144. const bytes = new Uint8Array(binary.length);
  1145. for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
  1146. return new Blob([bytes], { type: mime });
  1147. }
  1148. return new Blob([decodeURIComponent(data)], { type: mime });
  1149. }
  1150.  
  1151. async function srcToBlob(src) {
  1152. if (!src) throw new Error('empty image src');
  1153. if (src.startsWith('data:')) return dataUrlToBlob(src);
  1154. const fetchOpts = src.startsWith('blob:') ? {} : { credentials: 'include' };
  1155. const res = await nativeFetch(src, fetchOpts);
  1156. if (!res.ok) throw new Error(`image fetch failed: HTTP ${res.status}`);
  1157. return await res.blob();
  1158. }
  1159.  
  1160. function normalizeRestoreOptions(optionsOrAutoSubmitAfterRestore = false, focusPromptAfterRestore = false) {
  1161. if (typeof optionsOrAutoSubmitAfterRestore === 'object' && optionsOrAutoSubmitAfterRestore) {
  1162. return {
  1163. autoSubmitAfterRestore: !!optionsOrAutoSubmitAfterRestore.autoSubmitAfterRestore,
  1164. focusPromptAfterRestore: !!optionsOrAutoSubmitAfterRestore.focusPromptAfterRestore,
  1165. syncCountUiAfterRestore: !!optionsOrAutoSubmitAfterRestore.syncCountUiAfterRestore
  1166. };
  1167. }
  1168. return {
  1169. autoSubmitAfterRestore: !!optionsOrAutoSubmitAfterRestore,
  1170. focusPromptAfterRestore: !!focusPromptAfterRestore,
  1171. syncCountUiAfterRestore: false
  1172. };
  1173. }
  1174.  
  1175. async function captureComposerSnapshot(options = {}) {
  1176. const opts = options && typeof options === 'object' ? options : {};
  1177. const waitForContentMs = Math.max(0, Math.round(Number(opts.waitForContentMs) || 0));
  1178. const deadline = Date.now() + waitForContentMs;
  1179.  
  1180. let textarea = null;
  1181. let prompt = '';
  1182. let previews = [];
  1183.  
  1184. do {
  1185. textarea = getPromptTextarea();
  1186. prompt = textarea?.value || '';
  1187. previews = getComposerImagePreviews();
  1188. if (prompt.trim() || previews.length || !waitForContentMs) break;
  1189. await sleep(120);
  1190. } while (Date.now() < deadline);
  1191.  
  1192. const imageBlobs = [];
  1193. const imageTypes = [];
  1194. let blobSize = 0;
  1195.  
  1196. for (const [index, img] of previews.entries()) {
  1197. try {
  1198. const src = img.currentSrc || img.src || '';
  1199. if (!src) continue;
  1200. const srcKind = src.startsWith('data:') ? 'data' : src.startsWith('blob:') ? 'blob' : src.startsWith('http') ? 'http' : 'other';
  1201. log(`save image #${index + 1}: src kind = ${srcKind}`, 'warn');
  1202. const blob = await srcToBlob(src);
  1203. if (!(blob && blob.size > 0)) continue;
  1204. imageBlobs.push(blob);
  1205. imageTypes.push(blob.type || 'image/png');
  1206. blobSize += blob.size;
  1207. } catch (e) {
  1208. log(`save image #${index + 1} for restore failed: ${e.message}`, 'error');
  1209. }
  1210. }
  1211.  
  1212. return {
  1213. prompt,
  1214. imageBlobs,
  1215. imageTypes,
  1216. blobSize,
  1217. imageCount: imageBlobs.length,
  1218. hasImage: imageBlobs.length > 0
  1219. };
  1220. }
  1221.  
  1222. async function persistRestoreState(snapshot, optionsOrAutoSubmitAfterRestore = false, focusPromptAfterRestore = false, sourceLabel = 'composer') {
  1223. const restoreOptions = normalizeRestoreOptions(optionsOrAutoSubmitAfterRestore, focusPromptAfterRestore);
  1224. const prompt = snapshot?.prompt || '';
  1225. const hasImage = !!snapshot?.hasImage;
  1226. const imageTypes = Array.isArray(snapshot?.imageTypes) ? snapshot.imageTypes : [];
  1227. const blobSize = snapshot?.blobSize || 0;
  1228. const imageBlobs = Array.isArray(snapshot?.imageBlobs)
  1229. ? snapshot.imageBlobs.filter(blob => blob instanceof Blob && blob.size > 0)
  1230. : (snapshot?.imageBlob instanceof Blob && snapshot.imageBlob.size > 0 ? [snapshot.imageBlob] : []);
  1231. const imageCount = snapshot?.imageCount || imageBlobs.length || (hasImage ? 1 : 0);
  1232.  
  1233. if (imageBlobs.length) {
  1234. await idbSet(RESTORE_IMAGE_KEY, imageBlobs);
  1235. } else {
  1236. try { await idbDelete(RESTORE_IMAGE_KEY); } catch {}
  1237. }
  1238.  
  1239. const state = {
  1240. createdAt: Date.now(),
  1241. prompt,
  1242. hasImage: imageBlobs.length > 0,
  1243. imageType: imageTypes[0] || 'image/png',
  1244. imageTypes,
  1245. imageCount,
  1246. autoSubmitAfterRestore: !!restoreOptions.autoSubmitAfterRestore,
  1247. focusPromptAfterRestore: !!restoreOptions.focusPromptAfterRestore,
  1248. syncCountUiAfterRestore: !!restoreOptions.syncCountUiAfterRestore
  1249. };
  1250. sessionStorage.setItem(RESTORE_STATE_KEY, JSON.stringify(state));
  1251. log(`restore state saved [${sourceLabel}]: prompt=${prompt.trim() ? 'yes' : 'no'}, images=${imageBlobs.length}, bytes=${blobSize}, auto=${state.autoSubmitAfterRestore ? 'yes' : 'no'}, focus=${state.focusPromptAfterRestore ? 'yes' : 'no'}`, 'warn');
  1252. }
  1253.  
  1254. async function saveRestoreStateFromPreviousTask(optionsOrAutoSubmitAfterRestore = false, focusPromptAfterRestore = false) {
  1255. await clickNativeRetryAlways();
  1256. const snapshot = await captureComposerSnapshot({ waitForContentMs: 3200 });
  1257. await persistRestoreState(snapshot, optionsOrAutoSubmitAfterRestore, focusPromptAfterRestore, 'previous-task');
  1258. }
  1259.  
  1260. async function saveRestoreStateFromCurrentComposer(optionsOrAutoSubmitAfterRestore = false, focusPromptAfterRestore = false) {
  1261. const snapshot = await captureComposerSnapshot({ waitForContentMs: 1200 });
  1262. await persistRestoreState(snapshot, optionsOrAutoSubmitAfterRestore, focusPromptAfterRestore, 'current-composer');
  1263. }
  1264.  
  1265. async function saveEmptyRestoreState(optionsOrAutoSubmitAfterRestore = false, focusPromptAfterRestore = false) {
  1266. await persistRestoreState({ prompt: '', imageBlobs: [], imageTypes: [], blobSize: 0, imageCount: 0, hasImage: false }, optionsOrAutoSubmitAfterRestore, focusPromptAfterRestore, 'empty');
  1267. }
  1268.  
  1269. function setNativeTextareaValue(textarea, value) {
  1270. if (!(textarea instanceof HTMLTextAreaElement)) return;
  1271. const nextValue = String(value ?? '');
  1272. const previous = textarea.value;
  1273. try { textarea.focus(); } catch {}
  1274.  
  1275. const proto = HTMLTextAreaElement.prototype;
  1276. const desc = Object.getOwnPropertyDescriptor(proto, 'value');
  1277. if (desc && desc.set) desc.set.call(textarea, nextValue);
  1278. else textarea.value = nextValue;
  1279.  
  1280. try {
  1281. const tracker = textarea._valueTracker;
  1282. if (tracker && typeof tracker.setValue === 'function') tracker.setValue(previous);
  1283. } catch {}
  1284.  
  1285. try {
  1286. textarea.dispatchEvent(new InputEvent('beforeinput', {
  1287. bubbles: true,
  1288. cancelable: true,
  1289. inputType: 'insertText',
  1290. data: nextValue
  1291. }));
  1292. } catch {}
  1293. try {
  1294. textarea.dispatchEvent(new InputEvent('input', {
  1295. bubbles: true,
  1296. cancelable: false,
  1297. inputType: 'insertText',
  1298. data: nextValue
  1299. }));
  1300. } catch {
  1301. try { textarea.dispatchEvent(new Event('input', { bubbles: true })); } catch {}
  1302. }
  1303. try { textarea.dispatchEvent(new Event('change', { bubbles: true })); } catch {}
  1304. try { textarea.dispatchEvent(new KeyboardEvent('keyup', { key: 'End', code: 'End', bubbles: true })); } catch {}
  1305. try {
  1306. const len = textarea.value.length;
  1307. textarea.setSelectionRange(len, len);
  1308. } catch {}
  1309. }
  1310.  
  1311.  
  1312.  
  1313. function createTextClipboardData(text) {
  1314. const dt = new DataTransfer();
  1315. try { dt.setData('text/plain', String(text ?? '')); } catch {}
  1316. return dt;
  1317. }
  1318.  
  1319. function createTextPasteEvent(text) {
  1320. const dt = createTextClipboardData(text);
  1321. let evt;
  1322. try { evt = new ClipboardEvent('paste', { bubbles: true, cancelable: true }); }
  1323. catch { evt = new Event('paste', { bubbles: true, cancelable: true }); }
  1324. try { Object.defineProperty(evt, 'clipboardData', { value: dt }); } catch {}
  1325. return evt;
  1326. }
  1327.  
  1328. function restorePromptIntoTextarea(textarea, prompt, refresh = false) {
  1329. if (!(textarea instanceof HTMLTextAreaElement)) return false;
  1330. const nextValue = String(prompt ?? '');
  1331. const proto = HTMLTextAreaElement.prototype;
  1332. const desc = Object.getOwnPropertyDescriptor(proto, 'value');
  1333. const setter = desc && desc.set ? desc.set.bind(textarea) : null;
  1334. const tracker = (() => {
  1335. try { return textarea._valueTracker; } catch { return null; }
  1336. })();
  1337.  
  1338. const writeValue = (value, inputType, data) => {
  1339. const prev = textarea.value;
  1340. try { textarea.focus(); } catch {}
  1341. try { textarea.setSelectionRange(0, textarea.value.length); } catch {}
  1342. if (setter) setter(value);
  1343. else textarea.value = value;
  1344. try { textarea.defaultValue = value; } catch {}
  1345. try { textarea.textContent = value; } catch {}
  1346. try {
  1347. if (tracker && typeof tracker.setValue === 'function') tracker.setValue(prev);
  1348. } catch {}
  1349. try {
  1350. textarea.dispatchEvent(new InputEvent('beforeinput', {
  1351. bubbles: true,
  1352. cancelable: true,
  1353. inputType,
  1354. data
  1355. }));
  1356. } catch {}
  1357. try {
  1358. textarea.dispatchEvent(new InputEvent('input', {
  1359. bubbles: true,
  1360. cancelable: false,
  1361. inputType,
  1362. data
  1363. }));
  1364. } catch {
  1365. try { textarea.dispatchEvent(new Event('input', { bubbles: true })); } catch {}
  1366. }
  1367. try { textarea.dispatchEvent(new Event('change', { bubbles: true })); } catch {}
  1368. };
  1369.  
  1370. if (refresh || textarea.value === nextValue) {
  1371. writeValue('', 'deleteContentBackward', null);
  1372. }
  1373.  
  1374. writeValue(nextValue, 'insertText', nextValue);
  1375.  
  1376. try {
  1377. textarea.dispatchEvent(createTextPasteEvent(nextValue));
  1378. } catch {}
  1379. try { textarea.dispatchEvent(new KeyboardEvent('keydown', { key: 'End', code: 'End', bubbles: true })); } catch {}
  1380. try { textarea.dispatchEvent(new KeyboardEvent('keyup', { key: 'End', code: 'End', bubbles: true })); } catch {}
  1381. try {
  1382. const len = textarea.value.length;
  1383. textarea.setSelectionRange(len, len);
  1384. } catch {}
  1385. return textarea.value === nextValue;
  1386. }
  1387.  
  1388. function createClipboardDataWithFile(file) {
  1389. const dt = new DataTransfer();
  1390. dt.items.add(file);
  1391. return dt;
  1392. }
  1393.  
  1394. function createPasteEvent(file) {
  1395. const dt = createClipboardDataWithFile(file);
  1396. let evt;
  1397. try { evt = new ClipboardEvent('paste', { bubbles: true, cancelable: true }); }
  1398. catch { evt = new Event('paste', { bubbles: true, cancelable: true }); }
  1399. try { Object.defineProperty(evt, 'clipboardData', { value: dt }); } catch {}
  1400. try { Object.defineProperty(evt, 'dataTransfer', { value: dt }); } catch {}
  1401. return evt;
  1402. }
  1403.  
  1404. function createDragEvent(type, file) {
  1405. const dt = createClipboardDataWithFile(file);
  1406. let evt;
  1407. try { evt = new DragEvent(type, { bubbles: true, cancelable: true }); }
  1408. catch { evt = new Event(type, { bubbles: true, cancelable: true }); }
  1409. try { Object.defineProperty(evt, 'dataTransfer', { value: dt }); } catch {}
  1410. return evt;
  1411. }
  1412.  
  1413. function getBestFileInput() {
  1414. const textarea = getPromptTextarea();
  1415. const root = getComposerRoot();
  1416. const inputs = Array.from(document.querySelectorAll('input[type="file"]')).filter(el => el instanceof HTMLInputElement);
  1417. if (!inputs.length) return null;
  1418. const ranked = inputs.map((input, index) => {
  1419. let score = 0;
  1420. if (root && root.contains(input)) score += 1000;
  1421. if (textarea && input.closest('form') && textarea.closest('form') && input.closest('form') === textarea.closest('form')) score += 300;
  1422. if (isVisible(input)) score += 100;
  1423. score += index;
  1424. return { input, score };
  1425. }).sort((a, b) => b.score - a.score);
  1426. return ranked[0]?.input || null;
  1427. }
  1428.  
  1429. function tryRestoreViaFileInputFiles(files) {
  1430. const input = getBestFileInput();
  1431. log(`restore image: chosen file input = ${input ? 'yes' : 'no'}`, 'warn');
  1432. if (!input) return false;
  1433. try {
  1434. const dt = new DataTransfer();
  1435. for (const file of files) dt.items.add(file);
  1436. input.files = dt.files;
  1437. input.dispatchEvent(new Event('input', { bubbles: true }));
  1438. input.dispatchEvent(new Event('change', { bubbles: true }));
  1439. log(`restore image: file-input path attempted (${files.length} file${files.length === 1 ? '' : 's'})`, 'warn');
  1440. return true;
  1441. } catch (e) {
  1442. log(`restore image: file-input failed: ${e.message}`, 'error');
  1443. return false;
  1444. }
  1445. }
  1446.  
  1447. function tryRestoreViaPasteSingle(file, textarea) {
  1448. const root = getComposerRoot();
  1449. const target = textarea || root || document.body;
  1450. if (!target) return false;
  1451. try {
  1452. if (target instanceof HTMLElement && typeof target.focus === 'function') target.focus();
  1453. target.dispatchEvent(createPasteEvent(file));
  1454. log('restore image: single paste path attempted', 'warn');
  1455. return true;
  1456. } catch (e) {
  1457. log(`restore image: single paste failed: ${e.message}`, 'error');
  1458. return false;
  1459. }
  1460. }
  1461.  
  1462. function tryRestoreViaDropSingle(file, textarea) {
  1463. const root = getComposerRoot();
  1464. const target = root || textarea || document.body;
  1465. if (!target || !(target instanceof EventTarget)) return false;
  1466. try {
  1467. target.dispatchEvent(createDragEvent('dragenter', file));
  1468. target.dispatchEvent(createDragEvent('dragover', file));
  1469. target.dispatchEvent(createDragEvent('drop', file));
  1470. log('restore image: single drop path attempted', 'warn');
  1471. return true;
  1472. } catch (e) {
  1473. log(`restore image: single drop failed: ${e.message}`, 'error');
  1474. return false;
  1475. }
  1476. }
  1477.  
  1478. async function tryRestoreFilesPath(files, textarea) {
  1479. if (!Array.isArray(files) || !files.length) return '';
  1480. if (tryRestoreViaFileInputFiles(files)) return 'file';
  1481.  
  1482. let pasted = 0;
  1483. for (const file of files) {
  1484. if (tryRestoreViaPasteSingle(file, textarea)) {
  1485. pasted++;
  1486. await sleep(120);
  1487. continue;
  1488. }
  1489. pasted = 0;
  1490. break;
  1491. }
  1492. if (pasted === files.length) return 'paste';
  1493.  
  1494. let dropped = 0;
  1495. for (const file of files) {
  1496. if (tryRestoreViaDropSingle(file, textarea)) {
  1497. dropped++;
  1498. await sleep(120);
  1499. continue;
  1500. }
  1501. dropped = 0;
  1502. break;
  1503. }
  1504. if (dropped === files.length) return 'drop';
  1505. return '';
  1506. }
  1507.  
  1508. async function clearRestoreState() {
  1509. try { sessionStorage.removeItem(RESTORE_STATE_KEY); } catch {}
  1510. try { await idbDelete(RESTORE_IMAGE_KEY); } catch {}
  1511. }
  1512.  
  1513. async function restoreStateAfterReset() {
  1514. const raw = sessionStorage.getItem(RESTORE_STATE_KEY);
  1515. if (!raw) return;
  1516.  
  1517. const state = JSON.parse(raw);
  1518. let imageBlobs = [];
  1519. if (state.hasImage) {
  1520. try {
  1521. const stored = await idbGet(RESTORE_IMAGE_KEY);
  1522. imageBlobs = Array.isArray(stored)
  1523. ? stored.filter(blob => blob instanceof Blob && blob.size > 0)
  1524. : (stored instanceof Blob && stored.size > 0 ? [stored] : []);
  1525. log(`restore image blobs loaded: ${imageBlobs.length} item(s), ${imageBlobs.reduce((sum, blob) => sum + (blob?.size || 0), 0)} bytes`, 'warn');
  1526. } catch (e) {
  1527. log(`restore image load failed: ${e.message}`, 'error');
  1528. }
  1529. }
  1530.  
  1531. const wantedImageCount = Math.max(Number(state.imageCount) || 0, imageBlobs.length);
  1532. let attempts = 0;
  1533. let imageInjectDone = false;
  1534. let autoSubmitted = false;
  1535.  
  1536. const tick = async () => {
  1537. attempts++;
  1538. const textarea = getPromptTextarea();
  1539. const startBtnNow = findStartButton();
  1540. const startReadyNow = !!(startBtnNow && !startBtnNow.disabled && startBtnNow.getAttribute('aria-disabled') !== 'true');
  1541. if (state.prompt && textarea) {
  1542. const needsPromptWrite = textarea.value !== state.prompt;
  1543. const needsPromptRefresh = !state.hasImage && !startReadyNow && attempts <= 12 && (attempts === 1 || attempts % 2 === 0);
  1544. if (needsPromptWrite || needsPromptRefresh) {
  1545. const restored = restorePromptIntoTextarea(textarea, state.prompt, needsPromptRefresh);
  1546. log(`restore prompt attempt #${attempts}: ${restored ? 'applied' : 'failed'}`, restored ? 'warn' : 'error');
  1547. await sleep(90);
  1548. }
  1549. }
  1550.  
  1551. const promptOk = !state.prompt || (textarea && textarea.value === state.prompt);
  1552. const previews = getComposerImagePreviews();
  1553. let imageOk = !state.hasImage || previews.length >= wantedImageCount;
  1554.  
  1555. if (state.hasImage) {
  1556. if (!imageOk && imageBlobs.length && !imageInjectDone && attempts >= 2) {
  1557. const files = imageBlobs.map((blob, index) => {
  1558. const mime = blob.type || (state.imageTypes && state.imageTypes[index]) || state.imageType || 'image/png';
  1559. const ext = mime.includes('jpeg') ? 'jpg' : mime.includes('webp') ? 'webp' : 'png';
  1560. return new File([blob], `restore-${index + 1}.${ext}`, { type: mime, lastModified: Date.now() + index });
  1561. });
  1562. log(`restore attempt #${attempts}: trying image reinsert (${files.length} file${files.length === 1 ? '' : 's'})`, 'warn');
  1563. const method = await tryRestoreFilesPath(files, textarea);
  1564. imageInjectDone = true;
  1565. log(`restore attempt #${attempts}: chosen method = ${method || 'none'}`, method ? 'warn' : 'error');
  1566. }
  1567. }
  1568.  
  1569. if (promptOk && imageOk && state.autoSubmitAfterRestore && !autoSubmitted) {
  1570. const startState = await waitForStartReadyOrGlitch(12000);
  1571. if (startState.kind === 'start') {
  1572. autoSubmitted = true;
  1573. preferStoredImageCount(getForcedImageCount(), 12000, 'post-reset-submit');
  1574. const submitOutcome = await submitWithHighDemandRetry(startState.button, 'post-reset', { syncCountUiFirstAttempt: true, maxExtraRetries: 9 });
  1575. log(`post-reset auto submit result: ${submitOutcome.kind}`, submitOutcome.kind === 'glitch' ? 'error' : 'warn');
  1576. } else {
  1577. log(`post-reset auto submit skipped: ${startState.kind}`, 'warn');
  1578. autoSubmitted = true;
  1579. }
  1580. }
  1581.  
  1582. if (promptOk && imageOk && (!state.autoSubmitAfterRestore || autoSubmitted)) {
  1583. if (state.syncCountUiAfterRestore) {
  1584. try {
  1585. await syncForcedImageCountUi('post-reset-default');
  1586. } catch (e) {
  1587. log(`post-reset default count sync failed: ${e.message}`, 'error');
  1588. }
  1589. }
  1590. if (state.focusPromptAfterRestore && !state.autoSubmitAfterRestore) {
  1591. const focused = focusPromptTextarea();
  1592. log(`post-reset focus applied = ${focused ? 'yes' : 'no'}`, focused ? 'warn' : 'error');
  1593. }
  1594. await clearRestoreState();
  1595. log('restore completed', 'warn');
  1596. return;
  1597. }
  1598.  
  1599. if (attempts >= 24) {
  1600. log(`restore timed out: prompt=${promptOk ? 'ok' : 'no'}, images=${imageOk ? 'ok' : `${previews.length}/${wantedImageCount}`}`, 'warn');
  1601. return;
  1602. }
  1603.  
  1604. await sleep(700);
  1605. return tick();
  1606. };
  1607.  
  1608. await tick();
  1609. }
  1610.  
  1611. function scheduleHighDemandChecks(reason = 'submit') {
  1612. [900, 2200, 4200, 6800, 9500].forEach(delay => {
  1613. setTimeout(() => {
  1614. if (highDemandRecoveryInFlight > 0) return;
  1615. if (!hasHighDemandBanner()) return;
  1616. recoverHighDemandGlobally(`${reason}@${delay}`).catch(e => log(`high-demand watcher failed: ${e.message}`, 'error'));
  1617. }, delay);
  1618. });
  1619. }
  1620.  
  1621. function noteImageSubmitDispatched(reason = 'submit') {
  1622. scheduleHighDemandChecks(reason);
  1623. if (isAutoRehydrateEnabled()) schedulePostStartRehydrate(reason);
  1624. }
  1625.  
  1626. function schedulePostStartRehydrate(reason = 'submit') {
  1627. if (!isAutoRehydrateEnabled()) return;
  1628.  
  1629. const jobId = ++postStartRetryJobId;
  1630. const delays = [2200, 3400, 5200];
  1631.  
  1632. (async () => {
  1633. if (postStartRetryInFlight && jobId !== postStartRetryJobId) return;
  1634. postStartRetryInFlight = true;
  1635. try {
  1636. for (const delay of delays) {
  1637. await sleep(delay);
  1638. if (jobId !== postStartRetryJobId) return;
  1639. if (hasGlitchBanner() || hasHighDemandBanner()) continue;
  1640.  
  1641. const promptNow = (getPromptTextarea()?.value || '').trim();
  1642. const imageNow = !!getComposerImagePreview();
  1643. if (promptNow || imageNow) {
  1644. log(`post-submit rehydrate skipped: composer already filled (${reason})`, 'warn');
  1645. return;
  1646. }
  1647.  
  1648. const retryBtn = findNativeRetryButton();
  1649. if (!retryBtn) continue;
  1650.  
  1651. const ok = await clickNativeRetryAlways();
  1652. if (jobId !== postStartRetryJobId) return;
  1653. if (ok) {
  1654. focusPromptTextarea();
  1655. log(`post-submit rehydrate applied (${reason})`, 'warn');
  1656. return;
  1657. }
  1658. }
  1659. log(`post-submit rehydrate skipped/timed out (${reason})`, 'warn');
  1660. } catch (e) {
  1661. log(`post-submit rehydrate failed (${reason}): ${e.message}`, 'error');
  1662. } finally {
  1663. if (jobId === postStartRetryJobId) postStartRetryInFlight = false;
  1664. }
  1665. })();
  1666. }
  1667.  
  1668.  
  1669. function performCacheReset() {
  1670. log('starting cache reset', 'warn');
  1671.  
  1672. const lsPatterns = [
  1673. /flushAfter/i, /imagine/i, /generation/i, /grok-imagine/i,
  1674. /mixpanel|mp_/i, /distinct_id|device_id/i, /_rst|_r/i,
  1675. /event|payload/i, /quota|limit|usage|count|reset/i,
  1676. /video|vid|vidgen|grok-imagine-video/i, /request_type/i,
  1677. /credit|credits|demand|highdemand|purchase|balance|high demand/i
  1678. ];
  1679.  
  1680. for (let i = localStorage.length - 1; i >= 0; i--) {
  1681. const key = localStorage.key(i);
  1682. if (key && ![DEBUG_LOG_KEY, FORCE_2K_KEY, FORCE_COUNT_KEY, AUTO_REHYDRATE_KEY].includes(key) && lsPatterns.some(p => p.test(key))) {
  1683. localStorage.removeItem(key);
  1684. }
  1685. }
  1686.  
  1687. for (let i = sessionStorage.length - 1; i >= 0; i--) {
  1688. const key = sessionStorage.key(i);
  1689. if (key && ![RESTORE_STATE_KEY, DEFAULT_UI_SYNC_KEY].includes(key) && lsPatterns.some(p => p.test(key))) {
  1690. sessionStorage.removeItem(key);
  1691. }
  1692. }
  1693.  
  1694. const cookiePatterns = ['mp_', 'mixpanel', 'distinct_id', 'device_id', 'xai_', 'imagine', 'console', 'grok', '_rst', '_r', 'video', 'vid', 'credit', 'demand', 'high', 'purchase', 'balance'];
  1695. document.cookie.split(';').forEach(c => {
  1696. const name = c.split('=')[0]?.trim();
  1697. if (!name) return;
  1698. if (cookiePatterns.some(p => name.toLowerCase().includes(p.toLowerCase()))) {
  1699. ['.x.ai', 'console.x.ai', '.console.x.ai', ''].forEach(d => {
  1700. ['/', '/playground', '/playground/imagine', '/team', ''].forEach(p => {
  1701. document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=${p}${d ? ';domain=' + d : ''}`;
  1702. });
  1703. });
  1704. }
  1705. });
  1706.  
  1707. const u = new URL(location.href);
  1708. const now = Date.now();
  1709. u.searchParams.set('_r', now);
  1710. u.searchParams.set('_rst', now);
  1711. u.searchParams.set('_rstv', now);
  1712. u.searchParams.set('_vidrst', now);
  1713. location.replace(u.toString());
  1714. }
  1715.  
  1716. async function repeatReset(autoSubmitAfterRestore = false, focusPromptAfterRestore = false) {
  1717. await closeImageMenus(findToolbarAnchor()).catch(() => {});
  1718. await saveRestoreStateFromPreviousTask({ autoSubmitAfterRestore, focusPromptAfterRestore });
  1719. performCacheReset();
  1720. }
  1721.  
  1722. async function submitCurrentComposerThroughReset() {
  1723. await closeImageMenus(findToolbarAnchor()).catch(() => {});
  1724. await saveRestoreStateFromCurrentComposer({ autoSubmitAfterRestore: true, focusPromptAfterRestore: false });
  1725. performCacheReset();
  1726. }
  1727.  
  1728. async function newComposerAction() {
  1729. setForce2kEnabled(false);
  1730. setForcedImageCount(DEFAULT_IMAGE_COUNT);
  1731. pendingManualCountPick = 0;
  1732. pendingManualCountPickedAt = 0;
  1733. clearPreferredStoredImageCount();
  1734. update2kButton();
  1735. updateCountButton();
  1736. await closeImageMenus(findToolbarAnchor()).catch(() => {});
  1737. await saveEmptyRestoreState({ autoSubmitAfterRestore: false, focusPromptAfterRestore: true, syncCountUiAfterRestore: true });
  1738. performCacheReset();
  1739. }
  1740.  
  1741. async function smartRetryAction() {
  1742. log('smart Retry action started', 'warn');
  1743.  
  1744. if (hasGlitchBanner()) {
  1745. log('glitch banner visible before Retry -> switching to reset flow', 'warn');
  1746. await repeatReset(true);
  1747. return;
  1748. }
  1749.  
  1750. await clickNativeRetryAlways();
  1751.  
  1752. const state = await waitForStartReadyOrGlitch(12000);
  1753. if (state.kind === 'glitch') {
  1754. log('glitch banner detected before start -> switching to reset flow', 'warn');
  1755. await repeatReset(true);
  1756. return;
  1757. }
  1758.  
  1759. if (state.kind === 'timeout') {
  1760. log('Retry flow stopped: start button was not ready in time', 'warn');
  1761. return;
  1762. }
  1763.  
  1764. const submitOutcome = await submitWithHighDemandRetry(state.button, 'retry', { syncCountUiFirstAttempt: false, maxExtraRetries: 7 });
  1765. if (submitOutcome.kind === 'glitch') {
  1766. log('glitch banner detected after submit -> switching to reset flow', 'warn');
  1767. await repeatReset(true);
  1768. return;
  1769. }
  1770.  
  1771. log(`Retry flow finished: ${submitOutcome.kind}`, 'warn');
  1772. }
  1773.  
  1774.  
  1775.  
  1776. function restoreGlitchProxyButton() {
  1777. if (!(glitchProxyButton instanceof HTMLElement) || !glitchProxyState) {
  1778. glitchProxyButton = null;
  1779. glitchProxyState = null;
  1780. return;
  1781. }
  1782.  
  1783. try {
  1784. if (glitchProxyButton instanceof HTMLButtonElement) glitchProxyButton.disabled = !!glitchProxyState.disabled;
  1785. if (glitchProxyState.hadDisabledAttr) glitchProxyButton.setAttribute('disabled', '');
  1786. else glitchProxyButton.removeAttribute('disabled');
  1787. if (glitchProxyState.ariaDisabled == null) glitchProxyButton.removeAttribute('aria-disabled');
  1788. else glitchProxyButton.setAttribute('aria-disabled', glitchProxyState.ariaDisabled);
  1789. glitchProxyButton.style.pointerEvents = glitchProxyState.pointerEvents;
  1790. glitchProxyButton.style.opacity = glitchProxyState.opacity;
  1791. glitchProxyButton.style.cursor = glitchProxyState.cursor;
  1792. if (glitchProxyState.title == null) glitchProxyButton.removeAttribute('title');
  1793. else glitchProxyButton.setAttribute('title', glitchProxyState.title);
  1794. glitchProxyButton.removeAttribute('data-xai-glitch-proxy');
  1795. } catch {}
  1796.  
  1797. glitchProxyButton = null;
  1798. glitchProxyState = null;
  1799. }
  1800.  
  1801. function unlockGlitchProxyButton() {
  1802. const btn = findStartButton();
  1803. if (!(btn instanceof HTMLElement)) {
  1804. restoreGlitchProxyButton();
  1805. return;
  1806. }
  1807.  
  1808. if (glitchProxyButton && glitchProxyButton !== btn) restoreGlitchProxyButton();
  1809.  
  1810. if (!glitchProxyState || glitchProxyButton !== btn) {
  1811. glitchProxyButton = btn;
  1812. glitchProxyState = {
  1813. disabled: btn instanceof HTMLButtonElement ? btn.disabled : btn.hasAttribute('disabled'),
  1814. hadDisabledAttr: btn.hasAttribute('disabled'),
  1815. ariaDisabled: btn.getAttribute('aria-disabled'),
  1816. pointerEvents: btn.style.pointerEvents || '',
  1817. opacity: btn.style.opacity || '',
  1818. cursor: btn.style.cursor || '',
  1819. title: btn.getAttribute('title')
  1820. };
  1821. }
  1822.  
  1823. try {
  1824. if (btn instanceof HTMLButtonElement) btn.disabled = false;
  1825. btn.removeAttribute('disabled');
  1826. btn.setAttribute('aria-disabled', 'false');
  1827. btn.style.pointerEvents = 'auto';
  1828. btn.style.opacity = '1';
  1829. btn.style.cursor = 'pointer';
  1830. btn.setAttribute('title', 'xAI Tools: текущий composer будет сохранён, затем reset и submit');
  1831. btn.setAttribute('data-xai-glitch-proxy', '1');
  1832. } catch {}
  1833. }
  1834.  
  1835. function updateGlitchSubmitProxyState() {
  1836. if (hasGlitchBanner()) {
  1837. unlockGlitchProxyButton();
  1838. } else {
  1839. restoreGlitchProxyButton();
  1840. }
  1841. }
  1842.  
  1843. function isStartButtonTrigger(target) {
  1844. if (!(target instanceof Element)) return false;
  1845. const trigger = target.closest('button, [role="button"]');
  1846. const startBtn = findStartButton();
  1847. return !!(trigger && startBtn && trigger === startBtn);
  1848. }
  1849.  
  1850. async function runGlitchSubmitProxy(reason = 'submit') {
  1851. if (glitchSubmitProxyBusy) return;
  1852. glitchSubmitProxyBusy = true;
  1853. try {
  1854. log(`glitch submit proxy started (${reason})`, 'warn');
  1855. await submitCurrentComposerThroughReset();
  1856. } finally {
  1857. glitchSubmitProxyBusy = false;
  1858. }
  1859. }
  1860.  
  1861. function installGlitchSubmitProxy() {
  1862. document.addEventListener('click', (event) => {
  1863. if (!hasGlitchBanner()) return;
  1864. if (!isStartButtonTrigger(event.target)) return;
  1865. event.preventDefault();
  1866. event.stopPropagation();
  1867. if (typeof event.stopImmediatePropagation === 'function') event.stopImmediatePropagation();
  1868. runGlitchSubmitProxy('click').catch(e => log(`glitch submit proxy failed: ${e.message}`, 'error'));
  1869. }, true);
  1870.  
  1871. document.addEventListener('submit', (event) => {
  1872. if (!hasGlitchBanner()) return;
  1873. const form = event.target instanceof HTMLFormElement ? event.target : null;
  1874. const startBtn = findStartButton();
  1875. if (!form || !startBtn || startBtn.closest('form') !== form) return;
  1876. event.preventDefault();
  1877. event.stopPropagation();
  1878. if (typeof event.stopImmediatePropagation === 'function') event.stopImmediatePropagation();
  1879. runGlitchSubmitProxy('form-submit').catch(e => log(`glitch submit proxy failed: ${e.message}`, 'error'));
  1880. }, true);
  1881. }
  1882.  
  1883. function makeBaseButton() {
  1884. const btn = document.createElement('button');
  1885. btn.type = 'button';
  1886. btn.className =
  1887. 'inline-flex items-center justify-center gap-x-2 rounded-full ' +
  1888. 'text-sm font-medium transition-colors focus-visible:outline-none ' +
  1889. 'focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none ' +
  1890. 'disabled:opacity-50 border border-input text-primary ' +
  1891. 'hover:border-primary/15 shadow-sm hover:bg-overlay-hover ' +
  1892. 'h-9 px-3 py-2 bg-surface-l1 pointer-events-auto';
  1893. btn.style.margin = '0';
  1894. btn.style.pointerEvents = 'auto';
  1895. btn.style.transition = 'opacity 0.15s ease, transform 0.12s ease, background-color 0.15s ease, box-shadow 0.15s ease';
  1896. return btn;
  1897. }
  1898.  
  1899. function update2kButton() {
  1900. const btn = document.getElementById(BTN_2K_ID);
  1901. if (!btn) return;
  1902. const enabled = isForce2kEnabled();
  1903. btn.textContent = enabled ? '2K' : '1K';
  1904. btn.title = enabled ? 'Сейчас 2K' : 'Сейчас 1K';
  1905. btn.style.background = '';
  1906. btn.style.boxShadow = '';
  1907. }
  1908.  
  1909. function updateCountButton() {
  1910. const btn = document.getElementById(BTN_COUNT_ID);
  1911. if (!btn) return;
  1912. const count = getForcedImageCount();
  1913. btn.textContent = `x${count}`;
  1914. btn.title = `Сейчас x${count}`;
  1915. btn.style.background = '';
  1916. btn.style.boxShadow = '';
  1917. }
  1918.  
  1919. function downloadTextFile(text, filename) {
  1920. const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
  1921. const url = URL.createObjectURL(blob);
  1922. const a = document.createElement('a');
  1923. a.href = url;
  1924. a.download = filename;
  1925. a.style.display = 'none';
  1926. document.body.appendChild(a);
  1927. a.click();
  1928. a.remove();
  1929. setTimeout(() => URL.revokeObjectURL(url), 30000);
  1930. }
  1931.  
  1932. async function downloadPersistentLog() {
  1933. const text = getPersistentLog() || `${TAG} log is empty`;
  1934. const filename = `xai-tools-log ${fileStamp()}.txt`;
  1935. downloadTextFile(text, filename);
  1936. try {
  1937. if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
  1938. await navigator.clipboard.writeText(text);
  1939. log('persistent log downloaded and copied to clipboard', 'warn');
  1940. } else {
  1941. log('persistent log downloaded', 'warn');
  1942. }
  1943. } catch (e) {
  1944. log(`persistent log downloaded, clipboard copy failed: ${e.message}`, 'error');
  1945. }
  1946. }
  1947.  
  1948.  
  1949. function isLogButtonVisible() {
  1950. try {
  1951. return localStorage.getItem(SHOW_LOG_BTN_KEY) === '1';
  1952. } catch {
  1953. return false;
  1954. }
  1955. }
  1956.  
  1957. function setLogButtonVisible(visible) {
  1958. try {
  1959. localStorage.setItem(SHOW_LOG_BTN_KEY, visible ? '1' : '0');
  1960. } catch {}
  1961. return !!visible;
  1962. }
  1963.  
  1964. function updateLogButtonVisibility() {
  1965. const btn = document.getElementById(BTN_LOG_ID);
  1966. if (!btn) return;
  1967. const visible = isLogButtonVisible();
  1968. btn.style.display = visible ? '' : 'none';
  1969. positionPanel();
  1970. }
  1971.  
  1972.  
  1973. function createPanel() {
  1974. if (!document.body || document.getElementById(PANEL_ID)) return;
  1975.  
  1976. const panel = document.createElement('div');
  1977. panel.id = PANEL_ID;
  1978. panel.style.position = 'fixed';
  1979. panel.style.display = 'flex';
  1980. panel.style.gap = `${GAP_PX}px`;
  1981. panel.style.alignItems = 'center';
  1982. panel.style.zIndex = '9999';
  1983. panel.style.opacity = '0';
  1984. panel.style.pointerEvents = 'auto';
  1985. panel.style.transition = 'opacity 0.15s ease';
  1986. panel.style.left = '16px';
  1987. panel.style.top = '16px';
  1988.  
  1989. const btn2k = makeBaseButton();
  1990. btn2k.id = BTN_2K_ID;
  1991. btn2k.style.minWidth = '48px';
  1992. btn2k.addEventListener('click', () => {
  1993. const next = !isForce2kEnabled();
  1994. setForce2kEnabled(next);
  1995. update2kButton();
  1996. positionPanel();
  1997. log(`2K forcing ${next ? 'enabled' : 'disabled'}`, 'warn');
  1998. });
  1999.  
  2000.  
  2001. const btnCount = makeBaseButton();
  2002. btnCount.id = BTN_COUNT_ID;
  2003. btnCount.style.minWidth = '48px';
  2004. btnCount.addEventListener('click', () => {
  2005. const next = cycleForcedImageCount();
  2006. pendingManualCountPick = 0;
  2007. pendingManualCountPickedAt = 0;
  2008. preferStoredImageCount(next, 15000, 'panel-button');
  2009. updateCountButton();
  2010. positionPanel();
  2011. log(`forced image count -> x${next}`, 'warn');
  2012. syncForcedImageCountUi('panel-button').catch(e => log(`count UI sync failed: ${e.message}`, 'error'));
  2013. });
  2014.  
  2015. const btnRetry = makeBaseButton();
  2016. btnRetry.id = BTN_RETRY_ID;
  2017. btnRetry.textContent = 'Retry';
  2018. btnRetry.style.minWidth = '62px';
  2019. btnRetry.title = 'Вернуть прошлый вариант и сразу запустить';
  2020. btnRetry.addEventListener('click', () => smartRetryAction().catch(e => log(`smart Retry failed: ${e.message}`, 'error')));
  2021.  
  2022.  
  2023. const btnReset = makeBaseButton();
  2024. btnReset.id = BTN_RESET_ID;
  2025. btnReset.textContent = 'New';
  2026. btnReset.style.minWidth = '62px';
  2027. btnReset.title = 'Чистый composer после reset';
  2028. btnReset.addEventListener('click', () => newComposerAction().catch(e => log(`New action failed: ${e.message}`, 'error')));
  2029.  
  2030. const btnLog = makeBaseButton();
  2031. btnLog.id = BTN_LOG_ID;
  2032. btnLog.textContent = 'Log';
  2033. btnLog.style.minWidth = '54px';
  2034. btnLog.title = 'Download debug log';
  2035. btnLog.addEventListener('click', () => downloadPersistentLog().catch(e => log(`log download failed: ${e.message}`, 'error')));
  2036.  
  2037. panel.appendChild(btn2k);
  2038. panel.appendChild(btnCount);
  2039. panel.appendChild(btnRetry);
  2040. panel.appendChild(btnReset);
  2041. panel.appendChild(btnLog);
  2042. document.body.appendChild(panel);
  2043.  
  2044. update2kButton();
  2045. updateCountButton();
  2046. updateLogButtonVisibility();
  2047. }
  2048.  
  2049. function findToolbarAnchor() {
  2050. const textarea = getPromptTextarea();
  2051. const buttons = Array.from(document.querySelectorAll('button[aria-haspopup="menu"]')).filter(el => {
  2052. if (!(el instanceof HTMLElement)) return false;
  2053. if (!isVisible(el)) return false;
  2054. if (el.closest(`#${PANEL_ID}`)) return false;
  2055. const span = el.querySelector('span');
  2056. const chevron = el.querySelector('svg.lucide-chevrons-up-down');
  2057. return span?.textContent?.trim() === 'Image' && chevron;
  2058. });
  2059. if (!buttons.length) return null;
  2060. if (!textarea) return buttons[0] || null;
  2061.  
  2062. const textareaRect = textarea.getBoundingClientRect();
  2063. return buttons.map((btn, index) => {
  2064. const rect = btn.getBoundingClientRect();
  2065. const dx = Math.abs(rect.left - textareaRect.right);
  2066. const dy = Math.abs((rect.top + rect.height / 2) - (textareaRect.top + textareaRect.height / 2));
  2067. const score = dx + (dy * 2) + index;
  2068. return { btn, score };
  2069. }).sort((a, b) => a.score - b.score)[0]?.btn || buttons[0] || null;
  2070. }
  2071.  
  2072. function isViewerOpen() {
  2073. const controls = Array.from(document.querySelectorAll('button, a, [role="button"]')).filter(el => {
  2074. if (!isVisible(el)) return false;
  2075. if (el.closest(`#${PANEL_ID}`)) return false;
  2076. return true;
  2077. });
  2078. const hasDownload = controls.some(el => /\bdownload\b/.test(getText(el)));
  2079. const hasEdit = controls.some(el => /\bedit\b/.test(getText(el)));
  2080. return hasDownload && hasEdit;
  2081. }
  2082.  
  2083. function positionPanel() {
  2084. const panel = document.getElementById(PANEL_ID);
  2085. if (!panel) return;
  2086. if (isViewerOpen()) {
  2087. panel.style.opacity = '0';
  2088. panel.style.pointerEvents = 'none';
  2089. return;
  2090. }
  2091. const target = findToolbarAnchor();
  2092. if (!target) {
  2093. panel.style.opacity = '0';
  2094. panel.style.pointerEvents = 'none';
  2095. return;
  2096. }
  2097. const rect = target.getBoundingClientRect();
  2098. const panelRect = panel.getBoundingClientRect();
  2099. let left = rect.left - panelRect.width - GAP_PX;
  2100. let top = rect.top + ((rect.height - panelRect.height) / 2);
  2101. if (left < 16) left = 16;
  2102. if (top < 12) top = 12;
  2103. panel.style.left = `${left}px`;
  2104. panel.style.top = `${top}px`;
  2105. panel.style.opacity = '1';
  2106. panel.style.pointerEvents = 'auto';
  2107. }
  2108.  
  2109. function scoreImage(img) {
  2110. const rect = img.getBoundingClientRect();
  2111. return (img.naturalWidth || rect.width || 0) * (img.naturalHeight || rect.height || 0);
  2112. }
  2113.  
  2114. function isVariantImage(img) {
  2115. if (!(img instanceof HTMLImageElement)) return false;
  2116. if (!isVisible(img)) return false;
  2117. const src = img.currentSrc || img.src || '';
  2118. const alt = (img.alt || '').toLowerCase();
  2119. if (!src) return false;
  2120. const looksRelevant = src.startsWith('data:image/') || src.startsWith('blob:') || src.startsWith('http') || alt.includes('variant');
  2121. if (!looksRelevant) return false;
  2122. const w = img.naturalWidth || img.width || 0;
  2123. const h = img.naturalHeight || img.height || 0;
  2124. return w >= 256 && h >= 256;
  2125. }
  2126.  
  2127. function findImageNearTrigger(trigger) {
  2128. let node = trigger.closest('button, a, [role="button"]');
  2129. for (let i = 0; i < 10 && node; i++, node = node.parentElement) {
  2130. const imgs = Array.from(node.querySelectorAll('img')).filter(isVariantImage);
  2131. if (imgs.length) {
  2132. imgs.sort((a, b) => scoreImage(b) - scoreImage(a));
  2133. return imgs[0];
  2134. }
  2135. }
  2136. const all = Array.from(document.querySelectorAll('img')).filter(isVariantImage);
  2137. all.sort((a, b) => scoreImage(b) - scoreImage(a));
  2138. return all[0] || null;
  2139. }
  2140.  
  2141. function loadImage(src) {
  2142. return new Promise((resolve, reject) => {
  2143. const img = new Image();
  2144. img.decoding = 'async';
  2145. img.onload = () => resolve(img);
  2146. img.onerror = () => reject(new Error('image load failed'));
  2147. img.src = src;
  2148. });
  2149. }
  2150.  
  2151. function canvasToBlob(canvas, type, quality) {
  2152. return new Promise((resolve, reject) => {
  2153. canvas.toBlob(blob => blob ? resolve(blob) : reject(new Error('canvas.toBlob failed')), type, quality);
  2154. });
  2155. }
  2156.  
  2157. async function imageSrcToJpegBlob(src) {
  2158. const img = await loadImage(src);
  2159. const width = img.naturalWidth || img.width;
  2160. const height = img.naturalHeight || img.height;
  2161. if (!width || !height) throw new Error('invalid image dimensions');
  2162. const canvas = document.createElement('canvas');
  2163. canvas.width = width;
  2164. canvas.height = height;
  2165. const ctx = canvas.getContext('2d', { alpha: false });
  2166. ctx.fillStyle = '#ffffff';
  2167. ctx.fillRect(0, 0, width, height);
  2168. ctx.drawImage(img, 0, 0);
  2169. return await canvasToBlob(canvas, 'image/jpeg', JPEG_QUALITY);
  2170. }
  2171.  
  2172. function makeTimestampRu() {
  2173. const d = new Date();
  2174. return `${pad2(d.getDate())}.${pad2(d.getMonth() + 1)}.${d.getFullYear()} ${pad2(d.getHours())}-${pad2(d.getMinutes())}-${pad2(d.getSeconds())}`;
  2175. }
  2176.  
  2177. function makeFilename() {
  2178. return `Grok API - ${makeTimestampRu()}.jpg`;
  2179. }
  2180.  
  2181. function downloadBlob(blob, filename) {
  2182. const url = URL.createObjectURL(blob);
  2183. const a = document.createElement('a');
  2184. a.href = url;
  2185. a.download = filename;
  2186. a.style.display = 'none';
  2187. document.body.appendChild(a);
  2188. a.click();
  2189. a.remove();
  2190. setTimeout(() => URL.revokeObjectURL(url), 30000);
  2191. }
  2192.  
  2193. function markBypass(el) {
  2194. if (!(el instanceof Element)) return;
  2195. el.setAttribute(BYPASS_ATTR, '1');
  2196. setTimeout(() => {
  2197. try { el.removeAttribute(BYPASS_ATTR); } catch {}
  2198. }, 300);
  2199. }
  2200.  
  2201. function invokeOriginalDownload(trigger) {
  2202. if (!(trigger instanceof HTMLElement)) return;
  2203. markBypass(trigger);
  2204. trigger.click();
  2205. }
  2206.  
  2207. function isDownloadTrigger(el) {
  2208. if (!(el instanceof Element)) return false;
  2209. const trigger = el.closest('button, a, [role="button"]');
  2210. if (!trigger) return false;
  2211. if (trigger.getAttribute(BYPASS_ATTR) === '1') return false;
  2212. if ([BTN_2K_ID, BTN_COUNT_ID, BTN_RETRY_ID, BTN_RESET_ID, BTN_LOG_ID].includes(trigger.id)) return false;
  2213. if (trigger.closest(`#${PANEL_ID}`)) return false;
  2214. return /\bdownload\b|\bsave\b/.test(getText(trigger));
  2215. }
  2216.  
  2217. function installDownloadInterceptor() {
  2218. document.addEventListener('click', async (event) => {
  2219. const target = event.target;
  2220. if (!(target instanceof Element)) return;
  2221. if (!isDownloadTrigger(target)) return;
  2222. const trigger = target.closest('button, a, [role="button"]');
  2223. if (!trigger) return;
  2224. const img = findImageNearTrigger(trigger);
  2225. if (!img) return;
  2226. const src = img.currentSrc || img.src || '';
  2227. if (!src) return;
  2228.  
  2229. event.preventDefault();
  2230. event.stopPropagation();
  2231. if (typeof event.stopImmediatePropagation === 'function') event.stopImmediatePropagation();
  2232.  
  2233. try {
  2234. const jpegBlob = await imageSrcToJpegBlob(src);
  2235. const filename = makeFilename();
  2236. downloadBlob(jpegBlob, filename);
  2237. log(`saved as jpeg: ${filename}`);
  2238. } catch (e) {
  2239. log(`jpeg conversion failed, fallback to native: ${e.message}`, 'error');
  2240. invokeOriginalDownload(trigger);
  2241. }
  2242. }, true);
  2243. }
  2244.  
  2245. function installHighDemandWatcher() {
  2246. const observer = new MutationObserver(() => {
  2247. updateGlitchSubmitProxyState();
  2248. if (highDemandRecoveryInFlight > 0) return;
  2249. if (!hasHighDemandBanner()) return;
  2250. scheduleHighDemandWatch('mutation', 180);
  2251. });
  2252.  
  2253. observer.observe(document.documentElement, {
  2254. childList: true,
  2255. subtree: true
  2256. });
  2257. }
  2258.  
  2259.  
  2260. function installPanelHotkeys() {
  2261. document.addEventListener('keydown', (event) => {
  2262. if (!(event.ctrlKey && event.key === 'F12')) return;
  2263. if (event.altKey || event.shiftKey || event.metaKey) return;
  2264. event.preventDefault();
  2265. const next = !isLogButtonVisible();
  2266. setLogButtonVisible(next);
  2267. updateLogButtonVisibility();
  2268. log(`log button ${next ? 'shown' : 'hidden'} (Ctrl+F12)`, 'warn');
  2269. }, true);
  2270. }
  2271.  
  2272. function initUi() {
  2273. const rawRestore = (() => { try { return sessionStorage.getItem(RESTORE_STATE_KEY); } catch { return null; } })();
  2274. if (!rawRestore) {
  2275. setForce2kEnabled(false);
  2276. setForcedImageCount(DEFAULT_IMAGE_COUNT);
  2277. pendingManualCountPick = 0;
  2278. pendingManualCountPickedAt = 0;
  2279. clearPreferredStoredImageCount();
  2280. }
  2281. createPanel();
  2282. positionPanel();
  2283. installCountUiObserver();
  2284. installGlitchSubmitProxy();
  2285. installHighDemandWatcher();
  2286. installPanelHotkeys();
  2287. updateGlitchSubmitProxyState();
  2288. const mo = new MutationObserver(() => {
  2289. createPanel();
  2290. positionPanel();
  2291. updateGlitchSubmitProxyState();
  2292. });
  2293. mo.observe(document.documentElement, {
  2294. childList: true,
  2295. subtree: true
  2296. });
  2297. const throttled = (() => {
  2298. let raf = 0;
  2299. return () => {
  2300. cancelAnimationFrame(raf);
  2301. raf = requestAnimationFrame(() => {
  2302. positionPanel();
  2303. });
  2304. };
  2305. })();
  2306. window.addEventListener('resize', throttled);
  2307. window.addEventListener('scroll', throttled, { passive: true });
  2308. setTimeout(() => { positionPanel(); updateGlitchSubmitProxyState(); }, 150);
  2309. setTimeout(() => { positionPanel(); updateGlitchSubmitProxyState(); }, 500);
  2310. setTimeout(() => { positionPanel(); updateGlitchSubmitProxyState(); }, 1200);
  2311. scheduleCountUiStartupSync();
  2312. }
  2313.  
  2314. installDownloadInterceptor();
  2315.  
  2316. if (document.readyState !== 'loading') {
  2317. flushPersistentLogToConsole();
  2318. initUi();
  2319. restoreStateAfterReset().catch(e => log(`restore init failed: ${e.message}`, 'error'));
  2320. } else {
  2321. document.addEventListener('DOMContentLoaded', () => {
  2322. flushPersistentLogToConsole();
  2323. initUi();
  2324. restoreStateAfterReset().catch(e => log(`restore init failed: ${e.message}`, 'error'));
  2325. }, { once: true });
  2326. }
  2327.  
  2328. log(`ready; 2K forcing is ${isForce2kEnabled() ? 'ON' : 'OFF'}, image count is x${getForcedImageCount()}`, 'warn');
  2329. })();
Add Comment
Please, Sign In to add comment