From 3463b1f6cca6ec4696e8f555c7da32026f7b0846 Mon Sep 17 00:00:00 2001 From: 240403 <240403@epvc.pt> Date: Sat, 16 May 2026 20:19:23 +0100 Subject: [PATCH] QUIZZES FEITOS POHA --- lib/core/routing/app_router.dart | 7 + lib/core/services/rag_ai_service.dart | 16 + .../widgets/quick_access_widget.dart | 10 +- .../presentation/pages/quiz_list_page.dart | 755 +++++++++++++++++- 4 files changed, 749 insertions(+), 39 deletions(-) diff --git a/lib/core/routing/app_router.dart b/lib/core/routing/app_router.dart index 2e8e32b..4002e4d 100644 --- a/lib/core/routing/app_router.dart +++ b/lib/core/routing/app_router.dart @@ -139,6 +139,13 @@ class AppRouter { name: 'aiTutor', builder: (context, state) => const TutorChatPageSimple(), ), + + // Quiz List Route (independent — student access) + GoRoute( + path: quizList, + name: 'quizList', + builder: (context, state) => const QuizListPage(), + ), ], // Let splash screen handle all navigation logic diff --git a/lib/core/services/rag_ai_service.dart b/lib/core/services/rag_ai_service.dart index 3920e07..ce16c80 100644 --- a/lib/core/services/rag_ai_service.dart +++ b/lib/core/services/rag_ai_service.dart @@ -480,6 +480,22 @@ Usas formatação clara e organizada.'''; } } + /// Gerar quiz a partir de um prompt com contexto PDF embutido — sem histórico de conversa + static Future generateQuiz(String prompt) async { + final messages = >[ + { + 'role': 'system', + 'content': 'És um assistente educativo especializado em criar quizzes pedagógicos. ' + 'Cria sempre perguntas claras, baseadas exclusivamente no contexto fornecido.', + }, + { + 'role': 'user', + 'content': prompt, + }, + ]; + return await _callOllamaAPIWithMessages(messages); + } + /// Test the service with a simple query static Future testService() async { try { diff --git a/lib/features/dashboard/presentation/widgets/quick_access_widget.dart b/lib/features/dashboard/presentation/widgets/quick_access_widget.dart index e2c4eb7..8f5c33f 100644 --- a/lib/features/dashboard/presentation/widgets/quick_access_widget.dart +++ b/lib/features/dashboard/presentation/widgets/quick_access_widget.dart @@ -180,15 +180,7 @@ class QuickAccessWidget extends StatelessWidget { color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(16), - onTap: () { - print('DEBUG: AI Tutor card clicked!'); - try { - context.go('/ai-tutor'); - print('DEBUG: Navigation to AI Tutor successful'); - } catch (e) { - print('DEBUG: Navigation error: $e'); - } - }, + onTap: () => context.go('/quiz'), child: Padding( padding: const EdgeInsets.all(14), child: Column( diff --git a/lib/features/quiz/presentation/pages/quiz_list_page.dart b/lib/features/quiz/presentation/pages/quiz_list_page.dart index 7957fe2..f9fdb70 100644 --- a/lib/features/quiz/presentation/pages/quiz_list_page.dart +++ b/lib/features/quiz/presentation/pages/quiz_list_page.dart @@ -1,51 +1,746 @@ +import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import '../../../../core/services/materials_rag_service.dart'; +import '../../../../core/services/rag_ai_service.dart'; +import '../../../../core/utils/logger.dart'; -class QuizListPage extends StatelessWidget { +class QuizListPage extends StatefulWidget { const QuizListPage({super.key}); + @override + State createState() => _QuizListPageState(); +} + +class _QuizListPageState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + + List> _materials = []; + List> _history = []; + bool _loadingMaterials = true; + bool _loadingHistory = true; + + // generating state + String? _generatingForId; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + _loadMaterials(); + _loadHistory(); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + Future _loadMaterials() async { + final mats = await MaterialsRAGService.getAvailableMaterialsForStudent(); + if (mounted) setState(() { _materials = mats; _loadingMaterials = false; }); + } + + Future _loadHistory() async { + try { + final uid = FirebaseAuth.instance.currentUser?.uid; + if (uid == null) { if (mounted) setState(() => _loadingHistory = false); return; } + final snap = await FirebaseFirestore.instance + .collection('quizHistory') + .doc(uid) + .collection('quizzes') + .orderBy('createdAt', descending: true) + .limit(30) + .get(); + final list = snap.docs.map((d) => {'id': d.id, ...d.data()}).toList(); + if (mounted) setState(() { _history = list; _loadingHistory = false; }); + } catch (e) { + Logger.error('Error loading quiz history: $e'); + if (mounted) setState(() => _loadingHistory = false); + } + } + + Future _generateQuiz(Map material) async { + setState(() => _generatingForId = material['id']); + try { + final matId = material['id']!; + final matName = material['name'] ?? 'Material'; + + // Buscar contexto do PDF + final context = await MaterialsRAGService.getRelevantChunks( + userQuery: 'conteúdo geral resumo tópicos principais', + selectedMaterialIds: [matId], + ); + + if (context.isEmpty) { + if (mounted) _showSnack('Não foi possível aceder ao conteúdo do PDF.'); + return; + } + + // Gerar quiz via Ollama em formato JSON estruturado + final prompt = + 'Usa APENAS o seguinte contexto para criar um quiz. Não uses conhecimento externo.\n\n' + '$context\n\n' + 'Cria um quiz com 5 perguntas de escolha múltipla sobre o conteúdo acima.\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' + 'ans é o índice (0-3) da opção correcta.'; + + final raw = await RAGAIService.generateQuiz(prompt); + final questions = _parseQuizJson(raw); + + if (questions.isEmpty) { + if (mounted) _showSnack('Não foi possível gerar o quiz. Tenta novamente.'); + return; + } + + // Guardar no histórico (guardar JSON raw para poder rever) + final uid = FirebaseAuth.instance.currentUser?.uid; + if (uid != null) { + await FirebaseFirestore.instance + .collection('quizHistory') + .doc(uid) + .collection('quizzes') + .add({ + 'materialId': matId, + 'materialName': matName, + 'quizJson': raw, + 'createdAt': FieldValue.serverTimestamp(), + }); + await _loadHistory(); + } + + if (mounted) _showInteractiveQuiz(matName, questions); + } catch (e) { + Logger.error('Error generating quiz: $e'); + if (mounted) _showSnack('Erro ao gerar quiz. Tenta novamente.'); + } finally { + if (mounted) setState(() => _generatingForId = null); + } + } + + /// Parse do JSON gerado pelo modelo — tolerante a erros + List<_QuizQuestion> _parseQuizJson(String raw) { + try { + // Extrair o primeiro array JSON encontrado na string + final start = raw.indexOf('['); + final end = raw.lastIndexOf(']'); + if (start == -1 || end == -1 || end <= start) return []; + final jsonStr = raw.substring(start, end + 1); + final decoded = _jsonDecode(jsonStr); + if (decoded is! List) return []; + return decoded.map<_QuizQuestion?>((e) { + if (e is! Map) return null; + final q = e['q'] as String?; + final opts = (e['opts'] as List?)?.cast(); + final ans = e['ans'] as int?; + final exp = e['exp'] as String? ?? ''; + if (q == null || opts == null || ans == null) return null; + if (opts.length < 2 || ans < 0 || ans >= opts.length) return null; + return _QuizQuestion(question: q, options: opts, correctIndex: ans, explanation: exp); + }).whereType<_QuizQuestion>().toList(); + } catch (e) { + Logger.error('Quiz JSON parse error: $e'); + return []; + } + } + + dynamic _jsonDecode(String s) => jsonDecode(s); + + void _showInteractiveQuiz(String title, List<_QuizQuestion> questions) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + isDismissible: false, + builder: (_) => _InteractiveQuizSheet(title: title, questions: questions), + ); + } + + void _showQuizFromHistory(String title, String rawJson) { + final questions = _parseQuizJson(rawJson); + if (questions.isEmpty) { + _showSnack('Não foi possível carregar este quiz.'); + return; + } + _showInteractiveQuiz(title, questions); + } + + void _showSnack(String msg) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(msg), behavior: SnackBarBehavior.floating), + ); + } + @override Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; return PopScope( canPop: false, - onPopInvoked: (didPop) { + onPopInvokedWithResult: (didPop, _) { if (didPop) return; - // Navigate back to dashboard instead of exiting app context.go('/student-dashboard'); }, child: Scaffold( - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: cs.surface, appBar: AppBar( - title: const Text('Quizzes'), - backgroundColor: Theme.of(context).colorScheme.surface, - foregroundColor: Theme.of(context).colorScheme.onSurface, + title: const Text('Quiz'), + backgroundColor: cs.surface, + foregroundColor: cs.onSurface, elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => context.go('/student-dashboard'), + ), + bottom: TabBar( + controller: _tabController, + labelColor: cs.primary, + unselectedLabelColor: cs.onSurfaceVariant, + indicatorColor: cs.primary, + tabs: const [ + Tab(text: 'Gerar Quiz'), + Tab(text: 'Histórico'), + ], + ), ), - body: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - Theme.of(context).colorScheme.background, - Theme.of(context).colorScheme.primary.withOpacity(0.1), - Theme.of(context).colorScheme.secondary.withOpacity(0.05), - Theme.of(context).colorScheme.background, - ], - ), - ), - child: Center( - child: Text( - 'Quiz List - Coming Soon', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ), + body: TabBarView( + controller: _tabController, + children: [ + _buildMaterialsTab(cs), + _buildHistoryTab(cs), + ], ), ), ); } + + Widget _buildMaterialsTab(ColorScheme cs) { + if (_loadingMaterials) { + return const Center(child: CircularProgressIndicator()); + } + if (_materials.isEmpty) { + return Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.folder_open, size: 64, color: cs.onSurfaceVariant.withOpacity(0.4)), + const SizedBox(height: 16), + Text( + 'Nenhum material disponível.', + textAlign: TextAlign.center, + style: TextStyle(color: cs.onSurfaceVariant, fontSize: 16), + ), + const SizedBox(height: 8), + Text( + 'Inscreve-te numa turma para aceder aos PDFs do professor.', + textAlign: TextAlign.center, + style: TextStyle(color: cs.onSurfaceVariant.withOpacity(0.7), fontSize: 13), + ), + ], + ), + ), + ); + } + return ListView.separated( + padding: const EdgeInsets.all(16), + itemCount: _materials.length, + separatorBuilder: (_, __) => const SizedBox(height: 12), + itemBuilder: (context, i) { + final mat = _materials[i]; + final isGenerating = _generatingForId == mat['id']; + final name = mat['name'] ?? 'Material'; + return Container( + decoration: BoxDecoration( + color: cs.surface, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: cs.outline.withOpacity(0.15)), + boxShadow: [ + BoxShadow( + color: cs.shadow.withOpacity(0.05), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + leading: Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: cs.secondary.withOpacity(0.12), + borderRadius: BorderRadius.circular(10), + ), + child: Icon(Icons.picture_as_pdf, color: cs.secondary, size: 22), + ), + title: Text( + name.replaceAll('.pdf', '').replaceAll('_', ' '), + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + color: cs.onSurface, + ), + ), + subtitle: Text( + 'Toca para gerar um quiz', + style: TextStyle(fontSize: 12, color: cs.onSurfaceVariant), + ), + trailing: isGenerating + ? SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 2.5, + color: cs.primary, + ), + ) + : Icon(Icons.play_circle_outline, color: cs.primary, size: 28), + onTap: isGenerating ? null : () => _generateQuiz(mat), + ), + ); + }, + ); + } + + Widget _buildHistoryTab(ColorScheme cs) { + if (_loadingHistory) { + return const Center(child: CircularProgressIndicator()); + } + if (_history.isEmpty) { + return Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.history, size: 64, color: cs.onSurfaceVariant.withOpacity(0.4)), + const SizedBox(height: 16), + Text( + 'Ainda não geraste nenhum quiz.', + textAlign: TextAlign.center, + style: TextStyle(color: cs.onSurfaceVariant, fontSize: 16), + ), + ], + ), + ), + ); + } + return ListView.separated( + padding: const EdgeInsets.all(16), + itemCount: _history.length, + separatorBuilder: (_, __) => const SizedBox(height: 12), + itemBuilder: (context, i) { + final item = _history[i]; + final matName = (item['materialName'] as String? ?? 'Material') + .replaceAll('.pdf', '') + .replaceAll('_', ' '); + final ts = item['createdAt']; + String dateStr = ''; + if (ts is Timestamp) { + final dt = ts.toDate(); + dateStr = '${dt.day.toString().padLeft(2, '0')}/${dt.month.toString().padLeft(2, '0')}/${dt.year} ${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}'; + } + return Container( + decoration: BoxDecoration( + color: cs.surface, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: cs.outline.withOpacity(0.15)), + boxShadow: [ + BoxShadow( + color: cs.shadow.withOpacity(0.05), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + leading: Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: cs.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Icon(Icons.quiz, color: cs.primary, size: 22), + ), + title: Text( + matName, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + color: cs.onSurface, + ), + ), + subtitle: dateStr.isNotEmpty + ? Text(dateStr, style: TextStyle(fontSize: 12, color: cs.onSurfaceVariant)) + : null, + trailing: Icon(Icons.chevron_right, color: cs.onSurfaceVariant), + onTap: () => _showQuizFromHistory(matName, item['quizJson'] as String? ?? item['quizText'] as String? ?? ''), + ), + ); + }, + ); + } +} + +// ─── Modelo de dados ──────────────────────────────────────────────────────── + +class _QuizQuestion { + final String question; + final List options; + final int correctIndex; + final String explanation; + const _QuizQuestion({ + required this.question, + required this.options, + required this.correctIndex, + required this.explanation, + }); +} + +// ─── Sheet interativa ──────────────────────────────────────────────────────── + +class _InteractiveQuizSheet extends StatefulWidget { + final String title; + final List<_QuizQuestion> questions; + const _InteractiveQuizSheet({required this.title, required this.questions}); + + @override + State<_InteractiveQuizSheet> createState() => _InteractiveQuizSheetState(); +} + +class _InteractiveQuizSheetState extends State<_InteractiveQuizSheet> { + // índice da pergunta actual (-1 = resultados finais) + int _current = 0; + // respostas escolhidas: -1 = sem resposta + late List _chosen; + bool _submitted = false; + + @override + void initState() { + super.initState(); + _chosen = List.filled(widget.questions.length, -1); + } + + void _selectOption(int idx) { + if (_submitted) return; + setState(() => _chosen[_current] = idx); + } + + void _next() { + if (_current < widget.questions.length - 1) { + setState(() { _current++; }); + } else { + setState(() => _submitted = true); + } + } + + void _prev() { + if (_current > 0) setState(() { _current--; }); + } + + int get _score => List.generate(widget.questions.length, (i) { + return _chosen[i] == widget.questions[i].correctIndex ? 1 : 0; + }).fold(0, (a, b) => a + b); + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + return DraggableScrollableSheet( + initialChildSize: 0.93, + minChildSize: 0.6, + maxChildSize: 0.97, + builder: (_, scrollController) => Container( + decoration: BoxDecoration( + color: cs.surface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), + ), + child: Column( + children: [ + _buildHandle(cs), + _buildHeader(cs), + const Divider(height: 1), + Expanded( + child: _submitted + ? _buildResults(cs, scrollController) + : _buildQuestion(cs, scrollController), + ), + ], + ), + ), + ); + } + + Widget _buildHandle(ColorScheme cs) => Container( + margin: const EdgeInsets.only(top: 12, bottom: 4), + width: 40, + height: 4, + decoration: BoxDecoration( + color: cs.outline.withOpacity(0.3), + borderRadius: BorderRadius.circular(2), + ), + ); + + Widget _buildHeader(ColorScheme cs) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold, color: cs.onSurface), + ), + if (!_submitted) + Text( + 'Pergunta ${_current + 1} de ${widget.questions.length}', + style: TextStyle(fontSize: 12, color: cs.onSurfaceVariant), + ), + ], + ), + ), + IconButton( + icon: Icon(Icons.close, color: cs.onSurfaceVariant), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + ); + + Widget _buildQuestion(ColorScheme cs, ScrollController sc) { + final q = widget.questions[_current]; + final chosen = _chosen[_current]; + return ListView( + controller: sc, + padding: const EdgeInsets.fromLTRB(20, 16, 20, 24), + children: [ + // Barra de progresso + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: LinearProgressIndicator( + value: (_current + 1) / widget.questions.length, + minHeight: 6, + backgroundColor: cs.surfaceVariant, + valueColor: AlwaysStoppedAnimation(cs.primary), + ), + ), + const SizedBox(height: 20), + // Pergunta + Text( + q.question, + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: cs.onSurface, height: 1.4), + ), + const SizedBox(height: 20), + // Opções + ...List.generate(q.options.length, (i) { + final isSelected = chosen == i; + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: () => _selectOption(i), + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: BoxDecoration( + color: isSelected ? cs.primary.withOpacity(0.12) : cs.surfaceVariant.withOpacity(0.4), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isSelected ? cs.primary : cs.outline.withOpacity(0.2), + width: isSelected ? 2 : 1, + ), + ), + child: Row( + children: [ + Expanded( + child: Text( + q.options[i], + style: TextStyle( + fontSize: 14, + color: isSelected ? cs.primary : cs.onSurface, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + ), + ), + ), + if (isSelected) Icon(Icons.check_circle, color: cs.primary, size: 20), + ], + ), + ), + ), + ); + }), + const SizedBox(height: 8), + // Botões navegação + Row( + children: [ + if (_current > 0) + Expanded( + child: OutlinedButton( + onPressed: _prev, + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + child: const Text('Anterior'), + ), + ), + if (_current > 0) const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + onPressed: chosen == -1 ? null : _next, + style: ElevatedButton.styleFrom( + backgroundColor: cs.primary, + foregroundColor: cs.onPrimary, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + child: Text(_current < widget.questions.length - 1 ? 'Próxima' : 'Submeter'), + ), + ), + ], + ), + ], + ); + } + + Widget _buildResults(ColorScheme cs, ScrollController sc) { + final total = widget.questions.length; + final score = _score; + final pct = (score / total * 100).round(); + final Color scoreColor = pct >= 80 + ? const Color(0xFF10B981) + : pct >= 50 + ? const Color(0xFFF59E0B) + : const Color(0xFFEF4444); + + return ListView( + controller: sc, + padding: const EdgeInsets.fromLTRB(20, 20, 20, 32), + children: [ + // Resultado global + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: scoreColor.withOpacity(0.08), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: scoreColor.withOpacity(0.3)), + ), + child: Column( + children: [ + Text( + '$score / $total', + style: TextStyle(fontSize: 40, fontWeight: FontWeight.bold, color: scoreColor), + ), + const SizedBox(height: 4), + Text( + '$pct% de respostas correctas', + style: TextStyle(fontSize: 14, color: scoreColor), + ), + ], + ), + ), + const SizedBox(height: 24), + Text( + 'Revisão', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: cs.onSurface), + ), + const SizedBox(height: 12), + // Revisão pergunta a pergunta + ...List.generate(total, (i) { + final q = widget.questions[i]; + final chosen = _chosen[i]; + final isCorrect = chosen == q.correctIndex; + final revColor = isCorrect ? const Color(0xFF10B981) : const Color(0xFFEF4444); + return Container( + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: revColor.withOpacity(0.06), + borderRadius: BorderRadius.circular(14), + border: Border.all(color: revColor.withOpacity(0.25)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(isCorrect ? Icons.check_circle : Icons.cancel, color: revColor, size: 18), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Pergunta ${i + 1}', + style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: revColor), + ), + ), + ], + ), + const SizedBox(height: 8), + Text(q.question, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: cs.onSurface)), + const SizedBox(height: 8), + if (chosen >= 0 && chosen < q.options.length && !isCorrect) + Text( + 'A tua resposta: ${q.options[chosen]}', + style: const TextStyle(fontSize: 13, color: Color(0xFFEF4444)), + ), + Text( + 'Resposta correcta: ${q.options[q.correctIndex]}', + style: TextStyle(fontSize: 13, color: isCorrect ? const Color(0xFF10B981) : cs.onSurface, fontWeight: FontWeight.w500), + ), + if (q.explanation.isNotEmpty) ...[ + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: cs.surfaceVariant.withOpacity(0.5), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.info_outline, size: 14, color: cs.onSurfaceVariant), + const SizedBox(width: 6), + Expanded( + child: Text( + q.explanation, + style: TextStyle(fontSize: 12, color: cs.onSurfaceVariant, height: 1.4), + ), + ), + ], + ), + ), + ], + ], + ), + ); + }), + // Botão fechar + ElevatedButton( + onPressed: () => Navigator.of(context).pop(), + style: ElevatedButton.styleFrom( + backgroundColor: cs.primary, + foregroundColor: cs.onPrimary, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + child: const Text('Fechar'), + ), + ], + ); + } }