Greasy Fork is available in English.
Show Perplexity rate limits, settings, and model match/mismatch HUD
// ==UserScript==
// @name Perplexity Status HUD
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Show Perplexity rate limits, settings, and model match/mismatch HUD
// @match https://www.perplexity.ai/*
// @match https://perplexity.ai/*
// @run-at document-end
// @grant none
// @noframes
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const d = document;
const LS = 'ppx_v16_pos';
const MS = 'ppx_mod';
let M = { d: null, s: null, h: [], t: null };
try {
const sv = localStorage.getItem(MS);
if (sv) M = JSON.parse(sv);
} catch (_) {}
function svM() {
try {
localStorage.setItem(MS, JSON.stringify(M));
} catch (_) {}
}
function exM(t) {
if (!t || (!t.includes('display_model') && !t.includes('user_selected_model'))) return null;
try {
const j = JSON.parse(t);
const f = {};
function w(v) {
if (!v || typeof v !== 'object') return;
if (typeof v.display_model === 'string') f.d = v.display_model;
if (typeof v.user_selected_model === 'string') f.s = v.user_selected_model;
if (f.d && f.s) return;
for (const k in v) {
if (Object.prototype.hasOwnProperty.call(v, k)) w(v[k]);
}
}
w(j);
if (f.d || f.s) return f;
} catch (e) {}
const dm = /"display_model"\s*:\s*"([^"]+)"/.exec(t);
const us = /"user_selected_model"\s*:\s*"([^"]+)"/.exec(t);
if (dm || us) return { d: dm ? dm[1] : null, s: us ? us[1] : null };
return null;
}
const oF = window.fetch;
window.fetch = function (...a) {
return oF.apply(this, a).then(r => {
try {
r.clone().text().then(t => {
const m = exM(t);
if (m && (m.d || m.s)) {
if (M.d !== m.d || M.s !== m.s) {
M.d = m.d || M.d;
M.s = m.s || M.s;
M.t = Date.now();
if (!M.h.length || M.h[0].d !== M.d) {
M.h.unshift({ d: M.d, s: M.s, t: M.t });
if (M.h.length > 5) M.h.pop();
}
svM();
upd();
}
}
}).catch(() => {});
} catch (_) {}
return r;
});
};
const existing = d.getElementById('ppx-hud');
if (existing) existing.remove();
const st = d.createElement('style');
st.textContent = `
.px-h {
position: fixed;
z-index: 999999;
background: rgba(15, 23, 42, 0.95);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 12px;
padding: 10px 14px;
box-shadow: 0 10px 25px rgba(0,0,0,0.5);
font-family: -apple-system,system-ui,sans-serif;
user-select: none;
cursor: grab;
min-width: 160px;
transition: opacity 0.2s;
}
.px-h:active { cursor: grabbing; }
.px-h:hover .px-hc { opacity: 1; }
.px-hc {
position: absolute;
top: -8px;
right: -8px;
display: flex;
gap: 4px;
opacity: 0;
transition: opacity 0.2s;
}
.px-b {
width: 20px;
height: 20px;
border-radius: 50%;
border: none;
background: #3b82f6;
color: #fff;
font-size: 12px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
.px-b.c { background: #475569; }
.px-b:hover { background: #2563eb; }
.px-b.c:hover { background: #64748b; }
.px-r {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
}
.px-k {
color: #94a3b8;
font-size: 11px;
text-transform: uppercase;
font-weight: 600;
letter-spacing: 0.5px;
}
.px-v {
color: #f8fafc;
font-size: 13px;
font-weight: 700;
font-family: monospace;
}
.px-w { color: #fbbf24; }
.px-s {
height: 1px;
background: rgba(255,255,255,0.1);
margin: 10px 0 6px;
}
.px-f {
font-size: 11px;
color: #cbd5e1;
text-align: right;
}
.px-m-bg {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0,0,0,0.85);
z-index: 1000000;
backdrop-filter: blur(4px);
}
.px-m {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
width: 90%;
max-width: 600px;
background: #1e293b;
border-radius: 16px;
border: 1px solid rgba(255,255,255,0.1);
box-shadow: 0 25px 60px rgba(0,0,0,0.7);
font-family: -apple-system,system-ui,sans-serif;
overflow: hidden;
max-height: 90vh;
display: flex;
flex-direction: column;
}
.px-mh {
padding: 20px;
background: rgba(255,255,255,0.03);
border-bottom: 1px solid rgba(255,255,255,0.05);
display: flex;
justify-content: space-between;
align-items: center;
}
.px-mt {
font-size: 18px;
font-weight: 700;
color: #f1f5f9;
}
.px-mc { display: flex; gap: 8px; }
.px-mb { padding: 24px; overflow-y: auto; }
.px-g {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-bottom: 20px;
}
.px-c {
background: rgba(255,255,255,0.03);
padding: 14px;
border-radius: 12px;
border: 1px solid rgba(255,255,255,0.05);
}
.px-cl {
font-size: 11px;
color: #94a3b8;
text-transform: uppercase;
font-weight: 600;
margin-bottom: 4px;
}
.px-cv {
font-size: 18px;
font-weight: 700;
color: #f8fafc;
margin-bottom: 2px;
}
.px-cv.l { font-size: 22px; }
.px-cd {
font-size: 12px;
color: #94a3b8;
line-height: 1.3;
}
.px-st {
font-size: 13px;
font-weight: 600;
color: #e2e8f0;
margin-bottom: 10px;
}
.px-tg {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.px-t {
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.1);
padding: 6px 10px;
border-radius: 6px;
font-size: 12px;
color: #cbd5e1;
display: flex;
gap: 6px;
}
.px-tv {
font-family: monospace;
color: #818cf8;
font-weight: 700;
}
.px-sp { animation: s 1s linear infinite; }
@keyframes s { 100% { transform: rotate(360deg); } }
.px-ms {
display: inline-flex;
align-items: center;
justify-content: center;
width: 15px;
height: 15px;
border-radius: 50%;
font-size: 10px;
font-weight: bold;
font-family: system-ui;
line-height: 1;
color: #fff;
}
.px-md-w { background: #64748b; }
.px-md-o { background: #10b981; }
.px-md-e {
background: #ef4444;
animation: p 2s infinite;
}
@keyframes p {
0%,100% { opacity: 1; }
50% { opacity: 0.5; }
}
.px-wb {
word-break: break-all;
line-height: 1.2;
font-size: 13px;
font-family: monospace;
color: #cbd5e1;
margin-bottom: 8px;
}
`;
d.head.appendChild(st);
const h = d.createElement('div');
h.id = 'ppx-hud';
h.className = 'px-h';
const p = JSON.parse(localStorage.getItem(LS) || 'null');
if (p) {
h.style.left = `${p.x}px`;
h.style.top = `${p.y}px`;
} else {
h.style.top = '20px';
h.style.right = '80px';
}
h.innerHTML = '<div style="font-size:12px;color:#94a3b8;text-align:center">Loading...</div>';
d.body.appendChild(h);
let isDragging = false;
let sX, sY, iL, iT;
h.addEventListener('mousedown', e => {
if (e.target.tagName === 'BUTTON') return;
isDragging = false;
sX = e.clientX;
sY = e.clientY;
iL = h.offsetLeft;
iT = h.offsetTop;
const mH = m => {
const dX = m.clientX - sX;
const dY = m.clientY - sY;
if (Math.sqrt(dX * dX + dY * dY) > 5) {
isDragging = true;
h.style.left = `${iL + dX}px`;
h.style.top = `${iT + dY}px`;
h.style.right = 'auto';
}
};
const uH = () => {
window.removeEventListener('mousemove', mH);
window.removeEventListener('mouseup', uH);
if (isDragging) {
localStorage.setItem(LS, JSON.stringify({ x: h.offsetLeft, y: h.offsetTop }));
}
};
window.addEventListener('mousemove', mH);
window.addEventListener('mouseup', uH);
});
let cd;
function fd() {
return Promise.all([
fetch('/rest/rate-limit/all').then(r => r.json()),
fetch('/rest/user/settings').then(r => r.json())
]).then(a => {
cd = a;
return a;
});
}
function uM() {
const mt = M.d && M.s && M.d === M.s;
const ic = !M.d ? '⏯' : (mt ? '✓' : '!');
const cl = !M.d ? 'px-md-w' : (mt ? 'px-md-o' : 'px-md-e');
const tt = !M.d ? 'Awaiting query...' : `Req: ${M.s}\nGot: ${M.d}`;
return `
<div class="px-r" title="${tt}">
<span class="px-k">AI Model</span>
<span class="px-ms ${cl}">${ic}</span>
</div>
`;
}
function upd() {
if (!cd) return;
const r = cd[0];
const u = cd[1];
const rw = (k, v, w) => `
<div class="px-r">
<span class="px-k">${k}</span>
<span class="px-v ${w ? 'px-w' : ''}">${v}</span>
</div>
`;
h.innerHTML = `
<div class="px-hc">
<button class="px-b" id="px-rb" title="Reload">⟳</button>
<button class="px-b c" onclick="this.closest('#ppx-hud').remove()" title="Close">×</button>
</div>
${rw('Uploads', u.upload_limit, u.upload_limit < 10)}
${rw('Pro search', r.remaining_pro)}
${rw('Research', r.remaining_research)}
${rw('Labs', r.remaining_labs)}
${uM()}
<div class="px-s"></div>
<div class="px-f">Total queries: ${(u.query_count || 0).toLocaleString()}</div>
`;
d.getElementById('px-rb').onclick = e => {
e.stopPropagation();
rf();
};
h.onclick = e => {
if (!isDragging && e.target.tagName !== 'BUTTON') oM();
};
}
function gM(r, u) {
let sr = '';
let cn = '';
if (r.sources && r.sources.source_to_limit) {
const l = [];
Object.keys(r.sources.source_to_limit).forEach(k => {
const s = r.sources.source_to_limit[k];
if (s.monthly_limit > 0) {
l.push({
n: k.replace(/_mcp_cashmere|_mcp_merge|_direct/g, '').replace(/_/g, ' '),
v: `${s.remaining}/${s.monthly_limit}`
});
}
});
if (l.length) {
sr = `
<div style="margin-bottom:20px">
<div class="px-st">📊 Source Limits (Monthly)</div>
<div class="px-tg">
${l.map(i => `
<div class="px-t">
<span style="text-transform:capitalize">${i.n}</span>
<span class="px-tv">${i.v}</span>
</div>
`).join('')}
</div>
</div>
`;
}
}
if (u.connectors && u.connectors.connectors) {
const cL = u.connectors.connectors.filter(c => c.connected).map(c => c.name);
cn = cL.length ? cL.join(', ') : 'None connected';
}
let hs = '';
if (M.h.length) {
hs = M.h.map(i => {
const tm = new Date(i.t).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
const c = i.d === i.s ? '#10b981' : '#ef4444';
return `
<div style="font-size:12px;color:#94a3b8;padding:4px 0;border-bottom:1px solid rgba(255,255,255,0.05);display:flex;justify-content:space-between">
<span>${tm}</span>
<span style="font-family:monospace;color:${c}">${i.d}</span>
</div>
`;
}).join('');
}
const mt = M.d && M.s && M.d === M.s;
const ms = !M.d ? 'Awaiting query...' : (mt ? 'MATCH ✓' : 'MISMATCH ⚠️');
const mc = !M.d ? '#94a3b8' : (mt ? '#10b981' : '#ef4444');
return `
<div class="px-g">
<div class="px-c">
<div class="px-cl">File Uploads</div>
<div class="px-cv l ${u.upload_limit < 10 ? 'px-w' : ''}">${u.upload_limit ?? 'N/A'}</div>
<div class="px-cd">Manual thread attachments<br>(Weekly limit)</div>
</div>
<div class="px-c">
<div class="px-cl">Pro Search</div>
<div class="px-cv l">${r.remaining_pro}</div>
<div class="px-cd">Standard Pro queries<br>(Daily soft limit)</div>
</div>
<div class="px-c">
<div class="px-cl">Deep Research</div>
<div class="px-cv l">${r.remaining_research}</div>
<div class="px-cd">Comprehensive reports<br>(Monthly limit)</div>
</div>
<div class="px-c">
<div class="px-cl">Labs</div>
<div class="px-cv l">${r.remaining_labs}</div>
<div class="px-cd">Create files & apps<br>(Monthly limit)</div>
</div>
</div>
<div style="margin-bottom:20px;border:1px solid ${mc}40;border-radius:12px;background:rgba(255,255,255,0.02);padding:16px">
<div class="px-st">🤖 AI Model Monitoring</div>
<div class="px-g" style="margin-bottom:0;gap:20px">
<div style="display:flex;flex-direction:column;justify-content:center">
<div style="font-size:16px;font-weight:700;color:${mc};margin-bottom:12px">${ms}</div>
<div class="px-cl">Selected</div>
<div class="px-wb">${M.s || '—'}</div>
<div class="px-cl" style="margin-top:8px">Received</div>
<div class="px-wb">${M.d || '—'}</div>
</div>
<div>
<div class="px-cl" style="margin-bottom:8px">Recent History</div>
${hs || '<div style="font-size:12px;color:#64748b">No queries yet</div>'}
</div>
</div>
</div>
<div style="margin-bottom:20px">
<div class="px-st">🛡️ Account & Features</div>
<div class="px-g" style="grid-template-columns:1fr 1fr 1fr 1fr;gap:8px">
<div class="px-c" style="padding:10px">
<div class="px-cl">Tier</div>
<div class="px-cv" style="font-size:14px">${(u.subscription_tier === 'unknown' ? 'Pro' : u.subscription_tier).toUpperCase()}</div>
</div>
<div class="px-c" style="padding:10px">
<div class="px-cl">Training</div>
<div class="px-cv" style="font-size:14px">${u.disable_training ? 'OFF' : 'ON ⚠️'}</div>
</div>
<div class="px-c" style="padding:10px">
<div class="px-cl">Img Model</div>
<div class="px-cv" style="font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis" title="${u.default_image_generation_model}">
${u.default_image_generation_model || 'Default'}
</div>
</div>
<div class="px-c" style="padding:10px">
<div class="px-cl">Pages</div>
<div class="px-cv" style="font-size:14px">${u.pages_limit || 100} Cap</div>
</div>
</div>
<div style="font-size:12px;color:#cbd5e1;margin-top:8px">
<strong>Connectors:</strong> ${cn}
</div>
</div>
${sr}
<div style="display:flex;justify-content:center;gap:12px;font-size:13px;color:#94a3b8;margin-top:20px;padding-top:16px;border-top:1px solid rgba(255,255,255,0.1)">
<span>Total Queries: <strong style="color:#e2e8f0">${(u.query_count || 0).toLocaleString()}</strong></span>
<span>•</span>
<span>${u.time_zone || 'Unknown Region'}</span>
</div>
`;
}
function oM() {
if (!cd) return;
const r = cd[0];
const u = cd[1];
const bg = d.createElement('div');
bg.className = 'px-m-bg';
bg.id = 'px-bg';
bg.onclick = e => {
if (e.target === bg) bg.remove();
};
bg.innerHTML = `
<div class="px-m">
<div class="px-mh">
<div class="px-mt">⚡ Perplexity Status</div>
<div class="px-mc">
<button class="px-b" style="width:28px;height:28px;border-radius:6px;background:rgba(255,255,255,0.1);font-size:16px" id="px-mr" title="Reload">⟳</button>
<button class="px-b" style="width:28px;height:28px;border-radius:6px;background:rgba(255,255,255,0.1);font-size:16px" onclick="this.closest('.px-m-bg').remove()" title="Close">×</button>
</div>
</div>
<div class="px-mb" id="px-mb-c">
${gM(r, u)}
</div>
</div>
`;
d.body.appendChild(bg);
d.getElementById('px-mr').onclick = () => rf();
}
function rf() {
const rb = d.getElementById('px-rb');
const mr = d.getElementById('px-mr');
if (rb) rb.classList.add('px-sp');
if (mr) mr.classList.add('px-sp');
fd().then(a => {
upd();
const mc = d.getElementById('px-mb-c');
if (mc) mc.innerHTML = gM(a[0], a[1]);
}).finally(() => {
if (rb) rb.classList.remove('px-sp');
if (mr) mr.classList.remove('px-sp');
});
}
rf();
})();