
今話題沸騰中の 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


