import React, { useMemo, useState, useCallback } from 'react'; import { Link } from 'react-router-dom'; import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, } from 'recharts'; import type { AgentLensEvent, Session, EventQueryResult, SessionQueryResult, } from '@agentkitai/agentlens-core'; import { useApi } from '../hooks/useApi'; import { useSSE } from '../hooks/useSSE'; import { getOverviewStats, getEvents, getSessions, getLlmAnalytics, getAnalytics } from '../api/client'; import type { LlmAnalyticsResult, OverviewStats } from '../api/client'; import { MetricsGrid } from '../components/MetricsGrid'; import type { MetricCard } from '../components/MetricsGrid'; import { TimeRangePicker, DEFAULT_TIME_RANGE } from '../components/TimeRangePicker'; import type { TimeRange } from '../components/TimeRangePicker'; // ─── Helpers ──────────────────────────────────────────────────────── function formatHour(iso: string): string { const d = new Date(iso); return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } function statusColor(status: string): string { switch (status) { case 'active': return 'bg-green-100 text-green-700'; case 'completed': return 'bg-blue-100 text-blue-700'; case 'error': return 'bg-red-100 text-red-700'; default: return 'bg-gray-100 text-gray-600'; } } function severityColor(severity: string): string { switch (severity) { case 'error': return 'text-red-600'; case 'critical': return 'text-red-800 font-semibold'; case 'warn': return 'text-yellow-600'; default: return 'text-gray-600'; } } function relativeTime(iso: string): string { // Tolerate epoch-ms values / numeric strings like "1783015898521.0" (some ingest // paths store these) as well as ISO strings — otherwise new Date() yields NaN. const s = String(iso ?? '').trim(); const t = /^\d+(\.\d+)?$/.test(s) ? Math.round(Number(s)) : new Date(s).getTime(); if (!Number.isFinite(t)) return 'unknown'; const diff = Date.now() - t; const mins = Math.floor(diff / 60000); if (mins < 1) return 'just now'; if (mins < 60) return `${mins}m ago`; const hrs = Math.floor(mins / 60); if (hrs < 24) return `${hrs}h ago`; const days = Math.floor(hrs / 24); return `${days}d ago`; } // ─── Hourly bucketing ─────────────────────────────────────────────── interface HourlyBucket { hour: string; count: number; } function bucketByHour(events: AgentLensEvent[]): HourlyBucket[] { const now = new Date(); const buckets = new Map(); // Pre-fill 24 hourly buckets for (let i = 23; i >= 0; i--) { const d = new Date(now.getTime() - i * 3600_000); const key = d.toISOString().slice(0, 13); // YYYY-MM-DDTHH buckets.set(key, 0); } for (const ev of events) { const key = ev.timestamp.slice(0, 13); if (buckets.has(key)) { buckets.set(key, (buckets.get(key) ?? 0) + 1); } } return Array.from(buckets.entries()).map(([hour, count]) => ({ hour: formatHour(`${hour}:00:00Z`), count, })); } // ─── Component ────────────────────────────────────────────────────── export function Overview(): React.ReactElement { // SSE live counters (Story 14.4) const [liveEventDelta, setLiveEventDelta] = useState(0); const [liveSessionRefreshKey, setLiveSessionRefreshKey] = useState(0); // SSE connection for real-time updates const { connected: sseConnected } = useSSE({ url: '/api/stream', onEvent: useCallback(() => { // Increment live counter for each event received setLiveEventDelta((d) => d + 1); }, []), onSessionUpdate: useCallback(() => { // Trigger session list refresh setLiveSessionRefreshKey((k) => k + 1); }, []), }); const [timeRange, setTimeRange] = useState(DEFAULT_TIME_RANGE); const now = useMemo(() => new Date(), []); const todayStart = useMemo(() => { const d = new Date(now); d.setHours(0, 0, 0, 0); return d.toISOString(); }, [now]); const last24h = useMemo(() => new Date(now.getTime() - 86400_000).toISOString(), [now]); // ─── API call 1: Consolidated overview stats ──────────────────── const overview = useApi( () => getOverviewStats({ from: timeRange.from, to: timeRange.to }), [timeRange], ); // ─── API call 2: Recent sessions (refetched on SSE session updates) ── const sessions = useApi( () => getSessions({ limit: 10 }), [liveSessionRefreshKey], ); // ─── API call 3: Events for chart (use analytics buckets) ── const eventsChart = useApi<{ buckets: Array<{ timestamp: string; eventCount: number; toolCallCount: number; errorCount: number }> }>( () => getAnalytics(timeRange.range ? { range: timeRange.range, granularity: timeRange.granularity } : { from: timeRange.from, to: timeRange.to, granularity: timeRange.granularity }), [timeRange], ); // Recent errors (included as sub-query in eventsChart or separate small call) const recentErrors = useApi( () => getEvents({ severity: ['error', 'critical'], limit: 10, order: 'desc' }), [], ); // LLM analytics (selected range) const llmToday = useApi( () => getLlmAnalytics({ from: timeRange.from, to: timeRange.to, granularity: timeRange.granularity, }), [timeRange], ); // ─── Computed metrics ─────────────────────────────────────────── const metricsLoading = overview.loading; // Add live event delta to "Events Today" for real-time counter (Story 14.4) const eventsTodayCount = (overview.data?.eventsTodayCount ?? 0) + liveEventDelta; // LLM metrics const llmCallCount = llmToday.data?.summary?.totalCalls ?? 0; const llmCostUsd = llmToday.data?.summary?.totalCostUsd ?? 0; // Short suffix for metric card labels based on selected range const rangeSuffix = timeRange.range === '24h' ? 'Today' : timeRange.label.replace('Last ', ''); const cards: MetricCard[] = [ { label: `Sessions (${rangeSuffix})`, value: overview.data?.sessionsTodayCount ?? 0, currentValue: overview.data?.sessionsTodayCount ?? 0, previousValue: overview.data?.sessionsYesterdayCount ?? 0, }, { label: `Events (${rangeSuffix})`, value: eventsTodayCount, currentValue: eventsTodayCount, previousValue: overview.data?.eventsYesterdayCount ?? 0, }, { label: `Errors (${rangeSuffix})`, value: overview.data?.errorsTodayCount ?? 0, currentValue: overview.data?.errorsTodayCount ?? 0, previousValue: overview.data?.errorsYesterdayCount ?? 0, lowerIsBetter: true, }, { label: 'Active Agents', value: overview.data?.totalAgents ?? 0, }, ]; // Add LLM metrics when LLM data is available if (llmCallCount > 0 || (llmToday.data && !llmToday.loading)) { cards.push( { label: `LLM Calls (${rangeSuffix})`, value: llmCallCount, }, { label: `LLM Cost (${rangeSuffix})`, value: llmCostUsd < 0.01 ? `$${llmCostUsd.toFixed(4)}` : llmCostUsd < 1 ? `$${llmCostUsd.toFixed(3)}` : `$${llmCostUsd.toFixed(2)}`, }, ); } // Chart data — use pre-bucketed analytics data const chartData = useMemo(() => { if (!eventsChart.data?.buckets) return []; return eventsChart.data.buckets.map((b) => { const d = new Date(b.timestamp); const label = timeRange.granularity === 'day' ? d.toLocaleDateString([], { month: 'short', day: 'numeric' }) : formatHour(b.timestamp); const tooltip = timeRange.granularity === 'day' ? d.toLocaleDateString([], { weekday: 'short', month: 'short', day: 'numeric' }) : d.toLocaleDateString([], { month: 'short', day: 'numeric' }) + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); return { hour: label, tooltip, count: b.eventCount }; }); }, [eventsChart.data, timeRange.granularity]); const recentSessions: Session[] = sessions.data?.sessions ?? []; const recentErrorEvents: AgentLensEvent[] = recentErrors.data?.events ?? []; return (

Overview

Real-time overview of your agent activity.

{/* SSE Connection Indicator (Story 14.4) */}
{sseConnected ? ( <> Live ) : ( <> Connection lost — data may be stale )}
{/* Metrics Cards */} {/* Charts & Feeds Row */}
{/* Events Over Time */}

Events Over Time

{timeRange.label}

{eventsChart.loading ? (
) : ( { const item = payload?.[0]?.payload; return item?.tooltip ?? _label; }} /> )}
{/* Recent Sessions */}

Recent Sessions

10 most recent

{sessions.loading ? (
{[0, 1, 2, 3, 4].map((i) => (
))}
) : recentSessions.length === 0 ? (

No sessions yet

) : (
    {recentSessions.map((s) => (
  • {s.agentName ?? s.agentId}

    {s.id.slice(0, 12)}…

    {s.status} {relativeTime(s.startedAt)}
  • ))}
)}
{/* Recent Errors */}

Recent Errors

10 most recent error events

{recentErrors.loading ? (
{[0, 1, 2, 3].map((i) => (
))}
) : recentErrorEvents.length === 0 ? (

No errors — looking good! 🎉

) : (
    {recentErrorEvents.map((ev) => (
  • {ev.severity} {ev.eventType}

    Session: {ev.sessionId.slice(0, 12)}… | Agent: {ev.agentId}

    {relativeTime(ev.timestamp)}
  • ))}
)}
); }