Grok Rate Limit Display

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

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

})();