Greasy Fork 还支持 简体中文。

Perplexity Code Block Copy (AFU IT)

Enhanced code blocks in Perplexity with better selection and copy features for inline code

  1. // ==UserScript==
  2. // @name Perplexity Code Block Copy (AFU IT)
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0
  5. // @description Enhanced code blocks in Perplexity with better selection and copy features for inline code
  6. // @author AFU IT
  7. // @match https://www.perplexity.ai/*
  8. // @license MIT
  9. // @grant none
  10. // @run-at document-end
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. // Custom CSS to maintain grey text color during selection with smaller padding
  17. const customCSS = `
  18. code.enhanced-code::selection {
  19. background-color: rgba(255, 255, 255, 0.3) !important;
  20. color: grey !important;
  21. padding-top: 0.5px;
  22. padding-bottom: 0.5px;
  23. }
  24. code.enhanced-code::-moz-selection {
  25. background-color: rgba(255, 255, 255, 0.3) !important;
  26. color: grey !important;
  27. padding-top: 0.5px;
  28. padding-bottom: 0.5px;
  29. }
  30. `;
  31.  
  32. // Add custom CSS to the document
  33. function addCustomCSS() {
  34. if (!document.getElementById('perplexity-code-enhancer-css')) {
  35. const style = document.createElement('style');
  36. style.id = 'perplexity-code-enhancer-css';
  37. style.textContent = customCSS;
  38. document.head.appendChild(style);
  39. }
  40. }
  41.  
  42. // Function to check if answer is still generating
  43. function isAnswerGenerating() {
  44. // Look for elements that indicate answer generation is in progress
  45. return document.querySelector('.answer-loading, .generating, [data-generating="true"], .typing') !== null;
  46. }
  47.  
  48. // Variables to manage tooltip timer and last copied selection
  49. let tooltipTimer = null;
  50. let lastCopiedSelection = '';
  51. let generatingBlocks = new Set();
  52.  
  53. // Function to apply styles and add copy functionality
  54. function enhanceCodeBlocks() {
  55. const generating = isAnswerGenerating();
  56.  
  57. // Find all code blocks that haven't been enhanced yet
  58. const codeBlocks = document.querySelectorAll('code:not(.enhanced-code):not(.temp-styled):not(.enhanced-code-default)');
  59.  
  60. codeBlocks.forEach(codeBlock => {
  61. // If answer is still generating, mark this block but don't style it yet
  62. if (generating) {
  63. codeBlock.classList.add('temp-styled');
  64. generatingBlocks.add(codeBlock);
  65. addCopyFunctionality(codeBlock);
  66. return;
  67. }
  68.  
  69. // Answer is complete, apply full styling
  70. applyFinalStyling(codeBlock);
  71. });
  72.  
  73. // If generation has completed, process any blocks that were marked during generation
  74. if (!generating && generatingBlocks.size > 0) {
  75. generatingBlocks.forEach(block => {
  76. block.classList.remove('temp-styled');
  77. applyFinalStyling(block);
  78. });
  79. generatingBlocks.clear();
  80. }
  81. }
  82.  
  83. // Function to apply final styling to a code block after generation is complete
  84. function applyFinalStyling(codeBlock) {
  85. // Check if this is an inline code block (within a paragraph)
  86. const isInlineCode = !codeBlock.parentElement.tagName.toLowerCase().includes('pre') &&
  87. codeBlock.textContent.split('\n').length === 1;
  88.  
  89. // Apply styling for inline code blocks
  90. if (isInlineCode) {
  91. codeBlock.style.backgroundColor = '#20b8cb';
  92. codeBlock.style.color = 'black';
  93. codeBlock.classList.add('enhanced-code'); // Add class for selection styling
  94. } else {
  95. // Count the number of lines in the code block for multi-line blocks
  96. const lineCount = (codeBlock.textContent.match(/\n/g) || []).length + 1;
  97.  
  98. // Keep default styling for all code blocks
  99. codeBlock.classList.add('enhanced-code-default');
  100. }
  101.  
  102. // Add copy functionality if not already added
  103. if (!codeBlock.dataset.copyEnabled) {
  104. addCopyFunctionality(codeBlock);
  105. }
  106. }
  107.  
  108. // Function to add copy functionality to a code block
  109. function addCopyFunctionality(codeBlock) {
  110. // Skip if already processed
  111. if (codeBlock.dataset.copyEnabled === 'true') return;
  112.  
  113. // Common styling regardless of line count
  114. codeBlock.style.position = 'relative';
  115.  
  116. // Add a subtle hover effect
  117. codeBlock.addEventListener('mouseover', function() {
  118. this.style.opacity = '0.9';
  119. });
  120.  
  121. codeBlock.addEventListener('mouseout', function() {
  122. this.style.opacity = '1';
  123. });
  124.  
  125. // Add click event to copy code for inline code
  126. codeBlock.addEventListener('click', function(e) {
  127. // Check if this is an inline code and no text is selected
  128. const selection = window.getSelection();
  129. const selectedText = selection.toString();
  130.  
  131. // If this is an inline code and no specific selection, copy the whole inline code
  132. if (this.classList.contains('enhanced-code') && (!selectedText || selectedText.length === 0)) {
  133. const codeText = this.textContent;
  134. navigator.clipboard.writeText(codeText).then(() => {
  135. showCopiedTooltip(this, "Copied!", e.clientX, e.clientY);
  136. });
  137. e.preventDefault(); // Prevent default to avoid text selection
  138. return;
  139. }
  140. });
  141.  
  142. // Add mouseup event to copy selected text
  143. codeBlock.addEventListener('mouseup', function(e) {
  144. // Get selected text
  145. const selection = window.getSelection();
  146. const selectedText = selection.toString();
  147.  
  148. // If text is selected and different from last copied, copy it
  149. if (selectedText && selectedText.length > 0 && selectedText !== lastCopiedSelection) {
  150. lastCopiedSelection = selectedText;
  151. navigator.clipboard.writeText(selectedText).then(() => {
  152. showCopiedTooltip(this, "Selection copied!", e.clientX, e.clientY);
  153. // Clear any existing timer
  154. if (tooltipTimer) {
  155. clearTimeout(tooltipTimer);
  156. }
  157. // Set timer to remove tooltip
  158. tooltipTimer = setTimeout(() => {
  159. const existingTooltip = document.querySelector('.code-copied-tooltip');
  160. if (existingTooltip) {
  161. existingTooltip.style.opacity = '0';
  162. setTimeout(() => {
  163. existingTooltip.remove();
  164. }, 500);
  165. }
  166. tooltipTimer = null;
  167. lastCopiedSelection = '';
  168. }, 1500);
  169. });
  170. }
  171. });
  172.  
  173. // Add double click event to copy entire code
  174. codeBlock.addEventListener('dblclick', function(e) {
  175. e.preventDefault();
  176. const codeText = this.textContent;
  177. navigator.clipboard.writeText(codeText).then(() => {
  178. showCopiedTooltip(this, "All code copied!", e.clientX, e.clientY);
  179. // Clear any existing timer
  180. if (tooltipTimer) {
  181. clearTimeout(tooltipTimer);
  182. }
  183. // Set timer to remove tooltip
  184. tooltipTimer = setTimeout(() => {
  185. const existingTooltip = document.querySelector('.code-copied-tooltip');
  186. if (existingTooltip) {
  187. existingTooltip.style.opacity = '0';
  188. setTimeout(() => {
  189. existingTooltip.remove();
  190. }, 500);
  191. }
  192. tooltipTimer = null;
  193. lastCopiedSelection = '';
  194. }, 1500);
  195. });
  196. });
  197.  
  198. // Mark as processed
  199. codeBlock.dataset.copyEnabled = 'true';
  200. }
  201.  
  202. // Function to show a temporary tooltip close to the mouse cursor
  203. function showCopiedTooltip(element, message, x, y) {
  204. // Remove any existing tooltips
  205. const existingTooltip = document.querySelector('.code-copied-tooltip');
  206. if (existingTooltip) {
  207. existingTooltip.remove();
  208. }
  209.  
  210. // Create tooltip
  211. const tooltip = document.createElement('div');
  212. tooltip.textContent = message || 'Copied!';
  213. tooltip.className = 'code-copied-tooltip';
  214.  
  215. // Style the tooltip - using Perplexity's font family
  216. tooltip.style.position = 'fixed';
  217. tooltip.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
  218. tooltip.style.color = 'white';
  219. tooltip.style.padding = '4px 8px';
  220. tooltip.style.borderRadius = '4px';
  221. tooltip.style.fontSize = '12px'; // Smaller font size
  222. tooltip.style.fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif'; // Perplexity default font
  223. tooltip.style.zIndex = '10000';
  224. tooltip.style.pointerEvents = 'none';
  225.  
  226. // Add to document and get dimensions
  227. document.body.appendChild(tooltip);
  228.  
  229. // Position tooltip very close to the right of the mouse cursor
  230. const offsetX = 20; // pixels to the right (closer now)
  231.  
  232. // Use clientX/Y instead of pageX/Y for better positioning
  233. tooltip.style.left = (x + offsetX) + 'px';
  234.  
  235. // Calculate vertical position to center the tooltip to the mouse
  236. // We need to wait for the tooltip to be in the DOM to get its height
  237. setTimeout(() => {
  238. const tooltipHeight = tooltip.offsetHeight;
  239. tooltip.style.top = (y - (tooltipHeight / 4.5)) + 'px';
  240. }, 0);
  241. }
  242.  
  243. // Add the custom CSS
  244. addCustomCSS();
  245.  
  246. // Run initially
  247. enhanceCodeBlocks();
  248.  
  249. // Set up a MutationObserver to handle dynamically loaded content
  250. const observer = new MutationObserver(function(mutations) {
  251. enhanceCodeBlocks();
  252. });
  253.  
  254. // Start observing the document body for changes
  255. observer.observe(document.body, { childList: true, subtree: true });
  256.  
  257. // Also run periodically to catch when answer generation completes
  258. setInterval(enhanceCodeBlocks, 500);
  259. })();