mais correções e implementações nos quizzes
This commit is contained in:
@@ -66,30 +66,24 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
if (mounted) setState(() => _loadingTeacherQuizzes = false);
|
||||
return;
|
||||
}
|
||||
final classSnaps = await Future.wait(
|
||||
classIds.map((id) => FirebaseFirestore.instance.collection('classes').doc(id).get()),
|
||||
);
|
||||
final teacherIds = classSnaps
|
||||
.where((d) => d.exists)
|
||||
.map((d) => d.data()?['teacherId'] as String?)
|
||||
.whereType<String>()
|
||||
.toSet()
|
||||
.toList();
|
||||
if (teacherIds.isEmpty) {
|
||||
if (mounted) setState(() => _loadingTeacherQuizzes = false);
|
||||
return;
|
||||
}
|
||||
// Quizzes publicados por esses professores (máx 10 teacherIds por query)
|
||||
// Quizzes publicados para as turmas do aluno (array-contains-any, máx 10 por query)
|
||||
final classIdList = classIds.toList();
|
||||
final batches = <Future<QuerySnapshot>>[];
|
||||
for (int i = 0; i < teacherIds.length; i += 10) {
|
||||
for (int i = 0; i < classIdList.length; i += 10) {
|
||||
batches.add(FirebaseFirestore.instance
|
||||
.collection('teacherQuizzes')
|
||||
.where('teacherId', whereIn: teacherIds.sublist(i, (i + 10).clamp(0, teacherIds.length)))
|
||||
.where('classIds', arrayContainsAny: classIdList.sublist(i, (i + 10).clamp(0, classIdList.length)))
|
||||
.orderBy('createdAt', descending: true)
|
||||
.get());
|
||||
}
|
||||
final results = await Future.wait(batches);
|
||||
final quizzes = results.expand((s) => s.docs).map((d) => {'id': d.id, ...d.data() as Map<String, dynamic>}).toList();
|
||||
// deduplicar por id (pode aparecer em múltiplos batches)
|
||||
final seen = <String>{};
|
||||
final quizzes = results
|
||||
.expand((s) => s.docs)
|
||||
.where((d) => seen.add(d.id))
|
||||
.map((d) => {'id': d.id, ...d.data() as Map<String, dynamic>})
|
||||
.toList();
|
||||
if (mounted) setState(() { _teacherQuizzes = quizzes; _loadingTeacherQuizzes = false; });
|
||||
} catch (e) {
|
||||
Logger.error('Error loading teacher quizzes: $e');
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
@@ -56,6 +57,7 @@ class _TeacherQuizPageState extends State<TeacherQuizPage>
|
||||
|
||||
List<Map<String, String>> _materials = [];
|
||||
List<Map<String, dynamic>> _history = [];
|
||||
List<Map<String, String>> _teacherClasses = [];
|
||||
bool _loadingMaterials = true;
|
||||
bool _loadingHistory = true;
|
||||
String? _generatingForId;
|
||||
@@ -66,6 +68,7 @@ class _TeacherQuizPageState extends State<TeacherQuizPage>
|
||||
_tabController = TabController(length: 2, vsync: this);
|
||||
_loadMaterials();
|
||||
_loadHistory();
|
||||
_loadTeacherClasses();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -74,6 +77,24 @@ class _TeacherQuizPageState extends State<TeacherQuizPage>
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadTeacherClasses() async {
|
||||
try {
|
||||
final uid = FirebaseAuth.instance.currentUser?.uid;
|
||||
if (uid == null) return;
|
||||
final snap = await FirebaseFirestore.instance
|
||||
.collection('classes')
|
||||
.where('teacherId', isEqualTo: uid)
|
||||
.orderBy('createdAt', descending: true)
|
||||
.get();
|
||||
final classes = snap.docs
|
||||
.map((d) => {'id': d.id, 'name': d.data()['name'] as String? ?? d.id})
|
||||
.toList();
|
||||
if (mounted) setState(() => _teacherClasses = classes);
|
||||
} catch (e) {
|
||||
Logger.error('Error loading teacher classes: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadMaterials() async {
|
||||
try {
|
||||
final uid = FirebaseAuth.instance.currentUser?.uid;
|
||||
@@ -128,13 +149,14 @@ class _TeacherQuizPageState extends State<TeacherQuizPage>
|
||||
return;
|
||||
}
|
||||
|
||||
final numQuestions = 5 + Random().nextInt(16); // 5..20
|
||||
final prompt =
|
||||
'Usa APENAS o seguinte contexto para criar um quiz. Não uses conhecimento externo.\n\n'
|
||||
'$pdfContext\n\n'
|
||||
'Cria um quiz com 5 perguntas de escolha múltipla sobre o conteúdo acima.\n'
|
||||
'Cria um quiz com EXACTAMENTE $numQuestions perguntas de escolha múltipla sobre o conteúdo acima. Não repitas perguntas.\n'
|
||||
'Responde SOMENTE com JSON válido, sem texto adicional, sem markdown, sem blocos de código.\n'
|
||||
'Formato exacto:\n'
|
||||
'[{"q":"Pergunta aqui","opts":["A) opção","B) opção","C) opção","D) opção"],"ans":0,"exp":"Explicação breve da resposta correcta"},...]\n'
|
||||
'[{"q":"Pergunta aqui","opts":["A) opção","B) opção","C) opção","D) opção"],"ans":0,"exp":"Explicação breve da resposta correcta"},...}]\n'
|
||||
'ans é o índice (0-3) da opção correcta.';
|
||||
|
||||
final raw = await RAGAIService.generateQuiz(prompt);
|
||||
@@ -153,6 +175,7 @@ class _TeacherQuizPageState extends State<TeacherQuizPage>
|
||||
materialName: matName,
|
||||
materialId: matId,
|
||||
questions: questions,
|
||||
availableClasses: _teacherClasses,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -399,11 +422,13 @@ class _QuizEditorPage extends StatefulWidget {
|
||||
final String materialName;
|
||||
final String materialId;
|
||||
final List<_QuizQuestion> questions;
|
||||
final List<Map<String, String>> availableClasses;
|
||||
|
||||
const _QuizEditorPage({
|
||||
required this.materialName,
|
||||
required this.materialId,
|
||||
required this.questions,
|
||||
required this.availableClasses,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -412,6 +437,7 @@ class _QuizEditorPage extends StatefulWidget {
|
||||
|
||||
class _QuizEditorPageState extends State<_QuizEditorPage> {
|
||||
late List<_QuizQuestion> _questions;
|
||||
final Set<String> _selectedClassIds = {};
|
||||
bool _saving = false;
|
||||
|
||||
@override
|
||||
@@ -426,6 +452,21 @@ class _QuizEditorPageState extends State<_QuizEditorPage> {
|
||||
explanation: q.explanation,
|
||||
))
|
||||
.toList();
|
||||
// selecionar todas as turmas por defeito
|
||||
for (final c in widget.availableClasses) {
|
||||
if (c['id'] != null) _selectedClassIds.add(c['id']!);
|
||||
}
|
||||
}
|
||||
|
||||
void _addBlankQuestion() {
|
||||
setState(() {
|
||||
_questions.add(_QuizQuestion(
|
||||
question: '',
|
||||
options: ['A) ', 'B) ', 'C) ', 'D) '],
|
||||
correctIndex: 0,
|
||||
explanation: '',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _publish() async {
|
||||
@@ -439,6 +480,7 @@ class _QuizEditorPageState extends State<_QuizEditorPage> {
|
||||
'materialId': widget.materialId,
|
||||
'materialName': widget.materialName,
|
||||
'quizJson': jsonStr,
|
||||
'classIds': _selectedClassIds.toList(),
|
||||
'createdAt': FieldValue.serverTimestamp(),
|
||||
});
|
||||
if (mounted) Navigator.of(context).pop(true);
|
||||
@@ -474,22 +516,97 @@ class _QuizEditorPageState extends State<_QuizEditorPage> {
|
||||
child: Center(child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))),
|
||||
)
|
||||
: TextButton.icon(
|
||||
onPressed: _publish,
|
||||
onPressed: (widget.availableClasses.isEmpty || _selectedClassIds.isNotEmpty) ? _publish : null,
|
||||
icon: const Icon(Icons.publish),
|
||||
label: const Text('Publicar'),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ListView.separated(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: _questions.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 16),
|
||||
itemBuilder: (context, i) => _QuestionEditor(
|
||||
index: i,
|
||||
question: _questions[i],
|
||||
cs: cs,
|
||||
onChanged: () => setState(() {}),
|
||||
),
|
||||
body: ListView.builder(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
|
||||
itemCount: _questions.length + 2, // +1 turmas header +1 add button
|
||||
itemBuilder: (context, i) {
|
||||
if (i == 0) return _buildClassSelector(cs);
|
||||
if (i == _questions.length + 1) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: _addBlankQuestion,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Adicionar Pergunta'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
final qIdx = i - 1;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: _QuestionEditor(
|
||||
index: qIdx,
|
||||
question: _questions[qIdx],
|
||||
cs: cs,
|
||||
onChanged: () => setState(() {}),
|
||||
onDelete: _questions.length > 1
|
||||
? () => setState(() => _questions.removeAt(qIdx))
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildClassSelector(ColorScheme cs) {
|
||||
if (widget.availableClasses.isEmpty) return const SizedBox(height: 0);
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: cs.outline.withValues(alpha: 0.2)),
|
||||
boxShadow: [BoxShadow(color: cs.shadow.withValues(alpha: 0.05), blurRadius: 8, offset: const Offset(0, 2))],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.groups, color: cs.primary, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text('Turmas com acesso',
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: cs.onSurface)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: widget.availableClasses.map((c) {
|
||||
final id = c['id']!;
|
||||
final name = c['name'] ?? id;
|
||||
final selected = _selectedClassIds.contains(id);
|
||||
return FilterChip(
|
||||
label: Text(name, style: TextStyle(fontSize: 13, color: selected ? cs.onPrimary : cs.onSurface)),
|
||||
selected: selected,
|
||||
onSelected: (v) => setState(() => v ? _selectedClassIds.add(id) : _selectedClassIds.remove(id)),
|
||||
selectedColor: cs.primary,
|
||||
checkmarkColor: cs.onPrimary,
|
||||
backgroundColor: cs.surfaceContainerHighest.withValues(alpha: 0.5),
|
||||
side: BorderSide(color: selected ? cs.primary : cs.outline.withValues(alpha: 0.3)),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
if (_selectedClassIds.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text('Seleciona pelo menos uma turma.',
|
||||
style: TextStyle(fontSize: 12, color: cs.error)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -502,7 +619,8 @@ class _QuestionEditor extends StatefulWidget {
|
||||
final _QuizQuestion question;
|
||||
final ColorScheme cs;
|
||||
final VoidCallback onChanged;
|
||||
const _QuestionEditor({required this.index, required this.question, required this.cs, required this.onChanged});
|
||||
final VoidCallback? onDelete;
|
||||
const _QuestionEditor({required this.index, required this.question, required this.cs, required this.onChanged, this.onDelete});
|
||||
|
||||
@override
|
||||
State<_QuestionEditor> createState() => _QuestionEditorState();
|
||||
@@ -580,6 +698,15 @@ class _QuestionEditorState extends State<_QuestionEditor> {
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: cs.onSurface),
|
||||
),
|
||||
),
|
||||
if (widget.onDelete != null)
|
||||
IconButton(
|
||||
icon: Icon(Icons.delete_outline, color: cs.error, size: 18),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
onPressed: widget.onDelete,
|
||||
tooltip: 'Remover',
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Icon(_expanded ? Icons.expand_less : Icons.expand_more, color: cs.onSurfaceVariant),
|
||||
],
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user