Greasy Fork is available in English.

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

Adds floating copy button and navigation buttons

От 07.05.2025. Виж последната версия.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

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