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/chat/controller.dart'; import 'package:electronic_assistant/resource/colors.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'; class ChatPage extends BasePage { const ChatPage({super.key}); static start() { Get.toNamed(RoutePath.chat); } static startByTalk(TalkBean talkInfo, {Agenda? agenda}) { Get.toNamed(RoutePath.chat, arguments: [talkInfo, agenda]); } static startByTalkId(String talkId, {Agenda? agenda}) { Get.toNamed(RoutePath.chat, arguments: [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: const Icon(Icons.arrow_back_ios_new_rounded), onPressed: () { Navigator.pop(context); }, ), 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(24.w), boxShadow: const [ BoxShadow( color: Color(0x4CDDDEE8), blurRadius: 10, offset: Offset(0, 4), spreadRadius: 0, ) ]), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), 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) : 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); }), ), ); } Container _buildAssistantChatItemContent( bool? isStreamStarted, String content) { return Container( padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h), margin: EdgeInsets.symmetric(vertical: 10.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( md.markdownToHtml(content), textStyle: TextStyle( fontSize: 14.w, color: ColorName.primaryTextColor), ), )); } 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(), 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( 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), color: "#F6F6F6".color, 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], ), ), ); } }