import 'dart:async'; 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/consts/event_report_id.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/handler/event_handler.dart'; import 'package:electronic_assistant/module/chat/start/view.dart'; import 'package:electronic_assistant/module/chat/view.dart'; import 'package:electronic_assistant/resource/colors.gen.dart'; import 'package:electronic_assistant/resource/string.gen.dart'; import 'package:electronic_assistant/utils/toast_util.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:uuid/uuid.dart'; import '../../data/bean/stream_chat_origin_data.dart'; import '../../data/bean/talks.dart'; import '../../data/consts/error_code.dart'; import '../../dialog/model_explain_dialog.dart'; import '../../router/app_pages.dart'; import '../../utils/http_handler.dart'; import '../../widget/gradually_md_text.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(); final Rxn chatAiTagId = Rxn(); ChatFromType? fromType; bool isConsumeElectric = false; StreamSubscription? _streamChatSubscription; ProgressingChatItem? _nowProgressingChatItem; @override void onInit() { super.onInit(); loadMoreHistory(); checkArguments(); initReport(); } @override void onReady() { super.onReady(); if (accountRepository.userInfo.value?.profession == null || accountRepository.userInfo.value?.post == null) { showStartSheet(); } else { _showModelExplainDialog(); } } void checkArguments() { List arguments = Get.arguments ?? []; if (arguments.isEmpty) { return; } if (arguments[0] is ChatFromType) { fromType = arguments[0]; } if (arguments.length > 2 && arguments[2] is Agenda) { agenda.value = arguments[2]; } if (arguments.length > 1 && arguments[1] is TalkBean) { talkInfo.value = arguments[1]; sendInitialMessage(); } else if (arguments.length > 1 && arguments[1] is String) { talkRepository .talkInfo(arguments[1]) .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; _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) { for (var element in chatItems.reversed) { if (element is ChatItem && element.role == "assistant") { chatAiTagId.value = element.id; } } _scrollToBottom(); } }); } void showStartSheet() { if (fromType == ChatFromType.fromMain) { EventHandler.report(EventId.event_102001, params: { EventId.id: EventId.id_001, }); } else if (fromType == ChatFromType.fromTalkDetail) { EventHandler.report(EventId.event_102001, params: { EventId.id: EventId.id_002, }); } else if (fromType == ChatFromType.fromAnalysisBtn) { EventHandler.report(EventId.event_102001, params: { EventId.id: EventId.id_003, }); } Get.bottomSheet(const ChatStartPage(), isScrollControlled: true, barrierColor: ColorName.black55, backgroundColor: ColorName.transparent) .then((result) { if (result == null || result == false) { Get.back(); } else { _showModelExplainDialog(); } }); } void _showModelExplainDialog() { showModelExplainTipsDialog(); } void _sendMessage(String chatContent) { final lastItem = chatItems.first; if ((lastItem is ProgressingChatItem) && !(lastItem.isFinished.value || lastItem.isFailed.value)) { ToastUtil.showToast(StringName.chatReplying.tr, displayType: SmartToastType.onlyRefresh); return; } inputController.clear(); chatItems.insert(0, createUserChatItem(chatContent)); ProgressingChatItem progressingChatItem = ProgressingChatItem( id: const Uuid().v4(), graduallyController: GraduallyController(), conversationId: chatItems.last.conversationId, role: "assistant", content: "", createTime: DateTime.now().toString(), ); chatItems.insert(0, progressingChatItem); _nowProgressingChatItem?.graduallyController.dispose(); _nowProgressingChatItem = progressingChatItem; _scrollToBottom(); if (talkInfo.value == null) { EventHandler.report(EventId.event_102003, params: { EventId.id: EventId.id_002, }); } else { EventHandler.report(EventId.event_102003, params: { EventId.id: EventId.id_001, }); } _streamChatSubscription?.cancel(); chatRepository .streamChat(chatContent, talkId: talkInfo.value?.id, agendaId: agenda.value?.id) .then((stream) { progressingChatItem.setGraduallyFinishedListener(); _streamChatSubscription = 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; } isConsumeElectric = true; progressingChatItem.append(delta.content ?? ""); chatAiTagId.value = progressingChatItem.id; } catch (ignore) {} }, onDone: () { progressingChatItem.setAppendDone(); progressingChatItem.content = progressingChatItem.graduallyController.graduallyTxt; }, 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; } void initReport() { if (fromType == ChatFromType.fromMain) { EventHandler.report(EventId.event_102002, params: { EventId.id: EventId.id_001, }); } else if (fromType == ChatFromType.fromTalkDetail) { EventHandler.report(EventId.event_102002, params: { EventId.id: EventId.id_002, }); } else if (fromType == ChatFromType.fromAnalysisBtn) { EventHandler.report(EventId.event_102002, params: { EventId.id: EventId.id_003, }); } } void onCopyClick(String? content) { if (content != null) { Clipboard.setData(ClipboardData(text: content)); } ToastUtil.showToast(StringName.copySuccess.tr); } @override void onClose() { super.onClose(); if (isConsumeElectric) { accountRepository.refreshUserInfo(); } _nowProgressingChatItem?.graduallyController.dispose(); _streamChatSubscription?.cancel(); } }