historico

This commit is contained in:
2026-05-21 11:49:56 +01:00
parent 2f411d08a4
commit 5bda59f7af
5 changed files with 742 additions and 61 deletions

View File

@@ -3,34 +3,180 @@ import 'package:firebase_auth/firebase_auth.dart';
import '../utils/logger.dart';
/// Service for managing conversation history in Firestore
/// Structure: conversations/{conversationId}/messages/{messageId}
/// Structure: userChats/{userId}/conversations/{conversationId}/messages/{messageId}
class ChatMemoryService {
static final FirebaseFirestore _firestore = FirebaseFirestore.instance;
static final FirebaseAuth _auth = FirebaseAuth.instance;
/// Get or create a conversation for the current user
static Future<String> _getOrCreateConversationId() async {
/// Current active conversation ID
static String? _currentConversationId;
/// Get current conversation ID
static String? get currentConversationId => _currentConversationId;
/// Set current conversation ID
static void setCurrentConversationId(String? id) {
_currentConversationId = id;
}
/// Create a new conversation
static Future<String> createConversation({
required List<String> selectedMaterialIds,
String? title,
}) async {
final user = _auth.currentUser;
if (user == null) {
throw Exception('User not authenticated');
}
// For simplicity, use user's UID as conversation ID
// In a multi-conversation system, this would create new conversation docs
return user.uid;
final conversationRef = await _firestore
.collection('userChats')
.doc(user.uid)
.collection('conversations')
.add({
'title': title ?? 'Nova conversa',
'createdAt': FieldValue.serverTimestamp(),
'updatedAt': FieldValue.serverTimestamp(),
'selectedMaterialIds': selectedMaterialIds,
'messageCount': 0,
});
_currentConversationId = conversationRef.id;
Logger.info('Created new conversation: ${conversationRef.id}');
return conversationRef.id;
}
/// Update conversation materials
static Future<void> updateConversationMaterials({
required String conversationId,
required List<String> selectedMaterialIds,
}) async {
try {
await _firestore
.collection('userChats')
.doc(_auth.currentUser?.uid)
.collection('conversations')
.doc(conversationId)
.update({
'selectedMaterialIds': selectedMaterialIds,
'updatedAt': FieldValue.serverTimestamp(),
});
Logger.info('Updated materials for conversation: $conversationId');
} catch (e) {
Logger.error('Error updating conversation materials: $e');
}
}
/// Update conversation title
static Future<void> updateConversationTitle({
required String conversationId,
required String title,
}) async {
try {
await _firestore
.collection('userChats')
.doc(_auth.currentUser?.uid)
.collection('conversations')
.doc(conversationId)
.update({'title': title, 'updatedAt': FieldValue.serverTimestamp()});
Logger.info('Updated title for conversation: $conversationId');
} catch (e) {
Logger.error('Error updating conversation title: $e');
}
}
/// Get all conversations for current user
static Future<List<Map<String, dynamic>>> getConversations() async {
try {
final user = _auth.currentUser;
if (user == null) return [];
final snapshot = await _firestore
.collection('userChats')
.doc(user.uid)
.collection('conversations')
.orderBy('updatedAt', descending: true)
.get();
final conversations = snapshot.docs.map((doc) {
final data = doc.data();
return {
'id': doc.id,
'title': data['title'] as String? ?? 'Sem título',
'createdAt': data['createdAt'] as Timestamp?,
'updatedAt': data['updatedAt'] as Timestamp?,
'selectedMaterialIds':
(data['selectedMaterialIds'] as List<dynamic>?)?.cast<String>() ??
[],
'messageCount': data['messageCount'] as int? ?? 0,
};
}).toList();
Logger.info('Retrieved ${conversations.length} conversations');
return conversations;
} catch (e) {
Logger.error('Error getting conversations: $e');
return [];
}
}
/// Delete a conversation
static Future<void> deleteConversation(String conversationId) async {
try {
final user = _auth.currentUser;
if (user == null) return;
// Delete all messages in the conversation
final messagesSnapshot = await _firestore
.collection('userChats')
.doc(user.uid)
.collection('conversations')
.doc(conversationId)
.collection('messages')
.get();
final batch = _firestore.batch();
for (final doc in messagesSnapshot.docs) {
batch.delete(doc.reference);
}
// Delete the conversation document
batch.delete(
_firestore
.collection('userChats')
.doc(user.uid)
.collection('conversations')
.doc(conversationId),
);
await batch.commit();
if (_currentConversationId == conversationId) {
_currentConversationId = null;
}
Logger.info('Deleted conversation: $conversationId');
} catch (e) {
Logger.error('Error deleting conversation: $e');
}
}
/// Save a message to Firestore
static Future<void> saveMessage({
required String role, // 'user' or 'assistant'
required String content,
String? conversationId,
}) async {
try {
final conversationId = await _getOrCreateConversationId();
final user = _auth.currentUser;
if (user == null) return;
final convId = conversationId ?? _currentConversationId;
if (convId == null) {
Logger.warning('No conversation ID, message not saved');
return;
}
final messageData = {
'role': role,
'content': content,
@@ -39,26 +185,45 @@ class ChatMemoryService {
};
await _firestore
.collection('userChats')
.doc(user.uid)
.collection('conversations')
.doc(conversationId)
.doc(convId)
.collection('messages')
.add(messageData);
Logger.info('Message saved to Firestore: role=$role');
// Update conversation metadata
await _firestore
.collection('userChats')
.doc(user.uid)
.collection('conversations')
.doc(convId)
.update({
'updatedAt': FieldValue.serverTimestamp(),
'messageCount': FieldValue.increment(1),
});
Logger.info(
'Message saved to Firestore: role=$role, conversation=$convId',
);
} catch (e) {
Logger.error('Error saving message: $e');
}
}
/// Get the last N messages from conversation history
/// Get the last N messages from a specific conversation
/// Returns list of messages sorted by createdAt ascending (oldest first)
static Future<List<Map<String, dynamic>>> getRecentMessages({
static Future<List<Map<String, dynamic>>> getConversationMessages({
required String conversationId,
int limit = 20,
}) async {
try {
final conversationId = await _getOrCreateConversationId();
final user = _auth.currentUser;
if (user == null) return [];
final snapshot = await _firestore
.collection('userChats')
.doc(user.uid)
.collection('conversations')
.doc(conversationId)
.collection('messages')
@@ -68,64 +233,110 @@ class ChatMemoryService {
// Convert to list and reverse to get ascending order (oldest first)
final messages = snapshot.docs
.map((doc) => {
'role': doc.data()['role'] as String,
'content': doc.data()['content'] as String,
'createdAt': doc.data()['createdAt'] as Timestamp?,
})
.map(
(doc) => {
'role': doc.data()['role'] as String,
'content': doc.data()['content'] as String,
'createdAt': doc.data()['createdAt'] as Timestamp?,
},
)
.toList()
.reversed
.toList();
// Log de confirmação de ordem
if (messages.isNotEmpty) {
Logger.info('History order fixed. First message: ${messages.first['role']} - ${(messages.first['content'] as String).substring(0, (messages.first['content'] as String).length > 30 ? 30 : (messages.first['content'] as String).length)}...');
}
Logger.info('Retrieved ${messages.length} messages from history (oldest first)');
Logger.info(
'Retrieved ${messages.length} messages from conversation $conversationId',
);
return messages;
} catch (e) {
Logger.error('Error getting recent messages: $e');
Logger.error('Error getting conversation messages: $e');
return [];
}
}
/// Build messages array for API request
/// Get conversation details
static Future<Map<String, dynamic>?> getConversation(
String conversationId,
) async {
try {
final user = _auth.currentUser;
if (user == null) return null;
final doc = await _firestore
.collection('userChats')
.doc(user.uid)
.collection('conversations')
.doc(conversationId)
.get();
if (!doc.exists) return null;
final data = doc.data();
return {
'id': doc.id,
'title': data?['title'] as String? ?? 'Sem título',
'createdAt': data?['createdAt'] as Timestamp?,
'updatedAt': data?['updatedAt'] as Timestamp?,
'selectedMaterialIds':
(data?['selectedMaterialIds'] as List<dynamic>?)?.cast<String>() ??
[],
'messageCount': data?['messageCount'] as int? ?? 0,
};
} catch (e) {
Logger.error('Error getting conversation: $e');
return null;
}
}
/// Build messages array for API request from current conversation
/// Returns list of message maps with 'role' and 'content' keys
static Future<List<Map<String, String>>> buildMessagesForAPI({
required String currentUserMessage,
String? conversationId,
int historyLimit = 20,
}) async {
final messages = <Map<String, String>>[];
// 1. Get recent conversation history
final history = await getRecentMessages(limit: historyLimit);
final convId = conversationId ?? _currentConversationId;
if (convId != null) {
// Get recent conversation history
final history = await getConversationMessages(
conversationId: convId,
limit: historyLimit,
);
// 2. Add historical messages
for (final msg in history) {
messages.add({
'role': msg['role'] as String,
'content': msg['content'] as String,
});
// Add historical messages
for (final msg in history) {
messages.add({
'role': msg['role'] as String,
'content': msg['content'] as String,
});
}
}
// 3. Add current user message
messages.add({
'role': 'user',
'content': currentUserMessage,
});
// Add current user message
messages.add({'role': 'user', 'content': currentUserMessage});
Logger.info('Built messages array with ${messages.length} messages for API');
Logger.info(
'Built messages array with ${messages.length} messages for API',
);
return messages;
}
/// Clear conversation history for current user
/// Clear current conversation history
static Future<void> clearHistory() async {
try {
final conversationId = await _getOrCreateConversationId();
final user = _auth.currentUser;
if (user == null) return;
final convId = _currentConversationId;
if (convId == null) return;
final snapshot = await _firestore
.collection('userChats')
.doc(user.uid)
.collection('conversations')
.doc(conversationId)
.doc(convId)
.collection('messages')
.get();
@@ -136,6 +347,14 @@ class ChatMemoryService {
}
await batch.commit();
// Reset message count
await _firestore
.collection('userChats')
.doc(user.uid)
.collection('conversations')
.doc(convId)
.update({'messageCount': 0});
Logger.info('Conversation history cleared');
} catch (e) {
Logger.error('Error clearing history: $e');

View File

@@ -51,14 +51,19 @@ IMPORTANTE - RESPOSTAS COMPLETAS:
});
// PASSO 3 — BUSCAR MEMÓRIA DA CONVERSA NA Cloud Firestore
final conversationHistory = await ChatMemoryService.getRecentMessages(
limit: 20,
);
for (final msg in conversationHistory) {
messages.add({
'role': msg['role'] as String,
'content': msg['content'] as String,
});
final conversationId = ChatMemoryService.currentConversationId;
if (conversationId != null) {
final conversationHistory =
await ChatMemoryService.getConversationMessages(
conversationId: conversationId,
limit: 20,
);
for (final msg in conversationHistory) {
messages.add({
'role': msg['role'] as String,
'content': msg['content'] as String,
});
}
}
// PASSO 4 — BUSCAR PDFs DO PROFESSOR NO Firebase Storage (RAG CHUNK RETRIEVAL)
@@ -905,14 +910,19 @@ IMPORTANTE - RESPOSTAS COMPLETAS:
});
// PASSO 3 — BUSCAR MEMÓRIA DA CONVERSA NA Cloud Firestore (máx 4 para poupar heap)
final conversationHistory = await ChatMemoryService.getRecentMessages(
limit: 4,
);
for (final msg in conversationHistory) {
messages.add({
'role': msg['role'] as String,
'content': msg['content'] as String,
});
final conversationId = ChatMemoryService.currentConversationId;
if (conversationId != null) {
final conversationHistory =
await ChatMemoryService.getConversationMessages(
conversationId: conversationId,
limit: 4,
);
for (final msg in conversationHistory) {
messages.add({
'role': msg['role'] as String,
'content': msg['content'] as String,
});
}
}
// Log de confirmação de ordem do histórico