Displays remaining queries on grok.com (UPDATED for 4.3 & Imagine)
// ==UserScript==
// @name Grok Rate Limit Display
// @namespace http://tampermonkey.net/
// @version 5.6.5
// @description Displays remaining queries on grok.com (UPDATED for 4.3 & Imagine)
// @author Blankspeaker, Originally ported from CursedAtom's chrome extension
// @match https://grok.com/*
// @icon https://img.icons8.com/color/1200/grok--v2.jpg
// @license MIT
// @run-at document-end
// ==/UserScript==
// Grok Rate Limit Display Chrome Extension
// Default model updated to Grok 4
// Version 1.6.5
(function () {
'use strict';
// Inlined Interceptor function for Userscript MAIN world injection
function runInterceptor() {
// Grok 4.20 Request Interceptor
// Injected into the MAIN world to see request bodies
(function () {
function notifyUsage(model, url) {
window.dispatchEvent(new CustomEvent('GROK_USAGE_DETECTED_EVENT', {
detail: {
model: model,
url: url,
timestamp: Date.now()
}
}));
}
function notifyRateLimited(model) {
window.dispatchEvent(new CustomEvent('GROK_RATE_LIMITED_EVENT', {
detail: {
model: model,
timestamp: Date.now()
}
}));
}
function getModelFromBody(body) {
if (!body) return null;
// Normalize checking against different possible fields
const modeId = body.modeId || body.modelName || body.metadata?.request_metadata?.mode ||
body.metadata?.requestMetadata?.mode || body.metadata?.modelName || body.metadata?.model_name;
const modelMode = body.modelMode;
if (modeId === 'grok-420' || modeId === 'grok-420-computer-use-sa' || modelMode === 'MODEL_MODE_GROK_420') {
return modeId;
}
if (modeId === 'heavy' || modelMode === 'MODEL_MODE_HEAVY' || modeId === 'grok-4-heavy') {
return 'grok-4-heavy';
}
if (modeId === 'fast' || modelMode === 'MODEL_MODE_FAST' || modeId === 'grok-3') {
return 'grok-3';
}
if (modeId === 'expert' || modelMode === 'MODEL_MODE_EXPERT' || modeId === 'grok-4') {
return 'grok-4';
}
if (modeId === 'auto' || modelMode === 'MODEL_MODE_AUTO' || modeId === 'grok-4-auto') {
return 'auto';
}
return null;
}
function wrapFetch() {
if (window.fetch._isWrapped) return;
const originalFetch = window.fetch;
window.fetch = async function (...args) {
const urlOrRequest = args[0];
const options = args[1] || {};
const url = (urlOrRequest instanceof Request) ? urlOrRequest.url : String(urlOrRequest);
// Execute original fetch
const result = originalFetch.apply(this, args);
try {
if (url.includes('/rest/app-chat/conversations/new') || (url.includes('/responses') && url.includes('/rest/app-chat/conversations/'))) {
let body = null;
if (options.body) {
try {
if (typeof options.body === 'string') {
body = JSON.parse(options.body);
} else if (typeof options.body === 'object' && !(options.body instanceof ReadableStream)) {
body = options.body;
}
} catch (e) { }
}
if (!body && urlOrRequest instanceof Request) {
try {
body = await urlOrRequest.clone().json();
} catch (e) { }
}
const model = getModelFromBody(body);
if (model) {
if (model === 'auto') {
result.then(async (response) => {
if (response.ok) {
try {
const clone = response.clone();
const reader = clone.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
if (buffer.includes('"effort":"high"') || buffer.includes('"effort": "high"') ||
buffer.includes('"effortLevel":"high"') || buffer.includes('"is_high_effort":true')) {
notifyUsage('grok-4', url);
break;
} else if (buffer.includes('"effort":"low"') || buffer.includes('"effort": "low"') ||
buffer.includes('"effortLevel":"low"') || buffer.includes('"is_high_effort":false')) {
notifyUsage('grok-3', url);
break;
}
if (buffer.length > 50000) buffer = buffer.slice(-10000);
}
} catch(e) {}
}
}).catch(() => {});
} else {
notifyUsage(model, url);
}
// Also monitor the response for "Too many requests" errors
result.then(async (response) => {
if (response.status === 429 || !response.ok) {
try {
const respClone = response.clone();
const respData = await respClone.json();
if (respData?.error?.message === "Too many requests" || respData?.error?.code === 8) {
notifyRateLimited(model === 'auto' ? 'grok-4' : model);
}
} catch (e) { }
}
}).catch(() => { });
}
}
} catch (e) {
}
return result;
};
window.fetch._isWrapped = true;
}
function wrapXHR() {
if (XMLHttpRequest.prototype.send._isWrapped) return;
const originalOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (method, url) {
this._url = url;
return originalOpen.apply(this, arguments);
};
const originalSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function (body) {
try {
if (this._url && (this._url.includes('/conversations/new') || this._url.includes('/responses'))) {
let parsedBody = null;
if (typeof body === 'string') {
try {
parsedBody = JSON.parse(body);
} catch (e) { }
}
const model = getModelFromBody(parsedBody);
if (model) {
if (model !== 'auto') {
notifyUsage(model, this._url);
}
// For XHR, we listen to load/error events
this.addEventListener('load', () => {
try {
if (model === 'auto' && this.status >= 200 && this.status < 300) {
if (this.responseText.includes('"effort":"high"') || this.responseText.includes('"effort": "high"')) {
notifyUsage('grok-4', this._url);
} else if (this.responseText.includes('"effort":"low"') || this.responseText.includes('"effort": "low"')) {
notifyUsage('grok-3', this._url);
}
}
if (this.status === 429) {
notifyRateLimited(model === 'auto' ? 'grok-4' : model);
} else {
const respData = JSON.parse(this.responseText);
if (respData?.error?.message === "Too many requests" || respData?.error?.code === 8) {
notifyRateLimited(model === 'auto' ? 'grok-4' : model);
}
}
} catch (e) { }
});
}
}
} catch (e) { }
return originalSend.apply(this, arguments);
};
XMLHttpRequest.prototype.send._isWrapped = true;
}
// Wrap immediately
wrapFetch();
wrapXHR();
// Re-wrap on delays to catch late overrides from site scripts
setTimeout(wrapFetch, 1000);
setTimeout(wrapFetch, 3000);
setTimeout(wrapFetch, 10000);
})();
}
console.log('Grok Rate Limit Userscript loaded');
// Inject interceptor into MAIN world (inline for userscript compatibility)
try {
const s = document.createElement('script');
s.textContent = `(${runInterceptor.toString()})();`;
s.onload = function () { this.remove(); };
(document.head || document.documentElement).appendChild(s);
} catch (e) {
console.error('Grok Rate Limit Userscript: Failed to inject interceptor:', e);
}
// Listen for usage detected by interceptor
window.addEventListener('GROK_USAGE_DETECTED_EVENT', (event) => {
const { model } = event.detail;
console.log('Grok Rate Limit Extension: Usage detected for', model);
try {
if (chrome.runtime?.id) {
chrome.runtime.sendMessage({ type: 'GROK_USAGE_DETECTED', model: model });
}
} catch (e) {
console.warn('Grok Rate Limit Extension: Extension context invalidated. Please refresh the page.');
}
});
// Listen for rate limit errors detected by interceptor
window.addEventListener('GROK_RATE_LIMITED_EVENT', (event) => {
const { model } = event.detail;
console.warn('Grok Rate Limit Extension: UI detected rate limit hit for', model);
try {
if (chrome.runtime?.id) {
chrome.runtime.sendMessage({ type: 'RATE_LIMIT_HIT', model: model });
}
} catch (e) { }
});
// Listen for refresh messages from background script (extension context only)
if (typeof chrome !== 'undefined' && chrome.runtime?.onMessage) {
chrome.runtime.onMessage.addListener((message) => {
if (message.type === 'REFRESH_UI') {
if (lastQueryBar) {
// Poll up to 3 times if count hasn't changed yet
fetchAndUpdateRateLimit(lastQueryBar, true, 3);
}
}
});
}
let lastHigh = { remaining: null, wait: null };
let lastLow = { remaining: null, wait: null };
let lastBoth = { high: null, low: null, wait: null };
let lastImagine = { remaining: null, wait: null };
let lastMulti = null;
let grok420Fetches = 0;
let grok420HasDropped = false;
const MODEL_MAP = {
"Grok 4.3 (beta)": "grok-420-computer-use-sa",
"Grok 4.20 (Beta)": "grok-420",
"Grok 420": "grok-420",
"Grok 4": "grok-4",
"Grok 3": "grok-3",
"Grok 4 Heavy": "grok-4-heavy",
"Grok 4 With Effort Decider": "grok-4-auto",
"Auto": "grok-4-auto",
"Fast": "grok-3",
"Expert": "grok-4",
"Heavy": "grok-4-heavy",
"Grok 4 Fast": "grok-4-mini-thinking-tahoe",
"Grok 4.1": "grok-4-1-non-thinking-w-tool",
"Grok 4.1 Thinking": "grok-4-1-thinking-1129",
"Grok 2": "grok-2",
"Grok 2 Mini": "grok-2-mini",
};
const DEFAULT_KIND = "DEFAULT";
const DEFAULT_MODEL = "grok-4-auto";
const MODEL_SELECTOR = "button[aria-label='Model select']";
const QUERY_BAR_SELECTOR = ".query-bar";
const ELEMENT_WAIT_TIMEOUT_MS = 5000;
const RATE_LIMIT_CONTAINER_ID = "grok-rate-limit";
const cachedRateLimits = {};
let countdownTimer = null;
let isCountingDown = false;
let lastQueryBar = null;
let lastModelObserver = null;
let lastThinkObserver = null;
let lastSearchObserver = null;
let lastInputElement = null;
let lastSubmitButton = null;
let lastModelName = null;
// State for overlap checking
let overlapCheckInterval = null;
let isHiddenDueToOverlap = false;
const commonFinderConfigs = {
thinkButton: {
selector: "button",
ariaLabel: "Think",
svgPartialD: "M19 9C19 12.866",
},
deepSearchButton: {
selector: "button",
ariaLabelRegex: /Deep(er)?Search/i,
},
attachButton: {
selector: "button",
classContains: ["group/attach-button"],
},
submitButton: {
selector: "button",
svgPartialD: "M6 11L12 5M12 5L18 11M12 5V19",
}
};
// Function to check if current page is under /imagine
function isImaginePage() {
return window.location.pathname.startsWith('/imagine');
}
// Debounce function
function debounce(func, delay) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), delay);
};
}
// Function to find element based on config (OR logic for conditions)
function findElement(config, root = document) {
const elements = root.querySelectorAll(config.selector);
for (const el of elements) {
let satisfied = 0;
if (config.ariaLabel) {
if (el.getAttribute('aria-label') === config.ariaLabel) satisfied++;
}
if (config.ariaLabelRegex) {
const aria = el.getAttribute('aria-label');
if (aria && config.ariaLabelRegex.test(aria)) satisfied++;
}
if (config.svgPartialD) {
const path = el.querySelector('path');
if (path && path.getAttribute('d')?.includes(config.svgPartialD)) satisfied++;
}
if (config.classContains) {
if (config.classContains.some(cls => el.classList.contains(cls))) satisfied++;
}
if (satisfied > 0) {
return el;
}
}
return null;
}
// Function to format timer for display (H:MM:SS or MM:SS)
function formatTimer(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
if (hours > 0) {
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
} else {
return `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
}
// Function to check if text content overlaps with rate limit display
function checkTextOverlap(queryBar) {
const rateLimitContainer = document.getElementById(RATE_LIMIT_CONTAINER_ID);
if (!rateLimitContainer) return;
// Look for both mobile textarea and desktop contenteditable
const contentEditable = queryBar.querySelector('div[contenteditable="true"]');
const textArea = queryBar.querySelector('textarea[aria-label*="Ask Grok"]');
const inputElement = contentEditable || textArea;
if (!inputElement) return;
// Get the text content from either element
const textContent = inputElement.value || inputElement.textContent || '';
const textLength = textContent.trim().length;
// Calculate available space more accurately
const queryBarWidth = queryBar.offsetWidth;
const rateLimitWidth = rateLimitContainer.offsetWidth;
const availableSpace = queryBarWidth - rateLimitWidth - 100; // 100px buffer
// More aggressive detection for small screens
const isSmallScreen = window.innerWidth < 900 ||
availableSpace < 200 ||
window.screen?.width < 500 ||
document.documentElement.clientWidth < 500;
// Very conservative approach: on small screens hide immediately when typing
// On larger screens, give more room
const characterLimit = isSmallScreen ? 0 : 28;
const shouldHide = textLength > characterLimit;
if (shouldHide && !isHiddenDueToOverlap) {
// Add slide-right animation to match model picker
rateLimitContainer.style.transition = 'transform 0.2s ease-out, opacity 0.2s ease-out';
rateLimitContainer.style.transform = 'translateX(100%)';
rateLimitContainer.style.opacity = '0';
// After animation, hide completely
setTimeout(() => {
if (isHiddenDueToOverlap) {
rateLimitContainer.style.display = 'none';
}
}, 200);
isHiddenDueToOverlap = true;
} else if (!shouldHide && isHiddenDueToOverlap) {
// Show and slide back in
rateLimitContainer.style.display = '';
rateLimitContainer.style.transition = 'transform 0.2s ease-out, opacity 0.2s ease-out';
// Force a reflow to ensure display change takes effect
rateLimitContainer.offsetHeight;
rateLimitContainer.style.transform = 'translateX(0)';
rateLimitContainer.style.opacity = '0.8';
isHiddenDueToOverlap = false;
}
}
// Function to start overlap checking for a query bar
function startOverlapChecking(queryBar) {
// Clear any existing interval
if (overlapCheckInterval) {
clearInterval(overlapCheckInterval);
}
// Check for overlap less frequently to prevent flashing
overlapCheckInterval = setInterval(() => {
if (document.body.contains(queryBar)) {
checkTextOverlap(queryBar);
} else {
clearInterval(overlapCheckInterval);
overlapCheckInterval = null;
}
}, 500);
}
// Function to stop overlap checking
function stopOverlapChecking() {
if (overlapCheckInterval) {
clearInterval(overlapCheckInterval);
overlapCheckInterval = null;
}
isHiddenDueToOverlap = false;
}
// Function to remove any existing rate limit display
function removeExistingRateLimit() {
const existing = document.getElementById(RATE_LIMIT_CONTAINER_ID);
if (existing) {
existing.remove();
}
}
// Function to determine model key from SVG or text
function getCurrentModelKey(queryBar) {
if (isImaginePage()) {
const isPostPage = window.location.pathname.startsWith('/imagine/post');
// Multilingual matching for "Video" and "Image" radio tabs
const videoLabels = /Video|视频|影片|動画|비디오|Vidéo|Videoclipe/i;
const imageLabels = /Image|图片|图像|画像|이미지|Photo|Foto/i;
const radioButtons = Array.from(queryBar.querySelectorAll('button[role="radio"]'));
let videoBtn = null;
let imageBtn = null;
if (radioButtons.length >= 2) {
const videoIndex = radioButtons.findIndex(b => {
const text = b?.textContent || '';
const aria = b?.getAttribute('aria-label') || '';
return videoLabels.test(text) || videoLabels.test(aria);
});
if (videoIndex !== -1) {
videoBtn = radioButtons[videoIndex];
imageBtn = radioButtons[videoIndex === 0 ? 1 : 0];
} else {
// Fallback to position: first is Image, second is Video
imageBtn = radioButtons[0];
videoBtn = radioButtons[1];
}
} else {
videoBtn = radioButtons.find(b => {
const text = b?.textContent || '';
const aria = b?.getAttribute('aria-label') || '';
return videoLabels.test(text) || videoLabels.test(aria);
}) || queryBar.querySelector('button[role="radio"][aria-label="Video"]');
imageBtn = radioButtons.find(b => {
const text = b?.textContent || '';
const aria = b?.getAttribute('aria-label') || '';
return imageLabels.test(text) || imageLabels.test(aria);
}) || queryBar.querySelector('button[role="radio"][aria-label="Image"]');
}
// Video specifiers (480p, 720p) are technical constants and are not translated
const hasVideoControls = Array.from(queryBar.querySelectorAll('button')).some(b => b?.textContent?.includes('480p') || b?.textContent?.includes('720p'));
// Multilingual matching for "Quality" and "Speed" controls
const qualityLabels = /Quality|画质|质量|品质|画質|화질|품질|Calidad|Qualité|Qualität|Qualità|Qualidade|Качество/i;
const speedLabels = /Speed|速度|快速|高速|속도|Velocidad|Rápido|Vitesse|Rapide|Geschwindigkeit|Schnell|Velocità|Velocidade|Быстро/i;
const hasImageControls = Array.from(queryBar.querySelectorAll('button')).some(b => {
const text = b?.textContent || '';
return qualityLabels.test(text) || speedLabels.test(text);
});
// If controls are hidden (e.g., during generation), retain the last known mode
if (!videoBtn && !imageBtn && !hasVideoControls && !hasImageControls && lastModelName && ['image', 'imagePro', 'imageEdit', 'video', 'video720p'].includes(lastModelName)) {
return lastModelName;
}
let isVideoRadio = videoBtn && videoBtn.classList.contains('text-primary');
let isImageRadio = imageBtn && imageBtn.classList.contains('text-primary');
let isVideo = isVideoRadio || hasVideoControls;
let isImage = isImageRadio || (!isVideo);
// Default to image if none explicitly active
if (!isVideo && !isImage) {
isImage = true;
}
if (isVideo) {
const is720p = Array.from(queryBar.querySelectorAll('button')).some(b => b?.textContent?.includes('720p') && b.classList.contains('text-primary'));
return is720p ? 'video720p' : 'video';
} else {
if (isPostPage) {
return 'imageEdit';
} else {
const isQuality = Array.from(queryBar.querySelectorAll('button')).some(b => {
const text = b?.textContent || '';
return qualityLabels.test(text) && b.classList.contains('text-primary');
});
return isQuality ? 'imagePro' : 'image';
}
}
}
const modelButton = queryBar.querySelector(MODEL_SELECTOR);
if (!modelButton) return DEFAULT_MODEL;
// Check for text span first (updated selector for new UI)
const textElement = modelButton.querySelector('span.font-semibold');
if (textElement) {
const modelText = textElement.textContent.trim();
return MODEL_MAP[modelText] || DEFAULT_MODEL;
}
// Fallback to old chooser text span
const oldTextElement = modelButton.querySelector('span.inline-block');
if (oldTextElement) {
const modelText = oldTextElement.textContent.trim();
return MODEL_MAP[modelText] || DEFAULT_MODEL;
}
// New chooser: check SVG icon
const svg = modelButton.querySelector('svg');
if (svg) {
const pathsD = Array.from(svg.querySelectorAll('path'))
.map(p => p.getAttribute('d') || '')
.filter(d => d.length > 0)
.join(' ');
const hasBrainFill = svg.querySelector('path[class*="fill-yellow-100"]') !== null;
if (pathsD.includes('M6.5 12.5L11.5 17.5')) {
return 'grok-4-auto'; // Auto
} else if (pathsD.includes('M5 14.25L14 4')) {
return 'grok-3'; // Fast
} else if (hasBrainFill || pathsD.includes('M19 9C19 12.866')) {
return 'grok-4'; // Expert
} else if (pathsD.includes('M12 3a6 6 0 0 0 9 9')) {
return 'grok-4-mini-thinking-tahoe'; // Grok 4 Fast
} else if (pathsD.includes('M11 18H10C7.79086 18 6 16.2091 6 14V13')) {
return 'grok-4-heavy'; // Heavy
}
}
return DEFAULT_MODEL;
}
// Function to determine effort level based on model
function getEffortLevel(modelName) {
if (['image', 'imagePro', 'imageEdit', 'video', 'video720p'].includes(modelName)) {
return 'imagine';
} else if (modelName === 'grok-4-auto') {
return 'both';
} else if (modelName === 'grok-3') {
return 'low';
} else if (modelName === 'grok-4-1-non-thinking-w-tool') {
return 'low';
} else if (modelName === 'grok-4-1-thinking-1129') {
return 'high';
} else if (modelName === 'grok-420' || modelName === 'grok-420-computer-use-sa') {
return 'high';
} else {
// Grok 4, Heavy, and Grok 4.1 Thinking fall here
return 'high';
}
}
// Function to update or inject the rate limit display
function updateRateLimitDisplay(queryBar, response, effort, modelName, imagineData) {
let rateLimitContainer = document.getElementById(RATE_LIMIT_CONTAINER_ID);
if (!rateLimitContainer) {
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 text-fg-primary hover:bg-button-ghost-hover hover:border-border-l2 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 border-transparent cursor-pointer';
rateLimitContainer.style.opacity = '0.8';
rateLimitContainer.style.transition = 'opacity 0.1s ease-in-out';
rateLimitContainer.style.zIndex = '20';
let customTooltip = document.getElementById('grok-rate-limit-custom-tooltip');
if (!customTooltip) {
customTooltip = document.createElement('div');
customTooltip.id = 'grok-rate-limit-custom-tooltip';
customTooltip.className = 'z-50 p-4 rounded-xl border shadow-[0_8px_30px_rgb(0,0,0,0.12)] text-left font-sans cursor-default';
customTooltip.style.position = 'fixed';
customTooltip.style.display = 'none';
customTooltip.style.minWidth = '240px';
document.body.appendChild(customTooltip);
}
const showCustomTooltip = () => {
if (!rateLimitContainer._tooltipText) return;
const isDark = document.documentElement.classList.contains('dark') || document.body.style.backgroundColor === 'rgb(0, 0, 0)';
customTooltip.style.backgroundColor = isDark ? '#1a1a1a' : '#ffffff';
customTooltip.style.borderColor = isDark ? '#333333' : '#e5e5e5';
customTooltip.style.color = isDark ? '#e5e5e5' : '#1a1a1a';
customTooltip.innerHTML = rateLimitContainer._tooltipText;
customTooltip.style.display = 'block';
const rect = rateLimitContainer.getBoundingClientRect();
customTooltip.style.left = `${Math.max(10, rect.left + rect.width / 2 - customTooltip.offsetWidth / 2)}px`;
customTooltip.style.top = `${Math.max(10, rect.top - customTooltip.offsetHeight - 8)}px`;
};
const hideCustomTooltip = () => {
customTooltip.style.display = 'none';
};
if (!customTooltip._hasGlobalClick) {
document.addEventListener('click', (e) => {
if (customTooltip.style.display === 'block' && !customTooltip.contains(e.target)) {
const rateLimits = document.querySelectorAll('#' + RATE_LIMIT_CONTAINER_ID);
let clickedRateLimit = false;
rateLimits.forEach(rl => {
if (rl.contains(e.target)) clickedRateLimit = true;
});
if (!clickedRateLimit) {
customTooltip.style.display = 'none';
rateLimits.forEach(rl => rl._isClicked = false);
}
}
});
customTooltip._hasGlobalClick = true;
}
rateLimitContainer.addEventListener('mouseenter', () => {
rateLimitContainer._isHovered = true;
});
let pressTimer;
rateLimitContainer.addEventListener('mousedown', (e) => {
if (isImaginePage()) return;
if (e.button !== 0) return;
pressTimer = setTimeout(() => {
rateLimitContainer._isClicked = false;
hideCustomTooltip();
showResetMenu(rateLimitContainer, modelName, queryBar);
}, 2000);
});
rateLimitContainer.addEventListener('mouseup', () => clearTimeout(pressTimer));
rateLimitContainer.addEventListener('mouseleave', () => {
rateLimitContainer._isHovered = false;
clearTimeout(pressTimer);
});
rateLimitContainer.addEventListener('contextmenu', (e) => {
e.preventDefault();
rateLimitContainer._isClicked = true;
showCustomTooltip();
});
rateLimitContainer.addEventListener('click', (e) => {
if (pressTimer) clearTimeout(pressTimer);
if (rateLimitContainer._isClicked) {
rateLimitContainer._isClicked = false;
hideCustomTooltip();
} else {
rateLimitContainer._isClicked = true;
showCustomTooltip();
}
if (!document.getElementById('grok-rate-limit-reset-menu')) {
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('stroke', 'currentColor');
svg.setAttribute('stroke-width', '2');
svg.setAttribute('stroke-linecap', 'round');
svg.setAttribute('stroke-linejoin', 'round');
svg.setAttribute('class', 'lucide lucide-gauge stroke-[2] text-fg-secondary transition-colors duration-100');
svg.setAttribute('aria-hidden', 'true');
const contentDiv = document.createElement('div');
contentDiv.className = 'flex items-center';
rateLimitContainer.appendChild(svg);
rateLimitContainer.appendChild(contentDiv);
let inserted = false;
if (isImaginePage()) {
const actionLabels = /Submit|Edit|Make video|提交|发送|编辑|制作视频|生成视频|動画作成|영상 만들기|影片/i;
const submitBtn = Array.from(queryBar.querySelectorAll('button')).find(b => {
const aria = b.getAttribute('aria-label') || '';
const text = b.textContent || '';
if (actionLabels.test(aria) || actionLabels.test(text)) return true;
// Fallback to identifying by SVG arrow-up path
const path = b.querySelector('path');
if (path && path.getAttribute('d')?.includes('M6 11L12 5')) return true;
return false;
});
if (submitBtn && submitBtn.parentNode && submitBtn.parentNode.parentNode) {
submitBtn.parentNode.parentNode.insertBefore(rateLimitContainer, submitBtn.parentNode);
inserted = true;
}
}
if (!inserted) {
const modelSelector = queryBar.querySelector('#model-select-trigger')?.closest('.z-20') ||
queryBar.querySelector('button[aria-label="Model select"]')?.closest('.z-20') ||
queryBar.querySelector('button[aria-label="Model select"]');
const toolsContainer = queryBar.querySelector('div.ms-auto.flex.flex-row.items-end');
if (modelSelector && modelSelector.parentNode) {
modelSelector.parentNode.insertBefore(rateLimitContainer, modelSelector);
} else if (toolsContainer) {
toolsContainer.prepend(rateLimitContainer);
} else {
const bottomBar = queryBar.querySelector('div.absolute.inset-x-0.bottom-0');
if (bottomBar) {
bottomBar.appendChild(rateLimitContainer);
} else {
rateLimitContainer.remove();
rateLimitContainer = null;
return;
}
}
}
}
const contentDiv = rateLimitContainer.lastChild;
const svg = rateLimitContainer.querySelector('svg');
contentDiv.innerHTML = '';
const isBoth = effort === 'both';
const buildImagineSection = (full) => {
if (!full || full.error) return '';
const hours = Math.round((full.image?.windowSizeSeconds || 64800) / 3600);
return `
<div class="font-bold mb-3 mt-4 text-base border-b border-border-l2 pb-2">Imagine Gens Remaining</div>
<div class="flex justify-between items-center gap-6 mb-1.5">
<span class="text-fg-secondary">Speed Images:</span>
<span class="font-mono text-primary font-bold">${full.image?.remainingQueries ?? 0}</span>
</div>
<div class="flex justify-between items-center gap-6 mb-1.5">
<span class="text-fg-secondary">Quality Images:</span>
<span class="font-mono text-primary font-bold">${full.imagePro?.remainingQueries ?? 0}</span>
</div>
<div class="flex justify-between items-center gap-6 mb-1.5">
<span class="text-fg-secondary">Image Edits:</span>
<span class="font-mono text-primary font-bold">${full.imageEdit?.remainingQueries ?? 0}</span>
</div>
<div class="flex justify-between items-center gap-6 mb-1.5">
<span class="text-fg-secondary">480p Videos:</span>
<span class="font-mono text-primary font-bold">${full.video?.remainingQueries ?? 0}</span>
</div>
<div class="flex justify-between items-center gap-6 mb-3">
<span class="text-fg-secondary">720p Videos:</span>
<span class="font-mono text-primary font-bold">${full.video720p?.remainingQueries ?? 0}</span>
</div>
<div class="mt-2 pt-2 border-t border-border-l2 text-xs text-fg-secondary text-center">
Resets after ${hours} hrs
</div>
`;
};
if (response.error) {
if (isBoth) {
if (lastBoth.high !== null) {
appendNumberSpan(contentDiv, lastBoth.high, '');
rateLimitContainer._tooltipText = `High: ${lastBoth.high} | Low: ${lastBoth.low ?? 'Unknown'} queries remaining`;
setGaugeSVG(svg);
} else {
appendNumberSpan(contentDiv, 'Unavailable', '');
rateLimitContainer._tooltipText = 'Unavailable';
setGaugeSVG(svg);
}
} else {
const lastForEffort = effort === 'imagine' ? lastImagine : ((effort === 'high') ? lastHigh : lastLow);
if (lastForEffort.remaining !== null) {
appendNumberSpan(contentDiv, lastForEffort.remaining, '');
rateLimitContainer._tooltipText = `${lastForEffort.remaining} queries remaining`;
setGaugeSVG(svg);
} else {
appendNumberSpan(contentDiv, 'Unavailable', '');
rateLimitContainer._tooltipText = 'Unavailable';
setGaugeSVG(svg);
}
}
} else {
if (countdownTimer) {
clearInterval(countdownTimer);
countdownTimer = null;
}
if (isBoth) {
lastBoth.high = response.highRemaining;
lastBoth.low = response.lowRemaining;
lastBoth.wait = response.waitTimeSeconds;
const high = lastBoth.high;
const low = lastBoth.low;
const waitTimeSeconds = lastBoth.wait;
let currentCountdown = waitTimeSeconds;
let timerSpan = null;
if (high > 0) {
appendNumberSpan(contentDiv, high, '');
setGaugeSVG(svg);
} else if (waitTimeSeconds > 0) {
timerSpan = appendNumberSpan(contentDiv, formatTimer(currentCountdown), '#ff6347');
setClockSVG(svg);
} else {
appendNumberSpan(contentDiv, '0', '#ff6347');
setGaugeSVG(svg);
}
const updateTooltip = () => {
const hasHeavy = document.body.innerText.includes('Heavy') || (lastMulti?.heavy && lastMulti.heavy.remaining !== 'Unknown');
let titleHtml = "";
let chatSection = `
<div class="font-bold mb-3 text-base border-b border-border-l2 pb-2">Chat Queries Remaining</div>
<div class="flex justify-between items-center gap-6 mb-1.5">
<span class="text-fg-secondary">Auto:</span>
<span class="font-mono text-primary font-bold">${lastMulti?.auto?.remaining ?? 'Unknown'}</span>
</div>
<div class="flex justify-between items-center gap-6 mb-1.5">
<span class="text-fg-secondary">Grok 4.3 (Beta):</span>
<span class="font-mono text-primary font-bold">${lastMulti?.grok43?.remaining ?? 'Unknown'}</span>
</div>
${hasHeavy ? `
<div class="flex justify-between items-center gap-6 mb-1.5">
<span class="text-fg-secondary">Heavy:</span>
<span class="font-mono text-primary font-bold">${lastMulti?.heavy?.remaining ?? 'Unknown'}</span>
</div>
` : ''}
<div class="flex justify-between items-center gap-6 mb-1.5">
<span class="text-fg-secondary">Expert:</span>
<span class="font-mono text-primary font-bold">${lastMulti?.expert?.remaining ?? 'Unknown'}</span>
</div>
<div class="flex justify-between items-center gap-6 mb-3">
<span class="text-fg-secondary">Fast:</span>
<span class="font-mono text-primary font-bold">${lastMulti?.fast?.remaining ?? 'Unknown'}</span>
</div>
<div class="mt-2 pt-2 border-t border-border-l2 text-xs text-fg-secondary text-center">
Resets after ${lastMulti?.fast?.window || 2} hrs
</div>
`;
if (currentCountdown > 0) {
chatSection += `
<div class="mt-2 pt-2 border-t border-border-l2 text-xs text-[#ff6347] text-center font-bold">
Reset in ${formatTimer(currentCountdown)}
</div>
`;
}
titleHtml = chatSection + buildImagineSection(imagineData);
rateLimitContainer._tooltipText = titleHtml;
if (rateLimitContainer._isClicked) {
const customTooltip = document.getElementById('grok-rate-limit-custom-tooltip');
if (customTooltip) {
const isDark = document.documentElement.classList.contains('dark') || document.body.style.backgroundColor === 'rgb(0, 0, 0)';
customTooltip.style.backgroundColor = isDark ? '#1a1a1a' : '#ffffff';
customTooltip.style.borderColor = isDark ? '#333333' : '#e5e5e5';
customTooltip.style.color = isDark ? '#e5e5e5' : '#1a1a1a';
customTooltip.innerHTML = titleHtml;
customTooltip.style.display = 'block';
const rect = rateLimitContainer.getBoundingClientRect();
customTooltip.style.left = `${Math.max(10, rect.left + rect.width / 2 - customTooltip.offsetWidth / 2)}px`;
customTooltip.style.top = `${Math.max(10, rect.top - customTooltip.offsetHeight - 8)}px`;
}
}
};
updateTooltip();
if (waitTimeSeconds > 0) {
const isOutOfQueries = (high <= 0);
if (isOutOfQueries) {
isCountingDown = true;
}
countdownTimer = setInterval(() => {
currentCountdown--;
if (currentCountdown <= 0) {
clearInterval(countdownTimer);
countdownTimer = null;
if (isOutOfQueries) isCountingDown = false;
fetchAndUpdateRateLimit(queryBar, true);
} else {
if (timerSpan) timerSpan.textContent = formatTimer(currentCountdown);
updateTooltip();
}
}, 1000);
}
} else {
const lastForEffort = effort === 'imagine' ? lastImagine : ((effort === 'high') ? lastHigh : lastLow);
lastForEffort.remaining = response.remainingQueries;
lastForEffort.wait = response.waitTimeSeconds;
const remaining = lastForEffort.remaining;
const waitTimeSeconds = lastForEffort.wait;
let currentCountdown = waitTimeSeconds;
let timerSpan = null;
if (remaining > 0) {
let displayText = remaining;
appendNumberSpan(contentDiv, displayText, '');
setGaugeSVG(svg);
} else if (waitTimeSeconds > 0) {
timerSpan = appendNumberSpan(contentDiv, formatTimer(currentCountdown), '#ff6347');
setClockSVG(svg);
} else {
appendNumberSpan(contentDiv, '0', '#ff6347');
setGaugeSVG(svg);
}
const updateTooltip = () => {
const hasHeavy = document.body.innerText.includes('Heavy') || (lastMulti?.heavy && lastMulti.heavy.remaining !== 'Unknown');
let titleHtml = "";
if (isImaginePage() && effort === 'imagine') {
const full = response.fullImagineData;
if (full) {
titleHtml = buildImagineSection(full).replace('mt-4 ', '');
} else {
titleHtml = `${lastForEffort.remaining} queries remaining`;
}
} else {
let chatSection = `
<div class="font-bold mb-3 text-base border-b border-border-l2 pb-2">Chat Queries Remaining</div>
<div class="flex justify-between items-center gap-6 mb-1.5">
<span class="text-fg-secondary">Auto:</span>
<span class="font-mono text-primary font-bold">${lastMulti?.auto?.remaining ?? 'Unknown'}</span>
</div>
<div class="flex justify-between items-center gap-6 mb-1.5">
<span class="text-fg-secondary">Grok 4.3 (Beta):</span>
<span class="font-mono text-primary font-bold">${lastMulti?.grok43?.remaining ?? 'Unknown'}</span>
</div>
${hasHeavy ? `
<div class="flex justify-between items-center gap-6 mb-1.5">
<span class="text-fg-secondary">Heavy:</span>
<span class="font-mono text-primary font-bold">${lastMulti?.heavy?.remaining ?? 'Unknown'}</span>
</div>
` : ''}
<div class="flex justify-between items-center gap-6 mb-1.5">
<span class="text-fg-secondary">Expert:</span>
<span class="font-mono text-primary font-bold">${lastMulti?.expert?.remaining ?? 'Unknown'}</span>
</div>
<div class="flex justify-between items-center gap-6 mb-3">
<span class="text-fg-secondary">Fast:</span>
<span class="font-mono text-primary font-bold">${lastMulti?.fast?.remaining ?? 'Unknown'}</span>
</div>
<div class="mt-2 pt-2 border-t border-border-l2 text-xs text-fg-secondary text-center">
Resets after ${lastMulti?.fast?.window || 2} hrs
</div>
`;
if (currentCountdown > 0) {
chatSection += `
<div class="mt-2 pt-2 border-t border-border-l2 text-xs text-[#ff6347] text-center font-bold">
Reset in ${formatTimer(currentCountdown)}
</div>
`;
}
titleHtml = chatSection + buildImagineSection(imagineData);
}
rateLimitContainer._tooltipText = titleHtml;
if (rateLimitContainer._isClicked) {
const customTooltip = document.getElementById('grok-rate-limit-custom-tooltip');
if (customTooltip) {
const isDark = document.documentElement.classList.contains('dark') || document.body.style.backgroundColor === 'rgb(0, 0, 0)';
customTooltip.style.backgroundColor = isDark ? '#1a1a1a' : '#ffffff';
customTooltip.style.borderColor = isDark ? '#333333' : '#e5e5e5';
customTooltip.style.color = isDark ? '#e5e5e5' : '#1a1a1a';
customTooltip.innerHTML = titleHtml;
customTooltip.style.display = 'block';
const rect = rateLimitContainer.getBoundingClientRect();
customTooltip.style.left = `${Math.max(10, rect.left + rect.width / 2 - customTooltip.offsetWidth / 2)}px`;
customTooltip.style.top = `${Math.max(10, rect.top - customTooltip.offsetHeight - 8)}px`;
}
}
};
updateTooltip();
if (waitTimeSeconds > 0) {
const isOutOfQueries = (remaining <= 0);
if (isOutOfQueries) {
isCountingDown = true;
}
countdownTimer = setInterval(() => {
currentCountdown--;
if (currentCountdown <= 0) {
clearInterval(countdownTimer);
countdownTimer = null;
if (isOutOfQueries) isCountingDown = false;
fetchAndUpdateRateLimit(queryBar, true);
} else {
if (timerSpan) timerSpan.textContent = formatTimer(currentCountdown);
updateTooltip();
}
}, 1000);
}
}
}
}
function appendNumberSpan(parent, text, color) {
const span = document.createElement('span');
span.textContent = text;
if (color) span.style.color = color;
parent.appendChild(span);
return span;
}
function appendDivider(parent) {
const span = document.createElement('span');
span.textContent = '|';
span.style.margin = '0 8px';
span.style.color = '#9a9a9a';
parent.appendChild(span);
}
function setGaugeSVG(svg) {
if (svg) {
while (svg.firstChild) {
svg.removeChild(svg.firstChild);
}
const path1 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path1.setAttribute('d', 'm12 14 4-4');
const path2 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path2.setAttribute('d', 'M3.34 19a10 10 0 1 1 17.32 0');
svg.appendChild(path1);
svg.appendChild(path2);
svg.setAttribute('class', 'lucide lucide-gauge stroke-[2] text-fg-secondary transition-colors duration-100');
}
}
function setClockSVG(svg) {
if (svg) {
while (svg.firstChild) {
svg.removeChild(svg.firstChild);
}
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);
svg.setAttribute('class', 'stroke-[2] text-fg-secondary group-hover/rate-limit:text-fg-primary transition-colors duration-100');
}
}
function showResetMenu(anchorElement, modelName, queryBar) {
let existingMenu = document.getElementById('grok-rate-limit-reset-menu');
if (existingMenu) existingMenu.remove();
const menu = document.createElement('div');
menu.id = 'grok-rate-limit-reset-menu';
menu.className = 'flex flex-col gap-1 p-2 rounded-xl border z-[9999] shadow-lg';
menu.style.position = 'fixed';
menu.style.backgroundColor = '#1a1a1a';
menu.style.borderColor = '#333333';
menu.style.color = '#e5e5e5';
menu.style.minWidth = '140px';
menu.style.fontSize = '14px';
const createButton = (text, onClick, isCancel = false) => {
const btn = document.createElement('button');
btn.textContent = text;
btn.className = 'w-full text-left px-3 py-2 rounded-lg hover:bg-[#333333] transition-colors focus:outline-none';
if (isCancel) {
btn.style.color = '#9a9a9a';
btn.style.textAlign = 'center';
btn.style.marginTop = '4px';
}
btn.addEventListener('click', (e) => {
e.stopPropagation();
onClick();
menu.remove();
});
return btn;
};
const resetCurrentBtn = createButton('Reset Current', () => {
if (chrome.runtime?.id) {
chrome.runtime.sendMessage({ type: 'RESET_COUNTERS', model: modelName, action: 'current' }, () => {
fetchAndUpdateRateLimit(queryBar, true);
});
}
});
const resetAllBtn = createButton('Reset All', () => {
if (chrome.runtime?.id) {
chrome.runtime.sendMessage({ type: 'RESET_COUNTERS', model: modelName, action: 'all' }, () => {
fetchAndUpdateRateLimit(queryBar, true);
});
}
});
const cancelBtn = createButton('Cancel', () => {}, true);
menu.appendChild(resetCurrentBtn);
menu.appendChild(resetAllBtn);
menu.appendChild(cancelBtn);
document.body.appendChild(menu);
const rect = anchorElement.getBoundingClientRect();
const menuRect = menu.getBoundingClientRect();
let targetLeft = rect.left + (rect.width / 2) - (menuRect.width / 2);
if (targetLeft < 10) targetLeft = 10; // Prevent falling off left edge constraint
menu.style.left = `${targetLeft}px`;
menu.style.top = `${rect.top - menuRect.height - 8}px`;
const closeMenuOnOutsideClick = (e) => {
if (!menu.contains(e.target) && !anchorElement.contains(e.target)) {
menu.remove();
document.removeEventListener('mousedown', closeMenuOnOutsideClick);
}
};
setTimeout(() => document.addEventListener('mousedown', closeMenuOnOutsideClick), 0);
}
async function fetchImagineRateLimit(force = false) {
if (!force && cachedRateLimits['imagine_info'] !== undefined) {
return cachedRateLimits['imagine_info'];
}
try {
const response = await fetch(window.location.origin + '/rest/media/imagine/quota_info', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
credentials: 'include',
});
if (!response.ok) throw new Error(`HTTP error: Status ${response.status}`);
const data = await response.json();
cachedRateLimits['imagine_info'] = data;
return data;
} catch (error) {
console.error(`Failed to fetch imagine rate limit:`, error);
return { error: true };
}
}
// 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 process the rate limit data based on effort level
function processRateLimitData(data, effortLevel, modelName) {
if (data.error) {
return data;
}
if (effortLevel === 'imagine') {
const imagineData = data[modelName];
if (!imagineData) return { error: true };
return {
remainingQueries: imagineData.remainingQueries,
windowSizeSeconds: imagineData.windowSizeSeconds,
waitTimeSeconds: 0, // Assume no wait time for imagine limits based on JSON schema
fullImagineData: data
};
}
const getWaitTime = (obj) => {
if (!obj) return 0;
// Check prioritize waitTimeSeconds, then resetsAt (timestamp), then resetTime
if (obj.waitTimeSeconds) return obj.waitTimeSeconds;
if (obj.resetsAt) {
const wait = Math.round((obj.resetsAt - Date.now()) / 1000);
return wait > 0 ? wait : 0;
}
if (obj.resetTime) return obj.resetTime;
return 0;
};
if (effortLevel === 'both') {
const high = data.highEffortRateLimits?.remainingQueries;
const low = data.lowEffortRateLimits?.remainingQueries;
const waitTimeSeconds = Math.max(
getWaitTime(data.highEffortRateLimits),
getWaitTime(data.lowEffortRateLimits),
getWaitTime(data)
);
if (high !== undefined && low !== undefined && high !== null && low !== null) {
return {
highRemaining: high,
lowRemaining: low,
waitTimeSeconds: waitTimeSeconds
};
} else if (data.remainingQueries !== undefined) {
// If breakdown is missing (e.g. unauthenticated), fallback to top-level
return {
highRemaining: data.remainingQueries,
lowRemaining: data.remainingQueries, // Or maybe null? Let's show the same for both if we don't know
waitTimeSeconds: waitTimeSeconds
};
} else {
return { error: true };
}
} else {
let rateLimitsKey = effortLevel === 'high' ? 'highEffortRateLimits' : 'lowEffortRateLimits';
let remaining = data[rateLimitsKey]?.remainingQueries ?? data.remainingQueries;
if (remaining !== undefined) {
return {
remainingQueries: remaining,
waitTimeSeconds: getWaitTime(data[rateLimitsKey]) || getWaitTime(data)
};
} else {
return { error: true };
}
}
}
// Function to fetch and update rate limit
async function fetchAndUpdateRateLimit(queryBar, force = false, pollUntilChange = false) {
if (!queryBar || !document.body.contains(queryBar)) {
return;
}
const modelName = getCurrentModelKey(queryBar);
if (modelName !== lastModelName) {
force = true;
}
if (isCountingDown && !force) {
return;
}
const effortLevel = getEffortLevel(modelName);
let requestKind = DEFAULT_KIND;
if (modelName === 'grok-3') {
const thinkButton = findElement(commonFinderConfigs.thinkButton, queryBar);
const searchButton = findElement(commonFinderConfigs.deepSearchButton, queryBar);
if (thinkButton && thinkButton.getAttribute('aria-pressed') === 'true') {
requestKind = 'REASONING';
} else if (searchButton && searchButton.getAttribute('aria-pressed') === 'true') {
const searchAria = searchButton.getAttribute('aria-label') || '';
if (/deeper/i.test(searchAria)) {
requestKind = 'DEEPERSEARCH';
} else if (/deep/i.test(searchAria)) {
requestKind = 'DEEPSEARCH';
}
}
}
let data, imagineData, multiData;
if (isImaginePage()) {
data = await fetchImagineRateLimit(force);
imagineData = null;
} else {
const rateLimitContainer = document.getElementById(RATE_LIMIT_CONTAINER_ID);
const shouldFetchAll = rateLimitContainer && rateLimitContainer._isClicked;
if (shouldFetchAll) {
const modelsToFetch = [
{ key: 'grok-4-auto', name: 'auto' },
{ key: 'grok-420-computer-use-sa', name: 'grok43' },
{ key: 'grok-4-heavy', name: 'heavy' },
{ key: 'grok-4', name: 'expert' },
{ key: 'grok-3', name: 'fast' }
];
const promises = [
fetchRateLimit(modelName, requestKind, force),
fetchImagineRateLimit(force)
];
for (const m of modelsToFetch) {
promises.push(fetchRateLimit(m.key, DEFAULT_KIND, force));
}
const results = await Promise.all(promises);
data = results[0];
imagineData = results[1];
multiData = {
auto: results[2],
grok43: results[3],
heavy: results[4],
expert: results[5],
fast: results[6]
};
} else {
data = await fetchRateLimit(modelName, requestKind, force);
imagineData = null;
multiData = null;
}
}
if (chrome.runtime?.id) {
const syncWithBackground = (bucket, remaining, total, windowSize) => {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage({
type: 'SYNC_AND_GET_REMAINING',
model: bucket,
remainingQueries: remaining,
totalQueries: total,
windowSizeSeconds: windowSize
}, (response) => {
if (chrome.runtime.lastError) reject(chrome.runtime.lastError);
else resolve(response);
});
});
};
const getFromBackground = (bucket) => {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage({
type: 'GET_REMAINING',
model: bucket
}, (response) => {
if (chrome.runtime.lastError) reject(chrome.runtime.lastError);
else resolve(response);
});
});
};
if (!data.error) {
// API SUCCESS: Use API data but sync to background for redundancy
try {
if (effortLevel === 'both') {
let highRemaining = data.highEffortRateLimits?.remainingQueries ?? null;
let highTotal = data.highEffortRateLimits?.totalQueries ?? null;
let lowRemaining = data.lowEffortRateLimits?.remainingQueries ?? null;
let lowTotal = data.lowEffortRateLimits?.totalQueries ?? null;
// Check if we should poll for change (max 3 times, 2s apart)
if (pollUntilChange && typeof pollUntilChange === 'number' && pollUntilChange > 0) {
if (highRemaining === lastBoth.high && highRemaining !== null) {
console.log(`Grok Rate Limit Extension: Count unchanged (${highRemaining}), polling... (${pollUntilChange} retries left)`);
setTimeout(() => fetchAndUpdateRateLimit(queryBar, true, pollUntilChange - 1), 2000);
}
}
await syncWithBackground('grok-4', highRemaining, highTotal, data.windowSizeSeconds);
await syncWithBackground('grok-3', lowRemaining, lowTotal, data.windowSizeSeconds);
} else {
let rateLimitsKey = effortLevel === 'high' ? 'highEffortRateLimits' : 'lowEffortRateLimits';
let apiRemaining = data[rateLimitsKey]?.remainingQueries ?? data.remainingQueries;
let apiTotal = data[rateLimitsKey]?.totalQueries ?? data.totalQueries;
// Check if we should poll for change (max 3 times, 2s apart)
if (pollUntilChange && typeof pollUntilChange === 'number' && pollUntilChange > 0) {
const lastRemaining = effortLevel === 'high' ? lastHigh.remaining : lastLow.remaining;
if (apiRemaining === lastRemaining && apiRemaining !== null) {
console.log(`Grok Rate Limit Extension: Count unchanged (${apiRemaining}), polling... (${pollUntilChange} retries left)`);
setTimeout(() => fetchAndUpdateRateLimit(queryBar, true, pollUntilChange - 1), 2000);
}
}
await syncWithBackground(modelName, apiRemaining, apiTotal, data.windowSizeSeconds);
}
} catch (e) {
console.warn('Grok Rate Limit Extension: Failed to sync API data to background tracker.');
}
} else if (!isImaginePage()) {
// API FAILURE: Fallback to local background tracking (not available for imagine)
console.log('Grok Rate Limit Extension: API failed, falling back to local tracking.');
try {
if (effortLevel === 'both') {
const bgDataHigh = await getFromBackground('grok-4');
const bgDataLow = await getFromBackground('grok-3');
data = {
...data,
error: false, // Clear error since we have fallback
highEffortRateLimits: {
remainingQueries: bgDataHigh.remaining,
waitTimeSeconds: bgDataHigh.nextResetSeconds
},
lowEffortRateLimits: {
remainingQueries: bgDataLow.remaining,
waitTimeSeconds: bgDataLow.nextResetSeconds
},
windowSizeSeconds: bgDataHigh.windowSizeSeconds
};
} else {
const bgData = await getFromBackground(modelName);
let rateLimitsKey = effortLevel === 'high' ? 'highEffortRateLimits' : 'lowEffortRateLimits';
data = {
...data,
error: false,
remainingQueries: bgData.remaining,
waitTimeSeconds: bgData.nextResetSeconds,
windowSizeSeconds: bgData.windowSizeSeconds
};
// Ensure the specific effort level object is also populated
data[rateLimitsKey] = {
remainingQueries: bgData.remaining,
waitTimeSeconds: bgData.nextResetSeconds
};
}
} catch (e) {
console.error('Grok Rate Limit Extension: Both API and level fallback failed.', e);
}
}
}
if (multiData) {
lastMulti = {};
for (const [key, res] of Object.entries(multiData)) {
if (!res || res.error) {
lastMulti[key] = { remaining: 'Unknown', window: null };
} else {
let rem = res.remainingQueries;
if (rem === undefined && res.highEffortRateLimits) {
rem = res.highEffortRateLimits.remainingQueries;
}
if (rem === undefined && res.lowEffortRateLimits) {
rem = res.lowEffortRateLimits.remainingQueries;
}
lastMulti[key] = {
remaining: rem !== undefined ? rem : 'Unknown',
window: res.windowSizeSeconds ? Math.round(res.windowSizeSeconds / 3600) : null
};
}
}
} else {
if (!lastMulti) {
lastMulti = {
auto: { remaining: 'Unknown', window: null },
grok43: { remaining: 'Unknown', window: null },
heavy: { remaining: 'Unknown', window: null },
expert: { remaining: 'Unknown', window: null },
fast: { remaining: 'Unknown', window: null }
};
}
if (data && !data.error) {
const modelToNameMap = {
'grok-4-auto': 'auto',
'grok-420-computer-use-sa': 'grok43',
'grok-420': 'grok43',
'grok-4-heavy': 'heavy',
'grok-4': 'expert',
'grok-3': 'fast'
};
const mappedName = modelToNameMap[modelName];
if (mappedName) {
let rem = data.remainingQueries;
if (rem === undefined && data.highEffortRateLimits) {
rem = data.highEffortRateLimits.remainingQueries;
}
if (rem === undefined && data.lowEffortRateLimits) {
rem = data.lowEffortRateLimits.remainingQueries;
}
lastMulti[mappedName] = {
remaining: rem !== undefined ? rem : 'Unknown',
window: data.windowSizeSeconds ? Math.round(data.windowSizeSeconds / 3600) : null
};
}
}
}
const processedData = processRateLimitData(data, effortLevel, modelName);
updateRateLimitDisplay(queryBar, processedData, effortLevel, modelName, imagineData);
lastModelName = modelName;
}
// Function to observe the DOM for the query bar
function observeDOM() {
const handleVisibilityChange = () => {
if (document.visibilityState === 'visible' && lastQueryBar) {
fetchAndUpdateRateLimit(lastQueryBar, true);
}
};
// Add resize listener to handle mobile/desktop mode switches
const handleResize = debounce(() => {
if (lastQueryBar) {
checkTextOverlap(lastQueryBar);
}
}, 300);
document.addEventListener('visibilitychange', handleVisibilityChange);
window.addEventListener('resize', handleResize);
const initialQueryBar = document.querySelector(QUERY_BAR_SELECTOR);
if (initialQueryBar) {
removeExistingRateLimit();
fetchAndUpdateRateLimit(initialQueryBar);
lastQueryBar = initialQueryBar;
setupQueryBarObserver(initialQueryBar);
setupGrok3Observers(initialQueryBar);
setupSubmissionListeners(initialQueryBar);
// Start overlap checking
startOverlapChecking(initialQueryBar);
setTimeout(() => checkTextOverlap(initialQueryBar), 100);
}
const observer = new MutationObserver(() => {
const queryBar = document.querySelector(QUERY_BAR_SELECTOR);
if (queryBar && queryBar !== lastQueryBar) {
removeExistingRateLimit();
fetchAndUpdateRateLimit(queryBar);
if (lastModelObserver) {
lastModelObserver.disconnect();
}
if (lastThinkObserver) {
lastThinkObserver.disconnect();
}
if (lastSearchObserver) {
lastSearchObserver.disconnect();
}
setupQueryBarObserver(queryBar);
setupGrok3Observers(queryBar);
setupSubmissionListeners(queryBar);
// Start overlap checking
startOverlapChecking(queryBar);
setTimeout(() => checkTextOverlap(queryBar), 100);
// Removed redundant polling
lastQueryBar = queryBar;
} else if (!queryBar && lastQueryBar) {
removeExistingRateLimit();
stopOverlapChecking();
if (lastModelObserver) {
lastModelObserver.disconnect();
}
if (lastThinkObserver) {
lastThinkObserver.disconnect();
}
if (lastSearchObserver) {
lastSearchObserver.disconnect();
}
lastQueryBar = null;
lastModelObserver = null;
lastThinkObserver = null;
lastSearchObserver = null;
lastInputElement = null;
lastSubmitButton = null;
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
function setupQueryBarObserver(queryBar) {
const debouncedUpdate = debounce(() => {
fetchAndUpdateRateLimit(queryBar);
setupGrok3Observers(queryBar);
}, 300);
lastModelObserver = new MutationObserver(debouncedUpdate);
lastModelObserver.observe(queryBar, { childList: true, subtree: true, attributes: true, characterData: true });
}
function setupGrok3Observers(queryBar) {
const currentModel = getCurrentModelKey(queryBar);
if (currentModel === 'grok-3') {
const thinkButton = findElement(commonFinderConfigs.thinkButton, queryBar);
if (thinkButton) {
if (lastThinkObserver) lastThinkObserver.disconnect();
lastThinkObserver = new MutationObserver(() => {
fetchAndUpdateRateLimit(queryBar);
});
lastThinkObserver.observe(thinkButton, { attributes: true, attributeFilter: ['aria-pressed', 'class'] });
}
const searchButton = findElement(commonFinderConfigs.deepSearchButton, 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('div[contenteditable="true"]');
if (inputElement && inputElement !== lastInputElement) {
lastInputElement = inputElement;
const debouncedOverlapCheck = debounce(() => {
checkTextOverlap(queryBar);
}, 300);
inputElement.addEventListener('keydown', (e) => {
const modelName = getCurrentModelKey(queryBar);
if (e.key === 'Enter' && !e.shiftKey) {
// For locally tracked models, we rely on the interceptor, but we still want to refresh the UI
const locallyTrackedModels = ['grok-420', 'grok-420-computer-use-sa', 'grok-4-heavy', 'grok-3', 'grok-4', 'grok-4-auto'];
const delay = locallyTrackedModels.includes(modelName) ? 1000 : 3000;
setTimeout(() => fetchAndUpdateRateLimit(queryBar, true, 3), delay);
}
});
inputElement.addEventListener('input', debouncedOverlapCheck);
inputElement.addEventListener('focus', debouncedOverlapCheck);
inputElement.addEventListener('blur', () => {
setTimeout(() => {
checkTextOverlap(queryBar);
}, 200);
});
}
const bottomBar = queryBar.querySelector('div.absolute.inset-x-0.bottom-0');
const submitButton = bottomBar ? findElement(commonFinderConfigs.submitButton, bottomBar) : findElement(commonFinderConfigs.submitButton, queryBar);
if (submitButton && submitButton !== lastSubmitButton) {
lastSubmitButton = submitButton;
submitButton.addEventListener('click', () => {
const modelName = getCurrentModelKey(queryBar);
const locallyTrackedModels = ['grok-420', 'grok-420-computer-use-sa', 'grok-4-heavy', 'grok-3', 'grok-4', 'grok-4-auto'];
const delay = locallyTrackedModels.includes(modelName) ? 1000 : 3000;
setTimeout(() => fetchAndUpdateRateLimit(queryBar, true, 3), delay);
});
}
}
}
// Start observing the DOM for changes
observeDOM();
function showNagScreen() {
if (document.getElementById('grok-nag-screen')) return;
// Ensure font is injected
if (!document.getElementById('grok-nag-font')) {
const fontLink = document.createElement('link');
fontLink.id = 'grok-nag-font';
fontLink.rel = 'stylesheet';
fontLink.href = 'https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap';
document.head.appendChild(fontLink);
}
// Add Stylesheet scoped tightly to prevent pollution or resets from Grok.com
const style = document.createElement('style');
style.id = 'grok-nag-styles';
style.textContent = `
.grok-nag-overlay {
all: initial;
position: fixed;
inset: 0;
background: rgba(8, 8, 9, 0.75);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
z-index: 999999999;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1);
pointer-events: auto;
font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
box-sizing: border-box;
}
.grok-nag-overlay * {
box-sizing: border-box;
}
.grok-nag-overlay.active {
opacity: 1;
}
.grok-nag-dialog {
background: #0b0b0c;
border: 1px solid #27272a;
border-radius: 24px;
padding: 32px 28px;
width: 90%;
max-width: 400px;
box-shadow: 0 24px 48px rgba(0, 0, 0, 0.6);
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
transform: scale(0.9) translateY(20px);
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.grok-nag-overlay.active .grok-nag-dialog {
transform: scale(1) translateY(0);
}
.grok-nag-title-group {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
text-align: center;
}
.grok-nag-title {
color: #ffffff;
font-size: 20px;
font-weight: 700;
margin: 0;
line-height: 1.3;
letter-spacing: -0.02em;
font-family: 'Plus Jakarta Sans', sans-serif;
}
.grok-nag-author {
color: #a1a1aa;
font-size: 13px;
font-family: 'Plus Jakarta Sans', sans-serif;
}
.grok-nag-author-link {
color: #38bdf8;
text-decoration: none;
font-weight: 500;
}
.grok-nag-author-link:hover {
text-decoration: underline;
}
.grok-nag-body {
color: #e4e4e7;
font-size: 14px;
line-height: 1.5;
margin: 0;
text-align: center;
font-weight: 450;
font-family: 'Plus Jakarta Sans', sans-serif;
}
.grok-nag-buttons {
display: flex;
flex-direction: column;
gap: 10px;
width: 100%;
}
.grok-nag-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
height: 44px;
border-radius: 12px;
font-size: 14px;
font-weight: 600;
text-decoration: none;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
font-family: 'Plus Jakarta Sans', sans-serif;
}
.grok-nag-btn-donate {
background: linear-gradient(135deg, #FFDD00 0%, #FFB000 100%);
color: #000000 !important;
border: none;
box-shadow: 0 4px 12px rgba(255, 221, 0, 0.15);
}
.grok-nag-btn-donate:hover {
transform: translateY(-1.5px);
box-shadow: 0 6px 16px rgba(255, 221, 0, 0.25);
background: linear-gradient(135deg, #ffe333 0%, #ffbb1a 100%);
}
.grok-nag-btn-subscribe {
background: #1c1c1f;
color: #ffffff !important;
border: 1px solid #3f3f46;
}
.grok-nag-btn-subscribe:hover {
transform: translateY(-1.5px);
background: #27272a;
border-color: #52525b;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
}
.grok-nag-btn:active {
transform: translateY(0);
}
.grok-nag-close {
background: transparent;
color: #71717a;
border: none;
font-size: 13px;
font-weight: 500;
cursor: pointer;
text-decoration: none;
margin-top: 4px;
transition: color 0.2s;
font-family: 'Plus Jakarta Sans', sans-serif;
align-self: center;
}
.grok-nag-close:hover {
color: #a1a1aa;
text-decoration: underline;
}
.grok-nag-icon-container {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 50%;
width: 68px;
height: 68px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 4px;
}
.grok-nag-btn-icon {
width: 16px;
height: 16px;
}
`;
document.head.appendChild(style);
// Create elements
const overlay = document.createElement('div');
overlay.id = 'grok-nag-screen';
overlay.className = 'grok-nag-overlay';
overlay.innerHTML = `
<div class="grok-nag-dialog">
<div class="grok-nag-icon-container">
<svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m12 14 4-4"></path>
<path d="M3.34 19a10 10 0 1 1 17.32 0"></path>
</svg>
</div>
<div class="grok-nag-title-group">
<h2 class="grok-nag-title">Grok Rate Limit Display</h2>
<div class="grok-nag-author">by <a href="https://x.com/blankspeaker" target="_blank" class="grok-nag-author-link">@blankspeaker</a></div>
</div>
<p class="grok-nag-body">
Thank you for using Grok Rate Limit Display!<br><br>
This project is funded by your donations. If this extension makes your experience better, please consider donating or subscribing to help keep the project updated.
</p>
<div class="grok-nag-buttons">
<a href="https://buymeacoffee.com/blank_speaker" target="_blank" class="grok-nag-btn grok-nag-btn-donate">
<svg class="grok-nag-btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M17 8h1a4 4 0 1 1 0 8h-1"></path>
<path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z"></path>
<line x1="6" y1="2" x2="6" y2="4"></line>
<line x1="10" y1="2" x2="10" y2="4"></line>
<line x1="14" y1="2" x2="14" y2="4"></line>
</svg>
Donate
</a>
<a href="https://x.com/blankspeaker/creator-subscriptions/subscribe" target="_blank" class="grok-nag-btn grok-nag-btn-subscribe">
<svg class="grok-nag-btn-icon" viewBox="0 0 24 24" fill="currentColor">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
</svg>
Subscribe
</a>
<button class="grok-nag-close" id="grok-nag-close-btn">Close & Let's Go</button>
</div>
</div>
`;
document.body.appendChild(overlay);
// Animate in
setTimeout(() => {
overlay.classList.add('active');
}, 50);
// Dismiss logic
const dismiss = () => {
overlay.classList.remove('active');
setTimeout(() => {
overlay.remove();
style.remove();
}, 400);
// Save state
if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local) {
try {
chrome.storage.local.set({ hasShownNag: true });
} catch (e) {
localStorage.setItem('grok_hasShownNag', 'true');
}
} else {
if (typeof GM_setValue !== 'undefined') {
GM_setValue('hasShownNag', true);
} else {
localStorage.setItem('grok_hasShownNag', 'true');
}
}
};
document.getElementById('grok-nag-close-btn').addEventListener('click', dismiss);
}
// Check and show welcome nag screen on first launch of the extension
try {
if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local) {
chrome.storage.local.get(['hasShownNag'], (result) => {
if (!result.hasShownNag) {
if (document.body) {
showNagScreen();
} else {
document.addEventListener('DOMContentLoaded', showNagScreen);
}
}
});
} else {
const hasShown = typeof GM_getValue !== 'undefined' ? GM_getValue('hasShownNag', false) : localStorage.getItem('grok_hasShownNag');
if (!hasShown) {
if (document.body) {
showNagScreen();
} else {
document.addEventListener('DOMContentLoaded', showNagScreen);
}
}
}
} catch (e) {
console.warn('Grok Rate Limit Extension: Failed to check or show welcome nag screen:', e);
}
})();