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);
|
if (mounted) setState(() => _loadingTeacherQuizzes = false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final classSnaps = await Future.wait(
|
// Quizzes publicados para as turmas do aluno (array-contains-any, máx 10 por query)
|
||||||
classIds.map((id) => FirebaseFirestore.instance.collection('classes').doc(id).get()),
|
final classIdList = classIds.toList();
|
||||||
);
|
|
||||||
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)
|
|
||||||
final batches = <Future<QuerySnapshot>>[];
|
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
|
batches.add(FirebaseFirestore.instance
|
||||||
.collection('teacherQuizzes')
|
.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)
|
.orderBy('createdAt', descending: true)
|
||||||
.get());
|
.get());
|
||||||
}
|
}
|
||||||
final results = await Future.wait(batches);
|
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; });
|
if (mounted) setState(() { _teacherQuizzes = quizzes; _loadingTeacherQuizzes = false; });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger.error('Error loading teacher quizzes: $e');
|
Logger.error('Error loading teacher quizzes: $e');
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:math';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:cloud_firestore/cloud_firestore.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, String>> _materials = [];
|
||||||
List<Map<String, dynamic>> _history = [];
|
List<Map<String, dynamic>> _history = [];
|
||||||
|
List<Map<String, String>> _teacherClasses = [];
|
||||||
bool _loadingMaterials = true;
|
bool _loadingMaterials = true;
|
||||||
bool _loadingHistory = true;
|
bool _loadingHistory = true;
|
||||||
String? _generatingForId;
|
String? _generatingForId;
|
||||||
@@ -66,6 +68,7 @@ class _TeacherQuizPageState extends State<TeacherQuizPage>
|
|||||||
_tabController = TabController(length: 2, vsync: this);
|
_tabController = TabController(length: 2, vsync: this);
|
||||||
_loadMaterials();
|
_loadMaterials();
|
||||||
_loadHistory();
|
_loadHistory();
|
||||||
|
_loadTeacherClasses();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -74,6 +77,24 @@ class _TeacherQuizPageState extends State<TeacherQuizPage>
|
|||||||
super.dispose();
|
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 {
|
Future<void> _loadMaterials() async {
|
||||||
try {
|
try {
|
||||||
final uid = FirebaseAuth.instance.currentUser?.uid;
|
final uid = FirebaseAuth.instance.currentUser?.uid;
|
||||||
@@ -128,13 +149,14 @@ class _TeacherQuizPageState extends State<TeacherQuizPage>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final numQuestions = 5 + Random().nextInt(16); // 5..20
|
||||||
final prompt =
|
final prompt =
|
||||||
'Usa APENAS o seguinte contexto para criar um quiz. Não uses conhecimento externo.\n\n'
|
'Usa APENAS o seguinte contexto para criar um quiz. Não uses conhecimento externo.\n\n'
|
||||||
'$pdfContext\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'
|
'Responde SOMENTE com JSON válido, sem texto adicional, sem markdown, sem blocos de código.\n'
|
||||||
'Formato exacto:\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.';
|
'ans é o índice (0-3) da opção correcta.';
|
||||||
|
|
||||||
final raw = await RAGAIService.generateQuiz(prompt);
|
final raw = await RAGAIService.generateQuiz(prompt);
|
||||||
@@ -153,6 +175,7 @@ class _TeacherQuizPageState extends State<TeacherQuizPage>
|
|||||||
materialName: matName,
|
materialName: matName,
|
||||||
materialId: matId,
|
materialId: matId,
|
||||||
questions: questions,
|
questions: questions,
|
||||||
|
availableClasses: _teacherClasses,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -399,11 +422,13 @@ class _QuizEditorPage extends StatefulWidget {
|
|||||||
final String materialName;
|
final String materialName;
|
||||||
final String materialId;
|
final String materialId;
|
||||||
final List<_QuizQuestion> questions;
|
final List<_QuizQuestion> questions;
|
||||||
|
final List<Map<String, String>> availableClasses;
|
||||||
|
|
||||||
const _QuizEditorPage({
|
const _QuizEditorPage({
|
||||||
required this.materialName,
|
required this.materialName,
|
||||||
required this.materialId,
|
required this.materialId,
|
||||||
required this.questions,
|
required this.questions,
|
||||||
|
required this.availableClasses,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -412,6 +437,7 @@ class _QuizEditorPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _QuizEditorPageState extends State<_QuizEditorPage> {
|
class _QuizEditorPageState extends State<_QuizEditorPage> {
|
||||||
late List<_QuizQuestion> _questions;
|
late List<_QuizQuestion> _questions;
|
||||||
|
final Set<String> _selectedClassIds = {};
|
||||||
bool _saving = false;
|
bool _saving = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -426,6 +452,21 @@ class _QuizEditorPageState extends State<_QuizEditorPage> {
|
|||||||
explanation: q.explanation,
|
explanation: q.explanation,
|
||||||
))
|
))
|
||||||
.toList();
|
.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 {
|
Future<void> _publish() async {
|
||||||
@@ -439,6 +480,7 @@ class _QuizEditorPageState extends State<_QuizEditorPage> {
|
|||||||
'materialId': widget.materialId,
|
'materialId': widget.materialId,
|
||||||
'materialName': widget.materialName,
|
'materialName': widget.materialName,
|
||||||
'quizJson': jsonStr,
|
'quizJson': jsonStr,
|
||||||
|
'classIds': _selectedClassIds.toList(),
|
||||||
'createdAt': FieldValue.serverTimestamp(),
|
'createdAt': FieldValue.serverTimestamp(),
|
||||||
});
|
});
|
||||||
if (mounted) Navigator.of(context).pop(true);
|
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))),
|
child: Center(child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))),
|
||||||
)
|
)
|
||||||
: TextButton.icon(
|
: TextButton.icon(
|
||||||
onPressed: _publish,
|
onPressed: (widget.availableClasses.isEmpty || _selectedClassIds.isNotEmpty) ? _publish : null,
|
||||||
icon: const Icon(Icons.publish),
|
icon: const Icon(Icons.publish),
|
||||||
label: const Text('Publicar'),
|
label: const Text('Publicar'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: ListView.separated(
|
body: ListView.builder(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
|
||||||
itemCount: _questions.length,
|
itemCount: _questions.length + 2, // +1 turmas header +1 add button
|
||||||
separatorBuilder: (_, __) => const SizedBox(height: 16),
|
itemBuilder: (context, i) {
|
||||||
itemBuilder: (context, i) => _QuestionEditor(
|
if (i == 0) return _buildClassSelector(cs);
|
||||||
index: i,
|
if (i == _questions.length + 1) {
|
||||||
question: _questions[i],
|
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,
|
cs: cs,
|
||||||
onChanged: () => setState(() {}),
|
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 _QuizQuestion question;
|
||||||
final ColorScheme cs;
|
final ColorScheme cs;
|
||||||
final VoidCallback onChanged;
|
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
|
@override
|
||||||
State<_QuestionEditor> createState() => _QuestionEditorState();
|
State<_QuestionEditor> createState() => _QuestionEditorState();
|
||||||
@@ -580,6 +698,15 @@ class _QuestionEditorState extends State<_QuestionEditor> {
|
|||||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: cs.onSurface),
|
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),
|
Icon(_expanded ? Icons.expand_less : Icons.expand_more, color: cs.onSurfaceVariant),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user