本文正在参加「金石计划」
介绍
最近ChatGPT风靡全球,网络上对 OpenAI 和 ChatGPT 进行了大量宣传,尤其是最近发布的 GPT-4。此类工具的大量用例不断涌现,人们使用它完成写论文、画美女、生成代码等等操作,但到目前为止,人们使用 ChatGPT 最广泛的方式还是是通过chat.openai.com。我也一直在官网上使用 ChatGPT 干活,编写一些代码片段、优化代码、添加注释等,作为疲于应对日报的开发,现在已经利用ChatGPT解脱了。
以前的日报:
再看看现的日报多么内卷,再也不用担心日报被领导批评了。
工作内容:
然而,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,生成图像的大小。必须是
256x256
、512x512
、1024x1024
之一。 - response_format:默认为url,生成的图像返回的格式。必须是 或
url
、b64_json
之一。 - user:用户的唯一标识符。
我们可以使用flutter_nb_net库发送携带参数的请求,并解析响应。pub.dev 上也有一个集成 API 的包:dart_openai,想走捷径的可以使用这个包。
应用程序
先创建一个简单的应用程序,主页是一个常规的聊天页面,能够接受用户输入,使用 API 获取响应,然后将它们显示在聊天气泡中。
最终界面将如下所示:
聊天页面
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发送请求,得到返回的消息。
下面是效果:
聊天功能已经实现,可以开心的和 Chatgpt 尬聊了,日报再也难不倒我了。
后面还有一篇,介绍翻译和生成图片。