@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.
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +272 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/docs/PRD-workspace-fixes.md +178 -0
- package/landing/src/app/dashboard/page.tsx +9 -679
- package/landing/src/app/dashboard/verify/page.tsx +1 -1
- package/landing/src/app/login/page.tsx +1 -1
- package/landing/src/app/page.tsx +23 -7
- package/landing/src/app/providers/dashboard/layout.tsx +5 -4
- package/landing/src/app/providers/dashboard/page.tsx +11 -641
- package/landing/src/app/workspace/layout.tsx +30 -0
- package/landing/src/app/workspace/page.tsx +1637 -0
- package/landing/src/lib/stats.json +1 -1
- package/package.json +1 -1
- package/src/cli.ts +320 -0
- package/src/index.ts +10 -0
|
@@ -1,692 +1,22 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
130
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
<
|
|
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'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
|
-
}
|