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')}
|
{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')}
|
{by_model.map((m) => {
const share =
summary.total_tokens > 0
? (m.total_tokens / summary.total_tokens) * 100
: 0;
return (
|
{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
|
);
})}
);
}