// ==UserScript==
// @name Grok.com Video Prompt Saver & Auto-Retry
// @namespace http://tampermonkey.net/
// @version 8.0
// @description Saves prompts uniquely for every video ID (/imagine/post/xyz). Includes Auto-Retry Overlay.
// @author Kali
// @match https://grok.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=grok.com
// @grant none
// ==/UserScript==
(function() {
'use strict';
// --- CONFIGURATION ---
const TEXTAREA_SELECTOR = 'textarea';
const NOTIFICATION_SELECTOR = 'section[aria-label="Notifications alt+T"]';
const TARGET_SVG_PATH = "M5 11L12 4M12 4L19 11M12 4V21";
const ERROR_KEYWORDS = ['fail', 'error', 'moderate', 'policy', 'unable', 'wrong'];
// --- STATE ---
let autoRetryOn = false; // Always starts OFF
let isRetrying = false;
let retryInterval = null;
let overlayBtn = null;
let currentPath = window.location.pathname; // Track URL changes
// --- HELPER: LOGGING ---
function log(msg) { console.log(`[Grok Script] ${msg}`); }
// --- HELPER: GET UNIQUE STORAGE KEY ---
function getStorageKey() {
// This uses the full path (e.g., "/imagine/post/12345") as the key
// This ensures every single video has its own memory slot.
return `grok_prompt_storage_${window.location.pathname}`;
}
// --- PART 1: PROMPT SAVER (DYNAMIC) ---
// React-friendly value setter
function setNativeValue(element, value) {
const valueSetter = Object.getOwnPropertyDescriptor(element, 'value').set;
const prototype = Object.getPrototypeOf(element);
const prototypeValueSetter = Object.getOwnPropertyDescriptor(prototype, 'value').set;
if (valueSetter && valueSetter !== prototypeValueSetter) {
prototypeValueSetter.call(element, value);
} else {
valueSetter.call(element, value);
}
element.dispatchEvent(new Event('input', { bubbles: true }));
}
function attachToTextarea(textarea) {
const currentKey = getStorageKey();
// If we are already attached to this EXACT key (URL), do nothing.
// If the URL changed, this check fails, and we proceed to re-attach/restore.
if (textarea.dataset.attachedKey === currentKey) return;
textarea.dataset.attachedKey = currentKey;
log(`Connected to memory slot: ${currentKey}`);
// 1. RESTORE: Check if we have a saved prompt for THIS specific video URL
const savedPrompt = localStorage.getItem(currentKey);
// Only restore if we have a saved value.
// Note: We don't check if textarea is empty here, because if you switch videos,
// you want the prompt to swap immediately, even if the box had the previous video's text.
if (savedPrompt) {
log(`Restoring prompt for this video ID...`);
setNativeValue(textarea, savedPrompt);
}
// 2. SAVE: Listen for typing
// We clone the element to strip old event listeners if necessary,
// but simple attribute key tracking is usually safer for React.
textarea.addEventListener('input', (e) => {
const liveKey = getStorageKey(); // Always fetch fresh key in case URL changed mid-typing
localStorage.setItem(liveKey, e.target.value);
});
}
// --- PART 2: UI OVERLAY (TOGGLE) ---
function getTargetButton() {
let btn = document.querySelector('button[aria-label="Make video"]');
if (!btn) {
const allBtns = Array.from(document.querySelectorAll('button'));
btn = allBtns.find(b => {
const path = b.querySelector('path');
return path && path.getAttribute('d') === TARGET_SVG_PATH;
});
}
return btn;
}
function updateOverlayVisuals() {
if (!overlayBtn) return;
if (autoRetryOn) {
overlayBtn.innerHTML = '▶';
overlayBtn.style.backgroundColor = '#22c55e'; // Green
overlayBtn.title = "Auto-Retry ON";
} else {
overlayBtn.innerHTML = '■';
overlayBtn.style.backgroundColor = '#6b7280'; // Gray
overlayBtn.title = "Auto-Retry OFF";
}
}
function createOverlay() {
if (document.getElementById('grok-retry-toggle')) return;
overlayBtn = document.createElement('div');
overlayBtn.id = 'grok-retry-toggle';
// STYLE (Large 48px)
overlayBtn.style.position = 'fixed';
overlayBtn.style.width = '48px';
overlayBtn.style.height = '48px';
overlayBtn.style.fontSize = '24px';
overlayBtn.style.borderRadius = '50%';
overlayBtn.style.color = 'white';
overlayBtn.style.display = 'flex';
overlayBtn.style.justifyContent = 'center';
overlayBtn.style.alignItems = 'center';
overlayBtn.style.cursor = 'pointer';
overlayBtn.style.zIndex = '9999';
overlayBtn.style.boxShadow = '0 4px 8px rgba(0,0,0,0.4)';
overlayBtn.style.userSelect = 'none';
overlayBtn.style.transition = 'all 0.2s ease';
overlayBtn.addEventListener('click', (e) => {
e.stopPropagation();
autoRetryOn = !autoRetryOn;
updateOverlayVisuals();
overlayBtn.style.transform = 'scale(0.9)';
setTimeout(() => overlayBtn.style.transform = 'scale(1)', 100);
if (autoRetryOn) {
log('Toggle ON -> Triggering immediate click');
triggerRetrySequence();
}
});
updateOverlayVisuals();
document.body.appendChild(overlayBtn);
}
function updateOverlayPosition() {
const target = getTargetButton();
if (target && overlayBtn) {
const rect = target.getBoundingClientRect();
overlayBtn.style.display = 'flex';
overlayBtn.style.top = `${rect.bottom + 10}px`;
overlayBtn.style.left = `${rect.left + (rect.width / 2) - 24}px`;
} else if (overlayBtn) {
overlayBtn.style.display = 'none';
}
requestAnimationFrame(updateOverlayPosition);
}
// --- PART 3: AUTO-RETRY ---
function simulateRealClick(element) {
['mousedown', 'mouseup', 'click'].forEach(eventType => {
element.dispatchEvent(new MouseEvent(eventType, {
bubbles: true, cancelable: true, view: window, buttons: 1
}));
});
}
function triggerRetrySequence() {
if (!autoRetryOn || isRetrying) return;
isRetrying = true;
let attempts = 0;
if (retryInterval) clearInterval(retryInterval);
retryInterval = setInterval(() => {
attempts++;
const btn = getTargetButton();
if (btn && !btn.disabled && !btn.classList.contains('disabled')) {
const originalBg = btn.style.backgroundColor;
btn.style.backgroundColor = '#22c55e';
simulateRealClick(btn);
setTimeout(() => btn.style.backgroundColor = originalBg, 300);
clearInterval(retryInterval);
isRetrying = false;
} else if (attempts >= 40) { // 20 sec timeout
clearInterval(retryInterval);
isRetrying = false;
}
}, 500);
}
// --- PART 4: NAVIGATION & DOM OBSERVER ---
// This function runs constantly to detect if URL changed without page reload
function checkUrlChange() {
if (window.location.pathname !== currentPath) {
log(`URL Change detected: ${window.location.pathname}`);
currentPath = window.location.pathname;
// Find textarea and FORCE it to update its key
const t = document.querySelector(TEXTAREA_SELECTOR);
if (t) {
t.dataset.attachedKey = ''; // Reset key flag
attachToTextarea(t); // Re-run logic to load new prompt
}
}
}
function attachToNotificationArea(section) {
if (section.dataset.grokMonitorAttached === 'true') return;
section.dataset.grokMonitorAttached = 'true';
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.addedNodes.length) {
const text = section.innerText.toLowerCase();
if (ERROR_KEYWORDS.some(k => text.includes(k))) {
setTimeout(triggerRetrySequence, 500);
}
}
});
});
observer.observe(section, { childList: true, subtree: true, characterData: true });
}
const mainObserver = new MutationObserver((mutations) => {
checkUrlChange(); // Check URL on every DOM update
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1) {
if (node.tagName.toLowerCase() === 'textarea') attachToTextarea(node);
else {
const t = node.querySelector(TEXTAREA_SELECTOR);
if (t) attachToTextarea(t);
}
if (node.matches && node.matches(NOTIFICATION_SELECTOR)) attachToNotificationArea(node);
else {
const n = node.querySelector(NOTIFICATION_SELECTOR);
if (n) attachToNotificationArea(n);
}
}
});
});
});
mainObserver.observe(document.body, { childList: true, subtree: true });
// Initial Start
const iT = document.querySelector(TEXTAREA_SELECTOR);
if (iT) attachToTextarea(iT);
const iN = document.querySelector(NOTIFICATION_SELECTOR);
if (iN) attachToNotificationArea(iN);
createOverlay();
updateOverlayPosition();
})();