PerplexityTools - Floating Copy Code & Navigation Buttons (AFU IT)

Adds floating copy button and navigation buttons

Versión del día 7/5/2025. Echa un vistazo a la versión más reciente.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

  1. // ==UserScript==
  2. // @name PerplexityTools - Floating Copy Code & Navigation Buttons (AFU IT)
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0
  5. // @description Adds floating copy button and navigation buttons
  6. // @author AFU IT
  7. // @match https://www.perplexity.ai/*
  8. // @license MIT
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14.  
  15. const CHECK_INTERVAL = 2000; // Check every 2 seconds
  16. const LONG_PRESS_DURATION = 1000; // 1 second for long press
  17.  
  18. const originalFetch = window.fetch;
  19.  
  20. // Variables to track long press
  21. let upButtonTimer = null;
  22. let downButtonTimer = null;
  23. let isUpButtonLongPress = false;
  24. let isDownButtonLongPress = false;
  25.  
  26. // Helper function to scroll to the previous question
  27. function scrollToPreviousQuestion() {
  28. if (isUpButtonLongPress) return; // Skip if this is triggered by a long press
  29.  
  30. const queryBlocks = Array.from(document.querySelectorAll('.group.relative.mb-1.flex.items-end.gap-0\\.5'));
  31. if (!queryBlocks.length) return;
  32.  
  33. // Get all blocks positions
  34. const positions = queryBlocks.map(block => {
  35. const rect = block.getBoundingClientRect();
  36. return {
  37. element: block,
  38. top: rect.top,
  39. bottom: rect.bottom
  40. };
  41. });
  42.  
  43. // Sort by vertical position
  44. positions.sort((a, b) => a.top - b.top);
  45.  
  46. // Find the first block above the middle of the viewport
  47. const viewportMiddle = window.innerHeight / 2;
  48. let targetBlock = null;
  49.  
  50. for (let i = positions.length - 1; i >= 0; i--) {
  51. if (positions[i].top < viewportMiddle) {
  52. if (i > 0) {
  53. targetBlock = positions[i - 1].element;
  54. } else {
  55. // If we're at the first question, scroll to top
  56. window.scrollTo({ top: 0, behavior: 'smooth' });
  57. return;
  58. }
  59. break;
  60. }
  61. }
  62.  
  63. // If we found a target block, scroll to it at the top of the viewport
  64. if (targetBlock) {
  65. targetBlock.scrollIntoView({ behavior: 'smooth', block: "start" });
  66. } else if (positions.length > 0) {
  67. // If no suitable block found, go to the first one
  68. positions[0].element.scrollIntoView({ behavior: 'smooth', block: "start" });
  69. }
  70. }
  71.  
  72. // Helper function to scroll to the next question
  73. function scrollToNextQuestion() {
  74. if (isDownButtonLongPress) return; // Skip if this is triggered by a long press
  75.  
  76. const queryBlocks = Array.from(document.querySelectorAll('.group.relative.mb-1.flex.items-end.gap-0\\.5'));
  77. if (!queryBlocks.length) return;
  78.  
  79. // Get all blocks positions
  80. const positions = queryBlocks.map(block => {
  81. const rect = block.getBoundingClientRect();
  82. return {
  83. element: block,
  84. top: rect.top,
  85. bottom: rect.bottom
  86. };
  87. });
  88.  
  89. // Sort by vertical position
  90. positions.sort((a, b) => a.top - b.top);
  91.  
  92. // Find the first block below the middle of the viewport
  93. const viewportMiddle = window.innerHeight / 2;
  94. let targetBlock = null;
  95.  
  96. for (let i = 0; i < positions.length; i++) {
  97. if (positions[i].top > viewportMiddle) {
  98. targetBlock = positions[i].element;
  99. break;
  100. }
  101. }
  102.  
  103. // If we found a target block, scroll to it at the top of the viewport
  104. if (targetBlock) {
  105. targetBlock.scrollIntoView({ behavior: 'smooth', block: "start" });
  106. } else if (positions.length > 0) {
  107. // If no suitable block found, try to find the Related section
  108. const relatedSection = document.querySelector('.default.font-display.text-lg.font-medium:has(.fa-new-thread)');
  109. if (relatedSection) {
  110. relatedSection.scrollIntoView({ behavior: 'smooth', block: "start" });
  111. } else {
  112. // Or go to the bottom
  113. window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
  114. }
  115. }
  116. }
  117.  
  118. // Helper function to scroll to the top of the page
  119. function scrollToTop() {
  120. window.scrollTo({ top: 0, behavior: 'smooth' });
  121. }
  122.  
  123. // Helper function to scroll to the bottom of the page
  124. function scrollToBottom() {
  125. window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
  126. }
  127.  
  128. // Floating buttons functionality
  129. function addFloatingButtons() {
  130. // Find all pre elements that don't already have our buttons
  131. const codeBlocks = document.querySelectorAll('pre:not(.buttons-added)');
  132.  
  133. codeBlocks.forEach(block => {
  134. // Mark this block as processed
  135. block.classList.add('buttons-added');
  136.  
  137. // Create the copy button with Perplexity's styling
  138. const copyBtn = document.createElement('button');
  139. copyBtn.type = 'button';
  140. copyBtn.className = 'focus-visible:bg-offsetPlus dark:focus-visible:bg-offsetPlusDark hover:bg-offsetPlus text-textOff dark:text-textOffDark hover:text-textMain dark:hover:bg-offsetPlusDark dark:hover:text-textMainDark 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 active:scale-[0.97] active:duration-150 active:ease-outExpo origin-center whitespace-nowrap inline-flex text-sm h-8 aspect-square';
  141. copyBtn.style.cssText = `
  142. position: sticky;
  143. top: 95px;
  144. right: 40px;
  145. float: right;
  146. z-index: 100;
  147. margin-right: 5px;
  148. `;
  149.  
  150. copyBtn.innerHTML = `
  151. <div class="flex items-center min-w-0 font-medium gap-1.5 justify-center">
  152. <div class="flex shrink-0 items-center justify-center size-4">
  153. <svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="copy" class="svg-inline--fa fa-copy fa-fw fa-1x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
  154. <path fill="currentColor" d="M384 336l-192 0c-8.8 0-16-7.2-16-16l0-256c0-8.8 7.2-16 16-16l140.1 0L400 115.9 400 320c0 8.8-7.2 16-16 16zM192 384l192 0c35.3 0 64-28.7 64-64l0-204.1c0-12.7-5.1-24.9-14.1-33.9L366.1 14.1c-9-9-21.2-14.1-33.9-14.1L192 0c-35.3 0-64 28.7-64 64l0 256c0 35.3 28.7 64 64 64zM64 128c-35.3 0-64 28.7-64 64L0 448c0 35.3 28.7 64 64 64l192 0c35.3 0 64-28.7 64-64l0-32-48 0 0 32c0 8.8-7.2 16-16 16L64 464c-8.8 0-16-7.2-16-16l0-256c0-8.8 7.2-16 16-16l32 0 0-48-32 0z"></path>
  155. </svg>
  156. </div>
  157. </div>
  158. `;
  159.  
  160. copyBtn.addEventListener('click', () => {
  161. const code = block.querySelector('code').innerText;
  162. navigator.clipboard.writeText(code);
  163.  
  164. // Visual feedback
  165. const originalHTML = copyBtn.innerHTML;
  166. copyBtn.innerHTML = `
  167. <div class="flex items-center min-w-0 font-medium gap-1.5 justify-center">
  168. <div class="flex shrink-0 items-center justify-center size-4">
  169. <svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="check" class="svg-inline--fa fa-check" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
  170. <path fill="currentColor" d="M438.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L160 338.7 393.4 105.4c12.5-12.5 32.8-12.5 45.3 0z"></path>
  171. </svg>
  172. </div>
  173. </div>
  174. `;
  175.  
  176. setTimeout(() => {
  177. copyBtn.innerHTML = originalHTML;
  178. }, 2000);
  179. });
  180.  
  181. // Create the up arrow button
  182. const upBtn = document.createElement('button');
  183. upBtn.type = 'button';
  184. upBtn.className = 'focus-visible:bg-offsetPlus dark:focus-visible:bg-offsetPlusDark hover:bg-offsetPlus text-textOff dark:text-textOffDark hover:text-textMain dark:hover:bg-offsetPlusDark dark:hover:text-textMainDark 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 active:scale-[0.97] active:duration-150 active:ease-outExpo origin-center whitespace-nowrap inline-flex text-sm h-8 aspect-square';
  185. upBtn.style.cssText = `
  186. position: sticky;
  187. top: 95px;
  188. right: 40px;
  189. float: right;
  190. z-index: 100;
  191. margin-right: 5px;
  192. `;
  193.  
  194. upBtn.innerHTML = `
  195. <div class="flex items-center min-w-0 font-medium gap-1.5 justify-center">
  196. <div class="flex shrink-0 items-center justify-center size-4">
  197. <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  198. <path d="M12 19V5M5 12l7-7 7 7"/>
  199. </svg>
  200. </div>
  201. </div>
  202. `;
  203.  
  204. // Add long press functionality to up button
  205. upBtn.addEventListener('mousedown', () => {
  206. isUpButtonLongPress = false;
  207. upButtonTimer = setTimeout(() => {
  208. isUpButtonLongPress = true;
  209. scrollToTop();
  210. // Visual feedback for long press
  211. upBtn.style.backgroundColor = 'rgba(59, 130, 246, 0.2)'; // Light blue background
  212. setTimeout(() => {
  213. upBtn.style.backgroundColor = '';
  214. }, 500);
  215. }, LONG_PRESS_DURATION);
  216. });
  217.  
  218. upBtn.addEventListener('mouseup', () => {
  219. clearTimeout(upButtonTimer);
  220. if (!isUpButtonLongPress) {
  221. scrollToPreviousQuestion();
  222. }
  223. });
  224.  
  225. upBtn.addEventListener('mouseleave', () => {
  226. clearTimeout(upButtonTimer);
  227. });
  228.  
  229. upBtn.addEventListener('touchstart', (e) => {
  230. isUpButtonLongPress = false;
  231. upButtonTimer = setTimeout(() => {
  232. isUpButtonLongPress = true;
  233. scrollToTop();
  234. // Visual feedback for long press
  235. upBtn.style.backgroundColor = 'rgba(59, 130, 246, 0.2)'; // Light blue background
  236. setTimeout(() => {
  237. upBtn.style.backgroundColor = '';
  238. }, 500);
  239. }, LONG_PRESS_DURATION);
  240. e.preventDefault(); // Prevent default touch behavior
  241. }, { passive: false });
  242.  
  243. upBtn.addEventListener('touchend', (e) => {
  244. clearTimeout(upButtonTimer);
  245. if (!isUpButtonLongPress) {
  246. scrollToPreviousQuestion();
  247. }
  248. e.preventDefault();
  249. }, { passive: false });
  250.  
  251. upBtn.addEventListener('touchcancel', () => {
  252. clearTimeout(upButtonTimer);
  253. });
  254.  
  255. // Create the down arrow button
  256. const downBtn = document.createElement('button');
  257. downBtn.type = 'button';
  258. downBtn.className = 'focus-visible:bg-offsetPlus dark:focus-visible:bg-offsetPlusDark hover:bg-offsetPlus text-textOff dark:text-textOffDark hover:text-textMain dark:hover:bg-offsetPlusDark dark:hover:text-textMainDark 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 active:scale-[0.97] active:duration-150 active:ease-outExpo origin-center whitespace-nowrap inline-flex text-sm h-8 aspect-square';
  259. downBtn.style.cssText = `
  260. position: sticky;
  261. top: 95px;
  262. right: 40px;
  263. float: right;
  264. z-index: 100;
  265. margin-right: 5px;
  266. `;
  267.  
  268. downBtn.innerHTML = `
  269. <div class="flex items-center min-w-0 font-medium gap-1.5 justify-center">
  270. <div class="flex shrink-0 items-center justify-center size-4">
  271. <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  272. <path d="M12 5v14M5 12l7 7 7-7"/>
  273. </svg>
  274. </div>
  275. </div>
  276. `;
  277.  
  278. // Add long press functionality to down button
  279. downBtn.addEventListener('mousedown', () => {
  280. isDownButtonLongPress = false;
  281. downButtonTimer = setTimeout(() => {
  282. isDownButtonLongPress = true;
  283. scrollToBottom();
  284. // Visual feedback for long press
  285. downBtn.style.backgroundColor = 'rgba(59, 130, 246, 0.2)'; // Light blue background
  286. setTimeout(() => {
  287. downBtn.style.backgroundColor = '';
  288. }, 500);
  289. }, LONG_PRESS_DURATION);
  290. });
  291.  
  292. downBtn.addEventListener('mouseup', () => {
  293. clearTimeout(downButtonTimer);
  294. if (!isDownButtonLongPress) {
  295. scrollToNextQuestion();
  296. }
  297. });
  298.  
  299. downBtn.addEventListener('mouseleave', () => {
  300. clearTimeout(downButtonTimer);
  301. });
  302.  
  303. downBtn.addEventListener('touchstart', (e) => {
  304. isDownButtonLongPress = false;
  305. downButtonTimer = setTimeout(() => {
  306. isDownButtonLongPress = true;
  307. scrollToBottom();
  308. // Visual feedback for long press
  309. downBtn.style.backgroundColor = 'rgba(59, 130, 246, 0.2)'; // Light blue background
  310. setTimeout(() => {
  311. downBtn.style.backgroundColor = '';
  312. }, 500);
  313. }, LONG_PRESS_DURATION);
  314. e.preventDefault(); // Prevent default touch behavior
  315. }, { passive: false });
  316.  
  317. downBtn.addEventListener('touchend', (e) => {
  318. clearTimeout(downButtonTimer);
  319. if (!isDownButtonLongPress) {
  320. scrollToNextQuestion();
  321. }
  322. e.preventDefault();
  323. }, { passive: false });
  324.  
  325. downBtn.addEventListener('touchcancel', () => {
  326. clearTimeout(downButtonTimer);
  327. });
  328.  
  329. // Insert the buttons at the beginning of the pre element
  330. block.insertBefore(downBtn, block.firstChild);
  331. block.insertBefore(upBtn, block.firstChild);
  332. block.insertBefore(copyBtn, block.firstChild);
  333. });
  334. }
  335.  
  336. // Function to periodically check for new code blocks
  337. function checkForCodeBlocks() {
  338. addFloatingButtons();
  339. }
  340.  
  341. // Initial setup
  342. function init() {
  343. // Set up interval for checking code blocks
  344. setInterval(checkForCodeBlocks, CHECK_INTERVAL);
  345.  
  346. // Initial check for code blocks
  347. setTimeout(checkForCodeBlocks, 1000);
  348. }
  349.  
  350. // Initialize
  351. init();
  352.  
  353. // Listen for URL changes (for single-page apps)
  354. let lastUrl = window.location.href;
  355. new MutationObserver(() => {
  356. if (lastUrl !== window.location.href) {
  357. lastUrl = window.location.href;
  358. setTimeout(() => {
  359. addFloatingButtons();
  360. }, 1000); // Check after URL change
  361. }
  362. }).observe(document, { subtree: true, childList: true });
  363. })();