import { app, isRouteCurrent } from '../router.js'; import { api } from '../api.js'; import { escapeHTML, formatTokenCount, formatCost } from '../utils.js'; import { barTrack, barRankList, metricStrip, chartHeader } from '../components.js'; /* global uPlot */ let usageChart = null; let usageChartMode = 'activity'; let usageResizeObserver = null; let _usageSeries = []; export function cleanup() { if (usageChart) { usageChart.destroy(); usageChart = null; } if (usageResizeObserver) { usageResizeObserver.disconnect(); usageResizeObserver = null; } _usageSeries = []; } function buildChartData(series, mode) { if (!series || series.length === 0) return null; const ts = series.map(b => Math.floor(new Date(b.ts).getTime() / 1000)); if (mode === 'tokens') { return [ts, series.map(b => b.input_tokens || 0), series.map(b => b.output_tokens || 0)]; } if (mode === 'cost') { return [ts, series.map(b => b.cost || 0)]; } return [ts, series.map(b => b.runs || 0), series.map(b => b.tools || 0), series.map(b => b.errors || 0)]; } function renderChart(series, mode) { const container = document.getElementById('usage-chart'); if (!container) return; if (usageChart) { usageChart.destroy(); usageChart = null; } container.innerHTML = ''; const data = buildChartData(series, mode); if (!data) { container.innerHTML = '
No data for this period
'; return; } const width = container.clientWidth || 580; const height = 160; const axisStyle = { stroke: '#4e6070', grid: { stroke: 'rgba(28,38,55,0.6)', width: 1 }, ticks: { stroke: 'rgba(28,38,55,0.6)', width: 1 }, font: '11px Fira Code', }; let seriesDef; if (mode === 'tokens') { seriesDef = [ {}, { label: 'Input', stroke: '#22d3ee', width: 1.5, fill: 'rgba(34,211,238,0.08)', points: { show: false } }, { label: 'Output', stroke: '#34d399', width: 1.5, fill: 'rgba(52,211,153,0.08)', points: { show: false } }, ]; } else if (mode === 'cost') { seriesDef = [ {}, { label: 'Cost', stroke: '#fbbf24', width: 1.75, fill: 'rgba(251,191,36,0.1)', points: { show: false } }, ]; } else { seriesDef = [ {}, { label: 'Runs', stroke: '#34d399', width: 1.5, fill: 'rgba(52,211,153,0.08)', points: { show: false } }, { label: 'Tools', stroke: '#22d3ee', width: 1.5, fill: 'rgba(34,211,238,0.08)', points: { show: false } }, { label: 'Errors', stroke: '#f87171', width: 1.5, fill: 'rgba(248,113,113,0.08)', points: { show: false } }, ]; } usageChart = new window.uPlot({ width, height, cursor: { show: true }, scales: { x: { time: true }, y: { auto: true, min: 0 } }, axes: [{ ...axisStyle }, { ...axisStyle, size: 52 }], series: seriesDef, }, data, container); if (usageResizeObserver) usageResizeObserver.disconnect(); usageResizeObserver = new ResizeObserver(entries => { for (const entry of entries) { if (usageChart) usageChart.setSize({ width: entry.contentRect.width, height }); } }); usageResizeObserver.observe(container); } function renderFrameworkBreakdown(byFw) { const el = document.getElementById('usage-fw-breakdown'); if (!el) return; const entries = Object.entries(byFw || {}).sort((a, b) => { return (b[1].runs + b[1].tools + b[1].errors) - (a[1].runs + a[1].tools + a[1].errors); }); if (entries.length === 0) { el.innerHTML = 'No framework data
'; return; } const maxTotal = Math.max(...entries.map(([, s]) => s.runs + s.tools + s.errors), 1); el.innerHTML = entries.map(([name, stats]) => { const total = stats.runs + stats.tools + stats.errors; const cssClass = name.toLowerCase().replace(/[^a-z0-9-]/g, '-'); const active = (stats.active_sessions || 0) > 0; return `