6090508bad
- Add curly braces to all if/else/for/while statements - Fix indentation and trailing spaces - Auto-fixed 372 linting errors using eslint --fix - Remaining issues are warnings only (non-null assertions, explicit any types)
171 lines
4.6 KiB
JavaScript
171 lines
4.6 KiB
JavaScript
/**
|
|
* Flynn Token Usage Page
|
|
*
|
|
* Shows per-session token usage breakdown including input/output tokens,
|
|
* API calls, estimated cost, and delegation details.
|
|
* Auto-refreshes every 30 seconds.
|
|
*/
|
|
|
|
let _timer = null;
|
|
|
|
function formatNumber(n) {
|
|
return (n ?? 0).toLocaleString();
|
|
}
|
|
|
|
function formatCost(n) {
|
|
if (!n || n === 0) {return '$0.00';}
|
|
if (n < 0.01) {return `$${n.toFixed(4)}`;}
|
|
return `$${n.toFixed(2)}`;
|
|
}
|
|
|
|
function truncateId(id) {
|
|
if (!id) {return '-';}
|
|
if (id.length <= 24) {return id;}
|
|
return id.slice(0, 24) + '\u2026';
|
|
}
|
|
|
|
async function loadUsage(el, client) {
|
|
let data;
|
|
|
|
try {
|
|
data = await client.call('system.tokenUsage');
|
|
} catch (err) {
|
|
el.innerHTML = `<div class="empty-state">Failed to load usage: ${err.message}</div>`;
|
|
return;
|
|
}
|
|
|
|
const sessions = data?.sessions ?? [];
|
|
|
|
// Compute totals across all sessions
|
|
let totalInput = 0;
|
|
let totalOutput = 0;
|
|
let totalCalls = 0;
|
|
let totalCost = 0;
|
|
|
|
for (const s of sessions) {
|
|
totalInput += s.total?.inputTokens ?? 0;
|
|
totalOutput += s.total?.outputTokens ?? 0;
|
|
totalCalls += s.total?.calls ?? 0;
|
|
totalCost += s.total?.estimatedCost ?? 0;
|
|
}
|
|
|
|
// Summary cards
|
|
const summaryHtml = `
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<div class="stat-label">Total Input Tokens</div>
|
|
<div class="stat-value">${formatNumber(totalInput)}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Total Output Tokens</div>
|
|
<div class="stat-value">${formatNumber(totalOutput)}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Total Tokens</div>
|
|
<div class="stat-value">${formatNumber(totalInput + totalOutput)}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">API Calls</div>
|
|
<div class="stat-value">${formatNumber(totalCalls)}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Estimated Cost</div>
|
|
<div class="stat-value">${formatCost(totalCost)}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Active Sessions</div>
|
|
<div class="stat-value">${sessions.length}</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Per-session table
|
|
let tableHtml = '';
|
|
if (sessions.length === 0) {
|
|
tableHtml = '<div class="empty-state">No active sessions with usage data</div>';
|
|
} else {
|
|
const rows = sessions.map(s => {
|
|
const inTok = s.total?.inputTokens ?? 0;
|
|
const outTok = s.total?.outputTokens ?? 0;
|
|
const calls = s.total?.calls ?? 0;
|
|
const cost = s.total?.estimatedCost ?? 0;
|
|
|
|
// Build delegation breakdown if present
|
|
const delegationEntries = Object.entries(s.delegation ?? {});
|
|
let delegationCell = '<span class="text-muted">-</span>';
|
|
if (delegationEntries.length > 0) {
|
|
delegationCell = delegationEntries.map(([tier, stats]) =>
|
|
`<span class="badge ok">${tier}</span> ${formatNumber(stats.inputTokens)}/${formatNumber(stats.outputTokens)}`,
|
|
).join('<br>');
|
|
}
|
|
|
|
return `
|
|
<tr>
|
|
<td title="${s.sessionId}">${truncateId(s.sessionId)}</td>
|
|
<td>${formatNumber(inTok)}</td>
|
|
<td>${formatNumber(outTok)}</td>
|
|
<td>${formatNumber(inTok + outTok)}</td>
|
|
<td>${formatNumber(calls)}</td>
|
|
<td>${formatCost(cost)}</td>
|
|
<td>${delegationCell}</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
|
|
tableHtml = `
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Session</th>
|
|
<th>Input</th>
|
|
<th>Output</th>
|
|
<th>Total</th>
|
|
<th>Calls</th>
|
|
<th>Cost</th>
|
|
<th>Delegation</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${rows}
|
|
</tbody>
|
|
</table>
|
|
`;
|
|
}
|
|
|
|
el.innerHTML = `
|
|
<div class="usage-header">
|
|
<h1 class="page-title">Token Usage</h1>
|
|
<button class="btn btn-secondary" id="usage-refresh-btn">Refresh</button>
|
|
</div>
|
|
${summaryHtml}
|
|
<h2 class="section-title">Per-Session Breakdown</h2>
|
|
${tableHtml}
|
|
`;
|
|
|
|
// Wire up refresh button
|
|
const refreshBtn = el.querySelector('#usage-refresh-btn');
|
|
if (refreshBtn) {
|
|
refreshBtn.addEventListener('click', () => {
|
|
loadUsage(el, client).catch(() => {});
|
|
});
|
|
}
|
|
}
|
|
|
|
export const UsagePage = {
|
|
async render(el, client) {
|
|
await loadUsage(el, client);
|
|
|
|
// Auto-refresh every 30 seconds
|
|
_timer = setInterval(() => {
|
|
loadUsage(el, client).catch(() => {});
|
|
}, 30000);
|
|
},
|
|
|
|
teardown() {
|
|
if (_timer) {
|
|
clearInterval(_timer);
|
|
_timer = null;
|
|
}
|
|
},
|
|
};
|