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

Adds floating copy button and navigation buttons

Versão de: 07/05/2025. Veja: a última versão.

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