Perplexity.ai Limits Overlay

Overlays remaining message limits and updates automatically when a prompt is sent

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

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

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name         Perplexity.ai Limits Overlay
// @namespace    http://tampermonkey.net/
// @version      3.2
// @description  Overlays remaining message limits and updates automatically when a prompt is sent
// @match        https://www.perplexity.ai/*
// @run-at       document-start
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // ---------------------------
    // Configuration
    // ---------------------------
    const CONFIG = {
        apiEndpoint: 'https://www.perplexity.ai/rest/rate-limit/all',
        triggerEndpoint: '/rest/sse/perplexity_ask',
        checkDelays: [2000, 5000] // Check after 2s and again after 5s to be safe
    };

    let limitsBox = null;

    // ---------------------------
    // Network Interception (The Fix)
    // ---------------------------
    // We run with @grant none, so window.fetch IS the page's fetch.

    const originalFetch = window.fetch;
    window.fetch = async function(...args) {
        let url = args[0];
        if (url instanceof Request) url = url.url;
        url = url ? url.toString() : '';

        // Hook the specific ASK endpoint
        if (url.includes(CONFIG.triggerEndpoint)) {
            // Trigger updates
            CONFIG.checkDelays.forEach(delay => setTimeout(fetchLimits, delay));
        }

        return originalFetch.apply(this, args);
    };

    // Also hook XHR just in case Perplexity switches methods or uses mixed calls
    const originalXHROpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function(method, url) {
        this._url = url ? url.toString() : '';
        return originalXHROpen.apply(this, arguments);
    };

    const originalXHRSend = XMLHttpRequest.prototype.send;
    XMLHttpRequest.prototype.send = function() {
        if (this._url && this._url.includes(CONFIG.triggerEndpoint)) {
             CONFIG.checkDelays.forEach(delay => setTimeout(fetchLimits, delay));
        }
        return originalXHRSend.apply(this, arguments);
    };

    // ---------------------------
    // Fetch Limits From Server
    // ---------------------------
    async function fetchLimits() {
        try {
            // Add timestamp to prevent caching
            const timestamp = Date.now();
            const response = await originalFetch(`${CONFIG.apiEndpoint}?t=${timestamp}`, {
                credentials: 'include'
            });

            if (response.ok) {
                const data = await response.json();
                updateLimitsUI(data.remaining_pro);
            }
        } catch (error) {
            // Silent catch to avoid console spam
        }
    }

    // ---------------------------
    // UI Creation & Updates
    // ---------------------------
    function initUI() {
        if (document.getElementById('limits-box')) return;

        limitsBox = document.createElement('div');
        limitsBox.id = 'limits-box';
        limitsBox.style.cssText = `
            position: fixed;
            right: 20px;
            top: 20px;
            background-color: rgba(30, 30, 30, 0.95);
            color: #e8e8e8;
            border: 1px solid #444;
            border-radius: 8px;
            padding: 12px 16px;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
            font-size: 13px;
            z-index: 99999;
            box-shadow: 0 4px 12px rgba(0,0,0,0.5);
            cursor: grab;
            user-select: none;
            backdrop-filter: blur(5px);
            min-width: 180px;
            transition: transform 0.1s ease;
        `;

        document.body.appendChild(limitsBox);

        // Restore position from localStorage (since we removed GM_)
        const savedY = parseFloat(localStorage.getItem('perplexity_limits_pos_y')) || 20;
        const maxY = window.innerHeight - 100;
        limitsBox.style.transform = `translateY(${Math.min(savedY, maxY)}px)`;

        setupDraggable(limitsBox);
        fetchLimits();
    }

    function updateLimitsUI(value) {
        if (!limitsBox) initUI();

        limitsBox.innerHTML = `
            <div style="display: flex; justify-content: space-between; align-items: center; gap: 15px;">
                <span style="opacity: 0.8;">Remaining Queries:</span>
                <span style="font-weight: 700; color: #fff; font-size: 15px;">${value ?? '—'}</span>
            </div>
        `;
    }

    // ---------------------------
    // Draggable Logic
    // ---------------------------
    function setupDraggable(element) {
        let isDragging = false;
        let startY = 0;
        let initialTransformY = 0;

        element.addEventListener('mousedown', (e) => {
            isDragging = true;
            element.style.cursor = 'grabbing';
            startY = e.clientY;

            const style = window.getComputedStyle(element);
            const matrix = new WebKitCSSMatrix(style.transform);
            initialTransformY = matrix.m42;

            e.preventDefault();
        });

        document.addEventListener('mousemove', (e) => {
            if (!isDragging) return;

            const deltaY = e.clientY - startY;
            let newY = initialTransformY + deltaY;

            const maxY = window.innerHeight - element.offsetHeight;
            newY = Math.max(0, Math.min(newY, maxY));

            element.style.transform = `translateY(${newY}px)`;
        });

        document.addEventListener('mouseup', () => {
            if (isDragging) {
                isDragging = false;
                element.style.cursor = 'grab';

                const style = window.getComputedStyle(element);
                const matrix = new WebKitCSSMatrix(style.transform);
                // Save to localStorage
                localStorage.setItem('perplexity_limits_pos_y', matrix.m42);
            }
        });
    }

    // ---------------------------
    // Initialization
    // ---------------------------
    const observer = new MutationObserver(() => {
        if (document.body) {
            initUI();
            observer.disconnect();
        }
    });

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

})();