生成AIの一つであるDeepSeekのapiを用いて、このようにチャンピオン名(lol)のガイドを作ってくれるようにした。
この記事は前回の記事(Laravel Webスクレイピング機能の搭載 ②)
の続きである。
ビュー部分の作成
<!-- 検索フォームをスタイリング -->
<div class="max-w-7xl mx-auto px-6 mt-4">
<form id="championSearchForm" class="flex gap-2 mb-6">
<input type="text" id="championInput" name="query" placeholder="チャンピオン名を入力"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
<button type="submit" class="px-4 py-2 bg-blue-500 text-white font-semibold rounded-lg hover:bg-blue-600">
検索
</button>
</form>
</div>
まず検索フォームがトリガーとなってくるので再度こちらを見てみる。
ここでは検索フォームがid="championSearchForm"
と定義されており、また入力された情報はid="championInput"
と定義されていた。
<!-- 既存のテーブルの下に追加 -->
<!-- DeepSeek APIによるチャンピオンガイド -->
<!-- 既存のテーブルの下に追加 -->
<div class="max-w-6xl mx-auto px-14 mt-40 mb-20">
<div class="bg-white rounded-lg shadow-md p-6">
<h2 class="text-2xl font-bold text-gray-800 mb-4" id="guideTitle">ガレンの戦い方ガイド</h2>
<div id="championGuide" class="prose max-w-none">
<p class="text-gray-500">チャンピオンを検索するとガイドが表示されます</p>
</div>
</div>
</div>
こちらがDeepSeek apiを導入する箇所のhtml部分である。デフォルトで「チャンピオンを検索するとガイドが表示されます」と書かれているが、先ほどの検索フォームにsubmitつまり何かが検索されると、以下のJavaScriptの通りに実行されるシステムだ。
<script>
document.addEventListener('DOMContentLoaded', function() {
const guideElement = document.getElementById('championGuide');
const searchForm = document.getElementById('championSearchForm');
const guideTitle = document.getElementById('guideTitle');
searchForm.addEventListener('submit', async function(e) {
e.preventDefault();
const champion = document.getElementById('championInput').value.trim();
if (!champion) return;
guideElement.innerHTML = '<p class="text-blue-500">読み込み中...</p>';
guideTitle.textContent = `${champion}の戦い方ガイド`;
try {
const response = await fetch(`/champion-guide?champion=${encodeURIComponent(champion)}`);
const guide = await response.text();
// 改行を<br>タグに変換して表示
guideElement.innerHTML = guide.split('\n')
.map(line => `<p>${line}</p>`)
.join('');
} catch (error) {
guideElement.innerHTML = `<p class="text-red-500">エラー: ${error.message}</p>`;
}
});
});
</script>
async function(e) { e.preventDefault(); }
async function(e)
これは非同期関数 (async function) で、引数 e はイベントオブジェクトを受け取ります。
例えば、click や submit イベントで発生した情報を e に格納します。
e.preventDefault();
e に格納されているイベントのデフォルトの動作を止めます。
フォームの submit イベントでは、デフォルトではページがリロードされますが、これを防ぎます。
さて、
try {
const response = await fetch(`/champion-guide?champion=${encodeURIComponent(champion)}`);
const guide = await response.text();
// 改行を<br>タグに変換して表示
guideElement.innerHTML = guide.split('\n')
.map(line => `<p>${line}</p>`)
.join('');
}
ここの部分が非常に重要だ。
const response = await fetch("/champion-guide?champion=${encodeURIComponent(champion)}");
ここでルーターで/champion-guide
と名前定義されているコントローラーのメソッドの返り値を変数responseに格納する。
このとき、champion
を変数として送ります。(コントローラーの話はあとで)
const guide = await response.text();
response.text() でレスポンスを プレーンテキスト として取得。
JSON なら response.json() を使うが、ここでは単なるテキストなので text() を使用。
guideElement.innerHTML = guide.split('\n').map(line => "<p>${line}</p>").join('');
guide.split('\n') で、テキストの改行 \n を基準に分割 → 配列にする。
またそれぞれの行を<p>
タグで囲む。.joinの部分で配列を結合して、HTML に返す。
さて、コントローラー部分を見ていく。
コントローラーの作成
さて先ほどのfetch関数で述べた部分だが、ルーターに以下が定義されている。
Route::get('/champion-guide', [ChampionController::class, 'getGuide']);
したがってresponseの値はChampionControllerのgetGuideメソッドの返り値だ。
getGuide関数は以下のように定義されている。
public function getGuide(Request $request)
{
$champion = $request->query('champion', 'ガレン');
$apiKey = env('OPENROUTER_API_KEY');
try {
$response = Http::withHeaders([
'Authorization' => 'Bearer ' . $apiKey,
'HTTP-Referer' => env('APP_URL', 'http://localhost'),
'X-Title' => 'LoL Champion Guide',
'Content-Type' => 'application/json',
])->post('https://openrouter.ai/api/v1/chat/completions', [
'model' => 'anthropic/claude-3-haiku',
'messages' => [
[
'role' => 'user',
'content' => "{$champion}のリーグ・オブ・レジェンドのチャンピオンガイドを、初心者向けに詳細かつ具体的に作成してください。"
]
]
]);
$guide = $response->json();
// JSONからガイドの内容を抽出
$guideContent = $guide['choices'][0]['message']['content'] ?? 'ガイドが見つかりませんでした。';
// レスポンスを直接返す(HTMLエスケープ)
return response($guideContent)
->header('Content-Type', 'text/plain; charset=UTF-8');
} catch (\Exception $e) {
Log::error('API Request Failed', [
'error' => $e->getMessage()
]);
return response('エラーが発生しました: ' . $e->getMessage(), 500);
}
}
$apiKey = env('OPENROUTER_API_KEY');
ここで$apikeyにdeepseek apiの公式サイトで取得したキーを定義している。無料なので各自調べてほしい。簡単。
.envでキーを貼っておこう。
OPENROUTER_API_KEY=sk-or-v1-長い文字列
'Authorization' => 'Bearer ' . $apiKey,
APIキーを設定(認証に必要)。
'HTTP-Referer' => env('APP_URL', 'http://localhost'),
リファラを設定(APIがどこからのリクエストかを確認するため)。
APP_URL は .env ファイルで設定されている:
'X-Title' => 'LoL Champion Guide',
API のリクエスト目的を明示するためのカスタムヘッダー。
'Content-Type' => 'application/json',
リクエストのデータ形式を JSON に指定。
->post('https://openrouter.ai/api/v1/chat/completions', [
OpenRouter API のエンドポイント に POST リクエストを送信。
$guide = $response->json();
API のレスポンスを JSON として取得 する。
$guideContent = $guide['choices'][0]['message']['content'] ?? 'ガイドが見つかりませんでした。';
急に配列が出てきたと思ったが、json形式が以下のようになっていることが原因だ。
$guide = [
"choices" => [
[
"message" => [
"content" => "ガレンは耐久力が高く、初心者向けのチャンピオンです..."
]
]
]
];