Browse Source

refactor(chat): 移除废弃的chat模块及相关组件和控制器

weicu
lxm 1 year ago
parent
commit
560b8dc3f0
  1. 10
      lib/modules/chat/bindings/chat_binding.dart
  2. 992
      lib/modules/chat/controllers/chat_controller.dart
  3. 64
      lib/modules/chat/models/message_model.dart
  4. 69
      lib/modules/chat/views/animated_gradient_background.dart
  5. 100
      lib/modules/chat/views/chat_music_card.dart
  6. 208
      lib/modules/chat/views/chat_navigation_card.dart
  7. 820
      lib/modules/chat/views/chat_view.dart
  8. 144
      lib/modules/chat/views/cloud_background.dart
  9. 92
      lib/modules/chat/views/weather_day_card.dart

10
lib/modules/chat/bindings/chat_binding.dart

@ -1,10 +0,0 @@
import 'package:get/get.dart';
import '../controllers/chat_controller.dart';
class ChatBinding extends Bindings {
@override
void dependencies() {
// 注册控制器
Get.lazyPut<ChatController>(() => ChatController());
}
}

992
lib/modules/chat/controllers/chat_controller.dart

@ -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<AsrService>();
late SpotifService _spotifService;
// 添加ScrollController用于控制列表滚动
final ScrollController scrollController = ScrollController();
StreamSubscription? _locationStream;
String _latLng = '';
String _city = '深圳市';
// Agent 信息
late final Agent agent;
// 消息列表
final RxList<Message> messages = <Message>[].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<void> onInit() async {
super.onInit();
Logger.info('ChatController onInit');
// 从路由参数获取Agent信息
final Map<String, dynamic> 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<KouziAIService>();
break;
case 'health_advisor':
// 多功能AI聊天
Get.put(DeapsoundAIService());
_aiService = Get.find<DeapsoundAIService>();
break;
default:
// 默认使用OpenAIServiceAdapter
_aiService = Get.find<OpenAIServiceAdapter>();
Logger.info('使用OpenAIServiceAdapter');
}
Get.put(SpotifService());
_spotifService = Get.find<SpotifService>();
// 使用克隆音色语音合成
final GetStorage storage = GetStorage();
String timbre = storage.read('timbre') ?? '1';
if (timbre == '1') {
_ttsService = Get.find<TtsService>();
} else {
Get.put(VoiceCloneTtsService());
_ttsService = Get.find<VoiceCloneTtsService>();
}
_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<void> 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<Map<String, String>> 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<Map<String, String>> _buildMessageHistory() {
final List<Map<String, String>> 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<void> 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<void> 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<void> _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<void> _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<void> _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);
}
}

64
lib/modules/chat/models/message_model.dart

@ -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<String, dynamic> 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<String, dynamic> 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,
);
}
}

69
lib/modules/chat/views/animated_gradient_background.dart

@ -1,69 +0,0 @@
import 'package:flutter/material.dart';
class AnimatedGradientBackground extends StatefulWidget {
final Widget child;
const AnimatedGradientBackground(this.child, {super.key});
@override
State<AnimatedGradientBackground> createState() =>
_AnimatedGradientBackgroundState();
}
class _AnimatedGradientBackgroundState extends State<AnimatedGradientBackground>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<Alignment?> _beginAnimation;
late Animation<Alignment?> _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,
);
}
}

100
lib/modules/chat/views/chat_music_card.dart

@ -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<ChatController> {
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,
),
),
),
),
],
);
}
}

208
lib/modules/chat/views/chat_navigation_card.dart

@ -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<ChatNavigationCard> createState() => _ChatNavigationCardState();
}
class _ChatNavigationCardState extends State<ChatNavigationCard> {
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<String> _getMapUrl() async {
// 1. 获取路径数据
final route = await _fetchRouteData();
// 2. 解析并简化路径
List<LatLng> simplified = _simplifyPath(route, epsilon: 0.0001);
// 3. 计算地图参数
final zoom = _calculateAutoZoom(simplified);
// 4. 生成静态地图URL
return _buildStaticMapUrl(simplified, zoom);
}
Future<List<LatLng>> _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<LatLng> _parsePolyline(List<dynamic> steps) {
final List<LatLng> 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<LatLng> _simplifyPath(List<LatLng> points, {double epsilon = 0.0001}) {
return douglasPeucker(points, epsilon);
}
int _calculateAutoZoom(List<LatLng> 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<LatLng> 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<LatLng> douglasPeucker(List<LatLng> 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); // 高德使用经度在前纬度在后的格式
}

820
lib/modules/chat/views/chat_view.dart

@ -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<ChatController> {
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';
}
}
}

144
lib/modules/chat/views/cloud_background.dart

@ -1,144 +0,0 @@
import 'dart:math';
import 'package:flutter/material.dart';
class CircularFlowAnimation extends StatefulWidget {
const CircularFlowAnimation({super.key});
@override
State<CircularFlowAnimation> createState() => _CircularFlowAnimationState();
}
class _CircularFlowAnimationState extends State<CircularFlowAnimation>
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<double> 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<Color> 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;
}

92
lib/modules/chat/views/weather_day_card.dart

@ -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,
),
),
],
),
],
),
],
);
}
}
Loading…
Cancel
Save