Loading...

2024-01-16(火) 15:00

🤖 Next.jsでChatGPTとのチャットフォームを実装する

OpenAIChatGPTNext.js
Next.jsでChatGPTへの質問をできるようにし、回答を表示するところまでの実装手順を解説します。

目次

前提と注意事項

以下が前提と注意事項になります。

  • ChatGPT のアカウント(OpenAI のアカウント)登録が完了していること。
  • ChatGPT API のトークンを取得完了していること。
  • 使用した Next.js のバージョンは 14.0.4 です。
  • エラーハンドリングや細かい点は省略しているので各自追加してください。

もしまだ ChatGPT API(OpenAI API)のトークンを取得していない場合は、以下に取得手順をまとめていますので、必要な方はそちらを参考にしてみてください。

🦾 ChatGPT APIをセットアップし動作確認する

ChatGPTのAPIを使用するために必要なクレジットの購入とAPIキー生成手順を解説します。

ritaiz.com

この記事のゴール

この記事では、以下のようにフォームから質問を ChatGPT に対して投稿して、それに対する ChatGPT の回答を表示するところまでをゴールとします。

OpenAI 公式の Node.js ライブラリをインストールする

公式リポジトリに従い、以下を実行して Next.js プロジェクトに OpenAI の Node.js ライブラリをインストールします。

インストール
$ npm install --save openai
# or
$ yarn add openai

OpenAI API の環境変数を設定する

取得した OpenAI API キーを環境変数に設定します。

.env.local
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx

ChatGPT を呼び出すためのルートを作成する

ChatGPT を呼び出すためのルートを作成します。

/app/api/chatgpt/route.ts
import { NextResponse } from 'next/server';
import OpenAI from 'openai';
 
const openai = new OpenAI({
  apiKey: process.env['OPENAI_API_KEY'],
});
 
// POST /api/chatgpt
export async function POST(request: Request) {
  const data = await request.json();
  const model = 'gpt-3.5-turbo';
  const temperature = 0; //回答内容の一貫性、確信度を指定する。0に近いほどより一貫性があり、確信度の高い内容を出力する。2に近いほど、より冒険的で創造的な内容を出力する。0~2の間で指定する。
  const seed = 1234; // 乱数のシード値。同じ値を指定すると同じ結果が得られる。整数値を指定する。
  const n = 1; // 生成する回答の数。ただし、回答数が増えるほど、その分トークンを消費する。
  const max_tokens = 150; // 生成する最大トークン数
 
  try {
    const chatCompletion = await openai.chat.completions.create({
      messages: [{ role: 'user', content: data }],
      temperature: temperature,
      seed: seed,
      model: model,
      n: n,
      // max_tokens: max_tokens,
    });
 
    return NextResponse.json(
      { data: chatCompletion.choices[0].message.content },
      { status: 200 },
    );
  } catch (error) {
    console.log(error);
    return NextResponse.json({ data: error }, { status: 500 });
  }
}

上記のように、/app/api/chatgpt/route.tsを作成します。 ChatGPT へのリクエストの中で指定しているパラメータは以下のようになっています。

  • model : 使用するモデルを指定します。上記ではgpt-3.5-turboを使用しています。モデルによって料金が異なります。使用できるモデル一覧はこちら
  • temperature : 回答内容の一貫性、確信度を指定します。0 に近いほどより一貫性があり、確信度の高い内容を出力します。2 に近いほど、より冒険的で創造的な内容を出力します。0~2 の間で指定します。参考:公式ドキュメント
  • seed : 乱数のシード値を指定します。同じ値を指定すると同じ結果が得られます。整数値を指定します。
  • n : 生成する回答の数を指定します。ただし、回答数が増えるほど、その分トークンを消費します。
  • max_tokens : 生成する最大トークン数を指定します。

以上で ChatGPT へ質問を投げて回答を得るためのルートが作成できました。 あとは質問を入力するためのフォームと送信ボタンを用意して、そこからこのルートに対してリクエストを送信します。


なお、上記コードの 18 行目から 25 行目までの部分で ChatGPT に対してリクエストを送っていますが、これによって返却される応答は以下のようになっています。

ChatGPT APIからの応答内容
// chatCompletionの中身
        {
      id: 'chatcmpl-8sij30skjnynfhgsry2345b',
      object: 'chat.completion',
      created: 1705367640,
      model: 'gpt-3.5-turbo-0613',
      choices: [
        {
          index: 0,
          message: [Object],
          logprobs: null,
          finish_reason: 'stop'
        }
      ],
      usage: { prompt_tokens: 12, completion_tokens: 5, total_tokens: 17 },
      system_fingerprint: null
    }

上記のchoicesが回答になります。nパラメータを使うことで回答を複数要求することができるため、このchoicesは配列になっています。 そしてchoicesの中のmessageの中にcontentが含まれており、このcontentが ChatGPT による回答の文字列になります。 詳しくは以下の公式ドキュメントに記載されています。

Chat Completions response format

An example Chat Completions API response looks as follows

platform.openai.com

質問投稿フォームと回答表示用のコンポーネントを作成する

以下のように前節で作成したルートにリクエストを送信するためのフォームと、ChatGPT からの回答を表示するための適当なフォームを作成します。

/app/components/ChatGPTForm.tsx
'use client';
import { useState } from 'react';
 
export default function ChatGPTForm() {
  const [loading, setLoading] = useState(false);
  const [question, setQuestion] = useState('');
  const [chatMessages, setChatMessages] = useState<string[]>([]);
 
  const onSubmit = async () => {
    setLoading(true);
    chatMessages.push(question);
 
    // /api/chatgptにPOSTリクエストを送る
    const response = await fetch('/api/chatgpt', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(question),
    });
 
    if (response.status == 200) {
      const data = await response.json();
      chatMessages.push(data.data);
      setChatMessages([...chatMessages]);
      setQuestion('');
    }
 
    setLoading(false);
  };
 
  return (
    <>
      <form className='my-6 w-full md:w-4/5 mx-auto'>
        {chatMessages ? (
          <div className='flex flex-col justify-start'>
            {chatMessages.map((message, index) => {
              return (
                <div key={index} className={`text-left text-gray-800 mb-2`}>
                  {message}
                </div>
              );
            })}
          </div>
        ) : null}
        <div className='flex flex-col w-full'>
          <div className='flex flex-col sm:flex-row mb-5'>
            <div className='w-full'>
              <textarea
                className='py-2 px-3 w-full leading-tight rounded border text-gray-700 focus:outline-none focus:shadow-outline'
                value={question}
                onChange={(e) => setQuestion(e.target.value)}
              />
            </div>
          </div>
        </div>
        <div className='flex justify-center items-center mb-10 md:mb-20 sm:ml-8'>
          <button
            disabled={loading}
            onClick={onSubmit}
            className='flex justify-center items-center py-2 px-4 w-40 font-bold text-white bg-blue-800 hover:bg-blue-900 rounded-sm disabled:opacity-50 cursor-pointer'
          >
            {loading ? (
              <div role='status' className='mr-2'>
                <svg
                  aria-hidden='true'
                  className='w-8 h-8 mr-2 text-white animate-spin fill-blue-600'
                  viewBox='0 0 100 101'
                  fill='none'
                  xmlns='http://www.w3.org/2000/svg'
                >
                  <path
                    d='M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z'
                    fill='currentColor'
                  />
                  <path
                    d='M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z'
                    fill='currentFill'
                  />
                </svg>
                <span className='sr-only'>Loading...</span>
              </div>
            ) : null}{' '}
            {loading ? '処理中...' : '送信'}
          </button>
        </div>
      </form>
    </>
  );
}

動作確認

あとは以下のように適当なページにて、前節で作成したChatGPTFormを使用して動作確認を行います。

app/page.tsx
// import先は適宜置き換えてください。
import ChatGPTForm from './components/ChatGPTForm';
 
export default function Home() {
  return (
    <main>
      <div>
        <ChatGPTForm />
      </div>
    </main>
  );
}

上記のページにアクセスすると本記事冒頭に載せた動画のようなフォームが表示されます。

まとめ

ChatGPT API(OpenAI API)のドキュメントが充実しているため、チャットだけならばかなり簡単に実装できると思います。 ここではユーザーが入力した質問をそのまま ChatGPT に投げて回答を得るだけの実装を行いましたが、例えばドロップダウンやラジオボタンなどを用意して、ユーザーが選択した内容に応じて質問を生成し、それを ChatGPT に投げるといったこともできると思います。