diff --git a/.env.example b/.env.example
index b6a81a490..bb0e42b96 100644
--- a/.env.example
+++ b/.env.example
@@ -4,6 +4,11 @@ OPENAI_API_KEY=****
# Generate a random secret: https://generate-secret.vercel.app/32 or `openssl rand -base64 32`
AUTH_SECRET=****
+/*
+ * The following keys below are automatically created and
+ * added to your environment when you deploy on vercel
+ */
+
# Instructions to create kv database here: https://vercel.com/docs/storage/vercel-blob
BLOB_READ_WRITE_TOKEN=****
diff --git a/.eslintrc.json b/.eslintrc.json
index ed92fcc14..5f4cbd88a 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -2,10 +2,14 @@
"extends": [
"next/core-web-vitals",
"plugin:import/recommended",
- "plugin:import/typescript"
+ "plugin:import/typescript",
+ "prettier",
+ "plugin:tailwindcss/recommended"
],
- "plugins": ["import"],
+ "plugins": ["import", "tailwindcss"],
"rules": {
+ "tailwindcss/no-custom-classname": "off",
+ "tailwindcss/classnames-order": "off",
"import/order": [
"error",
{
@@ -33,5 +37,5 @@
}
}
},
- "ignorePatterns": ["**/shadcn/**"]
+ "ignorePatterns": ["**/components/ui/**"]
}
diff --git a/README.md b/README.md
index 79a670107..d55369c6a 100644
--- a/README.md
+++ b/README.md
@@ -1,49 +1,47 @@
-
+
Next.js AI Chatbot
- An open-source AI chatbot app template built with Next.js, the Vercel AI SDK, OpenAI, and Vercel KV.
+ An Open-Source AI Chatbot Template Built With Next.js and the AI SDK by Vercel.
Features ·
Model Providers ·
Deploy Your Own ·
- Running locally ·
- Authors
+ Running locally
## Features
- [Next.js](https://nextjs.org) App Router
-- React Server Components (RSCs), Suspense, and Server Actions
-- [Vercel AI SDK](https://sdk.vercel.ai/docs) for streaming chat UI
-- Support for OpenAI (default), Anthropic, Cohere, Hugging Face, or custom AI chat models and/or LangChain
+ - Advanced routing for seamless navigation and performance
+ - React Server Components (RSCs) and Server Actions for server-side rendering and increased performance
+- [AI SDK](https://sdk.vercel.ai/docs)
+ - Unified API for generating text, structured objects, and tool calls with LLMs
+ - Hooks for building dynamic chat and generative user interfaces
+ - Supports OpenAI (default), Anthropic, Cohere, and other model providers
- [shadcn/ui](https://ui.shadcn.com)
- Styling with [Tailwind CSS](https://tailwindcss.com)
- - [Radix UI](https://radix-ui.com) for headless component primitives
- - Icons from [Phosphor Icons](https://phosphoricons.com)
-- Chat History, rate limiting, and session storage with [Vercel KV](https://vercel.com/storage/kv)
-- [NextAuth.js](https://github.com/nextauthjs/next-auth) for authentication
+ - Component primitives from [Radix UI](https://radix-ui.com) for accessibility and flexibility
+- Data Persistence
+ - [Vercel Postgres powered by Neon](https://vercel.com/storage/postgres) for saving chat history and user data
+ - [Vercel Blob](https://vercel.com/storage/blob) for efficient file storage
+- [NextAuth.js](https://github.com/nextauthjs/next-auth)
+ - Simple and secure authentication
## Model Providers
-This template ships with OpenAI `gpt-3.5-turbo` as the default. However, thanks to the [Vercel AI SDK](https://sdk.vercel.ai/docs), you can switch LLM providers to [Anthropic](https://anthropic.com), [Cohere](https://cohere.com/), [Hugging Face](https://huggingface.co), or using [LangChain](https://js.langchain.com) with just a few lines of code.
+This template ships with OpenAI `gpt-4o` as the default. However, with the [AI SDK](https://sdk.vercel.ai/docs), you can switch LLM providers to [OpenAI](https://openai.com), [Anthropic](https://anthropic.com), [Cohere](https://cohere.com/), and [many more](https://sdk.vercel.ai/providers/ai-sdk-providers) with just a few lines of code.
## Deploy Your Own
You can deploy your own version of the Next.js AI Chatbot to Vercel with one click:
-[](https://vercel.com/new/clone?demo-title=Next.js+Chat&demo-description=A+full-featured%2C+hackable+Next.js+AI+chatbot+built+by+Vercel+Labs&demo-url=https%3A%2F%2Fchat.vercel.ai%2F&demo-image=%2F%2Fimages.ctfassets.net%2Fe5382hct74si%2F4aVPvWuTmBvzM5cEdRdqeW%2F4234f9baf160f68ffb385a43c3527645%2FCleanShot_2023-06-16_at_17.09.21.png&project-name=Next.js+Chat&repository-name=nextjs-chat&repository-url=https%3A%2F%2Fgithub.com%2Fvercel-labs%2Fai-chatbot&from=templates&skippable-integrations=1&env=OPENAI_API_KEY%2CAUTH_SECRET&envDescription=How+to+get+these+env+vars&envLink=https%3A%2F%2Fgithub.com%2Fvercel-labs%2Fai-chatbot%2Fblob%2Fmain%2F.env.example&teamCreateStatus=hidden&stores=[{"type":"kv"}])
-
-## Creating a KV Database Instance
-
-Follow the steps outlined in the [quick start guide](https://vercel.com/docs/storage/vercel-kv/quickstart#create-a-kv-database) provided by Vercel. This guide will assist you in creating and configuring your KV database instance on Vercel, enabling your application to interact with it.
-
-Remember to update your environment variables (`KV_URL`, `KV_REST_API_URL`, `KV_REST_API_TOKEN`, `KV_REST_API_READ_ONLY_TOKEN`) in the `.env` file with the appropriate credentials provided during the KV database setup.
+[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fai-chatbot&env=AUTH_SECRET,OPENAI_API_KEY&envDescription=Learn%20more%20about%20how%20to%20get%20the%20API%20Keys%20for%20the%20application&envLink=https%3A%2F%2Fgithub.com%2Fvercel%2Fai-chatbot%2Fblob%2Fmain%2F.env.example&demo-title=AI%20Chatbot&demo-description=An%20Open-Source%20AI%20Chatbot%20Template%20Built%20With%20Next.js%20and%20the%20AI%20SDK%20by%20Vercel.&demo-url=https%3A%2F%2Fchat.vercel.ai&stores=[{%22type%22:%22postgres%22},{%22type%22:%22blob%22}])
## Running locally
@@ -61,11 +59,3 @@ pnpm dev
```
Your app template should now be running on [localhost:3000](http://localhost:3000/).
-
-## Authors
-
-This library is created by [Vercel](https://vercel.com) and [Next.js](https://nextjs.org) team members, with contributions from:
-
-- Jared Palmer ([@jaredpalmer](https://twitter.com/jaredpalmer)) - [Vercel](https://vercel.com)
-- Shu Ding ([@shuding\_](https://twitter.com/shuding_)) - [Vercel](https://vercel.com)
-- shadcn ([@shadcn](https://twitter.com/shadcn)) - [Vercel](https://vercel.com)
diff --git a/app/(auth)/actions.ts b/app/(auth)/actions.ts
index db6db482d..d2e26bc9c 100644
--- a/app/(auth)/actions.ts
+++ b/app/(auth)/actions.ts
@@ -1,11 +1,18 @@
"use server";
+import { z } from "zod";
+
import { createUser, getUser } from "@/db/queries";
import { signIn } from "./auth";
+const authFormSchema = z.object({
+ email: z.string().email(),
+ password: z.string().min(6),
+});
+
export interface LoginActionState {
- status: "idle" | "in_progress" | "success" | "failed";
+ status: "idle" | "in_progress" | "success" | "failed" | "invalid_data";
}
export const login = async (
@@ -13,37 +20,66 @@ export const login = async (
formData: FormData,
): Promise => {
try {
+ const validatedData = authFormSchema.parse({
+ email: formData.get("email"),
+ password: formData.get("password"),
+ });
+
await signIn("credentials", {
- email: formData.get("email") as string,
- password: formData.get("password") as string,
+ email: validatedData.email,
+ password: validatedData.password,
redirect: false,
});
- return { status: "success" } as LoginActionState;
- } catch {
- return { status: "failed" } as LoginActionState;
+ return { status: "success" };
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ return { status: "invalid_data" };
+ }
+
+ return { status: "failed" };
}
};
export interface RegisterActionState {
- status: "idle" | "in_progress" | "success" | "failed" | "user_exists";
+ status:
+ | "idle"
+ | "in_progress"
+ | "success"
+ | "failed"
+ | "user_exists"
+ | "invalid_data";
}
-export const register = async (_: RegisterActionState, formData: FormData) => {
- let email = formData.get("email") as string;
- let password = formData.get("password") as string;
- let user = await getUser(email);
-
- if (user.length > 0) {
- return { status: "user_exists" } as RegisterActionState;
- } else {
- await createUser(email, password);
- await signIn("credentials", {
- email,
- password,
- redirect: false,
+export const register = async (
+ _: RegisterActionState,
+ formData: FormData,
+): Promise => {
+ try {
+ const validatedData = authFormSchema.parse({
+ email: formData.get("email"),
+ password: formData.get("password"),
});
- return { status: "success" } as RegisterActionState;
+ let [user] = await getUser(validatedData.email);
+
+ if (user) {
+ return { status: "user_exists" } as RegisterActionState;
+ } else {
+ await createUser(validatedData.email, validatedData.password);
+ await signIn("credentials", {
+ email: validatedData.email,
+ password: validatedData.password,
+ redirect: false,
+ });
+
+ return { status: "success" };
+ }
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ return { status: "invalid_data" };
+ }
+
+ return { status: "failed" };
}
};
diff --git a/app/(auth)/auth.ts b/app/(auth)/auth.ts
index ec804601f..e7a25e18d 100644
--- a/app/(auth)/auth.ts
+++ b/app/(auth)/auth.ts
@@ -1,13 +1,11 @@
import { compare } from "bcrypt-ts";
-import NextAuth, { User , Session } from "next-auth";
+import NextAuth, { User, Session } from "next-auth";
import Credentials from "next-auth/providers/credentials";
import { getUser } from "@/db/queries";
import { authConfig } from "./auth.config";
-
-
interface ExtendedSession extends Session {
user: User;
}
diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx
index 234846e19..f3a1cb4cb 100644
--- a/app/(auth)/login/page.tsx
+++ b/app/(auth)/login/page.tsx
@@ -2,18 +2,19 @@
import Link from "next/link";
import { useRouter } from "next/navigation";
-import { useActionState, useEffect } from "react";
+import { useActionState, useEffect, useState } from "react";
import { toast } from "sonner";
-import { Form } from "@/components/form";
-import { SubmitButton } from "@/components/submit-button";
+import { AuthForm } from "@/components/custom/auth-form";
+import { SubmitButton } from "@/components/custom/submit-button";
import { login, LoginActionState } from "../actions";
-
export default function Page() {
const router = useRouter();
+ const [email, setEmail] = useState("");
+
const [state, formAction] = useActionState(
login,
{
@@ -24,11 +25,18 @@ export default function Page() {
useEffect(() => {
if (state.status === "failed") {
toast.error("Invalid credentials!");
+ } else if (state.status === "invalid_data") {
+ toast.error("Failed validating your submission!");
} else if (state.status === "success") {
router.refresh();
}
}, [state.status, router]);
+ const handleSubmit = (formData: FormData) => {
+ setEmail(formData.get("email") as string);
+ formAction(formData);
+ };
+
return (
@@ -38,7 +46,7 @@ export default function Page() {
Use your email and password to sign in
-
+
);
diff --git a/app/(auth)/register/page.tsx b/app/(auth)/register/page.tsx
index 64b14352f..6ce4083d9 100644
--- a/app/(auth)/register/page.tsx
+++ b/app/(auth)/register/page.tsx
@@ -2,16 +2,18 @@
import Link from "next/link";
import { useRouter } from "next/navigation";
-import { useActionState, useEffect } from "react";
+import { useActionState, useEffect, useState } from "react";
import { toast } from "sonner";
-import { Form } from "@/components/form";
-import { SubmitButton } from "@/components/submit-button";
+import { AuthForm } from "@/components/custom/auth-form";
+import { SubmitButton } from "@/components/custom/submit-button";
import { register, RegisterActionState } from "../actions";
export default function Page() {
const router = useRouter();
+
+ const [email, setEmail] = useState("");
const [state, formAction] = useActionState(
register,
{
@@ -24,12 +26,19 @@ export default function Page() {
toast.error("Account already exists");
} else if (state.status === "failed") {
toast.error("Failed to create account");
+ } else if (state.status === "invalid_data") {
+ toast.error("Failed validating your submission!");
} else if (state.status === "success") {
toast.success("Account created successfully");
router.refresh();
}
}, [state, router]);
+ const handleSubmit = (formData: FormData) => {
+ setEmail(formData.get("email") as string);
+ formAction(formData);
+ };
+
return (
@@ -39,7 +48,7 @@ export default function Page() {
Create an account with your email and password
-
+
);
diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts
index a6cffe3df..827d5b39d 100644
--- a/app/(chat)/api/chat/route.ts
+++ b/app/(chat)/api/chat/route.ts
@@ -1,4 +1,4 @@
-import { convertToCoreMessages, streamText } from "ai";
+import { convertToCoreMessages, Message, streamText } from "ai";
import { z } from "zod";
import { customModel } from "@/ai";
@@ -6,7 +6,8 @@ import { auth } from "@/app/(auth)/auth";
import { deleteChatById, getChatById, saveChat } from "@/db/queries";
export async function POST(request: Request) {
- const { id, messages, selectedFilePathnames } = await request.json();
+ const { id, messages }: { id: string; messages: Array } =
+ await request.json();
const session = await auth();
@@ -17,11 +18,6 @@ export async function POST(request: Request) {
system:
"you are a friendly assistant! keep your responses concise and helpful.",
messages: coreMessages,
- experimental_providerMetadata: {
- files: {
- selection: selectedFilePathnames,
- },
- },
maxSteps: 5,
tools: {
getWeather: {
@@ -42,11 +38,15 @@ export async function POST(request: Request) {
},
onFinish: async ({ responseMessages }) => {
if (session && session.user && session.user.id) {
- await saveChat({
- id,
- messages: [...coreMessages, ...responseMessages],
- userId: session.user.id,
- });
+ try {
+ await saveChat({
+ id,
+ messages: [...coreMessages, ...responseMessages],
+ userId: session.user.id,
+ });
+ } catch (error) {
+ console.error("Failed to save chat");
+ }
}
},
experimental_telemetry: {
diff --git a/app/(chat)/api/files/upload/route.ts b/app/(chat)/api/files/upload/route.ts
index b7f177d87..f4f6382af 100644
--- a/app/(chat)/api/files/upload/route.ts
+++ b/app/(chat)/api/files/upload/route.ts
@@ -1,8 +1,24 @@
import { put } from "@vercel/blob";
import { NextResponse } from "next/server";
+import { z } from "zod";
// import { auth } from "@/app/(auth)/auth";
+const FileSchema = z.object({
+ file: z
+ .instanceof(File)
+ .refine((file) => file.size <= 5 * 1024 * 1024, {
+ message: "File size should be less than 5MB",
+ })
+ .refine(
+ (file) =>
+ ["image/jpeg", "image/png", "application/pdf"].includes(file.type),
+ {
+ message: "File type should be JPEG, PNG, or PDF",
+ },
+ ),
+});
+
export async function POST(request: Request) {
// const session = await auth();
@@ -22,6 +38,16 @@ export async function POST(request: Request) {
return NextResponse.json({ error: "No file uploaded" }, { status: 400 });
}
+ const validatedFile = FileSchema.safeParse({ file });
+
+ if (!validatedFile.success) {
+ const errorMessage = validatedFile.error.errors
+ .map((error) => error.message)
+ .join(", ");
+
+ return NextResponse.json({ error: errorMessage }, { status: 400 });
+ }
+
const filename = file.name;
const fileBuffer = await file.arrayBuffer();
diff --git a/app/(chat)/[id]/page.tsx b/app/(chat)/chat/[id]/page.tsx
similarity index 96%
rename from app/(chat)/[id]/page.tsx
rename to app/(chat)/chat/[id]/page.tsx
index 838b318b9..862b6bd0b 100644
--- a/app/(chat)/[id]/page.tsx
+++ b/app/(chat)/chat/[id]/page.tsx
@@ -2,10 +2,10 @@ import { CoreMessage, CoreToolMessage, Message, ToolInvocation } from "ai";
import { notFound } from "next/navigation";
import { auth } from "@/app/(auth)/auth";
-import { Chat as PreviewChat } from "@/components/chat";
+import { Chat as PreviewChat } from "@/components/custom/chat";
import { getChatById } from "@/db/queries";
import { Chat } from "@/db/schema";
-import { generateUUID } from "@/utils/functions";
+import { generateUUID } from "@/lib/utils";
function addToolMessageToChat({
toolMessage,
diff --git a/app/(chat)/page.tsx b/app/(chat)/page.tsx
index 28ae1f8a9..2b8d6d5e9 100644
--- a/app/(chat)/page.tsx
+++ b/app/(chat)/page.tsx
@@ -1,5 +1,5 @@
-import { Chat } from "@/components/chat";
-import { generateUUID } from "@/utils/functions";
+import { Chat } from "@/components/custom/chat";
+import { generateUUID } from "@/lib/utils";
import { auth } from "../(auth)/auth";
diff --git a/app/layout.tsx b/app/layout.tsx
index 5b906eb35..73e2a3871 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,11 +1,12 @@
import { Metadata } from "next";
import { Toaster } from "sonner";
-import "./globals.css";
-import { Navbar } from "@/components/navbar";
-import { ThemeProvider } from "@/components/theme-provider";
+import { Navbar } from "@/components/custom/navbar";
+import { ThemeProvider } from "@/components/custom/theme-provider";
import { KasadaClient } from "@/utils/kasada/kasada-client";
+import "./globals.css";
+
export const metadata: Metadata = {
metadataBase: new URL("https://chat.vercel.ai"),
title: "Next.js Chatbot Template",
diff --git a/components.json b/components.json
index 4736b1cb6..1e961d331 100644
--- a/components.json
+++ b/components.json
@@ -1,6 +1,6 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
- "style": "default",
+ "style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
@@ -12,9 +12,9 @@
},
"aliases": {
"components": "@/components",
- "utils": "@/utils/shadcn/functions",
- "ui": "@/components/shadcn",
- "lib": "@/utils",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
"hooks": "@/hooks"
}
-}
+}
\ No newline at end of file
diff --git a/components/form.tsx b/components/custom/auth-form.tsx
similarity index 84%
rename from components/form.tsx
rename to components/custom/auth-form.tsx
index 7f130aa8b..08610ffed 100644
--- a/components/form.tsx
+++ b/components/custom/auth-form.tsx
@@ -1,12 +1,14 @@
-import { Input } from "./shadcn/input";
-import { Label } from "./shadcn/label";
+import { Input } from "../ui/input";
+import { Label } from "../ui/label";
-export function Form({
+export function AuthForm({
action,
children,
+ defaultEmail = "",
}: {
action: any;
children: React.ReactNode;
+ defaultEmail?: string;
}) {
return (