apteva 0.4.57 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +216 -54
- package/cli.js +35 -0
- package/install.js +92 -0
- package/package.json +15 -76
- package/LICENSE +0 -63
- package/bin/apteva.js +0 -196
- package/dist/ActivityPage.kxzzb4yc.js +0 -3
- package/dist/ApiDocsPage.zq998hbm.js +0 -4
- package/dist/App.55rea8mn.js +0 -61
- package/dist/App.5ywb23z4.js +0 -53
- package/dist/App.6thds120.js +0 -4
- package/dist/App.9tctxzqm.js +0 -8
- package/dist/App.a8r8ttaz.js +0 -4
- package/dist/App.agsv5bje.js +0 -4
- package/dist/App.cepapqmx.js +0 -4
- package/dist/App.dp041gb3.js +0 -221
- package/dist/App.fds72zb5.js +0 -4
- package/dist/App.fg9qj2dq.js +0 -4
- package/dist/App.ndfejbm9.js +0 -4
- package/dist/App.nxmfmq1h.js +0 -13
- package/dist/App.qdfyt8ba.js +0 -4
- package/dist/App.x2d0ygt6.js +0 -4
- package/dist/App.yt9p4nr3.js +0 -20
- package/dist/App.zn4mw16t.js +0 -1
- package/dist/ConnectionsPage.8r96ryw7.js +0 -3
- package/dist/McpPage.3cwh0gnd.js +0 -3
- package/dist/SettingsPage.ykgdh5ev.js +0 -3
- package/dist/SkillsPage.4np1s65b.js +0 -3
- package/dist/TasksPage.4g08t7p6.js +0 -3
- package/dist/TelemetryPage.72w9pwcp.js +0 -3
- package/dist/TestsPage.z4fk3r7r.js +0 -3
- package/dist/ThreadsPage.63tcajeh.js +0 -3
- package/dist/apteva-kit.css +0 -1
- package/dist/icon.png +0 -0
- package/dist/index.html +0 -16
- package/dist/styles.css +0 -1
- package/scripts/postinstall.mjs +0 -102
- package/src/auth/index.ts +0 -394
- package/src/auth/middleware.ts +0 -213
- package/src/binary.ts +0 -536
- package/src/channels/index.ts +0 -40
- package/src/channels/telegram.ts +0 -311
- package/src/crypto.ts +0 -301
- package/src/db-tests.ts +0 -174
- package/src/db.ts +0 -3133
- package/src/integrations/agentdojo.ts +0 -559
- package/src/integrations/composio.ts +0 -437
- package/src/integrations/index.ts +0 -87
- package/src/integrations/skillsmp.ts +0 -318
- package/src/mcp-client.ts +0 -605
- package/src/mcp-handler.ts +0 -394
- package/src/mcp-platform.ts +0 -2403
- package/src/openapi.ts +0 -2410
- package/src/providers.ts +0 -597
- package/src/routes/api/agent-utils.ts +0 -890
- package/src/routes/api/agents.ts +0 -916
- package/src/routes/api/api-keys.ts +0 -95
- package/src/routes/api/channels.ts +0 -182
- package/src/routes/api/helpers.ts +0 -12
- package/src/routes/api/integrations.ts +0 -639
- package/src/routes/api/mcp.ts +0 -574
- package/src/routes/api/meta-agent.ts +0 -195
- package/src/routes/api/projects.ts +0 -112
- package/src/routes/api/providers.ts +0 -424
- package/src/routes/api/skills.ts +0 -537
- package/src/routes/api/system.ts +0 -333
- package/src/routes/api/telemetry.ts +0 -203
- package/src/routes/api/tests.ts +0 -148
- package/src/routes/api/triggers.ts +0 -518
- package/src/routes/api/users.ts +0 -148
- package/src/routes/api/webhooks.ts +0 -171
- package/src/routes/api.ts +0 -53
- package/src/routes/auth.ts +0 -251
- package/src/routes/share.ts +0 -86
- package/src/routes/static.ts +0 -131
- package/src/server.ts +0 -642
- package/src/test-runner.ts +0 -598
- package/src/triggers/agentdojo.ts +0 -253
- package/src/triggers/composio.ts +0 -264
- package/src/triggers/index.ts +0 -71
- package/src/tui/AgentList.tsx +0 -145
- package/src/tui/App.tsx +0 -102
- package/src/tui/Login.tsx +0 -104
- package/src/tui/api.ts +0 -72
- package/src/tui/index.tsx +0 -7
- package/src/web/App.tsx +0 -455
- package/src/web/components/activity/ActivityPage.tsx +0 -314
- package/src/web/components/activity/index.ts +0 -1
- package/src/web/components/agents/AgentCard.tsx +0 -189
- package/src/web/components/agents/AgentPanel.tsx +0 -2244
- package/src/web/components/agents/AgentsView.tsx +0 -180
- package/src/web/components/agents/CreateAgentModal.tsx +0 -475
- package/src/web/components/agents/index.ts +0 -4
- package/src/web/components/api/ApiDocsPage.tsx +0 -842
- package/src/web/components/auth/CreateAccountStep.tsx +0 -176
- package/src/web/components/auth/LoginPage.tsx +0 -91
- package/src/web/components/auth/index.ts +0 -2
- package/src/web/components/common/Icons.tsx +0 -250
- package/src/web/components/common/LoadingSpinner.tsx +0 -44
- package/src/web/components/common/Modal.tsx +0 -199
- package/src/web/components/common/Select.tsx +0 -97
- package/src/web/components/common/index.ts +0 -20
- package/src/web/components/connections/ConnectionsPage.tsx +0 -54
- package/src/web/components/connections/IntegrationsTab.tsx +0 -170
- package/src/web/components/connections/OverviewTab.tsx +0 -137
- package/src/web/components/connections/TriggersTab.tsx +0 -1346
- package/src/web/components/dashboard/Dashboard.tsx +0 -572
- package/src/web/components/dashboard/index.ts +0 -1
- package/src/web/components/index.ts +0 -21
- package/src/web/components/layout/ErrorBanner.tsx +0 -18
- package/src/web/components/layout/Header.tsx +0 -332
- package/src/web/components/layout/Sidebar.tsx +0 -231
- package/src/web/components/layout/index.ts +0 -3
- package/src/web/components/mcp/IntegrationsPanel.tsx +0 -857
- package/src/web/components/mcp/McpPage.tsx +0 -2515
- package/src/web/components/mcp/index.ts +0 -1
- package/src/web/components/meta-agent/MetaAgent.tsx +0 -245
- package/src/web/components/onboarding/OnboardingWizard.tsx +0 -404
- package/src/web/components/onboarding/index.ts +0 -1
- package/src/web/components/settings/SettingsPage.tsx +0 -2776
- package/src/web/components/settings/index.ts +0 -1
- package/src/web/components/skills/SkillsPage.tsx +0 -1200
- package/src/web/components/tasks/TasksPage.tsx +0 -1116
- package/src/web/components/tasks/index.ts +0 -1
- package/src/web/components/telemetry/TelemetryPage.tsx +0 -1129
- package/src/web/components/tests/TestsPage.tsx +0 -594
- package/src/web/components/threads/ThreadsPage.tsx +0 -315
- package/src/web/context/AuthContext.tsx +0 -242
- package/src/web/context/ProjectContext.tsx +0 -214
- package/src/web/context/TelemetryContext.tsx +0 -299
- package/src/web/context/ThemeContext.tsx +0 -90
- package/src/web/context/UIModeContext.tsx +0 -49
- package/src/web/context/index.ts +0 -12
- package/src/web/hooks/index.ts +0 -3
- package/src/web/hooks/useAgents.ts +0 -115
- package/src/web/hooks/useOnboarding.ts +0 -20
- package/src/web/hooks/useProviders.ts +0 -75
- package/src/web/icon.png +0 -0
- package/src/web/index.html +0 -16
- package/src/web/styles.css +0 -118
- package/src/web/themes.ts +0 -162
- package/src/web/types.ts +0 -298
|
@@ -1,1129 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect, useMemo, useRef, useCallback } from "react";
|
|
2
|
-
import { Select } from "../common/Select";
|
|
3
|
-
import { useTelemetryContext, useProjects, useAuth, useUIMode, type TelemetryEvent } from "../../context";
|
|
4
|
-
import {
|
|
5
|
-
AreaChart, Area, BarChart, Bar,
|
|
6
|
-
XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend,
|
|
7
|
-
} from "recharts";
|
|
8
|
-
|
|
9
|
-
interface TelemetryStats {
|
|
10
|
-
total_events: number;
|
|
11
|
-
total_llm_calls: number;
|
|
12
|
-
total_tool_calls: number;
|
|
13
|
-
total_errors: number;
|
|
14
|
-
total_input_tokens: number;
|
|
15
|
-
total_output_tokens: number;
|
|
16
|
-
total_cache_creation_tokens: number;
|
|
17
|
-
total_cache_read_tokens: number;
|
|
18
|
-
total_reasoning_tokens: number;
|
|
19
|
-
total_cost: number;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
interface UsageByAgent {
|
|
23
|
-
agent_id: string;
|
|
24
|
-
input_tokens: number;
|
|
25
|
-
output_tokens: number;
|
|
26
|
-
cache_creation_tokens: number;
|
|
27
|
-
cache_read_tokens: number;
|
|
28
|
-
reasoning_tokens: number;
|
|
29
|
-
llm_calls: number;
|
|
30
|
-
tool_calls: number;
|
|
31
|
-
errors: number;
|
|
32
|
-
cost: number;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface UsageByProject {
|
|
36
|
-
project_id: string | null;
|
|
37
|
-
input_tokens: number;
|
|
38
|
-
output_tokens: number;
|
|
39
|
-
cache_creation_tokens: number;
|
|
40
|
-
cache_read_tokens: number;
|
|
41
|
-
reasoning_tokens: number;
|
|
42
|
-
llm_calls: number;
|
|
43
|
-
tool_calls: number;
|
|
44
|
-
errors: number;
|
|
45
|
-
cost: number;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
interface DailyUsage {
|
|
49
|
-
date: string;
|
|
50
|
-
input_tokens: number;
|
|
51
|
-
output_tokens: number;
|
|
52
|
-
cache_creation_tokens: number;
|
|
53
|
-
cache_read_tokens: number;
|
|
54
|
-
reasoning_tokens: number;
|
|
55
|
-
llm_calls: number;
|
|
56
|
-
tool_calls: number;
|
|
57
|
-
errors: number;
|
|
58
|
-
cost: number;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Helper to extract stats from a single event
|
|
62
|
-
function extractEventStats(event: TelemetryEvent): {
|
|
63
|
-
llm_calls: number;
|
|
64
|
-
tool_calls: number;
|
|
65
|
-
errors: number;
|
|
66
|
-
input_tokens: number;
|
|
67
|
-
output_tokens: number;
|
|
68
|
-
cache_creation_tokens: number;
|
|
69
|
-
cache_read_tokens: number;
|
|
70
|
-
reasoning_tokens: number;
|
|
71
|
-
cost: number;
|
|
72
|
-
} {
|
|
73
|
-
const isLlm = event.category === "LLM";
|
|
74
|
-
const isTool = event.category === "TOOL";
|
|
75
|
-
const isError = event.level === "error";
|
|
76
|
-
const inputTokens = (event.data?.input_tokens as number) || 0;
|
|
77
|
-
const outputTokens = (event.data?.output_tokens as number) || 0;
|
|
78
|
-
const cacheCreationTokens = (event.data?.cache_creation_tokens as number) || 0;
|
|
79
|
-
const cacheReadTokens = (event.data?.cache_read_tokens as number) || 0;
|
|
80
|
-
const reasoningTokens = (event.data?.reasoning_tokens as number) || 0;
|
|
81
|
-
const cost = (event.cost as number) || 0;
|
|
82
|
-
|
|
83
|
-
return {
|
|
84
|
-
llm_calls: isLlm ? 1 : 0,
|
|
85
|
-
tool_calls: isTool ? 1 : 0,
|
|
86
|
-
errors: isError ? 1 : 0,
|
|
87
|
-
input_tokens: inputTokens,
|
|
88
|
-
output_tokens: outputTokens,
|
|
89
|
-
cache_creation_tokens: cacheCreationTokens,
|
|
90
|
-
cache_read_tokens: cacheReadTokens,
|
|
91
|
-
reasoning_tokens: reasoningTokens,
|
|
92
|
-
cost,
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function TelemetryPage() {
|
|
97
|
-
const { events: realtimeEvents, statusChangeCounter } = useTelemetryContext();
|
|
98
|
-
const { currentProjectId, currentProject, costTrackingEnabled, projectsEnabled, projects } = useProjects();
|
|
99
|
-
const { authFetch } = useAuth();
|
|
100
|
-
const { isDev, isBusiness, t } = useUIMode();
|
|
101
|
-
const [fetchedStats, setFetchedStats] = useState<TelemetryStats | null>(null);
|
|
102
|
-
const [historicalEvents, setHistoricalEvents] = useState<TelemetryEvent[]>([]);
|
|
103
|
-
const [fetchedUsage, setFetchedUsage] = useState<UsageByAgent[]>([]);
|
|
104
|
-
const [dailyUsage, setDailyUsage] = useState<DailyUsage[]>([]);
|
|
105
|
-
const [projectUsage, setProjectUsage] = useState<UsageByProject[]>([]);
|
|
106
|
-
const [loading, setLoading] = useState(true);
|
|
107
|
-
const [filter, setFilter] = useState({
|
|
108
|
-
level: "",
|
|
109
|
-
agent_id: "",
|
|
110
|
-
});
|
|
111
|
-
// Categories to hide (DATABASE hidden by default)
|
|
112
|
-
const [hiddenCategories, setHiddenCategories] = useState<Set<string>>(new Set(["DATABASE"]));
|
|
113
|
-
const [agents, setAgents] = useState<Array<{ id: string; name: string; projectId: string | null }>>([]);
|
|
114
|
-
const [expandedEvent, setExpandedEvent] = useState<string | null>(null);
|
|
115
|
-
|
|
116
|
-
// Time range filter
|
|
117
|
-
const [timeRange, setTimeRange] = useState<string>("all");
|
|
118
|
-
|
|
119
|
-
// Sort state for usage table
|
|
120
|
-
type SortKey = "agent" | "llm_calls" | "tool_calls" | "input_tokens" | "output_tokens" | "cache_creation_tokens" | "cache_read_tokens" | "reasoning_tokens" | "errors" | "cost";
|
|
121
|
-
const [sortKey, setSortKey] = useState<SortKey>("cost");
|
|
122
|
-
const [sortDir, setSortDir] = useState<"asc" | "desc">("desc");
|
|
123
|
-
|
|
124
|
-
const handleSort = (key: SortKey) => {
|
|
125
|
-
if (sortKey === key) {
|
|
126
|
-
setSortDir(d => d === "asc" ? "desc" : "asc");
|
|
127
|
-
} else {
|
|
128
|
-
setSortKey(key);
|
|
129
|
-
setSortDir("desc");
|
|
130
|
-
}
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
// Sort state for project usage table
|
|
134
|
-
type ProjectSortKey = "project" | "llm_calls" | "tool_calls" | "input_tokens" | "output_tokens" | "cache_creation_tokens" | "cache_read_tokens" | "reasoning_tokens" | "errors" | "cost";
|
|
135
|
-
const [projectSortKey, setProjectSortKey] = useState<ProjectSortKey>("cost");
|
|
136
|
-
const [projectSortDir, setProjectSortDir] = useState<"asc" | "desc">("desc");
|
|
137
|
-
|
|
138
|
-
const handleProjectSort = (key: ProjectSortKey) => {
|
|
139
|
-
if (projectSortKey === key) {
|
|
140
|
-
setProjectSortDir(d => d === "asc" ? "desc" : "asc");
|
|
141
|
-
} else {
|
|
142
|
-
setProjectSortKey(key);
|
|
143
|
-
setProjectSortDir("desc");
|
|
144
|
-
}
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
// Track the timestamp of the last fetch — only count realtime events arriving after this
|
|
148
|
-
const fetchTimestampRef = useRef<number>(0);
|
|
149
|
-
|
|
150
|
-
// Track which events are "new" (for animation) - stores event IDs with their arrival time
|
|
151
|
-
const [newEventIds, setNewEventIds] = useState<Set<string>>(new Set());
|
|
152
|
-
const seenEventIdsRef = useRef<Set<string>>(new Set());
|
|
153
|
-
|
|
154
|
-
// Fetch agents for dropdown
|
|
155
|
-
useEffect(() => {
|
|
156
|
-
const fetchAgents = async () => {
|
|
157
|
-
try {
|
|
158
|
-
const res = await authFetch("/api/agents");
|
|
159
|
-
const data = await res.json();
|
|
160
|
-
setAgents(data.agents || []);
|
|
161
|
-
} catch (e) {
|
|
162
|
-
console.error("Failed to fetch agents:", e);
|
|
163
|
-
}
|
|
164
|
-
};
|
|
165
|
-
fetchAgents();
|
|
166
|
-
}, [authFetch]);
|
|
167
|
-
|
|
168
|
-
// Filter agents by project
|
|
169
|
-
const filteredAgents = useMemo(() => {
|
|
170
|
-
if (currentProjectId === null) return agents;
|
|
171
|
-
if (currentProjectId === "unassigned") return agents.filter(a => !a.projectId);
|
|
172
|
-
return agents.filter(a => a.projectId === currentProjectId);
|
|
173
|
-
}, [agents, currentProjectId]);
|
|
174
|
-
|
|
175
|
-
// Get agent IDs for the current project
|
|
176
|
-
const projectAgentIds = useMemo(() => new Set(filteredAgents.map(a => a.id)), [filteredAgents]);
|
|
177
|
-
|
|
178
|
-
// Compute `since` ISO string from selected time range
|
|
179
|
-
const getSince = useCallback((): string | undefined => {
|
|
180
|
-
if (timeRange === "all") return undefined;
|
|
181
|
-
const now = new Date();
|
|
182
|
-
const ms: Record<string, number> = {
|
|
183
|
-
"1h": 3600000,
|
|
184
|
-
"6h": 6 * 3600000,
|
|
185
|
-
"24h": 24 * 3600000,
|
|
186
|
-
"7d": 7 * 24 * 3600000,
|
|
187
|
-
"30d": 30 * 24 * 3600000,
|
|
188
|
-
};
|
|
189
|
-
if (ms[timeRange]) {
|
|
190
|
-
return new Date(now.getTime() - ms[timeRange]).toISOString();
|
|
191
|
-
}
|
|
192
|
-
return undefined;
|
|
193
|
-
}, [timeRange]);
|
|
194
|
-
|
|
195
|
-
// Fetch stats and historical data (less frequently now since we have real-time)
|
|
196
|
-
const fetchData = async () => {
|
|
197
|
-
setLoading(true);
|
|
198
|
-
try {
|
|
199
|
-
// Build project filter param
|
|
200
|
-
const projectParam = currentProjectId === "unassigned" ? "null" : currentProjectId || "";
|
|
201
|
-
const since = getSince();
|
|
202
|
-
|
|
203
|
-
// Fetch stats
|
|
204
|
-
const statsParams = new URLSearchParams();
|
|
205
|
-
if (projectParam) statsParams.set("project_id", projectParam);
|
|
206
|
-
if (since) statsParams.set("since", since);
|
|
207
|
-
const statsRes = await authFetch(`/api/telemetry/stats${statsParams.toString() ? `?${statsParams}` : ""}`);
|
|
208
|
-
const statsData = await statsRes.json();
|
|
209
|
-
setFetchedStats(statsData.stats);
|
|
210
|
-
|
|
211
|
-
// Fetch historical events with filters
|
|
212
|
-
const params = new URLSearchParams();
|
|
213
|
-
if (filter.level) params.set("level", filter.level);
|
|
214
|
-
if (filter.agent_id) params.set("agent_id", filter.agent_id);
|
|
215
|
-
if (projectParam) params.set("project_id", projectParam);
|
|
216
|
-
if (since) params.set("since", since);
|
|
217
|
-
params.set("limit", "100"); // Fetch more since we filter client-side
|
|
218
|
-
|
|
219
|
-
const eventsRes = await authFetch(`/api/telemetry/events?${params}`);
|
|
220
|
-
const eventsData = await eventsRes.json();
|
|
221
|
-
const events = eventsData.events || [];
|
|
222
|
-
setHistoricalEvents(events);
|
|
223
|
-
|
|
224
|
-
// Record fetch time — realtime events before this are already in DB aggregations
|
|
225
|
-
fetchTimestampRef.current = Date.now();
|
|
226
|
-
|
|
227
|
-
// Fetch usage by agent
|
|
228
|
-
const usageParams = new URLSearchParams();
|
|
229
|
-
usageParams.set("group_by", "agent");
|
|
230
|
-
if (projectParam) usageParams.set("project_id", projectParam);
|
|
231
|
-
if (since) usageParams.set("since", since);
|
|
232
|
-
const usageRes = await authFetch(`/api/telemetry/usage?${usageParams}`);
|
|
233
|
-
const usageData = await usageRes.json();
|
|
234
|
-
setFetchedUsage(usageData.usage || []);
|
|
235
|
-
|
|
236
|
-
// Fetch daily usage for charts
|
|
237
|
-
const dailyParams = new URLSearchParams();
|
|
238
|
-
dailyParams.set("group_by", "day");
|
|
239
|
-
if (projectParam) dailyParams.set("project_id", projectParam);
|
|
240
|
-
if (since) dailyParams.set("since", since);
|
|
241
|
-
const dailyRes = await authFetch(`/api/telemetry/usage?${dailyParams}`);
|
|
242
|
-
const dailyData = await dailyRes.json();
|
|
243
|
-
// Sort by date ascending for charts
|
|
244
|
-
const sorted = (dailyData.usage || []).sort((a: DailyUsage, b: DailyUsage) =>
|
|
245
|
-
a.date.localeCompare(b.date)
|
|
246
|
-
);
|
|
247
|
-
setDailyUsage(sorted);
|
|
248
|
-
|
|
249
|
-
// Fetch usage by project (only when viewing all projects and feature is enabled)
|
|
250
|
-
if (projectsEnabled && currentProjectId === null) {
|
|
251
|
-
const projParams = new URLSearchParams();
|
|
252
|
-
projParams.set("group_by", "project");
|
|
253
|
-
if (since) projParams.set("since", since);
|
|
254
|
-
const projRes = await authFetch(`/api/telemetry/usage?${projParams}`);
|
|
255
|
-
const projData = await projRes.json();
|
|
256
|
-
setProjectUsage(projData.usage || []);
|
|
257
|
-
} else {
|
|
258
|
-
setProjectUsage([]);
|
|
259
|
-
}
|
|
260
|
-
} catch (e) {
|
|
261
|
-
console.error("Failed to fetch telemetry:", e);
|
|
262
|
-
}
|
|
263
|
-
setLoading(false);
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
useEffect(() => {
|
|
267
|
-
fetchData();
|
|
268
|
-
}, [filter, timeRange, currentProjectId, authFetch, statusChangeCounter]);
|
|
269
|
-
|
|
270
|
-
// Compute real-time stats from new events (not already counted in fetched stats)
|
|
271
|
-
const stats = useMemo(() => {
|
|
272
|
-
if (!fetchedStats) return null;
|
|
273
|
-
|
|
274
|
-
// Calculate deltas from real-time events not in fetched data
|
|
275
|
-
let deltaEvents = 0;
|
|
276
|
-
let deltaLlmCalls = 0;
|
|
277
|
-
let deltaToolCalls = 0;
|
|
278
|
-
let deltaErrors = 0;
|
|
279
|
-
let deltaInputTokens = 0;
|
|
280
|
-
let deltaOutputTokens = 0;
|
|
281
|
-
let deltaCacheCreationTokens = 0;
|
|
282
|
-
let deltaCacheReadTokens = 0;
|
|
283
|
-
let deltaReasoningTokens = 0;
|
|
284
|
-
let deltaCost = 0;
|
|
285
|
-
|
|
286
|
-
for (const event of realtimeEvents) {
|
|
287
|
-
// Only count events received via SSE AFTER the last fetch (DB already has everything before)
|
|
288
|
-
if (event._receivedAt && event._receivedAt > fetchTimestampRef.current) {
|
|
289
|
-
deltaEvents++;
|
|
290
|
-
const eventStats = extractEventStats(event);
|
|
291
|
-
deltaLlmCalls += eventStats.llm_calls;
|
|
292
|
-
deltaToolCalls += eventStats.tool_calls;
|
|
293
|
-
deltaErrors += eventStats.errors;
|
|
294
|
-
deltaInputTokens += eventStats.input_tokens;
|
|
295
|
-
deltaOutputTokens += eventStats.output_tokens;
|
|
296
|
-
deltaCacheCreationTokens += eventStats.cache_creation_tokens;
|
|
297
|
-
deltaCacheReadTokens += eventStats.cache_read_tokens;
|
|
298
|
-
deltaReasoningTokens += eventStats.reasoning_tokens;
|
|
299
|
-
deltaCost += eventStats.cost;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
return {
|
|
304
|
-
total_events: fetchedStats.total_events + deltaEvents,
|
|
305
|
-
total_llm_calls: fetchedStats.total_llm_calls + deltaLlmCalls,
|
|
306
|
-
total_tool_calls: fetchedStats.total_tool_calls + deltaToolCalls,
|
|
307
|
-
total_errors: fetchedStats.total_errors + deltaErrors,
|
|
308
|
-
total_input_tokens: fetchedStats.total_input_tokens + deltaInputTokens,
|
|
309
|
-
total_output_tokens: fetchedStats.total_output_tokens + deltaOutputTokens,
|
|
310
|
-
total_cache_creation_tokens: (fetchedStats.total_cache_creation_tokens || 0) + deltaCacheCreationTokens,
|
|
311
|
-
total_cache_read_tokens: (fetchedStats.total_cache_read_tokens || 0) + deltaCacheReadTokens,
|
|
312
|
-
total_reasoning_tokens: (fetchedStats.total_reasoning_tokens || 0) + deltaReasoningTokens,
|
|
313
|
-
total_cost: (fetchedStats.total_cost || 0) + deltaCost,
|
|
314
|
-
};
|
|
315
|
-
}, [fetchedStats, realtimeEvents]);
|
|
316
|
-
|
|
317
|
-
// Compute real-time usage by agent
|
|
318
|
-
const usage = useMemo(() => {
|
|
319
|
-
// Start with a copy of fetched usage as a map
|
|
320
|
-
const usageMap = new Map<string, UsageByAgent>();
|
|
321
|
-
for (const u of fetchedUsage) {
|
|
322
|
-
usageMap.set(u.agent_id, { ...u });
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// Add deltas from real-time events received after the last fetch
|
|
326
|
-
for (const event of realtimeEvents) {
|
|
327
|
-
if (event._receivedAt && event._receivedAt > fetchTimestampRef.current) {
|
|
328
|
-
const eventStats = extractEventStats(event);
|
|
329
|
-
const existing = usageMap.get(event.agent_id);
|
|
330
|
-
if (existing) {
|
|
331
|
-
existing.llm_calls += eventStats.llm_calls;
|
|
332
|
-
existing.tool_calls += eventStats.tool_calls;
|
|
333
|
-
existing.errors += eventStats.errors;
|
|
334
|
-
existing.input_tokens += eventStats.input_tokens;
|
|
335
|
-
existing.output_tokens += eventStats.output_tokens;
|
|
336
|
-
existing.cache_creation_tokens += eventStats.cache_creation_tokens;
|
|
337
|
-
existing.cache_read_tokens += eventStats.cache_read_tokens;
|
|
338
|
-
existing.reasoning_tokens += eventStats.reasoning_tokens;
|
|
339
|
-
existing.cost += eventStats.cost;
|
|
340
|
-
} else {
|
|
341
|
-
usageMap.set(event.agent_id, {
|
|
342
|
-
agent_id: event.agent_id,
|
|
343
|
-
llm_calls: eventStats.llm_calls,
|
|
344
|
-
tool_calls: eventStats.tool_calls,
|
|
345
|
-
errors: eventStats.errors,
|
|
346
|
-
input_tokens: eventStats.input_tokens,
|
|
347
|
-
output_tokens: eventStats.output_tokens,
|
|
348
|
-
cache_creation_tokens: eventStats.cache_creation_tokens,
|
|
349
|
-
cache_read_tokens: eventStats.cache_read_tokens,
|
|
350
|
-
reasoning_tokens: eventStats.reasoning_tokens,
|
|
351
|
-
cost: eventStats.cost,
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
return Array.from(usageMap.values());
|
|
358
|
-
}, [fetchedUsage, realtimeEvents]);
|
|
359
|
-
|
|
360
|
-
// Sorted usage for the table
|
|
361
|
-
const sortedUsage = useMemo(() => {
|
|
362
|
-
const sorted = [...usage];
|
|
363
|
-
sorted.sort((a, b) => {
|
|
364
|
-
if (sortKey === "agent") {
|
|
365
|
-
const aName = (agents.find(ag => ag.id === a.agent_id)?.name || a.agent_id).toLowerCase();
|
|
366
|
-
const bName = (agents.find(ag => ag.id === b.agent_id)?.name || b.agent_id).toLowerCase();
|
|
367
|
-
return sortDir === "asc" ? (aName < bName ? -1 : 1) : (aName > bName ? -1 : 1);
|
|
368
|
-
}
|
|
369
|
-
const aVal = a[sortKey] as number;
|
|
370
|
-
const bVal = b[sortKey] as number;
|
|
371
|
-
return sortDir === "asc" ? aVal - bVal : bVal - aVal;
|
|
372
|
-
});
|
|
373
|
-
return sorted;
|
|
374
|
-
}, [usage, sortKey, sortDir, agents]);
|
|
375
|
-
|
|
376
|
-
// Sorted project usage for the table
|
|
377
|
-
const sortedProjectUsage = useMemo(() => {
|
|
378
|
-
const sorted = [...projectUsage];
|
|
379
|
-
sorted.sort((a, b) => {
|
|
380
|
-
if (projectSortKey === "project") {
|
|
381
|
-
const aName = (a.project_id ? projects.find(p => p.id === a.project_id)?.name || a.project_id : "Unassigned").toLowerCase();
|
|
382
|
-
const bName = (b.project_id ? projects.find(p => p.id === b.project_id)?.name || b.project_id : "Unassigned").toLowerCase();
|
|
383
|
-
return projectSortDir === "asc" ? (aName < bName ? -1 : 1) : (aName > bName ? -1 : 1);
|
|
384
|
-
}
|
|
385
|
-
const aVal = a[projectSortKey] as number;
|
|
386
|
-
const bVal = b[projectSortKey] as number;
|
|
387
|
-
return projectSortDir === "asc" ? aVal - bVal : bVal - aVal;
|
|
388
|
-
});
|
|
389
|
-
return sorted;
|
|
390
|
-
}, [projectUsage, projectSortKey, projectSortDir, projects]);
|
|
391
|
-
|
|
392
|
-
// Compute real-time chart data by merging fetched daily usage with SSE deltas
|
|
393
|
-
const chartData = useMemo(() => {
|
|
394
|
-
const buckets = new Map<string, DailyUsage>();
|
|
395
|
-
const useDaily = dailyUsage.length > 1;
|
|
396
|
-
|
|
397
|
-
// Seed with fetched daily data
|
|
398
|
-
for (const d of dailyUsage) {
|
|
399
|
-
buckets.set(d.date, { ...d });
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// Add deltas from real-time events received after the last fetch
|
|
403
|
-
for (const event of realtimeEvents) {
|
|
404
|
-
if (event._receivedAt && event._receivedAt > fetchTimestampRef.current) {
|
|
405
|
-
const ts = new Date(event.timestamp);
|
|
406
|
-
const key = useDaily
|
|
407
|
-
? `${ts.getFullYear()}-${String(ts.getMonth() + 1).padStart(2, "0")}-${String(ts.getDate()).padStart(2, "0")}`
|
|
408
|
-
: `${ts.getFullYear()}-${String(ts.getMonth() + 1).padStart(2, "0")}-${String(ts.getDate()).padStart(2, "0")} ${String(ts.getHours()).padStart(2, "0")}:00`;
|
|
409
|
-
if (!buckets.has(key)) {
|
|
410
|
-
buckets.set(key, { date: key, llm_calls: 0, tool_calls: 0, errors: 0, input_tokens: 0, output_tokens: 0, cache_creation_tokens: 0, cache_read_tokens: 0, reasoning_tokens: 0, cost: 0 });
|
|
411
|
-
}
|
|
412
|
-
const b = buckets.get(key)!;
|
|
413
|
-
const s = extractEventStats(event);
|
|
414
|
-
b.llm_calls += s.llm_calls;
|
|
415
|
-
b.tool_calls += s.tool_calls;
|
|
416
|
-
b.errors += s.errors;
|
|
417
|
-
b.input_tokens += s.input_tokens;
|
|
418
|
-
b.output_tokens += s.output_tokens;
|
|
419
|
-
b.cache_creation_tokens += s.cache_creation_tokens;
|
|
420
|
-
b.cache_read_tokens += s.cache_read_tokens;
|
|
421
|
-
b.reasoning_tokens += s.reasoning_tokens;
|
|
422
|
-
b.cost += s.cost;
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
return Array.from(buckets.values()).sort((a, b) => a.date.localeCompare(b.date));
|
|
427
|
-
}, [dailyUsage, realtimeEvents]);
|
|
428
|
-
|
|
429
|
-
const getProjectName = (projectId: string | null) => {
|
|
430
|
-
if (!projectId) return "Unassigned";
|
|
431
|
-
const project = projects.find(p => p.id === projectId);
|
|
432
|
-
return project?.name || projectId;
|
|
433
|
-
};
|
|
434
|
-
|
|
435
|
-
const getProjectColor = (projectId: string | null) => {
|
|
436
|
-
if (!projectId) return undefined;
|
|
437
|
-
return projects.find(p => p.id === projectId)?.color;
|
|
438
|
-
};
|
|
439
|
-
|
|
440
|
-
// Merge real-time events with historical, filtering and deduping
|
|
441
|
-
const allEvents = React.useMemo(() => {
|
|
442
|
-
// Apply filters to real-time events
|
|
443
|
-
let filtered = realtimeEvents;
|
|
444
|
-
|
|
445
|
-
// Filter by project (for real-time events)
|
|
446
|
-
if (currentProjectId !== null) {
|
|
447
|
-
filtered = filtered.filter(e => projectAgentIds.has(e.agent_id));
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
if (filter.agent_id) {
|
|
451
|
-
filtered = filtered.filter(e => e.agent_id === filter.agent_id);
|
|
452
|
-
}
|
|
453
|
-
// Filter out hidden categories
|
|
454
|
-
if (hiddenCategories.size > 0) {
|
|
455
|
-
filtered = filtered.filter(e => !hiddenCategories.has(e.category));
|
|
456
|
-
}
|
|
457
|
-
if (filter.level) {
|
|
458
|
-
filtered = filtered.filter(e => e.level === filter.level);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// Filter historical events too
|
|
462
|
-
let filteredHistorical = historicalEvents;
|
|
463
|
-
if (hiddenCategories.size > 0) {
|
|
464
|
-
filteredHistorical = filteredHistorical.filter(e => !hiddenCategories.has(e.category));
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// Merge with historical, dedupe by ID
|
|
468
|
-
const seen = new Set(filtered.map(e => e.id));
|
|
469
|
-
const merged = [...filtered];
|
|
470
|
-
for (const evt of filteredHistorical) {
|
|
471
|
-
if (!seen.has(evt.id)) {
|
|
472
|
-
merged.push(evt);
|
|
473
|
-
seen.add(evt.id);
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// Sort by timestamp descending
|
|
478
|
-
merged.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
479
|
-
|
|
480
|
-
return merged.slice(0, 100);
|
|
481
|
-
}, [realtimeEvents, historicalEvents, filter, hiddenCategories, currentProjectId, projectAgentIds]);
|
|
482
|
-
|
|
483
|
-
// Track new events for animation - mark events as "new" when they first appear
|
|
484
|
-
useEffect(() => {
|
|
485
|
-
const newIds: string[] = [];
|
|
486
|
-
for (const event of realtimeEvents) {
|
|
487
|
-
if (!seenEventIdsRef.current.has(event.id)) {
|
|
488
|
-
seenEventIdsRef.current.add(event.id);
|
|
489
|
-
newIds.push(event.id);
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
if (newIds.length > 0) {
|
|
494
|
-
setNewEventIds(prev => {
|
|
495
|
-
const updated = new Set(prev);
|
|
496
|
-
newIds.forEach(id => updated.add(id));
|
|
497
|
-
return updated;
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
// Remove "new" status after 5 seconds
|
|
501
|
-
setTimeout(() => {
|
|
502
|
-
setNewEventIds(prev => {
|
|
503
|
-
const updated = new Set(prev);
|
|
504
|
-
newIds.forEach(id => updated.delete(id));
|
|
505
|
-
return updated;
|
|
506
|
-
});
|
|
507
|
-
}, 5000);
|
|
508
|
-
}
|
|
509
|
-
}, [realtimeEvents]);
|
|
510
|
-
|
|
511
|
-
const getAgentName = (agentId: string) => {
|
|
512
|
-
const agent = agents.find(a => a.id === agentId);
|
|
513
|
-
return agent?.name || agentId;
|
|
514
|
-
};
|
|
515
|
-
|
|
516
|
-
const formatNumber = (n: number) => {
|
|
517
|
-
if (n >= 1000000) return (n / 1000000).toFixed(1) + "M";
|
|
518
|
-
if (n >= 1000) return (n / 1000).toFixed(1) + "K";
|
|
519
|
-
return n.toString();
|
|
520
|
-
};
|
|
521
|
-
|
|
522
|
-
const levelColors: Record<string, string> = {
|
|
523
|
-
debug: "text-[var(--color-text-faint)]",
|
|
524
|
-
info: "text-blue-400",
|
|
525
|
-
warn: "text-yellow-400",
|
|
526
|
-
error: "text-red-400",
|
|
527
|
-
};
|
|
528
|
-
|
|
529
|
-
const categoryColors: Record<string, string> = {
|
|
530
|
-
LLM: "bg-purple-500/20 text-purple-400 border-purple-500/30",
|
|
531
|
-
TOOL: "bg-blue-500/20 text-blue-400 border-blue-500/30",
|
|
532
|
-
CHAT: "bg-green-500/20 text-green-400 border-green-500/30",
|
|
533
|
-
ERROR: "bg-red-500/20 text-red-400 border-red-500/30",
|
|
534
|
-
SYSTEM: "bg-gray-500/20 text-gray-400 border-gray-500/30",
|
|
535
|
-
TASK: "bg-yellow-500/20 text-yellow-400 border-yellow-500/30",
|
|
536
|
-
MEMORY: "bg-cyan-500/20 text-cyan-400 border-cyan-500/30",
|
|
537
|
-
MCP: "bg-orange-500/20 text-orange-400 border-orange-500/30",
|
|
538
|
-
DATABASE: "bg-pink-500/20 text-pink-400 border-pink-500/30",
|
|
539
|
-
};
|
|
540
|
-
|
|
541
|
-
const allCategories = ["LLM", "TOOL", "CHAT", "TASK", "MEMORY", "MCP", "SYSTEM", "DATABASE", "ERROR"];
|
|
542
|
-
|
|
543
|
-
const toggleCategory = (category: string) => {
|
|
544
|
-
setHiddenCategories(prev => {
|
|
545
|
-
const updated = new Set(prev);
|
|
546
|
-
if (updated.has(category)) {
|
|
547
|
-
updated.delete(category);
|
|
548
|
-
} else {
|
|
549
|
-
updated.add(category);
|
|
550
|
-
}
|
|
551
|
-
return updated;
|
|
552
|
-
});
|
|
553
|
-
};
|
|
554
|
-
|
|
555
|
-
const agentOptions = [
|
|
556
|
-
{ value: "", label: "All Agents" },
|
|
557
|
-
...filteredAgents.map(a => ({ value: a.id, label: a.name })),
|
|
558
|
-
];
|
|
559
|
-
|
|
560
|
-
const levelOptions = [
|
|
561
|
-
{ value: "", label: "All Levels" },
|
|
562
|
-
{ value: "debug", label: "Debug" },
|
|
563
|
-
{ value: "info", label: "Info" },
|
|
564
|
-
{ value: "warn", label: "Warn" },
|
|
565
|
-
{ value: "error", label: "Error" },
|
|
566
|
-
];
|
|
567
|
-
|
|
568
|
-
return (
|
|
569
|
-
<div className="flex-1 overflow-auto p-6">
|
|
570
|
-
<div>
|
|
571
|
-
{/* Header */}
|
|
572
|
-
<div className="mb-6">
|
|
573
|
-
<div className="flex items-center gap-3 mb-1">
|
|
574
|
-
{currentProject && (
|
|
575
|
-
<span
|
|
576
|
-
className="w-3 h-3 rounded-full"
|
|
577
|
-
style={{ backgroundColor: currentProject.color }}
|
|
578
|
-
/>
|
|
579
|
-
)}
|
|
580
|
-
<h1 className="text-2xl font-semibold">
|
|
581
|
-
{currentProjectId === null
|
|
582
|
-
? "Analytics"
|
|
583
|
-
: currentProjectId === "unassigned"
|
|
584
|
-
? "Analytics - Unassigned"
|
|
585
|
-
: `Analytics - ${currentProject?.name || ""}`}
|
|
586
|
-
</h1>
|
|
587
|
-
</div>
|
|
588
|
-
<p className="text-[var(--color-text-muted)]">
|
|
589
|
-
Monitor agent activity, usage, costs, and performance.
|
|
590
|
-
</p>
|
|
591
|
-
</div>
|
|
592
|
-
|
|
593
|
-
{/* Time Range Selector */}
|
|
594
|
-
<div className="flex gap-2 mb-6">
|
|
595
|
-
{[
|
|
596
|
-
{ value: "1h", label: "Last hour" },
|
|
597
|
-
{ value: "6h", label: "Last 6h" },
|
|
598
|
-
{ value: "24h", label: "Last 24h" },
|
|
599
|
-
{ value: "7d", label: "Last 7 days" },
|
|
600
|
-
{ value: "30d", label: "Last 30 days" },
|
|
601
|
-
{ value: "all", label: "All time" },
|
|
602
|
-
].map(opt => (
|
|
603
|
-
<button
|
|
604
|
-
key={opt.value}
|
|
605
|
-
onClick={() => setTimeRange(opt.value)}
|
|
606
|
-
className={`px-3 py-1.5 rounded text-sm transition ${
|
|
607
|
-
timeRange === opt.value
|
|
608
|
-
? "bg-[var(--color-accent)] text-black"
|
|
609
|
-
: "bg-[var(--color-surface-raised)] hover:bg-[var(--color-surface-raised)] text-[var(--color-text-secondary)]"
|
|
610
|
-
}`}
|
|
611
|
-
>
|
|
612
|
-
{opt.label}
|
|
613
|
-
</button>
|
|
614
|
-
))}
|
|
615
|
-
</div>
|
|
616
|
-
|
|
617
|
-
{/* Stats Cards */}
|
|
618
|
-
{stats && (
|
|
619
|
-
<div className="flex flex-wrap gap-4 mb-6">
|
|
620
|
-
<StatCard label="Events" value={formatNumber(stats.total_events)} />
|
|
621
|
-
<StatCard label="LLM Calls" value={formatNumber(stats.total_llm_calls)} />
|
|
622
|
-
{isDev && <StatCard label="Tool Calls" value={formatNumber(stats.total_tool_calls)} />}
|
|
623
|
-
<StatCard label="Errors" value={formatNumber(stats.total_errors)} color="red" />
|
|
624
|
-
{isDev && (
|
|
625
|
-
<>
|
|
626
|
-
<StatCard label="Input Tokens" value={formatNumber(stats.total_input_tokens)} />
|
|
627
|
-
<StatCard label="Output Tokens" value={formatNumber(stats.total_output_tokens)} />
|
|
628
|
-
{(stats.total_cache_creation_tokens > 0 || stats.total_cache_read_tokens > 0) && (
|
|
629
|
-
<>
|
|
630
|
-
<StatCard label="Cache Write" value={formatNumber(stats.total_cache_creation_tokens)} />
|
|
631
|
-
<StatCard label="Cache Read" value={formatNumber(stats.total_cache_read_tokens)} />
|
|
632
|
-
</>
|
|
633
|
-
)}
|
|
634
|
-
{stats.total_reasoning_tokens > 0 && (
|
|
635
|
-
<StatCard label="Reasoning" value={formatNumber(stats.total_reasoning_tokens)} />
|
|
636
|
-
)}
|
|
637
|
-
</>
|
|
638
|
-
)}
|
|
639
|
-
{costTrackingEnabled && (
|
|
640
|
-
<StatCard label="Total Cost" value={`$${stats.total_cost.toFixed(4)}`} color="orange" />
|
|
641
|
-
)}
|
|
642
|
-
</div>
|
|
643
|
-
)}
|
|
644
|
-
|
|
645
|
-
{/* Charts */}
|
|
646
|
-
{(() => {
|
|
647
|
-
const useDaily = dailyUsage.length > 1;
|
|
648
|
-
const chartLabel = useDaily ? "Daily" : "Hourly";
|
|
649
|
-
|
|
650
|
-
if (chartData.length === 0) return null;
|
|
651
|
-
|
|
652
|
-
return (
|
|
653
|
-
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
|
|
654
|
-
{/* Activity Chart */}
|
|
655
|
-
<div className="bg-[var(--color-surface)] card p-4">
|
|
656
|
-
<h3 className="text-sm font-medium text-[var(--color-text-secondary)] mb-4">{chartLabel} Activity</h3>
|
|
657
|
-
<ResponsiveContainer width="100%" height={200}>
|
|
658
|
-
<AreaChart data={chartData}>
|
|
659
|
-
<CartesianGrid strokeDasharray="3 3" stroke="var(--color-border)" />
|
|
660
|
-
<XAxis
|
|
661
|
-
dataKey="date"
|
|
662
|
-
stroke="var(--color-border-light)"
|
|
663
|
-
tick={{ fill: "var(--color-text-muted)", fontSize: 11 }}
|
|
664
|
-
tickFormatter={(v) => {
|
|
665
|
-
if (!useDaily && v.includes(" ")) {
|
|
666
|
-
return v.split(" ")[1];
|
|
667
|
-
}
|
|
668
|
-
const d = new Date(v + "T00:00:00");
|
|
669
|
-
return `${d.getMonth() + 1}/${d.getDate()}`;
|
|
670
|
-
}}
|
|
671
|
-
/>
|
|
672
|
-
<YAxis stroke="var(--color-border-light)" tick={{ fill: "var(--color-text-muted)", fontSize: 11 }} allowDecimals={false} />
|
|
673
|
-
<Tooltip
|
|
674
|
-
contentStyle={{
|
|
675
|
-
backgroundColor: "var(--color-surface)",
|
|
676
|
-
border: "1px solid var(--color-border-light)",
|
|
677
|
-
borderRadius: "8px",
|
|
678
|
-
fontSize: 12,
|
|
679
|
-
}}
|
|
680
|
-
labelStyle={{ color: "var(--color-text-secondary)" }}
|
|
681
|
-
cursor={{ stroke: "rgba(255,255,255,0.1)" }}
|
|
682
|
-
labelFormatter={(v) => useDaily ? new Date(v + "T00:00:00").toLocaleDateString() : v}
|
|
683
|
-
/>
|
|
684
|
-
<Legend
|
|
685
|
-
wrapperStyle={{ fontSize: 11 }}
|
|
686
|
-
iconType="circle"
|
|
687
|
-
iconSize={8}
|
|
688
|
-
/>
|
|
689
|
-
<Area
|
|
690
|
-
type="monotone"
|
|
691
|
-
dataKey="llm_calls"
|
|
692
|
-
name="LLM Calls"
|
|
693
|
-
stroke="var(--color-accent)"
|
|
694
|
-
fill="var(--color-accent)"
|
|
695
|
-
fillOpacity={0.15}
|
|
696
|
-
strokeWidth={1.5}
|
|
697
|
-
|
|
698
|
-
/>
|
|
699
|
-
<Area
|
|
700
|
-
type="monotone"
|
|
701
|
-
dataKey="tool_calls"
|
|
702
|
-
name="Tool Calls"
|
|
703
|
-
stroke="var(--color-accent-hover)"
|
|
704
|
-
fill="var(--color-accent-hover)"
|
|
705
|
-
fillOpacity={0.08}
|
|
706
|
-
strokeWidth={1.5}
|
|
707
|
-
|
|
708
|
-
/>
|
|
709
|
-
<Area
|
|
710
|
-
type="monotone"
|
|
711
|
-
dataKey="errors"
|
|
712
|
-
name="Errors"
|
|
713
|
-
stroke="#ef4444"
|
|
714
|
-
fill="#ef4444"
|
|
715
|
-
fillOpacity={0.1}
|
|
716
|
-
strokeWidth={1.5}
|
|
717
|
-
|
|
718
|
-
/>
|
|
719
|
-
</AreaChart>
|
|
720
|
-
</ResponsiveContainer>
|
|
721
|
-
</div>
|
|
722
|
-
|
|
723
|
-
{/* Token Usage Chart - stacked bars: Input (cache read / cache write / regular) and Output (reasoning / regular) */}
|
|
724
|
-
{(() => {
|
|
725
|
-
// Compute stacked segments for each bucket
|
|
726
|
-
const stackedData = chartData.map(d => {
|
|
727
|
-
const cacheRead = d.cache_read_tokens || 0;
|
|
728
|
-
const cacheWrite = d.cache_creation_tokens || 0;
|
|
729
|
-
// Anthropic: input_tokens includes cache_read but NOT cache_creation
|
|
730
|
-
// So regular = input_tokens - cache_read (don't subtract cache_write)
|
|
731
|
-
const regularInput = Math.max(0, d.input_tokens - cacheRead);
|
|
732
|
-
const reasoning = d.reasoning_tokens || 0;
|
|
733
|
-
const regularOutput = Math.max(0, d.output_tokens - reasoning);
|
|
734
|
-
return {
|
|
735
|
-
date: d.date,
|
|
736
|
-
// Input stack (bottom to top: cache read, cache write, regular)
|
|
737
|
-
input_cache_read: cacheRead,
|
|
738
|
-
input_cache_write: cacheWrite,
|
|
739
|
-
input_regular: regularInput,
|
|
740
|
-
// Output stack (bottom to top: reasoning, regular)
|
|
741
|
-
output_reasoning: reasoning,
|
|
742
|
-
output_regular: regularOutput,
|
|
743
|
-
};
|
|
744
|
-
});
|
|
745
|
-
const hasCache = stackedData.some(d => d.input_cache_read > 0 || d.input_cache_write > 0);
|
|
746
|
-
const hasReasoning = stackedData.some(d => d.output_reasoning > 0);
|
|
747
|
-
|
|
748
|
-
return (
|
|
749
|
-
<div className="bg-[var(--color-surface)] card p-4">
|
|
750
|
-
<h3 className="text-sm font-medium text-[var(--color-text-secondary)] mb-4">{chartLabel} Token Usage</h3>
|
|
751
|
-
<ResponsiveContainer width="100%" height={200}>
|
|
752
|
-
<BarChart data={stackedData}>
|
|
753
|
-
<CartesianGrid strokeDasharray="3 3" stroke="var(--color-border)" />
|
|
754
|
-
<XAxis
|
|
755
|
-
dataKey="date"
|
|
756
|
-
stroke="var(--color-border-light)"
|
|
757
|
-
tick={{ fill: "var(--color-text-muted)", fontSize: 11 }}
|
|
758
|
-
tickFormatter={(v) => {
|
|
759
|
-
if (!useDaily && v.includes(" ")) {
|
|
760
|
-
return v.split(" ")[1];
|
|
761
|
-
}
|
|
762
|
-
const d = new Date(v + "T00:00:00");
|
|
763
|
-
return `${d.getMonth() + 1}/${d.getDate()}`;
|
|
764
|
-
}}
|
|
765
|
-
/>
|
|
766
|
-
<YAxis
|
|
767
|
-
stroke="var(--color-border-light)"
|
|
768
|
-
tick={{ fill: "var(--color-text-muted)", fontSize: 11 }}
|
|
769
|
-
tickFormatter={(v) => {
|
|
770
|
-
if (v >= 1000000) return `${(v / 1000000).toFixed(1)}M`;
|
|
771
|
-
if (v >= 1000) return `${(v / 1000).toFixed(0)}K`;
|
|
772
|
-
return v;
|
|
773
|
-
}}
|
|
774
|
-
/>
|
|
775
|
-
<Tooltip
|
|
776
|
-
contentStyle={{
|
|
777
|
-
backgroundColor: "var(--color-surface)",
|
|
778
|
-
border: "1px solid var(--color-border-light)",
|
|
779
|
-
borderRadius: "8px",
|
|
780
|
-
fontSize: 12,
|
|
781
|
-
}}
|
|
782
|
-
labelStyle={{ color: "var(--color-text-secondary)" }}
|
|
783
|
-
cursor={{ fill: "rgba(255,255,255,0.03)" }}
|
|
784
|
-
labelFormatter={(v) => useDaily ? new Date(v + "T00:00:00").toLocaleDateString() : v}
|
|
785
|
-
formatter={(value: number, name: string) => [value.toLocaleString(), name]}
|
|
786
|
-
/>
|
|
787
|
-
<Legend
|
|
788
|
-
wrapperStyle={{ fontSize: 11 }}
|
|
789
|
-
iconType="circle"
|
|
790
|
-
iconSize={8}
|
|
791
|
-
/>
|
|
792
|
-
{/* Input stack */}
|
|
793
|
-
{hasCache && (
|
|
794
|
-
<Bar dataKey="input_cache_read" name="Input (Cache Read)" stackId="input" fill="#fdba74" />
|
|
795
|
-
)}
|
|
796
|
-
{hasCache && (
|
|
797
|
-
<Bar dataKey="input_cache_write" name="Input (Cache Write)" stackId="input" fill="#fb923c" />
|
|
798
|
-
)}
|
|
799
|
-
<Bar dataKey="input_regular" name={hasCache ? "Input (Regular)" : "Input Tokens"} stackId="input" fill="#f97316" radius={[2, 2, 0, 0]} />
|
|
800
|
-
{/* Output stack */}
|
|
801
|
-
{hasReasoning && (
|
|
802
|
-
<Bar dataKey="output_reasoning" name="Output (Reasoning)" stackId="output" fill="#c4b5fd" />
|
|
803
|
-
)}
|
|
804
|
-
<Bar dataKey="output_regular" name={hasReasoning ? "Output (Regular)" : "Output Tokens"} stackId="output" fill="#a78bfa" radius={[2, 2, 0, 0]} />
|
|
805
|
-
</BarChart>
|
|
806
|
-
</ResponsiveContainer>
|
|
807
|
-
</div>
|
|
808
|
-
);
|
|
809
|
-
})()}
|
|
810
|
-
</div>
|
|
811
|
-
);
|
|
812
|
-
})()}
|
|
813
|
-
|
|
814
|
-
{/* Usage by Agent */}
|
|
815
|
-
{usage.length > 0 && (() => {
|
|
816
|
-
const maxCost = Math.max(...sortedUsage.map(u => u.cost), 0.0001);
|
|
817
|
-
const hasCacheTokens = sortedUsage.some(u => (u.cache_creation_tokens || 0) > 0 || (u.cache_read_tokens || 0) > 0);
|
|
818
|
-
const hasReasoningTokens = sortedUsage.some(u => (u.reasoning_tokens || 0) > 0);
|
|
819
|
-
const SortHeader = ({ label, field, align = "right" }: { label: string; field: SortKey; align?: string }) => (
|
|
820
|
-
<th
|
|
821
|
-
className={`${align === "left" ? "text-left" : "text-right"} p-3 cursor-pointer hover:text-[var(--color-text-secondary)] select-none transition-colors`}
|
|
822
|
-
onClick={() => handleSort(field)}
|
|
823
|
-
>
|
|
824
|
-
<span className="inline-flex items-center gap-1">
|
|
825
|
-
{align === "right" && sortKey === field && (
|
|
826
|
-
<span className="text-orange-400">{sortDir === "asc" ? "\u25b2" : "\u25bc"}</span>
|
|
827
|
-
)}
|
|
828
|
-
{label}
|
|
829
|
-
{align === "left" && sortKey === field && (
|
|
830
|
-
<span className="text-orange-400">{sortDir === "asc" ? "\u25b2" : "\u25bc"}</span>
|
|
831
|
-
)}
|
|
832
|
-
</span>
|
|
833
|
-
</th>
|
|
834
|
-
);
|
|
835
|
-
|
|
836
|
-
return (
|
|
837
|
-
<div className="mb-6">
|
|
838
|
-
<h2 className="text-lg font-medium mb-3">{t("Usage by Agent", "Usage by Employee")}</h2>
|
|
839
|
-
<div className="bg-[var(--color-surface)] card overflow-hidden">
|
|
840
|
-
<table className="w-full text-sm">
|
|
841
|
-
<thead>
|
|
842
|
-
<tr className="border-b border-[var(--color-border)] text-[var(--color-text-muted)]">
|
|
843
|
-
<SortHeader label={t("Agent", "Employee")} field="agent" align="left" />
|
|
844
|
-
<SortHeader label="LLM Calls" field="llm_calls" />
|
|
845
|
-
{isDev && <SortHeader label="Tool Calls" field="tool_calls" />}
|
|
846
|
-
{isDev && <SortHeader label="Input Tokens" field="input_tokens" />}
|
|
847
|
-
{isDev && <SortHeader label="Output Tokens" field="output_tokens" />}
|
|
848
|
-
{isDev && hasCacheTokens && <SortHeader label="Cache Write" field="cache_creation_tokens" />}
|
|
849
|
-
{isDev && hasCacheTokens && <SortHeader label="Cache Read" field="cache_read_tokens" />}
|
|
850
|
-
{isDev && hasReasoningTokens && <SortHeader label="Reasoning" field="reasoning_tokens" />}
|
|
851
|
-
<SortHeader label="Errors" field="errors" />
|
|
852
|
-
{costTrackingEnabled && <SortHeader label="Est. Cost" field="cost" />}
|
|
853
|
-
</tr>
|
|
854
|
-
</thead>
|
|
855
|
-
<tbody>
|
|
856
|
-
{sortedUsage.map((u) => (
|
|
857
|
-
<tr key={u.agent_id} className="border-b border-[var(--color-border)] last:border-0 hover:bg-[var(--color-bg)]">
|
|
858
|
-
<td className="p-3 font-medium">{getAgentName(u.agent_id)}</td>
|
|
859
|
-
<td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.llm_calls)}</td>
|
|
860
|
-
{isDev && <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.tool_calls)}</td>}
|
|
861
|
-
{isDev && <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.input_tokens)}</td>}
|
|
862
|
-
{isDev && <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.output_tokens)}</td>}
|
|
863
|
-
{isDev && hasCacheTokens && (
|
|
864
|
-
<td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.cache_creation_tokens || 0)}</td>
|
|
865
|
-
)}
|
|
866
|
-
{isDev && hasCacheTokens && (
|
|
867
|
-
<td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.cache_read_tokens || 0)}</td>
|
|
868
|
-
)}
|
|
869
|
-
{isDev && hasReasoningTokens && (
|
|
870
|
-
<td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.reasoning_tokens || 0)}</td>
|
|
871
|
-
)}
|
|
872
|
-
<td className="p-3 text-right">
|
|
873
|
-
{u.errors > 0 ? (
|
|
874
|
-
<span className="text-red-400">{u.errors}</span>
|
|
875
|
-
) : (
|
|
876
|
-
<span className="text-[var(--color-text-faint)]">0</span>
|
|
877
|
-
)}
|
|
878
|
-
</td>
|
|
879
|
-
{costTrackingEnabled && (
|
|
880
|
-
<td className="p-3 text-right">
|
|
881
|
-
<div className="flex items-center justify-end gap-2">
|
|
882
|
-
<div className="w-16 h-1.5 bg-[var(--color-surface-raised)] rounded-full overflow-hidden">
|
|
883
|
-
<div
|
|
884
|
-
className="h-full bg-orange-500 rounded-full"
|
|
885
|
-
style={{ width: `${(u.cost / maxCost) * 100}%` }}
|
|
886
|
-
/>
|
|
887
|
-
</div>
|
|
888
|
-
<span className="text-[var(--color-text-secondary)] min-w-[60px] text-right">${u.cost.toFixed(4)}</span>
|
|
889
|
-
</div>
|
|
890
|
-
</td>
|
|
891
|
-
)}
|
|
892
|
-
</tr>
|
|
893
|
-
))}
|
|
894
|
-
</tbody>
|
|
895
|
-
</table>
|
|
896
|
-
</div>
|
|
897
|
-
</div>
|
|
898
|
-
);
|
|
899
|
-
})()}
|
|
900
|
-
|
|
901
|
-
{/* Usage by Project */}
|
|
902
|
-
{projectsEnabled && currentProjectId === null && sortedProjectUsage.length > 0 && (() => {
|
|
903
|
-
const maxCost = Math.max(...sortedProjectUsage.map(u => u.cost), 0.0001);
|
|
904
|
-
const hasProjCacheTokens = sortedProjectUsage.some(u => (u.cache_creation_tokens || 0) > 0 || (u.cache_read_tokens || 0) > 0);
|
|
905
|
-
const hasProjReasoningTokens = sortedProjectUsage.some(u => (u.reasoning_tokens || 0) > 0);
|
|
906
|
-
const PSortHeader = ({ label, field, align = "right" }: { label: string; field: ProjectSortKey; align?: string }) => (
|
|
907
|
-
<th
|
|
908
|
-
className={`${align === "left" ? "text-left" : "text-right"} p-3 cursor-pointer hover:text-[var(--color-text-secondary)] select-none transition-colors`}
|
|
909
|
-
onClick={() => handleProjectSort(field)}
|
|
910
|
-
>
|
|
911
|
-
<span className="inline-flex items-center gap-1">
|
|
912
|
-
{align === "right" && projectSortKey === field && (
|
|
913
|
-
<span className="text-orange-400">{projectSortDir === "asc" ? "\u25b2" : "\u25bc"}</span>
|
|
914
|
-
)}
|
|
915
|
-
{label}
|
|
916
|
-
{align === "left" && projectSortKey === field && (
|
|
917
|
-
<span className="text-orange-400">{projectSortDir === "asc" ? "\u25b2" : "\u25bc"}</span>
|
|
918
|
-
)}
|
|
919
|
-
</span>
|
|
920
|
-
</th>
|
|
921
|
-
);
|
|
922
|
-
|
|
923
|
-
return (
|
|
924
|
-
<div className="mb-6">
|
|
925
|
-
<h2 className="text-lg font-medium mb-3">Usage by Project</h2>
|
|
926
|
-
<div className="bg-[var(--color-surface)] card overflow-hidden">
|
|
927
|
-
<table className="w-full text-sm">
|
|
928
|
-
<thead>
|
|
929
|
-
<tr className="border-b border-[var(--color-border)] text-[var(--color-text-muted)]">
|
|
930
|
-
<PSortHeader label="Project" field="project" align="left" />
|
|
931
|
-
<PSortHeader label="LLM Calls" field="llm_calls" />
|
|
932
|
-
{isDev && <PSortHeader label="Tool Calls" field="tool_calls" />}
|
|
933
|
-
{isDev && <PSortHeader label="Input Tokens" field="input_tokens" />}
|
|
934
|
-
{isDev && <PSortHeader label="Output Tokens" field="output_tokens" />}
|
|
935
|
-
{isDev && hasProjCacheTokens && <PSortHeader label="Cache Write" field="cache_creation_tokens" />}
|
|
936
|
-
{isDev && hasProjCacheTokens && <PSortHeader label="Cache Read" field="cache_read_tokens" />}
|
|
937
|
-
{isDev && hasProjReasoningTokens && <PSortHeader label="Reasoning" field="reasoning_tokens" />}
|
|
938
|
-
<PSortHeader label="Errors" field="errors" />
|
|
939
|
-
{costTrackingEnabled && <PSortHeader label="Est. Cost" field="cost" />}
|
|
940
|
-
</tr>
|
|
941
|
-
</thead>
|
|
942
|
-
<tbody>
|
|
943
|
-
{sortedProjectUsage.map((u) => {
|
|
944
|
-
const color = getProjectColor(u.project_id);
|
|
945
|
-
return (
|
|
946
|
-
<tr key={u.project_id || "_unassigned"} className="border-b border-[var(--color-border)] last:border-0 hover:bg-[var(--color-bg)]">
|
|
947
|
-
<td className="p-3 font-medium">
|
|
948
|
-
<span className="inline-flex items-center gap-2">
|
|
949
|
-
{color && <span className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: color }} />}
|
|
950
|
-
{getProjectName(u.project_id)}
|
|
951
|
-
</span>
|
|
952
|
-
</td>
|
|
953
|
-
<td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.llm_calls)}</td>
|
|
954
|
-
{isDev && <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.tool_calls)}</td>}
|
|
955
|
-
{isDev && <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.input_tokens)}</td>}
|
|
956
|
-
{isDev && <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.output_tokens)}</td>}
|
|
957
|
-
{isDev && hasProjCacheTokens && (
|
|
958
|
-
<td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.cache_creation_tokens || 0)}</td>
|
|
959
|
-
)}
|
|
960
|
-
{isDev && hasProjCacheTokens && (
|
|
961
|
-
<td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.cache_read_tokens || 0)}</td>
|
|
962
|
-
)}
|
|
963
|
-
{isDev && hasProjReasoningTokens && (
|
|
964
|
-
<td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.reasoning_tokens || 0)}</td>
|
|
965
|
-
)}
|
|
966
|
-
<td className="p-3 text-right">
|
|
967
|
-
{u.errors > 0 ? (
|
|
968
|
-
<span className="text-red-400">{u.errors}</span>
|
|
969
|
-
) : (
|
|
970
|
-
<span className="text-[var(--color-text-faint)]">0</span>
|
|
971
|
-
)}
|
|
972
|
-
</td>
|
|
973
|
-
{costTrackingEnabled && (
|
|
974
|
-
<td className="p-3 text-right">
|
|
975
|
-
<div className="flex items-center justify-end gap-2">
|
|
976
|
-
<div className="w-16 h-1.5 bg-[var(--color-surface-raised)] rounded-full overflow-hidden">
|
|
977
|
-
<div
|
|
978
|
-
className="h-full bg-orange-500 rounded-full"
|
|
979
|
-
style={{ width: `${(u.cost / maxCost) * 100}%` }}
|
|
980
|
-
/>
|
|
981
|
-
</div>
|
|
982
|
-
<span className="text-[var(--color-text-secondary)] min-w-[60px] text-right">${u.cost.toFixed(4)}</span>
|
|
983
|
-
</div>
|
|
984
|
-
</td>
|
|
985
|
-
)}
|
|
986
|
-
</tr>
|
|
987
|
-
);
|
|
988
|
-
})}
|
|
989
|
-
</tbody>
|
|
990
|
-
</table>
|
|
991
|
-
</div>
|
|
992
|
-
</div>
|
|
993
|
-
);
|
|
994
|
-
})()}
|
|
995
|
-
|
|
996
|
-
{/* Filters */}
|
|
997
|
-
<div className="flex flex-wrap items-center gap-3 mb-4">
|
|
998
|
-
<div className="w-44">
|
|
999
|
-
<Select
|
|
1000
|
-
value={filter.agent_id}
|
|
1001
|
-
options={agentOptions}
|
|
1002
|
-
onChange={(value) => setFilter({ ...filter, agent_id: value })}
|
|
1003
|
-
placeholder="All Agents"
|
|
1004
|
-
/>
|
|
1005
|
-
</div>
|
|
1006
|
-
{/* Category toggles */}
|
|
1007
|
-
<div className="flex flex-wrap items-center gap-1.5 flex-1">
|
|
1008
|
-
{allCategories.map((cat) => {
|
|
1009
|
-
const isHidden = hiddenCategories.has(cat);
|
|
1010
|
-
const colorClass = categoryColors[cat] || "bg-[var(--color-surface-raised)] text-[var(--color-text-secondary)] border-[var(--color-border-light)]";
|
|
1011
|
-
return (
|
|
1012
|
-
<button
|
|
1013
|
-
key={cat}
|
|
1014
|
-
onClick={() => toggleCategory(cat)}
|
|
1015
|
-
className={`px-2 py-0.5 rounded text-xs border transition-all ${
|
|
1016
|
-
isHidden
|
|
1017
|
-
? "bg-[var(--color-surface-raised)] text-[var(--color-text-faint)] border-[var(--color-border-light)] opacity-50"
|
|
1018
|
-
: colorClass
|
|
1019
|
-
}`}
|
|
1020
|
-
>
|
|
1021
|
-
{cat}
|
|
1022
|
-
</button>
|
|
1023
|
-
);
|
|
1024
|
-
})}
|
|
1025
|
-
</div>
|
|
1026
|
-
<div className="flex items-center gap-2">
|
|
1027
|
-
<div className="w-36">
|
|
1028
|
-
<Select
|
|
1029
|
-
value={filter.level}
|
|
1030
|
-
options={levelOptions}
|
|
1031
|
-
onChange={(value) => setFilter({ ...filter, level: value })}
|
|
1032
|
-
placeholder="All Levels"
|
|
1033
|
-
/>
|
|
1034
|
-
</div>
|
|
1035
|
-
<button
|
|
1036
|
-
onClick={fetchData}
|
|
1037
|
-
className="px-3 py-2 bg-[var(--color-surface-raised)] hover:bg-[var(--color-surface-raised)] border border-[var(--color-border-light)] rounded text-sm transition"
|
|
1038
|
-
>
|
|
1039
|
-
Refresh
|
|
1040
|
-
</button>
|
|
1041
|
-
</div>
|
|
1042
|
-
</div>
|
|
1043
|
-
|
|
1044
|
-
{/* Events List */}
|
|
1045
|
-
<div className="bg-[var(--color-surface)] card">
|
|
1046
|
-
<div className="p-3 border-b border-[var(--color-border)] flex items-center justify-between">
|
|
1047
|
-
<h2 className="font-medium">Recent Events</h2>
|
|
1048
|
-
{realtimeEvents.length > 0 && (
|
|
1049
|
-
<span className="text-xs text-[var(--color-text-muted)]">
|
|
1050
|
-
{realtimeEvents.length} new
|
|
1051
|
-
</span>
|
|
1052
|
-
)}
|
|
1053
|
-
</div>
|
|
1054
|
-
|
|
1055
|
-
{loading && allEvents.length === 0 ? (
|
|
1056
|
-
<div className="p-8 text-center text-[var(--color-text-muted)]">Loading...</div>
|
|
1057
|
-
) : allEvents.length === 0 ? (
|
|
1058
|
-
<div className="p-8 text-center text-[var(--color-text-muted)]">
|
|
1059
|
-
No events yet. Events will appear here in real-time once agents start sending data.
|
|
1060
|
-
</div>
|
|
1061
|
-
) : (
|
|
1062
|
-
<div className="divide-y divide-[var(--color-border)]">
|
|
1063
|
-
{allEvents.map((event) => {
|
|
1064
|
-
const isNew = newEventIds.has(event.id);
|
|
1065
|
-
|
|
1066
|
-
return (
|
|
1067
|
-
<div
|
|
1068
|
-
key={event.id}
|
|
1069
|
-
className={`p-3 hover:bg-[var(--color-bg)] cursor-pointer transition-all duration-500 ${
|
|
1070
|
-
isNew ? "bg-green-500/5" : ""
|
|
1071
|
-
}`}
|
|
1072
|
-
style={{
|
|
1073
|
-
animation: isNew ? "slideIn 0.3s ease-out" : undefined,
|
|
1074
|
-
}}
|
|
1075
|
-
onClick={() => setExpandedEvent(expandedEvent === event.id ? null : event.id)}
|
|
1076
|
-
>
|
|
1077
|
-
<div className="flex items-start gap-3">
|
|
1078
|
-
<span className={`px-2 py-0.5 rounded text-xs border transition-colors duration-300 ${categoryColors[event.category] || "bg-[var(--color-surface-raised)] text-[var(--color-text-secondary)] border-[var(--color-border-light)]"}`}>
|
|
1079
|
-
{event.category}
|
|
1080
|
-
</span>
|
|
1081
|
-
<div className="flex-1 min-w-0">
|
|
1082
|
-
<div className="flex items-center gap-2">
|
|
1083
|
-
<span className="font-medium text-sm">{event.type}</span>
|
|
1084
|
-
<span className={`text-xs ${levelColors[event.level] || "text-[var(--color-text-muted)]"}`}>
|
|
1085
|
-
{event.level}
|
|
1086
|
-
</span>
|
|
1087
|
-
{event.duration_ms && (
|
|
1088
|
-
<span className="text-xs text-[var(--color-text-faint)]">{event.duration_ms}ms</span>
|
|
1089
|
-
)}
|
|
1090
|
-
<span
|
|
1091
|
-
className={`w-1.5 h-1.5 rounded-full bg-green-400 transition-opacity duration-1000 ${
|
|
1092
|
-
isNew ? "opacity-100" : "opacity-0"
|
|
1093
|
-
}`}
|
|
1094
|
-
/>
|
|
1095
|
-
</div>
|
|
1096
|
-
<div className="text-xs text-[var(--color-text-faint)] mt-1">
|
|
1097
|
-
{getAgentName(event.agent_id)} · {new Date(event.timestamp).toLocaleString()}
|
|
1098
|
-
</div>
|
|
1099
|
-
{event.error && (
|
|
1100
|
-
<div className="text-xs text-red-400 mt-1 font-mono">{event.error}</div>
|
|
1101
|
-
)}
|
|
1102
|
-
{expandedEvent === event.id && event.data && Object.keys(event.data).length > 0 && (
|
|
1103
|
-
<pre className="text-xs text-[var(--color-text-muted)] mt-2 p-2 bg-[var(--color-bg)] rounded overflow-x-auto">
|
|
1104
|
-
{JSON.stringify(event.data, null, 2)}
|
|
1105
|
-
</pre>
|
|
1106
|
-
)}
|
|
1107
|
-
</div>
|
|
1108
|
-
</div>
|
|
1109
|
-
</div>
|
|
1110
|
-
);
|
|
1111
|
-
})}
|
|
1112
|
-
</div>
|
|
1113
|
-
)}
|
|
1114
|
-
</div>
|
|
1115
|
-
</div>
|
|
1116
|
-
</div>
|
|
1117
|
-
);
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
function StatCard({ label, value, color }: { label: string; value: string; color?: string }) {
|
|
1121
|
-
return (
|
|
1122
|
-
<div className="bg-[var(--color-surface)] card p-4 flex-1 min-w-[120px]">
|
|
1123
|
-
<div className="text-[var(--color-text-muted)] text-xs mb-1">{label}</div>
|
|
1124
|
-
<div className={`text-2xl font-semibold ${color === "red" ? "text-red-400" : color === "orange" ? "text-orange-400" : ""}`}>
|
|
1125
|
-
{value}
|
|
1126
|
-
</div>
|
|
1127
|
-
</div>
|
|
1128
|
-
);
|
|
1129
|
-
}
|