view.dart 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649
  1. import 'package:electronic_assistant/base/base_page.dart';
  2. import 'package:electronic_assistant/data/bean/agenda.dart';
  3. import 'package:electronic_assistant/data/bean/chat_item.dart';
  4. import 'package:electronic_assistant/data/bean/file_chat_item.dart';
  5. import 'package:electronic_assistant/data/bean/reference_chat_item.dart';
  6. import 'package:electronic_assistant/module/chat/controller.dart';
  7. import 'package:electronic_assistant/resource/colors.gen.dart';
  8. import 'package:electronic_assistant/resource/string.gen.dart';
  9. import 'package:electronic_assistant/utils/expand.dart';
  10. import 'package:electronic_assistant/widget/gradually_md_text.dart';
  11. import 'package:flutter/cupertino.dart';
  12. import 'package:flutter/material.dart';
  13. import 'package:flutter/services.dart';
  14. import 'package:flutter_screenutil/flutter_screenutil.dart';
  15. import 'package:get/get.dart';
  16. import 'package:lottie/lottie.dart';
  17. import 'package:pull_to_refresh/pull_to_refresh.dart';
  18. import '../../data/bean/progressing_chat_item.dart';
  19. import '../../data/bean/talks.dart';
  20. import '../../resource/assets.gen.dart';
  21. import '../../router/app_pages.dart';
  22. enum ChatFromType {
  23. fromMain,
  24. fromTalkDetail,
  25. fromAnalysisBtn,
  26. fromTalkExample,
  27. fromMine,
  28. unknown
  29. }
  30. class ChatPage extends BasePage<ChatController> {
  31. const ChatPage({super.key});
  32. static start(ChatFromType fromType) {
  33. Get.toNamed(RoutePath.chat, arguments: [fromType]);
  34. }
  35. static startByTalk(ChatFromType fromType, TalkBean talkInfo,
  36. {Agenda? agenda}) {
  37. Get.toNamed(RoutePath.chat, arguments: [fromType, talkInfo, agenda]);
  38. }
  39. static startByTalkId(ChatFromType fromType, String talkId, {Agenda? agenda}) {
  40. Get.toNamed(RoutePath.chat, arguments: [fromType, talkId, agenda]);
  41. }
  42. @override
  43. bool immersive() {
  44. return true;
  45. }
  46. @override
  47. Color navigationBarColor() {
  48. return "#F6F6F6".color;
  49. }
  50. @override
  51. Widget buildBody(BuildContext context) {
  52. // 第一次启动时弹出定制窗口
  53. return Stack(
  54. children: [
  55. _buildBackgroundGradient(),
  56. _buildTopGradient(),
  57. Scaffold(
  58. backgroundColor: Colors.transparent,
  59. appBar: AppBar(
  60. leading: IconButton(
  61. icon: SizedBox(
  62. width: 24.w,
  63. height: 24.w,
  64. child: Assets.images.iconBack.image()),
  65. onPressed: () {
  66. Get.back();
  67. },
  68. ),
  69. scrolledUnderElevation: 0,
  70. backgroundColor: Colors.transparent,
  71. systemOverlayStyle: SystemUiOverlayStyle.dark,
  72. centerTitle: true,
  73. title: IntrinsicWidth(
  74. child: Row(
  75. mainAxisAlignment: MainAxisAlignment.center,
  76. children: [
  77. Image(
  78. image: Assets.images.iconChatXiaoTin.provider(),
  79. width: 28.w,
  80. height: 28.w),
  81. Container(
  82. margin: EdgeInsets.only(left: 6.w),
  83. child: Text('聊天',
  84. style: TextStyle(
  85. fontSize: 16.w,
  86. fontWeight: FontWeight.bold,
  87. color: ColorName.primaryTextColor))),
  88. ],
  89. ),
  90. ),
  91. ),
  92. body: buildBodyContent(context),
  93. )
  94. ],
  95. );
  96. }
  97. Widget buildBodyContent(BuildContext context) {
  98. return Column(
  99. children: [
  100. Expanded(
  101. child: Container(
  102. padding: EdgeInsets.symmetric(horizontal: 12.w),
  103. child: Obx(() {
  104. return NotificationListener<ScrollNotification>(
  105. onNotification: (scrollNotification) {
  106. if (scrollNotification is ScrollStartNotification) {
  107. FocusScope.of(context).unfocus();
  108. }
  109. return false;
  110. },
  111. child: SmartRefresher(
  112. controller: controller.refreshController,
  113. footer: CustomFooter(
  114. loadStyle: LoadStyle.ShowWhenLoading,
  115. builder: (context, mode) {
  116. if (mode == LoadStatus.loading ||
  117. mode == LoadStatus.canLoading) {
  118. return const SizedBox(
  119. height: 60.0,
  120. child: SizedBox(
  121. height: 20.0,
  122. width: 20.0,
  123. child: CupertinoActivityIndicator(),
  124. ),
  125. );
  126. } else {
  127. return Container();
  128. }
  129. },
  130. ),
  131. enablePullDown: false,
  132. enablePullUp: true,
  133. onLoading: controller.loadMoreHistory,
  134. onRefresh: controller.loadMoreHistory,
  135. child: ListView.builder(
  136. reverse: true,
  137. physics: const BouncingScrollPhysics(
  138. parent: AlwaysScrollableScrollPhysics()),
  139. controller: controller.listScrollController,
  140. itemBuilder: _chatItemBuilder,
  141. itemCount: controller.chatItems.length),
  142. ),
  143. );
  144. }),
  145. )),
  146. Container(
  147. margin: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h),
  148. width: 1.sw,
  149. decoration: BoxDecoration(
  150. color: Colors.white,
  151. borderRadius: BorderRadius.circular(12.w),
  152. boxShadow: const [
  153. BoxShadow(
  154. color: Color(0x4CDDDEE8),
  155. blurRadius: 10,
  156. offset: Offset(0, 4),
  157. spreadRadius: 0,
  158. )
  159. ]),
  160. child: Padding(
  161. padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
  162. child: Column(
  163. children: [
  164. Obx(() {
  165. TalkBean? talkInfo = controller.talkInfo.value;
  166. if (talkInfo == null) {
  167. return Container();
  168. } else {
  169. return _buildReferenceFile(talkInfo);
  170. }
  171. }),
  172. Row(
  173. crossAxisAlignment: CrossAxisAlignment.end,
  174. children: [
  175. Expanded(
  176. child: Container(
  177. margin: EdgeInsets.only(right: 6.w),
  178. child: CupertinoTextField(
  179. controller: controller.inputController,
  180. padding: EdgeInsets.symmetric(vertical: 3.w),
  181. style: TextStyle(
  182. fontSize: 14.w, color: ColorName.primaryTextColor),
  183. placeholder: '有问题尽管问我~',
  184. placeholderStyle: TextStyle(
  185. fontSize: 14.w, color: const Color(0xFFAFAFAF)),
  186. textCapitalization: TextCapitalization.sentences,
  187. textInputAction: TextInputAction.newline,
  188. cursorColor: ColorName.colorPrimary,
  189. decoration: const BoxDecoration(),
  190. expands: false,
  191. minLines: 1,
  192. maxLines: 6,
  193. ),
  194. )),
  195. GestureDetector(
  196. onTap: () {
  197. controller.onAddFileClick();
  198. },
  199. child: Image(
  200. image: Assets.images.iconChatAddFile.provider(),
  201. width: 26.w,
  202. height: 26.w),
  203. ),
  204. Container(
  205. margin: EdgeInsets.only(left: 16.w),
  206. child: GestureDetector(
  207. onTap: () {
  208. controller.onSendClick();
  209. },
  210. child: Image(
  211. image: Assets.images.iconChatSend.provider(),
  212. width: 26.w,
  213. height: 26.w),
  214. ),
  215. )
  216. ],
  217. )
  218. ],
  219. ),
  220. ),
  221. ),
  222. ],
  223. );
  224. }
  225. Widget _chatItemBuilder(BuildContext context, int index) {
  226. ChatItem chatItem = controller.chatItems[index];
  227. if (chatItem.role == 'user') {
  228. return _buildUserChatItem(context, chatItem);
  229. } else if (chatItem.role == 'assistant') {
  230. return _buildAssistantChatItem(context, chatItem);
  231. } else {
  232. return Container();
  233. }
  234. }
  235. Widget _buildAssistantChatItem(BuildContext context, ChatItem chatItem) {
  236. ProgressingChatItem? progressingChatItem;
  237. if (chatItem is ProgressingChatItem) {
  238. progressingChatItem = chatItem;
  239. }
  240. return Align(
  241. alignment: Alignment.centerLeft,
  242. child: IntrinsicWidth(
  243. child: progressingChatItem == null
  244. ? _buildAssistantChatItemContent(
  245. true, null, null, chatItem.content, chatItem.id)
  246. : Obx(() {
  247. bool? isStreamStarted = progressingChatItem == null
  248. ? null
  249. : progressingChatItem.isGradually.value ||
  250. progressingChatItem.isFinished.value ||
  251. progressingChatItem.isFailed.value;
  252. return _buildAssistantChatItemContent(
  253. progressingChatItem?.isFinished.value,
  254. isStreamStarted,
  255. progressingChatItem?.graduallyController,
  256. progressingChatItem!.isFailed.value
  257. ? progressingChatItem.error.value
  258. : chatItem.content,
  259. chatItem.id);
  260. }),
  261. ),
  262. );
  263. }
  264. Widget _buildAssistantChatItemContent(bool? isFinish, bool? isStreamStarted,
  265. GraduallyController? graduallyController, String? content, String id) {
  266. return Column(
  267. crossAxisAlignment: CrossAxisAlignment.start,
  268. children: [
  269. SizedBox(height: 10.h),
  270. if (isStreamStarted != null && isStreamStarted == false)
  271. Container(
  272. padding: const EdgeInsets.all(1),
  273. decoration: BoxDecoration(
  274. color: ColorName.colorPrimary,
  275. gradient: LinearGradient(
  276. colors: ['#B57AFF'.toColor(), '#4466FF'.toColor()],
  277. stops: const [0, 1.0],
  278. begin: Alignment.topLeft,
  279. end: Alignment.bottomRight,
  280. ),
  281. borderRadius: BorderRadius.only(
  282. topRight: Radius.circular(20.w),
  283. bottomRight: Radius.circular(20.w),
  284. bottomLeft: Radius.circular(20.w))),
  285. child: Container(
  286. decoration: BoxDecoration(
  287. color: ColorName.white,
  288. borderRadius: BorderRadius.only(
  289. topRight: Radius.circular(20.w),
  290. bottomRight: Radius.circular(20.w),
  291. bottomLeft: Radius.circular(20.w))),
  292. padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
  293. child: Lottie.asset(
  294. "assets/anim/anim_chat_response_loading.zip",
  295. width: 46.w,
  296. height: 20.w)),
  297. )
  298. else
  299. _buildAiContent(isFinish, content, graduallyController),
  300. Obx(() {
  301. return Visibility(
  302. visible: id == controller.chatAiTagId.value,
  303. child: Container(
  304. margin: EdgeInsets.only(top: 6.h),
  305. padding: EdgeInsets.symmetric(horizontal: 6.w, vertical: 3.w),
  306. decoration: BoxDecoration(
  307. borderRadius: BorderRadius.all(Radius.circular(12.w)),
  308. color: "#EFEEF1".color),
  309. child: Text(
  310. StringName.chatItemAiTag.tr,
  311. style: TextStyle(
  312. height: 1,
  313. fontSize: 10.sp,
  314. color: ColorName.tertiaryTextColor),
  315. ),
  316. ),
  317. );
  318. }),
  319. SizedBox(height: 10.h)
  320. ],
  321. );
  322. }
  323. Widget _buildAiContent(bool? isFinish, String? content,
  324. GraduallyController? graduallyController) {
  325. if (isFinish == true) {
  326. return Row(
  327. crossAxisAlignment: CrossAxisAlignment.end,
  328. children: [
  329. IntrinsicWidth(
  330. child: _buildGraduallyMdText(content, graduallyController),
  331. ),
  332. SizedBox(width: 10.w),
  333. Visibility(
  334. visible: isFinish == true,
  335. child: Padding(
  336. padding: EdgeInsets.only(bottom: 4.w),
  337. child: GestureDetector(
  338. onTap: () {
  339. controller.onCopyClick(content);
  340. },
  341. child: Assets.images.iconChatCopy
  342. .image(width: 28.w, height: 28.w)),
  343. ),
  344. )
  345. ],
  346. );
  347. } else {
  348. return _buildGraduallyMdText(content, graduallyController);
  349. }
  350. }
  351. Widget _buildGraduallyMdText(
  352. String? content, GraduallyController? graduallyController) {
  353. return Container(
  354. decoration: BoxDecoration(
  355. color: ColorName.white,
  356. border: Border.all(color: '#ECECEC'.color, width: 1.w),
  357. borderRadius: BorderRadius.only(
  358. topRight: Radius.circular(20.w),
  359. bottomRight: Radius.circular(20.w),
  360. bottomLeft: Radius.circular(20.w))),
  361. padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
  362. alignment: Alignment.centerLeft,
  363. constraints: BoxConstraints(
  364. maxWidth: 0.78.sw,
  365. ),
  366. child: GraduallyMdText(
  367. initTxt: content,
  368. graduallyController: graduallyController,
  369. textStyle:
  370. TextStyle(fontSize: 14.w, color: ColorName.primaryTextColor)),
  371. );
  372. }
  373. Widget _buildUserChatItem(BuildContext context, ChatItem chatItem) {
  374. if (chatItem is FileChatItem) {
  375. return _buildUserFileChatItem(context, chatItem);
  376. } else if (chatItem is ReferenceChatItem) {
  377. return _buildUserNormalChatItem(context, chatItem,
  378. referenceTalkTitle: chatItem.talkInfo.title.value);
  379. }
  380. return _buildUserNormalChatItem(context, chatItem,
  381. referenceTalkTitle: chatItem.talkTitle);
  382. }
  383. Widget _buildUserNormalChatItem(BuildContext context, ChatItem chatItem,
  384. {String? referenceTalkTitle}) {
  385. return Align(
  386. alignment: Alignment.centerRight,
  387. child: Column(
  388. crossAxisAlignment: CrossAxisAlignment.end,
  389. children: [
  390. IntrinsicWidth(
  391. child: Container(
  392. padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
  393. margin: referenceTalkTitle == null
  394. ? EdgeInsets.symmetric(vertical: 10.h)
  395. : EdgeInsets.only(top: 10.h),
  396. alignment: Alignment.centerRight,
  397. constraints: BoxConstraints(
  398. maxWidth: 0.78.sw,
  399. ),
  400. decoration: BoxDecoration(
  401. color: ColorName.colorPrimary,
  402. borderRadius: BorderRadius.only(
  403. topLeft: Radius.circular(16.w),
  404. bottomRight: Radius.circular(16.w),
  405. bottomLeft: Radius.circular(16.w))),
  406. child: SelectableText(chatItem.content,
  407. style: TextStyle(fontSize: 14.w, color: ColorName.white)),
  408. ),
  409. ),
  410. if (referenceTalkTitle != null)
  411. Container(
  412. padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
  413. margin: EdgeInsets.only(top: 8.h, bottom: 10.h),
  414. constraints: BoxConstraints(
  415. maxWidth: 0.78.sw,
  416. ),
  417. decoration: BoxDecoration(
  418. color: "#EFEFEF".color,
  419. borderRadius: BorderRadius.all(Radius.circular(10.w))),
  420. child: Row(
  421. children: [
  422. Image(
  423. image: Assets.images.iconReferenceChatArrow.provider(),
  424. width: 16.w,
  425. height: 16.w),
  426. Container(
  427. margin: EdgeInsets.only(right: 2.w, left: 4.w),
  428. child: Image(
  429. image: Assets.images.iconReferenceChatFile.provider(),
  430. width: 16.w,
  431. height: 16.w),
  432. ),
  433. Text(referenceTalkTitle,
  434. style: TextStyle(
  435. fontSize: 12.w,
  436. color: ColorName.secondaryTextColor,
  437. overflow: TextOverflow.ellipsis)),
  438. ],
  439. ),
  440. ),
  441. ],
  442. ),
  443. );
  444. }
  445. Widget _buildUserFileChatItem(BuildContext context, FileChatItem chatItem) {
  446. return Align(
  447. alignment: Alignment.centerRight,
  448. child: Container(
  449. padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 16.h),
  450. margin: EdgeInsets.symmetric(vertical: 10.h),
  451. constraints: BoxConstraints(
  452. maxWidth: 0.56.sw,
  453. ),
  454. decoration: BoxDecoration(
  455. color: ColorName.white,
  456. borderRadius: BorderRadius.only(
  457. topLeft: Radius.circular(16.w),
  458. bottomRight: Radius.circular(16.w),
  459. bottomLeft: Radius.circular(16.w)),
  460. border: Border.all(color: "#ECECEC".color, width: 1.w),
  461. ),
  462. child: Row(
  463. crossAxisAlignment: CrossAxisAlignment.center,
  464. children: [
  465. Container(
  466. margin: EdgeInsets.only(right: 6.w),
  467. child: Image(
  468. image: Assets.images.iconFilesFile.provider(),
  469. width: 30.w,
  470. height: 32.w),
  471. ),
  472. Flexible(
  473. child: Column(
  474. crossAxisAlignment: CrossAxisAlignment.start,
  475. children: [
  476. Text(chatItem.talkInfo.title.value.orEmpty,
  477. maxLines: 1,
  478. style: TextStyle(
  479. fontSize: 14.w,
  480. color: ColorName.primaryTextColor,
  481. fontWeight: FontWeight.bold,
  482. overflow: TextOverflow.ellipsis)),
  483. Text(chatItem.talkInfo.summary.value.orEmpty,
  484. maxLines: 1,
  485. style: TextStyle(
  486. fontSize: 12.w,
  487. color: ColorName.secondaryTextColor,
  488. overflow: TextOverflow.ellipsis)),
  489. ],
  490. ),
  491. ),
  492. ],
  493. ),
  494. ),
  495. );
  496. }
  497. _buildReferenceFile(TalkBean talkInfo) {
  498. if (talkInfo.oversizeFile == true) {
  499. return _buildOverSizeReference(talkInfo);
  500. } else {
  501. return _buildNormalReference(talkInfo);
  502. }
  503. }
  504. Container _buildOverSizeReference(TalkBean talkInfo) {
  505. return Container(
  506. margin: EdgeInsets.only(bottom: 14.h),
  507. padding: EdgeInsets.only(left: 8.w, top: 8.h, right: 10.w, bottom: 8.h),
  508. decoration: BoxDecoration(
  509. borderRadius: BorderRadius.all(Radius.circular(8.w)),
  510. border: Border.all(color: "#F0F0F0".color, width: 1.w),
  511. ),
  512. child: Column(
  513. children: [
  514. Row(
  515. children: [
  516. Text(talkInfo.title.value.orEmpty,
  517. style: TextStyle(
  518. fontWeight: FontWeight.bold,
  519. fontSize: 14.w,
  520. color: ColorName.primaryTextColor)),
  521. const Spacer(),
  522. GestureDetector(
  523. onTap: () => controller.onDeleteReference(),
  524. child: Container(
  525. margin: EdgeInsets.only(left: 8.w),
  526. child: Image(
  527. image:
  528. Assets.images.iconReferenceChatDeleteFile.provider(),
  529. width: 18.w,
  530. height: 18.w),
  531. ),
  532. ),
  533. ],
  534. ),
  535. Container(
  536. margin: EdgeInsets.only(top: 11.h),
  537. child: Row(
  538. children: [
  539. Container(
  540. margin: EdgeInsets.only(right: 2.w),
  541. child: Image(
  542. image: Assets.images.iconReferenceChatFile.provider(),
  543. width: 16.w,
  544. height: 16.w),
  545. ),
  546. Text("谈话·超长内容",
  547. style: TextStyle(
  548. fontSize: 12.w, color: ColorName.tertiaryTextColor)),
  549. ],
  550. ),
  551. )
  552. ],
  553. ),
  554. );
  555. }
  556. _buildNormalReference(TalkBean talkInfo) {
  557. return Container(
  558. decoration: BoxDecoration(
  559. color: "#F6F6F6".color,
  560. borderRadius: BorderRadius.all(Radius.circular(6.w)),
  561. ),
  562. padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 7.h),
  563. margin: EdgeInsets.only(bottom: 14.h),
  564. child: Row(
  565. children: [
  566. Image(
  567. image: Assets.images.iconReferenceChatArrow.provider(),
  568. width: 16.w,
  569. height: 16.w),
  570. Container(
  571. margin: EdgeInsets.only(right: 2.w, left: 4.w),
  572. child: Image(
  573. image: Assets.images.iconReferenceChatFile.provider(),
  574. width: 16.w,
  575. height: 16.w),
  576. ),
  577. Text(talkInfo.title.value.orEmpty,
  578. overflow: TextOverflow.ellipsis,
  579. maxLines: 1,
  580. style: TextStyle(
  581. fontSize: 12.w,
  582. color: ColorName.primaryTextColor,
  583. overflow: TextOverflow.ellipsis)),
  584. const Spacer(),
  585. Container(
  586. margin: EdgeInsets.only(left: 8.w),
  587. child: GestureDetector(
  588. onTap: () => controller.onDeleteReference(),
  589. child: Image(
  590. image: Assets.images.iconReferenceChatDeleteFile.provider(),
  591. width: 18.w,
  592. height: 18.w),
  593. ),
  594. ),
  595. ],
  596. ),
  597. );
  598. }
  599. Widget _buildTopGradient() {
  600. return Container(
  601. width: 1.sw,
  602. height: 128.h,
  603. decoration: BoxDecoration(
  604. gradient: LinearGradient(
  605. colors: ['#E8EBFF'.toColor(), '#00E8EBFF'.toColor()],
  606. begin: Alignment.topCenter,
  607. end: Alignment.bottomCenter,
  608. stops: const [0.5, 1.0],
  609. ),
  610. ));
  611. }
  612. Widget _buildBackgroundGradient() {
  613. return Container(
  614. width: 1.sw,
  615. height: 1.sh,
  616. decoration: BoxDecoration(
  617. gradient: LinearGradient(
  618. colors: ['#F2F8F4'.toColor(), '#F6F6F6'.toColor()],
  619. begin: Alignment.topCenter,
  620. end: Alignment.bottomCenter,
  621. stops: const [0, 1.0],
  622. ),
  623. ),
  624. );
  625. }
  626. }