运用 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.6hu.cc/files/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/files/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/files/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

咱们不希望展现初始系统音讯,因而假如 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…

全文完。