├── .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 | Next.js 13 and app template Router-ready AI chatbot. 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 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?demo-title=Next.js+Chat&demo-description=A+full-featured%2C+hackable+Next.js+AI+chatbot+built+by+Vercel+Labs&demo-url=https%3A%2F%2Fchat.vercel.ai%2F&demo-image=%2F%2Fimages.ctfassets.net%2Fe5382hct74si%2F4aVPvWuTmBvzM5cEdRdqeW%2F4234f9baf160f68ffb385a43c3527645%2FCleanShot_2023-06-16_at_17.09.21.png&project-name=Next.js+Chat&repository-name=nextjs-chat&repository-url=https%3A%2F%2Fgithub.com%2Fvercel-labs%2Fai-chatbot&from=templates&skippable-integrations=1&env=OPENAI_API_KEY%2CAUTH_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 |
45 | 52 | 53 | 54 |
55 |
56 | {chat.title.length > 120 57 | ? `${chat.title.slice(0, 120)}...` 58 | : chat.title} 59 |
60 |
61 |
62 |
63 | 71 | 72 | 73 |
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 | 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /components/chat-list.tsx: -------------------------------------------------------------------------------- 1 | import { type Message } from 'ai' 2 | 3 | import { Separator } from '@/components/ui/separator' 4 | import { ChatMessage } from '@/components/chat-message' 5 | 6 | export interface ChatList { 7 | messages: Message[] 8 | } 9 | 10 | export function ChatList({ messages }: ChatList) { 11 | if (!messages.length) { 12 | return null 13 | } 14 | 15 | return ( 16 |
17 | {messages.map((message, index) => ( 18 |
19 | 20 | {index < messages.length - 1 && ( 21 | 22 | )} 23 |
24 | ))} 25 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /components/chat-message-actions.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { type Message } from 'ai' 4 | 5 | import { Button } from '@/components/ui/button' 6 | import { IconCheck, IconCopy } from '@/components/ui/icons' 7 | import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard' 8 | import { cn } from '@/lib/utils' 9 | 10 | interface ChatMessageActionsProps extends React.ComponentProps<'div'> { 11 | message: Message 12 | } 13 | 14 | export function ChatMessageActions({ 15 | message, 16 | className, 17 | ...props 18 | }: ChatMessageActionsProps) { 19 | const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 }) 20 | 21 | const onCopy = () => { 22 | if (isCopied) return 23 | copyToClipboard(message.content) 24 | } 25 | 26 | return ( 27 |
34 | 38 |
39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /components/chat-message.tsx: -------------------------------------------------------------------------------- 1 | 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 | 47 | ) : ( 48 | messages?.length > 0 && ( 49 | 57 | ) 58 | )} 59 |
60 |
61 | { 63 | await append({ 64 | id, 65 | content: value, 66 | role: 'user' 67 | }) 68 | }} 69 | input={input} 70 | setInput={setInput} 71 | isLoading={isLoading} 72 | /> 73 | 74 |
75 |
76 |
77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /components/chat-scroll-anchor.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import { useInView } from 'react-intersection-observer' 5 | 6 | import { useAtBottom } from '@/lib/hooks/use-at-bottom' 7 | 8 | interface ChatScrollAnchorProps { 9 | trackVisibility?: boolean 10 | } 11 | 12 | export function ChatScrollAnchor({ trackVisibility }: ChatScrollAnchorProps) { 13 | const isAtBottom = useAtBottom() 14 | const { ref, entry, inView } = useInView({ 15 | trackVisibility, 16 | delay: 100, 17 | rootMargin: '0px 0px -150px 0px' 18 | }) 19 | 20 | React.useEffect(() => { 21 | if (isAtBottom && trackVisibility && !inView) { 22 | entry?.target.scrollIntoView({ 23 | block: 'start' 24 | }) 25 | } 26 | }, [inView, entry, isAtBottom, trackVisibility]) 27 | 28 | return
29 | } 30 | -------------------------------------------------------------------------------- /components/chat.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useChat, type Message } from 'ai/react' 4 | 5 | import { cn } from '@/lib/utils' 6 | import { ChatList } from '@/components/chat-list' 7 | import { ChatPanel } from '@/components/chat-panel' 8 | import { EmptyScreen } from '@/components/empty-screen' 9 | import { ChatScrollAnchor } from '@/components/chat-scroll-anchor' 10 | import { useLocalStorage } from '@/lib/hooks/use-local-storage' 11 | import { 12 | Dialog, 13 | DialogContent, 14 | DialogDescription, 15 | DialogFooter, 16 | DialogHeader, 17 | DialogTitle 18 | } from '@/components/ui/dialog' 19 | import { useState } from 'react' 20 | import { Button } from './ui/button' 21 | import { Input } from './ui/input' 22 | import { toast } from 'react-hot-toast' 23 | 24 | const IS_PREVIEW = process.env.VERCEL_ENV === 'preview' 25 | export interface ChatProps extends React.ComponentProps<'div'> { 26 | initialMessages?: Message[] 27 | id?: string 28 | } 29 | 30 | export function Chat({ id, initialMessages, className }: ChatProps) { 31 | const [previewToken, setPreviewToken] = useLocalStorage( 32 | 'ai-token', 33 | null 34 | ) 35 | const [previewTokenDialog, setPreviewTokenDialog] = useState(IS_PREVIEW) 36 | const [previewTokenInput, setPreviewTokenInput] = useState(previewToken ?? '') 37 | const { messages, append, reload, stop, isLoading, input, setInput } = 38 | useChat({ 39 | initialMessages, 40 | id, 41 | body: { 42 | id, 43 | previewToken 44 | }, 45 | onResponse(response) { 46 | if (response.status === 401) { 47 | toast.error(response.statusText) 48 | } 49 | } 50 | }) 51 | return ( 52 | <> 53 |
54 | {messages.length ? ( 55 | <> 56 | 57 | 58 | 59 | ) : ( 60 | 61 | )} 62 |
63 | 73 | 74 | 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 | 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 | 38 | 39 | 40 | 41 | Are you absolutely sure? 42 | 43 | This will permanently delete your chat history and remove your data 44 | from our servers. 45 | 46 | 47 | 48 | Cancel 49 | { 52 | event.preventDefault() 53 | startTransition(async () => { 54 | const result = await clearChats() 55 | 56 | if (result && 'error' in result) { 57 | toast.error(result.error) 58 | return 59 | } 60 | 61 | setOpen(false) 62 | router.push('/') 63 | }) 64 | }} 65 | > 66 | {isPending && } 67 | Delete 68 | 69 | 70 | 71 | 72 | ) 73 | } 74 | -------------------------------------------------------------------------------- /components/empty-screen.tsx: -------------------------------------------------------------------------------- 1 | import { UseChatHelpers } from 'ai/react' 2 | 3 | import { Button } from '@/components/ui/button' 4 | import { ExternalLink } from '@/components/external-link' 5 | import { IconArrowRight } from '@/components/ui/icons' 6 | 7 | const exampleMessages = [ 8 | { 9 | heading: 'Explain technical concepts', 10 | message: `What is a "serverless function"?` 11 | }, 12 | { 13 | heading: 'Summarize an article', 14 | message: 'Summarize the following article for a 2nd grader: \n' 15 | }, 16 | { 17 | heading: 'Draft an email', 18 | message: `Draft an email to my boss about the following: \n` 19 | } 20 | ] 21 | 22 | export function EmptyScreen({ setInput }: Pick) { 23 | return ( 24 |
25 |
26 |

27 | Welcome to Next.js AI Chatbot! 28 |

29 |

30 | This is an open source AI chatbot app template built with{' '} 31 | Next.js and{' '} 32 | 33 | Vercel KV 34 | 35 | . 36 |

37 |

38 | You can start a conversation here or try the following examples: 39 |

40 |
41 | {exampleMessages.map((message, index) => ( 42 | 51 | ))} 52 |
53 |
54 |
55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /components/external-link.tsx: -------------------------------------------------------------------------------- 1 | export function ExternalLink({ 2 | href, 3 | children 4 | }: { 5 | href: string 6 | children: React.ReactNode 7 | }) { 8 | return ( 9 | 14 | {children} 15 | 27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /components/footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { cn } from '@/lib/utils' 4 | import { ExternalLink } from '@/components/external-link' 5 | 6 | export function FooterText({ className, ...props }: React.ComponentProps<'p'>) { 7 | return ( 8 |

15 | Open source AI chatbot built with{' '} 16 | Next.js and{' '} 17 | 18 | Vercel KV 19 | 20 | . 21 |

22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /components/header.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Link from 'next/link' 3 | 4 | import { cn } from '@/lib/utils' 5 | import { auth } from '@/auth' 6 | import { clearChats } from '@/app/actions' 7 | import { Button, buttonVariants } from '@/components/ui/button' 8 | import { Sidebar } from '@/components/sidebar' 9 | import { SidebarList } from '@/components/sidebar-list' 10 | import { 11 | IconGitHub, 12 | IconNextChat, 13 | IconSeparator, 14 | IconVercel 15 | } from '@/components/ui/icons' 16 | import { SidebarFooter } from '@/components/sidebar-footer' 17 | import { ThemeToggle } from '@/components/theme-toggle' 18 | import { ClearHistory } from '@/components/clear-history' 19 | import { UserMenu } from '@/components/user-menu' 20 | import { LoginButton } from '@/components/login-button' 21 | 22 | export async function Header() { 23 | const session = await auth() 24 | return ( 25 |
26 |
27 | {session?.user ? ( 28 | 29 | }> 30 | {/* @ts-ignore */} 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | ) : ( 39 | 40 | 41 | 42 | 43 | )} 44 |
45 | 46 | {session?.user ? ( 47 | 48 | ) : ( 49 | 52 | )} 53 |
54 |
55 | 75 |
76 | ) 77 | } 78 | -------------------------------------------------------------------------------- /components/login-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import { signIn } from 'next-auth/react' 5 | 6 | import { cn } from '@/lib/utils' 7 | import { Button, type ButtonProps } from '@/components/ui/button' 8 | import { IconGitHub, IconSpinner } from '@/components/ui/icons' 9 | 10 | interface LoginButtonProps extends ButtonProps { 11 | showGithubIcon?: boolean 12 | text?: string 13 | } 14 | 15 | export function LoginButton({ 16 | text = 'Login with GitHub', 17 | showGithubIcon = true, 18 | className, 19 | ...props 20 | }: LoginButtonProps) { 21 | const [isLoading, setIsLoading] = React.useState(false) 22 | return ( 23 | 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /components/markdown.tsx: -------------------------------------------------------------------------------- 1 | import { FC, memo } from 'react' 2 | import ReactMarkdown, { Options } from 'react-markdown' 3 | 4 | export const MemoizedReactMarkdown: FC = memo( 5 | ReactMarkdown, 6 | (prevProps, nextProps) => 7 | prevProps.children === nextProps.children && 8 | prevProps.className === nextProps.className 9 | ) 10 | -------------------------------------------------------------------------------- /components/prompt-form.tsx: -------------------------------------------------------------------------------- 1 | import * 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 |
{ 40 | e.preventDefault() 41 | if (!input?.trim()) { 42 | return 43 | } 44 | setInput('') 45 | await onSubmit(input) 46 | }} 47 | ref={formRef} 48 | > 49 |
50 | 51 | 52 | 59 | 60 | New Chat 61 | 62 | 63 | New Chat 64 | 65 |