@nordsym/apiclaw 1.3.4 → 1.3.5

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.
@@ -1,692 +1,22 @@
1
1
  "use client";
2
2
 
3
- import { useState, useEffect, useCallback } from "react";
3
+ import { useEffect } from "react";
4
4
  import { useRouter } from "next/navigation";
5
- import {
6
- BarChart3,
7
- Zap,
8
- Users,
9
- TrendingUp,
10
- LogOut,
11
- Loader2,
12
- RefreshCw,
13
- AlertCircle,
14
- Trash2,
15
- Shield,
16
- Clock,
17
- Check,
18
- Crown,
19
- ChevronRight,
20
- } from "lucide-react";
21
- import {
22
- LineChart,
23
- Line,
24
- XAxis,
25
- YAxis,
26
- CartesianGrid,
27
- Tooltip,
28
- ResponsiveContainer,
29
- BarChart,
30
- Bar,
31
- } from "recharts";
32
- import Link from "next/link";
5
+ import { Loader2 } from "lucide-react";
33
6
 
34
- const CONVEX_URL = process.env.NEXT_PUBLIC_CONVEX_URL || "https://adventurous-avocet-799.convex.cloud";
35
-
36
- interface Workspace {
37
- id: string;
38
- email: string;
39
- tier: string;
40
- status: string;
41
- usageCount: number;
42
- usageLimit: number;
43
- usageRemaining: number;
44
- usagePercentage: number;
45
- createdAt: number;
46
- }
47
-
48
- interface Agent {
49
- id: string;
50
- fingerprint: string;
51
- lastUsedAt: number;
52
- createdAt: number;
53
- isCurrent: boolean;
54
- }
55
-
56
- interface UsageData {
57
- byProvider: { provider: string; calls: number; cost: number }[];
58
- byDay: { date: string; calls: number }[];
59
- total: number;
60
- }
61
-
62
- type TabType = "overview" | "agents" | "usage";
63
-
64
- export default function DashboardPage() {
7
+ export default function DashboardRedirect() {
65
8
  const router = useRouter();
66
- const [isLoading, setIsLoading] = useState(true);
67
- const [error, setError] = useState<string | null>(null);
68
- const [activeTab, setActiveTab] = useState<TabType>("overview");
69
-
70
- const [workspace, setWorkspace] = useState<Workspace | null>(null);
71
- const [agents, setAgents] = useState<Agent[]>([]);
72
- const [usage, setUsage] = useState<UsageData | null>(null);
73
- const [sessionToken, setSessionToken] = useState<string | null>(null);
74
-
75
- const fetchData = useCallback(async (token: string) => {
76
- try {
77
- // Fetch dashboard data
78
- const dashboardRes = await fetch(`${CONVEX_URL}/api/query`, {
79
- method: "POST",
80
- headers: { "Content-Type": "application/json" },
81
- body: JSON.stringify({
82
- path: "workspaces:getWorkspaceDashboard",
83
- args: { token },
84
- }),
85
- });
86
-
87
- const dashboardData = await dashboardRes.json();
88
- const dashboard = dashboardData.value || dashboardData;
89
-
90
- if (!dashboard) {
91
- throw new Error("Session expired");
92
- }
93
-
94
- setWorkspace(dashboard.workspace);
95
-
96
- // Fetch agents
97
- const agentsRes = await fetch(`${CONVEX_URL}/api/query`, {
98
- method: "POST",
99
- headers: { "Content-Type": "application/json" },
100
- body: JSON.stringify({
101
- path: "workspaces:getConnectedAgents",
102
- args: { token },
103
- }),
104
- });
105
-
106
- const agentsData = await agentsRes.json();
107
- setAgents(agentsData.value || agentsData || []);
108
-
109
- // Fetch usage
110
- const usageRes = await fetch(`${CONVEX_URL}/api/query`, {
111
- method: "POST",
112
- headers: { "Content-Type": "application/json" },
113
- body: JSON.stringify({
114
- path: "workspaces:getUsageBreakdown",
115
- args: { token },
116
- }),
117
- });
118
-
119
- const usageData = await usageRes.json();
120
- setUsage(usageData.value || usageData);
121
-
122
- } catch (err) {
123
- console.error("Fetch error:", err);
124
- throw err;
125
- }
126
- }, []);
127
9
 
128
10
  useEffect(() => {
129
- const checkSession = async () => {
130
- try {
131
- // Try cookie-based session first
132
- const sessionRes = await fetch("/api/workspace-auth/session");
133
- const sessionData = await sessionRes.json();
134
-
135
- if (sessionData.session) {
136
- // Get token from localStorage or cookie
137
- const token = localStorage.getItem("apiclaw_workspace_session");
138
- if (token) {
139
- setSessionToken(token);
140
- await fetchData(token);
141
- } else {
142
- // Session exists but no token - redirect to login
143
- router.push("/login");
144
- return;
145
- }
146
- } else {
147
- // No session - redirect to login
148
- router.push("/login");
149
- return;
150
- }
151
-
152
- setIsLoading(false);
153
- } catch (err) {
154
- console.error("Session check error:", err);
155
- router.push("/login");
156
- }
157
- };
158
-
159
- checkSession();
160
- }, [router, fetchData]);
161
-
162
- const handleLogout = async () => {
163
- try {
164
- await fetch("/api/workspace-auth/session", { method: "DELETE" });
165
- localStorage.removeItem("apiclaw_workspace_session");
166
- router.push("/login");
167
- } catch (err) {
168
- console.error("Logout error:", err);
169
- }
170
- };
171
-
172
- const handleRevokeAgent = async (agentId: string) => {
173
- if (!sessionToken) return;
174
-
175
- try {
176
- await fetch(`${CONVEX_URL}/api/mutation`, {
177
- method: "POST",
178
- headers: { "Content-Type": "application/json" },
179
- body: JSON.stringify({
180
- path: "workspaces:revokeAgentSession",
181
- args: { token: sessionToken, sessionId: agentId },
182
- }),
183
- });
184
-
185
- // Refresh agents list
186
- setAgents(agents.filter(a => a.id !== agentId));
187
- } catch (err) {
188
- console.error("Revoke error:", err);
189
- }
190
- };
191
-
192
- const handleRefresh = async () => {
193
- if (!sessionToken) return;
194
- setIsLoading(true);
195
- try {
196
- await fetchData(sessionToken);
197
- } catch (err) {
198
- setError("Failed to refresh data");
199
- } finally {
200
- setIsLoading(false);
201
- }
202
- };
203
-
204
- const tabs = [
205
- { id: "overview" as TabType, label: "Overview", icon: BarChart3 },
206
- { id: "agents" as TabType, label: "Agents", icon: Users },
207
- { id: "usage" as TabType, label: "Usage", icon: TrendingUp },
208
- ];
209
-
210
- if (isLoading) {
211
- return (
212
- <div className="min-h-screen flex items-center justify-center bg-[var(--background)]">
213
- <div className="text-center">
214
- <Loader2 className="w-12 h-12 text-accent animate-spin mx-auto mb-4" />
215
- <p className="text-[var(--text-muted)]">Loading dashboard...</p>
216
- </div>
217
- </div>
218
- );
219
- }
220
-
221
- if (error || !workspace) {
222
- return (
223
- <div className="min-h-screen flex items-center justify-center px-6 bg-[var(--background)]">
224
- <div className="text-center max-w-md">
225
- <AlertCircle className="w-16 h-16 text-red-500 mx-auto mb-4" />
226
- <h1 className="text-2xl font-bold mb-2">Something Went Wrong</h1>
227
- <p className="text-[var(--text-muted)] mb-6">{error || "Failed to load dashboard"}</p>
228
- <button onClick={handleRefresh} className="btn-primary">
229
- <RefreshCw className="w-5 h-5" />
230
- Try Again
231
- </button>
232
- </div>
233
- </div>
234
- );
235
- }
11
+ router.replace("/workspace");
12
+ }, [router]);
236
13
 
237
14
  return (
238
- <div className="min-h-screen bg-[var(--background)]">
239
- {/* Header */}
240
- <header className="border-b border-[var(--border)] bg-[var(--surface)]/50 backdrop-blur-xl sticky top-0 z-50">
241
- <div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between">
242
- <div className="flex items-center gap-4">
243
- <Link href="/" className="w-10 h-10 rounded-xl bg-accent/20 flex items-center justify-center text-xl">
244
- 🦞
245
- </Link>
246
- <div>
247
- <h1 className="font-bold text-lg">Workspace Dashboard</h1>
248
- <p className="text-sm text-[var(--text-muted)]">{workspace.email}</p>
249
- </div>
250
- </div>
251
- <div className="flex items-center gap-3">
252
- <div className="px-3 py-1 rounded-full bg-accent/20 text-accent text-sm font-medium flex items-center gap-1">
253
- <Crown className="w-4 h-4" />
254
- {workspace.tier}
255
- </div>
256
- <button
257
- onClick={handleRefresh}
258
- className="p-2 rounded-lg hover:bg-[var(--surface)] transition"
259
- title="Refresh"
260
- >
261
- <RefreshCw className="w-5 h-5 text-[var(--text-muted)]" />
262
- </button>
263
- <button
264
- onClick={handleLogout}
265
- className="p-2 rounded-lg hover:bg-[var(--surface)] transition"
266
- title="Sign out"
267
- >
268
- <LogOut className="w-5 h-5 text-[var(--text-muted)]" />
269
- </button>
270
- </div>
271
- </div>
272
- </header>
273
-
274
- <div className="max-w-7xl mx-auto px-6 py-8">
275
- {/* Tab Navigation */}
276
- <div className="flex items-center gap-1 p-1 bg-[var(--surface)] rounded-xl w-fit mb-8">
277
- {tabs.map((tab) => (
278
- <button
279
- key={tab.id}
280
- onClick={() => setActiveTab(tab.id)}
281
- className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${
282
- activeTab === tab.id
283
- ? "bg-accent text-white"
284
- : "text-[var(--text-muted)] hover:text-[var(--text-primary)]"
285
- }`}
286
- >
287
- <tab.icon className="w-4 h-4" />
288
- {tab.label}
289
- </button>
290
- ))}
291
- </div>
292
-
293
- {/* Tab Content */}
294
- {activeTab === "overview" && (
295
- <OverviewTab workspace={workspace} agents={agents} usage={usage} />
296
- )}
297
- {activeTab === "agents" && (
298
- <AgentsTab agents={agents} onRevoke={handleRevokeAgent} />
299
- )}
300
- {activeTab === "usage" && (
301
- <UsageTab workspace={workspace} usage={usage} />
302
- )}
15
+ <div className="min-h-screen flex items-center justify-center bg-[var(--background)]">
16
+ <div className="text-center">
17
+ <Loader2 className="w-12 h-12 text-[#ef4444] animate-spin mx-auto mb-4" />
18
+ <p className="text-[var(--text-muted)]">Redirecting to workspace...</p>
303
19
  </div>
304
20
  </div>
305
21
  );
306
22
  }
307
-
308
- // ============================================
309
- // OVERVIEW TAB
310
- // ============================================
311
-
312
- function OverviewTab({
313
- workspace,
314
- agents,
315
- usage,
316
- }: {
317
- workspace: Workspace;
318
- agents: Agent[];
319
- usage: UsageData | null;
320
- }) {
321
- const tierLimits: Record<string, number> = {
322
- free: 1000,
323
- pro: 10000,
324
- enterprise: 100000,
325
- };
326
-
327
- return (
328
- <div className="space-y-8">
329
- <h2 className="text-2xl font-bold">Overview</h2>
330
-
331
- {/* Quick Stats */}
332
- <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
333
- <div className="rounded-2xl border border-accent/30 bg-accent/10 p-6">
334
- <div className="flex items-center gap-3 mb-3">
335
- <Zap className="w-6 h-6 text-accent" />
336
- <span className="text-[var(--text-muted)]">API Calls</span>
337
- </div>
338
- <p className="text-4xl font-bold text-accent">{workspace.usageCount.toLocaleString()}</p>
339
- <p className="text-sm text-[var(--text-muted)] mt-1">
340
- of {workspace.usageLimit.toLocaleString()} limit
341
- </p>
342
- </div>
343
-
344
- <div className="rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] p-6">
345
- <div className="flex items-center gap-3 mb-3">
346
- <Users className="w-6 h-6 text-[var(--text-muted)]" />
347
- <span className="text-[var(--text-muted)]">Connected Agents</span>
348
- </div>
349
- <p className="text-4xl font-bold">{agents.length}</p>
350
- </div>
351
-
352
- <div className="rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] p-6">
353
- <div className="flex items-center gap-3 mb-3">
354
- <Shield className="w-6 h-6 text-[var(--text-muted)]" />
355
- <span className="text-[var(--text-muted)]">Usage Remaining</span>
356
- </div>
357
- <p className="text-4xl font-bold">{workspace.usageRemaining.toLocaleString()}</p>
358
- </div>
359
-
360
- <div className="rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] p-6">
361
- <div className="flex items-center gap-3 mb-3">
362
- <Check className="w-6 h-6 text-green-500" />
363
- <span className="text-[var(--text-muted)]">Status</span>
364
- </div>
365
- <p className="text-xl font-bold text-green-500 capitalize">{workspace.status}</p>
366
- </div>
367
- </div>
368
-
369
- {/* Usage Progress */}
370
- <div className="rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] p-6">
371
- <div className="flex items-center justify-between mb-4">
372
- <h3 className="font-bold text-lg">Monthly Usage</h3>
373
- <span className="text-sm text-[var(--text-muted)]">
374
- {workspace.usagePercentage.toFixed(1)}% used
375
- </span>
376
- </div>
377
- <div className="h-4 bg-[var(--surface)] rounded-full overflow-hidden">
378
- <div
379
- className={`h-full rounded-full transition-all duration-500 ${
380
- workspace.usagePercentage > 90 ? "bg-red-500" :
381
- workspace.usagePercentage > 70 ? "bg-yellow-500" : "bg-accent"
382
- }`}
383
- style={{ width: `${Math.min(workspace.usagePercentage, 100)}%` }}
384
- />
385
- </div>
386
- <div className="flex items-center justify-between mt-4 text-sm text-[var(--text-muted)]">
387
- <span>{workspace.usageCount.toLocaleString()} calls used</span>
388
- <span>{workspace.usageRemaining.toLocaleString()} remaining</span>
389
- </div>
390
-
391
- {workspace.usagePercentage > 80 && workspace.tier === "free" && (
392
- <div className="mt-4 p-4 rounded-xl bg-accent/10 border border-accent/30">
393
- <div className="flex items-center gap-2 text-accent mb-2">
394
- <AlertCircle className="w-5 h-5" />
395
- <span className="font-medium">Running low on API calls</span>
396
- </div>
397
- <p className="text-sm text-[var(--text-muted)] mb-3">
398
- Upgrade to Pro for 10,000 API calls/month and priority support.
399
- </p>
400
- <button className="btn-primary !py-2 !px-4 text-sm">
401
- Upgrade to Pro
402
- <ChevronRight className="w-4 h-4" />
403
- </button>
404
- </div>
405
- )}
406
- </div>
407
-
408
- {/* Recent Agents */}
409
- <div className="rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] p-6">
410
- <div className="flex items-center justify-between mb-4">
411
- <h3 className="font-bold text-lg">Recent Agents</h3>
412
- <button
413
- onClick={() => {/* setActiveTab would need to be passed down */}}
414
- className="text-sm text-accent hover:underline"
415
- >
416
- View all
417
- </button>
418
- </div>
419
- {agents.length > 0 ? (
420
- <div className="space-y-3">
421
- {agents.slice(0, 3).map((agent) => (
422
- <div key={agent.id} className="flex items-center justify-between p-4 rounded-xl bg-[var(--surface)]">
423
- <div className="flex items-center gap-3">
424
- <div className="w-10 h-10 rounded-full bg-accent/20 flex items-center justify-center">
425
- <Users className="w-5 h-5 text-accent" />
426
- </div>
427
- <div>
428
- <p className="font-medium">{agent.fingerprint}</p>
429
- <p className="text-sm text-[var(--text-muted)]">
430
- Last active: {new Date(agent.lastUsedAt).toLocaleDateString()}
431
- </p>
432
- </div>
433
- </div>
434
- {agent.isCurrent && (
435
- <span className="px-2 py-1 rounded-full bg-green-500/20 text-green-500 text-xs font-medium">
436
- Current
437
- </span>
438
- )}
439
- </div>
440
- ))}
441
- </div>
442
- ) : (
443
- <p className="text-[var(--text-muted)] text-center py-8">No agents connected yet</p>
444
- )}
445
- </div>
446
- </div>
447
- );
448
- }
449
-
450
- // ============================================
451
- // AGENTS TAB
452
- // ============================================
453
-
454
- function AgentsTab({
455
- agents,
456
- onRevoke,
457
- }: {
458
- agents: Agent[];
459
- onRevoke: (agentId: string) => void;
460
- }) {
461
- const [confirmRevoke, setConfirmRevoke] = useState<string | null>(null);
462
-
463
- const handleRevoke = (agentId: string) => {
464
- if (confirmRevoke === agentId) {
465
- onRevoke(agentId);
466
- setConfirmRevoke(null);
467
- } else {
468
- setConfirmRevoke(agentId);
469
- }
470
- };
471
-
472
- return (
473
- <div className="space-y-6">
474
- <div className="flex items-center justify-between">
475
- <h2 className="text-2xl font-bold">Connected Agents</h2>
476
- <p className="text-[var(--text-muted)]">{agents.length} total</p>
477
- </div>
478
-
479
- {agents.length === 0 ? (
480
- <div className="text-center py-16 rounded-2xl border border-dashed border-[var(--border)] bg-[var(--surface)]/50">
481
- <Users className="w-12 h-12 text-[var(--text-muted)] mx-auto mb-4" />
482
- <h3 className="font-semibold text-lg mb-2">No Agents Connected</h3>
483
- <p className="text-[var(--text-muted)] max-w-md mx-auto">
484
- When you register AI agents with your workspace, they&apos;ll appear here.
485
- You can monitor their activity and revoke access anytime.
486
- </p>
487
- </div>
488
- ) : (
489
- <div className="grid gap-4">
490
- {agents.map((agent) => (
491
- <div
492
- key={agent.id}
493
- className="rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] p-6"
494
- >
495
- <div className="flex items-center justify-between">
496
- <div className="flex items-center gap-4">
497
- <div className="w-12 h-12 rounded-full bg-accent/20 flex items-center justify-center">
498
- <Users className="w-6 h-6 text-accent" />
499
- </div>
500
- <div>
501
- <div className="flex items-center gap-2">
502
- <h3 className="font-semibold">{agent.fingerprint}</h3>
503
- {agent.isCurrent && (
504
- <span className="px-2 py-0.5 rounded-full bg-green-500/20 text-green-500 text-xs font-medium">
505
- Current Session
506
- </span>
507
- )}
508
- </div>
509
- <div className="flex items-center gap-4 mt-1 text-sm text-[var(--text-muted)]">
510
- <span className="flex items-center gap-1">
511
- <Clock className="w-4 h-4" />
512
- Last active: {new Date(agent.lastUsedAt).toLocaleString()}
513
- </span>
514
- <span>
515
- Created: {new Date(agent.createdAt).toLocaleDateString()}
516
- </span>
517
- </div>
518
- </div>
519
- </div>
520
-
521
- {!agent.isCurrent && (
522
- <button
523
- onClick={() => handleRevoke(agent.id)}
524
- className={`px-4 py-2 rounded-lg text-sm font-medium transition ${
525
- confirmRevoke === agent.id
526
- ? "bg-red-500 text-white"
527
- : "bg-red-500/10 text-red-500 hover:bg-red-500/20"
528
- }`}
529
- >
530
- <Trash2 className="w-4 h-4 inline-block mr-1" />
531
- {confirmRevoke === agent.id ? "Confirm Revoke" : "Revoke"}
532
- </button>
533
- )}
534
- </div>
535
- </div>
536
- ))}
537
- </div>
538
- )}
539
-
540
- <div className="rounded-xl bg-[var(--surface)] border border-[var(--border)] p-6">
541
- <h3 className="font-medium mb-2">About Agent Sessions</h3>
542
- <p className="text-sm text-[var(--text-muted)]">
543
- Each connected agent represents an AI system or MCP server that has authenticated
544
- with your workspace. Revoking an agent will immediately invalidate its session token,
545
- requiring it to re-authenticate.
546
- </p>
547
- </div>
548
- </div>
549
- );
550
- }
551
-
552
- // ============================================
553
- // USAGE TAB
554
- // ============================================
555
-
556
- function UsageTab({
557
- workspace,
558
- usage,
559
- }: {
560
- workspace: Workspace;
561
- usage: UsageData | null;
562
- }) {
563
- const hasData = usage && (usage.byProvider.length > 0 || usage.byDay.length > 0);
564
-
565
- return (
566
- <div className="space-y-8">
567
- <h2 className="text-2xl font-bold">Usage Analytics</h2>
568
-
569
- {/* Stats */}
570
- <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
571
- <div className="rounded-2xl border border-accent/30 bg-accent/10 p-6">
572
- <div className="flex items-center gap-3 mb-3">
573
- <Zap className="w-6 h-6 text-accent" />
574
- <span className="text-[var(--text-muted)]">Total API Calls</span>
575
- </div>
576
- <p className="text-4xl font-bold text-accent">
577
- {(usage?.total || workspace.usageCount).toLocaleString()}
578
- </p>
579
- </div>
580
-
581
- <div className="rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] p-6">
582
- <div className="flex items-center gap-3 mb-3">
583
- <TrendingUp className="w-6 h-6 text-[var(--text-muted)]" />
584
- <span className="text-[var(--text-muted)]">Providers Used</span>
585
- </div>
586
- <p className="text-4xl font-bold">{usage?.byProvider.length || 0}</p>
587
- </div>
588
-
589
- <div className="rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] p-6">
590
- <div className="flex items-center gap-3 mb-3">
591
- <Shield className="w-6 h-6 text-[var(--text-muted)]" />
592
- <span className="text-[var(--text-muted)]">Remaining</span>
593
- </div>
594
- <p className="text-4xl font-bold">{workspace.usageRemaining.toLocaleString()}</p>
595
- </div>
596
- </div>
597
-
598
- {hasData ? (
599
- <>
600
- {/* Usage Over Time Chart */}
601
- {usage!.byDay.length > 0 && (
602
- <div className="rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] p-6">
603
- <h3 className="font-semibold mb-4">Usage Over Time</h3>
604
- <div className="h-80">
605
- <ResponsiveContainer width="100%" height="100%">
606
- <LineChart data={usage!.byDay}>
607
- <CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
608
- <XAxis
609
- dataKey="date"
610
- tick={{ fontSize: 12, fill: "var(--text-muted)" }}
611
- tickFormatter={(d) => new Date(d).toLocaleDateString("en-US", { month: "short", day: "numeric" })}
612
- />
613
- <YAxis tick={{ fontSize: 12, fill: "var(--text-muted)" }} />
614
- <Tooltip
615
- contentStyle={{
616
- background: "var(--surface-elevated)",
617
- border: "1px solid var(--border)",
618
- borderRadius: "8px",
619
- }}
620
- labelFormatter={(d) => new Date(d).toLocaleDateString()}
621
- />
622
- <Line
623
- type="monotone"
624
- dataKey="calls"
625
- stroke="#ef4444"
626
- strokeWidth={2}
627
- dot={false}
628
- activeDot={{ r: 4, fill: "#ef4444" }}
629
- />
630
- </LineChart>
631
- </ResponsiveContainer>
632
- </div>
633
- </div>
634
- )}
635
-
636
- {/* Usage by Provider */}
637
- {usage!.byProvider.length > 0 && (
638
- <div className="rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] p-6">
639
- <h3 className="font-semibold mb-4">Usage by Provider</h3>
640
- <div className="grid lg:grid-cols-2 gap-6">
641
- <div className="h-64">
642
- <ResponsiveContainer width="100%" height="100%">
643
- <BarChart data={usage!.byProvider}>
644
- <CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
645
- <XAxis dataKey="provider" tick={{ fontSize: 12, fill: "var(--text-muted)" }} />
646
- <YAxis tick={{ fontSize: 12, fill: "var(--text-muted)" }} />
647
- <Tooltip
648
- contentStyle={{
649
- background: "var(--surface-elevated)",
650
- border: "1px solid var(--border)",
651
- borderRadius: "8px",
652
- }}
653
- />
654
- <Bar dataKey="calls" fill="#ef4444" radius={[4, 4, 0, 0]} />
655
- </BarChart>
656
- </ResponsiveContainer>
657
- </div>
658
- <div className="space-y-3">
659
- {usage!.byProvider.map((p, i) => (
660
- <div key={p.provider} className="flex items-center justify-between p-4 rounded-xl bg-[var(--surface)]">
661
- <div className="flex items-center gap-3">
662
- <span className="w-8 h-8 rounded-full bg-accent/20 text-accent flex items-center justify-center text-sm font-medium">
663
- {i + 1}
664
- </span>
665
- <span className="font-medium">{p.provider}</span>
666
- </div>
667
- <div className="text-right">
668
- <p className="font-semibold">{p.calls.toLocaleString()} calls</p>
669
- {p.cost > 0 && (
670
- <p className="text-sm text-[var(--text-muted)]">${p.cost.toFixed(2)}</p>
671
- )}
672
- </div>
673
- </div>
674
- ))}
675
- </div>
676
- </div>
677
- </div>
678
- )}
679
- </>
680
- ) : (
681
- <div className="rounded-2xl border border-dashed border-[var(--border)] bg-[var(--surface)]/50 p-12 text-center">
682
- <TrendingUp className="w-12 h-12 text-[var(--text-muted)] mx-auto mb-4" />
683
- <h3 className="font-semibold text-lg mb-2">No Usage Data Yet</h3>
684
- <p className="text-[var(--text-muted)] max-w-md mx-auto">
685
- When your agents start making API calls, usage analytics will appear here.
686
- Connect an agent to get started.
687
- </p>
688
- </div>
689
- )}
690
- </div>
691
- );
692
- }