Grok Rate Limit Display

Displays Grok rate limit on screen based on selected model/mode

Fra 13.07.2025. Se den seneste versjonen.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Grok Rate Limit Display
// @namespace    http://tampermonkey.net/
// @version      2.6
// @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/*
// @match        https://grok.x.ai/*
// @match        https://x.com/*
// @match        https://twitter.com/*
// @grant        none
// ==/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 };

    // 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 cursor-pointer';
        rateLimitContainer.style.opacity = '0.8';
        rateLimitContainer.style.transition = 'opacity 0.1s ease-in-out';

        // Add click event to update on click
        rateLimitContainer.addEventListener('click', () => {
          fetchAndUpdateRateLimit(queryBar);
        });

        // 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) {
      if (!queryBar || !document.body.contains(queryBar)) {
        console.log('Query bar not valid, skipping update');
        return;
      }
      // 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);
      }

      // Determine requestKind based on model
      let requestKind = 'DEFAULT';
      if (modelName === 'grok-3') {
        const thinkButton = queryBar.querySelector('button[aria-label="Think"]');
        if (thinkButton && thinkButton.getAttribute('aria-pressed') === 'true') {
          requestKind = 'REASONING';
        } else {
          let searchButton = queryBar.querySelector('button[aria-label^="Deep"]');
          if (searchButton && searchButton.getAttribute('aria-pressed') === 'true') {
            const modeSpan = searchButton.querySelector('span');
            if (modeSpan) {
              const modeText = modeSpan.textContent.trim();
              if (modeText === 'DeepSearch') {
                requestKind = 'DEEPSEARCH';
              } else if (modeText === 'DeeperSearch') {
                requestKind = 'DEEPERSEARCH';
              }
            }
          }
        }
      }

      fetch(window.location.origin + '/rest/rate-limits', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          requestKind: requestKind,
          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 && document.body.contains(queryBar)) {
            updateRateLimitDisplay(queryBar, data);
          }
        })
        .catch((err) => {
          if (queryBar && document.body.contains(queryBar)) {
            updateRateLimitDisplay(queryBar, { error: err.message });
          }
        });
    }

    // Function to setup or cleanup Grok-3 specific observers
    function setupGrok3Observers(queryBar, currentModel, lastThinkObserver, lastSearchObserver) {
      if (currentModel === 'grok-3') {
        if (!lastThinkObserver) {
          const thinkButton = queryBar.querySelector('button[aria-label="Think"]');
          if (thinkButton) {
            lastThinkObserver = new MutationObserver(() => {
              fetchAndUpdateRateLimit(queryBar);
            });
            lastThinkObserver.observe(thinkButton, { attributes: true, attributeFilter: ['aria-pressed', 'class'] });
          }
        }
        if (!lastSearchObserver) {
          const searchButton = queryBar.querySelector('button[aria-label^="Deep"]');
          if (searchButton) {
            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;
        }
      }
      return { lastThinkObserver, lastSearchObserver };
    }

    // Function to observe the DOM for the query bar
    function observeDOM() {
      let lastQueryBar = null;
      let lastModel = null;
      let lastModelObserver = null;
      let lastThinkObserver = null;
      let lastSearchObserver = null;
      let lastInputElement = null;
      let lastSendButton = 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);
              // Setup or cleanup Grok-3 observers
              const observers = setupGrok3Observers(lastQueryBar, currentModel, lastThinkObserver, lastSearchObserver);
              lastThinkObserver = observers.lastThinkObserver;
              lastSearchObserver = observers.lastSearchObserver;
            }
          });
          lastModelObserver.observe(modelSpan, { characterData: true, childList: true, subtree: true });
        }

        // Initial setup for Grok-3 observers if applicable
        const observers = setupGrok3Observers(initialQueryBar, lastModel, lastThinkObserver, lastSearchObserver);
        lastThinkObserver = observers.lastThinkObserver;
        lastSearchObserver = observers.lastSearchObserver;

        // Set up event listeners for query submission
        const inputElement = initialQueryBar.querySelector('textarea');
        if (inputElement) {
          lastInputElement = inputElement;
          inputElement.addEventListener('keydown', (e) => {
            if (e.key === 'Enter' && !e.shiftKey) {
              setTimeout(() => fetchAndUpdateRateLimit(lastQueryBar), 5000);
            }
          });
        }

        const sendButton = initialQueryBar.querySelector('div.h-10.relative.aspect-square');
        if (sendButton) {
          lastSendButton = sendButton;
          sendButton.addEventListener('click', () => {
            setTimeout(() => fetchAndUpdateRateLimit(lastQueryBar), 5000);
          });
        }
      }

      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;
          }
          if (lastThinkObserver) {
            lastThinkObserver.disconnect();
            lastThinkObserver = null;
          }
          if (lastSearchObserver) {
            lastSearchObserver.disconnect();
            lastSearchObserver = 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);
                // Setup or cleanup Grok-3 observers
                const observers = setupGrok3Observers(lastQueryBar, currentModel, lastThinkObserver, lastSearchObserver);
                lastThinkObserver = observers.lastThinkObserver;
                lastSearchObserver = observers.lastSearchObserver;
              }
            });
            lastModelObserver.observe(modelSpan, { characterData: true, childList: true, subtree: true });
          }

          // Initial setup for Grok-3 observers if applicable
          const observersInit = setupGrok3Observers(queryBar, lastModel, lastThinkObserver, lastSearchObserver);
          lastThinkObserver = observersInit.lastThinkObserver;
          lastSearchObserver = observersInit.lastSearchObserver;

          // Set up event listeners for query submission
          const inputElement = queryBar.querySelector('textarea');
          if (inputElement && inputElement !== lastInputElement) {
            lastInputElement = inputElement;
            inputElement.addEventListener('keydown', (e) => {
              if (e.key === 'Enter' && !e.shiftKey) {
                setTimeout(() => fetchAndUpdateRateLimit(lastQueryBar), 5000);
              }
            });
          }

          const sendButton = queryBar.querySelector('div.h-10.relative.aspect-square');
          if (sendButton && sendButton !== lastSendButton) {
            lastSendButton = sendButton;
            sendButton.addEventListener('click', () => {
              setTimeout(() => fetchAndUpdateRateLimit(lastQueryBar), 5000);
            });
          }
        } else if (!queryBar && lastQueryBar) {
          removeExistingRateLimit();
          if (lastModelObserver) {
            lastModelObserver.disconnect();
            lastModelObserver = null;
          }
          if (lastThinkObserver) {
            lastThinkObserver.disconnect();
            lastThinkObserver = null;
          }
          if (lastSearchObserver) {
            lastSearchObserver.disconnect();
            lastSearchObserver = null;
          }
          lastQueryBar = null;
          lastModel = null;
          lastInputElement = null;
          lastSendButton = 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();

})();