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

## コード

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

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

このファイルは `lib/chat.dart` として作成しています。チャット画面は[flutter\_chat\_ui | Flutter Package](https://pub.dev/packages/flutter_chat_ui)を使うことで、簡単に実現できます。

```dart
// パッケージのインポート
import 'package:flutter/material.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
import 'package:ncmb/ncmb.dart';

// チャットページのウィジェットを定義
class ChatPage extends StatefulWidget {
  const ChatPage({super.key});

  // StatefulWidgetを継承すると、Stateを作成するメソッドが必要になります
  @override
  State<ChatPage> createState() => _ChatPageState();
}

// _ChatPageStateクラスを定義
class _ChatPageState extends State<ChatPage> {

  // メインのUI構造を作成
  @override
  Widget build(BuildContext context) => Scaffold(
        body: Chat(
          messages: _messages,
          onSendPressed: _handleSendPressed,
          showUserAvatars: false,
          showUserNames: false,
          user: const types.User(id: 'user'),
        ),
      );
  // 省略
}
```

## Flutterの実装

### 必要な変数の定義

まずチャットメッセージ用の変数を定義します。 

```dart
// メッセージのリストを作成
List<types.TextMessage> _messages = [];
```

### 画面が初期化された際の処理

画面が初期化された際の処理で、過去のメッセージを取得します。

```dart
// 初期化時にメッセージをロード
@override
void initState() {
  super.initState();
  _loadMessages();
}
```

`_loadMessages` 関数で過去1日分からのデータを取得します。あまりメッセージが多いとOpenAIのAPI消費量が増えてしまうので注意してください。

```dart
  // 過去のメッセージをロードする関数
  void _loadMessages() async {
    // メッセージを取得するクエリを作成
    var query = NCMBQuery('Chat');
    // 1日前以降のメッセージを取得
    var date = DateTime.now().subtract(const Duration(days: 1));
    query.greaterThanOrEqualTo('createDate', date);
    query.order('createDate', descending: true); // 古いデータが上に来るようにする
    var ary = (await query.fetchAll()).map((o) => o as NCMBObject).toList();
    // 取得したNCMBObjectのリストをTextMessageに変換
    final messages = ary.map((e) => _toMessage(e)).toList();
    // メッセージを更新
    setState(() {
      _messages = messages;
    });
  }
```

`_toMessage` 関数はNCMBObjectをflutter_chat_uiのテキストメッセージに変換する関数です。

```dart
  // NCMBObjectをTextMessageに変換する関数
  types.TextMessage _toMessage(NCMBObject obj) {
    return types.TextMessage(
      author: types.User(id: obj.getString('role', defaultValue: 'user')),
      createdAt: obj.getDateTime('createDate').microsecondsSinceEpoch,
      id: obj.objectId!,
      text: obj.getString('content'),
    );
  }
```

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

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

```dart
void _handleSendPressed(types.PartialText message) async {
  // 以下はこの中に記述
};
```

### テキストを取得

まず入力されたテキストをNCMBに保存します。保存したら、それをチャット画面に反映します。

```dart
// メッセージを保存
var obj = NCMBObject('Chat');
obj.set('content', message.text);
obj.set('role', 'user');
await obj.save();
// メッセージをチャット画面に追加
_addMessage(_toMessage(obj));
```

`_addMessage` 関数はチャット画面にテキストメッセージを追加する関数です。

```dart
  // メッセージを追加する関数
  void _addMessage(types.TextMessage message) {
    setState(() {
      _messages.insert(0, message);
    });
  }
```

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

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

```dart
    // 過去のメッセージ内容を作成
    final messages = _messages
        .map((message) => {
              'role': message.author.id,
              'content': message.text,
            })
        .toList();
```

### OpenAI APIの呼び出し

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

```jsx
// OpenAIのAPIを呼び出す
var script = NCMBScript('text.js')
    .body('content', message.text)
    .body('messages', messages);
var res = (await script.post()) as Map<String, dynamic>;
```

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

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

```jsx
// OpenAIの返答を保存
var answer = NCMBObject('Chat');
answer.set('content', res['text'] as String);
answer.set('role', 'assistant');
await answer.save();
// OpenAIの返答をチャット画面に追加
_addMessage(_toMessage(answer));
```

これで完成です。

![localhost_63838_ (4).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/fdec5847-07b9-e15c-0705-7fcfd8373a5d.png)


## 全体のコード

`chat.dart` のコードは以下の通りです。

```dart
// パッケージのインポート
import 'package:flutter/material.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
import 'package:ncmb/ncmb.dart';

// チャットページのウィジェットを定義
class ChatPage extends StatefulWidget {
  const ChatPage({super.key});

  // StatefulWidgetを継承すると、Stateを作成するメソッドが必要になります
  @override
  State<ChatPage> createState() => _ChatPageState();
}

// _ChatPageStateクラスを定義
class _ChatPageState extends State<ChatPage> {
  // メッセージのリストを作成
  List<types.TextMessage> _messages = [];

  // 初期化時にメッセージをロード
  @override
  void initState() {
    super.initState();
    _loadMessages();
  }

  // メインのUI構造を作成
  @override
  Widget build(BuildContext context) => Scaffold(
        body: Chat(
          messages: _messages,
          onSendPressed: _handleSendPressed,
          showUserAvatars: false,
          showUserNames: false,
          user: const types.User(id: 'user'),
        ),
      );

  // メッセージを追加する関数
  void _addMessage(types.TextMessage message) {
    setState(() {
      _messages.insert(0, message);
    });
  }

  // NCMBObjectをTextMessageに変換する関数
  types.TextMessage _toMessage(NCMBObject obj) {
    return types.TextMessage(
      author: types.User(id: obj.getString('role', defaultValue: 'user')),
      createdAt: obj.getDateTime('createDate').microsecondsSinceEpoch,
      id: obj.objectId!,
      text: obj.getString('content'),
    );
  }

  // メッセージ送信時に呼ばれる関数
  void _handleSendPressed(types.PartialText message) async {
    // メッセージを保存
    var obj = NCMBObject('Chat');
    obj.set('content', message.text);
    obj.set('role', 'user');
    await obj.save();
    // メッセージをチャット画面に追加
    _addMessage(_toMessage(obj));
    // 過去のメッセージ内容を作成
    final messages = _messages
        .map((message) => {
              'role': message.author.id,
              'content': message.text,
            })
        .toList();
    // OpenAIのAPIを呼び出す
    var script = NCMBScript('text.js')
        .body('content', message.text)
        .body('messages', messages);
    var res = (await script.post()) as Map<String, dynamic>;
    // OpenAIの返答を保存
    var answer = NCMBObject('Chat');
    answer.set('content', res['text'] as String);
    answer.set('role', 'assistant');
    await answer.save();
    // OpenAIの返答をチャット画面に追加
    _addMessage(_toMessage(answer));
  }

  // 過去のメッセージをロードする関数
  void _loadMessages() async {
    // メッセージを取得するクエリを作成
    var query = NCMBQuery('Chat');
    // 1日前以降のメッセージを取得
    var date = DateTime.now().subtract(const Duration(days: 1));
    query.greaterThanOrEqualTo('createDate', date);
    query.order('createDate', descending: true); // 古いデータが上に来るようにする
    var ary = (await query.fetchAll()).map((o) => o as NCMBObject).toList();
    // 取得したNCMBObjectのリストをTextMessageに変換
    final messages = ary.map((e) => _toMessage(e)).toList();
    // メッセージを更新
    setState(() {
      _messages = messages;
    });
  }
}
```

## まとめ

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

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

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

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