Как разработать чат-бота с помощью React и OpenAI

Как разработать чат-бота с помощью React и OpenAI

1 марта 2023 г.

В последней статье мы создали сервер Node.js/Express, который предоставляет конечная точка /ask. Когда эта конечная точка срабатывает и мы включаем текстовое приглашение, конечная точка взаимодействует с API /completions OpenAI для создания и возврата продолжения этого текста.

Когда мы проверили это с помощью примера подсказки типа "Как погода в Дубае?", API вернул нам действительный ответ.

Сегодня мы собираемся создать пользовательский интерфейс (т. е. пользовательский интерфейс), напоминающий чат-бота, где пользователь может ввести вопрос и получить ответ от созданного нами серверного API Node.js.

Оглавление

  • Создание шаблона приложения для реагирования
  • Создание разметки & стили
  • Захват значения подсказки
  • Запуск API
  • Проксирование запроса
  • Тестирование нашего приложения
  • Заключительные мысли

Скаффолдинг приложения React

Мы будем создавать пользовательский интерфейс нашего приложения с помощью библиотеки React JavaScript. Для начала нам нужно быстро создать среду разработки React, и мы сделаем это с помощью Vite.

У меня есть планы написать электронное письмо, в котором более подробно рассматривается Vite, но в целом Vite — это инструмент сборки и сервер разработки, предназначенный для оптимизации процесса разработки современных веб-приложений. Вспомните Webpack, но с более быстрым временем сборки/запуска и несколькими дополнительными улучшениями.

Чтобы приступить к формированию нашего приложения React, мы будем следовать разделу Документация по началу работы в Vite и запустим следующее в наш терминал.

npm create vite@latest

Затем нам будет предоставлено несколько подсказок для заполнения. Мы укажем, что хотим, чтобы наш проект назывался custom_chat_gpt_frontend и чтобы он был приложением React/JavaScript.

$ npm create vite@latest
✔ Project name: custom_chat_gpt_frontend
✔ Select a framework: › React
✔ Select a variant: › JavaScript

Затем мы можем перейти в каталог проекта и запустить следующее, чтобы установить зависимости проекта.

npm install

После завершения установки зависимостей проекта мы запустим наш интерфейсный сервер с помощью:

npm run dev

Затем нам будет представлено запущенное приложение с каркасом по адресу http://localhost:5173/.

Создание разметки & стили

Мы начнем нашу работу с создания разметки (например, HTML/JSX) и стилей (например, CSS) нашего приложения.

В созданном приложении React мы заметим, что для нас было создано множество файлов и каталогов. Мы будем работать исключительно в каталоге src/. Для начала мы изменим автоматически сгенерированный код в нашем компоненте src/App.jsx, чтобы он просто возвращал «Hello world!».

import "./App.css";

function App() {
  return <h2>Hello world!</h2>;
}

export default App;

Мы удалим созданные стили CSS из нашего файла src/index.css и оставим только следующее.

html,
body,
#root {
  height: 100%;
  font-size: 14px;
  font-family: arial, sans-serif;
  margin: 0;
}

А в файле src/App.css мы удалим все изначально предоставленные классы CSS.

/* App.css CSS styles to go here */
/* ... */

Сохранив наши изменения, мы увидим сообщение «Hello world!». сообщение.

Мы не будем тратить много времени в этом электронном письме, разбирая стиль нашего пользовательского интерфейса. Подводя итог, наше финальное приложение будет содержать только один раздел поля ввода, который фиксирует то, что вводит пользователь, и ответ, возвращаемый API.

Мы стилизуем пользовательский интерфейс нашего приложения с помощью стандартного CSS. Мы вставим следующий CSS в наш файл src/App.css, который будет содержать весь необходимый нам CSS.

.app {
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background-color: rgba(0, 0, 0, 0.1);
}

.app-container {
  width: 1000px;
  max-width: 100%;
  padding: 0 20px;
  text-align: center;
}

.spotlight__wrapper {
  border-radius: 12px;
  border: 1px solid #dfe1e5;
  margin: auto;
  max-width: 600px;
  background-color: #fff;
}

.spotlight__wrapper:hover,
.spotlight__wrapper:focus {
  background-color: #fff;
  box-shadow: 0 1px 6px rgb(32 33 36 / 28%);
  border-color: rgba(223, 225, 229, 0);
}

.spotlight__input {
  display: block;
  height: 56px;
  width: 80%;
  border: 0;
  border-radius: 12px;
  outline: none;
  font-size: 1.2rem;
  color: #000;
  background-position: left 17px center;
  background-repeat: no-repeat;
  background-color: #fff;
  background-size: 3.5%;
  padding-left: 60px;
}

.spotlight__input::placeholder {
  line-height: 1.5em;
}

.spotlight__answer {
  min-height: 115px;
  line-height: 1.5em;
  letter-spacing: 0.1px;
  padding: 10px 30px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.spotlight__answer p::after {
  content: "";
  width: 2px;
  height: 14px;
  position: relative;
  top: 2px;
  left: 2px;
  background: black;
  display: inline-block;
  animation: cursor-blink 1s steps(2) infinite;
}

@keyframes cursor-blink {
  0% {
    opacity: 0;
  }
}

Теперь мы перейдем к созданию разметки/JSX нашего компонента <App />. В файле src/App.jsx мы обновим компонент, чтобы он сначала возвращал несколько элементов-оболочек <div />.

import "./App.css";

function App() {
  return (
    <div className="app">
      <div className="app-container">
        <div className="spotlight__wrapper">
          /* ... */
        </div>
      </div>
    </div>
  );
}

export default App;

Внутри наших элементов-оболочек мы разместим элемент <input /> и элемент <div /> для представления раздела ввода и раздела ответа соответственно. .

import "./App.css";
import lens from "./assets/lens.png";

function App() {
  return (
    <div className="app">
      <div className="app-container">
        <div className="spotlight__wrapper">
          <input
            type="text"
            className="spotlight__input"
            placeholder="Ask me anything..."
            style={{
              backgroundImage: `url(${lens})`,
            }}
          />
          <div className="spotlight__answer">
            Dubai is a desert city and has a warm and sunny climate throughout
          </div>
        </div>
      </div>
    </div>
  );
}

export default App;

Для элемента <input /> мы добавляем встроенное свойство стиля backgroundImage, где значением является изображение .png элемента увеличительное стекло, которое мы сохранили в нашем каталоге src/assets/. Вы можете найти копию этого изображения здесь< /а>.

После сохранения наших изменений пользовательский интерфейс приложения будет выглядеть так, как мы ожидали.

Захват значения подсказки

Следующим шагом будет захват значения prompt, которое вводит пользователь. Это необходимо сделать, поскольку мы намерены отправить это значение в API после отправки ввода. Мы зафиксируем введенное пользователем значение в свойстве состояния с пометкой prompt и инициализируем его значением undefined.

Когда пользователь вводит текст в элемент <input />, мы обновляем значение состояния prompt с помощью события onChange(). обработчик.

Мы хотим, чтобы ввод был «отправлен» в тот момент, когда пользователь нажимает клавишу «Ввод». Для этого мы воспользуемся обработчиком событий onKeyDown() и запустим функцию sendPrompt(), которую мы создадим.

В функции sendPrompt() мы вернемся раньше, если пользователь введет клавишу, отличную от клавиши "Enter". В противном случае мы получим console.log() значение состояния prompt.

Теперь, если мы введем что-то во входные данные и нажмем клавишу «Ввод», мы отобразим это входное значение в нашей консоли.

Запуск API

Последний шаг нашей реализации – запуск API, когда пользователь нажимает клавишу "Ввод" после ввода подсказки во входных данных.

Нам нужно зафиксировать два других свойства состояния, которые будут отражать информацию о нашем запросе API — состояние loading нашего запроса и ответ, возвращенный из успешного запроса. Мы инициализируем loading с false и answer с undefined.

В нашей функции sendPrompt() мы будем использовать оператор try/catch для обработки ошибок, которые могут возникнуть из-за асинхронного запроса к нашему API.

В начале блока try мы установим для свойства состояния loading значение true. Затем мы подготовим параметры запроса, а затем воспользуемся собственным методом браузера fetch() для запуска нашего запроса. Мы направим наш запрос на конечную точку с пометкой api/ask (чуть позже мы объясним почему).

Если ответ не будет успешным, мы выдадим ошибку (и console.log() ее). В противном случае мы получим значение ответа и обновим им наше свойство состояния answer.

В результате наша функция sendPrompt() в своем законченном состоянии выглядит следующим образом:

Прежде чем мы перейдем к тестированию того, что наш запрос работает должным образом, мы добавим еще несколько изменений в наш компонент.

Когда наше свойство состояния loading имеет значение true, мы хотим, чтобы ввод был отключен, и мы также хотим отображать вращающийся индикатор вместо изображения увеличительной линзы ( чтобы сообщить пользователю, что запрос "загружается").

Мы будем отображать вращающийся индикатор, условно диктуя значение стиля backgroundImage элемента <input /> в зависимости от состояния loading< /код> значение. Мы будем использовать этот спиннер GIF который мы сохраним в нашем каталоге src/assets/.

import { useState } from "react";
import "./App.css";
import loadingGif from "./assets/loading.gif";
import lens from "./assets/lens.png";

function App() {
  // ...

  return (
    <div className="app">
      <div className="app-container">
        <div className="spotlight__wrapper">
          <input
            // ...
            disabled={loading}
            style={{
              backgroundImage: loading ? `url(${loadingGif})` : `url(${lens})`,
            }}
            // ...
          />
          // ...
        </div>
      </div>
    </div>
  );
}

В разделе ответов нашей разметки мы условно добавим тег абзаца, который содержит значение {answer}, если оно определено.

import { useState } from "react";
import "./App.css";
import loadingGif from "./assets/loading.gif";
import lens from "./assets/lens.png";

function App() {
  // ...

  return (
    <div className="app">
      <div className="app-container">
        <div className="spotlight__wrapper">
          // ...
          <div className="spotlight__answer">{answer && <p>{answer}</p>}</div>
        </div>
      </div>
    </div>
  );
}

Последнее, что нам нужно сделать, это вернуть значение состояния {answer} в undefined, если пользователь когда-нибудь очистит ввод. Мы сделаем это с помощью хука React useEffect().

import { useState, useEffect } from "react";
// ...

function App() {
  const [prompt, updatePrompt] = useState(undefined);
  const [loading, setLoading] = useState(false);
  const [answer, setAnswer] = useState(undefined);

  useEffect(() => {
    if (prompt != null && prompt.trim() === "") {
      setAnswer(undefined);
    }
  }, [prompt]);

  // ...

  return (
    // ...
  );
}

export default App;

Это все изменения, которые мы внесем в наш компонент <App />! Прежде чем мы сможем протестировать наше приложение, нам нужно сделать одну небольшую вещь.

Проксирование запроса

В нашем проекте Vite React мы хотим отправлять запросы API на внутренний сервер, работающий в другом источнике (т. е. на другом порту localhost:5000), чем тот, с которого обслуживается веб-приложение. (localhost:5173). Однако из-за политики одного и того же источника, применяемой веб-браузерами, такие запросы может быть заблокирован по соображениям безопасности.

Чтобы обойти это при работе в среде разработки, мы можем настроить обратный прокси-сервер на внешнем сервере (например, на нашем сервере Vite) для пересылки запросов на внутренний сервер, эффективно делая API внутреннего сервера доступным из того же источника, что и внешний интерфейс. приложение.

Vite позволяет нам сделать это, изменив значение server.proxy в файле конфигурации Vite (то есть vite.config.js).

В файле vite.config.js, который уже существует в нашем проекте, мы укажем в качестве прокси-сервера конечную точку /api. Конечная точка /api будет переадресована на http://localhost:5000.

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      "/api": {
        target: "http://localhost:5000",
        changeOrigin: true,
        rewrite: (path) => path.replace(/^/api/, ""),
      },
    },
  },
});

Теперь, когда наш внешний интерфейс делает запрос к /api/ask, он перенаправляется на внутренний сервер, работающий по адресу http://localhost:5000/ask. р>

Тестирование нашего приложения

Мы закончили создание нашего простого приложения для чат-бота. Давайте проверим нашу работу!

Во-первых, нам нужно, чтобы наш сервер Node/Express из последнего учебника работал. Мы перейдем в этот каталог проекта и запустим node index.js, чтобы начать работу.

$ custom_chat_gpt: node index.js

Мы сохраним наши изменения в интерфейсном приложении и перезапустим интерфейсный сервер.

$ custom_chat_gpt_frontend: npm run dev

В пользовательском интерфейсе нашего внешнего приложения мы предоставим подсказку и нажмем «Ввод». Должен пройти короткий период загрузки, прежде чем ответ будет заполнен и показан нам!

Мы даже можем попытаться задать нашему чат-боту что-то более конкретное, например "Какие пончики самые вкусные в Торонто, Канада?".

Довольно забавно, когда я ищу пекарню Castro's Lounge здесь, в Торонто, я получаю бар и концертная площадка, а не пекарня. И застекленный & Confused Donuts находится в Syracuse, New York — not Toronto. Похоже, наш чат-бот еще можно немного улучшить — мы поговорим об этом в нашем последнем учебном письме из этой серии на следующей неделе 🙂.

Заключительные мысли


Исходная статья была разослана свежим интерфейсом информационный бюллетень.


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE