Monaca + NCMB + ChatGPTで画像生成アプリを作る（その3: チャット画面の作成）

NCMBでは公式SDKとしてSwift/Objective-C/Kotlin/Java/Unity/JavaScript SDKを用意しています。また、それ以外にもコミュニティSDKとして、非公式ながらFlutter/React Native/Google Apps Script/C#/Ruby/Python/PHPなど幅広い言語向けにSDKが開発されています。

今回は公式SDKの一つ、JavaScript SDKとFramework7を使ってOpenAIの画像生成APIを用いたAI画像生成アプリを作ってみます。[前回は画像生成に関わるスクリプトの処理について解説しました](https://qiita.com/goofmint/items/e29479a877987ab9e189)ので、今回はチャット画面を作成します。

## コード

今回のコードは [NCMBMania/monaca-image-gen](https://github.com/NCMBMania/monaca-image-gen) にアップロードしてあります。実装時の参考にしてください。

## チャット画面について

このファイルは `www/pages/chat.html` として作成しています。チャット画面はFramework7のメッセージコンポーネントを用いるので、HTMLはとても簡単です。

```html
<template>
  <div class="page" data-name="chat"> <!-- ページ全体のコンテナ -->
    <div class="navbar"> <!-- ナビゲーションバー -->
      <div class="navbar-bg"></div>
      <div class="navbar-inner sliding">
        <div class="title">AI画像生成</div> <!-- タイトル -->
      </div>
    </div>
    <div class="toolbar messagebar"> <!-- メッセージツールバー -->
      <div class="toolbar-inner">
        <div class="messagebar-area">
          <textarea class="resizable" placeholder="Message"></textarea> <!-- テキストエリア -->
        </div>
        <a class="link icon-only demo-send-message-link" @click=${send}> <!-- メッセージ送信ボタン -->
          <i class="icon f7-icons if-not-md">arrow_up_circle_fill</i>
          <i class="icon material-icons md-only">send</i>
        </a>
      </div>
      <div class="messagebar-sheet">
      </div>
    </div>
    <div class="page-content messages-content"> <!-- メッセージ表示エリア -->
      <div class="messages"> <!-- メッセージ表示用の要素 -->
      </div>
    </div>
  </div>
</template>
```

![0991 localhost - 0406215908.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/eac4ef55-db97-72fb-4caa-e43759c6dfcb.jpeg)


## JavaScriptの実装

JavaScriptは同じ `chat.html` の中に記述します。まず、スクリプトタグと基本となる関数のエクスポートを定義します。

```
<script>
	export default (props, { $f7, $onMounted }) => {
	  // この中に処理を記述する
	  return $render;
	}
</script>

```

`$f7` や `$onMounted` はFramework7の用意するユーティリティ変数です。何ができるかは[Framework7入門　ルーティング・DOM操作・ストアを学ぼう – モナカプレス](https://press.monaca.io/atsushi/9532)を参考にしてください。

### 必要な変数の定義

まずNCMBのデータストアのクラスやチャットメッセージ用の変数を定義します。

```jsx
// "ImageGen"というデータストアオブジェクトを初期化する。
const ImageGen = ncmb.DataStore("ImageGen");

// メッセージコンテナとメッセージ入力バーを保持する変数を宣言する。
let messages;
let messageBar;
```

### 画面がマウントされた際の処理

画面がHTMLとしてマウントされた際の処理でチャット画面の初期化や、過去のメッセージを取得します。

```jsx
// コンポーネントがマウントされた時に、メッセージコンテナとメッセージ入力バーを作成し、
// 過去のメッセージを表示する。
$onMounted(async () => {
  messages = $f7.messages.create({
    el: $('.messages'),
  });
  messageBar = $f7.messagebar.create({
    el: $('.messagebar'),
    attachments: []
  });
  // データは昨日以降を対象とする
  const date = new Date;
  date.setDate(date.getDate() - 1);
  // データストアからデータを取得する
  const ary = await ImageGen
    .greaterThanOrEqualTo('createDate', date)
    .fetchAll();
  // データをチャット画面に表示する
  for (const imageGen of ary) {
    await addMessage(imageGen);
  }
});
```

`addMessage` 関数はチャットデータを `messages` に登録し、画像がある場合にはファイルストアからダウンロードしています。ダウンロードされるのは `Blob` なので、 `URL.createObjectURL` を使って `blob://` ではじまるURLに変換しています。

```jsx
// メッセージをメッセージコンテナに追加する非同期関数。
  const addMessage = async (imageGen) => {
    // テキスト部分
    messages.addMessage({
      type: 'sent',
      avatar: '/assets/icons/person.png',
      name: 'Me',
      text: imageGen.content,
    });
    // 画像がなければ終了
    if (!imageGen.image) return;
    // 画像のダウンロード
    const blob = await ncmb.File.download(imageGen.image, 'blob');
    // BlobからURLを作成
    const url = URL.createObjectURL(blob);
    // 画像を表示
    messages.addMessage({
      type: 'received',
      avatar: '/assets/icons/robot.png',
      name: 'Gen',
      imageSrc: url,
    });
  };
```

## チャットを送信・保存する処理

チャットメッセージを書き込んで送信ボタンを押すと `send` 関数が呼び出されます。

```jsx
const send = async () => { // メッセージ送信のイベントハンドラ
  // 以下はこの中に記述
};
```

### テキストを取得

まず入力されたテキストを取得し、そのデータをNCMBのデータストアインスタンスにします（まだ保存しません）。

```jsx
// メッセージ入力バーからテキストを取得し、改行を<br />に変換する。
const prompt = messageBar.getValue().replace(/\n/g, '<br />').trim();
// テキストが空なら終了
if (prompt === '') return;
// データストアのオブジェクトを作成
const imageGen = new ImageGen();
// 入力されていた文字列をセット
imageGen.set('content', prompt);
```

### OpenAI APIの呼び出し

OpenAI APIは時間がかかるので、プログレスとしてチャット入力中の表示を出します。

```jsx
// メッセージを送信中にする
messages.showTyping();
```

そしてNCMBのスクリプトを実行します。これでOpenAI APIを呼び出して、結果を受け取ります。

```jsx
// 入力文字から画像を生成する
const res = await ncmb.Script
  .data({
    prompt,
  })
  .exec("POST", "image.js");
```

### 結果をデータストアに保存

受け取った結果は `imageGen` の `image` に紐付けて保存します。

```jsx
// 生成した画像のファイル名を取得
const { fileName } = JSON.parse(res.body);
// 画像のファイル名をセット
imageGen.set('image', fileName);
// データストアに保存
await imageGen.save();
```

後は表示の更新と、入力中表示を消して完了です。

```jsx
// メッセージをチャット画面に表示
addMessage(imageGen);
// メッセージ入力バーをクリア
messageBar.clear();
// メッセージの送信中表示をやめる
messages.hideTyping();
```

これで完成です。

## 実行例

以下は実行例です。なお、レスポンスが返ってくるまでは10秒以上かかるのでカットしています。

![名称未設定.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/72a6c2a2-b2d3-3c74-62c0-652f5bd9a0d0.gif)


## 全体のコード

`chat.html` のJavaScript部分のコードは以下の通りです。

```html
<template>
  <div class="page" data-name="chat"> <!-- ページ全体のコンテナ -->
    <div class="navbar"> <!-- ナビゲーションバー -->
      <div class="navbar-bg"></div>
      <div class="navbar-inner sliding">
        <div class="title">AI画像生成</div> <!-- タイトル -->
      </div>
    </div>
    <div class="toolbar messagebar"> <!-- メッセージツールバー -->
      <div class="toolbar-inner">
        <div class="messagebar-area">
          <textarea class="resizable" placeholder="Message"></textarea> <!-- テキストエリア -->
        </div>
        <a class="link icon-only demo-send-message-link" @click=${send}> <!-- メッセージ送信ボタン -->
          <i class="icon f7-icons if-not-md">arrow_up_circle_fill</i>
          <i class="icon material-icons md-only">send</i>
        </a>
      </div>
      <div class="messagebar-sheet">
      </div>
    </div>
    <div class="page-content messages-content"> <!-- メッセージ表示エリア -->
      <div class="messages"> <!-- メッセージ表示用の要素 -->
      </div>
    </div>
  </div>
</template>
<script>
// コンポーネントをエクスポートするデフォルト関数。
// この関数は、"props"と"$f7"と"$onMounted"を含むオブジェクトを引数に取ります。
export default (props, { $f7, $onMounted }) => {

  // "ImageGen"というデータストアオブジェクトを初期化する。
  const ImageGen = ncmb.DataStore("ImageGen");

  // メッセージコンテナとメッセージ入力バーを保持する変数を宣言する。
  let messages;
  let messageBar;

  // コンポーネントがマウントされた時に、メッセージコンテナとメッセージ入力バーを作成し、
  // 過去のメッセージを表示する。
  $onMounted(async () => {
    messages = $f7.messages.create({
      el: $('.messages'),
    });
    messageBar = $f7.messagebar.create({
      el: $('.messagebar'),
      attachments: []
    });
    // データは昨日以降を対象とする
    const date = new Date;
    date.setDate(date.getDate() - 1);
    // データストアからデータを取得する
    const ary = await ImageGen
      .greaterThanOrEqualTo('createDate', date)
      .fetchAll();
    // データをチャット画面に表示する
    for (const imageGen of ary) {
      await addMessage(imageGen);
    }
  });

  // メッセージをメッセージコンテナに追加する非同期関数。
  const addMessage = async (imageGen) => {
    // テキスト部分
    messages.addMessage({
      type: 'sent',
      avatar: '/assets/icons/person.png',
      name: 'Me',
      text: imageGen.content,
    });
    // 画像がなければ終了
    if (!imageGen.image) return;
    // 画像のダウンロード
    const blob = await ncmb.File.download(imageGen.image, 'blob');
    // BlobからURLを作成
    const url = URL.createObjectURL(blob);
    // 画像を表示
    messages.addMessage({
      type: 'received',
      avatar: '/assets/icons/robot.png',
      name: 'Gen',
      imageSrc: url,
    });
  };

  // メッセージを送信するための非同期関数。
  const send = async () => {
    // メッセージ入力バーからテキストを取得し、改行を<br />に変換する。
    const prompt = messageBar.getValue().replace(/\n/g, '<br />').trim();
    // テキストが空なら終了
    if (prompt === '') return;
    // データストアのオブジェクトを作成
    const imageGen = new ImageGen();
    // 入力されていた文字列をセット
    imageGen.set('content', prompt);
    // メッセージを送信中にする
    messages.showTyping();
    // 入力文字から画像を生成する
    const res = await ncmb.Script
      .data({
        prompt,
      })
      .exec("POST", "image.js");
    // 生成した画像のファイル名を取得
    const { fileName } = JSON.parse(res.body);
    // 画像のファイル名をセット
    imageGen.set('image', fileName);
    // データストアに保存
    await imageGen.save();
    // メッセージをチャット画面に表示
    addMessage(imageGen);
    // メッセージ入力バーをクリア
    messageBar.clear();
    // メッセージの送信中表示をやめる
    messages.hideTyping();
  };

  return $render;
}
</script>
```

## まとめ

今回のAI画像生成アプリではNCMBの以下の機能を利用しました。

- データストア
    - データ保存
    - データ検索
- ファイルストア
    - ファイルアップロード
    - ファイルダウンロード
- スクリプト
    - OpenAI APIの実行

NCMBには他にも認証、プッシュ通知などの機能があります。ぜひそれらの機能も利用してアプリを開発してください。

[mBaaSでサーバー開発不要！ | ニフクラ mobile backend](https://mbaas.nifcloud.com/)