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

Adds floating copy button and navigation buttons

  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. })();