view.dart 20 KB

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