チャットボットやバーチャルアシスタントの利用が拡大する中、多くの企業や開発者がAIを搭載した独自のチャットボット作成の方法を模索しています。ChatGPTはそのようなチャットボットの一つで、OpenAIによって作成され、人間のように会話をし、様々な質問に答えることができます。

構築するもの

今回の記事では、ReactとOpenAI APIを使用してChatGPTクローンアプリケーションを構築する方法をご紹介します。知的好奇心を満たすプロジェクトに挑戦したい方は、ReactとOpenAIの世界に飛び込んでみてはいかがでしょうか。

また、GitHubのリポジトリからKinstaのアプリケーションホスティングプラットフォームに直接デプロイする方法も扱います。Kinstaでは、プロジェクトを素早く本番環境に移行できるように、無料の.kinsta.appドメインをご用意しています。また、Kinstaの無料利用枠やホビープランを利用すれば、お気軽にお試し可能です。

ChatGPTクローンアプリのライブデモはこちらでご確認ください。

ChatGPTクローンアプリ
ChatGPTクローンアプリ

このプロジェクトについての詳細は、GitHubのリポジトリをご覧ください。

また、スタイル、Font Awesome CDNリンク、OpenAIパッケージ、基本構造などの要素を含むReactアプリケーションスタータープロジェクトを複製して、使い始めることも可能です。

前提条件

この記事は、真似しながら読み進められるように設計されています。そのため、並行してコーディングできるように、以下のものを用意・理解しておくことをおすすめします。

OpenAI APIとは

OpenAI APIは、GPT-3などのOpenAIの言語モデルにAPIでアクセスすることを可能にしたクラウド型プラットフォームです。これにより、開発者はモデルの開発やゼロからの学習を行うことなく、テキスト補完、感情(意図)分析、要約、翻訳などの自然言語処理機能をアプリケーションに追加することができます。

OpenAI APIを利用するには、OpenAIのウェブサイトでアカウントを作成し、APIキーを取得する必要があります。APIキーは、APIリクエストの認証や利用状況の把握に使用されます。

APIキーを取得すると、これを利用して言語モデルにテキストを送信し、レスポンスを受け取ることができます。

Reactの特徴

Reactは、ユーザーインターフェース構築を支える人気のJavaScriptライブラリです。2022年のStack Overflowの開発者調査によると、2番目によく使われているウェブ技術で、市場シェアの42.62%を占めています。

Reactを使って、ユーザーインターフェースのさまざまな部分を表す宣言型コンポーネントを作成できます。そのコンポーネントは、JavaScriptとHTMLを組み合わせたJSXと呼ばれる構文を用いて定義します。

コンポーネントライブラリやキットが豊富に存在し、OpenAI APIなどのAPIと簡単に連携・統合して、複雑なチャットインターフェースを構築可能です。このような理由で、ChatGPTクローンアプリの構築に最適な選択肢となっています。

React開発環境の構築

構築を始めるにあたり、create-react-appが利用できます。前提条件として、PCにNode.jsがインストールされている必要があります。Nodeがインストールされていることを確認するには、ターミナルで以下のコマンドを実行します。

node -v

これでバージョン情報が表示されれば、存在することになります。npxを使うには、Nodeのバージョンがv14.0.0以上、NPMのバージョンがv5.6以上である必要があります。そうでなければ、npm update -gを実行してアップデートしてください。その後、以下のコマンドを実行して、Reactプロジェクトをセットアップします。

npx create-react-app chatgpt-clone

補足)今回作成するアプリケーション名として「chatgpt-clone」を採用していますが、お好きな名前に変更可能です。

インストール作業には数分かかる場合があります。完了したら、ディレクトリに移動して、以下のコマンドを使用し、(Node.jsからOpenAI APIへのアクセスを助ける)Node.js OpenAIパッケージをインストールします。

npm install openai

これで、npm startを実行すると、localhost:3000でアプリケーションの動作を確認することができます。

create-react-appコマンドを使用してReactプロジェクトを作成すると、自動的にフォルダ構造が設定されます。主要なフォルダは、srcです─ここで開発を行います。このフォルダには、デフォルトで多くのファイルが含まれていますが、App.jsindex.jsindex.cssファイルだけ特に役割を理解しておくことをおすすめします。

  1. App.js:Reactアプリケーションのメインコンポーネントです。通常、アプリケーション内の他のすべてのコンポーネントをレンダリングするトップレベルのコンポーネントを扱うことになります。
  2. index.js:Reactアプリケーションのエントリポイントです。アプリを開いたときに最初に読み込まれるファイルであり、App.jsコンポーネントをブラウザにレンダリングする役割を担います。
  3. index.css:このファイルは、Reactアプリケーションの全体的なスタイルとレイアウトを定義するものです。

ReactとOpenAI APIでChatGPTクローンを構築する

ChatGPTクローンアプリケーションは、アプリケーションの理解や保守を容易にするために、2つのコンポーネントで構成することにします。その2つのコンポーネントとは、以下の通りです。

  1. フォームセクション:利用者がチャットボットと対話するためのテキストエリアとボタンで構成されます。
  2. 回答セクション:質問とそれに対応する回答は配列に格納され、このセクションに表示されます。配列の中を時系列に従いループすることで、最新のものを最初に表示します。

ChatGPT Cloneアプリケーションのセットアップ

まずアプリケーションのインターフェースを構築することから始め、その後、アプリケーションとOpenAI APIのやり取りに関する機能を実装します。まずは2つのコンポーネントの作成から始めましょう。整理のために、srcフォルダ内にcomponentsフォルダを作成し、すべてのコンポーネントをここに格納します。

フォームセクションのコンポーネント

シンプルなフォームです。textareaと送信buttonで構成されます。

// components/FormSection.jsx

const FormSection = () => {

    return (
        <div className="form-section">
            <textarea
                rows="5"
                className="form-control"
                placeholder="Ask me anything..."
            ></textarea>
            <button className="btn">
                Generate Response 🤖
            </button>
        </div>
    )
}

export default FormSection;

App.jsファイルにインポートすることで、以下のように表示されます。

フォームセクションの見た目
フォームセクションの見た目

回答セクションのコンポーネント

このセクションに、すべての質問と回答が表示されます。これをApp.jsファイルにインポートすると、以下のような見た目になります。

回答セクションの見た目
回答セクションの見た目

質問と回答を配列から取得し、ループすることで、コードの可読性と保守性を高めます。

// components/AnswerSection.jsx

const AnswerSection = () => {
    return (
        <>
            <hr className="hr-line" />
            <div className="answer-container">
                <div className="answer-section">
                    <p className="question">Who is the founder of OpenAi?</p>
                    <p className="answer">OpenAI was founded in December 2015 by Elon Musk, Sam Altman, Greg Brockman, Ilya Sutskever, Wojciech Zaremba, and John Schulman.</p>
                    <div className="copy-icon">
                        <i className="fa-solid fa-copy"></i>
                    </div>
                </div>
            </div>
        </>
    )
}

export default AnswerSection;

トップページ

これで両方のコンポーネントが作成できましたが、アプリケーションを実行しても何も表示されません。App.jsファイルにコンポーネントをインポートする必要があります。このアプリケーションでは、ルーティングを実装しません。つまり、App.jsファイルがアプリケーションのホームコンポーネント/ページとして機能することになります。

コンポーネントをインポートする前に、アプリケーションのタイトルや説明など、コンテンツを追加しておきましょう。

// App.js

import FormSection from './components/FormSection';
import AnswerSection from './components/AnswerSection';

const App = () => {
    return (
        <div>
            <div className="header-section">
                <h1>ChatGPT CLONE 🤖</h1>
                <p>
                    I am an automated question and answer system, designed to assist you
                    in finding relevant information. You are welcome to ask me any queries
                    you may have, and I will do my utmost to offer you a reliable
                    response. Kindly keep in mind that I am a machine and operate solely
                    based on programmed algorithms.
                </p>
            </div>

            <FormSection />
            <AnswerSection />
        </div>
    );
};

export default App;

上記のコードで、2つのコンポーネントをインポートし、アプリケーションに追加しています。アプリケーションを実行すると、以下のような表示になります。

ChatGPTクローンアプリの概観
ChatGPTクローンアプリの概観

機能追加とOpenAI APIの連携

これで、アプリケーションのユーザーインターフェースが完成しました。次のステップは、アプリケーションの機能面の実装です。OpenAI APIとやりとりしてレスポンスを取得できるようにします。まず、送信されたフォームの値を取得する必要があります。これを、OpenAI APIへのクエリに使用します。

フォームからデータを取得する

Reactでは、データの保存・更新にstate(状態)を使用します。機能コンポーネントでは、useState()フックを使って状態を扱います。状態を作成し、フォームからの値をそれに割り当て、値が変化するたびに状態を更新します。まず、useState()フックをFormSection.jsxコンポーネントにインポートし、newQuestionsを保存・更新するstateを作成しましょう。

// components/FormSection.jsx

import { useState } from 'react';

const FormSection = ({ generateResponse }) => {
    const [newQuestion, setNewQuestion] = useState('');

    return (
        // Form to submit a new question
    )
}

export default FormSection;

次に、textareaフィールドの値をstateに割り当て、入力値が変化するたびに状態を更新するonChange()イベントを作成します。

<textarea
    rows="5"
    className="form-control"
    placeholder="Ask me anything..."
    value={newQuestion}
    onChange={(e) => setNewQuestion(e.target.value)}
></textarea>

最後に、onClick()イベントを作成し、送信ボタンがクリックされるたびに関数を読み込みます。このメソッドはApp.jsファイルで作成され、propsとしてFormSection.jsxコンポーネントに渡されます。尚、newQuestionsetNewQuestionの値が引数になります。

<button className="btn" onClick={() => generateResponse(newQuestion, setNewQuestion)}>
    Generate Response 🤖
</button>

これで、フォームの値を保存・更新するstateができ、さらに、App.jsファイルからpropsとして渡されるメソッドも実装できました。クリックイベントを問題なく処理することができます。最終的に、コードは以下のようになります。

// components/FormSection.jsx

import { useState } from 'react';

const FormSection = ({ generateResponse }) => {
    const [newQuestion, setNewQuestion] = useState('');

    return (
        <div className="form-section">
            <textarea
                rows="5"
                className="form-control"
                placeholder="Ask me anything..."
                value={newQuestion}
                onChange={(e) => setNewQuestion(e.target.value)}
            ></textarea>
            <button className="btn" onClick={() => generateResponse(newQuestion, setNewQuestion)}>
                Generate Response 🤖
            </button>
        </div>
    )
}

export default FormSection;

次のステップでは、OpenAI APIとの対話の全プロセスを処理するメソッドをApp.jsファイル内に作成していきます。

OpenAI APIとの連動

ReactアプリケーションでOpenAI APIと対話し、APIキーを取得するには、OpenAI APIアカウントを作成する必要があります。OpenAIのウェブサイトから、Googleアカウントまたはメールを使用してアカウントを登録します。APIキーを生成するには、ウェブサイトの右上にある「Personal」をクリックします。いくつかのオプションが表示されるので、その中から「View API keys」をクリックします。

OpenAI API Keysにアクセスする
OpenAI API Keysにアクセスする

Create new secret key」ボタンをクリックし、OpenAIとのやり取りに使用する鍵(キー)を安全な場所に保存します。これで、OpenAIパッケージ(すでにインストール済みです)を設定方法と一緒にインポートして、OpenAIの初期化を進めることができます。生成されたキーでコンフィギュレーションを作成し、それを使ってOpenAIを初期化します。

// src/App.js

import { Configuration, OpenAIApi } from 'openai';

import FormSection from './components/FormSection';
import AnswerSection from './components/AnswerSection';

const App = () => {
    const configuration = new Configuration({
        apiKey: process.env.REACT_APP_OPENAI_API_KEY,
    });

    const openai = new OpenAIApi(configuration);

    return (
        // Render FormSection and AnswerSection
    );
};

export default App;

上記のコードでは、OpenAI APIキーは環境変数として.envファイルに格納されます。アプリケーションのルートフォルダに.envファイルを作成し、変数REACT_APP_OPENAI_API_KEYにキーを格納することができます。

// .env
REACT_APP_OPENAI_API_KEY = sk-xxxxxxxxxx…

App.jsファイル内にgenerateResponseメソッドを作成し、すでに作成したフォームから期待される2つのパラメータを渡し、リクエストの処理とAPIからのレスポンスの取得を行います。

// src/App.js

import FormSection from './components/FormSection';
import AnswerSection from './components/AnswerSection';

const App = () => {
    const generateResponse = (newQuestion, setNewQuestion) => {
        // Set up OpenAI API and handle response
    };

    return (
        // Render FormSection and AnswerSection
    );
};

export default App;

そして、OpenAI APIにリクエストを送る処理へと進みます。OpenAI APIでは、質問と回答(Q&A)、文法修正、翻訳など、多くの操作を行うことができます。これらの操作のそれぞれに対応する記法が用意されています。例えば、Q&Aのエンジン値はtext-davinci-00で、SQL翻訳のエンジン値はcode-davinci-002です。様々な例については、OpenAIのサンプルドキュメントをご覧ください。

今回は、Q&Aのみを扱うので次のようになります。

{
  model: "text-davinci-003",
  prompt: "Who is Obama?",
  temperature: 0,
  max_tokens: 100,
  top_p: 1,
  frequency_penalty: 0.0,
  presence_penalty: 0.0,
  stop: ["\"],
}

注)プロンプトの値を変更しています。

プロンプトは、フォームから送信される質問です。つまり、generateResponseメソッドにパラメータとして渡すフォームの入力内容から受け取る必要があります。これを行うには、オプションを定義してから、スプレッド演算子を使用して、プロンプトを含むかたちで変数completeOptionsを作成します。

// src/App.js

import { Configuration, OpenAIApi } from 'openai';
import FormSection from './components/FormSection';
import AnswerSection from './components/AnswerSection';

const App = () => {
    const configuration = new Configuration({
        apiKey: process.env.REACT_APP_OPENAI_API_KEY,
    });

    const openai = new OpenAIApi(configuration);

    const generateResponse = async (newQuestion, setNewQuestion) => {
        let options = {
            model: 'text-davinci-003',
            temperature: 0,
            max_tokens: 100,
            top_p: 1,
            frequency_penalty: 0.0,
            presence_penalty: 0.0,
            stop: ['/'],
        };

        let completeOptions = {
            ...options,
            prompt: newQuestion,
        };

    };

    return (
         // Render FormSection and AnswerSection
    );
};

export default App;

あとはOpenAIにcreateCompletionメソッドでリクエストを送り、レスポンスを受け取るだけです。

// src/App.js

import { Configuration, OpenAIApi } from 'openai';
import FormSection from './components/FormSection';
import AnswerSection from './components/AnswerSection';

import { useState } from 'react';

const App = () => {
    const configuration = new Configuration({
        apiKey: process.env.REACT_APP_OPENAI_API_KEY,
    });

    const openai = new OpenAIApi(configuration);

    const [storedValues, setStoredValues] = useState([]);

    const generateResponse = async (newQuestion, setNewQuestion) => {
        let options = {
            model: 'text-davinci-003',
            temperature: 0,
            max_tokens: 100,
            top_p: 1,
            frequency_penalty: 0.0,
            presence_penalty: 0.0,
            stop: ['/'],
        };

        let completeOptions = {
            ...options,
            prompt: newQuestion,
        };

        const response = await openai.createCompletion(completeOptions);

        console.log(response.data.choices[0].text);
    };

    return (
        // Render FormSection and AnswerSection
    );
};

export default App;

上のコードでは、答えのテキストがコンソールに表示されます。何か質問をして、アプリケーションをテストしてみてください。最後のステップでは、質問と回答の配列を保持するstateを作成し、その配列をAnswerSectionコンポーネントにpropsとして送信します。以下がApp.jsの最終的なコードになります。

// src/App.js
import { Configuration, OpenAIApi } from 'openai';

import FormSection from './components/FormSection';
import AnswerSection from './components/AnswerSection';

import { useState } from 'react';

const App = () => {
    const configuration = new Configuration({
        apiKey: process.env.REACT_APP_OPENAI_API_KEY,
    });

    const openai = new OpenAIApi(configuration);

    const [storedValues, setStoredValues] = useState([]);

    const generateResponse = async (newQuestion, setNewQuestion) => {
        let options = {
            model: 'text-davinci-003',
            temperature: 0,
            max_tokens: 100,
            top_p: 1,
            frequency_penalty: 0.0,
            presence_penalty: 0.0,
            stop: ['/'],
        };

        let completeOptions = {
            ...options,
            prompt: newQuestion,
        };

        const response = await openai.createCompletion(completeOptions);

        if (response.data.choices) {
            setStoredValues([
                {
                    question: newQuestion,
                    answer: response.data.choices[0].text,
                },
                ...storedValues,
            ]);
            setNewQuestion('');
        }
    };

    return (
        <div>
            <div className="header-section">
                <h1>ChatGPT CLONE 🤖</h1>
                    <p>
                       I am an automated question and answer system, designed to assist you
                        in finding relevant information. You are welcome to ask me any
                        queries you may have, and I will do my utmost to offer you a
                        reliable response. Kindly keep in mind that I am a machine and
                        operate solely based on programmed algorithms.
                    </p>
            </div>

            <FormSection generateResponse={generateResponse} />

            <AnswerSection storedValues={storedValues} />
        </div>
    );
};

export default App;

AnswerSectionコンポーネントの編集に進みましょう。App.jsからpropsの値を受け取り、JavaScriptのMap()メソッドを使って配列storedValuesを確認できるようにします。

// components/AnswerSection.jsx

const AnswerSection = ({ storedValues }) => {
    return (
        <>
            <hr className="hr-line" />
            <div className="answer-container">
                {storedValues.map((value, index) => {
                    return (
                        <div className="answer-section" key={index}>
                            <p className="question">{value.question}</p>
                            <p className="answer">{value.answer}</p>
                            <div className="copy-icon">
                                <i className="fa-solid fa-copy"></i>
                            </div>
                        </div>
                    );
                })}
            </div>
        </>
    )
}

export default AnswerSection;

アプリケーションを実行してみましょう。質問をすると、回答が下に表示されます。しかし、コピーボタンはこの時点では使えません。ボタンにonClick()イベントを追加し、機能を処理するメソッドが動作するにようにする必要があります。これには、navigator.clipboard.writeText()メソッドを使用します。実装すると、AnswerSectionコンポーネントは次のようになります。

// components/AnswerSection.jsx

const AnswerSection = ({ storedValues }) => {
    const copyText = (text) => {
        navigator.clipboard.writeText(text);
    };

    return (
        <>
            <hr className="hr-line" />
            <div className="answer-container">
                {storedValues.map((value, index) => {
                    return (
                        <div className="answer-section" key={index}>
                            <p className="question">{value.question}</p>
                            <p className="answer">{value.answer}</p>
                            <div
                                className="copy-icon"
                                onClick={() => copyText(value.answer)}
                            >
                                <i className="fa-solid fa-copy"></i>
                            </div>
                        </div>
                    );
                })}
            </div>
        </>
    )
}

export default AnswerSection;

アプリケーションを実行すると、ChatGPTクローンアプリケーションが動作するはずです。これで、アプリケーションをデプロイして、オンラインでアクセスしたり、他の人に共有したりできます。

ReactアプリケーションをKinstaにデプロイする方法

せっかくアプリケーションを構築したので、ローカルに置いておくだけでなく、ネット上でアクセスできるようにしてみましょう。GitHubKinstaを使って、その方法をご紹介します。

コードをGitHubにプッシュする

GitHubにコードをプッシュするには、Gitコマンドを使用します。Gitコマンドは、コードの変更を管理し、他の開発者と作業を進め、バージョン履歴を保持するのに便利です。

まずGitHubアカウントにログインして、画面右上の「+」ボタンをクリックし、ドロップダウンメニューから「New repository」を選択して、新しいリポジトリを作成します。

GitHubに新しいリポジトリを作成する
GitHubに新しいリポジトリを作成する

リポジトリに名前を付け、説明を追加(任意)し、公開か非公開かを選択します。完了したら「Create repository」をクリックして、リポジトリを作成します。

リポジトリが作成されたら、リポジトリのメインページから、コードをGitHubにプッシュするために必要なリポジトリURLを確認します。

リポジトリのURLを確認する
リポジトリのURLを確認する

ターミナルまたはコマンドプロンプトを開き、プロジェクトのディレクトリに移動します。以下のコマンドを1つずつ実行し、コードをGitHubリポジトリにプッシュします。

git init
git add .
git commit -m "my first commit"
git remote add origin [repository URL]
git push -u origin master

git initはローカルの Git リポジトリを初期化するもので、git add .は既存のディレクトリとそのサブディレクトリにあるすべてのファイルを新しいGitリポジトリに追加します。git commit -m "my first commit"は簡単なメッセージとともに変更をリポジトリにコミットします。git remote add origin [repository URL]はリポジトリのURLをリモートリポジトリとして設定し、git push -u origin masterはリモートリポジトリ(origin)にmasterブランチでコードをプッシュしています。

ChatGPTクローンアプリをKinstaにデプロイする

リポジトリをKinstaにデプロイする手順は以下の通りです。

    1. MyKinstaにログインします。
    2. 左サイドバーの「アプリケーション」をクリックし「サービスを追加」をクリックします。
    3. ReactアプリケーションをKinstaにデプロイするので、ドロップダウンメニューから「アプリケーション」を選択します。
    4. 表示されるポップアップから、デプロイするリポジトリを選択します。複数のブランチがある場合は、デプロイするブランチを選択し、アプリケーションに名前をつけることができます。利用可能な25の中からデータセンターの場所を選択します。Kinstaのシステムにより自動でstartコマンドが検出されます。
    5. GitHubのようなパブリックホストにAPIキーをプッシュするのは安全ではないので、ローカルで環境変数として追加しています。ホスティングの際は、同じ変数名でキーを値とし、環境変数として追加することができます。
ChatGPTクローンアプリをKinstaにデプロイする
ChatGPTクローンアプリをKinstaにデプロイする

アプリケーションのデプロイが開始され、数分後には、アプリケーションのリンクが表示されます(この例では「https://chatgpt-clone-g9q10.kinsta.app/Note」)自動デプロイメントを有効にすると、コードベースを変更してGitHubにプッシュするたびに、アプリケーションの再デプロイが行われます。

まとめ

OpenAI APIは、カスタマーサポートやパーソナルアシスタント、言語翻訳やコンテンツ作成など、幅広いアプリケーションの構築に応用できます。

今回は、ReactとOpenAIを使ってChatGPTクローンアプリケーションを構築する方法をご紹介しました。このアプリケーション/機能を他のアプリケーションに統合して、人間と会話しているかのようなサービスを展開することも可能です。

OpenAI APIでできること、このクローンアプリを改善する方法はまだまだあります。例えば、ブラウザを更新しても以前の質問と回答が消えないように、ローカルストレージを実装するといった調整も考えられます。

Kinstaの無料利用枠やホビープランでは、アプリケーションホスティングを気軽に試してみることができます。是非ご活用ください。

今回の記事に関連するプロジェクトやご経験をお持ちでしたら、以下のコメント欄からお聞かせください。

Joel Olawanle Kinsta

Kinstaでテクニカルエディターとして働くフロントエンド開発者。オープンソースをこよなく愛する講師でもあり、JavaScriptとそのフレームワークを中心に200件以上の技術記事を執筆している。