likes
comments
collection
share

Next.js + OpenAI API 快速创建 ChatGPT 聊天应用教程

作者站长头像
站长
· 阅读数 3

使用 Next.js, TypeScript, TailwindCSS 构建 ChatGPT 应用

先决条件

  • 本机已安装 Node.js 和 npm
  • 对 React 和 TypeScript 基本了解
  • 一个 OpenAI API key —— 你可以从 OpenAI 官网上注册账号并生成 API key

最终效果

跟着本教程,我们将使用 OpenAI API 来创建一个简单的像 ChatGPT 一样的聊天应用。

Next.js + OpenAI API 快速创建 ChatGPT 聊天应用教程

第一步:设置项目

我们将使用来自 Apideck 的 Next.js Starter Kit 来设置我们的项目。它已经预安装了 TypeScirpt, TailwindCSS 和 Apideck Components 库。

  1. 使用命令行创建一个新项目
yarn create-next-app --example https://github.com/apideck-io/next-starter-kit
  1. 设置你的项目名并选择新的目录。在项目根目录中,创建一个 .env.local 文件,并添加以下内容(使用实际的key来替换 YOUR_OPENAI_API_KEY):
OPENAI_API_KEY=YOUR_OPENAI_API_KEY

第二步:编写 API 客户端

为了不暴露你的 OpenAI key,我们需要要创建一个 API 端点来替代从浏览器直接请求 API。按照以下步骤使用 Next.js API 路由来设置你的端点:

  1. 在项目中的 pages 文件夹中创建一个名为 api 的新文件夹。
  2. api 文件夹内,创建一个名为 createMessage.ts 的新的 TypeScript 文件。
  3. 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 基于聊天的对话中的消息。每个消息都包含一个 rolecontentrole 可以是以下几种:

  • system 这是发送给 AI 的初始提示,指示它如何行动。例如,你可以使用 "你是 ChatGPT,一个 OpenAI 训练的语言模型"。或 "你是一个使用各种编程语言和开发工具开发软件程序、网页应用和移动应用的软件工程师"。尝试不同的初始消息可以帮助你微调 AI 的行为。
  • user 这代表用户的输入。例如,用户可以问,“你可以提供一个 JavaScript 函数来获取当前的天气吗?”
  • assitant 这是 AI 的响应,即 API 端点返回的消息。

第三步:创建消息函数

现在端点已经准备好连接 AI 了,我们可以开始设计我们的用户界面来促进交互。首先,我们来创建 sendMessage 函数。就是这样:

  1. utils 文件夹中创建一个新文件,名为 sendMessage.ts
  2. 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 组件,该组件将使用这些函数来创建一个可交互的聊天界面。遵照以下步骤:

  1. 在你项目的 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组件,我们需要再创建一个组件来渲染消息列表。

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

我们不希望展示初始系统消息,因此如果 rolesystem 的话我们返回 null。接着,我们基于 roleassitantuser 来调整一下消息的样式。

当我们等待响应时,我们需要展示一个加载元素。为了让 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>
  )
}
  1. 我们已经构建好了消息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 应用:

  1. 确保你的开发服务已运行。(yarn dev
  2. 在浏览器中打开你的应用程序的根 URL。(localhost:3000
  3. 你应该看到 UI 已经渲染出来。在底部的文本框中输入消息并点击 Send。AI 机器人将响应你的消息。

完成代码可以在这里查看。

原文连接:www.jakeprins.com/blog/how-to…

全文完。