Grok Auto-Retry (UI Control Panel + Custom Keybinds)

Retry logic with a clickable UI panel. Click the bottom footer to change the "Hide UI" keybind on the fly.

当前为 2025-12-04 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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">&rarr;</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);
        }
    }

})();