Perplexity Power Shortcuts

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.2.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

// alt+g to change to research and social in one keystroke
(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') {
            e.preventDefault();
            document.dispatchEvent(new KeyboardEvent('keydown', {
                key: 'i',
                code: 'KeyI',
                keyCode: 73,
                which: 73,
                ctrlKey: true,
                bubbles: true,
            }));
        }
    }, true);
})();



// alt+g to change to research and social in one keystroke
(function () {
    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;
    }
    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 sendEscapeKey() {
        document.dispatchEvent(new KeyboardEvent('keydown', {
            key: 'Escape',
            code: 'Escape',
            keyCode: 27,
            which: 27,
            bubbles: true,
        }));
    }
    document.addEventListener('keydown', function (e) {
        if (e.altKey && !e.shiftKey && !e.ctrlKey && !e.metaKey && !e.repeat && e.key.toLowerCase() === 'g') {
            e.preventDefault();
            clickSegmentedControlBySVGPath('M8.175 13.976a.876.876');
            setTimeout(() => {
                openSourcesMenuAndClickSubmenuByTestId('source-toggle-social');
                setTimeout(() => {
                    sendEscapeKey();
                }, 250);
            }, 400);
        }
    }, true);
})();