6. Next.js でチャットボット - 3

Postman

POSTリクエスト作成

POSTリクエストを作成します。

http://localhost:3000/api/chat

JSONデータ設定

「Body」タブで「raw」「JSON」で設定し、POSTするJSONデータを記述します。

レスポンス確認

【Send】ボタンをクリックすると、APIからJSONデータがレスポンスされました。

Gemini APIの利用

Gemini API処理

チャット履歴

チャット履歴「history」を用意します。データ型は「@google/generative-ai」のContentとします。

app/api/chat/route.ts
import { NextRequest, NextResponse } from 'next/server';
// Contentインポート
import { Content } from "@google/generative-ai";
import { Message } from "@/app/interfaces/Message";

const history: Content = [];
...

APIキー読み込み

.env.local から Gemini APIキーを読み込みます。

app/api/chat/route.ts
export async function POST(req: NextRequest) {
    // Gemini APIキーを読み込み
    const API_KEY = process.env.GEMINI_API_KEY;
    if (!API_KEY) return NextResponse.json({ error: "Not found API KEY" });

    const message = await req.json();
    if (!message) return NextResponse.json({ error: "Not found message" });
    ...
}

Gemini API連携

Gemni APIにプロンプトをリクエストし、ボットメッセージをレスポンスします。

app/api/chat/route.ts
import { NextRequest, NextResponse } from "next/server";
import { Content, GoogleGenerativeAI } from "@google/generative-ai";
import { Message } from "@/app/interfaces/Message";

const history: Content[] = [];

export async function POST(req: NextRequest) {
    const API_KEY = process.env.GEMINI_API_KEY;
    if (!API_KEY) return NextResponse.json({ error: "Not found API KEY" });

    // POSTデータ取得
    const message:Message = await req.json();
    if (!message) return NextResponse.json({ error: "Not found message" });

    // Gemni APIにリクエスト
    const genAI = new GoogleGenerativeAI(API_KEY);
    const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" });
    const chat = model.startChat({ history: history });

    try {
        const result = await chat.sendMessage(message.content);
        if (!result) return NextResponse.json({ error: "Not found message" });
        // ボットメッセージ
        const botMessage: Message = {
            sender: "bot",
            content: result.response.text()
        }

        const data = {
            client: message,
            // ボットメッセージ
            bot: botMessage,
        }

        return NextResponse.json(data);
    } catch (error) {
        console.log(error)
        return NextResponse.json({ error: "Gemini API error" });
    }
}
  • try-catchを利用して、Exception処理

Postman

動作確認

Postmanでチャットメッセージを変更して、Gemini APIでレスポンスを確認してみましょう。

ソース

app/api/chat/route.ts
import { NextRequest, NextResponse } from "next/server";
import { Content, GoogleGenerativeAI } from "@google/generative-ai";
import { Message } from "@/app/interfaces/Message";

const history: Content[] = [];

export async function POST(req: NextRequest) {
    const API_KEY = process.env.GEMINI_API_KEY;
    if (!API_KEY) return NextResponse.json({ error: "Not found API KEY" });

    const message:Message = await req.json();
    if (!message) return NextResponse.json({ error: "Not found message" });

    const genAI = new GoogleGenerativeAI(API_KEY);
    const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" });
    const chat = model.startChat({ history: history });

    try {
        const result = await chat.sendMessage(message.content);
        if (!result) return NextResponse.json({ error: "Not found message" });

        const botMessage: Message = {
            sender: "bot",
            content: result.response.text()
        }

        const data = {
            client: message,
            bot: botMessage,
        }

        return NextResponse.json(data);
    } catch (error) {
        return NextResponse.json({ error: "Gemini API error" });
    }
}

App Router

App Routerとは

App Routerは、Next.js 13から導入された新しいルーティングシステムで、従来のファイルベースのルーティングより柔軟で強力な機能となりました。

App Routerの主要機能

ファイルベースのルーティング

Next.jsではファイル構造に基づいて自動的にルーティングされます。 appディレクトリ内のファイルとフォルダがそのままルートになります。

Layoutとページの分離

レイアウト「app/layout.tsx」を定義することで、各ページ間で共有レイアウトを利用できます。

Server ComponentsとClient Components

Next.jsでは、Server ComponentsClient Componentsの両方をサポートします。SSR(サーバー側でレンダリングされるコンポーネント)とCSR(クライアント側で動作するコンポーネント)を使い分けることができます。

トップページのルーティング

App Routerでは「app」ディレクトリの中に「page.*」ファイルが存在すると自動ルーティングされます。

  • 拡張子の部分は「jsx」「tsx」

レイアウトとコンポーネント

Next.jsではデフォルトでは「layout.tsx」がレイアウト、「page.tsx」がページコンポーネントになっています。

トップページ
URI レイアウト ページコンポーネント
http://localhost:3000/ app/layout.tsx app/page.tsx

フロントエンド作成

ファイル構成

my-app/
├── app/
│   ├── api/
│   │   └── chat/
│   │       └── route.ts
│   └── page.tsx
├── styles/
│   └── globals.css
├── .env.local
├── tailwind.config.js
├── tsconfig.json
└── package.json

レイアウト

レイアウト修正

レイアウトファイル「app/layout.tsx」を開きます。

children

Next.jsのレイアウトファイルは、{children} の部分に各ページコンポーネントが表示されるように設計されています。

app/layout.tsx
export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className="container">{children}</body>
    </html>
  );
}

Metadata修正

「metadata」 の「title」「description」を修正します。

app/layout.tsx
// Metadata修正
export const metadata: Metadata = {
  title: "Gemini Chatbot",
  description: "Gemini APIを利用したチャットボットアプリです。",
};

CSS修正

「app/layout.tsx」のデフォルトは、同階層の「globals.css」を読み込んでいます。

app/layout.tsx
import type { Metadata } from "next";
// globals.css 読み込み
import "./globals.css";

「app/globals.css」を開きます。

余分なCSSを削除し、TailwindCSSだけにします。

@tailwind base;
@tailwind components;
@tailwind utilities;

動作確認

ページタイトルや説明文が変更されました。このように各ページの共通部分(HTMLの基本構造やナビゲーションなど)はレイアウトで管理します。

トップページ修正

今回のページはCSRに対応したClient Componentで作成します。

ページコンポーネントを開く

トップページのコンポーネント「app/page.tsx」を開きます。

use client

先頭にuse clientを追加してClient Componentにします。

app/page.tsx
// 追加:Client Componentとする 
'use client';

import { useState } from "react";

ページコンポーネント修正

JSXを修正します。

app/page.tsx
'use client';

export default function Home() {
    return (
        <main className="flex flex-col justify-center">
            <h1 className="text-2xl p-5">Gemini Chatbot</h1>
        </main>
    );
}

動作確認

トップページが変更されました。

メッセージ入力処理

フォーム作成

メッセージの入力フォームを作成します。

app/page.tsx
'use client';

export default function Home() {
  return (
    <main className="flex flex-col justify-center">
      <div className="bg-white shadow-md p-4 z-10">
        <h1 className="text-2xl p-5">Gemini Chatbot</h1>
        <div className="flex">
          <input
            type="text"
            className="w-full p-2 border border-gray-300 rounded-l mr-0"
            placeholder="Type your message..."
          />
          <button className="w-1/6 bg-blue-500 text-white p-2 rounded-r">
            Send
          </button>
        </div>
      </div>
    </main>
  );
}

フォーム確認

入力フォームが表示されるか確認します。

イベント処理

テキストボックスに入力したデータを設定するようにします。

データ定義

入力データの定義します。

app/page.tsx
export default function Home() {
    // 入力データ定義
    const [inputMessage, setInputMessage] = useState<string>('');

    ...
}

イベントハンドラー作成

changeMessageHandler() を追加します。

app/page.tsx
export default function Home() {
    const [inputMessage, setInputMessage] = useState<string>('');

    const changeMessageHandler = (e: React.ChangeEvent<HTMLInputElement>) => {

    };
    ....
}

テキストボックスの値を inputMessage() に設定します。

app/page.tsx
export default function Home() {
    const [inputMessage, setInputMessage] = useState<string>('');

    const changeMessageHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
        // デバック用
        console.log(e.target.value);
        // テキストボックスのデータを inputMessage に設定
        setInputMessage(e.target.value);
    };

    ....
}

イベント追加

inputタグにonChangeイベントを追加し、値には「inputMessage」を設定します。

app/page.tsx
export default function Home() {
    ...

    return (
        <main className="flex flex-col justify-center">
            <div className="bg-white shadow-md p-4 z-10">
                <h1 className="text-2xl p-5">Gemini Chatbot</h1>
                <div className="flex">
                    <input
                        onChange={changeMessageHandler}
                        value={inputMessage}
                        type="text"
                        className="w-full p-2 border border-gray-300 rounded-l mr-0"
                        placeholder="Type your message..."
                    />
                    <button className="w-1/6 bg-blue-500 text-white p-2 rounded-r">
                        Send
                    </button>
                </div>
            </div>
        </main>
    );
}

動作確認

テキストボックスに入力するたびに、メッセージがコンソール表示されるか確認します。

クリックイベント

ボタンをクリックしたら、メッセージをAPIに送信するようにします。

イベントハンドラー追加

ボタンクリックのイベントハンドラーを作成します。

app/page.tsx
export default function Home() {
    ...

    const sendHandler = async (e: React.MouseEvent<HTMLButtonElement>) => {
        if (inputMessage.trim() === '') return;

        setInputMessage('');
    };

    ...
}

イベント追加

buttonタグにonClickイベントを追加します。

app/page.tsx
export default function Home() {
    ...

    return (
        <main className="flex flex-col justify-center">
            <div className="bg-white shadow-md p-4 z-10">
                <h1 className="text-2xl p-5">Gemini Chatbot</h1>
                <div className="flex">
                    <input
                        onChange={changeMessageHandler}
                        value={inputMessage}
                        type="text"
                        className="w-full p-2 border border-gray-300 rounded-l mr-0"
                        placeholder="Type your message..."
                    />
                    <button className="w-1/6 bg-blue-500 text-white p-2 rounded-r"
                        onClick={sendHandler}>
                        Send
                    </button>
                </div>
            </div>
        </main>
    );
}

動作確認

【Send】ボタンをクリックして、テキストボックスがブランクになるか確認します。