@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,37 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { getServerSession } from 'next-auth';
|
|
3
|
+
import { authOptions } from '@/lib/auth/nextauth';
|
|
4
|
+
import { stripe } from '@/lib/stripe/client';
|
|
5
|
+
import { getOrCreateCustomer } from '@/lib/stripe/usage';
|
|
6
|
+
import { handleApiError } from '@/lib/utils/api-response';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
|
|
9
|
+
const checkoutSchema = z.object({ priceId: z.string() });
|
|
10
|
+
|
|
11
|
+
export async function POST(req: NextRequest) {
|
|
12
|
+
try {
|
|
13
|
+
const session = await getServerSession(authOptions);
|
|
14
|
+
if (!session?.user?.id || !session.user.email) {
|
|
15
|
+
return NextResponse.json({ success: false, error: { code: 'UNAUTHORIZED', message: 'Not authenticated' } }, { status: 401 });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const body = await req.json();
|
|
19
|
+
const { priceId } = checkoutSchema.parse(body);
|
|
20
|
+
|
|
21
|
+
const customerId = await getOrCreateCustomer(session.user.id, session.user.email);
|
|
22
|
+
|
|
23
|
+
const checkoutSession = await stripe.checkout.sessions.create({
|
|
24
|
+
customer: customerId,
|
|
25
|
+
mode: 'subscription',
|
|
26
|
+
payment_method_types: ['card'],
|
|
27
|
+
line_items: [{ price: priceId, quantity: 1 }],
|
|
28
|
+
success_url: `${process.env.NEXTAUTH_URL}/settings/billing?success=true`,
|
|
29
|
+
cancel_url: `${process.env.NEXTAUTH_URL}/settings/billing?canceled=true`,
|
|
30
|
+
metadata: { userId: session.user.id, priceId },
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return NextResponse.json({ success: true, data: { url: checkoutSession.url } });
|
|
34
|
+
} catch (err) {
|
|
35
|
+
return handleApiError(err);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { PLANS } from '@/lib/stripe/plans';
|
|
3
|
+
|
|
4
|
+
export async function GET() {
|
|
5
|
+
return NextResponse.json({
|
|
6
|
+
success: true,
|
|
7
|
+
data: PLANS.map((plan) => ({
|
|
8
|
+
id: plan.id,
|
|
9
|
+
name: plan.name,
|
|
10
|
+
price: plan.price,
|
|
11
|
+
interval: plan.interval,
|
|
12
|
+
stripePriceId: plan.stripePriceId,
|
|
13
|
+
features: plan.features,
|
|
14
|
+
})),
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { getServerSession } from 'next-auth';
|
|
3
|
+
import { authOptions } from '@/lib/auth/nextauth';
|
|
4
|
+
import { stripe } from '@/lib/stripe/client';
|
|
5
|
+
import { handleApiError } from '@/lib/utils/api-response';
|
|
6
|
+
import prisma from '@/lib/db/client';
|
|
7
|
+
|
|
8
|
+
export async function POST() {
|
|
9
|
+
try {
|
|
10
|
+
const session = await getServerSession(authOptions);
|
|
11
|
+
if (!session?.user?.id) {
|
|
12
|
+
return NextResponse.json({ success: false, error: { code: 'UNAUTHORIZED', message: 'Not authenticated' } }, { status: 401 });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const subscription = await prisma.subscription.findUnique({ where: { userId: session.user.id } });
|
|
16
|
+
if (!subscription?.stripeCustomerId) {
|
|
17
|
+
return NextResponse.json({ success: false, error: { code: 'NO_CUSTOMER', message: 'No billing account found' } }, { status: 400 });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const portalSession = await stripe.billingPortal.sessions.create({
|
|
21
|
+
customer: subscription.stripeCustomerId,
|
|
22
|
+
return_url: `${process.env.NEXTAUTH_URL}/settings/billing`,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return NextResponse.json({ success: true, data: { url: portalSession.url } });
|
|
26
|
+
} catch (err) {
|
|
27
|
+
return handleApiError(err);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { stripe } from '@/lib/stripe/client';
|
|
3
|
+
import { handleCheckoutComplete, handleSubscriptionUpdated, handleSubscriptionDeleted, handlePaymentFailed } from '@/lib/stripe/webhooks';
|
|
4
|
+
import { logger } from '@/lib/utils/logger';
|
|
5
|
+
|
|
6
|
+
export async function POST(req: NextRequest) {
|
|
7
|
+
const body = await req.text();
|
|
8
|
+
const signature = req.headers.get('stripe-signature');
|
|
9
|
+
|
|
10
|
+
if (!signature) {
|
|
11
|
+
return NextResponse.json({ error: 'Missing signature' }, { status: 400 });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let event;
|
|
15
|
+
try {
|
|
16
|
+
event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET ?? '');
|
|
17
|
+
} catch (err) {
|
|
18
|
+
logger.error('Webhook signature verification failed', { error: (err as Error).message });
|
|
19
|
+
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
switch (event.type) {
|
|
24
|
+
case 'checkout.session.completed':
|
|
25
|
+
await handleCheckoutComplete(event.data.object as import('stripe').Stripe.Checkout.Session);
|
|
26
|
+
break;
|
|
27
|
+
case 'customer.subscription.updated':
|
|
28
|
+
await handleSubscriptionUpdated(event.data.object as import('stripe').Stripe.Subscription);
|
|
29
|
+
break;
|
|
30
|
+
case 'customer.subscription.deleted':
|
|
31
|
+
await handleSubscriptionDeleted(event.data.object as import('stripe').Stripe.Subscription);
|
|
32
|
+
break;
|
|
33
|
+
case 'invoice.payment_failed':
|
|
34
|
+
await handlePaymentFailed(event.data.object as import('stripe').Stripe.Invoice);
|
|
35
|
+
break;
|
|
36
|
+
default:
|
|
37
|
+
logger.info('Unhandled webhook event', { type: event.type });
|
|
38
|
+
}
|
|
39
|
+
} catch (err) {
|
|
40
|
+
logger.error('Webhook handler error', { type: event.type, error: (err as Error).message });
|
|
41
|
+
return NextResponse.json({ error: 'Handler failed' }, { status: 500 });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return NextResponse.json({ received: true });
|
|
45
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { getServerSession } from 'next-auth';
|
|
3
|
+
import { authOptions } from '@/lib/auth/nextauth';
|
|
4
|
+
import { getUsageForPeriod } from '@/lib/stripe/usage';
|
|
5
|
+
import { getPlan } from '@/lib/stripe/plans';
|
|
6
|
+
import { handleApiError } from '@/lib/utils/api-response';
|
|
7
|
+
import prisma from '@/lib/db/client';
|
|
8
|
+
|
|
9
|
+
export async function GET() {
|
|
10
|
+
try {
|
|
11
|
+
const session = await getServerSession(authOptions);
|
|
12
|
+
if (!session?.user?.id) {
|
|
13
|
+
return NextResponse.json({ success: false, error: { code: 'UNAUTHORIZED', message: 'Not authenticated' } }, { status: 401 });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const usage = await getUsageForPeriod(session.user.id);
|
|
17
|
+
const subscription = await prisma.subscription.findUnique({ where: { userId: session.user.id } });
|
|
18
|
+
const planId = subscription?.stripePriceId ? 'pro' : 'free';
|
|
19
|
+
const plan = getPlan(planId);
|
|
20
|
+
const agentCount = await prisma.agent.count({ where: { userId: session.user.id } });
|
|
21
|
+
|
|
22
|
+
return NextResponse.json({
|
|
23
|
+
success: true,
|
|
24
|
+
data: {
|
|
25
|
+
planId,
|
|
26
|
+
usage: {
|
|
27
|
+
messages: usage.messages,
|
|
28
|
+
toolCalls: usage.toolCalls,
|
|
29
|
+
inputTokens: usage.inputTokens,
|
|
30
|
+
outputTokens: usage.outputTokens,
|
|
31
|
+
agents: agentCount,
|
|
32
|
+
},
|
|
33
|
+
limits: {
|
|
34
|
+
maxMessages: plan.features.maxMessages,
|
|
35
|
+
maxTokens: plan.features.maxTokens,
|
|
36
|
+
maxAgents: plan.features.maxAgents,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
} catch (err) {
|
|
41
|
+
return handleApiError(err);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
@layer base {
|
|
6
|
+
:root {
|
|
7
|
+
--background: 0 0% 100%;
|
|
8
|
+
--foreground: 222.2 84% 4.9%;
|
|
9
|
+
--card: 0 0% 100%;
|
|
10
|
+
--card-foreground: 222.2 84% 4.9%;
|
|
11
|
+
--popover: 0 0% 100%;
|
|
12
|
+
--popover-foreground: 222.2 84% 4.9%;
|
|
13
|
+
--primary: 221.2 83.2% 53.3%;
|
|
14
|
+
--primary-foreground: 210 40% 98%;
|
|
15
|
+
--secondary: 210 40% 96.1%;
|
|
16
|
+
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
17
|
+
--muted: 210 40% 96.1%;
|
|
18
|
+
--muted-foreground: 215.4 16.3% 46.9%;
|
|
19
|
+
--accent: 210 40% 96.1%;
|
|
20
|
+
--accent-foreground: 222.2 47.4% 11.2%;
|
|
21
|
+
--destructive: 0 84.2% 60.2%;
|
|
22
|
+
--destructive-foreground: 210 40% 98%;
|
|
23
|
+
--border: 214.3 31.8% 91.4%;
|
|
24
|
+
--input: 214.3 31.8% 91.4%;
|
|
25
|
+
--ring: 221.2 83.2% 53.3%;
|
|
26
|
+
--radius: 0.5rem;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.dark {
|
|
30
|
+
--background: 222.2 84% 4.9%;
|
|
31
|
+
--foreground: 210 40% 98%;
|
|
32
|
+
--card: 222.2 84% 4.9%;
|
|
33
|
+
--card-foreground: 210 40% 98%;
|
|
34
|
+
--popover: 222.2 84% 4.9%;
|
|
35
|
+
--popover-foreground: 210 40% 98%;
|
|
36
|
+
--primary: 217.2 91.2% 59.8%;
|
|
37
|
+
--primary-foreground: 222.2 47.4% 11.2%;
|
|
38
|
+
--secondary: 217.2 32.6% 17.5%;
|
|
39
|
+
--secondary-foreground: 210 40% 98%;
|
|
40
|
+
--muted: 217.2 32.6% 17.5%;
|
|
41
|
+
--muted-foreground: 215 20.2% 65.1%;
|
|
42
|
+
--accent: 217.2 32.6% 17.5%;
|
|
43
|
+
--accent-foreground: 210 40% 98%;
|
|
44
|
+
--destructive: 0 62.8% 30.6%;
|
|
45
|
+
--destructive-foreground: 210 40% 98%;
|
|
46
|
+
--border: 217.2 32.6% 17.5%;
|
|
47
|
+
--input: 217.2 32.6% 17.5%;
|
|
48
|
+
--ring: 224.3 76.3% 48%;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@layer base {
|
|
53
|
+
* {
|
|
54
|
+
@apply border-border;
|
|
55
|
+
}
|
|
56
|
+
body {
|
|
57
|
+
@apply bg-background text-foreground;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
import { Inter } from 'next/font/google';
|
|
3
|
+
import { Providers } from '@/components/auth-provider';
|
|
4
|
+
import './globals.css';
|
|
5
|
+
|
|
6
|
+
const inter = Inter({ subsets: ['latin'] });
|
|
7
|
+
|
|
8
|
+
export const metadata: Metadata = {
|
|
9
|
+
title: 'AgentKit — AI Agent Starter Kit',
|
|
10
|
+
description: 'Production-ready AI agent framework. Ship your AI SaaS this weekend with Next.js, streaming, tool calling, Stripe billing, and more.',
|
|
11
|
+
keywords: ['AI', 'agent', 'framework', 'Next.js', 'TypeScript', 'OpenAI', 'streaming', 'tool calling'],
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
15
|
+
return (
|
|
16
|
+
<html lang="en" suppressHydrationWarning>
|
|
17
|
+
<body className={inter.className}>
|
|
18
|
+
<Providers>{children}</Providers>
|
|
19
|
+
</body>
|
|
20
|
+
</html>
|
|
21
|
+
);
|
|
22
|
+
}
|
package/src/app/page.tsx
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Hero } from '@/components/landing/hero';
|
|
2
|
+
import { Features } from '@/components/landing/features';
|
|
3
|
+
import { LandingPricing } from '@/components/landing/pricing';
|
|
4
|
+
import { CTA } from '@/components/landing/cta';
|
|
5
|
+
|
|
6
|
+
export default function HomePage() {
|
|
7
|
+
return (
|
|
8
|
+
<main className="min-h-screen">
|
|
9
|
+
<nav className="flex items-center justify-between px-6 py-4 max-w-7xl mx-auto">
|
|
10
|
+
<div className="flex items-center gap-2 font-bold text-lg">
|
|
11
|
+
<span className="text-primary">🤖</span>
|
|
12
|
+
<span>AgentKit</span>
|
|
13
|
+
</div>
|
|
14
|
+
<div className="flex items-center gap-4">
|
|
15
|
+
<a href="#features" className="text-sm text-muted-foreground hover:text-foreground">Features</a>
|
|
16
|
+
<a href="#pricing" className="text-sm text-muted-foreground hover:text-foreground">Pricing</a>
|
|
17
|
+
<a href="/login" className="text-sm text-muted-foreground hover:text-foreground">Log in</a>
|
|
18
|
+
<a href="/signup" className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90">Sign up</a>
|
|
19
|
+
</div>
|
|
20
|
+
</nav>
|
|
21
|
+
<Hero />
|
|
22
|
+
<Features />
|
|
23
|
+
<LandingPricing />
|
|
24
|
+
<CTA />
|
|
25
|
+
<footer className="border-t py-8">
|
|
26
|
+
<div className="mx-auto max-w-7xl px-6 text-center text-sm text-muted-foreground">
|
|
27
|
+
<p>© {new Date().getFullYear()} AgentKit. Open source under MIT license.</p>
|
|
28
|
+
</div>
|
|
29
|
+
</footer>
|
|
30
|
+
</main>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { PricingTable } from '@/components/billing/pricing-table';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
|
|
6
|
+
export default function PricingPage() {
|
|
7
|
+
const router = useRouter();
|
|
8
|
+
return (
|
|
9
|
+
<div className="min-h-screen py-20">
|
|
10
|
+
<div className="mx-auto max-w-5xl px-6">
|
|
11
|
+
<div className="text-center mb-12">
|
|
12
|
+
<h1 className="text-4xl font-bold">Pricing</h1>
|
|
13
|
+
<p className="mt-4 text-lg text-muted-foreground">
|
|
14
|
+
Choose the plan that fits your needs. Start free, scale as you grow.
|
|
15
|
+
</p>
|
|
16
|
+
</div>
|
|
17
|
+
<PricingTable
|
|
18
|
+
onSelectPlan={() => {
|
|
19
|
+
router.push('/login');
|
|
20
|
+
}}
|
|
21
|
+
/>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { Button } from '@/components/ui/button';
|
|
5
|
+
import { Input } from '@/components/ui/input';
|
|
6
|
+
import { Textarea } from '@/components/ui/textarea';
|
|
7
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
8
|
+
import { Switch } from '@/components/ui/switch';
|
|
9
|
+
import { ModelSelector } from './model-selector';
|
|
10
|
+
import { ToolSelector } from './tool-selector';
|
|
11
|
+
import { AVAILABLE_MODELS } from '@/lib/ai/models';
|
|
12
|
+
|
|
13
|
+
interface AgentFormProps {
|
|
14
|
+
initialData?: {
|
|
15
|
+
name: string;
|
|
16
|
+
description: string;
|
|
17
|
+
systemPrompt: string;
|
|
18
|
+
model: string;
|
|
19
|
+
temperature: number;
|
|
20
|
+
maxTokens: number;
|
|
21
|
+
tools: string[];
|
|
22
|
+
isPublic: boolean;
|
|
23
|
+
};
|
|
24
|
+
onSubmit: (data: {
|
|
25
|
+
name: string;
|
|
26
|
+
description: string;
|
|
27
|
+
systemPrompt: string;
|
|
28
|
+
model: string;
|
|
29
|
+
temperature: number;
|
|
30
|
+
maxTokens: number;
|
|
31
|
+
tools: string[];
|
|
32
|
+
isPublic: boolean;
|
|
33
|
+
}) => Promise<void>;
|
|
34
|
+
isLoading?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function AgentForm({ initialData, onSubmit, isLoading }: AgentFormProps) {
|
|
38
|
+
const [name, setName] = useState(initialData?.name ?? '');
|
|
39
|
+
const [description, setDescription] = useState(initialData?.description ?? '');
|
|
40
|
+
const [systemPrompt, setSystemPrompt] = useState(initialData?.systemPrompt ?? '');
|
|
41
|
+
const [model, setModel] = useState(initialData?.model ?? AVAILABLE_MODELS[0]?.id ?? 'gpt-4o-mini');
|
|
42
|
+
const [temperature, setTemperature] = useState(initialData?.temperature ?? 0.7);
|
|
43
|
+
const [maxTokens, setMaxTokens] = useState(initialData?.maxTokens ?? 4096);
|
|
44
|
+
const [tools, setTools] = useState<string[]>(initialData?.tools ?? []);
|
|
45
|
+
const [isPublic, setIsPublic] = useState(initialData?.isPublic ?? false);
|
|
46
|
+
|
|
47
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
48
|
+
e.preventDefault();
|
|
49
|
+
await onSubmit({ name, description, systemPrompt, model, temperature, maxTokens, tools, isPublic });
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<form onSubmit={handleSubmit} className="space-y-6">
|
|
54
|
+
<Card>
|
|
55
|
+
<CardHeader>
|
|
56
|
+
<CardTitle>Basic Info</CardTitle>
|
|
57
|
+
</CardHeader>
|
|
58
|
+
<CardContent className="space-y-4">
|
|
59
|
+
<div className="space-y-2">
|
|
60
|
+
<label className="text-sm font-medium">Name</label>
|
|
61
|
+
<Input value={name} onChange={(e) => setName(e.target.value)} placeholder="My Agent" required />
|
|
62
|
+
</div>
|
|
63
|
+
<div className="space-y-2">
|
|
64
|
+
<label className="text-sm font-medium">Description</label>
|
|
65
|
+
<Input value={description} onChange={(e) => setDescription(e.target.value)} placeholder="What does this agent do?" />
|
|
66
|
+
</div>
|
|
67
|
+
<div className="space-y-2">
|
|
68
|
+
<label className="text-sm font-medium">System Prompt</label>
|
|
69
|
+
<Textarea
|
|
70
|
+
value={systemPrompt}
|
|
71
|
+
onChange={(e) => setSystemPrompt(e.target.value)}
|
|
72
|
+
placeholder="You are a helpful assistant that..."
|
|
73
|
+
rows={5}
|
|
74
|
+
required
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
77
|
+
<div className="flex items-center gap-3">
|
|
78
|
+
<Switch checked={isPublic} onCheckedChange={setIsPublic} />
|
|
79
|
+
<div>
|
|
80
|
+
<p className="text-sm font-medium">Public Agent</p>
|
|
81
|
+
<p className="text-xs text-muted-foreground">Allow others to use this agent</p>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</CardContent>
|
|
85
|
+
</Card>
|
|
86
|
+
|
|
87
|
+
<Card>
|
|
88
|
+
<CardHeader>
|
|
89
|
+
<CardTitle>Model Configuration</CardTitle>
|
|
90
|
+
</CardHeader>
|
|
91
|
+
<CardContent className="space-y-4">
|
|
92
|
+
<div className="space-y-2">
|
|
93
|
+
<label className="text-sm font-medium">Model</label>
|
|
94
|
+
<ModelSelector value={model} onChange={setModel} />
|
|
95
|
+
</div>
|
|
96
|
+
<div className="space-y-2">
|
|
97
|
+
<label className="text-sm font-medium">Temperature: {temperature}</label>
|
|
98
|
+
<input
|
|
99
|
+
type="range"
|
|
100
|
+
min="0"
|
|
101
|
+
max="2"
|
|
102
|
+
step="0.1"
|
|
103
|
+
value={temperature}
|
|
104
|
+
onChange={(e) => setTemperature(parseFloat(e.target.value))}
|
|
105
|
+
className="w-full"
|
|
106
|
+
/>
|
|
107
|
+
</div>
|
|
108
|
+
<div className="space-y-2">
|
|
109
|
+
<label className="text-sm font-medium">Max Tokens</label>
|
|
110
|
+
<Input
|
|
111
|
+
type="number"
|
|
112
|
+
value={maxTokens}
|
|
113
|
+
onChange={(e) => setMaxTokens(parseInt(e.target.value))}
|
|
114
|
+
min={256}
|
|
115
|
+
max={128000}
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
</CardContent>
|
|
119
|
+
</Card>
|
|
120
|
+
|
|
121
|
+
<Card>
|
|
122
|
+
<CardHeader>
|
|
123
|
+
<CardTitle>Tools</CardTitle>
|
|
124
|
+
</CardHeader>
|
|
125
|
+
<CardContent>
|
|
126
|
+
<ToolSelector selected={tools} onChange={setTools} />
|
|
127
|
+
</CardContent>
|
|
128
|
+
</Card>
|
|
129
|
+
|
|
130
|
+
<div className="flex justify-end gap-3">
|
|
131
|
+
<Button type="submit" disabled={isLoading}>
|
|
132
|
+
{isLoading ? 'Saving...' : initialData ? 'Update Agent' : 'Create Agent'}
|
|
133
|
+
</Button>
|
|
134
|
+
</div>
|
|
135
|
+
</form>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { AVAILABLE_MODELS, type ModelConfig } from '@/lib/ai/models';
|
|
4
|
+
import { cn } from '@/lib/utils/helpers';
|
|
5
|
+
|
|
6
|
+
interface ModelSelectorProps {
|
|
7
|
+
value: string;
|
|
8
|
+
onChange: (model: string) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function ModelSelector({ value, onChange }: ModelSelectorProps) {
|
|
12
|
+
return (
|
|
13
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
14
|
+
{AVAILABLE_MODELS.map((model) => (
|
|
15
|
+
<button
|
|
16
|
+
key={model.id}
|
|
17
|
+
type="button"
|
|
18
|
+
className={cn(
|
|
19
|
+
'flex flex-col items-start rounded-md border p-3 text-left transition-colors hover:bg-accent',
|
|
20
|
+
value === model.id && 'border-primary bg-accent',
|
|
21
|
+
)}
|
|
22
|
+
onClick={() => onChange(model.id)}
|
|
23
|
+
>
|
|
24
|
+
<span className="text-sm font-medium">{model.name}</span>
|
|
25
|
+
<span className="text-xs text-muted-foreground">{model.provider}</span>
|
|
26
|
+
<span className="text-xs text-muted-foreground mt-1">
|
|
27
|
+
{model.supportsTools && 'Tools • '}
|
|
28
|
+
{model.supportsStreaming && 'Streaming • '}
|
|
29
|
+
Up to {(model.maxTokens / 1000).toFixed(0)}K tokens
|
|
30
|
+
</span>
|
|
31
|
+
</button>
|
|
32
|
+
))}
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { getAllTools } from '@/lib/ai/tools';
|
|
4
|
+
import { cn } from '@/lib/utils/helpers';
|
|
5
|
+
|
|
6
|
+
interface ToolSelectorProps {
|
|
7
|
+
selected: string[];
|
|
8
|
+
onChange: (tools: string[]) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function ToolSelector({ selected, onChange }: ToolSelectorProps) {
|
|
12
|
+
const tools = getAllTools();
|
|
13
|
+
|
|
14
|
+
const toggleTool = (toolId: string) => {
|
|
15
|
+
if (selected.includes(toolId)) {
|
|
16
|
+
onChange(selected.filter((id) => id !== toolId));
|
|
17
|
+
} else {
|
|
18
|
+
onChange([...selected, toolId]);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
24
|
+
{tools.map((tool) => (
|
|
25
|
+
<button
|
|
26
|
+
key={tool.id}
|
|
27
|
+
type="button"
|
|
28
|
+
className={cn(
|
|
29
|
+
'flex flex-col items-start rounded-md border p-3 text-left transition-colors hover:bg-accent',
|
|
30
|
+
selected.includes(tool.id) && 'border-primary bg-accent',
|
|
31
|
+
)}
|
|
32
|
+
onClick={() => toggleTool(tool.id)}
|
|
33
|
+
>
|
|
34
|
+
<div className="flex items-center gap-2">
|
|
35
|
+
<div className={cn(
|
|
36
|
+
'h-4 w-4 rounded border flex items-center justify-center text-xs',
|
|
37
|
+
selected.includes(tool.id) && 'bg-primary text-primary-foreground border-primary',
|
|
38
|
+
)}>
|
|
39
|
+
{selected.includes(tool.id) && '✓'}
|
|
40
|
+
</div>
|
|
41
|
+
<span className="text-sm font-medium">{tool.name}</span>
|
|
42
|
+
</div>
|
|
43
|
+
<p className="text-xs text-muted-foreground mt-1 ml-6">{tool.description}</p>
|
|
44
|
+
</button>
|
|
45
|
+
))}
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { SessionProvider } from 'next-auth/react';
|
|
4
|
+
import { ThemeProvider } from 'next-themes';
|
|
5
|
+
import { Toaster } from 'react-hot-toast';
|
|
6
|
+
import { type ReactNode } from 'react';
|
|
7
|
+
|
|
8
|
+
export function Providers({ children }: { children: ReactNode }) {
|
|
9
|
+
return (
|
|
10
|
+
<SessionProvider>
|
|
11
|
+
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
|
|
12
|
+
{children}
|
|
13
|
+
<Toaster position="bottom-right" toastOptions={{ duration: 4000 }} />
|
|
14
|
+
</ThemeProvider>
|
|
15
|
+
</SessionProvider>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Badge } from '@/components/ui/badge';
|
|
2
|
+
import { cn } from '@/lib/utils/helpers';
|
|
3
|
+
|
|
4
|
+
interface PlanBadgeProps {
|
|
5
|
+
planId: string;
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const planStyles: Record<string, string> = {
|
|
10
|
+
free: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300',
|
|
11
|
+
pro: 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300',
|
|
12
|
+
team: 'bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function PlanBadge({ planId, className }: PlanBadgeProps) {
|
|
16
|
+
const name = planId.charAt(0).toUpperCase() + planId.slice(1);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<Badge className={cn(planStyles[planId] ?? planStyles.free, 'font-medium', className)}>
|
|
20
|
+
{name}
|
|
21
|
+
</Badge>
|
|
22
|
+
);
|
|
23
|
+
}
|