您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds floating copy button and navigation buttons
- // ==UserScript==
- // @name PerplexityTools - Floating Copy Code & Navigation Buttons (AFU IT)
- // @namespace http://tampermonkey.net/
- // @version 1.0
- // @description Adds floating copy button and navigation buttons
- // @author AFU IT
- // @match https://www.perplexity.ai/*
- // @license MIT
- // @grant none
- // ==/UserScript==
- (function() {
- 'use strict';
- const CHECK_INTERVAL = 2000; // Check every 2 seconds
- const LONG_PRESS_DURATION = 1000; // 1 second for long press
- const originalFetch = window.fetch;
- // Variables to track long press
- let upButtonTimer = null;
- let downButtonTimer = null;
- let isUpButtonLongPress = false;
- let isDownButtonLongPress = false;
- // Helper function to scroll to the previous question
- function scrollToPreviousQuestion() {
- if (isUpButtonLongPress) return; // Skip if this is triggered by a long press
- const queryBlocks = Array.from(document.querySelectorAll('.group.relative.mb-1.flex.items-end.gap-0\\.5'));
- if (!queryBlocks.length) return;
- // Get all blocks positions
- const positions = queryBlocks.map(block => {
- const rect = block.getBoundingClientRect();
- return {
- element: block,
- top: rect.top,
- bottom: rect.bottom
- };
- });
- // Sort by vertical position
- positions.sort((a, b) => a.top - b.top);
- // Find the first block above the middle of the viewport
- const viewportMiddle = window.innerHeight / 2;
- let targetBlock = null;
- for (let i = positions.length - 1; i >= 0; i--) {
- if (positions[i].top < viewportMiddle) {
- if (i > 0) {
- targetBlock = positions[i - 1].element;
- } else {
- // If we're at the first question, scroll to top
- window.scrollTo({ top: 0, behavior: 'smooth' });
- return;
- }
- break;
- }
- }
- // If we found a target block, scroll to it at the top of the viewport
- if (targetBlock) {
- targetBlock.scrollIntoView({ behavior: 'smooth', block: "start" });
- } else if (positions.length > 0) {
- // If no suitable block found, go to the first one
- positions[0].element.scrollIntoView({ behavior: 'smooth', block: "start" });
- }
- }
- // Helper function to scroll to the next question
- function scrollToNextQuestion() {
- if (isDownButtonLongPress) return; // Skip if this is triggered by a long press
- const queryBlocks = Array.from(document.querySelectorAll('.group.relative.mb-1.flex.items-end.gap-0\\.5'));
- if (!queryBlocks.length) return;
- // Get all blocks positions
- const positions = queryBlocks.map(block => {
- const rect = block.getBoundingClientRect();
- return {
- element: block,
- top: rect.top,
- bottom: rect.bottom
- };
- });
- // Sort by vertical position
- positions.sort((a, b) => a.top - b.top);
- // Find the first block below the middle of the viewport
- const viewportMiddle = window.innerHeight / 2;
- let targetBlock = null;
- for (let i = 0; i < positions.length; i++) {
- if (positions[i].top > viewportMiddle) {
- targetBlock = positions[i].element;
- break;
- }
- }
- // If we found a target block, scroll to it at the top of the viewport
- if (targetBlock) {
- targetBlock.scrollIntoView({ behavior: 'smooth', block: "start" });
- } else if (positions.length > 0) {
- // If no suitable block found, try to find the Related section
- const relatedSection = document.querySelector('.default.font-display.text-lg.font-medium:has(.fa-new-thread)');
- if (relatedSection) {
- relatedSection.scrollIntoView({ behavior: 'smooth', block: "start" });
- } else {
- // Or go to the bottom
- window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
- }
- }
- }
- // Helper function to scroll to the top of the page
- function scrollToTop() {
- window.scrollTo({ top: 0, behavior: 'smooth' });
- }
- // Helper function to scroll to the bottom of the page
- function scrollToBottom() {
- window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
- }
- // Floating buttons functionality
- function addFloatingButtons() {
- // Find all pre elements that don't already have our buttons
- const codeBlocks = document.querySelectorAll('pre:not(.buttons-added)');
- codeBlocks.forEach(block => {
- // Mark this block as processed
- block.classList.add('buttons-added');
- // Create the copy button with Perplexity's styling
- const copyBtn = document.createElement('button');
- copyBtn.type = 'button';
- 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';
- copyBtn.style.cssText = `
- position: sticky;
- top: 95px;
- right: 40px;
- float: right;
- z-index: 100;
- margin-right: 5px;
- `;
- copyBtn.innerHTML = `
- <div class="flex items-center min-w-0 font-medium gap-1.5 justify-center">
- <div class="flex shrink-0 items-center justify-center size-4">
- <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">
- <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>
- </svg>
- </div>
- </div>
- `;
- copyBtn.addEventListener('click', () => {
- const code = block.querySelector('code').innerText;
- navigator.clipboard.writeText(code);
- // Visual feedback
- const originalHTML = copyBtn.innerHTML;
- copyBtn.innerHTML = `
- <div class="flex items-center min-w-0 font-medium gap-1.5 justify-center">
- <div class="flex shrink-0 items-center justify-center size-4">
- <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">
- <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>
- </svg>
- </div>
- </div>
- `;
- setTimeout(() => {
- copyBtn.innerHTML = originalHTML;
- }, 2000);
- });
- // Create the up arrow button
- const upBtn = document.createElement('button');
- upBtn.type = 'button';
- 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';
- upBtn.style.cssText = `
- position: sticky;
- top: 95px;
- right: 40px;
- float: right;
- z-index: 100;
- margin-right: 5px;
- `;
- upBtn.innerHTML = `
- <div class="flex items-center min-w-0 font-medium gap-1.5 justify-center">
- <div class="flex shrink-0 items-center justify-center size-4">
- <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">
- <path d="M12 19V5M5 12l7-7 7 7"/>
- </svg>
- </div>
- </div>
- `;
- // Add long press functionality to up button
- upBtn.addEventListener('mousedown', () => {
- isUpButtonLongPress = false;
- upButtonTimer = setTimeout(() => {
- isUpButtonLongPress = true;
- scrollToTop();
- // Visual feedback for long press
- upBtn.style.backgroundColor = 'rgba(59, 130, 246, 0.2)'; // Light blue background
- setTimeout(() => {
- upBtn.style.backgroundColor = '';
- }, 500);
- }, LONG_PRESS_DURATION);
- });
- upBtn.addEventListener('mouseup', () => {
- clearTimeout(upButtonTimer);
- if (!isUpButtonLongPress) {
- scrollToPreviousQuestion();
- }
- });
- upBtn.addEventListener('mouseleave', () => {
- clearTimeout(upButtonTimer);
- });
- upBtn.addEventListener('touchstart', (e) => {
- isUpButtonLongPress = false;
- upButtonTimer = setTimeout(() => {
- isUpButtonLongPress = true;
- scrollToTop();
- // Visual feedback for long press
- upBtn.style.backgroundColor = 'rgba(59, 130, 246, 0.2)'; // Light blue background
- setTimeout(() => {
- upBtn.style.backgroundColor = '';
- }, 500);
- }, LONG_PRESS_DURATION);
- e.preventDefault(); // Prevent default touch behavior
- }, { passive: false });
- upBtn.addEventListener('touchend', (e) => {
- clearTimeout(upButtonTimer);
- if (!isUpButtonLongPress) {
- scrollToPreviousQuestion();
- }
- e.preventDefault();
- }, { passive: false });
- upBtn.addEventListener('touchcancel', () => {
- clearTimeout(upButtonTimer);
- });
- // Create the down arrow button
- const downBtn = document.createElement('button');
- downBtn.type = 'button';
- 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';
- downBtn.style.cssText = `
- position: sticky;
- top: 95px;
- right: 40px;
- float: right;
- z-index: 100;
- margin-right: 5px;
- `;
- downBtn.innerHTML = `
- <div class="flex items-center min-w-0 font-medium gap-1.5 justify-center">
- <div class="flex shrink-0 items-center justify-center size-4">
- <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">
- <path d="M12 5v14M5 12l7 7 7-7"/>
- </svg>
- </div>
- </div>
- `;
- // Add long press functionality to down button
- downBtn.addEventListener('mousedown', () => {
- isDownButtonLongPress = false;
- downButtonTimer = setTimeout(() => {
- isDownButtonLongPress = true;
- scrollToBottom();
- // Visual feedback for long press
- downBtn.style.backgroundColor = 'rgba(59, 130, 246, 0.2)'; // Light blue background
- setTimeout(() => {
- downBtn.style.backgroundColor = '';
- }, 500);
- }, LONG_PRESS_DURATION);
- });
- downBtn.addEventListener('mouseup', () => {
- clearTimeout(downButtonTimer);
- if (!isDownButtonLongPress) {
- scrollToNextQuestion();
- }
- });
- downBtn.addEventListener('mouseleave', () => {
- clearTimeout(downButtonTimer);
- });
- downBtn.addEventListener('touchstart', (e) => {
- isDownButtonLongPress = false;
- downButtonTimer = setTimeout(() => {
- isDownButtonLongPress = true;
- scrollToBottom();
- // Visual feedback for long press
- downBtn.style.backgroundColor = 'rgba(59, 130, 246, 0.2)'; // Light blue background
- setTimeout(() => {
- downBtn.style.backgroundColor = '';
- }, 500);
- }, LONG_PRESS_DURATION);
- e.preventDefault(); // Prevent default touch behavior
- }, { passive: false });
- downBtn.addEventListener('touchend', (e) => {
- clearTimeout(downButtonTimer);
- if (!isDownButtonLongPress) {
- scrollToNextQuestion();
- }
- e.preventDefault();
- }, { passive: false });
- downBtn.addEventListener('touchcancel', () => {
- clearTimeout(downButtonTimer);
- });
- // Insert the buttons at the beginning of the pre element
- block.insertBefore(downBtn, block.firstChild);
- block.insertBefore(upBtn, block.firstChild);
- block.insertBefore(copyBtn, block.firstChild);
- });
- }
- // Function to periodically check for new code blocks
- function checkForCodeBlocks() {
- addFloatingButtons();
- }
- // Initial setup
- function init() {
- // Set up interval for checking code blocks
- setInterval(checkForCodeBlocks, CHECK_INTERVAL);
- // Initial check for code blocks
- setTimeout(checkForCodeBlocks, 1000);
- }
- // Initialize
- init();
- // Listen for URL changes (for single-page apps)
- let lastUrl = window.location.href;
- new MutationObserver(() => {
- if (lastUrl !== window.location.href) {
- lastUrl = window.location.href;
- setTimeout(() => {
- addFloatingButtons();
- }, 1000); // Check after URL change
- }
- }).observe(document, { subtree: true, childList: true });
- })();