@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.
- package/.env.example +33 -0
- package/Dockerfile +35 -0
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/docker-compose.yml +28 -0
- package/next-env.d.ts +5 -0
- package/next.config.mjs +17 -0
- package/package.json +85 -0
- package/postcss.config.js +6 -0
- package/prisma/schema.prisma +157 -0
- package/prisma/seed.ts +46 -0
- package/src/app/(auth)/forgot-password/page.tsx +56 -0
- package/src/app/(auth)/layout.tsx +7 -0
- package/src/app/(auth)/login/page.tsx +83 -0
- package/src/app/(auth)/signup/page.tsx +108 -0
- package/src/app/(dashboard)/agents/[id]/edit/page.tsx +68 -0
- package/src/app/(dashboard)/agents/[id]/page.tsx +114 -0
- package/src/app/(dashboard)/agents/new/page.tsx +43 -0
- package/src/app/(dashboard)/agents/page.tsx +63 -0
- package/src/app/(dashboard)/api-keys/page.tsx +139 -0
- package/src/app/(dashboard)/dashboard/page.tsx +79 -0
- package/src/app/(dashboard)/layout.tsx +16 -0
- package/src/app/(dashboard)/settings/billing/page.tsx +59 -0
- package/src/app/(dashboard)/settings/page.tsx +45 -0
- package/src/app/(dashboard)/usage/page.tsx +46 -0
- package/src/app/api/agents/[id]/chat/route.ts +100 -0
- package/src/app/api/agents/[id]/chats/route.ts +36 -0
- package/src/app/api/agents/[id]/route.ts +97 -0
- package/src/app/api/agents/route.ts +84 -0
- package/src/app/api/api-keys/[id]/route.ts +25 -0
- package/src/app/api/api-keys/route.ts +72 -0
- package/src/app/api/auth/[...nextauth]/route.ts +5 -0
- package/src/app/api/auth/register/route.ts +53 -0
- package/src/app/api/health/route.ts +26 -0
- package/src/app/api/stripe/checkout/route.ts +37 -0
- package/src/app/api/stripe/plans/route.ts +16 -0
- package/src/app/api/stripe/portal/route.ts +29 -0
- package/src/app/api/stripe/webhook/route.ts +45 -0
- package/src/app/api/usage/route.ts +43 -0
- package/src/app/globals.css +59 -0
- package/src/app/layout.tsx +22 -0
- package/src/app/page.tsx +32 -0
- package/src/app/pricing/page.tsx +25 -0
- package/src/components/agents/agent-form.tsx +137 -0
- package/src/components/agents/model-selector.tsx +35 -0
- package/src/components/agents/tool-selector.tsx +48 -0
- package/src/components/auth-provider.tsx +17 -0
- package/src/components/billing/plan-badge.tsx +23 -0
- package/src/components/billing/pricing-table.tsx +95 -0
- package/src/components/billing/usage-meter.tsx +39 -0
- package/src/components/chat/chat-input.tsx +68 -0
- package/src/components/chat/chat-interface.tsx +152 -0
- package/src/components/chat/chat-message.tsx +50 -0
- package/src/components/chat/chat-sidebar.tsx +49 -0
- package/src/components/chat/code-block.tsx +38 -0
- package/src/components/chat/markdown-renderer.tsx +56 -0
- package/src/components/chat/streaming-text.tsx +46 -0
- package/src/components/dashboard/agent-card.tsx +52 -0
- package/src/components/dashboard/header.tsx +75 -0
- package/src/components/dashboard/sidebar.tsx +52 -0
- package/src/components/dashboard/stat-card.tsx +42 -0
- package/src/components/dashboard/usage-chart.tsx +42 -0
- package/src/components/landing/cta.tsx +30 -0
- package/src/components/landing/features.tsx +75 -0
- package/src/components/landing/hero.tsx +42 -0
- package/src/components/landing/pricing.tsx +28 -0
- package/src/components/ui/avatar.tsx +24 -0
- package/src/components/ui/badge.tsx +24 -0
- package/src/components/ui/button.tsx +39 -0
- package/src/components/ui/card.tsx +50 -0
- package/src/components/ui/dialog.tsx +73 -0
- package/src/components/ui/dropdown.tsx +77 -0
- package/src/components/ui/input.tsx +23 -0
- package/src/components/ui/skeleton.tsx +7 -0
- package/src/components/ui/switch.tsx +31 -0
- package/src/components/ui/table.tsx +48 -0
- package/src/components/ui/tabs.tsx +66 -0
- package/src/components/ui/textarea.tsx +20 -0
- package/src/hooks/use-agent.ts +44 -0
- package/src/hooks/use-streaming.ts +82 -0
- package/src/hooks/use-subscription.ts +40 -0
- package/src/hooks/use-usage.ts +43 -0
- package/src/hooks/use-user.ts +13 -0
- package/src/lib/agents/index.ts +60 -0
- package/src/lib/agents/memory/long-term.ts +241 -0
- package/src/lib/agents/memory/manager.ts +154 -0
- package/src/lib/agents/memory/short-term.ts +155 -0
- package/src/lib/agents/memory/types.ts +68 -0
- package/src/lib/agents/orchestration/debate.ts +170 -0
- package/src/lib/agents/orchestration/index.ts +103 -0
- package/src/lib/agents/orchestration/parallel.ts +143 -0
- package/src/lib/agents/orchestration/router.ts +199 -0
- package/src/lib/agents/orchestration/sequential.ts +127 -0
- package/src/lib/agents/orchestration/types.ts +68 -0
- package/src/lib/agents/tools/calculator.ts +131 -0
- package/src/lib/agents/tools/code-executor.ts +191 -0
- package/src/lib/agents/tools/file-reader.ts +129 -0
- package/src/lib/agents/tools/index.ts +48 -0
- package/src/lib/agents/tools/registry.ts +182 -0
- package/src/lib/agents/tools/web-search.ts +83 -0
- package/src/lib/ai/agent.ts +275 -0
- package/src/lib/ai/context.ts +68 -0
- package/src/lib/ai/memory.ts +98 -0
- package/src/lib/ai/models.ts +80 -0
- package/src/lib/ai/streaming.ts +80 -0
- package/src/lib/ai/tools.ts +149 -0
- package/src/lib/auth/middleware.ts +41 -0
- package/src/lib/auth/nextauth.ts +69 -0
- package/src/lib/db/client.ts +15 -0
- package/src/lib/rate-limit/limiter.ts +93 -0
- package/src/lib/rate-limit/rules.ts +38 -0
- package/src/lib/stripe/client.ts +25 -0
- package/src/lib/stripe/plans.ts +75 -0
- package/src/lib/stripe/usage.ts +123 -0
- package/src/lib/stripe/webhooks.ts +96 -0
- package/src/lib/utils/api-response.ts +85 -0
- package/src/lib/utils/errors.ts +73 -0
- package/src/lib/utils/helpers.ts +50 -0
- package/src/lib/utils/id.ts +21 -0
- package/src/lib/utils/logger.ts +38 -0
- package/src/lib/utils/validation.ts +44 -0
- package/src/middleware.ts +13 -0
- package/src/types/agent.ts +31 -0
- package/src/types/api.ts +38 -0
- package/src/types/billing.ts +35 -0
- package/src/types/chat.ts +30 -0
- package/src/types/next-auth.d.ts +19 -0
- package/tailwind.config.ts +72 -0
- package/tsconfig.json +28 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import { useSession, signOut } from 'next-auth/react';
|
|
5
|
+
import { useState } from 'react';
|
|
6
|
+
import { Button } from '@/components/ui/button';
|
|
7
|
+
import { Dropdown, DropdownTrigger, DropdownContent, DropdownItem, DropdownSeparator } from '@/components/ui/dropdown';
|
|
8
|
+
import { Avatar } from '@/components/ui/avatar';
|
|
9
|
+
|
|
10
|
+
export function Header() {
|
|
11
|
+
const { data: session } = useSession();
|
|
12
|
+
const [dropdownOpen, setDropdownOpen] = useState(false);
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<header className="sticky top-0 z-40 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
|
16
|
+
<div className="flex h-14 items-center px-6">
|
|
17
|
+
<Link href="/" className="flex items-center gap-2 font-bold text-lg">
|
|
18
|
+
<span className="text-primary">🤖</span>
|
|
19
|
+
<span>AgentKit</span>
|
|
20
|
+
</Link>
|
|
21
|
+
|
|
22
|
+
<nav className="ml-8 flex items-center gap-6 text-sm">
|
|
23
|
+
<Link href="/dashboard" className="text-muted-foreground hover:text-foreground transition-colors">
|
|
24
|
+
Dashboard
|
|
25
|
+
</Link>
|
|
26
|
+
<Link href="/agents" className="text-muted-foreground hover:text-foreground transition-colors">
|
|
27
|
+
Agents
|
|
28
|
+
</Link>
|
|
29
|
+
<Link href="/pricing" className="text-muted-foreground hover:text-foreground transition-colors">
|
|
30
|
+
Pricing
|
|
31
|
+
</Link>
|
|
32
|
+
</nav>
|
|
33
|
+
|
|
34
|
+
<div className="ml-auto flex items-center gap-4">
|
|
35
|
+
{session?.user ? (
|
|
36
|
+
<Dropdown>
|
|
37
|
+
<DropdownTrigger onClick={() => setDropdownOpen(!dropdownOpen)} className="flex items-center gap-2">
|
|
38
|
+
<Avatar
|
|
39
|
+
src={session.user.image}
|
|
40
|
+
alt={session.user.name ?? ''}
|
|
41
|
+
fallback={session.user.name}
|
|
42
|
+
className="h-8 w-8"
|
|
43
|
+
/>
|
|
44
|
+
</DropdownTrigger>
|
|
45
|
+
<DropdownContent open={dropdownOpen} onClose={() => setDropdownOpen(false)}>
|
|
46
|
+
<DropdownItem onClick={() => setDropdownOpen(false)}>
|
|
47
|
+
<Link href="/settings" className="w-full">Settings</Link>
|
|
48
|
+
</DropdownItem>
|
|
49
|
+
<DropdownItem onClick={() => setDropdownOpen(false)}>
|
|
50
|
+
<Link href="/settings/billing" className="w-full">Billing</Link>
|
|
51
|
+
</DropdownItem>
|
|
52
|
+
<DropdownItem onClick={() => setDropdownOpen(false)}>
|
|
53
|
+
<Link href="/api-keys" className="w-full">API Keys</Link>
|
|
54
|
+
</DropdownItem>
|
|
55
|
+
<DropdownSeparator />
|
|
56
|
+
<DropdownItem onClick={() => { signOut(); setDropdownOpen(false); }}>
|
|
57
|
+
Sign Out
|
|
58
|
+
</DropdownItem>
|
|
59
|
+
</DropdownContent>
|
|
60
|
+
</Dropdown>
|
|
61
|
+
) : (
|
|
62
|
+
<div className="flex items-center gap-2">
|
|
63
|
+
<Link href="/login">
|
|
64
|
+
<Button variant="ghost" size="sm">Log in</Button>
|
|
65
|
+
</Link>
|
|
66
|
+
<Link href="/signup">
|
|
67
|
+
<Button size="sm">Sign up</Button>
|
|
68
|
+
</Link>
|
|
69
|
+
</div>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</header>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import { usePathname } from 'next/navigation';
|
|
5
|
+
import { cn } from '@/lib/utils/helpers';
|
|
6
|
+
|
|
7
|
+
const navItems = [
|
|
8
|
+
{ label: 'Dashboard', href: '/dashboard' },
|
|
9
|
+
{ label: 'Agents', href: '/agents' },
|
|
10
|
+
{ label: 'API Keys', href: '/api-keys' },
|
|
11
|
+
{ label: 'Usage', href: '/usage' },
|
|
12
|
+
{ label: 'Settings', href: '/settings' },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export function Sidebar() {
|
|
16
|
+
const pathname = usePathname();
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<aside className="hidden md:flex md:w-56 md:flex-col md:border-r bg-background">
|
|
20
|
+
<div className="flex h-14 items-center border-b px-4">
|
|
21
|
+
<Link href="/" className="flex items-center gap-2 font-bold">
|
|
22
|
+
<span className="text-primary">🤖</span>
|
|
23
|
+
<span>AgentKit</span>
|
|
24
|
+
</Link>
|
|
25
|
+
</div>
|
|
26
|
+
<nav className="flex-1 space-y-1 p-3">
|
|
27
|
+
{navItems.map((item) => (
|
|
28
|
+
<Link
|
|
29
|
+
key={item.href}
|
|
30
|
+
href={item.href}
|
|
31
|
+
className={cn(
|
|
32
|
+
'flex items-center rounded-md px-3 py-2 text-sm font-medium transition-colors',
|
|
33
|
+
pathname === item.href || pathname.startsWith(item.href + '/')
|
|
34
|
+
? 'bg-accent text-accent-foreground'
|
|
35
|
+
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground',
|
|
36
|
+
)}
|
|
37
|
+
>
|
|
38
|
+
{item.label}
|
|
39
|
+
</Link>
|
|
40
|
+
))}
|
|
41
|
+
</nav>
|
|
42
|
+
<div className="border-t p-3">
|
|
43
|
+
<Link
|
|
44
|
+
href="/settings/billing"
|
|
45
|
+
className="flex items-center rounded-md px-3 py-2 text-sm text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors"
|
|
46
|
+
>
|
|
47
|
+
Upgrade Plan
|
|
48
|
+
</Link>
|
|
49
|
+
</div>
|
|
50
|
+
</aside>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Card, CardContent } from '@/components/ui/card';
|
|
2
|
+
import { cn } from '@/lib/utils/helpers';
|
|
3
|
+
|
|
4
|
+
interface StatCardProps {
|
|
5
|
+
title: string;
|
|
6
|
+
value: string | number;
|
|
7
|
+
description?: string;
|
|
8
|
+
icon?: React.ReactNode;
|
|
9
|
+
trend?: { value: number; positive: boolean };
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function StatCard({ title, value, description, icon, trend, className }: StatCardProps) {
|
|
14
|
+
return (
|
|
15
|
+
<Card className={className}>
|
|
16
|
+
<CardContent className="p-6">
|
|
17
|
+
<div className="flex items-center justify-between">
|
|
18
|
+
<p className="text-sm font-medium text-muted-foreground">{title}</p>
|
|
19
|
+
{icon && <div className="text-muted-foreground">{icon}</div>}
|
|
20
|
+
</div>
|
|
21
|
+
<div className="mt-2">
|
|
22
|
+
<p className="text-2xl font-bold">{value}</p>
|
|
23
|
+
{(description || trend) && (
|
|
24
|
+
<div className="flex items-center gap-1 mt-1">
|
|
25
|
+
{trend && (
|
|
26
|
+
<span className={cn(
|
|
27
|
+
'text-xs font-medium',
|
|
28
|
+
trend.positive ? 'text-green-600' : 'text-red-600',
|
|
29
|
+
)}>
|
|
30
|
+
{trend.positive ? '+' : ''}{trend.value}%
|
|
31
|
+
</span>
|
|
32
|
+
)}
|
|
33
|
+
{description && (
|
|
34
|
+
<span className="text-xs text-muted-foreground">{description}</span>
|
|
35
|
+
)}
|
|
36
|
+
</div>
|
|
37
|
+
)}
|
|
38
|
+
</div>
|
|
39
|
+
</CardContent>
|
|
40
|
+
</Card>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis, Tooltip } from 'recharts';
|
|
4
|
+
|
|
5
|
+
interface UsageChartProps {
|
|
6
|
+
data: { date: string; messages: number; tokens: number }[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function UsageChart({ data }: UsageChartProps) {
|
|
10
|
+
return (
|
|
11
|
+
<div className="h-[300px] w-full">
|
|
12
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
13
|
+
<BarChart data={data}>
|
|
14
|
+
<XAxis
|
|
15
|
+
dataKey="date"
|
|
16
|
+
stroke="#888888"
|
|
17
|
+
fontSize={12}
|
|
18
|
+
tickLine={false}
|
|
19
|
+
axisLine={false}
|
|
20
|
+
/>
|
|
21
|
+
<YAxis
|
|
22
|
+
stroke="#888888"
|
|
23
|
+
fontSize={12}
|
|
24
|
+
tickLine={false}
|
|
25
|
+
axisLine={false}
|
|
26
|
+
tickFormatter={(value) => `${value}`}
|
|
27
|
+
/>
|
|
28
|
+
<Tooltip
|
|
29
|
+
contentStyle={{
|
|
30
|
+
backgroundColor: 'hsl(var(--background))',
|
|
31
|
+
border: '1px solid hsl(var(--border))',
|
|
32
|
+
borderRadius: '6px',
|
|
33
|
+
fontSize: '12px',
|
|
34
|
+
}}
|
|
35
|
+
/>
|
|
36
|
+
<Bar dataKey="messages" fill="hsl(var(--primary))" radius={[4, 4, 0, 0]} name="Messages" />
|
|
37
|
+
<Bar dataKey="tokens" fill="hsl(var(--muted-foreground))" radius={[4, 4, 0, 0]} name="Tokens (K)" />
|
|
38
|
+
</BarChart>
|
|
39
|
+
</ResponsiveContainer>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import Link from 'next/link';
|
|
2
|
+
import { Button } from '@/components/ui/button';
|
|
3
|
+
|
|
4
|
+
export function CTA() {
|
|
5
|
+
return (
|
|
6
|
+
<section className="py-20 bg-primary text-primary-foreground">
|
|
7
|
+
<div className="mx-auto max-w-7xl px-6 lg:px-8 text-center">
|
|
8
|
+
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl">
|
|
9
|
+
Ready to ship your AI SaaS?
|
|
10
|
+
</h2>
|
|
11
|
+
<p className="mt-4 text-lg opacity-90 max-w-2xl mx-auto">
|
|
12
|
+
Join thousands of developers building AI products with AgentKit.
|
|
13
|
+
Get started in minutes, not months.
|
|
14
|
+
</p>
|
|
15
|
+
<div className="mt-8 flex items-center justify-center gap-4">
|
|
16
|
+
<Link href="/signup">
|
|
17
|
+
<Button size="lg" variant="secondary">
|
|
18
|
+
Start Building Free
|
|
19
|
+
</Button>
|
|
20
|
+
</Link>
|
|
21
|
+
<Link href="https://github.com" target="_blank" rel="noopener noreferrer">
|
|
22
|
+
<Button size="lg" variant="outline" className="border-primary-foreground text-primary-foreground hover:bg-primary-foreground hover:text-primary">
|
|
23
|
+
View on GitHub
|
|
24
|
+
</Button>
|
|
25
|
+
</Link>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</section>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
|
2
|
+
|
|
3
|
+
const features = [
|
|
4
|
+
{
|
|
5
|
+
title: 'AI Agent Engine',
|
|
6
|
+
description: 'Multi-model support (GPT-4o, Claude, Gemini) with streaming responses and tool calling out of the box.',
|
|
7
|
+
icon: '🤖',
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
title: 'Tool Calling',
|
|
11
|
+
description: 'Built-in tools: web search, calculator, JSON parser, code executor, file reader. Add custom tools easily.',
|
|
12
|
+
icon: '🔧',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
title: 'Streaming SSE',
|
|
16
|
+
description: 'Real-time streaming responses with Server-Sent Events. Works with any OpenAI-compatible API.',
|
|
17
|
+
icon: '⚡',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
title: 'Auth & Billing',
|
|
21
|
+
description: 'NextAuth with Google, GitHub, email. Stripe subscriptions with free/pro/team tiers.',
|
|
22
|
+
icon: '💳',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
title: 'Rate Limiting',
|
|
26
|
+
description: 'Sliding window rate limiter with in-memory or Redis backend. Per-plan limits.',
|
|
27
|
+
icon: '🛡',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
title: 'API Keys',
|
|
31
|
+
description: 'Generate API keys for programmatic access. Prefix-based identification with secure hashing.',
|
|
32
|
+
icon: '🔑',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
title: 'Usage Tracking',
|
|
36
|
+
description: 'Track messages, tokens, and tool calls per billing period. Usage-based analytics dashboard.',
|
|
37
|
+
icon: '📊',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
title: 'Dashboard UI',
|
|
41
|
+
description: 'Beautiful dashboard with agent management, chat interface, billing, and usage analytics.',
|
|
42
|
+
icon: '🎨',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
title: 'TypeScript Strict',
|
|
46
|
+
description: 'Full TypeScript strict mode with Zod validation. Type-safe API routes and database queries.',
|
|
47
|
+
icon: '📝',
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
export function Features() {
|
|
52
|
+
return (
|
|
53
|
+
<section id="features" className="py-20 bg-muted/50">
|
|
54
|
+
<div className="mx-auto max-w-7xl px-6 lg:px-8">
|
|
55
|
+
<div className="mx-auto max-w-2xl text-center mb-12">
|
|
56
|
+
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl">Everything you need</h2>
|
|
57
|
+
<p className="mt-4 text-lg text-muted-foreground">
|
|
58
|
+
A complete AI agent platform, ready for production.
|
|
59
|
+
</p>
|
|
60
|
+
</div>
|
|
61
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
62
|
+
{features.map((feature) => (
|
|
63
|
+
<Card key={feature.title}>
|
|
64
|
+
<CardHeader>
|
|
65
|
+
<span className="text-2xl" dangerouslySetInnerHTML={{ __html: feature.icon }} />
|
|
66
|
+
<CardTitle className="text-lg">{feature.title}</CardTitle>
|
|
67
|
+
<CardDescription>{feature.description}</CardDescription>
|
|
68
|
+
</CardHeader>
|
|
69
|
+
</Card>
|
|
70
|
+
))}
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</section>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import Link from 'next/link';
|
|
2
|
+
import { Button } from '@/components/ui/button';
|
|
3
|
+
|
|
4
|
+
export function Hero() {
|
|
5
|
+
return (
|
|
6
|
+
<section className="relative overflow-hidden py-20 sm:py-32">
|
|
7
|
+
<div className="mx-auto max-w-7xl px-6 lg:px-8">
|
|
8
|
+
<div className="mx-auto max-w-2xl text-center">
|
|
9
|
+
<p className="text-sm font-semibold leading-7 text-primary tracking-wide uppercase">
|
|
10
|
+
Open Source AI Agent Framework
|
|
11
|
+
</p>
|
|
12
|
+
<h1 className="mt-2 text-4xl font-bold tracking-tight sm:text-6xl lg:text-7xl">
|
|
13
|
+
Ship your AI SaaS
|
|
14
|
+
<span className="text-primary"> this weekend</span>
|
|
15
|
+
</h1>
|
|
16
|
+
<p className="mt-6 text-lg leading-8 text-muted-foreground max-w-2xl mx-auto">
|
|
17
|
+
Production-ready AI agent starter kit with Next.js 14, streaming, tool calling,
|
|
18
|
+
Stripe billing, auth, and more. Open source and self-hostable.
|
|
19
|
+
</p>
|
|
20
|
+
<div className="mt-10 flex items-center justify-center gap-4">
|
|
21
|
+
<Link href="/signup">
|
|
22
|
+
<Button size="lg">Get Started Free</Button>
|
|
23
|
+
</Link>
|
|
24
|
+
<Link href="https://github.com" target="_blank" rel="noopener noreferrer">
|
|
25
|
+
<Button size="lg" variant="outline">
|
|
26
|
+
<svg className="mr-2 h-4 w-4" fill="currentColor" viewBox="0 0 24 24"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
|
|
27
|
+
Star on GitHub
|
|
28
|
+
</Button>
|
|
29
|
+
</Link>
|
|
30
|
+
</div>
|
|
31
|
+
<div className="mt-8 flex items-center justify-center gap-6 text-sm text-muted-foreground">
|
|
32
|
+
<span>MIT License</span>
|
|
33
|
+
<span>•</span>
|
|
34
|
+
<span>TypeScript</span>
|
|
35
|
+
<span>•</span>
|
|
36
|
+
<span>Self-hostable</span>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</section>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { PricingTable } from '@/components/billing/pricing-table';
|
|
4
|
+
|
|
5
|
+
export function LandingPricing() {
|
|
6
|
+
return (
|
|
7
|
+
<section id="pricing" className="py-20">
|
|
8
|
+
<div className="mx-auto max-w-7xl px-6 lg:px-8">
|
|
9
|
+
<div className="mx-auto max-w-2xl text-center mb-12">
|
|
10
|
+
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl">Simple, transparent pricing</h2>
|
|
11
|
+
<p className="mt-4 text-lg text-muted-foreground">
|
|
12
|
+
Start free, upgrade when you need more.
|
|
13
|
+
</p>
|
|
14
|
+
</div>
|
|
15
|
+
<div className="mx-auto max-w-5xl">
|
|
16
|
+
<PricingTable
|
|
17
|
+
onSelectPlan={() => {
|
|
18
|
+
if (typeof window !== 'undefined') window.location.href = '/signup';
|
|
19
|
+
}}
|
|
20
|
+
/>
|
|
21
|
+
</div>
|
|
22
|
+
<p className="mt-8 text-center text-sm text-muted-foreground">
|
|
23
|
+
Self-hosted is always free. Cloud pricing applies to the managed service.
|
|
24
|
+
</p>
|
|
25
|
+
</div>
|
|
26
|
+
</section>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils/helpers';
|
|
2
|
+
|
|
3
|
+
export function Avatar({ className, src, alt, fallback }: {
|
|
4
|
+
className?: string;
|
|
5
|
+
src?: string | null;
|
|
6
|
+
alt?: string;
|
|
7
|
+
fallback?: string | null;
|
|
8
|
+
}) {
|
|
9
|
+
const initials = fallback ?? alt?.slice(0, 2).toUpperCase() ?? 'U';
|
|
10
|
+
|
|
11
|
+
if (src) {
|
|
12
|
+
return (
|
|
13
|
+
<div className={cn('relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full', className)}>
|
|
14
|
+
<img src={src} alt={alt ?? ''} className="aspect-square h-full w-full" />
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className={cn('relative flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden rounded-full bg-muted', className)}>
|
|
21
|
+
<span className="text-sm font-medium">{initials}</span>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type HTMLAttributes } from 'react';
|
|
2
|
+
import { cn } from '@/lib/utils/helpers';
|
|
3
|
+
|
|
4
|
+
export interface BadgeProps extends HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
variant?: 'default' | 'secondary' | 'destructive' | 'outline';
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Badge({ className, variant = 'default', ...props }: BadgeProps) {
|
|
9
|
+
return (
|
|
10
|
+
<div
|
|
11
|
+
className={cn(
|
|
12
|
+
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
|
13
|
+
{
|
|
14
|
+
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80': variant === 'default',
|
|
15
|
+
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80': variant === 'secondary',
|
|
16
|
+
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80': variant === 'destructive',
|
|
17
|
+
'text-foreground': variant === 'outline',
|
|
18
|
+
},
|
|
19
|
+
className,
|
|
20
|
+
)}
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { forwardRef, type ButtonHTMLAttributes } from 'react';
|
|
2
|
+
import { cn } from '@/lib/utils/helpers';
|
|
3
|
+
|
|
4
|
+
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
5
|
+
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
|
|
6
|
+
size?: 'default' | 'sm' | 'lg' | 'icon';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
10
|
+
({ className, variant = 'default', size = 'default', ...props }, ref) => {
|
|
11
|
+
return (
|
|
12
|
+
<button
|
|
13
|
+
className={cn(
|
|
14
|
+
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
|
15
|
+
{
|
|
16
|
+
'bg-primary text-primary-foreground hover:bg-primary/90': variant === 'default',
|
|
17
|
+
'bg-destructive text-destructive-foreground hover:bg-destructive/90': variant === 'destructive',
|
|
18
|
+
'border border-input bg-background hover:bg-accent hover:text-accent-foreground': variant === 'outline',
|
|
19
|
+
'bg-secondary text-secondary-foreground hover:bg-secondary/80': variant === 'secondary',
|
|
20
|
+
'hover:bg-accent hover:text-accent-foreground': variant === 'ghost',
|
|
21
|
+
'text-primary underline-offset-4 hover:underline': variant === 'link',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
'h-10 px-4 py-2': size === 'default',
|
|
25
|
+
'h-9 rounded-md px-3': size === 'sm',
|
|
26
|
+
'h-11 rounded-md px-8': size === 'lg',
|
|
27
|
+
'h-10 w-10': size === 'icon',
|
|
28
|
+
},
|
|
29
|
+
className,
|
|
30
|
+
)}
|
|
31
|
+
ref={ref}
|
|
32
|
+
{...props}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
Button.displayName = 'Button';
|
|
38
|
+
|
|
39
|
+
export { Button };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { forwardRef, type HTMLAttributes } from 'react';
|
|
2
|
+
import { cn } from '@/lib/utils/helpers';
|
|
3
|
+
|
|
4
|
+
const Card = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
5
|
+
({ className, ...props }, ref) => (
|
|
6
|
+
<div
|
|
7
|
+
ref={ref}
|
|
8
|
+
className={cn('rounded-lg border bg-card text-card-foreground shadow-sm', className)}
|
|
9
|
+
{...props}
|
|
10
|
+
/>
|
|
11
|
+
),
|
|
12
|
+
);
|
|
13
|
+
Card.displayName = 'Card';
|
|
14
|
+
|
|
15
|
+
const CardHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
16
|
+
({ className, ...props }, ref) => (
|
|
17
|
+
<div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
|
|
18
|
+
),
|
|
19
|
+
);
|
|
20
|
+
CardHeader.displayName = 'CardHeader';
|
|
21
|
+
|
|
22
|
+
const CardTitle = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLHeadingElement>>(
|
|
23
|
+
({ className, ...props }, ref) => (
|
|
24
|
+
<h3 ref={ref} className={cn('text-2xl font-semibold leading-none tracking-tight', className)} {...props} />
|
|
25
|
+
),
|
|
26
|
+
);
|
|
27
|
+
CardTitle.displayName = 'CardTitle';
|
|
28
|
+
|
|
29
|
+
const CardDescription = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(
|
|
30
|
+
({ className, ...props }, ref) => (
|
|
31
|
+
<p ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
|
|
32
|
+
),
|
|
33
|
+
);
|
|
34
|
+
CardDescription.displayName = 'CardDescription';
|
|
35
|
+
|
|
36
|
+
const CardContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
37
|
+
({ className, ...props }, ref) => (
|
|
38
|
+
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
|
|
39
|
+
),
|
|
40
|
+
);
|
|
41
|
+
CardContent.displayName = 'CardContent';
|
|
42
|
+
|
|
43
|
+
const CardFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
44
|
+
({ className, ...props }, ref) => (
|
|
45
|
+
<div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
CardFooter.displayName = 'CardFooter';
|
|
49
|
+
|
|
50
|
+
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
|
|
4
|
+
import { cn } from '@/lib/utils/helpers';
|
|
5
|
+
|
|
6
|
+
interface DialogContextValue {
|
|
7
|
+
open: boolean;
|
|
8
|
+
onOpenChange: (open: boolean) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const DialogContext = createContext<DialogContextValue>({ open: false, onOpenChange: () => {} });
|
|
12
|
+
|
|
13
|
+
export function Dialog({ open: controlledOpen, onOpenChange: controlledOnChange, children }: {
|
|
14
|
+
open?: boolean;
|
|
15
|
+
onOpenChange?: (open: boolean) => void;
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
}) {
|
|
18
|
+
const [internalOpen, setInternalOpen] = useState(false);
|
|
19
|
+
const open = controlledOpen ?? internalOpen;
|
|
20
|
+
const onOpenChange = controlledOnChange ?? setInternalOpen;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<DialogContext.Provider value={{ open, onOpenChange }}>
|
|
24
|
+
{children}
|
|
25
|
+
</DialogContext.Provider>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function DialogTrigger({ children, className }: { children: ReactNode; className?: string }) {
|
|
30
|
+
const { onOpenChange } = useContext(DialogContext);
|
|
31
|
+
return (
|
|
32
|
+
<button className={className} onClick={() => onOpenChange(true)}>
|
|
33
|
+
{children}
|
|
34
|
+
</button>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function DialogContent({ children, className }: { children: ReactNode; className?: string }) {
|
|
39
|
+
const { open, onOpenChange } = useContext(DialogContext);
|
|
40
|
+
|
|
41
|
+
if (!open) return null;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
45
|
+
<div className="fixed inset-0 bg-black/80" onClick={() => onOpenChange(false)} />
|
|
46
|
+
<div className={cn('fixed z-50 grid w-full max-w-lg gap-4 border bg-background p-6 shadow-lg sm:rounded-lg animate-fade-in', className)}>
|
|
47
|
+
{children}
|
|
48
|
+
<button
|
|
49
|
+
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100"
|
|
50
|
+
onClick={() => onOpenChange(false)}
|
|
51
|
+
>
|
|
52
|
+
<svg width="15" height="15" viewBox="0 0 15 15" fill="none"><path d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z" fill="currentColor" fillRule="evenodd" clipRule="evenodd" /></svg>
|
|
53
|
+
</button>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function DialogHeader({ className, ...props }: { className?: string; children?: ReactNode }) {
|
|
60
|
+
return <div className={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)} {...props} />;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function DialogTitle({ className, ...props }: { className?: string; children?: ReactNode }) {
|
|
64
|
+
return <h2 className={cn('text-lg font-semibold leading-none tracking-tight', className)} {...props} />;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function DialogDescription({ className, ...props }: { className?: string; children?: ReactNode }) {
|
|
68
|
+
return <p className={cn('text-sm text-muted-foreground', className)} {...props} />;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function DialogFooter({ className, ...props }: { className?: string; children?: ReactNode }) {
|
|
72
|
+
return <div className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)} {...props} />;
|
|
73
|
+
}
|