使用 Flutter 构建 ChatGPT 客户端应用程序

3,089 阅读5分钟

本文正在参加「金石计划」

介绍

最近ChatGPT风靡全球,网络上对 OpenAI 和 ChatGPT 进行了大量宣传,尤其是最近发布的 GPT-4。此类工具的大量用例不断涌现,人们使用它完成写论文、画美女、生成代码等等操作,但到目前为止,人们使用 ChatGPT 最广泛的方式还是是通过chat.openai.com。我也一直在官网上使用 ChatGPT 干活,编写一些代码片段、优化代码、添加注释等,作为疲于应对日报的开发,现在已经利用ChatGPT解脱了。

以前的日报:

image-20230411105644709.png

再看看现的日报多么内卷,再也不用担心日报被领导批评了。

工作内容:

image.png

然而,OpenAI 官方聊天界面的体验并不友好。它功能非常有限,而且经常性的聊天记录不能正常工作。作为开发人员,也想开发下一个 AI 应用,方便自己使用,一开始在选择语言的时候同时考虑过 Compose 和 Flutter,作为这两个框架的长期使用人员,我觉得 Flutter 非常适合 ChatGPT 客户端应用程序,凭借其跨平台能力和丰富的 UI 组件,Flutter 是此类项目的完美选择,只需编写一次代码,就可以在网络、iOS、Android 以及桌面平台:Windows、macOS 和 Linux 上发布我们的应用程序。最终,我选择了使用 Flutter 构建一个简单的 AI 应用,功能包含通过 API 与 OpenAI 的 ChatGPT 聊天、翻译、生成图片等,并且还带有深色、浅色主题适配。

这篇文章其实也是利用ChatGPT写的。

入门

在我们开发应用之前,让我们对 ChatGPT API 的基础知识理有一个基本的了解。

OpenAI 有许多具有不同功能的 AI 模型。但是我们今天要使用的模型**gpt-3.5-turbo **是已知最有能力的模型,具有更高的准确性、更快的响应时间和改进的自然语言理解能力。

在使用 API 之前,我们需要生成一个 API 密钥用来鉴权。可以在此处生成 API 密钥(需要在 OpenAI 上创建一个帐户)。

我们将使用以下 API:

Create chat completion

POST https://api.openai.com/v1/chat/completions
//Creates a completion for the chat message

body采用以下格式的JSON :

{
  "model": "gpt-3.5-turbo",
  "messages": [{"role": "user", "content": "Hello!"}]
}

并且,返回response这样的:

{
  "id": "chatcmpl-123",
  "object": "chat.completion",
  "created": 1677652288,
  "choices": [{
    "index": 0,
    "message": {
      "role": "assistant",
      "content": "Hello there, how may I assist you today?",
    },
    "finish_reason": "stop"
  }],
  "usage": {
    "prompt_tokens": 9,
    "completion_tokens": 12,
    "total_tokens": 21
  }
}

Create imageBeta

POST https://api.openai.com/v1/images/generations
//Creates an image given a prompt.

body采用以下格式的JSON :

{
  "prompt": "A cute baby sea otter",
  "n": 2,
  "size": "1024x1024"
}
  • prompt:必填,所需图像的文本描述。最大长度为 1000 个字符。
  • n:默认为1,要生成的图像数。必须介于 1 和 10 之间。
  • size:默认为1024x1024,生成图像的大小。必须是256x256512x5121024x1024之一。
  • response_format:默认为url,生成的图像返回的格式。必须是 或urlb64_json之一。
  • user:用户的唯一标识符。

我们可以使用flutter_nb_net库发送携带参数的请求,并解析响应。pub.dev 上也有一个集成 API 的包:dart_openai,想走捷径的可以使用这个包。

应用程序

先创建一个简单的应用程序,主页是一个常规的聊天页面,能够接受用户输入,使用 API 获取响应,然后将它们显示在聊天气泡中。

最终界面将如下所示:

image-20230412135901292.png

聊天页面

UI 的搭建分为三部分:

  • 标题栏:具有几个导航按钮。

  • 聊天列表:有用户气泡和 AI 气泡。

  • 输入框:接收用户输入并发送到 ChatGPT API。

标题栏的两个导航action,点击跳转翻译和生成图片页面:

actions: <Widget>[
  IconButton(
    onPressed: () async {
      Get.toNamed(Routes.TRANSLATE);
    },
    icon: const Icon(Icons.translate),
  ),
  IconButton(
    onPressed: () async {
      Get.toNamed(Routes.IMAGE);
    },
    icon: const Icon(Icons.image),
  ),
],

还有一个侧边栏菜单,可以设置 API key:

drawer: const DrawerWidget(),

聊天列表:

ListView.builder(
    controller: _listScrollController,
    itemCount: controller.getChatList.length, //chatList.length,
    itemBuilder: (context, index) {
      return ChatWidget(
        msg: controller
            .getChatList[index].msg, // chatList[index].msg,
        chatIndex: controller.getChatList[index]
            .chatIndex, //chatList[index].chatIndex,
        shouldAnimate:
            controller.getChatList.length - 1 == index,
      );
    }),

聊天列表的item是一个自定义控件ChatWidget,参数index区别用户和 AI。,用户的index是0。

建立聊天气泡

用户气泡:

class UserChatView extends StatelessWidget {
  const UserChatView({Key? key, required this.msg}) : super(key: key);

  final String msg;

  @override
  Widget build(BuildContext context) {
    return Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        const SizedBox(width: 40.0),
        Expanded(
          child: Align(
            alignment: Alignment.centerRight,
            child: Container(
              padding: const EdgeInsets.all(8.0),
              decoration: BoxDecoration(
                color: Get.theme.colorScheme.secondaryContainer,
                borderRadius: BorderRadius.circular(16.0),
              ),
              child: Text(
                msg,
                style: TextStyle(
                  color: Get.theme.colorScheme.onSecondaryContainer,
                ),
              ),
            ),
          ),
        ),
        const SizedBox(width: 8.0),
        const CircleAvatar(
          backgroundImage: AssetImage(Assets.imagesPerson),
          radius: 16.0,
        )
      ],
    );
  }
}

AI 气泡需要有打字机的效果,仿生机器人在一个一个输入,这个效果使用了animated_text_kit实现的:

class AiChatView extends StatelessWidget {
  const AiChatView({super.key, required this.msg, required this.shouldAnimate});

  final String msg;
  final bool shouldAnimate;

  @override
  Widget build(BuildContext context) {
    return Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        const CircleAvatar(
          backgroundImage: AssetImage(Assets.imagesOpenaiLogo),
          radius: 16.0,
        ),
        const SizedBox(width: 8.0),
        Expanded(
          child: Container(
            padding: const EdgeInsets.all(8.0),
            decoration: BoxDecoration(
              color: Get.theme.colorScheme.tertiaryContainer,
              borderRadius: BorderRadius.circular(16.0),
              boxShadow: [
                BoxShadow(
                  color: Colors.black.withOpacity(0.05),
                  blurRadius: 5,
                  offset: const Offset(0, 2),
                ),
              ],
            ),
            child: shouldAnimate
                ? InkWell(
                    onLongPress: () {
                      Clipboard.setData(ClipboardData(text: msg.trim()));
                      showToast("已经复制到剪切板");
                    },
                    child: DefaultTextStyle(
                      style: TextStyle(
                          color: Get.theme.colorScheme.onTertiaryContainer,
                          fontWeight: FontWeight.w700,
                          fontSize: 16),
                      child: AnimatedTextKit(
                          isRepeatingAnimation: false,
                          repeatForever: false,
                          displayFullTextOnTap: true,
                          totalRepeatCount: 1,
                          animatedTexts: [
                            TyperAnimatedText(
                              msg.trim(),
                            ),
                          ]),
                    ),
                  )
                : SelectableText(
                    msg.trim(),
                    style: TextStyle(
                        color: Get.theme.colorScheme.onTertiaryContainer,
                        fontWeight: FontWeight.w700,
                        fontSize: 16),
                  ),
          ),
        ),
        const SizedBox(width: 40.0),
      ],
    );
  }
}

外网的访问有点慢,所以在等待的时候,显示一个加载状态:

     if (controller.isTyping)
                SpinKitThreeBounce(
                  color: Get.theme.colorScheme.secondary,
                  size: 18,
                ),

逻辑层

状态管理依然使用getx,使用GetX Template Generator插件一键生成代码结构。

本来有选择模型的逻辑,后来去掉了,只有一个发消息和接受接收消息的逻辑:

  /// Method to send user message to GPT model and get answers
  Future<void> sendMessageAndGetAnswers({required String msg}) async {
    chatList.addAll(await repository.sendMessageGPT(
      message: msg,
    ));
    _isTyping = false;
    update();
  }

数据层:

class ChatRepository {

  Future<List<ChatBean>> sendMessageGPT({required String message}) async {
    var result = await post('/chat/completions',
        data: jsonEncode({
          "model": aiModel,
          "messages": [
            {
              "role": "user",
              "content": message,
            }
          ]
        }),
        decodeType: ChatModel());

    List<ChatBean> chatList = [];

    result.when(success: (model) {
      model.choices?.forEach((element) {
        var content = element.message?.content;
        if (content?.isNotEmpty ?? false) {
          chatList.add(ChatBean(msg: content!, chatIndex: 1));
        }
      });
    }, failure: (msg, __) {
      showToast(msg);
    });

    return chatList;
  }
}

网络请求使用非常棒的网络库flutter_nb_net发送请求,得到返回的消息。

下面是效果:

屏幕录制2023-04-11 14.41.27.gif

聊天功能已经实现,可以开心的和 Chatgpt 尬聊了,日报再也难不倒我了。

后面还有一篇,介绍翻译和生成图片。