Skip to content

Commit af11626

Browse files
Initial commit
Created from https://vercel.com/new
0 parents  commit af11626

20 files changed

+3172
-0
lines changed

.env.example

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Created by Vercel CLI
2+
OPENAI_API_KEY=
3+
# The temperature controls how much randomness is in the output
4+
AI_TEMP=0.7
5+
# The size of the response
6+
AI_MAX_TOKENS=100
7+
OPENAI_API_ORG=

.eslintrc.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"root": true,
3+
"extends": "next/core-web-vitals"
4+
}

.gitignore

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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+
15+
# Production
16+
build
17+
dist
18+
19+
# Misc
20+
.DS_Store
21+
*.pem
22+
23+
# Debug
24+
npm-debug.log*
25+
yarn-debug.log*
26+
yarn-error.log*
27+
28+
# Local ENV files
29+
.env.local
30+
.env.development.local
31+
.env.test.local
32+
.env.production.local
33+
34+
# Vercel
35+
.vercel
36+
37+
# Turborepo
38+
.turbo
39+
40+
# typescript
41+
*.tsbuildinfo
42+
43+
.env

README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# AI Chat GPT-3 example
2+
3+
This example shows how to implement a simple chat bot using Next.js, API Routes, and [OpenAI ChatGPT API](https://beta.openai.com/docs/api-reference/completions/create).
4+
5+
### Components
6+
7+
- Next.js
8+
- OpenAI API (ChatGPT) - streaming
9+
- API Routes (Edge runtime) - streaming
10+
11+
## How to Use
12+
13+
You can choose from one of the following two methods to use this repository:
14+
15+
### One-Click Deploy
16+
17+
Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=vercel-examples):
18+
19+
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/vercel/examples/tree/main/solutions/ai-chatgpt&project-name=ai-chatgpt&repository-name=ai-chatgpt&env=OPENAI_API_KEY)
20+
21+
### Clone and Deploy
22+
23+
Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [pnpm](https://pnpm.io/installation) to bootstrap the example:
24+
25+
```bash
26+
pnpm create next-app --example https://github.com/vercel/examples/tree/main/solutions/ai-chatgpt
27+
```
28+
29+
#### Set up environment variables
30+
31+
Rename [`.env.example`](.env.example) to `.env.local`:
32+
33+
```bash
34+
cp .env.example .env.local
35+
```
36+
37+
then, update `OPENAI_API_KEY` with your [OpenAI](https://beta.openai.com/account/api-keys) secret key.
38+
39+
Next, run Next.js in development mode:
40+
41+
```bash
42+
pnpm dev
43+
```
44+
45+
The app should be up and running at http://localhost:3000.
46+
47+
Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=edge-middleware-eap) ([Documentation](https://nextjs.org/docs/deployment)).

components/Button.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import clsx from 'clsx'
2+
3+
export function Button({ className, ...props }: any) {
4+
return (
5+
<button
6+
className={clsx(
7+
'inline-flex items-center gap-2 justify-center rounded-md py-2 px-3 text-sm outline-offset-2 transition active:transition-none',
8+
'bg-zinc-600 font-semibold text-zinc-100 hover:bg-zinc-400 active:bg-zinc-800 active:text-zinc-100/70',
9+
className
10+
)}
11+
{...props}
12+
/>
13+
)
14+
}

components/Chat.tsx

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { useEffect, useState } from 'react'
2+
import { Button } from './Button'
3+
import { type ChatGPTMessage, ChatLine, LoadingChatLine } from './ChatLine'
4+
import { useCookies } from 'react-cookie'
5+
6+
const COOKIE_NAME = 'nextjs-example-ai-chat-gpt3'
7+
8+
// default first message to display in UI (not necessary to define the prompt)
9+
export const initialMessages: ChatGPTMessage[] = [
10+
{
11+
role: 'assistant',
12+
content: 'Hi! I am a friendly AI assistant. Ask me anything!',
13+
},
14+
]
15+
16+
const InputMessage = ({ input, setInput, sendMessage }: any) => (
17+
<div className="mt-6 flex clear-both">
18+
<input
19+
type="text"
20+
aria-label="chat input"
21+
required
22+
className="min-w-0 flex-auto appearance-none rounded-md border border-zinc-900/10 bg-white px-3 py-[calc(theme(spacing.2)-1px)] shadow-md shadow-zinc-800/5 placeholder:text-zinc-400 focus:border-teal-500 focus:outline-none focus:ring-4 focus:ring-teal-500/10 sm:text-sm"
23+
value={input}
24+
onKeyDown={(e) => {
25+
if (e.key === 'Enter') {
26+
sendMessage(input)
27+
setInput('')
28+
}
29+
}}
30+
onChange={(e) => {
31+
setInput(e.target.value)
32+
}}
33+
/>
34+
<Button
35+
type="submit"
36+
className="ml-4 flex-none"
37+
onClick={() => {
38+
sendMessage(input)
39+
setInput('')
40+
}}
41+
>
42+
Say
43+
</Button>
44+
</div>
45+
)
46+
47+
export function Chat() {
48+
const [messages, setMessages] = useState<ChatGPTMessage[]>(initialMessages)
49+
const [input, setInput] = useState('')
50+
const [loading, setLoading] = useState(false)
51+
const [cookie, setCookie] = useCookies([COOKIE_NAME])
52+
53+
useEffect(() => {
54+
if (!cookie[COOKIE_NAME]) {
55+
// generate a semi random short id
56+
const randomId = Math.random().toString(36).substring(7)
57+
setCookie(COOKIE_NAME, randomId)
58+
}
59+
}, [cookie, setCookie])
60+
61+
// send message to API /api/chat endpoint
62+
const sendMessage = async (message: string) => {
63+
setLoading(true)
64+
const newMessages = [
65+
...messages,
66+
{ role: 'user', content: message } as ChatGPTMessage,
67+
]
68+
setMessages(newMessages)
69+
const last10messages = newMessages.slice(-10) // remember last 10 messages
70+
71+
const response = await fetch('/api/chat', {
72+
method: 'POST',
73+
headers: {
74+
'Content-Type': 'application/json',
75+
},
76+
body: JSON.stringify({
77+
messages: last10messages,
78+
user: cookie[COOKIE_NAME],
79+
}),
80+
})
81+
82+
console.log('Edge function returned.')
83+
84+
if (!response.ok) {
85+
throw new Error(response.statusText)
86+
}
87+
88+
// This data is a ReadableStream
89+
const data = response.body
90+
if (!data) {
91+
return
92+
}
93+
94+
const reader = data.getReader()
95+
const decoder = new TextDecoder()
96+
let done = false
97+
98+
let lastMessage = ''
99+
100+
while (!done) {
101+
const { value, done: doneReading } = await reader.read()
102+
done = doneReading
103+
const chunkValue = decoder.decode(value)
104+
105+
lastMessage = lastMessage + chunkValue
106+
107+
setMessages([
108+
...newMessages,
109+
{ role: 'assistant', content: lastMessage } as ChatGPTMessage,
110+
])
111+
112+
setLoading(false)
113+
}
114+
}
115+
116+
return (
117+
<div className="rounded-2xl border-zinc-100 lg:border lg:p-6">
118+
{messages.map(({ content, role }, index) => (
119+
<ChatLine key={index} role={role} content={content} />
120+
))}
121+
122+
{loading && <LoadingChatLine />}
123+
124+
{messages.length < 2 && (
125+
<span className="mx-auto flex flex-grow text-gray-600 clear-both">
126+
Type a message to start the conversation
127+
</span>
128+
)}
129+
<InputMessage
130+
input={input}
131+
setInput={setInput}
132+
sendMessage={sendMessage}
133+
/>
134+
</div>
135+
)
136+
}

components/ChatLine.tsx

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import clsx from 'clsx'
2+
import Balancer from 'react-wrap-balancer'
3+
4+
// wrap Balancer to remove type errors :( - @TODO - fix this ugly hack
5+
const BalancerWrapper = (props: any) => <Balancer {...props} />
6+
7+
type ChatGPTAgent = 'user' | 'system' | 'assistant'
8+
9+
export interface ChatGPTMessage {
10+
role: ChatGPTAgent
11+
content: string
12+
}
13+
14+
// loading placeholder animation for the chat line
15+
export const LoadingChatLine = () => (
16+
<div className="flex min-w-full animate-pulse px-4 py-5 sm:px-6">
17+
<div className="flex flex-grow space-x-3">
18+
<div className="min-w-0 flex-1">
19+
<p className="font-large text-xxl text-gray-900">
20+
<a href="#" className="hover:underline">
21+
AI
22+
</a>
23+
</p>
24+
<div className="space-y-4 pt-4">
25+
<div className="grid grid-cols-3 gap-4">
26+
<div className="col-span-2 h-2 rounded bg-zinc-500"></div>
27+
<div className="col-span-1 h-2 rounded bg-zinc-500"></div>
28+
</div>
29+
<div className="h-2 rounded bg-zinc-500"></div>
30+
</div>
31+
</div>
32+
</div>
33+
</div>
34+
)
35+
36+
// util helper to convert new lines to <br /> tags
37+
const convertNewLines = (text: string) =>
38+
text.split('\n').map((line, i) => (
39+
<span key={i}>
40+
{line}
41+
<br />
42+
</span>
43+
))
44+
45+
export function ChatLine({ role = 'assistant', content }: ChatGPTMessage) {
46+
if (!content) {
47+
return null
48+
}
49+
const formatteMessage = convertNewLines(content)
50+
51+
return (
52+
<div
53+
className={
54+
role != 'assistant' ? 'float-right clear-both' : 'float-left clear-both'
55+
}
56+
>
57+
<BalancerWrapper>
58+
<div className="float-right mb-5 rounded-lg bg-white px-4 py-5 shadow-lg ring-1 ring-zinc-100 sm:px-6">
59+
<div className="flex space-x-3">
60+
<div className="flex-1 gap-4">
61+
<p className="font-large text-xxl text-gray-900">
62+
<a href="#" className="hover:underline">
63+
{role == 'assistant' ? 'AI' : 'You'}
64+
</a>
65+
</p>
66+
<p
67+
className={clsx(
68+
'text ',
69+
role == 'assistant' ? 'font-semibold font- ' : 'text-gray-400'
70+
)}
71+
>
72+
{formatteMessage}
73+
</p>
74+
</div>
75+
</div>
76+
</div>
77+
</BalancerWrapper>
78+
</div>
79+
)
80+
}

next-env.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/// <reference types="next" />
2+
/// <reference types="next/image-types/global" />
3+
4+
// NOTE: This file should not be edited
5+
// see https://nextjs.org/docs/basic-features/typescript for more information.

package.json

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"name": "ai-chatgpt",
3+
"repository": "https://github.com/vercel/examples.git",
4+
"license": "MIT",
5+
"private": true,
6+
"scripts": {
7+
"dev": "next dev",
8+
"build": "next build",
9+
"start": "next start",
10+
"lint": "next lint"
11+
},
12+
"dependencies": {
13+
"@vercel/analytics": "^0.1.11",
14+
"@vercel/examples-ui": "^1.0.5",
15+
"clsx": "^1.2.1",
16+
"eventsource-parser": "^0.1.0",
17+
"next": "latest",
18+
"react": "latest",
19+
"react-cookie": "^4.1.1",
20+
"react-dom": "latest",
21+
"react-wrap-balancer": "^0.1.5"
22+
},
23+
"devDependencies": {
24+
"@types/node": "^17.0.45",
25+
"@types/react": "latest",
26+
"autoprefixer": "^10.4.14",
27+
"eslint": "^8.36.0",
28+
"eslint-config-next": "^12.3.4",
29+
"postcss": "^8.4.21",
30+
"tailwindcss": "^3.2.7",
31+
"turbo": "^1.8.3",
32+
"typescript": "^4.9.5"
33+
}
34+
}

0 commit comments

Comments
 (0)