apteva 0.4.56 → 0.7.0

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.
Files changed (142) hide show
  1. package/README.md +216 -54
  2. package/cli.js +35 -0
  3. package/install.js +92 -0
  4. package/package.json +12 -79
  5. package/LICENSE +0 -63
  6. package/bin/apteva.js +0 -196
  7. package/dist/ActivityPage.kxzzb4yc.js +0 -3
  8. package/dist/ApiDocsPage.zq998hbm.js +0 -4
  9. package/dist/App.55rea8mn.js +0 -61
  10. package/dist/App.5ywb23z4.js +0 -53
  11. package/dist/App.6thds120.js +0 -4
  12. package/dist/App.9tctxzqm.js +0 -8
  13. package/dist/App.a8r8ttaz.js +0 -4
  14. package/dist/App.agsv5bje.js +0 -4
  15. package/dist/App.cepapqmx.js +0 -4
  16. package/dist/App.dp041gb3.js +0 -221
  17. package/dist/App.fds72zb5.js +0 -4
  18. package/dist/App.fg9qj2dq.js +0 -4
  19. package/dist/App.ndfejbm9.js +0 -4
  20. package/dist/App.nxmfmq1h.js +0 -13
  21. package/dist/App.qdfyt8ba.js +0 -4
  22. package/dist/App.x2d0ygt6.js +0 -4
  23. package/dist/App.yt9p4nr3.js +0 -20
  24. package/dist/App.zn4mw16t.js +0 -1
  25. package/dist/ConnectionsPage.8r96ryw7.js +0 -3
  26. package/dist/McpPage.3cwh0gnd.js +0 -3
  27. package/dist/SettingsPage.ykgdh5ev.js +0 -3
  28. package/dist/SkillsPage.4np1s65b.js +0 -3
  29. package/dist/TasksPage.4g08t7p6.js +0 -3
  30. package/dist/TelemetryPage.72w9pwcp.js +0 -3
  31. package/dist/TestsPage.z4fk3r7r.js +0 -3
  32. package/dist/ThreadsPage.63tcajeh.js +0 -3
  33. package/dist/apteva-kit.css +0 -1
  34. package/dist/icon.png +0 -0
  35. package/dist/index.html +0 -16
  36. package/dist/styles.css +0 -1
  37. package/scripts/postinstall.mjs +0 -102
  38. package/src/auth/index.ts +0 -394
  39. package/src/auth/middleware.ts +0 -213
  40. package/src/binary.ts +0 -536
  41. package/src/channels/index.ts +0 -40
  42. package/src/channels/telegram.ts +0 -311
  43. package/src/crypto.ts +0 -301
  44. package/src/db-tests.ts +0 -174
  45. package/src/db.ts +0 -3133
  46. package/src/integrations/agentdojo.ts +0 -559
  47. package/src/integrations/composio.ts +0 -437
  48. package/src/integrations/index.ts +0 -87
  49. package/src/integrations/skillsmp.ts +0 -318
  50. package/src/mcp-client.ts +0 -605
  51. package/src/mcp-handler.ts +0 -394
  52. package/src/mcp-platform.ts +0 -2370
  53. package/src/openapi.ts +0 -2410
  54. package/src/providers.ts +0 -597
  55. package/src/routes/api/agent-utils.ts +0 -890
  56. package/src/routes/api/agents.ts +0 -916
  57. package/src/routes/api/api-keys.ts +0 -95
  58. package/src/routes/api/channels.ts +0 -182
  59. package/src/routes/api/helpers.ts +0 -12
  60. package/src/routes/api/integrations.ts +0 -639
  61. package/src/routes/api/mcp.ts +0 -574
  62. package/src/routes/api/meta-agent.ts +0 -195
  63. package/src/routes/api/projects.ts +0 -112
  64. package/src/routes/api/providers.ts +0 -424
  65. package/src/routes/api/skills.ts +0 -537
  66. package/src/routes/api/system.ts +0 -333
  67. package/src/routes/api/telemetry.ts +0 -203
  68. package/src/routes/api/tests.ts +0 -148
  69. package/src/routes/api/triggers.ts +0 -518
  70. package/src/routes/api/users.ts +0 -148
  71. package/src/routes/api/webhooks.ts +0 -171
  72. package/src/routes/api.ts +0 -53
  73. package/src/routes/auth.ts +0 -251
  74. package/src/routes/share.ts +0 -86
  75. package/src/routes/static.ts +0 -131
  76. package/src/server.ts +0 -642
  77. package/src/test-runner.ts +0 -598
  78. package/src/triggers/agentdojo.ts +0 -253
  79. package/src/triggers/composio.ts +0 -264
  80. package/src/triggers/index.ts +0 -71
  81. package/src/tui/AgentList.tsx +0 -145
  82. package/src/tui/App.tsx +0 -102
  83. package/src/tui/Login.tsx +0 -104
  84. package/src/tui/api.ts +0 -72
  85. package/src/tui/index.tsx +0 -7
  86. package/src/web/App.tsx +0 -455
  87. package/src/web/components/activity/ActivityPage.tsx +0 -314
  88. package/src/web/components/activity/index.ts +0 -1
  89. package/src/web/components/agents/AgentCard.tsx +0 -189
  90. package/src/web/components/agents/AgentPanel.tsx +0 -2244
  91. package/src/web/components/agents/AgentsView.tsx +0 -180
  92. package/src/web/components/agents/CreateAgentModal.tsx +0 -475
  93. package/src/web/components/agents/index.ts +0 -4
  94. package/src/web/components/api/ApiDocsPage.tsx +0 -842
  95. package/src/web/components/auth/CreateAccountStep.tsx +0 -176
  96. package/src/web/components/auth/LoginPage.tsx +0 -91
  97. package/src/web/components/auth/index.ts +0 -2
  98. package/src/web/components/common/Icons.tsx +0 -250
  99. package/src/web/components/common/LoadingSpinner.tsx +0 -44
  100. package/src/web/components/common/Modal.tsx +0 -199
  101. package/src/web/components/common/Select.tsx +0 -97
  102. package/src/web/components/common/index.ts +0 -20
  103. package/src/web/components/connections/ConnectionsPage.tsx +0 -54
  104. package/src/web/components/connections/IntegrationsTab.tsx +0 -170
  105. package/src/web/components/connections/OverviewTab.tsx +0 -137
  106. package/src/web/components/connections/TriggersTab.tsx +0 -1346
  107. package/src/web/components/dashboard/Dashboard.tsx +0 -572
  108. package/src/web/components/dashboard/index.ts +0 -1
  109. package/src/web/components/index.ts +0 -21
  110. package/src/web/components/layout/ErrorBanner.tsx +0 -18
  111. package/src/web/components/layout/Header.tsx +0 -332
  112. package/src/web/components/layout/Sidebar.tsx +0 -231
  113. package/src/web/components/layout/index.ts +0 -3
  114. package/src/web/components/mcp/IntegrationsPanel.tsx +0 -857
  115. package/src/web/components/mcp/McpPage.tsx +0 -2515
  116. package/src/web/components/mcp/index.ts +0 -1
  117. package/src/web/components/meta-agent/MetaAgent.tsx +0 -245
  118. package/src/web/components/onboarding/OnboardingWizard.tsx +0 -404
  119. package/src/web/components/onboarding/index.ts +0 -1
  120. package/src/web/components/settings/SettingsPage.tsx +0 -2776
  121. package/src/web/components/settings/index.ts +0 -1
  122. package/src/web/components/skills/SkillsPage.tsx +0 -1200
  123. package/src/web/components/tasks/TasksPage.tsx +0 -1116
  124. package/src/web/components/tasks/index.ts +0 -1
  125. package/src/web/components/telemetry/TelemetryPage.tsx +0 -1129
  126. package/src/web/components/tests/TestsPage.tsx +0 -594
  127. package/src/web/components/threads/ThreadsPage.tsx +0 -315
  128. package/src/web/context/AuthContext.tsx +0 -242
  129. package/src/web/context/ProjectContext.tsx +0 -214
  130. package/src/web/context/TelemetryContext.tsx +0 -299
  131. package/src/web/context/ThemeContext.tsx +0 -90
  132. package/src/web/context/UIModeContext.tsx +0 -49
  133. package/src/web/context/index.ts +0 -12
  134. package/src/web/hooks/index.ts +0 -3
  135. package/src/web/hooks/useAgents.ts +0 -115
  136. package/src/web/hooks/useOnboarding.ts +0 -20
  137. package/src/web/hooks/useProviders.ts +0 -75
  138. package/src/web/icon.png +0 -0
  139. package/src/web/index.html +0 -16
  140. package/src/web/styles.css +0 -118
  141. package/src/web/themes.ts +0 -162
  142. 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
- }