ChatGPTを使って「ChatGPTとチャットするアプリ」を作ってみた

[株式会社Never](https://neverjp.com/)のshoheiです。

株式会社Neverは「NEVER STOP CREATE 作りつづけること」をビジョンに掲げ、理想を実現するためにプロダクトを作り続ける組織です。モバイルアプリケーションの受託開発、技術支援、コンサルティングを行っております。アプリ開発のご依頼や開発面でのお困りの際はお気楽にお問合せください。
https://neverjp.com/contact/


# 概要

巷で話題のChatGPTを使ってアプリを作ってみたという企画です。

## ChatGPTについて
ChatGPTの概要について聞いてみました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183031/cede3031-ff2b-256f-1061-5810d11cf529.png" width=80% />

ChatGPTへやりたいことや質問をすると、欲しい情報が手に入ったり、ヒントを教えてくれます。また、めんどくさい作業をお願いすると、それを代わりにやってくれる便利なツールです。

めんどくさい作業の例. 47都道府県の番号と名称データの列挙など

## ChatGPTを触ってみた感想
エンジニアは新しい技術を習得する際や、課題調査をする際によくGoogle先生にお世話になります。Googleで検索してヒットした内容を自分で精査して欲しい情報に辿り着くというものです。

ChatGPTの登場で、その「ヒットした内容を自分で精査する」という事がショートカットされ、すぐに欲しい情報に辿り着けるようになりました。ただ、情報の信憑性については、ChatGPT自体の学習タイミングによりますので、古い情報が提供されることがあり、特にSDKのIFは古いものを教えてくれることがよくありました。

だとしても、知らないことを学ぶ取っ掛かりツールとしてはかなり強力で、知らない世界へ前線突破してくれるChatGPTは仕事のパートナーです。

1. ChatGPTにやりたいことを伝える
2. ChatGPTがサンプルコードと知見を教えてくれる（実際に動かすが、動かない）
4. ChatGPTにサンプルコードが間違っていることを伝える
5. ChatGPTが別の手段を教えてくれる（動く場合もあるし、動かない場合もある）
6. ChatGPTが教えてくれた内容をヒントに、GoogleやGithubで正しい情報を取りにいく

このような手順で私は使っています。
この手順で、ChatGPTに実際にアプリ作ってもらうとどうなるのか気になり、

<b>ChatGPTを使って「ChatGPTとチャットするアプリ」を作ってみました。</b>

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183031/35e96464-630b-8884-085c-5de16138e18c.png" width=35% />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183031/40f7047f-d6b6-4d19-334e-2a1f608ce670.png" width=35% />

言語は Flutter（Dart）です。

完成したソースコードは[こちら](https://github.com/never-inc/never_flutter_sample_code/tree/main/lib/talk_to_chat_gpt)


# ChatGPTのAPIを使う準備

## APIキーの取得
ChatGPTのAPIを使うために、APIキーを取得します。
https://platform.openai.com/

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183031/8b19e055-3672-8997-949f-90ea504ac2b0.png" width=100% />

右上のアイコンより、View API Keysを選択

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183031/e099a923-3226-970b-8e83-59f343574082.png" width=35% />

左メニューより、USER -> API Keys -> + Create new secret key を選択

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183031/9322eddd-c2e1-84fb-7f6d-497128df1955.png" width=100% />

ここに表示されるAPIキーを使ってChatGPTとやり取ります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183031/234e4ce3-19d8-c53e-9dfe-7b38577eebe8.png" width=70% />

## APIの情報
APIの情報はここから確認できます。
https://platform.openai.com/docs/api-reference/chat

今回はChatGPTと対話をするアプリなので`https://api.openai.com/v1/chat/completions` を使います。

```shell
curl https://api.openai.com/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -d '{
    "model": "gpt-3.5-turbo",
    "messages": [{"role": "user", "content": "Hello!"}]
  }'
```
`OPENAI_API_KEY`に先ほど取得したAPIキーをセットすると利用できます。

# ChatGPTにアプリを実装してもらう

## ChatGPTのAPIとやり取りする部分を実装

まずは、APIと直接やりとりする部分を実装してもらいましょう。
![スクリーンショット 2023-04-20 10.10.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183031/f06296df-5412-f414-31f8-b3b2a4a1ddc7.png)
ChatGPTのアウトプットがこちら
```dart
import 'dart:convert';
import 'package:http/http.dart' as http;

Future<void> main() async {
  final apiKey = 'your_openai_api_key';
  final prompt = 'サンプルの質問';

  final response = await chatGPT(apiKey, prompt);
  print('GPT-4 Response: $response');
}

Future<String> chatGPT(String apiKey, String prompt) async {
  final url = 'https://api.openai.com/v1/engines/davinci-codex/completions';
  final headers = {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer $apiKey',
  };
  final body = json.encode({
    'prompt': prompt,
    'max_tokens': 50,
    'n': 1,
    'stop': null,
    'temperature': 0.8,
  });

  final response = await http.post(Uri.parse(url), headers: headers, body: body);
  final jsonResponse = json.decode(response.body);

  if (response.statusCode == 200) {
    return jsonResponse['choices'][0]['text'].trim();
  } else {
    throw Exception('Failed to get response from ChatGPT: ${response.body}');
  }
}

```

`https://api.openai.com/v1/chat/completions` のサンプルコードがほしかったのですが、違うAPIを教えてくれました。

聞き方が悪いのか、APIのURLも記載して質問しましたが、欲しい情報が取得できず。
![スクリーンショット 2023-04-20 10.19.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183031/03fd3f0f-6bbf-69e2-b624-7aed98974d9e.png)
このAPIのbeta版が非推奨という扱いなのかわかりませんが、API部分は手動で修正しました。

[修正後のソースコード](https://github.com/never-inc/never_flutter_sample_code/blob/main/lib/talk_to_chat_gpt/sevices/chat_gpt_service.dart)

### ファイル名を決めてもらう

ファイル名を聞きました。
![スクリーンショット 2023-04-20 10.20.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183031/e63a20f7-c569-01b2-dcc1-a362fd9648c9.png)

素晴らしい。chat_gpt_service.dartにします。

### レスポンスのデータクラスを作ってもらう

[freezed](https://pub.dev/packages/freezed)が好きなので、freezedのデータクラスを作ってもらいました。
![スクリーンショット 2023-04-20 10.23.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183031/27937bec-fc28-ea25-5ebe-3a2e3cb63ed2.png)


長くなるので割愛しますが、ちゃんとfreezedのデータクラスを作ってくれました🎉

### APIキーを扱う術を教えてもらう

オープンソース上でAPIキーがバレるとまずいので、それを管理する方法を教えてもらいました。
![スクリーンショット 2023-04-20 10.26.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183031/aa607f7d-d744-978e-8aaa-6b938c0e90a6.png)
[flutter_dotenv](https://pub.dev/packages/flutter_dotenv)を用いて、環境変数にAPIキーをセットして用いる方法ですね（6.0.0は執筆時は無かったけど）

プロジェクトのルートに.envファイルを作成して、OPENAI_API_KEYの環境変数にAPIキーを定義します。

```sh
OPENAI_API_KEY=your_openai_api_key
```

キーの使い方まで教えてくれました。

```dart
import 'package:flutter_dotenv/flutter_dotenv.dart';

Future<ChatGPTResponse> chatGPT(String prompt) async {
  final apiKey = dotenv.env['OPENAI_API_KEY']; // 環境変数からAPIキーを取得
  // 以降のコードは変更なし
}
```

## チャット画面を実装

APIとやり取りする部分ができたので、ユーザーが操作するUI部分を作ってもらいましょう。
![スクリーンショット 2023-04-20 10.44.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183031/f13629a6-7f81-d7c4-dd9e-f2b37f73fec6.png)
```dart
import 'package:flutter/material.dart';

class ChatScreen extends StatefulWidget {
  const ChatScreen({Key? key}) : super(key: key);

  @override
  _ChatScreenState createState() => _ChatScreenState();
}

class _ChatScreenState extends State<ChatScreen> {
  final TextEditingController _textController = TextEditingController();
  final List<String> _messages = [];

  void _handleSubmitted(String text) {
    _textController.clear();
    setState(() {
      _messages.add(text);
    });
  }

  Widget _buildTextComposer() {
    return Container(
      margin: const EdgeInsets.symmetric(horizontal: 8.0),
      child: Row(
        children: <Widget>[
          Flexible(
            child: TextField(
              controller: _textController,
              onSubmitted: _handleSubmitted,
              decoration: const InputDecoration.collapsed(
                  hintText: 'メッセージを入力してください'),
            ),
          ),
          IconButton(
            icon: const Icon(Icons.send),
            onPressed: () => _handleSubmitted(_textController.text),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('チャット')),
      body: Column(
        children: <Widget>[
          Expanded(
            child: ListView.builder(
              itemCount: _messages.length,
              itemBuilder: (context, index) {
                return ListTile(
                  title: Text(_messages[index]),
                );
              },
            ),
          ),
          Divider(height: 1.0),
          Container(
            decoration: BoxDecoration(color: Theme.of(context).cardColor),
            child: _buildTextComposer(),
          ),
        ],
      ),
    );
  }
}
```

チャット画面でchatGPT関数とやりとりする方法も教えてもらいましょう。
![スクリーンショット 2023-04-20 10.46.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183031/042bdd3d-65d7-27d6-6052-9cc6c8d55951.png)
```dart
void _handleSubmitted(String text) async {
  _textController.clear();
  setState(() {
    _messages.add('User: $text');
  });

  // ChatGPT関数を使用して応答を取得する
  try {
    final response = await chatGPT(text);
    final chatGptResponse = response.choices[0].text.trim();

    setState(() {
      _messages.add('ChatGPT: $chatGptResponse');
    });
  } catch (e) {
    print('Error: $e');
  }
}
```

### エラーダイアログの実装
エラー時のUIについても教えてもらいました。
![スクリーンショット 2023-04-20 10.47.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183031/06718496-7d12-a696-6dbb-cc926ae04f82.png)
```dart
void _handleSubmitted(String text) async {
  _textController.clear();
  setState(() {
    _messages.add('User: $text');
  });

  // ChatGPT関数を使用して応答を取得する
  try {
    final response = await chatGPT(text);
    final chatGptResponse = response.choices[0].text.trim();

    setState(() {
      _messages.add('ChatGPT: $chatGptResponse');
    });
  } catch (e) {
    print('Error: $e');
    showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: const Text('エラーが発生しました'),
          content: Text('APIからの応答が得られませんでした。詳細：$e'),
          actions: <Widget>[
            TextButton(
              child: const Text('閉じる'),
              onPressed: () {
                Navigator.of(context).pop();
              },
            ),
          ],
        );
      },
    );
  }
}
```

### 操作性を良くする
メッセージ送信後と画面タップでキーボードを非表示にしたかったので、その方法を教えてもらいました。
![スクリーンショット 2023-04-20 10.50.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183031/04bafd31-a1d2-4466-1218-c50d1f345538.png)
```dart
import 'package:flutter/services.dart'; // 追加

class _ChatScreenState extends State<ChatScreen> {
  // 既存のコード...

  void _handleSubmitted(String text) async {
    // 既存のコード...
    FocusScope.of(context).unfocus(); // メッセージ送信後にキーボードを閉じる
  }

  // 既存のコード...

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('チャット')),
      body: GestureDetector( // 追加
        onTap: () {
          FocusScope.of(context).unfocus(); // 入力フォーム以外をタップした時にキーボードを閉じる
        },
        child: SafeArea(
          child: Column(
            children: <Widget>[
              // 既存のコード...
            ],
          ),
        ),
      ),
    );
  }
}
```

### 見た目を良くする
メッセージのWidgetがしょぼかったので、文句を言いました。
![スクリーンショット 2023-04-20 10.52.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183031/6f0b6488-a1b0-9a79-a948-33692e13c776.png)
```dart
Widget _buildMessageItem(String message, bool isUserMessage) {
  return Container(
    padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
    child: Row(
      mainAxisAlignment:
          isUserMessage ? MainAxisAlignment.end : MainAxisAlignment.start,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Flexible(
          child: Container(
            padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 14.0),
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(12.0),
              color: isUserMessage ? Colors.blue[100] : Colors.grey[200],
            ),
            child: Text(
              message,
              style: TextStyle(color: isUserMessage ? Colors.white : Colors.black),
            ),
          ),
        ),
      ],
    ),
  );
}
```

```dart
@override
Widget build(BuildContext context) {
  // 既存のコード...

  return Scaffold(
    appBar: AppBar(title: const Text('チャット')),
    body: GestureDetector(
      onTap: () {
        FocusScope.of(context).unfocus();
      },
      child: SafeArea(
        child: Column(
          children: <Widget>[
            Expanded(
              child: ListView.builder(
                itemCount: _messages.length,
                itemBuilder: (context, index) {
                  final message = _messages[index];
                  final isUserMessage = message.startsWith('User: ');
                  final displayMessage = isUserMessage
                      ? message.replaceFirst('User: ', '')
                      : message.replaceFirst('ChatGPT: ', '');
                  return _buildMessageItem(displayMessage, isUserMessage);
                },
              ),
            ),
            // 既存のコード...
          ],
        ),
      ),
    ),
  );
}
```

### UXを良くする
メッセージ送信してからレスポンスが来るまで時間がかかるので、Waiting中を表示してもらうようにしました。
![スクリーンショット 2023-04-20 10.57.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183031/48266400-e7b0-fbc8-b8ee-acc279632a8c.png)

FutureBuilder使うのは嫌だったので文句を言いました。
![スクリーンショット 2023-04-20 10.58.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183031/b147a00f-6bce-05d8-d99f-f36eed9156b8.png)
```dart
class ChatScreen extends StatefulWidget {
  // 既存のコード...
}

class _ChatScreenState extends State<ChatScreen> {
  // 既存のコード...

  bool _isLoading = false;

  // 既存のコード...

  Future<void> _handleSubmitted(String text) async {
    // 既存のコード...

    setState(() {
      _isLoading = true;
    });

    try {
      final chatGptResponse = await chatGPT(text);
      setState(() {
        _messages.add('ChatGPT: ${chatGptResponse.text}');
        _isLoading = false;
      });
    } catch (e) {
      setState(() {
        _isLoading = false;
      });
      showDialog(
        context: context,
        builder: (context) => AlertDialog(
          title: Text('Error'),
          content: Text('Failed to load response from ChatGPT'),
        ),
      );
    }
  }

  // 既存のコード...

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // 既存のコード...

      body: GestureDetector(
        onTap: () {
          FocusScope.of(context).unfocus();
        },
        child: SafeArea(
          child: Column(
            children: <Widget>[
              Expanded(
                child: ListView.builder(
                  itemCount: _messages.length,
                  itemBuilder: (context, index) {
                    // 既存のコード...
                  },
                ),
              ),
              Padding(
                padding: const EdgeInsets.all(8.0),
                child: Row(
                  children: <Widget>[
                    Expanded(
                      child: TextField(
                        controller: _textController,
                        onSubmitted: _handleSubmitted,
                        decoration: InputDecoration(hintText: 'Send a message'),
                      ),
                    ),
                    IconButton(
                      icon: Icon(Icons.send),
                      onPressed: () => _handleSubmitted(_textController.text),
                    ),
                  ],
                ),
              ),
              _isLoading ? Center(child: CircularProgressIndicator()) : Container(),
            ],
          ),
        ),
      ),
    );
  }
}
```

### 不具合の修正
日本語だと文字化けしたので、不具合を修正してもらいました。
![スクリーンショット 2023-04-20 10.55.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183031/69991b05-ba6c-5c28-777d-c9d4479c9578.png)
```dart
import 'dart:convert'; // 追加

// 既存のコード...

Future<ChatGPTResponse> chatGPT(String message) async {
  // 既存のコード...

  final response = await http.post(
    // 既存のコード...
  );

  if (response.statusCode == 200) {
    return ChatGPTResponse.fromJson(json.decode(utf8.decode(response.bodyBytes))); // 更新
  } else {
    throw Exception('Failed to load response from ChatGPT');
  }
}
```


# 完成
ChatGPTのAPIを使ってチャットできるアプリが完成しました🎉


一部、APIのところでChatGPTでは実装が厳しかった部分があり修正をしましたが、それ以外はChatGPTが書いてくれたコードで作りたいアプリが作れました👏

![タイトルなし.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183031/7758ee9e-b361-1878-6327-795be7c65db8.gif)

完成したソースコードは[こちら](https://github.com/never-inc/never_flutter_sample_code/tree/main/lib/talk_to_chat_gpt)

この方法で「AIを使ってXXXするアプリ」の量産ができるかも！？ただし、API利用には上限があるためコスパ（採算）と相談ですね

# 考察とまとめ
「すごい👏」の一言です。誤りはあるものの、ここまで実装してくれることに驚いています。

ChatGPTの活用方法に関して、冒頭でもお伝えしたとおり、書いてくれたソースコードはそのままでは使えないので、ChatGPTには「叩き台を作ってもらう」感覚で利用するのが良いと感じました。ご自身が触れてこなかった技術を調べる取っ掛かりとしては強力で、ChatGPTに教えてもらった内容をヒントに開発をスムーズに進めることができます。

また、ソースコードをアウトプットしてもらう際は、プロジェクトによってはソフトウェア設計が異なるため、ChatGPTにはどういう設計なのか伝える必要があります。ネット内に情報がある設計が学習されていれば望み通りの回答をしてくれるかもしれませんが、社内ドメイン知識で固められている設計では思ったような回答を得られず難しいだろうなと感じました。

そして今回は、ChatGPTに対して「日本語でやりたいことに対して少し条件をつけて質問する」ようにしました。質問する側として、ここは改善の余地があり、以下の対応をすればより精度の高い回答が得られるのではないかと思っています。

- プロンプトを組んで背景や条件を明記して質問する
- 英語で質問する

プロンプトについては、こちらのYouTubeチャンネルの方がかなり詳しく解説されておりますのでご参考ください。
https://www.youtube.com/@ShunsukeHayashi/videos

英語で質問する件については、日本語より英語で質問した方が精度が上がると聞きましたが、肌感そこまで感じなかったです。やはり質問の内容が大事（当たり前）かなと思います。


私個人として、ChatGPTを触り始めて間もないですが、開発業務につながる面白い使い方が見つかりましたらまた共有したいと思います。

それでは！