import 'package:electronic_assistant/base/base_page.dart'; import 'package:electronic_assistant/data/bean/agenda.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/reference_chat_item.dart'; import 'package:electronic_assistant/module/browser/view.dart'; import 'package:electronic_assistant/module/chat/controller.dart'; import 'package:electronic_assistant/resource/colors.gen.dart'; import 'package:electronic_assistant/resource/string.gen.dart'; import 'package:electronic_assistant/utils/expand.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.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:lottie/lottie.dart'; import 'package:markdown/markdown.dart' as md; import 'package:pull_to_refresh/pull_to_refresh.dart'; import '../../data/bean/progressing_chat_item.dart'; import '../../data/bean/talks.dart'; import '../../resource/assets.gen.dart'; import '../../router/app_pages.dart'; enum ChatFromType { fromMain, fromTalkDetail, fromAnalysisBtn, fromTalkExample, fromMine, unknown } class ChatPage extends BasePage { const ChatPage({super.key}); static start(ChatFromType fromType) { Get.toNamed(RoutePath.chat, arguments: [fromType]); } static startByTalk(ChatFromType fromType, TalkBean talkInfo, {Agenda? agenda}) { Get.toNamed(RoutePath.chat, arguments: [fromType, talkInfo, agenda]); } static startByTalkId(ChatFromType fromType, String talkId, {Agenda? agenda}) { Get.toNamed(RoutePath.chat, arguments: [fromType, talkId, agenda]); } @override bool immersive() { return true; } @override Color navigationBarColor() { return "#F6F6F6".color; } @override Widget buildBody(BuildContext context) { // 第一次启动时弹出定制窗口 return Stack( children: [ _buildBackgroundGradient(), _buildTopGradient(), Scaffold( backgroundColor: Colors.transparent, appBar: AppBar( leading: IconButton( icon: SizedBox( width: 24.w, height: 24.w, child: Assets.images.iconBack.image()), onPressed: () { Get.back(); }, ), scrolledUnderElevation: 0, backgroundColor: Colors.transparent, systemOverlayStyle: SystemUiOverlayStyle.dark, centerTitle: true, title: IntrinsicWidth( child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Image( image: Assets.images.iconChatXiaoTin.provider(), width: 28.w, height: 28.w), Container( margin: EdgeInsets.only(left: 6.w), child: Text('聊天', style: TextStyle( fontSize: 16.w, fontWeight: FontWeight.bold, color: ColorName.primaryTextColor))), ], ), ), ), body: buildBodyContent(context), ) ], ); } Widget buildBodyContent(BuildContext context) { return Column( children: [ Expanded( child: Container( padding: EdgeInsets.symmetric(horizontal: 12.w), child: Obx(() { return NotificationListener( onNotification: (scrollNotification) { if (scrollNotification is ScrollStartNotification) { FocusScope.of(context).unfocus(); } return false; }, child: SmartRefresher( controller: controller.refreshController, footer: CustomFooter( loadStyle: LoadStyle.ShowWhenLoading, builder: (context, mode) { if (mode == LoadStatus.loading || mode == LoadStatus.canLoading) { return const SizedBox( height: 60.0, child: SizedBox( height: 20.0, width: 20.0, child: CupertinoActivityIndicator(), ), ); } else { return Container(); } }, ), enablePullDown: false, enablePullUp: true, onLoading: controller.loadMoreHistory, onRefresh: controller.loadMoreHistory, child: ListView.builder( reverse: true, controller: controller.listScrollController, itemBuilder: _chatItemBuilder, itemCount: controller.chatItems.length), ), ); }), )), Container( margin: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h), width: 1.sw, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12.w), boxShadow: const [ BoxShadow( color: Color(0x4CDDDEE8), blurRadius: 10, offset: Offset(0, 4), spreadRadius: 0, ) ]), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), child: Column( children: [ Obx(() { TalkBean? talkInfo = controller.talkInfo.value; if (talkInfo == null) { return Container(); } else { return _buildReferenceFile(talkInfo); } }), Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Expanded( child: Container( margin: EdgeInsets.only(right: 6.w), child: CupertinoTextField( controller: controller.inputController, padding: EdgeInsets.symmetric(vertical: 3.w), style: TextStyle( fontSize: 14.w, color: ColorName.primaryTextColor), placeholder: '有问题尽管问我~', placeholderStyle: TextStyle( fontSize: 14.w, color: const Color(0xFFAFAFAF)), textCapitalization: TextCapitalization.sentences, textInputAction: TextInputAction.newline, cursorColor: ColorName.colorPrimary, decoration: const BoxDecoration(), expands: true, maxLines: null, minLines: null, ), )), GestureDetector( onTap: () { controller.onAddFileClick(); }, child: Image( image: Assets.images.iconChatAddFile.provider(), width: 26.w, height: 26.w), ), Container( margin: EdgeInsets.only(left: 16.w), child: GestureDetector( onTap: () { controller.onSendClick(); }, child: Image( image: Assets.images.iconChatSend.provider(), width: 26.w, height: 26.w), ), ) ], ) ], ), ), ), ], ); } Widget _chatItemBuilder(BuildContext context, int index) { ChatItem chatItem = controller.chatItems[index]; if (chatItem.role == 'user') { return _buildUserChatItem(context, chatItem); } else if (chatItem.role == 'assistant') { return _buildAssistantChatItem(context, chatItem); } else { return Container(); } } Widget _buildAssistantChatItem(BuildContext context, ChatItem chatItem) { ProgressingChatItem? progressingChatItem; if (chatItem is ProgressingChatItem) { progressingChatItem = chatItem; } return Align( alignment: Alignment.centerLeft, child: IntrinsicWidth( child: progressingChatItem == null ? _buildAssistantChatItemContent( null, chatItem.content, chatItem.id) : Obx(() { bool? isStreamStarted = progressingChatItem == null ? null : progressingChatItem.streamContent.isNotEmpty || progressingChatItem.isFinished.value || progressingChatItem.isFailed.value; return _buildAssistantChatItemContent( isStreamStarted, progressingChatItem!.isFailed.value ? progressingChatItem.error.value : progressingChatItem.streamContent.value, chatItem.id); }), ), ); } Widget _buildAssistantChatItemContent( bool? isStreamStarted, String content, String id) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 10.h), isStreamStarted != null && isStreamStarted == false ? Container( padding: const EdgeInsets.all(1), decoration: BoxDecoration( color: ColorName.colorPrimary, gradient: LinearGradient( colors: ['#B57AFF'.toColor(), '#4466FF'.toColor()], stops: const [0, 1.0], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.only( topRight: Radius.circular(20.w), bottomRight: Radius.circular(20.w), bottomLeft: Radius.circular(20.w))), child: Container( decoration: BoxDecoration( color: ColorName.white, borderRadius: BorderRadius.only( topRight: Radius.circular(20.w), bottomRight: Radius.circular(20.w), bottomLeft: Radius.circular(20.w))), padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h), child: Lottie.asset( "assets/anim/anim_chat_response_loading.zip", width: 46.w, height: 20.w)), ) : Container( decoration: BoxDecoration( color: ColorName.white, border: Border.all(color: '#ECECEC'.color, width: 1.w), borderRadius: BorderRadius.only( topRight: Radius.circular(20.w), bottomRight: Radius.circular(20.w), bottomLeft: Radius.circular(20.w))), padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h), alignment: Alignment.centerLeft, constraints: BoxConstraints( 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(), ]), textStyle: TextStyle( 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(() { return Visibility( visible: id == controller.chatAiTagId.value, child: Container( margin: EdgeInsets.only(top: 6.h), padding: EdgeInsets.symmetric(horizontal: 6.w, vertical: 3.w), decoration: BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(12.w)), color: "#EFEEF1".color), child: Text( StringName.chatItemAiTag.tr, style: TextStyle( height: 1, fontSize: 10.sp, color: ColorName.tertiaryTextColor), ), ), ); }), SizedBox(height: 10.h) ], ); } Widget _buildUserChatItem(BuildContext context, ChatItem chatItem) { if (chatItem is FileChatItem) { return _buildUserFileChatItem(context, chatItem); } else if (chatItem is ReferenceChatItem) { return _buildUserNormalChatItem(context, chatItem, referenceTalkTitle: chatItem.talkInfo.title.value); } return _buildUserNormalChatItem(context, chatItem, referenceTalkTitle: chatItem.talkTitle); } Widget _buildUserNormalChatItem(BuildContext context, ChatItem chatItem, {String? referenceTalkTitle}) { return Align( alignment: Alignment.centerRight, child: Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ IntrinsicWidth( child: Container( padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h), margin: referenceTalkTitle == null ? EdgeInsets.symmetric(vertical: 10.h) : EdgeInsets.only(top: 10.h), alignment: Alignment.centerRight, constraints: BoxConstraints( maxWidth: 0.78.sw, ), decoration: BoxDecoration( color: ColorName.colorPrimary, borderRadius: BorderRadius.only( topLeft: Radius.circular(16.w), bottomRight: Radius.circular(16.w), bottomLeft: Radius.circular(16.w))), child: SelectableText(chatItem.content, style: TextStyle(fontSize: 14.w, color: ColorName.white)), ), ), if (referenceTalkTitle != null) Container( padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h), margin: EdgeInsets.only(top: 8.h, bottom: 10.h), constraints: BoxConstraints( maxWidth: 0.78.sw, ), decoration: BoxDecoration( color: "#EFEFEF".color, borderRadius: BorderRadius.all(Radius.circular(10.w))), child: Row( children: [ Image( image: Assets.images.iconReferenceChatArrow.provider(), width: 16.w, height: 16.w), Container( margin: EdgeInsets.only(right: 2.w, left: 4.w), child: Image( image: Assets.images.iconReferenceChatFile.provider(), width: 16.w, height: 16.w), ), Text(referenceTalkTitle, style: TextStyle( fontSize: 12.w, color: ColorName.secondaryTextColor, overflow: TextOverflow.ellipsis)), ], ), ), ], ), ); } Widget _buildUserFileChatItem(BuildContext context, FileChatItem chatItem) { return Align( alignment: Alignment.centerRight, child: Container( padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 16.h), margin: EdgeInsets.symmetric(vertical: 10.h), constraints: BoxConstraints( maxWidth: 0.56.sw, ), decoration: BoxDecoration( color: ColorName.white, borderRadius: BorderRadius.only( topLeft: Radius.circular(16.w), bottomRight: Radius.circular(16.w), bottomLeft: Radius.circular(16.w)), border: Border.all(color: "#ECECEC".color, width: 1.w), ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Container( margin: EdgeInsets.only(right: 6.w), child: Image( image: Assets.images.iconFilesFile.provider(), width: 30.w, height: 32.w), ), Flexible( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(chatItem.talkInfo.title.value.orEmpty, maxLines: 1, style: TextStyle( fontSize: 14.w, color: ColorName.primaryTextColor, fontWeight: FontWeight.bold, overflow: TextOverflow.ellipsis)), Text(chatItem.talkInfo.summary.value.orEmpty, maxLines: 1, style: TextStyle( fontSize: 12.w, color: ColorName.secondaryTextColor, overflow: TextOverflow.ellipsis)), ], ), ), ], ), ), ); } _buildReferenceFile(TalkBean talkInfo) { if (talkInfo.oversizeFile == true) { return _buildOverSizeReference(talkInfo); } else { return _buildNormalReference(talkInfo); } } Container _buildOverSizeReference(TalkBean talkInfo) { return Container( margin: EdgeInsets.only(bottom: 14.h), padding: EdgeInsets.only(left: 8.w, top: 8.h, right: 10.w, bottom: 8.h), decoration: BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(8.w)), border: Border.all(color: "#F0F0F0".color, width: 1.w), ), child: Column( children: [ Row( children: [ Text(talkInfo.title.value.orEmpty, style: TextStyle( fontWeight: FontWeight.bold, fontSize: 14.w, color: ColorName.primaryTextColor)), const Spacer(), GestureDetector( onTap: () => controller.onDeleteReference(), child: Container( margin: EdgeInsets.only(left: 8.w), child: Image( image: Assets.images.iconReferenceChatDeleteFile.provider(), width: 18.w, height: 18.w), ), ), ], ), Container( margin: EdgeInsets.only(top: 11.h), child: Row( children: [ Container( margin: EdgeInsets.only(right: 2.w), child: Image( image: Assets.images.iconReferenceChatFile.provider(), width: 16.w, height: 16.w), ), Text("谈话·超长内容", style: TextStyle( fontSize: 12.w, color: ColorName.tertiaryTextColor)), ], ), ) ], ), ); } _buildNormalReference(TalkBean talkInfo) { return Container( decoration: BoxDecoration( color: "#F6F6F6".color, borderRadius: BorderRadius.all(Radius.circular(6.w)), ), padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 7.h), margin: EdgeInsets.only(bottom: 14.h), child: Row( children: [ Image( image: Assets.images.iconReferenceChatArrow.provider(), width: 16.w, height: 16.w), Container( margin: EdgeInsets.only(right: 2.w, left: 4.w), child: Image( image: Assets.images.iconReferenceChatFile.provider(), width: 16.w, height: 16.w), ), Text(talkInfo.title.value.orEmpty, overflow: TextOverflow.ellipsis, maxLines: 1, style: TextStyle( fontSize: 12.w, color: ColorName.primaryTextColor, overflow: TextOverflow.ellipsis)), const Spacer(), Container( margin: EdgeInsets.only(left: 8.w), child: GestureDetector( onTap: () => controller.onDeleteReference(), child: Image( image: Assets.images.iconReferenceChatDeleteFile.provider(), width: 18.w, height: 18.w), ), ), ], ), ); } Widget _buildTopGradient() { return Container( width: 1.sw, height: 128.h, decoration: BoxDecoration( gradient: LinearGradient( colors: ['#E8EBFF'.toColor(), '#00E8EBFF'.toColor()], begin: Alignment.topCenter, end: Alignment.bottomCenter, stops: const [0.5, 1.0], ), )); } Widget _buildBackgroundGradient() { return Container( width: 1.sw, height: 1.sh, decoration: BoxDecoration( gradient: LinearGradient( colors: ['#F2F8F4'.toColor(), '#F6F6F6'.toColor()], begin: Alignment.topCenter, end: Alignment.bottomCenter, stops: const [0, 1.0], ), ), ); } }