Includes "Session Navigation", Video/Image classification, Import/Export, and strict Focus-based Sync.
// ==UserScript==
// @name Grok Auto-Retry + Prompt Snippets + History + Favorites (v33 - Smart Sync)
// @namespace http://tampermonkey.net/
// @version 33
// @description Includes "Session Navigation", Video/Image classification, Import/Export, and strict Focus-based Sync.
// @author You
// @license MIT
// @match https://grok.com/*
// @match https://*.grok.com/*
// @match https://grok.x.ai/*
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_setClipboard
// ==/UserScript==
(function() {
'use strict';
// --- CONFIGURATION ---
// User provided selector for video:
const TARGET_TEXTAREA_SELECTOR = 'textarea[aria-label="Make a video"]';
// Image selectors
const IMAGE_EDITOR_SELECTOR = 'textarea[aria-label="Type to edit image..."]';
const IMAGE_PROMPT_SELECTOR = 'textarea[aria-label="Image prompt"]';
const IMAGE_IMAGINE_SELECTOR = 'p[data-placeholder="Type to imagine"]';
// Buttons
const RETRY_BUTTON_SELECTOR = 'button[aria-label="Make video"]';
const IMAGE_EDITOR_BUTTON_SELECTOR = 'button[aria-label="Generate"]';
const IMAGE_SUBMIT_BUTTON_SELECTOR = 'button[aria-label="Submit"]';
const MODERATION_PATTERNS = [
"content moderated",
"try a different idea",
"moderated",
"content policy",
"cannot generate",
"unable to generate"
];
const RETRY_DELAY_MS = 1500;
const OBSERVER_THROTTLE_MS = 300;
const MAX_HISTORY_ITEMS = 100;
const DEBUG_MODE = true;
// --- DEFAULT SNIPPETS ---
const DEFAULT_SNIPPETS = [
{
id: 'b1', label: 'Anime Stickers (Provocative)',
text: 'Surrounding the central image: thick decorative border made of overlapping colorful anime-style stickers featuring nude anime girls with exaggerated proportions in various provocative poses. Each sticker has a white outline and slight drop shadow. The stickers completely frame all four edges of the image with some overlap into the main content.'
},
{
id: 'b2', label: 'Anime Stickers (SFW)',
text: 'Surrounding the central image: thick decorative border made of overlapping colorful anime-style stickers featuring anime girls with exaggerated proportions in various poses. Each sticker has a white outline and slight drop shadow. The stickers completely frame all four edges of the image with some overlap into the main content.'
},
{ id: '1', label: 'Motion: Slow Mo', text: 'slow motion, high frame rate, smooth movement' },
{ id: '2', label: 'Style: Photorealistic', text: 'photorealistic, 8k resolution, highly detailed, unreal engine 5 render' },
{ id: '3', label: 'Lighting: Golden Hour', text: 'golden hour lighting, warm sun rays, lens flare, soft shadows' },
];
// --- LOAD SAVED SETTINGS ---
let maxRetries = GM_getValue('maxRetries', 5);
let uiToggleKey = GM_getValue('uiToggleKey', 'h');
let autoClickEnabled = GM_getValue('autoClickEnabled', true);
let isUiVisible = GM_getValue('isUiVisible', true);
let savedSnippets = GM_getValue('savedSnippets', DEFAULT_SNIPPETS);
let videoPromptHistory = GM_getValue('videoPromptHistory', []);
let imagePromptHistory = GM_getValue('imagePromptHistory', []);
let videoFavorites = GM_getValue('videoFavorites', []);
let imageFavorites = GM_getValue('imageFavorites', []);
let panelSize = GM_getValue('panelSize', { width: '300px', height: '460px' });
// --- LOAD SAVED POSITIONS ---
let mainPos = GM_getValue('pos_main', { top: 'auto', left: 'auto', bottom: '20px', right: '20px' });
let libPos = GM_getValue('pos_lib', { top: '100px', left: '100px' });
let favPos = GM_getValue('pos_fav', { top: '120px', left: '120px' });
let histPos = GM_getValue('pos_hist', { top: '140px', left: '140px' });
let isRetryEnabled = true;
let limitReached = false;
let currentRetryCount = 0;
let lastTypedPrompt = "";
let lastGenerationTimestamp = 0;
const GENERATION_COOLDOWN_MS = 3000;
let observerThrottle = false;
let moderationDetected = false;
let processingModeration = false;
let currentHistoryTab = 'video';
let currentFavoritesTab = 'video';
let currentEditingFavId = null;
let lastModerationCheck = 0;
let errorWaitInterval = null;
// --- SESSION NAV VARIABLES ---
let sessionPrompts = [""];
let sessionIndex = 0;
// --- DEBUG LOGGER ---
function debugLog(...args) {
if (DEBUG_MODE) {
console.log('[Grok Tools]', ...args);
}
}
// --- STYLES ---
GM_addStyle(`
#grok-control-panel {
position: fixed;
width: ${panelSize.width}; height: ${panelSize.height};
min-width: 280px; min-height: 250px; max-width: 90vw; max-height: 90vh;
background: linear-gradient(145deg, #0a0a0a 0%, #1a1a1a 100%);
border: 1px solid #2a2a2a;
border-radius: 16px;
padding: 15px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
color: #e0e0e0;
z-index: 99990;
box-shadow: 0 8px 32px rgba(0,0,0,0.9), 0 0 0 1px rgba(255,255,255,0.05);
display: flex;
flex-direction: column;
gap: 10px;
}
#grok-control-panel.hidden { display: none !important; }
.grok-header, .gl-header {
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
margin-left: 10px;
padding-bottom: 8px;
border-bottom: 1px solid #2a2a2a;
cursor: move;
user-select: none;
}
.gl-header {
padding: 12px 15px;
margin-left: 0;
background: linear-gradient(135deg, #1a1a1a 0%, #0f0f0f 100%);
border-radius: 16px 16px 0 0;
font-weight: bold; font-size: 13px; color: #f0f0f0;
}
#grok-resize-handle {
position: absolute; top: 0; left: 0; width: 15px; height: 15px;
cursor: nwse-resize; z-index: 99999;
}
#grok-resize-handle::after {
content: ''; position: absolute; top: 2px; left: 2px;
border-top: 6px solid #3b82f6; border-right: 6px solid transparent;
width: 0; height: 0; opacity: 0.7;
}
#grok-resize-handle:hover::after { opacity: 1; border-top-color: #60a5fa; }
.grok-title {
font-weight: bold; font-size: 14px; color: #f0f0f0;
text-shadow: 0 2px 4px rgba(0,0,0,0.5); pointer-events: none;
}
.grok-toggle-btn {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
border: none; color: white; padding: 6px 14px; border-radius: 20px;
font-size: 11px; font-weight: bold; cursor: pointer;
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.4);
transition: all 0.2s ease;
}
.grok-toggle-btn.off {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.4);
}
.grok-controls {
display: flex; align-items: center; justify-content: space-between;
font-size: 12px; color: #9ca3af; flex-shrink: 0; padding: 8px 0;
}
.grok-checkbox { display: flex; align-items: center; cursor: pointer; color: #d1d5db; }
.grok-checkbox input { margin-right: 6px; cursor: pointer; accent-color: #3b82f6; }
.grok-num-input {
width: 40px; background: #1f1f1f; border: 1px solid #2a2a2a;
color: #e0e0e0; border-radius: 6px; padding: 4px 6px; text-align: center;
}
/* NAV BUTTON STYLES */
.grok-prompt-header-row {
display: flex; justify-content: space-between; align-items: center; margin-bottom: -5px; flex-shrink: 0;
}
.grok-prompt-label {
font-size: 11px; font-weight: bold; color: #9ca3af;
}
.grok-nav-container { display: flex; gap: 4px; align-items: center; }
.grok-nav-btn {
background: #1f1f1f; border: 1px solid #333; color: #888;
cursor: pointer; padding: 2px 8px; border-radius: 4px;
font-size: 10px; transition: all 0.2s; min-width: 25px;
}
.grok-nav-btn:hover:not(:disabled) { background: #333; color: #fff; border-color: #555; }
.grok-nav-btn:disabled { opacity: 0.3; cursor: default; }
#grok-panel-prompt {
width: 100%; flex-grow: 1; background: #0f0f0f; border: 1px solid #2a2a2a;
border-radius: 8px; color: #e0e0e0; padding: 10px; font-size: 12px;
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace; resize: none;
box-sizing: border-box; transition: all 0.2s ease; margin-top: 5px;
}
#grok-panel-prompt:focus {
border-color: #3b82f6; outline: none; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}
.grok-btn-row { display: flex; gap: 8px; flex-shrink: 0; }
.grok-action-btn {
flex: 1; padding: 10px; border-radius: 8px; border: none; cursor: pointer;
font-weight: 600; font-size: 12px; transition: all 0.2s ease;
position: relative; overflow: hidden;
}
.grok-action-btn:hover { transform: translateY(-1px); }
#btn-open-library { background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); color: white; }
#btn-open-favorites { background: linear-gradient(135deg, #ec4899 0%, #db2777 100%); color: white; }
#btn-open-history { background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%); color: white; }
#btn-generate { background: linear-gradient(135deg, #1f1f1f 0%, #2a2a2a 100%); color: #e0e0e0; border: 1px solid #3a3a3a; }
#grok-status {
text-align: center; font-size: 11px; color: #10b981; padding-top: 8px;
border-top: 1px solid #2a2a2a; flex-shrink: 0; font-weight: 500;
}
.status-error { color: #ef4444 !important; }
.status-warning { color: #f59e0b !important; }
/* Import/Export Panel */
#grok-io-container.io-hidden { display: none; }
#grok-io-container {
display: flex; gap: 8px; margin-top: 5px; padding-top: 5px; border-top: 1px solid #2a2a2a;
}
.io-btn {
flex: 1; background: #1f1f1f; color: #9ca3af; border: 1px solid #2a2a2a;
border-radius: 6px; padding: 5px; font-size: 10px; cursor: pointer;
}
.io-btn:hover { background: #333; color: #fff; }
/* Modal Styles */
.grok-modal {
position: fixed;
width: 350px; height: 400px;
background: linear-gradient(145deg, #0a0a0a 0%, #1a1a1a 100%);
border: 1px solid #2a2a2a;
border-radius: 16px;
display: none; flex-direction: column;
z-index: 99995;
box-shadow: 0 8px 32px rgba(0,0,0,0.9), 0 0 0 1px rgba(255,255,255,0.05);
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}
.grok-modal.active { display: flex; }
.gl-close {
cursor: pointer; font-size: 20px; line-height: 1; color: #6b7280;
width: 24px; height: 24px; display: flex; align-items: center; justify-content: center;
}
.gl-close:hover { color: #f0f0f0; }
/* History Tab Styles */
.history-tabs { display: flex; background: #0f0f0f; border-bottom: 1px solid #2a2a2a; }
.history-tab {
flex: 1; padding: 10px; text-align: center; cursor: pointer;
font-size: 11px; font-weight: 600; color: #6b7280; border-bottom: 2px solid transparent;
}
.history-tab:hover { color: #9ca3af; background: #1a1a1a; }
.history-tab.active { color: #8b5cf6; border-bottom-color: #8b5cf6; background: #1a1a1a; }
.gl-view-list { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
.gl-list-content { overflow-y: auto; padding: 12px; flex: 1; display: flex; flex-direction: column; gap: 8px; }
.gl-list-content::-webkit-scrollbar { width: 8px; }
.gl-list-content::-webkit-scrollbar-thumb { background: #2a2a2a; border-radius: 4px; }
.gl-item {
background: linear-gradient(135deg, #1a1a1a 0%, #151515 100%);
border: 1px solid #2a2a2a; padding: 10px; border-radius: 8px;
display: flex; justify-content: space-between; align-items: center;
font-size: 12px; color: #e0e0e0; transition: all 0.2s ease;
}
.gl-item:hover { border-color: #3b82f6; background: linear-gradient(135deg, #1f1f1f 0%, #1a1a1a 100%); }
.gl-item-text { cursor: pointer; flex: 1; margin-right: 10px; }
.gl-item-text b { display: block; margin-bottom: 4px; color: #f0f0f0; }
.gl-item-text span {
color: #9ca3af; font-size: 10px; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.gl-item-actions { display: flex; gap: 6px; }
.gl-icon-btn {
background: #1f1f1f; border: 1px solid #2a2a2a; cursor: pointer;
font-size: 14px; color: #9ca3af; padding: 6px; border-radius: 6px;
width: 28px; height: 28px; display: flex; align-items: center; justify-content: center;
}
.gl-icon-btn:hover { color: #f0f0f0; background: #2a2a2a; transform: scale(1.1); }
.gl-icon-btn.favorite { color: #ec4899; }
.gl-create-btn, .history-clear-btn {
margin: 12px; padding: 10px; background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white; text-align: center; border-radius: 8px; cursor: pointer;
font-weight: 600; font-size: 12px;
}
.history-clear-btn { background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); }
.gl-view-editor { display: none; flex-direction: column; padding: 15px; height: 100%; gap: 10px; }
.gl-view-editor.active { display: flex; }
.gl-input, .gl-textarea {
background: #0f0f0f; border: 1px solid #2a2a2a; color: #e0e0e0;
padding: 10px; border-radius: 8px; font-size: 12px; width: 100%; box-sizing: border-box;
}
.gl-input:focus, .gl-textarea:focus { border-color: #3b82f6; outline: none; }
.gl-textarea { flex-grow: 1; resize: none; font-family: 'SF Mono', 'Monaco', 'Consolas', monospace; }
.gl-editor-buttons { display: flex; gap: 10px; margin-top: auto; }
.gl-btn { flex: 1; padding: 10px; border-radius: 8px; border: none; cursor: pointer; font-weight: 600; color: white; font-size: 12px; }
.gl-btn-save { background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); }
.gl-btn-cancel { background: linear-gradient(135deg, #374151 0%, #1f2937 100%); }
.history-item-time { font-size: 9px; color: #6b7280; margin-top: 3px; }
`);
// --- DOM CREATION (MAIN PANEL) ---
const panel = document.createElement('div');
panel.id = 'grok-control-panel';
// Apply saved position
if (mainPos.top !== 'auto') {
panel.style.top = mainPos.top;
panel.style.left = mainPos.left;
panel.style.bottom = 'auto';
panel.style.right = 'auto';
} else {
panel.style.bottom = mainPos.bottom;
panel.style.right = mainPos.right;
}
if (!isUiVisible) panel.classList.add('hidden');
panel.innerHTML = `
<div id="grok-resize-handle" title="Drag to Resize"></div>
<div class="grok-header" id="grok-main-header">
<span class="grok-title">Grok Tools v33</span>
<button id="grok-toggle-btn" class="grok-toggle-btn">ON</button>
</div>
<div class="grok-controls">
<label class="grok-checkbox">
<input type="checkbox" id="grok-autoclick-cb" ${autoClickEnabled ? 'checked' : ''}> Auto-Retry
</label>
<div>
Max: <input type="number" id="grok-retry-limit" value="${maxRetries}" class="grok-num-input" min="1">
</div>
</div>
<div class="grok-prompt-header-row">
<div class="grok-prompt-label">Prompt Editor</div>
<div class="grok-nav-container">
<button id="btn-sess-prev" class="grok-nav-btn" title="Previous Prompt (Alt+Left)" disabled>◀</button>
<button id="btn-sess-next" class="grok-nav-btn" title="Next Prompt (Alt+Right)" disabled>▶</button>
</div>
</div>
<textarea id="grok-panel-prompt" placeholder="Type or paste prompt here..."></textarea>
<div class="grok-btn-row">
<button id="btn-open-library" class="grok-action-btn">Snippets</button>
<button id="btn-open-favorites" class="grok-action-btn">❤️</button>
<button id="btn-open-history" class="grok-action-btn">History</button>
<button id="btn-generate" class="grok-action-btn">Generate</button>
</div>
<div id="grok-status">Ready</div>
<div id="grok-io-container" class="io-hidden">
<button id="btn-export-all" class="io-btn">Export Data ⬇️</button>
<button id="btn-import-all" class="io-btn">Import Data ⬆️</button>
<input type="file" id="grok-import-file" style="display:none" accept=".json">
</div>
<div style="font-size:9px; color:#555; text-align:center;">UI: Alt+${uiToggleKey.toUpperCase()} | I/O: Alt+I</div>
`;
document.body.appendChild(panel);
// --- LIBRARY MODAL ---
const modal = document.createElement('div');
modal.id = 'grok-library-modal';
modal.className = 'grok-modal';
modal.style.top = libPos.top;
modal.style.left = libPos.left;
modal.innerHTML = `
<div class="gl-header" id="lib-header"><span>Snippets Library</span><span class="gl-close">×</span></div>
<div class="gl-view-list" id="gl-view-list">
<div class="gl-list-content" id="gl-list-container"></div>
<div class="gl-create-btn" id="btn-create-snippet">Create New Snippet</div>
</div>
<div class="gl-view-editor" id="gl-view-editor">
<label style="font-size:11px; color:#8b98a5;">Label</label>
<input type="text" class="gl-input" id="gl-edit-label" placeholder="e.g. Cinematic Lighting">
<label style="font-size:11px; color:#8b98a5;">Prompt Text</label>
<textarea class="gl-textarea" id="gl-edit-text" placeholder="Content to append..."></textarea>
<div class="gl-editor-buttons">
<button class="gl-btn gl-btn-cancel" id="btn-edit-cancel">Cancel</button>
<button class="gl-btn gl-btn-save" id="btn-edit-save">Save Snippet</button>
</div>
</div>
`;
document.body.appendChild(modal);
// --- FAVORITES MODAL ---
const favoritesModal = document.createElement('div');
favoritesModal.id = 'grok-favorites-modal';
favoritesModal.className = 'grok-modal';
favoritesModal.style.top = favPos.top;
favoritesModal.style.left = favPos.left;
favoritesModal.innerHTML = `
<div class="gl-header" id="fav-header"><span>Favorites ❤️</span><span class="gl-close favorites-close">×</span></div>
<div class="history-tabs">
<div class="history-tab active" data-tab="video">🎥 Video</div>
<div class="history-tab" data-tab="image">🖼️ Image</div>
</div>
<div class="gl-view-list" id="favorites-view-list">
<div class="gl-list-content" id="favorites-list-container"></div>
</div>
<div class="gl-view-editor" id="favorites-view-viewer">
<label style="font-size:11px; color:#8b98a5;">Name / Label</label>
<input type="text" class="gl-input" id="fav-edit-label" placeholder="Favorite Name">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; margin-top:10px;">
<label style="font-size:11px; color:#8b98a5;">Prompt Text</label>
<span id="favorites-viewer-time" style="font-size:9px; color:#6b7280;"></span>
</div>
<textarea class="gl-textarea" id="favorites-viewer-text"></textarea>
<div class="gl-editor-buttons">
<button class="gl-btn gl-btn-cancel" id="btn-fav-viewer-back">Cancel</button>
<button class="gl-btn gl-btn-save" id="btn-fav-viewer-save">Save Changes</button>
</div>
</div>
`;
document.body.appendChild(favoritesModal);
// --- HISTORY MODAL ---
const historyModal = document.createElement('div');
historyModal.id = 'grok-history-modal';
historyModal.className = 'grok-modal';
historyModal.style.top = histPos.top;
historyModal.style.left = histPos.left;
historyModal.innerHTML = `
<div class="gl-header" id="hist-header"><span>Prompt History (100 max)</span><span class="gl-close history-close">×</span></div>
<div class="history-tabs">
<div class="history-tab active" data-tab="video">🎥 Video</div>
<div class="history-tab" data-tab="image">🖼️ Image</div>
</div>
<div class="gl-view-list" id="history-view-list">
<div class="gl-list-content" id="history-list-container"></div>
<div class="history-clear-btn" id="btn-clear-history">Clear This Tab's History</div>
</div>
<div class="gl-view-editor history-viewer" id="history-view-viewer">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;">
<label style="font-size:11px; color:#9ca3af; font-weight: 600;">Full Prompt</label>
<span id="history-viewer-time" style="font-size:9px; color:#6b7280;"></span>
</div>
<textarea class="gl-textarea" id="history-viewer-text" readonly></textarea>
<div class="gl-editor-buttons">
<button class="gl-btn gl-btn-cancel" id="btn-viewer-back">← Back</button>
<button class="gl-btn gl-btn-save" id="btn-viewer-use">Use This Prompt</button>
</div>
</div>
`;
document.body.appendChild(historyModal);
// --- DRAGGABLE FUNCTIONALITY ---
function makeDraggable(element, handleSelector, saveKey) {
const handle = element.querySelector(handleSelector);
let isDragging = false;
let startX, startY, initialLeft, initialTop;
handle.addEventListener('mousedown', (e) => {
const allModals = document.querySelectorAll('.grok-modal, #grok-control-panel');
let maxZ = 99990;
allModals.forEach(el => {
const z = parseInt(window.getComputedStyle(el).zIndex) || 0;
if(z > maxZ) maxZ = z;
});
element.style.zIndex = maxZ + 1;
isDragging = true;
startX = e.clientX;
startY = e.clientY;
const rect = element.getBoundingClientRect();
initialLeft = rect.left;
initialTop = rect.top;
element.style.right = 'auto';
element.style.bottom = 'auto';
element.style.width = rect.width + 'px';
document.body.style.cursor = 'move';
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
element.style.left = `${initialLeft + dx}px`;
element.style.top = `${initialTop + dy}px`;
});
document.addEventListener('mouseup', () => {
if (isDragging) {
isDragging = false;
document.body.style.cursor = '';
const rect = element.getBoundingClientRect();
const pos = { top: rect.top + 'px', left: rect.left + 'px' };
GM_setValue(saveKey, pos);
}
});
}
makeDraggable(panel, '#grok-main-header', 'pos_main');
makeDraggable(modal, '#lib-header', 'pos_lib');
makeDraggable(favoritesModal, '#fav-header', 'pos_fav');
makeDraggable(historyModal, '#hist-header', 'pos_hist');
// --- RESIZE LOGIC ---
const resizeHandle = document.getElementById('grok-resize-handle');
let isResizing = false;
let rStartX, rStartY, rStartW, rStartH;
resizeHandle.addEventListener('mousedown', (e) => {
isResizing = true;
rStartX = e.clientX;
rStartY = e.clientY;
const rect = panel.getBoundingClientRect();
rStartW = rect.width;
rStartH = rect.height;
e.preventDefault();
e.stopPropagation();
document.body.style.cursor = 'nwse-resize';
});
document.addEventListener('mousemove', (e) => {
if (!isResizing) return;
const deltaX = rStartX - e.clientX;
const deltaY = rStartY - e.clientY;
const newWidth = Math.max(280, rStartW + deltaX);
const newHeight = Math.max(250, rStartH + deltaY);
panel.style.width = newWidth + 'px';
panel.style.height = newHeight + 'px';
});
document.addEventListener('mouseup', () => {
if (isResizing) {
isResizing = false;
document.body.style.cursor = '';
GM_setValue('panelSize', { width: panel.style.width, height: panel.style.height });
}
});
// --- HELPER FUNCTIONS ---
function nativeValueSet(el, value) {
if (!el) return;
const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set;
setter.call(el, value);
el.dispatchEvent(new Event('input', { bubbles: true }));
}
function resetState(msg) {
limitReached = false;
currentRetryCount = 0;
moderationDetected = false;
processingModeration = false;
if (errorWaitInterval) {
clearInterval(errorWaitInterval);
errorWaitInterval = null;
}
updateStatus(msg);
}
function updateStatus(msg, type) {
const statusText = document.getElementById('grok-status');
if (!statusText) return;
statusText.textContent = msg;
statusText.className = '';
if (type === 'error') statusText.classList.add('status-error');
if (type === 'warning') statusText.classList.add('status-warning');
}
function formatTimestamp(timestamp) {
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
if (diff < 60000) return 'Just now';
if (diff < 3600000) {
const mins = Math.floor(diff / 60000);
return `${mins} min${mins > 1 ? 's' : ''} ago`;
}
if (diff < 86400000) {
const hours = Math.floor(diff / 3600000);
return `${hours} hr${hours > 1 ? 's' : ''} ago`;
}
return date.toLocaleDateString();
}
function escapeHtml(text) {
return text ? text.replace(/&/g, "&").replace(/</g, "<") : '';
}
function toggleModal(targetModal) {
const allModals = document.querySelectorAll('.grok-modal, #grok-control-panel');
let maxZ = 99990;
allModals.forEach(el => {
const z = parseInt(window.getComputedStyle(el).zIndex) || 0;
if(z > maxZ) maxZ = z;
});
targetModal.style.zIndex = maxZ + 1;
if (targetModal.classList.contains('active')) {
targetModal.classList.remove('active');
} else {
targetModal.classList.add('active');
}
}
// Helper to find the active grok input easily
function getGrokInput() {
return document.querySelector(TARGET_TEXTAREA_SELECTOR) ||
document.querySelector(IMAGE_EDITOR_SELECTOR) ||
document.querySelector(IMAGE_PROMPT_SELECTOR);
}
function getGrokImagine() {
return document.querySelector(IMAGE_IMAGINE_SELECTOR);
}
// --- SESSION HISTORY (UNDO/REDO) LOGIC ---
const promptBox = document.getElementById('grok-panel-prompt');
const btnSessPrev = document.getElementById('btn-sess-prev');
const btnSessNext = document.getElementById('btn-sess-next');
function updateNavButtons() {
btnSessPrev.disabled = (sessionIndex <= 0);
btnSessNext.disabled = (sessionIndex >= sessionPrompts.length - 1);
}
function pushToSessionHistory(newText) {
if (newText === sessionPrompts[sessionIndex]) return; // No change
// If we are in the middle of history and type something new, discard the "future"
if (sessionIndex < sessionPrompts.length - 1) {
sessionPrompts = sessionPrompts.slice(0, sessionIndex + 1);
}
sessionPrompts.push(newText);
sessionIndex = sessionPrompts.length - 1;
updateNavButtons();
}
function navigateSession(direction) {
if (direction === -1 && sessionIndex > 0) {
sessionIndex--;
} else if (direction === 1 && sessionIndex < sessionPrompts.length - 1) {
sessionIndex++;
} else {
return;
}
const text = sessionPrompts[sessionIndex];
promptBox.value = text;
updateNavButtons();
updateStatus(`History: ${sessionIndex + 1}/${sessionPrompts.length}`);
}
btnSessPrev.addEventListener('click', () => navigateSession(-1));
btnSessNext.addEventListener('click', () => navigateSession(1));
// Keyboard Shortcuts for Nav (Alt+Left / Alt+Right) inside prompt box
promptBox.addEventListener('keydown', (e) => {
if (e.altKey && e.key === 'ArrowLeft') {
e.preventDefault();
navigateSession(-1);
}
if (e.altKey && e.key === 'ArrowRight') {
e.preventDefault();
navigateSession(1);
}
});
// --- STATE MANAGEMENT (Favorites/Permanent History) ---
function addToHistory(prompt, type) {
if (!prompt || !prompt.trim()) return;
const arr = type === 'image' ? imagePromptHistory : videoPromptHistory;
const filtered = arr.filter(item => item.text !== prompt);
filtered.unshift({ id: Date.now().toString(), text: prompt, timestamp: Date.now(), type: type });
const limited = filtered.slice(0, MAX_HISTORY_ITEMS);
if (type === 'image') { imagePromptHistory = limited; GM_setValue('imagePromptHistory', imagePromptHistory); }
else { videoPromptHistory = limited; GM_setValue('videoPromptHistory', videoPromptHistory); }
}
function addToFavorites(prompt, type) {
if (!prompt || !prompt.trim()) return;
const arr = type === 'image' ? imageFavorites : videoFavorites;
if (arr.some(item => item.text === prompt)) {
updateStatus(`Already in favorites!`, 'error');
setTimeout(() => updateStatus('Ready'), 2000);
return;
}
arr.unshift({
id: Date.now().toString(),
text: prompt,
label: prompt.length > 40 ? prompt.substring(0, 40) + '...' : prompt,
timestamp: Date.now(),
type: type
});
if (type === 'image') GM_setValue('imageFavorites', arr);
else GM_setValue('videoFavorites', arr);
updateStatus(`Added to favorites! ❤️`);
setTimeout(() => updateStatus('Ready'), 2000);
}
// --- IMPORT / EXPORT LOGIC ---
function getAllData() {
return {
savedSnippets,
videoPromptHistory,
imagePromptHistory,
videoFavorites,
imageFavorites,
settings: {
maxRetries,
uiToggleKey,
autoClickEnabled,
panelSize,
mainPos, libPos, favPos, histPos
},
version: 33
};
}
function restoreData(data) {
if (!data) return alert("Invalid data");
try {
if (data.savedSnippets) GM_setValue('savedSnippets', data.savedSnippets);
if (data.videoPromptHistory) GM_setValue('videoPromptHistory', data.videoPromptHistory);
if (data.imagePromptHistory) GM_setValue('imagePromptHistory', data.imagePromptHistory);
if (data.videoFavorites) GM_setValue('videoFavorites', data.videoFavorites);
if (data.imageFavorites) GM_setValue('imageFavorites', data.imageFavorites);
if (data.settings) {
const s = data.settings;
if (s.maxRetries) GM_setValue('maxRetries', s.maxRetries);
if (s.uiToggleKey) GM_setValue('uiToggleKey', s.uiToggleKey);
if (s.autoClickEnabled !== undefined) GM_setValue('autoClickEnabled', s.autoClickEnabled);
if (s.panelSize) GM_setValue('panelSize', s.panelSize);
if (s.mainPos) GM_setValue('pos_main', s.mainPos);
}
alert("Import successful! Reloading page...");
location.reload();
} catch (e) {
console.error(e);
alert("Error importing data. Check console.");
}
}
document.getElementById('btn-export-all').addEventListener('click', () => {
const data = getAllData();
const blob = new Blob([JSON.stringify(data, null, 2)], {type: 'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `grok_tools_backup_${new Date().toISOString().slice(0,10)}.json`;
a.click();
URL.revokeObjectURL(url);
updateStatus("Export complete!");
});
const fileInput = document.getElementById('grok-import-file');
document.getElementById('btn-import-all').addEventListener('click', () => {
fileInput.click();
});
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (evt) => {
try {
const json = JSON.parse(evt.target.result);
if (confirm("This will overwrite your current saved snippets/history/favorites. Continue?")) {
restoreData(json);
}
} catch (err) {
alert("Invalid JSON file.");
}
};
reader.readAsText(file);
// Reset
fileInput.value = '';
});
// --- RENDER FUNCTIONS (Snippets/Favorites/History) ---
// (Snippet rendering logic kept same as v29, just condensed for length)
function renderSnippets() {
const listContainer = document.getElementById('gl-list-container');
listContainer.innerHTML = '';
savedSnippets.forEach(item => {
const el = document.createElement('div');
el.className = 'gl-item';
el.innerHTML = `
<div class="gl-item-text"><b>${escapeHtml(item.label)}</b><span>${escapeHtml(item.text)}</span></div>
<div class="gl-item-actions"><button class="gl-icon-btn gl-btn-edit">✎</button><button class="gl-icon-btn gl-btn-del">🗑</button></div>`;
el.querySelector('.gl-item-text').addEventListener('click', () => {
const cur = promptBox.value;
const newText = cur + (cur && !cur.endsWith(' ') ? ' ' : '') + item.text;
promptBox.value = newText;
// Note: We do NOT trigger a sync here automatically. User must type or click Generate.
pushToSessionHistory(newText);
modal.classList.remove('active');
});
el.querySelector('.gl-btn-edit').addEventListener('click', (e) => { e.stopPropagation(); showEditor(item); });
el.querySelector('.gl-btn-del').addEventListener('click', (e) => {
e.stopPropagation();
if (confirm(`Delete "${item.label}"?`)) {
savedSnippets = savedSnippets.filter(s => s.id !== item.id);
GM_setValue('savedSnippets', savedSnippets);
renderSnippets();
}
});
listContainer.appendChild(el);
});
}
const editLabel = document.getElementById('gl-edit-label');
const editText = document.getElementById('gl-edit-text');
let editingSnippetId = null;
function showEditor(item = null) {
document.getElementById('gl-view-list').style.display = 'none';
document.getElementById('gl-view-editor').classList.add('active');
editingSnippetId = item ? item.id : null;
editLabel.value = item ? item.label : '';
editText.value = item ? item.text : '';
editText.focus();
}
document.getElementById('btn-create-snippet').addEventListener('click', () => showEditor(null));
document.getElementById('btn-edit-cancel').addEventListener('click', () => {
document.getElementById('gl-view-editor').classList.remove('active');
document.getElementById('gl-view-list').style.display = 'flex';
});
document.getElementById('btn-edit-save').addEventListener('click', () => {
const label = editLabel.value.trim() || 'Untitled';
const text = editText.value.trim();
if (!text) return alert("Empty text");
if (editingSnippetId) {
const idx = savedSnippets.findIndex(s => s.id === editingSnippetId);
if (idx > -1) { savedSnippets[idx].label = label; savedSnippets[idx].text = text; }
} else {
savedSnippets.push({ id: Date.now().toString(), label, text });
}
GM_setValue('savedSnippets', savedSnippets);
document.getElementById('btn-edit-cancel').click();
renderSnippets();
});
// Favorites
function renderFavorites() {
const listContainer = document.getElementById('favorites-list-container');
listContainer.innerHTML = '';
const favArray = currentFavoritesTab === 'image' ? imageFavorites : videoFavorites;
if (favArray.length === 0) {
listContainer.innerHTML = `<div style="text-align:center; padding:20px; color:#555;">No favorites</div>`;
return;
}
favArray.forEach(item => {
const el = document.createElement('div');
el.className = 'gl-item';
el.innerHTML = `
<div class="gl-item-text">
<b>${escapeHtml(item.label)}</b>
<span>${escapeHtml(item.text)}</span>
<div class="history-item-time">${formatTimestamp(item.timestamp)}</div>
</div>
<div class="gl-item-actions"><button class="gl-icon-btn fav-btn-edit">✎</button><button class="gl-icon-btn fav-btn-del">🗑</button></div>`;
el.querySelector('.gl-item-text').addEventListener('click', () => {
promptBox.value = item.text;
pushToSessionHistory(item.text);
favoritesModal.classList.remove('active');
});
el.querySelector('.fav-btn-edit').addEventListener('click', (e) => { e.stopPropagation(); editFavorite(item); });
el.querySelector('.fav-btn-del').addEventListener('click', (e) => {
e.stopPropagation();
if (confirm(`Remove favorite?`)) {
if (currentFavoritesTab === 'image') imageFavorites = imageFavorites.filter(h => h.id !== item.id);
else videoFavorites = videoFavorites.filter(h => h.id !== item.id);
GM_setValue(currentFavoritesTab === 'image' ? 'imageFavorites' : 'videoFavorites', currentFavoritesTab === 'image' ? imageFavorites : videoFavorites);
renderFavorites();
}
});
listContainer.appendChild(el);
});
}
function editFavorite(item) {
document.getElementById('favorites-view-list').style.display = 'none';
document.getElementById('favorites-view-viewer').classList.add('active');
document.getElementById('fav-edit-label').value = item.label || item.text.substring(0,20);
document.getElementById('favorites-viewer-text').value = item.text;
document.getElementById('favorites-viewer-time').textContent = formatTimestamp(item.timestamp);
currentEditingFavId = item.id;
}
document.getElementById('btn-fav-viewer-back').addEventListener('click', () => {
document.getElementById('favorites-view-viewer').classList.remove('active');
document.getElementById('favorites-view-list').style.display = 'flex';
});
document.getElementById('btn-fav-viewer-save').addEventListener('click', () => {
const newLabel = document.getElementById('fav-edit-label').value.trim() || "Untitled";
const newText = document.getElementById('favorites-viewer-text').value.trim();
if (!newText || !currentEditingFavId) return;
let favArray = currentFavoritesTab === 'image' ? imageFavorites : videoFavorites;
const idx = favArray.findIndex(f => f.id === currentEditingFavId);
if (idx !== -1) {
favArray[idx].label = newLabel;
favArray[idx].text = newText;
GM_setValue(currentFavoritesTab === 'image' ? 'imageFavorites' : 'videoFavorites', favArray);
renderFavorites();
document.getElementById('btn-fav-viewer-back').click();
}
});
favoritesModal.querySelectorAll('.history-tab').forEach(tab => {
tab.addEventListener('click', () => {
favoritesModal.querySelectorAll('.history-tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
currentFavoritesTab = tab.dataset.tab;
renderFavorites();
});
});
// History
function renderHistory() {
const listContainer = document.getElementById('history-list-container');
listContainer.innerHTML = '';
const historyArray = currentHistoryTab === 'image' ? imagePromptHistory : videoPromptHistory;
if (historyArray.length === 0) {
listContainer.innerHTML = `<div style="text-align:center; padding:20px; color:#555;">No history</div>`;
return;
}
historyArray.forEach(item => {
const el = document.createElement('div');
el.className = 'gl-item';
const isFavorited = (currentHistoryTab === 'image' ? imageFavorites : videoFavorites).some(f => f.text === item.text);
el.innerHTML = `
<div class="gl-item-text">
<span>${escapeHtml(item.text)}</span>
<div class="history-item-time">${formatTimestamp(item.timestamp)}</div>
</div>
<div class="gl-item-actions">
<button class="gl-icon-btn history-btn-fav ${isFavorited ? 'favorite' : ''}">❤️</button>
<button class="gl-icon-btn history-btn-view">👁</button>
<button class="gl-icon-btn history-btn-del">🗑</button>
</div>`;
el.querySelector('.gl-item-text').addEventListener('click', () => {
promptBox.value = item.text;
pushToSessionHistory(item.text);
historyModal.classList.remove('active');
});
el.querySelector('.history-btn-fav').addEventListener('click', (e) => {
e.stopPropagation();
addToFavorites(item.text, item.type);
renderHistory();
});
el.querySelector('.history-btn-view').addEventListener('click', (e) => {
e.stopPropagation();
document.getElementById('history-view-list').style.display = 'none';
document.getElementById('history-view-viewer').classList.add('active');
document.getElementById('history-viewer-text').value = item.text;
document.getElementById('history-viewer-time').textContent = formatTimestamp(item.timestamp);
});
el.querySelector('.history-btn-del').addEventListener('click', (e) => {
e.stopPropagation();
if (currentHistoryTab === 'image') {
imagePromptHistory = imagePromptHistory.filter(h => h.id !== item.id);
GM_setValue('imagePromptHistory', imagePromptHistory);
} else {
videoPromptHistory = videoPromptHistory.filter(h => h.id !== item.id);
GM_setValue('videoPromptHistory', videoPromptHistory);
}
renderHistory();
});
listContainer.appendChild(el);
});
}
document.getElementById('btn-viewer-back').addEventListener('click', () => {
document.getElementById('history-view-viewer').classList.remove('active');
document.getElementById('history-view-list').style.display = 'flex';
});
document.getElementById('btn-viewer-use').addEventListener('click', () => {
const text = document.getElementById('history-viewer-text').value;
promptBox.value = text;
pushToSessionHistory(text);
historyModal.classList.remove('active');
document.getElementById('btn-viewer-back').click();
});
document.getElementById('btn-clear-history').addEventListener('click', () => {
if(confirm('Clear history for this tab?')) {
if(currentHistoryTab === 'image') { imagePromptHistory=[]; GM_setValue('imagePromptHistory', []); }
else { videoPromptHistory=[]; GM_setValue('videoPromptHistory', []); }
renderHistory();
}
});
historyModal.querySelectorAll('.history-tab').forEach(tab => {
tab.addEventListener('click', () => {
historyModal.querySelectorAll('.history-tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
currentHistoryTab = tab.dataset.tab;
renderHistory();
});
});
// --- BUTTON EVENT LISTENERS ---
document.getElementById('btn-open-library').addEventListener('click', () => {
toggleModal(modal);
if(modal.classList.contains('active')) renderSnippets();
});
document.getElementById('btn-open-favorites').addEventListener('click', () => {
toggleModal(favoritesModal);
if(favoritesModal.classList.contains('active')) renderFavorites();
});
document.getElementById('btn-open-history').addEventListener('click', () => {
toggleModal(historyModal);
if(historyModal.classList.contains('active')) renderHistory();
});
modal.querySelector('.gl-close').addEventListener('click', () => modal.classList.remove('active'));
favoritesModal.querySelector('.favorites-close').addEventListener('click', () => favoritesModal.classList.remove('active'));
historyModal.querySelector('.history-close').addEventListener('click', () => historyModal.classList.remove('active'));
// --- STRICT SYNC & CAPTURE LOGIC ---
// 1. UI Panel -> Website
// Only sync if the user is explicitly focused on the UI Panel.
promptBox.addEventListener('input', () => {
if (document.activeElement === promptBox) {
const grokTA = getGrokInput();
const imagineP = getGrokImagine();
lastTypedPrompt = promptBox.value;
if (grokTA) {
nativeValueSet(grokTA, lastTypedPrompt);
resetState("Ready");
} else if (imagineP) {
imagineP.textContent = lastTypedPrompt;
if (imagineP.classList.contains('is-empty') && lastTypedPrompt) imagineP.classList.remove('is-empty');
resetState("Ready");
}
}
});
// 2. Website -> UI Panel
// Only sync if the user is explicitly focused on the Website Input.
document.addEventListener('input', (e) => {
if (e.target.matches(TARGET_TEXTAREA_SELECTOR) ||
e.target.matches(IMAGE_EDITOR_SELECTOR) ||
e.target.matches(IMAGE_PROMPT_SELECTOR)) {
// Strict check: Only sync if this element is actively focused by the user
if (document.activeElement === e.target) {
promptBox.value = e.target.value;
lastTypedPrompt = e.target.value;
}
}
}, { capture: true, passive: true });
// 3. Generate Button Logic
// Forces a sync right before clicking the website button.
document.getElementById('btn-generate').addEventListener('click', () => {
// Detect Type
let type = 'video';
const vidEl = document.querySelector(TARGET_TEXTAREA_SELECTOR);
const imgEl = document.querySelector(IMAGE_PROMPT_SELECTOR);
const imgIm = document.querySelector(IMAGE_IMAGINE_SELECTOR);
if (vidEl) {
type = 'video';
} else if (imgEl || imgIm) {
type = 'image';
}
const grokTA = vidEl || document.querySelector(IMAGE_EDITOR_SELECTOR) || imgEl;
const realBtn = document.querySelector(RETRY_BUTTON_SELECTOR) ||
document.querySelector(IMAGE_EDITOR_BUTTON_SELECTOR) ||
document.querySelector(IMAGE_SUBMIT_BUTTON_SELECTOR);
const imagineP = imgIm;
if (!realBtn) return updateStatus("Button not found", "error");
const promptVal = promptBox.value.trim();
if (promptVal) {
addToHistory(promptVal, type);
pushToSessionHistory(promptVal);
}
// FORCE SYNC NOW
if (grokTA) nativeValueSet(grokTA, promptBox.value);
else if (imagineP) imagineP.textContent = promptBox.value;
setTimeout(() => {
if (!realBtn.disabled) {
realBtn.click();
lastGenerationTimestamp = Date.now();
updateStatus("Generation Started...");
} else {
updateStatus("Grok button disabled/processing.", "error");
}
}, 50);
});
// Image Capture (For clicks outside panel)
document.addEventListener('mousedown', (e) => {
const submitBtn = e.target.closest('button[aria-label="Submit"]');
if (submitBtn) {
const val = promptBox.value.trim() || lastTypedPrompt.trim();
if (val.length > 2) {
addToHistory(val, 'image');
pushToSessionHistory(val);
updateStatus("Prompt captured!");
}
}
}, true);
// --- MODERATION & RETRY LOGIC ---
function checkForModerationContent() {
const now = Date.now();
if (now - lastModerationCheck < 200) return null;
lastModerationCheck = now;
const toastSelectors = ['section[aria-label*="Notification"]', '[role="alert"]', '.toast'];
for (const sel of toastSelectors) {
const els = document.querySelectorAll(sel);
for (const el of els) {
const txt = (el.textContent || '').toLowerCase();
if (MODERATION_PATTERNS.some(p => txt.includes(p))) return { element: el, text: txt };
}
}
return null;
}
function waitForErrorDisappearance(element) {
if (errorWaitInterval) clearInterval(errorWaitInterval);
let safetyCounter = 0;
const POLL_MS = 500;
const MAX_WAIT_MS = 10000;
errorWaitInterval = setInterval(() => {
safetyCounter += POLL_MS;
const isConnected = document.body.contains(element);
let isVisible = false;
if (isConnected) {
const style = window.getComputedStyle(element);
isVisible = style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
}
if (!isConnected || !isVisible || safetyCounter >= MAX_WAIT_MS) {
clearInterval(errorWaitInterval);
errorWaitInterval = null;
updateStatus('Message cleared. Retrying...');
handleRetry(Date.now());
}
}, POLL_MS);
}
function handleRetry(now) {
if (!isRetryEnabled || limitReached) return;
if (now - lastGenerationTimestamp < GENERATION_COOLDOWN_MS) return;
const grokTA = getGrokInput();
const btn = document.querySelector(RETRY_BUTTON_SELECTOR) ||
document.querySelector(IMAGE_EDITOR_BUTTON_SELECTOR) ||
document.querySelector(IMAGE_SUBMIT_BUTTON_SELECTOR);
const imagineP = getGrokImagine();
// Note: For retry, we use lastTypedPrompt because the promptBox might not be focused
if ((grokTA || imagineP) && lastTypedPrompt) {
if (grokTA) nativeValueSet(grokTA, lastTypedPrompt);
else if (imagineP) imagineP.textContent = lastTypedPrompt;
if (autoClickEnabled && currentRetryCount >= maxRetries) {
updateStatus(`Limit Reached (${maxRetries})`, "error");
limitReached = true;
return;
}
if (autoClickEnabled && btn) {
currentRetryCount++;
updateStatus(`Retrying (${currentRetryCount}/${maxRetries})...`, 'warning');
setTimeout(() => {
if (!btn.disabled) {
btn.click();
lastGenerationTimestamp = Date.now();
}
processingModeration = false;
moderationDetected = false;
}, RETRY_DELAY_MS);
}
}
}
const observer = new MutationObserver(() => {
if (observerThrottle || !isRetryEnabled || limitReached) return;
observerThrottle = true;
setTimeout(() => { observerThrottle = false; }, OBSERVER_THROTTLE_MS);
if (!processingModeration) {
const mod = checkForModerationContent();
if (mod) {
debugLog('Moderation detected:', mod.text);
processingModeration = true;
moderationDetected = true;
updateStatus(`Moderation detected! Waiting...`, 'warning');
waitForErrorDisappearance(mod.element);
}
}
});
observer.observe(document.body, { childList: true, subtree: true, attributes: true, characterData: true });
// --- TOGGLES & SETTINGS ---
const toggleBtn = document.getElementById('grok-toggle-btn');
toggleBtn.addEventListener('click', () => {
isRetryEnabled = !isRetryEnabled;
toggleBtn.textContent = isRetryEnabled ? "ON" : "OFF";
toggleBtn.classList.toggle('off', !isRetryEnabled);
resetState(isRetryEnabled ? "Ready" : "Disabled");
if (!isRetryEnabled) updateStatus("Disabled", "error");
});
document.getElementById('grok-autoclick-cb').addEventListener('change', (e) => {
autoClickEnabled = e.target.checked;
GM_setValue('autoClickEnabled', autoClickEnabled);
});
document.getElementById('grok-retry-limit').addEventListener('change', (e) => {
maxRetries = parseInt(e.target.value);
GM_setValue('maxRetries', maxRetries);
});
// Keybinds
document.addEventListener('keydown', (e) => {
// Toggle Main UI
if (e.altKey && e.key.toLowerCase() === uiToggleKey) {
isUiVisible = !isUiVisible;
GM_setValue('isUiVisible', isUiVisible);
panel.classList.toggle('hidden', !isUiVisible);
e.preventDefault();
}
// Toggle Import/Export Buttons
if (e.altKey && e.key.toLowerCase() === 'i') {
const ioRow = document.getElementById('grok-io-container');
if (ioRow) {
ioRow.classList.toggle('io-hidden');
e.preventDefault();
}
}
});
window.addEventListener('beforeunload', () => {
observer.disconnect();
if (errorWaitInterval) clearInterval(errorWaitInterval);
});
debugLog('Grok Tools v33 Initialized');
})();