Retry logic with a clickable UI panel. Click the bottom footer to change the "Hide UI" keybind on the fly.
目前為
// ==UserScript==
// @name Grok Auto-Retry (UI Control Panel + Custom Keybinds)
// @namespace http://tampermonkey.net/
// @version 5.0
// @description Retry logic with a clickable UI panel. Click the bottom footer to change the "Hide UI" keybind on the fly.
// @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.";
const RETRY_DELAY_MS = 1000;
const COOLDOWN_MS = 3000;
// --- LOAD SAVED SETTINGS ---
let maxRetries = GM_getValue('maxRetries', 5);
let uiToggleKey = GM_getValue('uiToggleKey', 'h'); // Default 'h'
let isRetryEnabled = true;
let isUiVisible = true;
// --- STATE VARIABLES ---
let lastTypedPrompt = "";
let lastRetryTimestamp = 0;
let currentRetryCount = 0;
// --- STYLES ---
GM_addStyle(`
#grok-control-panel {
position: fixed;
bottom: 20px;
right: 20px;
width: 250px; /* Made slightly wider to fit the text */
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-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;
}
/* New Footer Styling */
#grok-footer-wrapper {
display: flex;
justify-content: space-between; /* Pushes text to left, key to right */
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; /* Slightly larger arrow */
}
.status-active { color: #ffd400 !important; }
.status-error { color: #f4212e !important; }
`);
// --- CREATE UI PANEL ---
const panel = document.createElement('div');
panel.id = 'grok-control-panel';
panel.innerHTML = `
<div class="grok-row">
<span class="grok-title">Auto-Retry</span>
<button id="grok-toggle-btn" class="grok-toggle-btn">ON</button>
</div>
<div class="grok-row">
<div class="grok-input-group">
<label for="grok-retry-limit">Max Limit:</label>
<input type="number" id="grok-retry-limit" value="${maxRetries}" min="1" max="99">
</div>
</div>
<div id="grok-status-text">Ready (0 / ${maxRetries})</div>
<!-- NEW FOOTER LAYOUT -->
<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 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. Toggle ON/OFF
toggleBtn.addEventListener('click', () => {
isRetryEnabled = !isRetryEnabled;
if (isRetryEnabled) {
toggleBtn.textContent = "ON";
toggleBtn.classList.remove('off');
updateStatus("Ready (Resumed)");
currentRetryCount = 0;
} else {
toggleBtn.textContent = "OFF";
toggleBtn.classList.add('off');
updateStatus("Script Disabled", "error");
}
});
// 2. Change 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}`);
});
// 3. CHANGE KEYBIND (Click Footer)
footerWrapper.addEventListener('click', () => {
const originalText = keybindDisplay.textContent;
// Update UI to show listening state
keybindDisplay.textContent = "Press key...";
footerWrapper.style.color = "#ffd400"; // Yellow highlight
function onKeyCapture(e) {
e.preventDefault();
e.stopPropagation();
// Ignore modifier keys alone
if (['Control', 'Alt', 'Shift', 'Meta'].includes(e.key)) return;
const newKey = e.key.toLowerCase();
// Validate (single char)
if (newKey.length === 1) {
uiToggleKey = newKey;
GM_setValue('uiToggleKey', uiToggleKey);
keybindDisplay.textContent = `Alt+${uiToggleKey.toUpperCase()}`;
} else {
// If invalid, revert
keybindDisplay.textContent = originalText;
}
// Clean up UI
footerWrapper.style.color = "";
document.removeEventListener('keydown', onKeyCapture, true);
}
// Listen for the next key press (capture phase to intercept it)
document.addEventListener('keydown', onKeyCapture, true);
});
// 4. Global Hotkey Listener (Hide/Show UI)
document.addEventListener('keydown', (e) => {
// Use the dynamic 'uiToggleKey' variable
if (e.altKey && e.key.toLowerCase() === uiToggleKey) {
e.preventDefault();
isUiVisible = !isUiVisible;
if (isUiVisible) {
panel.classList.remove('hidden');
} else {
panel.classList.add('hidden');
}
}
});
// --- HELPER: UPDATE STATUS TEXT ---
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');
// Update limits in status text if just resetting
if (msg.startsWith("Ready")) {
statusText.textContent = `Ready (0 / ${maxRetries})`;
}
}
// --- CORE LOGIC: CAPTURE PROMPT ---
document.addEventListener('input', (e) => {
if (e.target.matches(TARGET_TEXTAREA_SELECTOR)) {
const val = e.target.value;
if (val && val.trim().length > 0) {
if (val !== lastTypedPrompt) {
currentRetryCount = 0;
updateStatus(`Ready`);
}
lastTypedPrompt = val;
}
}
}, true);
// --- CORE LOGIC: MONITOR FOR MODERATION ---
const observer = new MutationObserver(() => {
if (!isRetryEnabled) 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;
handleRetry(now);
}
});
observer.observe(document.body, { childList: true, subtree: true, characterData: true });
// --- CORE LOGIC: PERFORM RETRY ---
function handleRetry(now) {
if (!lastTypedPrompt) return;
if (currentRetryCount >= maxRetries) {
updateStatus(`Limit Reached (${currentRetryCount}/${maxRetries})`, "error");
return;
}
const textarea = document.querySelector(TARGET_TEXTAREA_SELECTOR);
const button = document.querySelector(RETRY_BUTTON_SELECTOR);
if (textarea && button) {
lastRetryTimestamp = now;
currentRetryCount++;
updateStatus(`Restoring... (${currentRetryCount} / ${maxRetries})`, "active");
const nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set;
nativeTextAreaValueSetter.call(textarea, lastTypedPrompt);
textarea.dispatchEvent(new Event('input', { bubbles: true }));
setTimeout(() => {
updateStatus(`Retrying... (${currentRetryCount} / ${maxRetries})`, "active");
button.click();
}, RETRY_DELAY_MS);
}
}
})();