You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
321 lines
9.1 KiB
321 lines
9.1 KiB
import 'package:get/get.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:get_storage/get_storage.dart';
|
|
import 'dart:convert';
|
|
import '../../../data/services/volcano_ai_service.dart';
|
|
import '../../../data/services/volcano_tts_service.dart';
|
|
import '../models/message_model.dart';
|
|
|
|
class ChatController extends GetxController {
|
|
final String agentId;
|
|
final String agentName;
|
|
final String agentAvatar;
|
|
final String? agentSubtitle;
|
|
final String systemPrompt;
|
|
final String welcomeMessage;
|
|
final VolcanoAIService _aiService;
|
|
late final VolcanoTtsService _ttsService;
|
|
static const String _storagePrefix = 'chat_history_';
|
|
|
|
final messages = <Message>[].obs;
|
|
final isLoading = false.obs;
|
|
final textController = TextEditingController();
|
|
final scrollController = ScrollController();
|
|
final _storage = GetStorage();
|
|
|
|
// 用于存储当前正在流式生成的消息
|
|
final RxString currentStreamMessage = ''.obs;
|
|
Message? _currentAssistantMessage;
|
|
String _pendingTtsText = ''; // 用于累积TTS文本
|
|
static const int _minTtsLength = 20; // TTS最小文本长度
|
|
bool _isDisposed = false; // 添加销毁标记
|
|
|
|
ChatController({
|
|
required this.agentId,
|
|
required this.agentName,
|
|
required this.agentAvatar,
|
|
this.agentSubtitle,
|
|
required this.systemPrompt,
|
|
required this.welcomeMessage,
|
|
}) : _aiService = VolcanoAIService() {
|
|
_initTtsService();
|
|
}
|
|
|
|
void _initTtsService() {
|
|
try {
|
|
_ttsService = Get.find<VolcanoTtsService>();
|
|
} catch (_) {
|
|
// 如果找不到实例,创建一个新的
|
|
_ttsService = Get.put(VolcanoTtsService(), permanent: true);
|
|
}
|
|
// 默认开启TTS
|
|
_ttsService.isEnabled.value = true;
|
|
}
|
|
|
|
@override
|
|
void onInit() async {
|
|
super.onInit();
|
|
await _initializeController();
|
|
}
|
|
|
|
Future<void> _initializeController() async {
|
|
try {
|
|
await _loadChatHistory();
|
|
|
|
// 如果没有聊天记录,显示欢迎消息并生成个性化问候语
|
|
if (messages.isEmpty) {
|
|
// 添加欢迎消息
|
|
final initialMessage = Message(
|
|
role: 'assistant',
|
|
content: welcomeMessage,
|
|
timestamp: DateTime.now(),
|
|
);
|
|
messages.add(initialMessage);
|
|
|
|
// 生成并添加个性化问候语
|
|
try {
|
|
currentStreamMessage.value = '';
|
|
_currentAssistantMessage = Message(
|
|
role: 'assistant',
|
|
content: '',
|
|
timestamp: DateTime.now(),
|
|
);
|
|
messages.add(_currentAssistantMessage!);
|
|
|
|
await for (final chunk in _aiService.generateGreeting(
|
|
agentName: agentName,
|
|
systemPrompt: systemPrompt,
|
|
)) {
|
|
if (_isDisposed) break;
|
|
|
|
currentStreamMessage.value += chunk;
|
|
_updateAssistantMessage(currentStreamMessage.value);
|
|
|
|
// 累积文本用于TTS
|
|
if (!_isDisposed && _ttsService.isEnabled.value) {
|
|
_ttsService.speak(chunk);
|
|
}
|
|
}
|
|
|
|
// 保存聊天记录
|
|
if (!_isDisposed) {
|
|
await _saveChatHistory();
|
|
// 延迟滚动以确保布局完成
|
|
Future.delayed(const Duration(milliseconds: 100), _scrollToBottom);
|
|
}
|
|
} catch (e) {
|
|
print('生成问候语失败: $e');
|
|
// 如果生成问候语失败,移除空消息并只播放欢迎消息
|
|
if (_currentAssistantMessage != null) {
|
|
messages.remove(_currentAssistantMessage);
|
|
}
|
|
if (_ttsService.isEnabled.value) {
|
|
_ttsService.speak(welcomeMessage);
|
|
}
|
|
} finally {
|
|
if (!_isDisposed) {
|
|
_currentAssistantMessage = null;
|
|
currentStreamMessage.value = '';
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
Get.snackbar(
|
|
'Error',
|
|
'Failed to initialize chat: $e',
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void onClose() {
|
|
_isDisposed = true; // 标记为已销毁
|
|
textController.dispose();
|
|
scrollController.dispose();
|
|
_ttsService.stop(); // 停止当前播放
|
|
super.onClose();
|
|
}
|
|
|
|
Future<void> _loadChatHistory() async {
|
|
try {
|
|
final historyJson = _storage.read('${_storagePrefix}${agentId}');
|
|
if (historyJson != null) {
|
|
final List<dynamic> historyList = json.decode(historyJson);
|
|
messages.value = historyList.map((item) => Message.fromJson(item)).toList();
|
|
}
|
|
} catch (e) {
|
|
Get.snackbar(
|
|
'Error',
|
|
'Failed to load chat history: $e',
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> _saveChatHistory() async {
|
|
try {
|
|
final historyJson = json.encode(messages.map((msg) => msg.toJson()).toList());
|
|
await _storage.write('${_storagePrefix}${agentId}', historyJson);
|
|
} catch (e) {
|
|
Get.snackbar(
|
|
'Error',
|
|
'Failed to save chat history: $e',
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
);
|
|
}
|
|
}
|
|
|
|
void _scrollToBottom() {
|
|
if (scrollController.hasClients) {
|
|
scrollController.animateTo(
|
|
scrollController.position.maxScrollExtent,
|
|
duration: const Duration(milliseconds: 300),
|
|
curve: Curves.easeOut,
|
|
);
|
|
}
|
|
}
|
|
|
|
void _updateAssistantMessage(String content) {
|
|
if (_currentAssistantMessage != null) {
|
|
final index = messages.indexWhere((msg) =>
|
|
msg.timestamp == _currentAssistantMessage!.timestamp &&
|
|
msg.role == 'assistant'
|
|
);
|
|
|
|
if (index != -1) {
|
|
messages[index] = Message(
|
|
role: 'assistant',
|
|
content: content,
|
|
timestamp: _currentAssistantMessage!.timestamp,
|
|
);
|
|
_scrollToBottom();
|
|
}
|
|
}
|
|
}
|
|
|
|
void sendMessage(String text) async {
|
|
if (text.trim().isEmpty) return;
|
|
|
|
final userMessage = Message(
|
|
role: 'user',
|
|
content: text,
|
|
timestamp: DateTime.now(),
|
|
);
|
|
messages.add(userMessage);
|
|
textController.clear();
|
|
await _saveChatHistory();
|
|
_scrollToBottom();
|
|
|
|
isLoading.value = true;
|
|
try {
|
|
currentStreamMessage.value = '';
|
|
_currentAssistantMessage = Message(
|
|
role: 'assistant',
|
|
content: '',
|
|
timestamp: DateTime.now(),
|
|
);
|
|
messages.add(_currentAssistantMessage!);
|
|
_pendingTtsText = '';
|
|
|
|
await for (final chunk in _aiService.sendMessageStream(
|
|
messages: messages.map((m) => {'role': m.role, 'content': m.content}).toList(),
|
|
systemPrompt: systemPrompt,
|
|
)) {
|
|
if (_isDisposed) break;
|
|
|
|
currentStreamMessage.value += chunk;
|
|
_updateAssistantMessage(currentStreamMessage.value);
|
|
|
|
// 累积文本并合成
|
|
_pendingTtsText += chunk;
|
|
if (!_isDisposed && _ttsService.isEnabled.value) {
|
|
// 检查是否达到最小长度
|
|
if (_pendingTtsText.length >= _minTtsLength ) {
|
|
// 找到最后一个句子结束的位置
|
|
int lastSentenceEnd = _findLastSentenceEnd(_pendingTtsText);
|
|
if (lastSentenceEnd > 0) {
|
|
// 播放到最后一个句子结束的位置
|
|
String textToSpeak = _pendingTtsText.substring(0, lastSentenceEnd + 1);
|
|
_ttsService.speak(textToSpeak);
|
|
// 保留剩余的文本
|
|
_pendingTtsText = _pendingTtsText.substring(lastSentenceEnd + 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 处理剩余的文本
|
|
if (!_isDisposed && _ttsService.isEnabled.value && _pendingTtsText.isNotEmpty) {
|
|
_ttsService.speak(_pendingTtsText);
|
|
}
|
|
_pendingTtsText = '';
|
|
|
|
// 流式传输完成后保存聊天记录
|
|
if (!_isDisposed) {
|
|
await _saveChatHistory();
|
|
}
|
|
} catch (e) {
|
|
if (!_isDisposed) {
|
|
if (_currentAssistantMessage != null) {
|
|
messages.remove(_currentAssistantMessage);
|
|
}
|
|
Get.snackbar(
|
|
'Error',
|
|
'Failed to get response from AI: $e',
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
);
|
|
}
|
|
} finally {
|
|
if (!_isDisposed) {
|
|
_currentAssistantMessage = null;
|
|
currentStreamMessage.value = '';
|
|
isLoading.value = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
int _findLastSentenceEnd(String text) {
|
|
final sentenceEnds = [
|
|
text.lastIndexOf('。'),
|
|
text.lastIndexOf('!'),
|
|
text.lastIndexOf('?'),
|
|
text.lastIndexOf('.'),
|
|
text.lastIndexOf('!'),
|
|
text.lastIndexOf('?'),
|
|
];
|
|
|
|
return sentenceEnds.reduce((max, pos) => pos > max ? pos : max);
|
|
}
|
|
|
|
Future<void> clearChat() async {
|
|
try {
|
|
messages.clear();
|
|
await _storage.remove('${_storagePrefix}${agentId}');
|
|
|
|
// 重新添加欢迎消息
|
|
final initialMessage = Message(
|
|
role: 'assistant',
|
|
content: welcomeMessage,
|
|
timestamp: DateTime.now(),
|
|
);
|
|
messages.add(initialMessage);
|
|
await _saveChatHistory();
|
|
|
|
// 播放欢迎消息
|
|
if (_ttsService.isEnabled.value) {
|
|
_ttsService.speak(welcomeMessage);
|
|
}
|
|
} catch (e) {
|
|
Get.snackbar(
|
|
'Error',
|
|
'Failed to clear chat: $e',
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
);
|
|
}
|
|
}
|
|
|
|
void toggleTTS() {
|
|
_ttsService.toggleEnabled();
|
|
}
|
|
}
|