Perplexity Length Indicator

Adds character/token count indicator to Perplexity conversations

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
  1. // ==UserScript==
  2. // @name Perplexity Length Indicator
  3. // @namespace https://lugia19.com
  4. // @version 0.6
  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 = 30000; // Check every 15 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 bottom right container with the help button
  41. const bottomRightContainer = document.querySelector('.bottom-md.right-md.m-sm.fixed.hidden.md\\:block .flex.items-center.gap-2');
  42. if (!bottomRightContainer) return false;
  43.  
  44. // Create our indicator
  45. lengthIndicator = document.createElement('span');
  46. lengthIndicator.className = 'perplexity-length-indicator';
  47.  
  48. // Start hidden if not on a conversation page
  49. if (!isConversationPage()) {
  50. lengthIndicator.style.display = 'none';
  51. }
  52.  
  53. // Create token counter with styled text
  54. const counter = document.createElement('div');
  55. 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';
  56. counter.style.display = 'flex';
  57. counter.style.alignItems = 'center';
  58. counter.style.justifyContent = 'center';
  59.  
  60. // Create the text span with blue color
  61. const textSpan = document.createElement('span');
  62. textSpan.className = 'font-medium';
  63. textSpan.style.color = '#3b82f6'; // Keep the blue color
  64. textSpan.textContent = '0 tokens';
  65.  
  66. counter.appendChild(textSpan);
  67.  
  68. // Add components to the indicator
  69. lengthIndicator.appendChild(counter);
  70.  
  71. // Add the indicator at the beginning of the container (before the help button)
  72. bottomRightContainer.insertBefore(lengthIndicator, bottomRightContainer.firstChild);
  73.  
  74. // Reset injection retry counters
  75. clearTimeout(injectionRetryTimer);
  76. injectionAttempts = 0;
  77. injectionStartTime = 0;
  78.  
  79. return true;
  80. }
  81.  
  82. async function updateLengthIndicator() {
  83. // If not on a conversation page, hide the indicator
  84. if (!isConversationPage()) {
  85. if (lengthIndicator) {
  86. lengthIndicator.style.display = 'none';
  87. }
  88. return;
  89. }
  90.  
  91. // On conversation page, show the indicator
  92. if (lengthIndicator) {
  93. lengthIndicator.style.display = '';
  94. }
  95.  
  96. const conversationId = getConversationId();
  97. if (!conversationId) return;
  98.  
  99. try {
  100. const response = await fetch(`https://www.perplexity.ai/rest/thread/${conversationId}?with_schematized_response=true&limit=9999`);
  101. const data = await response.json();
  102.  
  103. let charCount = 0;
  104.  
  105. if (data.entries && Array.isArray(data.entries)) {
  106. data.entries.forEach(entry => {
  107. // Add query string length
  108. if (entry.query_str) {
  109. charCount += entry.query_str.length;
  110. }
  111.  
  112. // Add response text length
  113. if (entry.blocks && Array.isArray(entry.blocks)) {
  114. entry.blocks.forEach(block => {
  115. if (block.intended_usage === "ask_text" &&
  116. block.markdown_block &&
  117. block.markdown_block.answer) {
  118. charCount += block.markdown_block.answer.length;
  119. }
  120. });
  121. }
  122. });
  123. }
  124.  
  125. // Estimate tokens (char count / 4)
  126. const tokenCount = Math.round(charCount / 4);
  127.  
  128. // Update the indicator
  129. if (lengthIndicator) {
  130. const counterSpan = lengthIndicator.querySelector('span.font-medium');
  131. if (counterSpan) {
  132. counterSpan.textContent = `${tokenCount} tokens`;
  133. }
  134. }
  135.  
  136. } catch (error) {
  137. console.error('Error fetching conversation data:', error);
  138. }
  139. }
  140.  
  141. function startInjectionRetry() {
  142. // Start tracking injection attempts
  143. if (injectionStartTime === 0) {
  144. injectionStartTime = Date.now();
  145. }
  146.  
  147. // Try to inject the indicator
  148. const injected = injectLengthIndicator();
  149.  
  150. // If successful, update the indicator and stop retrying
  151. if (injected) {
  152. updateLengthIndicator();
  153. return;
  154. }
  155.  
  156. // Check if we've reached the maximum retry time
  157. injectionAttempts++;
  158. const elapsedTime = Date.now() - injectionStartTime;
  159.  
  160. if (elapsedTime < MAX_RETRY_TIME) {
  161. // Continue retrying
  162. injectionRetryTimer = setTimeout(startInjectionRetry, RETRY_INTERVAL);
  163. } else {
  164. // Reset counters after max retry time
  165. injectionAttempts = 0;
  166. injectionStartTime = 0;
  167. console.log('Failed to inject length indicator after maximum retry time');
  168. }
  169. }
  170.  
  171. function checkAndUpdate() {
  172. if (!isLengthIndicatorPresent()) {
  173. // Start the retry process for injection
  174. startInjectionRetry();
  175. } else {
  176. updateLengthIndicator();
  177. }
  178. }
  179.  
  180. // Setup fetch interception using the provided pattern
  181. window.fetch = async (...args) => {
  182. const [input, config] = args;
  183.  
  184. let url;
  185. if (input instanceof URL) {
  186. url = input.href;
  187. } else if (typeof input === 'string') {
  188. url = input;
  189. } else if (input instanceof Request) {
  190. url = input.url;
  191. }
  192.  
  193. const method = config?.method || (input instanceof Request ? input.method : 'GET');
  194. // Proceed with the original fetch
  195. const response = await originalFetch(...args);
  196.  
  197. // Check if this is a request to the perplexity_ask endpoint
  198. if (url && url.includes('perplexity.ai/rest/sse/perplexity_ask') && method === 'POST') {
  199. // Wait a bit for the response to be processed and update
  200. console.log("UPDATING, GOT RESPONSE!")
  201. setTimeout(checkAndUpdate, 10000);
  202. }
  203.  
  204. return response;
  205. };
  206.  
  207. // Initial check
  208. setTimeout(checkAndUpdate, 1000);
  209.  
  210. // Set up interval for regular checks
  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. // Update the visibility based on new URL
  219. if (lengthIndicator) {
  220. lengthIndicator.style.display = isConversationPage() ? '' : 'none';
  221. }
  222. setTimeout(checkAndUpdate, 1000); // Check after URL change
  223. }
  224. }).observe(document, { subtree: true, childList: true });
  225. })();