Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
PointerSoftware committed May 20, 2023
0 parents commit af11626
Show file tree
Hide file tree
Showing 20 changed files with 3,172 additions and 0 deletions.
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Created by Vercel CLI
OPENAI_API_KEY=
# The temperature controls how much randomness is in the output
AI_TEMP=0.7
# The size of the response
AI_MAX_TOKENS=100
OPENAI_API_ORG=
4 changes: 4 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"root": true,
"extends": "next/core-web-vitals"
}
43 changes: 43 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# Dependencies
/node_modules
/.pnp
.pnp.js

# Testing
/coverage

# Next.js
/.next/
/out/

# Production
build
dist

# Misc
.DS_Store
*.pem

# Debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Local ENV files
.env.local
.env.development.local
.env.test.local
.env.production.local

# Vercel
.vercel

# Turborepo
.turbo

# typescript
*.tsbuildinfo

.env
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# AI Chat GPT-3 example

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).

### Components

- Next.js
- OpenAI API (ChatGPT) - streaming
- API Routes (Edge runtime) - streaming

## How to Use

You can choose from one of the following two methods to use this repository:

### One-Click Deploy

Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=vercel-examples):

[![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)

### Clone and Deploy

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:

```bash
pnpm create next-app --example https://github.com/vercel/examples/tree/main/solutions/ai-chatgpt
```

#### Set up environment variables

Rename [`.env.example`](.env.example) to `.env.local`:

```bash
cp .env.example .env.local
```

then, update `OPENAI_API_KEY` with your [OpenAI](https://beta.openai.com/account/api-keys) secret key.

Next, run Next.js in development mode:

```bash
pnpm dev
```

The app should be up and running at http://localhost:3000.

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)).
14 changes: 14 additions & 0 deletions components/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import clsx from 'clsx'

export function Button({ className, ...props }: any) {
return (
<button
className={clsx(
'inline-flex items-center gap-2 justify-center rounded-md py-2 px-3 text-sm outline-offset-2 transition active:transition-none',
'bg-zinc-600 font-semibold text-zinc-100 hover:bg-zinc-400 active:bg-zinc-800 active:text-zinc-100/70',
className
)}
{...props}
/>
)
}
136 changes: 136 additions & 0 deletions components/Chat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { useEffect, useState } from 'react'
import { Button } from './Button'
import { type ChatGPTMessage, ChatLine, LoadingChatLine } from './ChatLine'
import { useCookies } from 'react-cookie'

const COOKIE_NAME = 'nextjs-example-ai-chat-gpt3'

// default first message to display in UI (not necessary to define the prompt)
export const initialMessages: ChatGPTMessage[] = [
{
role: 'assistant',
content: 'Hi! I am a friendly AI assistant. Ask me anything!',
},
]

const InputMessage = ({ input, setInput, sendMessage }: any) => (
<div className="mt-6 flex clear-both">
<input
type="text"
aria-label="chat input"
required
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"
value={input}
onKeyDown={(e) => {
if (e.key === 'Enter') {
sendMessage(input)
setInput('')
}
}}
onChange={(e) => {
setInput(e.target.value)
}}
/>
<Button
type="submit"
className="ml-4 flex-none"
onClick={() => {
sendMessage(input)
setInput('')
}}
>
Say
</Button>
</div>
)

export function Chat() {
const [messages, setMessages] = useState<ChatGPTMessage[]>(initialMessages)
const [input, setInput] = useState('')
const [loading, setLoading] = useState(false)
const [cookie, setCookie] = useCookies([COOKIE_NAME])

useEffect(() => {
if (!cookie[COOKIE_NAME]) {
// generate a semi random short id
const randomId = Math.random().toString(36).substring(7)
setCookie(COOKIE_NAME, randomId)
}
}, [cookie, setCookie])

// send message to API /api/chat endpoint
const sendMessage = async (message: string) => {
setLoading(true)
const newMessages = [
...messages,
{ role: 'user', content: message } as ChatGPTMessage,
]
setMessages(newMessages)
const last10messages = newMessages.slice(-10) // remember last 10 messages

const response = await fetch('/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
messages: last10messages,
user: cookie[COOKIE_NAME],
}),
})

console.log('Edge function returned.')

if (!response.ok) {
throw new Error(response.statusText)
}

// This data is a ReadableStream
const data = response.body
if (!data) {
return
}

const reader = data.getReader()
const decoder = new TextDecoder()
let done = false

let lastMessage = ''

while (!done) {
const { value, done: doneReading } = await reader.read()
done = doneReading
const chunkValue = decoder.decode(value)

lastMessage = lastMessage + chunkValue

setMessages([
...newMessages,
{ role: 'assistant', content: lastMessage } as ChatGPTMessage,
])

setLoading(false)
}
}

return (
<div className="rounded-2xl border-zinc-100 lg:border lg:p-6">
{messages.map(({ content, role }, index) => (
<ChatLine key={index} role={role} content={content} />
))}

{loading && <LoadingChatLine />}

{messages.length < 2 && (
<span className="mx-auto flex flex-grow text-gray-600 clear-both">
Type a message to start the conversation
</span>
)}
<InputMessage
input={input}
setInput={setInput}
sendMessage={sendMessage}
/>
</div>
)
}
80 changes: 80 additions & 0 deletions components/ChatLine.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import clsx from 'clsx'
import Balancer from 'react-wrap-balancer'

// wrap Balancer to remove type errors :( - @TODO - fix this ugly hack
const BalancerWrapper = (props: any) => <Balancer {...props} />

type ChatGPTAgent = 'user' | 'system' | 'assistant'

export interface ChatGPTMessage {
role: ChatGPTAgent
content: string
}

// loading placeholder animation for the chat line
export const LoadingChatLine = () => (
<div className="flex min-w-full animate-pulse px-4 py-5 sm:px-6">
<div className="flex flex-grow space-x-3">
<div className="min-w-0 flex-1">
<p className="font-large text-xxl text-gray-900">
<a href="#" className="hover:underline">
AI
</a>
</p>
<div className="space-y-4 pt-4">
<div className="grid grid-cols-3 gap-4">
<div className="col-span-2 h-2 rounded bg-zinc-500"></div>
<div className="col-span-1 h-2 rounded bg-zinc-500"></div>
</div>
<div className="h-2 rounded bg-zinc-500"></div>
</div>
</div>
</div>
</div>
)

// util helper to convert new lines to <br /> tags
const convertNewLines = (text: string) =>
text.split('\n').map((line, i) => (
<span key={i}>
{line}
<br />
</span>
))

export function ChatLine({ role = 'assistant', content }: ChatGPTMessage) {
if (!content) {
return null
}
const formatteMessage = convertNewLines(content)

return (
<div
className={
role != 'assistant' ? 'float-right clear-both' : 'float-left clear-both'
}
>
<BalancerWrapper>
<div className="float-right mb-5 rounded-lg bg-white px-4 py-5 shadow-lg ring-1 ring-zinc-100 sm:px-6">
<div className="flex space-x-3">
<div className="flex-1 gap-4">
<p className="font-large text-xxl text-gray-900">
<a href="#" className="hover:underline">
{role == 'assistant' ? 'AI' : 'You'}
</a>
</p>
<p
className={clsx(
'text ',
role == 'assistant' ? 'font-semibold font- ' : 'text-gray-400'
)}
>
{formatteMessage}
</p>
</div>
</div>
</div>
</BalancerWrapper>
</div>
)
}
5 changes: 5 additions & 0 deletions next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
34 changes: 34 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "ai-chatgpt",
"repository": "https://github.com/vercel/examples.git",
"license": "MIT",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@vercel/analytics": "^0.1.11",
"@vercel/examples-ui": "^1.0.5",
"clsx": "^1.2.1",
"eventsource-parser": "^0.1.0",
"next": "latest",
"react": "latest",
"react-cookie": "^4.1.1",
"react-dom": "latest",
"react-wrap-balancer": "^0.1.5"
},
"devDependencies": {
"@types/node": "^17.0.45",
"@types/react": "latest",
"autoprefixer": "^10.4.14",
"eslint": "^8.36.0",
"eslint-config-next": "^12.3.4",
"postcss": "^8.4.21",
"tailwindcss": "^3.2.7",
"turbo": "^1.8.3",
"typescript": "^4.9.5"
}
}
Loading

1 comment on commit af11626

@vercel
Copy link

@vercel vercel bot commented on af11626 May 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.