您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Jumps between Q&A blocks with visual feedback, context-awareness, and easy configuration.
当前为
// ==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); } })();