使用 Next.js, TypeScript, TailwindCSS 构建 ChatGPT 应用
先决条件
- 本机已安装 Node.js 和 npm
- 对 React 和 TypeScript 基本了解
- 一个 OpenAI API key —— 你可以从 OpenAI 官网上注册账号并生成 API key
最终效果
跟着本教程,我们将使用 OpenAI API 来创建一个简单的像 ChatGPT 一样的聊天应用。
第一步:设置项目
我们将使用来自 Apideck 的 Next.js Starter Kit 来设置我们的项目。它已经预安装了 TypeScirpt, TailwindCSS 和 Apideck Components 库。
- 使用命令行创建一个新项目
lua复制代码yarn create-next-app --example https://github.com/apideck-io/next-starter-kit
- 设置你的项目名并选择新的目录。在项目根目录中,创建一个
.env.local
文件,并添加以下内容(使用实际的key来替换YOUR_OPENAI_API_KEY
):
ini复制代码OPENAI_API_KEY=YOUR_OPENAI_API_KEY
第二步:编写 API 客户端
为了不暴露你的 OpenAI key,我们需要要创建一个 API 端点来替代从浏览器直接请求 API。按照以下步骤使用 Next.js API 路由来设置你的端点:
- 在项目中的 pages 文件夹中创建一个名为
api
的新文件夹。 - 在
api
文件夹内,创建一个名为createMessage.ts
的新的 TypeScript 文件。 - 在
createMessage.ts
文件中,我们可以使用 OpenAI SDK 或向 OpenAI API 发送 HTTP 请求,为我们与 AI 的“会话”生成新消息。在本教程中我们将直接调用 API。
以下是我们 API 路由的代码。
typescript复制代码import { NextApiRequest, NextApiResponse } from 'next'
export default async function createMessage(
req: NextApiRequest,
res: NextApiResponse
) {
const { messages } = req.body
const apiKey = process.env.OPENAI_API_KEY
const url = 'https://api.openai.com/v1/chat/completions'
const body = JSON.stringify({
messages,
model: 'gpt-3.5-turbo',
stream: false,
})
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
body,
})
const data = await response.json()
res.status(200).json({ data })
} catch (error) {
res.status(500).json({ error: error.message })
}
}
对于这个例子,我们使用了 gpt-3.5-turbo
模型,因为在撰写本文的时候它是可用的。如果你想用 GPT-4,你可以在必要的时候修改这个值。
messages
的值是一个数组,它存储了我们与 AI 基于聊天的对话中的消息。每个消息都包含一个 role
和 content
。role
可以是以下几种:
- system 这是发送给 AI 的初始提示,指示它如何行动。例如,你可以使用 “你是 ChatGPT,一个 OpenAI 训练的语言模型”。或 “你是一个使用各种编程语言和开发工具开发软件程序、网页应用和移动应用的软件工程师”。尝试不同的初始消息可以帮助你微调 AI 的行为。
- user 这代表用户的输入。例如,用户可以问,“你可以提供一个 JavaScript 函数来获取当前的天气吗?”
- assitant 这是 AI 的响应,即 API 端点返回的消息。
第三步:创建消息函数
现在端点已经准备好连接 AI 了,我们可以开始设计我们的用户界面来促进交互。首先,我们来创建 sendMessage
函数。就是这样:
- 在
utils
文件夹中创建一个新文件,名为sendMessage.ts
。 - 在
sendMessage.ts
中添加以下代码:
typescript复制代码import { ChatCompletionRequestMessage } from 'openai'
export const sendMessage = async (messages: ChatCompletionRequestMessage[]) => {
try {
const response = await fetch('/api/createMessage', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ messages }),
})
return await response.json()
} catch (error) {
console.log(error)
}
}
有了这个函数,你就可以在用户界面和 AI 之间通过 API 端点建立沟通了。
现在让我设置在 useMessages
hook 中创建新消息的逻辑。在 utils
文件夹里,创建一个名为 useMessages.tsx
的文件,并添加以下代码:
typescript复制代码import { useToast } from '@apideck/components'
import { ChatCompletionRequestMessage } from 'openai'
import {
ReactNode,
createContext,
useContext,
useEffect,
useState,
} from 'react'
import { sendMessage } from './sendMessage'
interface ContextProps {
messages: ChatCompletionRequestMessage[]
addMessage: (content: string) => Promise<void>
isLoadingAnswer: boolean
}
const ChatsContext = createContext<Partial<ContextProps>>({})
export function MessagesProvider({ children }: { children: ReactNode }) {
const { addToast } = useToast()
const [messages, setMessages] = useState<ChatCompletionRequestMessage[]>([])
const [isLoadingAnswer, setIsLoadingAnswer] = useState(false)
useEffect(() => {
const initializeChat = () => {
const systemMessage: ChatCompletionRequestMessage = {
role: 'system',
content: 'You are ChatGPT, a large language model trained by OpenAI.',
}
const welcomeMessage: ChatCompletionRequestMessage = {
role: 'assistant',
content: 'Hi, How can I help you today?',
}
setMessages([systemMessage, welcomeMessage])
}
// When no messages are present, we initialize the chat the system message and the welcome message
// We hide the system message from the user in the UI
if (!messages?.length) {
initializeChat()
}
}, [messages?.length, setMessages])
const addMessage = async (content: string) => {
setIsLoadingAnswer(true)
try {
const newMessage: ChatCompletionRequestMessage = {
role: 'user',
content,
}
const newMessages = [...messages, newMessage]
// Add the user message to the state so we can see it immediately
setMessages(newMessages)
const { data } = await sendMessage(newMessages)
const reply = data.choices[0].message
// Add the assistant message to the state
setMessages([...newMessages, reply])
} catch (error) {
// Show error when something goes wrong
addToast({ title: 'An error occurred', type: 'error' })
} finally {
setIsLoadingAnswer(false)
}
}
return (
<ChatsContext.Provider value={{ messages, addMessage, isLoadingAnswer }}>
{children}
</ChatsContext.Provider>
)
}
export const useMessages = () => {
return useContext(ChatsContext) as ContextProps
}
第四步:实现消息 UI 组件
设置好我们的函数之后,我们现在可以设计 UI 组件,该组件将使用这些函数来创建一个可交互的聊天界面。遵照以下步骤:
- 在你项目的
components
文件夹中创建一个名叫MessageForm.tsx
的新文件并添加以下代码:
typescript复制代码import { Button, TextArea } from '@apideck/components'
import { useState } from 'react'
import { useMessages } from 'utils/useMessages'
const MessageForm = () => {
const [content, setContent] = useState('')
const { addMessage } = useMessages()
const handleSubmit = async (e: any) => {
e?.preventDefault()
addMessage(content)
setContent('')
}
return (
<form
className="relative mx-auto max-w-3xl rounded-t-xl"
onSubmit={handleSubmit}
>
<div className=" supports-backdrop-blur:bg-white/95 h-[130px] rounded-t-xl border-t border-l border-r border-gray-200 border-gray-500/10 bg-white p-5 backdrop-blur dark:border-gray-50/[0.06]">
<label htmlFor="content" className="sr-only">
Your message
</label>
<TextArea
name="content"
placeholder="Enter your message here..."
rows={3}
value={content}
autoFocus
className="border-0 !p-3 text-gray-900 shadow-none ring-1 ring-gray-300/40 backdrop-blur focus:outline-none focus:ring-gray-300/80 dark:bg-gray-800/80 dark:text-white dark:placeholder-gray-400 dark:ring-0"
onChange={(e: any) => setContent(e.target.value)}
/>
<div className="absolute right-8 bottom-10">
<div className="flex space-x-3">
<Button className="" type="submit" size="small">
Send
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="ml-1 h-4 w-4"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"
/>
</svg>
</Button>
</div>
</div>
</div>
</form>
)
}
export default MessageForm
现在我们已经设置好了消息UI组件,我们需要再创建一个组件来渲染消息列表。
- 在
components
文件夹中创建一个名为MessageList.tsx
的新文件并添加以下代码:
typescript复制代码import { useMessages } from 'utils/useMessages'
const MessagesList = () => {
const { messages, isLoadingAnswer } = useMessages()
return (
<div className="mx-auto max-w-3xl pt-8">
{messages?.map((message, i) => {
const isUser = message.role === 'user'
if (message.role === 'system') return null
return (
<div
id={`message-${i}`}
className={`fade-up mb-4 flex ${
isUser ? 'justify-end' : 'justify-start'
} ${i === 1 ? 'max-w-md' : ''}`}
key={message.content}
>
{!isUser && (
<img
src="https://www.teamsmart.ai/next-assets/team/ai.jpg"
className="h-9 w-9 rounded-full"
alt="avatar"
/>
)}
<div
style={{ maxWidth: 'calc(100% - 45px)' }}
className={`group relative rounded-lg px-3 py-2 ${
isUser
? 'from-primary-700 to-primary-600 mr-2 bg-gradient-to-br text-white'
: 'ml-2 bg-gray-200 text-gray-700 dark:bg-gray-800 dark:text-gray-200'
}`}
>
{message.content.trim()}
</div>
{isUser && (
<img
src="https://www.teamsmart.ai/next-assets/profile-image.png"
className="h-9 w-9 cursor-pointer rounded-full"
alt="avatar"
/>
)}
</div>
)
})}
{isLoadingAnswer && (
<div className="mb-4 flex justify-start">
<img
src="https://www.teamsmart.ai/next-assets/team/ai.jpg"
className="h-9 w-9 rounded-full"
alt="avatar"
/>
<div className="loader relative ml-2 flex items-center justify-between space-x-1.5 rounded-full bg-gray-200 p-2.5 px-4 dark:bg-gray-800">
<span className="block h-3 w-3 rounded-full"></span>
<span className="block h-3 w-3 rounded-full"></span>
<span className="block h-3 w-3 rounded-full"></span>
</div>
</div>
)}
</div>
)
}
export default MessagesList
我们不希望展示初始系统消息,因此如果 role
是 system
的话我们返回 null
。接着,我们基于 role
是 assitant
或 user
来调整一下消息的样式。
当我们等待响应时,我们需要展示一个加载元素。为了让 loader
元素动起来,我们需要添加一些自定义的 CSS。在样式文件夹里,创建一个 globals.css
文件并添加以下样式:
css复制代码.loader span {
animation-name: bounce;
animation-duration: 1.5s;
animation-iteration-count: infinite;
animation-timing-function: ease-in-out;
}
.loader span:nth-child(2) {
animation-delay: 50ms;
}
.loader span:nth-child(3) {
animation-delay: 150ms;
}
确保在 _app.tsx
文件中导入这个 CSS 文件:
typescript复制代码import 'styles/globals.css'
import 'styles/tailwind.css'
import { ToastProvider } from '@apideck/components'
import { AppProps } from 'next/app'
export default function App({ Component, pageProps }: AppProps): JSX.Element {
return (
<ToastProvider>
<Component {...pageProps} />
</ToastProvider>
)
}
- 我们已经构建好了消息UI组件,现在可以在应用程序中使用它们了。打开
pages
目录并打开index.tsx
。在此文件中移除样板代码。
typescript复制代码import Layout from 'components/Layout'
import MessageForm from 'components/MessageForm'
import MessagesList from 'components/MessageList'
import { NextPage } from 'next'
import { MessagesProvider } from 'utils/useMessages'
const IndexPage: NextPage = () => {
return (
<MessagesProvider>
<Layout>
<MessagesList />
<div className="fixed bottom-0 right-0 left-0">
<MessageForm />
</div>
</Layout>
</MessagesProvider>
)
}
export default IndexPage
我们已经用 MessageProvider
包装了组件,因此我们可以在不同组件之间共享状态。我们还给 MessageForm
组件添加了一个 div 容器,因为它被固定在了页面底部。
第五步:运行这个聊天应用程序
现在我们可以运行这个聊天程序了。你可以这样测试你的 ChatGPT 应用:
- 确保你的开发服务已运行。(
yarn dev
) - 在浏览器中打开你的应用程序的根 URL。(
localhost:3000
) - 你应该看到 UI 已经渲染出来。在底部的文本框中输入消息并点击 Send。AI 机器人将响应你的消息。
完成代码可以在这里查看。
原文连接:www.jakeprins.com/blog/how-to…
全文完。