Fast retry. When limit is reached, it restores text but stops clicking (prevents crash).
目前為
// ==UserScript==
// @name Grok Auto-Retry (Persistent UI)
// @namespace http://tampermonkey.net/
// @version 7.5
// @description Fast retry. When limit is reached, it restores text but stops clicking (prevents crash).
// @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
// ==/UserScript==
(function() {
'use strict';
// --- CONFIGURATION ---
const TARGET_TEXTAREA_SELECTOR = 'textarea[aria-label="Make a video"]';
const RETRY_BUTTON_SELECTOR = 'button[aria-label="Make video"]';
const MODERATION_TEXT = "Content Moderated. Try a different idea.";
// FAST SETTINGS (Restored as requested)
const RETRY_DELAY_MS = 1000; // 1 second wait before clicking
const COOLDOWN_MS = 2000; // Short cooldown to prevent double-clicks
// --- 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 isRetryEnabled = true;
// --- STATE VARIABLES ---
let lastTypedPrompt = "";
let lastRetryTimestamp = 0;
let currentRetryCount = 0;
// CRITICAL FLAG: Stops the crash loop
let limitReached = false;
// --- STYLES ---
GM_addStyle(`
#grok-control-panel {
position: fixed;
bottom: 20px;
right: 20px;
width: 250px;
background-color: #15202b;
border: 1px solid #38444d;
border-radius: 12px;
padding: 15px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
color: white;
z-index: 99999;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
transition: opacity 0.3s ease;
}
#grok-control-panel.hidden {
opacity: 0;
pointer-events: none;
}
.grok-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.grok-title {
font-size: 14px;
font-weight: bold;
color: #fff;
}
.grok-toggle-btn {
background: #00ba7c;
border: none;
color: white;
padding: 4px 10px;
border-radius: 15px;
font-size: 11px;
font-weight: bold;
cursor: pointer;
transition: background 0.2s;
}
.grok-toggle-btn.off {
background: #f4212e;
}
.grok-input-group {
display: flex;
align-items: center;
font-size: 12px;
color: #8b98a5;
}
.grok-checkbox-group {
display: flex;
align-items: center;
font-size: 12px;
color: #fff;
margin-bottom: 8px;
cursor: pointer;
}
.grok-checkbox-group input {
margin-right: 8px;
cursor: pointer;
}
#grok-retry-limit {
width: 50px;
background: #273340;
border: 1px solid #38444d;
color: white;
border-radius: 4px;
padding: 4px;
margin-left: 8px;
text-align: center;
}
#grok-status-text {
font-size: 11px;
margin-top: 5px;
padding-top: 5px;
border-top: 1px solid #38444d;
color: #00ba7c;
text-align: center;
}
#grok-footer-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 6px;
font-size: 10px;
color: #555;
cursor: pointer;
user-select: none;
transition: color 0.2s;
}
#grok-footer-wrapper:hover {
color: #8b98a5;
}
.footer-arrow { margin: 0 5px; font-size: 12px; }
.status-active { color: #ffd400 !important; }
.status-error { color: #f4212e !important; }
`);
// --- CREATE UI PANEL ---
const panel = document.createElement('div');
panel.id = 'grok-control-panel';
if (!isUiVisible) {
panel.classList.add('hidden');
}
panel.innerHTML = `
<div class="grok-row">
<span class="grok-title">Fast Auto-Retry</span>
<button id="grok-toggle-btn" class="grok-toggle-btn">ON</button>
</div>
<label class="grok-checkbox-group" title="Uncheck to only restore text without clicking retry">
<input type="checkbox" id="grok-autoclick-cb" ${autoClickEnabled ? 'checked' : ''}>
Auto-Click Button
</label>
<div class="grok-row">
<div class="grok-input-group">
<label for="grok-retry-limit">Max Retries:</label>
<input type="number" id="grok-retry-limit" value="${maxRetries}" min="1" max="99">
</div>
</div>
<div id="grok-status-text">Ready</div>
<div id="grok-footer-wrapper" title="Click to change shortcut">
<span>Change Hide Key</span>
<span class="footer-arrow">→</span>
<span id="grok-keybind-display">Alt+${uiToggleKey.toUpperCase()}</span>
</div>
`;
document.body.appendChild(panel);
// --- UI REFERENCES ---
const toggleBtn = document.getElementById('grok-toggle-btn');
const autoClickCb = document.getElementById('grok-autoclick-cb');
const limitInput = document.getElementById('grok-retry-limit');
const statusText = document.getElementById('grok-status-text');
const footerWrapper = document.getElementById('grok-footer-wrapper');
const keybindDisplay = document.getElementById('grok-keybind-display');
// --- EVENT LISTENERS ---
// 1. Master Toggle ON/OFF
toggleBtn.addEventListener('click', () => {
isRetryEnabled = !isRetryEnabled;
if (isRetryEnabled) {
toggleBtn.textContent = "ON";
toggleBtn.classList.remove('off');
limitReached = false; // Reset lock
currentRetryCount = 0;
updateStatus("Ready (Resumed)");
} else {
toggleBtn.textContent = "OFF";
toggleBtn.classList.add('off');
updateStatus("Script Disabled", "error");
}
});
// 2. Auto-Click Checkbox
autoClickCb.addEventListener('change', (e) => {
autoClickEnabled = e.target.checked;
GM_setValue('autoClickEnabled', autoClickEnabled);
updateStatus(autoClickEnabled ? "Auto-Click Enabled" : "Restore Only Mode");
});
// 3. Limit Input
limitInput.addEventListener('change', (e) => {
let val = parseInt(e.target.value, 10);
if (val < 1) val = 1;
maxRetries = val;
GM_setValue('maxRetries', maxRetries);
updateStatus(`Limit set to ${maxRetries}`);
});
// 4. Change Keybind
footerWrapper.addEventListener('click', () => {
const originalText = keybindDisplay.textContent;
keybindDisplay.textContent = "Press key...";
footerWrapper.style.color = "#ffd400";
function onKeyCapture(e) {
e.preventDefault();
e.stopPropagation();
if (['Control', 'Alt', 'Shift', 'Meta'].includes(e.key)) return;
const newKey = e.key.toLowerCase();
if (newKey.length === 1) {
uiToggleKey = newKey;
GM_setValue('uiToggleKey', uiToggleKey);
keybindDisplay.textContent = `Alt+${uiToggleKey.toUpperCase()}`;
} else {
keybindDisplay.textContent = originalText;
}
footerWrapper.style.color = "";
document.removeEventListener('keydown', onKeyCapture, true);
}
document.addEventListener('keydown', onKeyCapture, true);
});
// 5. Hide/Show UI Hotkey
document.addEventListener('keydown', (e) => {
if (e.altKey && e.key.toLowerCase() === uiToggleKey) {
e.preventDefault();
isUiVisible = !isUiVisible;
GM_setValue('isUiVisible', isUiVisible);
panel.classList.toggle('hidden', !isUiVisible);
}
});
// --- HELPER: UPDATE STATUS ---
function updateStatus(msg, type = 'normal') {
statusText.textContent = msg;
statusText.className = '';
if (type === 'active') statusText.classList.add('status-active');
if (type === 'error') statusText.classList.add('status-error');
if (msg.startsWith("Ready")) {
statusText.textContent = autoClickEnabled
? `Ready (${currentRetryCount} / ${maxRetries})`
: `Ready (Restore Only)`;
}
}
// --- CORE: CAPTURE PROMPT ---
document.addEventListener('input', (e) => {
if (e.target.matches(TARGET_TEXTAREA_SELECTOR)) {
const val = e.target.value;
// When user types, we assume it's a new prompt.
// WE MUST RESET THE LIMIT LOCK HERE so the script works for the new prompt.
if (val && val.trim().length > 0) {
if (val !== lastTypedPrompt) {
currentRetryCount = 0;
limitReached = false; // <--- UNLOCK
updateStatus(`Ready`);
}
lastTypedPrompt = val;
}
}
}, true);
// --- CORE: MONITOR ---
const observer = new MutationObserver(() => {
if (!isRetryEnabled) return;
// CRITICAL: If limit was reached, DO NOT SCAN DOM.
// This effectively "turns off" the script until you type something new.
if (limitReached) return;
const allSpans = Array.from(document.querySelectorAll('span'));
const modSpan = allSpans.find(s => s.textContent.trim() === MODERATION_TEXT);
if (modSpan) {
const now = Date.now();
if (now - lastRetryTimestamp < COOLDOWN_MS) return;
handleRestore(now);
}
});
observer.observe(document.body, { childList: true, subtree: true, characterData: true });
// --- CORE: RESTORE & OPTIONAL CLICK ---
function handleRestore(now) {
if (!lastTypedPrompt) return;
lastRetryTimestamp = now;
const textarea = document.querySelector(TARGET_TEXTAREA_SELECTOR);
const button = document.querySelector(RETRY_BUTTON_SELECTOR);
if (textarea) {
// 1. ALWAYS RESTORE THE TEXT FIRST
const nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set;
nativeTextAreaValueSetter.call(textarea, lastTypedPrompt);
textarea.dispatchEvent(new Event('input', { bubbles: true }));
// 2. CHECK IF WE HIT THE LIMIT
// If we are AT or ABOVE the max retries, we DO NOT CLICK.
if (autoClickEnabled && currentRetryCount >= maxRetries) {
updateStatus(`Limit Reached (Text Restored)`, "error");
// CRITICAL: Set this flag. The MutationObserver will now ignore the page.
// This prevents the infinite loop crash.
limitReached = true;
return;
}
// 3. IF LIMIT NOT REACHED, CLICK BUTTON
if (autoClickEnabled && button) {
currentRetryCount++; // Increment count
updateStatus(`Retrying... (${currentRetryCount} / ${maxRetries})`, "active");
setTimeout(() => {
button.click();
}, RETRY_DELAY_MS);
} else {
updateStatus("Text Restored (Waiting)", "active");
setTimeout(() => updateStatus("Ready"), 2000);
}
}
}
})();