view.dart 11 KB

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