Perplexity Length Indicator

Adds character/token count indicator to Perplexity conversations

Versione datata 18/05/2025. Vedi la nuova versione l'ultima versione.

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 Length Indicator
// @namespace    https://lugia19.com
// @version      0.4
// @description  Adds character/token count indicator to Perplexity conversations
// @author       lugia19
// @license      MIT
// @match        https://www.perplexity.ai/*
// @grant        none
// ==/UserScript==


(function () {
	'use strict';

	const CHECK_INTERVAL = 30000; // Check every 15 seconds
	const RETRY_INTERVAL = 1000; // Retry every 1 second
	const MAX_RETRY_TIME = 30000; // Retry for up to 30 seconds

	let lengthIndicator = null;
	let injectionAttempts = 0;
	let injectionStartTime = 0;
	let injectionRetryTimer = null;
	const originalFetch = window.fetch;

	function isConversationPage() {
		return window.location.href.match(/https:\/\/www\.perplexity\.ai\/search\/.*-.*$/);
	}

	function getConversationId() {
		const match = window.location.href.match(/https:\/\/www\.perplexity\.ai\/search\/(.*)/);
		return match ? match[1] : null;
	}

	function isLengthIndicatorPresent() {
		return document.querySelector('.perplexity-length-indicator') !== null;
	}

	function injectLengthIndicator() {
		// Find the bottom right container with the help button
		const bottomRightContainer = document.querySelector('.bottom-md.right-md.m-sm.fixed.hidden.md\\:block .flex.items-center.gap-2');
		if (!bottomRightContainer) return false;

		// Create our indicator
		lengthIndicator = document.createElement('span');
		lengthIndicator.className = 'perplexity-length-indicator';

		// Start hidden if not on a conversation page
		if (!isConversationPage()) {
			lengthIndicator.style.display = 'none';
		}

		// Create token counter with styled text
		const counter = document.createElement('div');
		counter.className = 'bg-offsetPlus dark:bg-offsetPlusDark text-textMain dark:text-textMainDark md:hover:text-textOff md:dark:hover:text-textOffDark !bg-background dark:border-borderMain/25 dark:!bg-offset border shadow-subtle border-borderMain/50 font-sans focus:outline-none outline-none outline-transparent transition duration-300 ease-out font-sans select-none items-center relative group/button justify-center text-center items-center rounded-full cursor-pointer origin-center whitespace-nowrap inline-flex text-sm h-8 px-3';
		counter.style.display = 'flex';
		counter.style.alignItems = 'center';
		counter.style.justifyContent = 'center';

		// Create the text span with blue color
		const textSpan = document.createElement('span');
		textSpan.className = 'font-medium';
		textSpan.style.color = '#3b82f6'; // Keep the blue color
		textSpan.textContent = '0 tokens';

		counter.appendChild(textSpan);

		// Add components to the indicator
		lengthIndicator.appendChild(counter);

		// Add the indicator at the beginning of the container (before the help button)
		bottomRightContainer.insertBefore(lengthIndicator, bottomRightContainer.firstChild);

		// Reset injection retry counters
		clearTimeout(injectionRetryTimer);
		injectionAttempts = 0;
		injectionStartTime = 0;

		return true;
	}

	async function updateLengthIndicator() {
		// If not on a conversation page, hide the indicator
		if (!isConversationPage()) {
			if (lengthIndicator) {
				lengthIndicator.style.display = 'none';
			}
			return;
		}

		// On conversation page, show the indicator
		if (lengthIndicator) {
			lengthIndicator.style.display = '';
		}

		const conversationId = getConversationId();
		if (!conversationId) return;

		try {
			const response = await fetch(`https://www.perplexity.ai/rest/thread/${conversationId}?with_schematized_response=true&limit=9999`);
			const data = await response.json();

			let charCount = 0;

			if (data.entries && Array.isArray(data.entries)) {
				data.entries.forEach(entry => {
					// Add query string length
					if (entry.query_str) {
						charCount += entry.query_str.length;
					}

					// Add response text length
					if (entry.blocks && Array.isArray(entry.blocks)) {
						entry.blocks.forEach(block => {
							if (block.intended_usage === "ask_text" &&
								block.markdown_block &&
								block.markdown_block.answer) {
								charCount += block.markdown_block.answer.length;
							}
						});
					}
				});
			}

			// Estimate tokens (char count / 4)
			const tokenCount = Math.round(charCount / 4);

			// Update the indicator
			if (lengthIndicator) {
				const counterSpan = lengthIndicator.querySelector('span.font-medium');
				if (counterSpan) {
					counterSpan.textContent = `${tokenCount} tokens`;
				}
			}

		} catch (error) {
			console.error('Error fetching conversation data:', error);
		}
	}

	function startInjectionRetry() {
		// Start tracking injection attempts
		if (injectionStartTime === 0) {
			injectionStartTime = Date.now();
		}

		// Try to inject the indicator
		const injected = injectLengthIndicator();

		// If successful, update the indicator and stop retrying
		if (injected) {
			updateLengthIndicator();
			return;
		}

		// Check if we've reached the maximum retry time
		injectionAttempts++;
		const elapsedTime = Date.now() - injectionStartTime;

		if (elapsedTime < MAX_RETRY_TIME) {
			// Continue retrying
			injectionRetryTimer = setTimeout(startInjectionRetry, RETRY_INTERVAL);
		} else {
			// Reset counters after max retry time
			injectionAttempts = 0;
			injectionStartTime = 0;
			console.log('Failed to inject length indicator after maximum retry time');
		}
	}

	function checkAndUpdate() {
		if (!isLengthIndicatorPresent()) {
			// Start the retry process for injection
			startInjectionRetry();
		} else {
			updateLengthIndicator();
		}
	}

	// Setup fetch interception using the provided pattern
	window.fetch = async (...args) => {
		const [input, config] = args;

		let url;
		if (input instanceof URL) {
			url = input.href;
		} else if (typeof input === 'string') {
			url = input;
		} else if (input instanceof Request) {
			url = input.url;
		}

		const method = config?.method || (input instanceof Request ? input.method : 'GET');
		// Proceed with the original fetch
		const response = await originalFetch(...args);

		// Check if this is a request to the perplexity_ask endpoint
		if (url && url.includes('perplexity.ai/rest/sse/perplexity_ask') && method === 'POST') {
			// Wait a bit for the response to be processed and update
			console.log("UPDATING, GOT RESPONSE!")
			setTimeout(checkAndUpdate, 10000);
		}

		return response;
	};

	// Initial check
	setTimeout(checkAndUpdate, 1000);

	// Set up interval for regular checks
	setInterval(checkAndUpdate, CHECK_INTERVAL);

	// Listen for URL changes (for single-page apps)
	let lastUrl = window.location.href;
	new MutationObserver(() => {
		if (lastUrl !== window.location.href) {
			lastUrl = window.location.href;
			// Update the visibility based on new URL
			if (lengthIndicator) {
				lengthIndicator.style.display = isConversationPage() ? '' : 'none';
			}
			setTimeout(checkAndUpdate, 1000); // Check after URL change
		}
	}).observe(document, { subtree: true, childList: true });
})();