chatgpt flutter客户端项目实践
# 007.chatgpt flutter客户端项目实践
# 1 chatgpt api使用
# 1.1 基本使用
# 请求
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": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Hello!"}]
}'
2
3
4
5
6
7
8
# 响应:
{
"id": "chatcmpl-123",
"object": "chat.completion",
"created": 1677652288,
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": "\n\nHello there, how may I assist you today?",
},
"finish_reason": "stop"
}],
"usage": {
"prompt_tokens": 9,
"completion_tokens": 12,
"total_tokens": 21
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 1.2 如何具备上下文关联的功能
gpt/chat-completions-api (opens new window)
import openai
openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Who won the world series in 2020?"},
{"role": "assistant", "content": "The Los Angeles Dodgers won the World Series in 2020."},
{"role": "user", "content": "Where was it played?"}
]
)
2
3
4
5
6
7
8
9
10
11
将本次session的历史对话都塞到list中传给openai.
如果prompt很大或者对话比较多轮的话,将会比较耗费token.
很多开发者利用向量数据库方式节省token,
也可以不断让chatgpt帮你总结上文到多少字,作为下一次的输入.
# 1.3 json格式返回的支持
需要比较新的模型版本
To prevent these errors and improve model performance, when calling gpt-4-turbo-preview
or gpt-3.5-turbo-0125
, you can set response_format (opens new window) to { "type": "json_object" }
to enable JSON mode. When JSON mode is enabled, the model is constrained to only generate strings that parse into valid JSON object.
# 1.4 stream模式
直接使用1.1中的请求,默认为非stream模式,会在所有文本生成完后再返回,特点就是慢,字越多越慢.基本上十几秒到几分钟不等.
参数里加上:stream : true, 则会生成一段返回一段.
官方文档的介绍:
stream
boolean
Optional
Defaults to false
If set, partial message deltas will be sent, like in ChatGPT. Tokens will be sent as data-only server-sent events (opens new window) as they become available, with the stream terminated by a data: [DONE]
message. Example Python code (opens new window).
在dio里如何处理这种模式?
首先在options里设置responseType: dio2.ResponseType.stream,
dio!.post(
ChatHttpUrl.chatCompletionsReal(),
//HostType.getApiHost()+ ChatHttpUrl.chatCompletionsStream, 阿里云代理:cost: 18789 ms 18654ms
//https://xxx.tttt.top cf代理: 11107 ms,8948 ms,10968 ms
//http://xxxx4.tttt.top 无cf代理: 4085 ms 3427 ms 5931 ms
cancelToken: state.cancelToken,//用于点击按钮主动关闭
data: {
"model": model,
//gpt-3.5-turbo-16k
"messages": list,
// "max_tokens": 4096,
"stream": true
},
options: dio2.Options(
headers: headers,
//请求的Content-Type,默认值是"application/json; charset=utf-8",Headers.formUrlEncodedContentType会自动编码请求体.
//contentType: Headers.formUrlEncodedContentType,
//表示期望以那种格式(方式)接受响应数据。接受4种类型 `json`, `stream`, `plain`, `bytes`. 默认值是 `json`,
responseType: dio2.ResponseType.stream,
),
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
然后,用流的方式去解析:
Stream<List<int>> responseStream = response.data.stream;
List<int> chunks = [];
//changeTypingRate(false);
//Class 'String' has no instance getter 'stream'.
await for (List<int> chunk in responseStream) {
chunks.addAll(chunk);
String chunkString = Utf8Decoder(allowMalformed: true).convert(chunk);
//1 两个chuk之间偶发某个字乱码,如何解决?--> 不好解决,可以最后全部统一纠正
//2 String.fromCharCodes(chunk);--> 中文乱码, 需要用Utf8Decoder
2
3
4
5
6
7
8
9
解析出来的每个片段的文字如下: (------------------>为每个chunk的开头)
特征为:
- 完整的一行: data: {"id":"chatcmpl-6yssP5dAlMA7xZ12LREzqkmbtpNzh","object":"chat.completion.chunk","created":1679968645,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{"content":"德"},"index":0,"finish_reason":null}]}
- 可能在任何地方断句.
- 结束符: data: [DONE]
I/flutter ( 5793): data: {"id":"chatcmpl-6yssP5dAlMA7xZ12LREzqkmbtpNzh","object":"chat.completion.chunk","created":1679968645,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{"role":"assistant"},"index":0,"finish_reason":null}]}
I/flutter ( 5793):
I/flutter ( 5793): data: {"id":"chatcmpl-6yssP5dAlMA7xZ12LREzqkmbtpNzh","object":"chat.completion.chunk","created":1679968645,"m
I/flutter ( 5793): ------------------>
I/flutter ( 5793): odel":"gpt-3.5-turbo-0301","choices":[{"delta":{"content":"郭"},"index":0,"finish_reason":null}]}
I/flutter ( 5793):
I/flutter ( 5793): data: {"id":"chatcmpl-6yssP5dAlMA7xZ12LREzqkmbtpNzh","object":"chat.completion.chunk","created":1679968645,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{"content":"德"},"index":0,"finish_reason":null}]}
I/flutter ( 5793):
I/flutter ( 5793): data: {"id":"chatcmpl-6yssP5dAlMA7xZ12LREzqkmbtpNzh","object":"chat.completion.chunk","created":1679968645,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{"content":"纲"},"index":0,"finish_reason":null}]}
I/flutter ( 5793):
I/flutter ( 5793): data: {"id":"chatcmpl-6yssP5dAlMA7xZ12LREzqkmbtpNzh","object":"chat.completion.chunk","created":1679968645,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{"content":"是"},"index":0,"finish_reason":null}]}
I/flutter ( 5793):
I/flutter ( 5793): data: {"id":"chatcmpl-6yssP5dAlMA7xZ12LREzqkmbtpNzh","object":"chat.completion.chunk","created":1679968645,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{"content":"当"},"index":0,"finish_reason":null}]}
I/flutter ( 5793):
I/flutter ( 5793): data: {"id":"chatcmpl-6yssP5dAlMA7xZ12LREzqkmbtpNzh","object":"chat.completion.chunk","created":1679968645,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{"content":"今"},"index":0,"finish_reason":null}]}
I/flutter ( 5793):
I/flutter ( 5793): data: {"id":"chatcmpl-6yssP5dAlMA7xZ12LREzqkmbtpNzh","object":"chat.completion.chunk","created":1679968645,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{"content":"相"},"index":0,"finish_reason":null}]}
I/flutter ( 5793):
I/flutter ( 5793): data: {"id":"ch
//中间省略....
I/flutter ( 5793): data: {"id":"ch
I/flutter ( 5793): ------------------>
I/flutter ( 5793): atcmpl-6yssP5dAlMA7xZ12LREzqkmbtpNzh","object":"chat.completion.chunk","created":1679968645,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{"content":"声"},"index":0,"finish_reason":null}]}
I/flutter ( 5793): data: {"id":"chatcmpl-6yssP5dAlMA7xZ12LREzqkmbtpNzh","object":"chat.completion.chunk","created":1679968645,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{},"index":0,"finish_reason":"stop"}]}
I/flutter ( 5793):
I/flutter ( 5793): data: [DONE]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
处理方式: 不断拼接,并移除data: ,然后解析json,有断句的,放到下一批.
最后所有的字节数组汇总,统一解析一次.作为最终的内容,以更正中间解析过程中可能的乱码.
# 1.5 模型选择
2023.06.14
LATEST MODEL | DESCRIPTION | MAX TOKENS | TRAINING DATA |
---|---|---|---|
gpt-3.5-turbo | Most capable GPT-3.5 model and optimized for chat at 1/10th the cost of text-davinci-003. Will be updated with our latest model iteration 2 weeks after it is released. | 4,096 tokens | Up to Sep 2021 |
gpt-3.5-turbo-16k | Same capabilities as the standard gpt-3.5-turbo model but with 4 times the context. | 16,384 tokens | Up to Sep 2021 |
gpt-3.5-turbo-0613 | Snapshot of gpt-3.5-turbo from June 13th 2023 with function calling data. Unlike gpt-3.5-turbo, this model will not receive updates, and will be deprecated 3 months after a new version is released. | 4,096 tokens | Up to Sep 2021 |
gpt-3.5-turbo-16k-0613 | Snapshot of gpt-3.5-turbo-16k from June 13th 2023. Unlike gpt-3.5-turbo-16k, this model will not receive updates, and will be deprecated 3 months after a new version is released. | 16,384 tokens | Up to Sep 2021 |
text-davinci-003 | Can do any language task with better quality, longer output, and consistent instruction-following than the curie, babbage, or ada models. Also supports some additional features such as inserting text (opens new window). | 4,097 tokens | Up to Jun 2021 |
text-davinci-002 | Similar capabilities to text-davinci-003 but trained with supervised fine-tuning instead of reinforcement learning | 4,097 tokens | Up to Jun 2021 |
code-davinci-002 | Optimized for code-completion tasks | 8,001 tokens | Up to Jun 2021 |
# 免费账号的限速
rate-limits (opens new window)
# 付费账号的限速
2024.02.19
# 主要模型示例:
# token费用
1,000 tokens is about 750 words
With broad general knowledge and domain expertise, GPT-4 can follow complex instructions in natural language and solve difficult problems with accuracy.
Model | Input | Output |
---|---|---|
8K context | $0.03 / 1K tokens | $0.06 / 1K tokens |
32K context | $0.06 / 1K tokens | $0.12 / 1K tokens |
ChatGPT models are optimized for dialogue. The performance of gpt-3.5-turbo is on par with Instruct Davinci.
Learn more about ChatGPT (opens new window)
Model | Input | Output |
---|---|---|
4K context | $0.0015 / 1K tokens | $0.002 / 1K tokens |
16K context | $0.003 / 1K tokens | $0.004 / 1K tokens |
6月之前的数据:
# 2024价格
# gpt4-turbo
一张1024的图大概750token, 和1k的字符差不多
# gpt4
# gpt3.5 turbo
从价格来看,使用最新的模型反而最便宜.还支持json输出
这两个模型的区别:
gpt-3.5-turbo-0125是该家族的旗舰型号,支持16K上下文窗口,并针对对话进行了优化. 最便宜
gpt-3.5-turbo-instruct:
Instruct是OpenAI推出的一种针对指令性任务的语言模型。相较于通用的对话模型,Instruct模型专注于处理针对特定指令的场景,例如编程、问题解答、文本生成等任务。
注意: instruct不能用于chat:
This is not a chat model and thus not supported in the v1/chat/completions endpoint. Did you mean to use v1/completions?
说明如下:
旧一点的模型:
# image
DALL·E 3 1024分辨率的,一张图2.5毛, 略贵
# audio
# 使用策略:
(针对免费账号)
2200字以下的上下文,使用gpt-3.5-turbo-0613
超过后,使用gpt-3.5-turbo-16k-0613
避开3RPM的限速,达到60RPM.且费用低一些.
另外,限制对话轮数,太多时引导开启新一轮对话.(todo)
String model = "gpt-3.5-turbo-0613";//免费账号限额每分钟60个请求
if(strLength >= 2200){
model = "gpt-3.5-turbo-16k-0613";
}
2
3
4
5
# 2 跨越openai对中国的封锁
用海外的一台主机作为代理服务器,代我们去请求 api.openai.com
# 2.1 nginx配置
(nginx本身就支持流式响应)
不需要额外配置跨域,api.openai.com返回的头部本身就已经支持了跨域请求
server {
listen 443 ssl;
server_name xxx.yyy.top;
location / {
proxy_pass https://api.openai.com; # 转发规则
proxy_set_header Host 'api.openai.com'; # 修改转发请求头,让8080端口的应用可以受到真实的请求
proxy_set_header Authorization 'Bearer apikey........';
proxy_set_header referrer '';
proxy_read_timeout 1m;
proxy_ssl_server_name on; # 关键配置1
proxy_ssl_name api.openai.com; # 关键配置2 用于nginx和openai服务器握手识别hostname
proxy_ssl_protocols SSLv3 TLSv1.1 TLSv1.2 TLSv1.3;
proxy_ssl_verify off;
}
# add_header 'Access-Control-Allow-Headers' '*';
# add_header 'Access-Control-Allow-Origin' '*';
# add_header 'Access-Control-Allow-Credentials' true;
# add_header 'Access-Control-Allow-Methods' 'GET,POST,PUT,OPTIONS';
# add_header 'Access-Control-Max-Age' 17280;
# add_header 'Access-Control-Expose-Headers' '*' ;
# 跨域配置
# if ($request_method = OPTIONS ) { return 200; }
# gzip配置
gzip on;
gzip_buffers 32 4K;
gzip_comp_level 6;
gzip_min_length 100;
gzip_types application/json application/javascript text/css text/xml;
gzip_disable "MSIE [1-6]\."; #配置禁用gzip条件,支持正则。此处表示ie6及以下不启用gzip(因为ie低版本不支持)
gzip_vary on;
# ssl配置
ssl_certificate /etc/nginx/ssl/acme_cert.pem;
ssl_certificate_key /etc/nginx/ssl/acme_key.pem;
ssl_stapling on;
ssl_stapling_verify on;
ssl_reject_handshake off;
ssl_protocols TLSv1.2;
resolver 8.8.8.8 8.8.4.4 1.1.1.1 valid=60s;
resolver_timeout 2s;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
使用https://xxx.yyy.top加上对应的path即可访问openai的api了.
# 2.2 域名
使用 https://www.namesilo.com/ $1.88/年. dns可以使用cloudflare,或者使用namesilo自带的.
# 2.3 https证书
自动续期使用achem.sh
# 使用cloudflare 解析ip并开启proxy时的配置
要十分注意
(todo)
# 3 项目功能和架构设计
# 3.1 功能
抄官网的交互: 聊天界面+ 聊天列表.
需要用户管理和权限控制.
# 3.2 架构
# 后台项目脚手架:
https://github.com/YunaiV/ruoyi-vue-pro 前后端分离,单体应用. 不要搞分布式.
此项目自带console管理端
# flutter端脚手架
navigation项目+ 现有的一套登录/修改密码的ui
打包命令脚本化,将瘦身,打包等相关操作变化变成dart脚本,在项目内一键运行即可打包.
flutter web编译瘦身 (opens new window)
# 部署
后台打jar包部署到阿里云ecs上
flutter web打包部署到阿里云ecs上
Android/Mac/windows app打包传到七牛oss上.
app内置一键更新功能.
# 4 服务端
chatgpt生成sql建表语句+ 脚手架根据表结构生成模板代码
# 5 flutter端
# 5.1 聊天列表的实现
api使用后台的分页接口
界面直接使用封装好的下拉刷新上拉加载更多的组件:
LoadMoreListComponent(
url: ChatHttpUrl.getChatListByPager,
requestParam: {},
tag: "chatlist",
dataLoader: ChatListLoader(),
showEmptyReloadButton: true,
itemBuilder: (BuildContext context, int index, itemInfo) {
// info: RefStore.fromMap(itemInfo),
ChatListItem title = ChatListItem.fromJson(itemInfo);
//title.serverId = i
return QuickPopUpMenu(
menuItems: logic.itemLongPressMenus,
dataObj: itemInfo,
pressType: PressType.longPress,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: (){
if(forSideBar??false){
onItemClicked?.call(title);
}else{
logic.goChatDetail(title);
}
},
//onLongPress: logic.onLongPress(title),
child: item(title).marginSymmetric(horizontal: 16),
),
)
;
},
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class ChatListLoader extends IDataLoader{
void load({bool? isFirstIn, bool? fromRefresh,
bool? isLoadMore, int? pageIndex, int? pageSize,
String? url, Map<String, dynamic>? param,
Function(List list, bool hasMoreData)? success,
Function(String code, String msg)? fail, LoadMoreConfig? config}) {
var pageParam = {
config!.pageSizeKey: pageSize,
config.pageIndexKey: pageIndex
};
param!.addAll(pageParam);
HttpApi.get(url!, param,
success: (data){
LogApi.i(data.toString());
List list = data[config.listDataResponseKey];
bool hasMoreData = config.hasMoreData(data);
success?.call(list,hasMoreData);
},fail: fail);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 汉字在windows上一会粗一会儿细的问题:
不设置字体,默认robot,这种字体在windows上可能没有
使用谷歌的免费字体nonosans? 不行,太大,8M多
使用这个库: 系统有什么汉字字体就用什么字体.
chinese_font_library (opens new window)
chinese_font_library: ^1.0.1
return MaterialApp(
...
theme: Theme(
data: yourCustomThemeData.useSystemChineseFont(),
),
...
)
2
3
4
5
6
7
# 列表和聊天详情的响应式布局:
横屏时,侧边栏显示聊天列表
竖屏时,不显示聊天列表
实现:
测量宽高数据
高>宽时,不显示聊天列表
宽>高时,聊天列表和聊天界面按特定比例显示:
Widget fillWithChatListIfHorizontal(BuildContext context,Widget child) {//child是聊天界面
if(!landscape(context) ){
return child;
}
if(fromList){
return child.marginSymmetric(horizontal: 100);
}
return Row(
children: [
Flexible(child: ChatListPage(forSideBar: true,onItemClicked: (title){
state.chatTitleIdForOnlyShow = title.id;
state.title = title.title;
logic.loadChatList();
},).decorated(
color: const Color(0xFF4983FF)
),flex: 1,),
Flexible(child: child.marginSymmetric(horizontal: 50),flex: 4,),
],
);
}
bool landscape(BuildContext context){
var orientation = ScreenUtil.getOrientation(context);
//LogApi.i("屏幕方向0: $orientation");
MediaQueryData mediaQuery = MediaQuery.of(context);
var size = mediaQuery.size;
if(size.width > size.height){
orientation = Orientation.landscape;
}else{
orientation = Orientation.portrait;
}
//LogApi.i("屏幕方向: $orientation");
if(orientation == Orientation.portrait){
return false;
}
return true;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 长按删除的功能
把原来库里的demo的样式直接放到库中,方便使用:
https://pub.dev/packages/custom_pop_up_menu_fork
custom_pop_up_menu_fork: ^2.0.0
QuickPopUpMenu(
menuItems: logic.itemLongPressMenus,
dataObj: itemInfo,
pressType: PressType.longPress,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: (){
if(forSideBar??false){
onItemClicked?.call(title);
}else{
logic.goChatDetail(title);
}
},
//onLongPress: logic.onLongPress(title),
child: item(title).marginSymmetric(horizontal: 16),
),
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 5.2 聊天界面
# 5.2.1 设置文本背景长度自适应+自动换行
row里使用text,一般推荐包一层expand. 但expand会强制让child的宽度撑满,背景会撑满. 此处应该用Flexible.
const Flexible({
super.key,
this.flex = 1,
this.fit = FlexFit.loose,
required super.child,
});
class Expanded extends Flexible {
/// Creates a widget that expands a child of a [Row], [Column], or [Flex]
/// so that the child fills the available space along the flex widget's
/// main axis.
const Expanded({
super.key,
super.flex,
required super.child,
}) : super(fit: FlexFit.tight);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
实际代码:
Widget userRow() {
return Row(
//textDirection: TextDirection.rtl,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//Spacer(),
Image.asset(
AssetsImagesPkgChatgpt.person,
height: 30.0,
width: 30.0,
package: AssetsImagesPkgChatgpt.pkgName,
),
const SizedBox(
width: 10.0,
),
Flexible(
//使用flexible而不要用expand. expand会强制把child宽度撑大,而Flexible的约束是loose, 规定最大
child: TextWidget.asMarkDown(widget.msg.content??"")
? TextWidget.markDownWidget2(widget.msg.content??"") : ExtendedText(
widget.msg.content??"",
// textAlign: TextAlign.right,
softWrap: true,
selectionEnabled: true,
//selectionControls:MaterialExtendedTextSelectionControls(),
style: const TextStyle(
color: Colors.black,
fontSize: 16,
fontWeight: FontWeight.normal,
),
)
.paddingAll(8)
.decorated(
borderRadius: const BorderRadius.all(Radius.circular(8)),
color: Colors.lightBlueAccent)
.marginOnly(right: 16),
),
//Spacer(),
//.decorated(borderRadius: BorderRadius.all(Radius.circular(8)),color: Colors.lightBlue),
],
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 5.2.2 markdown文本显示
先判断是不是markdown,是才用markdown显示
(不要用flutter_markdown这个库,star很多,但效果极差)
markdown_widget: ^2.1.0
TextWidget.asMarkDown(widget.msg.content??"")
? TextWidget.markDownWidget2(widget.msg.content??"")
: ExtendedText(widget.msg.content??"",)
2
3
static bool asMarkDown(String label) {
if (label.contains("\n* ")) {
return true;
}
if (label.contains("\n+ ")) {
return true;
}
if (label.contains("\n- ")) {
return true;
}
label = label.replaceAll(" ", "");
if (label.contains("\n#")) {
return true;
}
if (label.contains("\n```")) {
return true;
}
/// 链接和图片
if (label.contains("](")) {
LogApi.d("is image of markdown $label");
return true;
}
/// 表格格式: https://www.runoob.com/markdown/md-table.html
/// | 左对齐 | 右对齐 | 居中对齐 |
/// | :-----| ----: | :----: |
if (label.contains("|--") || label.contains("|:--")) {
return true;
}
return false;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
markdownWidget:
static Widget markDownWidget2(String label2) {
final codeWrapper =
(child, text) => CodeWrapperWidget(child: child, text: text);
return md2.MarkdownWidget(data: label2,
shrinkWrap: true,
selectable: true,
physics: const NeverScrollableScrollPhysics(),
config: MarkdownConfig.defaultConfig.copy(configs: [
PreConfig().copy(wrapper: codeWrapper)
]),
);
}
2
3
4
5
6
7
8
9
10
11
12
其中,代码拷贝的按钮CodeWrapperWidget要自己实现:
从库的demo里拷贝代码即可:
class CodeWrapperWidget extends StatefulWidget {
final Widget child;
final String text;
const CodeWrapperWidget({Key? key, required this.child, required this.text})
: super(key: key);
State<CodeWrapperWidget> createState() => _PreWrapperState();
}
class _PreWrapperState extends State<CodeWrapperWidget> {
late Widget _switchWidget;
bool hasCopied = false;
void initState() {
super.initState();
_switchWidget = Icon(Icons.copy_rounded, key: UniqueKey());
}
Widget build(BuildContext context) {
return Stack(
children: [
widget.child,
Align(
alignment: Alignment.topRight,
child: Container(
padding: const EdgeInsets.all(16.0),
child: InkWell(
child: AnimatedSwitcher(
child: _switchWidget,
duration: Duration(milliseconds: 200),
),
onTap: () async {
if (hasCopied) return;
await Clipboard.setData(ClipboardData(text: widget.text));
_switchWidget = Icon(Icons.check, key: UniqueKey());
refresh();
Future.delayed(Duration(seconds: 2), () {
hasCopied = false;
_switchWidget = Icon(Icons.copy_rounded, key: UniqueKey());
refresh();
});
},
),
),
)
],
);
}
void refresh() {
if (mounted) setState(() {});
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# 5.2.3 loading状态
做到单条widget上,不要做到整个页面上
模仿讯飞星火的交互
# 5.2.4 利用chatgpt生成标题和描述
给予准确的描述prompt, chatgpt可以当做一个接口来使用
ChatMsg msg0 = ChatMsg();
msg0.role = ChatPageState.roleUser;
msg0.content = "please summery those messages above as a chat title in 10 words at most, "
"and summery description in max 30 words, both in Chinese language. show the result as Title: xxxx\n Desc: xxxx ";
2
3
4
# 5.2.5 纯本地数据库管理
flutter数据库选型 (opens new window)
# 5.2.6 tts功能实现
flutter_tts (opens new window)
调用系统原生的tts引擎实现文字转语音.
效果尚可,不如科大讯飞的讯飞星火衔接流畅,但重在免费.
如果接第三方tts引擎,费用很贵:
flutter语音转文字和文字转语音 (opens new window)
flutter_tts: ^3.6.3
参考官方demo,将tts功能包装成一个TtsWidget,供聊天item使用.
注意需要包含切换语言的功能
在Android上需要注册queries,否则找不到系统的tts引擎服务,表现为没有声音
<queries>
<intent>
<action android:name="android.intent.action.TTS_SERVICE" />
</intent>
</queries>
2
3
4
5
# 5.2.7 分享功能实现
分享落地页为一个web页面.
# 6 各端适配和打包
# 6.1 web端
flutter客户端项目适配web做的一些工作 (opens new window)
pullToRefresh在pc和web上的兼容问题 (opens new window): Lottie不兼容web的html渲染,需改用其他动画实现
flutter web编译瘦身 (opens new window): 缩减和替换字体icon文件
一键打包脚本
Future<void> packWeb() async {
String fontPath = "${Directory.current.path}/build/app/intermediates/assets/release/mergeReleaseAssets/flutter_assets/fonts/MaterialIcons-Regular.otf";
File file = File(fontPath);
if(!file.existsSync()){
print("android 打包treeshake的字体文件不存在,重新打包Android");
await buildAndroid();
print("android 打包成功,开始继续打包web");
packWeb();
}else{
print("android 打包treeshake的字体文件存在,直接打包web,打包后拷贝字体");
await buildWeb();
//web/assets/fonts/MaterialIcons-Regular.otf
File target = File("${Directory.current.path}/build/web/assets/fonts/MaterialIcons-Regular.otf");
file.copySync(target.path);
print("拷贝字体文件成功: ${target.path}");
//todo windows上,将web文件夹打包成zip,然后部署到服务器上
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 6.2 Android端
官方插件webview_flutter (opens new window)在Android上不支持input标签以及权限申请的处理: aop切入对应回调,实现其方法
具体blog链接: webview_flutter官方插件的增强-对inputfile和权限请求的支持 (opens new window)
# 6.3 macos端
一键打包脚本: 打包成dmg文件
Future<void> buildMacReal() async {
await buildMacOs();
packMacos().catchError((){
packMacos().catchError((){
packMacos().catchError((){
packMacos();
});
});
});
}
Future<void> buildMacOs(){
return exeShell("flutter build macos --release --tree-shake-icons");
}
Future<void> packMacos(){
return exeShell("hdiutil create -volname chatgpt -srcfolder ${Directory.current.path}/build/macos/Build/Products/Release/MyChatAI.app"
" -ov -format UDZO ${getPCUserPath()}/Downloads/MyChatAI-release.dmg");
}
String getPCUserPath() {
//var platform = Platform.isWindows ? 'win32' : 'linux';
var homeDir = Platform.environment['HOME'];
//var userDir = Platform.environment['USERPROFILE'] ?? '$homeDir\\AppData\\Local';
// return Directory('$userDir\\$platform\\flutter').path; // 此处以 Flutter 为例
return homeDir??"/";
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 7 访问加速
//HostType.getApiHost()+ ChatHttpUrl.chatCompletionsStream, 阿里云代理:cost: 18789 ms 18654ms
//https://tttt.xxx.top cf代理: 11107 ms,8948 ms,10968 ms
//http://tttt.xxxx.top 无cf代理: 4085 ms 3427 ms 5931 ms
// ip直连+指定host: 1.5s-3s. web不支持指定host
2
3
4