@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,95 @@
1
+ 'use client';
2
+
3
+ import { Button } from '@/components/ui/button';
4
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from '@/components/ui/card';
5
+ import { Badge } from '@/components/ui/badge';
6
+ import { cn } from '@/lib/utils/helpers';
7
+ import { PLANS, type Plan } from '@/lib/stripe/plans';
8
+
9
+ interface PricingTableProps {
10
+ currentPlanId?: string;
11
+ onSelectPlan: (plan: Plan) => void;
12
+ }
13
+
14
+ export function PricingTable({ currentPlanId, onSelectPlan }: PricingTableProps) {
15
+ return (
16
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
17
+ {PLANS.map((plan) => {
18
+ const isCurrent = plan.id === currentPlanId;
19
+ const isPopular = plan.id === 'pro';
20
+
21
+ return (
22
+ <Card
23
+ key={plan.id}
24
+ className={cn(
25
+ 'relative flex flex-col',
26
+ isPopular && 'border-primary shadow-lg scale-105',
27
+ isCurrent && 'ring-2 ring-primary',
28
+ )}
29
+ >
30
+ {isPopular && (
31
+ <div className="absolute -top-3 left-1/2 -translate-x-1/2">
32
+ <Badge>Most Popular</Badge>
33
+ </div>
34
+ )}
35
+ <CardHeader className="text-center">
36
+ <CardTitle className="text-xl">{plan.name}</CardTitle>
37
+ <CardDescription>
38
+ <span className="text-3xl font-bold text-foreground">${plan.price}</span>
39
+ <span className="text-muted-foreground">/mo</span>
40
+ </CardDescription>
41
+ </CardHeader>
42
+ <CardContent className="flex-1">
43
+ <ul className="space-y-2 text-sm">
44
+ <li className="flex items-center gap-2">
45
+ <span className="text-green-500">&#10003;</span>
46
+ Up to {plan.features.maxAgents} agents
47
+ </li>
48
+ <li className="flex items-center gap-2">
49
+ <span className="text-green-500">&#10003;</span>
50
+ {plan.features.maxMessages.toLocaleString()} messages/mo
51
+ </li>
52
+ <li className="flex items-center gap-2">
53
+ <span className="text-green-500">&#10003;</span>
54
+ {(plan.features.maxTokens / 1000000).toFixed(0)}M tokens
55
+ </li>
56
+ <li className="flex items-center gap-2">
57
+ <span className={plan.features.tools ? 'text-green-500' : 'text-muted-foreground'}>
58
+ {plan.features.tools ? '&#10003;' : '&#10007;'}
59
+ </span>
60
+ Tool calling
61
+ </li>
62
+ <li className="flex items-center gap-2">
63
+ <span className={plan.features.apiAccess ? 'text-green-500' : 'text-muted-foreground'}>
64
+ {plan.features.apiAccess ? '&#10003;' : '&#10007;'}
65
+ </span>
66
+ API access
67
+ </li>
68
+ <li className="flex items-center gap-2">
69
+ <span className={plan.features.customTools ? 'text-green-500' : 'text-muted-foreground'}>
70
+ {plan.features.customTools ? '&#10003;' : '&#10007;'}
71
+ </span>
72
+ Custom tools
73
+ </li>
74
+ <li className="flex items-center gap-2">
75
+ <span className="text-green-500">&#10003;</span>
76
+ {plan.features.support} support
77
+ </li>
78
+ </ul>
79
+ </CardContent>
80
+ <CardFooter>
81
+ <Button
82
+ className="w-full"
83
+ variant={isCurrent ? 'outline' : isPopular ? 'default' : 'secondary'}
84
+ disabled={isCurrent}
85
+ onClick={() => onSelectPlan(plan)}
86
+ >
87
+ {isCurrent ? 'Current Plan' : plan.price === 0 ? 'Get Started Free' : 'Upgrade Now'}
88
+ </Button>
89
+ </CardFooter>
90
+ </Card>
91
+ );
92
+ })}
93
+ </div>
94
+ );
95
+ }
@@ -0,0 +1,39 @@
1
+ import { cn } from '@/lib/utils/helpers';
2
+
3
+ interface UsageMeterProps {
4
+ label: string;
5
+ used: number;
6
+ limit: number;
7
+ unit?: string;
8
+ className?: string;
9
+ }
10
+
11
+ export function UsageMeter({ label, used, limit, unit = '', className }: UsageMeterProps) {
12
+ const percentage = limit > 0 ? Math.min((used / limit) * 100, 100) : 0;
13
+ const isNearLimit = percentage > 80;
14
+ const isAtLimit = percentage >= 100;
15
+
16
+ return (
17
+ <div className={cn('space-y-2', className)}>
18
+ <div className="flex items-center justify-between text-sm">
19
+ <span className="font-medium">{label}</span>
20
+ <span className={cn(
21
+ 'text-muted-foreground',
22
+ isAtLimit && 'text-destructive font-medium',
23
+ isNearLimit && !isAtLimit && 'text-yellow-600',
24
+ )}>
25
+ {used.toLocaleString()}{unit} / {limit.toLocaleString()}{unit}
26
+ </span>
27
+ </div>
28
+ <div className="h-2 w-full rounded-full bg-muted overflow-hidden">
29
+ <div
30
+ className={cn(
31
+ 'h-full rounded-full transition-all duration-500',
32
+ isAtLimit ? 'bg-destructive' : isNearLimit ? 'bg-yellow-500' : 'bg-primary',
33
+ )}
34
+ style={{ width: `${percentage}%` }}
35
+ />
36
+ </div>
37
+ </div>
38
+ );
39
+ }
@@ -0,0 +1,68 @@
1
+ 'use client';
2
+
3
+ import { useState, useRef, useEffect, type FormEvent } from 'react';
4
+ import { Button } from '@/components/ui/button';
5
+ import { cn } from '@/lib/utils/helpers';
6
+
7
+ interface ChatInputProps {
8
+ onSend: (message: string) => void;
9
+ onStop?: () => void;
10
+ disabled?: boolean;
11
+ isLoading?: boolean;
12
+ placeholder?: string;
13
+ }
14
+
15
+ export function ChatInput({ onSend, onStop, disabled, isLoading, placeholder }: ChatInputProps) {
16
+ const [message, setMessage] = useState('');
17
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
18
+
19
+ useEffect(() => {
20
+ if (textareaRef.current) {
21
+ textareaRef.current.style.height = 'auto';
22
+ textareaRef.current.style.height = Math.min(textareaRef.current.scrollHeight, 200) + 'px';
23
+ }
24
+ }, [message]);
25
+
26
+ const handleSubmit = (e: FormEvent) => {
27
+ e.preventDefault();
28
+ const trimmed = message.trim();
29
+ if (!trimmed || disabled) return;
30
+ onSend(trimmed);
31
+ setMessage('');
32
+ if (textareaRef.current) textareaRef.current.style.height = 'auto';
33
+ };
34
+
35
+ const handleKeyDown = (e: React.KeyboardEvent) => {
36
+ if (e.key === 'Enter' && !e.shiftKey) {
37
+ e.preventDefault();
38
+ handleSubmit(e);
39
+ }
40
+ };
41
+
42
+ return (
43
+ <form onSubmit={handleSubmit} className="flex items-end gap-2 p-4 border-t bg-background">
44
+ <textarea
45
+ ref={textareaRef}
46
+ value={message}
47
+ onChange={(e) => setMessage(e.target.value)}
48
+ onKeyDown={handleKeyDown}
49
+ placeholder={placeholder ?? 'Type your message...'}
50
+ disabled={disabled}
51
+ rows={1}
52
+ className={cn(
53
+ 'flex-1 resize-none rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
54
+ 'max-h-[200px]',
55
+ )}
56
+ />
57
+ {isLoading ? (
58
+ <Button type="button" variant="destructive" size="sm" onClick={onStop}>
59
+ Stop
60
+ </Button>
61
+ ) : (
62
+ <Button type="submit" size="sm" disabled={disabled || !message.trim()}>
63
+ Send
64
+ </Button>
65
+ )}
66
+ </form>
67
+ );
68
+ }
@@ -0,0 +1,152 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useRef, useCallback } from 'react';
4
+ import { ChatMessage } from './chat-message';
5
+ import { ChatInput } from './chat-input';
6
+ import { Skeleton } from '@/components/ui/skeleton';
7
+
8
+ interface Message {
9
+ id: string;
10
+ role: 'user' | 'assistant' | 'system' | 'tool';
11
+ content: string;
12
+ }
13
+
14
+ interface ChatInterfaceProps {
15
+ agentId: string;
16
+ chatId?: string;
17
+ initialMessages?: Message[];
18
+ }
19
+
20
+ export function ChatInterface({ agentId, chatId, initialMessages = [] }: ChatInterfaceProps) {
21
+ const [messages, setMessages] = useState<Message[]>(initialMessages);
22
+ const [isLoading, setIsLoading] = useState(false);
23
+ const [streamingContent, setStreamingContent] = useState('');
24
+ const abortRef = useRef<AbortController | null>(null);
25
+ const messagesEndRef = useRef<HTMLDivElement>(null);
26
+
27
+ const scrollToBottom = useCallback(() => {
28
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
29
+ }, []);
30
+
31
+ useEffect(() => {
32
+ scrollToBottom();
33
+ }, [messages, streamingContent, scrollToBottom]);
34
+
35
+ const handleSend = async (content: string) => {
36
+ const userMessage: Message = {
37
+ id: crypto.randomUUID(),
38
+ role: 'user',
39
+ content,
40
+ };
41
+
42
+ setMessages((prev) => [...prev, userMessage]);
43
+ setIsLoading(true);
44
+ setStreamingContent('');
45
+
46
+ const abortController = new AbortController();
47
+ abortRef.current = abortController;
48
+
49
+ try {
50
+ const response = await fetch(`/api/agents/${agentId}/chat`, {
51
+ method: 'POST',
52
+ headers: { 'Content-Type': 'application/json' },
53
+ body: JSON.stringify({ message: content, chatId }),
54
+ signal: abortController.signal,
55
+ });
56
+
57
+ if (!response.ok) throw new Error('Failed to send message');
58
+ if (!response.body) throw new Error('No response body');
59
+
60
+ const reader = response.body.getReader();
61
+ const decoder = new TextDecoder();
62
+ let assistantContent = '';
63
+
64
+ while (true) {
65
+ const { done, value } = await reader.read();
66
+ if (done) break;
67
+
68
+ const chunk = decoder.decode(value, { stream: true });
69
+ const lines = chunk.split('\n');
70
+
71
+ for (const line of lines) {
72
+ if (!line.startsWith('data: ')) continue;
73
+ const data = line.slice(6).trim();
74
+ if (data === '[DONE]') continue;
75
+
76
+ try {
77
+ const parsed = JSON.parse(data);
78
+ if (parsed.type === 'text') {
79
+ assistantContent += parsed.content;
80
+ setStreamingContent(assistantContent);
81
+ }
82
+ } catch {
83
+ // Skip malformed JSON
84
+ }
85
+ }
86
+ }
87
+
88
+ const assistantMessage: Message = {
89
+ id: crypto.randomUUID(),
90
+ role: 'assistant',
91
+ content: assistantContent,
92
+ };
93
+
94
+ setMessages((prev) => [...prev, assistantMessage]);
95
+ setStreamingContent('');
96
+ } catch (err) {
97
+ if ((err as Error).name !== 'AbortError') {
98
+ const errorMessage: Message = {
99
+ id: crypto.randomUUID(),
100
+ role: 'assistant',
101
+ content: 'Sorry, an error occurred. Please try again.',
102
+ };
103
+ setMessages((prev) => [...prev, errorMessage]);
104
+ }
105
+ } finally {
106
+ setIsLoading(false);
107
+ abortRef.current = null;
108
+ }
109
+ };
110
+
111
+ const handleStop = () => {
112
+ abortRef.current?.abort();
113
+ setIsLoading(false);
114
+ if (streamingContent) {
115
+ setMessages((prev) => [
116
+ ...prev,
117
+ { id: crypto.randomUUID(), role: 'assistant', content: streamingContent },
118
+ ]);
119
+ setStreamingContent('');
120
+ }
121
+ };
122
+
123
+ return (
124
+ <div className="flex flex-col h-full">
125
+ <div className="flex-1 overflow-y-auto">
126
+ {messages.length === 0 && !isLoading && (
127
+ <div className="flex items-center justify-center h-full text-muted-foreground">
128
+ <p>Start a conversation with this agent</p>
129
+ </div>
130
+ )}
131
+ {messages.map((msg) => (
132
+ <ChatMessage key={msg.id} role={msg.role} content={msg.content} />
133
+ ))}
134
+ {streamingContent && (
135
+ <ChatMessage role="assistant" content={streamingContent} isStreaming />
136
+ )}
137
+ {isLoading && !streamingContent && (
138
+ <div className="px-4 py-3">
139
+ <Skeleton className="h-4 w-3/4" />
140
+ </div>
141
+ )}
142
+ <div ref={messagesEndRef} />
143
+ </div>
144
+ <ChatInput
145
+ onSend={handleSend}
146
+ onStop={handleStop}
147
+ isLoading={isLoading}
148
+ disabled={isLoading}
149
+ />
150
+ </div>
151
+ );
152
+ }
@@ -0,0 +1,50 @@
1
+ 'use client';
2
+
3
+ import { cn } from '@/lib/utils/helpers';
4
+ import { Avatar } from '@/components/ui/avatar';
5
+ import { MarkdownRenderer } from './markdown-renderer';
6
+
7
+ interface ChatMessageProps {
8
+ role: 'user' | 'assistant' | 'system' | 'tool';
9
+ content: string;
10
+ isStreaming?: boolean;
11
+ metadata?: Record<string, unknown>;
12
+ }
13
+
14
+ export function ChatMessage({ role, content, isStreaming }: ChatMessageProps) {
15
+ const isUser = role === 'user';
16
+ const isTool = role === 'tool';
17
+
18
+ return (
19
+ <div className={cn('flex gap-3 px-4 py-3', isUser ? 'justify-end' : 'justify-start')}>
20
+ {!isUser && (
21
+ <Avatar
22
+ className="h-8 w-8 shrink-0"
23
+ fallback={isTool ? '🔧' : 'AI'}
24
+ />
25
+ )}
26
+ <div
27
+ className={cn(
28
+ 'max-w-[80%] rounded-lg px-4 py-2.5 text-sm',
29
+ isUser
30
+ ? 'bg-primary text-primary-foreground'
31
+ : isTool
32
+ ? 'bg-muted/50 border'
33
+ : 'bg-muted',
34
+ )}
35
+ >
36
+ {isUser ? (
37
+ <p className="whitespace-pre-wrap">{content}</p>
38
+ ) : (
39
+ <MarkdownRenderer content={content} />
40
+ )}
41
+ {isStreaming && (
42
+ <span className="inline-block w-2 h-4 ml-1 bg-current animate-pulse" />
43
+ )}
44
+ </div>
45
+ {isUser && (
46
+ <Avatar className="h-8 w-8 shrink-0" fallback="You" />
47
+ )}
48
+ </div>
49
+ );
50
+ }
@@ -0,0 +1,49 @@
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { cn } from '@/lib/utils/helpers';
5
+
6
+ interface ChatSidebarProps {
7
+ chats: { id: string; title: string; updatedAt: string; _count: { messages: number } }[];
8
+ activeChatId?: string;
9
+ agentId: string;
10
+ onNewChat?: () => void;
11
+ }
12
+
13
+ export function ChatSidebar({ chats, activeChatId, agentId, onNewChat }: ChatSidebarProps) {
14
+ return (
15
+ <div className="flex flex-col h-full w-64 border-r bg-background">
16
+ <div className="p-3 border-b">
17
+ <button
18
+ onClick={onNewChat}
19
+ className="w-full rounded-md border border-dashed border-input p-2 text-sm text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors"
20
+ >
21
+ + New Chat
22
+ </button>
23
+ </div>
24
+ <div className="flex-1 overflow-y-auto">
25
+ {chats.length === 0 ? (
26
+ <div className="p-4 text-sm text-muted-foreground text-center">
27
+ No conversations yet
28
+ </div>
29
+ ) : (
30
+ <div className="p-2 space-y-1">
31
+ {chats.map((chat) => (
32
+ <Link
33
+ key={chat.id}
34
+ href={`/agents/${agentId}?chat=${chat.id}`}
35
+ className={cn(
36
+ 'flex items-center gap-2 rounded-md px-3 py-2 text-sm transition-colors hover:bg-accent',
37
+ activeChatId === chat.id && 'bg-accent text-accent-foreground',
38
+ )}
39
+ >
40
+ <span className="truncate flex-1">{chat.title}</span>
41
+ <span className="text-xs text-muted-foreground">{chat._count.messages}</span>
42
+ </Link>
43
+ ))}
44
+ </div>
45
+ )}
46
+ </div>
47
+ </div>
48
+ );
49
+ }
@@ -0,0 +1,38 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { cn } from '@/lib/utils/helpers';
5
+
6
+ interface CodeBlockProps {
7
+ code: string;
8
+ language: string;
9
+ }
10
+
11
+ export function CodeBlock({ code, language }: CodeBlockProps) {
12
+ const [copied, setCopied] = useState(false);
13
+
14
+ const handleCopy = async () => {
15
+ await navigator.clipboard.writeText(code);
16
+ setCopied(true);
17
+ setTimeout(() => setCopied(false), 2000);
18
+ };
19
+
20
+ return (
21
+ <div className="my-2 rounded-lg border bg-zinc-950 dark:bg-zinc-900 overflow-hidden">
22
+ <div className="flex items-center justify-between px-4 py-2 bg-zinc-900 dark:bg-zinc-800">
23
+ <span className="text-xs text-zinc-400 font-mono">{language}</span>
24
+ <button
25
+ onClick={handleCopy}
26
+ className="text-xs text-zinc-400 hover:text-zinc-200 transition-colors"
27
+ >
28
+ {copied ? 'Copied!' : 'Copy'}
29
+ </button>
30
+ </div>
31
+ <pre className="p-4 overflow-x-auto">
32
+ <code className={cn('text-sm text-zinc-100 font-mono', `language-${language}`)}>
33
+ {code}
34
+ </code>
35
+ </pre>
36
+ </div>
37
+ );
38
+ }
@@ -0,0 +1,56 @@
1
+ 'use client';
2
+
3
+ import { useMemo } from 'react';
4
+ import ReactMarkdown from 'react-markdown';
5
+ import remarkGfm from 'remark-gfm';
6
+ import { CodeBlock } from './code-block';
7
+
8
+ interface MarkdownRendererProps {
9
+ content: string;
10
+ }
11
+
12
+ export function MarkdownRenderer({ content }: MarkdownRendererProps) {
13
+ const components = useMemo(() => ({
14
+ code({ className, children, ...props }: React.ComponentPropsWithoutRef<'code'> & { className?: string }) {
15
+ const match = /language-(\w+)/.exec(className ?? '');
16
+ const isInline = !match;
17
+
18
+ if (isInline) {
19
+ return (
20
+ <code className="rounded bg-muted px-1.5 py-0.5 text-xs font-mono" {...props}>
21
+ {children}
22
+ </code>
23
+ );
24
+ }
25
+
26
+ return (
27
+ <CodeBlock language={match[1] ?? 'text'} code={String(children).replace(/\n$/, '')} />
28
+ );
29
+ },
30
+ pre({ children }: React.ComponentPropsWithoutRef<'pre'>) {
31
+ return <>{children}</>;
32
+ },
33
+ a({ href, children }: React.ComponentPropsWithoutRef<'a'>) {
34
+ return (
35
+ <a href={href} target="_blank" rel="noopener noreferrer" className="text-primary underline hover:text-primary/80">
36
+ {children}
37
+ </a>
38
+ );
39
+ },
40
+ table({ children }: React.ComponentPropsWithoutRef<'table'>) {
41
+ return (
42
+ <div className="my-2 overflow-auto rounded border">
43
+ <table className="w-full text-sm">{children}</table>
44
+ </div>
45
+ );
46
+ },
47
+ }), []);
48
+
49
+ return (
50
+ <div className="prose prose-sm dark:prose-invert max-w-none break-words">
51
+ <ReactMarkdown remarkPlugins={[remarkGfm]} components={components}>
52
+ {content}
53
+ </ReactMarkdown>
54
+ </div>
55
+ );
56
+ }
@@ -0,0 +1,46 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState, useRef } from 'react';
4
+
5
+ interface StreamingTextProps {
6
+ text: string;
7
+ speed?: number;
8
+ className?: string;
9
+ }
10
+
11
+ export function StreamingText({ text, speed = 10, className }: StreamingTextProps) {
12
+ const [displayedText, setDisplayedText] = useState('');
13
+ const indexRef = useRef(0);
14
+
15
+ useEffect(() => {
16
+ if (indexRef.current >= text.length) return;
17
+
18
+ const interval = setInterval(() => {
19
+ if (indexRef.current < text.length) {
20
+ setDisplayedText(text.slice(0, indexRef.current + 1));
21
+ indexRef.current += 1;
22
+ } else {
23
+ clearInterval(interval);
24
+ }
25
+ }, speed);
26
+
27
+ return () => clearInterval(interval);
28
+ }, [text, speed]);
29
+
30
+ // Reset when text changes completely
31
+ useEffect(() => {
32
+ if (!text.startsWith(displayedText)) {
33
+ setDisplayedText(text);
34
+ indexRef.current = text.length;
35
+ }
36
+ }, [text, displayedText]);
37
+
38
+ return (
39
+ <span className={className}>
40
+ {displayedText}
41
+ {indexRef.current < text.length && (
42
+ <span className="inline-block w-1.5 h-4 ml-0.5 bg-current animate-pulse" />
43
+ )}
44
+ </span>
45
+ );
46
+ }
@@ -0,0 +1,52 @@
1
+ import Link from 'next/link';
2
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
3
+ import { Badge } from '@/components/ui/badge';
4
+ import { Button } from '@/components/ui/button';
5
+
6
+ interface AgentCardProps {
7
+ id: string;
8
+ name: string;
9
+ description?: string;
10
+ model: string;
11
+ isPublic: boolean;
12
+ messageCount: number;
13
+ lastUsed?: string | null;
14
+ }
15
+
16
+ export function AgentCard({ id, name, description, model, isPublic, messageCount, lastUsed }: AgentCardProps) {
17
+ return (
18
+ <Card className="group hover:shadow-md transition-shadow">
19
+ <CardHeader className="pb-3">
20
+ <div className="flex items-start justify-between">
21
+ <div>
22
+ <CardTitle className="text-lg">
23
+ <Link href={`/agents/${id}`} className="hover:text-primary transition-colors">
24
+ {name}
25
+ </Link>
26
+ </CardTitle>
27
+ {description && (
28
+ <CardDescription className="mt-1 line-clamp-2">{description}</CardDescription>
29
+ )}
30
+ </div>
31
+ {isPublic && <Badge variant="secondary">Public</Badge>}
32
+ </div>
33
+ </CardHeader>
34
+ <CardContent>
35
+ <div className="flex items-center justify-between">
36
+ <div className="flex items-center gap-3 text-sm text-muted-foreground">
37
+ <span className="font-mono text-xs bg-muted px-2 py-0.5 rounded">{model}</span>
38
+ <span>{messageCount} messages</span>
39
+ </div>
40
+ <Link href={`/agents/${id}`}>
41
+ <Button variant="outline" size="sm">Chat</Button>
42
+ </Link>
43
+ </div>
44
+ {lastUsed && (
45
+ <p className="text-xs text-muted-foreground mt-2">
46
+ Last used {lastUsed}
47
+ </p>
48
+ )}
49
+ </CardContent>
50
+ </Card>
51
+ );
52
+ }