Next.jsでOpenAI APIを叩くサイトを作る(23.9.1現在)

### はじめに

Next.jsでユーザがメッセージを入力し、OpenAI APIを通してGPT Model に送信し回答を表示するサイトを作成してみます。
Next.jsもOpenAI APIもバージョンにより、仕様が変わっており、過去の記事を参考にすると躓く所もあったので、最近の変更点をまとめたうえで、実際に作ってみます。
今後も仕様が変わることが想定されるため、本記事の参考とする場合も、公式ドキュメント等を随時確認してください。
なお、本記事の正確性や完全性について保証しませんので、ご注意ください。

### OpenAIやNext.jsの最近の変更点 

##### OpenAI APIについて

OpenAI APIは23年9月現在、23年3月発表のChat Completions APIの利用が推薦されています。
古いタイプのCompletions APIは非推薦のためなるべく利用しないほうが良いでしょう。

https://platform.openai.com/docs/guides/gpt/chat-completions-api

また、Node.js用のOpenAIライブラリが23年8月のv4で利用方法が変わりました。

https://www.npmjs.com/package/openai

v3からv4の変更点については以下の公式ドキュメントを参考にしてください。

https://github.com/openai/openai-node/discussions/217

上記から過去の技術記事などを参考にする場合は注意が必要です。

##### NEXT.js について

23年5月にApp Routerが導入されました。大きな変更点として従来のPage Routerとはディレクトリ構造が変わっています。
過去の参考記事とは、ディレクトリ構成が変わるところもありますのでご注意ください。
他にも様々な変更点があるため、詳しくは以下のドキュメントをご覧ください。

https://nextjs.org/docs/app

https://codezine.jp/article/detail/17925

### 対話サイトの作成

実際に Next.js で OpenAI API を叩き、GPT Modelと対話できる以下のようなサイトを作ります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/774545/de321b40-e419-77f5-4cdb-216ac5b94714.png)

本記事のサンプルコードはあくまでテスト用です。事業利用などには問題がありますのでご注意ください。  
また本記事の段階では、Next.jsの機能はほとんど使いません。

なお、本手順は以下を過去記事を参考にしています。

https://qiita.com/yu-Matsu/items/4ebd28d5ecf2fd8acf98#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB

https://dev.classmethod.jp/articles/first-openai-app-in-nextjs-with-tailwindcss/

#### 初期構築

- Open AI のAPI KEYを取得します。
  
https://qiita.com/Qnoir/items/8c580b51880c5899c4ab

- Next.jsのプロジェクトを作成（コンソールで`npx create-next-app`を実行）し、各オプションを選択します。
  基本的には既定値ですが、`src/` directory だけ作成するようにしています。
  ```
  npx create-next-app
  ```
  ![MicrosoftTeams-image (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/774545/eb2691db-b239-c829-f409-fa4ce8ce07e3.png)
 
詳しい手順は以下の記事を参考にしてください。

https://nextjs.org/docs/pages/api-reference/create-next-app

なお、JetBrains の IntelliJ IDEA Ultimate をお使いの方は、[こちらの方法](https://pleiades.io/help/idea/next.html)で環境の一括構築が可能です。
すこし主題とズレますが、JetBrainsのIDEは TypeScriptや Node.jsの開発がかなり効率化されるためおすすめです。

- openai ライブラリをインストールします。

```
npm install openai
```

#### 構成

今回は以下のような構成にします。

```
src/
  |- api/
  |    openai.ts -> OPEN AI API 通信用のモジュール
  |- app/
  |    chat/
  |       page.tsx -> ユーザからの入力及び回答の表示用UI
  |- page.tsx -> TOPページ（今回は使いません）
  |  ...
```

#### OPEN AI API 通信用モジュールの作成

OpenAI APIを叩くモジュール(`openai.ts`)を作成します。
page.tsxに記述することも可能ですが、今後のことも考えて別ファイルにモジュールとして作成します。
また、本来、OpenAIのAPIを叩く部分はAPI Routerなどを利用してサーバ側で実行をすることが推薦されます。詳しくは下部の注意事項をご覧ください。

プロジェクト直下に`api`フォルダを作成しその中に `openai.ts` を作成します。
```
src/
  |- api/
  |    openai.ts
```

`openai.ts` では、Chat Completions API を叩いて、回答結果を返却します。
このサンプルでは、可能なのは１対話に限定しています（過去の対話内容を利用しません）。
連続した対話を行いたい場合は、公式ドキュメントを参考に拡張してください。
また、本サンプルではモデルは`gpt-3.5-turbo`を指定しています。

```ts:openai.ts 
import OpenAI from 'openai';

const openai = new OpenAI({
  apiKey: "YOUR API KEY", // process.env["OPENAI_API_KEY"]
  dangerouslyAllowBrowser: true // ***注意*** クライアントサイドの実行を許可
});
export async function ask(content: string) {
  // メッセージを送信
  const completion = await openai.chat.completions.create({
    messages: [{ role: 'user', content: content }],
    model: "gpt-3.5-turbo",
  });
  // 回答結果を返却
  console.log(completion);
  const answer = completion.choices[0].message?.content
  return answer
}
```

:::note warn
本サンプルはあくまでテスト用です。特にAPI KEYの利用についてこのままでは問題があります。

OpenAI API をクライアント（ブラウザサイド）で叩こうとするとAPI Keyの流出につながるため以下の警告が出ます。この場合はエラー文の通り、`dangerouslyAllowBrowser` を `true` にすると実行できますが、リスクを認識した上であくまでテストなどで利用するようにしてください。くれぐれも公開システムで設定しないようにしましょう。

```
Unhandled Runtime Error
Error: It looks like you're running in a browser-like environment.

This is disabled by default, as it risks exposing your secret API credentials to attackers.
If you understand the risks and have appropriate mitigations in place,
you can set the `dangerouslyAllowBrowser` option to `true`, e.g.,

new OpenAI({ apiKey, dangerouslyAllowBrowser: true });

```

また、apiKey は環境変数などから取得するようにしましょう。
テストなどでハードコーディングする場合は、ソースをGitなどで共有しないように注意してください。
ちなみに、Next.jsの環境変数取得はデフォルトでサポートされていますが、クライアントサイドの環境変数取得などで少し癖があります。以下の公式ドキュメントをご覧ください。

https://nextjs.org/docs/pages/building-your-application/configuring/environment-variables

apiKeyの取扱いに関しての注意は以下の公式ドキュメントが詳しいです。

https://help.openai.com/en/articles/5112595-best-practices-for-api-key-safety

:::

#### 対話UIページの作成

ユーザにメッセージを入力させ回答を表示するページを作ります。
今回は `app` フォルダに `chat` フォルダを作成してその中に `page.tsx` を作成します。

```
src/
  |- app/
  |    chat/
  |       page.tsx
```

`page.tsx` では、入力用テキストエリアと送信用のボタン、回答エリアを作成します。
送信用ボタンを押すと、`@/api/openai`の`ask`を実行してメッセージをOpenAI(ChatGPT)に送信し、回答を取得し、回答エリアに表示します。

```tsx:page.ts
"use client";

import React, {useState} from "react";
import {ask} from "@/api/openai";

const Chat: React.FC = () => {
  const [prompt, setPrompt] = useState("");
  const [response, setResponse] = useState("");

  const handleSubmit = async (e:any) => {
    e.preventDefault();
    try {
      const response = await ask(
        prompt
      );
      const t = response ?? "";
      setResponse(t);
    } catch (error) {
      console.log(error);
    }
  };

  /** ユーザのメッセージ投稿と回答
   */
  return (
    <div>
        <div className="mt-3 p-3">
          <form onSubmit={handleSubmit}>
            <div className="space-y-3 bg-white px-4 py-5 sm:p-6">
              <label htmlFor="Prompt" className="block text-sm font-medium">
                質問文
              </label>
              <div>
                    <textarea
                      rows={3}
                      className="mt-1 px-2 block w-full rounded-md border-0 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:py-1.5 sm:text-sm sm:leading-6"
                      placeholder="ここに質問を入れてください"
                      value={prompt}
                      onChange={(e) => setPrompt(e.target.value)}
                    />
              </div>
              <div>
                <button
                  type="submit"
                  className="inline-flex justify-center rounded-md bg-indigo-600 py-2 px-3 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500"
                >
                  質問する
                </button>
              </div>
            </div>
          </form>
        </div>
        <div className="mt-3 p-3 md:col-span-2 md:mt-0">
          <div className="bg-white px-4 py-5 sm:p-6">
            <h2 className="text-base font-semibold leading-6 text-gray-900">質問の答え</h2>
            <p className="mt-1 text-sm text-gray-600">{response}</p>
          </div>
        </div>
    </div>
  );
};

export default Chat;
```

上記を作成後、コンソールから以下のコマンドを実行します。

```
npm run dev
```

実行が完了すると、`http://localhost:3000/`にサイトが構築されます。
`http://localhost:3000/chat` に接続すると対話ページが表示されるのでテキストエリアに質問を入力し、`質問する`を押すと、GPT Model(gpt-3.5-turbo)の回答結果が回答エリアに表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/774545/ed05af67-da99-2370-0015-ded1eec303bb.png)


### 商標

記載の会社名、製品名、サービス名等はそれぞれの会社の商標または登録商標です。