Next.js14 API Routes × GPTのAPIを使用して行き先を指図してくるおじさんを作る
できたもの
こんな感じで行きたい場所について相談すると、命令口調で横柄なおじさんが「お前はここに行け」といったスタンスで行き先を提案(指図)してくれます。
GWだけどどこ行ったらいいんだ。。みたいな時や、デートでカフェ出たけど次どこ行けばいいんだ。。みたいな瞬間にこのおじさんを召喚することで選択の苦悩から解放されます。
フロント
まずNext.jsのセットアップをします。
npx create-next-app destination-advice-app
cd destination-advice-app
次に、おじさんの画像が欲しいのでGPTのweb UIで「恰幅のいい髭がある行き先を指図してくる強面のおじさんの画像」のような雑なプロンプトを投げます。
それで生成された以下おじさんをpublicディレクトリに格納します。
次に実装です。フロントのコードは以下になります。今回はAPI Routesを使用しリクエストを受けた後の処理は、app/api/route.tsに置いたので、エンドポイントは「/api」となります。
API Routesを使用するので別途バックエンドの環境は作らず、Next.jsで完結させます。その為、デプロイもVarcelにホストすれば機能します。
簡易的なUIなので別途コンポネを作成せず、app/page.tsxに記載しました。
stateは3種類で、おじさんへの問いを管理するhint、おじさんからのレスを管理するresponse、おじさんからのレスを待つ間を管理するisLoadingとなります。
"use client"
import { useState } from 'react';
export default function Page() {
const [hint, setHint] = useState(''); // ユーザーの入力を管理する状態
const [response, setResponse] = useState(''); // APIからのレスポンスを管理する状態
const [isLoading, setIsLoading] = useState(false); // ローディング状態を管理する状態
const askDestination = async () => {
setIsLoading(true);
try {
const res = await fetch('/api', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ hint }),
});
const data = await res.json();
setResponse(data.message);
} catch (error) {
setResponse('エラーが発生しました。');
}
setIsLoading(false);
};
return (
<>
<div className='container'>
<p>
<img src="/uncle.webp" alt="おじさんの画像" />
</p>
<input
type="text"
value={hint}
onChange={(e) => setHint(e.target.value)}
placeholder="どんな場所に行きたいんだ?"
style={{ margin: '20px auto', padding: '10px', maxWidth: '500px', width: '100%' }}
/>
<button onClick={askDestination} disabled={isLoading} style={{ padding: '10px 20px' }}>
{isLoading ? '聞いています...' : '聞いてみる'}
</button>
<p>{response}</p>
</div>
</>
);
}
API
API側は以下になります。route.tsに記載します。
最初おじさんからのレスにGPT-4のモデルを使いましたが、回答の具体性は上がったもののレスが遅すぎてVarcelの無料ホスト環境だとサーバーエラーになることが多かったです。
その為、gpt-3.5-turbo-0125に切り替え、レスも10秒以内には返ってくるようになりました。
ただ、回答の質が下がったのか却って口調の投げやりさが増してワイルドな感じになりました。
たまに敬語になる時があるものの、想像していた行き先指図おじさんの口調は再現できました。
import type { NextRequest } from 'next/server';
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
async function fetchGPTResponse(prompt: string) {
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${OPENAI_API_KEY}`
},
body: JSON.stringify({
model: "gpt-3.5-turbo-0125",
messages: [{ role: "system", content: "おじさんが国内の行き先を横柄な命令口調で提案する。" },
{ role: "user", content: prompt }]
})
});
const data = await response.json();
return data.choices[0].message.content;
}
export async function POST(request: NextRequest) {
if (request.method === 'POST') {
const body = await request.json();
const hint = body.hint;
const gptPrompt = `観光地やレストランについて提案してください。ヒント: ${hint}`;
try {
const gptResponse = await fetchGPTResponse(gptPrompt);
return new Response(JSON.stringify({ message: gptResponse }), {
status: 200,
headers: {
'Content-Type': 'application/json'
}
});
} catch (error) {
console.error('Error calling OpenAI API:', error);
return new Response(JSON.stringify({ message: "サーバーエラーが発生しました。" }), {
status: 500,
headers: {
'Content-Type': 'application/json'
}
});
}
} else {
return new Response('Method Not Allowed', {
status: 405,
headers: {
'Allow': 'POST'
}
});
}
}
Discussion