view.dart 19 KB

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