// ==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 // @downloadURL https://update.greasyfork.icu/scripts/533963/Grokcom%20Rate%20Limit%20Display.user.js // @updateURL https://update.greasyfork.icu/scripts/533963/Grokcom%20Rate%20Limit%20Display.meta.js // ==/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: 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: 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); } })();