Grok Auto-Retry + Prompt Snippets (v15.3 Dark Mode Gen)

Bottom-right panel. Resize via Top-Left. Snippets Library. Dark Mode "Generate" button.

目前為 2025-12-06 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Grok Auto-Retry + Prompt Snippets (v15.3 Dark Mode Gen)
// @namespace    http://tampermonkey.net/
// @version      8.0
// @description  Bottom-right panel. Resize via Top-Left. Snippets Library. Dark Mode "Generate" button.
// @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 = 2000;

    // --- 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);

    // Save dimensions
    let panelSize = GM_getValue('panelSize', { width: '300px', height: '400px' });

    let isRetryEnabled = true;
    let limitReached = false;
    let currentRetryCount = 0;
    let lastTypedPrompt = "";
    let lastRetryTimestamp = 0;

    // --- STYLES ---
    GM_addStyle(`
        /* MAIN PANEL */
        #grok-control-panel {
            position: fixed;
            bottom: 20px;
            right: 20px;
            width: ${panelSize.width};
            height: ${panelSize.height};
            min-width: 280px;
            min-height: 250px;
            max-width: 90vw;
            max-height: 90vh;
            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: 99998;
            box-shadow: 0 4px 12px rgba(0,0,0,0.6);
            display: flex;
            flex-direction: column;
            gap: 10px;
        }
        #grok-control-panel.hidden { display: none; }

        /* RESIZE HANDLE (Top-Left) */
        #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 #1d9bf0; border-right: 6px solid transparent;
            width: 0; height: 0; opacity: 0.7;
        }
        #grok-resize-handle:hover::after { opacity: 1; border-top-color: #fff; }

        /* HEADER */
        .grok-header { display: flex; justify-content: space-between; align-items: center; flex-shrink: 0; margin-left: 10px; }
        .grok-title { font-weight: bold; font-size: 14px; color: #fff; }
        .grok-toggle-btn {
            background: #00ba7c; border: none; color: white;
            padding: 4px 12px; border-radius: 15px;
            font-size: 11px; font-weight: bold; cursor: pointer;
        }
        .grok-toggle-btn.off { background: #f4212e; }

        /* CONTROLS */
        .grok-controls { display: flex; align-items: center; justify-content: space-between; font-size: 12px; color: #8b98a5; flex-shrink: 0; }
        .grok-checkbox { display: flex; align-items: center; cursor: pointer; color: #fff; }
        .grok-checkbox input { margin-right: 6px; }
        .grok-num-input {
            width: 40px; background: #273340; border: 1px solid #38444d;
            color: white; border-radius: 4px; padding: 2px 5px; text-align: center;
        }

        /* PROMPT BOX */
        .grok-prompt-label { font-size: 11px; font-weight: bold; color: #8b98a5; margin-bottom: -5px; flex-shrink: 0; }
        #grok-panel-prompt {
            width: 100%; flex-grow: 1;
            background: #000; border: 1px solid #38444d; border-radius: 6px;
            color: #fff; padding: 8px; font-size: 12px; font-family: sans-serif;
            resize: none; box-sizing: border-box;
        }
        #grok-panel-prompt:focus { border-color: #1d9bf0; outline: none; }

        /* BUTTONS ROW */
        .grok-btn-row {
            display: flex; gap: 8px; flex-shrink: 0;
        }

        .grok-action-btn {
            flex: 1; padding: 8px; border-radius: 6px; border: none;
            cursor: pointer; font-weight: bold; font-size: 12px;
            transition: background 0.2s, border-color 0.2s;
        }
        #btn-open-library { background: #1d9bf0; color: white; }
        #btn-open-library:hover { background: #1a8cd8; }

        /* Dark Mode Generate Button */
        #btn-generate {
            background: #273340;
            color: #eff3f4;
            border: 1px solid #38444d;
        }
        #btn-generate:hover {
            background: #38444d;
            border-color: #6b7d8c;
        }

        /* STATUS */
        #grok-status { text-align: center; font-size: 11px; color: #00ba7c; padding-top: 5px; border-top: 1px solid #38444d; flex-shrink: 0; }
        .status-error { color: #f4212e !important; }

        /* --- LIBRARY MODAL (STACKED) --- */
        #grok-library-modal {
            position: fixed;
            right: 20px; /* Aligned with panel */
            /* Bottom is calculated via JS */
            width: 350px;
            height: 400px; /* Fixed height for library */
            background: #15202b; border: 1px solid #38444d; border-radius: 12px;
            display: none; flex-direction: column; z-index: 99999;
            box-shadow: 0 4px 20px rgba(0,0,0,0.8);
            font-family: -apple-system, BlinkMacSystemFont, sans-serif;
        }
        #grok-library-modal.active { display: flex; }

        .gl-header {
            padding: 10px; background: #192734; display: flex;
            justify-content: space-between; align-items: center;
            font-weight: bold; font-size: 13px; color: white;
            border-bottom: 1px solid #38444d;
        }
        .gl-close { cursor: pointer; font-size: 18px; line-height: 1; color: #8b98a5; }
        .gl-view-list { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
        .gl-list-content { overflow-y: auto; padding: 10px; flex: 1; display: flex; flex-direction: column; gap: 6px; }
        .gl-item {
            background: #192734; border: 1px solid #38444d; padding: 8px;
            border-radius: 4px; display: flex; justify-content: space-between; align-items: center;
            font-size: 12px; color: white;
        }
        .gl-item:hover { border-color: #1d9bf0; }
        .gl-item-text { cursor: pointer; flex: 1; margin-right: 10px; }
        .gl-item-text b { display: block; margin-bottom: 2px; }
        .gl-item-text span { color: #888; font-size: 10px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
        .gl-item-actions { display: flex; gap: 5px; }
        .gl-icon-btn { background: none; border: none; cursor: pointer; font-size: 14px; color: #8b98a5; padding: 2px; }
        .gl-icon-btn:hover { color: white; }
        .gl-create-btn {
            margin: 10px; padding: 8px; background: #00ba7c; color: white;
            text-align: center; border-radius: 6px; cursor: pointer; font-weight: bold; font-size: 12px;
        }
        .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: #273340; border: 1px solid #38444d; color: white;
            padding: 8px; border-radius: 4px; font-size: 12px; width: 100%; box-sizing: border-box;
        }
        .gl-textarea { flex-grow: 1; resize: none; }
        .gl-editor-buttons { display: flex; gap: 10px; margin-top: auto; }
        .gl-btn { flex: 1; padding: 8px; border-radius: 6px; border: none; cursor: pointer; font-weight: bold; color: white; }
        .gl-btn-save { background: #1d9bf0; }
        .gl-btn-cancel { background: #38444d; }
    `);

    // --- DOM CREATION ---
    const panel = document.createElement('div');
    panel.id = 'grok-control-panel';
    if (!isUiVisible) panel.classList.add('hidden');
    panel.innerHTML = `
        <div id="grok-resize-handle" title="Drag to Resize"></div>
        <div class="grok-header">
            <span class="grok-title">Grok Tools v15.3</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-label">Prompt Editor</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-generate" class="grok-action-btn">Generate</button>
        </div>

        <div id="grok-status">Ready</div>
        <div style="font-size:9px; color:#555; text-align:center;">Hide: Alt+${uiToggleKey.toUpperCase()}</div>
    `;
    document.body.appendChild(panel);

    // --- LIBRARY MODAL ---
    const modal = document.createElement('div');
    modal.id = 'grok-library-modal';
    modal.innerHTML = `
        <div class="gl-header"><span>Snippets Library</span><span class="gl-close">&times;</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);

    // --- RESIZE & POSITION LOGIC ---
    const resizeHandle = document.getElementById('grok-resize-handle');
    let isResizing = false;
    let startX, startY, startWidth, startHeight;

    // Helper to snap modal to top of panel
    function updateModalPosition() {
        const pHeight = panel.offsetHeight;
        // Panel Bottom (20px) + Panel Height + Gap (10px) = Modal Bottom
        modal.style.bottom = (20 + pHeight + 10) + 'px';
    }

    resizeHandle.addEventListener('mousedown', (e) => {
        isResizing = true;
        startX = e.clientX;
        startY = e.clientY;
        const rect = panel.getBoundingClientRect();
        startWidth = rect.width;
        startHeight = rect.height;
        e.preventDefault();
        document.body.style.cursor = 'nwse-resize';
    });

    document.addEventListener('mousemove', (e) => {
        if (!isResizing) return;
        const deltaX = startX - e.clientX;
        const deltaY = startY - e.clientY;
        const newWidth = Math.max(280, startWidth + deltaX);
        const newHeight = Math.max(250, startHeight + deltaY);

        panel.style.width = newWidth + 'px';
        panel.style.height = newHeight + 'px';

        // Keep modal attached while resizing
        updateModalPosition();
    });

    document.addEventListener('mouseup', () => {
        if (isResizing) {
            isResizing = false;
            document.body.style.cursor = '';
            const rect = panel.getBoundingClientRect();
            GM_setValue('panelSize', { width: rect.width + 'px', height: rect.height + 'px' });
            updateModalPosition();
        }
    });

    // --- REFS & LOGIC ---
    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');
    const promptBox = document.getElementById('grok-panel-prompt');
    const openLibBtn = document.getElementById('btn-open-library');
    const generateBtn = document.getElementById('btn-generate');

    // Modal Refs
    const modalEl = document.getElementById('grok-library-modal');
    const listContainer = document.getElementById('gl-list-container');
    const createBtn = document.getElementById('btn-create-snippet');
    const editLabel = document.getElementById('gl-edit-label');
    const editText = document.getElementById('gl-edit-text');
    let editingId = null;

    // Prompt Sync
    promptBox.addEventListener('input', () => {
        const grokTA = document.querySelector(TARGET_TEXTAREA_SELECTOR);
        if (grokTA) {
            lastTypedPrompt = promptBox.value;
            nativeValueSet(grokTA, lastTypedPrompt);
            resetState("Ready");
        }
    });

    function nativeValueSet(el, value) {
        const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set;
        setter.call(el, value);
        el.dispatchEvent(new Event('input', { bubbles: true }));
    }

    // --- MANUAL GENERATE BUTTON LOGIC ---
    generateBtn.addEventListener('click', () => {
        const grokTA = document.querySelector(TARGET_TEXTAREA_SELECTOR);
        const realBtn = document.querySelector(RETRY_BUTTON_SELECTOR);

        if (!grokTA || !realBtn) {
            updateStatus("Grok elements not found!", "error");
            return;
        }

        // Force sync value before clicking
        nativeValueSet(grokTA, promptBox.value);

        // Click the real button
        setTimeout(() => {
            if(!realBtn.disabled) {
                realBtn.click();
                updateStatus("Generation Started...");
            } else {
                updateStatus("Grok button disabled/processing.", "error");
            }
        }, 50);
    });

    // Modal Logic
    function renderSnippets() {
        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;
                promptBox.value = cur + (cur && !cur.endsWith(' ') ? ' ' : '') + item.text;
                promptBox.dispatchEvent(new Event('input'));
                modalEl.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);
        });
    }

    function showEditor(item = null) {
        document.getElementById('gl-view-list').style.display = 'none';
        document.getElementById('gl-view-editor').classList.add('active');
        editingId = item ? item.id : null;
        editLabel.value = item ? item.label : '';
        editText.value = item ? item.text : '';
        editText.focus();
    }

    createBtn.addEventListener('click', () => showEditor(null));

    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(editingId) {
            const idx = savedSnippets.findIndex(s => s.id === editingId);
            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('gl-view-editor').classList.remove('active');
        document.getElementById('gl-view-list').style.display = 'flex';
        renderSnippets();
    });

    document.getElementById('btn-edit-cancel').addEventListener('click', () => {
        document.getElementById('gl-view-editor').classList.remove('active');
        document.getElementById('gl-view-list').style.display = 'flex';
    });

    openLibBtn.addEventListener('click', () => {
        modalEl.classList.add('active');
        updateModalPosition(); // Snap to position
        renderSnippets();
    });

    document.querySelector('.gl-close').addEventListener('click', () => modalEl.classList.remove('active'));

    function escapeHtml(text) { return text ? text.replace(/&/g, "&amp;").replace(/</g, "&lt;") : ''; }

    // --- AUTO RETRY LOGIC ---
    toggleBtn.addEventListener('click', () => {
        isRetryEnabled = !isRetryEnabled;
        toggleBtn.textContent = isRetryEnabled ? "ON" : "OFF";
        toggleBtn.classList.toggle('off', !isRetryEnabled);
        resetState(isRetryEnabled ? "Ready" : "Disabled");
        if(!isRetryEnabled) statusText.className = 'status-error';
    });

    autoClickCb.addEventListener('change', (e) => GM_setValue('autoClickEnabled', (autoClickEnabled = e.target.checked)));
    limitInput.addEventListener('change', (e) => GM_setValue('maxRetries', (maxRetries = parseInt(e.target.value))));

    document.addEventListener('input', (e) => {
        if (e.target.matches(TARGET_TEXTAREA_SELECTOR)) {
            const val = e.target.value;
            if (val !== promptBox.value) { promptBox.value = val; lastTypedPrompt = val; }
            if (val.trim()) resetState("Ready");
        }
    }, true);

    const observer = new MutationObserver(() => {
        if (!isRetryEnabled || limitReached) return;
        const modSpan = Array.from(document.querySelectorAll('span')).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});

    function handleRetry(now) {
        lastRetryTimestamp = now;
        const grokTA = document.querySelector(TARGET_TEXTAREA_SELECTOR);
        const btn = document.querySelector(RETRY_BUTTON_SELECTOR);
        if (grokTA && lastTypedPrompt) {
            nativeValueSet(grokTA, lastTypedPrompt);
            if (autoClickEnabled && currentRetryCount >= maxRetries) {
                updateStatus("Limit Reached", "error");
                limitReached = true;
                return;
            }
            if (autoClickEnabled && btn) {
                currentRetryCount++;
                updateStatus(`Retrying (${currentRetryCount}/${maxRetries})`);
                setTimeout(() => btn.click(), RETRY_DELAY_MS);
            }
        }
    }

    function resetState(msg) { limitReached = false; currentRetryCount = 0; updateStatus(msg); }
    function updateStatus(msg, type) { statusText.textContent = msg; statusText.className = type === 'error' ? 'status-error' : ''; }

    document.addEventListener('keydown', (e) => {
        if (e.altKey && e.key.toLowerCase() === uiToggleKey) {
            isUiVisible = !isUiVisible;
            GM_setValue('isUiVisible', isUiVisible);
            panel.classList.toggle('hidden', !isUiVisible);
        }
    });
})();