view.dart 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. import 'package:electronic_assistant/base/base_page.dart';
  2. import 'package:electronic_assistant/data/bean/chat_item.dart';
  3. import 'package:electronic_assistant/module/chat/controller.dart';
  4. import 'package:electronic_assistant/resource/colors.gen.dart';
  5. import 'package:electronic_assistant/utils/expand.dart';
  6. import 'package:flutter/cupertino.dart';
  7. import 'package:flutter/material.dart';
  8. import 'package:flutter/services.dart';
  9. import 'package:flutter_screenutil/flutter_screenutil.dart';
  10. import 'package:get/get.dart';
  11. import 'package:lottie/lottie.dart';
  12. import 'package:pull_to_refresh/pull_to_refresh.dart';
  13. import '../../data/bean/progressing_chat_item.dart';
  14. import '../../resource/assets.gen.dart';
  15. class ChatPage extends BasePage<ChatController> {
  16. const ChatPage({super.key});
  17. @override
  18. bool immersive() {
  19. return true;
  20. }
  21. @override
  22. Widget buildBody(BuildContext context) {
  23. // 第一次启动时弹出定制窗口
  24. controller.showStartSheet(context);
  25. return Stack(
  26. children: [
  27. buildBackgroundGradient(),
  28. buildTopGradient(),
  29. Scaffold(
  30. backgroundColor: Colors.transparent,
  31. appBar: AppBar(
  32. leading: IconButton(
  33. icon: const Icon(Icons.arrow_back_ios_new_rounded),
  34. onPressed: () {
  35. Navigator.pop(context);
  36. },
  37. ),
  38. scrolledUnderElevation: 0,
  39. backgroundColor: Colors.transparent,
  40. systemOverlayStyle: SystemUiOverlayStyle.dark,
  41. centerTitle: true,
  42. title: IntrinsicWidth(
  43. child: Row(
  44. mainAxisAlignment: MainAxisAlignment.center,
  45. children: [
  46. Image(
  47. image: Assets.images.iconChatXiaoTin.provider(),
  48. width: 28.w,
  49. height: 28.w),
  50. Container(
  51. margin: EdgeInsets.only(left: 6.w),
  52. child: Text('聊天',
  53. style: TextStyle(
  54. fontSize: 16.w,
  55. fontWeight: FontWeight.bold,
  56. color: ColorName.primaryTextColor))),
  57. ],
  58. ),
  59. ),
  60. ),
  61. body: buildBodyContent(context),
  62. )
  63. ],
  64. );
  65. }
  66. Widget buildBodyContent(BuildContext context) {
  67. return Column(
  68. children: [
  69. Expanded(
  70. child: Container(
  71. padding: EdgeInsets.symmetric(horizontal: 12.w),
  72. child: Obx(() {
  73. return NotificationListener<ScrollNotification>(
  74. onNotification: (scrollNotification) {
  75. if (scrollNotification is ScrollStartNotification) {
  76. FocusScope.of(context).unfocus();
  77. }
  78. return false;
  79. },
  80. child: SmartRefresher(
  81. controller: controller.refreshController,
  82. footer: CustomFooter(
  83. loadStyle: LoadStyle.ShowWhenLoading,
  84. builder: (context, mode) {
  85. if (mode == LoadStatus.loading ||
  86. mode == LoadStatus.canLoading) {
  87. return const SizedBox(
  88. height: 60.0,
  89. child: SizedBox(
  90. height: 20.0,
  91. width: 20.0,
  92. child: CupertinoActivityIndicator(),
  93. ),
  94. );
  95. } else {
  96. return Container();
  97. }
  98. },
  99. ),
  100. enablePullDown: false,
  101. enablePullUp: true,
  102. onLoading: controller.loadMoreHistory,
  103. onRefresh: controller.loadMoreHistory,
  104. child: ListView.builder(
  105. reverse: true,
  106. controller: controller.listScrollController,
  107. itemBuilder: _chatItemBuilder,
  108. itemCount: controller.chatItems.length),
  109. ),
  110. );
  111. }),
  112. )),
  113. Container(
  114. margin: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h),
  115. width: 1.sw,
  116. decoration: BoxDecoration(
  117. color: Colors.white,
  118. borderRadius: BorderRadius.circular(24.w),
  119. boxShadow: const [
  120. BoxShadow(
  121. color: Color(0x4CDDDEE8),
  122. blurRadius: 10,
  123. offset: Offset(0, 4),
  124. spreadRadius: 0,
  125. )
  126. ]),
  127. child: Padding(
  128. padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
  129. child: Column(
  130. children: [
  131. Row(
  132. crossAxisAlignment: CrossAxisAlignment.end,
  133. children: [
  134. Expanded(
  135. child: Container(
  136. margin: EdgeInsets.only(right: 6.w),
  137. child: CupertinoTextField(
  138. controller: controller.inputController,
  139. padding: EdgeInsets.symmetric(vertical: 3.w),
  140. style: TextStyle(
  141. fontSize: 14.w, color: ColorName.primaryTextColor),
  142. placeholder: '有问题尽管问我~',
  143. placeholderStyle: TextStyle(
  144. fontSize: 14.w, color: const Color(0xFFAFAFAF)),
  145. textCapitalization: TextCapitalization.sentences,
  146. textInputAction: TextInputAction.newline,
  147. cursorColor: ColorName.colorPrimary,
  148. decoration: const BoxDecoration(),
  149. expands: true,
  150. maxLines: null,
  151. minLines: null,
  152. ),
  153. )),
  154. Image(
  155. image: Assets.images.iconChatAddFile.provider(),
  156. width: 26.w,
  157. height: 26.w),
  158. Container(
  159. margin: EdgeInsets.only(left: 16.w),
  160. child: GestureDetector(
  161. onTap: () {
  162. controller.sendMessage();
  163. },
  164. child: Image(
  165. image: Assets.images.iconChatSend.provider(),
  166. width: 26.w,
  167. height: 26.w),
  168. ),
  169. )
  170. ],
  171. )
  172. ],
  173. ),
  174. ),
  175. ),
  176. ],
  177. );
  178. }
  179. Widget _chatItemBuilder(BuildContext context, int index) {
  180. ChatItem chatItem = controller.chatItems[index];
  181. if (chatItem.role == 'user') {
  182. return _buildUserChatItem(context, chatItem);
  183. } else if (chatItem.role == 'assistant') {
  184. return _buildAssistantChatItem(context, chatItem);
  185. } else {
  186. return Container();
  187. }
  188. }
  189. Widget _buildAssistantChatItem(BuildContext context, ChatItem chatItem) {
  190. ProgressingChatItem? progressingChatItem;
  191. if (chatItem is ProgressingChatItem) {
  192. progressingChatItem = chatItem;
  193. }
  194. return Align(
  195. alignment: Alignment.centerLeft,
  196. child: IntrinsicWidth(
  197. child: progressingChatItem == null
  198. ? _buildAssistantChatItemContent(null, chatItem.content)
  199. : Obx(() {
  200. bool? isStreamStarted = progressingChatItem == null
  201. ? null
  202. : progressingChatItem.streamContent.isNotEmpty ||
  203. progressingChatItem.isFinished.value ||
  204. progressingChatItem.isFailed.value;
  205. return _buildAssistantChatItemContent(
  206. isStreamStarted,
  207. progressingChatItem!.isFailed.value
  208. ? progressingChatItem.error.value
  209. : progressingChatItem.streamContent.value);
  210. }),
  211. ),
  212. );
  213. }
  214. Container _buildAssistantChatItemContent(
  215. bool? isStreamStarted, String content) {
  216. return Container(
  217. padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
  218. margin: EdgeInsets.symmetric(vertical: 10.h),
  219. alignment: Alignment.centerLeft,
  220. constraints: BoxConstraints(
  221. maxWidth: 0.78.sw, // 65% of screen width
  222. ),
  223. decoration: BoxDecoration(
  224. border: isStreamStarted == null || isStreamStarted == true
  225. ? null
  226. : Border.all(color: ColorName.colorPrimary, width: 1.w),
  227. color: ColorName.white,
  228. borderRadius: BorderRadius.only(
  229. topRight: Radius.circular(20.w),
  230. bottomRight: Radius.circular(20.w),
  231. bottomLeft: Radius.circular(20.w))),
  232. child: isStreamStarted != null && isStreamStarted == false
  233. ? Lottie.asset("assets/anim/anim_chat_response_loading.zip",
  234. width: 46.w, height: 20.w)
  235. : SelectableText(content,
  236. style:
  237. TextStyle(fontSize: 14.w, color: ColorName.primaryTextColor)),
  238. );
  239. }
  240. Widget _buildUserChatItem(BuildContext context, ChatItem chatItem) {
  241. return Align(
  242. alignment: Alignment.centerRight,
  243. child: IntrinsicWidth(
  244. child: Container(
  245. padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
  246. margin: EdgeInsets.symmetric(vertical: 10.h),
  247. alignment: Alignment.centerRight,
  248. constraints: BoxConstraints(
  249. maxWidth: 0.78.sw, // 65% of screen width
  250. ),
  251. decoration: BoxDecoration(
  252. color: ColorName.colorPrimary,
  253. borderRadius: BorderRadius.only(
  254. topLeft: Radius.circular(16.w),
  255. bottomRight: Radius.circular(16.w),
  256. bottomLeft: Radius.circular(16.w))),
  257. child: Text(chatItem.content,
  258. style: TextStyle(fontSize: 14.w, color: ColorName.white)),
  259. ),
  260. ),
  261. );
  262. }
  263. Widget buildTopGradient() {
  264. return Container(
  265. width: 1.sw,
  266. height: 128.h,
  267. decoration: BoxDecoration(
  268. gradient: LinearGradient(
  269. colors: ['#E8EBFF'.toColor(), '#00E8EBFF'.toColor()],
  270. begin: Alignment.topCenter,
  271. end: Alignment.bottomCenter,
  272. stops: const [0.5, 1.0],
  273. ),
  274. ));
  275. }
  276. Widget buildBackgroundGradient() {
  277. return Container(
  278. width: 1.sw,
  279. height: 1.sh,
  280. decoration: BoxDecoration(
  281. gradient: LinearGradient(
  282. colors: ['#F2F8F4'.toColor(), '#F6F6F6'.toColor()],
  283. begin: Alignment.topCenter,
  284. end: Alignment.bottomCenter,
  285. stops: const [0, 1.0],
  286. ),
  287. ),
  288. );
  289. }
  290. }