ドシロウトもGemini APIで遊んでみたかったのでロリポですぐ動くコードを作った

## 0.はじめに

【注意】この記事は技術者向けではありません。シロウト向けなのでご注意ください。　

私は、だだのド素人でフロントエンドのJavaScriptが少し読めて、少しカスタマイズできる程度です。

そんな私が生成AIを試したくてGemini APIを試したときに動いたコードを共有します。

カスタマイズした画面はこんな感じです。（デザインいじってませんが…）

![コメント 2024-04-03 102332.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35574/3f654387-0867-56fc-af60-01eb1c77d412.png)


## 1.制約条件

ドシロウトなので以下の制約がありました。

* フロントエンドのみ(node.jsやPythonなどバックエンドはNG）
* ロリポップのようなレンタル共有WEBサーバーで動く
* ソースコードが公開されており、一般人でも利用できる

## 2.ともかくAPIキーの取得

APIキーの取得は、ネットで検索すれば、いろんな記事が出てくるのでそちらを参照ください。

さっくりいうと・・・

* [Google AI studio](https://aistudio.google.com/app/apikey)でAPIキーが取得できます。

    ![コメント 2024-04-03 130013.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35574/b8d85fa9-5446-21c3-2970-17a5cfaadfaf.png)

* [費用のページ](https://ai.google.dev/pricing?hl=ja)で料金を無料の範囲を確認しておきましょう。（概ね無料なのかな？）

    ![コメント 2024-04-03 112256.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35574/069c41df-5a4b-a6a2-2393-86092fa760ca.png)

## 3.カスタマイズした元のリポジトリ

カスタマイズというレベルじゃないかもしれませんが、元のGithubリポジトリは以下です。

> [Google AI JavaScript SDK](https://github.com/google/generative-ai-js)

リポジトリのクローンやダウンロードは必要な方は、ここからどうぞ

実際に動かすために必要＆カスタマイズしたのは以下のリポジトリの一部になります。

> [generative-ai-js/samples/web/](https://github.com/google/generative-ai-js/tree/main/samples/web)

Google本家のライブラリのソースコードサンプルなので、間違いないのですが、Node.jsが動く環境用でフロントエンドのみでは以下の理由で動かないサンプルです。

* Node.jsでhttpサーバーを動かす前提
* 環境変数にAPIキー設定/取得

## 4.実際のコード

リポジトリの一部から、ロリポップのようなレンタル共有WEBサーバーで動かすための最低限のコードです。

APIキーは、ソースに直接書き込みはできないので、プロンプトで入力する形にしました。

![コメント 2024-04-03 140117.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35574/5a8c50f2-4aca-8ace-496c-4b8b5ea961e2.png)

実際ファイル配置は以下の通りで必要なファイルは３つです。

![コメント 2024-04-03 110642.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35574/2a0bd20b-61b0-bcd0-24e3-9db04f4f1a56.png)

以下はコードです。

```html:index.html
<!doctype html>
<!--
 * @license
 * Copyright 2023 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
-->
<html>
  <head>
    <meta charset="utf-8" />
    <link rel="stylesheet" href="utils/main.css" />
    <link
      href="https://fonts.googleapis.com/css?family=Roboto:400,700"
      rel="stylesheet"
      type="text/css"
    />
    <title>Gemini - Chat</title>
  </head>

  <body>
    <header>Gemini - Chat</header>
    <div class="container">
      <div id="chat-history"></div>
    </div>
    <div class="form-container">
      <form id="form">
        <input id="prompt" />
        <button type="submit">Send</button>
      </form>
      <template id="thumb-template">
        <img class="thumb" />
      </template>
    </div>

    <script type="module">
      import {
        getGenerativeModel,
        scrollToDocumentBottom,
        updateUI,
      } from "./utils/shared.js";

      const promptInput = document.querySelector("#prompt");
      const historyElement = document.querySelector("#chat-history");
      let chat;

      document
        .querySelector("#form")
        .addEventListener("submit", async (event) => {
          event.preventDefault();

          if (!chat) {
            const model = await getGenerativeModel({ model: "gemini-pro" });
            chat = model.startChat({
              generationConfig: {
                maxOutputTokens: 2048,
              },
            });
          }

          const userMessage = promptInput.value;
          promptInput.value = "";

          // Create UI for the new user / assistant messages pair
          historyElement.innerHTML += `<div class="history-item user-role">
            <div class="name">User</div>
            <blockquote>${userMessage}</blockquote>
          </div>
          <div class="history-item model-role">
            <div class="name">Model</div>
            <blockquote></blockquote>
          </div>`;

          scrollToDocumentBottom();
          const resultEls = document.querySelectorAll(
            ".model-role > blockquote",
          );
          await updateUI(
            resultEls[resultEls.length - 1],
            () => chat.sendMessageStream(userMessage),
            true,
          );
        });
    </script>
  </body>
</html>
```

```css:main.css
/**
 * @license
 * Copyright 2023 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

* {
  box-sizing: border-box;
}

header {
  border-bottom: 2px solid rgb(127, 127, 127, 0.5);
  font-size: 24px;
  padding: 16px;
  text-align: center;
}

body {
  font-family: "Roboto", sans-serif;
  margin: 0;
}

.loading::after {
  content: "Loading...";
  display: block;
  font-size: 80%;
  font-style: italic;
  margin: 16px 0;
}

.loading {
  opacity: 0.5;
}

.error {
  color: red;
}

.container,
header,
.form-container {
  margin: 0 auto;
  max-width: 700px;
}

.form-container {
  border-bottom: 2px solid rgb(127, 127, 127, 0.5);
}

img.thumb {
  border: 1px solid grey;
  border-radius: 8px;
  height: 100px;
  margin: 0px 16px 16px 0;
  padding: 2px;
  width: 100px;
}

#form,
.history-item {
  align-items: center;
  display: flex;
  justify-content: center;
  padding: 16px 0;
}

#file {
  flex-grow: 0;
}

#prompt {
  margin: 4px;
  padding: 2px;
  width: 100%;
}

button {
  padding: 2px 16px;
}

.name {
  flex-shrink: 0;
  font-size: 80%;
  margin: 16px 16px 16px 0;
  opacity: 0.5;
  text-align: right;
  width: 50px;
}

blockquote {
  margin: 0;
}

.history-item {
  padding: 0 8px 0 0;
}

.history-item.model-role {
  background: rgba(127, 127, 127, 0.1);
}

.history-item > blockquote {
  flex-grow: 1;
  margin: 0;
}
```

```javascript:shared.js
/**
 * @license
 * Copyright 2023 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { GoogleGenerativeAI } from "https://esm.run/@google/generative-ai";
import { marked } from "https://esm.run/marked";

let API_KEY = window.prompt('Gemini api key',"");


/**
 * Returns a model instance.
 *
 * @param {GoogleGenerativeAI.ModelParams} params
 * @returns {GoogleGenerativeAI.GenerativeModel}
 */
export function getGenerativeModel(params) {
  // Fetch API key from server
  // If you need a new API key, get it from https://makersuite.google.com/app/apikey

  const genAI = new GoogleGenerativeAI(API_KEY);

  // For text-only inputs, use the gemini-pro model
  return genAI.getGenerativeModel(params);
}

/**
 * Converts a File object to a GoogleGenerativeAI.Part object.
 *
 * @param {Blob} file
 * @returns {GoogleGenerativeAI.Part}
 */
export async function fileToGenerativePart(file) {
  const base64EncodedDataPromise = new Promise((resolve) => {
    const reader = new FileReader();
    reader.onloadend = () => resolve(reader.result.split(",")[1]);
    reader.readAsDataURL(file);
  });
  return {
    inlineData: { data: await base64EncodedDataPromise, mimeType: file.type },
  };
}

/**
 * Scrolls the document all the way to the bottom.
 */
export function scrollToDocumentBottom() {
  const scrollingElement = document.scrollingElement || document.body;
  scrollingElement.scrollTop = scrollingElement.scrollHeight;
}

/**
 * Updates the `resultEl` with parsed markdown text returned by a `getResult()` call.
 *
 * @param {HTMLElement}} resultEl
 * @param {() => Promise<GoogleGenerativeAI.GenerateContentResponse>} getResult
 * @param {boolean} streaming
 */
export async function updateUI(resultEl, getResult, streaming) {
  resultEl.className = "loading";
  let text = "";
  try {
    const result = await getResult();

    if (streaming) {
      resultEl.innerText = "";
      for await (const chunk of result.stream) {
        // Get first candidate's current text chunk
        const chunkText = chunk.text();
        text += chunkText;
        resultEl.innerHTML = marked.parse(text);
        scrollToDocumentBottom();
      }
    } else {
      const response = await result.response;
      text = response.text();
    }

    resultEl.className = ""; // Remove .loading class
  } catch (err) {
    text += "\n\n> " + err;
    resultEl.className = "error";
  }
  resultEl.innerHTML = marked.parse(text);
  scrollToDocumentBottom();
}

```

CSSは全く変えておらず、HTML,JSも数行修正したり削除しただけです…💦

この３つのファイルをロリポップのようなレンタル共有WEBサーバーにアップロードすれば動きます。

## 5.終わりに

どうせサンプル作るならon browserのサンプルも作ってください⇒技術者の皆さま

ドシロウトもITで遊ばしてほしいですｗ

以　上