From 560b8dc3f0db65442ca16535094d9a398128010a Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Tue, 24 Jun 2025 23:53:18 +0800 Subject: [PATCH] =?UTF-8?q?refactor(chat):=20=E7=A7=BB=E9=99=A4=E5=BA=9F?= =?UTF-8?q?=E5=BC=83=E7=9A=84chat=E6=A8=A1=E5=9D=97=E5=8F=8A=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E7=BB=84=E4=BB=B6=E5=92=8C=E6=8E=A7=E5=88=B6=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/modules/chat/bindings/chat_binding.dart | 10 - .../chat/controllers/chat_controller.dart | 992 ------------------ lib/modules/chat/models/message_model.dart | 64 -- .../views/animated_gradient_background.dart | 69 -- lib/modules/chat/views/chat_music_card.dart | 100 -- .../chat/views/chat_navigation_card.dart | 208 ---- lib/modules/chat/views/chat_view.dart | 820 --------------- lib/modules/chat/views/cloud_background.dart | 144 --- lib/modules/chat/views/weather_day_card.dart | 92 -- 9 files changed, 2499 deletions(-) delete mode 100644 lib/modules/chat/bindings/chat_binding.dart delete mode 100644 lib/modules/chat/controllers/chat_controller.dart delete mode 100644 lib/modules/chat/models/message_model.dart delete mode 100644 lib/modules/chat/views/animated_gradient_background.dart delete mode 100644 lib/modules/chat/views/chat_music_card.dart delete mode 100644 lib/modules/chat/views/chat_navigation_card.dart delete mode 100644 lib/modules/chat/views/chat_view.dart delete mode 100644 lib/modules/chat/views/cloud_background.dart delete mode 100644 lib/modules/chat/views/weather_day_card.dart diff --git a/lib/modules/chat/bindings/chat_binding.dart b/lib/modules/chat/bindings/chat_binding.dart deleted file mode 100644 index 6b6ae6683..000000000 --- a/lib/modules/chat/bindings/chat_binding.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:get/get.dart'; -import '../controllers/chat_controller.dart'; - -class ChatBinding extends Bindings { - @override - void dependencies() { - // 注册控制器 - Get.lazyPut(() => ChatController()); - } -} diff --git a/lib/modules/chat/controllers/chat_controller.dart b/lib/modules/chat/controllers/chat_controller.dart deleted file mode 100644 index 64668f38a..000000000 --- a/lib/modules/chat/controllers/chat_controller.dart +++ /dev/null @@ -1,992 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import '../../../data/services/spotify_service.dart'; -import 'package:get/get.dart'; -import 'package:flutter/widgets.dart'; -import 'package:get_storage/get_storage.dart'; -import 'package:haptic_feedback/haptic_feedback.dart'; -import 'package:just_audio/just_audio.dart'; -import 'package:chat_storage/chat_storage.dart'; -import '../../../data/services/ai_service.dart'; -import '../../../data/services/deapsound_ai_service.dart'; -import '../../../data/services/kouzi_ai_service.dart'; -import '../../../data/services/speech_impl/voice_clone_tts_service.dart'; -import '../models/message_model.dart'; -import '../../../core/utils/logger.dart'; -import '../../../data/services/tts_service.dart'; -import '../../../data/providers/agent_provider.dart'; -import '../../../data/models/agent.dart'; -import '../../../data/services/asr_service.dart'; -import '../../../data/services/open_ai_service_adapter.dart'; - -class ChatController extends GetxController { - // 服务 - late AiService _aiService; - late TtsService _ttsService; - final AsrService _asrService = Get.find(); - - late SpotifService _spotifService; - - // 添加ScrollController用于控制列表滚动 - final ScrollController scrollController = ScrollController(); - - StreamSubscription? _locationStream; - String _latLng = ''; - String _city = '深圳市'; - - // 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 RxInt _currentPage = 1.obs; - final RxInt _totalPages = 1.obs; - final RxBool _isLoadingMore = false.obs; - final RxBool _hasMoreHistory = true.obs; - static const int _pageSize = 20; // 每页加载的消息数量 - - // 语音交互模式 - bool _isVoiceInteractionMode = false; - - // 语音识别状态 - final RxString recognizedText = ''.obs; - final RxBool isListening = false.obs; - final RxBool isSpeechDetected = false.obs; - - // 防抖计时器 - Timer? _scrollDebounceTimer; - - // 流式响应控制 - StreamSubscription? _aiResponseSubscription; - StreamSubscription? _recognitionSubscription; - StreamSubscription? _voiceInteractionSubscription; - - // 取消标志 - bool _shouldCancelAiResponse = false; - - // 音频输入源 - bool _audioSourceType = false; - - // 自动停止语音识别计时器 - Timer? _autoStopTimer; - - // 记录最后一次活动时间 - DateTime _lastActivityTime = DateTime.now(); - - // 音乐播放 - final player = AudioPlayer(); - final RxString audiourl = ''.obs; - final RxBool isPlay = false.obs; - - // 按住说话、文字输入切换 - final RxBool isTextInput = false.obs; - final RxBool isHoldSpeak = false.obs; - String finalResult = ''; - - // 按住说话防抖 - bool isDebounce = false; - Timer? _talkDebounceTimer; - Timer? _talkStartDebounceTimer; - - @override - Future onInit() async { - super.onInit(); - Logger.info('ChatController onInit'); - - // 从路由参数获取Agent信息 - final Map args = Get.arguments ?? {}; - final String agentId = - args['agent'] ?? args['agentId'] ?? 'personal_assistant'; - - // 获取Agent - final Agent? foundAgent = AgentProvider.getAgentById(agentId); - if (foundAgent == null) { - Get.back(); - Logger.error('无法找到Agent: $agentId'); - - return; - } - - agent = foundAgent; - - // 根据Agent ID选择不同的AI服务 - switch (agent.id) { - case 'cyber_girlfriend': - // 亲子陪伴 - Get.put(KouziAIService()); - _aiService = Get.find(); - break; - case 'health_advisor': - // 多功能AI聊天 - Get.put(DeapsoundAIService()); - _aiService = Get.find(); - break; - default: - // 默认使用OpenAIServiceAdapter - _aiService = Get.find(); - Logger.info('使用OpenAIServiceAdapter'); - } - Get.put(SpotifService()); - _spotifService = Get.find(); - // 使用克隆音色语音合成 - final GetStorage storage = GetStorage(); - String timbre = storage.read('timbre') ?? '1'; - if (timbre == '1') { - _ttsService = Get.find(); - } else { - Get.put(VoiceCloneTtsService()); - _ttsService = Get.find(); - } - - _ttsService.initialize(); - _asrService.initialize(); - - // 设置语音交互模式 - _isVoiceInteractionMode = args['isVoiceInteraction'] == true; - - // 设置TTS服务 - await _ttsService.setVoice(agent.voice); - - // 添加滚动监听器,用于触发加载更多 - scrollController.addListener(_scrollListener); - - // 加载聊天历史 (加载第一页) - await _loadChatHistory(); - - // 如果没有历史记录,添加欢迎消息 - if (messages.isEmpty && agent.welcomeMessage.isNotEmpty) { - messages.add(Message( - role: 'assistant', - content: agent.welcomeMessage, - timestamp: DateTime.now(), - )); - // 如果启用了TTS,播放欢迎消息 - // if (isTtsEnabled.value) { - // _ttsService.speak(agent.welcomeMessage); - // } - } - - // 如果isVoiceInteraction为true,自动激活语音识别 - if (args['isVoiceInteraction'] == true) { - _ttsService.speakOnce(agent.voiceWelcomeMessage ?? agent.welcomeMessage); - startVoiceRecognition(); - // 自动停止计时器会在startVoiceRecognition中启动 - } - - - player.playerStateStream.listen((state) async { - if (state.processingState == ProcessingState.completed) { - await player.seek(Duration.zero); - player.pause(); - isPlay.value = false; - } - }); - - // 使用微任务在布局完成后自动滚动到底部 - WidgetsBinding.instance.addPostFrameCallback((_) { - _scrollToBottom(); - }); - } - - - @override - void onClose() { - Logger.info('ChatController onClose'); - - // 清理资源 - scrollController.removeListener(_scrollListener); // 移除监听器 - scrollController.dispose(); - _aiResponseSubscription?.cancel(); - _recognitionSubscription?.cancel(); - _voiceInteractionSubscription?.cancel(); - _autoStopTimer?.cancel(); - _scrollDebounceTimer?.cancel(); // 清理防抖计时器 - _talkDebounceTimer?.cancel(); - _talkStartDebounceTimer?.cancel(); - - _locationStream?.cancel(); - - // 停止TTS - _ttsService.stop(); - - if (isVoiceRecognitionActive.value) { - _asrService.stopContinuousRecognition(); - } - - // 保存当前聊天记录 - if (agent.id != null) { - // _historyService.saveHistory(agent.id!, messages.toList()); - } - - player.dispose(); - - super.onClose(); - } - - /// 滚动到列表底部方法,添加防抖功能 - void _scrollToBottom() { - if (scrollController.hasClients && messages.isNotEmpty) { - try { - scrollController.animateTo( - scrollController.position.maxScrollExtent, - duration: Duration(milliseconds: 300), - curve: Curves.easeOut, - ); - } catch (e) { - Logger.error('滚动到底部失败', e); - } - } - } - - /// 中断当前响应 - void _interruptCurrentResponse(String reason) { - Logger.info('${reason}'); - - // 停止TTS - _ttsService.stop(); - - // 取消AI响应流订阅 - if (_aiResponseSubscription != null) { - _shouldCancelAiResponse = true; - _aiResponseSubscription!.cancel(); - _aiResponseSubscription = null; - - // 将AI中间消息转换为正常消息,保留已输出的内容 - final index = - messages.indexWhere((m) => m.isIntermediate && m.role == 'assistant'); - if (index != -1) { - messages[index] = messages[index].copyWith( - isIntermediate: false, - ); - messages.refresh(); - } - - // 重置发送状态 - isSending.value = false; - } - } - - // 发送消息 - Future sendMessage(String text) async { - if (text.trim().isEmpty) return; - - // 中断当前响应 - _interruptCurrentResponse("发送新消息,中断当前响应"); - - // 添加用户消息 - final userMessage = Message( - role: 'user', - content: text, - timestamp: DateTime.now(), - ); - messages.add(userMessage); - _saveMessageToDb(userMessage); - - // 清空输入框 - inputText.value = ''; - - // 添加中间状态的AI消息 - final intermediateMessage = Message.intermediate('assistant'); - messages.add(intermediateMessage); - messages.refresh(); - WidgetsBinding.instance.addPostFrameCallback((_) { - _scrollToBottom(); - }); - - isSending.value = true; - - try { - // 构建消息历史 - final List> messageHistory = _buildMessageHistory(); - - // 重置取消标志 - _shouldCancelAiResponse = false; - - String fullResponse = ''; - - // 获取AI响应流 - final responseStream = _aiService.sendMessageStream( - messages: messageHistory, - systemPrompt: agent.systemPrompt, - ); - Logger.info( - 'liwei----------- 发送用户提问 ${text}', - ); - // 订阅响应流 - _aiResponseSubscription = responseStream.listen( - (chunk) { - if (_shouldCancelAiResponse) return; - Logger.info('chat ${chunk}'); - if (chunk is Map) { - Logger.info( - 'liwei-----------收到卡片消息 ${chunk}', - ); - _updateIntermediateMessage(meta: chunk); - if (chunk['card_spotify'] != null) { - play_spotify(chunk['card_spotify']); - } - if (chunk['card_music'] != null) { - play_music(chunk['card_music']); - } - } else if (chunk is String) { - fullResponse += chunk; - - // 更新中间状态的消息内容 - _updateIntermediateMessage(content: fullResponse); - - // 如果TTS启用,使用流式处理发送到TTS - if (isTtsEnabled.value) { - String str = chunk.replaceAll('*', ''); - _ttsService.speakStream(str); - } - } - }, - onDone: () { - Logger.info( - 'liwei-----------结束当前问题 ${text}', - ); - if (_shouldCancelAiResponse) return; - - // 完成响应,将isIntermediate设为false - _finalizeResponse(fullResponse); - - // 如果TTS启用,处理缓冲区中的剩余文本 - if (isTtsEnabled.value) { - _ttsService.flushStream(); - } - }, - onError: (error) { - Logger.error('AI响应错误', error); - - // 更新中间状态的消息为错误消息 - _handleResponseError(); - }, - ); - } catch (e) { - Logger.error('发送消息失败', e); - - _handleResponseError(); - } - } - - // 构建消息历史 - List> _buildMessageHistory() { - final List> messageHistory = []; - - // 只取最近的10条消息作为上下文 - final recentMessages = messages.where((m) => !m.isIntermediate).toList(); - final startIndex = - recentMessages.length > 10 ? recentMessages.length - 10 : 0; - messageHistory.add({ - 'role': 'user', - 'content': '我当前所在城市是$_city,我的经纬度是$_latLng', - }); - - for (int i = startIndex; i < recentMessages.length; i++) { - final m = recentMessages[i]; - messageHistory.add({ - 'role': m.role, - 'content': m.content, - }); - } - - return messageHistory; - } - - // 更新中间状态的消息 - void _updateIntermediateMessage({Map? meta, String? content}) { - final index = - messages.indexWhere((m) => m.isIntermediate && m.role == 'assistant'); - if (index != -1) { - if (meta != null) { - Map messagesData = messages[index].meta ?? {}; - messages[index] = messages[index].copyWith( - meta: {...messagesData, ...meta}, - ); - } - messages[index] = messages[index].copyWith( - content: content, - ); - messages.refresh(); - _scrollToBottom(); - } - } - -// 播放音乐 - void play_music(Map card) async { - try { - isPlay.value = false; - if (audiourl.value != card['url']) { - audiourl.value = ''; - await player.setUrl(card['url']); - audiourl.value = card['url']; - player.play(); - } else { - player.play(); - } - isPlay.value = true; - } catch (e) { - Logger.error('加载异常: ${e.toString()}'); - } - } - -// 播放音乐 - void pause_music() async { - await player.pause(); - isPlay.value = false; - } - -// 播放音乐 - void play_spotify(Map card) async { - try { - await _spotifService.initialize(); - _spotifService.play(card['url'] as String); - isPlay.value = true; - audiourl.value = card['url'] as String; - } catch (e) { - Logger.error('加载异常: ${e.toString()}'); - } - } - - // 暂停播放音乐 - void pause_spotify() async { - try { - _spotifService.pause(); - isPlay.value = false; - } catch (e) { - Logger.error('加载异常: ${e.toString()}'); - } - } - - // 更新或添加最终响应 - void _finalizeResponse(String content) { - // 查找AI的中间状态消息 - final index = - messages.indexWhere((m) => m.isIntermediate && m.role == 'assistant'); - if (index != -1) { - // 更新中间状态消息为最终状态 - messages[index] = messages[index].copyWith( - content: content, - isIntermediate: false, - timestamp: DateTime.now(), // 更新时间戳为最终时间 - ); - // 保存最终的助手消息到数据库 - _saveMessageToDb(messages[index]); - } else { - // 如果没有中间状态消息,直接添加新消息 (理论上不应发生) - final assistantMsg = Message( - role: 'assistant', - content: content, - timestamp: DateTime.now(), - ); - messages.add(assistantMsg); - _saveMessageToDb(assistantMsg); // 保存到数据库 - } - - isLoading.value = false; - isSending.value = false; - _aiResponseSubscription = null; // 清理订阅 - _shouldCancelAiResponse = false; // 重置取消标志 - - // 响应完成后,重新启动语音识别的自动停止计时器 - if (_isVoiceInteractionMode) { - _startAutoStopTimer(); - } - } - - // 处理响应错误 - void _handleResponseError() { - // 查找AI的中间状态消息 - final index = - messages.indexWhere((m) => m.isIntermediate && m.role == 'assistant'); - if (index != -1) { - messages[index] = messages[index].copyWith( - content: '抱歉,我遇到了一些问题,无法回应您的请求。', - isIntermediate: false, - ); - messages.refresh(); - } - - isSending.value = false; - _aiResponseSubscription = null; - } - - // 切换TTS状态 - void toggleTts() { - isTtsEnabled.toggle(); - - // 如果关闭TTS,停止当前播放 - if (!isTtsEnabled.value) { - _ttsService.stop(); - } - } - - // 按下开始说话 - void talkLongPress() async { - _talkStartDebounceTimer?.cancel(); - _talkDebounceTimer?.cancel(); - isHoldSpeak.value = true; - Haptics.vibrate(HapticsType.success); - - _talkStartDebounceTimer = - Timer(const Duration(milliseconds: 260), () async { - if (isDebounce) return; - - // 中断当前响应 - _interruptCurrentResponse("发送新消息,中断当前响应"); - try { - final recognitionStream = - await _asrService.startContinuousRecognition(_audioSourceType); - await Future.delayed(const Duration(milliseconds: 260)); - isDebounce = true; - recognizedText.value = ''; - finalResult = ''; - - // 监听识别事件 - _recognitionSubscription = recognitionStream.listen( - _handleRecognitionEvent, - onError: _handleRecognitionError, - ); - } catch (e) { - Logger.error('启动语音识别失败', e); - isHoldSpeak.value = false; - } - }); - } - - // 松开停止说话 - void talkLongPressUp() async { - _talkStartDebounceTimer?.cancel(); - _talkDebounceTimer?.cancel(); - isHoldSpeak.value = false; - - _talkDebounceTimer = Timer(const Duration(milliseconds: 300), () async { - if (!isDebounce) return; - try { - await _asrService.stopContinuousRecognition(); - _recognitionSubscription?.cancel(); - _recognitionSubscription = null; - - String sendText = ''; - - final intermediateIndex = - messages.indexWhere((m) => m.isIntermediate && m.role == 'user'); - if (intermediateIndex >= 0) { - sendText = messages[intermediateIndex].content; - messages.removeAt(intermediateIndex); - } - - sendMessage(sendText); - - recognizedText.value = ''; - finalResult = ''; - - isDebounce = false; - } catch (e) { - Logger.error('停止语音识别失败', e); - } - }); - } - - // 开始语音识别 - Future startVoiceRecognition() async { - if (isVoiceRecognitionActive.value) return; - - try { - // 启动连续语音识别 - // 注意:在UI更新后,使用模拟波形动画替代实际麦克风输入的波形数据 - final recognitionStream = - await _asrService.startContinuousRecognition(_audioSourceType); - - isVoiceRecognitionActive.value = true; - isListening.value = true; - recognizedText.value = ''; - - // 启动自动停止计时器 - _startAutoStopTimer(); - - // 监听识别事件 - _recognitionSubscription = recognitionStream.listen( - _handleRecognitionEvent, - onError: _handleRecognitionError, - ); - } catch (e) { - Logger.error('启动语音识别失败', e); - isVoiceRecognitionActive.value = false; - isListening.value = false; - } - } - - // 处理语音识别事件 - void _handleRecognitionEvent(RecognitionEvent event) { - // 更新活动时间 - _updateActivityTime(); - - switch (event.type) { - case RecognitionEventType.intermediateResult: - _handleRecognizing(event.text); - break; - case RecognitionEventType.finalResult: - _handleFinalResult(event.text); - break; - case RecognitionEventType.error: - _handleRecognitionEventError(event.error); - break; - default: - // 不做任何处理 - break; - } - } - - // 更新最后活动时间 - void _updateActivityTime() { - _lastActivityTime = DateTime.now(); - } - - // 启动自动停止计时器 - void _startAutoStopTimer() { - // 取消现有计时器 - _autoStopTimer?.cancel(); - - // 初始化最后活动时间 - _updateActivityTime(); - - // 根据模式设置超时时间 - final int timeoutSeconds = _isVoiceInteractionMode ? 8 : 15; - - // 创建新的计时器,每秒检查一次 - _autoStopTimer = Timer.periodic(Duration(seconds: 1), (timer) { - // 检查是否有活动 - bool isActive = _ttsService.isSpeaking || isSpeechDetected.value; - - // 如果有活动,更新时间并继续等待 - if (isActive) { - _updateActivityTime(); - return; - } - - // 检查空闲时间 - int idleSeconds = DateTime.now().difference(_lastActivityTime).inSeconds; - if (idleSeconds >= timeoutSeconds) { - timer.cancel(); - _stopVoiceRecognitionAutomatically(); - } - }); - } - - // 处理语音识别中的状态 - void _handleRecognizing(String text) { - if (text.isEmpty) return; - - recognizedText.value = text; - - // 设置语音检测状态为true,用于控制波形动画 - if (!isSpeechDetected.value) { - isSpeechDetected.value = true; - } - - // 更新活动时间 - _updateActivityTime(); - - // 用户开始讲话时,立即停止当前TTS和中断AI响应 - if (isSending.value || _aiResponseSubscription != null) { - _interruptCurrentResponse("检测到用户开始讲话,中断当前响应"); - } - - // 查找或创建用户的中间状态消息 - final intermediateIndex = - messages.indexWhere((m) => m.isIntermediate && m.role == 'user'); - - if (intermediateIndex >= 0) { - // 更新已有的中间状态消息 - messages[intermediateIndex] = messages[intermediateIndex].copyWith( - content: isVoiceRecognitionActive.value ? text : finalResult + text, - ); - messages.refresh(); - } else { - // 添加新的中间状态消息 - var intermediateMessage = Message.intermediate('user'); - intermediateMessage = intermediateMessage.copyWith(content: text); - messages.add(intermediateMessage); - messages.refresh(); - } - - WidgetsBinding.instance.addPostFrameCallback((_) { - _scrollToBottom(); - }); - } - - // 处理语音识别最终结果 - void _handleFinalResult(String text) { - if (text.isEmpty) return; - - Logger.info('处理语音识别最终结果: $text'); - - // 更新活动时间 - _updateActivityTime(); - - if (isVoiceRecognitionActive.value) { - // 一直语音说话 - - recognizedText.value = text; - // 查找并移除用户的中间状态消息 - final intermediateIndex = - messages.indexWhere((m) => m.isIntermediate && m.role == 'user'); - if (intermediateIndex >= 0) { - messages.removeAt(intermediateIndex); - } - - // 发送识别到的文本 - sendMessage(text); - // 重置识别文本,但不关闭语音识别框 - recognizedText.value = ''; - // 重置语音检测状态 - isSpeechDetected.value = false; - } else { - // 按住说话 - - finalResult += text; - recognizedText.value = finalResult; - - final intermediateIndex = - messages.indexWhere((m) => m.isIntermediate && m.role == 'user'); - - if (intermediateIndex >= 0) { - // 更新已有的中间状态消息 - messages[intermediateIndex] = messages[intermediateIndex].copyWith( - content: finalResult, - ); - messages.refresh(); - } - } - } - - // 处理语音识别事件错误 - void _handleRecognitionEventError(String? error) { - Logger.error('语音识别错误: ${error ?? "未知错误"}'); - - _cleanupRecognition(); - } - - // 处理语音识别流错误 - void _handleRecognitionError(dynamic error) { - Logger.error('语音识别错误', error); - - _cleanupRecognition(); - } - - // 清理语音识别资源 - void _cleanupRecognition() { - // 查找并移除用户的中间状态消息 - final intermediateIndex = - messages.indexWhere((m) => m.isIntermediate && m.role == 'user'); - if (intermediateIndex >= 0) { - if (messages[intermediateIndex].content.trim().isEmpty) { - messages.removeAt(intermediateIndex); - } else { - // 如果中间状态消息有内容,将其转换为正式消息 - messages[intermediateIndex] = messages[intermediateIndex].copyWith( - isIntermediate: false, - ); - } - messages.refresh(); - } - - // 停止语音识别 - isVoiceRecognitionActive.value = false; - isListening.value = false; - isSpeechDetected.value = false; - _asrService.stopContinuousRecognition(); - } - - // 停止语音识别 - Future stopVoiceRecognition() async { - if (!isVoiceRecognitionActive.value) return; - - try { - await _asrService.stopContinuousRecognition(); - _recognitionSubscription?.cancel(); - _recognitionSubscription = null; - - isVoiceRecognitionActive.value = false; - isListening.value = false; - recognizedText.value = ''; - isSpeechDetected.value = false; - - // 查找并处理用户的中间状态消息 - final intermediateIndex = - messages.indexWhere((m) => m.isIntermediate && m.role == 'user'); - if (intermediateIndex >= 0) { - if (messages[intermediateIndex].content.trim().isEmpty) { - // 如果没有内容则移除 - messages.removeAt(intermediateIndex); - } else { - // 有内容则转为正式消息 - messages[intermediateIndex] = messages[intermediateIndex].copyWith( - isIntermediate: false, - ); - } - messages.refresh(); - } - } catch (e) { - Logger.error('停止语音识别失败', e); - } - } - - // 清空聊天记录 - void clearChat() { - messages.clear(); - _ttsService.stop(); - // 添加欢迎消息 - if (agent.welcomeMessage.isNotEmpty) { - messages.add(Message( - role: 'assistant', - content: agent.welcomeMessage, - timestamp: DateTime.now(), - )); - } - // 删除数据库中的历史记录 - ChatStorage.deleteMessages(sessionId: agent.id).catchError((e) { - Logger.error('清空数据库聊天记录失败: ${e.toString()}'); - }); - } - - // 滚动监听,用于触发加载更多历史记录 - void _scrollListener() { - // 当滚动到顶部并且有更多历史记录且不在加载中时,加载更多 - if (scrollController.position.pixels == - scrollController.position.minScrollExtent && - _hasMoreHistory.value && - !_isLoadingMore.value) { - _loadMoreHistory(); - } - } - - // 加载聊天历史 (首次加载或刷新) - Future _loadChatHistory() async { - isLoading.value = true; - _currentPage.value = 1; // 重置为第一页 - _hasMoreHistory.value = true; // 假设有更多 - - try { - final pagedResult = await ChatStorage.getMessages( - sessionId: agent.id, - page: _currentPage.value, - pageSize: _pageSize, - ); - - _totalPages.value = pagedResult.totalPages; - _hasMoreHistory.value = _currentPage.value < _totalPages.value; - - final historyMessages = pagedResult.messages.map((chatMsg) { - return Message( - role: chatMsg.sender, - content: chatMsg.message, - timestamp: DateTime.parse(chatMsg.timestamp), - meta: chatMsg.metadata != null ? jsonDecode(chatMsg.metadata!) : null, - ); - }).toList(); - - messages.assignAll(historyMessages.reversed); // 最新的在底部,所以要反转 - - // 滚动到底部 (如果是首次加载) - WidgetsBinding.instance.addPostFrameCallback((_) { - if (scrollController.hasClients) { - scrollController.jumpTo(scrollController.position.maxScrollExtent); - } - }); - } catch (e) { - Logger.error('加载聊天历史失败', e); - } finally { - isLoading.value = false; - } - } - - // 加载更多历史记录 - Future _loadMoreHistory() async { - if (_isLoadingMore.value || !_hasMoreHistory.value) return; - - _isLoadingMore.value = true; - _currentPage.value++; - - try { - final pagedResult = await ChatStorage.getMessages( - sessionId: agent.id, - page: _currentPage.value, - pageSize: _pageSize, - ); - - _totalPages.value = pagedResult.totalPages; - _hasMoreHistory.value = _currentPage.value < _totalPages.value; - - final olderMessages = pagedResult.messages.map((chatMsg) { - return Message( - role: chatMsg.sender, - content: chatMsg.message, - timestamp: DateTime.parse(chatMsg.timestamp), - meta: chatMsg.metadata != null ? jsonDecode(chatMsg.metadata!) : null, - ); - }).toList(); - - // 将旧消息插入到列表顶部 - messages.insertAll(0, olderMessages.reversed); - } catch (e) { - Logger.error('加载更多聊天历史失败', e); - _currentPage.value--; // 加载失败,恢复页码 - } finally { - _isLoadingMore.value = false; - } - } - - // 保存单条聊天消息到数据库 - Future _saveMessageToDb(Message message) async { - if (message.isIntermediate) return; // 不保存中间状态的消息 - try { - await ChatStorage.saveMessage( - sessionId: agent.id, - message: message.content, - sender: message.role, - metadata: jsonEncode(message.meta), - ); - } catch (e) { - Logger.error('保存消息到数据库失败', e); - } - } - - // 添加消息,供外部调用 (如用户发送消息、AI回复) - void addMessage(Message message) { - messages.add(message); - _saveMessageToDb(message); // 保存到数据库 - WidgetsBinding.instance.addPostFrameCallback((_) { - _scrollToBottom(); - }); - } - - void _stopVoiceRecognitionAutomatically() { - final int timeoutSeconds = _isVoiceInteractionMode ? 8 : 15; - Logger.info('${timeoutSeconds}秒无活动,自动停止语音识别'); - stopVoiceRecognition(); - } - - /// 检查是否为重复消息 - bool _isDuplicateMessage(Message message) { - return messages.any((m) => - m.role == message.role && - m.content == message.content && - (m.timestamp.millisecondsSinceEpoch - - message.timestamp.millisecondsSinceEpoch) - .abs() < - 5000); - } -} \ No newline at end of file diff --git a/lib/modules/chat/models/message_model.dart b/lib/modules/chat/models/message_model.dart deleted file mode 100644 index bb1b0cce3..000000000 --- a/lib/modules/chat/models/message_model.dart +++ /dev/null @@ -1,64 +0,0 @@ -class Message { - final String role; // 'user' or 'assistant' - final String content; - final Map? meta; - final DateTime timestamp; - final bool isIntermediate; // 中间状态标记,替代isLoading - - Message({ - required this.role, - required this.content, - required this.timestamp, - this.meta, - this.isIntermediate = false, // 默认不是中间状态 - }); - - // 从JSON构造函数 - factory Message.fromJson(Map json) { - return Message( - role: json['role'] as String, - content: json['content'] as String, - meta: json['meta'], - timestamp: DateTime.parse(json['timestamp'] as String), - isIntermediate: json['isIntermediate'] as bool? ?? false, - ); - } - - // 转换为JSON - Map toJson() { - return { - 'role': role, - 'content': content, - 'meta': meta, - 'timestamp': timestamp.toIso8601String(), - 'isIntermediate': isIntermediate, - }; - } - - // 创建一个中间状态的消息 - factory Message.intermediate(String role) { - return Message( - role: role, - content: '', - timestamp: DateTime.now(), - isIntermediate: true, - ); - } - - // 复制并修改 - Message copyWith({ - String? role, - String? content, - Map? meta, - DateTime? timestamp, - bool? isIntermediate, - }) { - return Message( - role: role ?? this.role, - content: content ?? this.content, - meta: meta ?? this.meta, - timestamp: timestamp ?? this.timestamp, - isIntermediate: isIntermediate ?? this.isIntermediate, - ); - } -} diff --git a/lib/modules/chat/views/animated_gradient_background.dart b/lib/modules/chat/views/animated_gradient_background.dart deleted file mode 100644 index 3a96b8bc5..000000000 --- a/lib/modules/chat/views/animated_gradient_background.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:flutter/material.dart'; - -class AnimatedGradientBackground extends StatefulWidget { - final Widget child; - - const AnimatedGradientBackground(this.child, {super.key}); - - @override - State createState() => - _AnimatedGradientBackgroundState(); -} - -class _AnimatedGradientBackgroundState extends State - with SingleTickerProviderStateMixin { - late AnimationController _controller; - late Animation _beginAnimation; - late Animation _endAnimation; - - final Color color1 = const Color(0xFFfbcfdd); - final Color color2 = const Color(0xFFe2e9fb); - - @override - void initState() { - super.initState(); - - _controller = AnimationController( - vsync: this, - duration: const Duration(seconds: 10), - )..repeat(reverse: true); - - // 创建起始点动画 - _beginAnimation = AlignmentTween( - begin: Alignment.topCenter, - end: Alignment.bottomLeft, - ).animate(_controller); - - // 创建结束点动画 - _endAnimation = AlignmentTween( - begin: Alignment.bottomCenter, - end: Alignment.topRight, - ).animate(_controller); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: _controller, - builder: (context, child) { - return Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [color1, color2], - begin: _beginAnimation.value!, - end: _endAnimation.value!, - ), - ), - child: child, - ); - }, - child: widget.child, - ); - } -} diff --git a/lib/modules/chat/views/chat_music_card.dart b/lib/modules/chat/views/chat_music_card.dart deleted file mode 100644 index 10a6ce757..000000000 --- a/lib/modules/chat/views/chat_music_card.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart'; - -import '../controllers/chat_controller.dart'; - -class ChatMusicCard extends GetView { - final int type; - final Map? data; - final bool isDarkMode; - - const ChatMusicCard(this.type, this.data, this.isDarkMode, {super.key}); - - @override - Widget build(BuildContext context) { - // final Map musiccards = data?['musiccards'].first; - // final Map musicaudio = data?['musicaudio']; - - return Row( - children: [ - Container( - width: 60.w, - height: 60.w, - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6.r), - ), - child: Image.network( - data?['image'], - fit: BoxFit.cover, - ), - ), - 10.horizontalSpace, - Expanded( - child: Text( - data?['name'], - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black, - fontSize: 16.sp, - height: 1.5, - ), - ), - ), - 10.horizontalSpace, - Obx( - () => controller.audiourl.value == data?["url"] && - controller.isPlay.value - ? GestureDetector( - onTap: () { - switch (type) { - case 0: - controller.pause_music(); - break; - case 1: - controller.pause_spotify(); - break; - default: - } - }, - child: Stack( - alignment: Alignment.center, - children: [ - Icon( - Icons.pause_rounded, - color: isDarkMode ? Colors.white : Colors.black, - ), - ], - ), - ) - : GestureDetector( - onTap: () { - switch (type) { - case 0: - controller.play_music(data!); - break; - case 1: - controller.play_spotify(data!); - break; - default: - } - }, - child: Container( - width: 38.w, - height: 38.w, - decoration: BoxDecoration( - color: isDarkMode ? Colors.white : Colors.black, - shape: BoxShape.circle, - ), - child: Icon( - Icons.play_arrow_rounded, - color: isDarkMode ? Colors.black : Colors.white, - ), - ), - ), - ), - ], - ); - } -} diff --git a/lib/modules/chat/views/chat_navigation_card.dart b/lib/modules/chat/views/chat_navigation_card.dart deleted file mode 100644 index ee9085dcd..000000000 --- a/lib/modules/chat/views/chat_navigation_card.dart +++ /dev/null @@ -1,208 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:math'; -import 'dart:math' as math; - -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:http/http.dart' as http; - -class ChatNavigationCard extends StatefulWidget { - final String content; - final Map? data; - final bool isDarkMode; - - const ChatNavigationCard(this.data, this.content, this.isDarkMode, - {super.key}); - - @override - State createState() => _ChatNavigationCardState(); -} - -class _ChatNavigationCardState extends State { - final String apiKey = '8bde023b7e5e00f321ea17929f10fbae'; - LatLng startPoint = LatLng(116.397428, 39.90923); // 起点坐标 - LatLng endPoint = LatLng(116.397026, 39.903303); // 终点坐标 - - late Future _mapUrl; - - @override - void initState() { - super.initState(); - _getLocation(); - } - - void _getLocation() { - List startPointList = widget.data?['location'].split(','); - List endPointList = widget.data?['destination'].split(','); - startPoint = LatLng( - double.parse(startPointList[0]), - double.parse(startPointList[1]), - ); - endPoint = LatLng( - double.parse(endPointList[0]), - double.parse(endPointList[1]), - ); - _mapUrl = _getMapUrl(); - } - - Future _getMapUrl() async { - // 1. 获取路径数据 - final route = await _fetchRouteData(); - - // 2. 解析并简化路径 - List simplified = _simplifyPath(route, epsilon: 0.0001); - - // 3. 计算地图参数 - final zoom = _calculateAutoZoom(simplified); - - // 4. 生成静态地图URL - return _buildStaticMapUrl(simplified, zoom); - } - - Future> _fetchRouteData() async { - final url = 'https://restapi.amap.com/v3/direction/driving?' - 'origin=${startPoint.lng},${startPoint.lat}&' - 'destination=${endPoint.lng},${endPoint.lat}&' - 'key=$apiKey'; - - final response = await http.get(Uri.parse(url)); - if (response.statusCode == 200) { - final data = json.decode(response.body); - return _parsePolyline(data['route']['paths'][0]['steps']); - } - throw Exception('Failed to load route'); - } - - List _parsePolyline(List steps) { - final List path = []; - for (final step in steps) { - final points = (step['polyline'] as String).split(';'); - for (final point in points) { - final coords = point.split(',').map(double.parse).toList(); - path.add(LatLng(coords[0], coords[1])); - } - } - return path; - } - - List _simplifyPath(List points, {double epsilon = 0.0001}) { - return douglasPeucker(points, epsilon); - } - - int _calculateAutoZoom(List points, - {int mapWidth = 1024, int mapHeight = 512}) { - if (points.isEmpty) return 15; // 默认缩放级别 - - // 1. 计算坐标范围 - double minLng = double.infinity; - double maxLng = -double.infinity; - double minLat = double.infinity; - double maxLat = -double.infinity; - - for (final p in points) { - final lng = p.lng; - final lat = p.lat; - - minLng = math.min(minLng, lng); - maxLng = math.max(maxLng, lng); - minLat = math.min(minLat, lat); - maxLat = math.max(maxLat, lat); - } - - // 3. 计算缩放级别(基于Leaflet算法改进) - const worldWidth = 256; // 高德瓦片尺寸 - const worldHeight = 256; - const zoomMax = 18; // 高德最大缩放级别 - - double latRad(double lat) { - final sin = math.sin(lat * math.pi / 180); - return math.log((1 + sin) / (1 - sin)) / 2; - } - - final latFraction = (latRad(maxLat) - latRad(minLat)) / math.pi; - final lngDiff = maxLng - minLng; - final lngFraction = ((lngDiff < 0) ? (lngDiff + 360) : lngDiff) / 360; - - final latZoom = - math.log((mapHeight / worldHeight) / latFraction) / math.ln2; - final lngZoom = math.log((mapWidth / worldWidth) / lngFraction) / math.ln2; - - return math.min(math.min(latZoom, lngZoom).floor(), zoomMax); - } - - String _buildStaticMapUrl(List path, int zoom) { - final pathParam = path.map((p) => '${p.lng},${p.lat}').join(';'); - return 'https://restapi.amap.com/v3/staticmap?' - 'zoom=$zoom&' - 'size=1024*512&' - 'paths=10,0x0000ff,1,,:$pathParam&' - 'markers=mid,,A:${startPoint.lng},${startPoint.lat}|' - 'mid,,B:${endPoint.lng},${endPoint.lat}&' - 'key=$apiKey'; - } - - List douglasPeucker(List points, double epsilon) { - if (points.length < 3) return points; - - double maxDistance = 0; - int index = 0; - - for (int i = 1; i < points.length - 1; i++) { - double d = _perpendicularDistance(points[i], points.first, points.last); - if (d > maxDistance) { - index = i; - maxDistance = d; - } - } - - if (maxDistance > epsilon) { - return [ - ...douglasPeucker(points.sublist(0, index + 1), epsilon), - ...douglasPeucker(points.sublist(index), epsilon).skip(1) - ]; - } - return [points.first, points.last]; - } - - double _perpendicularDistance(LatLng point, LatLng start, LatLng end) { - final area = (end.lng - start.lng) * (point.lat - start.lat) - - (end.lat - start.lat) * (point.lng - start.lng); - final length = - sqrt(pow(end.lng - start.lng, 2) + pow(end.lat - start.lat, 2)); - return area.abs() / length; - } - - @override - Widget build(BuildContext context) { - return Column( - children: [ - FutureBuilder( - future: _mapUrl, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - return Image.network(snapshot.data); - } - return const CircularProgressIndicator(); - }, - ), - 10.verticalSpace, - Text( - widget.content, - style: TextStyle( - color: widget.isDarkMode ? Colors.white : Colors.black, - fontSize: 16.sp, - height: 1.5, - ), - ), - ], - ); - } -} - -class LatLng { - final double lat; - final double lng; - - LatLng(this.lng, this.lat); // 高德使用经度在前纬度在后的格式 -} diff --git a/lib/modules/chat/views/chat_view.dart b/lib/modules/chat/views/chat_view.dart deleted file mode 100644 index 8675dae88..000000000 --- a/lib/modules/chat/views/chat_view.dart +++ /dev/null @@ -1,820 +0,0 @@ -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 '../../../core/theme/app_colors.dart'; -import 'animated_gradient_background.dart'; -import 'chat_music_card.dart'; -import 'chat_navigation_card.dart'; -import 'cloud_background.dart'; -import 'weather_day_card.dart'; - -class ChatView extends GetView { - const ChatView({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - - return Obx( - () => Scaffold( - backgroundColor: - isDarkMode ? AppColors.darkBackground : Colors.grey[50], - appBar: controller.isVoiceRecognitionActive.value - ? null - : _buildAppBar(isDarkMode), - body: controller.isVoiceRecognitionActive.value - ? _buildVoiceInputPanel(isDarkMode) - : _buildBody(isDarkMode), - ), - ); - } - - // 构建AppBar - PreferredSizeWidget _buildAppBar(bool isDarkMode) { - return AppBar( - elevation: 1, - backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white, - leading: IconButton( - icon: Icon(Icons.arrow_back_ios, - color: isDarkMode ? Colors.white : Colors.black87, size: 20.r), - onPressed: () => Get.back(), - ), - titleSpacing: 0, - title: Row( - children: [ - // AI头像 - Container( - width: 40.r, - height: 40.r, - decoration: BoxDecoration( - color: AppColors.primary, - borderRadius: BorderRadius.circular(20.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 3, - offset: const Offset(0, 1), - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(20.r), - child: controller.agent.avatarUrl.isNotEmpty - ? Image.asset( - controller.agent.avatarUrl, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Icon( - Icons.smart_toy_rounded, - color: Colors.white, - size: 22.r, - ); - }, - ) - : Icon( - Icons.smart_toy_rounded, - color: Colors.white, - size: 22.r, - ), - ), - ), - SizedBox(width: 12.w), - // AI名称和状态 - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - '${controller.agent.name}'.tr, - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.bold, - color: isDarkMode ? Colors.white : Colors.black87, - ), - ), - Row( - children: [ - Container( - width: 6.r, - height: 6.r, - decoration: const BoxDecoration( - color: Colors.green, - shape: BoxShape.circle, - ), - ), - SizedBox(width: 4.w), - Text( - 'online'.tr, // 在线 - style: TextStyle( - fontSize: 12.sp, - color: Colors.green, - ), - ), - ], - ), - ], - ), - ), - ], - ), - actions: [ - IconButton( - icon: Icon( - Icons.phone, - color: isDarkMode ? Colors.white : Colors.black87, - ), - onPressed: controller.startVoiceRecognition, - ), - // TTS开关 - Obx(() => IconButton( - icon: Icon( - controller.isTtsEnabled.value - ? Icons.volume_up - : Icons.volume_off, - color: controller.isTtsEnabled.value - ? AppColors.primary - : isDarkMode - ? Colors.grey[400] - : Colors.grey, - ), - onPressed: controller.toggleTts, - )), - // 更多选项 - Container( - width: 34.r, - height: 34.r, - margin: EdgeInsets.only(right: 8.w), - decoration: BoxDecoration( - color: - isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100], - shape: BoxShape.circle, - boxShadow: [ - if (!isDarkMode) - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 2, - spreadRadius: 0.5, - ), - ], - ), - child: PopupMenuButton( - icon: Icon(Icons.more_vert, - color: isDarkMode ? Colors.white : Colors.black87, size: 20.r), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.r), - ), - color: isDarkMode ? AppColors.cardBackground : Colors.white, - itemBuilder: (context) => [ - PopupMenuItem( - value: 'clear', - child: Row( - children: [ - const Icon(Icons.delete_outline, color: Colors.red), - SizedBox(width: 8.w), - Text( - 'clearChatHistory'.tr, // 清空聊天记录 - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black87, - ), - ), - ], - ), - onTap: () { - // 延迟执行,避免在弹出菜单关闭前执行 - Future.delayed( - const Duration(milliseconds: 100), - () => controller.clearChat(), - ); - }, - ), - ], - ), - ), - ], - ); - } - - // 构建主体内容 - Widget _buildBody(bool isDarkMode) { - return Column( - children: [ - // 聊天内容区域 - Expanded( - child: Obx(() { - if (controller.messages.isEmpty) { - return _buildEmptyState(isDarkMode); - } else { - return _buildChatList(isDarkMode); - } - }), - ), - - // 底部输入区域 - _buildTextInputPanel(isDarkMode), - ], - ); - } - - // 构建空状态 - Widget _buildEmptyState(bool isDarkMode) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: 80.w, - height: 80.w, - decoration: BoxDecoration( - color: - isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100], - shape: BoxShape.circle, - ), - child: Icon( - Icons.chat_bubble_outline, - size: 40.w, - color: - isDarkMode ? Colors.white.withOpacity(0.5) : Colors.grey[400], - ), - ), - SizedBox(height: 24.h), - Text( - '你好!我是${controller.agent.name}助手', - style: TextStyle( - fontSize: 18.sp, - fontWeight: FontWeight.bold, - color: isDarkMode ? Colors.white : Colors.black87, - ), - ), - SizedBox(height: 12.h), - Padding( - padding: EdgeInsets.symmetric(horizontal: 40.w), - child: Text( - '我能帮你回答问题、实时语音翻译、推荐音乐和播客等。有什么我可以帮你的吗?', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.grey[400] : Colors.grey[600], - height: 1.5, - ), - ), - ), - ], - ), - ); - } - - // 构建聊天列表 - Widget _buildChatList(bool isDarkMode) { - return 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 _buildMessageItem(message, isDarkMode); - }, - ); - } - - // 构建消息项 - Widget _buildMessageItem(Message message, bool isDarkMode) { - final isUser = message.role == 'user'; - - return Padding( - padding: EdgeInsets.only(bottom: 16.h), - child: Row( - mainAxisAlignment: - isUser ? MainAxisAlignment.end : MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!isUser) _buildAvatar(false, isDarkMode), - SizedBox(width: 8.w), - Flexible( - child: Column( - crossAxisAlignment: - isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start, - children: [ - // 消息气泡 - Container( - padding: EdgeInsets.all(12.r), - decoration: BoxDecoration( - color: isUser - ? AppColors.primary - : isDarkMode - ? AppColors.cardBackground - : Colors.grey[200], - borderRadius: BorderRadius.circular(16.r), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 消息内容 - _buildMessageContent(message, isDarkMode, isUser), - - // 加载中动画 - if (message.isIntermediate) - Padding( - padding: EdgeInsets.only(top: 8.h), - child: Row( - mainAxisSize: MainAxisSize.min, - children: List.generate( - 3, - (index) => _buildLoadingDot(index, isDarkMode), - ), - ), - ), - ], - ), - ), - - // 时间戳 - Padding( - padding: EdgeInsets.only(top: 4.h, left: 4.w, right: 4.w), - child: Text( - _formatTime(message.timestamp), - style: TextStyle( - fontSize: 10.sp, - color: isDarkMode ? Colors.grey[500] : Colors.grey[500], - ), - ), - ), - ], - ), - ), - SizedBox(width: 8.w), - if (isUser) _buildAvatar(true, isDarkMode), - ], - ), - ); - } - - Widget _buildMessageContent(Message message, bool isDarkMode, bool isUser) { - Widget box = const SizedBox(); - if (message.content.isNotEmpty) { - box = SelectableText( - message.content.tr, - style: TextStyle( - color: isUser - ? Colors.white - : isDarkMode - ? Colors.white - : Colors.black, - fontSize: 16.sp, - height: 1.5, - ), - ); - } - if (message.meta != null) { - if (message.meta?['card_weather'] != null) { - box = WeatherDayCard(message.meta?['card_weather'], isDarkMode); - } - if (message.meta?['card_music'] != null) { - box = ChatMusicCard(0, message.meta?['card_music'], isDarkMode); - } - if (message.meta?['card_spotify'] != null) { - box = ChatMusicCard(1, message.meta?['card_spotify'], isDarkMode); - } - if (message.meta?['card_navigation'] != null) { - box = ChatNavigationCard( - message.meta?['card_navigation'], message.content, isDarkMode); - } - } - return box; - } - - // 构建文本输入面板 - Widget _buildTextInputPanel(bool isDarkMode) { - final textController = - TextEditingController(text: controller.inputText.value); - - return Container( - height: 48.w, - margin: EdgeInsets.only(left: 16.w, right: 16.w, top: 10.w, bottom: 20.w), - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - color: controller.isHoldSpeak.value - ? AppColors.primary - : isDarkMode - ? AppColors.cardBackground - : Colors.white, - borderRadius: BorderRadius.circular(12.w), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 3, - offset: const Offset(0, -1), - ), - ], - ), - child: Row( - children: [ - if (!controller.isHoldSpeak.value) 6.horizontalSpace, - if (!controller.isHoldSpeak.value) - Padding( - padding: EdgeInsets.all(6.w), - child: Icon( - Icons.camera_alt_outlined, - size: 28.w, - ), - ), - Expanded( - child: Obx( - () => controller.isTextInput.value - ? Center( - child: TextField( - controller: textController, - style: TextStyle( - fontSize: 15.sp, - color: isDarkMode ? Colors.white : Colors.black87, - ), - textInputAction: TextInputAction.send, - decoration: InputDecoration( - hintText: 'inputMessage'.tr, // 输入消息... - hintStyle: TextStyle(color: Colors.grey[500]), - filled: false, - border: InputBorder.none, - focusedBorder: InputBorder.none, - contentPadding: EdgeInsets.only(left: 6.w), - ), - onSubmitted: (value) { - if (value.trim().isNotEmpty) { - controller.sendMessage(value); - textController.clear(); - controller.inputText.value = ''; // 清空输入框 - } - }, - ), - ) - : Listener( - onPointerDown: (event) { - controller.talkLongPress(); - }, - onPointerUp: (event) { - controller.talkLongPressUp(); - }, - child: Container( - padding: EdgeInsets.only( - left: controller.isHoldSpeak.value ? 0 : 40.w), - color: Colors.transparent, - child: controller.isHoldSpeak.value - ? Row( - mainAxisAlignment: MainAxisAlignment.center, - children: List.generate(40, (int index) { - return Container( - width: 3.w, - height: 10.w, - margin: - EdgeInsets.symmetric(horizontal: 2.w), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(3.r), - ), - ); - }), - ) - : Center( - child: Text( - '按住说话', - style: TextStyle( - fontSize: 16.sp, - color: isDarkMode - ? Colors.white - : Colors.black87, - ), - ), - ), - ), - ), - ), - ), - if (!controller.isHoldSpeak.value) - GestureDetector( - onTap: () { - controller.isTextInput.value = !controller.isTextInput.value; - }, - child: Container( - width: 28.w, - height: 28.w, - margin: EdgeInsets.all(6.w), - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(28.r), - border: Border.all( - color: isDarkMode ? Colors.white : Colors.black87, - width: 2, - ), - ), - child: Obx( - () => Icon( - controller.isTextInput.value ? Icons.mic : Icons.keyboard, - size: 20.w, - ), - ), - ), - ), - if (!controller.isHoldSpeak.value) - Container( - width: 28.w, - height: 28.w, - margin: EdgeInsets.all(6.w), - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(28.r), - border: Border.all( - color: isDarkMode ? Colors.white : Colors.black87, - width: 2, - ), - ), - child: Icon( - Icons.add, - size: 20.w, - ), - ), - if (!controller.isHoldSpeak.value) 6.horizontalSpace, - ], - ), - ); - } - - // 构建语音输入面板 - Widget _buildVoiceInputPanel(bool isDarkMode) { - return AnimatedGradientBackground( - SafeArea( - child: Column( - children: [ - Row( - children: [], - ), - Expanded( - child: Center( - child: Container( - width: 200.w, - height: 200.w, - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Colors.blue.withOpacity(0.4), - Colors.purple.withOpacity(0.4) - ], - begin: Alignment.centerLeft, - end: Alignment.centerRight, - ), - shape: BoxShape.circle, - ), - child: CircularFlowAnimation(), - ), - ), - ), - SizedBox( - height: 80.h, - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 20.w), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - // 语音录制按钮 - Container( - width: 60.r, - height: 60.r, - decoration: BoxDecoration( - color: isDarkMode - ? Colors.white.withOpacity(0.1) - : Colors.white, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 8, - spreadRadius: 1, - ), - ], - ), - child: Center( - child: Obx(() => Icon( - controller.isListening.value - ? Icons.mic - : Icons.mic_none, - color: controller.isListening.value - ? AppColors.primary - : Colors.grey.shade400, - size: 28.r, - )), - ), - ), - - // 中间音频波形和状态文本 - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // 音频波形区域 - Container( - height: 50.h, - child: Obx(() { - final isSpeaking = - controller.isSpeechDetected.value; - return Center( - child: Container( - width: 220.w, - height: 30.h, - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: List.generate( - 23, - (index) { - // 随机高度模拟波形 - double height = isSpeaking - ? (Random().nextDouble() * 20.0 + - 5.0) - .h - : 2.h; - - return SizedBox( - width: 6.w, - child: Center( - child: AnimatedContainer( - duration: const Duration( - milliseconds: 500), - width: 2.5.w, - height: height, - margin: EdgeInsets.symmetric( - horizontal: 1.5.w), - decoration: BoxDecoration( - color: isSpeaking - ? AppColors.primary - : isDarkMode - ? Colors.grey.shade600 - : Colors.grey.shade400, - borderRadius: - BorderRadius.circular(2.r), - ), - ), - ), - ); - }, - ), - ), - ), - ); - }), - ), - - // 语音状态文本 - Obx(() => Text( - controller.isSpeechDetected.value - ? 'imListening'.tr // 正在聆听... - : 'canStartTalking'.tr, // 你可以开始说话 - style: TextStyle( - fontSize: 14.sp, - color: isDarkMode - ? Colors.grey[400] - : Colors.grey[700], - fontWeight: FontWeight.w500, - ), - )), - ], - ), - ), - - // 关闭按钮 - Container( - width: 60.r, - height: 60.r, - decoration: BoxDecoration( - color: isDarkMode - ? Colors.white.withOpacity(0.1) - : Colors.white, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 8, - spreadRadius: 1, - ), - ], - ), - child: Center( - child: IconButton( - icon: Icon( - Icons.close, - color: isDarkMode - ? Colors.white - : Colors.grey.shade600, - size: 28.r, - ), - padding: EdgeInsets.zero, - onPressed: controller.stopVoiceRecognition, - ), - ), - ), - ], - ), - ), - ), - ], - ), - ), - ); - } - - // 构建头像 - Widget _buildAvatar(bool isUser, bool isDarkMode) { - return Container( - width: 36.r, - height: 36.r, - decoration: BoxDecoration( - color: isUser - ? (isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[200]) - : AppColors.primary, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 2, - offset: const Offset(0, 1), - ), - ], - ), - child: isUser - ? Center( - child: Icon( - Icons.person, - color: isDarkMode ? Colors.white : Colors.grey[700], - size: 20.r, - ), - ) - : ClipRRect( - borderRadius: BorderRadius.circular(18.r), - child: controller.agent.avatarUrl.isNotEmpty - ? Image.asset( - controller.agent.avatarUrl, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Icon( - Icons.smart_toy_rounded, - color: Colors.white, - size: 20.r, - ); - }, - ) - : Icon( - Icons.smart_toy_rounded, - color: Colors.white, - size: 20.r, - ), - ), - ); - } - - // 构建加载动画点 - Widget _buildLoadingDot(int index, bool isDarkMode) { - return AnimatedBuilder( - animation: const AlwaysStoppedAnimation(0), - builder: (context, child) { - return Container( - margin: EdgeInsets.symmetric(horizontal: 2.w), - width: 8.r, - height: 8.r, - decoration: BoxDecoration( - color: - isDarkMode ? Colors.white.withOpacity(0.5) : Colors.grey[400], - shape: BoxShape.circle, - ), - ); - }, - ); - } - - // 格式化时间 - String _formatTime(DateTime time) { - final now = DateTime.now(); - final today = DateTime(now.year, now.month, now.day); - //final yesterday = today.subtract(const Duration(days: 1)); - final messageDate = DateTime(time.year, time.month, time.day); - - String timeStr = - '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}'; - - if (messageDate == today) { - return timeStr; // 只显示时间,如 "14:30" - } - // else if (messageDate == yesterday) { - // return 'Yesterday $timeStr'; // 如 "Yesterday 14:30" - // } - else { - // 国际通用日期格式 "MM-dd HH:mm" - return '${time.month.toString().padLeft(2, '0')}-${time.day.toString().padLeft(2, '0')} $timeStr'; - } - } -} diff --git a/lib/modules/chat/views/cloud_background.dart b/lib/modules/chat/views/cloud_background.dart deleted file mode 100644 index 995e6a168..000000000 --- a/lib/modules/chat/views/cloud_background.dart +++ /dev/null @@ -1,144 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; - -class CircularFlowAnimation extends StatefulWidget { - const CircularFlowAnimation({super.key}); - - @override - State createState() => _CircularFlowAnimationState(); -} - -class _CircularFlowAnimationState extends State - with SingleTickerProviderStateMixin { - late AnimationController _controller; - - @override - void initState() { - super.initState(); - _controller = AnimationController( - vsync: this, - duration: const Duration(seconds: 20), - )..repeat(); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return CustomPaint( - painter: CircularFlowPainter(_controller), - size: Size.infinite, - ); - } -} - -class CircularFlowPainter extends CustomPainter { - final Animation animation; - - CircularFlowPainter(this.animation) : super(repaint: animation); - - @override - void paint(Canvas canvas, Size size) { - final center = Offset(size.width / 2, size.height / 2); - const baseRadius = 150.0; - final time = animation.value * 2 * pi; - - // 绘制圆形路径 - _drawCircularPath(canvas, center, baseRadius); - - // 绘制三个围绕圆形运动的渐变云朵 - _drawOrbitingCloud( - canvas: canvas, - center: center, - time: time, - colors: [Colors.blue, Colors.purple], - orbitRadius: baseRadius, - speed: 0.5, - direction: 1, - ); - - _drawOrbitingCloud( - canvas: canvas, - center: center, - time: time, - colors: [Colors.pink, Colors.orange], - orbitRadius: baseRadius * 0.8, - speed: 1.0, - direction: -1, - ); - - _drawOrbitingCloud( - canvas: canvas, - center: center, - time: time, - colors: [Colors.green, Colors.yellow], - orbitRadius: baseRadius * 1.2, - speed: 0.3, - direction: 1, - ); - - _drawOrbitingCloud( - canvas: canvas, - center: center, - time: time, - colors: [Colors.red, Colors.deepOrangeAccent], - orbitRadius: baseRadius * 0.5, - speed: 0.8, - direction: -1, - ); - } - - void _drawCircularPath(Canvas canvas, Offset center, double radius) { - final paint = Paint() - ..color = Colors.red - ..style = PaintingStyle.stroke - ..strokeWidth = 2 - ..maskFilter = MaskFilter.blur(BlurStyle.normal, 4); - - canvas.drawCircle(center, radius, paint); - } - - void _drawOrbitingCloud({ - required Canvas canvas, - required Offset center, - required double time, - required List colors, - required double orbitRadius, - required double speed, - required int direction, - }) { - final angle = time * speed * direction; - final position = Offset( - center.dx + orbitRadius * cos(angle), - center.dy + orbitRadius * sin(angle), - ); - - final gradient = RadialGradient( - colors: colors, - stops: const [0.3, 1.0], - radius: 0.6, - focal: Alignment( - 0.1 + cos(angle * 2) * 0.3, - 0.1 + sin(angle * 2) * 0.3, - ), - ); - - final paint = Paint() - ..shader = gradient.createShader(Rect.fromCircle( - center: position, - radius: 100 + 30 * sin(time * 3), - )) - ..maskFilter = MaskFilter.blur(BlurStyle.normal, 40) - ..blendMode = BlendMode.plus; - - canvas.drawCircle(position, 80 + 20 * cos(time * 2), paint); - } - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => true; -} diff --git a/lib/modules/chat/views/weather_day_card.dart b/lib/modules/chat/views/weather_day_card.dart deleted file mode 100644 index afb14dd70..000000000 --- a/lib/modules/chat/views/weather_day_card.dart +++ /dev/null @@ -1,92 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; - -class WeatherDayCard extends StatelessWidget { - final Map? data; - final bool isDarkMode; - - const WeatherDayCard(this.data, this.isDarkMode, {super.key}); - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - data?['city'], - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black, - fontSize: 10.sp, - ), - ), - Text( - '${data?['temperature']}°', - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black, - fontSize: 30.sp, - ), - ), - ], - ), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Icon( - Icons.sunny, - color: isDarkMode ? Colors.white : Colors.amber[600]!, - size: 18.w, - ), - Text( - '${data?['weatherCondition']},${data?['windDirection']}风', - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black, - fontSize: 14.sp, - ), - ), - Row( - children: [ - Text( - '湿\n度', - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black, - fontSize: 10.sp, - height: 1, - ), - ), - 5.horizontalSpace, - Text( - '${data?['humidity']}', - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black, - fontSize: 20.sp, - ), - ), - 10.horizontalSpace, - Text( - '风\n力', - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black, - fontSize: 10.sp, - height: 1, - ), - ), - 5.horizontalSpace, - Text( - '${data?['windForce']}', - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black, - fontSize: 20.sp, - ), - ), - ], - ), - ], - ), - ], - ); - } -}