Quiz e tutor chat modificações
This commit is contained in:
@@ -29,11 +29,11 @@ class RAGAIService {
|
|||||||
// PASSO 2 — ADICIONAR SYSTEM MESSAGE DO GOAT (SEMPRE PRIMEIRO)
|
// PASSO 2 — ADICIONAR SYSTEM MESSAGE DO GOAT (SEMPRE PRIMEIRO)
|
||||||
messages.add({
|
messages.add({
|
||||||
'role': 'system',
|
'role': 'system',
|
||||||
'content': '''Tu és "O GOAT", o Assistente IA oficial do Teach it.
|
'content': '''Tu és "Alt", o Assistente IA oficial do Teach it.
|
||||||
|
|
||||||
Nunca referes o nome do modelo.
|
Nunca referes o nome do modelo.
|
||||||
Nunca dizes que és Qwen ou OpenAI.
|
Nunca dizes que és Qwen ou OpenAI.
|
||||||
Respondes sempre como o GOAT.
|
Respondes sempre como a Alt.
|
||||||
|
|
||||||
Tens personalidade confiante, motivadora e orgulhosa.
|
Tens personalidade confiante, motivadora e orgulhosa.
|
||||||
Ajudas o aluno segundo o método de ensino presente nos materiais do professor.
|
Ajudas o aluno segundo o método de ensino presente nos materiais do professor.
|
||||||
@@ -41,7 +41,9 @@ Usas formatação Markdown clara e organizada.''',
|
|||||||
});
|
});
|
||||||
|
|
||||||
// PASSO 3 — BUSCAR MEMÓRIA DA CONVERSA NA Cloud Firestore
|
// PASSO 3 — BUSCAR MEMÓRIA DA CONVERSA NA Cloud Firestore
|
||||||
final conversationHistory = await ChatMemoryService.getRecentMessages(limit: 20);
|
final conversationHistory = await ChatMemoryService.getRecentMessages(
|
||||||
|
limit: 20,
|
||||||
|
);
|
||||||
for (final msg in conversationHistory) {
|
for (final msg in conversationHistory) {
|
||||||
messages.add({
|
messages.add({
|
||||||
'role': msg['role'] as String,
|
'role': msg['role'] as String,
|
||||||
@@ -58,33 +60,27 @@ Usas formatação Markdown clara e organizada.''',
|
|||||||
if (pdfContext.isNotEmpty) {
|
if (pdfContext.isNotEmpty) {
|
||||||
messages.add({
|
messages.add({
|
||||||
'role': 'system',
|
'role': 'system',
|
||||||
'content': pdfContext, // Já vem formatado com [CHUNK 1], [CHUNK 2], etc.
|
'content':
|
||||||
|
pdfContext, // Já vem formatado com [CHUNK 1], [CHUNK 2], etc.
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// PASSO 5 — SÓ AGORA adicionar a pergunta do user
|
// PASSO 5 — SÓ AGORA adicionar a pergunta do user
|
||||||
messages.add({
|
messages.add({'role': 'user', 'content': userQuery});
|
||||||
'role': 'user',
|
|
||||||
'content': userQuery,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Log do tamanho do array para verificação
|
// Log do tamanho do array para verificação
|
||||||
Logger.info('Built messages array with ${messages.length} messages for API');
|
Logger.info(
|
||||||
|
'Built messages array with ${messages.length} messages for API',
|
||||||
|
);
|
||||||
|
|
||||||
// Save user message to Firestore (after building the messages array)
|
// Save user message to Firestore (after building the messages array)
|
||||||
await ChatMemoryService.saveMessage(
|
await ChatMemoryService.saveMessage(role: 'user', content: userQuery);
|
||||||
role: 'user',
|
|
||||||
content: userQuery,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Call Ollama API with complete messages array
|
// Call Ollama API with complete messages array
|
||||||
final response = await _callOllamaAPIWithMessages(messages);
|
final response = await _callOllamaAPIWithMessages(messages);
|
||||||
|
|
||||||
// Save AI response to memory
|
// Save AI response to memory
|
||||||
await ChatMemoryService.saveMessage(
|
await ChatMemoryService.saveMessage(role: 'assistant', content: response);
|
||||||
role: 'assistant',
|
|
||||||
content: response,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Process response and create RAGResponse
|
// Process response and create RAGResponse
|
||||||
final ragResponse = _createRAGResponse(
|
final ragResponse = _createRAGResponse(
|
||||||
@@ -181,11 +177,12 @@ Usas formatação Markdown clara e organizada.''',
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// System message for O GOAT identity (for legacy calls)
|
/// System message for O GOAT identity (for legacy calls)
|
||||||
static const String _systemMessage = '''Tu és "O GOAT", o Assistente IA oficial do Teach it.
|
static const String _systemMessage =
|
||||||
|
'''Tu és "Alt", o Assistente IA oficial do Teach it.
|
||||||
|
|
||||||
Nunca referes o nome do modelo.
|
Nunca referes o nome do modelo.
|
||||||
Nunca dizes que és Qwen ou OpenAI.
|
Nunca dizes que és Qwen ou OpenAI.
|
||||||
Respondes sempre como o GOAT.
|
Respondes sempre como a Alt.
|
||||||
|
|
||||||
Tens personalidade confiante, motivadora e orgulhosa.
|
Tens personalidade confiante, motivadora e orgulhosa.
|
||||||
Ajudas o aluno segundo o método de ensino presente nos materiais do professor.
|
Ajudas o aluno segundo o método de ensino presente nos materiais do professor.
|
||||||
@@ -485,13 +482,11 @@ Usas formatação clara e organizada.''';
|
|||||||
final messages = <Map<String, String>>[
|
final messages = <Map<String, String>>[
|
||||||
{
|
{
|
||||||
'role': 'system',
|
'role': 'system',
|
||||||
'content': 'És um assistente educativo especializado em criar quizzes pedagógicos. '
|
'content':
|
||||||
|
'És um assistente educativo especializado em criar quizzes pedagógicos. '
|
||||||
'Cria sempre perguntas claras, baseadas exclusivamente no contexto fornecido.',
|
'Cria sempre perguntas claras, baseadas exclusivamente no contexto fornecido.',
|
||||||
},
|
},
|
||||||
{
|
{'role': 'user', 'content': prompt},
|
||||||
'role': 'user',
|
|
||||||
'content': prompt,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
return await _callOllamaAPIWithMessages(messages);
|
return await _callOllamaAPIWithMessages(messages);
|
||||||
}
|
}
|
||||||
@@ -524,13 +519,39 @@ Usas formatação clara e organizada.''';
|
|||||||
final words = q.split(RegExp(r'\s+'));
|
final words = q.split(RegExp(r'\s+'));
|
||||||
if (words.length > 8) return false;
|
if (words.length > 8) return false;
|
||||||
const followUpStarters = [
|
const followUpStarters = [
|
||||||
'e ', 'e o', 'e a', 'e os', 'e as', 'mas ', 'então ',
|
'e ',
|
||||||
'explica', 'explique', 'explica melhor', 'melhor', 'mais detalhes',
|
'e o',
|
||||||
'podes', 'pode ', 'consegues', 'e se ', 'e quando',
|
'e a',
|
||||||
'dá um exemplo', 'da um exemplo', 'um exemplo', 'exemplo',
|
'e os',
|
||||||
'como assim', 'o que significa', 'porquê', 'porque isso',
|
'e as',
|
||||||
'e o ponto', 'e a regra', 'continua', 'continua',
|
'mas ',
|
||||||
'o que mais', 'mais algum', 'e depois', 'e agora',
|
'então ',
|
||||||
|
'explica',
|
||||||
|
'explique',
|
||||||
|
'explica melhor',
|
||||||
|
'melhor',
|
||||||
|
'mais detalhes',
|
||||||
|
'podes',
|
||||||
|
'pode ',
|
||||||
|
'consegues',
|
||||||
|
'e se ',
|
||||||
|
'e quando',
|
||||||
|
'dá um exemplo',
|
||||||
|
'da um exemplo',
|
||||||
|
'um exemplo',
|
||||||
|
'exemplo',
|
||||||
|
'como assim',
|
||||||
|
'o que significa',
|
||||||
|
'porquê',
|
||||||
|
'porque isso',
|
||||||
|
'e o ponto',
|
||||||
|
'e a regra',
|
||||||
|
'continua',
|
||||||
|
'continua',
|
||||||
|
'o que mais',
|
||||||
|
'mais algum',
|
||||||
|
'e depois',
|
||||||
|
'e agora',
|
||||||
];
|
];
|
||||||
return followUpStarters.any((s) => q.startsWith(s) || q == s.trim());
|
return followUpStarters.any((s) => q.startsWith(s) || q == s.trim());
|
||||||
}
|
}
|
||||||
@@ -549,11 +570,11 @@ Usas formatação clara e organizada.''';
|
|||||||
// PASSO 2 — ADICIONAR SYSTEM MESSAGE DO GOAT (SEMPRE PRIMEIRO)
|
// PASSO 2 — ADICIONAR SYSTEM MESSAGE DO GOAT (SEMPRE PRIMEIRO)
|
||||||
messages.add({
|
messages.add({
|
||||||
'role': 'system',
|
'role': 'system',
|
||||||
'content': '''Tu és "O GOAT", o Assistente IA oficial do Teach it.
|
'content': '''Tu és "Alt", o Assistente IA oficial do Teach it.
|
||||||
|
|
||||||
Nunca referes o nome do modelo.
|
Nunca referes o nome do modelo.
|
||||||
Nunca dizes que és Qwen ou OpenAI.
|
Nunca dizes que és Qwen ou OpenAI.
|
||||||
Respondes sempre como o GOAT.
|
Respondes sempre como Alt.
|
||||||
|
|
||||||
Tens personalidade confiante, motivadora e orgulhosa.
|
Tens personalidade confiante, motivadora e orgulhosa.
|
||||||
Usas formatação Markdown clara e organizada.
|
Usas formatação Markdown clara e organizada.
|
||||||
@@ -566,7 +587,9 @@ REGRAS CRÍTICAS SOBRE O CONTEXTO:
|
|||||||
});
|
});
|
||||||
|
|
||||||
// PASSO 3 — BUSCAR MEMÓRIA DA CONVERSA NA Cloud Firestore (máx 4 para poupar heap)
|
// PASSO 3 — BUSCAR MEMÓRIA DA CONVERSA NA Cloud Firestore (máx 4 para poupar heap)
|
||||||
final conversationHistory = await ChatMemoryService.getRecentMessages(limit: 4);
|
final conversationHistory = await ChatMemoryService.getRecentMessages(
|
||||||
|
limit: 4,
|
||||||
|
);
|
||||||
for (final msg in conversationHistory) {
|
for (final msg in conversationHistory) {
|
||||||
messages.add({
|
messages.add({
|
||||||
'role': msg['role'] as String,
|
'role': msg['role'] as String,
|
||||||
@@ -584,7 +607,9 @@ REGRAS CRÍTICAS SOBRE O CONTEXTO:
|
|||||||
String pdfContext;
|
String pdfContext;
|
||||||
if (_isFollowUp(userQuery) && _lastPdfContext.isNotEmpty) {
|
if (_isFollowUp(userQuery) && _lastPdfContext.isNotEmpty) {
|
||||||
pdfContext = _lastPdfContext;
|
pdfContext = _lastPdfContext;
|
||||||
Logger.info('Follow-up detected — reusing last PDF context (${pdfContext.length} chars)');
|
Logger.info(
|
||||||
|
'Follow-up detected — reusing last PDF context (${pdfContext.length} chars)',
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
pdfContext = await MaterialsRAGService.getRelevantChunks(
|
pdfContext = await MaterialsRAGService.getRelevantChunks(
|
||||||
userQuery: userQuery,
|
userQuery: userQuery,
|
||||||
@@ -594,25 +619,36 @@ REGRAS CRÍTICAS SOBRE O CONTEXTO:
|
|||||||
);
|
);
|
||||||
if (pdfContext.isNotEmpty) {
|
if (pdfContext.isNotEmpty) {
|
||||||
_lastPdfContext = pdfContext;
|
_lastPdfContext = pdfContext;
|
||||||
Logger.info('PDF context sent to model (${pdfContext.length} chars): ${pdfContext.length > 300 ? pdfContext.substring(0, 300) : pdfContext}');
|
Logger.info(
|
||||||
|
'PDF context sent to model (${pdfContext.length} chars): ${pdfContext.length > 300 ? pdfContext.substring(0, 300) : pdfContext}',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (pdfContext.isEmpty && selectedMaterialIds != null && selectedMaterialIds.isNotEmpty) {
|
if (pdfContext.isEmpty &&
|
||||||
|
selectedMaterialIds != null &&
|
||||||
|
selectedMaterialIds.isNotEmpty) {
|
||||||
// Contexto vazio com materiais seleccionados — retornar resposta local imediatamente
|
// Contexto vazio com materiais seleccionados — retornar resposta local imediatamente
|
||||||
const noContextReply =
|
const noContextReply =
|
||||||
'Neste momento não tenho acesso ao conteúdo do ficheiro selecionado. '
|
'Neste momento não tenho acesso ao conteúdo do ficheiro selecionado. '
|
||||||
'Tenta novamente ou faz uma pergunta geral — estou aqui para ajudar! 💪';
|
'Tenta novamente ou faz uma pergunta geral — estou aqui para ajudar! 💪';
|
||||||
await ChatMemoryService.saveMessage(role: 'user', content: userQuery);
|
await ChatMemoryService.saveMessage(role: 'user', content: userQuery);
|
||||||
await ChatMemoryService.saveMessage(role: 'assistant', content: noContextReply);
|
await ChatMemoryService.saveMessage(
|
||||||
|
role: 'assistant',
|
||||||
|
content: noContextReply,
|
||||||
|
);
|
||||||
return noContextReply;
|
return noContextReply;
|
||||||
}
|
}
|
||||||
if (pdfContext.isEmpty && (selectedMaterialIds == null || selectedMaterialIds.isEmpty)) {
|
if (pdfContext.isEmpty &&
|
||||||
|
(selectedMaterialIds == null || selectedMaterialIds.isEmpty)) {
|
||||||
// Sem material seleccionado — pedir ao utilizador para seleccionar um
|
// Sem material seleccionado — pedir ao utilizador para seleccionar um
|
||||||
const noMaterialReply =
|
const noMaterialReply =
|
||||||
'Para responder a perguntas sobre conteúdo, preciso que selecciones um material primeiro. '
|
'Para responder a perguntas sobre conteúdo, preciso que selecciones um material primeiro. '
|
||||||
'📚 Usa o botão de materiais para escolher um PDF e depois faz a tua pergunta!';
|
'📚 Usa o botão de materiais para escolher um PDF e depois faz a tua pergunta!';
|
||||||
await ChatMemoryService.saveMessage(role: 'user', content: userQuery);
|
await ChatMemoryService.saveMessage(role: 'user', content: userQuery);
|
||||||
await ChatMemoryService.saveMessage(role: 'assistant', content: noMaterialReply);
|
await ChatMemoryService.saveMessage(
|
||||||
|
role: 'assistant',
|
||||||
|
content: noMaterialReply,
|
||||||
|
);
|
||||||
return noMaterialReply;
|
return noMaterialReply;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -625,12 +661,11 @@ $pdfContext
|
|||||||
|
|
||||||
Pergunta: $userQuery'''
|
Pergunta: $userQuery'''
|
||||||
: userQuery;
|
: userQuery;
|
||||||
messages.add({
|
messages.add({'role': 'user', 'content': userContent});
|
||||||
'role': 'user',
|
|
||||||
'content': userContent,
|
|
||||||
});
|
|
||||||
|
|
||||||
Logger.info('USING RAG AI SERVICE - Built messages array with ${messages.length} messages');
|
Logger.info(
|
||||||
|
'USING RAG AI SERVICE - Built messages array with ${messages.length} messages',
|
||||||
|
);
|
||||||
|
|
||||||
// Save user message to Firestore
|
// Save user message to Firestore
|
||||||
await ChatMemoryService.saveMessage(role: 'user', content: userQuery);
|
await ChatMemoryService.saveMessage(role: 'user', content: userQuery);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_animate/flutter_animate.dart';
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
import '../../../../core/services/auth_service.dart';
|
import '../../../../core/services/auth_service.dart';
|
||||||
import '../../../../core/services/chat_memory_service.dart';
|
import '../../../../core/services/chat_memory_service.dart';
|
||||||
import '../../../../core/services/materials_rag_service.dart';
|
import '../../../../core/services/materials_rag_service.dart';
|
||||||
@@ -26,6 +27,7 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
|
|||||||
List<Map<String, dynamic>> _messages = [];
|
List<Map<String, dynamic>> _messages = [];
|
||||||
|
|
||||||
List<Map<String, String>> _availableMaterials = [];
|
List<Map<String, String>> _availableMaterials = [];
|
||||||
|
Map<String, String> _classNames = {}; // classId → name
|
||||||
Set<String> _selectedMaterialIds = {};
|
Set<String> _selectedMaterialIds = {};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -38,8 +40,28 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
|
|||||||
Future<void> _loadAvailableMaterials() async {
|
Future<void> _loadAvailableMaterials() async {
|
||||||
final materials =
|
final materials =
|
||||||
await MaterialsRAGService.getAvailableMaterialsForStudent();
|
await MaterialsRAGService.getAvailableMaterialsForStudent();
|
||||||
|
// Collect unique classIds that don't have a name yet
|
||||||
|
final classIds = materials
|
||||||
|
.map((m) => m['classId'])
|
||||||
|
.whereType<String>()
|
||||||
|
.toSet();
|
||||||
|
final namesMap = <String, String>{};
|
||||||
|
if (classIds.isNotEmpty) {
|
||||||
|
final docs = await Future.wait(
|
||||||
|
classIds.map(
|
||||||
|
(id) =>
|
||||||
|
FirebaseFirestore.instance.collection('classes').doc(id).get(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
for (final doc in docs.where((d) => d.exists)) {
|
||||||
|
namesMap[doc.id] = doc.data()?['name'] as String? ?? doc.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() => _availableMaterials = materials);
|
setState(() {
|
||||||
|
_availableMaterials = materials;
|
||||||
|
_classNames = namesMap;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -567,102 +589,332 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Group materials by classId; ungrouped go to '__geral__'
|
||||||
|
Map<String, List<Map<String, String>>> _groupMaterialsByClass() {
|
||||||
|
final groups = <String, List<Map<String, String>>>{};
|
||||||
|
for (final m in _availableMaterials) {
|
||||||
|
final cid = m['classId'];
|
||||||
|
final key = (cid != null && _classNames.containsKey(cid))
|
||||||
|
? cid
|
||||||
|
: '__geral__';
|
||||||
|
groups.putIfAbsent(key, () => []).add(m);
|
||||||
|
}
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
void _showMaterialsPicker() {
|
void _showMaterialsPicker() {
|
||||||
|
final groups = _groupMaterialsByClass();
|
||||||
|
final disciplineIds = groups.keys.where((k) => k != '__geral__').toList()
|
||||||
|
..sort((a, b) => (_classNames[a] ?? a).compareTo(_classNames[b] ?? b));
|
||||||
|
if (groups.containsKey('__geral__')) disciplineIds.add('__geral__');
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (dialogContext) {
|
builder: (dialogContext) {
|
||||||
final tempSelected = Set<String>.from(_selectedMaterialIds);
|
final tempSelected = Set<String>.from(_selectedMaterialIds);
|
||||||
|
// Disciplines start collapsed
|
||||||
|
final expanded = <String, bool>{
|
||||||
|
for (final k in disciplineIds) k: false,
|
||||||
|
};
|
||||||
|
final searchController = TextEditingController();
|
||||||
|
String searchQuery = '';
|
||||||
return StatefulBuilder(
|
return StatefulBuilder(
|
||||||
builder: (context, setDialogState) => AlertDialog(
|
builder: (context, setDialogState) {
|
||||||
shape: RoundedRectangleBorder(
|
final cs = Theme.of(context).colorScheme;
|
||||||
borderRadius: BorderRadius.circular(16),
|
// Filter groups by search query
|
||||||
),
|
final filteredDisciplineIds = disciplineIds.where((groupKey) {
|
||||||
title: const Text(
|
if (searchQuery.isEmpty) return true;
|
||||||
'Escolher Materiais',
|
final q = searchQuery.toLowerCase();
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
final label = groupKey == '__geral__'
|
||||||
),
|
? 'geral'
|
||||||
content: SizedBox(
|
: (_classNames[groupKey] ?? groupKey).toLowerCase();
|
||||||
width: double.maxFinite,
|
if (label.contains(q)) return true;
|
||||||
child: Column(
|
return (groups[groupKey] ?? []).any(
|
||||||
mainAxisSize: MainAxisSize.min,
|
(m) => (m['name'] ?? '').toLowerCase().contains(q),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
);
|
||||||
children: [
|
}).toList();
|
||||||
Text(
|
// Auto-expand disciplines that match by material name (not discipline name)
|
||||||
'Seleciona os materiais que o tutor deve analisar:',
|
for (final groupKey in filteredDisciplineIds) {
|
||||||
style: TextStyle(
|
if (searchQuery.isNotEmpty) {
|
||||||
fontSize: 13,
|
final q = searchQuery.toLowerCase();
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
final label = groupKey == '__geral__'
|
||||||
),
|
? 'geral'
|
||||||
),
|
: (_classNames[groupKey] ?? groupKey).toLowerCase();
|
||||||
const SizedBox(height: 12),
|
if (!label.contains(q)) expanded[groupKey] = true;
|
||||||
ConstrainedBox(
|
}
|
||||||
constraints: const BoxConstraints(maxHeight: 300),
|
}
|
||||||
child: ListView(
|
final viewInsets = MediaQuery.of(context).viewInsets;
|
||||||
shrinkWrap: true,
|
final screenHeight = MediaQuery.of(context).size.height;
|
||||||
children: _availableMaterials.map((material) {
|
// Leave room for keyboard + dialog chrome (~360px for title/search/actions/padding)
|
||||||
final id = material['id']!;
|
final listMaxHeight = (screenHeight - viewInsets.bottom - 360)
|
||||||
final name = material['name']!;
|
.clamp(60.0, 340.0);
|
||||||
final isChecked = tempSelected.contains(id);
|
return AlertDialog(
|
||||||
return CheckboxListTile(
|
shape: RoundedRectangleBorder(
|
||||||
value: isChecked,
|
borderRadius: BorderRadius.circular(16),
|
||||||
onChanged: (val) {
|
),
|
||||||
setDialogState(() {
|
title: const Text(
|
||||||
if (val == true) {
|
'Escolher Materiais',
|
||||||
tempSelected.add(id);
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
} else {
|
),
|
||||||
tempSelected.remove(id);
|
content: SizedBox(
|
||||||
}
|
width: double.maxFinite,
|
||||||
});
|
child: Column(
|
||||||
},
|
mainAxisSize: MainAxisSize.min,
|
||||||
title: Text(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
name,
|
children: [
|
||||||
style: const TextStyle(fontSize: 14),
|
// Search bar
|
||||||
maxLines: 2,
|
TextField(
|
||||||
overflow: TextOverflow.ellipsis,
|
controller: searchController,
|
||||||
|
onChanged: (v) =>
|
||||||
|
setDialogState(() => searchQuery = v.trim()),
|
||||||
|
style: TextStyle(fontSize: 13, color: cs.onSurface),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Pesquisar disciplina ou material…',
|
||||||
|
hintStyle: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: cs.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
prefixIcon: Icon(
|
||||||
|
Icons.search,
|
||||||
|
size: 18,
|
||||||
|
color: cs.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
suffixIcon: searchQuery.isNotEmpty
|
||||||
|
? IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
Icons.close,
|
||||||
|
size: 16,
|
||||||
|
color: cs.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
searchController.clear();
|
||||||
|
setDialogState(() => searchQuery = '');
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
isDense: true,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 10,
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: cs.outline.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
controlAffinity: ListTileControlAffinity.leading,
|
),
|
||||||
dense: true,
|
enabledBorder: OutlineInputBorder(
|
||||||
contentPadding: EdgeInsets.zero,
|
borderRadius: BorderRadius.circular(10),
|
||||||
);
|
borderSide: BorderSide(
|
||||||
}).toList(),
|
color: cs.outline.withValues(alpha: 0.3),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide: BorderSide(color: cs.primary),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(maxHeight: listMaxHeight),
|
||||||
|
child: filteredDisciplineIds.isEmpty
|
||||||
|
? Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 24),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
'Nenhum resultado para "$searchQuery"',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: cs.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
|
children: filteredDisciplineIds.map((groupKey) {
|
||||||
|
// When searching, filter materials too
|
||||||
|
final allMats = groups[groupKey]!;
|
||||||
|
final mats = searchQuery.isEmpty
|
||||||
|
? allMats
|
||||||
|
: allMats.where((m) {
|
||||||
|
final q = searchQuery.toLowerCase();
|
||||||
|
final label = groupKey == '__geral__'
|
||||||
|
? 'geral'
|
||||||
|
: (_classNames[groupKey] ?? '')
|
||||||
|
.toLowerCase();
|
||||||
|
return label.contains(q) ||
|
||||||
|
(m['name'] ?? '')
|
||||||
|
.toLowerCase()
|
||||||
|
.contains(q);
|
||||||
|
}).toList();
|
||||||
|
final label = groupKey == '__geral__'
|
||||||
|
? 'Geral'
|
||||||
|
: (_classNames[groupKey] ?? groupKey);
|
||||||
|
final isExpanded = expanded[groupKey] ?? false;
|
||||||
|
// Count how many in this group are selected
|
||||||
|
final selectedInGroup = mats
|
||||||
|
.where(
|
||||||
|
(m) => tempSelected.contains(m['id']),
|
||||||
|
)
|
||||||
|
.length;
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Discipline header row
|
||||||
|
InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
onTap: () => setDialogState(
|
||||||
|
() => expanded[groupKey] = !isExpanded,
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: cs.primary.withValues(
|
||||||
|
alpha: 0.07,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.folder_outlined,
|
||||||
|
size: 16,
|
||||||
|
color: cs.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: cs.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (selectedInGroup > 0)
|
||||||
|
Container(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(
|
||||||
|
horizontal: 6,
|
||||||
|
vertical: 2,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: cs.primary.withValues(
|
||||||
|
alpha: 0.15,
|
||||||
|
),
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'$selectedInGroup',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: cs.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Icon(
|
||||||
|
isExpanded
|
||||||
|
? Icons.expand_less
|
||||||
|
: Icons.expand_more,
|
||||||
|
size: 18,
|
||||||
|
color: cs.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Material items
|
||||||
|
if (isExpanded)
|
||||||
|
...mats.map((material) {
|
||||||
|
final id = material['id']!;
|
||||||
|
final name = material['name']!;
|
||||||
|
final cleanName = name
|
||||||
|
.replaceAll('.pdf', '')
|
||||||
|
.replaceAll('_', ' ');
|
||||||
|
final isChecked = tempSelected.contains(
|
||||||
|
id,
|
||||||
|
);
|
||||||
|
return CheckboxListTile(
|
||||||
|
value: isChecked,
|
||||||
|
onChanged: (val) {
|
||||||
|
setDialogState(() {
|
||||||
|
if (val == true) {
|
||||||
|
tempSelected.add(id);
|
||||||
|
} else {
|
||||||
|
tempSelected.remove(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
title: Text(
|
||||||
|
cleanName,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
controlAffinity:
|
||||||
|
ListTileControlAffinity.leading,
|
||||||
|
dense: true,
|
||||||
|
contentPadding: const EdgeInsets.only(
|
||||||
|
left: 16,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
setDialogState(() => tempSelected.clear());
|
||||||
|
setState(() {
|
||||||
|
_selectedMaterialIds.clear();
|
||||||
|
_messages.clear();
|
||||||
|
});
|
||||||
|
ChatMemoryService.clearHistory();
|
||||||
|
RAGAIService.clearLastContext();
|
||||||
|
Navigator.of(dialogContext).pop();
|
||||||
|
},
|
||||||
|
child: const Text('Limpar'),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_selectedMaterialIds = tempSelected;
|
||||||
|
_messages.clear();
|
||||||
|
});
|
||||||
|
ChatMemoryService.clearHistory();
|
||||||
|
RAGAIService.clearLastContext();
|
||||||
|
Navigator.of(dialogContext).pop();
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
child: const Text('Confirmar'),
|
||||||
),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
setDialogState(() => tempSelected.clear());
|
|
||||||
setState(() {
|
|
||||||
_selectedMaterialIds.clear();
|
|
||||||
_messages.clear();
|
|
||||||
});
|
|
||||||
ChatMemoryService.clearHistory();
|
|
||||||
RAGAIService.clearLastContext();
|
|
||||||
Navigator.of(dialogContext).pop();
|
|
||||||
},
|
|
||||||
child: const Text('Limpar'),
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () {
|
|
||||||
setState(() {
|
|
||||||
_selectedMaterialIds = tempSelected;
|
|
||||||
_messages.clear();
|
|
||||||
});
|
|
||||||
ChatMemoryService.clearHistory();
|
|
||||||
RAGAIService.clearLastContext();
|
|
||||||
Navigator.of(dialogContext).pop();
|
|
||||||
},
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
child: const Text('Confirmar'),
|
],
|
||||||
),
|
);
|
||||||
],
|
},
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user