view.dart 18 KB

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