import 'dart:convert'; import 'package:electronic_assistant/base/base_controller.dart'; import 'package:electronic_assistant/data/bean/agenda.dart'; import 'package:electronic_assistant/data/bean/chat_item.dart'; import 'package:electronic_assistant/data/bean/file_chat_item.dart'; import 'package:electronic_assistant/data/bean/progressing_chat_item.dart'; import 'package:electronic_assistant/data/bean/reference_chat_item.dart'; import 'package:electronic_assistant/data/bean/stream_chat_origin_data.dart'; import 'package:electronic_assistant/data/repositories/account_repository.dart'; import 'package:electronic_assistant/data/repositories/chat_repository.dart'; import 'package:electronic_assistant/data/repositories/talk_repository.dart'; import 'package:electronic_assistant/module/chat/start/view.dart'; import 'package:electronic_assistant/resource/colors.gen.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:uuid/uuid.dart'; import '../../data/bean/talks.dart'; import '../../data/consts/error_code.dart'; import '../../router/app_pages.dart'; import '../../utils/http_handler.dart'; class ChatController extends BaseController { final RefreshController refreshController = RefreshController(initialLoadStatus: LoadStatus.loading); final ScrollController listScrollController = ScrollController(); final TextEditingController inputController = TextEditingController(); final RxList chatItems = [].obs; final Rxn talkInfo = Rxn(); final Rxn agenda = Rxn(); @override void onInit() { super.onInit(); loadMoreHistory(); checkArguments(); } @override void onReady() { super.onReady(); if (accountRepository.userInfo?.profession == null || accountRepository.userInfo?.post == null) { showStartSheet(); } } void checkArguments() { List arguments = Get.arguments ?? []; if (arguments.isEmpty) { return; } if (arguments.length > 1 && arguments[1] is Agenda) { agenda.value = arguments[1]; } if (arguments[0] is TalkBean) { talkInfo.value = arguments[0]; sendInitialMessage(); } else if (arguments[0] is String) { talkRepository .talkInfo(arguments[0]) .then((response) => talkInfo.value = response.talkInfo) .then((_) => sendInitialMessage()); } } void sendInitialMessage() { final talkInfo = this.talkInfo.value; if (talkInfo == null) { return; } chatItems.insert( 0, FileChatItem(talkInfo, id: const Uuid().v4(), conversationId: "", role: "user", content: "", createTime: DateTime.now().toString())); if (agenda.value != null) { _sendMessage("以这份谈话为背景,你为我执行“${agenda.value?.content}”这一事项"); } } onSendClick() { if (inputController.text.isEmpty) { return; } String chatContent = inputController.text; inputController.clear(); _sendMessage(chatContent); } void onAddFileClick() { Get.toNamed(RoutePath.fileSearch)?.then((talkInfo) { if (talkInfo is TalkBean) { this.talkInfo.value = talkInfo; agenda.value = null; sendInitialMessage(); } }); } Future loadMoreHistory() { bool isEmpty = chatItems.isEmpty; return chatRepository .chatHistory(chatItems.isEmpty ? null : chatItems.last.id) .then((value) => chatItems.addAll(value)) .whenComplete(() => refreshController.loadComplete()) .whenComplete(() { if (isEmpty) { _scrollToBottom(); } }); } void showStartSheet() { WidgetsBinding.instance.addPostFrameCallback((_) { showModalBottomSheet( context: Get.context!, isScrollControlled: true, barrierColor: ColorName.black55, backgroundColor: ColorName.transparent, builder: (BuildContext context) { return const ChatStartPage(); }, ); }); } void _sendMessage(String chatContent) { chatItems.insert(0, createUserChatItem(chatContent)); ProgressingChatItem progressingChatItem = ProgressingChatItem( id: const Uuid().v4(), conversationId: chatItems.last.conversationId, role: "assistant", content: "", createTime: DateTime.now().toString(), ); chatItems.insert(0, progressingChatItem); _scrollToBottom(); chatRepository .streamChat(chatContent, talkId: talkInfo.value?.id, agendaId: agenda.value?.id) .then((stream) { stream.listen((event) { try { Map json = jsonDecode(event.data); if (json.isEmpty) { return; } StreamChatOriginData data = StreamChatOriginData.fromJson(json); if (data.choices == null || data.choices!.isEmpty) { return; } Delta? delta = data.choices![0].delta; if (delta == null) { return; } progressingChatItem.append(delta.content ?? ""); } catch (ignore) {} }, onDone: () { progressingChatItem.content = progressingChatItem.streamContent.value; progressingChatItem.isFinished.value = true; }, onError: (error) { progressingChatItem.isFailed.value = true; progressingChatItem.error.value = "网络错误,请检查网络连接"; debugPrint("error: $error"); debugPrintStack(); }); }).catchError((error) { progressingChatItem.isFailed.value = true; if (error is ServerErrorException) { progressingChatItem.error.value = error.message ?? "服务出错,请稍后再试"; if (error.code == ErrorCode.errorCodeNoLogin) { Get.toNamed(RoutePath.login)?.then((loginSuccess) async { if (loginSuccess != null && loginSuccess) { _clearChat(); await loadMoreHistory(); _sendMessage(chatContent); } }); } else if (error.code == ErrorCode.errorCodeNoProfession) { showStartSheet(); } } else { progressingChatItem.error.value = "网络错误,请检查网络连接"; debugPrint("error: $error"); debugPrintStack(); } }); } void _clearChat() async { return chatItems.clear(); } void _scrollToBottom() { Future.delayed(const Duration(milliseconds: 300), () { listScrollController.animateTo( 0, duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ); }); } createUserChatItem(String chatContent) { final id = const Uuid().v4(); final conversationId = chatItems.isEmpty ? "" : chatItems.last.conversationId; const role = "user"; final content = chatContent; final createTime = DateTime.now().toString(); final talkInfo = this.talkInfo.value; return talkInfo == null ? ChatItem( id: id, conversationId: conversationId, role: role, content: chatContent, createTime: createTime) : ReferenceChatItem( talkInfo: talkInfo, id: id, conversationId: conversationId, role: role, content: content, createTime: createTime); } onDeleteReference() { talkInfo.value = null; agenda.value = null; } }