Claude Inline Usage Tracker

Minimal usage bar below Claude input

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

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

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

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

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==UserScript==
// @name         Claude Inline Usage Tracker
// @namespace    usage-tracker-of-claude
// @author       Niko
// @version      2.7
// @description  Minimal usage bar below Claude input
// @match        https://claude.ai/*
// @grant        none
// @run-at       document-idle
// @license      GNU General Public License v3.0
// ==/UserScript==

(() => {
  'use strict';

  const ID = 'cut', SID = 'cut-style', API = '/api/organizations';
  const POLL = 60_000, HOVER_REFRESH = 30_000, MIN_GAP = 15_000;
  const WARN = 60, DANGER = 80;
  const A = 'cut-anchor', H = 'cut-hover';

  const ROWS = [
    ['five_hour',      'Current Session'],
    ['seven_day',      'Weekly Limit (All)'],
    ['seven_day_opus', 'Weekly Limit (Opus)'],
  ];

  const S = { org: null, inflight: null, last: null, lastAt: 0, anchor: null, ui: null, poll: 0, sched: 0, mo: null };

  const clamp = (v) => (v = +v || 0) < 0 ? 0 : v > 100 ? 100 : v;
  const fmt = (iso) => {
    if (!iso) return 'N/A';
    const m = Math.round((new Date(iso).getTime() - Date.now()) / 60000);
    if (m < 1) return 'Resetting soon';
    if (m < 60) return `In ${m} min`;
    const h = (m / 60) | 0;
    return h < 24 ? `In ${h} hr` : `In ${((h / 24) | 0)} days`;
  };

  const jget = (u) => fetch(u, { credentials: 'include' }).then(r => { if (!r.ok) throw new Error(r.status); return r.json(); });

  async function orgId() {
    if (S.org) return S.org;
    const orgs = await jget(API);
    return (S.org = orgs?.[0]?.uuid ?? null);
  }

  function usage(force) {
    const now = Date.now();
    if (!force && now - S.lastAt < MIN_GAP) return Promise.resolve(S.last);
    if (S.inflight) return S.inflight;

    return (S.inflight = (async () => {
      try {
        const id = await orgId();
        if (!id) return S.last;
        const d = await jget(`${API}/${id}/usage`);
        if (d) { S.last = d; S.lastAt = Date.now(); }
        return S.last;
      } catch (e) {
        S.org = null;
        return S.last;
      } finally {
        S.inflight = null;
      }
    })());
  }

  function style() {
    if (document.getElementById(SID)) return;
    const s = document.createElement('style');
    s.id = SID;
    s.textContent = `
#${ID}{position:absolute;inset:auto 16px -15px;z-index:30;font-family:var(--font-ui,system-ui,-apple-system,Segoe UI,Roboto,sans-serif);color:hsl(var(--text-100))}
#${ID} .t{height:12px;display:flex;align-items:center;cursor:pointer}
#${ID} .b{width:100%;height:3px;background:hsla(var(--border-300)/.12);border-radius:999px;overflow:hidden;transition:height .16s ease}
#${ID} .t:hover .b{height:4px}
#${ID} .f{height:100%;width:0%;background:hsl(var(--brand-000));transition:width .25s ease}
#${ID} .w{background:hsl(var(--warning-100))}
#${ID} .d{background:hsl(var(--danger-100))}
#${ID} .p{position:absolute;bottom:14px;left:0;right:0;background:hsl(var(--bg-000));border-radius:16px;display:flex;flex-direction:column;gap:10px;padding:12px 14px 10px;box-shadow:0 .25rem 1.25rem hsl(var(--always-black)/3.5%),0 0 0 .5px hsla(var(--border-300)/.15);opacity:0;visibility:hidden;pointer-events:none;transform:translateY(8px);transition:opacity .16s ease,transform .16s ease,visibility 0s linear .16s}
#${ID} .t:hover + .p{opacity:1;visibility:visible;transform:translateY(0);transition:opacity .16s ease,transform .16s ease}
#${ID} .hh{display:flex;justify-content:space-between;align-items:flex-end;gap:12px;margin-bottom:6px;font-size:13px;line-height:1.1}
#${ID} .l{font-weight:550;color:hsl(var(--text-100))}
#${ID} .m{font-size:12px;font-weight:430;color:hsl(var(--text-500));white-space:nowrap}
#${ID} .k{width:100%;height:6px;background:hsla(var(--border-300)/.12);border-radius:999px;overflow:hidden}
.${A}{transition:background-color .2s ease,box-shadow .2s ease,border-color .2s ease}
.${A}.${H}{background-color:transparent!important;box-shadow:none!important;border-color:transparent!important}
.${A}>:not(#${ID}){transition:opacity .2s ease}
.${A}.${H}>:not(#${ID}){opacity:0!important;pointer-events:none!important}
@media (prefers-reduced-motion:reduce){#${ID} .b,#${ID} .f,#${ID} .p,.${A},.${A}>:not(#${ID}){transition:none!important}}
`;
    document.head.appendChild(s);
  }

  function clsFor(p) { return p > DANGER ? 'f d' : p > WARN ? 'f w' : 'f'; }

  function setFill(el, p) {
    const sp = '' + p;
    if (el.dataset.p !== sp) {
      el.dataset.p = sp;
      el.style.width = sp + '%';
      const c = clsFor(p);
      if (el.className !== c) el.className = c;
    }
  }

  function build() {
    const root = document.createElement('div');
    root.id = ID;

    root.innerHTML =
      `<div class="t"><div class="b"><div class="f" data-role="tf"></div></div></div>` +
      `<div class="p">` +
      ROWS.map(([, label], i) =>
        `<div class="r" data-i="${i}">` +
          `<div class="hh"><span class="l">${label}</span><span class="m" data-role="m"></span></div>` +
          `<div class="k"><div class="f" data-role="f"></div></div>` +
        `</div>`
      ).join('') +
      `</div>`;

    const tf = root.querySelector('[data-role="tf"]');
    const rEls = [...root.querySelectorAll('.r')];
    const metas = rEls.map(r => r.querySelector('[data-role="m"]'));
    const fills = rEls.map(r => r.querySelector('[data-role="f"]'));

    root.addEventListener('pointerenter', () => {
      S.anchor && S.anchor.classList.add(H);
      if (Date.now() - S.lastAt > HOVER_REFRESH) refresh(1);
    }, { passive: true });
    root.addEventListener('pointerleave', () => { S.anchor && S.anchor.classList.remove(H); }, { passive: true });

    return { root, tf, rEls, metas, fills };
  }

  function render(d) {
    if (!S.ui || !d) return;

    setFill(S.ui.tf, clamp(d?.five_hour?.utilization));

    for (let i = 0; i < ROWS.length; i++) {
      const key = ROWS[i][0];
      const b = d?.[key];
      const row = S.ui.rEls[i];
      if (!b) { row.hidden = true; continue; }
      row.hidden = false;

      const p = clamp(b.utilization);
      setFill(S.ui.fills[i], p);

      const t = `${p}% · ${fmt(b.resets_at)}`;
      const m = S.ui.metas[i];
      if (m.dataset.t !== t) { m.dataset.t = t; m.textContent = t; }
    }
  }

  async function refresh(force) {
    if (!S.ui || (!force && document.hidden)) return;
    render(await usage(!!force));
  }

  function findAnchor() {
    const ed = document.querySelector('[contenteditable="true"].tiptap');
    if (!ed) return null;
    const fs = ed.closest('fieldset');
    if (!fs) return null;
    return fs.querySelector('div[class*="bg-bg-000"][class*="rounded-[20px]"]') || fs;
  }

  function attach() {
    const a = findAnchor();
    if (!a) return;

    const existing = document.getElementById(ID);
    if (a === S.anchor && existing && a.contains(existing)) return;

    existing && existing.remove();

    a.classList.add(A);
    if (getComputedStyle(a).position === 'static') a.style.position = 'relative';

    S.anchor = a;
    S.ui = build();
    a.insertBefore(S.ui.root, a.firstChild);

    refresh(1);
  }

  function scheduleAttach() {
    if (S.sched) return;
    const cb = () => { S.sched = 0; attach(); };
    S.sched = window.requestIdleCallback ? requestIdleCallback(cb, { timeout: 800 }) : requestAnimationFrame(cb);
  }

  function startPoll() {
    stopPoll();
    const tick = () => {
      if (document.hidden) { S.poll = 0; return; }
      refresh(0);
      S.poll = setTimeout(tick, POLL);
    };
    S.poll = setTimeout(tick, POLL);
  }

  function stopPoll() { S.poll && clearTimeout(S.poll); S.poll = 0; }

  function hooks() {
    const patch = (m) => {
      const o = history[m];
      history[m] = function () { const r = o.apply(this, arguments); scheduleAttach(); return r; };
    };
    patch('pushState'); patch('replaceState');
    addEventListener('popstate', scheduleAttach, { passive: true });
    addEventListener('hashchange', scheduleAttach, { passive: true });

    let t = 0;
    S.mo = new MutationObserver(() => {
      if (t) return;
      t = setTimeout(() => { t = 0; scheduleAttach(); }, 200);
    });
    S.mo.observe(document.body, { childList: true, subtree: true });

    document.addEventListener('visibilitychange', () => {
      if (document.hidden) stopPoll();
      else { scheduleAttach(); refresh(1); startPoll(); }
    }, { passive: true });

    addEventListener('focus', () => !document.hidden && refresh(1), { passive: true });
  }

  function init() {
    style();
    hooks();
    scheduleAttach();
    startPoll();
  }

  init();
})();