Histórico a funcionar
This commit is contained in:
@@ -327,7 +327,8 @@ class _TutorChatPageState extends State<TutorChatPage>
|
||||
|
||||
void _addWelcomeMessage() {
|
||||
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!
|
||||
|
||||
|
||||
@@ -36,7 +36,8 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
|
||||
}
|
||||
|
||||
Future<void> _loadAvailableMaterials() async {
|
||||
final materials = await MaterialsRAGService.getAvailableMaterialsForStudent();
|
||||
final materials =
|
||||
await MaterialsRAGService.getAvailableMaterialsForStudent();
|
||||
if (mounted) {
|
||||
setState(() => _availableMaterials = materials);
|
||||
}
|
||||
@@ -86,7 +87,7 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'AI Study Assistant',
|
||||
'Assistente de Estudo AI',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -374,13 +375,21 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: _selectedMaterialIds.isEmpty
|
||||
? Theme.of(context).colorScheme.outline.withOpacity(0.15)
|
||||
: Theme.of(context).colorScheme.primary.withOpacity(0.12),
|
||||
? Theme.of(
|
||||
context,
|
||||
).colorScheme.outline.withOpacity(0.15)
|
||||
: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withOpacity(0.12),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: _selectedMaterialIds.isEmpty
|
||||
? Theme.of(context).colorScheme.outline.withOpacity(0.4)
|
||||
: Theme.of(context).colorScheme.primary.withOpacity(0.5),
|
||||
? Theme.of(
|
||||
context,
|
||||
).colorScheme.outline.withOpacity(0.4)
|
||||
: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
@@ -390,7 +399,9 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
|
||||
Icons.attach_file,
|
||||
size: 14,
|
||||
color: _selectedMaterialIds.isEmpty
|
||||
? Theme.of(context).colorScheme.onSurfaceVariant
|
||||
? Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
@@ -401,8 +412,12 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: _selectedMaterialIds.isEmpty
|
||||
? Theme.of(context).colorScheme.onSurfaceVariant
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
? Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant
|
||||
: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
@@ -410,10 +425,10 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_selectedMaterialIds.isNotEmpty) ...
|
||||
_selectedMaterialIds.map((id) {
|
||||
final name = _availableMaterials
|
||||
.firstWhere(
|
||||
if (_selectedMaterialIds.isNotEmpty)
|
||||
..._selectedMaterialIds.map((id) {
|
||||
final name =
|
||||
_availableMaterials.firstWhere(
|
||||
(m) => m['id'] == id,
|
||||
orElse: () => {'id': id, 'name': id},
|
||||
)['name'] ??
|
||||
@@ -460,34 +475,34 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
|
||||
// Text field
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _messageController,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 14,
|
||||
),
|
||||
hintText: 'Faz a tua pergunta!',
|
||||
hintStyle: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 16,
|
||||
controller: _messageController,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 14,
|
||||
),
|
||||
hintText: 'Faz a tua pergunta!',
|
||||
hintStyle: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
onSubmitted: (_) => _handleSendMessage(),
|
||||
textInputAction: TextInputAction.send,
|
||||
onChanged: (value) {
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
onSubmitted: (_) => _handleSendMessage(),
|
||||
textInputAction: TextInputAction.send,
|
||||
onChanged: (value) {
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// Send button
|
||||
// Send button
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Container(
|
||||
@@ -508,7 +523,9 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
|
||||
: null,
|
||||
color: _messageController.text.isNotEmpty
|
||||
? null
|
||||
: Theme.of(context).colorScheme.outline.withOpacity(0.3),
|
||||
: Theme.of(
|
||||
context,
|
||||
).colorScheme.outline.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
boxShadow: _messageController.text.isNotEmpty
|
||||
? [
|
||||
@@ -523,7 +540,8 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
|
||||
: null,
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: _messageController.text.isNotEmpty && !_isLoading
|
||||
onPressed:
|
||||
_messageController.text.isNotEmpty && !_isLoading
|
||||
? _handleSendMessage
|
||||
: null,
|
||||
icon: _isLoading
|
||||
|
||||
@@ -32,8 +32,12 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
|
||||
// disciplina seleccionada no tab "Gerar Quiz" (null = vista de disciplinas)
|
||||
String? _selectedMaterialDisciplineId;
|
||||
// disciplina seleccionada no tab "Histórico" (null = vista de disciplinas)
|
||||
String? _selectedHistoryDisciplineId;
|
||||
// disciplina seleccionada no tab "Do Professor" (null = vista de disciplinas)
|
||||
String? _selectedDisciplineId;
|
||||
// classId → name para o histórico
|
||||
Map<String, String> _historyClassNames = {};
|
||||
|
||||
// generating state
|
||||
String? _generatingForId;
|
||||
@@ -261,9 +265,92 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
.limit(30)
|
||||
.get();
|
||||
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)
|
||||
setState(() {
|
||||
_history = list;
|
||||
_historyClassNames = histClassNames;
|
||||
_loadingHistory = false;
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -651,7 +738,7 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
Future<void> _previewPDF(Map<String, String> mat, String name) async {
|
||||
final matId = mat['id']!;
|
||||
final matName = name.replaceAll('.pdf', '').replaceAll('_', ' ');
|
||||
|
||||
|
||||
// Mostrar loading
|
||||
showDialog(
|
||||
context: context,
|
||||
@@ -666,25 +753,30 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
try {
|
||||
// Obter o texto completo do PDF usando o método existente
|
||||
final teacherId = mat['teacherId'];
|
||||
if (teacherId == null) {
|
||||
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;
|
||||
}
|
||||
|
||||
final fullText = await MaterialsRAGService.getFullPDFText(matName, teacherId);
|
||||
|
||||
|
||||
final fullText = await MaterialsRAGService.getFullPDFText(
|
||||
matName,
|
||||
teacherId,
|
||||
);
|
||||
|
||||
Navigator.of(context).pop(); // Fechar loading
|
||||
|
||||
|
||||
if (fullText.isEmpty) {
|
||||
_showSnack('Não foi possível carregar o conteúdo do PDF.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Mostrar o conteúdo em um diálogo scrollável melhorado
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
@@ -711,14 +803,19 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
height: 4,
|
||||
margin: const EdgeInsets.symmetric(vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.4),
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant.withOpacity(0.4),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
// Header melhorado
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 16,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
@@ -728,7 +825,9 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(24),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -740,9 +839,15 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
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),
|
||||
Expanded(
|
||||
@@ -777,24 +882,37 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
),
|
||||
child: IconButton(
|
||||
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),
|
||||
|
||||
|
||||
// Stats bar
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.white.withOpacity(0.2)),
|
||||
border: Border.all(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
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),
|
||||
Expanded(
|
||||
child: Text(
|
||||
@@ -807,7 +925,10 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
@@ -827,32 +948,45 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
// Content area melhorado
|
||||
Expanded(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.1),
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.outline.withOpacity(0.1),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Content header
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 16,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.text_fields,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 18),
|
||||
Icon(
|
||||
Icons.text_fields,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Conteúdo do Material',
|
||||
@@ -864,9 +998,14 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
@@ -881,7 +1020,7 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
// Text content
|
||||
Expanded(
|
||||
child: Container(
|
||||
@@ -904,25 +1043,31 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
// Footer melhorado
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(24)),
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
bottom: Radius.circular(24),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(Icons.info_outline,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 16),
|
||||
child: Icon(
|
||||
Icons.info_outline,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
@@ -933,7 +1078,9 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
'Texto extraído automaticamente do PDF',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
@@ -942,15 +1089,19 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
'Formatação otimizada para melhor legibilidade',
|
||||
style: TextStyle(
|
||||
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,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
size: 16),
|
||||
Icon(
|
||||
Icons.keyboard_arrow_up,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -958,7 +1109,6 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
} catch (e) {
|
||||
Logger.error('Error previewing PDF: $e');
|
||||
Navigator.of(context).pop(); // Fechar loading
|
||||
@@ -1089,6 +1239,10 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
setState(() => _selectedMaterialDisciplineId = null);
|
||||
return;
|
||||
}
|
||||
if (_selectedHistoryDisciplineId != null) {
|
||||
setState(() => _selectedHistoryDisciplineId = null);
|
||||
return;
|
||||
}
|
||||
if (_selectedDisciplineId != null) {
|
||||
setState(() => _selectedDisciplineId = null);
|
||||
return;
|
||||
@@ -1107,6 +1261,8 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
onPressed: () {
|
||||
if (_selectedMaterialDisciplineId != null) {
|
||||
setState(() => _selectedMaterialDisciplineId = null);
|
||||
} else if (_selectedHistoryDisciplineId != null) {
|
||||
setState(() => _selectedHistoryDisciplineId = null);
|
||||
} else if (_selectedDisciplineId != null) {
|
||||
setState(() => _selectedDisciplineId = null);
|
||||
} else {
|
||||
@@ -1349,16 +1505,13 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
ColorScheme cs,
|
||||
) {
|
||||
final cleanName = name.replaceAll('.pdf', '').replaceAll('_', ' ');
|
||||
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surface,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: cs.outline.withOpacity(0.08),
|
||||
width: 1,
|
||||
),
|
||||
border: Border.all(color: cs.outline.withOpacity(0.08), width: 1),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: cs.shadow.withOpacity(0.04),
|
||||
@@ -1376,7 +1529,9 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
onTap: isGenerating ? null : () => _showMaterialOptions(mat, name, cs),
|
||||
onTap: isGenerating
|
||||
? null
|
||||
: () => _showMaterialOptions(mat, name, cs),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
@@ -1403,11 +1558,7 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.picture_as_pdf,
|
||||
color: cs.secondary,
|
||||
size: 26,
|
||||
),
|
||||
Icon(Icons.picture_as_pdf, color: cs.secondary, size: 26),
|
||||
if (!isGenerating)
|
||||
Positioned(
|
||||
bottom: 2,
|
||||
@@ -1429,9 +1580,9 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
const SizedBox(width: 16),
|
||||
|
||||
|
||||
// Content
|
||||
Expanded(
|
||||
child: Column(
|
||||
@@ -1450,7 +1601,10 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.primaryContainer.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
@@ -1458,11 +1612,7 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.touch_app,
|
||||
size: 12,
|
||||
color: cs.primary,
|
||||
),
|
||||
Icon(Icons.touch_app, size: 12, color: cs.primary),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
isGenerating ? 'A gerar...' : 'Toca para opções',
|
||||
@@ -1478,18 +1628,18 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
// Action indicator
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: isGenerating
|
||||
color: isGenerating
|
||||
? cs.primary.withOpacity(0.1)
|
||||
: cs.surfaceVariant.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: isGenerating
|
||||
color: isGenerating
|
||||
? cs.primary.withOpacity(0.2)
|
||||
: 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('_', ' ');
|
||||
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
@@ -1552,7 +1706,7 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
// Header melhorado com gradient
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 24),
|
||||
@@ -1567,9 +1721,7 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: cs.outline.withOpacity(0.1),
|
||||
),
|
||||
border: Border.all(color: cs.outline.withOpacity(0.1)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -1620,9 +1772,9 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
const SizedBox(width: 16),
|
||||
|
||||
|
||||
// Title section
|
||||
Expanded(
|
||||
child: Column(
|
||||
@@ -1641,7 +1793,10 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.primaryContainer.withOpacity(0.6),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
@@ -1661,19 +1816,15 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
|
||||
// Instructions
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.lightbulb_outline,
|
||||
color: cs.tertiary,
|
||||
size: 20,
|
||||
),
|
||||
Icon(Icons.lightbulb_outline, color: cs.tertiary, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
@@ -1688,9 +1839,9 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
|
||||
// Options melhoradas
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
@@ -1714,9 +1865,9 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
_previewPDF(mat, name);
|
||||
},
|
||||
),
|
||||
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
|
||||
// Generate Quiz option com design premium
|
||||
_buildPremiumOptionTile(
|
||||
icon: Icons.quiz_rounded,
|
||||
@@ -1738,7 +1889,7 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
),
|
||||
@@ -1765,10 +1916,7 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
decoration: BoxDecoration(
|
||||
gradient: gradient,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: color.withOpacity(0.2),
|
||||
width: 1.5,
|
||||
),
|
||||
border: Border.all(color: color.withOpacity(0.2), width: 1.5),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: color.withOpacity(0.1),
|
||||
@@ -1786,10 +1934,7 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: color.withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
border: Border.all(color: color.withOpacity(0.3), width: 1),
|
||||
),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
@@ -1815,9 +1960,9 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
const SizedBox(width: 16),
|
||||
|
||||
|
||||
// Text content
|
||||
Expanded(
|
||||
child: Column(
|
||||
@@ -1852,7 +1997,7 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
// Arrow indicator
|
||||
Container(
|
||||
width: 32,
|
||||
@@ -1861,11 +2006,7 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.chevron_right,
|
||||
color: color,
|
||||
size: 18,
|
||||
),
|
||||
child: Icon(Icons.chevron_right, color: color, size: 18),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1918,10 +2059,7 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: cs.onSurfaceVariant,
|
||||
),
|
||||
style: TextStyle(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) {
|
||||
if (_loadingHistory) {
|
||||
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(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: _history.length,
|
||||
itemCount: realDisciplineIds.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 12),
|
||||
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')
|
||||
.replaceAll('.pdf', '')
|
||||
.replaceAll('_', ' ');
|
||||
|
||||
Reference in New Issue
Block a user