// ==UserScript==
// @name Perplexity Code Block Copy (AFU IT)
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Enhanced code blocks in Perplexity with better selection and copy features for inline code
// @author AFU IT
// @match https://www.perplexity.ai/*
// @license MIT
// @grant none
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
// Custom CSS to maintain grey text color during selection with smaller padding
const customCSS = `
code.enhanced-code::selection {
background-color: rgba(255, 255, 255, 0.3) !important;
color: grey !important;
padding-top: 0.5px;
padding-bottom: 0.5px;
}
code.enhanced-code::-moz-selection {
background-color: rgba(255, 255, 255, 0.3) !important;
color: grey !important;
padding-top: 0.5px;
padding-bottom: 0.5px;
}
`;
// Add custom CSS to the document
function addCustomCSS() {
if (!document.getElementById('perplexity-code-enhancer-css')) {
const style = document.createElement('style');
style.id = 'perplexity-code-enhancer-css';
style.textContent = customCSS;
document.head.appendChild(style);
}
}
// Function to check if answer is still generating
function isAnswerGenerating() {
// Look for elements that indicate answer generation is in progress
return document.querySelector('.answer-loading, .generating, [data-generating="true"], .typing') !== null;
}
// Variables to manage tooltip timer and last copied selection
let tooltipTimer = null;
let lastCopiedSelection = '';
let generatingBlocks = new Set();
// Function to apply styles and add copy functionality
function enhanceCodeBlocks() {
const generating = isAnswerGenerating();
// Find all code blocks that haven't been enhanced yet
const codeBlocks = document.querySelectorAll('code:not(.enhanced-code):not(.temp-styled):not(.enhanced-code-default)');
codeBlocks.forEach(codeBlock => {
// If answer is still generating, mark this block but don't style it yet
if (generating) {
codeBlock.classList.add('temp-styled');
generatingBlocks.add(codeBlock);
addCopyFunctionality(codeBlock);
return;
}
// Answer is complete, apply full styling
applyFinalStyling(codeBlock);
});
// If generation has completed, process any blocks that were marked during generation
if (!generating && generatingBlocks.size > 0) {
generatingBlocks.forEach(block => {
block.classList.remove('temp-styled');
applyFinalStyling(block);
});
generatingBlocks.clear();
}
}
// Function to apply final styling to a code block after generation is complete
function applyFinalStyling(codeBlock) {
// Check if this is an inline code block (within a paragraph)
const isInlineCode = !codeBlock.parentElement.tagName.toLowerCase().includes('pre') &&
codeBlock.textContent.split('\n').length === 1;
// Apply styling for inline code blocks
if (isInlineCode) {
codeBlock.style.backgroundColor = '#20b8cb';
codeBlock.style.color = 'black';
codeBlock.classList.add('enhanced-code'); // Add class for selection styling
} else {
// Count the number of lines in the code block for multi-line blocks
const lineCount = (codeBlock.textContent.match(/\n/g) || []).length + 1;
// Keep default styling for all code blocks
codeBlock.classList.add('enhanced-code-default');
}
// Add copy functionality if not already added
if (!codeBlock.dataset.copyEnabled) {
addCopyFunctionality(codeBlock);
}
}
// Function to add copy functionality to a code block
function addCopyFunctionality(codeBlock) {
// Skip if already processed
if (codeBlock.dataset.copyEnabled === 'true') return;
// Common styling regardless of line count
codeBlock.style.position = 'relative';
// Add a subtle hover effect
codeBlock.addEventListener('mouseover', function() {
this.style.opacity = '0.9';
});
codeBlock.addEventListener('mouseout', function() {
this.style.opacity = '1';
});
// Add click event to copy code for inline code
codeBlock.addEventListener('click', function(e) {
// Check if this is an inline code and no text is selected
const selection = window.getSelection();
const selectedText = selection.toString();
// If this is an inline code and no specific selection, copy the whole inline code
if (this.classList.contains('enhanced-code') && (!selectedText || selectedText.length === 0)) {
const codeText = this.textContent;
navigator.clipboard.writeText(codeText).then(() => {
showCopiedTooltip(this, "Copied!", e.clientX, e.clientY);
});
e.preventDefault(); // Prevent default to avoid text selection
return;
}
});
// Add mouseup event to copy selected text
codeBlock.addEventListener('mouseup', function(e) {
// Get selected text
const selection = window.getSelection();
const selectedText = selection.toString();
// If text is selected and different from last copied, copy it
if (selectedText && selectedText.length > 0 && selectedText !== lastCopiedSelection) {
lastCopiedSelection = selectedText;
navigator.clipboard.writeText(selectedText).then(() => {
showCopiedTooltip(this, "Selection copied!", e.clientX, e.clientY);
// Clear any existing timer
if (tooltipTimer) {
clearTimeout(tooltipTimer);
}
// Set timer to remove tooltip
tooltipTimer = setTimeout(() => {
const existingTooltip = document.querySelector('.code-copied-tooltip');
if (existingTooltip) {
existingTooltip.style.opacity = '0';
setTimeout(() => {
existingTooltip.remove();
}, 500);
}
tooltipTimer = null;
lastCopiedSelection = '';
}, 1500);
});
}
});
// Add double click event to copy entire code
codeBlock.addEventListener('dblclick', function(e) {
e.preventDefault();
const codeText = this.textContent;
navigator.clipboard.writeText(codeText).then(() => {
showCopiedTooltip(this, "All code copied!", e.clientX, e.clientY);
// Clear any existing timer
if (tooltipTimer) {
clearTimeout(tooltipTimer);
}
// Set timer to remove tooltip
tooltipTimer = setTimeout(() => {
const existingTooltip = document.querySelector('.code-copied-tooltip');
if (existingTooltip) {
existingTooltip.style.opacity = '0';
setTimeout(() => {
existingTooltip.remove();
}, 500);
}
tooltipTimer = null;
lastCopiedSelection = '';
}, 1500);
});
});
// Mark as processed
codeBlock.dataset.copyEnabled = 'true';
}
// Function to show a temporary tooltip close to the mouse cursor
function showCopiedTooltip(element, message, x, y) {
// Remove any existing tooltips
const existingTooltip = document.querySelector('.code-copied-tooltip');
if (existingTooltip) {
existingTooltip.remove();
}
// Create tooltip
const tooltip = document.createElement('div');
tooltip.textContent = message || 'Copied!';
tooltip.className = 'code-copied-tooltip';
// Style the tooltip - using Perplexity's font family
tooltip.style.position = 'fixed';
tooltip.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
tooltip.style.color = 'white';
tooltip.style.padding = '4px 8px';
tooltip.style.borderRadius = '4px';
tooltip.style.fontSize = '12px'; // Smaller font size
tooltip.style.fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif'; // Perplexity default font
tooltip.style.zIndex = '10000';
tooltip.style.pointerEvents = 'none';
// Add to document and get dimensions
document.body.appendChild(tooltip);
// Position tooltip very close to the right of the mouse cursor
const offsetX = 20; // pixels to the right (closer now)
// Use clientX/Y instead of pageX/Y for better positioning
tooltip.style.left = (x + offsetX) + 'px';
// Calculate vertical position to center the tooltip to the mouse
// We need to wait for the tooltip to be in the DOM to get its height
setTimeout(() => {
const tooltipHeight = tooltip.offsetHeight;
tooltip.style.top = (y - (tooltipHeight / 4.5)) + 'px';
}, 0);
}
// Add the custom CSS
addCustomCSS();
// Run initially
enhanceCodeBlocks();
// Set up a MutationObserver to handle dynamically loaded content
const observer = new MutationObserver(function(mutations) {
enhanceCodeBlocks();
});
// Start observing the document body for changes
observer.observe(document.body, { childList: true, subtree: true });
// Also run periodically to catch when answer generation completes
setInterval(enhanceCodeBlocks, 500);
})();