您需要先安装一个扩展,例如 篡改猴、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);
- }
- })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址