import React, { useEffect, useMemo, useState, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { ComposedChart, Area, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend, } from 'recharts'; import { Coins, ArrowDownToLine, ArrowUpFromLine, Gauge, AlertTriangle, TrendingUp, } from 'lucide-react'; import { httpClient } from '@/app/infra/http/HttpClient'; interface TokenSummary { total_calls: number; success_calls: number; error_calls: number; total_input_tokens: number; total_output_tokens: number; total_tokens: number; total_cost: number; avg_tokens_per_call: number; avg_duration_ms: number; avg_tokens_per_second: number; zero_token_success_calls: number; } interface TokenByModel { model_name: string; calls: number; error_calls: number; input_tokens: number; output_tokens: number; total_tokens: number; cost: number; avg_tokens_per_call: number; avg_duration_ms: number; } interface TokenTimeseriesPoint { bucket: string; input_tokens: number; output_tokens: number; total_tokens: number; calls: number; } interface TokenStatistics { summary: TokenSummary; by_model: TokenByModel[]; timeseries: TokenTimeseriesPoint[]; bucket: string; } interface TokenMonitoringProps { botIds?: string[]; pipelineIds?: string[]; startTime?: string; endTime?: string; /** Bumped by the parent to trigger a refetch on manual refresh. */ refreshKey?: number; } function formatNumber(n: number): string { if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(2)}M`; if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`; return n.toLocaleString(); } const TOOLTIP_STYLE: React.CSSProperties = { backgroundColor: 'var(--card)', border: '1px solid var(--border)', borderRadius: '12px', boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)', fontSize: '13px', padding: '12px', color: 'var(--foreground)', }; function MetricTile({ icon, label, value, sub, accent, }: { icon: React.ReactNode; label: string; value: string; sub?: string; accent?: string; }) { return (
{icon} {label}
{value}
{sub &&
{sub}
}
); } export default function TokenMonitoring({ botIds, pipelineIds, startTime, endTime, refreshKey, }: TokenMonitoringProps) { const { t } = useTranslation(); const [bucket, setBucket] = useState<'hour' | 'day'>('hour'); const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const botIdsKey = JSON.stringify(botIds); const pipelineIdsKey = JSON.stringify(pipelineIds); const fetchStats = useCallback(async () => { setLoading(true); setError(null); try { const result = await httpClient.getTokenStatistics({ botId: botIds, pipelineId: pipelineIds, startTime, endTime, bucket, }); setStats(result); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } finally { setLoading(false); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [botIdsKey, pipelineIdsKey, startTime, endTime, bucket, refreshKey]); useEffect(() => { fetchStats(); }, [fetchStats]); const chartData = useMemo(() => { if (!stats) return []; return stats.timeseries.map((p) => ({ bucket: p.bucket, input: p.input_tokens, output: p.output_tokens, total: p.total_tokens, })); }, [stats]); if (loading) { return (
{Array.from({ length: 6 }).map((_, i) => (
))}
); } if (error) { return (
{t('monitoring.tokens.loadError', { error })}
); } if (!stats || stats.summary.total_calls === 0) { return (
{t('monitoring.tokens.noData')}
); } const { summary, by_model } = stats; return (
{/* Data-quality warning: streamed calls that recorded 0 tokens */} {summary.zero_token_success_calls > 0 && (
{t('monitoring.tokens.zeroTokenWarning', { count: summary.zero_token_success_calls, })}
)} {/* Summary tiles */}
} label={t('monitoring.tokens.totalTokens')} value={formatNumber(summary.total_tokens)} sub={t('monitoring.tokens.acrossCalls', { count: summary.total_calls, })} accent="#8b5cf6" /> } label={t('monitoring.tokens.inputTokens')} value={formatNumber(summary.total_input_tokens)} accent="#3b82f6" /> } label={t('monitoring.tokens.outputTokens')} value={formatNumber(summary.total_output_tokens)} accent="#10b981" /> } label={t('monitoring.tokens.avgPerCall')} value={formatNumber(summary.avg_tokens_per_call)} accent="#f59e0b" /> } label={t('monitoring.tokens.throughput')} value={`${summary.avg_tokens_per_second}`} sub={t('monitoring.tokens.tokensPerSec')} accent="#06b6d4" /> } label={t('monitoring.tokens.errorCalls')} value={`${summary.error_calls}`} sub={t('monitoring.tokens.ofTotal', { count: summary.total_calls })} accent="#ef4444" />
{/* Token usage over time */}

{t('monitoring.tokens.usageOverTime')}

{(['hour', 'day'] as const).map((b) => ( ))}
formatNumber(Number(v))} /> formatNumber(Number(value))} />
{/* Per-model breakdown */}

{t('monitoring.tokens.byModel')}

{by_model.map((m) => { const share = summary.total_tokens > 0 ? (m.total_tokens / summary.total_tokens) * 100 : 0; return ( ); })}
{t('monitoring.tokens.model')} {t('monitoring.tokens.calls')} {t('monitoring.tokens.inputTokens')} {t('monitoring.tokens.outputTokens')} {t('monitoring.tokens.totalTokens')} {t('monitoring.tokens.avgPerCall')} {t('monitoring.tokens.avgLatency')}
{m.model_name}
{m.calls} {m.error_calls > 0 && ( {' '} ({m.error_calls}✕) )} {formatNumber(m.input_tokens)} {formatNumber(m.output_tokens)} {formatNumber(m.total_tokens)} {formatNumber(m.avg_tokens_per_call)} {m.avg_duration_ms}ms
); }