Draggable, resizable usage-limits panel with dark/light theme memory
// ==UserScript==
// @name Perplexity – Usage Limits & Settings
// @namespace https://perplexity.ai/
// @version 5.0.4
// @license MIT
// @description Draggable, resizable usage-limits panel with dark/light theme memory
// @author userscript
// @match https://www.perplexity.ai/*
// @grant none
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
// ── CONFIG ─────────────────────────────────────────────────────────────────
const SEARCH_LIMITS = {
remaining_pro: { label: 'Pro', total: 200 },
remaining_research: { label: 'Research', total: 20 },
remaining_agentic_research: { label: 'Agentic', total: null },
remaining_labs: { label: 'Labs', total: 25 },
};
const SETTINGS_FIELDS = [
{ key: 'upload_limit', label: 'Upload Limit', type: 'number' },
{ key: 'pages_limit', label: 'Pages Limit', type: 'number' },
{ key: 'create_limit', label: 'Create Limit', type: 'number' },
{ key: 'article_image_upload_limit', label: 'Img Upload Limit', type: 'number' },
{ key: 'daily_attachment_limit', label: 'Daily Attachments', type: 'number' },
{ key: 'disable_training', label: 'Training Disabled', type: 'bool' },
{ key: 'has_ai_profile', label: 'AI Profile', type: 'bool' },
{ key: 'default_model', label: 'Default Model', type: 'string' },
{ key: 'default_image_generation_model', label: 'Image Model', type: 'string' },
{ key: 'default_video_generation_model', label: 'Video Model', type: 'string' },
];
const MCP_NAMES = {
asana_mcp_merge: 'Asana', box: 'Box',
cbinsights_mcp_cashmere: 'CBInsights', confluence_mcp_merge: 'Confluence',
crunchbase: 'Crunchbase', dropbox: 'Dropbox',
factset: 'FactSet', gcal: 'Google Calendar',
github_mcp_direct: 'GitHub', google_drive: 'Google Drive',
jira_mcp_merge: 'Jira', linear_alt: 'Linear',
microsoft_teams_mcp_merge: 'MS Teams', notion_mcp: 'Notion',
onedrive: 'OneDrive', org: 'Organization',
outlook: 'Outlook', pitchbook_mcp_cashmere: 'Pitchbook',
scholar: 'Scholar', sharepoint: 'SharePoint',
slack_direct: 'Slack', social: 'Social',
statista_mcp_cashmere: 'Statista', web: 'Web',
wiley_mcp_cashmere: 'Wiley',
};
const INPUT_SEL = [
'textarea[placeholder]', '[data-testid="search-input"]',
'div[contenteditable="true"]', '[role="textbox"]', 'form textarea',
].join(', ');
// ── PREFERENCES (localStorage) ─────────────────────────────────────────────
const PREFS_KEY = 'pplx_panel_v5';
const loadPrefs = () => { try { return JSON.parse(localStorage.getItem(PREFS_KEY)) || {}; } catch { return {}; } };
const savePrefs = patch => { try { localStorage.setItem(PREFS_KEY, JSON.stringify({ ...loadPrefs(), ...patch })); } catch {} };
// ── CSS ────────────────────────────────────────────────────────────────────
document.head.appendChild(Object.assign(document.createElement('style'), { textContent: `
/* ── CSS variables — dark (default) ─────────────────────── */
#pplx-panel {
--bg: #1c1c1c;
--bg-head: rgba(255,255,255,0.025);
--bg-foot: rgba(255,255,255,0.01);
--border: rgba(255,255,255,0.09);
--sep: rgba(255,255,255,0.06);
--track: rgba(255,255,255,0.08);
--hover: rgba(255,255,255,0.035);
--text: #ededed;
--lbl: #95959d;
--sec: #a3a3a4;
--dim: #a0a0aa;
--shadow: 0 0 0 0.5px rgba(255,255,255,0.04), 0 4px 8px rgba(0,0,0,0.3), 0 14px 32px rgba(0,0,0,0.5);
--resize-bg: rgba(255,255,255,0.12);
--cursor-drag: grab;
}
/* ── CSS variables — light ───────────────────────────────── */
#pplx-panel.pp-light {
--bg: #ffffff;
--bg-head: rgba(0,0,0,0.015);
--bg-foot: rgba(0,0,0,0.01);
--border: rgba(0,0,0,0.1);
--sep: rgba(0,0,0,0.07);
--track: rgba(0,0,0,0.09);
--hover: rgba(0,0,0,0.03);
--text: #3f4145;
--lbl: #6b7280;
--sec: #5f636c;
--dim: #cecece;
--time: #616872
--shadow: 0 0 0 1px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.1), 0 12px 28px rgba(0,0,0,0.08);
--resize-bg: rgba(0,0,0,0.12);
--cursor-drag: grab;
}
/* ── Panel shell ─────────────────────────────────────────── */
#pplx-panel {
position: fixed;
z-index: 99999;
width: 222px;
min-width: 185px;
min-height: 180px;
max-height: calc(100vh - 20px);
display: flex;
flex-direction: column;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 14px;
overflow: hidden;
color: var(--text);
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
font-size: 12.5px;
line-height: 1.45;
box-shadow: var(--shadow);
transition: opacity 0.2s ease, box-shadow 0.2s ease;
user-select: none;
}
#pplx-panel.pp-hidden { opacity: 0; pointer-events: none; }
#pplx-panel.pp-dragging { transition: none; cursor: grabbing !important; box-shadow: var(--shadow), 0 0 0 2px #20b2aa44; }
/* ── Header ─────────────────────────────────────────────── */
.pp-head {
flex-shrink: 0;
display: flex; align-items: center; justify-content: space-between;
padding: 9px 11px 9px 13px;
background: var(--bg-head);
border-bottom: 1px solid var(--sep);
cursor: var(--cursor-drag);
}
.pp-head-left { display: flex; align-items: center; gap: 7px; }
.pp-head-label {
font-size: 11px; font-weight: 600; letter-spacing: 0.3px;
color: var(--lbl);
pointer-events: none;
}
#pplx-countdown { font-size: 10px; font-weight: 400; color: #fbbf24; margin-left: 1px; }
.pp-dot {
flex-shrink: 0;
width: 6px; height: 6px; border-radius: 50%;
background: var(--dim);
transition: background 0.35s, box-shadow 0.35s;
pointer-events: none;
}
.pp-dot.live { background: #20b2aa; box-shadow: 0 0 0 3px rgba(32,178,170,0.2); }
.pp-dot.pending { background: #fbbf24; box-shadow: 0 0 0 3px rgba(251,191,36,0.2); }
.pp-head-actions { display: flex; gap: 1px; flex-shrink: 0; }
.pp-btn-icon {
appearance: none; background: none; border: none;
padding: 3px 5px; border-radius: 6px; cursor: pointer;
color: var(--sec); font-size: 13px; line-height: 1;
transition: color 0.15s, background 0.15s;
}
.pp-btn-icon:hover { color: var(--text); background: var(--hover); }
/* ── Scrollable body ─────────────────────────────────────── */
.pp-body {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
min-height: 60px;
padding: 4px 0 2px;
scrollbar-width: thin;
scrollbar-color: var(--dim) transparent;
}
/* ── Section label ───────────────────────────────────────── */
.pp-sec {
padding: 9px 13px 4px;
font-size: 9.5px; font-weight: 700;
letter-spacing: 0.9px; text-transform: uppercase;
color: var(--sec);
}
.pp-sec.first { padding-top: 6px; }
.pp-sep { height: 1px; margin: 4px 0; background: var(--sep); }
/* ── Standard row ────────────────────────────────────────── */
.pp-row {
display: flex; align-items: center; justify-content: space-between;
padding: 3.5px 13px; gap: 8px; min-height: 24px;
}
.pp-row:hover { background: var(--hover); }
.pp-lbl { color: var(--lbl); font-size: 11.5px; white-space: nowrap; flex-shrink: 0; }
.pp-val { font-size: 12px; font-weight: 600; text-align: right; color: var(--text); }
.pp-val.v-zero { color: var(--dim); font-weight: 400; font-style: italic; }
.pp-val.v-crit { color: #f87171; }
.pp-val.v-low { color: #fb923c; }
.pp-val.v-warn { color: #fbbf24; }
.pp-val.v-bool-y { color: #20b2aa; }
.pp-val.v-bool-n { color: var(--lbl); font-weight: 400; }
.pp-val.v-str { color: #818cf8; font-weight: 500; font-size: 11px; }
/* ── Search / MCP row (with bar) ─────────────────────────── */
.pp-bar-row {
padding: 4px 13px 7px;
}
.pp-bar-row:hover { background: var(--hover); }
.pp-bar-top {
display: flex; align-items: baseline;
justify-content: space-between;
margin-bottom: 4px;
}
.pp-bar-count { font-size: 12px; font-weight: 600; color: var(--text); }
.pp-bar-total { font-size: 10px; font-weight: 400; color: var(--dim); }
.pp-track {
height: 3px;
background: var(--track);
border-radius: 99px; overflow: hidden;
}
.pp-fill { height: 100%; border-radius: 99px; transition: width 0.55s cubic-bezier(.4,0,.2,1); }
/* ── Footer ─────────────────────────────────────────────── */
.pp-foot {
flex-shrink: 0;
display: flex; align-items: center; justify-content: space-between;
padding: 5px 11px 6px 13px;
border-top: 1px solid var(--sep);
background: var(--bg-foot);
}
.pp-timestamp { font-size: 9.5px; color: var(--time); }
.pp-btn-sm {
appearance: none; background: none; cursor: pointer;
border: 1px solid var(--border);
border-radius: 6px; padding: 2px 9px;
font-size: 10px; color: var(--sec);
font-family: inherit;
transition: border-color 0.15s, color 0.15s;
}
.pp-btn-sm:hover { border-color: var(--lbl); color: var(--text); }
/* ── Resize handle (bottom-right corner) ─────────────────── */
.pp-resize {
position: absolute;
bottom: 0; right: 0;
width: 16px; height: 16px;
cursor: se-resize;
z-index: 10;
/* subtle diagonal lines matching Perplexity's corner affordance */
background:
linear-gradient(135deg,
transparent 30%,
var(--resize-bg) 30%, var(--resize-bg) 40%,
transparent 40%,
transparent 55%,
var(--resize-bg) 55%, var(--resize-bg) 65%,
transparent 65%
);
border-radius: 0 0 14px 0;
}
/* ── Toggle pill ─────────────────────────────────────────── */
#pplx-toggle {
position: fixed; z-index: 99998;
background: var(--bg, #1c1c1c);
border: 1px solid var(--border, rgba(255,255,255,0.09));
border-radius: 99px;
height: 27px; padding: 0 11px;
display: flex; align-items: center; gap: 5px;
cursor: pointer;
color: var(--sec, #52525b);
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
font-size: 10.5px; font-weight: 600; letter-spacing: 0.3px;
white-space: nowrap;
box-shadow: 0 2px 8px rgba(0,0,0,0.35);
transition: border-color 0.15s, color 0.15s;
}
#pplx-toggle.pp-light {
background: #ffffff;
border-color: rgba(0,0,0,0.1);
color: #6b7280;
box-shadow: 0 2px 8px rgba(0,0,0,0.12);
}
#pplx-toggle:hover { border-color: var(--lbl, #71717a); color: var(--text, #ededed); }
.pp-toggle-dot {
width: 5px; height: 5px; border-radius: 50%;
background: #3f3f46;
transition: background 0.35s;
flex-shrink: 0;
}
/* ── States ─────────────────────────────────────────────── */
.pp-loading { padding: 18px 13px; text-align: center; font-size: 11px; color: var(--sec); }
.pp-error { padding: 8px 13px; font-size: 11px; color: #f87171; }
` }));
// ── HELPERS ────────────────────────────────────────────────────────────────
const fillColor = pct => pct > 60 ? '#20b2aa' : pct > 25 ? '#fbbf24' : '#f87171';
function searchValClass(n) {
if (n === 0) return 'v-zero';
if (n <= 3) return 'v-crit';
if (n <= 10) return 'v-low';
if (n <= 25) return 'v-warn';
return '';
}
function settingRender(v, type) {
if (v === null || v === undefined) return { html: '—', cls: 'v-zero' };
if (type === 'bool') return { html: v ? '✓' : '✗', cls: v ? 'v-bool-y' : 'v-bool-n' };
if (type === 'string') return { html: v, cls: 'v-str' };
return { html: String(v), cls: '' };
}
function barRowHTML(label, remaining, total) {
const pct = total > 0 ? Math.round((remaining / total) * 100) : 0;
const col = fillColor(pct);
const vcls = searchValClass(remaining);
return `<div class="pp-bar-row">
<div class="pp-bar-top">
<span class="pp-lbl">${label}</span>
<span class="pp-bar-count ${vcls}">${remaining}<span class="pp-bar-total"> / ${total}</span></span>
</div>
<div class="pp-track"><div class="pp-fill" style="width:${pct}%;background:${col}"></div></div>
</div>`;
}
// ── BUILD HTML ─────────────────────────────────────────────────────────────
function buildHTML(rate, settings) {
let h = '<div class="pp-sec first">Searches</div>';
if (rate) {
Object.entries(SEARCH_LIMITS).forEach(([key, { label, total }]) => {
const remaining = rate[key];
if (remaining === undefined) return;
if (total !== null) {
h += barRowHTML(label, remaining, total);
} else {
const vcls = searchValClass(remaining);
h += `<div class="pp-row"><span class="pp-lbl">${label}</span><span class="pp-val ${vcls}">${remaining}</span></div>`;
}
});
} else {
h += '<div class="pp-error">⚠ Rate limit data unavailable</div>';
}
const mcpRaw = rate?.sources?.source_to_limit;
if (mcpRaw) {
const active = Object.entries(mcpRaw).filter(([, v]) => v.monthly_limit !== null && v.monthly_limit !== 0);
if (active.length) {
h += '<div class="pp-sep"></div><div class="pp-sec">MCP Sources</div>';
active.forEach(([key, { monthly_limit, remaining }]) => {
h += barRowHTML(MCP_NAMES[key] || key, remaining, monthly_limit);
});
}
}
h += '<div class="pp-sep"></div><div class="pp-sec">Settings</div>';
if (settings) {
SETTINGS_FIELDS.forEach(({ key, label, type }) => {
const raw = settings[key];
if (raw === undefined) return;
const { html: vhtml, cls } = settingRender(raw, type);
h += `<div class="pp-row"><span class="pp-lbl">${label}</span><span class="pp-val ${cls}">${vhtml}</span></div>`;
});
} else {
h += '<div class="pp-error">⚠ Settings data unavailable</div>';
}
return h;
}
// ── FETCH & RENDER ─────────────────────────────────────────────────────────
async function fetchAndRender() {
const panel = document.getElementById('pplx-panel');
if (!panel) return;
const body = panel.querySelector('.pp-body');
const dot = panel.querySelector('.pp-dot');
const tdot = document.querySelector('.pp-toggle-dot');
const ts = panel.querySelector('.pp-timestamp');
const cd = document.getElementById('pplx-countdown');
if (dot) dot.classList.remove('live', 'pending');
if (tdot) tdot.style.background = '#3f3f46';
const [r1, r2] = await Promise.allSettled([
fetch('/rest/rate-limit/all', { credentials: 'include' }).then(r => r.json()),
fetch('/rest/user/settings', { credentials: 'include' }).then(r => r.json()),
]);
const rate = r1.status === 'fulfilled' ? r1.value : null;
const settings = r2.status === 'fulfilled' ? r2.value : null;
if (body) body.innerHTML = buildHTML(rate, settings);
if (dot) dot.classList.add('live');
if (tdot) tdot.style.background = '#20b2aa';
if (ts) ts.textContent = new Date().toLocaleTimeString('en-CA', { hour:'2-digit', minute:'2-digit', second:'2-digit' });
if (cd) cd.textContent = '';
}
// ── POST-QUERY REFRESH ─────────────────────────────────────────────────────
let _rt = null, _ct = null;
function scheduleRefresh() {
clearTimeout(_rt); clearInterval(_ct);
const dot = document.querySelector('#pplx-panel .pp-dot');
const tdot = document.querySelector('.pp-toggle-dot');
const cd = document.getElementById('pplx-countdown');
if (dot) { dot.classList.remove('live'); dot.classList.add('pending'); }
if (tdot) tdot.style.background = '#fbbf24';
let s = 3;
if (cd) cd.textContent = ` (${s}s)`;
_ct = setInterval(() => { s--; if (cd) cd.textContent = s > 0 ? ` (${s}s)` : ''; if (s <= 0) clearInterval(_ct); }, 1000);
_rt = setTimeout(() => { clearInterval(_ct); fetchAndRender(); }, 3000);
}
function hookSubmitEvents() {
const _orig = window.fetch;
window.fetch = function (...a) {
try {
const url = typeof a[0] === 'string' ? a[0] : a[0] instanceof Request ? a[0].url : '';
if ((a[1]?.method || 'GET').toUpperCase() === 'POST' &&
url.includes('/rest/') && !url.includes('/rest/rate-limit') && !url.includes('/rest/user/settings'))
scheduleRefresh();
} catch (_) {}
return _orig.apply(this, a);
};
document.addEventListener('keydown', e => {
if (e.key === 'Enter' && !e.shiftKey &&
(e.target.matches(INPUT_SEL) || !!e.target.closest('[role="textbox"],form')))
scheduleRefresh();
}, true);
document.addEventListener('click', e => {
const b = e.target.closest('button');
if (!b) return;
const lbl = (b.getAttribute('aria-label') || '').toLowerCase();
if (b.type === 'submit' || lbl.includes('send') || lbl.includes('submit') || lbl.includes('search'))
scheduleRefresh();
}, true);
}
// ── THEME ──────────────────────────────────────────────────────────────────
function applyTheme(isDark) {
const panel = document.getElementById('pplx-panel');
const toggle = document.getElementById('pplx-toggle');
if (!panel) return;
panel.classList.toggle('pp-light', !isDark);
if (toggle) toggle.classList.toggle('pp-light', !isDark);
const btn = panel.querySelector('[data-a="theme"]');
if (btn) btn.textContent = isDark ? '☀' : '☾';
}
function toggleTheme() {
const panel = document.getElementById('pplx-panel');
if (!panel) return;
const nowDark = panel.classList.contains('pp-light'); // was light → going dark
applyTheme(nowDark);
savePrefs({ dark: nowDark });
}
// ── DRAG ───────────────────────────────────────────────────────────────────
function makeDraggable(panel) {
const handle = panel.querySelector('.pp-head');
let sx = 0, sy = 0;
handle.addEventListener('mousedown', e => {
if (e.target.closest('button')) return;
e.preventDefault();
const rect = panel.getBoundingClientRect();
sx = e.clientX - rect.left;
sy = e.clientY - rect.top;
panel.classList.add('pp-dragging');
const onMove = e => {
let x = e.clientX - sx;
let y = e.clientY - sy;
x = Math.max(0, Math.min(window.innerWidth - panel.offsetWidth, x));
y = Math.max(0, Math.min(window.innerHeight - panel.offsetHeight, y));
panel.style.left = x + 'px';
panel.style.top = y + 'px';
panel.style.bottom = 'auto';
panel.style.right = 'auto';
};
const onUp = () => {
panel.classList.remove('pp-dragging');
savePrefs({ x: parseInt(panel.style.left), y: parseInt(panel.style.top), anchored: true });
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
};
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
}
// ── RESIZE ─────────────────────────────────────────────────────────────────
function makeResizable(panel) {
const handle = panel.querySelector('.pp-resize');
handle.addEventListener('mousedown', e => {
e.preventDefault(); e.stopPropagation();
const sx = e.clientX, sy = e.clientY;
const sw = panel.offsetWidth, sh = panel.offsetHeight;
const onMove = e => {
const w = Math.max(185, sw + e.clientX - sx);
const h = Math.max(180, sh + e.clientY - sy);
panel.style.width = w + 'px';
panel.style.height = h + 'px';
};
const onUp = () => {
savePrefs({ w: panel.offsetWidth, h: panel.offsetHeight });
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
};
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
}
// ── POSITION (respects saved anchor) ───────────────────────────────────────
function positionPanel() {
const panel = document.getElementById('pplx-panel');
const toggle = document.getElementById('pplx-toggle');
if (!panel || !toggle) return;
const prefs = loadPrefs();
// Panel: use saved position if user moved it, else auto-anchor to input
if (prefs.anchored && prefs.x !== undefined) {
const x = Math.max(0, Math.min(window.innerWidth - panel.offsetWidth, prefs.x));
const y = Math.max(0, Math.min(window.innerHeight - panel.offsetHeight, prefs.y || 0));
panel.style.left = x + 'px'; panel.style.top = y + 'px'; panel.style.bottom = 'auto';
} else {
const inp = document.querySelector(INPUT_SEL);
if (inp) {
const r = inp.getBoundingClientRect();
const pw = panel.offsetWidth || 222;
const lft = Math.max(8, r.left - pw - 12);
const ph = panel.offsetHeight || 380;
const top = Math.min(window.innerHeight - ph - 8, Math.max(8, r.top + r.height / 2 - ph / 2));
panel.style.left = lft + 'px'; panel.style.top = top + 'px'; panel.style.bottom = 'auto';
} else {
panel.style.left = '12px'; panel.style.bottom = '90px'; panel.style.top = 'auto';
}
}
// Toggle always tracks the input box
const inp = document.querySelector(INPUT_SEL);
if (inp) {
const r = inp.getBoundingClientRect();
const pw = panel.offsetWidth || 222;
const lft = Math.max(8, r.left - pw - 12);
toggle.style.left = lft + 'px';
toggle.style.bottom = (window.innerHeight - r.top + 10) + 'px';
toggle.style.top = 'auto';
} else {
toggle.style.left = '12px'; toggle.style.bottom = '58px'; toggle.style.top = 'auto';
}
}
// ── INJECT ─────────────────────────────────────────────────────────────────
function inject() {
if (document.getElementById('pplx-panel')) return;
const prefs = loadPrefs();
const isDark = prefs.dark !== false;
const panel = document.createElement('div');
panel.id = 'pplx-panel';
panel.innerHTML = `
<div class="pp-head">
<div class="pp-head-left">
<span class="pp-dot"></span>
<span class="pp-head-label">Usage Limits</span>
<span id="pplx-countdown"></span>
</div>
<div class="pp-head-actions">
<button class="pp-btn-icon" data-a="theme" title="Toggle theme">☀</button>
<button class="pp-btn-icon" data-a="refresh" title="Refresh">↻</button>
<button class="pp-btn-icon" data-a="close" title="Hide">✕</button>
</div>
</div>
<div class="pp-body"><div class="pp-loading">Loading…</div></div>
<div class="pp-foot">
<span class="pp-timestamp">—</span>
<button class="pp-btn-sm" data-a="refresh">Refresh</button>
</div>
<div class="pp-resize" title="Resize"></div>
`;
// Restore saved size
if (prefs.w) panel.style.width = prefs.w + 'px';
if (prefs.h) panel.style.height = prefs.h + 'px';
document.body.appendChild(panel);
const toggle = document.createElement('button');
toggle.id = 'pplx-toggle';
toggle.innerHTML = '<span class="pp-toggle-dot"></span>Limits';
document.body.appendChild(toggle);
// Apply saved theme
applyTheme(isDark);
// Events
panel.addEventListener('click', e => {
const a = e.target.closest('[data-a]')?.dataset.a;
if (a === 'close') panel.classList.add('pp-hidden');
if (a === 'refresh') fetchAndRender();
if (a === 'theme') toggleTheme();
});
toggle.addEventListener('click', () => panel.classList.toggle('pp-hidden'));
makeDraggable(panel);
makeResizable(panel);
positionPanel();
fetchAndRender();
window.addEventListener('resize', positionPanel);
}
// ── BOOT ───────────────────────────────────────────────────────────────────
hookSubmitEvents();
const boot = () => setTimeout(inject, 1600);
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot);
else boot();
let _lastUrl = location.href;
new MutationObserver(() => {
if (location.href !== _lastUrl) {
_lastUrl = location.href;
setTimeout(() => { if (!document.getElementById('pplx-panel')) inject(); else positionPanel(); }, 1200);
} else positionPanel();
}).observe(document.body, { childList: true, subtree: false });
window.addEventListener('popstate', () => setTimeout(positionPanel, 600));
})();