Files
ai_chat_assistant/lib/services/message_service.dart
2025-08-13 15:17:13 +08:00

378 lines
12 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:io';
import 'package:ai_chat_assistant/utils/common_util.dart';
import 'package:basic_intl/intl.dart';
import 'package:flutter/foundation.dart';
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';
import '../services/tts_service.dart';
import 'command_service.dart';
import 'package:fluttertoast/fluttertoast.dart';
class MessageService extends ChangeNotifier {
static final MessageService _instance = MessageService._internal();
factory MessageService() => _instance;
MessageService._internal();
final ChatSseService _chatSseService = ChatSseService();
final LocalTtsService _ttsService = LocalTtsService();
final AudioRecorderService _audioService = AudioRecorderService();
final VoiceRecognitionService _recognitionService = VoiceRecognitionService();
final TextClassificationService _classificationService =
TextClassificationService();
final VehicleCommandService _vehicleCommandService = VehicleCommandService();
final List<ChatMessage> _messages = [];
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;
void initializeEmpty() {
_messages.clear();
notifyListeners();
}
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);
await _audioService.startRecording();
addMessage("", true, MessageStatus.listening);
_latestUserMessageId = messages.last.id;
} 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);
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);
changeState(MessageServiceState.replying);
await reply(recognizedText);
} catch (e) {
// todo
} finally {
changeState(MessageServiceState.idle);
}
}
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);
if (!_isReplyAborted) {
_ttsService.pushTextForStreamTTS(vehicleCommandResponse.tips!);
_ttsService.markSSEStreamCompleted();
}
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,
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);
}
void handleStreamResponse(String messageId, String responseText,
String completeText, bool isComplete) {
if (_isReplyAborted) {
return;
}
try {
if (isComplete) {
if (completeText.isNotEmpty) {
_ttsService.pushTextForStreamTTS(completeText);
}
_ttsService.markSSEStreamCompleted();
replaceMessage(
id: messageId,
text: responseText,
status: MessageStatus.completed,
);
} else {
if (completeText.isNotEmpty) {
_ttsService.pushTextForStreamTTS(completeText);
}
replaceMessage(
id: messageId, text: responseText, status: MessageStatus.thinking);
}
} catch (e) {
abortReply();
}
}
Future<void> abortReply() async {
_isReplyAborted = true;
_ttsService.stop();
_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();
}
}