From a32acc0a07d05ae9767b71117728a4aaf232edbe Mon Sep 17 00:00:00 2001 From: wolfplus Date: Tue, 4 Mar 2025 18:14:12 +0000 Subject: [PATCH] v003 --- .cursorrules | 2 +- .../com/example/deep_voice/AzureAsrHelper.kt | 4 +- lib/core/bindings/initial_binding.dart | 8 + lib/core/routes/app_pages.dart | 2 +- lib/core/widgets/message_bubble.dart | 94 ++ lib/data/services/azure_asr_service.dart | 43 +- .../services/background_agent_service.dart | 743 +++++----- lib/data/services/chat_history_service.dart | 97 ++ lib/data/services/my_audio_handler.dart | 119 +- lib/data/services/volcano_asr_service.dart | 3 + lib/modules/chat/bindings/chat_binding.dart | 11 +- .../chat/controllers/chat_controller.dart | 1283 ++++++----------- .../controllers/voice_input_controller.dart | 348 ----- lib/modules/chat/models/message_model.dart | 31 +- lib/modules/chat/views/chat_view.dart | 969 ++++++------- .../chat/widgets/voice_input_panel.dart | 302 ---- .../controllers/translation_controller.dart | 463 +++--- .../translation_detail_controller.dart | 81 +- .../views/translation_detail_view.dart | 160 +- .../views/translation_history_view.dart | 75 +- .../translation/views/translation_view.dart | 81 +- linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 26 files changed, 2062 insertions(+), 2867 deletions(-) create mode 100644 lib/core/widgets/message_bubble.dart create mode 100644 lib/data/services/chat_history_service.dart delete mode 100644 lib/modules/chat/controllers/voice_input_controller.dart delete mode 100644 lib/modules/chat/widgets/voice_input_panel.dart diff --git a/.cursorrules b/.cursorrules index 8381a6e52..bd3eac47b 100644 --- a/.cursorrules +++ b/.cursorrules @@ -4,6 +4,6 @@ 4. 尽可能使用GetX提供的各种功能, 而不是使用原生的StatefulWidget 5. 不要增加重试逻辑,出现失败直接返回错误 6. 尽可能避免使用try catch -7. 尽可能避免打印日志 +7. 尽可能避免打印日志,仅在出错时记录日志 8. 请使用较小的代码量改动,最安全的方法进行代码编写和优化 diff --git a/android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt b/android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt index d961f95a7..ef2ea95a1 100644 --- a/android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt +++ b/android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt @@ -233,7 +233,7 @@ class AzureAsrHelper { // 开始连续识别 recognizer.startContinuousRecognitionAsync().get() isContinuousRecognitionActive = true - Log.d(TAG, "连续识别已开始") + Log.d(TAG, "连续识别已开始kt") return true } @@ -273,7 +273,7 @@ class AzureAsrHelper { stopRecognitionSemaphore.acquire() isContinuousRecognitionActive = false - Log.d(TAG, "连续识别已停止") + Log.d(TAG, "连续识别已停止kt") callback.onSessionStopped() return true } catch (e: Exception) { diff --git a/lib/core/bindings/initial_binding.dart b/lib/core/bindings/initial_binding.dart index be7cc8d13..06813727d 100644 --- a/lib/core/bindings/initial_binding.dart +++ b/lib/core/bindings/initial_binding.dart @@ -7,6 +7,8 @@ import '../../data/services/volcano_asr_service.dart'; import '../../data/services/volcano_tts_api_service.dart'; import '../../data/services/volcano_translation_service.dart'; import '../../data/services/azure_asr_service.dart'; +import '../../data/services/background_agent_service.dart'; +import '../../data/services/chat_history_service.dart'; import '../../core/utils/logger.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; @@ -23,6 +25,12 @@ class InitialBinding extends Bindings { final volcanoTranslationService = Get.put(VolcanoTranslationService(), permanent: true); final azureAsrService = Get.put(AzureAsrService(), permanent: true); + // 注册ChatHistoryService (先注册ChatHistoryService) + final chatHistoryService = Get.put(ChatHistoryService(), permanent: true); + + // 注册后台代理服务 (后注册BackgroundAgentService) + final backgroundAgentService = Get.put(BackgroundAgentService(), permanent: true); + // 只注册全局控制器 Get.put(PermissionController(), permanent: true); diff --git a/lib/core/routes/app_pages.dart b/lib/core/routes/app_pages.dart index 14d6fbe98..88508b2a2 100644 --- a/lib/core/routes/app_pages.dart +++ b/lib/core/routes/app_pages.dart @@ -31,7 +31,7 @@ abstract class AppPages { ), GetPage( name: Routes.chat, - page: () => const ChatView(), + page: () => ChatView(), binding: ChatBinding(), ), GetPage( diff --git a/lib/core/widgets/message_bubble.dart b/lib/core/widgets/message_bubble.dart new file mode 100644 index 000000000..7b125ea7c --- /dev/null +++ b/lib/core/widgets/message_bubble.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../../modules/chat/models/message_model.dart'; +import '../theme/app_colors.dart'; + +class MessageBubble extends StatelessWidget { + final Message message; + final bool isUser; + final String? userAvatar; + final String? agentAvatar; + + const MessageBubble({ + Key? key, + required this.message, + required this.isUser, + this.userAvatar, + this.agentAvatar, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only(bottom: 16.h), + child: Row( + mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + child: Container( + padding: EdgeInsets.all(12.r), + decoration: BoxDecoration( + color: isUser ? AppColors.primary : Colors.grey[200], + borderRadius: BorderRadius.circular(16.r), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 消息内容 + if (message.content.isNotEmpty) + SelectableText( + message.content, + style: TextStyle( + color: isUser ? Colors.white : Colors.black, + fontSize: 16.sp, + height: 1.5, + letterSpacing: 0.2, + ), + textAlign: TextAlign.left, + maxLines: null, + enableInteractiveSelection: true, + textWidthBasis: TextWidthBasis.longestLine, + ), + + // 加载指示器 + if (message.isLoading) + Padding( + padding: EdgeInsets.only(top: message.content.isNotEmpty ? 8.h : 0), + child: _buildLoadingIndicator(), + ), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildLoadingIndicator() { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 12.w, + height: 12.h, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + isUser ? Colors.white : AppColors.primary, + ), + ), + ), + SizedBox(width: 8.w), + Text( + '思考中...', + style: TextStyle( + color: isUser ? Colors.white : Colors.black, + fontSize: 12.sp, + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/data/services/azure_asr_service.dart b/lib/data/services/azure_asr_service.dart index 06f6813b8..3f5fa5f43 100644 --- a/lib/data/services/azure_asr_service.dart +++ b/lib/data/services/azure_asr_service.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import '../../core/utils/logger.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; /// 识别事件类型 enum RecognitionEventType { @@ -209,23 +210,39 @@ class AzureAsrService extends GetxService { Future startContinuousRecognition() async { _checkInitialized(); - if (_isContinuousRecognitionActive) { - Logger.warning('连续识别已经在进行中'); - // 先停止当前正在进行的识别任务 - Logger.info('尝试停止当前正在进行的识别任务'); - try { - await stopContinuousRecognition(); - // 添加短暂延迟,确保资源被正确释放 - await Future.delayed(Duration(milliseconds: 300)); - } catch (e) { - Logger.error('停止当前识别任务失败', e); - // 继续尝试启动新的识别任务 - } + // 无论内部状态如何,先尝试停止任何可能正在进行的识别任务 + Logger.info('在开始新的语音识别前,确保停止任何可能正在进行的识别任务'); + try { + await stopContinuousRecognition(); + // 添加短暂延迟,确保资源被正确释放 + await Future.delayed(Duration(milliseconds: 300)); + } catch (e) { + Logger.error('停止当前识别任务失败', e); + // 继续尝试启动新的识别任务 } try { // 重置识别器,确保资源被正确释放 - await _resetRecognizer(); + // await _resetRecognizer(); + + // 重新初始化底层服务 - 通过连续调用两次resetRecognizer确保完全重置 + Logger.info('重新初始化语音识别底层服务'); + try { + // 第一次调用resetRecognizer + await _methodChannel.invokeMethod('resetRecognizer'); + + // 短暂延迟,确保第一次重置完成 + await Future.delayed(Duration(milliseconds: 200)); + + // 第二次调用resetRecognizer,确保完全重置 + await _methodChannel.invokeMethod('resetRecognizer'); + + // 再次短暂延迟 + await Future.delayed(Duration(milliseconds: 200)); + } catch (e) { + Logger.error('重新初始化底层服务失败', e); + // 继续尝试启动识别 + } // 创建事件流控制器 _eventStreamController = StreamController.broadcast(); diff --git a/lib/data/services/background_agent_service.dart b/lib/data/services/background_agent_service.dart index 13e4188a0..49e9ce711 100644 --- a/lib/data/services/background_agent_service.dart +++ b/lib/data/services/background_agent_service.dart @@ -1,465 +1,458 @@ -import 'package:get/get.dart'; import 'dart:async'; -import 'volcano_ai_service.dart'; -import 'azure_asr_service.dart'; -import 'volcano_tts_api_service.dart'; -import '../../modules/chat/models/message_model.dart'; +import 'package:get/get.dart'; +import 'package:flutter/widgets.dart'; import '../../core/utils/logger.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; import '../providers/agent_provider.dart'; +import '../models/agent.dart'; +import '../../modules/chat/controllers/chat_controller.dart'; +import '../../modules/chat/models/message_model.dart'; +import 'azure_asr_service.dart'; +import 'volcano_ai_service.dart'; +import 'volcano_tts_api_service.dart'; +import 'chat_history_service.dart'; +/// 后台代理服务,用于在应用处于后台时处理AI交互 class BackgroundAgentService extends GetxService { - static BackgroundAgentService get to => Get.find(); - - final VolcanoAIService _aiService; - final VolcanoTtsApiService _ttsService; - final AzureAsrService _voiceRecognitionService; - final List _messageHistory = []; - bool _isProcessing = false; - bool _isListening = false; - StreamSubscription? _recognitionSubscription; + // 服务实例 + final AzureAsrService _asrService = Get.find(); + final VolcanoAIService _aiService = Get.find(); + final VolcanoTtsApiService _ttsService = Get.find(); + final ChatHistoryService _historyService = Get.find(); - - // 当前使用的agent ID - String? _currentAgentId; - - // 当前使用的agent voice - String _agentVoice = 'zh_female_meilinvyou_moon_bigtts'; - - // 可观察的状态 + // 状态控制 + final RxBool isProcessing = false.obs; final RxBool isListening = false.obs; - final RxString recognizedText = ''.obs; - - // 添加一个标志,表示是否已经识别到语音 - bool _hasRecognizedSpeech = false; - - // 添加一个标志,表示是否已经收到最终结果 - bool _hasFinalResult = false; - // 单一的交互超时计时器 (30秒) - Timer? _interactionTimer; + // 语音识别状态 + final RxString recognizedText = ''.obs; - // 添加一个变量来跟踪当前的AI响应流订阅 + // 流式响应控制 + StreamSubscription? _recognitionSubscription; StreamSubscription? _aiResponseSubscription; - // 添加一个标志,表示是否应该取消当前的AI响应 - bool _shouldCancelAiResponse = false; + // 定时器,用于检测无活动超时 + Timer? _inactivityTimer; - // 当前系统提示词 - String? _currentSystemPrompt; - - BackgroundAgentService() - : _aiService = VolcanoAIService(), - _ttsService = Get.find(), - _voiceRecognitionService = Get.find(); + // 当前处理的Agent + Agent? _currentAgent; - // 启动或重置交互计时器 - void _resetInteractionTimer() { - // 取消之前的计时器 - _interactionTimer?.cancel(); - - // 设置30秒的超时计时器 - _interactionTimer = Timer(const Duration(seconds: 30), () { - // 检查TTS是否正在播放 - if (_ttsService.isPlaying) { - // 如果TTS正在播放,重置定时器 - _resetInteractionTimer(); - } else { - // 如果TTS不在播放,且30秒内没有检测到用户输入,退出交互 - Logger.info('30秒内没有检测到用户输入,退出交互'); - _endInteraction(); + // 消息历史 - 与ChatController共享 + List> get messageHistory { + // 首先尝试从ChatController获取消息历史 + if (Get.isRegistered()) { + final chatController = Get.find(); + // 检查ChatController是否正在与personal_assistant交互 + if (chatController.agent?.id == 'personal_assistant') { + return chatController.messages.map((m) => { + 'role': m.role, + 'content': m.content, + }).toList(); } - }); + } - } - - // 结束交互 - Future _endInteraction() async { - // 完成当前的recognitionCompleter(如果有) - if (_recognitionCompleter != null && !_recognitionCompleter!.isCompleted) { - _recognitionCompleter!.complete(''); + // 如果无法从ChatController获取,则使用本地存储的历史记录 + if (_localMessageHistory.isEmpty) { + _loadChatHistory(); } - _isProcessing = false; + return _localMessageHistory; + } + + // 本地消息历史备份 + final List> _localMessageHistory = []; + + // 构造函数 + BackgroundAgentService() { + Logger.info('BackgroundAgentService 已创建'); + } + + // TTS是否正在播放,直接从VolcanoTtsApiService获取 + bool get isTtsSpeaking => _ttsService.isPlaying; + + @override + void onClose() { + // 取消所有订阅 + _recognitionSubscription?.cancel(); + _aiResponseSubscription?.cancel(); - // 播放退出提示 - await _ttsService.speakSingle("没有听到您说话,已退出语音交互", speaker: _agentVoice); + // 停止TTS + if (_ttsService.isPlaying) { + _ttsService.stop(); + } // 停止语音识别 - if (_isListening) { - await stopVoiceRecognition(); - Logger.info('退出交互,已停止语音识别'); + if (isListening.value) { + _asrService.stopContinuousRecognition(); } - // 清除当前系统提示词 - _currentSystemPrompt = null; - // 清除当前agent ID - _currentAgentId = null; - // 重置agent voice为默认值 - _agentVoice = 'zh_female_meilinvyou_moon_bigtts'; + // 取消定时器 + _inactivityTimer?.cancel(); + + super.onClose(); } - // 开始语音交互 + /// 开始代理交互 + /// + /// [agentId] 要交互的代理ID Future startAgentInteraction(String agentId) async { - // 设置当前agent ID - _currentAgentId = agentId; - - // 获取agent信息,只查询一次 - final agent = AgentProvider.getAgentById(agentId); - if (agent == null) { - Logger.error('无法获取agent: $agentId'); - return; - } - - // 存储需要的值 - _agentVoice = agent.voice; - _currentSystemPrompt = agent.systemPrompt; - - // 播放提示音 - await _ttsService.speakSingle("我在听", speaker: _agentVoice); + // 先说"我在听",然后再播放欢迎消息 + const listeningMessage = "我在听"; + _ttsService.speakSingle(listeningMessage); - if (_isProcessing) { - print('已经在处理交互,忽略此次请求'); + // 防止重复处理 + if (isProcessing.value) { + Logger.warning('已经在处理交互,忽略此次请求'); return; } - _isProcessing = true; + isProcessing.value = true; try { - // 确保之前的语音识别已经停止 - if (_isListening) { - await stopVoiceRecognition(); + // 获取Agent + final agent = AgentProvider.getAgentById(agentId); + if (agent == null) { + Logger.error('无法获取Agent: $agentId'); + isProcessing.value = false; + return; } - // 启动语音识别 - await startVoiceRecognition(); + _currentAgent = agent; - // 启动交互计时器 - _resetInteractionTimer(); - } catch (e) { - Logger.error('启动语音交互失败: $e'); - _isProcessing = false; + // 加载该Agent的聊天历史 + _loadChatHistory(); - // 播放错误提示 - await _ttsService.speakSingle("抱歉,启动语音交互失败", speaker: _agentVoice); - } - } - - // 停止语音交互 - Future stopAgentInteraction() async { - // 取消交互计时器 - _interactionTimer?.cancel(); - _interactionTimer = null; - - // 停止TTS播放 - if (_ttsService.isPlaying) { - await _ttsService.stop(); - } - - // 停止语音识别 - if (_isListening) { - await stopVoiceRecognition(); - Logger.info('手动停止交互,已停止语音识别'); + // 启动语音识别 - 保持持续识别状态 + await _startVoiceRecognition(); + + // 启动无活动定时器 + _startInactivityTimer(); + + } catch (e) { + Logger.error('启动代理交互失败', e); + isProcessing.value = false; } - - // 清除处理状态 - _isProcessing = false; - _currentSystemPrompt = null; - // 重置agent voice为默认值 - _agentVoice = 'zh_female_meilinvyou_moon_bigtts'; - - Logger.info('手动停止交互,已结束TTS会话'); } - // 添加一个 Completer 用于在识别到最终结果时完成 - Completer? _recognitionCompleter; - - // 开始语音识别 - Future startVoiceRecognition() async { - if (_isListening) { - await stopVoiceRecognition(); + /// 启动语音识别 + Future _startVoiceRecognition() async { + if (isListening.value) { + // 如果已经在监听,不需要重新启动 + Logger.info('语音识别已经在运行,无需重新启动'); + return; } try { - Logger.info('开始初始化语音识别服务...'); - // 确保语音识别服务已初始化 - if (!await _voiceRecognitionService.initialize( - subscriptionKey: dotenv.env['AZURE_ASR_SUBSCRIPTION_KEY'] ?? '', - serviceRegion: dotenv.env['AZURE_ASR_SERVICE_REGION'] ?? 'eastasia', - language: dotenv.env['AZURE_ASR_LANGUAGE'] ?? 'zh-CN', - )) { - // 初始化失败,直接抛出异常 - Logger.error('语音识别服务初始化失败'); - throw Exception('无法初始化语音识别服务'); - } - - // 开始连续识别 - final success = await _voiceRecognitionService.startContinuousRecognition(); - if (!success) { - Logger.error('启动连续语音识别失败'); - throw Exception('无法启动语音识别'); - } - - _isListening = true; - isListening.value = true; - recognizedText.value = ''; - _hasRecognizedSpeech = false; - _hasFinalResult = false; - - // 添加一个变量来存储完整的用户输入 - String fullUserInput = ''; + // 启动语音识别 + final success = await _asrService.startContinuousRecognition(); - // 使用语音识别服务的recognitionStream而不是方法的返回值 - _recognitionSubscription = _voiceRecognitionService.recognitionStream?.listen((event) { - if (event.type == RecognitionEventType.finalResult) { - // 添加日志输出最终识别结果 - Logger.info('语音识别最终结果: ${event.text}'); - - if (event.text.isNotEmpty) { - // 将最终结果添加到完整输入中,并添加适当的标点符号 - if (fullUserInput.isNotEmpty && !fullUserInput.endsWith('。') && - !fullUserInput.endsWith('?') && !fullUserInput.endsWith('!') && - !fullUserInput.endsWith('.') && !fullUserInput.endsWith('?') && - !fullUserInput.endsWith('!')) { - fullUserInput += ','; - } - fullUserInput += event.text; - - // 更新可观察的识别文本 - recognizedText.value = fullUserInput; - - _hasRecognizedSpeech = true; - _hasFinalResult = true; - - // 重置交互计时器,因为用户刚刚说了话 - _resetInteractionTimer(); - - - _processUserInput(fullUserInput); - - // 重置用户输入 - fullUserInput = ''; - recognizedText.value = ''; - _hasRecognizedSpeech = false; - _hasFinalResult = false; - } - } else if (event.type == RecognitionEventType.recognizing) { - // 添加日志输出中间识别结果 - Logger.info('语音识别中间结果: ${event.text}'); - - if (event.text.isNotEmpty) { - // 更新当前的中间结果,但不添加到完整输入中 - recognizedText.value = fullUserInput + (fullUserInput.isEmpty ? "" : ",") + event.text; + if (success) { + isListening.value = true; + recognizedText.value = ''; + + // 监听识别事件 + _recognitionSubscription = _asrService.recognitionStream?.listen( + (event) { + // 重置无活动定时器 + _resetInactivityTimer(); - _hasRecognizedSpeech = true; - - // 检测到用户说话,重置交互计时器 - _resetInteractionTimer(); - - // 如果系统正在播放TTS或接收AI响应,检测到用户开始说话时立即中断 - if (_ttsService.isPlaying && event.text.trim().isNotEmpty) { - // 停止当前TTS播放 - _ttsService.stop(); - - // 设置标志,表示应该取消当前的AI响应 - _shouldCancelAiResponse = true; - - // 取消当前的AI响应流订阅 - _aiResponseSubscription?.cancel(); - _aiResponseSubscription = null; - - - // 添加日志输出中断TTS播放 - Logger.info('检测到用户说话,中断TTS播放'); + switch (event.type) { + case RecognitionEventType.recognizing: + // 实时更新识别文本 + if (event.text.isNotEmpty) { + recognizedText.value = event.text; + + // 如果正在播放TTS,检测到用户说话则中断当前播放和AI回复 + if (_ttsService.isPlaying && event.text.trim().length > 3) { + _interruptCurrentResponse("检测到用户说话,中断当前响应"); + } + } + break; + case RecognitionEventType.finalResult: + // 最终结果 + if (event.text.isNotEmpty) { + recognizedText.value = event.text; + + // 处理识别到的文本 + _processRecognizedText(event.text); + + // 重置识别文本但保持识别状态 + recognizedText.value = ''; + } + break; + case RecognitionEventType.sessionStopped: + case RecognitionEventType.canceled: + // 会话结束,表示识别在其它地方被启动,直接退出后台服务 + isListening.value = false; + Logger.info('语音识别会话被中止,退出后台代理服务'); + stopInteraction(); + break; + default: + break; } - } - } else if (event.type == RecognitionEventType.error) { - // 添加日志输出识别错误 - Logger.error('语音识别错误: ${event.error}'); - print('识别错误: ${event.error}'); - } - }, onError: (error) { - // 添加日志输出语音识别流错误 - Logger.error('语音识别流错误: $error'); - print('语音识别流错误: $error'); - _isListening = false; - isListening.value = false; - }); - - // 启动交互计时器 - _resetInteractionTimer(); - + }, + onError: (error) { + Logger.error('语音识别错误', error); + isListening.value = false; + + // 如果出错,尝试重新启动语音识别 + _restartVoiceRecognition(); + }, + ); + } else { + Logger.error('启动语音识别失败'); + isProcessing.value = false; + } } catch (e) { - Logger.error('启动语音识别失败: $e'); - print('启动语音识别失败: $e'); - _isListening = false; + Logger.error('启动语音识别失败', e); isListening.value = false; - rethrow; + isProcessing.value = false; } } - // 处理用户输入 - Future _processUserInput(String userInput) async { - if (userInput.isEmpty || _currentSystemPrompt == null) { - return; - } + /// 处理识别到的文本 + Future _processRecognizedText(String text) async { + if (text.trim().isEmpty || _currentAgent == null) return; try { - // 保存用户消息到历史记录 - _messageHistory.add(Message( - role: 'user', - content: userInput, - timestamp: DateTime.now(), - )); + // 添加用户消息到历史记录 + _addMessageToHistory('user', text); - // 构建用于生成回应的消息列表 - final messages = [ - {'role': 'system', 'content': _currentSystemPrompt!}, - {'role': 'user', 'content': userInput}, - ]; - + // 启动TTS会话,使用当前Agent的voice + _ttsService.startSession(_currentAgent!.voice); + // 获取AI响应 String fullResponse = ''; - Logger.info('开始生成AI响应...'); - // 重置取消标志 - _shouldCancelAiResponse = false; - - // 创建一个本地变量来跟踪是否已取消 - bool isCancelled = false; - - // 使用成员变量中存储的agent voice - _ttsService.startSession(_agentVoice); - // 获取AI响应流 final responseStream = _aiService.sendMessageStream( - messages: messages, - systemPrompt: _currentSystemPrompt!, + messages: messageHistory, + systemPrompt: _currentAgent!.systemPrompt, ); - // 创建一个订阅来处理响应流 + // 订阅响应流 _aiResponseSubscription = responseStream.listen( (chunk) { - // 如果已经设置了取消标志,则不处理这个块 - if (_shouldCancelAiResponse) { - isCancelled = true; - return; - } - fullResponse += chunk; - // 直接将每个文本块传递给 TTS 接口,不进行切句或攒句处理 - try { - // 使用成员变量中存储的agent voice - _ttsService.speak(chunk, speaker: _agentVoice); - - // Logger.info('直接播放 AI 响应块: $chunk'); - } catch (e) { - Logger.error('TTS 播放出错: $e'); - } - }, - onError: (e) { - Logger.error('AI响应流错误: $e'); - print('AI响应流错误: $e'); - // 清理订阅 - _aiResponseSubscription = null; + // 如果TTS启用,发送到TTS + _ttsService.speak(chunk); + + // 重置无活动定时器 + _resetInactivityTimer(); }, onDone: () { + // 添加AI响应到历史记录 + _addMessageToHistory('assistant', fullResponse); + + // 结束TTS会话 _ttsService.endSession(); - // 如果已取消,不处理剩余的文本 - if (!isCancelled && !_shouldCancelAiResponse) { - Logger.info('AI 响应生成完成'); - } - _aiResponseSubscription = null; + // 注意:我们不再在这里重启语音识别,因为它应该已经在运行 + }, + onError: (error) { + Logger.error('AI响应错误', error); - // AI响应完成后,重新启动超时计时器 - _resetInteractionTimer(); + // 播放错误消息 + const errorMessage = '抱歉,我遇到了一些问题,无法回应您的请求。'; + _ttsService.speak(errorMessage); - // 如果响应被取消,不保存到历史记录 - if (!_shouldCancelAiResponse && fullResponse.isNotEmpty) { - // 保存助手回复到历史记录 - _messageHistory.add(Message( - role: 'assistant', - content: fullResponse, - timestamp: DateTime.now(), - )); - - // Logger.info('保存AI响应到历史记录,长度: ${fullResponse.length}'); - } - - // 限制历史记录长度 - if (_messageHistory.length > 20) { - _messageHistory.removeRange(0, _messageHistory.length - 20); - Logger.info('历史记录超过20条,已裁剪'); - } - } + // 添加错误消息到历史记录 + _addMessageToHistory('assistant', errorMessage); + + // 结束TTS会话 + _ttsService.endSession(); + }, ); } catch (e) { - Logger.error('处理用户输入失败: $e'); - print('处理用户输入失败: $e'); + Logger.error('处理识别文本失败', e); + + // 播放错误消息 + const errorMessage = '抱歉,我遇到了一些问题,无法回应您的请求。'; + _ttsService.speak(errorMessage); - // 播放错误提示,使用成员变量中存储的agent voice - await _ttsService.speak("抱歉,出现了一些问题", speaker: _agentVoice); + // 添加错误消息到历史记录 + _addMessageToHistory('assistant', errorMessage); } } - - // 停止语音识别并返回识别的文本 - Future stopVoiceRecognition() async { - if (!_isListening) { - return ''; + + /// 重新启动语音识别(仅在识别会话意外终止时使用) + Future _restartVoiceRecognition() async { + // 停止当前的语音识别 + if (isListening.value) { + await _asrService.stopContinuousRecognition(); + isListening.value = false; + + // 取消当前的订阅 + _recognitionSubscription?.cancel(); + _recognitionSubscription = null; } + // 等待一段时间,确保资源被正确释放 + await Future.delayed(Duration(milliseconds: 500)); + + // 重新启动语音识别 + await _startVoiceRecognition(); + } + + /// 添加消息到历史记录 + void _addMessageToHistory(String role, String content) { + // 添加到本地历史记录 + _localMessageHistory.add({ + 'role': role, + 'content': content, + }); + + // 保持本地历史记录不超过10条消息 + if (_localMessageHistory.length > 10) { + _localMessageHistory.removeAt(0); + } + + // 如果ChatController可用,同步消息到ChatController + if (Get.isRegistered()) { + final chatController = Get.find(); + if (chatController.agent?.id == 'personal_assistant') { + // 向ChatController添加消息 + final message = Message( + role: role, + content: content, + timestamp: DateTime.now(), + ); + chatController.addMessage(message); + } + } else { + // 如果ChatController不可用,保存到本地存储 + _saveChatHistory(); + } + } + + // 加载聊天历史 + void _loadChatHistory() { + if (_currentAgent == null) return; + try { - // 取消订阅 - await _recognitionSubscription?.cancel(); - _recognitionSubscription = null; - - // 停止语音识别 - await _voiceRecognitionService.stopContinuousRecognition(); - - // 获取最终识别结果 - final result = recognizedText.value; + // 加载聊天历史 + final messages = _historyService.loadHistory(_currentAgent!.id); - // 添加日志输出停止语音识别的最终结果 - Logger.info('停止语音识别,最终结果: $result'); + // 清空本地历史,然后添加加载的历史 + _localMessageHistory.clear(); - // 重置状态 - _isListening = false; - isListening.value = false; - - return result; + // 将Message转换为Map格式 + if (messages.isNotEmpty) { + for (final message in messages) { + _localMessageHistory.add({ + 'role': message.role, + 'content': message.content, + }); + } + Logger.info('已加载${messages.length}条聊天历史记录,代理: ${_currentAgent!.id}'); + } } catch (e) { - // 添加日志输出停止语音识别失败 - Logger.error('停止语音识别失败: $e'); - print('停止语音识别失败: $e'); - _isListening = false; - isListening.value = false; + Logger.error('加载聊天历史记录失败', e); + } + } + + // 保存聊天历史 + void _saveChatHistory() { + if (_currentAgent == null) return; + + try { + // 将本地历史转换为Message列表 + final messages = _localMessageHistory.map((m) => Message( + role: m['role']!, + content: m['content']!, + timestamp: DateTime.now(), + )).toList(); - // 直接返回当前已识别的文本,不尝试重置 - final result = recognizedText.value; - Logger.info('停止语音识别失败,使用当前识别结果: $result'); - return result; + // 保存到ChatHistoryService + _historyService.saveHistory(_currentAgent!.id, messages); + } catch (e) { + Logger.error('保存聊天历史记录失败', e); } } - - List get messageHistory => List.unmodifiable(_messageHistory); - - void clearHistory() { - _messageHistory.clear(); + + /// 启动无活动定时器 + void _startInactivityTimer() { + _inactivityTimer?.cancel(); + _inactivityTimer = Timer(Duration(seconds: 30), () { + // 如果30秒内没有TTS输出或用户说话,停止语音识别 + if (!_ttsService.isPlaying && isProcessing.value) { + Logger.info('检测到30秒内无活动,停止语音识别'); + stopInteraction(); + } + }); } - - @override - void onClose() { - _recognitionSubscription?.cancel(); - _interactionTimer?.cancel(); - _interactionTimer = null; - _aiResponseSubscription?.cancel(); + + /// 重置无活动定时器 + void _resetInactivityTimer() { + if (_inactivityTimer != null) { + _inactivityTimer!.cancel(); + _startInactivityTimer(); + } + } + + /// 中断当前响应 + void _interruptCurrentResponse(String reason) { + Logger.info(reason); - // 停止TTS播放 + // 停止TTS if (_ttsService.isPlaying) { _ttsService.stop(); } - super.onClose(); + // 取消AI响应流订阅 + _aiResponseSubscription?.cancel(); + _aiResponseSubscription = null; + } + + /// 响应新的媒体事件(双击媒体按钮) + Future respondToNewMediaEvent() async { + // 不管当前状态如何,总是中断当前响应 + _interruptCurrentResponse("检测到新的媒体事件,中断当前响应"); + + // 回答"我在听" + const listeningMessage = "我在听"; + _ttsService.speakSingle(listeningMessage); + + // 不需要重启语音识别,因为它应该已经在运行 + // 如果识别会话已经终止,确保重新启动 + if (!isListening.value) { + await _restartVoiceRecognition(); + } + + // 重置无活动定时器 + _resetInactivityTimer(); + } + + /// 停止当前交互 + Future stopInteraction() async { + try { + // 保存聊天历史 + _saveChatHistory(); + + // 取消所有订阅 + _recognitionSubscription?.cancel(); + _aiResponseSubscription?.cancel(); + + // 停止TTS + if (_ttsService.isPlaying) { + _ttsService.stop(); + } + + // 停止语音识别 + // if (isListening.value) { + // await _asrService.stopContinuousRecognition(); + // isListening.value = false; + // } + + // 取消定时器 + _inactivityTimer?.cancel(); + + // 重置状态 + isProcessing.value = false; + _currentAgent = null; + + Logger.info('已停止后台代理交互'); + } catch (e) { + Logger.error('停止交互失败', e); + } } } \ No newline at end of file diff --git a/lib/data/services/chat_history_service.dart b/lib/data/services/chat_history_service.dart new file mode 100644 index 000000000..181f73851 --- /dev/null +++ b/lib/data/services/chat_history_service.dart @@ -0,0 +1,97 @@ +import 'dart:convert'; +import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; +import '../../modules/chat/models/message_model.dart'; +import '../../core/utils/logger.dart'; + +/// 聊天记录管理服务,负责保存和加载聊天记录 +class ChatHistoryService extends GetxService { + final GetStorage _storage = GetStorage(); + + // 存储键名 + static const String _historyKeyPrefix = 'chat_history_'; + static const int _maxHistoryCount = 20; // 每个对话最多保存的消息数 + + @override + void onInit() { + super.onInit(); + Logger.info('ChatHistoryService 已初始化'); + } + + /// 生成历史记录存储键 + String _getHistoryKey(String agentId) => '$_historyKeyPrefix$agentId'; + + /// 保存聊天记录 + /// + /// [agentId] 代理ID + /// [messages] 消息列表 + Future saveHistory(String agentId, List messages) async { + try { + // 过滤掉加载中的消息 + final filteredMessages = messages.where((m) => !m.isLoading).toList(); + + // 如果消息超过最大数量,只保留最新的消息 + final messagesToSave = filteredMessages.length > _maxHistoryCount + ? filteredMessages.sublist(filteredMessages.length - _maxHistoryCount) + : filteredMessages; + + // 转换为JSON + final jsonList = messagesToSave.map((m) => m.toJson()).toList(); + final jsonString = jsonEncode(jsonList); + + // 保存到存储 + await _storage.write(_getHistoryKey(agentId), jsonString); + Logger.info('已保存${messagesToSave.length}条聊天记录,代理: $agentId'); + } catch (e) { + Logger.error('保存聊天记录失败', e); + } + } + + /// 加载聊天记录 + /// + /// [agentId] 代理ID + /// 返回消息列表,如果没有找到或解析错误则返回空列表 + List loadHistory(String agentId) { + try { + final jsonString = _storage.read(_getHistoryKey(agentId)); + + if (jsonString == null) { + return []; + } + + final jsonList = jsonDecode(jsonString) as List; + final messages = jsonList.map((json) => Message.fromJson(json)).toList(); + + Logger.info('已加载${messages.length}条聊天记录,代理: $agentId'); + return messages; + } catch (e) { + Logger.error('加载聊天记录失败', e); + return []; + } + } + + /// 清除特定代理的聊天记录 + Future clearHistory(String agentId) async { + try { + await _storage.remove(_getHistoryKey(agentId)); + Logger.info('已清除聊天记录,代理: $agentId'); + } catch (e) { + Logger.error('清除聊天记录失败', e); + } + } + + /// 清除所有聊天记录 + Future clearAllHistory() async { + try { + final keys = _storage.getKeys(); + for (final key in keys) { + if (key.startsWith(_historyKeyPrefix)) { + await _storage.remove(key); + } + } + Logger.info('已清除所有聊天记录'); + } catch (e) { + Logger.error('清除所有聊天记录失败', e); + } + } +} \ No newline at end of file diff --git a/lib/data/services/my_audio_handler.dart b/lib/data/services/my_audio_handler.dart index debe7b64b..9275095eb 100644 --- a/lib/data/services/my_audio_handler.dart +++ b/lib/data/services/my_audio_handler.dart @@ -5,6 +5,7 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'background_agent_service.dart'; import '../../data/providers/agent_provider.dart'; +import '../../core/utils/logger.dart'; /// 自定义 AudioHandler,用于捕获媒体按键事件(例如蓝牙耳机按钮) class MyAudioHandler extends BaseAudioHandler { @@ -29,19 +30,20 @@ class MyAudioHandler extends BaseAudioHandler { _initializeHandler(); _initializeBackgroundAgent(); _startStatusUpdateTimer(); - print('MyAudioHandler 已创建,准备接收媒体按钮事件'); + Logger.info('MyAudioHandler 已创建,准备接收媒体按钮事件'); } void _initializeBackgroundAgent() { try { if (Get.isRegistered()) { _backgroundAgent = Get.find(); + Logger.info('BackgroundAgent 初始化成功'); } else { _backgroundAgent = Get.put(BackgroundAgentService(), permanent: true); + Logger.info('BackgroundAgent 已创建并初始化成功'); } - print('BackgroundAgent 初始化成功'); } catch (e) { - print('初始化 BackgroundAgent 失败: $e'); + Logger.error('初始化 BackgroundAgent 失败', e); _backgroundAgent = null; } } @@ -87,7 +89,7 @@ class MyAudioHandler extends BaseAudioHandler { ); mediaItem.add(item); - print('AudioHandler 媒体项已设置'); + Logger.info('AudioHandler 媒体项已设置'); // 立即更新播放状态,确保通知显示 _updatePlaybackState( @@ -95,7 +97,7 @@ class MyAudioHandler extends BaseAudioHandler { processingState: AudioProcessingState.ready, ); } catch (e) { - print('初始化 AudioHandler 失败: $e'); + Logger.error('初始化 AudioHandler 失败', e); } } @@ -109,9 +111,9 @@ class MyAudioHandler extends BaseAudioHandler { playing: true, processingState: AudioProcessingState.ready, ); - print('定期更新播放状态成功'); + Logger.info('定期更新播放状态成功'); } catch (e) { - print('定期更新播放状态失败: $e'); + Logger.error('定期更新播放状态失败', e); } }); } @@ -132,19 +134,19 @@ class MyAudioHandler extends BaseAudioHandler { // 如果状态变为播放,确保前台服务启动 if (playing) { - print('更新播放状态为播放中,确保前台服务启动'); + Logger.info('更新播放状态为播放中,确保前台服务启动'); } } catch (e) { - print('更新播放状态失败: $e'); + Logger.error('更新播放状态失败', e); } } Future _handleInteraction() async { - print('处理媒体按钮交互'); + Logger.info('处理媒体按钮交互'); // 防止重复处理 if (_isHandlingInteraction) { - print('已经在处理交互,忽略此次请求'); + Logger.info('已经在处理交互,忽略此次请求'); return; } @@ -160,14 +162,25 @@ class MyAudioHandler extends BaseAudioHandler { // 直接获取 personal_assistant agent final agent = AgentProvider.getAgentById('personal_assistant'); if (agent == null) { - print('无法获取personal_assistant agent,无法激活语音助手'); + Logger.error('无法获取personal_assistant agent,无法激活语音助手'); + _isHandlingInteraction = false; return; } - if (Get.context != null && - WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed) { + // 检查app是否在前台 + final isInForeground = Get.context != null && + WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed; + + if (isInForeground) { // 应用在前台,导航到聊天页面 - print('应用在前台,导航到聊天页面'); + Logger.info('应用在前台,导航到聊天页面'); + + // 如果BackgroundAgent正在运行,先停止它 + if (_backgroundAgent != null && _backgroundAgent!.isProcessing.value) { + await _backgroundAgent!.stopInteraction(); + Logger.info('已停止后台代理服务,切换到前台模式'); + } + // 导航到聊天页面,并标记需要在进入时播放语音应答 Get.toNamed('/chat', arguments: { 'agentId': agent.id, @@ -181,18 +194,26 @@ class MyAudioHandler extends BaseAudioHandler { }); } else { // 应用在后台,使用 BackgroundAgent 处理交互 - print('应用在后台,使用 BackgroundAgent 处理交互'); + Logger.info('应用在后台,使用 BackgroundAgent 处理交互'); if (_backgroundAgent != null) { - // 调用 BackgroundAgent 的语音识别和 AI 交互功能,传入agent ID - await _backgroundAgent!.startAgentInteraction(agent.id); + + if (_backgroundAgent!.isProcessing.value) { + // 如果后台服务已在处理,则中断当前响应并回复"我在听" + Logger.info('检测到媒体按钮再次按下,中断当前AI回复'); + await _backgroundAgent!.respondToNewMediaEvent(); + } else { + // 首次启动后台服务 + Logger.info('首次启动后台服务'); + await _backgroundAgent!.startAgentInteraction(agent.id); + } } else { - print('BackgroundAgent 未初始化,无法处理交互'); + Logger.error('BackgroundAgent 未初始化,无法处理交互'); // 尝试重新初始化 BackgroundAgent _initializeBackgroundAgent(); } } } catch (e) { - print('处理媒体按钮交互失败: $e'); + Logger.error('处理媒体按钮交互失败', e); } finally { _isHandlingInteraction = false; } @@ -200,7 +221,7 @@ class MyAudioHandler extends BaseAudioHandler { @override Future play() async { - print('收到播放命令'); + Logger.info('收到播放命令'); try { _updatePlaybackState( playing: true, @@ -208,13 +229,13 @@ class MyAudioHandler extends BaseAudioHandler { ); await _handleInteraction(); } catch (e) { - print('处理播放命令失败: $e'); + Logger.error('处理播放命令失败', e); } } @override Future pause() async { - print('收到暂停命令'); + Logger.info('收到暂停命令'); try { _updatePlaybackState( playing: false, @@ -222,16 +243,22 @@ class MyAudioHandler extends BaseAudioHandler { ); await _handleInteraction(); } catch (e) { - print('处理暂停命令失败: $e'); + Logger.error('处理暂停命令失败', e); } } @override Future stop() async { - print('停止 AudioHandler'); + Logger.info('停止 AudioHandler'); _isDisposed = true; try { _statusUpdateTimer?.cancel(); + + // 如果有正在运行的后台服务,停止它 + if (_backgroundAgent != null && _backgroundAgent!.isProcessing.value) { + await _backgroundAgent!.stopInteraction(); + } + await super.stop(); _customEventController.close(); _updatePlaybackState( @@ -239,13 +266,13 @@ class MyAudioHandler extends BaseAudioHandler { processingState: AudioProcessingState.idle, ); } catch (e) { - print('停止 AudioHandler 时发生错误: $e'); + Logger.error('停止 AudioHandler 时发生错误', e); } } @override Future skipToPrevious() async { - print('收到上一曲命令'); + Logger.info('收到上一曲命令'); try { _updatePlaybackState( playing: true, @@ -253,13 +280,13 @@ class MyAudioHandler extends BaseAudioHandler { ); await _handleInteraction(); } catch (e) { - print('处理上一曲命令失败: $e'); + Logger.error('处理上一曲命令失败', e); } } @override Future skipToNext() async { - print('收到下一曲命令'); + Logger.info('收到下一曲命令'); try { _updatePlaybackState( playing: true, @@ -267,29 +294,29 @@ class MyAudioHandler extends BaseAudioHandler { ); await _handleInteraction(); } catch (e) { - print('处理下一曲命令失败: $e'); + Logger.error('处理下一曲命令失败', e); } } @override Future seek(Duration position) async { - print('收到跳转命令: $position'); + Logger.info('收到跳转命令: $position'); } @override Future customAction(String name, [Map? extras]) async { - print('收到自定义命令: $name'); + Logger.info('收到自定义命令: $name'); try { if (name == 'media_button') { - print('收到媒体按钮事件'); + Logger.info('收到媒体按钮事件'); // 检查是否是重复触发(防止短时间内多次触发) final now = DateTime.now(); if (_lastMediaButtonEventTime != null) { final difference = now.difference(_lastMediaButtonEventTime!); - if (difference.inSeconds < 2) { - print('媒体按钮事件触发过于频繁,忽略此次事件'); + if (difference.inMilliseconds < 500) { + Logger.info('媒体按钮事件触发过于频繁,忽略此次事件'); return; } } @@ -304,31 +331,41 @@ class MyAudioHandler extends BaseAudioHandler { await _handleInteraction(); } else if (name == 'ping') { // 用于检查服务是否响应的测试命令 - print('收到 ping 命令,服务正常运行'); + Logger.info('收到 ping 命令,服务正常运行'); return 'pong'; } } catch (e) { - print('处理自定义命令失败: $e'); + Logger.error('处理自定义命令失败', e); } } @override Future onTaskRemoved() async { - print('服务被系统移除'); + Logger.info('服务被系统移除'); try { + // 如果有正在运行的后台服务,停止它 + if (_backgroundAgent != null && _backgroundAgent!.isProcessing.value) { + await _backgroundAgent!.stopInteraction(); + } + await stop(); } catch (e) { - print('处理服务移除失败: $e'); + Logger.error('处理服务移除失败', e); } } @override Future onNotificationDeleted() async { - print('通知被用户移除'); + Logger.info('通知被用户移除'); try { + // 如果有正在运行的后台服务,停止它 + if (_backgroundAgent != null && _backgroundAgent!.isProcessing.value) { + await _backgroundAgent!.stopInteraction(); + } + await stop(); } catch (e) { - print('处理通知移除失败: $e'); + Logger.error('处理通知移除失败', e); } } } diff --git a/lib/data/services/volcano_asr_service.dart b/lib/data/services/volcano_asr_service.dart index 72c54f5f4..d7e5c1fa4 100644 --- a/lib/data/services/volcano_asr_service.dart +++ b/lib/data/services/volcano_asr_service.dart @@ -240,6 +240,9 @@ class VolcanoAsrService extends GetxService { /// 开始连续识别 Future startContinuousRecognition() async { + + //启动前先尝试停止当前识别 + if (_isListening.value) { Logger.warning('已经在进行语音识别,请先停止当前识别'); return false; diff --git a/lib/modules/chat/bindings/chat_binding.dart b/lib/modules/chat/bindings/chat_binding.dart index 87b7c9d34..9cbd6e39b 100644 --- a/lib/modules/chat/bindings/chat_binding.dart +++ b/lib/modules/chat/bindings/chat_binding.dart @@ -1,16 +1,9 @@ import 'package:get/get.dart'; import '../controllers/chat_controller.dart'; -import '../../../data/services/volcano_tts_api_service.dart'; -class ChatBinding implements Bindings { +class ChatBinding extends Bindings { @override void dependencies() { - // Ensure TTS service is registered first - if (!Get.isRegistered()) { - Get.put(VolcanoTtsApiService()); - } - - // Create new controller instance - Get.put(ChatController()); + Get.lazyPut(() => ChatController()); } } \ No newline at end of file diff --git a/lib/modules/chat/controllers/chat_controller.dart b/lib/modules/chat/controllers/chat_controller.dart index d308222ec..a91fe4c0e 100644 --- a/lib/modules/chat/controllers/chat_controller.dart +++ b/lib/modules/chat/controllers/chat_controller.dart @@ -1,944 +1,509 @@ +import 'dart:async'; 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 'package:flutter/widgets.dart'; +import '../models/message_model.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/services/azure_asr_service.dart'; import '../../../data/providers/agent_provider.dart'; +import '../../../data/models/agent.dart'; +import '../../../core/utils/logger.dart'; +import '../../../data/services/chat_history_service.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 = [].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; - } + // 服务 + final VolcanoAIService _aiService = Get.find(); + final VolcanoTtsApiService _ttsService = Get.find(); + final AzureAsrService _asrService = Get.find(); + final ChatHistoryService _historyService = Get.find(); + + // 添加ScrollController用于控制列表滚动 + final ScrollController scrollController = ScrollController(); + + // Agent 信息 + late final Agent agent; + + // 消息列表 + final RxList messages = [].obs; + + // 输入控制 + final RxString inputText = ''.obs; + + // 状态控制 + final RxBool isLoading = false.obs; + final RxBool isSending = false.obs; + final RxBool isTtsEnabled = true.obs; + final RxBool isVoiceRecognitionActive = false.obs; + + // 语音识别状态 + final RxString recognizedText = ''.obs; + final RxBool isListening = false.obs; + + // 临时消息ID,用于语音识别中的消息 + String? _tempRecognitionMessageId; + + // 流式响应控制 + StreamSubscription? _aiResponseSubscription; + StreamSubscription? _recognitionSubscription; + + // 取消标志 + bool _shouldCancelAiResponse = false; - // 开始语音输入 - void startVoiceInput() { - // 确保先停止任何可能正在进行的语音识别会话 - if (Get.isRegistered()) { - final voiceController = Get.find(); - voiceController.stopRecording(); - Get.delete(); + @override + void onInit() { + super.onInit(); + + // 从路由参数获取Agent信息 + final Map args = Get.arguments ?? {}; + final String agentId = args['agentId'] ?? 'personal_assistant'; + + // 获取Agent + final Agent? foundAgent = AgentProvider.getAgentById(agentId); + if (foundAgent == null) { + Logger.error('无法找到Agent: $agentId'); + Get.back(); + return; } - - // 如果TTS正在播放,先停止它 - if (_isTtsSpeaking) { - try { - // 立即停止当前声音播放 - _ttsService.stop(); - // 结束当前TTS会话 - _ttsService.endSession(); - _isTtsSpeaking = false; - } catch (e) { - debugPrint('停止TTS播放失败: $e'); + + agent = foundAgent; + + // 加载聊天历史 + _loadChatHistory(); + + // 如果没有历史记录,添加欢迎消息 + if (messages.isEmpty && agent.welcomeMessage.isNotEmpty) { + messages.add(Message( + role: 'assistant', + content: agent.welcomeMessage, + timestamp: DateTime.now(), + )); + // 如果启用了TTS,播放欢迎消息 + if (isTtsEnabled.value) { + _ttsService.speakSingle(agent.welcomeMessage); } } - - // 如果正在处理AI响应,中断处理 - if (isLoading.value) { - isLoading.value = false; - // 清除当前正在生成的消息 - if (_currentAssistantMessage != null) { - // 如果消息内容为空,则移除该消息 - if (_currentAssistantMessage!.content.isEmpty) { - messages.remove(_currentAssistantMessage); - } - _currentAssistantMessage = null; - } - currentStreamMessage.value = ''; + + + // 如果playVoiceOnEnter为true,自动激活语音识别 + if (args['playVoiceOnEnter'] == true) { + // 延迟一段时间后激活语音识别,等待TTS播放完成 + startVoiceRecognition(); } - - 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, - ); - } - } + _scrollToBottom(); }); - - // 创建语音输入控制器 - 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; - } - } + @override + void onClose() { + // 关闭ScrollController + scrollController.dispose(); - void stopRecording() { - if (isRecording.value) { - isRecording.value = false; - } - } + // 取消所有订阅 + _aiResponseSubscription?.cancel(); + _recognitionSubscription?.cancel(); - void toggleMute() { - isVoiceMuted.value = !isVoiceMuted.value; - } + // 停止TTS + if (_ttsService.isPlaying) { + _ttsService.stop(); + } - void stopVoiceInput() { // 停止语音识别 - if (Get.isRegistered()) { - try { - Get.find().stopRecording(); - Get.delete(); - } 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); + if (isVoiceRecognitionActive.value) { + _asrService.stopContinuousRecognition(); } - _currentVoiceMessage = null; - - // 滚动到底部 - WidgetsBinding.instance.addPostFrameCallback((_) { - if (!_isDisposed && messages.isNotEmpty) { - _scrollToBottom(animate: true); - } - }); - - // 重置按住说话状态 - // 删除语音输入控制器 - try { - if (Get.isRegistered()) { - Get.delete(); - } - } catch (e) { - debugPrint('Error deleting VoiceInputController: $e'); - } - - // 确保按住说话状态被重置 - isRecordingVoice.value = false; + super.onClose(); } - void handleRecognizing(String text) { - if (text.isEmpty) return; - - // 如果TTS正在播放,确保停止它 - if (_isTtsSpeaking) { + /// 滚动到列表底部方法 + void _scrollToBottom() { + if (scrollController.hasClients && messages.isNotEmpty) { 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, + scrollController.animateTo( + scrollController.position.maxScrollExtent, + duration: Duration(milliseconds: 300), + curve: Curves.easeOut, ); - - // 滚动到底部 - _scrollToBottom(animate: false); + } catch (e) { + Logger.error('滚动到底部失败', e); } } } - 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); + // 发送消息 + Future sendMessage(String text) async { + if (text.trim().isEmpty) return; + + // 停止TTS + if (_ttsService.isPlaying) { + _ttsService.stop(); } - - // 保存聊天历史 - _saveChatHistory(); - - // 滚动到底部 - _scrollToBottom(animate: true); - - // 重置状态 - _currentVoiceMessage = null; - isRecognizing.value = false; - - // 处理AI响应 - _processAIResponse(userMessage); - } - - // 处理AI响应 - Future _processAIResponse(Message userMessage) async { - if (_isDisposed) return; - - // TTS会话状态 - bool ttsSessionStarted = false; - final speaker = agentVoice; - - // 创建一个标志,用于跟踪流是否应该继续处理 - bool shouldContinueProcessing = true; - - // 创建监听器,用于检测用户交互 - StreamSubscription? voiceInputSubscription; - StreamSubscription? voicePanelSubscription; - + + // 添加用户消息 + final userMessage = Message( + role: 'user', + content: text, + timestamp: DateTime.now(), + ); + messages.add(userMessage); + + // 滚动到底部显示用户消息 + Future.delayed(Duration(milliseconds: 100), _scrollToBottom); + + // 清空输入框 + inputText.value = ''; + + // 添加加载中的消息 + final loadingMessage = Message.loading(); + messages.add(loadingMessage); + + // 再次滚动到底部显示加载中的消息 + Future.delayed(Duration(milliseconds: 100), _scrollToBottom); + + isSending.value = true; + try { - isLoading.value = true; - - // 创建助手消息 - currentStreamMessage.value = ''; - _currentAssistantMessage = Message( - role: 'assistant', - content: '', - timestamp: DateTime.now(), - ); - messages.add(_currentAssistantMessage!); - + // 构建消息历史 + final List> messageHistory = []; + + // 只取最近的10条消息作为上下文 + final recentMessages = messages.where((m) => !m.isLoading).toList(); + final startIndex = + recentMessages.length > 10 ? recentMessages.length - 10 : 0; + + for (int i = startIndex; i < recentMessages.length; i++) { + final m = recentMessages[i]; + messageHistory.add({ + 'role': m.role, + 'content': m.content, + }); + } + + // 重置取消标志 + _shouldCancelAiResponse = false; + // 准备TTS会话 if (isTtsEnabled.value) { - await _ttsService.endSession(); - - if (!_ttsService.isConnected.value) { - await _ttsService.connect(); - } - - ttsSessionStarted = await _ttsService.startSession(speaker); + await _ttsService.startSession(agent.voice); } - // 设置监听器,检测用户是否开始说话 - 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(); - } + String fullResponse = ''; - // 保存聊天历史 - 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; - } - } - } + // 获取AI响应流 + final responseStream = _aiService.sendMessageStream( + messages: messageHistory, + systemPrompt: agent.systemPrompt, + ); - // 播放文本(用于独立播放场景) - Future _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(); - } - } + // 订阅响应流 + _aiResponseSubscription = responseStream.listen( + (chunk) { + if (_shouldCancelAiResponse) return; - // 确保滚动到底部 - void _ensureScrollToBottom() { - _scrollToBottom(); - - // 延迟滚动以确保视图已构建 - Future.delayed(const Duration(milliseconds: 100), () { - if (!_isDisposed) _scrollToBottom(animate: true); - }); - - // 帧回调确保在渲染后滚动 - WidgetsBinding.instance.addPostFrameCallback((_) { - if (!_isDisposed) _scrollToBottom(animate: true); - }); - } + fullResponse += chunk; - // 滚动监听器 - void _scrollListener() { - if (_isDisposed) return; - - // 检测是否接近底部 - if (scrollController.hasClients) { - final maxScroll = scrollController.position.maxScrollExtent; - final currentScroll = scrollController.offset; - isAtBottom.value = (maxScroll - currentScroll) < 50; - } - } + // 更新加载中的消息内容,但保持isLoading状态 + final index = messages.indexWhere((m) => m.isLoading); + if (index != -1) { + // 直接更新消息内容,然后使用refresh()触发UI更新 + messages[index] = messages[index].copyWith( + content: fullResponse, + isLoading: true, // 明确设置为true以确保状态一致 + ); - @override - void onInit() { - super.onInit(); - messageController = TextEditingController(); - scrollController = ScrollController(); - - // 清理可能存在的语音输入控制器 - if (Get.isRegistered()) { - try { - Get.find().stopRecording(); - Get.delete(); - } catch (e) { - debugPrint('清理语音控制器失败: $e'); - } - } - - // 初始化服务和数据 - _initTtsService(); - _initFromArguments(); - _initializeController(); - - // 添加滚动监听器 - scrollController.addListener(_scrollListener); - - // 确保滚动到底部 - _ensureScrollToBottom(); - } + // 使用refresh()方法刷新列表,触发UI更新 + messages.refresh(); - void _initFromArguments() { - try { - final arguments = Get.arguments; - if (arguments == null) { - throw Exception('No arguments provided to ChatController'); - } + // 每次收到新内容后滚动到底部 + Future.delayed(Duration(milliseconds: 100), _scrollToBottom); + } - final args = arguments as Map; - 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, - ); - } - } + // 如果TTS启用,发送到TTS + if (isTtsEnabled.value && !_shouldCancelAiResponse) { + // 直接将文本块发送到TTS,服务会自己处理断句 + _ttsService.speak(chunk); + } + }, + onDone: () { + if (_shouldCancelAiResponse) return; + + // 完成响应,此时才将isLoading设为false + final index = messages.indexWhere((m) => m.isLoading); + if (index != -1) { + messages[index] = messages[index].copyWith( + content: fullResponse, + isLoading: false, + ); + + // 滚动到底部显示完整回复 + Future.delayed(Duration(milliseconds: 100), _scrollToBottom); + } - void _initTtsService() { - _ttsService = Get.find(); - isTtsEnabled.value = true; - - // 设置TTS事件监听 - _setupTtsEventListener(); - } - - void _setupTtsEventListener() { - _ttsEventSubscription = _ttsService.eventStream.listen(_handleTtsEvent); - } - - void _handleTtsEvent(Map 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; - } - } + // 结束TTS会话 + if (isTtsEnabled.value) { + _ttsService.endSession(); + } - Future _initializeController() async { - try { - await _loadChatHistory(); + isSending.value = false; + + // 保存聊天历史 + _saveChatHistory(); + }, + onError: (error) { + Logger.error('AI响应错误', error); + + // 更新加载中的消息为错误消息 + final index = messages.indexWhere((m) => m.isLoading); + if (index != -1) { + messages[index] = messages[index].copyWith( + content: '抱歉,我遇到了一些问题,无法回应您的请求。', + isLoading: false, + ); + } - await _showWelcomeMessages(); - } catch (e) { - Get.snackbar( - 'Error', - 'Failed to initialize chat: $e', - snackPosition: SnackPosition.BOTTOM, - ); - } - } + // 结束TTS会话 + if (isTtsEnabled.value) { + _ttsService.endSession(); + } - Future _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(); - } - }); + isSending.value = false; + }, + ); } catch (e) { - debugPrint('显示欢迎消息失败: $e'); - - // 播放静态欢迎消息 - if (isTtsEnabled.value) { - _speakText(welcomeMessage); - } - - // 仍然启动语音输入 - if (!_isDisposed) { - Future.delayed(const Duration(milliseconds: 300), () { - startVoiceInput(); - }); + Logger.error('发送消息失败', e); + + // 更新加载中的消息为错误消息 + final index = messages.indexWhere((m) => m.isLoading); + if (index != -1) { + messages[index] = messages[index].copyWith( + content: '抱歉,我遇到了一些问题,无法回应您的请求。', + isLoading: false, + ); } + + isSending.value = false; } } - Future _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; - } + // 切换TTS状态 + void toggleTts() { + isTtsEnabled.toggle(); - // 播放问候语 - 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 = ''; - } + // 如果关闭TTS,停止当前播放 + if (!isTtsEnabled.value && _ttsService.isPlaying) { + _ttsService.stop(); } } - @override - void onClose() { - _isDisposed = true; - - // 停止语音输入和TTS - stopVoiceInput(); - - // 确保TTS完全停止并清理资源 - if (isTtsEnabled.value) { - try { + // 开始语音识别 + Future startVoiceRecognition() async { - _ttsService.stop(); - // 停止当前TTS播放并清除未播放的数据 - _ttsService.endSession(); - - // 确保断开TTS连接,释放所有资源 - _ttsService.disconnect(); - } catch (e) { - debugPrint('关闭TTS服务时出错: $e'); + + if (isVoiceRecognitionActive.value) return; + + // 每次收到新内容后滚动到底部 + Future.delayed(Duration(milliseconds: 100), _scrollToBottom); + // 停止TTS + if (_ttsService.isPlaying) { + _ttsService.stop(); + + // 如果正在接收AI响应,取消它 + if (isSending.value) { + _shouldCancelAiResponse = true; + _aiResponseSubscription?.cancel(); + + // 移除加载中的消息 + messages.removeWhere((m) => m.isLoading); + isSending.value = false; } } - - // 取消TTS事件订阅 - if (_ttsEventSubscription != null) { - _ttsEventSubscription!.cancel(); - _ttsEventSubscription = null; - } - - // 重置状态 - currentStreamMessage.value = ''; - _currentAssistantMessage = null; - _isTtsSpeaking = false; - - // 释放控制器 - messageController.dispose(); - scrollController.dispose(); - - super.onClose(); - } - Future _loadChatHistory() async { try { - final historyJson = _storage.read('${_storagePrefix}$agentId'); - if (historyJson != null) { - final List historyList = json.decode(historyJson); - messages.value = historyList.map((item) => Message.fromJson(item)).toList(); - - // 如果有历史消息,等待下一帧再滚动到底部 - if (messages.isNotEmpty) { - WidgetsBinding.instance.addPostFrameCallback((_) { - _scrollToBottom(animate: true); - }); - } + // 启动语音识别 + final success = await _asrService.startContinuousRecognition(); + + if (success) { + isVoiceRecognitionActive.value = true; + isListening.value = true; + recognizedText.value = ''; + + // 不再添加"正在聆听..."的临时消息 + + // 监听识别事件 + _recognitionSubscription = _asrService.recognitionStream?.listen( + (event) { + switch (event.type) { + case RecognitionEventType.recognizing: + // 实时更新识别文本 + if (event.text.isNotEmpty) { + recognizedText.value = event.text; + + // 检查是否已经有临时消息 + final tempMessageIndex = messages.indexWhere((m) => + m.role == 'user' && + m.timestamp.millisecondsSinceEpoch > + DateTime.now().millisecondsSinceEpoch - 5000); + + if (tempMessageIndex >= 0) { + // 更新已有的临时消息 + messages[tempMessageIndex] = + messages[tempMessageIndex].copyWith( + content: event.text, + ); + // 每次收到新内容后滚动到底部 + Future.delayed( + Duration(milliseconds: 100), _scrollToBottom); + // 使用refresh()方法刷新列表 + messages.refresh(); + } else { + // 添加新的临时消息,但只有在有实际内容时 + final tempMessage = Message( + role: 'user', + content: event.text, + timestamp: DateTime.now(), + ); + messages.add(tempMessage); + } + } + break; + case RecognitionEventType.finalResult: + // 最终结果 + if (event.text.isNotEmpty) { + recognizedText.value = event.text; + + // 查找并移除临时消息 + final tempMessageIndex = messages.indexWhere((m) => + m.role == 'user' && + m.timestamp.millisecondsSinceEpoch > + DateTime.now().millisecondsSinceEpoch - 5000); + + if (tempMessageIndex >= 0) { + messages.removeAt(tempMessageIndex); + } + + // 发送识别到的文本 + sendMessage(event.text); + + // 重置识别文本 + recognizedText.value = ''; + } + break; + case RecognitionEventType.sessionStopped: + case RecognitionEventType.canceled: + // 会话结束 + isVoiceRecognitionActive.value = false; + isListening.value = false; + + // 查找并移除临时消息 + final tempMessageIndex = messages.indexWhere((m) => + m.role == 'user' && + m.timestamp.millisecondsSinceEpoch > + DateTime.now().millisecondsSinceEpoch - 5000); + + if (tempMessageIndex >= 0 && + messages[tempMessageIndex].content.trim().isEmpty) { + messages.removeAt(tempMessageIndex); + } + break; + default: + break; + } + }, + onError: (error) { + Logger.error('语音识别错误', error); + isVoiceRecognitionActive.value = false; + isListening.value = false; + + // 查找并移除临时消息 + final tempMessageIndex = messages.indexWhere((m) => + m.role == 'user' && + m.timestamp.millisecondsSinceEpoch > + DateTime.now().millisecondsSinceEpoch - 5000); + + if (tempMessageIndex >= 0 && + messages[tempMessageIndex].content.trim().isEmpty) { + messages.removeAt(tempMessageIndex); + } + }, + ); + } else { + Logger.error('启动语音识别失败'); } } catch (e) { - Get.snackbar( - 'Error', - 'Failed to load chat history: $e', - snackPosition: SnackPosition.BOTTOM, - ); + Logger.error('启动语音识别失败', e); + isVoiceRecognitionActive.value = false; + isListening.value = false; } } - Future _saveChatHistory() async { + // 停止语音识别 + Future stopVoiceRecognition() async { + if (!isVoiceRecognitionActive.value) return; + 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, - ); - } - } + await _asrService.stopContinuousRecognition(); + _recognitionSubscription?.cancel(); + _recognitionSubscription = null; - 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); - } - } + isVoiceRecognitionActive.value = false; + isListening.value = false; + recognizedText.value = ''; - 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); - } - } - } + // 查找并移除临时消息 + final tempMessageIndex = messages.indexWhere((m) => + m.role == 'user' && + m.timestamp.millisecondsSinceEpoch > + DateTime.now().millisecondsSinceEpoch - 5000); - void clearMessage() { - if (!_isDisposed && messageController.text.isNotEmpty) { - messageController.clear(); + if (tempMessageIndex >= 0 && + messages[tempMessageIndex].content.trim().isEmpty) { + messages.removeAt(tempMessageIndex); + } + } catch (e) { + Logger.error('停止语音识别失败', e); } } - Future sendMessage() async { - if (_isDisposed) return; - - final text = messageController.text.trim(); - if (text.isEmpty) return; + // 清空聊天记录 + void clearChat() { + messages.clear(); - // 创建用户消息 - final userMessage = Message( - role: 'user', - content: text, - timestamp: DateTime.now(), - ); - messages.add(userMessage); - clearMessage(); - await _saveChatHistory(); - _scrollToBottom(animate: true); + // 添加欢迎消息 + if (agent.welcomeMessage.isNotEmpty) { + messages.add(Message( + role: 'assistant', + content: agent.welcomeMessage, + timestamp: DateTime.now(), + )); + } - // 处理AI响应 - await _processAIResponse(userMessage); + // 删除存储的历史记录 + _historyService.clearHistory(agent.id); } - Future clearChat() async { - try { + // 加载聊天历史 + void _loadChatHistory() { + final history = _historyService.loadHistory(agent.id); + if (history.isNotEmpty) { messages.clear(); - await _storage.remove('${_storagePrefix}$agentId'); - await _showWelcomeMessages(); - } catch (e) { - Get.snackbar( - 'Error', - 'Failed to clear chat: $e', - snackPosition: SnackPosition.BOTTOM, - ); + messages.addAll(history); } } - void toggleTTS() { - // 切换TTS启用状态 - isTtsEnabled.toggle(); - if (!isTtsEnabled.value) { - _ttsService.endSession(); - } + // 保存聊天历史 + void _saveChatHistory() { + _historyService.saveHistory(agent.id, messages); } - /// 生成简单的问候语 - Future 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 addMessage(Message message) { + messages.add(message); + _saveChatHistory(); - // 开始按住说话 - void startPressToTalk() { - isRecordingVoice.value = true; - - startVoiceInput(); - } - - // 结束按住说话 - void endPressToTalk() { - isRecordingVoice.value = false; - - stopVoiceInput(); + // 滚动到底部显示新添加的消息 + Future.delayed(Duration(milliseconds: 100), _scrollToBottom); } } diff --git a/lib/modules/chat/controllers/voice_input_controller.dart b/lib/modules/chat/controllers/voice_input_controller.dart deleted file mode 100644 index 79034adc1..000000000 --- a/lib/modules/chat/controllers/voice_input_controller.dart +++ /dev/null @@ -1,348 +0,0 @@ -import 'dart:async'; -import 'dart:math'; -import 'package:get/get.dart'; -import 'package:flutter/foundation.dart'; -import '../../../data/services/azure_asr_service.dart'; -import '../../../data/services/volcano_tts_api_service.dart'; -import '../../../core/utils/logger.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; - -class VoiceInputController extends GetxController { - // Observable states - final isConnecting = true.obs; - final isMuted = false.obs; - final isRecording = false.obs; - final recognizedText = ''.obs; - - // 添加一个新的可观察状态,表示用户是否正在说话 - final isUserSpeaking = false.obs; - - // 语音识别服务 - late final AzureAsrService _voiceService; - late final VolcanoTtsApiService _ttsService; - - // 连续识别相关 - StreamSubscription? _recognitionSubscription; - - // TTS相关订阅 - StreamSubscription? _ttsEventSubscription; - bool _isTtsSpeaking = false; - - // 添加一个标志,表示是否已经识别到语音 - bool _hasRecognizedSpeech = false; - - // 添加一个 Completer 用于在识别到最终结果时完成 - Completer? _recognitionCompleter; - - // 添加一个标志,表示是否已经收到最终结果 - bool _hasFinalResult = false; - - // 添加一个计时器,用于定期更新用户说话状态 - Timer? _speakingCheckTimer; - - // 最后一次识别到语音的时间 - DateTime? _lastSpeechTime; - - // Callbacks - final Function(String) onRecordingResult; - final VoidCallback onClosePanel; - final Function(String) onRecognizing; - - VoiceInputController({ - required this.onRecordingResult, - required this.onClosePanel, - required this.onRecognizing, - }); - - @override - void onInit() { - super.onInit(); - _initializeVoiceService(); - _ttsService = Get.find(); - - // 设置TTS事件监听 - _setupTtsEventListener(); - - // 启动定期检查用户是否正在说话的计时器 - _startSpeakingCheckTimer(); - } - - // 设置TTS事件监听 - void _setupTtsEventListener() { - _ttsEventSubscription?.cancel(); - _ttsEventSubscription = _ttsService.eventStream.listen(_handleTtsEvent); - } - - // 处理TTS事件 - void _handleTtsEvent(Map event) { - final eventType = event['eventType']; - - switch (eventType) { - case 'ttsSentenceStart': - _isTtsSpeaking = true; - break; - case 'ttsSentenceEnd': - case 'sessionFinished': - case 'error': - _isTtsSpeaking = false; - break; - } - } - - Future _initializeVoiceService() async { - try { - // 获取语音识别服务实例 - _voiceService = Get.find(); - - // 初始化语音识别服务 - await _voiceService.initialize( - subscriptionKey: dotenv.env['AZURE_ASR_SUBSCRIPTION_KEY'] ?? '', - serviceRegion: dotenv.env['AZURE_ASR_SERVICE_REGION'] ?? 'eastasia', - language: dotenv.env['AZURE_ASR_LANGUAGE'] ?? 'zh-CN', - ); - - // 初始化完成后更新状态 - isConnecting.value = false; - - // 自动开始连续语音识别 - await startContinuousRecognition(); - } catch (e) { - Logger.error('初始化语音识别服务失败', e); - Get.snackbar( - 'Error', - '初始化语音识别服务失败: $e', - snackPosition: SnackPosition.BOTTOM, - ); - } - } - - Future startContinuousRecognition() async { - if (isConnecting.value) return; - if (isRecording.value) return; // 已经在录音中 - - try { - isRecording.value = true; - recognizedText.value = ''; // 清空识别文本,但不再在面板中显示 - _hasRecognizedSpeech = false; - _hasFinalResult = false; - - // 创建一个 Completer 来处理语音识别完成 - _recognitionCompleter = Completer(); - - // 开始连续语音识别 - final success = await _voiceService.startContinuousRecognition(); - if (!success) { - isRecording.value = false; - throw Exception('无法启动语音识别'); - } - - // 订阅识别事件流 - _recognitionSubscription = _voiceService.recognitionStream?.listen((event) { - switch (event.type) { - case RecognitionEventType.recognizing: - // 不再更新面板中的识别文本,而是通过回调传递给ChatController - if (event.text.isNotEmpty) { - _hasRecognizedSpeech = true; - _lastSpeechTime = DateTime.now(); - - // 设置用户正在说话状态 - isUserSpeaking.value = true; - - // 如果系统正在播放TTS,检测到用户开始说话时立即中断 - if (event.text.trim().isNotEmpty) { - print('检测到用户开始说话,中断TTS播放'); - _ttsService.stop(); - _ttsService.endSession(); // 结束当前TTS会话 - } - } - - // 调用识别中回调,将中间结果传递给ChatController - onRecognizing(event.text); - break; - - case RecognitionEventType.finalResult: - // 更新最终识别结果 - _hasFinalResult = true; - - // 重置用户说话状态 - isUserSpeaking.value = false; - - if (event.text.isNotEmpty) { - // 不再更新面板中的识别文本 - _hasRecognizedSpeech = true; - _lastSpeechTime = DateTime.now(); - - // 调用最终结果回调,发送识别到的句子 - onRecordingResult(event.text); - - // 清空识别状态,准备下一句,但不停止识别 - _hasRecognizedSpeech = false; - _hasFinalResult = false; - - // 完成当前识别,但不停止连续识别 - if (_recognitionCompleter != null && !_recognitionCompleter!.isCompleted) { - _recognitionCompleter!.complete(event.text); - } - } else { - // 如果最终结果为空,但之前有中间结果,仍然标记为有语音 - if (_hasRecognizedSpeech) { - print('最终识别结果为空,但之前有中间结果,使用最后的中间结果'); - - // 清空识别状态,准备下一句,但不停止识别 - _hasRecognizedSpeech = false; - _hasFinalResult = false; - } - } - break; - - case RecognitionEventType.error: - print('语音识别错误: ${event.error}'); - Get.snackbar( - 'Error', - '语音识别错误: ${event.error}', - snackPosition: SnackPosition.BOTTOM, - ); - // 尝试重新启动识别 - restartRecognition(); - break; - - case RecognitionEventType.sessionStopped: - // 会话结束,尝试重新启动 - isRecording.value = false; - restartRecognition(); - break; - - default: - break; - } - }, onError: (error) { - print('语音识别流错误: $error'); - Get.snackbar( - 'Error', - '语音识别流错误: $error', - snackPosition: SnackPosition.BOTTOM, - ); - isRecording.value = false; - // 尝试重新启动识别 - restartRecognition(); - }); - - } catch (e) { - print('语音识别失败: $e'); - Get.snackbar( - 'Error', - '启动语音识别失败: $e', - snackPosition: SnackPosition.BOTTOM, - ); - isRecording.value = false; - } finally { - // 清理资源 - if (_recognitionCompleter != null && !_recognitionCompleter!.isCompleted) { - _recognitionCompleter!.complete(''); - } - _recognitionCompleter = null; - } - } - - // 重新启动识别 - Future restartRecognition() async { - try { - // 先停止当前识别 - await stopRecording(); - // 延迟一下再重新启动 - await Future.delayed(const Duration(milliseconds: 500)); - // 重新启动连续识别 - await startContinuousRecognition(); - } catch (e) { - print('重新启动语音识别失败: $e'); - } - } - - Future stopRecording() async { - if (!isRecording.value) return; - - try { - // 取消事件订阅 - await _recognitionSubscription?.cancel(); - _recognitionSubscription = null; - - // 停止连续识别 - if (_voiceService.isContinuousRecognitionActive()) { - await _voiceService.stopContinuousRecognition(); - } - - isRecording.value = false; - _hasRecognizedSpeech = false; - _hasFinalResult = false; - } catch (e) { - print('停止语音识别失败: $e'); - isRecording.value = false; - } - } - - void toggleMute() { - // 只切换静音状态,不再影响录音 - // 因为已启用回声消除,不需要在录音时停止TTS - isMuted.value = !isMuted.value; - } - - // 手动开始录音(用户点击按钮) - void startRecording() { - if (!isRecording.value) { - startContinuousRecognition(); - } - } - - // 启动定期检查用户是否正在说话的计时器 - void _startSpeakingCheckTimer() { - _speakingCheckTimer?.cancel(); - _speakingCheckTimer = Timer.periodic(const Duration(milliseconds: 200), (_) { - _checkIfUserIsSpeaking(); - }); - } - - // 检查用户是否正在说话 - void _checkIfUserIsSpeaking() { - // 如果有最近的识别文本,且距离现在不超过1秒,认为用户正在说话 - if (_hasRecognizedSpeech && !_hasFinalResult && _lastSpeechTime != null) { - final now = DateTime.now(); - final timeSinceLastSpeech = now.difference(_lastSpeechTime!); - - // 如果最近1秒内有语音识别,认为用户正在说话 - if (timeSinceLastSpeech.inMilliseconds < 1000) { - isUserSpeaking.value = true; - return; - } - } - - // 默认情况下,如果正在录音但没有最近的语音识别,随机生成一个说话状态 - // 保持这个功能,让麦克风图标有动画效果 - if (isRecording.value) { - // 随机生成一个值,有20%的概率认为用户正在说话 - isUserSpeaking.value = Random().nextDouble() < 0.2; - } else { - isUserSpeaking.value = false; - } - } - - @override - void onClose() { - // 停止录音 - stopRecording(); - - // 取消计时器 - _speakingCheckTimer?.cancel(); - - // 取消TTS事件订阅 - _ttsEventSubscription?.cancel(); - - // 重置状态 - isConnecting.value = false; - isMuted.value = false; - isRecording.value = false; - recognizedText.value = ''; - isUserSpeaking.value = false; - - super.onClose(); - } -} \ No newline at end of file diff --git a/lib/modules/chat/models/message_model.dart b/lib/modules/chat/models/message_model.dart index c051b616e..6307989ae 100644 --- a/lib/modules/chat/models/message_model.dart +++ b/lib/modules/chat/models/message_model.dart @@ -1,12 +1,14 @@ class Message { - final String role; + final String role; // 'user' or 'assistant' final String content; final DateTime timestamp; + final bool isLoading; Message({ required this.role, required this.content, required this.timestamp, + this.isLoading = false, }); // 从JSON构造函数 @@ -15,6 +17,7 @@ class Message { role: json['role'] as String, content: json['content'] as String, timestamp: DateTime.parse(json['timestamp'] as String), + isLoading: json['isLoading'] as bool? ?? false, ); } @@ -24,6 +27,32 @@ class Message { 'role': role, 'content': content, 'timestamp': timestamp.toIso8601String(), + 'isLoading': isLoading, }; } + + // 创建一个加载中的消息 + factory Message.loading() { + return Message( + role: 'assistant', + content: '', + timestamp: DateTime.now(), + isLoading: true, + ); + } + + // 复制并修改 + Message copyWith({ + String? role, + String? content, + DateTime? timestamp, + bool? isLoading, + }) { + return Message( + role: role ?? this.role, + content: content ?? this.content, + timestamp: timestamp ?? this.timestamp, + isLoading: isLoading ?? this.isLoading, + ); + } } \ No newline at end of file diff --git a/lib/modules/chat/views/chat_view.dart b/lib/modules/chat/views/chat_view.dart index b74ed740b..8069a1821 100644 --- a/lib/modules/chat/views/chat_view.dart +++ b/lib/modules/chat/views/chat_view.dart @@ -1,625 +1,490 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'dart:math'; import '../controllers/chat_controller.dart'; import '../models/message_model.dart'; -import '../../../data/services/volcano_tts_api_service.dart'; -import '../widgets/voice_input_panel.dart'; +import '../../../core/theme/app_colors.dart'; +import '../../../core/widgets/message_bubble.dart'; class ChatView extends GetView { - const ChatView({Key? key}) : super(key: key); + ChatView({Key? key}) : super(key: key); + + // 添加ScrollController用于控制滚动 + final ScrollController _scrollController = ScrollController(); @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async { - // 停止TTS服务 - final tts = Get.find(); - await tts.endSession(); - - // 停止语音输入 - if (controller.isVoiceInputVisible.value) { - controller.stopVoiceInput(); + // 监听消息列表变化,自动滚动到底部 + ever(controller.messages, (_) { + // 使用Future.delayed确保在UI更新后滚动 + Future.delayed(Duration(milliseconds: 50), () { + if (_scrollController.hasClients && + _scrollController.position.maxScrollExtent > 0 && + (_scrollController.position.maxScrollExtent - _scrollController.offset) < 200) { + _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: Duration(milliseconds: 100), + curve: Curves.easeOut, + ); } - - return true; - }, - child: Scaffold( - backgroundColor: Colors.white, - appBar: _buildAppBar(), - body: SafeArea( - child: Stack( - children: [ - // 主要内容 - Column( - children: [ - Expanded( - child: _buildMessageList(), - ), - _buildInputSection(), - ], - ), - ], - ), - ), - ), - ); - } + }); + }); - PreferredSizeWidget _buildAppBar() { - return AppBar( - backgroundColor: Colors.white, - elevation: 0, - leading: IconButton( - icon: Icon( - Icons.arrow_back_ios, - color: Colors.black, - size: 20.sp, + return Scaffold( + backgroundColor: Colors.white, // 设置Scaffold背景为白色 + appBar: AppBar( + leading: IconButton( + icon: Icon(Icons.arrow_back_ios, color: Colors.black), + onPressed: () => Get.back(), ), - onPressed: () async { - // 停止TTS服务 - final tts = Get.find(); - await tts.endSession(); - - // 停止语音输入 - if (controller.isVoiceInputVisible.value) { - controller.stopVoiceInput(); - } - - Get.back(); - }, - ), - title: Column( - children: [ - Text( - controller.agentName, - style: TextStyle( - fontSize: 17.sp, - fontWeight: FontWeight.w600, - color: Colors.black, - ), - ), - if (controller.agentSubtitle != null) - Text( - controller.agentSubtitle!, - style: TextStyle( - fontSize: 12.sp, - color: Colors.grey[600], - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, + title: Row( + children: [ + CircleAvatar( + backgroundImage: AssetImage(controller.agent.avatarUrl), + radius: 18.r, ), - ], - ), - centerTitle: true, - actions: [ - IconButton( - icon: Obx(() => Icon( - controller.isTtsEnabled.value - ? Icons.volume_up - : Icons.volume_off, - color: Colors.black, - size: 24.sp, - )), - onPressed: controller.toggleTTS, - ), - IconButton( - icon: Icon( - Icons.more_horiz, - color: Colors.black, - size: 24.sp, - ), - onPressed: () => _showMoreOptions(), - ), - ], - ); - } - - Widget _buildMessageList() { - return Stack( - children: [ - Obx(() => ListView.builder( - controller: controller.scrollController, - padding: EdgeInsets.only( - top: 16.h, - bottom: controller.isVoiceInputVisible.value - ? 160.h - : (controller.isInputCollapsed.value ? 70.h : 90.h), - ), - itemCount: controller.messages.length, - itemBuilder: (context, index) => _buildMessage(controller.messages[index]), - physics: const AlwaysScrollableScrollPhysics(), - clipBehavior: Clip.none, - keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, - )), - Obx(() { - if (!controller.isLoading.value) return const SizedBox.shrink(); - return Positioned( - bottom: controller.isVoiceInputVisible.value - ? 170.h - : (controller.isInputCollapsed.value ? 80.h : 100.h), - left: 0, - right: 0, - child: Container( - padding: EdgeInsets.symmetric(vertical: 8.h), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, + SizedBox(width: 10.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ - SizedBox( - width: 16.w, - height: 16.w, - child: const CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Color(0xFF4A6FE5)), + Text( + controller.agent.name, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + Text( + controller.agent.description.length > 20 + ? '${controller.agent.description.substring(0, 20)}...' + : controller.agent.description, + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey, ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ], ), ), - ); - }), - ], - ); - } - - Widget _buildInputSection() { - return Obx(() { - if (controller.isVoiceInputVisible.value) { - return Column( - mainAxisSize: MainAxisSize.min, + ], + ), + actions: [ + // TTS开关 + Obx(() => IconButton( + icon: Icon( + controller.isTtsEnabled.value + ? Icons.volume_up + : Icons.volume_off, + color: controller.isTtsEnabled.value + ? AppColors.primary + : Colors.grey, + ), + onPressed: controller.toggleTts, + )), + // 更多选项 + PopupMenuButton( + icon: Icon(Icons.more_vert, color: Colors.black), + itemBuilder: (context) => [ + PopupMenuItem( + value: 'clear', + child: Row( + children: [ + Icon(Icons.delete_outline, color: Colors.red), + SizedBox(width: 8.w), + Text('清空对话'), + ], + ), + ), + ], + onSelected: (value) { + if (value == 'clear') { + controller.clearChat(); + } + }, + ), + ], + elevation: 0, + backgroundColor: Colors.white, + ), + body: Container( + color: Colors.white, // 确保body背景也是白色 + child: Column( children: [ - VoiceInputPanel( - onRecordingResult: controller.handleVoiceResult, - onClose: controller.stopVoiceInput, - onRecognizing: controller.handleRecognizing, + // 消息列表 + Expanded( + child: Obx(() => ListView.builder( + controller: controller.scrollController, + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h), + itemCount: controller.messages.length, + reverse: false, + physics: const AlwaysScrollableScrollPhysics(), + cacheExtent: 1000, // 增加缓存范围,减少重建 + itemBuilder: (context, index) { + final message = controller.messages[index]; + return MessageBubble( + key: ObjectKey(message), // 使用ObjectKey而不是ValueKey + message: message, + isUser: message.role == 'user', + ); + }, + )), ), + + // 输入区域 - 根据当前模式显示不同的输入面板 + Obx(() => controller.isVoiceRecognitionActive.value + ? _buildVoiceInputPanel() + : _buildTextInputPanel()), ], - ); - } - return _buildTextInput(); - }); + ), + ), + ); } - Widget _buildTextInput() { + // 构建文本输入面板 + Widget _buildTextInputPanel() { + // 创建一个TextEditingController来控制输入框 + final TextEditingController textController = TextEditingController(text: controller.inputText.value); + + // 监听输入变化 + textController.addListener(() { + controller.inputText.value = textController.text; + }); + return Container( padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), decoration: BoxDecoration( color: Colors.white, - border: Border( - top: BorderSide( - color: Colors.grey[200]!, - width: 1, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 5, + offset: Offset(0, -1), ), - ), + ], ), - child: Obx(() { - if (controller.isRecordingVoice.value) { - return _buildRecordingInput(); - } else if (!controller.isVoiceMode.value) { - return _buildExpandedInput(); - } else { - return _buildDefaultInput(); - } - }), - ); - } - - Widget _buildDefaultInput() { - return Row( - children: [ - Container( - width: 40.w, - height: 40.w, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Colors.grey[100], - border: Border.all( - color: Colors.grey[300]!, - width: 0.5, - ), - ), - child: IconButton( + child: Row( + children: [ + // 语音输入按钮 + IconButton( icon: Icon( - Icons.camera_alt, - color: Colors.grey[600], - size: 20.sp, + Icons.mic_none, + color: Colors.grey, ), - onPressed: () { - // 拍照功能 - }, - padding: EdgeInsets.zero, + onPressed: controller.startVoiceRecognition, ), - ), - SizedBox(width: 8.w), - Expanded( - child: GestureDetector( - onTap: () { - // 点击打开VoiceInputPanel - controller.startVoiceInput(); - }, - child: Container( - height: 48.h, - decoration: BoxDecoration( - color: Colors.grey[100], - borderRadius: BorderRadius.circular(24.r), - border: Border.all( - color: Colors.grey[300]!, - width: 0.5, + // 文本输入框 + Expanded( + child: TextField( + controller: textController, + decoration: InputDecoration( + hintText: '发消息...', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(20.r), + borderSide: BorderSide.none, ), - ), - child: Center( - child: Text( - '点击开始连续语音对话', - style: TextStyle( - color: Colors.grey[600], - fontSize: 16.sp, - ), + filled: true, + fillColor: Colors.grey[200], + contentPadding: EdgeInsets.symmetric( + horizontal: 16.w, + vertical: 8.h, ), ), + maxLines: null, + textInputAction: TextInputAction.send, + onSubmitted: (value) { + if (value.trim().isNotEmpty) { + controller.sendMessage(value); + textController.clear(); + } + }, ), ), - ), - SizedBox(width: 8.w), - Container( - width: 40.w, - height: 40.w, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Colors.grey[100], - border: Border.all( - color: Colors.grey[300]!, - width: 0.5, - ), - ), - child: IconButton( - icon: Icon( - Icons.keyboard, - color: Colors.grey[600], - size: 20.sp, - ), - onPressed: controller.toggleInputMode, - padding: EdgeInsets.zero, - ), - ), - ], + // 发送按钮 + Obx(() => IconButton( + icon: Icon( + Icons.send, + color: controller.inputText.value.trim().isEmpty + ? Colors.grey + : AppColors.primary, + ), + onPressed: controller.inputText.value.trim().isEmpty + ? null + : () { + controller.sendMessage(controller.inputText.value); + textController.clear(); + }, + )), + ], + ), ); } - Widget _buildExpandedInput() { - return Row( - children: [ - Container( - width: 40.w, - height: 40.w, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Colors.grey[100], - border: Border.all( - color: Colors.grey[300]!, - width: 0.5, - ), - ), - child: IconButton( - icon: Icon( - Icons.mic, - color: const Color(0xFF4A6FE5), - size: 20.sp, - ), - onPressed: controller.toggleInputMode, - padding: EdgeInsets.zero, - ), + // 构建语音输入面板 + Widget _buildVoiceInputPanel() { + final screenWidth = Get.width; + + return Container( + width: screenWidth, + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xFFFFFFFF), // 纯白色开始 + Color(0xFFE8D5F5), // 更深的紫色结束 + ], + stops: [0.3, 1.0], // 调整颜色分布,让白色占据更多空间 ), - SizedBox(width: 8.w), - Expanded( - child: Container( - padding: EdgeInsets.symmetric(horizontal: 12.w), - decoration: BoxDecoration( - color: Colors.grey[100], - borderRadius: BorderRadius.circular(24.r), - border: Border.all( - color: Colors.grey[300]!, - width: 0.5, - ), - ), - child: TextField( - controller: controller.messageController, - decoration: InputDecoration( - hintText: '输入消息...', - hintStyle: TextStyle( - color: Colors.grey[400], - fontSize: 14.sp, - ), - border: InputBorder.none, - contentPadding: EdgeInsets.symmetric(vertical: 12.h), - ), - maxLines: 5, - minLines: 1, - style: TextStyle( - fontSize: 14.sp, - color: Colors.black87, + ), + child: SafeArea( + child: SizedBox( + height: 100.h, + child: Center( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 20.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // 左侧麦克风按钮 + _buildMicrophoneButton(), + + // 中间音频波形和状态文本 + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // 音频波形 + _buildAudioWaveform(), + SizedBox(height: 10.h), + // 状态文本 + Obx(() => Text( + controller.recognizedText.isEmpty ? '你可以开始说话' : '正在聆听...', + style: TextStyle( + fontSize: 16.sp, + color: Colors.grey.shade700, + fontWeight: FontWeight.w500, + ), + )), + ], + ), + ), + + // 右侧关闭按钮 + _buildCloseButton(), + ], ), - textInputAction: TextInputAction.send, - onSubmitted: (_) => controller.sendMessage(), ), ), ), - SizedBox(width: 8.w), - Container( - width: 40.w, - height: 40.w, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: Color(0xFF4A6FE5), - ), - child: IconButton( - icon: Icon( - Icons.send, - color: Colors.white, - size: 20.sp, - ), - onPressed: controller.sendMessage, - padding: EdgeInsets.zero, - ), - ), - ], + ), ); } - - Widget _buildRecordingInput() { + + // 麦克风按钮 + Widget _buildMicrophoneButton() { return Container( - height: 48.h, + width: 60.w, + height: 60.w, decoration: BoxDecoration( - color: const Color(0xFF4A6FE5), - borderRadius: BorderRadius.circular(24.r), + color: Colors.white, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + spreadRadius: 1, + ), + ], ), child: Center( - child: Row( - mainAxisSize: MainAxisSize.min, - children: List.generate( - 40, - (index) => Container( - width: 4.w, - height: 4.h, - margin: EdgeInsets.symmetric(horizontal: 1.w), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(2.r), - ), - ), + child: Obx(() => IconButton( + icon: Icon( + controller.isListening.value ? Icons.mic : Icons.mic_none, + color: controller.isListening.value ? AppColors.primary : Colors.grey.shade400, + size: 28.sp, ), - ), + onPressed: controller.stopVoiceRecognition, + padding: EdgeInsets.zero, + )), ), ); } - - void _showMoreOptions() { - Get.bottomSheet( - Container( - padding: EdgeInsets.all(20.w), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(20.r), - topRight: Radius.circular(20.r), + + // 关闭按钮 + Widget _buildCloseButton() { + return Container( + width: 60.w, + height: 60.w, + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + spreadRadius: 1, ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - leading: const Icon(Icons.delete_outline, color: Colors.red), - title: const Text('清空对话'), - onTap: () { - Get.back(); - controller.clearChat(); - }, - ), - ], + ], + ), + child: Center( + child: IconButton( + icon: Icon( + Icons.close, + color: Colors.grey.shade600, + size: 28.sp, + ), + onPressed: controller.stopVoiceRecognition, + padding: EdgeInsets.zero, ), ), ); } - - Widget _buildMessage(Message message) { - final isUser = message.role == 'user'; - final isSystem = message.role == 'system'; - final isRecognizing = isUser && message.content.startsWith('🎤 '); - - if (isSystem) { - // 系统消息使用特殊样式 - return Padding( - padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), - child: Container( - padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), - decoration: BoxDecoration( - color: Colors.grey[100], - borderRadius: BorderRadius.circular(12.r), - border: Border.all( - color: Colors.grey[300]!, - width: 0.5, - ), - ), - child: Row( - children: [ - Icon( - Icons.info_outline, - color: Colors.grey[600], - size: 20.sp, - ), - SizedBox(width: 8.w), - Expanded( - child: Text( - message.content, - style: TextStyle( - fontSize: 14.sp, - color: Colors.grey[700], - height: 1.5, - ), - ), - ), - ], - ), - ), - ); - } + + // 音频波形 + Widget _buildAudioWaveform() { + final barCount = 23; // 条形数量 - return Padding( - padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), - child: Row( - mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!isUser) ...[ - CircleAvatar( - radius: 20.r, - backgroundImage: AssetImage(controller.agentAvatar), - backgroundColor: Colors.transparent, - ), - SizedBox(width: 12.w), - ], - Flexible( - child: Container( - padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), - decoration: BoxDecoration( - color: isUser - ? (isRecognizing ? Colors.purple.withOpacity(0.7) : const Color(0xFF4A6FE5)) - : const Color(0xFFF5F5F5), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(isUser ? 18.r : 4.r), - topRight: Radius.circular(isUser ? 4.r : 18.r), - bottomLeft: Radius.circular(18.r), - bottomRight: Radius.circular(18.r), - ), - // 为正在识别的消息添加脉动效果 - boxShadow: isRecognizing - ? [ + return SizedBox( + width: 220.w, + height: 50.h, + child: Obx(() { + final isSpeaking = controller.recognizedText.isNotEmpty; + final isRec = controller.isListening.value; + + return Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate( + barCount, + (index) { + // 创建动态高度效果 + double height = isRec + ? _getWaveHeight(index, isSpeaking, barCount).clamp(1.h, 22.5.h) + : 2.h; + + // 动画持续时间 + final duration = Duration( + milliseconds: isSpeaking ? 60 + (index % 4) * 20 : 150 + (index % 3) * 50 + ); + + // 颜色计算 + Color barColor; + if (isRec) { + if (isSpeaking) { + final middleIndex = barCount ~/ 2; + final distance = (index - middleIndex).abs(); + final maxDistance = middleIndex; + final colorIntensity = 1.0 - (distance / maxDistance) * 0.2; + + barColor = index % 3 == 0 + ? AppColors.primary + : AppColors.primary.withOpacity(colorIntensity); + } else { + barColor = AppColors.primary.withOpacity(0.3); + } + } else { + barColor = Colors.grey.shade400; + } + + return SizedBox( + width: 5.5.w, + child: Center( + child: AnimatedContainer( + duration: duration, + curve: isSpeaking ? Curves.elasticOut : Curves.easeInOut, + width: isSpeaking ? 2.8.w : 2.5.w, + height: height, + margin: EdgeInsets.symmetric(horizontal: 1.2.w), + decoration: BoxDecoration( + color: barColor, + borderRadius: BorderRadius.circular(2.r), + boxShadow: isSpeaking ? [ BoxShadow( - color: Colors.purple.withOpacity(0.3), - blurRadius: 8, - spreadRadius: 2, - ), - ] - : [], - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (isRecognizing) ...[ - Icon( - Icons.mic, - color: Colors.white, - size: 16.sp, - ), - SizedBox(width: 8.w), - ], - Flexible( - child: Text( - // 如果是正在识别的消息,移除麦克风图标前缀 - isRecognizing ? message.content.substring(2) : message.content, - style: TextStyle( - fontSize: 16.sp, - color: isUser ? Colors.white : Colors.black87, - height: 1.5, - ), + color: AppColors.primary.withOpacity(0.4), + blurRadius: 5, + spreadRadius: 1.2, + ) + ] : null, ), ), - if (isRecognizing) ...[ - SizedBox(width: 8.w), - _buildPulsatingDot(), - ], - ], - ), - ), + ), + ); + }, ), - if (isUser) ...[ - SizedBox(width: 12.w), - CircleAvatar( - radius: 20.r, - backgroundColor: const Color(0xFFEEF4FF), - child: Icon( - Icons.person, - color: const Color(0xFF4A6FE5), - size: 24.sp, - ), - ), - ], - ], - ), + ); + }), ); } - // 创建脉动点动画 - Widget _buildPulsatingDot() { - return Obx(() { - // 使用controller.isRecognizing来控制动画是否显示 - if (!controller.isRecognizing.value) return const SizedBox.shrink(); + // 生成动态波形高度 + double _getWaveHeight(int index, bool isSpeaking, int barCount) { + try { + // 基础高度 + double baseHeight = isSpeaking ? 5.h : 3.h; - return _PulsatingDot(); - }); - } -} - -// 脉动点动画组件 -class _PulsatingDot extends StatefulWidget { - @override - _PulsatingDotState createState() => _PulsatingDotState(); -} - -class _PulsatingDotState extends State<_PulsatingDot> with SingleTickerProviderStateMixin { - late AnimationController _animationController; - late Animation _animation; - - @override - void initState() { - super.initState(); - _animationController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 1000), - )..repeat(reverse: true); - - _animation = Tween(begin: 0.5, end: 1.0).animate(_animationController); - } - - @override - void dispose() { - _animationController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return SizedBox( - width: 16.w, - height: 16.w, - child: AnimatedBuilder( - animation: _animation, - builder: (context, child) { - return Stack( - alignment: Alignment.center, - children: [ - Opacity( - opacity: 1.0 - _animation.value, - child: Container( - width: 16.w * _animation.value, - height: 16.w * _animation.value, - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.5), - shape: BoxShape.circle, - ), - ), - ), - Container( - width: 6.w, - height: 6.w, - decoration: const BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), - ), - ], - ); - }, - ), - ); + // 获取当前时间毫秒数 + final DateTime now = DateTime.now(); + final int milliseconds = now.millisecondsSinceEpoch; + + // 计算中心位置 + final middleIndex = barCount ~/ 2; + final distanceFromMiddle = (index - middleIndex).abs(); + + // 使用多个正弦波叠加 + double freqMultiplier = isSpeaking ? 2.2 : 1.0; + double ampMultiplier = isSpeaking ? 1.8 : 1.0; + + double wave1 = sin((milliseconds / 150) * freqMultiplier + (index * 0.8)) * 2.h * ampMultiplier; + double wave2 = sin((milliseconds / 280) * freqMultiplier + (index * 0.5)) * 1.5.h * ampMultiplier; + double wave3 = sin((milliseconds / 340) * freqMultiplier + (index * 0.3)) * 1.25.h * ampMultiplier; + double wave4 = cos((milliseconds / 220) * freqMultiplier + (index * 0.7)) * 1.h * ampMultiplier; + + // 基于距离中心的位置计算高度衰减 + double positionFactor = isSpeaking + ? 1.0 - (distanceFromMiddle / middleIndex) * 0.2 + : 1.0 - (distanceFromMiddle / middleIndex) * 0.5; + + if (isSpeaking) { + // 用户说话时的波形 + double timeFactor = 0.9 + sin(milliseconds / 600) * 0.5; + + // 增强中心突出效果 + double centerBoost = pow(positionFactor, 1.2) * 10.h; + + // 增加随机性 + double randomFactor = (Random().nextDouble() * 4 - 2) * 1.75.h; + + // 增强脉冲效果 + double pulseEffect = sin(milliseconds / 300) * 2.5.h * positionFactor; + + // 添加额外的随机波动 + double voiceVariation = sin(milliseconds / 80 + index * 1.2) * 1.5.h * Random().nextDouble(); + + // 添加爆发效果 + double burstEffect = 0; + if (Random().nextDouble() > 0.97) { + burstEffect = 5.h * positionFactor; + } + + return baseHeight + (wave1 + wave2 + wave3 + wave4) * timeFactor + + centerBoost + randomFactor + pulseEffect + voiceVariation + burstEffect; + } else { + // 用户不说话时的波形 + double idleWave = (wave1 + wave2) * 0.35 * positionFactor; + double gentlePulse = sin(milliseconds / 800) * 1.h * positionFactor; + return baseHeight + idleWave + gentlePulse; + } + } catch (e) { + // 发生错误时返回默认高度 + return 2.h; + } } } \ No newline at end of file diff --git a/lib/modules/chat/widgets/voice_input_panel.dart b/lib/modules/chat/widgets/voice_input_panel.dart deleted file mode 100644 index dbd67d731..000000000 --- a/lib/modules/chat/widgets/voice_input_panel.dart +++ /dev/null @@ -1,302 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'dart:math'; -import 'dart:async'; -import '../controllers/voice_input_controller.dart'; - -class VoiceInputPanel extends GetView { - const VoiceInputPanel({ - Key? key, - required this.onRecordingResult, - required this.onClose, - required this.onRecognizing, - }) : super(key: key); - - final Function(String) onRecordingResult; - final VoidCallback onClose; - final Function(String) onRecognizing; - - @override - Widget build(BuildContext context) { - return Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Color(0xFFFFFFFF), // 纯白色开始 - Color(0xFFE8D5F5), // 更深的紫色结束 - ], - stops: [0.3, 1.0], // 调整颜色分布,让白色占据更多空间 - ), - ), - child: SafeArea( - child: Container( - height: 120.h, - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // 主要内容:麦克风图标、音频波形、关闭按钮 - Padding( - padding: EdgeInsets.symmetric(horizontal: 20.w), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - // 左侧麦克风图标 - Container( - width: 60.w, - height: 60.w, - decoration: BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 10, - spreadRadius: 1, - ), - ], - ), - child: Center( - child: Obx(() => Icon( - controller.isRecording.value ? Icons.mic : Icons.mic_none, - color: controller.isRecording.value ? Colors.purple : Colors.purple.shade300, - size: 28.sp, - )), - ), - ), - - // 中间音频波形和状态文本 - Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildAudioWaveform(), - SizedBox(height: 10.h), - // 文字提示 - 根据状态显示不同文本 - Obx(() => Text( - _getStatusText(), - style: TextStyle( - fontSize: 16.sp, - color: Colors.grey.shade700, - fontWeight: FontWeight.w500, - ), - )), - ], - ), - - // 右侧关闭按钮 - Container( - width: 60.w, - height: 60.w, - decoration: BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 10, - spreadRadius: 1, - ), - ], - ), - child: Center( - child: IconButton( - icon: Icon( - Icons.close, - color: Colors.grey.shade600, - size: 28.sp, - ), - onPressed: onClose, - padding: EdgeInsets.zero, - ), - ), - ), - ], - ), - ), - - // 底部进度条和识别文本 - Padding( - padding: EdgeInsets.only(top: 15.h), - child: Column( - children: [ - // 移除显示当前识别的文本,因为现在直接显示在聊天窗口中 - - // 进度条 - Container( - width: 250.w, - height: 4.h, - decoration: BoxDecoration( - color: Colors.purple.withOpacity(0.2), - borderRadius: BorderRadius.circular(2.r), - ), - child: Obx(() => controller.isRecording.value - ? _buildProgressIndicator() - : Container() - ), - ), - ], - ), - ), - ], - ), - ), - ), - ), - ); - } - - // 根据状态返回不同的提示文本 - String _getStatusText() { - if (controller.isConnecting.value) { - return '正在连接语音服务...'; - } else if (controller.isMuted.value) { - return '语音已静音'; - } else if (controller.isRecording.value) { - return '正在聆听...'; - } else { - return '你可以开始说话'; - } - } - - // 构建进度指示器 - Widget _buildProgressIndicator() { - return Container( - width: 250.w, - height: 4.h, - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [Colors.purple.shade300, Colors.purple.shade100], - begin: Alignment.centerLeft, - end: Alignment.centerRight, - ), - borderRadius: BorderRadius.circular(2.r), - ), - ); - } - - // 检查用户是否正在说话 - bool _isUserSpeaking() { - // 使用控制器的isUserSpeaking属性 - return controller.isUserSpeaking.value; - } - - Widget _buildAudioWaveform() { - return Obx(() { - final isSpeaking = _isUserSpeaking(); - final barCount = 15; // 增加条形数量,使波形更丰富 - - // 根据控制器状态动态生成波形高度 - return Container( - width: 180.w, // 增加宽度以容纳更多条形 - height: 40.h, // 增加高度以允许更大的波动 - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: List.generate( - barCount, - (index) { - // 创建动态高度效果,模拟声波 - double height = controller.isRecording.value - ? _getWaveHeight(index, isSpeaking, barCount) // 当正在录音时显示动态高度 - : 4.h; // 默认高度 - - // 根据位置计算不同的动画持续时间,使波形更自然 - final duration = Duration( - milliseconds: 100 + (index % 3) * 50 - ); - - // 根据是否说话和位置计算颜色 - Color barColor; - if (controller.isRecording.value) { - if (isSpeaking) { - // 创建渐变色效果,从中间向两边渐变 - final middleIndex = barCount ~/ 2; - final distance = (index - middleIndex).abs(); - final maxDistance = middleIndex; - final colorIntensity = 1.0 - (distance / maxDistance) * 0.5; - - barColor = Colors.purple.withOpacity(colorIntensity); - } else { - barColor = Colors.purple.shade200; - } - } else { - barColor = Colors.grey.shade400; - } - - return AnimatedContainer( - duration: duration, - curve: Curves.easeInOut, // 使用更平滑的动画曲线 - width: 3.w, // 减小宽度,使波形更精细 - height: height, - margin: EdgeInsets.symmetric(horizontal: 1.5.w), // 减小间距 - decoration: BoxDecoration( - color: barColor, - borderRadius: BorderRadius.circular(2.r), - // 添加轻微的阴影效果,增强视觉效果 - boxShadow: isSpeaking ? [ - BoxShadow( - color: Colors.purple.withOpacity(0.2), - blurRadius: 3, - spreadRadius: 0.5, - ) - ] : null, - ), - ); - }, - ), - ), - ); - }); - } - - // 生成动态波形高度 - double _getWaveHeight(int index, bool isSpeaking, int barCount) { - // 基础高度 - double baseHeight = 6.h; - - // 获取当前时间毫秒数 - final DateTime now = DateTime.now(); - final int milliseconds = now.millisecondsSinceEpoch; - - // 计算中心位置 - final middleIndex = barCount ~/ 2; - final distanceFromMiddle = (index - middleIndex).abs(); - - // 使用多个正弦波叠加,创建更自然的波形 - // 每个条形使用不同的频率和相位,创建不规则波形 - double wave1 = sin((milliseconds / 200) + (index * 0.7)) * 3.h; - double wave2 = sin((milliseconds / 350) + (index * 0.5)) * 2.h; - double wave3 = sin((milliseconds / 500) + (index * 0.3)) * 1.5.h; - - // 基于距离中心的位置计算高度衰减 - double positionFactor = 1.0 - (distanceFromMiddle / middleIndex) * 0.5; - - if (isSpeaking) { - // 用户说话时的波形 - - // 创建一个随时间变化的振幅因子,使波形看起来更有活力 - double timeFactor = 0.7 + sin(milliseconds / 1000) * 0.3; - - // 中心位置的条形高度最高 - double centerBoost = pow(positionFactor, 1.5) * 10.h; - - // 添加随机因素,使波形更自然 - double randomFactor = (Random().nextDouble() * 2 - 1) * 2.h; - - // 根据时间添加脉动效果 - double pulseEffect = sin(milliseconds / 600) * 3.h * positionFactor; - - // 组合所有效果 - return baseHeight + (wave1 + wave2 + wave3) * timeFactor + centerBoost + randomFactor + pulseEffect; - } else { - // 用户不说话时的波形 - 更平静,但仍有微小变化 - double idleWave = (wave1 + wave2) * 0.5 * positionFactor; - return baseHeight + idleWave; - } - } -} \ No newline at end of file diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index 5371164bf..280388457 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -11,7 +11,8 @@ import '../../../data/services/volcano_tts_api_service.dart'; class TranslationController extends GetxController { // Services final AzureAsrService _asrService = Get.find(); - final VolcanoTranslationService _translationService = Get.find(); + final VolcanoTranslationService _translationService = + Get.find(); final VolcanoTtsApiService _ttsService = Get.find(); final GetStorage _storage = GetStorage(); @@ -23,12 +24,10 @@ class TranslationController extends GetxController { final isRecognizing = false.obs; final isTranslating = false.obs; final isTtsEnabled = true.obs; - final recognizedText = ''.obs; - final translatedText = ''.obs; final sourceLanguage = '中文 (Chinese)'.obs; final targetLanguage = '英语 (English)'.obs; final translationHistory = [].obs; - + // 支持的语言列表 - 改为Map结构,键为显示名称,值为语言代码 final Map supportedLanguages = { '中文 (Chinese)': 'zh', @@ -61,16 +60,16 @@ class TranslationController extends GetxController { '挪威语 (Norwegian)': 'no', '马来语 (Malay)': 'ms', }; - + // Current translation item - TranslationItem? currentItem; - + final Rx currentItem = Rx(null); + // Subscription for recognition stream StreamSubscription? _recognitionSubscription; - + // Scroll controller for ListView final ScrollController scrollController = ScrollController(); - + // Timer for debouncing translation requests Timer? _translationDebounceTimer; @@ -80,13 +79,13 @@ class TranslationController extends GetxController { @override void onInit() { super.onInit(); - + // 加载历史记录 loadTranslationHistory(); - + // Initialize the translation service _initTranslationService(); - + // 添加后处理,确保在历史记录加载后滚动到底部 WidgetsBinding.instance.addPostFrameCallback((_) { // 延迟执行滚动操作,确保视图已完全渲染 @@ -94,7 +93,7 @@ class TranslationController extends GetxController { _scrollToBottom(); }); }); - + // Start voice recognition automatically when the page is loaded startRecognition(); } @@ -107,7 +106,7 @@ class TranslationController extends GetxController { scrollController.dispose(); super.onClose(); } - + // Initialize the translation service Future _initTranslationService() async { try { @@ -129,13 +128,13 @@ class TranslationController extends GetxController { // Start voice recognition Future startRecognition() async { if (isRecognizing.value) return; - + try { // 生成新的会话ID currentSessionId = DateTime.now().millisecondsSinceEpoch.toString(); - + // Initialize a new translation item - currentItem = TranslationItem( + currentItem.value = TranslationItem( sourceText: '', translatedText: '', sourceLanguage: sourceLanguage.value, @@ -144,19 +143,22 @@ class TranslationController extends GetxController { sessionId: currentSessionId!, isFirstInSession: true, // 标记为会话中的第一条 ); - + // Start the voice recognition service final success = await _asrService.startContinuousRecognition(); - + if (success) { isRecognizing.value = true; - - // Reset the recognized text - recognizedText.value = ''; - translatedText.value = ''; - + + // Reset the current item + if (currentItem.value != null) { + currentItem.value!.sourceText = ''; + currentItem.value!.translatedText = ''; + } + // Listen for recognition events - _recognitionSubscription = _asrService.recognitionStream?.listen((event) { + _recognitionSubscription = + _asrService.recognitionStream?.listen((event) { switch (event.type) { case RecognitionEventType.finalResult: if (event.text.isNotEmpty) { @@ -196,258 +198,247 @@ class TranslationController extends GetxController { // Stop voice recognition Future stopRecognition() async { if (!isRecognizing.value) return; - + try { await _asrService.stopContinuousRecognition(); _recognitionSubscription?.cancel(); _recognitionSubscription = null; isRecognizing.value = false; - - // 添加当前项到历史(如果有内容) - finalizeCurrentItem(); - + // 重置当前会话ID currentSessionId = null; - - // 重置当前显示的文本 - recognizedText.value = ''; - translatedText.value = ''; + + // 重置当前项 + currentItem.value = null; } catch (e) { - Get.snackbar( - 'Error', - 'Failed to stop voice recognition: ${e.toString()}', - snackPosition: SnackPosition.BOTTOM, - ); + Get.snackbar('Error', '停止语音识别失败: ${e.toString()}', + snackPosition: SnackPosition.BOTTOM); } } - + // Handle intermediate recognition result void handleIntermediateResult(String text) { if (text.isEmpty) return; - + // 如果当前没有活跃的翻译项,创建一个新的 - if (currentItem == null) { - // 如果没有当前会话ID,创建一个新的 - if (currentSessionId == null) { - currentSessionId = DateTime.now().millisecondsSinceEpoch.toString(); - } - - currentItem = TranslationItem( + if (currentItem.value == null) { + currentSessionId ??= DateTime.now().millisecondsSinceEpoch.toString(); + + currentItem.value = TranslationItem( sourceText: text, translatedText: '', sourceLanguage: sourceLanguage.value, targetLanguage: targetLanguage.value, timestamp: DateTime.now(), sessionId: currentSessionId!, - isFirstInSession: translationHistory.isEmpty || + isFirstInSession: translationHistory.isEmpty || translationHistory.last.sessionId != currentSessionId, ); + currentItem.refresh(); + } else { // 更新当前翻译项 - currentItem!.sourceText = text; + currentItem.value!.sourceText = text; + // 通知 GetX 更新 + currentItem.refresh(); } - - // 实时更新识别文本 - recognizedText.value = text; - - // Debounce translation requests to avoid too many API calls + + // 防抖处理翻译请求 _translationDebounceTimer?.cancel(); _translationDebounceTimer = Timer(const Duration(milliseconds: 300), () { - translateText(text, isFinal: false); + if (currentItem.value != null && + currentItem.value!.sourceText.isNotEmpty) { + translateText(currentItem.value!.sourceText); + } }); - - // Scroll to the bottom to show the latest content + + // 滚动到底部显示最新内容 _scrollToBottom(); } - + // Handle final recognition result - void handleFinalResult(String text) { - if (text.isEmpty) return; - - // 更新当前翻译项(如果不存在则创建一个) - if (currentItem == null) { - // 如果没有当前会话ID,创建一个新的 - if (currentSessionId == null) { - currentSessionId = DateTime.now().millisecondsSinceEpoch.toString(); + void handleFinalResult(String text) async { + // 如果没有文本或当前项为空,直接返回 + if (text.isEmpty || currentItem.value == null) return; + + // 更新当前项的源文本 + currentItem.value!.sourceText = text; + currentItem.refresh(); + + // 添加到历史记录 + translationHistory.add(currentItem.value!); + saveTranslationHistory(); + + // 重置当前项 + currentItem.value = null; + + // 对最终结果进行一次翻译,确保翻译结果是最新的 + final translationResult = await translateText(text); + if (translationResult != null) { + if (isTtsEnabled.value) { + await playTranslatedText(translationResult); } - - currentItem = TranslationItem( - sourceText: text, - translatedText: '', - sourceLanguage: sourceLanguage.value, - targetLanguage: targetLanguage.value, - timestamp: DateTime.now(), - sessionId: currentSessionId!, - isFirstInSession: translationHistory.isEmpty || - translationHistory.last.sessionId != currentSessionId, - ); - } else { - currentItem!.sourceText = text; } - - // 更新识别文本 - recognizedText.value = text; - - // 翻译最终结果 (添加到历史的逻辑已移至translateText方法) - translateText(text, isFinal: true).then((_) { - // 作为安全措施,确保当前项被添加到历史中 - // 这是为了处理translateText中可能因为条件不满足而没有添加到历史的情况 - finalizeCurrentItem(); - }); - - // Scroll to the bottom to show the latest content + // 滚动到底部显示最新内容 _scrollToBottom(); } - - // Finalize the current translation item and add it to history - void finalizeCurrentItem() { - if (currentItem != null && - currentItem!.sourceText.isNotEmpty && - currentItem!.translatedText.isNotEmpty && - !translationHistory.contains(currentItem)) { - translationHistory.add(currentItem!); - saveTranslationHistory(); - currentItem = null; + + // 检测文本语言 + String detectLanguage(String text) { + if (text.isEmpty) return sourceLanguage.value; + + // 检测各种语言特征 + final bool containsChinese = RegExp(r'[\u4e00-\u9fa5]').hasMatch(text); + final bool containsLatin = RegExp(r'[a-zA-Z]').hasMatch(text); + final bool containsJapanese = + RegExp(r'[\u3040-\u309F\u30A0-\u30FF]').hasMatch(text); + final bool containsKorean = + RegExp(r'[\uAC00-\uD7AF\u1100-\u11FF]').hasMatch(text); + final bool containsCyrillic = RegExp(r'[\u0400-\u04FF]').hasMatch(text); + final bool containsArabic = RegExp(r'[\u0600-\u06FF]').hasMatch(text); + final bool containsThai = RegExp(r'[\u0E00-\u0E7F]').hasMatch(text); + + // 获取当前设置的源语言代码 + final String sourceCode = supportedLanguages[sourceLanguage.value] ?? 'zh'; + + // 根据文本特征判断语言 + if (containsChinese && sourceCode != 'zh') { + return '中文 (Chinese)'; + } else if (containsJapanese && sourceCode != 'ja') { + return '日语 (Japanese)'; + } else if (containsKorean && sourceCode != 'ko') { + return '韩语 (Korean)'; + } else if (containsCyrillic && sourceCode != 'ru') { + return '俄语 (Russian)'; + } else if (containsArabic && sourceCode != 'ar') { + return '阿拉伯语 (Arabic)'; + } else if (containsThai && sourceCode != 'th') { + return '泰语 (Thai)'; + } else if (containsLatin && sourceCode == 'zh') { + return '英语 (English)'; } - } - // Handle voice recognition result (legacy method, kept for compatibility) - void handleVoiceResult(String text) { - handleFinalResult(text); + // 默认返回当前设置的源语言 + return sourceLanguage.value; } // Translate the recognized text - Future translateText(String text, {bool isFinal = true}) async { - if (text.isEmpty) return; - + Future translateText(String sourceText) async { + if (sourceText.isEmpty) return null; + isTranslating.value = true; - - // 保存当前的翻译项引用,确保异步返回时更新正确的项 - final TranslationItem? itemToUpdate = currentItem; - + String? translationResult; + try { - // 获取源语言和目标语言的代码 - final String sourceCode = supportedLanguages[sourceLanguage.value] ?? 'zh'; - - // 尝试检测输入文本的语言(这里简化处理,实际项目可能需要更复杂的语言检测) - bool shouldSwapLanguages = false; - - // 简单的语言检测逻辑 - 检查是否包含中文字符 - final bool containsChinese = RegExp(r'[\u4e00-\u9fa5]').hasMatch(text); - final bool containsLatin = RegExp(r'[a-zA-Z]').hasMatch(text); - - // 如果源语言是中文但文本不包含中文字符,且包含拉丁字符,可能需要交换语言 - if (sourceCode == 'zh' && !containsChinese && containsLatin) { - shouldSwapLanguages = true; - } - // 如果源语言不是中文但文本包含中文字符,可能需要交换语言 - else if (sourceCode != 'zh' && containsChinese) { - shouldSwapLanguages = true; - } - + // 检测源文本语言 + final detectedLanguage = detectLanguage(sourceText); + final bool shouldSwapLanguages = detectedLanguage != sourceLanguage.value; + // 调用翻译服务 final result = await _translationService.translateText( - text: text, - sourceLanguage: shouldSwapLanguages ? targetLanguage.value : sourceLanguage.value, - targetLanguage: shouldSwapLanguages ? sourceLanguage.value : targetLanguage.value, + text: sourceText, + sourceLanguage: + shouldSwapLanguages ? detectedLanguage : sourceLanguage.value, + targetLanguage: + shouldSwapLanguages ? sourceLanguage.value : targetLanguage.value, ); - + if (result != null) { - // 实时更新翻译文本 - translatedText.value = result; - - // 确保更新的是发起翻译请求时的翻译项,而不是当前可能已经变化的翻译项 - if (itemToUpdate != null && itemToUpdate.sourceText == text) { - itemToUpdate.translatedText = result; - - // 如果是最终结果,并且当前项仍然是发起请求时的项,则添加到历史 - if (isFinal && itemToUpdate == currentItem) { - // 检查是否已经存在相同会话ID的项 - bool isFirstInSession = true; - for (var item in translationHistory) { - if (item.sessionId == itemToUpdate.sessionId) { - isFirstInSession = false; - break; - } - } - - // 创建一个新的TranslationItem,包含正确的isFirstInSession值 - final updatedItem = TranslationItem( - sourceText: itemToUpdate.sourceText, - translatedText: itemToUpdate.translatedText, - sourceLanguage: itemToUpdate.sourceLanguage, - targetLanguage: itemToUpdate.targetLanguage, - timestamp: itemToUpdate.timestamp, - sessionId: itemToUpdate.sessionId, - isFirstInSession: isFirstInSession, - ); - - translationHistory.add(updatedItem); + translationResult = result; + + // 更新翻译结果 + if (currentItem.value != null && + currentItem.value!.sourceText.contains(sourceText)) { + // 直接更新并刷新 + currentItem.value!.translatedText = result; + currentItem.refresh(); + } else { + // 在历史记录中查找匹配的项 + final index = translationHistory.indexWhere( + (historyItem) => historyItem.sourceText == sourceText); + + if (index != -1) { + // 直接更新历史记录项 + translationHistory[index].translatedText = result; + // 通知 GetX 更新 + translationHistory.refresh(); + saveTranslationHistory(); - currentItem = null; // 重要:将当前项设为null,表示已完成 - - // 重置当前显示的文本,避免与历史项重复 - recognizedText.value = ''; - translatedText.value = ''; - } - - // Play TTS if enabled and this is a final result - if (isTtsEnabled.value && isFinal) { - playTranslatedText(result); } + // 滚动到底部显示最新内容 + _scrollToBottom(); } } + return translationResult; } catch (e) { - Get.snackbar( - 'Error', - 'Translation failed: ${e.toString()}', - snackPosition: SnackPosition.BOTTOM, - ); + Get.snackbar('Error', '翻译失败: ${e.toString()}', + snackPosition: SnackPosition.BOTTOM); + return null; } finally { isTranslating.value = false; } } - // Play the translated text using TTS + // 播放翻译文本 Future playTranslatedText(String text) async { if (text.isEmpty) return; - + try { await _ttsService.speakSingle(text); } catch (e) { - Get.snackbar( - 'Error', - 'Failed to play translation: ${e.toString()}', - snackPosition: SnackPosition.BOTTOM, - ); + Get.snackbar('Error', '播放翻译失败: ${e.toString()}', + snackPosition: SnackPosition.BOTTOM); + } + } + + // 停止所有操作(语音识别和TTS) + Future stopAll() async { + try { + // 停止语音识别 + if (isRecognizing.value) { + await stopRecognition(); + } + + // 停止TTS播放 + await _ttsService.stop(); + } catch (e) { + Get.snackbar('Error', '停止操作失败: ${e.toString()}', + snackPosition: SnackPosition.BOTTOM); } } - // Play a specific translation item + // 播放特定翻译项 Future playTranslationItem(TranslationItem item) async { + if (item.translatedText.isEmpty) return; await playTranslatedText(item.translatedText); } - - // Scroll to the bottom of the list + + // 播放文本 + Future playText(String text) async { + if (text.isEmpty) return; + await playTranslatedText(text); + } + + // 滚动到列表底部 void _scrollToBottom() { - if (scrollController.hasClients) { - try { - scrollController.animateTo( - scrollController.position.maxScrollExtent, - duration: const Duration(milliseconds: 300), - curve: Curves.easeOut, - ); - } catch (e) { - // 如果滚动失败,尝试延迟后再次滚动 - Future.delayed(const Duration(milliseconds: 200), () { - if (scrollController.hasClients) { - scrollController.jumpTo(scrollController.position.maxScrollExtent); - } - }); - } - } else { - // 如果scrollController还没有客户端,延迟后再尝试 - Future.delayed(const Duration(milliseconds: 200), _scrollToBottom); + if (!scrollController.hasClients) { + Future.delayed(const Duration(milliseconds: 100), _scrollToBottom); + return; + } + + try { + scrollController.animateTo( + scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } catch (e) { + // 如果滚动失败,尝试延迟后再次滚动 + Future.delayed(const Duration(milliseconds: 100), () { + if (scrollController.hasClients) { + scrollController.jumpTo(scrollController.position.maxScrollExtent); + } + }); } } @@ -456,19 +447,19 @@ class TranslationController extends GetxController { final temp = sourceLanguage.value; sourceLanguage.value = targetLanguage.value; targetLanguage.value = temp; - + // 如果正在识别,需要停止并重新开始 if (isRecognizing.value) { stopRecognition(); startRecognition(); } } - + // 设置源语言 void setSourceLanguage(String language) { if (sourceLanguage.value != language) { sourceLanguage.value = language; - + // 如果正在识别,需要停止并重新开始 if (isRecognizing.value) { stopRecognition(); @@ -476,7 +467,7 @@ class TranslationController extends GetxController { } } } - + // 设置目标语言 void setTargetLanguage(String language) { if (targetLanguage.value != language) { @@ -488,13 +479,14 @@ class TranslationController extends GetxController { void loadTranslationHistory() { final historyJson = _storage.read(_historyKey); if (historyJson != null) { - final List historyList = jsonDecode(historyJson); - translationHistory.value = historyList.map((item) => TranslationItem.fromJson(item)).toList(); - - // 确保在历史记录加载后滚动到底部 - WidgetsBinding.instance.addPostFrameCallback((_) { - _scrollToBottom(); - }); + try { + final List historyList = jsonDecode(historyJson); + translationHistory.value = + historyList.map((item) => TranslationItem.fromJson(item)).toList(); + } catch (e) { + // 如果解析失败,清除历史记录 + _storage.remove(_historyKey); + } } } @@ -502,10 +494,11 @@ class TranslationController extends GetxController { void saveTranslationHistory() { // 限制历史记录数量,保留最新的记录 if (translationHistory.length > _maxHistoryItems) { - translationHistory.removeRange(0, translationHistory.length - _maxHistoryItems); + translationHistory.removeRange( + 0, translationHistory.length - _maxHistoryItems); } - - final List> historyJson = + + final List> historyJson = translationHistory.map((item) => item.toJson()).toList(); _storage.write(_historyKey, jsonEncode(historyJson)); } @@ -524,7 +517,7 @@ class TranslationItem { final String sourceLanguage; final String targetLanguage; final DateTime timestamp; - final String sessionId; + String sessionId; final bool isFirstInSession; TranslationItem({ @@ -538,13 +531,21 @@ class TranslationItem { }); factory TranslationItem.fromJson(Map json) { + DateTime timestamp; + try { + timestamp = DateTime.parse(json['timestamp']); + } catch (e) { + timestamp = DateTime.now(); + } + return TranslationItem( - sourceText: json['sourceText'], - translatedText: json['translatedText'], - sourceLanguage: json['sourceLanguage'], - targetLanguage: json['targetLanguage'], - timestamp: DateTime.parse(json['timestamp']), - sessionId: json['sessionId'] ?? DateTime.now().millisecondsSinceEpoch.toString(), + sourceText: json['sourceText'] ?? '', + translatedText: json['translatedText'] ?? '', + sourceLanguage: json['sourceLanguage'] ?? '中文 (Chinese)', + targetLanguage: json['targetLanguage'] ?? '英语 (English)', + timestamp: timestamp, + sessionId: + json['sessionId'] ?? DateTime.now().millisecondsSinceEpoch.toString(), isFirstInSession: json['isFirstInSession'] ?? false, ); } @@ -560,4 +561,4 @@ class TranslationItem { 'isFirstInSession': isFirstInSession, }; } -} \ No newline at end of file +} diff --git a/lib/modules/translation/controllers/translation_detail_controller.dart b/lib/modules/translation/controllers/translation_detail_controller.dart index 961cb8f6b..53efac1b0 100644 --- a/lib/modules/translation/controllers/translation_detail_controller.dart +++ b/lib/modules/translation/controllers/translation_detail_controller.dart @@ -3,6 +3,7 @@ import '../../../data/services/volcano_ai_service.dart'; import '../controllers/translation_controller.dart'; import '../controllers/translation_history_controller.dart'; import 'package:get_storage/get_storage.dart'; +import 'dart:async'; class TranslationDetailController extends GetxController { // 依赖注入 @@ -13,6 +14,9 @@ class TranslationDetailController extends GetxController { // 存储键前缀 static const String _summaryKeyPrefix = 'translation_summary_'; + // 超时设置(秒) + final int _timeoutDuration = 30; + // 状态变量 final selectedTabIndex = 0.obs; final isGeneratingSummary = false.obs; @@ -76,6 +80,64 @@ class TranslationDetailController extends GetxController { return sessionItems.map((e) => e.translatedText).join('\n\n'); } + // 使用延长的超时时间重试提取摘要 + Future retryExtractSummaryWithExtendedTimeout(String text) async { + // 延长超时时间(60秒) + final int extendedTimeout = 60; + + isGeneratingSummary.value = true; + + try { + // 构建提示词 + final messages = [ + {'role': 'user', 'content': '请为以下文本生成一个简洁的摘要,突出重点内容:\n\n$text'}, + ]; + + // 系统提示词 + const systemPrompt = ''' +你是一个专业的文本摘要助手,擅长从会议记录、对话和文本中提取关键信息并生成简洁的摘要。 +请按照以下格式生成摘要: + +1. 首先用1-2句话对整体内容进行简要概括 +2. 然后使用项目符号列表(•)列出关键点或重要信息 +3. 如果是对话内容,请特别标注出讨论的结论或达成的共识 + +请遵循以下原则: +1. 保留原文的核心信息和关键点 +2. 使用简洁、清晰的语言 +3. 保持客观,不添加个人观点 +4. 摘要总长度应该是原文的1/3或更短 +5. 确保列表中的每个要点都简明扼要,突出重点 +'''; + + String result; + try { + // 使用延长的超时限制 + result = await _aiService.sendMessage( + messages: messages, + systemPrompt: systemPrompt, + ).timeout(Duration(seconds: extendedTimeout), onTimeout: () { + throw TimeoutException('请求超时,请稍后重试'); + }); + } on TimeoutException catch (_) { + summaryResult.value = '生成摘要超时,请检查网络连接后重试'; + return summaryResult.value; + } + + summaryResult.value = result; + + // 保存摘要 + saveSummary(result); + + return result; + } catch (e) { + summaryResult.value = '生成摘要失败: ${e.toString()}'; + return summaryResult.value; + } finally { + isGeneratingSummary.value = false; + } + } + // 提取摘要 Future extractSummary(String text) async { isGeneratingSummary.value = true; @@ -103,11 +165,20 @@ class TranslationDetailController extends GetxController { 5. 确保列表中的每个要点都简明扼要,突出重点 '''; - // 调用AI服务生成摘要 - final result = await _aiService.sendMessage( - messages: messages, - systemPrompt: systemPrompt, - ); + // 添加超时处理 + String result; + try { + // 使用Future.timeout添加超时限制 + result = await _aiService.sendMessage( + messages: messages, + systemPrompt: systemPrompt, + ).timeout(Duration(seconds: _timeoutDuration), onTimeout: () { + throw TimeoutException('请求超时,请稍后重试'); + }); + } on TimeoutException catch (_) { + summaryResult.value = '生成摘要超时,请稍后重试'; + return summaryResult.value; + } summaryResult.value = result; diff --git a/lib/modules/translation/views/translation_detail_view.dart b/lib/modules/translation/views/translation_detail_view.dart index 92151543d..0d7e2f67c 100644 --- a/lib/modules/translation/views/translation_detail_view.dart +++ b/lib/modules/translation/views/translation_detail_view.dart @@ -211,21 +211,26 @@ class TranslationDetailView extends GetView { ), SizedBox(width: 12.w), // 播放按钮 - GestureDetector( - onTap: () { - controller.playTranslationItem(item); - }, - child: Container( - width: 28.w, - height: 28.h, - decoration: BoxDecoration( - color: Colors.blue.withAlpha(10), - shape: BoxShape.circle, - ), - child: Icon( - Icons.volume_up, - color: Colors.blue, - size: 16.sp, + Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + controller.playTranslationItem(item); + }, + borderRadius: BorderRadius.circular(20.r), + splashColor: Colors.blue.withOpacity(0.2), + child: Container( + width: 28.w, + height: 28.h, + decoration: BoxDecoration( + color: Colors.blue.withAlpha(10), + shape: BoxShape.circle, + ), + child: Icon( + Icons.volume_up, + color: Colors.blue, + size: 16.sp, + ), ), ), ), @@ -316,46 +321,48 @@ class TranslationDetailView extends GetView { mainAxisAlignment: MainAxisAlignment.end, children: [ // 播放按钮 - GestureDetector( - onTap: () { - controller.playText(controller.summaryResult.value); - }, - child: Container( - padding: EdgeInsets.all(8.r), - decoration: BoxDecoration( - color: Colors.blue.withAlpha(10), - shape: BoxShape.circle, - ), - child: Icon( - Icons.volume_up, - color: Colors.blue, - size: 18.sp, + Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + controller.playText(controller.summaryResult.value); + }, + borderRadius: BorderRadius.circular(20.r), + splashColor: Colors.blue.withOpacity(0.2), + child: Padding( + padding: EdgeInsets.all(8.r), + child: Icon( + Icons.volume_up, + color: Colors.blue, + size: 18.sp, + ), ), ), ), SizedBox(width: 12.w), // 复制按钮 - GestureDetector( - onTap: () { - // 复制摘要到剪贴板 - Clipboard.setData(ClipboardData(text: controller.summaryResult.value)); - Get.snackbar( - '已复制', - '摘要已复制到剪贴板', - snackPosition: SnackPosition.BOTTOM, - duration: Duration(seconds: 2), - ); - }, - child: Container( - padding: EdgeInsets.all(8.r), - decoration: BoxDecoration( - color: Colors.blue.withAlpha(10), - shape: BoxShape.circle, - ), - child: Icon( - Icons.copy, - color: Colors.blue, - size: 18.sp, + Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + // 复制摘要到剪贴板 + Clipboard.setData(ClipboardData(text: controller.summaryResult.value)); + Get.snackbar( + '已复制', + '摘要已复制到剪贴板', + snackPosition: SnackPosition.BOTTOM, + duration: Duration(seconds: 2), + ); + }, + borderRadius: BorderRadius.circular(20.r), + splashColor: Colors.blue.withOpacity(0.2), + child: Padding( + padding: EdgeInsets.all(8.r), + child: Icon( + Icons.copy, + color: Colors.blue, + size: 18.sp, + ), ), ), ), @@ -455,7 +462,58 @@ class TranslationDetailView extends GetView { // 提取摘要方法 void _extractSummary(String text) async { - await controller.extractSummary(text); + final result = await controller.extractSummary(text); + + // 检查是否超时,如果超时则显示重试选项 + if (result.contains('超时')) { + Get.dialog( + AlertDialog( + title: Text( + '请求超时', + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.bold, + ), + ), + content: Text( + '生成摘要请求超时,可能是网络问题或服务器繁忙。', + style: TextStyle( + fontSize: 16.sp, + ), + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: Text( + '取消', + style: TextStyle( + fontSize: 16.sp, + color: Colors.grey[700], + ), + ), + ), + TextButton( + onPressed: () { + Get.back(); + // 使用延长的超时时间重试 + controller.retryExtractSummaryWithExtendedTimeout(text); + }, + child: Text( + '重试', + style: TextStyle( + fontSize: 16.sp, + color: Colors.blue, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.r), + ), + ), + ); + } } // 分享内容方法 diff --git a/lib/modules/translation/views/translation_history_view.dart b/lib/modules/translation/views/translation_history_view.dart index 0a0d6c227..110d26b97 100644 --- a/lib/modules/translation/views/translation_history_view.dart +++ b/lib/modules/translation/views/translation_history_view.dart @@ -126,54 +126,55 @@ class TranslationHistoryView extends GetView { // 构建组内的历史记录项 Widget _buildHistoryItemInGroup(TranslationItem item) { - return GestureDetector( + return InkWell( onTap: () { // 点击查看详情 _showTranslationDetail(item); }, - child: Stack( - children: [ - Padding( - padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 源文本 - Text( - item.sourceText, - style: TextStyle( - fontSize: 15.sp, - fontWeight: FontWeight.w500, - color: Colors.black, + splashColor: Colors.blue.withOpacity(0.1), + highlightColor: Colors.blue.withOpacity(0.05), + borderRadius: BorderRadius.circular(8.r), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 源文本 + Text( + item.sourceText, + style: TextStyle( + fontSize: 15.sp, + fontWeight: FontWeight.w500, + color: Colors.black, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - SizedBox(height: 6.h), - // 翻译文本 - Text( - item.translatedText, - style: TextStyle( - fontSize: 14.sp, - color: Colors.grey[700], + SizedBox(height: 6.h), + // 翻译文本 + Text( + item.translatedText, + style: TextStyle( + fontSize: 14.sp, + color: Colors.grey[700], + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ], + ], + ), ), - ), - Positioned( - right: 12.w, - top: 0, - bottom: 0, - child: Icon( + Icon( Icons.chevron_right, color: Colors.grey[350], size: 20.sp, ), - ), - ], + ], + ), ), ); } diff --git a/lib/modules/translation/views/translation_view.dart b/lib/modules/translation/views/translation_view.dart index 4f84f039c..07c30aee3 100644 --- a/lib/modules/translation/views/translation_view.dart +++ b/lib/modules/translation/views/translation_view.dart @@ -28,8 +28,8 @@ class TranslationView extends GetView { size: 20.sp, ), onPressed: () { - // Stop recognition before navigating back - controller.stopRecognition(); + // Stop recognition and TTS before navigating back + controller.stopAll(); Get.back(); }, ), @@ -116,14 +116,12 @@ class TranslationView extends GetView { size: 24.sp, ), onPressed: () async { - // 停止语音识别,释放资源 - controller.stopRecognition(); + // 停止语音识别和TTS,释放资源 + await controller.stopAll(); await Get.to( () => const TranslationHistoryView(), binding: TranslationHistoryBinding(), ); - // 返回时重新启动语音识别 - controller.startRecognition(); }, tooltip: '查看历史记录', ), @@ -146,21 +144,20 @@ class TranslationView extends GetView { return Obx(() { final itemCount = controller.translationHistory.length; - // 添加对recognizedText和translatedText的监听 - final hasRecognizedText = controller.recognizedText.value.isNotEmpty; - final hasTranslatedText = controller.translatedText.value.isNotEmpty; - final showCurrentText = hasRecognizedText || hasTranslatedText; + // 使用 currentItem 来判断是否显示当前项 + final hasCurrentItem = controller.currentItem.value != null && + (controller.currentItem.value!.sourceText.isNotEmpty || controller.currentItem.value!.translatedText.isNotEmpty); return ListView.builder( controller: controller.scrollController, padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), - itemCount: itemCount + (showCurrentText ? 1 : 0), + itemCount: itemCount + (hasCurrentItem ? 1 : 0), itemBuilder: (context, index) { // Current item (being translated) - if (showCurrentText && index == itemCount) { + if (hasCurrentItem && index == itemCount) { return _buildTranslationItemWidget( - controller.recognizedText.value, - controller.translatedText.value, + controller.currentItem.value!.sourceText, + controller.currentItem.value!.translatedText, true, ); } @@ -256,32 +253,38 @@ class TranslationView extends GetView { ), ], ), - child: Center( - child: Obx(() { - final isRecognizing = controller.isRecognizing.value; - return GestureDetector( - onTap: () { - if (isRecognizing) { - controller.stopRecognition(); - } else { - controller.startRecognition(); - } - }, - child: Container( - width: 60.w, - height: 60.w, - decoration: BoxDecoration( - color: isRecognizing ? Colors.red : Colors.blue, - shape: BoxShape.circle, - ), - child: Icon( - isRecognizing ? Icons.pause : Icons.play_arrow, - color: Colors.white, - size: 30.sp, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + + + // 主要控制按钮 - 开始/暂停语音识别 + Obx(() { + final isRecognizing = controller.isRecognizing.value; + return GestureDetector( + onTap: () { + if (isRecognizing) { + controller.stopAll(); + } else { + controller.startRecognition(); + } + }, + child: Container( + width: 60.w, + height: 60.w, + decoration: BoxDecoration( + color: isRecognizing ? Colors.red : Colors.blue, + shape: BoxShape.circle, + ), + child: Icon( + isRecognizing ? Icons.pause : Icons.play_arrow, + color: Colors.white, + size: 30.sp, + ), ), - ), - ); - }), + ); + }), + ], ), ); } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index ded0e30eb..fab21e7ca 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,11 +6,15 @@ #include "generated_plugin_registrant.h" +#include #include #include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); g_autoptr(FlPluginRegistrar) record_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin"); record_linux_plugin_register_with_registrar(record_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 500c17552..6915aceb9 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_selector_linux record_linux url_launcher_linux window_size diff --git a/pubspec.yaml b/pubspec.yaml index 716905c6f..372521069 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,6 +36,7 @@ dependencies: uuid: ^4.3.3 record: ^5.2.1 share_plus: ^10.0.0 + image_picker: ^1.1.2 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 1967da96a..e9a97bd05 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,7 @@ #include "generated_plugin_registrant.h" +#include #include #include #include @@ -13,6 +14,8 @@ #include void RegisterPlugins(flutter::PluginRegistry* registry) { + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); PermissionHandlerWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); RecordWindowsPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 795d3bc0a..ce9dfe67d 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_selector_windows permission_handler_windows record_windows share_plus