view.dart 11 KB

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