Histórico a funcionar

This commit is contained in:
2026-05-17 23:24:27 +01:00
parent 058bbaaea2
commit 4a5209b239
3 changed files with 447 additions and 150 deletions

View File

@@ -327,7 +327,8 @@ class _TutorChatPageState extends State<TutorChatPage>
void _addWelcomeMessage() { void _addWelcomeMessage() {
final welcomeMessage = { final welcomeMessage = {
'content': '''**Olá! Sou o GOAT, o teu Assistente IA oficial do Teach it.** 🐐 'content':
'''**Olá! Sou a Alt, o teu Assistente IA oficial do Teach it.**
Estou aqui para te ajudar a aprender de forma confiante e motivadora! Estou aqui para te ajudar a aprender de forma confiante e motivadora!

View File

@@ -36,7 +36,8 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
} }
Future<void> _loadAvailableMaterials() async { Future<void> _loadAvailableMaterials() async {
final materials = await MaterialsRAGService.getAvailableMaterialsForStudent(); final materials =
await MaterialsRAGService.getAvailableMaterialsForStudent();
if (mounted) { if (mounted) {
setState(() => _availableMaterials = materials); setState(() => _availableMaterials = materials);
} }
@@ -86,7 +87,7 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'AI Study Assistant', 'Assistente de Estudo AI',
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@@ -374,13 +375,21 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: _selectedMaterialIds.isEmpty color: _selectedMaterialIds.isEmpty
? Theme.of(context).colorScheme.outline.withOpacity(0.15) ? Theme.of(
: Theme.of(context).colorScheme.primary.withOpacity(0.12), context,
).colorScheme.outline.withOpacity(0.15)
: Theme.of(
context,
).colorScheme.primary.withOpacity(0.12),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
border: Border.all( border: Border.all(
color: _selectedMaterialIds.isEmpty color: _selectedMaterialIds.isEmpty
? Theme.of(context).colorScheme.outline.withOpacity(0.4) ? Theme.of(
: Theme.of(context).colorScheme.primary.withOpacity(0.5), context,
).colorScheme.outline.withOpacity(0.4)
: Theme.of(
context,
).colorScheme.primary.withOpacity(0.5),
), ),
), ),
child: Row( child: Row(
@@ -390,7 +399,9 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
Icons.attach_file, Icons.attach_file,
size: 14, size: 14,
color: _selectedMaterialIds.isEmpty color: _selectedMaterialIds.isEmpty
? Theme.of(context).colorScheme.onSurfaceVariant ? Theme.of(
context,
).colorScheme.onSurfaceVariant
: Theme.of(context).colorScheme.primary, : Theme.of(context).colorScheme.primary,
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
@@ -401,8 +412,12 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: _selectedMaterialIds.isEmpty color: _selectedMaterialIds.isEmpty
? Theme.of(context).colorScheme.onSurfaceVariant ? Theme.of(
: Theme.of(context).colorScheme.primary, context,
).colorScheme.onSurfaceVariant
: Theme.of(
context,
).colorScheme.primary,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), ),
@@ -410,10 +425,10 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
), ),
), ),
), ),
if (_selectedMaterialIds.isNotEmpty) ... if (_selectedMaterialIds.isNotEmpty)
_selectedMaterialIds.map((id) { ..._selectedMaterialIds.map((id) {
final name = _availableMaterials final name =
.firstWhere( _availableMaterials.firstWhere(
(m) => m['id'] == id, (m) => m['id'] == id,
orElse: () => {'id': id, 'name': id}, orElse: () => {'id': id, 'name': id},
)['name'] ?? )['name'] ??
@@ -460,34 +475,34 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
// Text field // Text field
Expanded( Expanded(
child: TextField( child: TextField(
controller: _messageController, controller: _messageController,
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
decoration: InputDecoration( decoration: InputDecoration(
border: InputBorder.none, border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
horizontal: 16, horizontal: 16,
vertical: 14, vertical: 14,
), ),
hintText: 'Faz a tua pergunta!', hintText: 'Faz a tua pergunta!',
hintStyle: TextStyle( hintStyle: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant, color: Theme.of(context).colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
fontSize: 16, fontSize: 16,
),
),
onSubmitted: (_) => _handleSendMessage(),
textInputAction: TextInputAction.send,
onChanged: (value) {
setState(() {});
},
), ),
), ),
onSubmitted: (_) => _handleSendMessage(),
textInputAction: TextInputAction.send,
onChanged: (value) {
setState(() {});
},
),
),
// Send button // Send button
Padding( Padding(
padding: const EdgeInsets.only(right: 8.0), padding: const EdgeInsets.only(right: 8.0),
child: Container( child: Container(
@@ -508,7 +523,9 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
: null, : null,
color: _messageController.text.isNotEmpty color: _messageController.text.isNotEmpty
? null ? null
: Theme.of(context).colorScheme.outline.withOpacity(0.3), : Theme.of(
context,
).colorScheme.outline.withOpacity(0.3),
borderRadius: BorderRadius.circular(22), borderRadius: BorderRadius.circular(22),
boxShadow: _messageController.text.isNotEmpty boxShadow: _messageController.text.isNotEmpty
? [ ? [
@@ -523,7 +540,8 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
: null, : null,
), ),
child: IconButton( child: IconButton(
onPressed: _messageController.text.isNotEmpty && !_isLoading onPressed:
_messageController.text.isNotEmpty && !_isLoading
? _handleSendMessage ? _handleSendMessage
: null, : null,
icon: _isLoading icon: _isLoading

View File

@@ -32,8 +32,12 @@ class _QuizListPageState extends State<QuizListPage>
// disciplina seleccionada no tab "Gerar Quiz" (null = vista de disciplinas) // disciplina seleccionada no tab "Gerar Quiz" (null = vista de disciplinas)
String? _selectedMaterialDisciplineId; String? _selectedMaterialDisciplineId;
// disciplina seleccionada no tab "Histórico" (null = vista de disciplinas)
String? _selectedHistoryDisciplineId;
// disciplina seleccionada no tab "Do Professor" (null = vista de disciplinas) // disciplina seleccionada no tab "Do Professor" (null = vista de disciplinas)
String? _selectedDisciplineId; String? _selectedDisciplineId;
// classId → name para o histórico
Map<String, String> _historyClassNames = {};
// generating state // generating state
String? _generatingForId; String? _generatingForId;
@@ -261,9 +265,92 @@ class _QuizListPageState extends State<QuizListPage>
.limit(30) .limit(30)
.get(); .get();
final list = snap.docs.map((d) => {'id': d.id, ...d.data()}).toList(); final list = snap.docs.map((d) => {'id': d.id, ...d.data()}).toList();
// Resolver nomes de disciplinas para o histórico
// 1) Usar _materialClassNames (já carregados) para quizzes gerados pelo aluno
// 2) Para teacher quizzes, buscar classId via teacherQuizzes collection
final histClassNames = Map<String, String>.from(_materialClassNames);
// Enriquecer items de histórico com classId se em falta
final teacherQuizIds = list
.where((e) => e['teacherQuizId'] != null && e['classId'] == null)
.map((e) => e['teacherQuizId'] as String)
.toSet();
if (teacherQuizIds.isNotEmpty) {
final tqDocs = await Future.wait(
teacherQuizIds.map(
(id) => FirebaseFirestore.instance
.collection('teacherQuizzes')
.doc(id)
.get(),
),
);
for (final doc in tqDocs.where((d) => d.exists)) {
final cids = doc.data()?['classIds'] as List<dynamic>?;
if (cids != null && cids.isNotEmpty) {
final cid = cids.first as String;
// Associar teacherQuizId → classId
for (final item in list) {
if (item['teacherQuizId'] == doc.id && item['classId'] == null) {
item['classId'] = cid;
}
}
// Buscar nome da classe se ainda não temos
if (!histClassNames.containsKey(cid)) {
final classDoc = await FirebaseFirestore.instance
.collection('classes')
.doc(cid)
.get();
if (classDoc.exists) {
histClassNames[cid] =
classDoc.data()?['name'] as String? ?? cid;
}
}
}
}
}
// Para quizzes gerados pelo aluno sem classId: inferir pelo materialId
final materialIds = list
.where((e) => e['materialId'] != null && e['classId'] == null)
.map((e) => e['materialId'] as String)
.toSet();
if (materialIds.isNotEmpty) {
// Procurar nos materiais já carregados
for (final mat in _materials) {
if (materialIds.contains(mat['id']) && mat['classId'] != null) {
for (final item in list) {
if (item['materialId'] == mat['id'] && item['classId'] == null) {
item['classId'] = mat['classId'];
}
}
}
}
// Buscar nomes de classes extra
final extraCids = list
.map((e) => e['classId'] as String?)
.whereType<String>()
.where((id) => !histClassNames.containsKey(id))
.toSet();
if (extraCids.isNotEmpty) {
final docs = await Future.wait(
extraCids.map(
(id) => FirebaseFirestore.instance
.collection('classes')
.doc(id)
.get(),
),
);
for (final doc in docs.where((d) => d.exists)) {
histClassNames[doc.id] = doc.data()?['name'] as String? ?? doc.id;
}
}
}
if (mounted) if (mounted)
setState(() { setState(() {
_history = list; _history = list;
_historyClassNames = histClassNames;
_loadingHistory = false; _loadingHistory = false;
}); });
} catch (e) { } catch (e) {
@@ -651,7 +738,7 @@ class _QuizListPageState extends State<QuizListPage>
Future<void> _previewPDF(Map<String, String> mat, String name) async { Future<void> _previewPDF(Map<String, String> mat, String name) async {
final matId = mat['id']!; final matId = mat['id']!;
final matName = name.replaceAll('.pdf', '').replaceAll('_', ' '); final matName = name.replaceAll('.pdf', '').replaceAll('_', ' ');
// Mostrar loading // Mostrar loading
showDialog( showDialog(
context: context, context: context,
@@ -666,25 +753,30 @@ class _QuizListPageState extends State<QuizListPage>
), ),
), ),
); );
try { try {
// Obter o texto completo do PDF usando o método existente // Obter o texto completo do PDF usando o método existente
final teacherId = mat['teacherId']; final teacherId = mat['teacherId'];
if (teacherId == null) { if (teacherId == null) {
Navigator.of(context).pop(); Navigator.of(context).pop();
_showSnack('Erro: não foi possível identificar o professor do material.'); _showSnack(
'Erro: não foi possível identificar o professor do material.',
);
return; return;
} }
final fullText = await MaterialsRAGService.getFullPDFText(matName, teacherId); final fullText = await MaterialsRAGService.getFullPDFText(
matName,
teacherId,
);
Navigator.of(context).pop(); // Fechar loading Navigator.of(context).pop(); // Fechar loading
if (fullText.isEmpty) { if (fullText.isEmpty) {
_showSnack('Não foi possível carregar o conteúdo do PDF.'); _showSnack('Não foi possível carregar o conteúdo do PDF.');
return; return;
} }
// Mostrar o conteúdo em um diálogo scrollável melhorado // Mostrar o conteúdo em um diálogo scrollável melhorado
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
@@ -711,14 +803,19 @@ class _QuizListPageState extends State<QuizListPage>
height: 4, height: 4,
margin: const EdgeInsets.symmetric(vertical: 12), margin: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.4), color: Theme.of(
context,
).colorScheme.onSurfaceVariant.withOpacity(0.4),
borderRadius: BorderRadius.circular(2), borderRadius: BorderRadius.circular(2),
), ),
), ),
// Header melhorado // Header melhorado
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 16,
),
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
colors: [ colors: [
@@ -728,7 +825,9 @@ class _QuizListPageState extends State<QuizListPage>
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
), ),
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), borderRadius: const BorderRadius.vertical(
top: Radius.circular(24),
),
), ),
child: Column( child: Column(
children: [ children: [
@@ -740,9 +839,15 @@ class _QuizListPageState extends State<QuizListPage>
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2), color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white.withOpacity(0.3)), border: Border.all(
color: Colors.white.withOpacity(0.3),
),
),
child: Icon(
Icons.picture_as_pdf,
color: Colors.white,
size: 24,
), ),
child: Icon(Icons.picture_as_pdf, color: Colors.white, size: 24),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded( Expanded(
@@ -777,24 +882,37 @@ class _QuizListPageState extends State<QuizListPage>
), ),
child: IconButton( child: IconButton(
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close, color: Colors.white, size: 20), icon: const Icon(
Icons.close,
color: Colors.white,
size: 20,
),
), ),
), ),
], ],
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Stats bar // Stats bar
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15), color: Colors.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white.withOpacity(0.2)), border: Border.all(
color: Colors.white.withOpacity(0.2),
),
), ),
child: Row( child: Row(
children: [ children: [
Icon(Icons.description, color: Colors.white.withOpacity(0.9), size: 18), Icon(
Icons.description,
color: Colors.white.withOpacity(0.9),
size: 18,
),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: Text( child: Text(
@@ -807,7 +925,10 @@ class _QuizListPageState extends State<QuizListPage>
), ),
), ),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2), color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
@@ -827,32 +948,45 @@ class _QuizListPageState extends State<QuizListPage>
], ],
), ),
), ),
// Content area melhorado // Content area melhorado
Expanded( Expanded(
child: Container( child: Container(
margin: const EdgeInsets.all(20), margin: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest, color: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
border: Border.all( border: Border.all(
color: Theme.of(context).colorScheme.outline.withOpacity(0.1), color: Theme.of(
context,
).colorScheme.outline.withOpacity(0.1),
), ),
), ),
child: Column( child: Column(
children: [ children: [
// Content header // Content header
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest, color: Theme.of(
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), context,
).colorScheme.surfaceContainerHighest,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(16),
),
), ),
child: Row( child: Row(
children: [ children: [
Icon(Icons.text_fields, Icon(
color: Theme.of(context).colorScheme.primary, Icons.text_fields,
size: 18), color: Theme.of(context).colorScheme.primary,
size: 18,
),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
'Conteúdo do Material', 'Conteúdo do Material',
@@ -864,9 +998,14 @@ class _QuizListPageState extends State<QuizListPage>
), ),
const Spacer(), const Spacer(),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary.withOpacity(0.1), color: Theme.of(
context,
).colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: Text( child: Text(
@@ -881,7 +1020,7 @@ class _QuizListPageState extends State<QuizListPage>
], ],
), ),
), ),
// Text content // Text content
Expanded( Expanded(
child: Container( child: Container(
@@ -904,25 +1043,31 @@ class _QuizListPageState extends State<QuizListPage>
), ),
), ),
), ),
// Footer melhorado // Footer melhorado
Container( Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant, color: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(24)), borderRadius: const BorderRadius.vertical(
bottom: Radius.circular(24),
),
), ),
child: Row( child: Row(
children: [ children: [
Container( Container(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary.withOpacity(0.1), color: Theme.of(
context,
).colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: Icon(Icons.info_outline, child: Icon(
color: Theme.of(context).colorScheme.primary, Icons.info_outline,
size: 16), color: Theme.of(context).colorScheme.primary,
size: 16,
),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
@@ -933,7 +1078,9 @@ class _QuizListPageState extends State<QuizListPage>
'Texto extraído automaticamente do PDF', 'Texto extraído automaticamente do PDF',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant, color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), ),
@@ -942,15 +1089,19 @@ class _QuizListPageState extends State<QuizListPage>
'Formatação otimizada para melhor legibilidade', 'Formatação otimizada para melhor legibilidade',
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.8), color: Theme.of(
context,
).colorScheme.onSurfaceVariant.withOpacity(0.8),
), ),
), ),
], ],
), ),
), ),
Icon(Icons.keyboard_arrow_up, Icon(
color: Theme.of(context).colorScheme.onSurfaceVariant, Icons.keyboard_arrow_up,
size: 16), color: Theme.of(context).colorScheme.onSurfaceVariant,
size: 16,
),
], ],
), ),
), ),
@@ -958,7 +1109,6 @@ class _QuizListPageState extends State<QuizListPage>
), ),
), ),
); );
} catch (e) { } catch (e) {
Logger.error('Error previewing PDF: $e'); Logger.error('Error previewing PDF: $e');
Navigator.of(context).pop(); // Fechar loading Navigator.of(context).pop(); // Fechar loading
@@ -1089,6 +1239,10 @@ class _QuizListPageState extends State<QuizListPage>
setState(() => _selectedMaterialDisciplineId = null); setState(() => _selectedMaterialDisciplineId = null);
return; return;
} }
if (_selectedHistoryDisciplineId != null) {
setState(() => _selectedHistoryDisciplineId = null);
return;
}
if (_selectedDisciplineId != null) { if (_selectedDisciplineId != null) {
setState(() => _selectedDisciplineId = null); setState(() => _selectedDisciplineId = null);
return; return;
@@ -1107,6 +1261,8 @@ class _QuizListPageState extends State<QuizListPage>
onPressed: () { onPressed: () {
if (_selectedMaterialDisciplineId != null) { if (_selectedMaterialDisciplineId != null) {
setState(() => _selectedMaterialDisciplineId = null); setState(() => _selectedMaterialDisciplineId = null);
} else if (_selectedHistoryDisciplineId != null) {
setState(() => _selectedHistoryDisciplineId = null);
} else if (_selectedDisciplineId != null) { } else if (_selectedDisciplineId != null) {
setState(() => _selectedDisciplineId = null); setState(() => _selectedDisciplineId = null);
} else { } else {
@@ -1349,16 +1505,13 @@ class _QuizListPageState extends State<QuizListPage>
ColorScheme cs, ColorScheme cs,
) { ) {
final cleanName = name.replaceAll('.pdf', '').replaceAll('_', ' '); final cleanName = name.replaceAll('.pdf', '').replaceAll('_', ' ');
return Container( return Container(
margin: const EdgeInsets.only(bottom: 4), margin: const EdgeInsets.only(bottom: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: cs.surface, color: cs.surface,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
border: Border.all( border: Border.all(color: cs.outline.withOpacity(0.08), width: 1),
color: cs.outline.withOpacity(0.08),
width: 1,
),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: cs.shadow.withOpacity(0.04), color: cs.shadow.withOpacity(0.04),
@@ -1376,7 +1529,9 @@ class _QuizListPageState extends State<QuizListPage>
color: Colors.transparent, color: Colors.transparent,
child: InkWell( child: InkWell(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
onTap: isGenerating ? null : () => _showMaterialOptions(mat, name, cs), onTap: isGenerating
? null
: () => _showMaterialOptions(mat, name, cs),
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Row( child: Row(
@@ -1403,11 +1558,7 @@ class _QuizListPageState extends State<QuizListPage>
child: Stack( child: Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
Icon( Icon(Icons.picture_as_pdf, color: cs.secondary, size: 26),
Icons.picture_as_pdf,
color: cs.secondary,
size: 26,
),
if (!isGenerating) if (!isGenerating)
Positioned( Positioned(
bottom: 2, bottom: 2,
@@ -1429,9 +1580,9 @@ class _QuizListPageState extends State<QuizListPage>
], ],
), ),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
// Content // Content
Expanded( Expanded(
child: Column( child: Column(
@@ -1450,7 +1601,10 @@ class _QuizListPageState extends State<QuizListPage>
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: cs.primaryContainer.withOpacity(0.5), color: cs.primaryContainer.withOpacity(0.5),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
@@ -1458,11 +1612,7 @@ class _QuizListPageState extends State<QuizListPage>
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon( Icon(Icons.touch_app, size: 12, color: cs.primary),
Icons.touch_app,
size: 12,
color: cs.primary,
),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
isGenerating ? 'A gerar...' : 'Toca para opções', isGenerating ? 'A gerar...' : 'Toca para opções',
@@ -1478,18 +1628,18 @@ class _QuizListPageState extends State<QuizListPage>
], ],
), ),
), ),
// Action indicator // Action indicator
Container( Container(
width: 40, width: 40,
height: 40, height: 40,
decoration: BoxDecoration( decoration: BoxDecoration(
color: isGenerating color: isGenerating
? cs.primary.withOpacity(0.1) ? cs.primary.withOpacity(0.1)
: cs.surfaceVariant.withOpacity(0.5), : cs.surfaceVariant.withOpacity(0.5),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all( border: Border.all(
color: isGenerating color: isGenerating
? cs.primary.withOpacity(0.2) ? cs.primary.withOpacity(0.2)
: cs.outline.withOpacity(0.1), : cs.outline.withOpacity(0.1),
), ),
@@ -1519,9 +1669,13 @@ class _QuizListPageState extends State<QuizListPage>
); );
} }
void _showMaterialOptions(Map<String, String> mat, String name, ColorScheme cs) { void _showMaterialOptions(
Map<String, String> mat,
String name,
ColorScheme cs,
) {
final cleanName = name.replaceAll('.pdf', '').replaceAll('_', ' '); final cleanName = name.replaceAll('.pdf', '').replaceAll('_', ' ');
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
@@ -1552,7 +1706,7 @@ class _QuizListPageState extends State<QuizListPage>
borderRadius: BorderRadius.circular(3), borderRadius: BorderRadius.circular(3),
), ),
), ),
// Header melhorado com gradient // Header melhorado com gradient
Container( Container(
margin: const EdgeInsets.symmetric(horizontal: 24), margin: const EdgeInsets.symmetric(horizontal: 24),
@@ -1567,9 +1721,7 @@ class _QuizListPageState extends State<QuizListPage>
end: Alignment.bottomRight, end: Alignment.bottomRight,
), ),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
border: Border.all( border: Border.all(color: cs.outline.withOpacity(0.1)),
color: cs.outline.withOpacity(0.1),
),
), ),
child: Row( child: Row(
children: [ children: [
@@ -1620,9 +1772,9 @@ class _QuizListPageState extends State<QuizListPage>
], ],
), ),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
// Title section // Title section
Expanded( Expanded(
child: Column( child: Column(
@@ -1641,7 +1793,10 @@ class _QuizListPageState extends State<QuizListPage>
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: cs.primaryContainer.withOpacity(0.6), color: cs.primaryContainer.withOpacity(0.6),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
@@ -1661,19 +1816,15 @@ class _QuizListPageState extends State<QuizListPage>
], ],
), ),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
// Instructions // Instructions
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 24), padding: const EdgeInsets.symmetric(horizontal: 24),
child: Row( child: Row(
children: [ children: [
Icon( Icon(Icons.lightbulb_outline, color: cs.tertiary, size: 20),
Icons.lightbulb_outline,
color: cs.tertiary,
size: 20,
),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: Text( child: Text(
@@ -1688,9 +1839,9 @@ class _QuizListPageState extends State<QuizListPage>
], ],
), ),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
// Options melhoradas // Options melhoradas
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 24), padding: const EdgeInsets.symmetric(horizontal: 24),
@@ -1714,9 +1865,9 @@ class _QuizListPageState extends State<QuizListPage>
_previewPDF(mat, name); _previewPDF(mat, name);
}, },
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Generate Quiz option com design premium // Generate Quiz option com design premium
_buildPremiumOptionTile( _buildPremiumOptionTile(
icon: Icons.quiz_rounded, icon: Icons.quiz_rounded,
@@ -1738,7 +1889,7 @@ class _QuizListPageState extends State<QuizListPage>
], ],
), ),
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
], ],
), ),
@@ -1765,10 +1916,7 @@ class _QuizListPageState extends State<QuizListPage>
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: gradient, gradient: gradient,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
border: Border.all( border: Border.all(color: color.withOpacity(0.2), width: 1.5),
color: color.withOpacity(0.2),
width: 1.5,
),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: color.withOpacity(0.1), color: color.withOpacity(0.1),
@@ -1786,10 +1934,7 @@ class _QuizListPageState extends State<QuizListPage>
decoration: BoxDecoration( decoration: BoxDecoration(
color: color.withOpacity(0.15), color: color.withOpacity(0.15),
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
border: Border.all( border: Border.all(color: color.withOpacity(0.3), width: 1),
color: color.withOpacity(0.3),
width: 1,
),
), ),
child: Stack( child: Stack(
alignment: Alignment.center, alignment: Alignment.center,
@@ -1815,9 +1960,9 @@ class _QuizListPageState extends State<QuizListPage>
], ],
), ),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
// Text content // Text content
Expanded( Expanded(
child: Column( child: Column(
@@ -1852,7 +1997,7 @@ class _QuizListPageState extends State<QuizListPage>
], ],
), ),
), ),
// Arrow indicator // Arrow indicator
Container( Container(
width: 32, width: 32,
@@ -1861,11 +2006,7 @@ class _QuizListPageState extends State<QuizListPage>
color: color.withOpacity(0.1), color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
child: Icon( child: Icon(Icons.chevron_right, color: color, size: 18),
Icons.chevron_right,
color: color,
size: 18,
),
), ),
], ],
), ),
@@ -1918,10 +2059,7 @@ class _QuizListPageState extends State<QuizListPage>
const SizedBox(height: 2), const SizedBox(height: 2),
Text( Text(
subtitle, subtitle,
style: TextStyle( style: TextStyle(fontSize: 12, color: cs.onSurfaceVariant),
fontSize: 12,
color: cs.onSurfaceVariant,
),
), ),
], ],
), ),
@@ -1933,6 +2071,18 @@ class _QuizListPageState extends State<QuizListPage>
); );
} }
Map<String, List<Map<String, dynamic>>> _groupHistoryByDiscipline() {
final groups = <String, List<Map<String, dynamic>>>{};
for (final item in _history) {
final cid = item['classId'] as String?;
final groupId = (cid != null && _historyClassNames.containsKey(cid))
? cid
: '__geral__';
groups.putIfAbsent(groupId, () => []).add(item);
}
return groups;
}
Widget _buildHistoryTab(ColorScheme cs) { Widget _buildHistoryTab(ColorScheme cs) {
if (_loadingHistory) { if (_loadingHistory) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
@@ -1960,12 +2110,140 @@ class _QuizListPageState extends State<QuizListPage>
), ),
); );
} }
final groups = _groupHistoryByDiscipline();
final realDisciplineIds = groups.keys
.where((k) => k != '__geral__' && _historyClassNames.containsKey(k))
.toList();
// Sem disciplinas reais ou só 1 → lista plana
if (realDisciplineIds.length <= 1) {
return _buildHistoryList(cs, _history);
}
// Vista de quizzes de uma disciplina
if (_selectedHistoryDisciplineId != null) {
final items = groups[_selectedHistoryDisciplineId] ?? [];
final dName =
_historyClassNames[_selectedHistoryDisciplineId] ??
_selectedHistoryDisciplineId!;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(8, 8, 16, 0),
child: Row(
children: [
IconButton(
icon: Icon(Icons.arrow_back, color: cs.onSurface),
onPressed: () =>
setState(() => _selectedHistoryDisciplineId = null),
),
const SizedBox(width: 4),
Expanded(
child: Text(
dName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: cs.onSurface,
),
),
),
Text(
'${items.length} quiz${items.length != 1 ? 'zes' : ''}',
style: TextStyle(fontSize: 13, color: cs.onSurfaceVariant),
),
],
),
),
const Divider(height: 1),
Expanded(child: _buildHistoryList(cs, items)),
],
);
}
// Vista de disciplinas
return ListView.separated( return ListView.separated(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
itemCount: _history.length, itemCount: realDisciplineIds.length,
separatorBuilder: (_, __) => const SizedBox(height: 12), separatorBuilder: (_, __) => const SizedBox(height: 12),
itemBuilder: (context, i) { itemBuilder: (context, i) {
final item = _history[i]; final dId = realDisciplineIds[i];
final dName = _historyClassNames[dId] ?? dId;
final count = groups[dId]!.length;
return InkWell(
borderRadius: BorderRadius.circular(16),
onTap: () => setState(() => _selectedHistoryDisciplineId = dId),
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: cs.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: cs.outline.withValues(alpha: 0.15)),
boxShadow: [
BoxShadow(
color: cs.shadow.withValues(alpha: 0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: cs.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(Icons.history_edu, color: cs.primary, size: 26),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
dName,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: cs.onSurface,
),
),
const SizedBox(height: 4),
Text(
'$count quiz${count != 1 ? 'zes' : ''}',
style: TextStyle(
fontSize: 13,
color: cs.onSurfaceVariant,
),
),
],
),
),
Icon(Icons.chevron_right, color: cs.onSurfaceVariant),
],
),
),
);
},
);
}
Widget _buildHistoryList(ColorScheme cs, List<Map<String, dynamic>> items) {
return ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: items.length,
separatorBuilder: (_, __) => const SizedBox(height: 12),
itemBuilder: (context, i) {
final item = items[i];
final matName = (item['materialName'] as String? ?? 'Material') final matName = (item['materialName'] as String? ?? 'Material')
.replaceAll('.pdf', '') .replaceAll('.pdf', '')
.replaceAll('_', ' '); .replaceAll('_', ' ');