Browse Source

[fix]优化聊天回复卡顿的问题

zk 1 year ago
parent
commit
27d2b933dd

+ 14 - 2
lib/data/bean/progressing_chat_item.dart

@@ -1,8 +1,12 @@
+import 'dart:async';
+
 import 'package:electronic_assistant/data/bean/chat_item.dart';
 import 'package:electronic_assistant/data/bean/chat_item.dart';
 import 'package:get/get.dart';
 import 'package:get/get.dart';
 
 
+import '../../widget/gradually_md_text.dart';
+
 class ProgressingChatItem extends ChatItem {
 class ProgressingChatItem extends ChatItem {
-  final RxString streamContent = "".obs;
+  final GraduallyController graduallyController;
 
 
   final RxBool isFinished = false.obs;
   final RxBool isFinished = false.obs;
 
 
@@ -10,14 +14,22 @@ class ProgressingChatItem extends ChatItem {
 
 
   final RxString error = "".obs;
   final RxString error = "".obs;
 
 
+  final RxBool isGradually = false.obs;
+
   ProgressingChatItem(
   ProgressingChatItem(
       {required super.id,
       {required super.id,
+      required this.graduallyController,
       required super.conversationId,
       required super.conversationId,
       required super.role,
       required super.role,
       required super.content,
       required super.content,
       required super.createTime});
       required super.createTime});
 
 
+  void setAppendDone() {
+    graduallyController.appendDone();
+  }
+
   void append(String content) {
   void append(String content) {
-    streamContent.value += content;
+    graduallyController.append(content);
+    isGradually.value = true;
   }
   }
 }
 }

+ 16 - 2
lib/module/chat/controller.dart

@@ -1,3 +1,4 @@
+import 'dart:async';
 import 'dart:convert';
 import 'dart:convert';
 
 
 import 'package:electronic_assistant/base/base_controller.dart';
 import 'package:electronic_assistant/base/base_controller.dart';
@@ -25,6 +26,7 @@ import '../../data/consts/error_code.dart';
 import '../../dialog/model_explain_dialog.dart';
 import '../../dialog/model_explain_dialog.dart';
 import '../../router/app_pages.dart';
 import '../../router/app_pages.dart';
 import '../../utils/http_handler.dart';
 import '../../utils/http_handler.dart';
+import '../../widget/gradually_md_text.dart';
 
 
 class ChatController extends BaseController {
 class ChatController extends BaseController {
   final RefreshController refreshController =
   final RefreshController refreshController =
@@ -42,6 +44,8 @@ class ChatController extends BaseController {
 
 
   bool isConsumeElectric = false;
   bool isConsumeElectric = false;
 
 
+  final Set<StreamSubscription> _streamChatSubscriptions = {};
+
   @override
   @override
   void onInit() {
   void onInit() {
     super.onInit();
     super.onInit();
@@ -175,6 +179,7 @@ class ChatController extends BaseController {
 
 
     ProgressingChatItem progressingChatItem = ProgressingChatItem(
     ProgressingChatItem progressingChatItem = ProgressingChatItem(
       id: const Uuid().v4(),
       id: const Uuid().v4(),
+      graduallyController: GraduallyController(),
       conversationId: chatItems.last.conversationId,
       conversationId: chatItems.last.conversationId,
       role: "assistant",
       role: "assistant",
       content: "",
       content: "",
@@ -196,7 +201,8 @@ class ChatController extends BaseController {
         .streamChat(chatContent,
         .streamChat(chatContent,
             talkId: talkInfo.value?.id, agendaId: agenda.value?.id)
             talkId: talkInfo.value?.id, agendaId: agenda.value?.id)
         .then((stream) {
         .then((stream) {
-      stream.listen((event) {
+      StreamSubscription? sub;
+      sub = stream.listen((event) {
         try {
         try {
           Map<String, dynamic> json = jsonDecode(event.data);
           Map<String, dynamic> json = jsonDecode(event.data);
           if (json.isEmpty) {
           if (json.isEmpty) {
@@ -215,14 +221,19 @@ class ChatController extends BaseController {
           chatAiTagId.value = progressingChatItem.id;
           chatAiTagId.value = progressingChatItem.id;
         } catch (ignore) {}
         } catch (ignore) {}
       }, onDone: () {
       }, onDone: () {
-        progressingChatItem.content = progressingChatItem.streamContent.value;
+        _streamChatSubscriptions.remove(sub);
+        progressingChatItem.setAppendDone();
+        progressingChatItem.content =
+            progressingChatItem.graduallyController.graduallyTxt;
         progressingChatItem.isFinished.value = true;
         progressingChatItem.isFinished.value = true;
       }, onError: (error) {
       }, onError: (error) {
+        _streamChatSubscriptions.remove(sub);
         progressingChatItem.isFailed.value = true;
         progressingChatItem.isFailed.value = true;
         progressingChatItem.error.value = "网络错误,请检查网络连接";
         progressingChatItem.error.value = "网络错误,请检查网络连接";
         debugPrint("error: $error");
         debugPrint("error: $error");
         debugPrintStack();
         debugPrintStack();
       });
       });
+      _streamChatSubscriptions.add(sub);
     }).catchError((error) {
     }).catchError((error) {
       progressingChatItem.isFailed.value = true;
       progressingChatItem.isFailed.value = true;
       if (error is ServerErrorException) {
       if (error is ServerErrorException) {
@@ -311,5 +322,8 @@ class ChatController extends BaseController {
     if (isConsumeElectric) {
     if (isConsumeElectric) {
       accountRepository.refreshUserInfo();
       accountRepository.refreshUserInfo();
     }
     }
+    for (var sub in _streamChatSubscriptions) {
+      sub.cancel();
+    }
   }
   }
 }
 }

+ 11 - 78
lib/module/chat/view.dart

@@ -3,21 +3,18 @@ import 'package:electronic_assistant/data/bean/agenda.dart';
 import 'package:electronic_assistant/data/bean/chat_item.dart';
 import 'package:electronic_assistant/data/bean/chat_item.dart';
 import 'package:electronic_assistant/data/bean/file_chat_item.dart';
 import 'package:electronic_assistant/data/bean/file_chat_item.dart';
 import 'package:electronic_assistant/data/bean/reference_chat_item.dart';
 import 'package:electronic_assistant/data/bean/reference_chat_item.dart';
-import 'package:electronic_assistant/module/browser/view.dart';
 import 'package:electronic_assistant/module/chat/controller.dart';
 import 'package:electronic_assistant/module/chat/controller.dart';
 import 'package:electronic_assistant/resource/colors.gen.dart';
 import 'package:electronic_assistant/resource/colors.gen.dart';
 import 'package:electronic_assistant/resource/string.gen.dart';
 import 'package:electronic_assistant/resource/string.gen.dart';
 import 'package:electronic_assistant/utils/expand.dart';
 import 'package:electronic_assistant/utils/expand.dart';
+import 'package:electronic_assistant/widget/gradually_md_text.dart';
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
-import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
 import 'package:get/get.dart';
 import 'package:get/get.dart';
 import 'package:lottie/lottie.dart';
 import 'package:lottie/lottie.dart';
-import 'package:markdown/markdown.dart' as md;
 import 'package:pull_to_refresh/pull_to_refresh.dart';
 import 'package:pull_to_refresh/pull_to_refresh.dart';
-
 import '../../data/bean/progressing_chat_item.dart';
 import '../../data/bean/progressing_chat_item.dart';
 import '../../data/bean/talks.dart';
 import '../../data/bean/talks.dart';
 import '../../resource/assets.gen.dart';
 import '../../resource/assets.gen.dart';
@@ -255,26 +252,27 @@ class ChatPage extends BasePage<ChatController> {
       child: IntrinsicWidth(
       child: IntrinsicWidth(
         child: progressingChatItem == null
         child: progressingChatItem == null
             ? _buildAssistantChatItemContent(
             ? _buildAssistantChatItemContent(
-                null, chatItem.content, chatItem.id)
+                null, null, chatItem.content, chatItem.id)
             : Obx(() {
             : Obx(() {
                 bool? isStreamStarted = progressingChatItem == null
                 bool? isStreamStarted = progressingChatItem == null
                     ? null
                     ? null
-                    : progressingChatItem.streamContent.isNotEmpty ||
+                    : progressingChatItem.isGradually.value ||
                         progressingChatItem.isFinished.value ||
                         progressingChatItem.isFinished.value ||
                         progressingChatItem.isFailed.value;
                         progressingChatItem.isFailed.value;
                 return _buildAssistantChatItemContent(
                 return _buildAssistantChatItemContent(
                     isStreamStarted,
                     isStreamStarted,
+                    progressingChatItem?.graduallyController,
                     progressingChatItem!.isFailed.value
                     progressingChatItem!.isFailed.value
                         ? progressingChatItem.error.value
                         ? progressingChatItem.error.value
-                        : progressingChatItem.streamContent.value,
+                        : chatItem.content,
                     chatItem.id);
                     chatItem.id);
               }),
               }),
       ),
       ),
     );
     );
   }
   }
 
 
-  Widget _buildAssistantChatItemContent(
-      bool? isStreamStarted, String content, String id) {
+  Widget _buildAssistantChatItemContent(bool? isStreamStarted,
+      GraduallyController? graduallyController, String? content, String id) {
     return Column(
     return Column(
       crossAxisAlignment: CrossAxisAlignment.start,
       crossAxisAlignment: CrossAxisAlignment.start,
       children: [
       children: [
@@ -321,77 +319,12 @@ class ChatPage extends BasePage<ChatController> {
                 constraints: BoxConstraints(
                 constraints: BoxConstraints(
                   maxWidth: 0.78.sw,
                   maxWidth: 0.78.sw,
                 ),
                 ),
-                child: SelectionArea(
-                  child: HtmlWidget(
-                    onTapUrl: (url) {
-                      BrowserPage.start(url);
-                      return true;
-                    },
-                    md.markdownToHtml(content, inlineSyntaxes: [
-                      md.InlineHtmlSyntax(),
-                      md.StrikethroughSyntax(),
-                      md.EmojiSyntax(),
-                      md.ColorSwatchSyntax(),
-                      md.AutolinkExtensionSyntax(),
-                      md.ImageSyntax()
-                    ], blockSyntaxes: [
-                      const md.FencedCodeBlockSyntax(),
-                      const md.HeaderWithIdSyntax(),
-                      const md.SetextHeaderWithIdSyntax(),
-                      const md.UnorderedListWithCheckboxSyntax(),
-                      const md.OrderedListWithCheckboxSyntax(),
-                      const md.FootnoteDefSyntax(),
-                      const md.AlertBlockSyntax(),
-                    ]),
+                child: GraduallyMdText(
+                    initTxt: content,
+                    graduallyController: graduallyController,
                     textStyle: TextStyle(
                     textStyle: TextStyle(
-                        fontSize: 14.w, color: ColorName.primaryTextColor),
-                  ),
-                ),
+                        fontSize: 14.w, color: ColorName.primaryTextColor)),
               ),
               ),
-        // Container(
-        //     padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
-        //     alignment: Alignment.centerLeft,
-        //     constraints: BoxConstraints(
-        //       maxWidth: 0.78.sw,
-        //     ),
-        //     decoration: BoxDecoration(
-        //         border: isStreamStarted == null || isStreamStarted == true
-        //             ? null
-        //             : Border.all(color: ColorName.colorPrimary, width: 1.w),
-        //         color: ColorName.white,
-        //         borderRadius: BorderRadius.only(
-        //             topRight: Radius.circular(20.w),
-        //             bottomRight: Radius.circular(20.w),
-        //             bottomLeft: Radius.circular(20.w))),
-        //     child: isStreamStarted != null && isStreamStarted == false
-        //         ? Lottie.asset("assets/anim/anim_chat_response_loading.zip",
-        //             width: 46.w, height: 20.w)
-        //         : SelectionArea(
-        //             child: HtmlWidget(
-        //               onTapUrl: (url) {
-        //                 BrowserPage.start(url);
-        //                 return true;
-        //               },
-        //               md.markdownToHtml(content, inlineSyntaxes: [
-        //                 md.InlineHtmlSyntax(),
-        //                 md.StrikethroughSyntax(),
-        //                 md.EmojiSyntax(),
-        //                 md.ColorSwatchSyntax(),
-        //                 md.AutolinkExtensionSyntax(),
-        //                 md.ImageSyntax()
-        //               ], blockSyntaxes: [
-        //                 const md.FencedCodeBlockSyntax(),
-        //                 const md.HeaderWithIdSyntax(),
-        //                 const md.SetextHeaderWithIdSyntax(),
-        //                 const md.UnorderedListWithCheckboxSyntax(),
-        //                 const md.OrderedListWithCheckboxSyntax(),
-        //                 const md.FootnoteDefSyntax(),
-        //                 const md.AlertBlockSyntax(),
-        //               ]),
-        //               textStyle: TextStyle(
-        //                   fontSize: 14.w, color: ColorName.primaryTextColor),
-        //             ),
-        //           )),
         Obx(() {
         Obx(() {
           return Visibility(
           return Visibility(
             visible: id == controller.chatAiTagId.value,
             visible: id == controller.chatAiTagId.value,

+ 0 - 1
lib/popup/agenda_time_popup.dart

@@ -1,4 +1,3 @@
-import 'package:electronic_assistant/dialog/add_agenda_dialog.dart';
 import 'package:electronic_assistant/popup/template_utils.dart';
 import 'package:electronic_assistant/popup/template_utils.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/widgets.dart';
 import 'package:flutter/widgets.dart';

+ 129 - 0
lib/widget/gradually_md_text.dart

@@ -0,0 +1,129 @@
+import 'dart:async';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
+import 'package:markdown/markdown.dart' as md;
+import '../module/browser/view.dart';
+
+class GraduallyMdText extends StatefulWidget {
+  const GraduallyMdText({
+    super.key,
+    this.initTxt,
+    this.graduallyController,
+    this.textStyle,
+  });
+
+  final TextStyle? textStyle;
+  final GraduallyController? graduallyController;
+  final String? initTxt;
+
+  @override
+  State<GraduallyMdText> createState() => _GraduallyMdTextState();
+}
+
+typedef GraduallyTxtListener = void Function(String txt);
+
+class GraduallyController {
+  String graduallyTxt = '';
+  int progressIndex = 0;
+  Timer? appendTimer;
+  bool? isAppendDone;
+  bool? isShowDone;
+
+  GraduallyTxtListener? listener;
+
+  setGraduallyTxtListener(GraduallyTxtListener listener) {
+    this.listener = listener;
+  }
+
+  append(String txt) {
+    graduallyTxt += txt;
+    _startAppend();
+  }
+
+  void _initAppend() {
+    progressIndex = 0;
+    isAppendDone = null;
+    isShowDone = null;
+    appendTimer = null;
+  }
+
+  appendDone() {
+    isAppendDone = true;
+    // debugPrint('GraduallyMdText: appendDone');
+  }
+
+  _startAppend() {
+    if (appendTimer != null) {
+      return;
+    }
+    _initAppend();
+    appendTimer = Timer.periodic(const Duration(milliseconds: 50), (timer) {
+      // debugPrint('GraduallyMdText-progressIndex: $progressIndex');
+      if (progressIndex < graduallyTxt.length) {
+        progressIndex += 1;
+        listener?.call(graduallyTxt.substring(0, progressIndex));
+      } else if (isAppendDone == true) {
+        dispose();
+      }
+    });
+  }
+
+  dispose() {
+    appendTimer?.cancel();
+    isAppendDone = null;
+    isShowDone = true;
+    appendTimer = null;
+  }
+}
+
+class _GraduallyMdTextState extends State<GraduallyMdText> {
+  String mdTxt = '';
+
+  @override
+  void initState() {
+    super.initState();
+    mdTxt = widget.initTxt ?? '';
+    widget.graduallyController?.setGraduallyTxtListener((txt) {
+      setState(() {
+        mdTxt = txt;
+      });
+    });
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    // debugPrint('GraduallyMdText-dispose');
+    widget.graduallyController?.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return SelectionArea(
+      child: HtmlWidget(
+        onTapUrl: (url) {
+          BrowserPage.start(url);
+          return true;
+        },
+        md.markdownToHtml(mdTxt, inlineSyntaxes: [
+          md.InlineHtmlSyntax(),
+          md.StrikethroughSyntax(),
+          md.EmojiSyntax(),
+          md.ColorSwatchSyntax(),
+          md.AutolinkExtensionSyntax(),
+          md.ImageSyntax()
+        ], blockSyntaxes: [
+          const md.FencedCodeBlockSyntax(),
+          const md.HeaderWithIdSyntax(),
+          const md.SetextHeaderWithIdSyntax(),
+          const md.UnorderedListWithCheckboxSyntax(),
+          const md.OrderedListWithCheckboxSyntax(),
+          const md.FootnoteDefSyntax(),
+          const md.AlertBlockSyntax(),
+        ]),
+        textStyle: widget.textStyle,
+      ),
+    );
+  }
+}