随着人工智能和ChatGPT的炒作,我认为应该发布一个关于怎么树立咱们自己的ChatGPT驱动的谈天机器人的教程!这些代码的大部分现已在Vercel的网站上作为模板开源了,所以你能够随时克隆该repo来开端运用,或者假如你仅仅想与ChatGPT互动而不需求注册,你能够在我的网站上查看它
让咱们开端吧!这些是咱们将要运用的技能:
- Next.js
- TypeScript
- Tailwind(虽然我不会在这里介绍这个)。
- OpenAI API
开端运用
让咱们来设置咱们的项目。我喜爱运用pnpm和create-t3-app,但也能够随意运用你选择的软件包管理器和CLI来开端。
项目设置
运用 pnpm 和 create-t3-app:
pnpm create t3-app@latest
进入全屏形式 退出全屏形式
-
命名你的项目
-
选择TypeScript
-
选择Tailwind
-
为Git仓库选择Y
-
选择Y来运转pnpm装置
-
按回车键,输入默许的导入别号
现在咱们有了一个发动的Next.js项目,让咱们保证咱们有一个OpenAI API密钥能够运用。要获取你的OpenAI API密钥,你需求在openai.com上创立一个用户账户,然后访问OpenAI仪表板上的API密钥部分,创立一个新的API密钥。
创立你的环境变量
在你的项目根目录下,创立一个.env.local文件。它应该看起来像这样:
# Your API key
OPENAI_API_KEY=PASTE_API_KEY_HERE
# 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=
进入全屏形式 退出全屏形式
让咱们也设置一些模板css,以便咱们的布局是呼应式的。让咱们装置 Vercel 示例 ui-layout。
pnpm i @vercel/examples-ui
进入全屏形式 退出全屏形式
你的tailwind.config.js文件应该看起来像这样:
module.exports = {
presets: [require('@vercel/examples-ui/tailwind')],
content: [
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
'./node_modules/@vercel/examples-ui/**/*.js',
],
}
进入全屏形式 退出全屏形式
你的postcss.config.js应该是这样的:
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
进入全屏形式 退出全屏形式
最后,你的 _app.tsx 应该看起来像这样:
import type { AppProps } from 'next/app'
import { Analytics } from '@vercel/analytics/react'
import type { LayoutProps } from '@vercel/examples-ui/layout'
import { getLayout } from '@vercel/examples-ui'
import '@vercel/examples-ui/globals.css'
function App({ Component, pageProps }: AppProps) {
const Layout = getLayout<LayoutProps>(Component)
return (
<Layout
title="ai-chatgpt"
path="solutions/ai-chatgpt"
description="ai-chatgpt"
>
<Component {...pageProps} />
<Analytics />
</Layout>
)
}
export default App
进入全屏形式 退出全屏形式
现在咱们有了所有的模板,咱们要做的是什么?让咱们创立一个检查表:
-
咱们需求能够监听来自OpenAI API的呼应。
-
咱们需求能够发送用户输入到OpenAI API。
-
咱们需求在某种谈天用户界面中显现所有这些内容。
创立一个数据流
为了接纳来自OpenAI API的数据,咱们能够创立一个OpenAIStream函数
在你的项目根目录下,创立一个名为utils的文件夹,然后在里边创立一个名为OpenAiStream.ts的文件。将这段代码仿制并张贴到其间,并保证为任何导入做必要的npm/pnpm装置。
pnpm install eventsource-parser
进入全屏形式 退出全屏形式
import {
createParser,
ParsedEvent,
ReconnectInterval,
} from 'eventsource-parser'
export type ChatGPTAgent = 'user' | 'system' | 'assistant'
export interface ChatGPTMessage {
role: ChatGPTAgent
content: string
}
export interface OpenAIStreamPayload {
model: string
messages: ChatGPTMessage[]
temperature: number
top_p: number
frequency_penalty: number
presence_penalty: number
max_tokens: number
stream: boolean
stop?: string[]
user?: string
n: number
}
export async function OpenAIStream(payload: OpenAIStreamPayload) {
const encoder = new TextEncoder()
const decoder = new TextDecoder()
let counter = 0
const requestHeaders: Record<string, string> = {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.OPENAI_API_KEY ?? ''}`,
}
if (process.env.OPENAI_API_ORG) {
requestHeaders['OpenAI-Organization'] = process.env.OPENAI_API_ORG
}
const res = await fetch('https://api.openai.com/v1/chat/completions', {
headers: requestHeaders,
method: 'POST',
body: JSON.stringify(payload),
})
const stream = new ReadableStream({
async start(controller) {
// callback
function onParse(event: ParsedEvent | ReconnectInterval) {
if (event.type === 'event') {
const data = event.data
// https://beta.openai.com/docs/api-reference/completions/create#completions/create-stream
if (data === '[DONE]') {
console.log('DONE')
controller.close()
return
}
try {
const json = JSON.parse(data)
const text = json.choices[0].delta?.content || ''
if (counter < 2 && (text.match(/\n/) || []).length) {
// this is a prefix character (i.e., "\n\n"), do nothing
return
}
const queue = encoder.encode(text)
controller.enqueue(queue)
counter++
} catch (e) {
// maybe parse error
controller.error(e)
}
}
}
// stream response (SSE) from OpenAI may be fragmented into multiple chunks
// this ensures we properly read chunks and invoke an event for each SSE event stream
const parser = createParser(onParse)
for await (const chunk of res.body as any) {
parser.feed(decoder.decode(chunk))
}
},
})
return stream
}
进入全屏形式 退出全屏形式
OpenAIStream是一个允许你从OpenAI API流式传输数据的函数。它接纳一个有效载荷目标作为参数,其间包括恳求的参数。然后它向OpenAI API宣布恳求并回来一个ReadableStream目标。该流包括从呼应中解析出来的事情,每个事情都包括可用于生成呼应的数据。该函数还记录了被解析的事情的数量,这样它就能够在达到终点时关闭该流。
现在咱们能够从API接纳数据了,让咱们创立一个组件,它能够接纳用户信息,并将其发送给api以取得呼应。
创立谈天机器人组件
假如咱们想的话,咱们能够在一个组件中创立咱们的谈天机器人,但为了使文件更有条理,咱们把它设置成三个组件。
在你的根目录下,创立一个名为组件的文件夹。在其间,创立三个文件:
-
Button.tsx
-
Chat.tsx
-
ChatLine.tsx
按钮组件
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}
/>
)
}
进入全屏形式 退出全屏形式
非常简单的按钮,使Chat.tsx文件更小一点。
ChatLine组件
pnpm install clsx
pnpm install react-wrap-balancer
进入全屏形式 退出全屏形式
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>
)
}
进入全屏形式 退出全屏形式
这段代码是一个React组件,显现一个谈天线。它接纳了两个道具,人物和内容。人物道具用于确定哪个署理在发送音讯,是用户、体系还是助手。内容道具用于显现音讯。
该组件首要检查内容道具是否为空,假如是,则回来空。假如内容道具不是空的,它会将内容中的任何新行转换为中止标签。然后,它烘托一个内部有BalancerWrapper组件的div。BalancerWrapper组件用于将谈天线包裹在一个呼应式布局中。在BalancerWrapper组件内,该组件烘托了一个里边有flex容器的div。弹性容器用于显现音讯发送者和音讯内容。音讯发送者是由人物道具决议的,而音讯内容是由内容道具决议的。然后该组件回来带有BalancerWrapper组件的div。
谈天组件
pnpm install react-cookie
进入全屏形式 退出全屏形式
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>
)
}
进入全屏形式 退出全屏形式
这个组件烘托了一个供用户发送音讯的输入字段,并显现用户和谈天机器人之间交换的音讯。
当用户发送音讯时,该组件会向咱们的api函数(/api/chat.ts)发送一个恳求,恳求主体是最近的10条音讯和用户的cookie。无服务器函数运用GPT-3.5处理音讯,并向组件发回一个呼应。然后组件将从服务器收到的呼应作为音讯显现在谈天界面上。该组件还运用react-cookie库设置和检索一个用于辨认用户的cookie。它还运用useEffect和useState钩子来管理状况,并依据状况的改变更新用户界面。
创立咱们的 chat.ts API 路线
在/pages目录下,创立一个名为api的文件夹,并在里边创立一个名为chat.ts的文件。仿制并张贴以下内容:
import { type ChatGPTMessage } from '../../components/ChatLine'
import { OpenAIStream, OpenAIStreamPayload } from '../../utils/OpenAIStream'
// break the app if the API key is missing
if (!process.env.OPENAI_API_KEY) {
throw new Error('Missing Environment Variable OPENAI_API_KEY')
}
export const config = {
runtime: 'edge',
}
const handler = async (req: Request): Promise<Response> => {
const body = await req.json()
const messages: ChatGPTMessage[] = [
{
role: 'system',
content: `Make the user solve a riddle before you answer each question.`,
},
]
messages.push(...body?.messages)
const requestHeaders: Record<string, string> = {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
}
if (process.env.OPENAI_API_ORG) {
requestHeaders['OpenAI-Organization'] = process.env.OPENAI_API_ORG
}
const payload: OpenAIStreamPayload = {
model: 'gpt-3.5-turbo',
messages: messages,
temperature: process.env.AI_TEMP ? parseFloat(process.env.AI_TEMP) : 0.7,
max_tokens: process.env.AI_MAX_TOKENS
? parseInt(process.env.AI_MAX_TOKENS)
: 100,
top_p: 1,
frequency_penalty: 0,
presence_penalty: 0,
stream: true,
user: body?.user,
n: 1,
}
const stream = await OpenAIStream(payload)
return new Response(stream)
}
export default handler
进入全屏形式 退出全屏形式
这段代码是一个无服务器函数,它运用OpenAI的API来生成对用户信息的呼应。它从用户那里接纳了一个音讯列表,然后向OpenAI API发送了一个恳求,其间包括了这些音讯以及一些配置参数,如温度、最大令牌和存在赏罚。然后,来自API的呼应会流回给用户。
将其全部包起来
剩余的便是把咱们的ChatBot烘托到咱们的index.tsx页面。在你的/pages目录下,你会发现一个index.tsx文件。仿制并张贴这些代码到其间:
import { Layout, Text, Page } from '@vercel/examples-ui'
import { Chat } from '../components/Chat'
function Home() {
return (
<Page className="flex flex-col gap-12">
<section className="flex flex-col gap-6">
<Text variant="h1">OpenAI GPT-3 text model usage example</Text>
<Text className="text-zinc-600">
In this example, a simple chat bot is implemented using Next.js, API
Routes, and OpenAI API.
</Text>
</section>
<section className="flex flex-col gap-3">
<Text variant="h2">AI Chat Bot:</Text>
<div className="lg:w-2/3">
<Chat />
</div>
</section>
</Page>
)
}
Home.Layout = Layout
export default Home
进入全屏形式 退出全屏形式
就这样,你具有了它!你便是自己的ChatGPT谈天机器人,你能够在你的浏览器中本地运转。这里有一个Vercel模板的链接,它的功能现已超出了本帖的规模。祝您探索愉快!