view.dart 20 KB

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