Perplexity Power Shortcuts

Keyboard Shortcuts for Perplexity.ai: GSAP-powered scrolling, edit message, focus input, set sources (Academic/Social/GitHub), Search, Research.

Versão de: 28/10/2025. Veja: a última versão.

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==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);
})();