Keyboard Shortcuts for Perplexity.ai: GSAP-powered scrolling, edit message, focus input, set sources (Academic/Social/GitHub), Search, Research.
// ==UserScript== // @name Perplexity Power Shortcuts // @namespace http://tampermonkey.net/ // @version 1.1.0 // @description Keyboard Shortcuts for Perplexity.ai: GSAP-powered scrolling, edit message, focus input, set sources (Academic/Social/GitHub), Search, Research. // @author Brian Hurd // @match https://perplexity.ai/* // @match https://www.perplexity.ai/* // @grant none // @run-at document-end // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/gsap.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/ScrollToPlugin.min.js // ==/UserScript== // Alt+t → Scroll to top of main thread // Alt+z → Scroll to bottom of main thread // Alt+a → Scroll up one message block // Alt+f → Scroll down one message block // Alt+e → Edit lowest user message (clicks [data-testid="edit-query-button"]; simulates hover if needed; falls back to Search if none found) // Alt+Shift+s → Click Search mode (SVG d^="M11 2.125a8.378 8.378") // Alt+r → Click Research mode (SVG d^="M8.175 13.976a.876.876") // Alt+w → Focus chat input (#ask-input contenteditable) // Alt+p → Set Sources → Academic (First: Set Sources SVG d^="M3 12a9 9 0 1 0", then submenu data-testid="source-toggle-scholar") // Alt+s → Set Sources → Social (submenu data-testid="source-toggle-social") // Alt+g → Set Sources → GitHub (submenu by testid if present or text "GitHub") // Alt+n → Start new conversation (function () { 'use strict'; // Helper: get scrollable container function getScrollableContainer() { return document.querySelector('.scrollable-container.scrollbar-subtle.flex.flex-1.basis-0.overflow-auto'); } function scrollToTop() { const c = getScrollableContainer(); if (c) c.scrollTop = 0; } function scrollToBottom() { const c = getScrollableContainer(); if (c) c.scrollTop = c.scrollHeight; } // Scroll up/down one message function getMessageBlocks() { // Find blocks with edit button descendant, robust for Perplexity const blocks = []; document.querySelectorAll('button[data-testid="edit-query-button"]').forEach(btn => { let block = btn.closest('.mb-xs.group.relative.flex.items-end'); if (!block) { let el = btn.parentElement; for (let i = 0; i < 8 && el; i++) { if (el.classList.contains('mb-xs') && el.classList.contains('group')) break; el = el.parentElement; } block = el; } if (block && !blocks.includes(block)) blocks.push(block); }); return blocks; } function scrollUpOneMessage() { const sc = getScrollableContainer(); if (!sc) return; const blocks = getMessageBlocks(); if (!blocks.length) return; const y = sc.scrollTop; let targetTop = 0; for (let i = blocks.length - 1; i >= 0; i--) { const top = blocks[i].offsetTop; if (top < y - 10) { targetTop = top; break; } } sc.scrollTop = targetTop; } function scrollDownOneMessage() { const sc = getScrollableContainer(); if (!sc) return; const blocks = getMessageBlocks(); if (!blocks.length) return; const y = sc.scrollTop; let targetTop = sc.scrollHeight; for (let i = 0; i < blocks.length; i++) { const top = blocks[i].offsetTop; if (top > y + 10) { targetTop = top; break; } } sc.scrollTop = targetTop; } // Edit lowest user message function isVisible(el) { if (!el) return false; const rect = el.getBoundingClientRect(); return !!(rect.width && rect.height && rect.bottom > 0 && rect.right > 0 && rect.top < (window.innerHeight || document.documentElement.clientHeight) && rect.left < (window.innerWidth || document.documentElement.clientWidth)); } function simulateHover(el) { if (!el) return; el.dispatchEvent(new PointerEvent('pointerenter', { bubbles: true })); el.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true })); el.dispatchEvent(new PointerEvent('mouseover', { bubbles: true })); } function editLowestMessage() { const btns = Array.from(document.querySelectorAll('button[data-testid="edit-query-button"]')); if (!btns.length) return false; let target = null; let maxBottom = -Infinity; for (const btn of btns) { const rect = btn.getBoundingClientRect(); if (isVisible(btn) && rect.bottom > maxBottom) { target = btn; maxBottom = rect.bottom; } } if (!target) target = btns[btns.length - 1]; if (!target) return false; const group = target.closest('.mb-xs.group.relative.flex.items-end'); if (group) simulateHover(group); setTimeout(() => { target.click(); }, 120); return true; } // Focus chat input function focusChatInput() { const el = document.querySelector('#ask-input[contenteditable="true"]'); if (!el) return false; el.focus(); const range = document.createRange(); range.selectNodeContents(el); range.collapse(false); const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); return true; } // Search/Research mode by SVG path (segment control) function clickSegmentedControlBySVGPath(pathPrefix) { const segButtons = Array.from(document.querySelectorAll('button[role="radio"]')); for (const btn of segButtons) { const svg = btn.querySelector('svg'); if (svg) { const path = svg.querySelector('path'); if (path && path.getAttribute('d') && path.getAttribute('d').startsWith(pathPrefix)) { if (btn.getAttribute('aria-checked') !== "true") { btn.click(); return true; } } } } return false; } // Sources menu helpers function openSourcesMenuAndClickSubmenuByTestId(testid) { const sourcesBtn = document.querySelector('button[data-testid="sources-switcher-button"]'); if (!sourcesBtn) return false; sourcesBtn.click(); setTimeout(() => { const menu = document.querySelector('div[role="menu"], ul[role="menu"]'); if (menu) { const item = menu.querySelector(`[data-testid="${testid}"]`); if (item) item.click(); } }, 250); return true; } function openSourcesMenuAndClickSubmenuByText(text) { const sourcesBtn = document.querySelector('button[data-testid="sources-switcher-button"]'); if (!sourcesBtn) return false; sourcesBtn.click(); setTimeout(() => { const menu = document.querySelector('div[role="menu"], ul[role="menu"]'); if (menu) { const items = Array.from(menu.querySelectorAll('[role="menuitem"],button,[data-testid]')); const target = items.find(n => (n.innerText || n.textContent || '').toLowerCase().includes(text.toLowerCase())); if (target) target.click(); } }, 250); return true; } // Keyboard handler with new mapping! document.addEventListener('keydown', function (e) { if (!e.altKey || e.repeat) return; // Alt+Shift+S = Search mode if (e.shiftKey && e.key.toLowerCase() === 's') { clickSegmentedControlBySVGPath('M11 2.125a8.378'); e.preventDefault(); return; } // Alt+P = Sources → Academic if (!e.shiftKey && e.key.toLowerCase() === 'p') { openSourcesMenuAndClickSubmenuByTestId('source-toggle-scholar'); e.preventDefault(); return; } // Alt+S = Sources → Social (not mode) if (!e.shiftKey && e.key.toLowerCase() === 's') { openSourcesMenuAndClickSubmenuByTestId('source-toggle-social'); e.preventDefault(); return; } // Alt+G = Sources → Github if (!e.shiftKey && e.key.toLowerCase() === 'g') { // Try by testid, fallback to text if (!openSourcesMenuAndClickSubmenuByTestId('source-toggle-github')) { openSourcesMenuAndClickSubmenuByText('github'); } e.preventDefault(); return; } // Alt+R = Research mode if (!e.shiftKey && e.key.toLowerCase() === 'r') { clickSegmentedControlBySVGPath('M8.175 13.976a.876.876'); e.preventDefault(); return; } // Alt+E = Edit lowest message (with fallback) if (!e.shiftKey && e.key.toLowerCase() === 'e') { if (!editLowestMessage()) { // Fallback: activate Search mode clickSegmentedControlBySVGPath('M11 2.125a8.378'); } e.preventDefault(); return; } // Scrolling/etc switch (e.key.toLowerCase()) { case 't': scrollToTop(); e.preventDefault(); break; case 'z': scrollToBottom(); e.preventDefault(); break; case 'a': scrollUpOneMessage(); e.preventDefault(); break; case 'f': scrollDownOneMessage(); e.preventDefault(); break; case 'w': focusChatInput(); e.preventDefault(); break; } }, true); })(); // alt+n starts new conversation (function() { document.addEventListener('keydown', function(e) { if (e.altKey && !e.shiftKey && !e.ctrlKey && !e.metaKey && !e.repeat && e.key.toLowerCase() === 'n') { const btn = document.querySelector('button[data-testid="sidebar-new-thread"]'); if (btn) btn.click(); } }, true); })();