Monaca + NCMB + ChatGPTでAIチャットアプリを作る（その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を使ってChatGPTを用いたチャットアプリを作ってみます。[前回はスクリプトでの処理について解説しました](https://qiita.com/goofmint/items/f1d7449dcecfb4075002)ので、今回はチャット画面を作成します。

## コード

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


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

このファイルは `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>
```

## JavaScriptの実装

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

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

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

### 必要な変数の定義

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

```jsx
const Chat = ncmb.DataStore("Chat"); // データストア"Chat"の生成
let messages; // メッセージを表示するための変数
let messageBar; // メッセージを入力するための変数
const chats = []; // チャット履歴を保持するための配列
```

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

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

```jsx
$onMounted(async () => { // コンポーネントがマウントされたときに実行される関数
  messages = $f7.messages.create({ // messagesの初期化
    el: $('.messages'), // 表示する要素
  });
  messageBar = $f7.messagebar.create({ // messageBarの初期化
    el: $('.messagebar'), // 入力する要素
    attachments: [] // 添付ファイルの初期値
  });
  const date = new Date;
  date.setDate(date.getDate() - 1);
  const ary = await Chat
    .greaterThanOrEqualTo('createDate', date) // createDateが指定日以降のデータを取得
    .fetchAll(); // 取得した全てのデータを配列で取得
  ary.forEach(chat => addMessage(chat)); // 取得したデータをメッセージ表示エリアに追加
  $update(); // レンダリングの更新
});
```

`addMessage` 関数はチャットデータを配列に登録し、チャット画面用のデータを作成します。

```jsx
const addMessage = (chat) => { // メッセージ表示エリアにメッセージを追加する関数
  messages.addMessage({ // メッセージを追加
    text: chat.content, // メッセージの内容
    type: chat.role === 'user' ? 'sent' : 'received', // メッセージの送信元
    name: chat.role === 'user' ? 'Me': 'Chat GPT', // メッセージの送信先
    avatar: chat.role === 'user' ? '/assets/icons/person.png' : '/assets/icons/robot.png', // アバター画像
  });
  chats.push(chat); // チャット履歴に追加
};
```

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

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

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

### テキストを取得

まず入力されたテキストを取得し、そのデータをNCMBに保存します。

```jsx
const content = messageBar.getValue().replace(/\n/g, '<br />').trim(); // 入力されたメッセージを取得
if (content === '') return; // メッセージが空の場合は送信しない
const chat = await saveChat('user', content); // チャットを保存
```

`saveChat` 関数は以下の通りです。これだけでクラウドのデータベースにチャットメッセージが保存されます。

```jsx
const saveChat = (role, content) => { // チャットを保存する関数
  const chat = new Chat(); // 新しいチャットを生成
  return chat
    .set('content', content) // チャットの内容を設定
    .set('role', role) // チャットの送信元を設定
    .save(); // チャットを保存
};
```

保存されたチャットは先ほどの `addMessage` 関数を使って配列に追加します。追加したら表示を更新し、さらにその画面更新が完了するのを `$tick` で待ちます。

```jsx
addMessage(chat); // メッセージ表示エリアにメッセージを追加
$update(); // レンダリングの更新
await $tick(); // レンダリングの完了を待つ
```

### これまでのメッセージ履歴を作成

ここからOpenAI API部分の処理になります。まずデータストアから取得したメッセージ履歴を使って、過去のやり取りを作成します。

```jsx
// ここからAIチャット
const pastMessages = chats.map(chat => { // チャット履歴をAIに送信するための配列を作成
  return {
    role: chat.role,
    content: chat.content,
  };
});
```

### OpenAI APIの呼び出し

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

```jsx
messages.showTyping(); // タイピング中の表示
```

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

```jsx
const res = await ncmb.Script // テキスト生成スクリプトを実行
  .data({
    content,
    messages: pastMessages,
  })
  .exec("POST", "text.js");
```

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

受け取った結果はボット（assistant）のメッセージとしてデータストアに保存します。

```jsx
const { text } = JSON.parse(res.body); // スクリプトの結果を取得
const aiChat = await saveChat('assistant', text); // AIの返答を保存
addMessage(aiChat); // メッセージ表示エリアにAIの返答を追加
```

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

```jsx
messages.hideTyping(); // タイピング中の表示を非表示にする
messageBar.clear(); // 入力欄をクリア
$update(); // レンダリングの更新
```

これで完成です。

![0987 localhost - 0406171301.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/fc82d5bf-c985-509f-b3a0-8ac34c3dafa6.jpeg)


## 全体のコード

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

```jsx
export default (props, { $f7, $onMounted, $store, $update, $tick }) => {
  const Chat = ncmb.DataStore("Chat"); // データストア"Chat"の生成
  let messages; // メッセージを表示するための変数
  let messageBar; // メッセージを入力するための変数
  const chats = []; // チャット履歴を保持するための配列
  $onMounted(async () => { // コンポーネントがマウントされたときに実行される関数
    messages = $f7.messages.create({ // messagesの初期化
      el: $('.messages'), // 表示する要素
    });
    messageBar = $f7.messagebar.create({ // messageBarの初期化
      el: $('.messagebar'), // 入力する要素
      attachments: [] // 添付ファイルの初期値
    });
    const date = new Date;
    date.setDate(date.getDate() - 1);
    const ary = await Chat
      .greaterThanOrEqualTo('createDate', date) // createDateが指定日以降のデータを取得
      .fetchAll(); // 取得した全てのデータを配列で取得
    ary.forEach(chat => addMessage(chat)); // 取得したデータをメッセージ表示エリアに追加
    $update(); // レンダリングの更新
  });

  const addMessage = (chat) => { // メッセージ表示エリアにメッセージを追加する関数
    messages.addMessage({ // メッセージを追加
      text: chat.content, // メッセージの内容
      type: chat.role === 'user' ? 'sent' : 'received', // メッセージの送信元
      name: chat.role === 'user' ? 'Me': 'Chat GPT', // メッセージの送信先
      avatar: chat.role === 'user' ? '/assets/icons/person.png' : '/assets/icons/robot.png', // アバター画像
    });
    chats.push(chat); // チャット履歴に追加
  };
  const saveChat = (role, content) => { // チャットを保存する関数
    const chat = new Chat(); // 新しいチャットを生成
    return chat
      .set('content', content) // チャットの内容を設定
      .set('role', role) // チャットの送信元を設定
      .save(); // チャットを保存
  };
  const send = async () => { // メッセージ送信のイベントハンドラ
    const content = messageBar.getValue().replace(/\n/g, '<br />').trim(); // 入力されたメッセージを取得
    if (content === '') return; // メッセージが空の場合は送信しない
    const chat = await saveChat('user', content); // チャットを保存
    addMessage(chat); // メッセージ表示エリアにメッセージを追加
    $update(); // レンダリングの更新
    await $tick(); // レンダリングの完了を待つ
    // ここからAIチャット
    const pastMessages = chats.map(chat => { // チャット履歴をAIに送信するための配列を作成
      return {
        role: chat.role,
        content: chat.content,
      };
    });
    messages.showTyping(); // タイピング中の表示
    const res = await ncmb.Script // テキスト生成スクリプトを実行
      .data({
        content,
        messages: pastMessages,
      })
      .exec("POST", "text.js");
    const { text } = JSON.parse(res.body); // スクリプトの結果を取得
    const aiChat = await saveChat('assistant', text); // AIの返答を保存
    addMessage(aiChat); // メッセージ表示エリアにAIの返答を追加
    messages.hideTyping(); // タイピング中の表示を非表示にする
    messageBar.clear(); // 入力欄をクリア
    $update(); // レンダリングの更新
  };
  return $render;
}
```

## まとめ

今回のAIチャットアプリではNCMBの以下の機能を利用しました。

- データストア
    - データ保存
    - データ検索
- スクリプト
    - OpenAI APIの実行

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

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