mirror of
https://github.com/leonvanzyl/agentic-coding-starter-kit.git
synced 2026-01-30 22:32:07 +00:00
npx command
This commit is contained in:
15
create-agentic-app/template/src/app/api/chat/route.ts
Normal file
15
create-agentic-app/template/src/app/api/chat/route.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import { streamText, UIMessage, convertToModelMessages } from "ai";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { messages }: { messages: UIMessage[] } = await req.json();
|
||||
|
||||
const result = streamText({
|
||||
model: openai(process.env.OPENAI_MODEL || "gpt-5-mini"),
|
||||
messages: convertToModelMessages(messages),
|
||||
});
|
||||
|
||||
return (
|
||||
result as unknown as { toUIMessageStreamResponse: () => Response }
|
||||
).toUIMessageStreamResponse();
|
||||
}
|
||||
126
create-agentic-app/template/src/app/api/diagnostics/route.ts
Normal file
126
create-agentic-app/template/src/app/api/diagnostics/route.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
type StatusLevel = "ok" | "warn" | "error";
|
||||
|
||||
interface DiagnosticsResponse {
|
||||
timestamp: string;
|
||||
env: {
|
||||
POSTGRES_URL: boolean;
|
||||
BETTER_AUTH_SECRET: boolean;
|
||||
GOOGLE_CLIENT_ID: boolean;
|
||||
GOOGLE_CLIENT_SECRET: boolean;
|
||||
OPENAI_API_KEY: boolean;
|
||||
NEXT_PUBLIC_APP_URL: boolean;
|
||||
};
|
||||
database: {
|
||||
connected: boolean;
|
||||
schemaApplied: boolean;
|
||||
error?: string;
|
||||
};
|
||||
auth: {
|
||||
configured: boolean;
|
||||
routeResponding: boolean | null;
|
||||
};
|
||||
ai: {
|
||||
configured: boolean;
|
||||
};
|
||||
overallStatus: StatusLevel;
|
||||
}
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const env = {
|
||||
POSTGRES_URL: Boolean(process.env.POSTGRES_URL),
|
||||
BETTER_AUTH_SECRET: Boolean(process.env.BETTER_AUTH_SECRET),
|
||||
GOOGLE_CLIENT_ID: Boolean(process.env.GOOGLE_CLIENT_ID),
|
||||
GOOGLE_CLIENT_SECRET: Boolean(process.env.GOOGLE_CLIENT_SECRET),
|
||||
OPENAI_API_KEY: Boolean(process.env.OPENAI_API_KEY),
|
||||
NEXT_PUBLIC_APP_URL: Boolean(process.env.NEXT_PUBLIC_APP_URL),
|
||||
} as const;
|
||||
|
||||
// Database checks
|
||||
let dbConnected = false;
|
||||
let schemaApplied = false;
|
||||
let dbError: string | undefined;
|
||||
if (env.POSTGRES_URL) {
|
||||
try {
|
||||
const [{ db }, { sql }, schema] = await Promise.all([
|
||||
import("@/lib/db"),
|
||||
import("drizzle-orm"),
|
||||
import("@/lib/schema"),
|
||||
]);
|
||||
// Ping DB
|
||||
await db.execute(sql`select 1`);
|
||||
dbConnected = true;
|
||||
try {
|
||||
// Touch a known table to verify migrations
|
||||
await db.select().from(schema.user).limit(1);
|
||||
schemaApplied = true;
|
||||
} catch {
|
||||
schemaApplied = false;
|
||||
}
|
||||
} catch (err) {
|
||||
dbConnected = false;
|
||||
dbError = err instanceof Error ? err.message : "Unknown database error";
|
||||
}
|
||||
} else {
|
||||
dbConnected = false;
|
||||
schemaApplied = false;
|
||||
dbError = "POSTGRES_URL is not set";
|
||||
}
|
||||
|
||||
// Auth route check: we consider the route responding if it returns any HTTP response
|
||||
// for /api/auth/session (status codes in the 2xx-4xx range are acceptable for readiness)
|
||||
const origin = (() => {
|
||||
try {
|
||||
return new URL(req.url).origin;
|
||||
} catch {
|
||||
return process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
|
||||
}
|
||||
})();
|
||||
|
||||
let authRouteResponding: boolean | null = null;
|
||||
try {
|
||||
const res = await fetch(`${origin}/api/auth/session`, {
|
||||
method: "GET",
|
||||
headers: { Accept: "application/json" },
|
||||
cache: "no-store",
|
||||
});
|
||||
authRouteResponding = res.status >= 200 && res.status < 500;
|
||||
} catch {
|
||||
authRouteResponding = false;
|
||||
}
|
||||
|
||||
const authConfigured =
|
||||
env.BETTER_AUTH_SECRET && env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET;
|
||||
const aiConfigured = env.OPENAI_API_KEY; // We avoid live-calling the AI provider here
|
||||
|
||||
const overallStatus: StatusLevel = (() => {
|
||||
if (!env.POSTGRES_URL || !dbConnected || !schemaApplied) return "error";
|
||||
if (!authConfigured) return "error";
|
||||
// AI is optional; warn if not configured
|
||||
if (!aiConfigured) return "warn";
|
||||
return "ok";
|
||||
})();
|
||||
|
||||
const body: DiagnosticsResponse = {
|
||||
timestamp: new Date().toISOString(),
|
||||
env,
|
||||
database: {
|
||||
connected: dbConnected,
|
||||
schemaApplied,
|
||||
error: dbError,
|
||||
},
|
||||
auth: {
|
||||
configured: authConfigured,
|
||||
routeResponding: authRouteResponding,
|
||||
},
|
||||
ai: {
|
||||
configured: aiConfigured,
|
||||
},
|
||||
overallStatus,
|
||||
};
|
||||
|
||||
return NextResponse.json(body, {
|
||||
status: 200,
|
||||
});
|
||||
}
|
||||
207
create-agentic-app/template/src/app/chat/page.tsx
Normal file
207
create-agentic-app/template/src/app/chat/page.tsx
Normal file
@@ -0,0 +1,207 @@
|
||||
"use client";
|
||||
|
||||
import { useChat } from "@ai-sdk/react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { UserProfile } from "@/components/auth/user-profile";
|
||||
import { useSession } from "@/lib/auth-client";
|
||||
import { useState, type ReactNode } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import type { Components } from "react-markdown";
|
||||
|
||||
const H1: React.FC<React.HTMLAttributes<HTMLHeadingElement>> = (props) => (
|
||||
<h1 className="mt-2 mb-3 text-2xl font-bold" {...props} />
|
||||
);
|
||||
const H2: React.FC<React.HTMLAttributes<HTMLHeadingElement>> = (props) => (
|
||||
<h2 className="mt-2 mb-2 text-xl font-semibold" {...props} />
|
||||
);
|
||||
const H3: React.FC<React.HTMLAttributes<HTMLHeadingElement>> = (props) => (
|
||||
<h3 className="mt-2 mb-2 text-lg font-semibold" {...props} />
|
||||
);
|
||||
const Paragraph: React.FC<React.HTMLAttributes<HTMLParagraphElement>> = (
|
||||
props
|
||||
) => <p className="mb-3 leading-7 text-sm" {...props} />;
|
||||
const UL: React.FC<React.HTMLAttributes<HTMLUListElement>> = (props) => (
|
||||
<ul className="mb-3 ml-5 list-disc space-y-1 text-sm" {...props} />
|
||||
);
|
||||
const OL: React.FC<React.OlHTMLAttributes<HTMLOListElement>> = (props) => (
|
||||
<ol className="mb-3 ml-5 list-decimal space-y-1 text-sm" {...props} />
|
||||
);
|
||||
const LI: React.FC<React.LiHTMLAttributes<HTMLLIElement>> = (props) => (
|
||||
<li className="leading-6" {...props} />
|
||||
);
|
||||
const Anchor: React.FC<React.AnchorHTMLAttributes<HTMLAnchorElement>> = (
|
||||
props
|
||||
) => (
|
||||
<a
|
||||
className="underline underline-offset-2 text-primary hover:opacity-90"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
const Blockquote: React.FC<React.BlockquoteHTMLAttributes<HTMLElement>> = (
|
||||
props
|
||||
) => (
|
||||
<blockquote
|
||||
className="mb-3 border-l-2 border-border pl-3 text-muted-foreground"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
const Code: Components["code"] = ({ children, className, ...props }) => {
|
||||
const match = /language-(\w+)/.exec(className || "");
|
||||
const isInline = !match;
|
||||
|
||||
if (isInline) {
|
||||
return (
|
||||
<code className="rounded bg-muted px-1 py-0.5 text-xs" {...props}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<pre className="mb-3 w-full overflow-x-auto rounded-md bg-muted p-3">
|
||||
<code className="text-xs leading-5" {...props}>
|
||||
{children}
|
||||
</code>
|
||||
</pre>
|
||||
);
|
||||
};
|
||||
const HR: React.FC<React.HTMLAttributes<HTMLHRElement>> = (props) => (
|
||||
<hr className="my-4 border-border" {...props} />
|
||||
);
|
||||
const Table: React.FC<React.TableHTMLAttributes<HTMLTableElement>> = (
|
||||
props
|
||||
) => (
|
||||
<div className="mb-3 overflow-x-auto">
|
||||
<table className="w-full border-collapse text-sm" {...props} />
|
||||
</div>
|
||||
);
|
||||
const TH: React.FC<React.ThHTMLAttributes<HTMLTableCellElement>> = (props) => (
|
||||
<th
|
||||
className="border border-border bg-muted px-2 py-1 text-left"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
const TD: React.FC<React.TdHTMLAttributes<HTMLTableCellElement>> = (props) => (
|
||||
<td className="border border-border px-2 py-1" {...props} />
|
||||
);
|
||||
|
||||
const markdownComponents: Components = {
|
||||
h1: H1,
|
||||
h2: H2,
|
||||
h3: H3,
|
||||
p: Paragraph,
|
||||
ul: UL,
|
||||
ol: OL,
|
||||
li: LI,
|
||||
a: Anchor,
|
||||
blockquote: Blockquote,
|
||||
code: Code,
|
||||
hr: HR,
|
||||
table: Table,
|
||||
th: TH,
|
||||
td: TD,
|
||||
};
|
||||
|
||||
type TextPart = { type?: string; text?: string };
|
||||
type MaybePartsMessage = {
|
||||
display?: ReactNode;
|
||||
parts?: TextPart[];
|
||||
content?: TextPart[];
|
||||
};
|
||||
|
||||
function renderMessageContent(message: MaybePartsMessage): ReactNode {
|
||||
if (message.display) return message.display;
|
||||
const parts = Array.isArray(message.parts)
|
||||
? message.parts
|
||||
: Array.isArray(message.content)
|
||||
? message.content
|
||||
: [];
|
||||
return parts.map((p, idx) =>
|
||||
p?.type === "text" && p.text ? (
|
||||
<ReactMarkdown key={idx} components={markdownComponents}>
|
||||
{p.text}
|
||||
</ReactMarkdown>
|
||||
) : null
|
||||
);
|
||||
}
|
||||
|
||||
export default function ChatPage() {
|
||||
const { data: session, isPending } = useSession();
|
||||
const { messages, sendMessage, status } = useChat();
|
||||
const [input, setInput] = useState("");
|
||||
|
||||
if (isPending) {
|
||||
return <div className="container mx-auto px-4 py-12">Loading...</div>;
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-12">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<UserProfile />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="flex justify-between items-center mb-6 pb-4 border-b">
|
||||
<h1 className="text-2xl font-bold">AI Chat</h1>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Welcome, {session.user.name}!
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="min-h-[50vh] overflow-y-auto space-y-4 mb-4">
|
||||
{messages.length === 0 && (
|
||||
<div className="text-center text-muted-foreground">
|
||||
Start a conversation with AI
|
||||
</div>
|
||||
)}
|
||||
{messages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className={`p-3 rounded-lg ${
|
||||
message.role === "user"
|
||||
? "bg-primary text-primary-foreground ml-auto max-w-[80%]"
|
||||
: "bg-muted max-w-[80%]"
|
||||
}`}
|
||||
>
|
||||
<div className="text-sm font-medium mb-1">
|
||||
{message.role === "user" ? "You" : "AI"}
|
||||
</div>
|
||||
<div>{renderMessageContent(message as MaybePartsMessage)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const text = input.trim();
|
||||
if (!text) return;
|
||||
sendMessage({ role: "user", parts: [{ type: "text", text }] });
|
||||
setInput("");
|
||||
}}
|
||||
className="flex gap-2"
|
||||
>
|
||||
<input
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
placeholder="Type your message..."
|
||||
className="flex-1 p-2 border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!input.trim() || status === "streaming"}
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
79
create-agentic-app/template/src/app/dashboard/page.tsx
Normal file
79
create-agentic-app/template/src/app/dashboard/page.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
"use client";
|
||||
|
||||
import { useSession } from "@/lib/auth-client";
|
||||
import { UserProfile } from "@/components/auth/user-profile";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Lock } from "lucide-react";
|
||||
import { useDiagnostics } from "@/hooks/use-diagnostics";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { data: session, isPending } = useSession();
|
||||
const { isAiReady, loading: diagnosticsLoading } = useDiagnostics();
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-screen">
|
||||
Loading...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-12">
|
||||
<div className="max-w-3xl mx-auto text-center">
|
||||
<div className="mb-8">
|
||||
<Lock className="w-16 h-16 mx-auto mb-4 text-muted-foreground" />
|
||||
<h1 className="text-2xl font-bold mb-2">Protected Page</h1>
|
||||
<p className="text-muted-foreground mb-6">
|
||||
You need to sign in to access the dashboard
|
||||
</p>
|
||||
</div>
|
||||
<UserProfile />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-6">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h1 className="text-3xl font-bold">Dashboard</h1>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="p-6 border border-border rounded-lg">
|
||||
<h2 className="text-xl font-semibold mb-2">AI Chat</h2>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Start a conversation with AI using the Vercel AI SDK
|
||||
</p>
|
||||
{(diagnosticsLoading || !isAiReady) ? (
|
||||
<Button disabled={true}>
|
||||
Go to Chat
|
||||
</Button>
|
||||
) : (
|
||||
<Button asChild>
|
||||
<Link href="/chat">Go to Chat</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-6 border border-border rounded-lg">
|
||||
<h2 className="text-xl font-semibold mb-2">Profile</h2>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Manage your account settings and preferences
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<p>
|
||||
<strong>Name:</strong> {session.user.name}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Email:</strong> {session.user.email}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
BIN
create-agentic-app/template/src/app/favicon.ico
Normal file
BIN
create-agentic-app/template/src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
117
create-agentic-app/template/src/app/globals.css
Normal file
117
create-agentic-app/template/src/app/globals.css
Normal file
@@ -0,0 +1,117 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.21 0.006 285.885);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.705 0.015 286.067);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.21 0.006 285.885);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.705 0.015 286.067);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.92 0.004 286.32);
|
||||
--primary-foreground: oklch(0.21 0.006 285.885);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.552 0.016 285.938);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.552 0.016 285.938);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
47
create-agentic-app/template/src/app/layout.tsx
Normal file
47
create-agentic-app/template/src/app/layout.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { SiteHeader } from "@/components/site-header";
|
||||
import { SiteFooter } from "@/components/site-footer";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Agentic Coding Boilerplate",
|
||||
description:
|
||||
"Complete agentic coding boilerplate with authentication, database, AI integration, and modern tooling - perfect for building AI-powered applications and autonomous agents by Leon van Zyl",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<SiteHeader />
|
||||
{children}
|
||||
<SiteFooter />
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
174
create-agentic-app/template/src/app/page.tsx
Normal file
174
create-agentic-app/template/src/app/page.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { SetupChecklist } from "@/components/setup-checklist";
|
||||
import { useDiagnostics } from "@/hooks/use-diagnostics";
|
||||
import { StarterPromptModal } from "@/components/starter-prompt-modal";
|
||||
import { Video, Shield, Database, Palette, Bot } from "lucide-react";
|
||||
|
||||
export default function Home() {
|
||||
const { isAuthReady, isAiReady, loading } = useDiagnostics();
|
||||
return (
|
||||
<main className="flex-1 container mx-auto px-4 py-12">
|
||||
<div className="max-w-4xl mx-auto text-center space-y-8">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-center gap-3 mb-2">
|
||||
<div className="flex items-center justify-center w-12 h-12 rounded-xl bg-primary/10">
|
||||
<Bot className="h-7 w-7 text-primary" />
|
||||
</div>
|
||||
<h1 className="text-5xl font-bold tracking-tight bg-gradient-to-r from-primary via-primary/90 to-primary/70 bg-clip-text text-transparent">
|
||||
Starter Kit
|
||||
</h1>
|
||||
</div>
|
||||
<h2 className="text-2xl font-semibold text-muted-foreground">
|
||||
Complete Boilerplate for AI Applications
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground">
|
||||
A complete agentic coding boilerplate with authentication, database, AI
|
||||
integration, and modern tooling for building AI-powered applications
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* YouTube Tutorial Video */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-2xl font-semibold flex items-center justify-center gap-2">
|
||||
<Video className="h-6 w-6" />
|
||||
Video Tutorial
|
||||
</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Watch the complete walkthrough of this agentic coding boilerplate:
|
||||
</p>
|
||||
<div className="relative w-full max-w-3xl mx-auto">
|
||||
<div className="relative pb-[56.25%] h-0 overflow-hidden rounded-lg border">
|
||||
<iframe
|
||||
className="absolute top-0 left-0 w-full h-full"
|
||||
src="https://www.youtube.com/embed/T0zFZsr_d0Q"
|
||||
title="Agentic Coding Boilerplate Tutorial"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mt-12">
|
||||
<div className="p-6 border rounded-lg">
|
||||
<h3 className="font-semibold mb-2 flex items-center gap-2">
|
||||
<Shield className="h-4 w-4" />
|
||||
Authentication
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Better Auth with Google OAuth integration
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 border rounded-lg">
|
||||
<h3 className="font-semibold mb-2 flex items-center gap-2">
|
||||
<Database className="h-4 w-4" />
|
||||
Database
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Drizzle ORM with PostgreSQL setup
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 border rounded-lg">
|
||||
<h3 className="font-semibold mb-2 flex items-center gap-2">
|
||||
<Bot className="h-4 w-4" />
|
||||
AI Ready
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Vercel AI SDK with OpenAI integration
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 border rounded-lg">
|
||||
<h3 className="font-semibold mb-2 flex items-center gap-2">
|
||||
<Palette className="h-4 w-4" />
|
||||
UI Components
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
shadcn/ui with Tailwind CSS
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 mt-12">
|
||||
<SetupChecklist />
|
||||
|
||||
<h3 className="text-2xl font-semibold">Next Steps</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-left">
|
||||
<div className="p-4 border rounded-lg">
|
||||
<h4 className="font-medium mb-2">
|
||||
1. Set up environment variables
|
||||
</h4>
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
Copy <code>.env.example</code> to <code>.env.local</code> and
|
||||
configure:
|
||||
</p>
|
||||
<ul className="text-sm text-muted-foreground space-y-1 list-disc list-inside">
|
||||
<li>POSTGRES_URL (PostgreSQL connection string)</li>
|
||||
<li>GOOGLE_CLIENT_ID (OAuth credentials)</li>
|
||||
<li>GOOGLE_CLIENT_SECRET (OAuth credentials)</li>
|
||||
<li>OPENAI_API_KEY (for AI functionality)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="p-4 border rounded-lg">
|
||||
<h4 className="font-medium mb-2">2. Set up your database</h4>
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
Run database migrations:
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<code className="text-sm bg-muted p-2 rounded block">
|
||||
npm run db:generate
|
||||
</code>
|
||||
<code className="text-sm bg-muted p-2 rounded block">
|
||||
npm run db:migrate
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 border rounded-lg">
|
||||
<h4 className="font-medium mb-2">3. Try the features</h4>
|
||||
<div className="space-y-2">
|
||||
{loading || !isAuthReady ? (
|
||||
<Button size="sm" className="w-full" disabled={true}>
|
||||
View Dashboard
|
||||
</Button>
|
||||
) : (
|
||||
<Button asChild size="sm" className="w-full">
|
||||
<Link href="/dashboard">View Dashboard</Link>
|
||||
</Button>
|
||||
)}
|
||||
{loading || !isAiReady ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
disabled={true}
|
||||
>
|
||||
Try AI Chat
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
>
|
||||
<Link href="/chat">Try AI Chat</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 border rounded-lg">
|
||||
<h4 className="font-medium mb-2">4. Start building</h4>
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
Customize the components, add your own pages, and build your
|
||||
application on top of this solid foundation.
|
||||
</p>
|
||||
<StarterPromptModal />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
222
create-agentic-app/template/src/app/profile/page.tsx
Normal file
222
create-agentic-app/template/src/app/profile/page.tsx
Normal file
@@ -0,0 +1,222 @@
|
||||
"use client";
|
||||
|
||||
import { useSession } from "@/lib/auth-client";
|
||||
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Mail, Calendar, User, Shield, ArrowLeft } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function ProfilePage() {
|
||||
const { data: session, isPending } = useSession();
|
||||
const router = useRouter();
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div>Loading...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
router.push("/");
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = session.user;
|
||||
const createdDate = user.createdAt ? new Date(user.createdAt).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
}) : null;
|
||||
|
||||
return (
|
||||
<div className="container max-w-4xl mx-auto py-8 px-4">
|
||||
<div className="flex items-center gap-4 mb-8">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => router.back()}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Back
|
||||
</Button>
|
||||
<h1 className="text-3xl font-bold">Your Profile</h1>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6">
|
||||
{/* Profile Overview Card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center space-x-4">
|
||||
<Avatar className="h-20 w-20">
|
||||
<AvatarImage
|
||||
src={user.image || ""}
|
||||
alt={user.name || "User"}
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
<AvatarFallback className="text-lg">
|
||||
{(
|
||||
user.name?.[0] ||
|
||||
user.email?.[0] ||
|
||||
"U"
|
||||
).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-2xl font-semibold">{user.name}</h2>
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Mail className="h-4 w-4" />
|
||||
<span>{user.email}</span>
|
||||
{user.emailVerified && (
|
||||
<Badge variant="outline" className="text-green-600 border-green-600">
|
||||
<Shield className="h-3 w-3 mr-1" />
|
||||
Verified
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{createdDate && (
|
||||
<div className="flex items-center gap-2 text-muted-foreground text-sm">
|
||||
<Calendar className="h-4 w-4" />
|
||||
<span>Member since {createdDate}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
{/* Account Information */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Account Information</CardTitle>
|
||||
<CardDescription>
|
||||
Your account details and settings
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-muted-foreground">
|
||||
Full Name
|
||||
</label>
|
||||
<div className="p-3 border rounded-md bg-muted/10">
|
||||
{user.name || "Not provided"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-muted-foreground">
|
||||
Email Address
|
||||
</label>
|
||||
<div className="p-3 border rounded-md bg-muted/10 flex items-center justify-between">
|
||||
<span>{user.email}</span>
|
||||
{user.emailVerified && (
|
||||
<Badge variant="outline" className="text-green-600 border-green-600">
|
||||
Verified
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium">Account Status</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="flex items-center justify-between p-4 border rounded-lg">
|
||||
<div className="space-y-1">
|
||||
<p className="font-medium">Email Verification</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Email address verification status
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant={user.emailVerified ? "default" : "secondary"}>
|
||||
{user.emailVerified ? "Verified" : "Unverified"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-4 border rounded-lg">
|
||||
<div className="space-y-1">
|
||||
<p className="font-medium">Account Type</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Your account access level
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant="outline">Standard</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Account Activity */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Recent Activity</CardTitle>
|
||||
<CardDescription>
|
||||
Your recent account activity and sessions
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-4 border rounded-lg">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="h-2 w-2 bg-green-500 rounded-full"></div>
|
||||
<div>
|
||||
<p className="font-medium">Current Session</p>
|
||||
<p className="text-sm text-muted-foreground">Active now</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-green-600 border-green-600">
|
||||
Active
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Quick Actions</CardTitle>
|
||||
<CardDescription>
|
||||
Manage your account settings and preferences
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Button variant="outline" className="justify-start h-auto p-4" disabled>
|
||||
<User className="h-4 w-4 mr-2" />
|
||||
<div className="text-left">
|
||||
<div className="font-medium">Edit Profile</div>
|
||||
<div className="text-xs text-muted-foreground">Update your information</div>
|
||||
</div>
|
||||
</Button>
|
||||
<Button variant="outline" className="justify-start h-auto p-4" disabled>
|
||||
<Shield className="h-4 w-4 mr-2" />
|
||||
<div className="text-left">
|
||||
<div className="font-medium">Security Settings</div>
|
||||
<div className="text-xs text-muted-foreground">Manage security options</div>
|
||||
</div>
|
||||
</Button>
|
||||
<Button variant="outline" className="justify-start h-auto p-4" disabled>
|
||||
<Mail className="h-4 w-4 mr-2" />
|
||||
<div className="text-left">
|
||||
<div className="font-medium">Email Preferences</div>
|
||||
<div className="text-xs text-muted-foreground">Configure notifications</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-4">
|
||||
Additional profile management features coming soon.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import { signIn, useSession } from "@/lib/auth-client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export function SignInButton() {
|
||||
const { data: session, isPending } = useSession();
|
||||
|
||||
if (isPending) {
|
||||
return <Button disabled>Loading...</Button>;
|
||||
}
|
||||
|
||||
if (session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={async () => {
|
||||
await signIn.social({
|
||||
provider: "google",
|
||||
callbackURL: "/dashboard",
|
||||
});
|
||||
}}
|
||||
>
|
||||
Sign in
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import { signOut, useSession } from "@/lib/auth-client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export function SignOutButton() {
|
||||
const { data: session, isPending } = useSession();
|
||||
const router = useRouter();
|
||||
|
||||
if (isPending) {
|
||||
return <Button disabled>Loading...</Button>;
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={async () => {
|
||||
await signOut();
|
||||
router.replace("/");
|
||||
router.refresh();
|
||||
}}
|
||||
>
|
||||
Sign out
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
"use client";
|
||||
|
||||
import { useSession, signOut } from "@/lib/auth-client";
|
||||
import { SignInButton } from "./sign-in-button";
|
||||
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { User, LogOut } from "lucide-react";
|
||||
|
||||
export function UserProfile() {
|
||||
const { data: session, isPending } = useSession();
|
||||
const router = useRouter();
|
||||
|
||||
if (isPending) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-4 p-6">
|
||||
<SignInButton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const handleSignOut = async () => {
|
||||
await signOut();
|
||||
router.replace("/");
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Avatar className="size-8 cursor-pointer hover:opacity-80 transition-opacity">
|
||||
<AvatarImage
|
||||
src={session.user?.image || ""}
|
||||
alt={session.user?.name || "User"}
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
<AvatarFallback>
|
||||
{(
|
||||
session.user?.name?.[0] ||
|
||||
session.user?.email?.[0] ||
|
||||
"U"
|
||||
).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
<DropdownMenuLabel className="font-normal">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-sm font-medium leading-none">
|
||||
{session.user?.name}
|
||||
</p>
|
||||
<p className="text-xs leading-none text-muted-foreground">
|
||||
{session.user?.email}
|
||||
</p>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/profile" className="flex items-center">
|
||||
<User className="mr-2 h-4 w-4" />
|
||||
Your Profile
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={handleSignOut} variant="destructive">
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
Log out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
148
create-agentic-app/template/src/components/setup-checklist.tsx
Normal file
148
create-agentic-app/template/src/components/setup-checklist.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CheckCircle2, XCircle } from "lucide-react";
|
||||
|
||||
type DiagnosticsResponse = {
|
||||
timestamp: string;
|
||||
env: {
|
||||
POSTGRES_URL: boolean;
|
||||
BETTER_AUTH_SECRET: boolean;
|
||||
GOOGLE_CLIENT_ID: boolean;
|
||||
GOOGLE_CLIENT_SECRET: boolean;
|
||||
OPENAI_API_KEY: boolean;
|
||||
NEXT_PUBLIC_APP_URL: boolean;
|
||||
};
|
||||
database: {
|
||||
connected: boolean;
|
||||
schemaApplied: boolean;
|
||||
error?: string;
|
||||
};
|
||||
auth: {
|
||||
configured: boolean;
|
||||
routeResponding: boolean | null;
|
||||
};
|
||||
ai: {
|
||||
configured: boolean;
|
||||
};
|
||||
overallStatus: "ok" | "warn" | "error";
|
||||
};
|
||||
|
||||
function StatusIcon({ ok }: { ok: boolean }) {
|
||||
return ok ? (
|
||||
<div title="ok">
|
||||
<CheckCircle2 className="h-4 w-4 text-green-600" aria-label="ok" />
|
||||
</div>
|
||||
) : (
|
||||
<div title="not ok">
|
||||
<XCircle className="h-4 w-4 text-red-600" aria-label="not-ok" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SetupChecklist() {
|
||||
const [data, setData] = useState<DiagnosticsResponse | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
async function load() {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await fetch("/api/diagnostics", { cache: "no-store" });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const json = (await res.json()) as DiagnosticsResponse;
|
||||
setData(json);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Failed to load diagnostics");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, []);
|
||||
|
||||
const steps = [
|
||||
{
|
||||
key: "env",
|
||||
label: "Environment variables",
|
||||
ok:
|
||||
!!data?.env.POSTGRES_URL &&
|
||||
!!data?.env.BETTER_AUTH_SECRET &&
|
||||
!!data?.env.GOOGLE_CLIENT_ID &&
|
||||
!!data?.env.GOOGLE_CLIENT_SECRET,
|
||||
detail:
|
||||
"Requires POSTGRES_URL, BETTER_AUTH_SECRET, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET",
|
||||
},
|
||||
{
|
||||
key: "db",
|
||||
label: "Database connected & schema",
|
||||
ok: !!data?.database.connected && !!data?.database.schemaApplied,
|
||||
detail: data?.database.error
|
||||
? `Error: ${data.database.error}`
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
key: "auth",
|
||||
label: "Auth configured",
|
||||
ok: !!data?.auth.configured,
|
||||
detail:
|
||||
data?.auth.routeResponding === false
|
||||
? "Auth route not responding"
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
key: "ai",
|
||||
label: "AI integration (optional)",
|
||||
ok: !!data?.ai.configured,
|
||||
detail: !data?.ai.configured
|
||||
? "Set OPENAI_API_KEY for AI chat"
|
||||
: undefined,
|
||||
},
|
||||
] as const;
|
||||
|
||||
const completed = steps.filter((s) => s.ok).length;
|
||||
|
||||
return (
|
||||
<div className="p-6 border rounded-lg text-left">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="font-semibold">Setup checklist</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{completed}/{steps.length} completed
|
||||
</p>
|
||||
</div>
|
||||
<Button size="sm" onClick={load} disabled={loading}>
|
||||
{loading ? "Checking..." : "Re-check"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{error ? <div className="text-sm text-destructive">{error}</div> : null}
|
||||
|
||||
<ul className="space-y-2">
|
||||
{steps.map((s) => (
|
||||
<li key={s.key} className="flex items-start gap-2">
|
||||
<div className="mt-0.5">
|
||||
<StatusIcon ok={Boolean(s.ok)} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium">{s.label}</div>
|
||||
{s.detail ? (
|
||||
<div className="text-sm text-muted-foreground">{s.detail}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{data ? (
|
||||
<div className="mt-4 text-xs text-muted-foreground">
|
||||
Last checked: {new Date(data.timestamp).toLocaleString()}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
24
create-agentic-app/template/src/components/site-footer.tsx
Normal file
24
create-agentic-app/template/src/components/site-footer.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { GitHubStars } from "./ui/github-stars";
|
||||
|
||||
export function SiteFooter() {
|
||||
return (
|
||||
<footer className="border-t py-6 text-center text-sm text-muted-foreground">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="flex flex-col items-center space-y-3">
|
||||
<GitHubStars repo="leonvanzyl/agentic-coding-starter-kit" />
|
||||
<p>
|
||||
Built using Agentic Coding Boilerplate by{" "}
|
||||
<a
|
||||
href="https://youtube.com/@leonvanzyl"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Leon van Zyl
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
30
create-agentic-app/template/src/components/site-header.tsx
Normal file
30
create-agentic-app/template/src/components/site-header.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import Link from "next/link";
|
||||
import { UserProfile } from "@/components/auth/user-profile";
|
||||
import { ModeToggle } from "./ui/mode-toggle";
|
||||
import { Bot } from "lucide-react";
|
||||
|
||||
export function SiteHeader() {
|
||||
return (
|
||||
<header className="border-b">
|
||||
<div className="container mx-auto px-4 py-4 flex justify-between items-center">
|
||||
<h1 className="text-2xl font-bold">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center gap-2 text-primary hover:text-primary/80 transition-colors"
|
||||
>
|
||||
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-primary/10">
|
||||
<Bot className="h-5 w-5" />
|
||||
</div>
|
||||
<span className="bg-gradient-to-r from-primary to-primary/70 bg-clip-text text-transparent">
|
||||
Starter Kit
|
||||
</span>
|
||||
</Link>
|
||||
</h1>
|
||||
<div className="flex items-center gap-4">
|
||||
<UserProfile />
|
||||
<ModeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Copy, Check } from "lucide-react";
|
||||
|
||||
const STARTER_PROMPT = `I'm working with an agentic coding boilerplate project that includes authentication, database integration, and AI capabilities. Here's what's already set up:
|
||||
|
||||
## Current Agentic Coding Boilerplate Structure
|
||||
- **Authentication**: Better Auth with Google OAuth integration
|
||||
- **Database**: Drizzle ORM with PostgreSQL setup
|
||||
- **AI Integration**: Vercel AI SDK with OpenAI integration
|
||||
- **UI**: shadcn/ui components with Tailwind CSS
|
||||
- **Current Routes**:
|
||||
- \`/\` - Home page with setup instructions and feature overview
|
||||
- \`/dashboard\` - Protected dashboard page (requires authentication)
|
||||
- \`/chat\` - AI chat interface (requires OpenAI API key)
|
||||
|
||||
## Important Context
|
||||
This is an **agentic coding boilerplate/starter template** - all existing pages and components are meant to be examples and should be **completely replaced** to build the actual AI-powered application.
|
||||
|
||||
### CRITICAL: You MUST Override All Boilerplate Content
|
||||
**DO NOT keep any boilerplate components, text, or UI elements unless explicitly requested.** This includes:
|
||||
|
||||
- **Remove all placeholder/demo content** (setup checklists, welcome messages, boilerplate text)
|
||||
- **Replace the entire navigation structure** - don't keep the existing site header or nav items
|
||||
- **Override all page content completely** - don't append to existing pages, replace them entirely
|
||||
- **Remove or replace all example components** (setup-checklist, starter-prompt-modal, etc.)
|
||||
- **Replace placeholder routes and pages** with the actual application functionality
|
||||
|
||||
### Required Actions:
|
||||
1. **Start Fresh**: Treat existing components as temporary scaffolding to be removed
|
||||
2. **Complete Replacement**: Build the new application from scratch using the existing tech stack
|
||||
3. **No Hybrid Approach**: Don't try to integrate new features alongside existing boilerplate content
|
||||
4. **Clean Slate**: The final application should have NO trace of the original boilerplate UI or content
|
||||
|
||||
The only things to preserve are:
|
||||
- **All installed libraries and dependencies** (DO NOT uninstall or remove any packages from package.json)
|
||||
- **Authentication system** (but customize the UI/flow as needed)
|
||||
- **Database setup and schema** (but modify schema as needed for your use case)
|
||||
- **Core configuration files** (next.config.ts, tsconfig.json, tailwind.config.ts, etc.)
|
||||
- **Build and development scripts** (keep all npm/pnpm scripts in package.json)
|
||||
|
||||
## Tech Stack
|
||||
- Next.js 15 with App Router
|
||||
- TypeScript
|
||||
- Tailwind CSS
|
||||
- Better Auth for authentication
|
||||
- Drizzle ORM + PostgreSQL
|
||||
- Vercel AI SDK
|
||||
- shadcn/ui components
|
||||
- Lucide React icons
|
||||
|
||||
## AI Model Configuration
|
||||
**IMPORTANT**: When implementing any AI functionality, always use the \`OPENAI_MODEL\` environment variable for the model name instead of hardcoding it:
|
||||
|
||||
\`\`\`typescript
|
||||
// ✓ Correct - Use environment variable
|
||||
const model = process.env.OPENAI_MODEL || "gpt-5-mini";
|
||||
model: openai(model)
|
||||
|
||||
// ✗ Incorrect - Don't hardcode model names
|
||||
model: openai("gpt-5-mini")
|
||||
\`\`\`
|
||||
|
||||
This allows for easy model switching without code changes and ensures consistency across the application.
|
||||
|
||||
## Component Development Guidelines
|
||||
**Always prioritize shadcn/ui components** when building the application:
|
||||
|
||||
1. **First Choice**: Use existing shadcn/ui components from the project
|
||||
2. **Second Choice**: Install additional shadcn/ui components using \`pnpm dlx shadcn@latest add <component-name>\`
|
||||
3. **Last Resort**: Only create custom components or use other libraries if shadcn/ui doesn't provide a suitable option
|
||||
|
||||
The project already includes several shadcn/ui components (button, dialog, avatar, etc.) and follows their design system. Always check the [shadcn/ui documentation](https://ui.shadcn.com/docs/components) for available components before implementing alternatives.
|
||||
|
||||
## What I Want to Build
|
||||
[PROJECT_DESCRIPTION]
|
||||
|
||||
## Request
|
||||
Please help me transform this boilerplate into my actual application. **You MUST completely replace all existing boilerplate code** to match my project requirements. The current implementation is just temporary scaffolding that should be entirely removed and replaced.
|
||||
|
||||
## Final Reminder: COMPLETE REPLACEMENT REQUIRED
|
||||
**⚠️ IMPORTANT**: Do not preserve any of the existing boilerplate UI, components, or content. The user expects a completely fresh application that implements their requirements from scratch. Any remnants of the original boilerplate (like setup checklists, welcome screens, demo content, or placeholder navigation) indicate incomplete implementation.
|
||||
|
||||
**Success Criteria**: The final application should look and function as if it was built from scratch for the specific use case, with no evidence of the original boilerplate template.
|
||||
|
||||
## Post-Implementation Documentation
|
||||
After completing the implementation, you MUST document any new features or significant changes in the \`/docs/features/\` directory:
|
||||
|
||||
1. **Create Feature Documentation**: For each major feature implemented, create a markdown file in \`/docs/features/\` that explains:
|
||||
- What the feature does
|
||||
- How it works
|
||||
- Key components and files involved
|
||||
- Usage examples
|
||||
- Any configuration or setup required
|
||||
|
||||
2. **Update Existing Documentation**: If you modify existing functionality, update the relevant documentation files to reflect the changes.
|
||||
|
||||
3. **Document Design Decisions**: Include any important architectural or design decisions made during implementation.
|
||||
|
||||
This documentation helps maintain the project and assists future developers working with the codebase.
|
||||
|
||||
Think hard about the solution and implementing the user's requirements.`;
|
||||
|
||||
export function StarterPromptModal() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [projectDescription, setProjectDescription] = useState("");
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopy = async () => {
|
||||
const finalPrompt = projectDescription.trim()
|
||||
? STARTER_PROMPT.replace(
|
||||
"[PROJECT_DESCRIPTION]",
|
||||
projectDescription.trim()
|
||||
)
|
||||
: STARTER_PROMPT.replace("\n[PROJECT_DESCRIPTION]\n", "");
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(finalPrompt);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (err) {
|
||||
console.error("Failed to copy text: ", err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm" className="w-full">
|
||||
<Copy className="w-4 h-4 mr-2" />
|
||||
Get AI Starter Prompt
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Generate AI Starter Prompt</DialogTitle>
|
||||
<DialogDescription>
|
||||
Create a comprehensive prompt to help AI agents create your project
|
||||
for you.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="project-description"
|
||||
className="text-sm font-medium mb-2 block"
|
||||
>
|
||||
Describe your project (optional)
|
||||
</label>
|
||||
<textarea
|
||||
id="project-description"
|
||||
placeholder="e.g., A task management app for teams with real-time collaboration, project timelines, and AI-powered task prioritization..."
|
||||
value={projectDescription}
|
||||
onChange={(e) => setProjectDescription(e.target.value)}
|
||||
className="w-full h-24 px-3 py-2 border rounded-md resize-none text-sm"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Optional: Add details about your project to get a more tailored
|
||||
prompt
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleCopy} className="flex-1">
|
||||
{copied ? (
|
||||
<>
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
Copied!
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="w-4 h-4 mr-2" />
|
||||
Copy Starter Prompt
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setIsOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-muted-foreground border-t pt-3">
|
||||
<strong>How to use:</strong> Copy this prompt and paste it into
|
||||
Claude Code, Cursor, or any AI coding assistant to get started with
|
||||
your project.
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NextThemesProvider>) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||
}
|
||||
53
create-agentic-app/template/src/components/ui/avatar.tsx
Normal file
53
create-agentic-app/template/src/components/ui/avatar.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Avatar({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
||||
return (
|
||||
<AvatarPrimitive.Root
|
||||
data-slot="avatar"
|
||||
className={cn(
|
||||
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarImage({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||
return (
|
||||
<AvatarPrimitive.Image
|
||||
data-slot="avatar-image"
|
||||
className={cn("aspect-square size-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarFallback({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||
return (
|
||||
<AvatarPrimitive.Fallback
|
||||
data-slot="avatar-fallback"
|
||||
className={cn(
|
||||
"bg-muted flex size-full items-center justify-center rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
||||
36
create-agentic-app/template/src/components/ui/badge.tsx
Normal file
36
create-agentic-app/template/src/components/ui/badge.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"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",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
59
create-agentic-app/template/src/components/ui/button.tsx
Normal file
59
create-agentic-app/template/src/components/ui/button.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
79
create-agentic-app/template/src/components/ui/card.tsx
Normal file
79
create-agentic-app/template/src/components/ui/card.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-2xl font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
143
create-agentic-app/template/src/components/ui/dialog.tsx
Normal file
143
create-agentic-app/template/src/components/ui/dialog.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
}
|
||||
257
create-agentic-app/template/src/components/ui/dropdown-menu.tsx
Normal file
257
create-agentic-app/template/src/components/ui/dropdown-menu.tsx
Normal file
@@ -0,0 +1,257 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function DropdownMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Trigger
|
||||
data-slot="dropdown-menu-trigger"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuContent({
|
||||
className,
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
variant?: "default" | "destructive"
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioGroup
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Github } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
interface GitHubStarsProps {
|
||||
repo: string;
|
||||
}
|
||||
|
||||
export function GitHubStars({ repo }: GitHubStarsProps) {
|
||||
const [stars, setStars] = useState<number | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchStars() {
|
||||
try {
|
||||
const response = await fetch(`https://api.github.com/repos/${repo}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setStars(data.stargazers_count);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch GitHub stars:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
fetchStars();
|
||||
}, [repo]);
|
||||
|
||||
const formatStars = (count: number) => {
|
||||
if (count >= 1000) {
|
||||
return `${(count / 1000).toFixed(1)}k`;
|
||||
}
|
||||
return count.toString();
|
||||
};
|
||||
|
||||
return (
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<a
|
||||
href={`https://github.com/${repo}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Github className="h-4 w-4" />
|
||||
{loading ? "..." : stars !== null ? formatStars(stars) : "0"}
|
||||
</a>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Moon, Sun } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
export function ModeToggle() {
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="icon">
|
||||
<Sun className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
|
||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setTheme("light")}>
|
||||
Light
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
||||
Dark
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("system")}>
|
||||
System
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
24
create-agentic-app/template/src/components/ui/separator.tsx
Normal file
24
create-agentic-app/template/src/components/ui/separator.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface SeparatorProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
orientation?: "horizontal" | "vertical"
|
||||
}
|
||||
|
||||
const Separator = React.forwardRef<HTMLDivElement, SeparatorProps>(
|
||||
({ className, orientation = "horizontal", ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
Separator.displayName = "Separator"
|
||||
|
||||
export { Separator }
|
||||
68
create-agentic-app/template/src/hooks/use-diagnostics.ts
Normal file
68
create-agentic-app/template/src/hooks/use-diagnostics.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
type DiagnosticsResponse = {
|
||||
timestamp: string;
|
||||
env: {
|
||||
POSTGRES_URL: boolean;
|
||||
BETTER_AUTH_SECRET: boolean;
|
||||
GOOGLE_CLIENT_ID: boolean;
|
||||
GOOGLE_CLIENT_SECRET: boolean;
|
||||
OPENAI_API_KEY: boolean;
|
||||
NEXT_PUBLIC_APP_URL: boolean;
|
||||
};
|
||||
database: {
|
||||
connected: boolean;
|
||||
schemaApplied: boolean;
|
||||
error?: string;
|
||||
};
|
||||
auth: {
|
||||
configured: boolean;
|
||||
routeResponding: boolean | null;
|
||||
};
|
||||
ai: {
|
||||
configured: boolean;
|
||||
};
|
||||
overallStatus: "ok" | "warn" | "error";
|
||||
};
|
||||
|
||||
export function useDiagnostics() {
|
||||
const [data, setData] = useState<DiagnosticsResponse | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
async function fetchDiagnostics() {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await fetch("/api/diagnostics", { cache: "no-store" });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const json = (await res.json()) as DiagnosticsResponse;
|
||||
setData(json);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Failed to load diagnostics");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchDiagnostics();
|
||||
}, []);
|
||||
|
||||
const isAuthReady =
|
||||
data?.auth.configured &&
|
||||
data?.database.connected &&
|
||||
data?.database.schemaApplied;
|
||||
const isAiReady = data?.ai.configured;
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
refetch: fetchDiagnostics,
|
||||
isAuthReady: Boolean(isAuthReady),
|
||||
isAiReady: Boolean(isAiReady),
|
||||
};
|
||||
}
|
||||
13
create-agentic-app/template/src/lib/auth-client.ts
Normal file
13
create-agentic-app/template/src/lib/auth-client.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createAuthClient } from "better-auth/react"
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000",
|
||||
})
|
||||
|
||||
export const {
|
||||
signIn,
|
||||
signOut,
|
||||
signUp,
|
||||
useSession,
|
||||
getSession,
|
||||
} = authClient
|
||||
15
create-agentic-app/template/src/lib/auth.ts
Normal file
15
create-agentic-app/template/src/lib/auth.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { betterAuth } from "better-auth"
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle"
|
||||
import { db } from "./db"
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: drizzleAdapter(db, {
|
||||
provider: "pg",
|
||||
}),
|
||||
socialProviders: {
|
||||
google: {
|
||||
clientId: process.env.GOOGLE_CLIENT_ID as string,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
|
||||
},
|
||||
},
|
||||
})
|
||||
12
create-agentic-app/template/src/lib/db.ts
Normal file
12
create-agentic-app/template/src/lib/db.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { drizzle } from "drizzle-orm/postgres-js";
|
||||
import postgres from "postgres";
|
||||
import * as schema from "./schema";
|
||||
|
||||
const connectionString = process.env.POSTGRES_URL as string;
|
||||
|
||||
if (!connectionString) {
|
||||
throw new Error("POSTGRES_URL environment variable is not set");
|
||||
}
|
||||
|
||||
const client = postgres(connectionString);
|
||||
export const db = drizzle(client, { schema });
|
||||
51
create-agentic-app/template/src/lib/schema.ts
Normal file
51
create-agentic-app/template/src/lib/schema.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { pgTable, text, timestamp, boolean } from "drizzle-orm/pg-core";
|
||||
|
||||
export const user = pgTable("user", {
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
email: text("email").notNull().unique(),
|
||||
emailVerified: boolean("emailVerified"),
|
||||
image: text("image"),
|
||||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||||
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const session = pgTable("session", {
|
||||
id: text("id").primaryKey(),
|
||||
expiresAt: timestamp("expiresAt").notNull(),
|
||||
token: text("token").notNull().unique(),
|
||||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||||
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||||
ipAddress: text("ipAddress"),
|
||||
userAgent: text("userAgent"),
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const account = pgTable("account", {
|
||||
id: text("id").primaryKey(),
|
||||
accountId: text("accountId").notNull(),
|
||||
providerId: text("providerId").notNull(),
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
accessToken: text("accessToken"),
|
||||
refreshToken: text("refreshToken"),
|
||||
idToken: text("idToken"),
|
||||
accessTokenExpiresAt: timestamp("accessTokenExpiresAt"),
|
||||
refreshTokenExpiresAt: timestamp("refreshTokenExpiresAt"),
|
||||
scope: text("scope"),
|
||||
password: text("password"),
|
||||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||||
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const verification = pgTable("verification", {
|
||||
id: text("id").primaryKey(),
|
||||
identifier: text("identifier").notNull(),
|
||||
value: text("value").notNull(),
|
||||
expiresAt: timestamp("expiresAt").notNull(),
|
||||
createdAt: timestamp("createdAt").defaultNow(),
|
||||
updatedAt: timestamp("updatedAt").defaultNow(),
|
||||
});
|
||||
6
create-agentic-app/template/src/lib/utils.ts
Normal file
6
create-agentic-app/template/src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
Reference in New Issue
Block a user