💬
Nextjs ChatGPT APIで簡易的なチャットアプリを作る方法
環境
- Next.js : 14.0.3
- React : 18.0.0
- OpenAI : 4.19.0
- Recoil : 0.7.7
- tailwind css : 3.3.0
概要
Next.jsとchatGPTのAPIを使用して、簡単な会話アプリを作成します。
今回は、会話の記録はStateで管理するため、リロードをすると会話の記録は画面上から消えます。
また、APIとのやりとりにはRoute Handlers
を使用します。
手順
- 各インストール
- Next.jsアプリの作成
npx create-next-app@latest chat-app
- OpenAIライブラリのインストール
npm i openai
- Recoilのインストール
npm install recoil
- Next.jsアプリの作成
- 会話記録用のグローバルStateを作成
- UIの作成
- チャット画面の大枠
- チャットの吹き出し
- チャットの送信フォーム
- APIの作成
- OpenAIライブラリのインスタンスを作成する関数
- そのインスタンスを使用してOpenAIの**
chat.completions.create
**メソッドを呼ぶ関数 -
Route Handlers
を使用してAPIを叩く
作成方法
各インストールは上述の通り。
2. 会話記録用のグローバルStateを作成
- チャットの会話を記録するためのStateをRecoilを使用して作成します。
/states/chatLogState.ts
// states/chatLogState.ts
import { atom } from 'recoil';
// メッセージオブジェクトの型を定義
interface Message {
id: number;
content: string;
sender: string;
}
// gptResponseStateの型をMessageの配列として定義
export const chatLogState = atom<Message[]>({
key: 'chatLogState',
default: [
{ id: 1, content: "こんにちは!", sender: "user" },
{ id: 2, content: "元気ですか?", sender: "other" }
],
});
デフォルトでメッセージを2つ表示させておきます。
id
はメッセージの一意な識別子、content
はメッセージの内容、sender
はメッセージの送信者を示します。
今回は、GPTからの返答はsender:”other”
としておきます。
- チャットのメッセージ(吹き出し)を表示するコンポーネント(
<ChatMessage />
)とメッセージの送信フォーム(<ChatForm />
)でRecoil
のState
が使えるように<RecoilRoot></RecoilRoot>
で囲みます。
'use client'
import React from 'react'
import { RecoilRoot, useRecoilState } from 'recoil'
import ChatMessage from './ChatMessage'
import ChatForm from './ChatForm'
const ChatClient = () => {
return (
<RecoilRoot >
{/* <!-- メッセージエリア --> */}
<div className="flex-grow overflow-auto p-6 space-y-5 ">
<ChatMessage />
</div>
{/* <!-- テキスト入力フォーム --> */}
<ChatForm />
</RecoilRoot>
)
}
export default ChatClient
<RecoilRoot>
はクライアントコンポーネントでしか使えないので、’use client’
でクライアントコンポーネントにします。
3. UIの作成
チャット画面の大枠
-
app/chat/page.tsx
にチャット画面の大枠を作成します。 - Recoilで作成したStateを使用するには、クライアントコンポーネントにする必要がりますが、大枠はサーバーコンポーネントにしたいため、Stateを使用するメッセージエリアとフォームを持つ
<ChatClient />
コンポーネントを別で作成し、Page.tsxで読み込みます。
import React from 'react'
import ChatClient from '../components/ChatClient';
async function ChatPage() {
return (
<div className="flex flex-col h-screen">
{/* <!-- チャットヘッダー --> */}
<div className="p-3 bg-gray-800 text-white">
<h1 className="text-lg">チャットルーム</h1>
</div>
<ChatClient />
</div>
)
}
export default ChatPage
---
// app/components/ChatClient.tsx
'use client'
import React from 'react'
import { RecoilRoot, useRecoilState } from 'recoil'
import ChatMessage from './ChatMessage'
import ChatForm from './ChatForm'
const ChatClient = () => {
return (
<RecoilRoot >
{/* <!-- メッセージエリア --> */}
<div className="flex-grow overflow-auto p-6 space-y-5 ">
<ChatMessage />
</div>
{/* <!-- テキスト入力エリア --> */}
<ChatForm />
</RecoilRoot>
)
}
export default ChatClient
チャットの吹き出し
- チャットのメッセージ(吹き出し)を作成します。
- Stateで管理している配列の一つひとつがメッセージとなっているため、
map
で繰り返し処理を行う
'use client'
import React from 'react'
import { useRecoilState } from 'recoil';
import { chatLogState } from '../states/chatLogState';
type MessageType = {
id: number;
content: string;
sender: string;
};
const ChatMessage = () => {
const [chatLog, setChatLog] = useRecoilState(chatLogState)
return (
<>
{chatLog.map((message:MessageType) => {
return (
<div
key={message.id}
className={`flex items-end ${message.sender === 'user' ? 'justify-end' : ''}`}>
{message.sender === 'other' && (
<div className="flex-shrink-0 mr-2">
<div className="h-8 w-8 bg-gray-300 rounded-full" /> {/* アイコンの代わり */}
</div>
)}
<div
className={`rounded p-2 ${message.sender === 'user' ? 'bg-blue-200' : 'bg-gray-500'}`}>
<p className="text-sm">{message.content}</p>
</div>
</div>
)
})}
</>
)
}
export default ChatMessage
- 三項演算子を使用して、
sender
がuser
とother
の時で吹き出しの表示位置を変更します。 - 同様に、
sender
がother
の時のみ吹き出しの横にアイコンを表示させます。今回はアイコンの代わりにただの丸を表示します。
チャットの送信フォーム
- メッセージを送信するためのフォームを作成します。
'use client'
import React, { useState } from 'react'
import { useRecoilState, useResetRecoilState } from 'recoil'
import { chatLogState } from '../states/chatLogState'
const ChatForm = () => {
const [input, setInput] = useState<string>("")
const [chatLog, setChatLog] = useRecoilState(chatLogState)
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setInput("");
};
return (
<form onSubmit={handleSubmit} className="p-3 bg-gray-200 flex justify-between items-center">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
className="w-full p-2 mr-2 rounded focus:outline-none text-gray-800"
placeholder="メッセージを入力..."
/>
<button type="submit" className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
送信
</button>
</form>
)
}
export default ChatForm
-
useState
でフォーム送信ボックス内のテキストを取得・管理します。 - まだAPIは作成していないため、この段階ではボタンをクリックするとフォームのテキストが消えるだけです。
ここまでで、以下のような画面が出来上がっているはずです。
4. APIの作成
ここから、ChatGPTのAPIから返答をもらうためのAPIを作成していきます。
OpenAIライブラリのインスタンスを作成する関数を作成
- まず
/app/services/openai-service.ts
に、APIにアクセスするためのインスタンスを作成する関数を用意します。
import OpenAI from "openai";
export const openaiClient = () => {
return new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
};
-
.env.local
にAPI KEY
を設定しておき(OPEN_API_KEY=sk-*********************
)、上記の関数で指定します。
chat.completions.create
**メソッドを呼ぶ関数を作成
OpenAIの**- 上記のインスタンスを使用して
/app/services/openai-service.ts
内に、ChatGPTにメッセージを投げる関数を作成します
import OpenAI from "openai";
export const openaiClient = () => {
return new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
};
export const sendPromptToGpt = async (prompt:string) => {
const openai = openaiClient();
const completion = await openai.chat.completions.create({
model: "gpt-3.5-turbo",
messages: [{ "role": "user", "content": prompt }],
temperature: 1,
max_tokens: 256,
top_p: 1,
frequency_penalty: 0,
presence_penalty: 0,
});
const gptResponseMessage = completion.choices[0].message.content;
return gptResponseMessage;
};
// 以下レスポンスの構造
// {
// "choices": [
// {
// "finish_reason": "stop",
// "index": 0,
// "message": {
// "content": "The 2020 World Series was played in Texas at Globe Life Field in Arlington.",
// "role": "assistant"
// }
// }
// ],
// "created": 1677664795,
// "id": "chatcmpl-7QyqpwdfhqwajicIEznoc6Q47XAyW",
// "model": "gpt-3.5-turbo-0613",
// "object": "chat.completion",
// "usage": {
// "completion_tokens": 17,
// "prompt_tokens": 57,
// "total_tokens": 74
// }
// }
-
sendPromptToGpt
関数では、引数としてprompt
(=ユーザーからのメッセージ)を受け取ります。 - 受け取った
prompt
をmessages: [{ "role": "user", "content": prompt }]
のように、content
の値として設定します。 - そのほかの
model
やtemperature
などはお好みです。 - GPTからの返答は
.choices[0].message.content
にあるので、それを取得しreturn
します
Route Handlers
を使用してAPIを叩く
最後に、Route Handler
で先ほど作った関数を使用できるようにAPIを作成します。
主な手順は2つです。
-
app/api/response/route.ts
に先ほど作った関数を呼び出すAPIを作成する - フォーム送信イベントで、APIを呼び出し、送信したメッセージと返答を
State
に追加する
-
app/api/response/route.ts
に先ほど作った関数を呼び出すAPIを作成する
// app/api/response/route.ts
import { NextResponse } from 'next/server';
import { openaiClient, sendPromptToGpt } from "@/app/services/openai-service";
export async function POST(request: Request) {
const openai = openaiClient();
const { prompt } = await request.json();
const gptResponseMessage = await sendPromptToGpt(prompt);
const response = NextResponse.json({ gptResponseMessage })
return response;
}
- POST関数を作成し、
fetch(/api/response — method:POST ~~ )
で作動できるようにします。 -
openaiClient()
で先ほど作成したOpenAIライブラリのインスタンスを作成します -
request
で送られてくるユーザーからのメッセージを{ prompt }
に代入します。 -
sendPromptToGpt
関数を呼び出し、引数にprompt
を指定します。 -
sendPromptToGpt
関数からGPTからの返答がreturnされるので、それをgptResponseMessage
に代入します。 -
response
に代入したJSONレスポンスをreturn
します
- フォーム送信イベントで、APIを呼び出し、送信したメッセージと返答を
State
に追加する
'use client'
import React, { useState } from 'react'
import { useRecoilState, useResetRecoilState } from 'recoil'
import { chatLogState } from '../states/chatLogState'
const ChatForm = () => {
const [input, setInput] = useState<string>("")
const [chatLog, setChatLog] = useRecoilState(chatLogState)
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const newId = chatLog.length > 0 ? chatLog[chatLog.length - 1].id + 1 : 1;
const newUserMessage = { id: newId, content: input, sender: "user" };
const updatedMessages = [...chatLog, newUserMessage];
setChatLog(updatedMessages);
setInput("");
try {
const res = await fetch(`/api/response`, {
method: "POST",
headers: {
"Content-Type": "application/json;charset=UTF-8",
},
body: JSON.stringify({ prompt: input }),
});
if (!res.ok) {
throw new Error('Response error');
}
const result = await res.json();
const newGptId = newId + 1;
const newGptMessage = { id: newGptId, content: result.gptResponseMessage, sender: "other" };
setChatLog([...updatedMessages, newGptMessage]);
} catch (error) {
console.error('Error fetching GPT response:', error);
}
};
return (
<form onSubmit={handleSubmit} className="p-3 bg-gray-200 flex justify-between items-center">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
className="w-full p-2 mr-2 rounded focus:outline-none text-gray-800"
placeholder="メッセージを入力..."
/>
<button type="submit" className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
送信
</button>
</form>
)
}
export default ChatForm
-
handleSubmit
関数の中身を作成していきます。 - まず、ユーザーが送信する新規メッセージの処理を記述します。
- これまでのメッセージが格納されている
chatLog
から、全部のメッセージ数をカウント・取得し、その数より+1
した値をnewId
とします。 - idに
newId
を指定し、content
に現在フォームに入力されている値(=input
)を指定し、sender
はuser
として、ユーザーの新規メッセージを作成します。 - これまでのメッセージをスプレッド構文で展開し、ユーザーの新規メッセージを追加した新しい配列を定義します。
-
setChatLog(新しい配列)
でchatLogを更新します。 - フォームに表示されているテキストは不要なため、ここで画面から削除します
- これまでのメッセージが格納されている
- 次に、ChatGPTからの返答を獲得するためのAPIを叩きます
- 先ほど
app/api/response/route.ts
に作成したPOST関数を使って、ChatGPTにメッセージを送信します -
fetch
の引数に/api/response
とmethod
,headers
,body
を指定してリクエストします。 - bodyには、フォーム内のテキスト(=
input
)をprompt
として渡します。 -
res.json()
を使用して、レスポンスのボディからJSONデータを取得します。ここに、ChatGPTからの返答が入っています。(POST関数でconst response = NextResponse.json({ gptResponseMessage })
をしている) - 先ほど作成したユーザーの新規メッセージに
+1
した値をnewGptId
に代入します。 - idに
newGptId
を指定し、content
にChatGPTからの返答(=result.gptResponseMessage
)を指定し、sender
はother
として、ChatGPTの返答メッセージを作成します。 - これまでのメッセージをスプレッド構文で展開し、ChatGPTの返答メッセージを追加した新しい配列を定義し、
chatLog
を更新します。
- 先ほど
以上で、チャットの記録をStateで管理する簡単なチャットアプリを作成することができました。
Discussion