agent-manager-test1 / web /src /components /UsagePanel.tsx
lvwerra's picture
lvwerra HF Staff
Faster warm paths and honest numbers for Usage and Overview
0dd286d
Raw
History Blame Contribute Delete
6.79 kB
import { useEffect, useState } from 'react';
import * as api from '../api';
import type { ProviderUsage, QuotaWindow, Traces, TraceStats } from '../api';
import Logo from './Logo';
const PROVS = [
{ id: 'claude', label: 'Claude Code', color: '#d97757' },
{ id: 'codex', label: 'Codex', color: '#5eb6a6' },
{ id: 'gemini', label: 'Gemini CLI', color: '#4796e3' },
];
const fmtTok = (n = 0) =>
n >= 1e9 ? `${(n / 1e9).toFixed(1)}B` : n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
const resetStr = (s?: number) => {
if (!s) return '';
const mins = Math.round((s * 1000 - Date.now()) / 60000);
if (mins <= 0) return 'resetting';
if (mins < 60) return `resets in ${mins}m`;
return `resets in ${Math.floor(mins / 60)}h ${mins % 60}m`;
};
const fmtAgo = (ts: number) => {
if (!ts) return '—';
const m = Math.round((Date.now() - ts) / 60000);
if (m < 1) return 'now';
if (m < 60) return `${m}m ago`;
if (m < 48 * 60) return `${Math.round(m / 60)}h ago`;
return `${Math.round(m / 1440)}d ago`;
};
const topTools = (tools: Record<string, number>) =>
Object.entries(tools).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([k, v]) => `${k} ${v}`).join(' · ');
function Bar({ label, q }: { label: string; q?: QuotaWindow }) {
if (!q || q.usedPercent == null) return null;
const pct = Math.max(0, Math.min(100, Math.round(q.usedPercent)));
return (
<div className="qrow">
<span className="qlabel">{label}</span>
<div className="qbar"><div className="qfill" style={{ width: `${pct}%` }} /></div>
<span className="qpct">{pct}%<span className="s-muted"> {resetStr(q.resetsAt)}</span></span>
</div>
);
}
function TraceRow({ label, cli, path, st, strong }: { label: string; cli?: string; path?: string | null; st: TraceStats; strong?: boolean }) {
const cachePct = st.tokensIn + st.cacheRead > 0 ? Math.round((st.cacheRead / (st.tokensIn + st.cacheRead)) * 100) : 0;
return (
<tr className={strong ? 'tr-total' : undefined}>
<td className="tr-agent" title={path || undefined}>
{cli && <Logo cli={cli} size={12} />}
<span>{label}</span>
</td>
<td>{st.turns}</td>
<td>{st.prompts}</td>
<td title={topTools(st.tools) || undefined}>{st.toolCalls}</td>
<td>{st.web}</td>
<td title={`${cachePct}% served from cache (${fmtTok(st.cacheRead)} cached)`}>{fmtTok(st.tokensIn)}</td>
<td>{fmtTok(st.tokensOut)}</td>
<td className="tr-when">{fmtAgo(st.lastTs)}</td>
</tr>
);
}
export default function UsagePanel() {
// Each provider loads independently (undefined = loading, null = failed), so
// the page frame paints immediately and cards fill in as answers arrive —
// one hung provider no longer blanks the whole page.
const [prov, setProv] = useState<Record<string, ProviderUsage | null | undefined>>({});
const [t, setT] = useState<Traces | null>(null);
useEffect(() => {
let alive = true;
for (const p of PROVS) {
api.getUsage(p.id)
.then((r) => { if (alive) setProv((m) => ({ ...m, [p.id]: r.providers[p.id] || {} })); })
.catch(() => { if (alive) setProv((m) => ({ ...m, [p.id]: null })); });
}
api.getTraces().then((r) => { if (alive) setT(r); }).catch(() => {});
return () => { alive = false; };
}, []);
return (
<div className="usage">
{PROVS.map((p) => {
const d = prov[p.id];
const q = d?.quota;
return (
<div key={p.id} className="usage-card">
<div className="usage-head">
<span className="status" style={{ background: p.color }} />
<b>{p.label}</b>
</div>
{d === undefined ? (
<>
<div className="usage-stats">
<div><span className="s-muted">Today</span><span className="skel" style={{ width: 64, height: 12, marginTop: 4 }} /></div>
<div><span className="s-muted">This week</span><span className="skel" style={{ width: 64, height: 12, marginTop: 4 }} /></div>
</div>
<div className="usage-quota"><span className="skel" style={{ width: '78%' }} /></div>
</>
) : d === null ? (
<div className="s-help">unavailable — is ccusage installed?</div>
) : (
<>
<div className="usage-stats">
<div><span className="s-muted">Today</span><b>{d.tokensToday == null ? '—' : `${fmtTok(d.tokensToday)} tok`}</b></div>
<div><span className="s-muted">This week</span><b>{d.tokensWeek == null ? '—' : `${fmtTok(d.tokensWeek)} tok`}</b></div>
</div>
{q ? (
<div className="usage-quota">
<Bar label="5-hour" q={q.fiveHour} />
<Bar label="Weekly" q={q.weekly} />
{!q.fiveHour && !q.weekly && <div className="s-help">No quota yet — run a session to populate.</div>}
</div>
) : p.id === 'gemini' ? (
<div className="s-help">No quota (consumer tier deprecated — uses an API key).</div>
) : (
<div className="s-help">No quota yet — run a session to populate.</div>
)}
</>
)}
</div>
);
})}
<h3>Traces</h3>
<div className="s-help">
Parsed from every agent transcript stored on this Space (Claude, Codex, opencode, Hermes, OpenClaw) —
hover the tools count for the breakdown, tokens-in for the cache share. The total also counts
traces of deleted agents.
</div>
<div className="table-scroll">
<table className="traces-table">
<thead>
<tr><th>agent</th><th>turns</th><th>prompts</th><th>tools</th><th>web</th><th>tok in</th><th>tok out</th><th>last active</th></tr>
</thead>
<tbody>
{!t ? (
[0, 1, 2].map((i) => (
<tr key={i}><td colSpan={8}><span className="skel" style={{ width: `${86 - i * 14}%` }} /></td></tr>
))
) : (
<>
{t.sessions.map((s) => <TraceRow key={s.id} label={s.name} cli={s.cli} path={s.path} st={s} />)}
<TraceRow label={`total (${t.totals.files} files)`} st={t.totals} strong />
</>
)}
</tbody>
</table>
</div>
<div className="s-help">
Token counts and quota are read from each agent's local logs on the Space. They reflect the state as of that agent's <em>last model call here</em> — running a session updates them; activity outside the Space won't.
</div>
</div>
);
}