
今話題沸騰中の OpenAI が開発した人工知能(AI)チャットボット「ChatGPT」のAPI が公開されました。
これにより、今後加速度的にあらゆる開発言語で ChatGPT の API が利用され、様々なアプリやサービスに組み込まれて便利になっていくと思います。
そこで、今回は試しに ChatGPT API を利用した、誰でもゼロから作れる簡単なチャットプログラムを React で作ってみたいと思います。
2023/3/14 に最新の言語モデル GPT-4 が公開されました。
GPT-4 では、テキストだけでなく、画像、音声、動画もインプットとして渡すことができるようになっています。
例えば、手書きのメモやスケッチをスマホで撮影した画像を送るだけで AI が回答や成果物を提示してくれます。
開発環境の作成
まずはローカルに React の開発環境をセットアップします。
Node.js がインストールされていない場合は、以下を参考に React でのアプリ開発をする環境をまずは整えておきましょう。
早速アプリを作っていきます。
まず、ターミナルを起動して開発環境を作成するディレクトリまで移動しておきます。
以下では /Users/myname/LocalSites
というパスに移動しています。
cd /Users/myname/LocalSites
今回のサンプルで作るアプリ(ディレクトリ)名は「chat-sample-app」として移動した場所に React アプリを作成します。
npx create-react-app chat-sample-app
プロンプトが正常に返ってきたら、「chat-sample-app」というアプリ専用のフォルダが作成されているので、そこに移動し、ローカルサーバーの動作確認をします。
# アプリディレクトリに移動 cd chat-sample-app # ローカルのサーバーを起動 npm start
npm start
でブラウザでローカルのページが自動的に開き、以下の表示になればローカルの開発環境は整いました。

参照できない場合は、URL を http://localhost:3000/ として直接開いてみてください。
ChatGPT API と通信する処理
axios ライブラリのインストール
ChatGPT API のように外部のリソースとデータをやり取りするには、fetch というブラウザの標準ライブラリを利用することで実現できますが、axios というライブラリを利用する方がエラーハンドリングやデータの取得が楽になり、コードも簡潔で読みやすくなるため、今回はこちらのライブラリを使用することにします。
npm install axios --save
chat.js の作成
インストールが完了したら、いよいよ ChatGPT と会話するためのコードを書いていきます。
/chat-sample-app/src
ディレクトリに、今回はとりあえず「chat.js」という名前の JavaScript ファイルを作成し、axios モジュールをまずはインポートします。
import axios from 'axios';
OpenAI のドキュメントを参考に、ChatGPT API のエンドポイントとデータのやり取りに必要なパラメータを準備します。
https://platform.openai.com/docs/api-reference/chat
現時点のエンドポイントは https://api.openai.com/v1/chat/completions、パラメータはチャットプログラムのモデルID(model
)、そして渡すメッセージ(messages
) が必須のようです。
これらの情報を元に axios を利用して API にリクエストを送信します。
import axios from 'axios'; const API_URL = 'https://api.openai.com/v1/'; const MODEL = 'gpt-3.5-turbo'; const API_KEY='APIキーをここに貼り付け'; export const chat = async ( message ) => { try { const response = await axios.post( `${ API_URL }chat/completions`, { // モデル ID の指定 model: MODEL, // 質問内容 messages: [ { 'role': 'user', 'content': message, } ], }, { // 送信する HTTP ヘッダー(認証情報) headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${ API_KEY }` } }); // 回答の取得 return response.data.choices[0].message.content; } catch ( error ) { console.error( error ); return null; } }
- model
-
利用する ChatGPT のモデル名。
現時点では、gpt-4
,gpt-4-0314
,gpt-4-32k
,gpt-4-32k-0314
,gpt-3.5-turbo
,gpt-3.5-turbo-0301
のいずれかを指定できます。2023/3/14 に最新モデル GPT-4 が公開されました。
ただし、最新モデルを試すには ChatGPT Plus ($20/月)へアップグレードする必要があります。 - messages
-
ChatGPT に送るメッセージオブジェクトの配列。
オブジェクトには、
role
(AIへ送信するコンテンツの役割)とcontent
(メッセージ、指示)を指定します。role
にはsystem
,assistant
,user
の3タイプを指定できます。最低限必要な
role
はuser
で、この場合のcontent
は AI に送る文章、つまりユーザーから ChatGPT に質問するメッセージを指定します。role: usermessages: [ { 'role': 'user', 'content': '簡単で美味しいおかずのレシピを教えてください。' } ]
assistant
のcontent
は ChatGPT からの返答を渡します。
直前のチャットの返答内容をassistant
のcontent
に渡しておくと、抽象的な文章でもやり取りを踏まえた自然な回答が返ってきます。role: assistantmessages: [ { 'role': 'assistant', 'content': '以下は簡単で美味しいおかずのレシピです。\n【豆腐とひき肉の煮物】\n必要な材料:- 絹豆腐 1丁 ...' } ]
sysytem
は AI への指示役としての情報を渡すことができます。
具体的には、AI のキャラ設定や語尾、文頭、文末の決め台詞などをcontent
に細かく指示しておくと、そのキャラクターになりきって回答してくれるようになります。role: systemmessages: [ { 'role': 'system', 'content': 'あなたは「北斗の拳」のジャギです。ジャギになったつもりで罵詈雑言を交えながら回答してください。一人称は「俺様」にしてください。最後は「{a}より優れた{b}などいねぇ!」で締めてください。変数({a},{b})の部分はランダムに変えてください。' } ]
その他にもいくつかパラメータが提供されていますが、他のパラメータについては公式ドキュメントを参考にしてください。
https://platform.openai.com/docs/api-reference/chat
API へのリクエストが成功すると、以下のようなレスポンスが返ってきます。
{ "id": "chatcmpl-123", "object": "chat.completion", "created": 1677652288, "choices": [{ "index": 0, "message": { "role": "assistant", "content": "\n\nHello there, how may I assist you today?", }, "finish_reason": "stop" }], "usage": { "prompt_tokens": 9, "completion_tokens": 12, "total_tokens": 21 } }
このうち、必要なのは “choices” -> “message” -> “content” のデータなので、chat.js が返す値(回答)は以下のようにします。
return response.data.choices[0].message.content;
App.js の入れ替え
最初に npx
コマンドで作成したプロジェクトに出力されている /chat-sample-app/src/App.js のコードを以下に置き換えます。
import React, { useState } from 'react'; import { chat } from './chat'; // chat.js のインポート const App = () => { // メッセージの状態管理用 const [ message, setMessage ] = useState( '' ); // 回答の状態管理用 const [ answer, setAnswer ] = useState( '' ); // メッセージの格納 const handleMessageChange = ( event ) => { setMessage( event.target.value ); } // 「質問」ボタンを押したときの処理 const handleSubmit = async ( event ) => { event.preventDefault(); // chat.js にメッセージを渡して API から回答を取得 const responseText = await chat( message ); // 回答の格納 setAnswer( responseText ); } // チャットフォームの表示 return ( <div> <form onSubmit={ handleSubmit }> <label> <textarea rows='5' cols='50' value={ message } onChange={ handleMessageChange } /> </label> <div> <button type="submit">質問する</button> </div> </form> { answer && ( <div> <h2>回答:</h2> <p>{ answer }</p> </div> ) } </div> ); } export default App;
<textarea>
要素で質問内容を入力するフォームを表示し、入力したメッセージは保管されます。
「質問する」ボタンがクリックされると保管されているメッセージを chat.js からインポートした chat
関数に渡して axios
で ChatGPT の API と通信を行い、レスポンスが成功すると受け取ったデータ(JSON)から回答にあたるデータ(content
)のみが返ってきます。
回答を受け取るとその値は setAnswer
関数で answer
変数に格納され、画面に表示されます。
フォームの確認
ここまで完了したら、とりあえずローカル( http://localhost:3000/ )のページを更新して以下のフォームが表示されることを確認しましょう。

API キーが指定されていないのでこの時点ではまだ機能しません。
API キーの取得
完成したフォームから ChatGPT とデータをやり取りするための API キーを生成します。
OpenAI のアカウントを作成していない場合は、まずはアカウントを作成します。
ログイン後、右上のアカウントのロゴをクリックして「View API Keys」を選択します。

「Create new secret key」をクリックして API キーを生成し、コピーします。


API キーのセット
chat.js にてまだ未指定の状態だった API キー用の定数(API_KEY
)にコピーした キーを貼り付けます。
const API_KEY='APIキーをここに貼り付け';
動作確認
これで ChatGPT の API を利用する基本準備が整いました。
再度ページを更新して適当に質問してみましょう。

AI からちゃんと回答が返ってきました。
ただ、レスポンスに含まれる改行コード(\n
) は HTML では当然解釈されないため、Webページ上では1行になってしまい見づらいです。
改行コードが見つかった場合は、改行タグ(<br />
)を挿入して表示するようにしてみます。
質問と回答は pre
要素で括っても良い場合は、改行はそのまま表示されるので以下の修正は不要です。
{ answer && ( <div> <h2>回答:</h2> <p>{ answer.split( /\n/ ) .map( ( item, index ) => { return ( <React.Fragment key={ index }> { item } <br /> </React.Fragment> ); } ) } </p> </div> ) }

ちゃんと改行されるようになりました。
処理の最適化と精度の向上
ここまでのコードのままでは、コンポーネントのレンダリングや送信ボタンをクリックされるたびに API リクエストを送信するため大量のリクエストを送信してしまう恐れがあります。
また、API と通信中の間のローディング表示がないため、リクエストを実行してもしばらく反応がないようにみえてしまいます。
なるべく不要な API リクエストを行わないよう事前にチェックをしたり、ローディング表示にも対応させてみましょう。
ローディング表示の状態管理には React の useState
フックを利用します。
さらに続けてチャットを行うことを考慮し、直前のメッセージ内容は React の useRef
フックで保持しておくようにします。
// メッセージの状態管理用のステート const [ message, setMessage ] = useState( '' ); // 回答の状態間利用のステート const [ answer, setAnswer ] = useState( '' ); // 会話の記録用のステート const [ coversation, setCoversation ] = useState( [] ); // ローディング表示用のステート const [ loading, setLoading ] = useState( false ); // 前回のメッセージの保持、比較用 const prevMessageRef = useRef( '' );
会話内容の記録
チャットは連続したやりとりを通して会話の内容や流れを考慮しながら行う必要があります。
この自然な会話の流れを実現するには、ユーザーと AI の会話の内容を記録し、そこに続けて質問する新しいメッセージを追加しながら API リクエストを送信する必要があります。
具体的には、会話をする度に conversation
変数にユーザーと AI のメッセージのやり取り(オブジェクトの配列)を追加で記録し、role: user
の content
の値として conversation
変数を渡すようにすることで、それまでのやり取りを踏まえた上で AI がより適切な回答をしてくれるようになります。
// 回答が取得されたとき useEffect( () => { // 直前のチャット内容 const newConversation = [ { 'role': 'user', // ユーザー 'content': message, // 直前の質問内容 }, { 'role': 'assistant', // ChatGPT 'content': answer, // 直前の回答 }, ]; // 会話の記録(直前のチャット内容の追加) setConversation( [ ...conversation, ...newConversation ] ); // メッセージの消去(フォームのクリア) setMessage( '' ); }, [ answer ] );
useEffect
フックによって answer
の変更を監視し、AI から回答(answer
)が得られるとその会話の内容をスプレッド構文(...
)で展開して conversation
に追加で記録します。
質問の送信と回答の取得
「質問する」ボタンがクリックされると、<form onSubmit={ handleSubmit }>
によって handleSubmit
関数が実行されますが、このフォーム送信処理の中でローディング表示の開始とフォーム内容のチェックをして API リクエストの送信を判断するようにします。
また、フォームを送信するたびに handleSubmit
関数が再生成されないように useCallback
フックを利用して loading
, message
, conversation
の値に変更があった場合のみに限定します。
// フォーム送信時の処理 const handleSubmit = useCallback( async ( event ) => { event.preventDefault(); // フォームが空のとき if ( !message ) { alert( 'メッセージがありません。' ); return; } // APIリクエスト中はスルー if ( loading ) return; // APIリクエストを開始する前にローディング表示を開始 setLoading( true ); try { // API リクエスト const response = await axios.post( `${ API_URL }chat/completions`, { model: MODEL, messages: [ ...coversation, { 'role': 'user', 'content': message, } ], }, { // HTTPヘッダー(認証) headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${ API_KEY }` } }); // 回答の取得 setAnswer( response.data.choices[0].message.content.trim() ); } catch ( error ) { // エラーハンドリング console.error( error ); } finally { // 後始末 setLoading( false ); // ローディング終了 prevMessageRef.current = message; // 今回のメッセージを保持 } }, [ loading, message, conversation ] );
フォームが送信されると、まず内容(message
)を確認し、空の場合はアラートを表示して終了するようにします。
さらに短時間に大量の API リクエストを送信しないよう、直前の API リクエストが完了していない間(ローディング中)は何も処理せずスルーさせます。
API リクエストが行われる直前にはローディング中のフラグを立てます( setLoading( true )
)。
finally
によって、リクエストが成功しても失敗しても最後にローディング中の状態を終了( setLoading( false )
)し、今回の質問内容( message
)を一旦保存( prevMessageRef.current = message
)します。
チャットコンテンツのメモ化
質問をして AI から回答が返ってきたらその内容を HTML でフォームの下に表示していますが、質問と回答のテキスト以外は更新する必要がないため、React.memo
メソッドで1度レンダリングされた結果を再利用して不要なレンダリングを行わないようにします。
// チャット内容 const ChatContent = React.memo( ( { prevMessage, answer } ) => { return ( <div className='result'> <div className='current-message'> <h2>質問:</h2> <p>{ prevMessage }</p> </div> <div className='current-answer'> <h2>回答:</h2> <p>{ answer.split( /\n/ ) .map( ( item, index ) => { return ( <React.Fragment key={ index }> { item } <br /> </React.Fragment> ); } ) } </p> </div> </div> ) } );
これにより、return
内の回答を表示する部分は以下のように書き換えます。
{ answer && !loading && ( <ChatContent prevMessage={ prevMessageRef.current } answer={ answer } /> ) }
ローディング表示
API リクエスト中( loading == true
)のときだけローディング表示をします。
回答が表示される要素の前にローディング表示の要素を仕込んでおきます。
{ loading && ( <div> <p>回答中...</p> </div> ) }
全体のコード(chat.js)
ここまでの処理をまとめた chat.js 全体のコードは以下のように書き換えます。
import React, { useCallback, useEffect, useState, useRef } from 'react'; import axios from 'axios'; const API_URL = 'https://api.openai.com/v1/'; const MODEL = 'gpt-3.5-turbo'; const API_KEY='ここにAPIキーを指定'; const Chat = () => { // メッセージの状態管理用のステート const [ message, setMessage ] = useState( '' ); // 回答の状態管理用のステート const [ answer, setAnswer ] = useState( '' ); // 会話の記録用のステート const [ conversation, setConversation ] = useState( [] ); // ローディング表示用のステート const [ loading, setLoading ] = useState( false ); // 前回のメッセージの保持、比較用 const prevMessageRef = useRef( '' ); // 回答が取得されたとき useEffect( () => { // 直前のチャット内容 const newConversation = [ { 'role': 'assistant', 'content': answer, }, { 'role': 'user', 'content': message, } ]; // 会話の記録(直前のチャット内容の追加) setConversation( [ ...conversation, ...newConversation ] ); // メッセージの消去(フォームのクリア) setMessage( '' ); }, [ answer ] ); // フォーム送信時の処理 const handleSubmit = useCallback( async ( event ) => { event.preventDefault(); // フォームが空のとき if ( !message ) { alert( 'メッセージがありません。' ); return; } // APIリクエスト中はスルー if ( loading ) return; // APIリクエストを開始する前にローディング表示を開始 setLoading( true ); try { // API リクエスト const response = await axios.post( `${ API_URL }chat/completions`, { model: MODEL, messages: [ ...conversation, { 'role': 'user', 'content': message, }, ], }, { // HTTPヘッダー(認証) headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${ API_KEY }` } }); // 回答の取得 setAnswer( response.data.choices[0].message.content.trim() ); } catch ( error ) { // エラーハンドリング console.error( error ); } finally { // 後始末 setLoading( false ); // ローディング終了 prevMessageRef.current = message; // 今回のメッセージを保持 } }, [ loading, message, conversation ] ); // チャット内容 const ChatContent = React.memo( ( { prevMessage, answer } ) => { return ( <div className='result'> <div className='current-message'> <h2>質問:</h2> <p>{ prevMessage }</p> </div> <div className='current-answer'> <h2>回答:</h2> <p>{ answer.split( /\n/ ) .map( ( item, index ) => { return ( <React.Fragment key={ index }> { item } <br /> </React.Fragment> ); } ) } </p> </div> </div> ) } ); // フォームの表示 return ( <div className='container'> <form className='chat-form' onSubmit={ handleSubmit }> <label> <textarea className='message' rows='5' cols='50' value={ message } onChange={ e => { setMessage( e.target.value ) ; } } /> </label> <div className='submit'> <button type="submit">質問する</button> </div> </form> { loading && ( <div className='loading'> <p>回答中...</p> </div> ) } { answer && !loading && ( <ChatContent prevMessage={ prevMessageRef.current } answer={ answer } /> ) } </div> ); } export default Chat;
フォームの内容( return
)も chat.js にまとめたので、Chat()
関数としてエクスポートしておき、index.js からインポートできるようにします。
index.js の修正
npx
コマンドで最初に作成された index.js から、不要になった App.js のインポートと読み込みを削除し、代わりに chat.js をインポートし、<App />
から <Chat />
に置き換えます。
import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; import Chat from './chat'; // -> 追加 import reportWebVitals from './reportWebVitals'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <React.StrictMode> <Chat /> </React.StrictMode> ); reportWebVitals();
動作確認
改めて http://localhost:3000 を更新して動作確認をしてみます。

「質問する」ボタンをクリック(フォーム送信)すると、まず「回答中…」というローディング表示を行い、API リクエストが完了しデータ(回答)を受け取るとローディング表示は解除され、質問と回答が表示されます。
フォームの内容は自動的にクリアされ、再びチャットができるようになります。
スタイルシートについては全くいじっていないので、見た目についてはここでは全く言及していませんが、chat.js のフォームの構成要素をカスタマイズし、それぞれ class をつけて /src/inidex.css に CSS を記述すれば自由に変更できます。
また、ローディング中の表示もテキストではなくスピナーアイコンや CSS SVG で構成した図形に変更すると、視覚的にはよりいいかもしれません。
AI にキャラを設定する(system)
role
で system
をセットした場合、その content
には AI にどのように振る舞って回答するかを指示しておくことができます。
つまり、チャットボットに様々なキャラクターになりきってもらって会話をやり取りすることができるようになります。
例えば、会話記録用のステート(conversation
)の初期値を以下のようにしておくと、北斗の拳のジャギになりきった文面でチャットボットから返答が返ってきます。
// 会話の記録用のステート const [ conversation, setConversation ] = useState( [ { 'role': 'system', 'content': 'あなたは「北斗の拳」のジャギです。ジャギになったつもりで罵詈雑言を交えながら常に偉そうに回答してください。一人称は「俺様」にしてください。文末は「{a}より優れた{b}などいねぇ!」で締めてください。変数({a},{b})の部分はランダムに変えてください。', } ] );
content
には、どんなキャラクターで、文頭や文末、一人称などまで日本語の文章で指示できるのがすごいです。
さらに変数的な要素を文章中に織り交ぜてもそれを解釈してくれます。
このようにチャットボットの初期設定を system から指示をしておくと、会話の様子が一変して以下のようなチャットを楽しむことができます。




いかかでしょうか。
ChatGPT、めちゃくちゃ賢いですね。。
ちなみに、このチャットボットアプリで表示しているキャラクターの画像も OpenAI の画像生成 API で作成した AI 画像です。
React で画像生成 API を利用した Web アプリのサンプルは以下をご覧ください。
まとめ
今回は React で ChatGTP の API を利用して外部から AI とチャットをする簡単な Web アプリを作ってみましたが、とても簡単に実装できることがわかったと思います。
これをベースに、WordPress のブロックエディターや FSE(フルサイト編集)のどこかにチャットフォームを呼び出せるようにしておき、例えばデザイン中にあるカラーコードに合う他のカラーコードのパターンが知りたいときなどに、エディター上から直接 AI に回答のフォーマット(JSON形式など)を指定して質問してみるなど、様々な形で活用できるのではと思います。
OpenAI では他にも欲しい画像の説明を解釈して画像生成をする AI の API もあり、提供中の WordPress のブロックエディター用プラグイン「DigiPress Ex – Blocks」に組み込んで、欲しい画像を AI で生成し、直接アップロードをしてブロックで利用する画像として指定するなど、現在どのような便利な連携方法があるか検討しています。
画像生成の API はまた別の機会に試してみたいと思います。
画像素材 : 著作者:upklyak/出典:Freepik