開篇
在本篇博客文章中,我們將通過OpenAI的API構(gòu)建一個簡單的對話界面,與ChatGPT進(jìn)行聊天。
近來OpenAI和ChatGPT的熱度很高,尤其是最近發(fā)布的GPT-4。這類工具的大量使用案例已經(jīng)涌現(xiàn)出來,但目前人們最常用ChatGPT的方式是通過chat.openai.com進(jìn)行聊天。我一直在使用ChatGPT來激發(fā)靈感、編寫一些Flutter代碼片段,甚至是撰寫這篇博客文章的大綱!當(dāng)然,它建議的大綱非常樂觀,因此我不得不略過一些部分,但它仍然提供了足夠的指引,讓我能夠立即開始。
然而,OpenAI的官方聊天界面體驗(yàn)并不好。它非常有限,聊天歷史記錄經(jīng)常無法正常工作。已經(jīng)有人使用Web技術(shù)構(gòu)建了更好的UI和用戶體驗(yàn)的ChatGPT客戶端應(yīng)用程序,例如TypingMind。
作為Flutter開發(fā)者,我不禁想到Flutter非常適合作為ChatGPT客戶端應(yīng)用程序的選擇!Flutter具有跨平臺能力和豐富的UI組件,是這樣一個項(xiàng)目的完美選擇。我們可以編寫一次代碼,然后將應(yīng)用程序發(fā)布在Web、iOS、Android以及桌面平臺上:Windows、macOS和Linux。
ChatGPT API
要使用OpenAI的任何API,您需要注冊并獲取API密鑰。請注意,API使用可能會產(chǎn)生費(fèi)用,您需要提供付款詳細(xì)信息。我們將使用的gpt-3.5-turbo模型相對便宜,除非您經(jīng)常使用它,否則不應(yīng)花費(fèi)太多錢。
具體來說,我們將使用Chat API(聊天完成),該API支持兩個OpenAI的模型:gpt-3.5-turbo和gpt-4。我們可以在此處找到Chat API的完整參考,其中涉及在
https://api.openai.com/v1/chat/completions 上執(zhí)行POST請求。
此時,我們可以使用http庫向Chat API發(fā)送必需的數(shù)據(jù)并解析響應(yīng)。但是,由于Dart和Flutter社區(qū)的貢獻(xiàn),已經(jīng)在pub.dev上提供了一個可用的包:dart_openai。它將為我們進(jìn)行API請求并返回解析后的響應(yīng),因此我們只需獲取響應(yīng)文本并在應(yīng)用程序中顯示即可。
以下是一個接受用戶消息并返回ChatGPT響應(yīng)的方法:
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;
}
上面的方法接受用戶的最后一條消息以及對話中的所有先前消息。請注意,在API請求中,ChatGPT的響應(yīng)標(biāo)有助手的角色。
現(xiàn)在,我們把最終版本的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'; // 設(shè)置模型為"gpt-3.5-turbo"
ChatApi() { // 構(gòu)造函數(shù),設(shè)置OpenAI的apiKey和organization
OpenAI.apiKey = openAiApiKey;
OpenAI.organization = openAiOrg;
}
Future<String> completeChat(List<ChatMessage> messages) async {
// 定義方法completeChat,接收ChatMessage列表類型參數(shù)messages,并返回Future<String>類型
final chatCompletion = await OpenAI.instance.chat.create( // 調(diào)用OpenAI的chat.create方法,獲取ChatCompletionModel對象
model: _model, // 傳遞模型參數(shù)
messages: messages // 傳遞對話歷史消息參數(shù),并將ChatMessage列表轉(zhuǎn)換為OpenAIChatCompletionChoiceMessageModel列表
.map((e) => OpenAIChatCompletionChoiceMessageModel(
role: e.isUserMessage ? 'user' : 'assistant', // 指定消息發(fā)送者角色
content: e.content, // 傳遞消息文本內(nèi)容
))
.toList(),
);
return chatCompletion.choices.first.message.content; // 返回ChatCompletionModel對象中的消息內(nèi)容
}
}
請注意,在構(gòu)造函數(shù)中,我們設(shè)置了API密鑰和組織ID。如果沒有API密鑰,任何請求都將失敗。組織ID是可選的,如果您在OpenAI平臺上設(shè)置了多個組織,可以提供組織ID。
// secrets.dart
const openAiApiKey = 'YOUR_API_KEY';
const openAiOrg = 'YOUR_ORGANIZATION_ID';
為了避免將敏感信息提交到版本控制中,我們通常會將其存儲在 secrets 文件中并在 .gitignore 中添加 secrets 文件,以便在提交時忽略這些文件。在 GitHub 上的項(xiàng)目存儲庫中,通常會提供一個名為 secrets_example.dart 的文件,其中包含占位符值,供開發(fā)人員參考并創(chuàng)建自己的 secrets 文件。
API密鑰的注意事項(xiàng)
在本文中,我們正在構(gòu)建一個客戶端應(yīng)用程序。像這樣硬編碼API密鑰的應(yīng)用程序不應(yīng)該發(fā)布。由于API使用可能會產(chǎn)生費(fèi)用,您不希望暴露自己的API密鑰。
如果您想發(fā)布這樣的應(yīng)用程序,您有兩個選擇:
1.允許用戶提供自己的API密鑰開始聊天。用戶可以通過應(yīng)用程序提供他們的密鑰,您可以將其安全地存儲在本地存儲中,以在每個API請求中使用。
2.而不是直接調(diào)用聊天API,可以調(diào)用一個服務(wù)器或邊緣函數(shù),然后使用自己的令牌調(diào)用聊天API。這樣,您就不會暴露自己的API密鑰,可以控制流量,并具有其他授權(quán)和速率限制要求。如果您選擇這種方法,您可能需要考慮賺錢,因?yàn)榻?jīng)常使用應(yīng)用程序的用戶將花費(fèi)您的錢!
構(gòu)建前端界面
現(xiàn)在 Chat API 已準(zhǔn)備好使用,是時候開始構(gòu)建 UI 了。如果你從零開始,可以使用 "flutter create" 命令初始化一個 Flutter 項(xiàng)目:
flutter create my_chatgpt_client
UI將是相當(dāng)標(biāo)準(zhǔn)的,包含兩個主要的組件:消息編輯器和消息氣泡。主屏幕將是聊天中所有消息的列表(作為消息氣泡),消息編輯器在底部,我們可以在其中輸入消息。
讓我們從消息編輯器小部件開始:
// widgets/message_composer.dart
import 'package:flutter/material.dart';
class MessageComposer extends StatelessWidget {
MessageComposer({
required this.onSubmitted, // 回調(diào)函數(shù),當(dāng)用戶輸入文本提交時調(diào)用
required this.awaitingResponse, // 是否正在等待 Chat API 的響應(yīng)
super.key,
});
final TextEditingController _messageController = TextEditingController(); // 用于文本輸入的控制器
final void Function(String) onSubmitted; // 回調(diào)函數(shù),當(dāng)用戶輸入文本提交時調(diào)用
final bool awaitingResponse; // 是否正在等待 Chat API 的響應(yīng)
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(12), // 內(nèi)邊距
color: Theme.of(context).colorScheme.secondaryContainer.withOpacity(0.05), // 顏色
child: SafeArea(
child: Row(
children: [
Expanded(
child: !awaitingResponse // 如果沒有正在等待 Chat API 的響應(yīng)
? TextField(
controller: _messageController, // 將文本控制器傳遞給 TextField 組件
onSubmitted: onSubmitted, // 當(dāng)用戶提交文本時調(diào)用 onSubmitted 回調(diào)函數(shù)
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...'), // 正在獲取響應(yīng)
),
],
),
),
IconButton(
onPressed: !awaitingResponse // 如果沒有正在等待 Chat API 的響應(yīng)
? () => onSubmitted(_messageController.text) // 當(dāng)用戶點(diǎn)擊發(fā)送按鈕時調(diào)用 onSubmitted 回調(diào)函數(shù)
: null, // 禁用按鈕
icon: const Icon(Icons.send), // 發(fā)送圖標(biāo)
),
],
),
),
);
}
}
消息編輯器將在文本字段被提交(例如,按下Enter鍵)或者我們點(diǎn)擊右側(cè)的發(fā)送按鈕時,調(diào)用我們傳遞給它的onSubmitted方法。我們可以使用awaitingResponse標(biāo)志隱藏文本字段并禁用發(fā)送按鈕。當(dāng)我們等待API的響應(yīng)時,我們將將此標(biāo)志設(shè)置為true以表示消息正在提交中。
消息氣泡小部件是一個簡單的容器,根據(jù)消息是用戶消息還是AI生成的消息,具有不同的背景顏色和發(fā)送者名稱:
// 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; // 消息內(nèi)容
final bool isUserMessage; // 是否為用戶消息
@override
Widget build(BuildContext context) {
final themeData = Theme.of(context);
return Container(
margin: const EdgeInsets.all(8), // 設(shè)置組件的邊距
decoration: BoxDecoration(
color: isUserMessage // 根據(jù)消息類型設(shè)置不同的背景顏色
? themeData.colorScheme.primary.withOpacity(0.4)
: themeData.colorScheme.secondary.withOpacity(0.4),
borderRadius: const BorderRadius.all(Radius.circular(12)), // 設(shè)置圓角邊框
),
child: Padding(
padding: const EdgeInsets.all(12), // 設(shè)置內(nèi)邊距
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
isUserMessage ? 'You' : 'AI', // 根據(jù)消息類型顯示發(fā)送者
style: const TextStyle(fontWeight: FontWeight.bold),
),
],
),
const SizedBox(height: 8),
Text(content), // 顯示消息內(nèi)容
],
),
),
);
}
}
現(xiàn)在我們已經(jīng)準(zhǔn)備好了所有必要的小部件,現(xiàn)在讓我們將它們?nèi)拷M合在主頁上。下面是主聊天頁面的代碼:
// 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,
Key? key,
}) : super(key: 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: [
// 使用_spread_操作符將每個聊天消息轉(zhuǎn)換為MessageBubble widget
..._messages.map(
(msg) => MessageBubble(
content: msg.content,
isUserMessage: msg.isUserMessage,
),
),
],
),
),
// 使用MessageComposer widget顯示用戶輸入
MessageComposer(
onSubmitted: _onSubmitted,
awaitingResponse: _awaitingResponse,
),
],
),
);
}
這是一個有狀態(tài)的widget,它開始時顯示消息"How can I help?",這樣我們就不會從空白聊天開始了。
最后一部分是_onSubmitted方法,當(dāng)用戶提交一條消息時,它會通過消息編輯器被調(diào)用。
// _onSubmitted方法將會在用戶輸入一條新的聊天消息后被調(diào)用
Future<void> _onSubmitted(String message) async {
setState(() {
// 在UI中顯示用戶剛剛輸入的消息
_messages.add(ChatMessage(message, true));
// 等待服務(wù)器的回復(fù)
_awaitingResponse = true;
});
// 發(fā)送用戶的消息到API,等待API返回回復(fù)
final response = await widget.chatApi.completeChat(_messages);
setState(() {
// 在UI中顯示API的回復(fù)
_messages.add(ChatMessage(response, false));
// 回復(fù)已經(jīng)接收完成,不再等待
_awaitingResponse = false;
});
}
當(dāng)用戶提交一條新的消息時,我們會在 setState 中將該消息添加到聊天消息中,并將 _awaitingResponse 設(shè)置為 true。這將在對話中顯示用戶的消息,并禁用消息編輯器。
接下來,我們將所有消息傳遞給聊天 API,并等待響應(yīng)。一旦我們得到了響應(yīng),我們會在 _messages 中添加它作為聊天消息,并在第二個 setState 調(diào)用中將 _awaitingResponse 設(shè)置回 false。
至此,對話流程就完成了!現(xiàn)在讓我們看看它的實(shí)際效果:
這是應(yīng)用程序的代碼和主要方法:
import 'package:chatgpt_client/api/chat_api.dart';
import 'package:chatgpt_client/chat_page.dart';
import 'package:flutter/material.dart';
void main() {
// 運(yùn)行ChatApp
runApp(ChatApp(chatApi: ChatApi()));
}
class ChatApp extends StatelessWidget {
// 帶有chatApi參數(shù)的構(gòu)造函數(shù)
const ChatApp({required this.chatApi, Key? key}) : super(key: key);
final ChatApi chatApi; // chatApi屬性
@override
Widget build(BuildContext context) {
return MaterialApp(
// MaterialApp,包含了應(yīng)用的所有基本元素
title: 'ChatGPT Client', // 應(yīng)用的標(biāo)題
theme: ThemeData(
// 設(shè)置應(yīng)用的主題色
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.teal, // 主色調(diào)
secondary: Colors.lime, // 次要色調(diào)
),
),
home: ChatPage(chatApi: chatApi), // ChatPage是這個應(yīng)用的首頁
);
}
}
解析markdown
在我們之前與ChatGPT的對話中,我們問了一個跟進(jìn)問題“給我展示代碼”。
我們在回復(fù)中得到了相當(dāng)數(shù)量的Flutter代碼,但它們都是以markdown格式呈現(xiàn)的!我們將使用markdown_widget包來解決這個問題。
flutter pub add markdown_widget
在MessageBubble組件中,用MarkdownWidget替換包含消息內(nèi)容的Text組件:
MarkdownWidget(
data: content,
shrinkWrap: true,
)
經(jīng)過一次熱重載,我們可以看到代碼現(xiàn)在已經(jīng)被正確解析了。這很容易!
錯誤處理
如果我們從OpenAI得到了錯誤響應(yīng)怎么辦?在測試中,我遇到了一些429(Too Many Requests)異常。如果你調(diào)用API太頻繁,也可能出現(xiàn)這種錯誤,但也可能是因?yàn)镺penAI API總共接收到了太多的請求。
我們至少要處理這個錯誤并顯示一個有用的消息。下面是修改后的_onSubmitted方法:
Future<void> _onSubmitted(String message) async {
setState(() {
_messages.add(ChatMessage(message, true)); // 將輸入的消息添加到對話列表
_awaitingResponse = true; // 設(shè)置正在等待響應(yīng)標(biāo)志
});
try {
final response = await widget.chatApi.completeChat(_messages); // 調(diào)用 ChatApi 的 completeChat 方法來獲取 OpenAI 的響應(yīng)
setState(() {
_messages.add(ChatMessage(response, false)); // 將響應(yīng)添加到對話列表
_awaitingResponse = false; // 取消等待響應(yīng)標(biāo)志
});
} catch (err) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('An error occurred. Please try again.')), // 顯示錯誤信息提示條
);
setState(() {
_awaitingResponse = false; // 取消等待響應(yīng)標(biāo)志
});
}
}
當(dāng)然,這還可以進(jìn)一步改進(jìn)。我們可以提供一個選項(xiàng),在不需要發(fā)送新消息的情況下重試響應(yīng),但也可以在 ChatApi 中自動重試請求而不顯示錯誤。
結(jié)束
總結(jié)一下,現(xiàn)在我們已經(jīng)有一個完全可用的聊天應(yīng)用程序,可以隨時在任何平臺上與ChatGPT聊天了!
在這篇文章中,我們展示了如何構(gòu)建一個基本的聊天應(yīng)用程序,通過OpenAI的聊天API與ChatGPT進(jìn)行對話。我們還添加了一些附加功能,如markdown解析和錯誤處理。
這些功能相當(dāng)基礎(chǔ),但是我們可以通過這樣的應(yīng)用程序?qū)崿F(xiàn)更多有用的功能,比如能夠復(fù)制和/或共享響應(yīng)。此外,我們可以使用本地或云數(shù)據(jù)庫來存儲對話,以便隨時訪問它們。
您可以在這里找到源代碼。
https://github.com/dartling/chatgpt_client
今天的分享就到這里,感謝你的閱讀,希望能夠幫助到你,文章創(chuàng)作不易,如果你喜歡我的分享,別忘了點(diǎn)贊轉(zhuǎn)發(fā),讓更多有需要的人看到,最后別忘記關(guān)注「前端達(dá)人」,你的支持將是我分享最大的動力,后續(xù)我會持續(xù)輸出更多內(nèi)容,敬請期待。
原文:
https://dartling.dev/building-a-chatgpt-client-app-with-flutter作者:Christos文章來源:http://www.zghlxwxcb.cn/news/detail-451779.html
直接翻譯,有自行改編和添加部分,翻譯水平有限,難免有疏漏,歡迎指正文章來源地址http://www.zghlxwxcb.cn/news/detail-451779.html
到了這里,關(guān)于使用Flutter構(gòu)建ChatGPT客戶端:快速入門指南的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!