技术经验谈 技术经验谈
首页
  • 最佳实践

    • 抓包
    • 数据库操作
  • ui

    • 《JavaScript教程》
    • 《JavaScript高级程序设计》
    • 《ES6 教程》
    • 《Vue》
    • 《React》
    • 《TypeScript 从零实现 axios》
    • 《Git》
    • TypeScript
    • JS设计模式总结
  • 总纲
  • 整体开发框架
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

hss01248

一号线程序员
首页
  • 最佳实践

    • 抓包
    • 数据库操作
  • ui

    • 《JavaScript教程》
    • 《JavaScript高级程序设计》
    • 《ES6 教程》
    • 《Vue》
    • 《React》
    • 《TypeScript 从零实现 axios》
    • 《Git》
    • TypeScript
    • JS设计模式总结
  • 总纲
  • 整体开发框架
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 日志体系

  • springboot

  • ruoyi-vue-pro

    • ruoyi-vue-pro-oauth2支持不同客户端同时登录
    • 项目运行备忘
    • flutter项目

      • pullToRefresh在pc和web上的兼容问题
      • flutter字体问题汇总
      • flutter语音转文字和文字转语音
      • OCR识别技术选型
      • 语音转写-录音转文字各平台价格对比
      • dio拦截器实现OAuth2体系下登录态的维持
      • chatgpt flutter客户端项目实践
        • 1.1 基本使用
          • 请求
          • 响应:
        • 1.2 如何具备上下文关联的功能
        • 1.3 json格式返回的支持
        • 1.4 stream模式
        • 1.5 模型选择
          • 免费账号的限速
          • 付费账号的限速
          • 主要模型示例:
          • token费用
          • 2024价格
          • gpt4-turbo
          • gpt4
          • gpt3.5 turbo
          • image
          • audio
          • 使用策略:
        • 2.1 nginx配置
        • 2.2 域名
        • 2.3 https证书
          • 使用cloudflare 解析ip并开启proxy时的配置
        • 3.1 功能
        • 3.2 架构
          • 后台项目脚手架:
          • flutter端脚手架
          • 部署
        • 5.1 聊天列表的实现
          • 汉字在windows上一会粗一会儿细的问题:
          • 列表和聊天详情的响应式布局:
          • 长按删除的功能
        • 5.2 聊天界面
          • 5.2.1 设置文本背景长度自适应+自动换行
          • 5.2.2 markdown文本显示
          • 5.2.3 loading状态
          • 5.2.4 利用chatgpt生成标题和描述
          • 5.2.5 纯本地数据库管理
          • 5.2.6 tts功能实现
          • 5.2.7 分享功能实现
        • 6.1 web端
        • 6.2 Android端
        • 6.3 macos端
      • chatgpt图片识别描述功能
    • spring boot内实现流式代理
  • IT工具链
  • java学习路线和视频链接--尚硅谷
  • JDK动态代理原理和应用
  • jvm一图流
  • linux运维
  • spring boot笔记
  • spring-cloud学习资料和路线
  • springcloud alibaba
  • Springcloud学习笔记
  • 从java编译原理到Android aop
  • 大数据
  • 操作系统原理一图流
  • 汇编语言一图流
  • 泛型
  • 网关
  • 面试题精讲
  • java
  • ruoyi-vue-pro
  • flutter项目
hss01248
2023-06-28
目录

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!"}]
  }'

1
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
  }
}

1
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?"}
    ]
)
1
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,
        ),
      )
1
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
1
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]
1
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 模型选择

模型列表 (opens new window)

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)

61BAC84F-5F9A-43FD-853D-ABB764656126

# 付费账号的限速

2024.02.19

image-20240219092824117

# 主要模型示例:

image-20240219093353270

# token费用

1,000 tokens is about 750 words

pricing (opens new window)

With broad general knowledge and domain expertise, GPT-4 can follow complex instructions in natural language and solve difficult problems with accuracy.

Learn more (opens new window)

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月之前的数据:

EC78EBE6-1F08-4D34-9CB7-35720102AB69

# 2024价格

# gpt4-turbo

一张1024的图大概750token, 和1k的字符差不多

image-20240219101807495

# gpt4

image-20240219102925349

# gpt3.5 turbo

从价格来看,使用最新的模型反而最便宜.还支持json输出

image-20240219101945641

这两个模型的区别:

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-20240219111259602

旧一点的模型:

image-20240219102346786

# image

DALL·E 3 1024分辨率的,一张图2.5毛, 略贵

image-20240219102103716

# audio

image-20240219102257669

# 使用策略:

(针对免费账号)

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";
    }
1
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;

}
1
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),
                  ),
                )

                ;
            },
          );
1
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{
  @override
  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);
  }

}
1
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
1
return MaterialApp(
    ...
    theme: Theme(
        data: yourCustomThemeData.useSystemChineseFont(),
    ),
    ...
)
1
2
3
4
5
6
7

# 列表和聊天详情的响应式布局:

横屏时,侧边栏显示聊天列表

竖屏时,不显示聊天列表

image-20230628170025677

image-20230628170052074

实现:

测量宽高数据

高>宽时,不显示聊天列表

宽>高时,聊天列表和聊天界面按特定比例显示:

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;
  }
1
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
1

image-20230628174545099

image-20230628174641092

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),
                  ),
                )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 5.2 聊天界面

# 5.2.1 设置文本背景长度自适应+自动换行

image-20230628192252703

image-20230628192324286

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);
}
1
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),
      ],
    );
  }
1
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文本显示

效果展示 (opens new window)

先判断是不是markdown,是才用markdown显示

(不要用flutter_markdown这个库,star很多,但效果极差)

markdown_widget: ^2.1.0
1
TextWidget.asMarkDown(widget.msg.content??"")
? TextWidget.markDownWidget2(widget.msg.content??"") 
: ExtendedText(widget.msg.content??"",)
1
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;
  }
1
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)
      ]),
    );
  }
1
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);

  @override
  State<CodeWrapperWidget> createState() => _PreWrapperState();
}

class _PreWrapperState extends State<CodeWrapperWidget> {
  late Widget _switchWidget;
  bool hasCopied = false;

  @override
  void initState() {
    super.initState();
    _switchWidget = Icon(Icons.copy_rounded, key: UniqueKey());
  }

  @override
  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(() {});
  }
}
1
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 ";
1
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
1

参考官方demo,将tts功能包装成一个TtsWidget,供聊天item使用.

注意需要包含切换语言的功能

image-20230629115023550

image-20230629115639715

在Android上需要注册queries,否则找不到系统的tts引擎服务,表现为没有声音

    <queries>
        <intent>
            <action android:name="android.intent.action.TTS_SERVICE" />
        </intent>
    </queries>
1
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,然后部署到服务器上

  }

}
1
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??"/";
}
1
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
1
2
3
4
编辑 (opens new window)
上次更新: 2024/02/19, 14:21:18
dio拦截器实现OAuth2体系下登录态的维持
chatgpt图片识别描述功能

← dio拦截器实现OAuth2体系下登录态的维持 chatgpt图片识别描述功能→

最近更新
01
截图后的自动压缩工具
12-27
02
图片视频文件根据exif批量重命名
12-27
03
chatgpt图片识别描述功能
02-20
更多文章>
Theme by Vdoing | Copyright © 2020-2025 | 粤ICP备20041795号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式