Grok Rate Limit Display

Displays Grok rate limit on screen

Per 11-07-2025. Zie de nieuwste versie.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

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

})();