Perplexity Scroll Buttons (AFU IT)

Jumps between Q&A blocks with visual feedback, context-awareness, and easy configuration.

ของเมื่อวันที่ 08-07-2025 ดู เวอร์ชันล่าสุด

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey, Greasemonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Perplexity Scroll Buttons (AFU IT)
// @namespace    PerplexityTools
// @version      1.2
// @description  Jumps between Q&A blocks with visual feedback, context-awareness, and easy configuration.
// @author       AFU IT
// @match        https://*.perplexity.ai/*
// @license      MIT
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // =================================================================================
    // ---
    // --- CONFIGURATION DASHBOARD ---
    // ---
    // =================================================================================
    const config = {
        // --- Colors ---
        colors: {
            active: '#20b8cd', // Main button color
            holding: '#147a8a', // Color when a button is being held down
            disabled: '#777777', // Color for a disabled button (e.g., at top/bottom of page)
        },

        // --- Timings ---
        holdDuration: 300, // Time in ms to distinguish a "click" from a "hold"
        scrollCheckThrottle: 150, // How often (in ms) to check scroll position for context-awareness

        // --- Positions ---
        positions: {
            down: '120px', // Distance from the bottom for the down-arrow button
            up: '162px', // Distance from the bottom for the up-arrow button
            auto: '204px', // Distance from the bottom for the auto-scroll button
            right: '20px', // Distance from the right for all buttons
        },

        // --- Selectors ---
        selectors: {
            scrollContainer: '.scrollable-container.scrollbar-subtle',
            messageBlock: 'div[data-cplx-component="message-block"]', // The target for jumping
        },
    };
    // =================================================================================


    // --- Global State ---
    let autoScrollInterval = null;
    let isAutoScrollEnabled = true;
    let pressTimer = null;


    // --- Core Functions ---

    /**
     * Finds the next/previous message block and scrolls to it.
     * @param {string} direction - 'up' or 'down'.
     * @returns {boolean} - True if a target was found, otherwise false.
     */
    function scrollToBlock(direction) {
        const scrollContainer = document.querySelector(config.selectors.scrollContainer);
        if (!scrollContainer) return false;

        const blocks = Array.from(document.querySelectorAll(config.selectors.messageBlock));
        if (blocks.length === 0) return false;

        const currentScrollTop = scrollContainer.scrollTop;
        let targetBlock = null;

        if (direction === 'down') {
            targetBlock = blocks.find(block => block.offsetTop > currentScrollTop + 10);
        } else {
            targetBlock = blocks.slice().reverse().find(block => block.offsetTop < currentScrollTop - 10);
        }

        if (targetBlock) {
            scrollContainer.scrollTo({ top: targetBlock.offsetTop, behavior: 'smooth' });
            return true;
        }
        return false;
    }

    /**
     * Checks scroll position and enables/disables buttons accordingly.
     */
    function updateButtonStates() {
        const sc = document.querySelector(config.selectors.scrollContainer);
        const topBtn = document.getElementById('scroll-top-btn');
        const bottomBtn = document.getElementById('scroll-bottom-btn');

        if (!sc || !topBtn || !bottomBtn) return;

        const atTop = sc.scrollTop < 10;
        const atBottom = sc.scrollHeight - sc.scrollTop - sc.clientHeight < 10;

        // --- Update Top Button ---
        if (atTop) {
            topBtn.style.backgroundColor = config.colors.disabled;
            topBtn.style.opacity = '0.5';
            topBtn.style.pointerEvents = 'none';
        } else {
            topBtn.style.backgroundColor = config.colors.active;
            topBtn.style.opacity = '1';
            topBtn.style.pointerEvents = 'auto';
        }

        // --- Update Bottom Button ---
        if (atBottom) {
            bottomBtn.style.backgroundColor = config.colors.disabled;
            bottomBtn.style.opacity = '0.5';
            bottomBtn.style.pointerEvents = 'none';
        } else {
            bottomBtn.style.backgroundColor = config.colors.active;
            bottomBtn.style.opacity = '1';
            bottomBtn.style.pointerEvents = 'auto';
        }
    }

    /**
     * Utility to limit how often a function can run.
     */
    function throttle(func, limit) {
        let inThrottle;
        return function() {
            const args = arguments;
            const context = this;
            if (!inThrottle) {
                func.apply(context, args);
                inThrottle = true;
                setTimeout(() => inThrottle = false, limit);
            }
        };
    }


    /**
     * Creates and adds all the buttons to the page.
     */
    function addScrollButtons() {
        document.getElementById('scroll-bottom-btn')?.remove();
        document.getElementById('scroll-top-btn')?.remove();
        document.getElementById('auto-scroll-btn')?.remove();

        const commonStyle = `position: fixed; right: ${config.positions.right}; width: 32px; height: 32px; color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; z-index: 99999; box-shadow: 0 2px 5px rgba(0,0,0,0.2); transition: all 0.2s; user-select: none;`;

        // --- Bottom "Down" Button ---
        const bottomButton = document.createElement('div');
        bottomButton.id = 'scroll-bottom-btn';
        bottomButton.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M19 12l-7 7-7-7"></path></svg>';
        bottomButton.title = 'Click: Next Question | Hold: Scroll to Bottom';
        bottomButton.style.cssText = `${commonStyle} bottom: ${config.positions.down}; background: ${config.colors.active};`;

        bottomButton.addEventListener('mousedown', function() {
            this.style.backgroundColor = config.colors.holding; // Visual feedback for hold
            pressTimer = setTimeout(() => {
                const sc = document.querySelector(config.selectors.scrollContainer);
                if (sc) sc.scrollTo({ top: sc.scrollHeight, behavior: 'smooth' });
                pressTimer = null;
            }, config.holdDuration);
        });
        bottomButton.addEventListener('mouseup', function() {
            this.style.backgroundColor = config.colors.active;
            if (pressTimer) {
                clearTimeout(pressTimer);
                if (!scrollToBlock('down')) { // Fallback if no block found
                    const sc = document.querySelector(config.selectors.scrollContainer);
                    if (sc) sc.scrollTo({ top: sc.scrollHeight, behavior: 'smooth' });
                }
            }
        });
        bottomButton.addEventListener('mouseleave', function() { this.style.backgroundColor = config.colors.active; clearTimeout(pressTimer); });

        // --- Top "Up" Button ---
        const topButton = document.createElement('div');
        topButton.id = 'scroll-top-btn';
        topButton.innerHTML = '<svg width="14" height="14" 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"></path></svg>';
        topButton.title = 'Click: Previous Question | Hold: Scroll to Top';
        topButton.style.cssText = `${commonStyle} bottom: ${config.positions.up}; background: ${config.colors.active};`;

        topButton.addEventListener('mousedown', function() {
            this.style.backgroundColor = config.colors.holding;
            pressTimer = setTimeout(() => {
                const sc = document.querySelector(config.selectors.scrollContainer);
                if (sc) sc.scrollTo({ top: 0, behavior: 'smooth' });
                pressTimer = null;
            }, config.holdDuration);
        });
        topButton.addEventListener('mouseup', function() {
            this.style.backgroundColor = config.colors.active;
            if (pressTimer) {
                clearTimeout(pressTimer);
                if (!scrollToBlock('up')) { // Fallback if no block found
                    const sc = document.querySelector(config.selectors.scrollContainer);
                    if (sc) sc.scrollTo({ top: 0, behavior: 'smooth' });
                }
            }
        });
        topButton.addEventListener('mouseleave', function() { this.style.backgroundColor = config.colors.active; clearTimeout(pressTimer); });

        // --- Auto-Scroll Toggle Button ---
        const autoButton = document.createElement('div');
        autoButton.id = 'auto-scroll-btn';
        autoButton.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="3" width="12" height="18" rx="6" ry="6"></rect><line x1="12" y1="7" x2="12" y2="11"></line></svg>';
        autoButton.title = 'Toggle auto-scroll';
        autoButton.style.cssText = `${commonStyle} bottom: ${config.positions.auto}; background: ${isAutoScrollEnabled ? config.colors.active : config.colors.disabled};`;
        autoButton.addEventListener('click', function() {
            toggleAutoScroll();
            this.style.backgroundColor = isAutoScrollEnabled ? config.colors.active : config.colors.disabled;
        });

        document.body.appendChild(bottomButton);
        document.body.appendChild(topButton);
        document.body.appendChild(autoButton);

        // Set the initial state of the buttons
        updateButtonStates();
    }

    // --- Auto-Scroll & Initialization ---
    function isGenerating() { return !!document.querySelector('button[aria-label="Stop generating response"]'); }
    function autoScrollToBottom() {
        const sc = document.querySelector(config.selectors.scrollContainer);
        if (sc) sc.scrollTo({ top: sc.scrollHeight, behavior: 'smooth' });
    }
    function toggleAutoScroll() {
        isAutoScrollEnabled = !isAutoScrollEnabled;
        isAutoScrollEnabled ? startAutoScroll() : stopAutoScroll();
    }
    function startAutoScroll() {
        if (!autoScrollInterval) autoScrollInterval = setInterval(() => { if (isGenerating()) autoScrollToBottom(); }, 300);
    }
    function stopAutoScroll() {
        if (autoScrollInterval) { clearInterval(autoScrollInterval); autoScrollInterval = null; }
    }
    function initialize() {
        addScrollButtons();
        if (isAutoScrollEnabled) startAutoScroll();

        // Add context-aware scroll listener
        const scrollContainer = document.querySelector(config.selectors.scrollContainer);
        if (scrollContainer) {
            scrollContainer.addEventListener('scroll', throttle(updateButtonStates, config.scrollCheckThrottle));
        }

        const observer = new MutationObserver(() => {
            if (!document.getElementById('auto-scroll-btn')) {
                setTimeout(() => {
                    initialize(); // Re-run the whole setup if buttons disappear
                }, 1000);
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }

    // --- Start ---
    if (document.readyState === 'complete') {
        initialize();
    } else {
        window.addEventListener('load', initialize);
    }
})();