【AIアプリ】Next.js (React) とChatGPT APIを使用したチャットボットの作り方

当サイトでは一部リンクに広告が含まれています
アイキャッチ

皆さん、ChatGPT 使っていますか?

人気が出てから間も無く、API も公開されて企業や個人でも開発に使用する方が非常に増えていますよね。
今回は Next.js と ChatGPT を組み合わせたチャットボットの作り方を丁寧に解説します。

最終的に以下のようなチャットボットを作ることができます。

Reactフレームワークの一種です。
クライアントサイドとサーバーサイドの両方で実行されるJavaScriptアプリケーションを簡単に構築できます。

今回のチュートリアルでは、Next.jsを使用して、ChatGPT APIを呼び出し、ユーザーが質問をすることができるチャットボットを作成します。

Next.js を使うには、まず JavaScript や React を習得する必要があります。

まだ自信がなければ、ロードマップに沿って改めて学習しておきましょう!

目次

今回作るアプリの特徴

使用する技術は以下の通りです。

  • Next.js (Reactのフレームワーク)
  • TypeScript
  • TailwindCSS(CSSフレームワーク)
  • axios
  • ChatGPT

前提条件

このチュートリアルを始める前に、以下のツールがインストールされていることを確認してください。

  • Node.js (最新の LTS バージョン推奨)
  • npm または yarn

プロジェクトのセットアップ

まず、以下のコマンドで新しい Next.js プロジェクトを作成します。

$ npx create-next-app@latest nextjs-chatgpt

なお、この時に create-next-app というコマンドのインストール、またはアップデートが求められる場合がありますので y と入力してください。

Need to install the following packages:
  create-next-app@13.4.9
Ok to proceed? (y)

コマンド実行後、いくつか対話式で設定項目を聞かれますので、以下のように設定しましょう
左右キーで選択した後、Enter で設定を決定します。

$ npx create-next-app@latest nextjs-chatgpt
✔ Would you like to use TypeScript? … Yes
✔ Would you like to use ESLint? … Yes
✔ Would you like to use Tailwind CSS? … Yes
✔ Would you like to use src/ directory? … No
✔ Would you like to use App Router? (recommended) … No
✔ Would you like to customize the default import alias? … No

次に、必要なライブラリをインストールします。

OpenAI と通信するためのライブラリとして openaiaxios
そして見た目を整えるために TailwindCSS 関連のライブラリをインストールします。

次のコマンドを1行ずつ実行してください。

$ cd nextjs-chatgpt
$ npm i openai axios
$ npm i -D tailwindcss postcss autoprefixer

これで必要なライブラリをインストールできました。

ChatGPT と通信する API 作成

それでは、ChatGPT と通信し、質問文と回答をやり取りするための API をプロジェクト内に作成します。

API キーを取得

事前に OpenAI の API キーを取得しておきます。
取得済みの方はそのまま使い回してOKですが、まだの方は以下の記事を参考に API キーを取得しておいてください。

環境変数に API キーを設定

API キーが悪用されないよう、環境変数として設定することで秘匿化しておきます。
プロジェクト直下に .env.local というファイルを作成し、先ほど取得した API キーを以下のように貼り付けます。

OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

上記のように環境変数を設定しておくと、Next.js では以下のような書き方により環境変数を取得することができます。

process.env.OPENAI_API_KEY

API エンドポイントを作成

まず、pages/api ディレクトリ内に chatgpt.ts というファイルを作成してください。
ターミナルで以下のコマンドを実行します。

$ touch pages/api/chatgpt.ts

作成できたら chatgpt.ts に以下のコードを記述します。

// 必要なモジュールをインポートします
import type { NextApiRequest, NextApiResponse } from 'next';
import { Configuration, OpenAIApi } from 'openai';

// 応答データの型定義を作成
type ResponseData = {
  text: string;
};

// APIリクエストに送信される body の型定義を作成
interface GenerateNextApiRequest extends NextApiRequest {
  body: {
    prompt: string;
  };
}

// OpenAI API の設定を作成
const configuration = new Configuration({
  apiKey: process.env.OPENAI_API_KEY,
});

// OpenAI API クライアントを初期化
const openai = new OpenAIApi(configuration);

// APIハンドラ関数を定義
export default async function handler(req: GenerateNextApiRequest, res: NextApiResponse<ResponseData>) {
  // リクエストからプロンプトを取得
  const prompt = req.body.prompt;

  // プロンプトが空の場合 400 エラーを返す
  if (!prompt || prompt === '') {
    res.status(400).json({ text: 'Prompt is required' });
    return;
  }

  // OpenAI API にプロンプトを送信して回答を生成
  // https://beta.openai.com/docs/api-reference/completions/create
  const aiResult = await openai.createCompletion({
    model: 'text-davinci-003',
    prompt: `${prompt}`,
    temperature: 0.9,
    max_tokens: 2048,
    frequency_penalty: 0.5,
    presence_penalty: 0,
  });

  // 生成された回答を取得
  const response = aiResult.data.choices[0].text?.trim() || 'Sorry, there was an error.';
  res.status(200).json({ text: response });
}

chatgpt.tsでは、入力された質問(プロンプト)を受け取り、OpenAI APIを使って回答を生成しています。

生成された回答はJSON形式で返されるので必要なテキストを抽出し、Reactで作成したチャットボットUIが表示します。

チャットボットUI作成

余計なCSSの削除

初期設定時点では、プロジェクト全体に適用される余計なCSSが含まれています。
そのため、プロジェクトの styles フォルダにある globals.css を開き、以下の部分だけ残して他を削除します。

@tailwind base;
@tailwind components;
@tailwind utilities;

コンポーネントの基本構造を作成

まずはチャットボットUIの基本構造だけ作成します。

pages/index.tsx を開き、以下のように書き換えます。

import React, { useState } from 'react';

const Chat = () => {
  return (
    <div className="mx-auto my-16 min-w-1/2 max-w-2xl px-4">
      <div className="bg-gray-700 rounded-md md:flex md:items-center md:justify-between py-4 px-4">
        <div className="min-w-0 flex-1">
          <h2 className="text-2xl font-bold leading-7 text-white">Next ChatBot</h2>
        </div>
      </div>
      <div className="px-4">
        {/* 質問入力フォームとボタン */}
        {/* 回答表示 */}
      </div>
    </div>
  );
}

export default Chat;

質問フォーム・回答エリア作成

では大枠ができたので、質問入力フォームと送信ボタン、ならびに返ってきた回答を表示するエリアを追加します。

pages/index.tsx を修正していきましょう。

フォームはtextarea要素を使用し、入力値はprompt、回答は answer という値でuseStateを用いて管理します。

import React, { useState } from 'react';

const Chat = () => {
  const [prompt, setPrompt] = useState('');
  const [answer, setAnswer] = useState('');

  return (
    <div className="mx-auto my-16 min-w-1/2 max-w-2xl px-4">
      <div className="bg-gray-700 rounded-md md:flex md:items-center md:justify-between py-4 px-4">
        <div className="min-w-0 flex-1">
          <h2 className="text-2xl font-bold leading-7 text-white">Next ChatBot</h2>
        </div>
      </div>
      <div className="px-4">
        <div className="py-8">
          <label
            htmlFor="email"
            className="block font-medium leading-6 text-lg text-gray-900 pb-2"
          >
            質問フォーム:
          </label>
          <div className="mt-2">
            <textarea
              id="question"
              className="block w-full rounded-md border-0 py-1.5 px-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
              placeholder="質問したいことを入力してください"
              maxLength={500}
              rows={5}
              value={prompt}
              onChange={(e) => setPrompt(e.target.value)}
            />
          </div>
        </div>
        <div className="flex justify-end mb-8">
          <button
            className="rounded-md bg-indigo-600 px-6 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
            disabled={isLoading || prompt.length === 0}
            onClick={generateAnswer}
          >
            質問する
          </button>
        </div>
        {answer && (
          <>
            <div className="font-medium leading-6 text-lg text-gray-900 pb-2">回答:</div>
            <p className="mt-2 text-gray-700">{answer}</p>
          </>
        )}
      </div>
    </div>
  );
};

export default Chat;

ChatGPT API との通信機能を追加

質問を送信して回答を取得するgenerateAnswer**関数**を作成します。

読み込み中の状態はisLoadingで管理し、エラーメッセージはerrorで管理します。
また、念のため15秒でタイムアウトするように設定していきます。

まず、pages/index.tsx の一番上で axios を読み込みます。

import axios from 'axios';

次に ChatGPT とやり取りをする generateAnswer 関数と、必要な state を用意します。

return 文の上に以下のコードを追記してください。

...

const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');

const generateAnswer = async () => {
  setIsLoading(true);
  setError('');

  try {
    const res = await axios.post('/api/chatgpt', { prompt }, { timeout: 15000 });
    setAnswer(res.data.text);
  } catch (e: any) {
    if (e.code === 'ECONNABORTED') {
      setError('タイムアウト: 15秒以内に回答が返ってきませんでした。');
    } else {
      setError('エラーが発生しました。');
    }
  } finally {
    setIsLoading(false);
  }
};

return (
  ...

では、関数をボタンに組み込みましょう。
また、読み込み中・エラーの表示もできるようにしてあげます。

button 要素以下を以下のように修正します。

...

        <div className="flex justify-end mb-8">
          <button
            className="rounded-md bg-indigo-600 px-6 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
            disabled={isLoading || prompt.length === 0}
            onClick={generateAnswer}
          >
            質問する
          </button>
        </div>
        {isLoading ? (
          <div className="font-medium leading-6 text-lg text-indigo-700 pb-2">読み込み中...</div>
        ) : (
          <>
            {error && <div className="mt-4 text-red-500">{error}</div>}
            {answer && (
              <>
                <div className="font-medium leading-6 text-lg text-gray-900 pb-2">回答:</div>
                <p className="mt-2 text-gray-700">{answer}</p>
              </>
            )}
          </>
        )}
...

最終的に pages/index.tsx は以下のようになります。

import axios from 'axios';
import React, { useState } from 'react';

const Chat = () => {
  const [prompt, setPrompt] = useState('');
  const [answer, setAnswer] = useState('');

  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState('');

  const generateAnswer = async () => {
    setIsLoading(true);
    setError('');

    try {
      const res = await axios.post('/api/chatgpt', { prompt }, { timeout: 15000 });
      setAnswer(res.data.text);
    } catch (e: any) {
      if (e.code === 'ECONNABORTED') {
        setError('タイムアウト: 15秒以内に回答が返ってきませんでした。');
      } else {
        setError('エラーが発生しました。');
      }
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div className="mx-auto my-16 min-w-1/2 max-w-2xl px-4">
      <div className="bg-gray-700 rounded-md md:flex md:items-center md:justify-between py-4 px-4">
        <div className="min-w-0 flex-1">
          <h2 className="text-2xl font-bold leading-7 text-white">Next ChatBot</h2>
        </div>
      </div>
      <div className="px-4">
        <div className="py-8">
          <label
            htmlFor="email"
            className="block font-medium leading-6 text-lg text-gray-900 pb-2"
          >
            質問フォーム:
          </label>
          <div className="mt-2">
            <textarea
              id="question"
              className="block w-full rounded-md border-0 py-1.5 px-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
              placeholder="質問したいことを入力してください"
              maxLength={500}
              rows={5}
              value={prompt}
              onChange={(e) => setPrompt(e.target.value)}
            />
          </div>
        </div>
        <div className="flex justify-end mb-8">
          <button
            className="rounded-md bg-indigo-600 px-6 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
            disabled={isLoading || prompt.length === 0}
            onClick={generateAnswer}
          >
            質問する
          </button>
        </div>
        {isLoading ? (
          <div className="font-medium leading-6 text-lg text-indigo-700 pb-2">読み込み中...</div>
        ) : (
          <>
            {error && <div className="mt-4 text-red-500">{error}</div>}
            {answer && (
              <>
                <div className="font-medium leading-6 text-lg text-gray-900 pb-2">回答:</div>
                <p className="mt-2 text-gray-700">{answer}</p>
              </>
            )}
          </>
        )}
      </div>
    </div>
  );
};

export default Chat;

動作確認

では、動作確認してみましょう。

プロジェクト直下で以下のコマンドを実行し、アプリを起動します。

$ npm run dev

起動できたら http://localhost:3000 にアクセスしてみましょう。

以下のような画面が表示されればOKです。

実際に質問文を入力してから「質問する」ボタンをクリックしましょう。

「読み込み中」と表示が変わって少し待った後、回答が表示されるはずです。

まとめ

今回は質問文に対して回答するだけですので、実際の ChatGPT と機能としては変わりません。

そのため、例えば定型的に組み込みたい条件文をあらかじめ prompt の文章として組み込んでおくことで
特定分野に強いチャットボットを作ってみるなど、ご自身の好きなようにカスタマイズしてみてください。

フルスタックエンジニアを目指している方へおすすめの記事

本記事ではフロントエンド技術にのみ焦点をあて、バックエンドについては触れませんでした。

バックエンドも学び、フルスタックエンジニアになりたい方には以下の記事もおすすめです。

目次