0. はじめに
この記事では、AIチャットアプリを作成していきます!
最後まで読むと↓のようなアプリを作れるようになります 👏
工夫したポイント
- ストリーム形式でレスポンスを受け取る
- ユーザ体験の向上 ✨
1. 技術スタック
1.1. Hono.js
バックエンド(API)を作成します。
最近耳にする機会が増えたのでこれを機に触ってみたかったので採用しました。
※フロント側で直接OpenAI APIを呼べばいいので、正直このチャットアプリにAPIは不要ですがHono.js使いたかったんです・・
ChatGPT曰く、以下のような特徴があります。
- 軽量で高速: 最小限のコア機能を提供し、レスポンスが非常に高速であるため、高パフォーマンスなAPIを簡単に構築できます
- シンプルなAPI: 直感的で使いやすいAPI設計により、少ないコードでWebアプリケーションやAPIを構築できます
- クロスランタイム対応: Hono.jsは、Node.js、Deno、Bunといった複数のJavaScriptランタイムをサポートし、柔軟に選択できます
- ミドルウェアサポート: ミドルウェア機能を簡単に追加でき、リクエストの処理やエラーハンドリングなどが簡単に実装できます
ランタイムには Bun を採用しました。これも個人的には初の試みです。
1.2. React
フロントページを作成します。
業務ではVue.jsを使うことが多いので、プライベートではReactを使うことが多いです。
1.3. OpenAI API
今回作るチャットアプリの要です。
2. 事前準備 - OpenAI APIキー発行
OpenAI APIを利用するためには、OpenAI API Keyを発行する必要があります。
OpenAI APIの利用には料金が発生します
料金体制はこちらをご確認ください
2.1. OpenAIアカウントを作成する
登録画面はこちら
2.2. 支払い情報を登録する
Settings > Billing から残高をチャージできるはずです。
最低5ドルからチャージ可能で、デフォルトで「Auto recharge is off」になっているので、チャージした金額以上請求が来ることはないはず(ちょっと不安)
2.3. APIキーを発行する
Settings > API keys からAPIキーを作成します。
APIキーは一度しか表示されないためメモしておいてください。
3. 構成
成果物がイメージしやすいように最終的な構成を示しておきます。
ほとんどのファイルが後述する bun init
や npm create vite@latest
コマンドで作成されたものです。
フロントエンドとバックエンドが同じリポジトリ内で管理されるモノリポジトリ構成にしてます。
ai_chat_app/
∟ backend/
∟ index.ts
∟ package.json
∟ frontend/
∟ src/
∟ App.tsx
∟ main.tsx
∟ index.html
∟ package.json
※説明に不要なファイルは省略してます
4. バックエンド( Hono.js )の実装
この章は /backend
配下で作業を行います。
# ルートディレクトリ作成(ここではプロジェクト名をai_chat_appとしてます)
mkdir ai_chat_app
cd ai_chat_app
# バックエンド用ディレクトリ作成
mkdir backend
cd backend
Bun を使うのでまだ入っていない方は下記コマンドでインストールします。
curl -fsSL https://bun.sh/install | bash
4.1. bun init
bun init
質問に答えていく
package name (backend): # パッケージ名の入力
entry point (index.ts): # エントリーポイントの入力
4.2. 必要なパッケージのインストール
# Hono.js
bun add hono
# OpenAI API
bun add openai
4.3. 【本題】 API実装
仕様
HTTPメソッド | エンドポイント |
---|---|
POST | /openai/stream/ |
リクエストパラメータ
パラメータ | 必須 | 説明 |
---|---|---|
message | ○ | AIへの質問 |
理解が難しそうな部分にはコメント入れてます!
import { Hono } from "hono";
import { OpenAI } from "openai";
const app = new Hono();
const openai = new OpenAI({
apiKey: "ご自身のAPIキーを入れてください",
});
app.post("/openai/stream/", async (c) => {
const { message } = await c.req.json();
if (!message) {
return c.json({ error: "message is required" }, 400);
}
// OpenAI APIへリクエスト
const stream = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [{ role: "user", content: message }],
stream: true,
});
return new Response(
// ストリーム作成
new ReadableStream({
async start(controller) {
// OpenAI APIから返される stream のデータをチャンク(データ片)ごとに順番に処理
for await (const chunk of stream) {
// 新しく受け取ったテキスト
const text = chunk.choices[0]?.delta?.content || '';
// ストリームに追加
controller.enqueue(text);
}
// ストリームを閉じる
controller.close();
},
}),
{
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
},
);
});
export default {
port: 3000,
fetch: app.fetch,
};
4.4. 動作確認
bun index.ts
http://localhost:3000/
で立ち上がった場合、下記のようなリクエストで動作確認します。
curl -X POST http://localhost:3000/openai/stream/ \
-H "Content-Type: application/json" \
-d '{"message": "今日の晩ごはんのレシピを考えて"}'
バックエンド完了!!!
5. フロントエンド(React)の実装
5.1. Reactプロジェクト作成
npm create vite@latest
質問に答えていく
✔ Project name: … frontend
✔ Select a framework: › React
✔ Select a variant: › TypeScript
この章はここで作成した /frontend
配下で作業を行います。
cd frontend
5.2. 【本題】 フロントページ実装
ここも理解が難しい部分はコメント入れてます!
※スタイルは省略
import React, { useState, useRef, useEffect } from 'react'
import './App.css'
type Message = {
role: 'user' | 'assistant'
content: string
}
function App() {
const [messages, setMessages] = useState<Message[]>([])
const [input, setInput] = useState('')
const [streamData, setStreamData] = useState('') // ストリームデータを一時的に格納
const [isLoading, setIsLoading] = useState(false)
const messagesEndRef = useRef<HTMLDivElement>(null)
// 最後のメッセージまでスクロール(必須機能ではないです)
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}
useEffect(() => {
scrollToBottom()
}, [messages])
// ユーザからメッセージが送信された際の処理
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!input.trim() || isLoading) return
const userMessage = { role: 'user', content: input }
setMessages(prev => [...prev, userMessage])
setInput('')
setIsLoading(true)
try {
// 4章で作成したAPIへリクエスト
const response = await fetch("http://localhost:3000/openai/stream/", {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ message: input })
})
const reader = response.body?.getReader()
const decoder = new TextDecoder()
let fullText = ''
while (true) {
const { done, value } = await reader?.read() || { done: true, value: null }
if (done) break // ストリームの終了が宣言されたらループ終了
const text = decoder.decode(value, { stream: true })
setStreamData(prev => prev + text)
fullText += text
}
setMessages(prev => [...prev, {
role: 'assistant',
content: fullText
}])
} catch (error) {
console.error('Error:', error)
setMessages(prev => [...prev, {
role: 'assistant',
content: 'エラーが発生しました。もう一度お試しください。'
}])
} finally {
setStreamData('')
setIsLoading(false)
}
}
return (
<div className="chat-container">
<div className="messages">
{messages.map((message, index) => (
<div
key={index}
className={`message ${message.role === 'user' ? 'user' : 'assistant'}`}
>
<div className="message-content">{message.content}</div>
</div>
))}
{streamData && (
<div className="message ai">
<div className="message-content">{streamData}</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
<form onSubmit={handleSubmit} className="input-form">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="メッセージを入力..."
disabled={isLoading}
/>
<button type="submit" disabled={isLoading || !input.trim()}>
送信
</button>
</form>
</div>
)
}
export default App
5.3. 動作確認
npm run dev
これで冒頭にお見せしたチャットアプリが再現されていると思います!
6. まとめ
この記事では、Hono.jsとReact、OpenAI APIを組み合わせて、AIチャットアプリをゼロから構築しました。
ストリーム形式でレスポンスを受け取ることでスムーズなユーザー体験を簡単に実現することができました!
また、Hono.jsやBunなど個人的に新しい技術に触れる良い機会にもなりました。
最後まで読んでいただきありがとうございました!!!