Perfexity.ai

Resizable Sidebar · Smart Scroll · Notifications · Settings in Profile Menu

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Perfexity.ai
// @namespace    https://greasyfork.org/users/1566018
// @version      2.1
// @description  Resizable Sidebar · Smart Scroll · Notifications · Settings in Profile Menu
// @author       ReNDoM
// @match        https://www.perplexity.ai/*
// @grant        none
// @run-at       document-end
// @homepageURL  https://greasyfork.org/scripts/564497
// @supportURL   https://greasyfork.org/scripts/564497/feedback
// ==/UserScript==

(function () {
    'use strict';

    // ─── Constants ────────────────────────────────────────────────────────────────
    const SIDEBAR_MIN   = 200;
    const SIDEBAR_MAX   = 800;
    const SIDEBAR_DEFAULT = 280;
    const KEY_WIDTH     = 'perfexity_sidebar_width';
    const KEY_SETTINGS  = 'perfexity_settings';

    // ─── State ────────────────────────────────────────────────────────────────────
    let isResizing      = false;
    let sidebarOuter    = null;
    let sidebarInner    = null;
    let sidebarWrapper  = null;
    let mainContent     = null;
    let resizeHandle    = null;
    let audioCtx        = null;
    let hasRedirected   = false;
    let hasNotified     = false;

    const savedWidth = parseInt(localStorage.getItem(KEY_WIDTH)) || SIDEBAR_DEFAULT;

    let settings = {
        autoScroll:               true,
        autoRedirect:             true,
        resizeHandle:             true,
        notificationEnabled:      true,
        notificationDesktop:      true,
        notificationSound:        true,
        notificationOnlyInactive: false,
        ...JSON.parse(localStorage.getItem(KEY_SETTINGS) || '{}')
    };

    const saveSettings = () => localStorage.setItem(KEY_SETTINGS, JSON.stringify(settings));

    // ─── Audio ────────────────────────────────────────────────────────────────────
    function initAudio() {
        if (audioCtx) return;
        try {
            audioCtx = new (window.AudioContext || window.webkitAudioContext)();
            if (audioCtx.state === 'suspended') audioCtx.resume();
        } catch (e) { console.warn('[Perfexity] AudioContext failed:', e); }
    }

    function ensureAudio() {
        return new Promise(resolve => {
            if (!audioCtx) initAudio();
            if (!audioCtx || audioCtx.state === 'running') return resolve();
            audioCtx.resume().then(resolve).catch(resolve);
        });
    }

    function playSound() {
        if (!settings.notificationSound) return;
        ensureAudio().then(() => {
            if (!audioCtx) return;
            try {
                const osc  = audioCtx.createOscillator();
                const gain = audioCtx.createGain();
                osc.connect(gain);
                gain.connect(audioCtx.destination);
                osc.frequency.setValueAtTime(800, audioCtx.currentTime);
                osc.frequency.exponentialRampToValueAtTime(400, audioCtx.currentTime + 0.1);
                gain.gain.setValueAtTime(0, audioCtx.currentTime);
                gain.gain.linearRampToValueAtTime(0.3, audioCtx.currentTime + 0.01);
                gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.3);
                osc.start(audioCtx.currentTime);
                osc.stop(audioCtx.currentTime + 0.3);
            } catch (e) { console.warn('[Perfexity] Sound failed:', e); }
        });
    }

    // ─── Notifications ────────────────────────────────────────────────────────────
    function requestPermission() {
        if ('Notification' in window && Notification.permission === 'default')
            Notification.requestPermission();
    }

    function notify() {
        if (!settings.notificationEnabled || hasNotified) return;
        if (settings.notificationOnlyInactive && !document.hidden) return;
        hasNotified = true;
        playSound();
        if (settings.notificationDesktop && Notification.permission === 'granted') {
            new Notification('Perfexity.ai', {
                body: 'Antwort ist fertig generiert!',
                icon: 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%2320d9d2"><path d="M20.5 11H19V7c0-1.1-.9-2-2-2h-4V3.5C13 2.12 11.88 1 10.5 1S8 2.12 8 3.5V5H4c-1.1 0-1.99.9-1.99 2v3.8H3.5c1.49 0 2.7 1.21 2.7 2.7s-1.21 2.7-2.7 2.7H2V20c0 1.1.9 2 2 2h3.8v-1.5c0-1.49 1.21-2.7 2.7-2.7 1.49 0 2.7 1.21 2.7 2.7V22H17c1.1 0 2-.9 2-2v-4h1.5c1.38 0 2.5-1.12 2.5-2.5S21.88 11 20.5 11z"/></svg>',
                tag: 'perfexity-done',
                requireInteraction: false
            });
        }
    }

    // ─── Answer Detection ─────────────────────────────────────────────────────────
    function watchAnswers() {
        let wasGenerating = false;
        setInterval(() => {
            const isGenerating = !!(
                document.querySelector('button[aria-label*="Antwort anhalten"]') ||
                document.querySelector('button[aria-label*="anhalten"]')
            );
            if (isGenerating && !wasGenerating) {
                wasGenerating = true;
                hasNotified = false;
                if (settings.autoScroll) scrollToBottom();
            }
            if (!isGenerating && wasGenerating) {
                wasGenerating = false;
                setTimeout(() => ensureAudio().then(notify), 1000);
            }
        }, 500);
    }

    // ─── Scroll ───────────────────────────────────────────────────────────────────
    function getScrollable() {
        const main = document.querySelector('main') || document.querySelector('[role="main"]');
        if (!main) return document.body;
        for (const el of main.querySelectorAll('*')) {
            const s = window.getComputedStyle(el);
            if ((s.overflowY === 'auto' || s.overflowY === 'scroll') && el.scrollHeight > el.clientHeight + 5)
                return el;
        }
        return main;
    }

    function scrollToBottom() {
        if (!settings.autoScroll) return;
        setTimeout(() => getScrollable().scrollTo({ top: 999999, behavior: 'smooth' }), 800);
    }

    function scrollOnLoad() {
        if (!settings.autoScroll) return;
        const run = () => document.querySelectorAll('*').forEach(el => {
            const s = window.getComputedStyle(el);
            if ((s.overflowY === 'auto' || s.overflowY === 'scroll') && el.scrollHeight > el.clientHeight + 10)
                el.scrollTo({ top: 999999, behavior: 'smooth' });
        });
        [1200, 2500, 4500].forEach(t => setTimeout(run, t));
    }

    // ─── Auto-Redirect ────────────────────────────────────────────────────────────
    function redirectToLast() {
        if (!settings.autoRedirect || window.location.pathname !== '/' || hasRedirected) return;
        const t = setInterval(() => {
            const a = document.querySelector('a[data-testid^="thread-title"]');
            if (a) {
                clearInterval(t);
                const href = a.getAttribute('href');
                if (href && href !== '/') { hasRedirected = true; window.location.href = href; }
            }
        }, 200);
        setTimeout(() => clearInterval(t), 5000);
    }

    // ─── Sidebar DOM ──────────────────────────────────────────────────────────────
    function findElements() {
        sidebarOuter = document.querySelector('.group\\/sidebar');
        if (!sidebarOuter) return false;
        sidebarInner   = sidebarOuter.querySelector(':scope > div');
        sidebarWrapper = sidebarOuter.parentElement;
        mainContent    = sidebarWrapper?.nextElementSibling
        || document.querySelector('.flex.size-full.flex-1 > div:last-child')
        || document.querySelector('[class*="grow"][class*="flex-col"][class*="isolate"]')
        || null;
        return !!sidebarWrapper;
    }

    function applyWidth(w) {
        if (!sidebarWrapper) return;

        sidebarWrapper.style.setProperty('width',     `${w}px`, 'important');
        sidebarWrapper.style.setProperty('min-width', `${w}px`, 'important');
        sidebarWrapper.style.setProperty('flex-shrink', '0',    'important');

        for (const el of [sidebarOuter, sidebarInner].filter(Boolean)) {
            el.style.setProperty('width',     `${w}px`, 'important');
            el.style.setProperty('min-width', `${w}px`, 'important');
            el.style.setProperty('max-width', `${w}px`, 'important');
        }

        const navInner = sidebarOuter.querySelector('div[style*="margin-left: 8px"]');
        if (navInner) {
            navInner.style.setProperty('width', `${w - 16}px`, 'important');
            navInner.querySelector(':scope > div.absolute')
                ?.style.setProperty('width', `${w - 16}px`, 'important');
        }

        const hist = sidebarOuter.querySelector('.-ml-md[style*="width"]')
        || sidebarOuter.querySelector('[class*="-ml-md"][class*="shrink-0"][style*="width"]');
        if (hist) hist.style.setProperty('width', `${w - 8}px`, 'important');

        if (mainContent) mainContent.style.setProperty('min-width', '0', 'important');

        localStorage.setItem(KEY_WIDTH, w);
        if (resizeHandle) resizeHandle.style.left = `${w - 6}px`;
    }

    function isSidebarVisible() {
        if (!sidebarOuter) return false;
        const r = sidebarOuter.getBoundingClientRect();
        return r.left >= 0 && r.width > 50;
    }

    function updateHandle() {
        if (resizeHandle)
            resizeHandle.style.display = settings.resizeHandle && isSidebarVisible() ? 'block' : 'none';
    }

    // ─── Resize Handle ────────────────────────────────────────────────────────────
    function initResize() {
        if (!findElements()) { setTimeout(initResize, 1000); return; }

        document.getElementById('perfexity-resize-handle')?.remove();
        applyWidth(savedWidth);

        resizeHandle = document.createElement('div');
        resizeHandle.id = 'perfexity-resize-handle';
        resizeHandle.style.cssText = `
            position:fixed;top:0;bottom:0;left:${savedWidth - 6}px;width:12px;
            background:transparent;cursor:col-resize;z-index:9999;
            display:none;pointer-events:auto;
        `;
        resizeHandle.addEventListener('mouseenter', () => {
            resizeHandle.style.background = 'linear-gradient(90deg,transparent,#21808D,transparent)';
        });
        resizeHandle.addEventListener('mouseleave', () => {
            if (!isResizing) resizeHandle.style.background = 'transparent';
        });
        document.body.appendChild(resizeHandle);
        updateHandle();

        // Periodic DOM check (SPA)
        setInterval(() => {
            if (!sidebarWrapper || !document.contains(sidebarWrapper)) {
                if (findElements()) applyWidth(parseInt(localStorage.getItem(KEY_WIDTH)) || savedWidth);
            }
            updateHandle();
        }, 2000);

        let startX = 0, startW = 0;

        resizeHandle.addEventListener('mousedown', e => {
            isResizing = true;
            startX = e.clientX;
            startW = parseInt(localStorage.getItem(KEY_WIDTH)) || savedWidth;
            document.body.style.cssText += 'cursor:col-resize;user-select:none;';
            e.preventDefault();
        });

        document.addEventListener('mousemove', e => {
            if (!isResizing) return;
            applyWidth(Math.max(SIDEBAR_MIN, Math.min(SIDEBAR_MAX, startW + e.clientX - startX)));
        });

        document.addEventListener('mouseup', () => {
            if (!isResizing) return;
            isResizing = false;
            document.body.style.cursor = '';
            document.body.style.userSelect = '';
            resizeHandle.style.background = 'transparent';
        });
    }

    // ─── Settings Menu ────────────────────────────────────────────────────────────
    const SETTINGS_DEFS = [
        { key: 'autoScroll',               label: 'Auto-Scroll',              desc: 'Scrollt automatisch nach unten beim Generieren',  indent: false },
        { key: 'autoRedirect',             label: 'Letzten Thread öffnen',    desc: 'Öffnet den letzten Thread beim Start',            indent: false },
        { key: 'resizeHandle',             label: 'Resize-Handle',            desc: 'Zeigt den Griff zum Verbreitern der Sidebar',     indent: false },
        { key: 'notificationEnabled',      label: 'Benachrichtigungen',       desc: 'Benachrichtigt wenn die Antwort fertig ist',      indent: false },
        { key: 'notificationDesktop',      label: '🔔 Desktop-Notification',   desc: 'Zeigt eine Browser-Benachrichtigung an',          indent: true  },
        { key: 'notificationSound',        label: '🔊 Sound',                  desc: 'Spielt einen dezenten Ton ab',                   indent: true  },
        { key: 'notificationOnlyInactive', label: 'Nur bei inaktivem Tab',    desc: 'Nur benachrichtigen wenn Tab nicht sichtbar',     indent: true  },
    ];

    function injectSettingsMenu() {
        const obs = new MutationObserver(() => {
            const dropdown = document.querySelector('.bg-raised.shadow-overlay[style*="min-width: 220px"]');
            if (!dropdown || dropdown.querySelector('#perfexity-section')) return;

            const firstSep = dropdown.querySelector('[role="separator"]');
            if (!firstSep) return;

            const currentWidth = parseInt(localStorage.getItem(KEY_WIDTH)) || savedWidth;

            // Separator
            const sep = document.createElement('div');
            sep.setAttribute('role', 'separator');
            sep.setAttribute('aria-orientation', 'horizontal');
            sep.className = 'border-subtlest my-xs mx-sm border-t';

            // Section
            const section = document.createElement('div');
            section.id = 'perfexity-section';
            section.style.padding = '4px 0';
            section.innerHTML = `
                <style>
                    .pf-hd{display:flex;align-items:center;gap:8px;padding:6px 12px 4px;font-size:11px;font-weight:600;opacity:.4;text-transform:uppercase;letter-spacing:.05em;pointer-events:none;}
                    .pf-row{display:flex;align-items:center;justify-content:space-between;padding:6px 12px;border-radius:8px;cursor:pointer;gap:12px;font-size:13px;color:var(--color-foreground);}
                    .pf-row:hover{background:rgba(128,128,128,.08);}
                    .pf-ind{margin-left:16px;padding-left:10px;border-left:2px solid rgba(128,128,128,.2);}
                    .pf-lbl{flex:1;min-width:0;}
                    .pf-lbl-title{font-size:13px;}
                    .pf-lbl-desc{font-size:11px;opacity:.5;margin-top:1px;}
                    .pf-tog{position:relative;display:inline-flex;align-items:center;border:none;border-radius:999px;width:34px;height:20px;padding:0;cursor:pointer;flex-shrink:0;transition:background .15s;}
                    .pf-tog-thumb{position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:#fff;transition:transform .15s;}
                    .pf-sl-wrap{padding:4px 12px 8px;}
                    .pf-sl-row{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;font-size:12px;color:var(--color-foreground);}
                    .pf-sl-val{color:#21808D;font-weight:500;}
                    .pf-sl{-webkit-appearance:none;appearance:none;width:100%;height:4px;border-radius:2px;background:rgba(128,128,128,.2);outline:none;cursor:pointer;}
                    .pf-sl::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;border-radius:50%;background:#21808D;cursor:pointer;}
                    .pf-sl::-moz-range-thumb{width:14px;height:14px;border-radius:50%;background:#21808D;border:none;cursor:pointer;}
                    .pf-ft{text-align:center;font-size:10px;opacity:.3;padding:2px 0 4px;}
                </style>
                <div class="pf-hd">
                    <svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor"><path d="M20.5 11H19V7c0-1.1-.9-2-2-2h-4V3.5C13 2.12 11.88 1 10.5 1S8 2.12 8 3.5V5H4c-1.1 0-1.99.9-1.99 2v3.8H3.5c1.49 0 2.7 1.21 2.7 2.7s-1.21 2.7-2.7 2.7H2V20c0 1.1.9 2 2 2h3.8v-1.5c0-1.49 1.21-2.7 2.7-2.7 1.49 0 2.7 1.21 2.7 2.7V22H17c1.1 0 2-.9 2-2v-4h1.5c1.38 0 2.5-1.12 2.5-2.5S21.88 11 20.5 11z"/></svg>
                    Perfexity.ai
                </div>
                ${SETTINGS_DEFS.map(({ key, label, desc, indent }) => `
                    <div class="pf-row${indent ? ' pf-ind' : ''}" data-key="${key}">
                        <div class="pf-lbl">
                            <div class="pf-lbl-title">${label}</div>
                            <div class="pf-lbl-desc">${desc}</div>
                        </div>
                        <button type="button" role="switch" class="pf-tog"
                            aria-checked="${settings[key]}"
                            style="background:${settings[key] ? '#21808D' : 'rgba(128,128,128,.3)'}">
                            <span class="pf-tog-thumb"
                                style="transform:translateX(${settings[key] ? '14px' : '0'})"></span>
                        </button>
                    </div>
                `).join('')}
                <div class="pf-sl-wrap">
                    <div class="pf-sl-row">
                        <span>Sidebar-Breite</span>
                        <span class="pf-sl-val" id="pf-w-val">${currentWidth}px</span>
                    </div>
                    <input type="range" class="pf-sl" id="pf-w-sl" min="${SIDEBAR_MIN}" max="${SIDEBAR_MAX}" value="${currentWidth}">
                </div>
                <div class="pf-ft">Perfexity.ai v2.0</div>
            `;

            // Show/hide sub-settings
            const setSubVisible = () => SETTINGS_DEFS.filter(s => s.indent).forEach(s => {
                const r = section.querySelector(`[data-key="${s.key}"]`);
                if (r) r.style.display = settings.notificationEnabled ? 'flex' : 'none';
            });
            setSubVisible();

            // Toggle events
            section.querySelectorAll('[data-key]').forEach(row => {
                row.addEventListener('click', e => {
                    e.stopPropagation();
                    const key = row.dataset.key;
                    settings[key] = !settings[key];
                    saveSettings();
                    const btn   = row.querySelector('.pf-tog');
                    const thumb = row.querySelector('.pf-tog-thumb');
                    btn.setAttribute('aria-checked', settings[key]);
                    btn.style.background          = settings[key] ? '#21808D' : 'rgba(128,128,128,.3)';
                    thumb.style.transform         = `translateX(${settings[key] ? '14px' : '0'})`;
                    if (key === 'resizeHandle')        updateHandle();
                    if (key === 'notificationEnabled') { setSubVisible(); if (settings[key]) requestPermission(); }
                    if (key === 'notificationDesktop' && settings[key]) requestPermission();
                    if (key === 'notificationSound'   && settings[key]) playSound();
                });
            });

            // Slider
            const sl  = section.querySelector('#pf-w-sl');
            const val = section.querySelector('#pf-w-val');
            sl.addEventListener('input', e => { val.textContent = `${e.target.value}px`; applyWidth(+e.target.value); });
            sl.addEventListener('click',     e => e.stopPropagation());
            sl.addEventListener('mousedown', e => e.stopPropagation());

            firstSep.after(section);
            firstSep.after(sep);
        });

        obs.observe(document.body, { childList: true, subtree: true });
    }

    // ─── Init ─────────────────────────────────────────────────────────────────────
    injectSettingsMenu();
    setTimeout(redirectToLast,  1500);
    setTimeout(initResize,      1200);
    setTimeout(watchAnswers,    3000);
    setTimeout(scrollOnLoad,    1200);
    setTimeout(requestPermission, 4000);

    // Init AudioContext on first interaction
    document.addEventListener('click', function once() {
        initAudio();
        document.removeEventListener('click', once);
    }, { passive: true });

    // SPA navigation reset
    let lastUrl = location.href;
    setInterval(() => {
        if (location.href === lastUrl) return;
        lastUrl       = location.href;
        hasNotified   = false;
        hasRedirected = false;
        sidebarOuter  = sidebarInner = sidebarWrapper = mainContent = null;
        setTimeout(() => {
            if (findElements()) applyWidth(parseInt(localStorage.getItem(KEY_WIDTH)) || savedWidth);
            updateHandle();
        }, 800);
    }, 1000);

})();