Skip to content

Commit

Permalink
Refactor to use hooks (vercel#436)
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremyphilemon authored Oct 11, 2024
1 parent 124efca commit cb60f8b
Show file tree
Hide file tree
Showing 139 changed files with 8,935 additions and 8,790 deletions.
22 changes: 13 additions & 9 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
# You must first activate a Billing Account here: https://platform.openai.com/account/billing/overview
# Then get your OpenAI API Key here: https://platform.openai.com/account/api-keys
OPENAI_API_KEY=XXXXXXXX
# Get your OpenAI API Key here: https://platform.openai.com/account/api-keys
OPENAI_API_KEY=****

# Generate a random secret: https://generate-secret.vercel.app/32 or `openssl rand -base64 32`
AUTH_SECRET=XXXXXXXX
AUTH_SECRET=****

# Instructions to create kv database here: https://vercel.com/docs/storage/vercel-kv/quickstart and
KV_URL=XXXXXXXX
KV_REST_API_URL=XXXXXXXX
KV_REST_API_TOKEN=XXXXXXXX
KV_REST_API_READ_ONLY_TOKEN=XXXXXXXX
/*
* 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=****

# Instructions to create a database here: https://vercel.com/docs/storage/vercel-postgres/quickstart
POSTGRES_URL=****
41 changes: 41 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"extends": [
"next/core-web-vitals",
"plugin:import/recommended",
"plugin:import/typescript",
"prettier",
"plugin:tailwindcss/recommended"
],
"plugins": ["import", "tailwindcss"],
"rules": {
"tailwindcss/no-custom-classname": "off",
"tailwindcss/classnames-order": "off",
"import/order": [
"error",
{
"groups": [
"builtin",
"external",
"internal",
["parent", "sibling"],
"index",
"object",
"type"
],
"newlines-between": "always",
"alphabetize": {
"order": "asc",
"caseInsensitive": true
}
}
]
},
"settings": {
"import/resolver": {
"typescript": {
"alwaysTryTypes": true
}
}
},
"ignorePatterns": ["**/components/ui/**"]
}
44 changes: 17 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,49 +1,47 @@
<a href="https://chat.vercel.ai/">
<img alt="Next.js 14 and App Router-ready AI chatbot." src="https://chat.vercel.ai/opengraph-image.png">
<img alt="Next.js 14 and App Router-ready AI chatbot." src="app/(chat)/opengraph-image.png">
<h1 align="center">Next.js AI Chatbot</h1>
</a>

<p align="center">
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.
</p>

<p align="center">
<a href="#features"><strong>Features</strong></a> ·
<a href="#model-providers"><strong>Model Providers</strong></a> ·
<a href="#deploy-your-own"><strong>Deploy Your Own</strong></a> ·
<a href="#running-locally"><strong>Running locally</strong></a> ·
<a href="#authors"><strong>Authors</strong></a>
<a href="#running-locally"><strong>Running locally</strong></a>
</p>
<br/>

## 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:

[![Deploy with Vercel](https://vercel.com/button)](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.
[![Deploy with Vercel](https://vercel.com/button)](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

Expand All @@ -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)
3 changes: 3 additions & 0 deletions ai/custom-middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Experimental_LanguageModelV1Middleware } from "ai";

export const customMiddleware: Experimental_LanguageModelV1Middleware = {};
8 changes: 8 additions & 0 deletions ai/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { openai } from "@ai-sdk/openai";
import { experimental_wrapLanguageModel as wrapLanguageModel } from "ai";
import { customMiddleware } from "./custom-middleware";

export const customModel = wrapLanguageModel({
model: openai("gpt-4o"),
middleware: customMiddleware,
});
85 changes: 85 additions & 0 deletions app/(auth)/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"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" | "invalid_data";
}

export const login = async (
_: LoginActionState,
formData: FormData,
): Promise<LoginActionState> => {
try {
const validatedData = authFormSchema.parse({
email: formData.get("email"),
password: formData.get("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" };
}
};

export interface RegisterActionState {
status:
| "idle"
| "in_progress"
| "success"
| "failed"
| "user_exists"
| "invalid_data";
}

export const register = async (
_: RegisterActionState,
formData: FormData,
): Promise<RegisterActionState> => {
try {
const validatedData = authFormSchema.parse({
email: formData.get("email"),
password: formData.get("password"),
});

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" };
}
};
1 change: 1 addition & 0 deletions app/(auth)/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { GET, POST } from "@/app/(auth)/auth";
39 changes: 39 additions & 0 deletions app/(auth)/auth.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { NextAuthConfig } from "next-auth";

export const authConfig = {
pages: {
signIn: "/login",
newUser: "/",
},
providers: [
// added later in auth.ts since it requires bcrypt which is only compatible with Node.js
// while this file is also used in non-Node.js environments
],
callbacks: {
authorized({ auth, request: { nextUrl } }) {
let isLoggedIn = !!auth?.user;
let isOnChat = nextUrl.pathname.startsWith("/");
let isOnRegister = nextUrl.pathname.startsWith("/register");
let isOnLogin = nextUrl.pathname.startsWith("/login");

if (isLoggedIn && (isOnLogin || isOnRegister)) {
return Response.redirect(new URL("/", nextUrl));
}

if (isOnRegister || isOnLogin) {
return true; // Always allow access to register and login pages
}

if (isOnChat) {
if (isLoggedIn) return true;
return false; // Redirect unauthenticated users to login page
}

if (isLoggedIn) {
return Response.redirect(new URL("/", nextUrl));
}

return true;
},
},
} satisfies NextAuthConfig;
53 changes: 53 additions & 0 deletions app/(auth)/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { compare } from "bcrypt-ts";
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;
}

export const {
handlers: { GET, POST },
auth,
signIn,
signOut,
} = NextAuth({
...authConfig,
providers: [
Credentials({
credentials: {},
async authorize({ email, password }: any) {
let users = await getUser(email);
if (users.length === 0) return null;
let passwordsMatch = await compare(password, users[0].password!);
if (passwordsMatch) return users[0] as any;
},
}),
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.id = user.id;
}

return token;
},
async session({
session,
token,
}: {
session: ExtendedSession;
token: any;
}) {
if (session.user) {
session.user.id = token.id as string;
}

return session;
},
},
});
Loading

0 comments on commit cb60f8b

Please sign in to comment.