运用 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 库。
- 运用命令行创立一个新项目
yarn create-next-app --example https://github.com/apideck-io/next-starter-kit
- 设置你的项目名并挑选新的目录。在项目根目录中,创立一个
.env.local
文件,并增加以下内容(运用实际的key来替换YOUR_OPENAI_API_KEY
):
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 路由的代码。
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
中增加以下代码:
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
的文件,并增加以下代码:
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
的新文件并增加以下代码:
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
的新文件并增加以下代码:
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.6hu.cc/storage/2023/08/1690921062-b079070a58f342e.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.6hu.cc/storage/2023/08/1690921055-15ef4b0aae03f1a.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.6hu.cc/storage/2023/08/1690921062-b079070a58f342e.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
文件并增加以下款式:
.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 文件:
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
。在此文件中移除样板代码。
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…
全文完。