view.dart 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  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. 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. TalkBean? 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. GestureDetector(
  181. onTap: () {
  182. controller.onAddFileClick();
  183. },
  184. child: Image(
  185. image: Assets.images.iconChatAddFile.provider(),
  186. width: 26.w,
  187. height: 26.w),
  188. ),
  189. Container(
  190. margin: EdgeInsets.only(left: 16.w),
  191. child: GestureDetector(
  192. onTap: () {
  193. controller.onSendClick();
  194. },
  195. child: Image(
  196. image: Assets.images.iconChatSend.provider(),
  197. width: 26.w,
  198. height: 26.w),
  199. ),
  200. )
  201. ],
  202. )
  203. ],
  204. ),
  205. ),
  206. ),
  207. ],
  208. );
  209. }
  210. Widget _chatItemBuilder(BuildContext context, int index) {
  211. ChatItem chatItem = controller.chatItems[index];
  212. if (chatItem.role == 'user') {
  213. return _buildUserChatItem(context, chatItem);
  214. } else if (chatItem.role == 'assistant') {
  215. return _buildAssistantChatItem(context, chatItem);
  216. } else {
  217. return Container();
  218. }
  219. }
  220. Widget _buildAssistantChatItem(BuildContext context, ChatItem chatItem) {
  221. ProgressingChatItem? progressingChatItem;
  222. if (chatItem is ProgressingChatItem) {
  223. progressingChatItem = chatItem;
  224. }
  225. return Align(
  226. alignment: Alignment.centerLeft,
  227. child: IntrinsicWidth(
  228. child: progressingChatItem == null
  229. ? _buildAssistantChatItemContent(null, chatItem.content)
  230. : Obx(() {
  231. bool? isStreamStarted = progressingChatItem == null
  232. ? null
  233. : progressingChatItem.streamContent.isNotEmpty ||
  234. progressingChatItem.isFinished.value ||
  235. progressingChatItem.isFailed.value;
  236. return _buildAssistantChatItemContent(
  237. isStreamStarted,
  238. progressingChatItem!.isFailed.value
  239. ? progressingChatItem.error.value
  240. : progressingChatItem.streamContent.value);
  241. }),
  242. ),
  243. );
  244. }
  245. Container _buildAssistantChatItemContent(
  246. bool? isStreamStarted, String content) {
  247. return Container(
  248. padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
  249. margin: EdgeInsets.symmetric(vertical: 10.h),
  250. alignment: Alignment.centerLeft,
  251. constraints: BoxConstraints(
  252. maxWidth: 0.78.sw,
  253. ),
  254. decoration: BoxDecoration(
  255. border: isStreamStarted == null || isStreamStarted == true
  256. ? null
  257. : Border.all(color: ColorName.colorPrimary, width: 1.w),
  258. color: ColorName.white,
  259. borderRadius: BorderRadius.only(
  260. topRight: Radius.circular(20.w),
  261. bottomRight: Radius.circular(20.w),
  262. bottomLeft: Radius.circular(20.w))),
  263. child: isStreamStarted != null && isStreamStarted == false
  264. ? Lottie.asset("assets/anim/anim_chat_response_loading.zip",
  265. width: 46.w, height: 20.w)
  266. : SelectableText(content,
  267. style:
  268. TextStyle(fontSize: 14.w, color: ColorName.primaryTextColor)),
  269. );
  270. }
  271. Widget _buildUserChatItem(BuildContext context, ChatItem chatItem) {
  272. if (chatItem is FileChatItem) {
  273. return _buildUserFileChatItem(context, chatItem);
  274. } else if (chatItem is ReferenceChatItem) {
  275. return _buildUserNormalChatItem(context, chatItem,
  276. referenceTalkTitle: chatItem.talkInfo.title);
  277. }
  278. return _buildUserNormalChatItem(context, chatItem,
  279. referenceTalkTitle: chatItem.talkTitle);
  280. }
  281. Widget _buildUserNormalChatItem(BuildContext context, ChatItem chatItem,
  282. {String? referenceTalkTitle}) {
  283. return Align(
  284. alignment: Alignment.centerRight,
  285. child: Column(
  286. crossAxisAlignment: CrossAxisAlignment.end,
  287. children: [
  288. IntrinsicWidth(
  289. child: Container(
  290. padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
  291. margin: referenceTalkTitle == null
  292. ? EdgeInsets.symmetric(vertical: 10.h)
  293. : EdgeInsets.only(top: 10.h),
  294. alignment: Alignment.centerRight,
  295. constraints: BoxConstraints(
  296. maxWidth: 0.78.sw,
  297. ),
  298. decoration: BoxDecoration(
  299. color: ColorName.colorPrimary,
  300. borderRadius: BorderRadius.only(
  301. topLeft: Radius.circular(16.w),
  302. bottomRight: Radius.circular(16.w),
  303. bottomLeft: Radius.circular(16.w))),
  304. child: Text(chatItem.content,
  305. style: TextStyle(fontSize: 14.w, color: ColorName.white)),
  306. ),
  307. ),
  308. if (referenceTalkTitle != null)
  309. Container(
  310. padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
  311. margin: EdgeInsets.only(top: 8.h, bottom: 10.h),
  312. constraints: BoxConstraints(
  313. maxWidth: 0.78.sw,
  314. ),
  315. decoration: BoxDecoration(
  316. color: "#EFEFEF".color,
  317. borderRadius: BorderRadius.all(Radius.circular(10.w))),
  318. child: Row(
  319. children: [
  320. Image(
  321. image: Assets.images.iconReferenceChatArrow.provider(),
  322. width: 16.w,
  323. height: 16.w),
  324. Container(
  325. margin: EdgeInsets.only(right: 2.w, left: 4.w),
  326. child: Image(
  327. image: Assets.images.iconReferenceChatFile.provider(),
  328. width: 16.w,
  329. height: 16.w),
  330. ),
  331. Text(referenceTalkTitle,
  332. style: TextStyle(
  333. fontSize: 12.w,
  334. color: ColorName.secondaryTextColor,
  335. overflow: TextOverflow.ellipsis)),
  336. ],
  337. ),
  338. ),
  339. ],
  340. ),
  341. );
  342. }
  343. Widget _buildUserFileChatItem(BuildContext context, FileChatItem chatItem) {
  344. return Align(
  345. alignment: Alignment.centerRight,
  346. child: Container(
  347. padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 16.h),
  348. margin: EdgeInsets.symmetric(vertical: 10.h),
  349. constraints: BoxConstraints(
  350. maxWidth: 0.56.sw,
  351. ),
  352. decoration: BoxDecoration(
  353. color: ColorName.white,
  354. borderRadius: BorderRadius.only(
  355. topLeft: Radius.circular(16.w),
  356. bottomRight: Radius.circular(16.w),
  357. bottomLeft: Radius.circular(16.w)),
  358. border: Border.all(color: "#ECECEC".color, width: 1.w),
  359. ),
  360. child: Row(
  361. crossAxisAlignment: CrossAxisAlignment.center,
  362. children: [
  363. Container(
  364. margin: EdgeInsets.only(right: 6.w),
  365. child: Image(
  366. image: Assets.images.iconFilesFile.provider(),
  367. width: 30.w,
  368. height: 32.w),
  369. ),
  370. Flexible(
  371. child: Column(
  372. crossAxisAlignment: CrossAxisAlignment.start,
  373. children: [
  374. Text(chatItem.talkInfo.title ?? '',
  375. maxLines: 1,
  376. style: TextStyle(
  377. fontSize: 14.w,
  378. color: ColorName.primaryTextColor,
  379. fontWeight: FontWeight.bold,
  380. overflow: TextOverflow.ellipsis)),
  381. Text(chatItem.talkInfo.summary ?? '',
  382. maxLines: 1,
  383. style: TextStyle(
  384. fontSize: 12.w,
  385. color: ColorName.secondaryTextColor,
  386. overflow: TextOverflow.ellipsis)),
  387. ],
  388. ),
  389. ),
  390. ],
  391. ),
  392. ),
  393. );
  394. }
  395. _buildReferenceFile(TalkBean talkInfo) {
  396. if (talkInfo.oversizeFile == true) {
  397. return _buildOverSizeReference(talkInfo);
  398. } else {
  399. return _buildNormalReference(talkInfo);
  400. }
  401. }
  402. Container _buildOverSizeReference(TalkBean talkInfo) {
  403. return Container(
  404. margin: EdgeInsets.only(bottom: 14.h),
  405. padding: EdgeInsets.only(left: 8.w, top: 8.h, right: 10.w, bottom: 8.h),
  406. decoration: BoxDecoration(
  407. borderRadius: BorderRadius.all(Radius.circular(8.w)),
  408. border: Border.all(color: "#F0F0F0".color, width: 1.w),
  409. ),
  410. child: Column(
  411. children: [
  412. Row(
  413. children: [
  414. Text(talkInfo.title ?? '',
  415. style: TextStyle(
  416. fontWeight: FontWeight.bold,
  417. fontSize: 14.w,
  418. color: ColorName.primaryTextColor)),
  419. const Spacer(),
  420. Container(
  421. margin: EdgeInsets.only(left: 8.w),
  422. child: Image(
  423. image: Assets.images.iconReferenceChatDeleteFile.provider(),
  424. width: 18.w,
  425. height: 18.w),
  426. ),
  427. ],
  428. ),
  429. Container(
  430. margin: EdgeInsets.only(top: 11.h),
  431. child: Row(
  432. children: [
  433. Container(
  434. margin: EdgeInsets.only(right: 2.w),
  435. child: Image(
  436. image: Assets.images.iconReferenceChatFile.provider(),
  437. width: 16.w,
  438. height: 16.w),
  439. ),
  440. Text("谈话·超长内容",
  441. style: TextStyle(
  442. fontSize: 12.w, color: ColorName.tertiaryTextColor)),
  443. ],
  444. ),
  445. )
  446. ],
  447. ),
  448. );
  449. }
  450. _buildNormalReference(TalkBean talkInfo) {
  451. return Container(
  452. margin: EdgeInsets.only(bottom: 14.h),
  453. child: Row(
  454. children: [
  455. Image(
  456. image: Assets.images.iconReferenceChatArrow.provider(),
  457. width: 16.w,
  458. height: 16.w),
  459. Container(
  460. margin: EdgeInsets.only(right: 2.w, left: 4.w),
  461. child: Image(
  462. image: Assets.images.iconReferenceChatFile.provider(),
  463. width: 16.w,
  464. height: 16.w),
  465. ),
  466. Text(talkInfo.title ?? '',
  467. overflow: TextOverflow.ellipsis,
  468. maxLines: 1,
  469. style: TextStyle(
  470. fontSize: 12.w,
  471. color: ColorName.primaryTextColor,
  472. overflow: TextOverflow.ellipsis)),
  473. const Spacer(),
  474. Container(
  475. margin: EdgeInsets.only(left: 8.w),
  476. color: "#F6F6F6".color,
  477. child: GestureDetector(
  478. onTap: () => controller.onDeleteReference(),
  479. child: Image(
  480. image: Assets.images.iconReferenceChatDeleteFile.provider(),
  481. width: 18.w,
  482. height: 18.w),
  483. ),
  484. ),
  485. ],
  486. ),
  487. );
  488. }
  489. Widget _buildTopGradient() {
  490. return Container(
  491. width: 1.sw,
  492. height: 128.h,
  493. decoration: BoxDecoration(
  494. gradient: LinearGradient(
  495. colors: ['#E8EBFF'.toColor(), '#00E8EBFF'.toColor()],
  496. begin: Alignment.topCenter,
  497. end: Alignment.bottomCenter,
  498. stops: const [0.5, 1.0],
  499. ),
  500. ));
  501. }
  502. Widget _buildBackgroundGradient() {
  503. return Container(
  504. width: 1.sw,
  505. height: 1.sh,
  506. decoration: BoxDecoration(
  507. gradient: LinearGradient(
  508. colors: ['#F2F8F4'.toColor(), '#F6F6F6'.toColor()],
  509. begin: Alignment.topCenter,
  510. end: Alignment.bottomCenter,
  511. stops: const [0, 1.0],
  512. ),
  513. ),
  514. );
  515. }
  516. }