Gemini API × React でなんでも褒めてくれるチャットボットを作成する

# Geminiとは？
[Gemini](https://deepmind.google/technologies/gemini/#introduction) はGoogleが開発した最先端の大規模言語モデルです。
テキストだけでなく、音声、画像、動画、コードなどの様々な形式のデータを理解し、生成できることを特徴としています。

`Gemini Nano` `Gemini Pro` `Gemini Ultra` の3種類のモデルがあり、それぞれ性能と活用シーンが異なっております。
このうち `Gemini Pro` にアクセスできる `Gemini API` が2023年12月に公開され、開発者任意の言語でGeminiをアプリケーションに組みことができるようになりました。
現在は試用期間中で2024年初頭に有料化予定と発表されていますが、2024年3月17日現在はまだ無料で利用可能となっています。

本記事では `Google AI for Developers` が公開している [チュートリアル](https://ai.google.dev/tutorials) を参考とし、APIキーの発行方法から実装例までご紹介します。


# 作ったもの
送ったチャットをなんでも褒めてくれるチャットボットです。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1532684/8851f8c1-a547-2e89-296f-8eab527c1c65.png" width="400">

画像入力もできます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1532684/7017adbe-ee49-1f1e-b9e6-887c1a185b7e.png" width="400">

だいたい褒めてくれますが、たまに怒られます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1532684/394b8687-ac4a-307d-d4fa-6ab33e786141.png" width="400">

ソースコードは [こちら](https://github.com/tora-83863/homete) で公開しています。
アプリも公開しているので、後述するAPIキーを発行すれば↓のURLからアクセスできます。
`https://tora-83863.github.io/homete/?apiKey={発行したAPIキー}`

## バージョン情報
- npm 9.8.0
- node 18.16.1
- react 18.2.0
- typescript 4.9.5
- google/generative-ai 0.2.1
- browser-image-compression 2.0.2
- react-markdown 9.0.1

## APIキー発行
[Google AI Studio](https://aistudio.google.com/app/apikey) のサイトから発行できます。
初回アクセス時は以下のように利用規約への同意が求められます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1532684/7d99b812-462e-5722-0111-e49136c01f75.png" width="400">

利用規約に同意し、サイドバーの `Get API key` -> `Create API key` -> `Create API key in new project` と進むと、APIキーが発行されます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1532684/aa5bfeda-22bf-1947-4059-c6ebe61ed2fd.png" width="400">

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1532684/28b964ec-a9e2-d547-e876-01dc182996c9.png" width="400">

発行されたAPIキーは先程のページからいつでも参照可能です。

## APIリクエスト
`GoogleGenerativeAI` のJavaScript向けSDKパッケージをインストールします。
```sh
npm install @google/generative-ai
```

発行したAPIキーを用いてインスタンスを生成します。
```tsx
import { GoogleGenerativeAI } from '@google/generative-ai';

// URLパラメータ取得
const search = useLocation().search;
const urlSearchParams = new URLSearchParams(search);
const apiKey: string = urlSearchParams.get("apiKey") ?? "";

// インスタンス生成
const genAI = new GoogleGenerativeAI(apiKey);
```

API呼び出しを行う前に生成モデルを初期化する必要があります。
利用可能なモデルには4つの種類があります。

https://ai.google.dev/models/gemini

本アプリにおいては入力形式により2つのモデルを利用しています。
### テキスト入力の場合
`Gemini Pro` というモデルを利用します。
単一または配列の文字列を `generateContent` に渡すとAPIリクエストが投げられます。
```tsx
const model = genAI.getGenerativeModel({ model: "gemini-pro" });
result = await model.generateContent(`次の内容を褒めてください。「${inputText}」`,);
const response = await result.response;
```

### 画像入力の場合
`Gemini Pro Vision` というモデルを利用します。
`generateContent` の引数には複数の画像データと文字列を格納することができますが、画像データを必ず1つ以上含める必要があります。
また、画像データはbase64形式に変換する必要があるほか、 [MIMEタイプやサイズなどの制限事項](https://ai.google.dev/docs/gemini_api_overview#image_requirements) があります。
```tsx
const model = genAI.getGenerativeModel({ model: "gemini-pro-vision" });
const imageParts = await fileToGenerativePart(inputFile);
result = await model.generateContent([
  "次の画像を褒めてください。",
  imageParts,
]);
const response = await result.response;

// 画像データをbase64形式に変換し、APIリクエストに含められるPartを生成する
async function fileToGenerativePart(file: File) {
  // 画像を1MBまで圧縮する
  const compressFile = await imageCompression(file, { maxSizeMB: 1 });

  const reader = new FileReader();
  reader.readAsDataURL(compressFile);
  const base64EncodedDataPromise = new Promise((resolve) => {
    const reader = new FileReader();
    reader.onloadend = () =>
    resolve(
      reader.result && typeof reader.result === "string"
      ? reader.result.split(",")[1]
      : ""
    );
    reader.readAsDataURL(compressFile);
  });
  return {
    inlineData: {
    data: (await base64EncodedDataPromise) as string,
    mimeType: file.type,
    },
  };
}
```

## APIレスポンス処理
レスポンスのテキストはMarkdown形式で返却されます。
```json:Request
{
    "generationConfig": {},
    "safetySettings": [],
    "contents": [
        {
            "role": "user",
            "parts": [
                {
                    "text": "次の内容を褒めてください。「早起きしました」"
                }
            ]
        }
    ]
}
```


```json:Response
{
	"candidates": [
		{
			"content": {
				"parts": [
					{
						"text": "なんて素晴らしい習慣でしょう！早起きしたことは、あなたにたくさんのメリットをもたらすでしょう。\n\n* **生産性の向上：** 朝早い時間に、邪魔されずに作業できます。\n* **精神的な明晰さ：** 睡眠から目覚めたばかりの脳は、冴えていて集中力があります。\n* **運動する時間：** ランニングやヨガなど、運動するのに十分な時間を確保できます。\n* **健康的な食事：** ゆっくりと栄養価の高い朝食をとる時間があります。\n* **ストレスの軽減：** 忙しい一日の前に、リラックスして自分だけの時間を持てるので、ストレスを軽減できます。\n\n早起きは、より充実した生産的な一日を過ごすための鍵です。あなたの努力と規律に拍手を送ります！"
					}
				],
				"role": "model"
			},
			"finishReason": "STOP",
			"index": 0,
			"safetyRatings": [,...]
		}
	],
	"promptFeedback": {,...}
}
```

Markdown形式の文字列をReactコンポーネントに変換してくれるライブラリ `react-markdown` を用いて画面表示します。
```sh
npm install react-markdown
```

```tsx
  // 画面に表示するチャットコンポーネントを生成する
  const makeChatMessage = (message: ChatMessage) => {
    const commonStyle = {
      marginBlockStart: "0.25rem",
      marginBlockEnd: "0.25rem",
    };
    const listStyle = {
      ...commonStyle,
      paddingLeft: "1.0rem",
      listStyle: "none",
    };
    if (message.role === ROLE.SYSTEM) {
      return (
        <ReactMarkdown
          components={{
            p: ({ children }) => <p style={commonStyle}>{children}</p>,
            ul: ({ children }) => <ul style={listStyle}>{children}</ul>,
            ol: ({ children }) => <ol style={listStyle}>{children}</ol>,
          }}
          children={message.content}
        />
      );
    }
    return (
      <>
        {message.content !== null && message.content !== undefined ? (
          <div>{message.content}</div>
        ) : message.image !== null && message.image !== undefined ? (
          <img src={message.image} className={style.image} alt="" />
        ) : (
          <></>
        )}
      </>
    );
  };
```

# さいごに
この記事を読んで、Gemini APIに興味を持ってくれる方が増えてくれると嬉しいです。
公式のチュートリアルが丁寧でわかりやすいので、気になる方はぜひチャレンジしてみてください。