Perplexity Length Indicator

Adds character/token count indicator to Perplexity conversations

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_link:Tampermonkey}.

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         Perplexity Length Indicator
// @namespace    https://lugia19.com
// @version      0.7
// @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 use element with the question mark icon
		const useElements = document.querySelectorAll('use');
		let useElement = null;
		for (const el of useElements) {
			const href = el.getAttribute('xlink:href') || el.getAttributeNS('http://www.w3.org/1999/xlink', 'href') || el.getAttribute('href');
			if (href === '#pplx-icon-question-mark') {
				useElement = el;
				break;
			}
		}
		if (!useElement) return false;

		// Travel up parents to find the target container
		let targetContainer = useElement.parentElement;
		while (targetContainer) {
			if (targetContainer.tagName === 'DIV' &&
				targetContainer.classList.contains('gap-sm') &&
				targetContainer.classList.contains('flex') &&
				targetContainer.classList.contains('items-center')) {
				break;
			}
			targetContainer = targetContainer.parentElement;
		}

		if (!targetContainer) 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)
		targetContainer.insertBefore(lengthIndicator, targetContainer.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 });
})();