You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
944 lines
26 KiB
944 lines
26 KiB
import 'package:get/get.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:get_storage/get_storage.dart';
|
|
import 'dart:convert';
|
|
import 'package:flutter/rendering.dart';
|
|
import '../../../data/services/volcano_ai_service.dart';
|
|
import '../../../data/services/volcano_tts_api_service.dart';
|
|
import '../models/message_model.dart';
|
|
import '../controllers/voice_input_controller.dart';
|
|
import 'dart:async';
|
|
import '../../../data/providers/agent_provider.dart';
|
|
|
|
class ChatController extends GetxController {
|
|
String agentId = '';
|
|
String agentName = '';
|
|
String agentAvatar = '';
|
|
String? agentSubtitle;
|
|
String systemPrompt = '';
|
|
String welcomeMessage = '';
|
|
bool playVoiceOnEnter = false;
|
|
String agentVoice = 'zh_female_meilinvyou_moon_bigtts';
|
|
|
|
final VolcanoAIService _aiService = VolcanoAIService();
|
|
late final VolcanoTtsApiService _ttsService;
|
|
static const String _storagePrefix = 'chat_history_';
|
|
|
|
final messages = <Message>[].obs;
|
|
final isLoading = false.obs;
|
|
late final TextEditingController messageController;
|
|
late final ScrollController scrollController;
|
|
final _storage = GetStorage();
|
|
|
|
// TTS启用状态
|
|
final isTtsEnabled = true.obs;
|
|
|
|
// 用于存储当前正在流式生成的消息
|
|
final RxString currentStreamMessage = ''.obs;
|
|
Message? _currentAssistantMessage;
|
|
bool _isDisposed = false;
|
|
|
|
// TTS事件订阅
|
|
StreamSubscription? _ttsEventSubscription;
|
|
bool _isTtsSpeaking = false;
|
|
|
|
// 语音输入相关状态
|
|
final isRecording = false.obs;
|
|
final recordingText = ''.obs;
|
|
final isVoiceInputVisible = false.obs;
|
|
final isVoiceConnecting = true.obs;
|
|
final isVoiceMuted = false.obs;
|
|
Message? _currentVoiceMessage;
|
|
|
|
// 添加一个标志,表示当前是否有正在进行的语音识别
|
|
final isRecognizing = false.obs;
|
|
|
|
// 输入框状态
|
|
final isInputCollapsed = false.obs;
|
|
final isVoiceMode = true.obs; // 默认为语音模式
|
|
final isRecordingVoice = false.obs; // 是否正在录音(按住说话状态)
|
|
|
|
// 添加是否在底部的状态变量
|
|
final RxBool isAtBottom = true.obs;
|
|
|
|
// 切换输入框折叠状态
|
|
void toggleInputCollapsed() {
|
|
isInputCollapsed.value = !isInputCollapsed.value;
|
|
}
|
|
|
|
// 切换语音/文本输入模式
|
|
void toggleInputMode() {
|
|
isVoiceMode.value = !isVoiceMode.value;
|
|
}
|
|
|
|
// 开始语音输入
|
|
void startVoiceInput() {
|
|
// 确保先停止任何可能正在进行的语音识别会话
|
|
if (Get.isRegistered<VoiceInputController>()) {
|
|
final voiceController = Get.find<VoiceInputController>();
|
|
voiceController.stopRecording();
|
|
Get.delete<VoiceInputController>();
|
|
}
|
|
|
|
// 如果TTS正在播放,先停止它
|
|
if (_isTtsSpeaking) {
|
|
try {
|
|
// 立即停止当前声音播放
|
|
_ttsService.stop();
|
|
// 结束当前TTS会话
|
|
_ttsService.endSession();
|
|
_isTtsSpeaking = false;
|
|
} catch (e) {
|
|
debugPrint('停止TTS播放失败: $e');
|
|
}
|
|
}
|
|
|
|
// 如果正在处理AI响应,中断处理
|
|
if (isLoading.value) {
|
|
isLoading.value = false;
|
|
// 清除当前正在生成的消息
|
|
if (_currentAssistantMessage != null) {
|
|
// 如果消息内容为空,则移除该消息
|
|
if (_currentAssistantMessage!.content.isEmpty) {
|
|
messages.remove(_currentAssistantMessage);
|
|
}
|
|
_currentAssistantMessage = null;
|
|
}
|
|
currentStreamMessage.value = '';
|
|
}
|
|
|
|
isVoiceInputVisible.value = true;
|
|
isVoiceConnecting.value = true;
|
|
isRecording.value = false;
|
|
recordingText.value = '';
|
|
isRecognizing.value = false;
|
|
|
|
// 当语音面板打开时,确保ListView滚动到适当位置,防止最后的消息被遮挡
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (!_isDisposed && scrollController.hasClients && messages.isNotEmpty) {
|
|
// 计算需要额外滚动的距离(语音面板高度)
|
|
final extraScrollDistance = 120.0;
|
|
|
|
// 获取当前滚动位置
|
|
final currentPosition = scrollController.position.pixels;
|
|
final maxScrollExtent = scrollController.position.maxScrollExtent;
|
|
|
|
// 如果已经接近底部,则向上滚动一定距离,确保最后的消息可见
|
|
if (maxScrollExtent - currentPosition < extraScrollDistance) {
|
|
scrollController.animateTo(
|
|
currentPosition + extraScrollDistance,
|
|
duration: const Duration(milliseconds: 300),
|
|
curve: Curves.easeOut,
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
// 创建语音输入控制器
|
|
Get.put(VoiceInputController(
|
|
onRecordingResult: handleVoiceResult,
|
|
onClosePanel: stopVoiceInput,
|
|
onRecognizing: handleRecognizing,
|
|
));
|
|
|
|
// 滚动到底部
|
|
_scrollToBottom(animate: true);
|
|
}
|
|
|
|
void startRecording() {
|
|
if (!isVoiceConnecting.value && isVoiceInputVisible.value) {
|
|
// 不再需要禁用TTS,因为已启用回声消除
|
|
|
|
// 当用户开始说话时,停止当前TTS播放
|
|
if (_isTtsSpeaking) {
|
|
try {
|
|
// 立即停止当前声音播放
|
|
_ttsService.stop();
|
|
// 结束当前TTS会话
|
|
_ttsService.endSession();
|
|
_isTtsSpeaking = false;
|
|
} catch (e) {
|
|
debugPrint('停止TTS播放失败: $e');
|
|
}
|
|
}
|
|
|
|
// 如果正在处理AI响应,中断处理
|
|
if (isLoading.value) {
|
|
isLoading.value = false;
|
|
// 清除当前正在生成的消息
|
|
if (_currentAssistantMessage != null) {
|
|
// 如果消息内容为空,则移除该消息
|
|
if (_currentAssistantMessage!.content.isEmpty) {
|
|
messages.remove(_currentAssistantMessage);
|
|
}
|
|
_currentAssistantMessage = null;
|
|
}
|
|
currentStreamMessage.value = '';
|
|
}
|
|
|
|
isRecording.value = true;
|
|
}
|
|
}
|
|
|
|
void stopRecording() {
|
|
if (isRecording.value) {
|
|
isRecording.value = false;
|
|
}
|
|
}
|
|
|
|
void toggleMute() {
|
|
isVoiceMuted.value = !isVoiceMuted.value;
|
|
}
|
|
|
|
void stopVoiceInput() {
|
|
// 停止语音识别
|
|
if (Get.isRegistered<VoiceInputController>()) {
|
|
try {
|
|
Get.find<VoiceInputController>().stopRecording();
|
|
Get.delete<VoiceInputController>();
|
|
} catch (e) {
|
|
debugPrint('停止语音输入失败: $e');
|
|
}
|
|
}
|
|
|
|
// 重置状态
|
|
isVoiceInputVisible.value = false;
|
|
isVoiceConnecting.value = true;
|
|
isRecording.value = false;
|
|
recordingText.value = '';
|
|
isRecognizing.value = false;
|
|
|
|
// 清理临时语音消息
|
|
if (_currentVoiceMessage != null && (_currentVoiceMessage!.content == '🎤 ...' || _currentVoiceMessage!.content.startsWith('🎤 '))) {
|
|
messages.remove(_currentVoiceMessage);
|
|
}
|
|
_currentVoiceMessage = null;
|
|
|
|
// 滚动到底部
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (!_isDisposed && messages.isNotEmpty) {
|
|
_scrollToBottom(animate: true);
|
|
}
|
|
});
|
|
|
|
// 重置按住说话状态
|
|
|
|
// 删除语音输入控制器
|
|
try {
|
|
if (Get.isRegistered<VoiceInputController>()) {
|
|
Get.delete<VoiceInputController>();
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Error deleting VoiceInputController: $e');
|
|
}
|
|
|
|
// 确保按住说话状态被重置
|
|
isRecordingVoice.value = false;
|
|
}
|
|
|
|
void handleRecognizing(String text) {
|
|
if (text.isEmpty) return;
|
|
|
|
// 如果TTS正在播放,确保停止它
|
|
if (_isTtsSpeaking) {
|
|
try {
|
|
// 立即停止当前声音播放
|
|
_ttsService.stop();
|
|
// 不需要每次都结束会话,只需要停止当前播放
|
|
_isTtsSpeaking = false;
|
|
} catch (e) {
|
|
debugPrint('停止TTS播放失败: $e');
|
|
}
|
|
}
|
|
|
|
// 更新识别中的文本
|
|
recordingText.value = text;
|
|
|
|
// 如果还没有创建临时语音消息,则创建一个
|
|
if (_currentVoiceMessage == null) {
|
|
_currentVoiceMessage = Message(
|
|
role: 'user',
|
|
content: '🎤 $text',
|
|
timestamp: DateTime.now(),
|
|
);
|
|
messages.add(_currentVoiceMessage!);
|
|
isRecognizing.value = true;
|
|
|
|
// 滚动到底部
|
|
_scrollToBottom(animate: true);
|
|
} else {
|
|
// 更新现有的临时语音消息
|
|
final index = messages.indexWhere((msg) =>
|
|
msg.timestamp == _currentVoiceMessage!.timestamp &&
|
|
msg.role == _currentVoiceMessage!.role);
|
|
|
|
if (index != -1) {
|
|
messages[index] = Message(
|
|
role: 'user',
|
|
content: '🎤 $text',
|
|
timestamp: _currentVoiceMessage!.timestamp,
|
|
);
|
|
|
|
// 滚动到底部
|
|
_scrollToBottom(animate: false);
|
|
}
|
|
}
|
|
}
|
|
|
|
void handleVoiceResult(String text) {
|
|
if (text.isEmpty) return;
|
|
|
|
Message userMessage;
|
|
|
|
// 处理现有的临时语音消息
|
|
if (_currentVoiceMessage != null) {
|
|
final index = messages.indexWhere((msg) =>
|
|
msg.timestamp == _currentVoiceMessage!.timestamp &&
|
|
msg.role == _currentVoiceMessage!.role);
|
|
|
|
if (index != -1) {
|
|
// 更新消息,移除麦克风图标
|
|
userMessage = Message(
|
|
role: 'user',
|
|
content: text,
|
|
timestamp: _currentVoiceMessage!.timestamp,
|
|
);
|
|
messages[index] = userMessage;
|
|
} else {
|
|
// 创建新消息
|
|
userMessage = Message(
|
|
role: 'user',
|
|
content: text,
|
|
timestamp: DateTime.now(),
|
|
);
|
|
messages.add(userMessage);
|
|
}
|
|
} else {
|
|
// 创建新消息
|
|
userMessage = Message(
|
|
role: 'user',
|
|
content: text,
|
|
timestamp: DateTime.now(),
|
|
);
|
|
messages.add(userMessage);
|
|
}
|
|
|
|
// 保存聊天历史
|
|
_saveChatHistory();
|
|
|
|
// 滚动到底部
|
|
_scrollToBottom(animate: true);
|
|
|
|
// 重置状态
|
|
_currentVoiceMessage = null;
|
|
isRecognizing.value = false;
|
|
|
|
// 处理AI响应
|
|
_processAIResponse(userMessage);
|
|
}
|
|
|
|
// 处理AI响应
|
|
Future<void> _processAIResponse(Message userMessage) async {
|
|
if (_isDisposed) return;
|
|
|
|
// TTS会话状态
|
|
bool ttsSessionStarted = false;
|
|
final speaker = agentVoice;
|
|
|
|
// 创建一个标志,用于跟踪流是否应该继续处理
|
|
bool shouldContinueProcessing = true;
|
|
|
|
// 创建监听器,用于检测用户交互
|
|
StreamSubscription? voiceInputSubscription;
|
|
StreamSubscription? voicePanelSubscription;
|
|
|
|
try {
|
|
isLoading.value = true;
|
|
|
|
// 创建助手消息
|
|
currentStreamMessage.value = '';
|
|
_currentAssistantMessage = Message(
|
|
role: 'assistant',
|
|
content: '',
|
|
timestamp: DateTime.now(),
|
|
);
|
|
messages.add(_currentAssistantMessage!);
|
|
|
|
// 准备TTS会话
|
|
if (isTtsEnabled.value) {
|
|
await _ttsService.endSession();
|
|
|
|
if (!_ttsService.isConnected.value) {
|
|
await _ttsService.connect();
|
|
}
|
|
|
|
ttsSessionStarted = await _ttsService.startSession(speaker);
|
|
}
|
|
|
|
// 设置监听器,检测用户是否开始说话
|
|
voiceInputSubscription = isRecording.listen((isRecordingNow) {
|
|
if (isRecordingNow) {
|
|
// 用户开始说话,设置标志为false
|
|
shouldContinueProcessing = false;
|
|
|
|
// 如果TTS正在播放,立即停止
|
|
if (_isTtsSpeaking) {
|
|
try {
|
|
_ttsService.stop();
|
|
_isTtsSpeaking = false;
|
|
} catch (e) {
|
|
debugPrint('停止TTS播放失败: $e');
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// 设置监听器,检测用户是否打开语音输入面板
|
|
voicePanelSubscription = isVoiceInputVisible.listen((isVisible) {
|
|
if (isVisible) {
|
|
// 用户打开语音输入面板,设置标志为false
|
|
shouldContinueProcessing = false;
|
|
|
|
// 如果TTS正在播放,立即停止
|
|
if (_isTtsSpeaking) {
|
|
try {
|
|
_ttsService.stop();
|
|
_isTtsSpeaking = false;
|
|
} catch (e) {
|
|
debugPrint('停止TTS播放失败: $e');
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// 处理AI流式响应
|
|
await for (final chunk in _aiService.sendMessageStream(
|
|
messages: messages.map((m) => {'role': m.role, 'content': m.content}).toList(),
|
|
systemPrompt: systemPrompt,
|
|
)) {
|
|
// 检查是否应该继续处理
|
|
if (_isDisposed || !shouldContinueProcessing) {
|
|
// 用户开始说话或控制器已销毁,中止流处理
|
|
break;
|
|
}
|
|
|
|
// 更新UI
|
|
currentStreamMessage.value += chunk;
|
|
_updateAssistantMessage(currentStreamMessage.value);
|
|
|
|
// 直接将文本片段发送到TTS接口
|
|
if (isTtsEnabled.value && ttsSessionStarted && chunk.isNotEmpty) {
|
|
await _ttsService.speak(chunk, speaker: speaker);
|
|
}
|
|
}
|
|
|
|
// 结束TTS会话
|
|
if (isTtsEnabled.value && ttsSessionStarted) {
|
|
await _ttsService.endSession();
|
|
}
|
|
|
|
// 保存聊天历史
|
|
if (!_isDisposed) {
|
|
await _saveChatHistory();
|
|
}
|
|
} catch (e) {
|
|
// 错误处理
|
|
if (!_isDisposed) {
|
|
if (_currentAssistantMessage != null) {
|
|
messages.remove(_currentAssistantMessage);
|
|
}
|
|
|
|
Get.snackbar('Error', 'Failed to get response from AI', snackPosition: SnackPosition.BOTTOM);
|
|
|
|
// 结束TTS会话
|
|
if (isTtsEnabled.value && ttsSessionStarted) {
|
|
await _ttsService.endSession();
|
|
}
|
|
}
|
|
} finally {
|
|
// 取消监听器
|
|
voiceInputSubscription?.cancel();
|
|
voicePanelSubscription?.cancel();
|
|
|
|
// 重置状态
|
|
if (!_isDisposed) {
|
|
_currentAssistantMessage = null;
|
|
currentStreamMessage.value = '';
|
|
isLoading.value = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 播放文本(用于独立播放场景)
|
|
Future<void> _speakText(String text) async {
|
|
if (text.isEmpty || !isTtsEnabled.value) return;
|
|
|
|
try {
|
|
if (!_ttsService.isConnected.value) {
|
|
await _ttsService.connect();
|
|
}
|
|
|
|
final speaker = agentVoice;
|
|
|
|
await _ttsService.startSession(speaker);
|
|
await _ttsService.speak(text, speaker: speaker);
|
|
await _ttsService.endSession();
|
|
} catch (e) {
|
|
debugPrint('TTS播放出错: $e');
|
|
await _ttsService.endSession();
|
|
}
|
|
}
|
|
|
|
// 确保滚动到底部
|
|
void _ensureScrollToBottom() {
|
|
_scrollToBottom();
|
|
|
|
// 延迟滚动以确保视图已构建
|
|
Future.delayed(const Duration(milliseconds: 100), () {
|
|
if (!_isDisposed) _scrollToBottom(animate: true);
|
|
});
|
|
|
|
// 帧回调确保在渲染后滚动
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (!_isDisposed) _scrollToBottom(animate: true);
|
|
});
|
|
}
|
|
|
|
// 滚动监听器
|
|
void _scrollListener() {
|
|
if (_isDisposed) return;
|
|
|
|
// 检测是否接近底部
|
|
if (scrollController.hasClients) {
|
|
final maxScroll = scrollController.position.maxScrollExtent;
|
|
final currentScroll = scrollController.offset;
|
|
isAtBottom.value = (maxScroll - currentScroll) < 50;
|
|
}
|
|
}
|
|
|
|
@override
|
|
void onInit() {
|
|
super.onInit();
|
|
messageController = TextEditingController();
|
|
scrollController = ScrollController();
|
|
|
|
// 清理可能存在的语音输入控制器
|
|
if (Get.isRegistered<VoiceInputController>()) {
|
|
try {
|
|
Get.find<VoiceInputController>().stopRecording();
|
|
Get.delete<VoiceInputController>();
|
|
} catch (e) {
|
|
debugPrint('清理语音控制器失败: $e');
|
|
}
|
|
}
|
|
|
|
// 初始化服务和数据
|
|
_initTtsService();
|
|
_initFromArguments();
|
|
_initializeController();
|
|
|
|
// 添加滚动监听器
|
|
scrollController.addListener(_scrollListener);
|
|
|
|
// 确保滚动到底部
|
|
_ensureScrollToBottom();
|
|
}
|
|
|
|
void _initFromArguments() {
|
|
try {
|
|
final arguments = Get.arguments;
|
|
if (arguments == null) {
|
|
throw Exception('No arguments provided to ChatController');
|
|
}
|
|
|
|
final args = arguments as Map<String, dynamic>;
|
|
agentId = args['agentId'] as String? ?? '';
|
|
agentName = args['agentName'] as String? ?? 'Assistant';
|
|
agentVoice = args['agentVoice'] as String? ?? 'zh_female_meilinvyou_moon_bigtts';
|
|
agentAvatar = args['agentAvatar'] as String? ?? 'assets/images/default_avatar.png';
|
|
agentSubtitle = args['agentSubtitle'] as String?;
|
|
systemPrompt = args['systemPrompt'] as String? ?? '';
|
|
welcomeMessage = args['welcomeMessage'] as String? ?? 'Hello! How can I help you today?';
|
|
playVoiceOnEnter = args['playVoiceOnEnter'] as bool? ?? false;
|
|
|
|
} catch (e) {
|
|
debugPrint('🔴 Error initializing ChatController: $e');
|
|
Get.back(); // 返回上一页
|
|
Get.snackbar(
|
|
'Error',
|
|
'Failed to initialize chat: Invalid parameters',
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
);
|
|
}
|
|
}
|
|
|
|
void _initTtsService() {
|
|
_ttsService = Get.find<VolcanoTtsApiService>();
|
|
isTtsEnabled.value = true;
|
|
|
|
// 设置TTS事件监听
|
|
_setupTtsEventListener();
|
|
}
|
|
|
|
void _setupTtsEventListener() {
|
|
_ttsEventSubscription = _ttsService.eventStream.listen(_handleTtsEvent);
|
|
}
|
|
|
|
void _handleTtsEvent(Map<String, dynamic> event) {
|
|
if (_isDisposed) return;
|
|
|
|
final String eventType = event['eventType'] as String? ?? 'unknown';
|
|
|
|
// 根据事件类型更新TTS状态
|
|
switch (eventType) {
|
|
case 'ttsSentenceStart':
|
|
case 'playbackStarted':
|
|
case 'synthesisBegin':
|
|
_isTtsSpeaking = true;
|
|
break;
|
|
case 'ttsSentenceEnd':
|
|
case 'playbackCompleted':
|
|
case 'synthesisEnd':
|
|
case 'sessionFinished':
|
|
case 'connectionFinished':
|
|
case 'connectionFailed':
|
|
case 'sessionFailed':
|
|
case 'playbackError':
|
|
_isTtsSpeaking = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
Future<void> _initializeController() async {
|
|
try {
|
|
await _loadChatHistory();
|
|
|
|
await _showWelcomeMessages();
|
|
} catch (e) {
|
|
Get.snackbar(
|
|
'Error',
|
|
'Failed to initialize chat: $e',
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> _showWelcomeMessages() async {
|
|
if (!agentName.isNotEmpty || !playVoiceOnEnter) return;
|
|
|
|
try {
|
|
// 生成动态问候语
|
|
await _generatePersonalizedGreeting();
|
|
await _saveChatHistory();
|
|
_scrollToBottom(animate: true);
|
|
|
|
// 添加系统消息
|
|
messages.add(Message(
|
|
role: 'system',
|
|
content: '已为您自动启用连续语音对话模式,您可以直接开始说话',
|
|
timestamp: DateTime.now(),
|
|
));
|
|
await _saveChatHistory();
|
|
|
|
// 启动语音输入
|
|
Future.delayed(const Duration(milliseconds: 300), () {
|
|
if (!_isDisposed) {
|
|
startVoiceInput();
|
|
}
|
|
});
|
|
} catch (e) {
|
|
debugPrint('显示欢迎消息失败: $e');
|
|
|
|
// 播放静态欢迎消息
|
|
if (isTtsEnabled.value) {
|
|
_speakText(welcomeMessage);
|
|
}
|
|
|
|
// 仍然启动语音输入
|
|
if (!_isDisposed) {
|
|
Future.delayed(const Duration(milliseconds: 300), () {
|
|
startVoiceInput();
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _generatePersonalizedGreeting() async {
|
|
if (_isDisposed) return;
|
|
|
|
try {
|
|
// 创建助手消息
|
|
currentStreamMessage.value = '';
|
|
_currentAssistantMessage = Message(
|
|
role: 'assistant',
|
|
content: '',
|
|
timestamp: DateTime.now(),
|
|
);
|
|
messages.add(_currentAssistantMessage!);
|
|
|
|
// 构建问候语请求
|
|
final greetingMessages = [
|
|
{'role': 'system', 'content': systemPrompt},
|
|
{'role': 'user', 'content': '请用一句简短的话向我打个招呼,要体现你的特点,不要超过18个字。'},
|
|
];
|
|
|
|
// 生成问候语
|
|
String fullGreeting = '';
|
|
await for (final chunk in _aiService.sendMessageStream(
|
|
messages: greetingMessages,
|
|
systemPrompt: systemPrompt,
|
|
)) {
|
|
if (_isDisposed) break;
|
|
|
|
// 更新消息内容
|
|
currentStreamMessage.value += chunk;
|
|
_updateAssistantMessage(currentStreamMessage.value);
|
|
fullGreeting += chunk;
|
|
}
|
|
|
|
// 播放问候语
|
|
if (isTtsEnabled.value && fullGreeting.isNotEmpty) {
|
|
await _speakText(fullGreeting);
|
|
}
|
|
} catch (e) {
|
|
debugPrint('生成问候语失败: $e');
|
|
if (_currentAssistantMessage != null) {
|
|
messages.remove(_currentAssistantMessage);
|
|
}
|
|
} finally {
|
|
if (!_isDisposed) {
|
|
_currentAssistantMessage = null;
|
|
currentStreamMessage.value = '';
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
void onClose() {
|
|
_isDisposed = true;
|
|
|
|
// 停止语音输入和TTS
|
|
stopVoiceInput();
|
|
|
|
// 确保TTS完全停止并清理资源
|
|
if (isTtsEnabled.value) {
|
|
try {
|
|
|
|
_ttsService.stop();
|
|
// 停止当前TTS播放并清除未播放的数据
|
|
_ttsService.endSession();
|
|
|
|
// 确保断开TTS连接,释放所有资源
|
|
_ttsService.disconnect();
|
|
} catch (e) {
|
|
debugPrint('关闭TTS服务时出错: $e');
|
|
}
|
|
}
|
|
|
|
// 取消TTS事件订阅
|
|
if (_ttsEventSubscription != null) {
|
|
_ttsEventSubscription!.cancel();
|
|
_ttsEventSubscription = null;
|
|
}
|
|
|
|
// 重置状态
|
|
currentStreamMessage.value = '';
|
|
_currentAssistantMessage = null;
|
|
_isTtsSpeaking = false;
|
|
|
|
// 释放控制器
|
|
messageController.dispose();
|
|
scrollController.dispose();
|
|
|
|
super.onClose();
|
|
}
|
|
|
|
Future<void> _loadChatHistory() async {
|
|
try {
|
|
final historyJson = _storage.read('${_storagePrefix}$agentId');
|
|
if (historyJson != null) {
|
|
final List<dynamic> historyList = json.decode(historyJson);
|
|
messages.value = historyList.map((item) => Message.fromJson(item)).toList();
|
|
|
|
// 如果有历史消息,等待下一帧再滚动到底部
|
|
if (messages.isNotEmpty) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_scrollToBottom(animate: true);
|
|
});
|
|
}
|
|
}
|
|
} catch (e) {
|
|
Get.snackbar(
|
|
'Error',
|
|
'Failed to load chat history: $e',
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> _saveChatHistory() async {
|
|
try {
|
|
final historyJson =
|
|
json.encode(messages.map((msg) => msg.toJson()).toList());
|
|
await _storage.write('${_storagePrefix}$agentId', historyJson);
|
|
} catch (e) {
|
|
Get.snackbar(
|
|
'Error',
|
|
'Failed to save chat history: $e',
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
);
|
|
}
|
|
}
|
|
|
|
void _scrollToBottom({bool animate = false}) {
|
|
if (!scrollController.hasClients) return;
|
|
|
|
final position = scrollController.position.maxScrollExtent;
|
|
if (animate) {
|
|
scrollController.animateTo(
|
|
position,
|
|
duration: const Duration(milliseconds: 300),
|
|
curve: Curves.easeOut,
|
|
);
|
|
} else {
|
|
scrollController.jumpTo(position);
|
|
}
|
|
}
|
|
|
|
void _updateAssistantMessage(String content) {
|
|
if (_currentAssistantMessage == null) return;
|
|
|
|
final index = messages.indexWhere((msg) =>
|
|
msg.timestamp == _currentAssistantMessage?.timestamp &&
|
|
msg.role == _currentAssistantMessage?.role);
|
|
|
|
if (index != -1) {
|
|
// 使用非空断言,因为我们已经在方法开始时检查了非空
|
|
final timestamp = _currentAssistantMessage!.timestamp;
|
|
messages[index] = Message(
|
|
role: 'assistant',
|
|
content: content,
|
|
timestamp: timestamp,
|
|
);
|
|
// 只在非初始化阶段滚动
|
|
if (!playVoiceOnEnter || content.isNotEmpty) {
|
|
_scrollToBottom(animate: false);
|
|
}
|
|
}
|
|
}
|
|
|
|
void clearMessage() {
|
|
if (!_isDisposed && messageController.text.isNotEmpty) {
|
|
messageController.clear();
|
|
}
|
|
}
|
|
|
|
Future<void> sendMessage() async {
|
|
if (_isDisposed) return;
|
|
|
|
final text = messageController.text.trim();
|
|
if (text.isEmpty) return;
|
|
|
|
// 创建用户消息
|
|
final userMessage = Message(
|
|
role: 'user',
|
|
content: text,
|
|
timestamp: DateTime.now(),
|
|
);
|
|
messages.add(userMessage);
|
|
clearMessage();
|
|
await _saveChatHistory();
|
|
_scrollToBottom(animate: true);
|
|
|
|
// 处理AI响应
|
|
await _processAIResponse(userMessage);
|
|
}
|
|
|
|
Future<void> clearChat() async {
|
|
try {
|
|
messages.clear();
|
|
await _storage.remove('${_storagePrefix}$agentId');
|
|
await _showWelcomeMessages();
|
|
} catch (e) {
|
|
Get.snackbar(
|
|
'Error',
|
|
'Failed to clear chat: $e',
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
);
|
|
}
|
|
}
|
|
|
|
void toggleTTS() {
|
|
// 切换TTS启用状态
|
|
isTtsEnabled.toggle();
|
|
if (!isTtsEnabled.value) {
|
|
_ttsService.endSession();
|
|
}
|
|
}
|
|
|
|
/// 生成简单的问候语
|
|
Future<void> generateSimpleGreeting() async {
|
|
if (_isDisposed) return;
|
|
|
|
try {
|
|
// 创建助手消息
|
|
currentStreamMessage.value = '';
|
|
_currentAssistantMessage = Message(
|
|
role: 'assistant',
|
|
content: '',
|
|
timestamp: DateTime.now(),
|
|
);
|
|
messages.add(_currentAssistantMessage!);
|
|
|
|
// 构建用于生成问候语的消息列表
|
|
final greetingMessages = [
|
|
{'role': 'system', 'content': systemPrompt},
|
|
{'role': 'user', 'content': '请用一句简短的话向我打个招呼,要体现你的特点,不要超过12个字。'},
|
|
];
|
|
|
|
// 生成问候语
|
|
String fullGreeting = '';
|
|
await for (final chunk in _aiService.sendMessageStream(
|
|
messages: greetingMessages,
|
|
systemPrompt: systemPrompt,
|
|
)) {
|
|
if (_isDisposed) break;
|
|
|
|
// 更新消息内容
|
|
currentStreamMessage.value += chunk;
|
|
_updateAssistantMessage(currentStreamMessage.value);
|
|
fullGreeting += chunk;
|
|
}
|
|
|
|
// 播放问候语
|
|
if (isTtsEnabled.value && fullGreeting.isNotEmpty) {
|
|
await _speakText(fullGreeting);
|
|
}
|
|
} catch (e) {
|
|
debugPrint('生成问候语失败: $e');
|
|
// 移除未完成的消息
|
|
if (_currentAssistantMessage != null) {
|
|
messages.remove(_currentAssistantMessage);
|
|
}
|
|
} finally {
|
|
if (!_isDisposed) {
|
|
_currentAssistantMessage = null;
|
|
currentStreamMessage.value = '';
|
|
}
|
|
}
|
|
}
|
|
|
|
// 开始按住说话
|
|
void startPressToTalk() {
|
|
isRecordingVoice.value = true;
|
|
|
|
startVoiceInput();
|
|
}
|
|
|
|
// 结束按住说话
|
|
void endPressToTalk() {
|
|
isRecordingVoice.value = false;
|
|
|
|
stopVoiceInput();
|
|
}
|
|
}
|
|
|