Grok.com Rate Limit Display

Displays rate limit (Queries left: Remaining of Total / WindowSize) on Grok.com

// ==UserScript==
// @name         Grok.com Rate Limit Display
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Displays rate limit (Queries left: Remaining of Total / WindowSize) on Grok.com
// @author       Blankspeaker (based on the chrome extension by CursedAtom) & Grok
// @match        https://grok.com/*
// @grant        none
// @license      GNU GPLv3
// ==/UserScript==

/*
 * DEBUG_MODE: Set to true for detailed logs (e.g., polling, span creation, click events).
 * Set to false for minimal logs (only errors). Default: false.
 */
(function() {
    'use strict';

    const DEBUG_MODE = false; // Set to true for troubleshooting, false for minimal output

    // Function to log messages based on debug mode
    function log(message) {
        if (DEBUG_MODE) {
            console.log(`[Grok Rate Limit] ${message}`);
        }
    }

    // Function to poll for an element
    async function pollForElement(selector, maxAttempts = 5, interval = 1000) {
        let attempts = 0;
        while (attempts < maxAttempts) {
            const element = document.querySelector(selector);
            if (element) {
                log(`Element found: ${selector}`);
                return element;
            }
            attempts++;
            log(`Element not found: ${selector}, attempt ${attempts}/${maxAttempts}`);
            await new Promise(resolve => setTimeout(resolve, interval));
        }
        log(`Max attempts reached, could not find element: ${selector}`);
        return null;
    }

    // Function to convert seconds to hours for display
    function secondsToHours(seconds) {
        if (!seconds || isNaN(seconds)) {
            return 'Unknown';
        }
        const hours = Math.floor(seconds / 3600);
        return hours === 0 ? '<1' : hours;
    }

    // Function to fetch rate limit
    async function fetchRateLimit() {
        try {
            // Use a simpler selector to match the span containing "Grok 2" or "Grok 3"
            const selector = 'span.inline-block.text-primary.text-xs';
            const modelSpan = await pollForElement(selector);
            let modelName = 'grok-3'; // Default to grok-3
            if (modelSpan) {
                const modelText = modelSpan.textContent.trim();
                if (modelText === 'Grok 2') {
                    modelName = 'grok-2';
                } else if (modelText === 'Grok 3') {
                    modelName = 'grok-3';
                }
                log(`Model detected: ${modelText}, setting modelName to ${modelName}`);
            } else {
                log('Model span not found, defaulting to modelName: grok-3');
            }

            const response = await fetch('https://grok.com/rest/rate-limits', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({
                    requestKind: 'DEFAULT',
                    modelName: modelName,
                }),
                credentials: 'include',
            });

            if (!response.ok) {
                throw new Error(`HTTP error: ${response.status}`);
            }

            const data = await response.json();
            log('Rate limit data: ' + JSON.stringify(data));
            return data;
        } catch (error) {
            console.error('[Grok Rate Limit] Failed to fetch rate limit:', error);
            return null;
        }
    }

    // Function to update or create the rate limit display
    async function displayRateLimit() {
        try {
            // Check if rate limit span already exists
            const existingSpan = document.querySelector('span.rate-limit-span');
            if (existingSpan) {
                // Update existing span
                const rateLimitData = await fetchRateLimit();
                if (rateLimitData) {
                    const remaining = rateLimitData.remainingQueries ?? 'Unknown';
                    const total = rateLimitData.totalQueries ?? 'Unknown';
                    const windowSizeHours = secondsToHours(rateLimitData.windowSizeSeconds);
                    existingSpan.textContent = `Queries left: ${remaining} of ${total} / ${windowSizeHours}hr `;
                }
                log('Rate limit span updated');
                return true; // Indicate span exists
            }

            // Try primary target: <a> with href="/chat" (for private chat page)
            let targetLink = document.querySelector('a.inline-flex.items-center.justify-center.gap-2[href="/chat"]');
            if (!targetLink) {
                // Fall back to alternative target: <a> with href="/chat#private" (for main page)
                targetLink = document.querySelector('a.inline-flex.items-center.justify-center.gap-2[href="/chat#private"][aria-label="Switch to Private Chat"]');
                if (!targetLink) {
                    // Broaden the selector to any link containing "chat" in href
                    targetLink = document.querySelector('a[href*="/chat"]');
                    if (!targetLink) {
                        log('All target chat links not found');
                        return false; // Indicate failure to find target
                    }
                }
            }

            // Fetch rate limit data
            const rateLimitData = await fetchRateLimit();
            if (!rateLimitData) {
                return false;
            }

            // Extract rate limit info
            const remaining = rateLimitData.remainingQueries ?? 'Unknown';
            const total = rateLimitData.totalQueries ?? 'Unknown';
            const windowSizeHours = secondsToHours(rateLimitData.windowSizeSeconds);

            // Create rate limit display element
            const rateLimitSpan = document.createElement('span');
            rateLimitSpan.textContent = `Queries left: ${remaining} of ${total} / ${windowSizeHours}hr `;
            rateLimitSpan.classList.add('rate-limit-span');
            rateLimitSpan.style.marginRight = '8px';
            rateLimitSpan.style.color = '#666';
            rateLimitSpan.style.fontSize = '12px';
            rateLimitSpan.style.display = 'inline-flex';
            rateLimitSpan.style.alignItems = 'center';

            // Insert to the left of the target link
            targetLink.parentNode.insertBefore(rateLimitSpan, targetLink);
            log('Rate limit span created');
            return true; // Indicate success
        } catch (error) {
            console.error('[Grok Rate Limit] Error in displayRateLimit:', error);
            return false;
        }
    }

    // Function to poll for either target element with retries
    async function pollForTarget(maxAttempts = 5, interval = 3000) {
        try {
            let attempts = 0;
            while (attempts < maxAttempts) {
                // Early exit if span already exists
                if (document.querySelector('span.rate-limit-span')) {
                    log('Rate limit span already exists, stopping polling');
                    return;
                }
                const success = await displayRateLimit();
                if (success) {
                    return; // Exit once displayed
                }
                attempts++;
                log(`Polling attempt ${attempts}/${maxAttempts}`);
                await new Promise(resolve => setTimeout(resolve, interval));
            }
            log('Max attempts reached, could not find either target chat link');
        } catch (error) {
            console.error('[Grok Rate Limit] Error in pollForTarget:', error);
        }
    }

    // Run initially with a delay to allow page to load
    try {
        setTimeout(() => {
            pollForTarget();
        }, 2000); // Start after 2 seconds
    } catch (error) {
        console.error('[Grok Rate Limit] Error setting up delayed polling:', error);
    }

    // Periodically refresh rate limit (every 120 seconds)
    try {
        setInterval(async () => {
            const existingSpan = document.querySelector('span.rate-limit-span');
            if (existingSpan) {
                const rateLimitData = await fetchRateLimit();
                if (rateLimitData) {
                    const remaining = rateLimitData.remainingQueries ?? 'Unknown';
                    const total = rateLimitData.totalQueries ?? 'Unknown';
                    const windowSizeHours = secondsToHours(rateLimitData.windowSizeSeconds);
                    existingSpan.textContent = `Queries left: ${remaining} of ${total} / ${windowSizeHours}hr `;
                    log('Rate limit span refreshed');
                }
            }
        }, 120000); // Refresh every 2 minutes
    } catch (error) {
        console.error('[Grok Rate Limit] Error setting up periodic refresh:', error);
    }

    // Add click listener for the send button SVG to refresh rate limit after 1 second
    try {
        document.addEventListener('click', async (event) => {
            const svg = event

.target.closest('svg[width="20"][height="20"][viewBox="0 0 24 24"][fill="none"][class="stroke-[2] relative"]');
            if (svg && svg.querySelector('path[d="M5 11L12 4M12 4L19 11M12 4V21"]')) {
                log('Send button SVG clicked, scheduling refresh');
                setTimeout(async () => {
                    const existingSpan = document.querySelector('span.rate-limit-span');
                    if (existingSpan) {
                        const rateLimitData = await fetchRateLimit();
                        if (rateLimitData) {
                            const remaining = rateLimitData.remainingQueries ?? 'Unknown';
                            const total = rateLimitData.totalQueries ?? 'Unknown';
                            const windowSizeHours = secondsToHours(rateLimitData.windowSizeSeconds);
                            existingSpan.textContent = `Queries left: ${remaining} of ${total} / ${windowSizeHours}hr `;
                            log('Rate limit span refreshed after click');
                        }
                    } else {
                        // If no span exists, trigger full display logic
                        await displayRateLimit();
                    }
                }, 1000); // Refresh after 1 second
            }
        });
    } catch (error) {
        console.error('[Grok Rate Limit] Error setting up click listener:', error);
    }
})();