@theihtisham/ai-agent-starter-kit 1.0.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 (130) hide show
  1. package/.env.example +33 -0
  2. package/Dockerfile +35 -0
  3. package/LICENSE +21 -0
  4. package/README.md +73 -0
  5. package/docker-compose.yml +28 -0
  6. package/next-env.d.ts +5 -0
  7. package/next.config.mjs +17 -0
  8. package/package.json +85 -0
  9. package/postcss.config.js +6 -0
  10. package/prisma/schema.prisma +157 -0
  11. package/prisma/seed.ts +46 -0
  12. package/src/app/(auth)/forgot-password/page.tsx +56 -0
  13. package/src/app/(auth)/layout.tsx +7 -0
  14. package/src/app/(auth)/login/page.tsx +83 -0
  15. package/src/app/(auth)/signup/page.tsx +108 -0
  16. package/src/app/(dashboard)/agents/[id]/edit/page.tsx +68 -0
  17. package/src/app/(dashboard)/agents/[id]/page.tsx +114 -0
  18. package/src/app/(dashboard)/agents/new/page.tsx +43 -0
  19. package/src/app/(dashboard)/agents/page.tsx +63 -0
  20. package/src/app/(dashboard)/api-keys/page.tsx +139 -0
  21. package/src/app/(dashboard)/dashboard/page.tsx +79 -0
  22. package/src/app/(dashboard)/layout.tsx +16 -0
  23. package/src/app/(dashboard)/settings/billing/page.tsx +59 -0
  24. package/src/app/(dashboard)/settings/page.tsx +45 -0
  25. package/src/app/(dashboard)/usage/page.tsx +46 -0
  26. package/src/app/api/agents/[id]/chat/route.ts +100 -0
  27. package/src/app/api/agents/[id]/chats/route.ts +36 -0
  28. package/src/app/api/agents/[id]/route.ts +97 -0
  29. package/src/app/api/agents/route.ts +84 -0
  30. package/src/app/api/api-keys/[id]/route.ts +25 -0
  31. package/src/app/api/api-keys/route.ts +72 -0
  32. package/src/app/api/auth/[...nextauth]/route.ts +5 -0
  33. package/src/app/api/auth/register/route.ts +53 -0
  34. package/src/app/api/health/route.ts +26 -0
  35. package/src/app/api/stripe/checkout/route.ts +37 -0
  36. package/src/app/api/stripe/plans/route.ts +16 -0
  37. package/src/app/api/stripe/portal/route.ts +29 -0
  38. package/src/app/api/stripe/webhook/route.ts +45 -0
  39. package/src/app/api/usage/route.ts +43 -0
  40. package/src/app/globals.css +59 -0
  41. package/src/app/layout.tsx +22 -0
  42. package/src/app/page.tsx +32 -0
  43. package/src/app/pricing/page.tsx +25 -0
  44. package/src/components/agents/agent-form.tsx +137 -0
  45. package/src/components/agents/model-selector.tsx +35 -0
  46. package/src/components/agents/tool-selector.tsx +48 -0
  47. package/src/components/auth-provider.tsx +17 -0
  48. package/src/components/billing/plan-badge.tsx +23 -0
  49. package/src/components/billing/pricing-table.tsx +95 -0
  50. package/src/components/billing/usage-meter.tsx +39 -0
  51. package/src/components/chat/chat-input.tsx +68 -0
  52. package/src/components/chat/chat-interface.tsx +152 -0
  53. package/src/components/chat/chat-message.tsx +50 -0
  54. package/src/components/chat/chat-sidebar.tsx +49 -0
  55. package/src/components/chat/code-block.tsx +38 -0
  56. package/src/components/chat/markdown-renderer.tsx +56 -0
  57. package/src/components/chat/streaming-text.tsx +46 -0
  58. package/src/components/dashboard/agent-card.tsx +52 -0
  59. package/src/components/dashboard/header.tsx +75 -0
  60. package/src/components/dashboard/sidebar.tsx +52 -0
  61. package/src/components/dashboard/stat-card.tsx +42 -0
  62. package/src/components/dashboard/usage-chart.tsx +42 -0
  63. package/src/components/landing/cta.tsx +30 -0
  64. package/src/components/landing/features.tsx +75 -0
  65. package/src/components/landing/hero.tsx +42 -0
  66. package/src/components/landing/pricing.tsx +28 -0
  67. package/src/components/ui/avatar.tsx +24 -0
  68. package/src/components/ui/badge.tsx +24 -0
  69. package/src/components/ui/button.tsx +39 -0
  70. package/src/components/ui/card.tsx +50 -0
  71. package/src/components/ui/dialog.tsx +73 -0
  72. package/src/components/ui/dropdown.tsx +77 -0
  73. package/src/components/ui/input.tsx +23 -0
  74. package/src/components/ui/skeleton.tsx +7 -0
  75. package/src/components/ui/switch.tsx +31 -0
  76. package/src/components/ui/table.tsx +48 -0
  77. package/src/components/ui/tabs.tsx +66 -0
  78. package/src/components/ui/textarea.tsx +20 -0
  79. package/src/hooks/use-agent.ts +44 -0
  80. package/src/hooks/use-streaming.ts +82 -0
  81. package/src/hooks/use-subscription.ts +40 -0
  82. package/src/hooks/use-usage.ts +43 -0
  83. package/src/hooks/use-user.ts +13 -0
  84. package/src/lib/agents/index.ts +60 -0
  85. package/src/lib/agents/memory/long-term.ts +241 -0
  86. package/src/lib/agents/memory/manager.ts +154 -0
  87. package/src/lib/agents/memory/short-term.ts +155 -0
  88. package/src/lib/agents/memory/types.ts +68 -0
  89. package/src/lib/agents/orchestration/debate.ts +170 -0
  90. package/src/lib/agents/orchestration/index.ts +103 -0
  91. package/src/lib/agents/orchestration/parallel.ts +143 -0
  92. package/src/lib/agents/orchestration/router.ts +199 -0
  93. package/src/lib/agents/orchestration/sequential.ts +127 -0
  94. package/src/lib/agents/orchestration/types.ts +68 -0
  95. package/src/lib/agents/tools/calculator.ts +131 -0
  96. package/src/lib/agents/tools/code-executor.ts +191 -0
  97. package/src/lib/agents/tools/file-reader.ts +129 -0
  98. package/src/lib/agents/tools/index.ts +48 -0
  99. package/src/lib/agents/tools/registry.ts +182 -0
  100. package/src/lib/agents/tools/web-search.ts +83 -0
  101. package/src/lib/ai/agent.ts +275 -0
  102. package/src/lib/ai/context.ts +68 -0
  103. package/src/lib/ai/memory.ts +98 -0
  104. package/src/lib/ai/models.ts +80 -0
  105. package/src/lib/ai/streaming.ts +80 -0
  106. package/src/lib/ai/tools.ts +149 -0
  107. package/src/lib/auth/middleware.ts +41 -0
  108. package/src/lib/auth/nextauth.ts +69 -0
  109. package/src/lib/db/client.ts +15 -0
  110. package/src/lib/rate-limit/limiter.ts +93 -0
  111. package/src/lib/rate-limit/rules.ts +38 -0
  112. package/src/lib/stripe/client.ts +25 -0
  113. package/src/lib/stripe/plans.ts +75 -0
  114. package/src/lib/stripe/usage.ts +123 -0
  115. package/src/lib/stripe/webhooks.ts +96 -0
  116. package/src/lib/utils/api-response.ts +85 -0
  117. package/src/lib/utils/errors.ts +73 -0
  118. package/src/lib/utils/helpers.ts +50 -0
  119. package/src/lib/utils/id.ts +21 -0
  120. package/src/lib/utils/logger.ts +38 -0
  121. package/src/lib/utils/validation.ts +44 -0
  122. package/src/middleware.ts +13 -0
  123. package/src/types/agent.ts +31 -0
  124. package/src/types/api.ts +38 -0
  125. package/src/types/billing.ts +35 -0
  126. package/src/types/chat.ts +30 -0
  127. package/src/types/next-auth.d.ts +19 -0
  128. package/tailwind.config.ts +72 -0
  129. package/tsconfig.json +28 -0
  130. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,108 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { signIn } from 'next-auth/react';
5
+ import { useRouter } from 'next/navigation';
6
+ import Link from 'next/link';
7
+ import { Button } from '@/components/ui/button';
8
+ import { Input } from '@/components/ui/input';
9
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from '@/components/ui/card';
10
+
11
+ export default function SignupPage() {
12
+ const router = useRouter();
13
+ const [name, setName] = useState('');
14
+ const [email, setEmail] = useState('');
15
+ const [password, setPassword] = useState('');
16
+ const [error, setError] = useState('');
17
+ const [loading, setLoading] = useState(false);
18
+
19
+ const handleSubmit = async (e: React.FormEvent) => {
20
+ e.preventDefault();
21
+ setLoading(true);
22
+ setError('');
23
+
24
+ // NextAuth doesn't have a built-in signup — use credentials provider
25
+ // In production, create user via API route first, then sign in
26
+ try {
27
+ const res = await fetch('/api/auth/register', {
28
+ method: 'POST',
29
+ headers: { 'Content-Type': 'application/json' },
30
+ body: JSON.stringify({ name, email, password }),
31
+ });
32
+
33
+ if (!res.ok) {
34
+ const data = await res.json();
35
+ setError(data.error ?? 'Registration failed');
36
+ setLoading(false);
37
+ return;
38
+ }
39
+
40
+ const result = await signIn('credentials', {
41
+ email,
42
+ password,
43
+ redirect: false,
44
+ });
45
+
46
+ if (result?.error) {
47
+ setError('Auto-login failed. Please log in manually.');
48
+ setLoading(false);
49
+ } else {
50
+ router.push('/dashboard');
51
+ }
52
+ } catch {
53
+ setError('Something went wrong');
54
+ setLoading(false);
55
+ }
56
+ };
57
+
58
+ return (
59
+ <Card>
60
+ <CardHeader className="text-center">
61
+ <CardTitle className="text-2xl">Create your account</CardTitle>
62
+ <CardDescription>Start building AI agents in minutes</CardDescription>
63
+ </CardHeader>
64
+ <CardContent>
65
+ <div className="grid gap-4">
66
+ <div className="grid grid-cols-2 gap-3">
67
+ <Button variant="outline" onClick={() => signIn('google', { callbackUrl: '/dashboard' })}>
68
+ Google
69
+ </Button>
70
+ <Button variant="outline" onClick={() => signIn('github', { callbackUrl: '/dashboard' })}>
71
+ GitHub
72
+ </Button>
73
+ </div>
74
+ <div className="relative">
75
+ <div className="absolute inset-0 flex items-center"><span className="w-full border-t" /></div>
76
+ <div className="relative flex justify-center text-xs uppercase">
77
+ <span className="bg-card px-2 text-muted-foreground">or continue with email</span>
78
+ </div>
79
+ </div>
80
+ <form onSubmit={handleSubmit} className="space-y-4">
81
+ <div className="space-y-2">
82
+ <label className="text-sm font-medium">Name</label>
83
+ <Input value={name} onChange={(e) => setName(e.target.value)} required />
84
+ </div>
85
+ <div className="space-y-2">
86
+ <label className="text-sm font-medium">Email</label>
87
+ <Input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
88
+ </div>
89
+ <div className="space-y-2">
90
+ <label className="text-sm font-medium">Password</label>
91
+ <Input type="password" value={password} onChange={(e) => setPassword(e.target.value)} minLength={8} required />
92
+ </div>
93
+ {error && <p className="text-sm text-destructive">{error}</p>}
94
+ <Button type="submit" className="w-full" disabled={loading}>
95
+ {loading ? 'Creating account...' : 'Create account'}
96
+ </Button>
97
+ </form>
98
+ </div>
99
+ </CardContent>
100
+ <CardFooter className="justify-center">
101
+ <p className="text-sm text-muted-foreground">
102
+ Already have an account?{' '}
103
+ <Link href="/login" className="text-primary hover:underline">Log in</Link>
104
+ </p>
105
+ </CardFooter>
106
+ </Card>
107
+ );
108
+ }
@@ -0,0 +1,68 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState, use } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import { AgentForm } from '@/components/agents/agent-form';
6
+ import toast from 'react-hot-toast';
7
+
8
+ export default function EditAgentPage({ params }: { params: Promise<{ id: string }> }) {
9
+ const { id } = use(params);
10
+ const router = useRouter();
11
+ const [agent, setAgent] = useState<{
12
+ name: string;
13
+ description: string;
14
+ systemPrompt: string;
15
+ model: string;
16
+ temperature: number;
17
+ maxTokens: number;
18
+ tools: string[];
19
+ isPublic: boolean;
20
+ } | null>(null);
21
+ const [loading, setLoading] = useState(true);
22
+
23
+ useEffect(() => {
24
+ fetch(`/api/agents/${id}`)
25
+ .then((r) => r.json())
26
+ .then((res) => {
27
+ if (res.success) setAgent(res.data);
28
+ })
29
+ .catch(() => {})
30
+ .finally(() => setLoading(false));
31
+ }, [id]);
32
+
33
+ const handleUpdate = async (data: {
34
+ name: string;
35
+ description: string;
36
+ systemPrompt: string;
37
+ model: string;
38
+ temperature: number;
39
+ maxTokens: number;
40
+ tools: string[];
41
+ isPublic: boolean;
42
+ }) => {
43
+ const res = await fetch(`/api/agents/${id}`, {
44
+ method: 'PATCH',
45
+ headers: { 'Content-Type': 'application/json' },
46
+ body: JSON.stringify(data),
47
+ });
48
+
49
+ if (!res.ok) {
50
+ const err = await res.json();
51
+ toast.error(err.error?.message ?? 'Failed to update agent');
52
+ return;
53
+ }
54
+
55
+ toast.success('Agent updated!');
56
+ router.push(`/agents/${id}`);
57
+ };
58
+
59
+ if (loading) return <p>Loading...</p>;
60
+ if (!agent) return <p>Agent not found.</p>;
61
+
62
+ return (
63
+ <div className="max-w-2xl mx-auto">
64
+ <h1 className="text-2xl font-bold mb-6">Edit Agent</h1>
65
+ <AgentForm initialData={agent} onSubmit={handleUpdate} />
66
+ </div>
67
+ );
68
+ }
@@ -0,0 +1,114 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState, use } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import { ChatInterface } from '@/components/chat/chat-interface';
6
+ import { ChatSidebar } from '@/components/chat/chat-sidebar';
7
+ import { Button } from '@/components/ui/button';
8
+ import { Skeleton } from '@/components/ui/skeleton';
9
+ import toast from 'react-hot-toast';
10
+
11
+ interface AgentData {
12
+ id: string;
13
+ name: string;
14
+ description?: string;
15
+ model: string;
16
+ systemPrompt: string;
17
+ temperature: number;
18
+ maxTokens: number;
19
+ tools: string[];
20
+ isPublic: boolean;
21
+ }
22
+
23
+ interface ChatData {
24
+ id: string;
25
+ title: string;
26
+ updatedAt: string;
27
+ _count: { messages: number };
28
+ }
29
+
30
+ export default function AgentPage({ params }: { params: Promise<{ id: string }> }) {
31
+ const { id } = use(params);
32
+ const router = useRouter();
33
+ const [agent, setAgent] = useState<AgentData | null>(null);
34
+ const [chats, setChats] = useState<ChatData[]>([]);
35
+ const [activeChatId, setActiveChatId] = useState<string | undefined>();
36
+ const [loading, setLoading] = useState(true);
37
+
38
+ useEffect(() => {
39
+ Promise.all([
40
+ fetch(`/api/agents/${id}`).then((r) => r.json()),
41
+ fetch(`/api/agents/${id}/chats`).then((r) => r.json()),
42
+ ])
43
+ .then(([agentRes, chatsRes]) => {
44
+ if (agentRes.success) setAgent(agentRes.data);
45
+ if (chatsRes.success) {
46
+ setChats(chatsRes.data ?? []);
47
+ if (chatsRes.data?.length > 0) setActiveChatId(chatsRes.data[0].id);
48
+ }
49
+ })
50
+ .catch(() => {})
51
+ .finally(() => setLoading(false));
52
+ }, [id]);
53
+
54
+ const handleNewChat = async () => {
55
+ const res = await fetch(`/api/agents/${id}/chats`, { method: 'POST' });
56
+ if (res.ok) {
57
+ const result = await res.json();
58
+ setChats((prev) => [result.data, ...prev]);
59
+ setActiveChatId(result.data.id);
60
+ }
61
+ };
62
+
63
+ const handleDelete = async () => {
64
+ if (!confirm('Delete this agent? This cannot be undone.')) return;
65
+ const res = await fetch(`/api/agents/${id}`, { method: 'DELETE' });
66
+ if (res.ok) {
67
+ toast.success('Agent deleted');
68
+ router.push('/agents');
69
+ }
70
+ };
71
+
72
+ if (loading) {
73
+ return (
74
+ <div className="space-y-4">
75
+ <Skeleton className="h-8 w-48" />
76
+ <Skeleton className="h-[500px]" />
77
+ </div>
78
+ );
79
+ }
80
+
81
+ if (!agent) {
82
+ return <p className="text-muted-foreground">Agent not found.</p>;
83
+ }
84
+
85
+ return (
86
+ <div className="flex h-[calc(100vh-8rem)] rounded-lg border overflow-hidden">
87
+ <ChatSidebar
88
+ chats={chats}
89
+ activeChatId={activeChatId}
90
+ agentId={id}
91
+ onNewChat={handleNewChat}
92
+ />
93
+ <div className="flex-1 flex flex-col">
94
+ <div className="flex items-center justify-between border-b px-4 py-2">
95
+ <div>
96
+ <h2 className="font-semibold">{agent.name}</h2>
97
+ <p className="text-xs text-muted-foreground">{agent.model}</p>
98
+ </div>
99
+ <div className="flex gap-2">
100
+ <Button variant="outline" size="sm" onClick={() => router.push(`/agents/${id}/edit`)}>
101
+ Edit
102
+ </Button>
103
+ <Button variant="destructive" size="sm" onClick={handleDelete}>
104
+ Delete
105
+ </Button>
106
+ </div>
107
+ </div>
108
+ <div className="flex-1">
109
+ <ChatInterface agentId={id} chatId={activeChatId} />
110
+ </div>
111
+ </div>
112
+ </div>
113
+ );
114
+ }
@@ -0,0 +1,43 @@
1
+ 'use client';
2
+
3
+ import { useRouter } from 'next/navigation';
4
+ import toast from 'react-hot-toast';
5
+ import { AgentForm } from '@/components/agents/agent-form';
6
+
7
+ export default function NewAgentPage() {
8
+ const router = useRouter();
9
+
10
+ const handleCreate = async (data: {
11
+ name: string;
12
+ description: string;
13
+ systemPrompt: string;
14
+ model: string;
15
+ temperature: number;
16
+ maxTokens: number;
17
+ tools: string[];
18
+ isPublic: boolean;
19
+ }) => {
20
+ const res = await fetch('/api/agents', {
21
+ method: 'POST',
22
+ headers: { 'Content-Type': 'application/json' },
23
+ body: JSON.stringify(data),
24
+ });
25
+
26
+ if (!res.ok) {
27
+ const err = await res.json();
28
+ toast.error(err.error?.message ?? 'Failed to create agent');
29
+ return;
30
+ }
31
+
32
+ const result = await res.json();
33
+ toast.success('Agent created!');
34
+ router.push(`/agents/${result.data.id}`);
35
+ };
36
+
37
+ return (
38
+ <div className="max-w-2xl mx-auto">
39
+ <h1 className="text-2xl font-bold mb-6">Create New Agent</h1>
40
+ <AgentForm onSubmit={handleCreate} />
41
+ </div>
42
+ );
43
+ }
@@ -0,0 +1,63 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import Link from 'next/link';
5
+ import { Button } from '@/components/ui/button';
6
+ import { AgentCard } from '@/components/dashboard/agent-card';
7
+
8
+ interface Agent {
9
+ id: string;
10
+ name: string;
11
+ description?: string;
12
+ model: string;
13
+ isPublic: boolean;
14
+ messageCount: number;
15
+ lastUsed?: string | null;
16
+ }
17
+
18
+ export default function AgentsPage() {
19
+ const [agents, setAgents] = useState<Agent[]>([]);
20
+ const [loading, setLoading] = useState(true);
21
+
22
+ useEffect(() => {
23
+ fetch('/api/agents')
24
+ .then((res) => res.json())
25
+ .then((res) => {
26
+ if (res.success) setAgents(res.data ?? []);
27
+ })
28
+ .catch(() => {})
29
+ .finally(() => setLoading(false));
30
+ }, []);
31
+
32
+ return (
33
+ <div className="space-y-6">
34
+ <div className="flex items-center justify-between">
35
+ <h1 className="text-2xl font-bold">Agents</h1>
36
+ <Link href="/agents/new">
37
+ <Button>+ New Agent</Button>
38
+ </Link>
39
+ </div>
40
+
41
+ {loading ? (
42
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
43
+ {[1, 2, 3].map((i) => (
44
+ <div key={i} className="h-40 rounded-lg border animate-pulse bg-muted" />
45
+ ))}
46
+ </div>
47
+ ) : agents.length === 0 ? (
48
+ <div className="text-center py-12">
49
+ <p className="text-muted-foreground mb-4">No agents yet. Create your first AI agent.</p>
50
+ <Link href="/agents/new">
51
+ <Button>Create Agent</Button>
52
+ </Link>
53
+ </div>
54
+ ) : (
55
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
56
+ {agents.map((agent) => (
57
+ <AgentCard key={agent.id} {...agent} />
58
+ ))}
59
+ </div>
60
+ )}
61
+ </div>
62
+ );
63
+ }
@@ -0,0 +1,139 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import { Button } from '@/components/ui/button';
5
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
6
+ import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@/components/ui/table';
7
+ import toast from 'react-hot-toast';
8
+
9
+ interface ApiKeyData {
10
+ id: string;
11
+ name: string;
12
+ prefix: string;
13
+ lastUsed: string | null;
14
+ createdAt: string;
15
+ }
16
+
17
+ export default function ApiKeysPage() {
18
+ const [keys, setKeys] = useState<ApiKeyData[]>([]);
19
+ const [newKeyName, setNewKeyName] = useState('');
20
+ const [createdKey, setCreatedKey] = useState<string | null>(null);
21
+ const [loading, setLoading] = useState(true);
22
+
23
+ useEffect(() => {
24
+ fetch('/api/api-keys')
25
+ .then((r) => r.json())
26
+ .then((res) => {
27
+ if (res.success) setKeys(res.data ?? []);
28
+ })
29
+ .catch(() => {})
30
+ .finally(() => setLoading(false));
31
+ }, []);
32
+
33
+ const handleCreate = async () => {
34
+ if (!newKeyName.trim()) return;
35
+ const res = await fetch('/api/api-keys', {
36
+ method: 'POST',
37
+ headers: { 'Content-Type': 'application/json' },
38
+ body: JSON.stringify({ name: newKeyName }),
39
+ });
40
+
41
+ if (res.ok) {
42
+ const result = await res.json();
43
+ setCreatedKey(result.data.key);
44
+ setNewKeyName('');
45
+ toast.success('API key created');
46
+ // Refresh list
47
+ const listRes = await fetch('/api/api-keys');
48
+ const listData = await listRes.json();
49
+ if (listData.success) setKeys(listData.data ?? []);
50
+ }
51
+ };
52
+
53
+ const handleDelete = async (id: string) => {
54
+ if (!confirm('Delete this API key?')) return;
55
+ const res = await fetch(`/api/api-keys/${id}`, { method: 'DELETE' });
56
+ if (res.ok) {
57
+ setKeys((prev) => prev.filter((k) => k.id !== id));
58
+ toast.success('API key deleted');
59
+ }
60
+ };
61
+
62
+ return (
63
+ <div className="max-w-3xl mx-auto space-y-6">
64
+ <h1 className="text-2xl font-bold">API Keys</h1>
65
+
66
+ {createdKey && (
67
+ <Card className="border-green-500">
68
+ <CardContent className="p-4">
69
+ <p className="text-sm font-medium mb-2">New API Key (copy now — it won&apos;t be shown again):</p>
70
+ <code className="block bg-muted p-3 rounded text-sm font-mono break-all">{createdKey}</code>
71
+ <Button
72
+ variant="outline"
73
+ size="sm"
74
+ className="mt-2"
75
+ onClick={() => { navigator.clipboard.writeText(createdKey); toast.success('Copied!'); }}
76
+ >
77
+ Copy
78
+ </Button>
79
+ </CardContent>
80
+ </Card>
81
+ )}
82
+
83
+ <Card>
84
+ <CardHeader>
85
+ <CardTitle>Create New Key</CardTitle>
86
+ </CardHeader>
87
+ <CardContent>
88
+ <div className="flex gap-3">
89
+ <input
90
+ className="flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm"
91
+ placeholder="Key name (e.g., Production)"
92
+ value={newKeyName}
93
+ onChange={(e) => setNewKeyName(e.target.value)}
94
+ />
95
+ <Button onClick={handleCreate} disabled={!newKeyName.trim()}>Create</Button>
96
+ </div>
97
+ </CardContent>
98
+ </Card>
99
+
100
+ <Card>
101
+ <CardHeader>
102
+ <CardTitle>Your Keys</CardTitle>
103
+ </CardHeader>
104
+ <CardContent>
105
+ <Table>
106
+ <TableHeader>
107
+ <TableRow>
108
+ <TableHead>Name</TableHead>
109
+ <TableHead>Prefix</TableHead>
110
+ <TableHead>Last Used</TableHead>
111
+ <TableHead>Created</TableHead>
112
+ <TableHead></TableHead>
113
+ </TableRow>
114
+ </TableHeader>
115
+ <TableBody>
116
+ {loading ? (
117
+ <TableRow><TableCell colSpan={5}>Loading...</TableCell></TableRow>
118
+ ) : keys.length === 0 ? (
119
+ <TableRow><TableCell colSpan={5} className="text-muted-foreground">No API keys yet</TableCell></TableRow>
120
+ ) : (
121
+ keys.map((key) => (
122
+ <TableRow key={key.id}>
123
+ <TableCell className="font-medium">{key.name}</TableCell>
124
+ <TableCell><code className="text-xs bg-muted px-1.5 py-0.5 rounded">{key.prefix}...</code></TableCell>
125
+ <TableCell className="text-muted-foreground">{key.lastUsed ?? 'Never'}</TableCell>
126
+ <TableCell className="text-muted-foreground">{new Date(key.createdAt).toLocaleDateString()}</TableCell>
127
+ <TableCell>
128
+ <Button variant="ghost" size="sm" onClick={() => handleDelete(key.id)}>Delete</Button>
129
+ </TableCell>
130
+ </TableRow>
131
+ ))
132
+ )}
133
+ </TableBody>
134
+ </Table>
135
+ </CardContent>
136
+ </Card>
137
+ </div>
138
+ );
139
+ }
@@ -0,0 +1,79 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import { StatCard } from '@/components/dashboard/stat-card';
5
+ import { AgentCard } from '@/components/dashboard/agent-card';
6
+ import { Skeleton } from '@/components/ui/skeleton';
7
+
8
+ interface DashboardData {
9
+ agentCount: number;
10
+ messageCount: number;
11
+ tokenCount: number;
12
+ agents: {
13
+ id: string;
14
+ name: string;
15
+ description?: string;
16
+ model: string;
17
+ isPublic: boolean;
18
+ messageCount: number;
19
+ lastUsed?: string | null;
20
+ }[];
21
+ }
22
+
23
+ export default function DashboardPage() {
24
+ const [data, setData] = useState<DashboardData | null>(null);
25
+ const [loading, setLoading] = useState(true);
26
+
27
+ useEffect(() => {
28
+ fetch('/api/agents')
29
+ .then((res) => res.json())
30
+ .then((res) => {
31
+ if (res.success && res.data) {
32
+ setData({
33
+ agentCount: res.data.length,
34
+ messageCount: res.data.reduce((sum: number, a: { messageCount?: number }) => sum + (a.messageCount ?? 0), 0),
35
+ tokenCount: 0,
36
+ agents: res.data.slice(0, 5),
37
+ });
38
+ }
39
+ })
40
+ .catch(() => {})
41
+ .finally(() => setLoading(false));
42
+ }, []);
43
+
44
+ if (loading) {
45
+ return (
46
+ <div className="space-y-6">
47
+ <h1 className="text-2xl font-bold">Dashboard</h1>
48
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
49
+ {[1, 2, 3].map((i) => <Skeleton key={i} className="h-32" />)}
50
+ </div>
51
+ </div>
52
+ );
53
+ }
54
+
55
+ return (
56
+ <div className="space-y-6">
57
+ <div className="flex items-center justify-between">
58
+ <h1 className="text-2xl font-bold">Dashboard</h1>
59
+ </div>
60
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
61
+ <StatCard title="Total Agents" value={data?.agentCount ?? 0} description="Active agents" />
62
+ <StatCard title="Messages" value={data?.messageCount ?? 0} description="This month" />
63
+ <StatCard title="API Calls" value="—" description="Coming soon" />
64
+ </div>
65
+ <div>
66
+ <h2 className="text-lg font-semibold mb-4">Recent Agents</h2>
67
+ {data?.agents?.length ? (
68
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
69
+ {data.agents.map((agent) => (
70
+ <AgentCard key={agent.id} {...agent} />
71
+ ))}
72
+ </div>
73
+ ) : (
74
+ <p className="text-muted-foreground text-sm">No agents yet. Create your first agent to get started.</p>
75
+ )}
76
+ </div>
77
+ </div>
78
+ );
79
+ }
@@ -0,0 +1,16 @@
1
+ 'use client';
2
+
3
+ import { Header } from '@/components/dashboard/header';
4
+ import { Sidebar } from '@/components/dashboard/sidebar';
5
+
6
+ export default function DashboardLayout({ children }: { children: React.ReactNode }) {
7
+ return (
8
+ <div className="min-h-screen flex flex-col">
9
+ <Header />
10
+ <div className="flex flex-1">
11
+ <Sidebar />
12
+ <main className="flex-1 overflow-auto p-6">{children}</main>
13
+ </div>
14
+ </div>
15
+ );
16
+ }