├── .env.example
├── .eslintrc.json
├── .gitignore
├── LICENSE
├── README.md
├── app
├── actions.ts
├── api
│ ├── auth
│ │ └── [...nextauth]
│ │ │ └── route.ts
│ └── chat
│ │ └── route.ts
├── chat
│ └── [id]
│ │ └── page.tsx
├── globals.css
├── layout.tsx
├── opengraph-image.png
├── page.tsx
├── share
│ └── [id]
│ │ ├── opengraph-image.tsx
│ │ └── page.tsx
├── sign-in
│ └── page.tsx
└── twitter-image.png
├── assets
└── fonts
│ ├── Inter-Bold.woff
│ └── Inter-Regular.woff
├── auth.ts
├── components
├── button-scroll-to-bottom.tsx
├── chat-list.tsx
├── chat-message-actions.tsx
├── chat-message.tsx
├── chat-panel.tsx
├── chat-scroll-anchor.tsx
├── chat.tsx
├── clear-history.tsx
├── empty-screen.tsx
├── external-link.tsx
├── footer.tsx
├── header.tsx
├── login-button.tsx
├── markdown.tsx
├── prompt-form.tsx
├── providers.tsx
├── sidebar-actions.tsx
├── sidebar-footer.tsx
├── sidebar-item.tsx
├── sidebar-list.tsx
├── sidebar.tsx
├── tailwind-indicator.tsx
├── theme-toggle.tsx
├── toaster.tsx
├── ui
│ ├── alert-dialog.tsx
│ ├── badge.tsx
│ ├── button.tsx
│ ├── codeblock.tsx
│ ├── dialog.tsx
│ ├── dropdown-menu.tsx
│ ├── icons.tsx
│ ├── input.tsx
│ ├── label.tsx
│ ├── select.tsx
│ ├── separator.tsx
│ ├── sheet.tsx
│ ├── switch.tsx
│ ├── textarea.tsx
│ └── tooltip.tsx
└── user-menu.tsx
├── lib
├── analytics.ts
├── fonts.ts
├── hooks
│ ├── use-at-bottom.tsx
│ ├── use-copy-to-clipboard.tsx
│ ├── use-enter-submit.tsx
│ └── use-local-storage.ts
├── types.ts
└── utils.ts
├── middleware.ts
├── next-env.d.ts
├── next.config.js
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── prettier.config.cjs
├── public
├── apple-touch-icon.png
├── favicon-16x16.png
├── favicon.ico
├── next.svg
├── thirteen.svg
└── vercel.svg
├── tailwind.config.js
└── tsconfig.json
/.env.example:
--------------------------------------------------------------------------------
1 | # You must first activate a Billing Account here: https://platform.openai.com/account/billing/overview
2 | # Then get your OpenAI API Key here: https://platform.openai.com/account/api-keys
3 | OPENAI_API_KEY=XXXXXXXX
4 |
5 | # Generate a random secret: https://generate-secret.vercel.app/32 or `openssl rand -base64 32`
6 | AUTH_SECRET=XXXXXXXX
7 | # Create a GitHub OAuth app here: https://github.com/settings/applications/new
8 | # Authorization callback URL: https://authjs.dev/reference/core/providers_github#callback-url
9 | AUTH_GITHUB_ID=XXXXXXXX
10 | AUTH_GITHUB_SECRET=XXXXXXXX
11 | # Support OAuth login on preview deployments, see: https://authjs.dev/guides/basics/deployment#securing-a-preview-deployment
12 | # Set the following only when deployed. In this example, we can reuse the same OAuth app, but if you are storing users, we recommend using a different OAuth app for development/production so that you don't mix your test and production user base.
13 | # AUTH_REDIRECT_PROXY_URL=https://YOURAPP.vercel.app/api/auth
14 |
15 | # Instructions to create kv database here: https://vercel.com/docs/storage/vercel-kv/quickstart and
16 | KV_URL=XXXXXXXX
17 | KV_REST_API_URL=XXXXXXXX
18 | KV_REST_API_TOKEN=XXXXXXXX
19 | KV_REST_API_READ_ONLY_TOKEN=XXXXXXXX
20 |
21 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/eslintrc",
3 | "root": true,
4 | "extends": [
5 | "next/core-web-vitals",
6 | "prettier",
7 | "plugin:tailwindcss/recommended"
8 | ],
9 | "plugins": ["tailwindcss"],
10 | "rules": {
11 | "tailwindcss/no-custom-classname": "off"
12 | },
13 | "settings": {
14 | "tailwindcss": {
15 | "callees": ["cn", "cva"],
16 | "config": "tailwind.config.js"
17 | }
18 | },
19 | "overrides": [
20 | {
21 | "files": ["*.ts", "*.tsx"],
22 | "parser": "@typescript-eslint/parser"
23 | }
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 | .pnp
6 | .pnp.js
7 |
8 | # testing
9 | coverage
10 |
11 | # next.js
12 | .next/
13 | out/
14 | build
15 |
16 | # misc
17 | .DS_Store
18 | *.pem
19 |
20 | # debug
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 | .pnpm-debug.log*
25 |
26 | # local env files
27 | .env.local
28 | .env.development.local
29 | .env.test.local
30 | .env.production.local
31 |
32 | # turbo
33 | .turbo
34 |
35 | .contentlayer
36 | .env
37 | .vercel
38 | .vscode
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2023 Vercel, Inc.
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | Next.js AI Chatbot
4 |
5 |
6 |
7 | An open-source AI chatbot app template built with Next.js, the Vercel AI SDK, OpenAI, and Vercel KV.
8 |
9 |
10 |
11 | Features ·
12 | Model Providers ·
13 | Deploy Your Own ·
14 | Running locally ·
15 | Authors
16 |
17 |
18 |
19 | ## Features
20 |
21 | - [Next.js](https://nextjs.org) App Router
22 | - React Server Components (RSCs), Suspense, and Server Actions
23 | - [Vercel AI SDK](https://sdk.vercel.ai/docs) for streaming chat UI
24 | - Support for OpenAI (default), Anthropic, Hugging Face, or custom AI chat models and/or LangChain
25 | - Edge runtime-ready
26 | - [shadcn/ui](https://ui.shadcn.com)
27 | - Styling with [Tailwind CSS](https://tailwindcss.com)
28 | - [Radix UI](https://radix-ui.com) for headless component primitives
29 | - Icons from [Phosphor Icons](https://phosphoricons.com)
30 | - Chat History, rate limiting, and session storage with [Vercel KV](https://vercel.com/storage/kv)
31 | - [NextAuth.js](https://github.com/nextauthjs/next-auth) for authentication
32 |
33 | ## Model Providers
34 |
35 | 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), [Hugging Face](https://huggingface.co), or using [LangChain](https://js.langchain.com) with just a few lines of code.
36 |
37 | ## Deploy Your Own
38 |
39 | You can deploy your own version of the Next.js AI Chatbot to Vercel with one click:
40 |
41 | [](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_GITHUB_ID%2CAUTH_GITHUB_SECRET%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"}])
42 |
43 | ## Creating a KV Database Instance
44 |
45 | 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.
46 |
47 | 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.
48 |
49 |
50 | ## Running locally
51 |
52 | You will need to use the environment variables [defined in `.env.example`](.env.example) to run Next.js AI Chatbot. It's recommended you use [Vercel Environment Variables](https://vercel.com/docs/concepts/projects/environment-variables) for this, but a `.env` file is all that is necessary.
53 |
54 | > Note: You should not commit your `.env` file or it will expose secrets that will allow others to control access to your various OpenAI and authentication provider accounts.
55 |
56 | 1. Install Vercel CLI: `npm i -g vercel`
57 | 2. Link local instance with Vercel and GitHub accounts (creates `.vercel` directory): `vercel link`
58 | 3. Download your environment variables: `vercel env pull`
59 |
60 | ```bash
61 | pnpm install
62 | pnpm dev
63 | ```
64 |
65 | Your app template should now be running on [localhost:3000](http://localhost:3000/).
66 |
67 | ## Authors
68 |
69 | This library is created by [Vercel](https://vercel.com) and [Next.js](https://nextjs.org) team members, with contributions from:
70 |
71 | - Jared Palmer ([@jaredpalmer](https://twitter.com/jaredpalmer)) - [Vercel](https://vercel.com)
72 | - Shu Ding ([@shuding\_](https://twitter.com/shuding_)) - [Vercel](https://vercel.com)
73 | - shadcn ([@shadcn](https://twitter.com/shadcn)) - [Contractor](https://shadcn.com)
74 |
--------------------------------------------------------------------------------
/app/actions.ts:
--------------------------------------------------------------------------------
1 | 'use server'
2 |
3 | import { revalidatePath } from 'next/cache'
4 | import { redirect } from 'next/navigation'
5 | import { kv } from '@vercel/kv'
6 |
7 | import { auth } from '@/auth'
8 | import { type Chat } from '@/lib/types'
9 |
10 | export async function getChats(userId?: string | null) {
11 | if (!userId) {
12 | return []
13 | }
14 |
15 | try {
16 | const pipeline = kv.pipeline()
17 | const chats: string[] = await kv.zrange(`user:chat:${userId}`, 0, -1, {
18 | rev: true
19 | })
20 |
21 | for (const chat of chats) {
22 | pipeline.hgetall(chat)
23 | }
24 |
25 | const results = await pipeline.exec()
26 |
27 | return results as Chat[]
28 | } catch (error) {
29 | return []
30 | }
31 | }
32 |
33 | export async function getChat(id: string, userId: string) {
34 | const chat = await kv.hgetall(`chat:${id}`)
35 |
36 | if (!chat || (userId && chat.userId !== userId)) {
37 | return null
38 | }
39 |
40 | return chat
41 | }
42 |
43 | export async function removeChat({ id, path }: { id: string; path: string }) {
44 | const session = await auth()
45 |
46 | if (!session) {
47 | return {
48 | error: 'Unauthorized'
49 | }
50 | }
51 |
52 | const uid = await kv.hget(`chat:${id}`, 'userId')
53 |
54 | if (uid !== session?.user?.id) {
55 | return {
56 | error: 'Unauthorized'
57 | }
58 | }
59 |
60 | await kv.del(`chat:${id}`)
61 | await kv.zrem(`user:chat:${session.user.id}`, `chat:${id}`)
62 |
63 | revalidatePath('/')
64 | return revalidatePath(path)
65 | }
66 |
67 | export async function clearChats() {
68 | const session = await auth()
69 |
70 | if (!session?.user?.id) {
71 | return {
72 | error: 'Unauthorized'
73 | }
74 | }
75 |
76 | const chats: string[] = await kv.zrange(`user:chat:${session.user.id}`, 0, -1)
77 | if (!chats.length) {
78 | return redirect('/')
79 | }
80 | const pipeline = kv.pipeline()
81 |
82 | for (const chat of chats) {
83 | pipeline.del(chat)
84 | pipeline.zrem(`user:chat:${session.user.id}`, chat)
85 | }
86 |
87 | await pipeline.exec()
88 |
89 | revalidatePath('/')
90 | return redirect('/')
91 | }
92 |
93 | export async function getSharedChat(id: string) {
94 | const chat = await kv.hgetall(`chat:${id}`)
95 |
96 | if (!chat || !chat.sharePath) {
97 | return null
98 | }
99 |
100 | return chat
101 | }
102 |
103 | export async function shareChat(chat: Chat) {
104 | const session = await auth()
105 |
106 | if (!session?.user?.id || session.user.id !== chat.userId) {
107 | return {
108 | error: 'Unauthorized'
109 | }
110 | }
111 |
112 | const payload = {
113 | ...chat,
114 | sharePath: `/share/${chat.id}`
115 | }
116 |
117 | await kv.hmset(`chat:${chat.id}`, payload)
118 |
119 | return payload
120 | }
121 |
--------------------------------------------------------------------------------
/app/api/auth/[...nextauth]/route.ts:
--------------------------------------------------------------------------------
1 | export { GET, POST } from '@/auth'
2 | export const runtime = 'edge'
3 |
--------------------------------------------------------------------------------
/app/api/chat/route.ts:
--------------------------------------------------------------------------------
1 | import { kv } from '@vercel/kv'
2 | import { OpenAIStream, StreamingTextResponse } from 'ai'
3 | import { Configuration, OpenAIApi } from 'openai-edge'
4 |
5 | import { auth } from '@/auth'
6 | import { nanoid } from '@/lib/utils'
7 |
8 | export const runtime = 'edge'
9 |
10 | const configuration = new Configuration({
11 | apiKey: process.env.OPENAI_API_KEY
12 | })
13 |
14 | const openai = new OpenAIApi(configuration)
15 |
16 | export async function POST(req: Request) {
17 | const json = await req.json()
18 | const { messages, previewToken } = json
19 | const userId = (await auth())?.user.id
20 |
21 | if (!userId) {
22 | return new Response('Unauthorized', {
23 | status: 401
24 | })
25 | }
26 |
27 | if (previewToken) {
28 | configuration.apiKey = previewToken
29 | }
30 |
31 | const res = await openai.createChatCompletion({
32 | model: 'gpt-3.5-turbo',
33 | messages,
34 | temperature: 0.7,
35 | stream: true
36 | })
37 |
38 | const stream = OpenAIStream(res, {
39 | async onCompletion(completion) {
40 | const title = json.messages[0].content.substring(0, 100)
41 | const id = json.id ?? nanoid()
42 | const createdAt = Date.now()
43 | const path = `/chat/${id}`
44 | const payload = {
45 | id,
46 | title,
47 | userId,
48 | createdAt,
49 | path,
50 | messages: [
51 | ...messages,
52 | {
53 | content: completion,
54 | role: 'assistant'
55 | }
56 | ]
57 | }
58 | await kv.hmset(`chat:${id}`, payload)
59 | await kv.zadd(`user:chat:${userId}`, {
60 | score: createdAt,
61 | member: `chat:${id}`
62 | })
63 | }
64 | })
65 |
66 | return new StreamingTextResponse(stream)
67 | }
68 |
--------------------------------------------------------------------------------
/app/chat/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | import { type Metadata } from 'next'
2 | import { notFound, redirect } from 'next/navigation'
3 |
4 | import { auth } from '@/auth'
5 | import { getChat } from '@/app/actions'
6 | import { Chat } from '@/components/chat'
7 |
8 | export const runtime = 'edge'
9 | export const preferredRegion = 'home'
10 |
11 | export interface ChatPageProps {
12 | params: {
13 | id: string
14 | }
15 | }
16 |
17 | export async function generateMetadata({
18 | params
19 | }: ChatPageProps): Promise {
20 | const session = await auth()
21 |
22 | if (!session?.user) {
23 | return {}
24 | }
25 |
26 | const chat = await getChat(params.id, session.user.id)
27 | return {
28 | title: chat?.title.toString().slice(0, 50) ?? 'Chat'
29 | }
30 | }
31 |
32 | export default async function ChatPage({ params }: ChatPageProps) {
33 | const session = await auth()
34 |
35 | if (!session?.user) {
36 | redirect(`/sign-in?next=/chat/${params.id}`)
37 | }
38 |
39 | const chat = await getChat(params.id, session.user.id)
40 |
41 | if (!chat) {
42 | notFound()
43 | }
44 |
45 | if (chat?.userId !== session?.user?.id) {
46 | notFound()
47 | }
48 |
49 | return
50 | }
51 |
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 240 10% 3.9%;
9 |
10 | --muted: 240 4.8% 95.9%;
11 | --muted-foreground: 240 3.8% 46.1%;
12 |
13 | --popover: 0 0% 100%;
14 | --popover-foreground: 240 10% 3.9%;
15 |
16 | --card: 0 0% 100%;
17 | --card-foreground: 240 10% 3.9%;
18 |
19 | --border: 240 5.9% 90%;
20 | --input: 240 5.9% 90%;
21 |
22 | --primary: 240 5.9% 10%;
23 | --primary-foreground: 0 0% 98%;
24 |
25 | --secondary: 240 4.8% 95.9%;
26 | --secondary-foreground: 240 5.9% 10%;
27 |
28 | --accent: 240 4.8% 95.9%;
29 | --accent-foreground: ;
30 |
31 | --destructive: 0 84.2% 60.2%;
32 | --destructive-foreground: 0 0% 98%;
33 |
34 | --ring: 240 5% 64.9%;
35 |
36 | --radius: 0.5rem;
37 | }
38 |
39 | .dark {
40 | --background: 240 10% 3.9%;
41 | --foreground: 0 0% 98%;
42 |
43 | --muted: 240 3.7% 15.9%;
44 | --muted-foreground: 240 5% 64.9%;
45 |
46 | --popover: 240 10% 3.9%;
47 | --popover-foreground: 0 0% 98%;
48 |
49 | --card: 240 10% 3.9%;
50 | --card-foreground: 0 0% 98%;
51 |
52 | --border: 240 3.7% 15.9%;
53 | --input: 240 3.7% 15.9%;
54 |
55 | --primary: 0 0% 98%;
56 | --primary-foreground: 240 5.9% 10%;
57 |
58 | --secondary: 240 3.7% 15.9%;
59 | --secondary-foreground: 0 0% 98%;
60 |
61 | --accent: 240 3.7% 15.9%;
62 | --accent-foreground: ;
63 |
64 | --destructive: 0 62.8% 30.6%;
65 | --destructive-foreground: 0 85.7% 97.3%;
66 |
67 | --ring: 240 3.7% 15.9%;
68 | }
69 | }
70 |
71 | @layer base {
72 | * {
73 | @apply border-border;
74 | }
75 | body {
76 | @apply bg-background text-foreground;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Metadata } from 'next'
2 |
3 | import { Toaster } from 'react-hot-toast'
4 |
5 | import '@/app/globals.css'
6 | import { fontMono, fontSans } from '@/lib/fonts'
7 | import { cn } from '@/lib/utils'
8 | import { TailwindIndicator } from '@/components/tailwind-indicator'
9 | import { Providers } from '@/components/providers'
10 | import { Header } from '@/components/header'
11 |
12 | export const metadata: Metadata = {
13 | title: {
14 | default: 'Next.js AI Chatbot',
15 | template: `%s - Next.js AI Chatbot`
16 | },
17 | description: 'An AI-powered chatbot template built with Next.js and Vercel.',
18 | themeColor: [
19 | { media: '(prefers-color-scheme: light)', color: 'white' },
20 | { media: '(prefers-color-scheme: dark)', color: 'black' }
21 | ],
22 | icons: {
23 | icon: '/favicon.ico',
24 | shortcut: '/favicon-16x16.png',
25 | apple: '/apple-touch-icon.png'
26 | }
27 | }
28 |
29 | interface RootLayoutProps {
30 | children: React.ReactNode
31 | }
32 |
33 | export default function RootLayout({ children }: RootLayoutProps) {
34 | return (
35 |
36 |
37 |
44 |
45 |
46 |
47 | {/* @ts-ignore */}
48 |
49 | {children}
50 |
51 |
52 |
53 |
54 |
55 | )
56 | }
57 |
--------------------------------------------------------------------------------
/app/opengraph-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shadcn/ai-chatbot/0a201f63c3c2a1cf78ccb69f96dbe25ae3f9d1ba/app/opengraph-image.png
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { nanoid } from '@/lib/utils'
2 | import { Chat } from '@/components/chat'
3 |
4 | export const runtime = 'edge'
5 |
6 | export default function IndexPage() {
7 | const id = nanoid()
8 |
9 | return
10 | }
11 |
--------------------------------------------------------------------------------
/app/share/[id]/opengraph-image.tsx:
--------------------------------------------------------------------------------
1 | import { ImageResponse } from 'next/server'
2 |
3 | import { getSharedChat } from '@/app/actions'
4 |
5 | export const runtime = 'edge'
6 |
7 | export const alt = 'AI Chatbot'
8 |
9 | export const size = {
10 | width: 1200,
11 | height: 630
12 | }
13 |
14 | export const contentType = 'image/png'
15 |
16 | const interRegular = fetch(
17 | new URL('../../../assets/fonts/Inter-Regular.woff', import.meta.url)
18 | ).then(res => res.arrayBuffer())
19 |
20 | const interBold = fetch(
21 | new URL('../../../assets/fonts/Inter-Bold.woff', import.meta.url)
22 | ).then(res => res.arrayBuffer())
23 |
24 | interface ImageProps {
25 | params: {
26 | id: string
27 | }
28 | }
29 |
30 | export default async function Image({ params }: ImageProps) {
31 | const chat = await getSharedChat(params.id)
32 |
33 | if (!chat || !chat?.sharePath) {
34 | return null
35 | }
36 |
37 | const textAlign = chat?.title?.length > 40 ? 'items-start' : 'items-center'
38 |
39 | return new ImageResponse(
40 | (
41 |
42 |
43 |
44 |
55 |
56 | {chat.title.length > 120
57 | ? `${chat.title.slice(0, 120)}...`
58 | : chat.title}
59 |
60 |
61 |
62 |
74 |
75 | ...
76 |
77 |
78 |
79 |
80 |
81 |
90 |
91 | Built with{' '}
92 |
Vercel AI SDK
&
93 |
KV
94 |
95 |
96 |
chat.vercel.ai
97 |
98 |
99 | ),
100 | {
101 | ...size,
102 | fonts: [
103 | {
104 | name: 'Inter',
105 | data: await interRegular,
106 | style: 'normal',
107 | weight: 400
108 | },
109 | {
110 | name: 'Inter',
111 | data: await interBold,
112 | style: 'normal',
113 | weight: 700
114 | }
115 | ]
116 | }
117 | )
118 | }
119 |
--------------------------------------------------------------------------------
/app/share/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | import { type Metadata } from 'next'
2 | import { notFound } from 'next/navigation'
3 |
4 | import { formatDate } from '@/lib/utils'
5 | import { getSharedChat } from '@/app/actions'
6 | import { ChatList } from '@/components/chat-list'
7 | import { FooterText } from '@/components/footer'
8 |
9 | export const runtime = 'edge'
10 | export const preferredRegion = 'home'
11 |
12 | interface SharePageProps {
13 | params: {
14 | id: string
15 | }
16 | }
17 |
18 | export async function generateMetadata({
19 | params
20 | }: SharePageProps): Promise {
21 | const chat = await getSharedChat(params.id)
22 |
23 | return {
24 | title: chat?.title.slice(0, 50) ?? 'Chat'
25 | }
26 | }
27 |
28 | export default async function SharePage({ params }: SharePageProps) {
29 | const chat = await getSharedChat(params.id)
30 |
31 | if (!chat || !chat?.sharePath) {
32 | notFound()
33 | }
34 |
35 | return (
36 | <>
37 |
38 |
39 |
40 |
41 |
{chat.title}
42 |
43 | {formatDate(chat.createdAt)} · {chat.messages.length} messages
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | >
52 | )
53 | }
54 |
--------------------------------------------------------------------------------
/app/sign-in/page.tsx:
--------------------------------------------------------------------------------
1 | import { auth } from '@/auth'
2 | import { LoginButton } from '@/components/login-button'
3 | import { redirect } from 'next/navigation'
4 |
5 | export default async function SignInPage() {
6 | const session = await auth()
7 | // redirect to home if user is already logged in
8 | if (session?.user) {
9 | redirect('/')
10 | }
11 | return (
12 |
13 |
14 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/app/twitter-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shadcn/ai-chatbot/0a201f63c3c2a1cf78ccb69f96dbe25ae3f9d1ba/app/twitter-image.png
--------------------------------------------------------------------------------
/assets/fonts/Inter-Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shadcn/ai-chatbot/0a201f63c3c2a1cf78ccb69f96dbe25ae3f9d1ba/assets/fonts/Inter-Bold.woff
--------------------------------------------------------------------------------
/assets/fonts/Inter-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shadcn/ai-chatbot/0a201f63c3c2a1cf78ccb69f96dbe25ae3f9d1ba/assets/fonts/Inter-Regular.woff
--------------------------------------------------------------------------------
/auth.ts:
--------------------------------------------------------------------------------
1 | import NextAuth, { type DefaultSession } from 'next-auth'
2 | import GitHub from 'next-auth/providers/github'
3 |
4 | declare module 'next-auth' {
5 | interface Session {
6 | user: {
7 | /** The user's id. */
8 | id: string
9 | } & DefaultSession['user']
10 | }
11 | }
12 |
13 | export const {
14 | handlers: { GET, POST },
15 | auth,
16 | CSRF_experimental // will be removed in future
17 | } = NextAuth({
18 | providers: [GitHub],
19 | callbacks: {
20 | jwt({ token, profile }) {
21 | if (profile) {
22 | token.id = profile.id
23 | token.image = profile.avatar_url || profile.picture
24 | }
25 | return token
26 | },
27 | authorized({ auth }) {
28 | return !!auth?.user // this ensures there is a logged in user for -every- request
29 | }
30 | },
31 | pages: {
32 | signIn: '/sign-in' // overrides the next-auth default signin page https://authjs.dev/guides/basics/pages
33 | }
34 | })
35 |
--------------------------------------------------------------------------------
/components/button-scroll-to-bottom.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 |
5 | import { cn } from '@/lib/utils'
6 | import { useAtBottom } from '@/lib/hooks/use-at-bottom'
7 | import { Button, type ButtonProps } from '@/components/ui/button'
8 | import { IconArrowDown } from '@/components/ui/icons'
9 |
10 | export function ButtonScrollToBottom({ className, ...props }: ButtonProps) {
11 | const isAtBottom = useAtBottom()
12 |
13 | return (
14 |
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/components/chat-list.tsx:
--------------------------------------------------------------------------------
1 | import { type Message } from 'ai'
2 |
3 | import { Separator } from '@/components/ui/separator'
4 | import { ChatMessage } from '@/components/chat-message'
5 |
6 | export interface ChatList {
7 | messages: Message[]
8 | }
9 |
10 | export function ChatList({ messages }: ChatList) {
11 | if (!messages.length) {
12 | return null
13 | }
14 |
15 | return (
16 |
17 | {messages.map((message, index) => (
18 |
19 |
20 | {index < messages.length - 1 && (
21 |
22 | )}
23 |
24 | ))}
25 |
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/components/chat-message-actions.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { type Message } from 'ai'
4 |
5 | import { Button } from '@/components/ui/button'
6 | import { IconCheck, IconCopy } from '@/components/ui/icons'
7 | import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard'
8 | import { cn } from '@/lib/utils'
9 |
10 | interface ChatMessageActionsProps extends React.ComponentProps<'div'> {
11 | message: Message
12 | }
13 |
14 | export function ChatMessageActions({
15 | message,
16 | className,
17 | ...props
18 | }: ChatMessageActionsProps) {
19 | const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 })
20 |
21 | const onCopy = () => {
22 | if (isCopied) return
23 | copyToClipboard(message.content)
24 | }
25 |
26 | return (
27 |
34 |
38 |
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/components/chat-message.tsx:
--------------------------------------------------------------------------------
1 | // Inspired by Chatbot-UI and modified to fit the needs of this project
2 | // @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Chat/ChatMessage.tsx
3 |
4 | import { Message } from 'ai'
5 | import remarkGfm from 'remark-gfm'
6 | import remarkMath from 'remark-math'
7 |
8 | import { cn } from '@/lib/utils'
9 | import { CodeBlock } from '@/components/ui/codeblock'
10 | import { MemoizedReactMarkdown } from '@/components/markdown'
11 | import { IconOpenAI, IconUser } from '@/components/ui/icons'
12 | import { ChatMessageActions } from '@/components/chat-message-actions'
13 |
14 | export interface ChatMessageProps {
15 | message: Message
16 | }
17 |
18 | export function ChatMessage({ message, ...props }: ChatMessageProps) {
19 | return (
20 |
24 |
32 | {message.role === 'user' ? : }
33 |
34 |
35 | {children}
41 | },
42 | code({ node, inline, className, children, ...props }) {
43 | if (children.length) {
44 | if (children[0] == '▍') {
45 | return (
46 | ▍
47 | )
48 | }
49 |
50 | children[0] = (children[0] as string).replace('`▍`', '▍')
51 | }
52 |
53 | const match = /language-(\w+)/.exec(className || '')
54 |
55 | if (inline) {
56 | return (
57 |
58 | {children}
59 |
60 | )
61 | }
62 |
63 | return (
64 |
70 | )
71 | }
72 | }}
73 | >
74 | {message.content}
75 |
76 |
77 |
78 |
79 | )
80 | }
81 |
--------------------------------------------------------------------------------
/components/chat-panel.tsx:
--------------------------------------------------------------------------------
1 | import { type UseChatHelpers } from 'ai/react'
2 |
3 | import { Button } from '@/components/ui/button'
4 | import { PromptForm } from '@/components/prompt-form'
5 | import { ButtonScrollToBottom } from '@/components/button-scroll-to-bottom'
6 | import { IconRefresh, IconStop } from '@/components/ui/icons'
7 | import { FooterText } from '@/components/footer'
8 |
9 | export interface ChatPanelProps
10 | extends Pick<
11 | UseChatHelpers,
12 | | 'append'
13 | | 'isLoading'
14 | | 'reload'
15 | | 'messages'
16 | | 'stop'
17 | | 'input'
18 | | 'setInput'
19 | > {
20 | id?: string
21 | }
22 |
23 | export function ChatPanel({
24 | id,
25 | isLoading,
26 | stop,
27 | append,
28 | reload,
29 | input,
30 | setInput,
31 | messages
32 | }: ChatPanelProps) {
33 | return (
34 |
35 |
36 |
37 |
38 | {isLoading ? (
39 |
47 | ) : (
48 | messages?.length > 0 && (
49 |
57 | )
58 | )}
59 |
60 |
61 |
{
63 | await append({
64 | id,
65 | content: value,
66 | role: 'user'
67 | })
68 | }}
69 | input={input}
70 | setInput={setInput}
71 | isLoading={isLoading}
72 | />
73 |
74 |
75 |
76 |
77 | )
78 | }
79 |
--------------------------------------------------------------------------------
/components/chat-scroll-anchor.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import { useInView } from 'react-intersection-observer'
5 |
6 | import { useAtBottom } from '@/lib/hooks/use-at-bottom'
7 |
8 | interface ChatScrollAnchorProps {
9 | trackVisibility?: boolean
10 | }
11 |
12 | export function ChatScrollAnchor({ trackVisibility }: ChatScrollAnchorProps) {
13 | const isAtBottom = useAtBottom()
14 | const { ref, entry, inView } = useInView({
15 | trackVisibility,
16 | delay: 100,
17 | rootMargin: '0px 0px -150px 0px'
18 | })
19 |
20 | React.useEffect(() => {
21 | if (isAtBottom && trackVisibility && !inView) {
22 | entry?.target.scrollIntoView({
23 | block: 'start'
24 | })
25 | }
26 | }, [inView, entry, isAtBottom, trackVisibility])
27 |
28 | return
29 | }
30 |
--------------------------------------------------------------------------------
/components/chat.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useChat, type Message } from 'ai/react'
4 |
5 | import { cn } from '@/lib/utils'
6 | import { ChatList } from '@/components/chat-list'
7 | import { ChatPanel } from '@/components/chat-panel'
8 | import { EmptyScreen } from '@/components/empty-screen'
9 | import { ChatScrollAnchor } from '@/components/chat-scroll-anchor'
10 | import { useLocalStorage } from '@/lib/hooks/use-local-storage'
11 | import {
12 | Dialog,
13 | DialogContent,
14 | DialogDescription,
15 | DialogFooter,
16 | DialogHeader,
17 | DialogTitle
18 | } from '@/components/ui/dialog'
19 | import { useState } from 'react'
20 | import { Button } from './ui/button'
21 | import { Input } from './ui/input'
22 | import { toast } from 'react-hot-toast'
23 |
24 | const IS_PREVIEW = process.env.VERCEL_ENV === 'preview'
25 | export interface ChatProps extends React.ComponentProps<'div'> {
26 | initialMessages?: Message[]
27 | id?: string
28 | }
29 |
30 | export function Chat({ id, initialMessages, className }: ChatProps) {
31 | const [previewToken, setPreviewToken] = useLocalStorage(
32 | 'ai-token',
33 | null
34 | )
35 | const [previewTokenDialog, setPreviewTokenDialog] = useState(IS_PREVIEW)
36 | const [previewTokenInput, setPreviewTokenInput] = useState(previewToken ?? '')
37 | const { messages, append, reload, stop, isLoading, input, setInput } =
38 | useChat({
39 | initialMessages,
40 | id,
41 | body: {
42 | id,
43 | previewToken
44 | },
45 | onResponse(response) {
46 | if (response.status === 401) {
47 | toast.error(response.statusText)
48 | }
49 | }
50 | })
51 | return (
52 | <>
53 |
54 | {messages.length ? (
55 | <>
56 |
57 |
58 | >
59 | ) : (
60 |
61 | )}
62 |
63 |
73 |
74 |
109 | >
110 | )
111 | }
112 |
--------------------------------------------------------------------------------
/components/clear-history.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import { useRouter } from 'next/navigation'
5 | import { toast } from 'react-hot-toast'
6 |
7 | import { ServerActionResult } from '@/lib/types'
8 | import { Button } from '@/components/ui/button'
9 | import {
10 | AlertDialog,
11 | AlertDialogAction,
12 | AlertDialogCancel,
13 | AlertDialogContent,
14 | AlertDialogDescription,
15 | AlertDialogFooter,
16 | AlertDialogHeader,
17 | AlertDialogTitle,
18 | AlertDialogTrigger
19 | } from '@/components/ui/alert-dialog'
20 | import { IconSpinner } from '@/components/ui/icons'
21 |
22 | interface ClearHistoryProps {
23 | clearChats: () => ServerActionResult
24 | }
25 |
26 | export function ClearHistory({ clearChats }: ClearHistoryProps) {
27 | const [open, setOpen] = React.useState(false)
28 | const [isPending, startTransition] = React.useTransition()
29 | const router = useRouter()
30 |
31 | return (
32 |
33 |
34 |
38 |
39 |
40 |
41 | Are you absolutely sure?
42 |
43 | This will permanently delete your chat history and remove your data
44 | from our servers.
45 |
46 |
47 |
48 | Cancel
49 | {
52 | event.preventDefault()
53 | startTransition(async () => {
54 | const result = await clearChats()
55 |
56 | if (result && 'error' in result) {
57 | toast.error(result.error)
58 | return
59 | }
60 |
61 | setOpen(false)
62 | router.push('/')
63 | })
64 | }}
65 | >
66 | {isPending && }
67 | Delete
68 |
69 |
70 |
71 |
72 | )
73 | }
74 |
--------------------------------------------------------------------------------
/components/empty-screen.tsx:
--------------------------------------------------------------------------------
1 | import { UseChatHelpers } from 'ai/react'
2 |
3 | import { Button } from '@/components/ui/button'
4 | import { ExternalLink } from '@/components/external-link'
5 | import { IconArrowRight } from '@/components/ui/icons'
6 |
7 | const exampleMessages = [
8 | {
9 | heading: 'Explain technical concepts',
10 | message: `What is a "serverless function"?`
11 | },
12 | {
13 | heading: 'Summarize an article',
14 | message: 'Summarize the following article for a 2nd grader: \n'
15 | },
16 | {
17 | heading: 'Draft an email',
18 | message: `Draft an email to my boss about the following: \n`
19 | }
20 | ]
21 |
22 | export function EmptyScreen({ setInput }: Pick) {
23 | return (
24 |
25 |
26 |
27 | Welcome to Next.js AI Chatbot!
28 |
29 |
30 | This is an open source AI chatbot app template built with{' '}
31 | Next.js and{' '}
32 |
33 | Vercel KV
34 |
35 | .
36 |
37 |
38 | You can start a conversation here or try the following examples:
39 |
40 |
41 | {exampleMessages.map((message, index) => (
42 |
51 | ))}
52 |
53 |
54 |
55 | )
56 | }
57 |
--------------------------------------------------------------------------------
/components/external-link.tsx:
--------------------------------------------------------------------------------
1 | export function ExternalLink({
2 | href,
3 | children
4 | }: {
5 | href: string
6 | children: React.ReactNode
7 | }) {
8 | return (
9 |
14 | {children}
15 |
27 |
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/components/footer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { cn } from '@/lib/utils'
4 | import { ExternalLink } from '@/components/external-link'
5 |
6 | export function FooterText({ className, ...props }: React.ComponentProps<'p'>) {
7 | return (
8 |
15 | Open source AI chatbot built with{' '}
16 | Next.js and{' '}
17 |
18 | Vercel KV
19 |
20 | .
21 |
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/components/header.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import Link from 'next/link'
3 |
4 | import { cn } from '@/lib/utils'
5 | import { auth } from '@/auth'
6 | import { clearChats } from '@/app/actions'
7 | import { Button, buttonVariants } from '@/components/ui/button'
8 | import { Sidebar } from '@/components/sidebar'
9 | import { SidebarList } from '@/components/sidebar-list'
10 | import {
11 | IconGitHub,
12 | IconNextChat,
13 | IconSeparator,
14 | IconVercel
15 | } from '@/components/ui/icons'
16 | import { SidebarFooter } from '@/components/sidebar-footer'
17 | import { ThemeToggle } from '@/components/theme-toggle'
18 | import { ClearHistory } from '@/components/clear-history'
19 | import { UserMenu } from '@/components/user-menu'
20 | import { LoginButton } from '@/components/login-button'
21 |
22 | export async function Header() {
23 | const session = await auth()
24 | return (
25 |
76 | )
77 | }
78 |
--------------------------------------------------------------------------------
/components/login-button.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import { signIn } from 'next-auth/react'
5 |
6 | import { cn } from '@/lib/utils'
7 | import { Button, type ButtonProps } from '@/components/ui/button'
8 | import { IconGitHub, IconSpinner } from '@/components/ui/icons'
9 |
10 | interface LoginButtonProps extends ButtonProps {
11 | showGithubIcon?: boolean
12 | text?: string
13 | }
14 |
15 | export function LoginButton({
16 | text = 'Login with GitHub',
17 | showGithubIcon = true,
18 | className,
19 | ...props
20 | }: LoginButtonProps) {
21 | const [isLoading, setIsLoading] = React.useState(false)
22 | return (
23 |
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/components/markdown.tsx:
--------------------------------------------------------------------------------
1 | import { FC, memo } from 'react'
2 | import ReactMarkdown, { Options } from 'react-markdown'
3 |
4 | export const MemoizedReactMarkdown: FC = memo(
5 | ReactMarkdown,
6 | (prevProps, nextProps) =>
7 | prevProps.children === nextProps.children &&
8 | prevProps.className === nextProps.className
9 | )
10 |
--------------------------------------------------------------------------------
/components/prompt-form.tsx:
--------------------------------------------------------------------------------
1 | import { UseChatHelpers } from 'ai/react'
2 | import * as React from 'react'
3 | import Textarea from 'react-textarea-autosize'
4 |
5 | import { Button, buttonVariants } from '@/components/ui/button'
6 | import { IconArrowElbow, IconPlus } from '@/components/ui/icons'
7 | import {
8 | Tooltip,
9 | TooltipContent,
10 | TooltipTrigger
11 | } from '@/components/ui/tooltip'
12 | import { useEnterSubmit } from '@/lib/hooks/use-enter-submit'
13 | import { cn } from '@/lib/utils'
14 | import { useRouter } from 'next/navigation'
15 |
16 | export interface PromptProps
17 | extends Pick {
18 | onSubmit: (value: string) => Promise
19 | isLoading: boolean
20 | }
21 |
22 | export function PromptForm({
23 | onSubmit,
24 | input,
25 | setInput,
26 | isLoading
27 | }: PromptProps) {
28 | const { formRef, onKeyDown } = useEnterSubmit()
29 | const inputRef = React.useRef(null)
30 | const router = useRouter()
31 |
32 | React.useEffect(() => {
33 | if (inputRef.current) {
34 | inputRef.current.focus()
35 | }
36 | }, [])
37 |
38 | return (
39 |
98 | )
99 | }
100 |
--------------------------------------------------------------------------------
/components/providers.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import { ThemeProvider as NextThemesProvider } from 'next-themes'
5 | import { ThemeProviderProps } from 'next-themes/dist/types'
6 |
7 | import { TooltipProvider } from '@/components/ui/tooltip'
8 |
9 | export function Providers({ children, ...props }: ThemeProviderProps) {
10 | return (
11 |
12 | {children}
13 |
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/components/sidebar-actions.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import { useRouter } from 'next/navigation'
5 | import { toast } from 'react-hot-toast'
6 |
7 | import { type Chat, ServerActionResult } from '@/lib/types'
8 | import { cn, formatDate } from '@/lib/utils'
9 | import {
10 | AlertDialog,
11 | AlertDialogAction,
12 | AlertDialogCancel,
13 | AlertDialogContent,
14 | AlertDialogDescription,
15 | AlertDialogFooter,
16 | AlertDialogHeader,
17 | AlertDialogTitle
18 | } from '@/components/ui/alert-dialog'
19 | import { Button } from '@/components/ui/button'
20 | import {
21 | Dialog,
22 | DialogContent,
23 | DialogDescription,
24 | DialogFooter,
25 | DialogHeader,
26 | DialogTitle
27 | } from '@/components/ui/dialog'
28 | import {
29 | IconShare,
30 | IconSpinner,
31 | IconTrash,
32 | IconUsers
33 | } from '@/components/ui/icons'
34 | import Link from 'next/link'
35 | import { badgeVariants } from '@/components/ui/badge'
36 | import {
37 | Tooltip,
38 | TooltipContent,
39 | TooltipTrigger
40 | } from '@/components/ui/tooltip'
41 |
42 | interface SidebarActionsProps {
43 | chat: Chat
44 | removeChat: (args: { id: string; path: string }) => ServerActionResult
45 | shareChat: (chat: Chat) => ServerActionResult
46 | }
47 |
48 | export function SidebarActions({
49 | chat,
50 | removeChat,
51 | shareChat
52 | }: SidebarActionsProps) {
53 | const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false)
54 | const [shareDialogOpen, setShareDialogOpen] = React.useState(false)
55 | const [isRemovePending, startRemoveTransition] = React.useTransition()
56 | const [isSharePending, startShareTransition] = React.useTransition()
57 | const router = useRouter()
58 |
59 | const copyShareLink = React.useCallback(async (chat: Chat) => {
60 | if (!chat.sharePath) {
61 | return toast.error('Could not copy share link to clipboard')
62 | }
63 |
64 | const url = new URL(window.location.href)
65 | url.pathname = chat.sharePath
66 | navigator.clipboard.writeText(url.toString())
67 | setShareDialogOpen(false)
68 | toast.success('Share link copied to clipboard', {
69 | style: {
70 | borderRadius: '10px',
71 | background: '#333',
72 | color: '#fff',
73 | fontSize: '14px'
74 | },
75 | iconTheme: {
76 | primary: 'white',
77 | secondary: 'black'
78 | }
79 | })
80 | }, [])
81 |
82 | return (
83 | <>
84 |
85 |
86 |
87 |
95 |
96 | Share chat
97 |
98 |
99 |
100 |
109 |
110 | Delete chat
111 |
112 |
113 |
174 |
175 |
176 |
177 | Are you absolutely sure?
178 |
179 | This will permanently delete your chat message and remove your
180 | data from our servers.
181 |
182 |
183 |
184 |
185 | Cancel
186 |
187 | {
190 | event.preventDefault()
191 | startRemoveTransition(async () => {
192 | const result = await removeChat({
193 | id: chat.id,
194 | path: chat.path
195 | })
196 |
197 | if (result && 'error' in result) {
198 | toast.error(result.error)
199 | return
200 | }
201 |
202 | setDeleteDialogOpen(false)
203 | router.refresh()
204 | router.push('/')
205 | toast.success('Chat deleted')
206 | })
207 | }}
208 | >
209 | {isRemovePending && }
210 | Delete
211 |
212 |
213 |
214 |
215 | >
216 | )
217 | }
218 |
--------------------------------------------------------------------------------
/components/sidebar-footer.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils'
2 |
3 | export function SidebarFooter({
4 | children,
5 | className,
6 | ...props
7 | }: React.ComponentProps<'div'>) {
8 | return (
9 |
13 | {children}
14 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/components/sidebar-item.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import Link from 'next/link'
4 | import { usePathname } from 'next/navigation'
5 |
6 | import { type Chat } from '@/lib/types'
7 | import { cn } from '@/lib/utils'
8 | import { buttonVariants } from '@/components/ui/button'
9 | import { IconMessage, IconUsers } from '@/components/ui/icons'
10 | import {
11 | Tooltip,
12 | TooltipContent,
13 | TooltipTrigger
14 | } from '@/components/ui/tooltip'
15 |
16 | interface SidebarItemProps {
17 | chat: Chat
18 | children: React.ReactNode
19 | }
20 |
21 | export function SidebarItem({ chat, children }: SidebarItemProps) {
22 | const pathname = usePathname()
23 | const isActive = pathname === chat.path
24 |
25 | if (!chat?.id) return null
26 |
27 | return (
28 |
29 |
30 | {chat.sharePath ? (
31 |
32 |
36 |
37 |
38 | This is a shared chat.
39 |
40 | ) : (
41 |
42 | )}
43 |
44 |
52 |
56 | {chat.title}
57 |
58 |
59 | {isActive &&
{children}
}
60 |
61 | )
62 | }
63 |
--------------------------------------------------------------------------------
/components/sidebar-list.tsx:
--------------------------------------------------------------------------------
1 | import { getChats, removeChat, shareChat } from '@/app/actions'
2 | import { SidebarActions } from '@/components/sidebar-actions'
3 | import { SidebarItem } from '@/components/sidebar-item'
4 |
5 | export interface SidebarListProps {
6 | userId?: string
7 | }
8 |
9 | export async function SidebarList({ userId }: SidebarListProps) {
10 | const chats = await getChats(userId)
11 |
12 | return (
13 |
14 | {chats?.length ? (
15 |
16 | {chats.map(
17 | chat =>
18 | chat && (
19 |
20 |
25 |
26 | )
27 | )}
28 |
29 | ) : (
30 |
31 |
No chat history
32 |
33 | )}
34 |
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/components/sidebar.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 |
5 | import { Button } from '@/components/ui/button'
6 | import {
7 | Sheet,
8 | SheetContent,
9 | SheetHeader,
10 | SheetTitle,
11 | SheetTrigger
12 | } from '@/components/ui/sheet'
13 | import { IconSidebar } from '@/components/ui/icons'
14 |
15 | export interface SidebarProps {
16 | children?: React.ReactNode
17 | }
18 |
19 | export function Sidebar({ children }: SidebarProps) {
20 | return (
21 |
22 |
23 |
27 |
28 |
29 |
30 | Chat History
31 |
32 | {children}
33 |
34 |
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/components/tailwind-indicator.tsx:
--------------------------------------------------------------------------------
1 | export function TailwindIndicator() {
2 | if (process.env.NODE_ENV === 'production') return null
3 |
4 | return (
5 |
6 |
xs
7 |
sm
8 |
md
9 |
lg
10 |
xl
11 |
2xl
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/components/theme-toggle.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import { useTheme } from 'next-themes'
5 |
6 | import { Button } from '@/components/ui/button'
7 | import { IconMoon, IconSun } from '@/components/ui/icons'
8 |
9 | export function ThemeToggle() {
10 | const { setTheme, theme } = useTheme()
11 | const [_, startTransition] = React.useTransition()
12 |
13 | return (
14 |
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/components/toaster.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | export { Toaster } from 'react-hot-toast'
4 |
--------------------------------------------------------------------------------
/components/ui/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
5 |
6 | import { cn } from '@/lib/utils'
7 | import { buttonVariants } from '@/components/ui/button'
8 |
9 | const AlertDialog = AlertDialogPrimitive.Root
10 |
11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger
12 |
13 | const AlertDialogPortal = ({
14 | className,
15 | children,
16 | ...props
17 | }: AlertDialogPrimitive.AlertDialogPortalProps) => (
18 |
19 |
20 | {children}
21 |
22 |
23 | )
24 | AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName
25 |
26 | const AlertDialogOverlay = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, children, ...props }, ref) => (
30 |
38 | ))
39 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
40 |
41 | const AlertDialogContent = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef
44 | >(({ className, ...props }, ref) => (
45 |
46 |
47 |
55 |
56 | ))
57 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
58 |
59 | const AlertDialogHeader = ({
60 | className,
61 | ...props
62 | }: React.HTMLAttributes) => (
63 |
70 | )
71 | AlertDialogHeader.displayName = 'AlertDialogHeader'
72 |
73 | const AlertDialogFooter = ({
74 | className,
75 | ...props
76 | }: React.HTMLAttributes) => (
77 |
84 | )
85 | AlertDialogFooter.displayName = 'AlertDialogFooter'
86 |
87 | const AlertDialogTitle = React.forwardRef<
88 | React.ElementRef,
89 | React.ComponentPropsWithoutRef
90 | >(({ className, ...props }, ref) => (
91 |
96 | ))
97 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
98 |
99 | const AlertDialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | AlertDialogDescription.displayName =
110 | AlertDialogPrimitive.Description.displayName
111 |
112 | const AlertDialogAction = React.forwardRef<
113 | React.ElementRef,
114 | React.ComponentPropsWithoutRef
115 | >(({ className, ...props }, ref) => (
116 |
121 | ))
122 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
123 |
124 | const AlertDialogCancel = React.forwardRef<
125 | React.ElementRef,
126 | React.ComponentPropsWithoutRef
127 | >(({ className, ...props }, ref) => (
128 |
137 | ))
138 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
139 |
140 | export {
141 | AlertDialog,
142 | AlertDialogTrigger,
143 | AlertDialogContent,
144 | AlertDialogHeader,
145 | AlertDialogFooter,
146 | AlertDialogTitle,
147 | AlertDialogDescription,
148 | AlertDialogAction,
149 | AlertDialogCancel
150 | }
151 |
--------------------------------------------------------------------------------
/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { cva, type VariantProps } from 'class-variance-authority'
3 |
4 | import { cn } from '@/lib/utils'
5 |
6 | const badgeVariants = cva(
7 | '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',
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
13 | secondary:
14 | 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
15 | destructive:
16 | 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
17 | outline: 'text-foreground'
18 | }
19 | },
20 | defaultVariants: {
21 | variant: 'default'
22 | }
23 | }
24 | )
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Slot } from '@radix-ui/react-slot'
3 | import { cva, type VariantProps } from 'class-variance-authority'
4 |
5 | import { cn } from '@/lib/utils'
6 |
7 | const buttonVariants = cva(
8 | 'inline-flex items-center justify-center rounded-md text-sm font-medium shadow ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | 'bg-primary text-primary-foreground shadow-md hover:bg-primary/90',
14 | destructive:
15 | 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
16 | outline:
17 | 'border border-input hover:bg-accent hover:text-accent-foreground',
18 | secondary:
19 | 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
20 | ghost: 'shadow-none hover:bg-accent hover:text-accent-foreground',
21 | link: 'text-primary underline-offset-4 shadow-none hover:underline'
22 | },
23 | size: {
24 | default: 'h-8 px-4 py-2',
25 | sm: 'h-8 rounded-md px-3',
26 | lg: 'h-11 rounded-md px-8',
27 | icon: 'h-8 w-8 p-0'
28 | }
29 | },
30 | defaultVariants: {
31 | variant: 'default',
32 | size: 'default'
33 | }
34 | }
35 | )
36 |
37 | export interface ButtonProps
38 | extends React.ButtonHTMLAttributes,
39 | VariantProps {
40 | asChild?: boolean
41 | }
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : 'button'
46 | return (
47 |
52 | )
53 | }
54 | )
55 | Button.displayName = 'Button'
56 |
57 | export { Button, buttonVariants }
58 |
--------------------------------------------------------------------------------
/components/ui/codeblock.tsx:
--------------------------------------------------------------------------------
1 | // Inspired by Chatbot-UI and modified to fit the needs of this project
2 | // @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Markdown/CodeBlock.tsx
3 |
4 | 'use client'
5 |
6 | import { FC, memo } from 'react'
7 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
8 | import { coldarkDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'
9 |
10 | import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard'
11 | import { IconCheck, IconCopy, IconDownload } from '@/components/ui/icons'
12 | import { Button } from '@/components/ui/button'
13 |
14 | interface Props {
15 | language: string
16 | value: string
17 | }
18 |
19 | interface languageMap {
20 | [key: string]: string | undefined
21 | }
22 |
23 | export const programmingLanguages: languageMap = {
24 | javascript: '.js',
25 | python: '.py',
26 | java: '.java',
27 | c: '.c',
28 | cpp: '.cpp',
29 | 'c++': '.cpp',
30 | 'c#': '.cs',
31 | ruby: '.rb',
32 | php: '.php',
33 | swift: '.swift',
34 | 'objective-c': '.m',
35 | kotlin: '.kt',
36 | typescript: '.ts',
37 | go: '.go',
38 | perl: '.pl',
39 | rust: '.rs',
40 | scala: '.scala',
41 | haskell: '.hs',
42 | lua: '.lua',
43 | shell: '.sh',
44 | sql: '.sql',
45 | html: '.html',
46 | css: '.css'
47 | // add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component
48 | }
49 |
50 | export const generateRandomString = (length: number, lowercase = false) => {
51 | const chars = 'ABCDEFGHJKLMNPQRSTUVWXY3456789' // excluding similar looking characters like Z, 2, I, 1, O, 0
52 | let result = ''
53 | for (let i = 0; i < length; i++) {
54 | result += chars.charAt(Math.floor(Math.random() * chars.length))
55 | }
56 | return lowercase ? result.toLowerCase() : result
57 | }
58 |
59 | const CodeBlock: FC = memo(({ language, value }) => {
60 | const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 })
61 |
62 | const downloadAsFile = () => {
63 | if (typeof window === 'undefined') {
64 | return
65 | }
66 | const fileExtension = programmingLanguages[language] || '.file'
67 | const suggestedFileName = `file-${generateRandomString(
68 | 3,
69 | true
70 | )}${fileExtension}`
71 | const fileName = window.prompt('Enter file name' || '', suggestedFileName)
72 |
73 | if (!fileName) {
74 | // User pressed cancel on prompt.
75 | return
76 | }
77 |
78 | const blob = new Blob([value], { type: 'text/plain' })
79 | const url = URL.createObjectURL(blob)
80 | const link = document.createElement('a')
81 | link.download = fileName
82 | link.href = url
83 | link.style.display = 'none'
84 | document.body.appendChild(link)
85 | link.click()
86 | document.body.removeChild(link)
87 | URL.revokeObjectURL(url)
88 | }
89 |
90 | const onCopy = () => {
91 | if (isCopied) return
92 | copyToClipboard(value)
93 | }
94 |
95 | return (
96 |
97 |
98 |
{language}
99 |
100 |
109 |
118 |
119 |
120 |
138 | {value}
139 |
140 |
141 | )
142 | })
143 | CodeBlock.displayName = 'CodeBlock'
144 |
145 | export { CodeBlock }
146 |
--------------------------------------------------------------------------------
/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as DialogPrimitive from '@radix-ui/react-dialog'
5 |
6 | import { cn } from '@/lib/utils'
7 | import { IconClose } from '@/components/ui/icons'
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = ({
14 | className,
15 | children,
16 | ...props
17 | }: DialogPrimitive.DialogPortalProps) => (
18 |
19 |
20 | {children}
21 |
22 |
23 | )
24 | DialogPortal.displayName = DialogPrimitive.Portal.displayName
25 |
26 | const DialogOverlay = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, ...props }, ref) => (
30 |
38 | ))
39 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
40 |
41 | const DialogContent = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef
44 | >(({ className, children, ...props }, ref) => (
45 |
46 |
47 |
55 | {children}
56 |
57 |
58 | Close
59 |
60 |
61 |
62 | ))
63 | DialogContent.displayName = DialogPrimitive.Content.displayName
64 |
65 | const DialogHeader = ({
66 | className,
67 | ...props
68 | }: React.HTMLAttributes) => (
69 |
76 | )
77 | DialogHeader.displayName = 'DialogHeader'
78 |
79 | const DialogFooter = ({
80 | className,
81 | ...props
82 | }: React.HTMLAttributes) => (
83 |
90 | )
91 | DialogFooter.displayName = 'DialogFooter'
92 |
93 | const DialogTitle = React.forwardRef<
94 | React.ElementRef,
95 | React.ComponentPropsWithoutRef
96 | >(({ className, ...props }, ref) => (
97 |
105 | ))
106 | DialogTitle.displayName = DialogPrimitive.Title.displayName
107 |
108 | const DialogDescription = React.forwardRef<
109 | React.ElementRef,
110 | React.ComponentPropsWithoutRef
111 | >(({ className, ...props }, ref) => (
112 |
117 | ))
118 | DialogDescription.displayName = DialogPrimitive.Description.displayName
119 |
120 | export {
121 | Dialog,
122 | DialogTrigger,
123 | DialogContent,
124 | DialogHeader,
125 | DialogFooter,
126 | DialogTitle,
127 | DialogDescription
128 | }
129 |
--------------------------------------------------------------------------------
/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
5 |
6 | import { cn } from '@/lib/utils'
7 |
8 | const DropdownMenu = DropdownMenuPrimitive.Root
9 |
10 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
11 |
12 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
13 |
14 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
15 |
16 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
17 |
18 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
19 |
20 | const DropdownMenuSubContent = React.forwardRef<
21 | React.ElementRef,
22 | React.ComponentPropsWithoutRef
23 | >(({ className, ...props }, ref) => (
24 |
32 | ))
33 | DropdownMenuSubContent.displayName =
34 | DropdownMenuPrimitive.SubContent.displayName
35 |
36 | const DropdownMenuContent = React.forwardRef<
37 | React.ElementRef,
38 | React.ComponentPropsWithoutRef
39 | >(({ className, sideOffset = 4, ...props }, ref) => (
40 |
41 |
50 |
51 | ))
52 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
53 |
54 | const DropdownMenuItem = React.forwardRef<
55 | React.ElementRef,
56 | React.ComponentPropsWithoutRef & {
57 | inset?: boolean
58 | }
59 | >(({ className, inset, ...props }, ref) => (
60 |
69 | ))
70 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
71 |
72 | const DropdownMenuLabel = React.forwardRef<
73 | React.ElementRef,
74 | React.ComponentPropsWithoutRef & {
75 | inset?: boolean
76 | }
77 | >(({ className, inset, ...props }, ref) => (
78 |
87 | ))
88 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
89 |
90 | const DropdownMenuSeparator = React.forwardRef<
91 | React.ElementRef,
92 | React.ComponentPropsWithoutRef
93 | >(({ className, ...props }, ref) => (
94 |
99 | ))
100 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
101 |
102 | const DropdownMenuShortcut = ({
103 | className,
104 | ...props
105 | }: React.HTMLAttributes) => {
106 | return (
107 |
111 | )
112 | }
113 | DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
114 |
115 | export {
116 | DropdownMenu,
117 | DropdownMenuTrigger,
118 | DropdownMenuContent,
119 | DropdownMenuItem,
120 | DropdownMenuLabel,
121 | DropdownMenuSeparator,
122 | DropdownMenuShortcut,
123 | DropdownMenuGroup,
124 | DropdownMenuPortal,
125 | DropdownMenuSub,
126 | DropdownMenuSubContent,
127 | DropdownMenuRadioGroup
128 | }
129 |
--------------------------------------------------------------------------------
/components/ui/icons.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 |
5 | import { cn } from '@/lib/utils'
6 |
7 | function IconNextChat({
8 | className,
9 | inverted,
10 | ...props
11 | }: React.ComponentProps<'svg'> & { inverted?: boolean }) {
12 | const id = React.useId()
13 |
14 | return (
15 |
88 | )
89 | }
90 |
91 | function IconOpenAI({ className, ...props }: React.ComponentProps<'svg'>) {
92 | return (
93 |
104 | )
105 | }
106 |
107 | function IconVercel({ className, ...props }: React.ComponentProps<'svg'>) {
108 | return (
109 |
121 | )
122 | }
123 |
124 | function IconGitHub({ className, ...props }: React.ComponentProps<'svg'>) {
125 | return (
126 |
137 | )
138 | }
139 |
140 | function IconSeparator({ className, ...props }: React.ComponentProps<'svg'>) {
141 | return (
142 |
156 | )
157 | }
158 |
159 | function IconArrowDown({ className, ...props }: React.ComponentProps<'svg'>) {
160 | return (
161 |
170 | )
171 | }
172 |
173 | function IconArrowRight({ className, ...props }: React.ComponentProps<'svg'>) {
174 | return (
175 |
184 | )
185 | }
186 |
187 | function IconUser({ className, ...props }: React.ComponentProps<'svg'>) {
188 | return (
189 |
198 | )
199 | }
200 |
201 | function IconPlus({ className, ...props }: React.ComponentProps<'svg'>) {
202 | return (
203 |
212 | )
213 | }
214 |
215 | function IconArrowElbow({ className, ...props }: React.ComponentProps<'svg'>) {
216 | return (
217 |
226 | )
227 | }
228 |
229 | function IconSpinner({ className, ...props }: React.ComponentProps<'svg'>) {
230 | return (
231 |
240 | )
241 | }
242 |
243 | function IconMessage({ className, ...props }: React.ComponentProps<'svg'>) {
244 | return (
245 |
254 | )
255 | }
256 |
257 | function IconTrash({ className, ...props }: React.ComponentProps<'svg'>) {
258 | return (
259 |
268 | )
269 | }
270 |
271 | function IconRefresh({ className, ...props }: React.ComponentProps<'svg'>) {
272 | return (
273 |
282 | )
283 | }
284 |
285 | function IconStop({ className, ...props }: React.ComponentProps<'svg'>) {
286 | return (
287 |
296 | )
297 | }
298 |
299 | function IconSidebar({ className, ...props }: React.ComponentProps<'svg'>) {
300 | return (
301 |
310 | )
311 | }
312 |
313 | function IconMoon({ className, ...props }: React.ComponentProps<'svg'>) {
314 | return (
315 |
324 | )
325 | }
326 |
327 | function IconSun({ className, ...props }: React.ComponentProps<'svg'>) {
328 | return (
329 |
338 | )
339 | }
340 |
341 | function IconCopy({ className, ...props }: React.ComponentProps<'svg'>) {
342 | return (
343 |
352 | )
353 | }
354 |
355 | function IconCheck({ className, ...props }: React.ComponentProps<'svg'>) {
356 | return (
357 |
366 | )
367 | }
368 |
369 | function IconDownload({ className, ...props }: React.ComponentProps<'svg'>) {
370 | return (
371 |
380 | )
381 | }
382 |
383 | function IconClose({ className, ...props }: React.ComponentProps<'svg'>) {
384 | return (
385 |
394 | )
395 | }
396 |
397 | function IconEdit({ className, ...props }: React.ComponentProps<'svg'>) {
398 | return (
399 |
414 | )
415 | }
416 |
417 | function IconShare({ className, ...props }: React.ComponentProps<'svg'>) {
418 | return (
419 |
428 | )
429 | }
430 |
431 | function IconUsers({ className, ...props }: React.ComponentProps<'svg'>) {
432 | return (
433 |
442 | )
443 | }
444 |
445 | function IconExternalLink({
446 | className,
447 | ...props
448 | }: React.ComponentProps<'svg'>) {
449 | return (
450 |
459 | )
460 | }
461 |
462 | function IconChevronUpDown({
463 | className,
464 | ...props
465 | }: React.ComponentProps<'svg'>) {
466 | return (
467 |
476 | )
477 | }
478 |
479 | export {
480 | IconEdit,
481 | IconNextChat,
482 | IconOpenAI,
483 | IconVercel,
484 | IconGitHub,
485 | IconSeparator,
486 | IconArrowDown,
487 | IconArrowRight,
488 | IconUser,
489 | IconPlus,
490 | IconArrowElbow,
491 | IconSpinner,
492 | IconMessage,
493 | IconTrash,
494 | IconRefresh,
495 | IconStop,
496 | IconSidebar,
497 | IconMoon,
498 | IconSun,
499 | IconCopy,
500 | IconCheck,
501 | IconDownload,
502 | IconClose,
503 | IconShare,
504 | IconUsers,
505 | IconExternalLink,
506 | IconChevronUpDown
507 | }
508 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import { cn } from '@/lib/utils'
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = 'Input'
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as SelectPrimitive from '@radix-ui/react-select'
5 |
6 | import { cn } from '@/lib/utils'
7 | import {
8 | IconArrowDown,
9 | IconCheck,
10 | IconChevronUpDown
11 | } from '@/components/ui/icons'
12 |
13 | const Select = SelectPrimitive.Root
14 |
15 | const SelectGroup = SelectPrimitive.Group
16 |
17 | const SelectValue = SelectPrimitive.Value
18 |
19 | const SelectTrigger = React.forwardRef<
20 | React.ElementRef,
21 | React.ComponentPropsWithoutRef
22 | >(({ className, children, ...props }, ref) => (
23 |
31 | {children}
32 |
33 |
34 |
35 |
36 | ))
37 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
38 |
39 | const SelectContent = React.forwardRef<
40 | React.ElementRef,
41 | React.ComponentPropsWithoutRef
42 | >(({ className, children, position = 'popper', ...props }, ref) => (
43 |
44 |
54 |
61 | {children}
62 |
63 |
64 |
65 | ))
66 | SelectContent.displayName = SelectPrimitive.Content.displayName
67 |
68 | const SelectLabel = React.forwardRef<
69 | React.ElementRef,
70 | React.ComponentPropsWithoutRef
71 | >(({ className, ...props }, ref) => (
72 |
77 | ))
78 | SelectLabel.displayName = SelectPrimitive.Label.displayName
79 |
80 | const SelectItem = React.forwardRef<
81 | React.ElementRef,
82 | React.ComponentPropsWithoutRef
83 | >(({ className, children, ...props }, ref) => (
84 |
92 |
93 |
94 |
95 |
96 |
97 | {children}
98 |
99 | ))
100 | SelectItem.displayName = SelectPrimitive.Item.displayName
101 |
102 | const SelectSeparator = React.forwardRef<
103 | React.ElementRef,
104 | React.ComponentPropsWithoutRef
105 | >(({ className, ...props }, ref) => (
106 |
111 | ))
112 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
113 |
114 | export {
115 | Select,
116 | SelectGroup,
117 | SelectValue,
118 | SelectTrigger,
119 | SelectContent,
120 | SelectLabel,
121 | SelectItem,
122 | SelectSeparator
123 | }
124 |
--------------------------------------------------------------------------------
/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as SeparatorPrimitive from '@radix-ui/react-separator'
5 |
6 | import { cn } from '@/lib/utils'
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = 'horizontal', decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | )
29 | Separator.displayName = SeparatorPrimitive.Root.displayName
30 |
31 | export { Separator }
32 |
--------------------------------------------------------------------------------
/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as SheetPrimitive from '@radix-ui/react-dialog'
5 |
6 | import { cn } from '@/lib/utils'
7 | import { IconClose } from '@/components/ui/icons'
8 |
9 | const Sheet = SheetPrimitive.Root
10 |
11 | const SheetTrigger = SheetPrimitive.Trigger
12 |
13 | const SheetClose = SheetPrimitive.Close
14 |
15 | const SheetPortal = ({
16 | className,
17 | children,
18 | ...props
19 | }: SheetPrimitive.DialogPortalProps) => (
20 |
24 | {children}
25 |
26 | )
27 | SheetPortal.displayName = SheetPrimitive.Portal.displayName
28 |
29 | const SheetOverlay = React.forwardRef<
30 | React.ElementRef,
31 | React.ComponentPropsWithoutRef
32 | >(({ className, children, ...props }, ref) => (
33 |
41 | ))
42 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
43 |
44 | const SheetContent = React.forwardRef<
45 | React.ElementRef,
46 | React.ComponentPropsWithoutRef
47 | >(({ className, children, ...props }, ref) => (
48 |
49 |
57 | {children}
58 |
59 |
60 | Close
61 |
62 |
63 |
64 | ))
65 | SheetContent.displayName = SheetPrimitive.Content.displayName
66 |
67 | const SheetHeader = ({
68 | className,
69 | ...props
70 | }: React.HTMLAttributes) => (
71 |
72 | )
73 | SheetHeader.displayName = 'SheetHeader'
74 |
75 | const SheetFooter = ({
76 | className,
77 | ...props
78 | }: React.HTMLAttributes) => (
79 |
86 | )
87 | SheetFooter.displayName = 'SheetFooter'
88 |
89 | const SheetTitle = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => (
93 |
98 | ))
99 | SheetTitle.displayName = SheetPrimitive.Title.displayName
100 |
101 | const SheetDescription = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | SheetDescription.displayName = SheetPrimitive.Description.displayName
112 |
113 | export {
114 | Sheet,
115 | SheetTrigger,
116 | SheetClose,
117 | SheetContent,
118 | SheetHeader,
119 | SheetFooter,
120 | SheetTitle,
121 | SheetDescription
122 | }
123 |
--------------------------------------------------------------------------------
/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SwitchPrimitives from "@radix-ui/react-switch"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ))
27 | Switch.displayName = SwitchPrimitives.Root.displayName
28 |
29 | export { Switch }
30 |
--------------------------------------------------------------------------------
/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import { cn } from '@/lib/utils'
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | )
20 | }
21 | )
22 | Textarea.displayName = 'Textarea'
23 |
24 | export { Textarea }
25 |
--------------------------------------------------------------------------------
/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as TooltipPrimitive from '@radix-ui/react-tooltip'
5 |
6 | import { cn } from '@/lib/utils'
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ))
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
31 |
--------------------------------------------------------------------------------
/components/user-menu.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import Image from 'next/image'
4 | import { type Session } from 'next-auth'
5 | import { signOut } from 'next-auth/react'
6 |
7 | import { Button } from '@/components/ui/button'
8 | import {
9 | DropdownMenu,
10 | DropdownMenuContent,
11 | DropdownMenuItem,
12 | DropdownMenuSeparator,
13 | DropdownMenuTrigger
14 | } from '@/components/ui/dropdown-menu'
15 | import { IconExternalLink } from '@/components/ui/icons'
16 |
17 | export interface UserMenuProps {
18 | user: Session['user']
19 | }
20 |
21 | function getUserInitials(name: string) {
22 | const [firstName, lastName] = name.split(' ')
23 | return lastName ? `${firstName[0]}${lastName[0]}` : firstName.slice(0, 2)
24 | }
25 |
26 | export function UserMenu({ user }: UserMenuProps) {
27 | return (
28 |
29 |
30 |
31 |
46 |
47 |
48 |
49 | {user?.name}
50 | {user?.email}
51 |
52 |
53 |
54 |
60 | Vercel Homepage
61 |
62 |
63 |
64 |
66 | signOut({
67 | callbackUrl: '/'
68 | })
69 | }
70 | className="text-xs"
71 | >
72 | Log Out
73 |
74 |
75 |
76 |
77 | )
78 | }
79 |
--------------------------------------------------------------------------------
/lib/analytics.ts:
--------------------------------------------------------------------------------
1 | import type { NextApiRequest } from 'next'
2 | import type { NextFetchEvent, NextRequest } from 'next/server'
3 |
4 | export const initAnalytics = ({
5 | request,
6 | event
7 | }: {
8 | request: NextRequest | NextApiRequest | Request
9 | event?: NextFetchEvent
10 | }) => {
11 | const endpoint = process.env.VERCEL_URL
12 |
13 | return {
14 | track: async (eventName: string, data?: any) => {
15 | try {
16 | if (!endpoint && process.env.NODE_ENV === 'development') {
17 | console.log(
18 | `[Vercel Web Analytics] Track "${eventName}"` +
19 | (data ? ` with data ${JSON.stringify(data || {})}` : '')
20 | )
21 | return
22 | }
23 |
24 | const headers: { [key: string]: string } = {}
25 | Object.entries(request.headers).map(([key, value]) => {
26 | headers[key] = value
27 | })
28 |
29 | const body = {
30 | o: headers.referer,
31 | ts: new Date().getTime(),
32 | r: '',
33 | en: eventName,
34 | ed: data
35 | }
36 |
37 | const promise = fetch(
38 | `https://${process.env.VERCEL_URL}/_vercel/insights/event`,
39 | {
40 | headers: {
41 | 'content-type': 'application/json',
42 | 'user-agent': headers['user-agent'] as string,
43 | 'x-forwarded-for': headers['x-forwarded-for'] as string,
44 | 'x-va-server': '1'
45 | },
46 | body: JSON.stringify(body),
47 | method: 'POST'
48 | }
49 | )
50 |
51 | if (event) {
52 | event.waitUntil(promise)
53 | }
54 | {
55 | await promise
56 | }
57 | } catch (err) {
58 | console.error(err)
59 | }
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/lib/fonts.ts:
--------------------------------------------------------------------------------
1 | import { JetBrains_Mono as FontMono, Inter as FontSans } from 'next/font/google'
2 |
3 | export const fontSans = FontSans({
4 | subsets: ['latin'],
5 | variable: '--font-sans'
6 | })
7 |
8 | export const fontMono = FontMono({
9 | subsets: ['latin'],
10 | variable: '--font-mono'
11 | })
12 |
--------------------------------------------------------------------------------
/lib/hooks/use-at-bottom.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | export function useAtBottom(offset = 0) {
4 | const [isAtBottom, setIsAtBottom] = React.useState(false)
5 |
6 | React.useEffect(() => {
7 | const handleScroll = () => {
8 | setIsAtBottom(
9 | window.innerHeight + window.scrollY >=
10 | document.body.offsetHeight - offset
11 | )
12 | }
13 |
14 | window.addEventListener('scroll', handleScroll, { passive: true })
15 | handleScroll()
16 |
17 | return () => {
18 | window.removeEventListener('scroll', handleScroll)
19 | }
20 | }, [offset])
21 |
22 | return isAtBottom
23 | }
24 |
--------------------------------------------------------------------------------
/lib/hooks/use-copy-to-clipboard.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 |
5 | export interface useCopyToClipboardProps {
6 | timeout?: number
7 | }
8 |
9 | export function useCopyToClipboard({
10 | timeout = 2000
11 | }: useCopyToClipboardProps) {
12 | const [isCopied, setIsCopied] = React.useState(false)
13 |
14 | const copyToClipboard = (value: string) => {
15 | if (typeof window === 'undefined' || !navigator.clipboard?.writeText) {
16 | return
17 | }
18 |
19 | if (!value) {
20 | return
21 | }
22 |
23 | navigator.clipboard.writeText(value).then(() => {
24 | setIsCopied(true)
25 |
26 | setTimeout(() => {
27 | setIsCopied(false)
28 | }, timeout)
29 | })
30 | }
31 |
32 | return { isCopied, copyToClipboard }
33 | }
34 |
--------------------------------------------------------------------------------
/lib/hooks/use-enter-submit.tsx:
--------------------------------------------------------------------------------
1 | import { useRef, type RefObject } from 'react'
2 |
3 | export function useEnterSubmit(): {
4 | formRef: RefObject
5 | onKeyDown: (event: React.KeyboardEvent) => void
6 | } {
7 | const formRef = useRef(null)
8 |
9 | const handleKeyDown = (
10 | event: React.KeyboardEvent
11 | ): void => {
12 | if (
13 | event.key === 'Enter' &&
14 | !event.shiftKey &&
15 | !event.nativeEvent.isComposing
16 | ) {
17 | formRef.current?.requestSubmit()
18 | event.preventDefault()
19 | }
20 | }
21 |
22 | return { formRef, onKeyDown: handleKeyDown }
23 | }
24 |
--------------------------------------------------------------------------------
/lib/hooks/use-local-storage.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 |
3 | export const useLocalStorage = (
4 | key: string,
5 | initialValue: T
6 | ): [T, (value: T) => void] => {
7 | const [storedValue, setStoredValue] = useState(initialValue)
8 |
9 | useEffect(() => {
10 | // Retrieve from localStorage
11 | const item = window.localStorage.getItem(key)
12 | if (item) {
13 | setStoredValue(JSON.parse(item))
14 | }
15 | }, [key])
16 |
17 | const setValue = (value: T) => {
18 | // Save state
19 | setStoredValue(value)
20 | // Save to localStorage
21 | window.localStorage.setItem(key, JSON.stringify(value))
22 | }
23 | return [storedValue, setValue]
24 | }
25 |
--------------------------------------------------------------------------------
/lib/types.ts:
--------------------------------------------------------------------------------
1 | import { type Message } from 'ai'
2 |
3 | export interface Chat extends Record {
4 | id: string
5 | title: string
6 | createdAt: Date
7 | userId: string
8 | path: string
9 | messages: Message[]
10 | sharePath?: string
11 | }
12 |
13 | export type ServerActionResult = Promise<
14 | | Result
15 | | {
16 | error: string
17 | }
18 | >
19 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from 'clsx'
2 | import { customAlphabet } from 'nanoid'
3 | import { twMerge } from 'tailwind-merge'
4 |
5 | export function cn(...inputs: ClassValue[]) {
6 | return twMerge(clsx(inputs))
7 | }
8 |
9 | export const nanoid = customAlphabet(
10 | '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
11 | 7
12 | ) // 7-character random string
13 |
14 | export async function fetcher(
15 | input: RequestInfo,
16 | init?: RequestInit
17 | ): Promise {
18 | const res = await fetch(input, init)
19 |
20 | if (!res.ok) {
21 | const json = await res.json()
22 | if (json.error) {
23 | const error = new Error(json.error) as Error & {
24 | status: number
25 | }
26 | error.status = res.status
27 | throw error
28 | } else {
29 | throw new Error('An unexpected error occurred')
30 | }
31 | }
32 |
33 | return res.json()
34 | }
35 |
36 | export function formatDate(input: string | number | Date): string {
37 | const date = new Date(input)
38 | return date.toLocaleDateString('en-US', {
39 | month: 'long',
40 | day: 'numeric',
41 | year: 'numeric'
42 | })
43 | }
44 |
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | export { auth as middleware } from './auth'
2 |
3 | export const config = {
4 | matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)']
5 | }
6 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | module.exports = {
3 | reactStrictMode: true,
4 | experimental: {
5 | serverActions: true,
6 | },
7 | images: {
8 | remotePatterns: [
9 | {
10 | protocol: 'https',
11 | hostname: 'avatars.githubusercontent.com',
12 | port: '',
13 | pathname: '**',
14 | },
15 | ],
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next-template",
3 | "version": "0.0.2",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "lint:fix": "next lint --fix",
11 | "preview": "next build && next start",
12 | "type-check": "tsc --noEmit",
13 | "format:write": "prettier --write \"{app,lib,components}/**/*.{ts,tsx,mdx}\" --cache",
14 | "format:check": "prettier --check \"{app,lib,components}**/*.{ts,tsx,mdx}\" --cache"
15 | },
16 | "dependencies": {
17 | "@radix-ui/react-alert-dialog": "^1.0.4",
18 | "@radix-ui/react-dialog": "^1.0.4",
19 | "@radix-ui/react-dropdown-menu": "^2.0.5",
20 | "@radix-ui/react-label": "^2.0.2",
21 | "@radix-ui/react-select": "^1.2.2",
22 | "@radix-ui/react-separator": "^1.0.3",
23 | "@radix-ui/react-slot": "^1.0.2",
24 | "@radix-ui/react-switch": "^1.0.3",
25 | "@radix-ui/react-tooltip": "^1.0.6",
26 | "@vercel/analytics": "^1.0.0",
27 | "@vercel/kv": "^0.2.1",
28 | "@vercel/og": "^0.5.7",
29 | "ai": "^2.1.6",
30 | "class-variance-authority": "^0.4.0",
31 | "clsx": "^1.2.1",
32 | "focus-trap-react": "^10.1.1",
33 | "nanoid": "^4.0.2",
34 | "next": "13.4.7-canary.1",
35 | "next-auth": "0.0.0-manual.83c4ebd1",
36 | "next-themes": "^0.2.1",
37 | "openai-edge": "^0.5.1",
38 | "react": "^18.2.0",
39 | "react-dom": "^18.2.0",
40 | "react-hot-toast": "^2.4.1",
41 | "react-intersection-observer": "^9.4.4",
42 | "react-markdown": "^8.0.7",
43 | "react-syntax-highlighter": "^15.5.0",
44 | "react-textarea-autosize": "^8.4.1",
45 | "remark-gfm": "^3.0.1",
46 | "remark-math": "^5.1.1"
47 | },
48 | "devDependencies": {
49 | "@tailwindcss/typography": "^0.5.9",
50 | "@types/node": "^17.0.12",
51 | "@types/react": "^18.0.22",
52 | "@types/react-dom": "^18.0.7",
53 | "@types/react-syntax-highlighter": "^15.5.6",
54 | "@typescript-eslint/parser": "^5.59.7",
55 | "autoprefixer": "^10.4.13",
56 | "eslint": "^8.31.0",
57 | "eslint-config-next": "13.4.7-canary.1",
58 | "eslint-config-prettier": "^8.3.0",
59 | "eslint-plugin-tailwindcss": "^3.12.0",
60 | "postcss": "^8.4.21",
61 | "prettier": "^2.7.1",
62 | "tailwind-merge": "^1.12.0",
63 | "tailwindcss": "^3.3.1",
64 | "tailwindcss-animate": "^1.0.5",
65 | "typescript": "^5.1.3"
66 | },
67 | "packageManager": "pnpm@8.6.3"
68 | }
69 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/prettier.config.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import('prettier').Config} */
2 | module.exports = {
3 | endOfLine: 'lf',
4 | semi: false,
5 | useTabs: false,
6 | singleQuote: true,
7 | arrowParens: 'avoid',
8 | tabWidth: 2,
9 | trailingComma: 'none',
10 | importOrder: [
11 | '^(react/(.*)$)|^(react$)',
12 | '^(next/(.*)$)|^(next$)',
13 | '',
14 | '',
15 | '^types$',
16 | '^@/types/(.*)$',
17 | '^@/config/(.*)$',
18 | '^@/lib/(.*)$',
19 | '^@/hooks/(.*)$',
20 | '^@/components/ui/(.*)$',
21 | '^@/components/(.*)$',
22 | '^@/registry/(.*)$',
23 | '^@/styles/(.*)$',
24 | '^@/app/(.*)$',
25 | '',
26 | '^[./]'
27 | ],
28 | importOrderSeparation: false,
29 | importOrderSortSpecifiers: true,
30 | importOrderBuiltinModulesToTop: true,
31 | importOrderParserPlugins: ['typescript', 'jsx', 'decorators-legacy'],
32 | importOrderMergeDuplicateImports: true,
33 | importOrderCombineTypeAndValueImports: true
34 | }
35 |
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shadcn/ai-chatbot/0a201f63c3c2a1cf78ccb69f96dbe25ae3f9d1ba/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shadcn/ai-chatbot/0a201f63c3c2a1cf78ccb69f96dbe25ae3f9d1ba/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shadcn/ai-chatbot/0a201f63c3c2a1cf78ccb69f96dbe25ae3f9d1ba/public/favicon.ico
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/thirteen.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const { fontFamily } = require('tailwindcss/defaultTheme')
2 |
3 | /** @type {import('tailwindcss').Config} */
4 | module.exports = {
5 | darkMode: ['class'],
6 | content: ['app/**/*.{ts,tsx}', 'components/**/*.{ts,tsx}'],
7 | theme: {
8 | container: {
9 | center: true,
10 | padding: '2rem',
11 | screens: {
12 | '2xl': '1400px'
13 | }
14 | },
15 | extend: {
16 | fontFamily: {
17 | sans: ['var(--font-sans)', ...fontFamily.sans]
18 | },
19 | colors: {
20 | border: 'hsl(var(--border))',
21 | input: 'hsl(var(--input))',
22 | ring: 'hsl(var(--ring))',
23 | background: 'hsl(var(--background))',
24 | foreground: 'hsl(var(--foreground))',
25 | primary: {
26 | DEFAULT: 'hsl(var(--primary))',
27 | foreground: 'hsl(var(--primary-foreground))'
28 | },
29 | secondary: {
30 | DEFAULT: 'hsl(var(--secondary))',
31 | foreground: 'hsl(var(--secondary-foreground))'
32 | },
33 | destructive: {
34 | DEFAULT: 'hsl(var(--destructive))',
35 | foreground: 'hsl(var(--destructive-foreground))'
36 | },
37 | muted: {
38 | DEFAULT: 'hsl(var(--muted))',
39 | foreground: 'hsl(var(--muted-foreground))'
40 | },
41 | accent: {
42 | DEFAULT: 'hsl(var(--accent))',
43 | foreground: 'hsl(var(--accent-foreground))'
44 | },
45 | popover: {
46 | DEFAULT: 'hsl(var(--popover))',
47 | foreground: 'hsl(var(--popover-foreground))'
48 | },
49 | card: {
50 | DEFAULT: 'hsl(var(--card))',
51 | foreground: 'hsl(var(--card-foreground))'
52 | }
53 | },
54 | borderRadius: {
55 | lg: `var(--radius)`,
56 | md: `calc(var(--radius) - 2px)`,
57 | sm: 'calc(var(--radius) - 4px)'
58 | },
59 | keyframes: {
60 | 'accordion-down': {
61 | from: { height: 0 },
62 | to: { height: 'var(--radix-accordion-content-height)' }
63 | },
64 | 'accordion-up': {
65 | from: { height: 'var(--radix-accordion-content-height)' },
66 | to: { height: 0 }
67 | },
68 | 'slide-from-left': {
69 | '0%': {
70 | transform: 'translateX(-100%)'
71 | },
72 | '100%': {
73 | transform: 'translateX(0)'
74 | }
75 | },
76 | 'slide-to-left': {
77 | '0%': {
78 | transform: 'translateX(0)'
79 | },
80 | '100%': {
81 | transform: 'translateX(-100%)'
82 | }
83 | }
84 | },
85 | animation: {
86 | 'slide-from-left':
87 | 'slide-from-left 0.3s cubic-bezier(0.82, 0.085, 0.395, 0.895)',
88 | 'slide-to-left':
89 | 'slide-to-left 0.25s cubic-bezier(0.82, 0.085, 0.395, 0.895)',
90 | 'accordion-down': 'accordion-down 0.2s ease-out',
91 | 'accordion-up': 'accordion-up 0.2s ease-out'
92 | }
93 | }
94 | },
95 | plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography')]
96 | }
97 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "noEmit": true,
9 | "incremental": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "baseUrl": ".",
17 | "paths": {
18 | "@/*": ["./*"]
19 | },
20 | "plugins": [
21 | {
22 | "name": "next"
23 | }
24 | ],
25 | "strictNullChecks": true
26 | },
27 | "include": [
28 | "next-env.d.ts",
29 | "next-auth.d.ts",
30 | "**/*.ts",
31 | "**/*.tsx",
32 | ".next/types/**/*.ts"
33 | ],
34 | "exclude": ["node_modules"]
35 | }
36 |
--------------------------------------------------------------------------------