Perplexity Scroll Buttons (AFU IT)

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

  1. // ==UserScript==
  2. // @name Perplexity Scroll Buttons (AFU IT)
  3. // @namespace PerplexityTools
  4. // @version 1.2
  5. // @description Jumps between Q&A blocks with visual feedback, context-awareness, and easy configuration.
  6. // @author AFU IT
  7. // @match https://*.perplexity.ai/*
  8. // @license MIT
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14.  
  15. // =================================================================================
  16. // ---
  17. // --- CONFIGURATION DASHBOARD ---
  18. // ---
  19. // =================================================================================
  20. const config = {
  21. // --- Colors ---
  22. colors: {
  23. active: '#20b8cd', // Main button color
  24. holding: '#147a8a', // Color when a button is being held down
  25. disabled: '#777777', // Color for a disabled button (e.g., at top/bottom of page)
  26. },
  27.  
  28. // --- Timings ---
  29. holdDuration: 300, // Time in ms to distinguish a "click" from a "hold"
  30. scrollCheckThrottle: 150, // How often (in ms) to check scroll position for context-awareness
  31.  
  32. // --- Positions ---
  33. positions: {
  34. down: '120px', // Distance from the bottom for the down-arrow button
  35. up: '162px', // Distance from the bottom for the up-arrow button
  36. auto: '204px', // Distance from the bottom for the auto-scroll button
  37. right: '20px', // Distance from the right for all buttons
  38. },
  39.  
  40. // --- Selectors ---
  41. selectors: {
  42. scrollContainer: '.scrollable-container.scrollbar-subtle',
  43. messageBlock: 'div[data-cplx-component="message-block"]', // The target for jumping
  44. },
  45. };
  46. // =================================================================================
  47.  
  48.  
  49. // --- Global State ---
  50. let autoScrollInterval = null;
  51. let isAutoScrollEnabled = true;
  52. let pressTimer = null;
  53.  
  54.  
  55. // --- Core Functions ---
  56.  
  57. /**
  58. * Finds the next/previous message block and scrolls to it.
  59. * @param {string} direction - 'up' or 'down'.
  60. * @returns {boolean} - True if a target was found, otherwise false.
  61. */
  62. function scrollToBlock(direction) {
  63. const scrollContainer = document.querySelector(config.selectors.scrollContainer);
  64. if (!scrollContainer) return false;
  65.  
  66. const blocks = Array.from(document.querySelectorAll(config.selectors.messageBlock));
  67. if (blocks.length === 0) return false;
  68.  
  69. const currentScrollTop = scrollContainer.scrollTop;
  70. let targetBlock = null;
  71.  
  72. if (direction === 'down') {
  73. targetBlock = blocks.find(block => block.offsetTop > currentScrollTop + 10);
  74. } else {
  75. targetBlock = blocks.slice().reverse().find(block => block.offsetTop < currentScrollTop - 10);
  76. }
  77.  
  78. if (targetBlock) {
  79. scrollContainer.scrollTo({ top: targetBlock.offsetTop, behavior: 'smooth' });
  80. return true;
  81. }
  82. return false;
  83. }
  84.  
  85. /**
  86. * Checks scroll position and enables/disables buttons accordingly.
  87. */
  88. function updateButtonStates() {
  89. const sc = document.querySelector(config.selectors.scrollContainer);
  90. const topBtn = document.getElementById('scroll-top-btn');
  91. const bottomBtn = document.getElementById('scroll-bottom-btn');
  92.  
  93. if (!sc || !topBtn || !bottomBtn) return;
  94.  
  95. const atTop = sc.scrollTop < 10;
  96. const atBottom = sc.scrollHeight - sc.scrollTop - sc.clientHeight < 10;
  97.  
  98. // --- Update Top Button ---
  99. if (atTop) {
  100. topBtn.style.backgroundColor = config.colors.disabled;
  101. topBtn.style.opacity = '0.5';
  102. topBtn.style.pointerEvents = 'none';
  103. } else {
  104. topBtn.style.backgroundColor = config.colors.active;
  105. topBtn.style.opacity = '1';
  106. topBtn.style.pointerEvents = 'auto';
  107. }
  108.  
  109. // --- Update Bottom Button ---
  110. if (atBottom) {
  111. bottomBtn.style.backgroundColor = config.colors.disabled;
  112. bottomBtn.style.opacity = '0.5';
  113. bottomBtn.style.pointerEvents = 'none';
  114. } else {
  115. bottomBtn.style.backgroundColor = config.colors.active;
  116. bottomBtn.style.opacity = '1';
  117. bottomBtn.style.pointerEvents = 'auto';
  118. }
  119. }
  120.  
  121. /**
  122. * Utility to limit how often a function can run.
  123. */
  124. function throttle(func, limit) {
  125. let inThrottle;
  126. return function() {
  127. const args = arguments;
  128. const context = this;
  129. if (!inThrottle) {
  130. func.apply(context, args);
  131. inThrottle = true;
  132. setTimeout(() => inThrottle = false, limit);
  133. }
  134. };
  135. }
  136.  
  137.  
  138. /**
  139. * Creates and adds all the buttons to the page.
  140. */
  141. function addScrollButtons() {
  142. document.getElementById('scroll-bottom-btn')?.remove();
  143. document.getElementById('scroll-top-btn')?.remove();
  144. document.getElementById('auto-scroll-btn')?.remove();
  145.  
  146. 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;`;
  147.  
  148. // --- Bottom "Down" Button ---
  149. const bottomButton = document.createElement('div');
  150. bottomButton.id = 'scroll-bottom-btn';
  151. 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>';
  152. bottomButton.title = 'Click: Next Question | Hold: Scroll to Bottom';
  153. bottomButton.style.cssText = `${commonStyle} bottom: ${config.positions.down}; background: ${config.colors.active};`;
  154.  
  155. bottomButton.addEventListener('mousedown', function() {
  156. this.style.backgroundColor = config.colors.holding; // Visual feedback for hold
  157. pressTimer = setTimeout(() => {
  158. const sc = document.querySelector(config.selectors.scrollContainer);
  159. if (sc) sc.scrollTo({ top: sc.scrollHeight, behavior: 'smooth' });
  160. pressTimer = null;
  161. }, config.holdDuration);
  162. });
  163. bottomButton.addEventListener('mouseup', function() {
  164. this.style.backgroundColor = config.colors.active;
  165. if (pressTimer) {
  166. clearTimeout(pressTimer);
  167. if (!scrollToBlock('down')) { // Fallback if no block found
  168. const sc = document.querySelector(config.selectors.scrollContainer);
  169. if (sc) sc.scrollTo({ top: sc.scrollHeight, behavior: 'smooth' });
  170. }
  171. }
  172. });
  173. bottomButton.addEventListener('mouseleave', function() { this.style.backgroundColor = config.colors.active; clearTimeout(pressTimer); });
  174.  
  175. // --- Top "Up" Button ---
  176. const topButton = document.createElement('div');
  177. topButton.id = 'scroll-top-btn';
  178. 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>';
  179. topButton.title = 'Click: Previous Question | Hold: Scroll to Top';
  180. topButton.style.cssText = `${commonStyle} bottom: ${config.positions.up}; background: ${config.colors.active};`;
  181.  
  182. topButton.addEventListener('mousedown', function() {
  183. this.style.backgroundColor = config.colors.holding;
  184. pressTimer = setTimeout(() => {
  185. const sc = document.querySelector(config.selectors.scrollContainer);
  186. if (sc) sc.scrollTo({ top: 0, behavior: 'smooth' });
  187. pressTimer = null;
  188. }, config.holdDuration);
  189. });
  190. topButton.addEventListener('mouseup', function() {
  191. this.style.backgroundColor = config.colors.active;
  192. if (pressTimer) {
  193. clearTimeout(pressTimer);
  194. if (!scrollToBlock('up')) { // Fallback if no block found
  195. const sc = document.querySelector(config.selectors.scrollContainer);
  196. if (sc) sc.scrollTo({ top: 0, behavior: 'smooth' });
  197. }
  198. }
  199. });
  200. topButton.addEventListener('mouseleave', function() { this.style.backgroundColor = config.colors.active; clearTimeout(pressTimer); });
  201.  
  202. // --- Auto-Scroll Toggle Button ---
  203. const autoButton = document.createElement('div');
  204. autoButton.id = 'auto-scroll-btn';
  205. 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>';
  206. autoButton.title = 'Toggle auto-scroll';
  207. autoButton.style.cssText = `${commonStyle} bottom: ${config.positions.auto}; background: ${isAutoScrollEnabled ? config.colors.active : config.colors.disabled};`;
  208. autoButton.addEventListener('click', function() {
  209. toggleAutoScroll();
  210. this.style.backgroundColor = isAutoScrollEnabled ? config.colors.active : config.colors.disabled;
  211. });
  212.  
  213. document.body.appendChild(bottomButton);
  214. document.body.appendChild(topButton);
  215. document.body.appendChild(autoButton);
  216.  
  217. // Set the initial state of the buttons
  218. updateButtonStates();
  219. }
  220.  
  221. // --- Auto-Scroll & Initialization ---
  222. function isGenerating() { return !!document.querySelector('button[aria-label="Stop generating response"]'); }
  223. function autoScrollToBottom() {
  224. const sc = document.querySelector(config.selectors.scrollContainer);
  225. if (sc) sc.scrollTo({ top: sc.scrollHeight, behavior: 'smooth' });
  226. }
  227. function toggleAutoScroll() {
  228. isAutoScrollEnabled = !isAutoScrollEnabled;
  229. isAutoScrollEnabled ? startAutoScroll() : stopAutoScroll();
  230. }
  231. function startAutoScroll() {
  232. if (!autoScrollInterval) autoScrollInterval = setInterval(() => { if (isGenerating()) autoScrollToBottom(); }, 300);
  233. }
  234. function stopAutoScroll() {
  235. if (autoScrollInterval) { clearInterval(autoScrollInterval); autoScrollInterval = null; }
  236. }
  237. function initialize() {
  238. addScrollButtons();
  239. if (isAutoScrollEnabled) startAutoScroll();
  240.  
  241. // Add context-aware scroll listener
  242. const scrollContainer = document.querySelector(config.selectors.scrollContainer);
  243. if (scrollContainer) {
  244. scrollContainer.addEventListener('scroll', throttle(updateButtonStates, config.scrollCheckThrottle));
  245. }
  246.  
  247. const observer = new MutationObserver(() => {
  248. if (!document.getElementById('auto-scroll-btn')) {
  249. setTimeout(() => {
  250. initialize(); // Re-run the whole setup if buttons disappear
  251. }, 1000);
  252. }
  253. });
  254. observer.observe(document.body, { childList: true, subtree: true });
  255. }
  256.  
  257. // --- Start ---
  258. if (document.readyState === 'complete') {
  259. initialize();
  260. } else {
  261. window.addEventListener('load', initialize);
  262. }
  263. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址