├── .env.example
├── .eslintrc.json
├── .gitignore
├── LICENSE
├── PROMPT.md
├── 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-auth.d.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
6 | NEXTAUTH_SECRET=XXXXXXXX
7 |
8 | ## Only required for localhost
9 | NEXTAUTH_URL=http://localhost:3000
10 |
11 | ## Create a GitHub OAuth app here: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app
12 | AUTH_GITHUB_ID=XXXXXXXX
13 | AUTH_GITHUB_SECRET=XXXXXXXX
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.
--------------------------------------------------------------------------------
/PROMPT.md:
--------------------------------------------------------------------------------
1 | You are a helpful, friendly assistant whose sole purpose is answering questions about the AI Engineer Summit.
2 |
3 | What is an AI Engineer? A new category of engineer that straddles the line between ML Engineer & Software Engineer.
4 |
5 | - they are familiar with the tradeoffs between various state of the art FMs - both open source and closed, and can provide technical guidance on selection and deployment for companies ramping up their AI capabilities
6 | - they are familiar with multiple modalities of FMs, including audio, code, image, etc, and can apply them when needed
7 | - they are proficient with the latest research in prompt engineering techniques and know when to use them (and when they are unnecessary)
8 | - they are familiar with all the tooling - LangChain, LlamaIndex, Pinecone/Weaviate/Chroma, Guardrails etc - that is the state of the art for LLM enabled software
9 | - they can ship full AI apps to production - including handling real world concerns of latency, model drift, scaling, security (rate limiting, cost control, prompt injection), data privacy, optimization aspects
10 | - they are experimenting with new AI UX modalities that unlock the massive capability overhang from the last 5 years of exponential growth in LLM capabilities
11 | - they do not train their own LLMs to start with (that is for the MLEs)
12 |
13 | The AI Engineer Summit is a 2 day conference in San Francisco from Oct 8-10, where up to 1000 developers meet to learn and advance their skills and network as an AI engineer, for companies to find highly skilled AI engineers, and for new startups and large infrastructure companies alike to launch their latest capabilities.
14 |
15 | Day 1 features workshops & keynotes to catch up on the State of the Art, to contextualize & summarize the industry for both newcomers and seasoned veterans alike.
16 |
17 | Day 2 advances the industry by featuring exclusive startup & product launches, and talks that educate and inspire as to what’s possible and what’s next.
18 |
19 | Around the conference we will have the largest **expo** of AI Engineer tooling and infrastructure vendors in San Francisco, and **workshops** with the best trainers for people to level up.
20 |
21 | # Expectations
22 |
23 | ### **Attendees**
24 |
25 | A total of 800-1000 of the top AI engineers:
26 |
27 | - 500 - 700 full-access tickets: high-signal attendees (software engineers & founders)
28 | - Additional ~300 community-tier expo-only attendees. Likely to be mostly younger engineers, aspiring engineers and founders, curious full-time devs, and students.
29 |
30 | 20,000-30,000 people expected for online stream (based on past experience)
31 |
32 | # About the organizers
33 |
34 | **Benjamin Dunphy** is an entrepreneur, brand builder, and conference producer. He built the Jamstack Conf brand for Netlify and produced the first 4 in-person events. He also built the Reactathon brand and produced all 7 conferences. 2023 was his last Reactathon event; he is putting all of his energy and resources into building this AI event into the premier AI Engineer conference in the world.
35 |
36 | **Shawn Swyx Wang** is writer and co-host of Latent Space, the [leading podcast](https://hn.algolia.com/?dateRange=all&page=0&prefix=true&query=latent.space&sort=byPopularity&type=story) for AI Engineers, and a highly regarded speaker and member of the JavaScript, Cloud, and DevTools community, having worked on or led developer experience at AWS and 3 devtools unicorns (Netlify, Temporal, Airbyte). He is also the founder of smol.ai, the model distillation company.
37 |
38 | # Tickets
39 |
40 | Early bird tickets are $299 for full access, $99 for expo only.
41 |
42 | From Sep 1 onwards, full tickets will be $399, expo only $149.
43 |
44 | # Sponsors
45 |
46 | ## Presenting Sponsor Benefits
47 |
48 | - Be an intimate part of the opening keynote presentation. Content + speaker must be approved by organizers. Must be technical or technical-adjacent talk. Estimated 15 - 20 mins stage time.
49 | - Send your keynote speaker to the speaker dinner + 1 additional technical guest
50 | - Access to VIP space
51 | - Access to private meeting space
52 | - Teach a workshop on workshop day at the event venue (Tue Oct 3)
53 | - Requires content + instructor approval
54 | - Largest, centralized sponsor booth in the expo
55 | - + 1 smaller satellite booth
56 | - Logo presence
57 | - Logo on stage
58 | - Logo on conference badge
59 | - Logo in website hero “AI DevCon presented by Microsoft & SmolAI”
60 | - Logo largest & first in “Sponsors” section of the website, with up to 150-word description
61 | - Logo on the livestream
62 | - Logo shown before all the individual talk recordings in the intro prepend, plus during all picture-in-picture frames (speaker + slides)
63 | - Logo largest and first on all sponsor signs around the venue
64 | - Non-stage Video & Content
65 | - On-site video interview with your keynote speaker with professional cinematographers
66 | - Interview on the popular [Latent Space Podcast](https://www.latent.space/podcast) with your keynote speaker (audio + video recording)
67 | - 15 tickets to the conference (for employees + strategic invites)
68 | - Unlimited 50% off discount codes to share privately
69 |
70 | Presenting Sponsor Price: $250,000
71 |
72 | ## Gold Sponsor Benefits
73 |
74 | - Send your keynote speaker to the speaker dinner + 1 additional technical guest
75 | - Access to VIP space
76 | - Access to private meeting space
77 | - Teach a workshop on workshop day at the event venue (Tue Oct 8)
78 | - Requires content + instructor approval
79 | - Largest, centralized sponsor booth in the expo
80 | - + 1 smaller satellite booth
81 | - Logo presence
82 | - Logo on stage
83 | - Logo on conference badge
84 | - Logo in website hero “AI DevCon presented by Microsoft & SmolAI”
85 | - Logo largest & first in “Sponsors” section of the website, with up to 150-word description
86 | - Logo on the livestream
87 | - Logo shown before all the individual talk recordings in the intro prepend, plus during all picture-in-picture frames (speaker + slides)
88 | - Logo largest and first on all sponsor signs around the venue
89 | - Non-stage Video & Content
90 | - On-site video interview with your keynote speaker with professional cinematographers
91 | - Interview on the popular [Latent Space Podcast](https://www.latent.space/podcast) with your keynote speaker (audio + video recording)
92 | - 5 tickets to the conference (for employees + strategic invites)
93 |
94 | Presenting Sponsor Price: $50,000
--------------------------------------------------------------------------------
/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 | - [Next Auth](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&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 |
--------------------------------------------------------------------------------
/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 | import { auth } from '@/auth'
5 | import { nanoid } from '@/lib/utils'
6 |
7 | export const runtime = 'edge'
8 |
9 | const configuration = new Configuration({
10 | apiKey: process.env.OPENAI_API_KEY
11 | })
12 |
13 | const openai = new OpenAIApi(configuration)
14 |
15 | // vercel edge function cannot read from filesystem
16 | // # copy from PROMPT.md
17 | const PROMPT = `
18 | You are a helpful, friendly assistant whose sole purpose is answering questions about the AI Engineer Summit.
19 |
20 | What is an AI Engineer? A new category of engineer that straddles the line between ML Engineer & Software Engineer.
21 |
22 | - they are familiar with the tradeoffs between various state of the art FMs - both open source and closed, and can provide technical guidance on selection and deployment for companies ramping up their AI capabilities
23 | - they are familiar with multiple modalities of FMs, including audio, code, image, etc, and can apply them when needed
24 | - they are proficient with the latest research in prompt engineering techniques and know when to use them (and when they are unnecessary)
25 | - they are familiar with all the tooling - LangChain, LlamaIndex, Pinecone/Weaviate/Chroma, Guardrails etc - that is the state of the art for LLM enabled software
26 | - they can ship full AI apps to production - including handling real world concerns of latency, model drift, scaling, security (rate limiting, cost control, prompt injection), data privacy, optimization aspects
27 | - they are experimenting with new AI UX modalities that unlock the massive capability overhang from the last 5 years of exponential growth in LLM capabilities
28 | - they do not train their own LLMs to start with (that is for the MLEs)
29 |
30 | The AI Engineer Summit is a 2 day conference in San Francisco from Oct 8-10, where up to 1000 developers meet to learn and advance their skills and network as an AI engineer, for companies to find highly skilled AI engineers, and for new startups and large infrastructure companies alike to launch their latest capabilities.
31 |
32 | Day 1 features workshops & keynotes to catch up on the State of the Art, to contextualize & summarize the industry for both newcomers and seasoned veterans alike.
33 |
34 | Day 2 advances the industry by featuring exclusive startup & product launches, and talks that educate and inspire as to what’s possible and what’s next.
35 |
36 | Around the conference we will have the largest **expo** of AI Engineer tooling and infrastructure vendors in San Francisco, and **workshops** with the best trainers for people to level up.
37 |
38 | # Expectations
39 |
40 | ### **Attendees**
41 |
42 | A total of 800-1000 of the top AI engineers:
43 |
44 | - 500 - 700 full-access tickets: high-signal attendees (software engineers & founders)
45 | - Additional ~300 community-tier expo-only attendees. Likely to be mostly younger engineers, aspiring engineers and founders, curious full-time devs, and students.
46 |
47 | 20,000-30,000 people expected for online stream (based on past experience)
48 |
49 | # About the organizers
50 |
51 | **Benjamin Dunphy** is an entrepreneur, brand builder, and conference producer. He built the Jamstack Conf brand for Netlify and produced the first 4 in-person events. He also built the Reactathon brand and produced all 7 conferences. 2023 was his last Reactathon event; he is putting all of his energy and resources into building this AI event into the premier AI Engineer conference in the world.
52 |
53 | **Shawn Swyx Wang** is writer and co-host of Latent Space, the [leading podcast](https://hn.algolia.com/?dateRange=all&page=0&prefix=true&query=latent.space&sort=byPopularity&type=story) for AI Engineers, and a highly regarded speaker and member of the JavaScript, Cloud, and DevTools community, having worked on or led developer experience at AWS and 3 devtools unicorns (Netlify, Temporal, Airbyte). He is also the founder of smol.ai, the model distillation company.
54 |
55 | # Tickets
56 |
57 | Early bird tickets are $299 for full access, $99 for expo only.
58 |
59 | From Sep 1 onwards, full tickets will be $399, expo only $149.
60 |
61 | # Sponsors
62 |
63 | ## Presenting Sponsor Benefits
64 |
65 | - Be an intimate part of the opening keynote presentation. Content + speaker must be approved by organizers. Must be technical or technical-adjacent talk. Estimated 15 - 20 mins stage time.
66 | - Send your keynote speaker to the speaker dinner + 1 additional technical guest
67 | - Access to VIP space
68 | - Access to private meeting space
69 | - Teach a workshop on workshop day at the event venue (Tue Oct 3)
70 | - Requires content + instructor approval
71 | - Largest, centralized sponsor booth in the expo
72 | - + 1 smaller satellite booth
73 | - Logo presence
74 | - Logo on stage
75 | - Logo on conference badge
76 | - Logo in website hero “AI DevCon presented by Microsoft & SmolAI”
77 | - Logo largest & first in “Sponsors” section of the website, with up to 150-word description
78 | - Logo on the livestream
79 | - Logo shown before all the individual talk recordings in the intro prepend, plus during all picture-in-picture frames (speaker + slides)
80 | - Logo largest and first on all sponsor signs around the venue
81 | - Non-stage Video & Content
82 | - On-site video interview with your keynote speaker with professional cinematographers
83 | - Interview on the popular [Latent Space Podcast](https://www.latent.space/podcast) with your keynote speaker (audio + video recording)
84 | - 15 tickets to the conference (for employees + strategic invites)
85 | - Unlimited 50% off discount codes to share privately
86 |
87 | Presenting Sponsor Price: $250,000
88 |
89 | ## Gold Sponsor Benefits
90 |
91 | - Send your keynote speaker to the speaker dinner + 1 additional technical guest
92 | - Access to VIP space
93 | - Access to private meeting space
94 | - Teach a workshop on workshop day at the event venue (Tue Oct 8)
95 | - Requires content + instructor approval
96 | - Largest, centralized sponsor booth in the expo
97 | - + 1 smaller satellite booth
98 | - Logo presence
99 | - Logo on stage
100 | - Logo on conference badge
101 | - Logo in website hero “AI DevCon presented by Microsoft & SmolAI”
102 | - Logo largest & first in “Sponsors” section of the website, with up to 150-word description
103 | - Logo on the livestream
104 | - Logo shown before all the individual talk recordings in the intro prepend, plus during all picture-in-picture frames (speaker + slides)
105 | - Logo largest and first on all sponsor signs around the venue
106 | - Non-stage Video & Content
107 | - On-site video interview with your keynote speaker with professional cinematographers
108 | - Interview on the popular [Latent Space Podcast](https://www.latent.space/podcast) with your keynote speaker (audio + video recording)
109 | - 5 tickets to the conference (for employees + strategic invites)
110 |
111 | Presenting Sponsor Price: $50,000
112 | `
113 |
114 | export async function POST(req: Request) {
115 | const json = await req.json()
116 | const { messages, previewToken } = json
117 | const session = await auth()
118 |
119 | if (session == null) {
120 | return new Response('Unauthorized', {
121 | status: 401
122 | })
123 | }
124 |
125 | if (previewToken) {
126 | configuration.apiKey = previewToken
127 | }
128 |
129 | const res = await openai.createChatCompletion({
130 | model: 'gpt-3.5-turbo',
131 | messages,
132 | temperature: 0.7,
133 | stream: true
134 | })
135 |
136 | const stream = OpenAIStream(res, {
137 | async onCompletion(completion) {
138 | const title = json.messages[0].content.substring(0, 100)
139 | const userId = session?.user?.id
140 | if (userId) {
141 | const id = json.id ?? nanoid()
142 | const createdAt = Date.now()
143 | const path = `/chat/${id}`
144 | const payload = {
145 | id,
146 | title,
147 | userId,
148 | createdAt,
149 | path,
150 | messages: [
151 | {
152 | role: 'system',
153 | assistant: PROMPT
154 | },
155 | ...messages,
156 | {
157 | content: completion,
158 | role: 'assistant'
159 | }
160 | ]
161 | }
162 | await kv.hmset(`chat:${id}`, payload)
163 | await kv.zadd(`user:chat:${userId}`, {
164 | score: createdAt,
165 | member: `chat:${id}`
166 | })
167 | }
168 | }
169 | })
170 |
171 | return new StreamingTextResponse(stream)
172 | }
173 |
--------------------------------------------------------------------------------
/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/swyxio/ai-engineer/94622644657339f4df976aea96b889f4018284df/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 |
88 |
89 |
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/swyxio/ai-engineer/94622644657339f4df976aea96b889f4018284df/app/twitter-image.png
--------------------------------------------------------------------------------
/assets/fonts/Inter-Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swyxio/ai-engineer/94622644657339f4df976aea96b889f4018284df/assets/fonts/Inter-Bold.woff
--------------------------------------------------------------------------------
/assets/fonts/Inter-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swyxio/ai-engineer/94622644657339f4df976aea96b889f4018284df/assets/fonts/Inter-Regular.woff
--------------------------------------------------------------------------------
/auth.ts:
--------------------------------------------------------------------------------
1 | import NextAuth from 'next-auth'
2 | import GitHub from 'next-auth/providers/github'
3 | import CredentialsProvider from 'next-auth/providers/credentials'
4 |
5 | // We default to using GitHub for authentication for local development and production.
6 | // On Preview deployments, we use a dummy credentials provider. This allows folks to easily
7 | // test the app without having to create a custom GitHub OAuth app or change the callback URL
8 | // just to test the application on previews.
9 |
10 | // We have a custom /sign-in page for non-preview environments. In preview environments, the user
11 | // will be redirected to /api/auth/signin instead.
12 | export const {
13 | handlers: { GET, POST },
14 | auth,
15 | CSRF_experimental
16 | // @ts-ignore
17 | } = NextAuth({
18 | // @ts-ignore
19 | providers: [
20 | process.env.VERCEL_ENV === 'preview'
21 | ? CredentialsProvider({
22 | name: 'Credentials',
23 | credentials: {
24 | username: {
25 | label: 'Username',
26 | type: 'text',
27 | placeholder: 'jsmith'
28 | },
29 | password: { label: 'Password', type: 'password' }
30 | },
31 | async authorize(credentials) {
32 | return {
33 | id: 1,
34 | name: 'J Smith',
35 | email: 'jsmith@example.com',
36 | picture: 'https://i.pravatar.cc/150?u=jsmith@example.com'
37 | } as any
38 | }
39 | })
40 | : GitHub
41 | ],
42 | debugger: true,
43 | callbacks: {
44 | // @ts-ignore
45 | jwt: async ({ token, profile }) => {
46 | if (profile?.id) {
47 | token.id = profile.id
48 | token.image = profile.picture
49 | }
50 | return token
51 | },
52 | // @ts-ignore
53 | authorized({ auth }) {
54 | return !!auth?.user
55 | },
56 | trustHost: true
57 | },
58 | ...(process.env.VERCEL_ENV === 'preview'
59 | ? {
60 | pages: {
61 | signIn: '/sign-in'
62 | }
63 | }
64 | : {})
65 | })
66 |
--------------------------------------------------------------------------------
/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 |
23 | window.scrollTo({
24 | top: document.body.offsetHeight,
25 | behavior: 'smooth'
26 | })
27 | }
28 | {...props}
29 | >
30 |
31 | Scroll to bottom
32 |
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 |
35 | {isCopied ? : }
36 | Copy message
37 |
38 |
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/components/chat-message.tsx:
--------------------------------------------------------------------------------
1 | import { Message } from 'ai'
2 | import remarkGfm from 'remark-gfm'
3 | import remarkMath from 'remark-math'
4 |
5 | import { cn } from '@/lib/utils'
6 | import { CodeBlock } from '@/components/ui/codeblock'
7 | import { MemoizedReactMarkdown } from '@/components/markdown'
8 | import { IconOpenAI, IconUser } from '@/components/ui/icons'
9 | import { ChatMessageActions } from '@/components/chat-message-actions'
10 |
11 | export interface ChatMessageProps {
12 | message: Message
13 | }
14 |
15 | export function ChatMessage({ message, ...props }: ChatMessageProps) {
16 | return (
17 |
21 |
29 | {message.role === 'user' ? : }
30 |
31 |
32 | {children}
38 | },
39 | code({ node, inline, className, children, ...props }) {
40 | if (children.length) {
41 | if (children[0] == '▍') {
42 | return (
43 | ▍
44 | )
45 | }
46 |
47 | children[0] = (children[0] as string).replace('`▍`', '▍')
48 | }
49 |
50 | const match = /language-(\w+)/.exec(className || '')
51 |
52 | if (inline) {
53 | return (
54 |
55 | {children}
56 |
57 | )
58 | }
59 |
60 | return (
61 |
67 | )
68 | }
69 | }}
70 | >
71 | {message.content}
72 |
73 |
74 |
75 |
76 | )
77 | }
78 |
--------------------------------------------------------------------------------
/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 | stop()}
42 | className="bg-background"
43 | >
44 |
45 | Stop generating
46 |
47 | ) : (
48 | messages?.length > 0 && (
49 | reload()}
52 | className="bg-background"
53 | >
54 |
55 | Regenerate response
56 |
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 |
75 |
76 |
77 | Enter your OpenAI Key
78 |
79 | If you have not obtained your OpenAI API key, you can do so by{' '}
80 |
84 | signing up
85 | {' '}
86 | on the OpenAI website. This is only necessary for preview
87 | environments so that the open source community can test the app.
88 | The token will be saved to your browser's local storage under
89 | the name ai-token
.
90 |
91 |
92 | setPreviewTokenInput(e.target.value)}
96 | />
97 |
98 | {
100 | setPreviewToken(previewTokenInput)
101 | setPreviewTokenDialog(false)
102 | }}
103 | >
104 | Save Token
105 |
106 |
107 |
108 |
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 |
35 | {isPending && }
36 | Clear history
37 |
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 | setInput(message.message)}
47 | >
48 |
49 | {message.heading}
50 |
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 |
22 |
26 |
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 | {
26 | setIsLoading(true)
27 | // next-auth signIn() function doesn't work yet at Edge Runtime due to usage of BroadcastChannel
28 | signIn('github', { callbackUrl: `/` })
29 | }}
30 | disabled={isLoading}
31 | className={cn(className)}
32 | {...props}
33 | >
34 | {isLoading ? (
35 |
36 | ) : showGithubIcon ? (
37 |
38 | ) : null}
39 | {text}
40 |
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 * as React from 'react'
2 | import Link from 'next/link'
3 | import Textarea from 'react-textarea-autosize'
4 | import { UseChatHelpers } from 'ai/react'
5 |
6 | import { useEnterSubmit } from '@/lib/hooks/use-enter-submit'
7 | import { cn } from '@/lib/utils'
8 | import { Button, buttonVariants } from '@/components/ui/button'
9 | import {
10 | Tooltip,
11 | TooltipContent,
12 | TooltipTrigger
13 | } from '@/components/ui/tooltip'
14 | import { IconArrowElbow, IconPlus } from '@/components/ui/icons'
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 |
31 | React.useEffect(() => {
32 | if (inputRef.current) {
33 | inputRef.current.focus()
34 | }
35 | }, [])
36 |
37 | return (
38 |
93 | )
94 | }
95 |
--------------------------------------------------------------------------------
/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 | setShareDialogOpen(true)}
91 | >
92 |
93 | Share
94 |
95 |
96 | Share chat
97 |
98 |
99 |
100 | setDeleteDialogOpen(true)}
105 | >
106 |
107 | Delete
108 |
109 |
110 | Delete chat
111 |
112 |
113 |
114 |
115 |
116 | Share link to chat
117 |
118 | Anyone with the URL will be able to view the shared chat.
119 |
120 |
121 |
122 |
{chat.title}
123 |
124 | {formatDate(chat.createdAt)} · {chat.messages.length} messages
125 |
126 |
127 |
128 | {chat.sharePath && (
129 |
137 |
138 | {chat.sharePath}
139 |
140 | )}
141 | {
144 | startShareTransition(async () => {
145 | if (chat.sharePath) {
146 | await new Promise(resolve => setTimeout(resolve, 500))
147 | copyShareLink(chat)
148 | return
149 | }
150 |
151 | const result = await shareChat(chat)
152 |
153 | if (result && 'error' in result) {
154 | toast.error(result.error)
155 | return
156 | }
157 |
158 | copyShareLink(result)
159 | })
160 | }}
161 | >
162 | {isSharePending ? (
163 | <>
164 |
165 | Copying...
166 | >
167 | ) : (
168 | <>Copy link>
169 | )}
170 |
171 |
172 |
173 |
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 |
24 |
25 | Toggle Sidebar
26 |
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 | {
18 | startTransition(() => {
19 | setTheme(theme === 'light' ? 'dark' : 'light')
20 | })
21 | }}
22 | >
23 | {!theme ? null : theme === 'dark' ? (
24 |
25 | ) : (
26 |
27 | )}
28 | Toggle theme
29 |
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 | 'use client'
2 |
3 | import { FC, memo } from 'react'
4 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
5 | import { coldarkDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'
6 |
7 | import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard'
8 | import { IconCheck, IconCopy, IconDownload } from '@/components/ui/icons'
9 | import { Button } from '@/components/ui/button'
10 |
11 | interface Props {
12 | language: string
13 | value: string
14 | }
15 |
16 | interface languageMap {
17 | [key: string]: string | undefined
18 | }
19 |
20 | export const programmingLanguages: languageMap = {
21 | javascript: '.js',
22 | python: '.py',
23 | java: '.java',
24 | c: '.c',
25 | cpp: '.cpp',
26 | 'c++': '.cpp',
27 | 'c#': '.cs',
28 | ruby: '.rb',
29 | php: '.php',
30 | swift: '.swift',
31 | 'objective-c': '.m',
32 | kotlin: '.kt',
33 | typescript: '.ts',
34 | go: '.go',
35 | perl: '.pl',
36 | rust: '.rs',
37 | scala: '.scala',
38 | haskell: '.hs',
39 | lua: '.lua',
40 | shell: '.sh',
41 | sql: '.sql',
42 | html: '.html',
43 | css: '.css'
44 | // add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component
45 | }
46 |
47 | export const generateRandomString = (length: number, lowercase = false) => {
48 | const chars = 'ABCDEFGHJKLMNPQRSTUVWXY3456789' // excluding similar looking characters like Z, 2, I, 1, O, 0
49 | let result = ''
50 | for (let i = 0; i < length; i++) {
51 | result += chars.charAt(Math.floor(Math.random() * chars.length))
52 | }
53 | return lowercase ? result.toLowerCase() : result
54 | }
55 |
56 | const CodeBlock: FC = memo(({ language, value }) => {
57 | const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 })
58 |
59 | const downloadAsFile = () => {
60 | if (typeof window === 'undefined') {
61 | return
62 | }
63 | const fileExtension = programmingLanguages[language] || '.file'
64 | const suggestedFileName = `file-${generateRandomString(
65 | 3,
66 | true
67 | )}${fileExtension}`
68 | const fileName = window.prompt('Enter file name' || '', suggestedFileName)
69 |
70 | if (!fileName) {
71 | // User pressed cancel on prompt.
72 | return
73 | }
74 |
75 | const blob = new Blob([value], { type: 'text/plain' })
76 | const url = URL.createObjectURL(blob)
77 | const link = document.createElement('a')
78 | link.download = fileName
79 | link.href = url
80 | link.style.display = 'none'
81 | document.body.appendChild(link)
82 | link.click()
83 | document.body.removeChild(link)
84 | URL.revokeObjectURL(url)
85 | }
86 |
87 | const onCopy = () => {
88 | if (isCopied) return
89 | copyToClipboard(value)
90 | }
91 |
92 | return (
93 |
94 |
95 |
{language}
96 |
97 |
103 |
104 | Download
105 |
106 |
112 | {isCopied ? : }
113 | Copy code
114 |
115 |
116 |
117 |
135 | {value}
136 |
137 |
138 | )
139 | })
140 | CodeBlock.displayName = 'CodeBlock'
141 |
142 | export { CodeBlock }
143 |
--------------------------------------------------------------------------------
/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 |
22 |
23 |
31 |
32 |
37 |
38 |
46 |
47 |
52 |
53 |
54 |
62 |
71 |
72 |
73 |
74 |
75 |
79 |
86 |
87 |
88 | )
89 | }
90 |
91 | function IconOpenAI({ className, ...props }: React.ComponentProps<'svg'>) {
92 | return (
93 |
101 | OpenAI icon
102 |
103 |
104 | )
105 | }
106 |
107 | function IconVercel({ className, ...props }: React.ComponentProps<'svg'>) {
108 | return (
109 |
116 |
120 |
121 | )
122 | }
123 |
124 | function IconGitHub({ className, ...props }: React.ComponentProps<'svg'>) {
125 | return (
126 |
134 | GitHub
135 |
136 |
137 | )
138 | }
139 |
140 | function IconSeparator({ className, ...props }: React.ComponentProps<'svg'>) {
141 | return (
142 |
154 |
155 |
156 | )
157 | }
158 |
159 | function IconArrowDown({ className, ...props }: React.ComponentProps<'svg'>) {
160 | return (
161 |
168 |
169 |
170 | )
171 | }
172 |
173 | function IconArrowRight({ className, ...props }: React.ComponentProps<'svg'>) {
174 | return (
175 |
182 |
183 |
184 | )
185 | }
186 |
187 | function IconUser({ className, ...props }: React.ComponentProps<'svg'>) {
188 | return (
189 |
196 |
197 |
198 | )
199 | }
200 |
201 | function IconPlus({ className, ...props }: React.ComponentProps<'svg'>) {
202 | return (
203 |
210 |
211 |
212 | )
213 | }
214 |
215 | function IconArrowElbow({ className, ...props }: React.ComponentProps<'svg'>) {
216 | return (
217 |
224 |
225 |
226 | )
227 | }
228 |
229 | function IconSpinner({ className, ...props }: React.ComponentProps<'svg'>) {
230 | return (
231 |
238 |
239 |
240 | )
241 | }
242 |
243 | function IconMessage({ className, ...props }: React.ComponentProps<'svg'>) {
244 | return (
245 |
252 |
253 |
254 | )
255 | }
256 |
257 | function IconTrash({ className, ...props }: React.ComponentProps<'svg'>) {
258 | return (
259 |
266 |
267 |
268 | )
269 | }
270 |
271 | function IconRefresh({ className, ...props }: React.ComponentProps<'svg'>) {
272 | return (
273 |
280 |
281 |
282 | )
283 | }
284 |
285 | function IconStop({ className, ...props }: React.ComponentProps<'svg'>) {
286 | return (
287 |
294 |
295 |
296 | )
297 | }
298 |
299 | function IconSidebar({ className, ...props }: React.ComponentProps<'svg'>) {
300 | return (
301 |
308 |
309 |
310 | )
311 | }
312 |
313 | function IconMoon({ className, ...props }: React.ComponentProps<'svg'>) {
314 | return (
315 |
322 |
323 |
324 | )
325 | }
326 |
327 | function IconSun({ className, ...props }: React.ComponentProps<'svg'>) {
328 | return (
329 |
336 |
337 |
338 | )
339 | }
340 |
341 | function IconCopy({ className, ...props }: React.ComponentProps<'svg'>) {
342 | return (
343 |
350 |
351 |
352 | )
353 | }
354 |
355 | function IconCheck({ className, ...props }: React.ComponentProps<'svg'>) {
356 | return (
357 |
364 |
365 |
366 | )
367 | }
368 |
369 | function IconDownload({ className, ...props }: React.ComponentProps<'svg'>) {
370 | return (
371 |
378 |
379 |
380 | )
381 | }
382 |
383 | function IconClose({ className, ...props }: React.ComponentProps<'svg'>) {
384 | return (
385 |
392 |
393 |
394 | )
395 | }
396 |
397 | function IconEdit({ className, ...props }: React.ComponentProps<'svg'>) {
398 | return (
399 |
408 |
413 |
414 | )
415 | }
416 |
417 | function IconShare({ className, ...props }: React.ComponentProps<'svg'>) {
418 | return (
419 |
426 |
427 |
428 | )
429 | }
430 |
431 | function IconUsers({ className, ...props }: React.ComponentProps<'svg'>) {
432 | return (
433 |
440 |
441 |
442 | )
443 | }
444 |
445 | function IconExternalLink({
446 | className,
447 | ...props
448 | }: React.ComponentProps<'svg'>) {
449 | return (
450 |
457 |
458 |
459 | )
460 | }
461 |
462 | function IconChevronUpDown({
463 | className,
464 | ...props
465 | }: React.ComponentProps<'svg'>) {
466 | return (
467 |
474 |
475 |
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 |
32 | {user?.image ? (
33 |
38 | ) : (
39 |
40 | {user?.name ? getUserInitials(user?.name) : null}
41 |
42 | )}
43 | {user?.name}
44 |
45 |
46 |
47 |
48 | {user?.name}
49 | {user?.email}
50 |
51 |
52 |
53 |
59 | Vercel Homepage
60 |
61 |
62 |
63 |
65 | signOut({
66 | callbackUrl: '/'
67 | })
68 | }
69 | className="text-xs"
70 | >
71 | Log Out
72 |
73 |
74 |
75 |
76 | )
77 | }
78 |
--------------------------------------------------------------------------------
/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/chat']
5 | }
6 |
--------------------------------------------------------------------------------
/next-auth.d.ts:
--------------------------------------------------------------------------------
1 | import NextAuth, { DefaultSession } from 'next-auth'
2 |
3 | declare module 'next-auth' {
4 | /**
5 | * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
6 | */
7 | interface Session {
8 | user: {
9 | /** The user's postal address. */
10 | id: string
11 | } & DefaultSession['user']
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/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 | };
8 |
--------------------------------------------------------------------------------
/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 dev",
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.4cd21ea5",
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 | "pnpm": {
68 | "overrides": {
69 | "@auth/nextjs": "0.0.0-manual.223c6467"
70 | }
71 | },
72 | "packageManager": "pnpm@8.6.3"
73 | }
74 |
--------------------------------------------------------------------------------
/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/swyxio/ai-engineer/94622644657339f4df976aea96b889f4018284df/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swyxio/ai-engineer/94622644657339f4df976aea96b889f4018284df/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swyxio/ai-engineer/94622644657339f4df976aea96b889f4018284df/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 |
--------------------------------------------------------------------------------