// ==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);
}
})();