Perplexity Status HUD

Show Perplexity rate limits, settings, and model match/mismatch HUD

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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