Greasy Fork is available in English.

Grok Rate Limit Display

Displays Grok rate limit on screen

// ==UserScript==
// @name         Grok Rate Limit Display
// @namespace    http://tampermonkey.net/
// @version      2.5
// @description  Displays Grok rate limit on screen
// @author       CursedAtom , ported by Blankspeaker & Grok
// @match        https://grok.com/*
// @match        https://grok.x.ai/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // Variable to store the last known rate limit values
    let lastRateLimit = { remainingQueries: null, totalQueries: null };

    // Function to remove any existing rate limit display
    function removeExistingRateLimit() {
      const existing = document.getElementById('grok-rate-limit');
      if (existing) {
        existing.remove();
      }
    }

    // Function to normalize model names (lowercase, hyphenated)
    function normalizeModelName(modelName) {
      return modelName.toLowerCase().replace(/\s+/g, '-');
    }

    // Function to update or inject the rate limit display
    function updateRateLimitDisplay(queryBar, response) {
      let rateLimitContainer = document.getElementById('grok-rate-limit');

      // If no container exists, create and insert it
      if (!rateLimitContainer) {
        const bottomBar = queryBar.querySelector('.flex.gap-1\\.5.absolute.inset-x-0.bottom-0');
        if (!bottomBar) {
          console.log('Bottom bar not found');
          return;
        }

        const attachButton = bottomBar.querySelector('button[aria-label="Attach"]');
        if (!attachButton) {
          console.log('Attach button not found');
          return;
        }

        rateLimitContainer = document.createElement('div');
        rateLimitContainer.id = 'grok-rate-limit';
        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';
        rateLimitContainer.style.opacity = '0.8';
        rateLimitContainer.style.transition = 'opacity 0.1s ease-in-out';

        // Add a gauge SVG icon
        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);

        // Add a span for the text
        const textSpan = document.createElement('span');
        rateLimitContainer.appendChild(textSpan);

        attachButton.insertAdjacentElement('afterend', rateLimitContainer);
      }

      // Update text based on response
      const textSpan = rateLimitContainer.querySelector('span');
      if (response.error) {
        // If there's an error, use the last known rate limit values if available
        if (lastRateLimit.remainingQueries !== null && lastRateLimit.totalQueries !== null) {
          textSpan.textContent = `${lastRateLimit.remainingQueries}/${lastRateLimit.totalQueries}`;
        } else {
          // If no previous data exists, show a neutral message without "Error"
          textSpan.textContent = 'Unavailable';
        }
      } else {
        const { remainingQueries, totalQueries } = response;
        // Store the latest rate limit values
        lastRateLimit.remainingQueries = remainingQueries;
        lastRateLimit.totalQueries = totalQueries;
        textSpan.textContent = `${remainingQueries}/${totalQueries}`;
      }
    }

    // Function to fetch and update rate limit
    function fetchAndUpdateRateLimit(queryBar) {
      // Detect current model from the UI
      let modelName = 'grok-4'; // Updated default to grok-4 as of 2025
      const modelSpan = queryBar.querySelector('span.inline-block.text-primary');
      if (modelSpan) {
        const modelText = modelSpan.textContent.trim();
        modelName = normalizeModelName(modelText);
      }

      fetch(window.location.origin + '/rest/rate-limits', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          requestKind: 'DEFAULT',
          modelName: modelName,
        }),
        credentials: 'include',
      })
        .then((res) => {
          if (!res.ok) {
            if (res.status === 401 || res.status === 403) {
              throw new Error('Authentication required: Please sign in.');
            }
            throw new Error(`HTTP error: Status ${res.status}`);
          }
          return res.json();
        })
        .then((data) => {
          if (queryBar) {
            updateRateLimitDisplay(queryBar, data);
          }
        })
        .catch((err) => {
          if (queryBar) {
            updateRateLimitDisplay(queryBar, { error: err.message });
          }
        });
    }

    // Function to observe the DOM for the query bar
    function observeDOM() {
      let lastQueryBar = null;
      let lastModel = null;
      let lastModelObserver = null;

      // Initial check for query bar
      const initialQueryBar = document.querySelector('.query-bar');
      if (initialQueryBar) {
        removeExistingRateLimit();
        fetchAndUpdateRateLimit(initialQueryBar);
        lastQueryBar = initialQueryBar;

        const modelSpan = initialQueryBar.querySelector('span.inline-block.text-primary');
        if (modelSpan) {
          lastModel = normalizeModelName(modelSpan.textContent.trim());
          lastModelObserver = new MutationObserver(() => {
            const currentModel = normalizeModelName(modelSpan.textContent.trim());
            if (currentModel !== lastModel) {
              lastModel = currentModel;
              fetchAndUpdateRateLimit(lastQueryBar);
            }
          });
          lastModelObserver.observe(modelSpan, { characterData: true, childList: true, subtree: true });
        }
      }

      const observer = new MutationObserver((mutations) => {
        const queryBar = document.querySelector('.query-bar');

        // Handle query bar changes
        if (queryBar && queryBar !== lastQueryBar) {
          removeExistingRateLimit();
          fetchAndUpdateRateLimit(queryBar);
          lastQueryBar = queryBar;

          if (lastModelObserver) {
            lastModelObserver.disconnect();
            lastModelObserver = null;
          }

          const modelSpan = queryBar.querySelector('span.inline-block.text-primary');
          if (modelSpan) {
            lastModel = normalizeModelName(modelSpan.textContent.trim());
            lastModelObserver = new MutationObserver(() => {
              const currentModel = normalizeModelName(modelSpan.textContent.trim());
              if (currentModel !== lastModel) {
                lastModel = currentModel;
                fetchAndUpdateRateLimit(lastQueryBar);
              }
            });
            lastModelObserver.observe(modelSpan, { characterData: true, childList: true, subtree: true });
          }
        } else if (!queryBar && lastQueryBar) {
          removeExistingRateLimit();
          if (lastModelObserver) {
            lastModelObserver.disconnect();
            lastModelObserver = null;
          }
          lastQueryBar = null;
          lastModel = null;
        }
      });

      observer.observe(document.body, {
        childList: true,
        subtree: true,
      });

      // Polling every 5 minutes for rate limit updates when tab is focused
      setInterval(() => {
        if (lastQueryBar && document.hasFocus()) {
          fetchAndUpdateRateLimit(lastQueryBar);
        }
      }, 300 * 1000);
    }

    // Start observing the DOM for changes
    observeDOM();

})();