// ==UserScript==
// @name Grok Rate Limit Display
// @namespace http://tampermonkey.net/
// @version 2.7
// @description Displays Grok rate limit on screen based on selected model/mode
// @author Blankspeaker, Originally ported from CursedAtom's chrome extension
// @match https://grok.com/*
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
console.log('Grok Rate Limit Script loaded');
// Variable to store the last known rate limit values
let lastRateLimit = { remainingQueries: null, totalQueries: null };
const MODEL_MAP = {
"Grok 4": "grok-4",
"Grok 3": "grok-3",
"Grok 4 Heavy": "grok-4-heavy",
};
const DEFAULT_MODEL = "grok-4";
const DEFAULT_KIND = "DEFAULT";
const POLL_INTERVAL_MS = 30000;
const MODEL_SELECTOR = "span.inline-block.text-primary";
const QUERY_BAR_SELECTOR = ".query-bar";
const ELEMENT_WAIT_TIMEOUT_MS = 5000;
const ATTACH_SVG_PATH = "M10 9V15C10 16.1046 10.8954 17 12 17V17C13.1046 17 14 16.1046 14 15V7C14 4.79086 12.2091 3 10 3V3C7.79086 3 6 4.79086 6 7V15C6 18.3137 8.68629 21 12 21V21C15.3137 21 18 18.3137 18 15V8";
const SUBMIT_SVG_PATH = "M5 11L12 4M12 4L19 11M12 4V21";
const LOADING_SVG_PATH = "M4 9.2v5.6c0 1.116 0 1.673.11 2.134a4 4 0 0 0 2.956 2.956c.46.11 1.018.11 2.134.11h5.6c1.116 0 1.673 0 2.134-.11a4 4 0 0 0 2.956-2.956c.11-.46.11-1.018.11-2.134V9.2c0-1.116 0-1.673-.11-2.134a4 4 0 0 0-2.956-2.955C16.474 4 15.916 4 14.8 4H9.2c-1.116 0-1.673 0-2.134.11a4 4 0 0 0-2.955 2.956C4 7.526 4 8.084 4 9.2Z";
const RATE_LIMIT_CONTAINER_ID = "grok-rate-limit";
const cachedRateLimits = {};
// Function to wait for element appearance
function waitForElement(selector, timeout = ELEMENT_WAIT_TIMEOUT_MS, root = document) {
return new Promise((resolve) => {
let element = root.querySelector(selector);
if (element) {
resolve(element);
return;
}
const observer = new MutationObserver(() => {
element = root.querySelector(selector);
if (element) {
observer.disconnect();
resolve(element);
}
});
observer.observe(root, { childList: true, subtree: true });
setTimeout(() => {
observer.disconnect();
resolve(null);
}, timeout);
});
}
// Function to find button by text or aria-label
function findButtonByText(text, startsWith = false, root) {
const buttons = root.querySelectorAll('button');
for (const btn of buttons) {
const btnText = btn.textContent?.trim();
const ariaLabel = btn.getAttribute('aria-label')?.trim();
const matchesBtn = startsWith ? (btnText?.startsWith(text) || ariaLabel?.startsWith(text)) : (btnText === text || ariaLabel === text);
if (matchesBtn) {
return btn;
}
const span = btn.querySelector('span');
const spanText = span?.textContent?.trim();
const matchesSpan = startsWith ? spanText?.startsWith(text) : spanText === text;
if (matchesSpan) {
return btn;
}
}
return null;
}
// Async find button
async function findButtonByTextAsync(text, startsWith = false, timeout = ELEMENT_WAIT_TIMEOUT_MS, root = document) {
let found = findButtonByText(text, startsWith, root);
if (found) {
return found;
}
return new Promise((resolve) => {
const observer = new MutationObserver(() => {
found = findButtonByText(text, startsWith, root);
if (found) {
observer.disconnect();
resolve(found);
}
});
observer.observe(root, { childList: true, subtree: true });
setTimeout(() => {
observer.disconnect();
resolve(null);
}, timeout);
});
}
// Function to remove any existing rate limit display
function removeExistingRateLimit() {
const existing = document.getElementById(RATE_LIMIT_CONTAINER_ID);
if (existing) {
existing.remove();
}
}
// Function to normalize model names
function normalizeModelName(modelName) {
const trimmed = modelName.trim();
if (!trimmed) {
return DEFAULT_MODEL;
}
return MODEL_MAP[trimmed] || trimmed.toLowerCase().replace(/\s+/g, "-");
}
// Function to update or inject the rate limit display
function updateRateLimitDisplay(queryBar, response) {
let rateLimitContainer = document.getElementById(RATE_LIMIT_CONTAINER_ID);
if (!rateLimitContainer) {
const bottomBar = queryBar.querySelector('.flex.gap-1\\.5.absolute.inset-x-0.bottom-0');
if (!bottomBar) {
return;
}
const attachButton = bottomBar.querySelector('button[aria-label="Attach"]');
if (!attachButton) {
return;
}
rateLimitContainer = document.createElement('div');
rateLimitContainer.id = RATE_LIMIT_CONTAINER_ID;
rateLimitContainer.className = 'inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-60 disabled:cursor-not-allowed [&_svg]:duration-100 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:-mx-0.5 select-none border-border-l2 text-fg-primary hover:bg-button-ghost-hover disabled:hover:bg-transparent h-10 px-3.5 py-2 text-sm rounded-full group/rate-limit transition-colors duration-100 relative overflow-hidden border cursor-pointer';
rateLimitContainer.style.opacity = '0.8';
rateLimitContainer.style.transition = 'opacity 0.1s ease-in-out';
rateLimitContainer.addEventListener('click', () => {
fetchAndUpdateRateLimit(queryBar, true);
});
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', '18');
svg.setAttribute('height', '18');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('fill', 'none');
svg.setAttribute('class', 'stroke-[2] text-fg-secondary group-hover/rate-limit:text-fg-primary transition-colors duration-100');
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('cx', '12');
circle.setAttribute('cy', '12');
circle.setAttribute('r', '8');
circle.setAttribute('stroke', 'currentColor');
circle.setAttribute('stroke-width', '2');
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', 'M12 12L12 6');
path.setAttribute('stroke', 'currentColor');
path.setAttribute('stroke-width', '2');
path.setAttribute('stroke-linecap', 'round');
svg.appendChild(circle);
svg.appendChild(path);
rateLimitContainer.appendChild(svg);
const textSpan = document.createElement('span');
rateLimitContainer.appendChild(textSpan);
attachButton.insertAdjacentElement('afterend', rateLimitContainer);
}
const textSpan = rateLimitContainer.querySelector('span');
if (response.error) {
if (lastRateLimit.remainingQueries !== null && lastRateLimit.totalQueries !== null) {
textSpan.textContent = `${lastRateLimit.remainingQueries}/${lastRateLimit.totalQueries}`;
} else {
textSpan.textContent = 'Unavailable';
}
} else {
const { remainingQueries, totalQueries } = response;
lastRateLimit.remainingQueries = remainingQueries;
lastRateLimit.totalQueries = totalQueries;
textSpan.textContent = `${remainingQueries}/${totalQueries}`;
}
}
// Function to fetch rate limit
async function fetchRateLimit(modelName, requestKind, force = false) {
if (!force) {
const cached = cachedRateLimits[modelName]?.[requestKind];
if (cached !== undefined) {
return cached;
}
}
try {
const response = await fetch(window.location.origin + '/rest/rate-limits', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
requestKind,
modelName,
}),
credentials: 'include',
});
if (!response.ok) {
throw new Error(`HTTP error: Status ${response.status}`);
}
const data = await response.json();
if (!cachedRateLimits[modelName]) {
cachedRateLimits[modelName] = {};
}
cachedRateLimits[modelName][requestKind] = data;
return data;
} catch (error) {
console.error(`Failed to fetch rate limit:`, error);
if (!cachedRateLimits[modelName]) {
cachedRateLimits[modelName] = {};
}
cachedRateLimits[modelName][requestKind] = undefined;
return { error: true };
}
}
// Function to fetch and update rate limit
async function fetchAndUpdateRateLimit(queryBar, force = false) {
if (!queryBar || !document.body.contains(queryBar)) {
return;
}
const modelSpan = await waitForElement(MODEL_SELECTOR, ELEMENT_WAIT_TIMEOUT_MS, queryBar);
let modelName = DEFAULT_MODEL;
if (modelSpan) {
modelName = normalizeModelName(modelSpan.textContent.trim());
}
let requestKind = DEFAULT_KIND;
if (modelName === 'grok-3') {
const thinkButton = await findButtonByTextAsync('Think', false, ELEMENT_WAIT_TIMEOUT_MS, queryBar);
const searchButton = await findButtonByTextAsync('Deep', true, ELEMENT_WAIT_TIMEOUT_MS, queryBar);
if (thinkButton && thinkButton.getAttribute('aria-pressed') === 'true') {
requestKind = 'REASONING';
} else if (searchButton && searchButton.getAttribute('aria-pressed') === 'true') {
const modeSpan = searchButton.querySelector('span');
let modeText = modeSpan ? modeSpan.textContent.trim() : searchButton.getAttribute('aria-label');
if (modeText === 'DeepSearch') {
requestKind = 'DEEPSEARCH';
} else if (modeText === 'DeeperSearch') {
requestKind = 'DEEPERSEARCH';
}
}
}
const data = await fetchRateLimit(modelName, requestKind, force);
updateRateLimitDisplay(queryBar, data);
}
// Function to observe the DOM for the query bar
function observeDOM() {
let lastQueryBar = null;
let lastModelObserver = null;
let lastThinkObserver = null;
let lastSearchObserver = null;
let lastInputElement = null;
let pollInterval = null;
const handleVisibilityChange = () => {
if (document.visibilityState === 'visible' && lastQueryBar) {
fetchAndUpdateRateLimit(lastQueryBar, true);
pollInterval = setInterval(() => fetchAndUpdateRateLimit(lastQueryBar, true), POLL_INTERVAL_MS);
} else {
if (pollInterval) {
clearInterval(pollInterval);
pollInterval = null;
}
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
const initialQueryBar = document.querySelector(QUERY_BAR_SELECTOR);
if (initialQueryBar) {
removeExistingRateLimit();
fetchAndUpdateRateLimit(initialQueryBar);
lastQueryBar = initialQueryBar;
setupModelObserver(initialQueryBar);
setupGrok3Observers(initialQueryBar);
setupSubmissionListeners(initialQueryBar);
if (document.visibilityState === 'visible') {
pollInterval = setInterval(() => fetchAndUpdateRateLimit(lastQueryBar, true), POLL_INTERVAL_MS);
}
}
const observer = new MutationObserver(() => {
const queryBar = document.querySelector(QUERY_BAR_SELECTOR);
if (queryBar && queryBar !== lastQueryBar) {
removeExistingRateLimit();
fetchAndUpdateRateLimit(queryBar);
lastQueryBar = queryBar;
if (lastModelObserver) {
lastModelObserver.disconnect();
}
if (lastThinkObserver) {
lastThinkObserver.disconnect();
}
if (lastSearchObserver) {
lastSearchObserver.disconnect();
}
setupModelObserver(queryBar);
setupGrok3Observers(queryBar);
setupSubmissionListeners(queryBar);
if (document.visibilityState === 'visible') {
if (pollInterval) clearInterval(pollInterval);
pollInterval = setInterval(() => fetchAndUpdateRateLimit(lastQueryBar, true), POLL_INTERVAL_MS);
}
} else if (!queryBar && lastQueryBar) {
removeExistingRateLimit();
if (lastModelObserver) {
lastModelObserver.disconnect();
}
if (lastThinkObserver) {
lastThinkObserver.disconnect();
}
if (lastSearchObserver) {
lastSearchObserver.disconnect();
}
lastQueryBar = null;
lastModelObserver = null;
lastThinkObserver = null;
lastSearchObserver = null;
lastInputElement = null;
if (pollInterval) {
clearInterval(pollInterval);
pollInterval = null;
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
function setupModelObserver(queryBar) {
const modelSpan = queryBar.querySelector(MODEL_SELECTOR);
if (modelSpan) {
lastModelObserver = new MutationObserver(() => {
fetchAndUpdateRateLimit(queryBar);
setupGrok3Observers(queryBar);
});
lastModelObserver.observe(modelSpan, { characterData: true, childList: true, subtree: true });
}
}
async function setupGrok3Observers(queryBar) {
const modelSpan = queryBar.querySelector(MODEL_SELECTOR);
const currentModel = normalizeModelName(modelSpan ? modelSpan.textContent.trim() : DEFAULT_MODEL);
if (currentModel === 'grok-3') {
const thinkButton = await findButtonByTextAsync('Think', false, ELEMENT_WAIT_TIMEOUT_MS, queryBar);
if (thinkButton) {
if (lastThinkObserver) lastThinkObserver.disconnect();
lastThinkObserver = new MutationObserver(() => {
fetchAndUpdateRateLimit(queryBar);
});
lastThinkObserver.observe(thinkButton, { attributes: true, attributeFilter: ['aria-pressed', 'class'] });
}
const searchButton = await findButtonByTextAsync('Deep', true, ELEMENT_WAIT_TIMEOUT_MS, queryBar);
if (searchButton) {
if (lastSearchObserver) lastSearchObserver.disconnect();
lastSearchObserver = new MutationObserver(() => {
fetchAndUpdateRateLimit(queryBar);
});
lastSearchObserver.observe(searchButton, { attributes: true, attributeFilter: ['aria-pressed', 'class'], childList: true, subtree: true, characterData: true });
}
} else {
if (lastThinkObserver) {
lastThinkObserver.disconnect();
lastThinkObserver = null;
}
if (lastSearchObserver) {
lastSearchObserver.disconnect();
lastSearchObserver = null;
}
}
}
function setupSubmissionListeners(queryBar) {
const inputElement = queryBar.querySelector('textarea');
if (inputElement && inputElement !== lastInputElement) {
lastInputElement = inputElement;
inputElement.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
console.log('Enter pressed for submit');
setTimeout(() => fetchAndUpdateRateLimit(queryBar, true), 5000);
}
});
}
}
}
// Start observing the DOM for changes
observeDOM();
})();