Grok Rate Limit Display

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

Per 14-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.7
// @description  Displays Grok rate limit on screen based on selected model/mode
// @author       Blankspeaker, Originally ported from CursedAtom's chrome extension
// @match        https://grok.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    console.log('Grok Rate Limit Script loaded');

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

    const MODEL_MAP = {
        "Grok 4": "grok-4",
        "Grok 3": "grok-3",
        "Grok 4 Heavy": "grok-4-heavy",
    };

    const DEFAULT_MODEL = "grok-4";
    const DEFAULT_KIND = "DEFAULT";
    const POLL_INTERVAL_MS = 30000;
    const MODEL_SELECTOR = "span.inline-block.text-primary";
    const QUERY_BAR_SELECTOR = ".query-bar";
    const ELEMENT_WAIT_TIMEOUT_MS = 5000;

    const ATTACH_SVG_PATH = "M10 9V15C10 16.1046 10.8954 17 12 17V17C13.1046 17 14 16.1046 14 15V7C14 4.79086 12.2091 3 10 3V3C7.79086 3 6 4.79086 6 7V15C6 18.3137 8.68629 21 12 21V21C15.3137 21 18 18.3137 18 15V8";
    const SUBMIT_SVG_PATH = "M5 11L12 4M12 4L19 11M12 4V21";
    const LOADING_SVG_PATH = "M4 9.2v5.6c0 1.116 0 1.673.11 2.134a4 4 0 0 0 2.956 2.956c.46.11 1.018.11 2.134.11h5.6c1.116 0 1.673 0 2.134-.11a4 4 0 0 0 2.956-2.956c.11-.46.11-1.018.11-2.134V9.2c0-1.116 0-1.673-.11-2.134a4 4 0 0 0-2.956-2.955C16.474 4 15.916 4 14.8 4H9.2c-1.116 0-1.673 0-2.134.11a4 4 0 0 0-2.955 2.956C4 7.526 4 8.084 4 9.2Z";

    const RATE_LIMIT_CONTAINER_ID = "grok-rate-limit";

    const cachedRateLimits = {};

    // Function to wait for element appearance
    function waitForElement(selector, timeout = ELEMENT_WAIT_TIMEOUT_MS, root = document) {
      return new Promise((resolve) => {
        let element = root.querySelector(selector);
        if (element) {
          resolve(element);
          return;
        }

        const observer = new MutationObserver(() => {
          element = root.querySelector(selector);
          if (element) {
            observer.disconnect();
            resolve(element);
          }
        });

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

        setTimeout(() => {
          observer.disconnect();
          resolve(null);
        }, timeout);
      });
    }

    // Function to find button by text or aria-label
    function findButtonByText(text, startsWith = false, root) {
      const buttons = root.querySelectorAll('button');
      for (const btn of buttons) {
        const btnText = btn.textContent?.trim();
        const ariaLabel = btn.getAttribute('aria-label')?.trim();
        const matchesBtn = startsWith ? (btnText?.startsWith(text) || ariaLabel?.startsWith(text)) : (btnText === text || ariaLabel === text);
        if (matchesBtn) {
          return btn;
        }

        const span = btn.querySelector('span');
        const spanText = span?.textContent?.trim();
        const matchesSpan = startsWith ? spanText?.startsWith(text) : spanText === text;
        if (matchesSpan) {
          return btn;
        }
      }
      return null;
    }

    // Async find button
    async function findButtonByTextAsync(text, startsWith = false, timeout = ELEMENT_WAIT_TIMEOUT_MS, root = document) {
      let found = findButtonByText(text, startsWith, root);
      if (found) {
        return found;
      }

      return new Promise((resolve) => {
        const observer = new MutationObserver(() => {
          found = findButtonByText(text, startsWith, root);
          if (found) {
            observer.disconnect();
            resolve(found);
          }
        });

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

        setTimeout(() => {
          observer.disconnect();
          resolve(null);
        }, timeout);
      });
    }

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

    // Function to normalize model names
    function normalizeModelName(modelName) {
      const trimmed = modelName.trim();
      if (!trimmed) {
        return DEFAULT_MODEL;
      }
      return MODEL_MAP[trimmed] || trimmed.toLowerCase().replace(/\s+/g, "-");
    }

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

      if (!rateLimitContainer) {
        const bottomBar = queryBar.querySelector('.flex.gap-1\\.5.absolute.inset-x-0.bottom-0');
        if (!bottomBar) {
          return;
        }

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

        rateLimitContainer = document.createElement('div');
        rateLimitContainer.id = RATE_LIMIT_CONTAINER_ID;
        rateLimitContainer.className = 'inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-60 disabled:cursor-not-allowed [&_svg]:duration-100 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:-mx-0.5 select-none border-border-l2 text-fg-primary hover:bg-button-ghost-hover disabled:hover:bg-transparent h-10 px-3.5 py-2 text-sm rounded-full group/rate-limit transition-colors duration-100 relative overflow-hidden border cursor-pointer';
        rateLimitContainer.style.opacity = '0.8';
        rateLimitContainer.style.transition = 'opacity 0.1s ease-in-out';

        rateLimitContainer.addEventListener('click', () => {
          fetchAndUpdateRateLimit(queryBar, true);
        });

        const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        svg.setAttribute('width', '18');
        svg.setAttribute('height', '18');
        svg.setAttribute('viewBox', '0 0 24 24');
        svg.setAttribute('fill', 'none');
        svg.setAttribute('class', 'stroke-[2] text-fg-secondary group-hover/rate-limit:text-fg-primary transition-colors duration-100');

        const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
        circle.setAttribute('cx', '12');
        circle.setAttribute('cy', '12');
        circle.setAttribute('r', '8');
        circle.setAttribute('stroke', 'currentColor');
        circle.setAttribute('stroke-width', '2');

        const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
        path.setAttribute('d', 'M12 12L12 6');
        path.setAttribute('stroke', 'currentColor');
        path.setAttribute('stroke-width', '2');
        path.setAttribute('stroke-linecap', 'round');

        svg.appendChild(circle);
        svg.appendChild(path);
        rateLimitContainer.appendChild(svg);

        const textSpan = document.createElement('span');
        rateLimitContainer.appendChild(textSpan);

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

      const textSpan = rateLimitContainer.querySelector('span');
      if (response.error) {
        if (lastRateLimit.remainingQueries !== null && lastRateLimit.totalQueries !== null) {
          textSpan.textContent = `${lastRateLimit.remainingQueries}/${lastRateLimit.totalQueries}`;
        } else {
          textSpan.textContent = 'Unavailable';
        }
      } else {
        const { remainingQueries, totalQueries } = response;
        lastRateLimit.remainingQueries = remainingQueries;
        lastRateLimit.totalQueries = totalQueries;
        textSpan.textContent = `${remainingQueries}/${totalQueries}`;
      }
    }

    // Function to fetch rate limit
    async function fetchRateLimit(modelName, requestKind, force = false) {
      if (!force) {
        const cached = cachedRateLimits[modelName]?.[requestKind];
        if (cached !== undefined) {
          return cached;
        }
      }

      try {
        const response = await fetch(window.location.origin + '/rest/rate-limits', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            requestKind,
            modelName,
          }),
          credentials: 'include',
        });

        if (!response.ok) {
          throw new Error(`HTTP error: Status ${response.status}`);
        }

        const data = await response.json();
        if (!cachedRateLimits[modelName]) {
          cachedRateLimits[modelName] = {};
        }
        cachedRateLimits[modelName][requestKind] = data;
        return data;
      } catch (error) {
        console.error(`Failed to fetch rate limit:`, error);
        if (!cachedRateLimits[modelName]) {
          cachedRateLimits[modelName] = {};
        }
        cachedRateLimits[modelName][requestKind] = undefined;
        return { error: true };
      }
    }

    // Function to fetch and update rate limit
    async function fetchAndUpdateRateLimit(queryBar, force = false) {
      if (!queryBar || !document.body.contains(queryBar)) {
        return;
      }
      const modelSpan = await waitForElement(MODEL_SELECTOR, ELEMENT_WAIT_TIMEOUT_MS, queryBar);
      let modelName = DEFAULT_MODEL;
      if (modelSpan) {
        modelName = normalizeModelName(modelSpan.textContent.trim());
      }

      let requestKind = DEFAULT_KIND;
      if (modelName === 'grok-3') {
        const thinkButton = await findButtonByTextAsync('Think', false, ELEMENT_WAIT_TIMEOUT_MS, queryBar);
        const searchButton = await findButtonByTextAsync('Deep', true, ELEMENT_WAIT_TIMEOUT_MS, queryBar);

        if (thinkButton && thinkButton.getAttribute('aria-pressed') === 'true') {
          requestKind = 'REASONING';
        } else if (searchButton && searchButton.getAttribute('aria-pressed') === 'true') {
          const modeSpan = searchButton.querySelector('span');
          let modeText = modeSpan ? modeSpan.textContent.trim() : searchButton.getAttribute('aria-label');
          if (modeText === 'DeepSearch') {
            requestKind = 'DEEPSEARCH';
          } else if (modeText === 'DeeperSearch') {
            requestKind = 'DEEPERSEARCH';
          }
        }
      }

      const data = await fetchRateLimit(modelName, requestKind, force);
      updateRateLimitDisplay(queryBar, data);
    }

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

      const handleVisibilityChange = () => {
        if (document.visibilityState === 'visible' && lastQueryBar) {
          fetchAndUpdateRateLimit(lastQueryBar, true);
          pollInterval = setInterval(() => fetchAndUpdateRateLimit(lastQueryBar, true), POLL_INTERVAL_MS);
        } else {
          if (pollInterval) {
            clearInterval(pollInterval);
            pollInterval = null;
          }
        }
      };

      document.addEventListener('visibilitychange', handleVisibilityChange);

      const initialQueryBar = document.querySelector(QUERY_BAR_SELECTOR);
      if (initialQueryBar) {
        removeExistingRateLimit();
        fetchAndUpdateRateLimit(initialQueryBar);
        lastQueryBar = initialQueryBar;

        setupModelObserver(initialQueryBar);
        setupGrok3Observers(initialQueryBar);
        setupSubmissionListeners(initialQueryBar);

        if (document.visibilityState === 'visible') {
          pollInterval = setInterval(() => fetchAndUpdateRateLimit(lastQueryBar, true), POLL_INTERVAL_MS);
        }
      }

      const observer = new MutationObserver(() => {
        const queryBar = document.querySelector(QUERY_BAR_SELECTOR);

        if (queryBar && queryBar !== lastQueryBar) {
          removeExistingRateLimit();
          fetchAndUpdateRateLimit(queryBar);
          lastQueryBar = queryBar;

          if (lastModelObserver) {
            lastModelObserver.disconnect();
          }
          if (lastThinkObserver) {
            lastThinkObserver.disconnect();
          }
          if (lastSearchObserver) {
            lastSearchObserver.disconnect();
          }

          setupModelObserver(queryBar);
          setupGrok3Observers(queryBar);
          setupSubmissionListeners(queryBar);

          if (document.visibilityState === 'visible') {
            if (pollInterval) clearInterval(pollInterval);
            pollInterval = setInterval(() => fetchAndUpdateRateLimit(lastQueryBar, true), POLL_INTERVAL_MS);
          }
        } else if (!queryBar && lastQueryBar) {
          removeExistingRateLimit();
          if (lastModelObserver) {
            lastModelObserver.disconnect();
          }
          if (lastThinkObserver) {
            lastThinkObserver.disconnect();
          }
          if (lastSearchObserver) {
            lastSearchObserver.disconnect();
          }
          lastQueryBar = null;
          lastModelObserver = null;
          lastThinkObserver = null;
          lastSearchObserver = null;
          lastInputElement = null;
          if (pollInterval) {
            clearInterval(pollInterval);
            pollInterval = null;
          }
        }
      });

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

      function setupModelObserver(queryBar) {
        const modelSpan = queryBar.querySelector(MODEL_SELECTOR);
        if (modelSpan) {
          lastModelObserver = new MutationObserver(() => {
            fetchAndUpdateRateLimit(queryBar);
            setupGrok3Observers(queryBar);
          });
          lastModelObserver.observe(modelSpan, { characterData: true, childList: true, subtree: true });
        }
      }

      async function setupGrok3Observers(queryBar) {
        const modelSpan = queryBar.querySelector(MODEL_SELECTOR);
        const currentModel = normalizeModelName(modelSpan ? modelSpan.textContent.trim() : DEFAULT_MODEL);
        if (currentModel === 'grok-3') {
          const thinkButton = await findButtonByTextAsync('Think', false, ELEMENT_WAIT_TIMEOUT_MS, queryBar);
          if (thinkButton) {
            if (lastThinkObserver) lastThinkObserver.disconnect();
            lastThinkObserver = new MutationObserver(() => {
              fetchAndUpdateRateLimit(queryBar);
            });
            lastThinkObserver.observe(thinkButton, { attributes: true, attributeFilter: ['aria-pressed', 'class'] });
          }
          const searchButton = await findButtonByTextAsync('Deep', true, ELEMENT_WAIT_TIMEOUT_MS, queryBar);
          if (searchButton) {
            if (lastSearchObserver) lastSearchObserver.disconnect();
            lastSearchObserver = new MutationObserver(() => {
              fetchAndUpdateRateLimit(queryBar);
            });
            lastSearchObserver.observe(searchButton, { attributes: true, attributeFilter: ['aria-pressed', 'class'], childList: true, subtree: true, characterData: true });
          }
        } else {
          if (lastThinkObserver) {
            lastThinkObserver.disconnect();
            lastThinkObserver = null;
          }
          if (lastSearchObserver) {
            lastSearchObserver.disconnect();
            lastSearchObserver = null;
          }
        }
      }

      function setupSubmissionListeners(queryBar) {
        const inputElement = queryBar.querySelector('textarea');
        if (inputElement && inputElement !== lastInputElement) {
          lastInputElement = inputElement;
          inputElement.addEventListener('keydown', (e) => {
            if (e.key === 'Enter' && !e.shiftKey) {
              console.log('Enter pressed for submit');
              setTimeout(() => fetchAndUpdateRateLimit(queryBar, true), 5000);
            }
          });
        }
      }
    }

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

})();