Resizable Sidebar · Smart Scroll · Notifications · Settings in Profile Menu
// ==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);
})();