Perplexity Power Shortcuts

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

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey, Greasemonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

You will need to install an extension such as Tampermonkey to install this script.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्क्रिप्ट व्यवस्थापक एक्स्टेंशन इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्क्रिप्ट व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्टाईल व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

// ==UserScript==
// @name         Perplexity Power Shortcuts
// @namespace    http://tampermonkey.net/
// @version      1.4.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+j         → Scroll up one message block
// Alt+k         → 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+a         → 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


// #1
// Css tweaks (hide download comet)
(function() {
    var target = document.querySelector('div.h-bannerHeight.rounded-sm');
    if (target) target.style.display = 'none';
})();


// #2
// on page reload, automatically run the “Research + Social” sequence (same as your alt+g logic) on page load, waiting 1500 ms.
(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,
        }));
    }
    function doAltGSequence() {
        clickSegmentedControlBySVGPath('M8.175 13.976a.876.876');
        setTimeout(() => {
            openSourcesMenuAndClickSubmenuByTestId('source-toggle-social');
            setTimeout(() => {
                sendEscapeKey();
            }, 350);
        }, 250);
    }
    if (document.readyState === 'complete' || document.readyState === 'interactive') {
        setTimeout(doAltGSequence, 1500);
    } else {
        window.addEventListener('DOMContentLoaded', function () {
            setTimeout(doAltGSequence, 1500);
        });
    }
})();

// #3
// Alt+t         → Scroll to top of main thread
// Alt+z         → Scroll to bottom of main thread
// Alt+j         → Scroll up one message block
// Alt+k         → 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 (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")

(function () {
    'use strict';

    /* Ensure ScrollTo works, but never block if GSAP isn't ready */
    function ensureScrollToReady() {
        const start = Date.now();
        (function waitAndRegister() {
            const gs = window.gsap;
            const plug = window.ScrollToPlugin;
            if (gs && plug) {
                try {
                    if (!(gs.plugins && gs.plugins.ScrollToPlugin)) {
                        gs.registerPlugin(plug);
                    }
                    return; // success
                } catch (e) {
                    // keep polling until timeout
                }
            }
            if (Date.now() - start < 4000) setTimeout(waitAndRegister, 100);
        })();
    }
    // Kick off registration (safe even if @require already loaded)
    ensureScrollToReady();



    const __SCROLL_CACHE = { el: null };

function getScrollableContainer(forceRecalc = false) {
    const isScrollable = (el) => {
        if (!el) return false;
        const st = getComputedStyle(el);
        const oy = st.overflowY || st.overflow || '';
        return /(auto|scroll)/.test(oy) && (el.scrollHeight - el.clientHeight > 2);
    };
    const nearestScrollable = (el) => {
        let cur = el;
        while (cur && cur !== document.body && cur !== document.documentElement) {
            if (isScrollable(cur)) return cur;
            cur = cur.parentElement;
        }
        return null;
    };

    // Use cached container if still valid
    if (!forceRecalc && __SCROLL_CACHE.el && document.contains(__SCROLL_CACHE.el) && isScrollable(__SCROLL_CACHE.el)) {
        return __SCROLL_CACHE.el;
    }

    // Prefer anchors that are guaranteed to be inside the chat thread
    const anchorSelectors = [
        'div[id^="Markdown-Content-"]',                          // assistant message root
        '.mb-xs.group.relative.flex.items-end',                  // user prompt group
        '.group.relative.flex.items-end.mb-xs',
        'button[data-testid="edit-query-button"]'                // climb from edit btn if present
    ];
    let best = null;
    document.querySelectorAll(anchorSelectors.join(',')).forEach(a => {
        const sc = a.tagName === 'BUTTON' ? nearestScrollable(a.closest('.group') || a) : nearestScrollable(a);
        if (sc && (!best || sc.clientHeight > best.clientHeight)) best = sc;
    });

    // Known selector(s) as a fallback
    if (!best) {
        const known = document.querySelector(
            '.scrollable-container.scrollbar-subtle.flex.flex-1.basis-0.overflow-auto, .scrollbar-subtle.flex.flex-1.basis-0.overflow-auto'
        );
        if (isScrollable(known)) best = known;
    }

    // Generic fallback: biggest visible scrollable region
    if (!best) {
        const candidates = Array.from(document.querySelectorAll('div,main,section')).filter(n => {
            try {
                const st = getComputedStyle(n);
                const oy = st.overflowY || st.overflow || '';
                return /(auto|scroll)/.test(oy) &&
                       n.clientHeight >= window.innerHeight * 0.5 &&
                       (n.scrollHeight - n.clientHeight > 2);
            } catch { return false; }
        }).sort((a, b) => b.clientHeight - a.clientHeight);
        if (candidates[0]) best = candidates[0];
    }

    if (!best) best = document.scrollingElement || document.documentElement;
    __SCROLL_CACHE.el = best;
    return best;
}

    function scrollToTop() {
    const c = getScrollableContainer();
    if (!c) return;
    scrollToPosition(c, 0);
}

function scrollToBottom() {
    const c = getScrollableContainer();
    if (!c) return;

    const isRoot = c === document.documentElement || c === document.body || c === document.scrollingElement;
    const target = (window.gsap && window.ScrollToPlugin && isRoot) ? window : c;

    if (window.gsap && window.ScrollToPlugin) {
        try {
            if (!(gsap.plugins && gsap.plugins.ScrollToPlugin)) gsap.registerPlugin(ScrollToPlugin);
            gsap.to(target, { duration: 0.55, scrollTo: { y: "max" }, ease: "power4.out", overwrite: "auto" });
            return;
        } catch (_) {}
    }
    try { (isRoot ? window : c).scrollTo({ top: c.scrollHeight, behavior: 'smooth' }); }
    catch (_) { c.scrollTop = c.scrollHeight; }
}

    function scrollToPosition(container, top) {
    if (!container) return;
    const isRoot = container === document.documentElement || container === document.body || container === document.scrollingElement;
    const target = (window.gsap && window.ScrollToPlugin && isRoot) ? window : container;

    let usedGSAP = false;
    if (window.gsap && window.ScrollToPlugin) {
        try {
            if (!(gsap.plugins && gsap.plugins.ScrollToPlugin)) gsap.registerPlugin(ScrollToPlugin);
            gsap.to(target, { duration: 0.55, scrollTo: { y: top }, ease: "power4.out", overwrite: "auto" });
            usedGSAP = true;
        } catch (_) {}
    }
    if (!usedGSAP) {
        try {
            if (isRoot && 'scrollTo' in window) window.scrollTo({ top, behavior: 'smooth' });
            else container.scrollTo({ top, behavior: 'smooth' });
        } catch (_) {
            if (isRoot) (document.scrollingElement || document.documentElement).scrollTop = top;
            else container.scrollTop = top;
        }
    }
}



    /* Build a unified, ordered list of “message anchors”:
   - user prompt top: .group.relative.flex.items-end.mb-xs
   - assistant answer top: #Markdown-Content-*
*/
    function getMessageAnchors() {
        const set = new Set();

        // User header/group container (contains Edit/Copy button group)
        document.querySelectorAll(
            '.group.relative.flex.items-end.mb-xs, .mb-xs.group.relative.flex.items-end'
        ).forEach(el => set.add(el));

        // If present, also climb from Edit button → group (covers future class changes)
        document.querySelectorAll('button[data-testid="edit-query-button"]').forEach(btn => {
            const grp = btn.closest('.group') || btn.closest('.mb-xs');
            if (grp) set.add(grp);
        });

        // Assistant content root(s)
        document.querySelectorAll('div[id^="Markdown-Content-"]').forEach(el => set.add(el));

        // Fallbacks if DOM shifts
        if (set.size === 0) {
            document.querySelectorAll(
                'div[role="textbox"][data-lexical-editor="true"], [data-testid="message"], [data-anchor^="message-"]'
            ).forEach(el => set.add(el));
        }
        return Array.from(set);
    }

    function getTopRelativeToContainer(el, sc) {
        const scRect = sc.getBoundingClientRect();
        const rect = el.getBoundingClientRect();
        return sc.scrollTop + (rect.top - scRect.top);
    }

    function getSortedAnchorTops(sc) {
        const anchors = getMessageAnchors();
        const tops = anchors
        .map(el => ({ el, top: Math.round(getTopRelativeToContainer(el, sc)) }))
        .filter(d => Number.isFinite(d.top))
        .sort((a, b) => a.top - b.top);

        // De-dup nearly-equal tops (e.g., if two anchors share the same top)
        const dedup = [];
        for (const d of tops) {
            if (!dedup.length || Math.abs(d.top - dedup[dedup.length - 1].top) > 2) dedup.push(d);
        }
        return dedup;
    }

    // Scroll helpers using the anchors.
    // Alt+J: go to the start of the current message; if already near it, go to the previous.
    // Alt+K: go to the next message anchor.
    function scrollUpOneMessage() {
        const sc = getScrollableContainer();
        if (!sc) return;

        const anchors = getSortedAnchorTops(sc);
        if (!anchors.length) return;

        const y = sc.scrollTop;
        const tol = 8;      // treat within 8px as “at” an anchor
        const snap = 24;    // if we’re more than 24px past the anchor, snap back to it

        // Last anchor at/above current position
        let idx = -1;
        for (let i = 0; i < anchors.length; i++) {
            if (anchors[i].top <= y + tol) idx = i; else break;
        }

        let target;
        if (idx === -1) {
            // We are above the first anchor: go to the very first anchor
            target = anchors[0].top;
        } else if (y > anchors[idx].top + snap) {
            // We’re inside the current message: go to its start
            target = anchors[idx].top;
        } else {
            // We’re already at (or very near) the current start: go to previous
            target = idx > 0 ? anchors[idx - 1].top : 0;
        }
        scrollToPosition(sc, Math.max(0, target - 50));

    }

    function scrollDownOneMessage() {
        const sc = getScrollableContainer();
        if (!sc) return;

        const anchors = getSortedAnchorTops(sc);
        if (!anchors.length) return;

        const y = sc.scrollTop;
        const tol = 8;

        // First anchor strictly below current position
        let target = null;
        for (let i = 0; i < anchors.length; i++) {
            if (anchors[i].top > y + tol) { target = anchors[i].top; break; }
        }
        scrollToPosition(sc, target != null ? target : sc.scrollHeight);
    }
    // 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
    // Sources menu helpers with Escape key auto-close
    function sendEscapeKey() {
        document.dispatchEvent(new KeyboardEvent('keydown', {
            key: 'Escape',
            code: 'Escape',
            keyCode: 27,
            which: 27,
            bubbles: true,
        }));
    }

    // testidOrText: either the data-testid or text to match
    // byTestId: if true, use data-testid; else, use text match
    function openSourcesMenuAndClickAndClose(testidOrText, byTestId) {
        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"]');
            let clicked = false;
            if (menu) {
                if (byTestId) {
                    const item = menu.querySelector(`[data-testid="${testidOrText}"]`);
                    if (item) {
                        item.click();
                        clicked = true;
                    }
                }
                // Fallback to text if not found or not using testid
                if (!clicked && !byTestId) {
                    const items = Array.from(menu.querySelectorAll('[role="menuitem"],button,[data-testid]'));
                    const target = items.find(n => (n.innerText || n.textContent || '').toLowerCase().includes(testidOrText.toLowerCase()));
                    if (target) {
                        target.click();
                        clicked = true;
                    }
                }
            }
            setTimeout(sendEscapeKey, 250);
        }, 250);
        return true;
    }



    /* Keyboard handler with stronger suppression so site scripts don't swallow keys */
    document.addEventListener('keydown', function (e) {
        if (!e.altKey || e.repeat) return;
        const key = e.key.toLowerCase();

        // Modes / menus
        if (e.shiftKey && key === 's') {
            e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
            clickSegmentedControlBySVGPath('M11 2.125a8.378');
            return;
        }
        if (!e.shiftKey && key === 'a') {
            e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
            openSourcesMenuAndClickAndClose('source-toggle-scholar', true);
            return;
        }
        if (!e.shiftKey && key === 's') {
            e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
            openSourcesMenuAndClickAndClose('source-toggle-social', true);
            return;
        }
        if (!e.shiftKey && key === 'g') {
            e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
            openSourcesMenuAndClickAndClose('source-toggle-github', true);
            setTimeout(() => openSourcesMenuAndClickAndClose('github', false), 600);
            return;
        }
        if (!e.shiftKey && key === 'r') {
            e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
            clickSegmentedControlBySVGPath('M8.175 13.976a.876.876');
            return;
        }
        if (!e.shiftKey && key === 'e') {
            e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
            if (!editLowestMessage()) {
                clickSegmentedControlBySVGPath('M11 2.125a8.378');
            }
            return;
        }

        // Scrolling and focus
        switch (key) {
            case 't':
                e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
                scrollToTop(); break;
            case 'z':
                e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
                scrollToBottom(); break;
            case 'j':
                e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
                scrollUpOneMessage(); break;
            case 'k':
                e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
                scrollDownOneMessage(); break;
            case 'w':
                e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
                focusChatInput(); break;
        }
    }, true);

})();

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