Melhorias no comportamento do chat com IA;
Adição do histórico de conversas com IA.
This commit is contained in:
@@ -14,12 +14,24 @@ class ChatHistoryPage extends StatefulWidget {
|
||||
|
||||
class _ChatHistoryPageState extends State<ChatHistoryPage> {
|
||||
List<Map<String, dynamic>> _conversations = [];
|
||||
List<Map<String, dynamic>> _filteredConversations = [];
|
||||
bool _isLoading = true;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
String _selectedDateFilter = 'all'; // all, today, yesterday, week, month
|
||||
final Set<String> _selectedConversationIds = {};
|
||||
bool _isSelectionMode = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadConversations();
|
||||
_searchController.addListener(_filterConversations);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadConversations() async {
|
||||
@@ -29,6 +41,7 @@ class _ChatHistoryPageState extends State<ChatHistoryPage> {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_conversations = conversations;
|
||||
_filteredConversations = conversations;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
@@ -40,6 +53,136 @@ class _ChatHistoryPageState extends State<ChatHistoryPage> {
|
||||
}
|
||||
}
|
||||
|
||||
void _toggleSelectionMode() {
|
||||
setState(() {
|
||||
_isSelectionMode = !_isSelectionMode;
|
||||
if (!_isSelectionMode) {
|
||||
_selectedConversationIds.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _toggleConversationSelection(String conversationId) {
|
||||
setState(() {
|
||||
if (_selectedConversationIds.contains(conversationId)) {
|
||||
_selectedConversationIds.remove(conversationId);
|
||||
} else {
|
||||
_selectedConversationIds.add(conversationId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _deleteSelectedConversations() async {
|
||||
if (_selectedConversationIds.isEmpty) return;
|
||||
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('Eliminar ${_selectedConversationIds.length} conversas'),
|
||||
content: const Text('Tem certeza que deseja eliminar estas conversas?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('Cancelar'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||
child: const Text('Eliminar'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed != true) return;
|
||||
|
||||
try {
|
||||
for (final id in _selectedConversationIds) {
|
||||
await ChatMemoryService.deleteConversation(id);
|
||||
}
|
||||
await _loadConversations();
|
||||
setState(() {
|
||||
_selectedConversationIds.clear();
|
||||
_isSelectionMode = false;
|
||||
});
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
const Icon(Icons.delete_outline, color: Colors.white),
|
||||
const SizedBox(width: 12),
|
||||
const Text('Conversas eliminadas'),
|
||||
],
|
||||
),
|
||||
backgroundColor: Colors.orange,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.error('Error deleting conversations: $e');
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
const Icon(Icons.error_outline, color: Colors.white),
|
||||
const SizedBox(width: 12),
|
||||
const Text('Erro ao eliminar conversas'),
|
||||
],
|
||||
),
|
||||
backgroundColor: Colors.orange,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _filterConversations() {
|
||||
final searchQuery = _searchController.text.toLowerCase();
|
||||
setState(() {
|
||||
_filteredConversations = _conversations.where((conv) {
|
||||
// Filter by search query (title)
|
||||
final title = (conv['title'] as String? ?? '').toLowerCase();
|
||||
final matchesSearch = title.contains(searchQuery);
|
||||
|
||||
// Filter by date
|
||||
final matchesDate = _matchesDateFilter(conv);
|
||||
|
||||
return matchesSearch && matchesDate;
|
||||
}).toList();
|
||||
});
|
||||
}
|
||||
|
||||
bool _matchesDateFilter(Map<String, dynamic> conv) {
|
||||
final updatedAt = conv['updatedAt'] as Timestamp?;
|
||||
if (updatedAt == null) return true;
|
||||
final date = updatedAt.toDate();
|
||||
final now = DateTime.now();
|
||||
final difference = now.difference(date);
|
||||
|
||||
switch (_selectedDateFilter) {
|
||||
case 'today':
|
||||
return difference.inDays == 0;
|
||||
case 'yesterday':
|
||||
return difference.inDays == 1;
|
||||
case 'week':
|
||||
return difference.inDays < 7;
|
||||
case 'month':
|
||||
return difference.inDays < 30;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deleteConversation(String conversationId) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
@@ -66,15 +209,41 @@ class _ChatHistoryPageState extends State<ChatHistoryPage> {
|
||||
await ChatMemoryService.deleteConversation(conversationId);
|
||||
await _loadConversations();
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('Conversa eliminada')));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
const Icon(Icons.delete_outline, color: Colors.white),
|
||||
const SizedBox(width: 12),
|
||||
const Text('Conversa eliminada'),
|
||||
],
|
||||
),
|
||||
backgroundColor: Colors.orange,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.error('Error deleting conversation: $e');
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Erro ao eliminar conversa')),
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
const Icon(Icons.error_outline, color: Colors.white),
|
||||
const SizedBox(width: 12),
|
||||
const Text('Erro ao eliminar conversa'),
|
||||
],
|
||||
),
|
||||
backgroundColor: Colors.orange,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -111,7 +280,9 @@ class _ChatHistoryPageState extends State<ChatHistoryPage> {
|
||||
onPressed: () => context.go('/student-dashboard'),
|
||||
),
|
||||
title: Text(
|
||||
'Histórico de Conversas',
|
||||
_isSelectionMode
|
||||
? '${_selectedConversationIds.length} selecionadas'
|
||||
: 'Histórico de Conversas',
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 18,
|
||||
@@ -119,55 +290,157 @@ class _ChatHistoryPageState extends State<ChatHistoryPage> {
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.refresh, color: cs.onSurface),
|
||||
onPressed: _loadConversations,
|
||||
if (_isSelectionMode)
|
||||
IconButton(
|
||||
icon: Icon(Icons.delete, color: Colors.red),
|
||||
onPressed: _selectedConversationIds.isEmpty
|
||||
? null
|
||||
: _deleteSelectedConversations,
|
||||
)
|
||||
else
|
||||
IconButton(
|
||||
icon: Icon(Icons.checklist, color: cs.onSurface),
|
||||
onPressed: _toggleSelectionMode,
|
||||
),
|
||||
if (!_isSelectionMode)
|
||||
IconButton(
|
||||
icon: Icon(Icons.refresh, color: cs.onSurface),
|
||||
onPressed: _loadConversations,
|
||||
),
|
||||
if (_isSelectionMode)
|
||||
IconButton(
|
||||
icon: Icon(Icons.close, color: cs.onSurface),
|
||||
onPressed: _toggleSelectionMode,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// Search and filters
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
// Search bar
|
||||
TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Pesquisar conversas...',
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
suffixIcon: _searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
},
|
||||
)
|
||||
: null,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: cs.outline.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: cs.surface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Date filter chips
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
_buildDateFilterChip('Todas', 'all', cs),
|
||||
const SizedBox(width: 8),
|
||||
_buildDateFilterChip('Hoje', 'today', cs),
|
||||
const SizedBox(width: 8),
|
||||
_buildDateFilterChip('Ontem', 'yesterday', cs),
|
||||
const SizedBox(width: 8),
|
||||
_buildDateFilterChip('Última semana', 'week', cs),
|
||||
const SizedBox(width: 8),
|
||||
_buildDateFilterChip('Último mês', 'month', cs),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Conversation list
|
||||
Expanded(
|
||||
child: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _filteredConversations.isEmpty
|
||||
? Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.chat_bubble_outline,
|
||||
size: 64,
|
||||
color: cs.onSurfaceVariant.withValues(alpha: 0.4),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
_conversations.isEmpty
|
||||
? 'Sem conversas ainda'
|
||||
: 'Nenhuma conversa encontrada',
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_conversations.isEmpty
|
||||
? 'Começa uma conversa com o Vico!'
|
||||
: 'Tenta ajustar os filtros',
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant.withValues(alpha: 0.7),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: ListView.separated(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: _filteredConversations.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 12),
|
||||
itemBuilder: (context, index) {
|
||||
final conversation = _filteredConversations[index];
|
||||
return _buildConversationCard(conversation, cs);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _conversations.isEmpty
|
||||
? Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.chat_bubble_outline,
|
||||
size: 64,
|
||||
color: cs.onSurfaceVariant.withValues(alpha: 0.4),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Sem conversas ainda',
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Começa uma conversa com o Vico!',
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant.withValues(alpha: 0.7),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: ListView.separated(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: _conversations.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 12),
|
||||
itemBuilder: (context, index) {
|
||||
final conversation = _conversations[index];
|
||||
return _buildConversationCard(conversation, cs);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDateFilterChip(String label, String value, ColorScheme cs) {
|
||||
final isSelected = _selectedDateFilter == value;
|
||||
return FilterChip(
|
||||
label: Text(label),
|
||||
selected: isSelected,
|
||||
onSelected: (selected) {
|
||||
setState(() {
|
||||
_selectedDateFilter = value;
|
||||
});
|
||||
_filterConversations();
|
||||
},
|
||||
backgroundColor: cs.surface,
|
||||
selectedColor: cs.primary.withValues(alpha: 0.2),
|
||||
labelStyle: TextStyle(
|
||||
color: isSelected ? cs.primary : cs.onSurface,
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
side: BorderSide(
|
||||
color: isSelected ? cs.primary : cs.outline.withValues(alpha: 0.3),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -175,10 +448,16 @@ class _ChatHistoryPageState extends State<ChatHistoryPage> {
|
||||
Map<String, dynamic> conversation,
|
||||
ColorScheme cs,
|
||||
) {
|
||||
final isSelected = _selectedConversationIds.contains(conversation['id']);
|
||||
|
||||
return Dismissible(
|
||||
key: Key(conversation['id']),
|
||||
direction: DismissDirection.endToStart,
|
||||
onDismissed: (_) => _deleteConversation(conversation['id']),
|
||||
direction: _isSelectionMode
|
||||
? DismissDirection.none
|
||||
: DismissDirection.endToStart,
|
||||
onDismissed: _isSelectionMode
|
||||
? null
|
||||
: (_) => _deleteConversation(conversation['id']),
|
||||
background: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
@@ -190,16 +469,24 @@ class _ChatHistoryPageState extends State<ChatHistoryPage> {
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
ChatMemoryService.setCurrentConversationId(conversation['id']);
|
||||
context.go('/ai-tutor/${conversation['id']}');
|
||||
if (_isSelectionMode) {
|
||||
_toggleConversationSelection(conversation['id']);
|
||||
} else {
|
||||
ChatMemoryService.setCurrentConversationId(conversation['id']);
|
||||
context.go('/ai-tutor/${conversation['id']}');
|
||||
}
|
||||
},
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surface,
|
||||
color: isSelected ? cs.primary.withValues(alpha: 0.1) : cs.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: cs.outline.withValues(alpha: 0.15)),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? cs.primary
|
||||
: cs.outline.withValues(alpha: 0.15),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: cs.shadow.withValues(alpha: 0.05),
|
||||
@@ -213,21 +500,34 @@ class _ChatHistoryPageState extends State<ChatHistoryPage> {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [cs.primary, cs.primary.withValues(alpha: 0.7)],
|
||||
if (_isSelectionMode)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 12),
|
||||
child: Checkbox(
|
||||
value: isSelected,
|
||||
onChanged: (_) =>
|
||||
_toggleConversationSelection(conversation['id']),
|
||||
),
|
||||
)
|
||||
else
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
cs.primary,
|
||||
cs.primary.withValues(alpha: 0.7),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.chat_bubble,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.chat_bubble,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
|
||||
@@ -77,28 +77,55 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
|
||||
final conversation = await ChatMemoryService.getConversation(
|
||||
conversationId,
|
||||
);
|
||||
if (conversation == null) return;
|
||||
if (conversation == null) {
|
||||
Logger.warning('Conversation not found: $conversationId');
|
||||
return;
|
||||
}
|
||||
|
||||
final messages = await ChatMemoryService.getConversationMessages(
|
||||
conversationId: conversationId,
|
||||
limit: 50,
|
||||
);
|
||||
|
||||
final materialIds = conversation['selectedMaterialIds'] as List<dynamic>?;
|
||||
if (materialIds != null) {
|
||||
setState(() {
|
||||
_selectedMaterialIds = materialIds.cast<String>().toSet();
|
||||
_materialsConfirmed = true;
|
||||
});
|
||||
// Safely extract material IDs with type checking
|
||||
try {
|
||||
final materialIdsRaw = conversation['selectedMaterialIds'];
|
||||
if (materialIdsRaw != null && materialIdsRaw is List) {
|
||||
final materialIds = (materialIdsRaw as List)
|
||||
.where((item) => item is String)
|
||||
.cast<String>()
|
||||
.toSet();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_selectedMaterialIds = materialIds;
|
||||
_materialsConfirmed = materialIds.isNotEmpty;
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.error('Error loading material IDs: $e');
|
||||
// Continue without materials rather than crash
|
||||
}
|
||||
|
||||
// Load messages from Firestore
|
||||
// Load messages from Firestore with safe casting
|
||||
final loadedMessages = messages.map((msg) {
|
||||
return {
|
||||
'content': msg['content'] as String,
|
||||
'isUser': msg['role'] == 'user',
|
||||
'timestamp': msg['createdAt'] as Timestamp? ?? DateTime.now(),
|
||||
};
|
||||
try {
|
||||
return {
|
||||
'content': msg['content']?.toString() ?? '',
|
||||
'isUser': msg['role']?.toString() == 'user',
|
||||
'timestamp': msg['createdAt'] is Timestamp
|
||||
? (msg['createdAt'] as Timestamp).toDate()
|
||||
: DateTime.now(),
|
||||
};
|
||||
} catch (e) {
|
||||
Logger.error('Error mapping message: $e');
|
||||
// Return a safe fallback message
|
||||
return {
|
||||
'content': '[Mensagem indisponível]',
|
||||
'isUser': false,
|
||||
'timestamp': DateTime.now(),
|
||||
};
|
||||
}
|
||||
}).toList();
|
||||
|
||||
if (mounted) {
|
||||
@@ -109,6 +136,12 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.error('Error loading conversation: $e');
|
||||
// Show error to user
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_messages = [];
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,6 +289,42 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Nova conversa'),
|
||||
content: const Text(
|
||||
'Isto vai limpar o chat atual e guardá-lo no histórico. Continuar?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('Cancelar'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.red,
|
||||
),
|
||||
child: const Text('Limpar'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed == true) {
|
||||
ChatMemoryService.setCurrentConversationId(null);
|
||||
context.go('/ai-tutor');
|
||||
}
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.add_comment,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
tooltip: 'Nova conversa',
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
),
|
||||
@@ -1289,12 +1358,6 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
|
||||
setState(() {
|
||||
_selectedMaterialIds = tempSelected;
|
||||
_materialsConfirmed = true;
|
||||
if (!isFirst && selectionChanged) {
|
||||
final welcome = _messages.isNotEmpty
|
||||
? [_messages.first]
|
||||
: <Map<String, dynamic>>[];
|
||||
_messages = welcome;
|
||||
}
|
||||
});
|
||||
if (isFirst) _addWelcomeMessage();
|
||||
if (selectionChanged) {
|
||||
@@ -1319,9 +1382,10 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
|
||||
}
|
||||
|
||||
void _addWelcomeMessage() {
|
||||
final welcomeText =
|
||||
'Olá! Estou pronto para te ajudar com os materiais que selecionaste. Faz a tua pergunta sempre que quiseres! 💪';
|
||||
final welcomeMessage = {
|
||||
'content':
|
||||
'''Olá! Estou pronto para te ajudar com os materiais que selecionaste. Faz a tua pergunta sempre que quiseres! 💪''',
|
||||
'content': welcomeText,
|
||||
'isUser': false,
|
||||
'timestamp': DateTime.now(),
|
||||
};
|
||||
@@ -1330,29 +1394,81 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
|
||||
_messages.add(welcomeMessage);
|
||||
});
|
||||
|
||||
// Save welcome message to Firestore so the AI knows it already greeted the user
|
||||
ChatMemoryService.saveMessage(role: 'assistant', content: welcomeText);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_scrollToBottom();
|
||||
});
|
||||
}
|
||||
|
||||
/// Get names of selected materials
|
||||
List<String> _getSelectedMaterialNames() {
|
||||
return _selectedMaterialIds.map((id) {
|
||||
final material = _availableMaterials.firstWhere(
|
||||
(m) => m['id'] == id,
|
||||
orElse: () => {'id': id, 'name': 'Material'},
|
||||
);
|
||||
return material['name'] ?? 'Material';
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/// Determine discipline name from selected materials
|
||||
String? _getDisciplineName() {
|
||||
if (_selectedMaterialIds.isEmpty) return null;
|
||||
// Find classId of first selected material
|
||||
final firstId = _selectedMaterialIds.first;
|
||||
final material = _availableMaterials.firstWhere(
|
||||
(m) => m['id'] == firstId,
|
||||
orElse: () => {},
|
||||
);
|
||||
final classId = material['classId'];
|
||||
if (classId != null && _classNames.containsKey(classId)) {
|
||||
return _classNames[classId];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Detect if current discipline is mathematics
|
||||
bool _isMathematics() {
|
||||
final discipline = _getDisciplineName();
|
||||
if (discipline == null) return false;
|
||||
final lower = discipline.toLowerCase();
|
||||
final mathKeywords = [
|
||||
'matemática',
|
||||
'matematica',
|
||||
'math',
|
||||
'álgebra',
|
||||
'algebra',
|
||||
'geometria',
|
||||
'cálculo',
|
||||
'calculo',
|
||||
'estatística',
|
||||
'estatistica',
|
||||
'física',
|
||||
'fisica',
|
||||
'química',
|
||||
'quimica',
|
||||
];
|
||||
return mathKeywords.any((k) => lower.contains(k));
|
||||
}
|
||||
|
||||
Future<void> _handleSendMessage() async {
|
||||
if (_messageController.text.trim().isEmpty) return;
|
||||
|
||||
final userMessage = _messageController.text.trim();
|
||||
|
||||
// Save user message to Firestore
|
||||
await ChatMemoryService.saveMessage(role: 'user', content: userMessage);
|
||||
|
||||
// Update conversation title if it's the first message
|
||||
// Update conversation title if it's still the default "Nova conversa"
|
||||
final currentId = ChatMemoryService.currentConversationId;
|
||||
if (currentId != null && _messages.isEmpty) {
|
||||
final title = userMessage.length > 30
|
||||
? '${userMessage.substring(0, 30)}...'
|
||||
: userMessage;
|
||||
await ChatMemoryService.updateConversationTitle(
|
||||
conversationId: currentId,
|
||||
title: title,
|
||||
);
|
||||
if (currentId != null) {
|
||||
final conversation = await ChatMemoryService.getConversation(currentId);
|
||||
if (conversation != null && conversation['title'] == 'Nova conversa') {
|
||||
final title = ChatMemoryService.generateConversationTitle(userMessage);
|
||||
await ChatMemoryService.updateConversationTitle(
|
||||
conversationId: currentId,
|
||||
title: title,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Add user message
|
||||
@@ -1377,6 +1493,11 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
|
||||
selectedMaterialIds: _selectedMaterialIds.isEmpty
|
||||
? null
|
||||
: _selectedMaterialIds.toList(),
|
||||
selectedMaterialNames: _selectedMaterialIds.isEmpty
|
||||
? null
|
||||
: _getSelectedMaterialNames(),
|
||||
disciplineName: _getDisciplineName(),
|
||||
isMathematics: _isMathematics(),
|
||||
);
|
||||
|
||||
final preview = replyText.length > 50
|
||||
@@ -1384,12 +1505,6 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
|
||||
: replyText;
|
||||
Logger.info('Ollama response received: $preview...');
|
||||
|
||||
// Save assistant message to Firestore
|
||||
await ChatMemoryService.saveMessage(
|
||||
role: 'assistant',
|
||||
content: replyText,
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_messages.add({
|
||||
'content': replyText,
|
||||
|
||||
Reference in New Issue
Block a user