Greasy Fork is available in English.

Perplexity Token Counter

Adds character/token count indicator to Perplexity conversations

  1. // ==UserScript==
  2. // @name Perplexity Token Counter
  3. // @namespace https://lugia19.com
  4. // @version 0.3
  5. // @description Adds character/token count indicator to Perplexity conversations
  6. // @author lugia19
  7. // @license MIT
  8. // @match https://www.perplexity.ai/*
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12.  
  13. (function () {
  14. 'use strict';
  15.  
  16. const CHECK_INTERVAL = 15000; // Check every 30 seconds
  17. const RETRY_INTERVAL = 1000; // Retry every 1 second
  18. const MAX_RETRY_TIME = 30000; // Retry for up to 30 seconds
  19.  
  20. let lengthIndicator = null;
  21. let injectionAttempts = 0;
  22. let injectionStartTime = 0;
  23. let injectionRetryTimer = null;
  24. const originalFetch = window.fetch;
  25.  
  26. function isConversationPage() {
  27. return window.location.href.match(/https:\/\/www\.perplexity\.ai\/search\/.*-.*$/);
  28. }
  29.  
  30. function getConversationId() {
  31. const match = window.location.href.match(/https:\/\/www\.perplexity\.ai\/search\/(.*)/);
  32. return match ? match[1] : null;
  33. }
  34.  
  35. function isLengthIndicatorPresent() {
  36. return document.querySelector('.perplexity-length-indicator') !== null;
  37. }
  38.  
  39. function injectLengthIndicator() {
  40. // Find the middle section with the chat title
  41. const middleSections = document.querySelectorAll('.hidden.min-w-0.grow.items-center.justify-center.text-center.md\\:flex');
  42. if (!middleSections.length) return false;
  43.  
  44. const middleSection = middleSections[0];
  45.  
  46. // Check if there's a chat title at the end
  47. const titleContainer = middleSection.querySelector('span.min-w-0');
  48. if (!titleContainer) return false;
  49.  
  50. // Create our indicator
  51. lengthIndicator = document.createElement('div');
  52. lengthIndicator.className = 'perplexity-length-indicator';
  53. lengthIndicator.style.display = 'flex';
  54. lengthIndicator.style.alignItems = 'center';
  55. lengthIndicator.style.marginLeft = '4px';
  56.  
  57. // Add a slash divider first
  58. const divider = document.createElement('div');
  59. divider.className = 'ultraLight font-sans text-sm text-textOff/50 dark:text-textOffDark/50';
  60. divider.innerHTML = `
  61. <svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="slash-forward" class="svg-inline--fa fa-slash-forward fa-xs" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
  62. <path fill="currentColor" d="M308.1 3.3c11.4 6.7 15.3 21.4 8.6 32.8l-272 464c-6.7 11.4-21.4 15.3-32.8 8.6S-3.4 487.3 3.3 475.9l272-464C282 .4 296.7-3.4 308.1 3.3z"></path>
  63. </svg>
  64. `;
  65.  
  66. // Create token counter with styled text
  67. const counter = document.createElement('div');
  68. counter.className = 'py-sm pl-sm truncate default font-sans text-xs font-medium';
  69. counter.style.color = '#3b82f6'; // Blue color as requested
  70. counter.innerHTML = `<span>0 tokens</span>`;
  71.  
  72. // Add components to the indicator
  73. lengthIndicator.appendChild(divider);
  74. lengthIndicator.appendChild(counter);
  75.  
  76. // Add the indicator after the title container
  77. middleSection.appendChild(lengthIndicator);
  78.  
  79. // Reset injection retry counters
  80. clearTimeout(injectionRetryTimer);
  81. injectionAttempts = 0;
  82. injectionStartTime = 0;
  83.  
  84. return true;
  85. }
  86.  
  87. async function updateLengthIndicator() {
  88. const conversationId = getConversationId();
  89. if (!conversationId) return;
  90.  
  91. try {
  92. const response = await fetch(`https://www.perplexity.ai/rest/thread/${conversationId}?with_schematized_response=true&limit=9999`);
  93. const data = await response.json();
  94.  
  95. let charCount = 0;
  96.  
  97. if (data.entries && Array.isArray(data.entries)) {
  98. data.entries.forEach(entry => {
  99. // Add query string length
  100. if (entry.query_str) {
  101. charCount += entry.query_str.length;
  102. }
  103.  
  104. // Add response text length
  105. if (entry.blocks && Array.isArray(entry.blocks)) {
  106. entry.blocks.forEach(block => {
  107. if (block.intended_usage === "ask_text" &&
  108. block.markdown_block &&
  109. block.markdown_block.answer) {
  110. charCount += block.markdown_block.answer.length;
  111. }
  112. });
  113. }
  114. });
  115. }
  116.  
  117. // Estimate tokens (char count / 4)
  118. const tokenCount = Math.round(charCount / 4);
  119.  
  120. // Update the indicator
  121. if (lengthIndicator) {
  122. const counterSpan = lengthIndicator.querySelector('.py-sm.pl-sm span');
  123. if (counterSpan) {
  124. counterSpan.textContent = `${tokenCount} tokens`;
  125. }
  126. }
  127.  
  128. } catch (error) {
  129. console.error('Error fetching conversation data:', error);
  130. }
  131. }
  132.  
  133. function startInjectionRetry() {
  134. // Start tracking injection attempts
  135. if (injectionStartTime === 0) {
  136. injectionStartTime = Date.now();
  137. }
  138.  
  139. // Try to inject the indicator
  140. const injected = injectLengthIndicator();
  141.  
  142. // If successful, update the indicator and stop retrying
  143. if (injected) {
  144. updateLengthIndicator();
  145. return;
  146. }
  147.  
  148. // Check if we've reached the maximum retry time
  149. injectionAttempts++;
  150. const elapsedTime = Date.now() - injectionStartTime;
  151.  
  152. if (elapsedTime < MAX_RETRY_TIME) {
  153. // Continue retrying
  154. injectionRetryTimer = setTimeout(startInjectionRetry, RETRY_INTERVAL);
  155. } else {
  156. // Reset counters after max retry time
  157. injectionAttempts = 0;
  158. injectionStartTime = 0;
  159. console.log('Failed to inject length indicator after maximum retry time');
  160. }
  161. }
  162.  
  163. function checkAndUpdate() {
  164. if (isConversationPage()) {
  165. if (!isLengthIndicatorPresent()) {
  166. // Start the retry process for injection
  167. startInjectionRetry();
  168. } else {
  169. updateLengthIndicator();
  170. }
  171. } else {
  172. // Not a conversation page, reset injection tracking
  173. clearTimeout(injectionRetryTimer);
  174. injectionAttempts = 0;
  175. injectionStartTime = 0;
  176. }
  177. }
  178.  
  179. // Setup fetch interception using the provided pattern
  180. window.fetch = async (...args) => {
  181. const [input, config] = args;
  182.  
  183. let url;
  184. if (input instanceof URL) {
  185. url = input.href;
  186. } else if (typeof input === 'string') {
  187. url = input;
  188. } else if (input instanceof Request) {
  189. url = input.url;
  190. }
  191.  
  192. const method = config?.method || (input instanceof Request ? input.method : 'GET');
  193. // Proceed with the original fetch
  194. const response = await originalFetch(...args);
  195.  
  196. // Check if this is a request to the perplexity_ask endpoint
  197. if (url && url.includes('perplexity.ai/rest/sse/perplexity_ask') && method === 'POST') {
  198. // Wait a bit for the response to be processed and update
  199. console.log("UPDATING, GOT RESPONSE!")
  200. setTimeout(checkAndUpdate, 10000);
  201. }
  202.  
  203.  
  204. return response;
  205. };
  206.  
  207. // Initial check
  208. setTimeout(checkAndUpdate, 1000);
  209.  
  210. // Set up interval for regular checks (30 seconds)
  211. setInterval(checkAndUpdate, CHECK_INTERVAL);
  212.  
  213. // Listen for URL changes (for single-page apps)
  214. let lastUrl = window.location.href;
  215. new MutationObserver(() => {
  216. if (lastUrl !== window.location.href) {
  217. lastUrl = window.location.href;
  218. clearTimeout(injectionRetryTimer); // Clear any ongoing retries
  219. injectionAttempts = 0;
  220. injectionStartTime = 0;
  221. setTimeout(checkAndUpdate, 1000); // Check after URL change
  222. }
  223. }).observe(document, { subtree: true, childList: true });
  224. })();