/** * LLM Analytics Page (Story 4.2) * * Route: /llm * * Features: * - Summary cards: Total LLM Calls, Total Cost, Avg Latency, Total Tokens * - Cost by Model: Stacked bar chart by provider/model over time * - Model comparison table: Provider | Model | Calls | Input Tokens | Output Tokens | Cost | Avg Latency * - Time series: LLM calls over time with cost overlay * - Filters: date range, agent dropdown, model filter, provider filter */ import React, { useMemo, useState } from 'react'; import { BarChart, Bar, LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, Legend, } from 'recharts'; import { useApi } from '../hooks/useApi'; import { getLlmAnalytics, getAgents } from '../api/client'; import type { LlmAnalyticsResult } from '../api/client'; import type { Agent } from '@agentkitai/agentlens-core'; import { OptimizationPanel } from '../components/optimization/OptimizationPanel'; // ─── Types ────────────────────────────────────────────────────────── type ActiveTab = 'analytics' | 'optimization'; type TimeRange = '24h' | '7d' | '30d'; interface TimeRangeConfig { label: string; from: () => string; granularity: 'hour' | 'day' | 'week'; } const TIME_RANGES: Record = { '24h': { label: 'Last 24 Hours', from: () => new Date(Date.now() - 86400_000).toISOString(), granularity: 'hour', }, '7d': { label: 'Last 7 Days', from: () => new Date(Date.now() - 7 * 86400_000).toISOString(), granularity: 'day', }, '30d': { label: 'Last 30 Days', from: () => new Date(Date.now() - 30 * 86400_000).toISOString(), granularity: 'day', }, }; // ─── Colors ───────────────────────────────────────────────────────── const COLORS = [ '#6366f1', // indigo-500 '#0c8ce9', // brand blue '#10b981', // emerald '#f59e0b', // amber '#ef4444', // red '#8b5cf6', // violet '#ec4899', // pink '#14b8a6', // teal '#f97316', // orange '#06b6d4', // cyan ]; // ─── Formatters ───────────────────────────────────────────────────── function formatBucketLabel(ts: string, range: TimeRange): string { if (!ts) return ''; if (range === '24h') { const d = new Date(ts); return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } const d = new Date(ts); return d.toLocaleDateString([], { month: 'short', day: 'numeric' }); } function formatCost(usd: number): string { if (usd < 0.01) return `$${usd.toFixed(4)}`; if (usd < 1) return `$${usd.toFixed(3)}`; return `$${usd.toFixed(2)}`; } function formatNumber(n: number): string { if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`; if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`; return String(n); } function formatLatency(ms: number): string { if (ms < 1000) return `${Math.round(ms)}ms`; return `${(ms / 1000).toFixed(1)}s`; } // ─── Metric Card ──────────────────────────────────────────────────── function MetricCard({ label, value, icon, }: { label: string; value: string | number; icon: string; }) { return (
{icon} {label}
{value}
); } // ─── Sort helpers ─────────────────────────────────────────────────── type SortKey = 'provider' | 'model' | 'calls' | 'inputTokens' | 'outputTokens' | 'costUsd' | 'avgLatencyMs'; type SortDir = 'asc' | 'desc'; // ─── Component ────────────────────────────────────────────────────── export function LlmAnalytics(): React.ReactElement { const [activeTab, setActiveTab] = useState('analytics'); const [range, setRange] = useState('24h'); const [agentId, setAgentId] = useState(''); const [modelFilter, setModelFilter] = useState(''); const [providerFilter, setProviderFilter] = useState(''); const [sortKey, setSortKey] = useState('costUsd'); const [sortDir, setSortDir] = useState('desc'); const config = TIME_RANGES[range]; const from = useMemo(() => config.from(), [range]); const to = useMemo(() => new Date().toISOString(), [range]); // Fetch LLM analytics data const { data, loading, error } = useApi( () => getLlmAnalytics({ from, to, granularity: config.granularity, agentId: agentId || undefined, model: modelFilter || undefined, provider: providerFilter || undefined, }), [from, to, config.granularity, agentId, modelFilter, providerFilter], ); // Fetch agents for dropdown const { data: agents } = useApi(() => getAgents(), []); // Extract unique providers and models for filter dropdowns const { providers, models } = useMemo(() => { if (!data?.byModel) return { providers: [] as string[], models: [] as string[] }; const provSet = new Set(); const modSet = new Set(); for (const row of data.byModel) { provSet.add(row.provider); modSet.add(row.model); } return { providers: Array.from(provSet).sort(), models: Array.from(modSet).sort(), }; }, [data]); // Chart data: cost by model over time (stacked bar) const modelIds = useMemo(() => { if (!data?.byModel) return []; return data.byModel.map((m) => `${m.provider}/${m.model}`); }, [data]); const costByModelChartData = useMemo(() => { if (!data?.byTime || !data?.byModel) return []; // For stacked chart, we only have aggregate byTime. Show total cost per bucket. return data.byTime.map((b) => ({ label: formatBucketLabel(b.bucket, range), cost: b.costUsd, calls: b.calls, })); }, [data, range]); // Time series data: calls over time with cost overlay const timeSeriesData = useMemo(() => { if (!data?.byTime) return []; return data.byTime.map((b) => ({ label: formatBucketLabel(b.bucket, range), calls: b.calls, cost: b.costUsd, avgLatency: b.avgLatencyMs, })); }, [data, range]); // Sort model table const sortedModels = useMemo(() => { if (!data?.byModel) return []; return [...data.byModel].sort((a, b) => { const aVal = a[sortKey]; const bVal = b[sortKey]; if (typeof aVal === 'string' && typeof bVal === 'string') { return sortDir === 'asc' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal); } const aNum = Number(aVal); const bNum = Number(bVal); return sortDir === 'asc' ? aNum - bNum : bNum - aNum; }); }, [data, sortKey, sortDir]); const handleSort = (key: SortKey) => { if (sortKey === key) { setSortDir((d) => (d === 'asc' ? 'desc' : 'asc')); } else { setSortKey(key); setSortDir('desc'); } }; const sortIndicator = (key: SortKey) => { if (sortKey !== key) return ''; return sortDir === 'asc' ? ' ↑' : ' ↓'; }; return (
{/* Header + Tabs */}

LLM Analytics

Usage, cost, and performance metrics for LLM calls.

{([ { key: 'analytics' as ActiveTab, label: 'Analytics' }, { key: 'optimization' as ActiveTab, label: 'Optimization' }, ]).map(({ key, label }) => ( ))}
{activeTab === 'optimization' ? ( ) : (<> {/* Time Range */}
{(Object.keys(TIME_RANGES) as TimeRange[]).map((key) => ( ))}
{/* Filters */}
{(agentId || providerFilter || modelFilter) && ( )}
{/* Error Banner */} {error && (
Error loading LLM analytics: {error}
)} {/* Summary Metrics */} {data && (
)} {/* Charts Row: Cost Over Time + Calls Over Time */}
{/* Cost by Model Over Time */}

Cost Over Time

{config.label}

{loading ? (
) : costByModelChartData.length === 0 ? (
No LLM cost data in this period
) : ( `$${v}`} /> [ name === 'cost' ? formatCost(value) : value, name === 'cost' ? 'Cost (USD)' : 'Calls', ]} /> )}
{/* LLM Calls Over Time with Cost Overlay */}

LLM Calls Over Time

{config.label}

{loading ? (
) : timeSeriesData.length === 0 ? (
No LLM calls in this period
) : ( `$${v}`} /> { if (name === 'Cost') return [formatCost(value), name]; if (name === 'Avg Latency') return [formatLatency(value), name]; return [value, name]; }} /> )}
{/* Model Comparison Table */} {data && data.byModel.length > 0 && (

Model Comparison

Breakdown by provider and model

{([ ['provider', 'Provider'], ['model', 'Model'], ['calls', 'Calls'], ['inputTokens', 'Input Tokens'], ['outputTokens', 'Output Tokens'], ['costUsd', 'Cost'], ['avgLatencyMs', 'Avg Latency'], ] as [SortKey, string][]).map(([key, label]) => ( ))} {sortedModels.map((row) => ( ))}
handleSort(key)} className={`px-4 py-3 text-xs font-semibold text-gray-500 uppercase cursor-pointer hover:text-gray-700 ${ key === 'provider' || key === 'model' ? 'text-left' : 'text-right' }`} > {label} {sortIndicator(key)}
{row.provider} {row.model} {formatNumber(row.calls)} {formatNumber(row.inputTokens)} {formatNumber(row.outputTokens)} {formatCost(row.costUsd)} {formatLatency(row.avgLatencyMs)}
)} {/* Empty State */} {!loading && data && data.summary.totalCalls === 0 && (
🧠

No LLM calls yet

Start tracking LLM calls by using the SDK's{' '} logLlmCall() {' '} method or the MCP{' '} agentlens_log_llm_call {' '} tool.

)} )}
); }