Grok Rate Limit Display

Displays Grok rate limit on screen

目前為 2025-07-11 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Grok Rate Limit Display
// @namespace    http://tampermonkey.net/
// @version      2.4
// @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;

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

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

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

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

      // Polling every 1 second for model changes
      setInterval(() => {
        if (lastQueryBar && document.hasFocus()) {
          const modelSpan = lastQueryBar.querySelector('span.inline-block.text-primary');
          if (modelSpan) {
            const currentModel = normalizeModelName(modelSpan.textContent.trim());
            if (currentModel !== lastModel) {
              lastModel = currentModel;
              fetchAndUpdateRateLimit(lastQueryBar);
            }
          }
        }
      }, 1000);
    }

    // Initial check when the script loads
    const initialQueryBar = document.querySelector('.query-bar');
    if (initialQueryBar) {
      fetchAndUpdateRateLimit(initialQueryBar);
    }

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

})();