Files
ai_chat_assistant/lib/services/message_service.dart

400 lines
13 KiB
Dart
Raw Normal View History

2025-08-18 11:26:58 +08:00
import 'dart:async';
2025-08-12 13:36:42 +08:00
import 'package:ai_chat_assistant/utils/common_util.dart';
2025-08-15 09:37:53 +08:00
import 'package:ai_chat_assistant/utils/tts_util.dart';
2025-08-12 13:36:42 +08:00
import 'package:basic_intl/intl.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
2025-08-12 13:36:42 +08:00
import 'package:permission_handler/permission_handler.dart';
import 'package:uuid/uuid.dart';
import '../enums/vehicle_command_type.dart';
import '../enums/message_service_state.dart';
import '../enums/message_status.dart';
import '../models/chat_message.dart';
import '../models/vehicle_cmd.dart';
import '../services/chat_sse_service.dart';
import '../services/classification_service.dart';
import '../services/control_recognition_service.dart';
// import '../services/audio_recorder_service.dart';
// import '../services/voice_recognition_service.dart';
2025-08-12 13:36:42 +08:00
import 'command_service.dart';
import 'package:fluttertoast/fluttertoast.dart';
class MessageService extends ChangeNotifier {
2025-08-18 11:26:58 +08:00
static const MethodChannel _asrChannel = MethodChannel('com.example.ai_chat_assistant/asr');
2025-08-12 13:36:42 +08:00
static final MessageService _instance = MessageService._internal();
factory MessageService() => _instance;
2025-08-18 11:26:58 +08:00
Completer<String>? _asrCompleter;
MessageService._internal() {
2025-08-18 11:26:58 +08:00
_asrChannel.setMethodCallHandler((call) async {
switch (call.method) {
case "onAsrResult":
replaceMessage(id: _latestUserMessageId!, text: call.arguments);
break;
case "onAsrStop":
replaceMessage(
id: _latestUserMessageId!, status: MessageStatus.normal);
2025-08-18 11:26:58 +08:00
if (_asrCompleter != null && !_asrCompleter!.isCompleted) {
_asrCompleter!.complete(messages.last.text);
}
break;
}
});
}
2025-08-12 13:36:42 +08:00
final ChatSseService _chatSseService = ChatSseService();
2025-08-13 16:00:23 +08:00
// final LocalTtsService _ttsService = LocalTtsService();
// final AudioRecorderService _audioService = AudioRecorderService();
// final VoiceRecognitionService _recognitionService = VoiceRecognitionService();
2025-08-12 13:36:42 +08:00
final TextClassificationService _classificationService =
TextClassificationService();
final VehicleCommandService _vehicleCommandService = VehicleCommandService();
2025-08-13 15:17:13 +08:00
final List<ChatMessage> _messages = [];
2025-08-12 13:36:42 +08:00
List<ChatMessage> get messages => List.unmodifiable(_messages);
String? _latestUserMessageId;
String? _latestAssistantMessageId;
MessageServiceState _state = MessageServiceState.idle;
bool get isRecording => _state == MessageServiceState.recording;
bool get isRecognizing => _state == MessageServiceState.recognizing;
bool get isReplying => _state == MessageServiceState.replying;
bool get isBusy => _state != MessageServiceState.idle;
bool _isReplyAborted = true;
2025-08-13 15:17:13 +08:00
void initializeEmpty() {
_messages.clear();
notifyListeners();
}
2025-08-12 13:36:42 +08:00
Future<void> startVoiceInput() async {
if (await Permission.microphone.status == PermissionStatus.denied) {
PermissionStatus status = await Permission.microphone.request();
if (status == PermissionStatus.permanentlyDenied) {
await Fluttertoast.showToast(
msg: Intl.getCurrentLocale().startsWith('zh')
? "请在设置中开启麦克风权限"
: "Please enable microphone in the settings");
}
return;
}
if (_state != MessageServiceState.idle) {
return;
}
try {
abortReply();
_latestUserMessageId = null;
_latestAssistantMessageId = null;
_isReplyAborted = false;
changeState(MessageServiceState.recording);
addMessage("", true, MessageStatus.listening);
_latestUserMessageId = messages.last.id;
2025-08-18 11:26:58 +08:00
_asrChannel.invokeMethod("start");
2025-08-12 13:36:42 +08:00
} catch (e) {
print('录音开始出错: $e');
}
}
void changeState(MessageServiceState state) {
_state = state;
notifyListeners();
}
void addMessage(String text, bool isUser, MessageStatus status) {
_messages.add(ChatMessage(
id: Uuid().v1(),
text: text,
isUser: isUser,
timestamp: DateTime.now(),
status: status));
notifyListeners();
}
Future<void> stopAndProcessVoiceInput() async {
if (_state != MessageServiceState.recording) {
changeState(MessageServiceState.idle);
return;
}
try {
changeState(MessageServiceState.recognizing);
2025-08-18 11:26:58 +08:00
_asrChannel.invokeMethod("stop");
_asrCompleter = Completer<String>();
final recognizedText = await _asrCompleter!.future;
// final audioData = await _audioService.stopRecording();
// replaceMessage(
// id: _latestUserMessageId!,
// text: "",
// status: MessageStatus.recognizing);
// if (audioData == null || audioData.isEmpty) {
// removeMessageById(_latestUserMessageId!);
// return;
// }
// final recognizedText =
// await _recognitionService.recognizeSpeech(audioData);
// if (recognizedText == null || recognizedText.isEmpty) {
// removeMessageById(_latestUserMessageId!);
// return;
// }
// replaceMessage(
// id: _latestUserMessageId!,
// text: recognizedText,
// status: MessageStatus.normal);
2025-08-12 13:36:42 +08:00
changeState(MessageServiceState.replying);
await reply(recognizedText);
} catch (e) {
2025-08-18 11:26:58 +08:00
print(e);
2025-08-12 13:36:42 +08:00
} finally {
changeState(MessageServiceState.idle);
2025-08-18 11:26:58 +08:00
_asrCompleter = null;
2025-08-12 13:36:42 +08:00
}
}
int findMessageIndexById(String? id) {
return id == null ? -1 : _messages.indexWhere((msg) => msg.id == id);
}
void replaceMessage({
required String id,
String? text,
MessageStatus? status,
}) {
final index = findMessageIndexById(id);
if (index == -1) {
return;
}
final message = _messages[index];
_messages[index] = ChatMessage(
id: message.id,
text: text ?? message.text,
isUser: message.isUser,
timestamp: message.timestamp,
status: status ?? message.status,
);
notifyListeners();
}
Future<void> reply(String text) async {
addMessage("", false, MessageStatus.thinking);
_latestAssistantMessageId = messages.last.id;
bool isChinese = CommonUtil.containChinese(text);
try {
if (_isReplyAborted) {
return;
}
final category = await _classificationService.classifyText(text);
switch (category) {
case -1:
await occurError(isChinese);
break;
case 2:
await handleVehicleControl(text, isChinese);
break;
case 4:
await answerWrongQuestion(isChinese);
break;
default:
await answerQuestion(text, isChinese);
break;
}
} catch (e) {
await occurError(isChinese);
}
}
Future<void> handleVehicleControl(String text, bool isChinese) async {
if (_isReplyAborted) {
return;
}
final vehicleCommandResponse =
await _vehicleCommandService.getCommandFromText(text);
if (vehicleCommandResponse == null) {
if (_isReplyAborted) {
return;
}
String msg = isChinese
? "无法识别车辆控制命令,请重试"
: "Cannot recognize the vehicle control command, please try again";
replaceMessage(
id: _latestAssistantMessageId!,
text: msg,
status: MessageStatus.normal);
} else {
if (_isReplyAborted) {
return;
}
replaceMessage(
id: _latestAssistantMessageId!,
text: vehicleCommandResponse.tips!,
status: MessageStatus.executing);
2025-08-15 09:37:53 +08:00
if (!_isReplyAborted) {
if (await TtsUtil.start(isChinese) == true) {
TtsUtil.send(vehicleCommandResponse.tips!);
}
}
2025-08-12 13:36:42 +08:00
bool containOpenAC = false;
for (var command in vehicleCommandResponse.commands) {
if (_isReplyAborted) {
return;
}
if (command.type == VehicleCommandType.unknown ||
command.error.isNotEmpty) {
continue;
}
if (command.type == VehicleCommandType.openAC) {
containOpenAC = true;
}
if (containOpenAC && command.type == VehicleCommandType.changeACTemp) {
await Future.delayed(const Duration(milliseconds: 2000),
() => processCommand(command, isChinese));
} else {
await processCommand(command, isChinese);
}
}
replaceMessage(
id: _latestAssistantMessageId!,
text: vehicleCommandResponse.tips!,
status: MessageStatus.normal);
notifyListeners();
}
}
Future<void> processCommand(VehicleCommand command, bool isChinese) async {
String msg;
MessageStatus status;
final (isSuccess, result) = await CommandService.executeCommand(
command.type,
params: command.params);
if (command.type == VehicleCommandType.locateCar) {
msg = isChinese
? "很抱歉,定位车辆位置失败"
: "Sorry, locate the vehicle unsuccessfully";
status = MessageStatus.failure;
if (isSuccess && result != null && result.isNotEmpty) {
String address = result['address'];
if (address.isNotEmpty) {
msg = isChinese
? "已为您找到车辆位置:\"$address\""
: "The vehicle location is \"$address\"";
status = MessageStatus.success;
}
}
} else {
if (isSuccess) {
msg = isChinese
? "为您执行\"${command.type.chinese}\"成功"
: "Execute \"${command.type.english}\" successful";
status = MessageStatus.success;
} else {
msg = isChinese
? "很抱歉,\"${command.type.chinese}\"失败"
: "Sorry, execute \"${command.type.english}\" unsuccessfully";
status = MessageStatus.failure;
}
}
addMessage(msg, false, status);
}
Future<void> answerWrongQuestion(bool isChinese) async {
if (_isReplyAborted) {
return;
}
const chineseAnswer = "尊敬的ID.UNYX车主\n" +
"很抱歉,我暂时无法理解您的需求,请您更详细地描述,以便我能更好地为您服务。\n" +
"作为您的智能助手,我专注于以下服务:\n" +
"1. ID.UNYX车型功能咨询例如如何调节座椅记忆功能\n" +
"2. 日常用车问题解答,例如:充电注意事项有哪些?\n" +
"3. 基础车辆控制协助,例如:帮我打开空调。";
const englishAnswer = "Dear ID.UNYX Owner,\n" +
"I'm sorry that unable to understand your request currently. Please describe it in more detail so that I can better assist you.\n" +
"As your intelligent assistant, I specialize in the following services:\n" +
"1. ID.UNYX model feature consultation, such as: How do I adjust the seat memory function?\n" +
"2. Daily vehicle usage inquiries, such as: What are the charging precautions?\n" +
"3. Basic vehicle control assistance, such as: Help me turn on the air conditioning.";
replaceMessage(
id: _latestAssistantMessageId!,
text: isChinese ? chineseAnswer : englishAnswer,
status: MessageStatus.normal);
}
Future<void> answerQuestion(String text, isChinese) async {
if (_isReplyAborted) {
return;
}
_chatSseService.request(
messageId: _latestAssistantMessageId!,
text: text,
2025-08-15 09:37:53 +08:00
isChinese: isChinese,
2025-08-12 13:36:42 +08:00
onStreamResponse: handleStreamResponse,
);
}
Future<void> occurError(bool isChinese) async {
if (_isReplyAborted) {
return;
}
replaceMessage(
id: _latestAssistantMessageId!,
text: isChinese
? "很抱歉,众众暂时无法回答这个问题,请重试"
: "Sorry, I can not answer this question for the moment, please try again",
status: MessageStatus.normal);
}
2025-08-15 09:37:53 +08:00
void handleStreamResponse(String messageId, String responseText, bool isComplete) {
2025-08-12 13:36:42 +08:00
if (_isReplyAborted) {
return;
}
try {
if (isComplete) {
replaceMessage(
id: messageId,
text: responseText,
status: MessageStatus.completed,
);
} else {
replaceMessage(
id: messageId, text: responseText, status: MessageStatus.thinking);
}
} catch (e) {
abortReply();
}
}
Future<void> abortReply() async {
_isReplyAborted = true;
_chatSseService.abort();
int index = findMessageIndexById(_latestAssistantMessageId);
if (index == -1 || messages[index].status != MessageStatus.thinking) {
return;
}
replaceMessage(
id: _latestAssistantMessageId!, status: MessageStatus.aborted);
}
void removeMessageById(String id) {
_messages.removeWhere((msg) => msg.id == id);
notifyListeners();
}
void clearMessages() {
_messages.clear();
notifyListeners();
}
void removeNonListeningMessages() {
_messages.removeWhere((message) => message.status != MessageStatus.listening);
notifyListeners();
}
2025-08-12 13:36:42 +08:00
}