用Flutter构建ChatGTP聊天应用

介绍

本文将通过OpenAI的API构建一个简单的ChatGPT对话界面。

最近,OpenAI和ChatGPT越来越受到热捧,尤其是最近发布了GPT-4。这种工具的使用案例已经涌现出很多,但迄今为止人们最常使用的ChatGPT方式是通过chat.openai.com进行聊天。我一直在使用ChatGPT来头脑风暴,写一些Flutter代码片段,甚至为本文写大纲!当然,它建议的大纲非常丰富,所以我不得不略去一些部分,但它仍然提供了足够的指引,现在我们开始构建APP吧。

官方聊天界面体验并不好,非常受限,而且聊天记录经常无法正常工作。已经有人已经开发具有更好的UI和用户体验的ChatGPT客户端应用程序,例如网页版的TypingMind。

作为Flutter开发人员,我认为Flutter非常适合开发ChatGPT客户端!Flutter具有跨平台能力和丰富的UI组件,是这样一个项目的完美选择。我们可以只编写一次代码,并将应用程序发布到Web、iOS、Android以及桌面平台:Windows、macOS和Linux。

ChatGPT的API

首先我们需要在官网上注册一个帐号(platform.openai.com/signup). 请注意,使用API可能需要花费一定的费用,并且您需要提供付款信息。具体而言,我们将使用的gpt-3.5-turbo模型相当便宜,除非经常使用它,否则不会花费超过几美分。
具体而言,我们将使用chat api完成该项目。支持两个OpenAI模型:gpt-3.5-turbo和gpt-4。我们可以在这里(api.openai.com/v1/chat/com…
现在我们可以使用http库向Chat API发送所需数据的请求并解析响应。但是在pub.dev上已经有一个可用的包:dart_openai。(还有另外一个chat_gpt_sdk,大家可以自行使用)。它将为我们进行API请求并返回解析后的响应,因此我们只需获取响应文本并在应用程序中显示它即可。

下面是一个调用chatgtp的示例:

Future<String> completeChat(String message) async {
  final chatCompletion = await OpenAI.instance.chat.create(
    model: 'gpt-3.5-turbo',
    messages: [
      OpenAIChatCompletionChoiceMessageModel(
        content: message,
        role: 'user',
      ),
    ],
  );
  return chatCompletion.choices.first.message.content;
}

因为这是同一次会话,我们需要将之前的消息传递给请求,这样ChatGPT就有了完整的会话上下文,而不仅仅是用户的最后一条消息。

class ChatMessage {
  ChatMessage(this.content, this.isUserMessage);

  final String content;
  final bool isUserMessage;
}

Future<String> completeChat(List<ChatMessage> messages) async {
  final chatCompletion = await OpenAI.instance.chat.create(
    model: 'gpt-3.5-turbo',
    messages: [
      ...previousMessages.map(
        (e) => OpenAIChatCompletionChoiceMessageModel(
          role: e.isUserMessage ? 'user' : 'assistant',
          content: e.content,
        ),
      ),
    ],
  );
  return chatCompletion.choices.first.message.content;
}

上述方法接受用户的最后一条消息和对话中的所有先前消息。请注意,ChatGPT的响应在API请求中标记有助手角色。
让我们将完整的completeChat方法放入ChatApi类的最终版本中,以供以后使用。

// models/chat_message.dart
class ChatMessage {
  ChatMessage(this.content, this.isUserMessage);

  final String content;
  final bool isUserMessage;
}
// api/chat_api.dart
import 'package:chatgpt_client/models/chat_message.dart';
import 'package:chatgpt_client/secrets.dart';
import 'package:dart_openai/openai.dart';

class ChatApi {
  static const _model = 'gpt-3.5-turbo';

  ChatApi() {
    OpenAI.apiKey = openAiApiKey;
    OpenAI.organization = openAiOrg;
  }

  Future<String> completeChat(List<ChatMessage> messages) async {
    final chatCompletion = await OpenAI.instance.chat.create(
      model: _model,
      messages: messages
          .map((e) => OpenAIChatCompletionChoiceMessageModel(
                role: e.isUserMessage ? 'user' : 'assistant',
                content: e.content,
              ))
          .toList(),
    );
    return chatCompletion.choices.first.message.content;
  }
}

请注意,在构造函数中,我们正在设置API密钥和组织ID。没有API密钥,任何请求都将失败。组织ID是可选的,如果在OpenAI平台上设置了组织,则可以提供它。

// secrets.dart
const openAiApiKey = 'YOUR_API_KEY';
const openAiOrg = 'YOUR_ORGANIZATION_ID';

secrets 文件被包含在 .gitignore 中,以避免将其提交到版本控制中。

API 密钥的说明

在本文中,我们正在构建一个APP。像这样具有硬编码的API密钥的应用程序不应该发布。由于API使用可能会产生费用,所以会不希望暴露你的API密钥。
如果您想发布这样的APP,您有两个选择:
1.允许用户提供他们自己的API密钥以开始聊天。用户可以通过应用程序提供他们的密钥,并且您可以安全地将其存储在本地存储中,以在每个API请求中使用。
2.不直接调用聊天API,请调用一个自有服务器,然后使用您自己的令牌调用聊天API。这样,您就不会暴露您的API密钥,可以控制流量,并具有其他授权和速率限制要求。如果您采用此方法,您可能需要考虑赚钱,因为频繁使用应用程序的用户会给您带来费用!

聊天API

下面让我们开始吧,用 flutter create 创建一个Flutter工程:

flutter create my_chatgpt_client

界面包含两个主要的组件:消息编辑器和消息气泡。主屏幕将是聊天中所有消息的列表(作为消息气泡),消息编辑器位于底部,我们可以在其中输入消息。
让我们从消息编辑器开始:

// widgets/message_composer.dart
import 'package:flutter/material.dart';

class MessageComposer extends StatelessWidget {
  MessageComposer({
    required this.onSubmitted,
    required this.awaitingResponse,
    super.key,
  });

  final TextEditingController _messageController = TextEditingController();

  final void Function(String) onSubmitted;
  final bool awaitingResponse;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(12),
      color: Theme.of(context).colorScheme.secondaryContainer.withOpacity(0.05),
      child: SafeArea(
        child: Row(
          children: [
            Expanded(
              child: !awaitingResponse
                  ? TextField(
                      controller: _messageController,
                      onSubmitted: onSubmitted,
                      decoration: const InputDecoration(
                        hintText: 'Write your message here...',
                        border: InputBorder.none,
                      ),
                    )
                  : Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: const [
                        SizedBox(
                          height: 24,
                          width: 24,
                          child: CircularProgressIndicator(),
                        ),
                        Padding(
                          padding: EdgeInsets.all(16),
                          child: Text('Fetching response...'),
                        ),
                      ],
                    ),
            ),
            IconButton(
              onPressed: !awaitingResponse
                  ? () => onSubmitted(_messageController.text)
                  : null,
              icon: const Icon(Icons.send),
            ),
          ],
        ),
      ),
    );
  }
}

消息组件将在提交文本时(例如通过按回车键)或在右侧点击发送按钮时调用我们传递给它的onSubmitted方法。通过awaitingResponse标志,我们可以隐藏文本组件并禁用发送按钮。
消息气泡小部件是一个简单的容器,具有不同的背景颜色和发送者名称:

// widgets/message_bubble.dart
import 'package:flutter/material.dart';

class MessageBubble extends StatelessWidget {
  const MessageBubble({
    required this.content,
    required this.isUserMessage,
    super.key,
  });

  final String content;
  final bool isUserMessage;

  @override
  Widget build(BuildContext context) {
    final themeData = Theme.of(context);
    return Container(
      margin: const EdgeInsets.all(8),
      decoration: BoxDecoration(
        color: isUserMessage
            ? themeData.colorScheme.primary.withOpacity(0.4)
            : themeData.colorScheme.secondary.withOpacity(0.4),
        borderRadius: const BorderRadius.all(Radius.circular(12)),
      ),
      child: Padding(
        padding: const EdgeInsets.all(12),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                Text(
                  isUserMessage ? 'You' : 'AI',
                  style: const TextStyle(fontWeight: FontWeight.bold),
                ),
              ],
            ),
            const SizedBox(height: 8),
            Text(content),
          ],
        ),
      ),
    );
  }
}

现在我们已经有了所有必要的功能,让我们把它们全部放在主页面上。
下面是主聊天页面的代码:

// chat_page.dart
import 'package:chatgpt_client/api/chat_api.dart';
import 'package:chatgpt_client/models/chat_message.dart';
import 'package:chatgpt_client/widgets/message_bubble.dart';
import 'package:chatgpt_client/widgets/message_composer.dart';
import 'package:flutter/material.dart';

class ChatPage extends StatefulWidget {
  const ChatPage({
    required this.chatApi,
    super.key,
  });

  final ChatApi chatApi;

  @override
  State<ChatPage> createState() => _ChatPageState();
}

class _ChatPageState extends State<ChatPage> {
  final _messages = <ChatMessage>[
    ChatMessage('Hello, how can I help?', false),
  ];
  var _awaitingResponse = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Chat')),
      body: Column(
        children: [
          Expanded(
            child: ListView(
              children: [
                ..._messages.map(
                  (msg) => MessageBubble(
                    content: msg.content,
                    isUserMessage: msg.isUserMessage,
                  ),
                ),
              ],
            ),
          ),
          MessageComposer(
            onSubmitted: _onSubmitted,
            awaitingResponse: _awaitingResponse,
          ),
        ],
      ),
    );
  }
}

这是一个有状态的小部件,从消息“How can I help?”开始,这样我们就不会以空白聊天开始。
最后一部分是_onSubmitted方法。

Future<void> _onSubmitted(String message) async {
  setState(() {
    _messages.add(ChatMessage(message, true));
    _awaitingResponse = true;
  });
  final response = await widget.chatApi.completeChat(_messages);
  setState(() {
    _messages.add(ChatMessage(response, false));
    _awaitingResponse = false;
  });
}

当提交一条消息时,我们将消息添加到聊天消息中,并在setState调用中将_awaitingResponse设置为true,在对话中显示用户消息,并禁用消息编辑器。
接下来,我们将所有消息传递给聊天API并等待响应。一旦我们收到响应,我们会将其作为聊天消息添加到_messages中,并在第二个setState调用中将_awaitingResponse设置回false。
这就是对话流程的全部!让我们看看它的实际效果:

用Flutter构建ChatGTP聊天应用

下面是 Appmain 的代码:

import 'package:chatgpt_client/api/chat_api.dart';
import 'package:chatgpt_client/chat_page.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(ChatApp(chatApi: ChatApi()));
}

class ChatApp extends StatelessWidget {
  const ChatApp({required this.chatApi, super.key});

  final ChatApi chatApi;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'ChatGPT Client',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.teal,
          secondary: Colors.lime,
        ),
      ),
      home: ChatPage(chatApi: chatApi),
    );
  }
}

解析 markdown

在我们之前与ChatGPT的对话中,我们提出了追问的问题“给我看代码”。

用Flutter构建ChatGTP聊天应用

我们在响应中得到了相当数量的Flutter代码,但它们都是用markdown编写的!让我们使用markdown_widget插件来解决这个问题。

flutter pub add markdown_widget

MessageBubble 组件中, 用MarkdownWidget替换Text:

MarkdownWidget(
  data: content,
  shrinkWrap: true,
)

在看看新的效果吧:

用Flutter构建ChatGTP聊天应用

错误处理

如果我们从OpenAI得到一个错误响应怎么办?在测试时,我遇到了一些429(太多请求)的异常。如果您调用API太频繁,或者OpenAI API总体上得到了太多请求,这种错误可能会发生。下面是修订后的_onSubmitted方法:

Future<void> _onSubmitted(String message) async {
  setState(() {
    _messages.add(ChatMessage(message, true));
    _awaitingResponse = true;
  });
  try {
    final response = await widget.chatApi.completeChat(_messages);
    setState(() {
      _messages.add(ChatMessage(response, false));
      _awaitingResponse = false;
    });
  } catch (err) {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('An error occurred. Please try again.')),
    );
    setState(() {
      _awaitingResponse = false;
    });
  }
}

当然,这还可以进一步改进。我们可以提供一个选项,允许在不需要发送新消息的情况下重试响应,同时在ChatApi中自动重试请求而不显示错误。

总结

我们现在拥有一个完全可用的聊天APP,可以在任何平台上随时与ChatGPT聊天!
在本文中,我们展示了如何构建一个基本的聊天应用程序,通过OpenAI的聊天API与ChatGPT进行对话。我们还添加了一些额外的功能,例如markdown解析和错误处理。
现在的功能相当基本,但我们添加更多的功能:例如能够复制和共享响应、我们可以使用本地或云数据库存储对话,以便随时访问它们。

你有兴趣用Fltter构建一个ChatGPT客户端吗?或者你有兴趣在后续的文章中添加更多功能,比如对话存储和组织等方面来改进这个APP吗?欢迎大家评论。

本文首发于公众号,转载请注明来源。

原文链接:https://juejin.cn/post/7226549820201500730 作者:AaronLei

(0)
上一篇 2023年4月29日 上午10:33
下一篇 2023年4月29日 上午10:43

相关推荐

发表回复

登录后才能评论