diff --git a/android/app/src/main/kotlin/com/example/teachit/MainActivity.kt b/android/app/src/main/kotlin/com/example/teachit/MainActivity.kt index 447059f..4a9946a 100644 --- a/android/app/src/main/kotlin/com/example/teachit/MainActivity.kt +++ b/android/app/src/main/kotlin/com/example/teachit/MainActivity.kt @@ -1,5 +1,34 @@ package com.example.teachit +import android.os.Build +import android.view.View +import android.view.WindowInsets +import android.view.WindowInsetsController +import androidx.core.view.WindowCompat import io.flutter.embedding.android.FlutterActivity -class MainActivity : FlutterActivity() +class MainActivity : FlutterActivity() { + override fun onResume() { + super.onResume() + hideSystemBars() + } + + private fun hideSystemBars() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + window.setDecorFitsSystemWindows(false) + val controller = window.insetsController + controller?.let { + it.hide(WindowInsets.Type.systemBars()) + it.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } + } else { + @Suppress("DEPRECATION") + window.decorView.systemUiVisibility = ( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE + or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + ) + } + } +} diff --git a/lib/features/ai_tutor/presentation/pages/tutor_chat_page_simple.dart b/lib/features/ai_tutor/presentation/pages/tutor_chat_page_simple.dart index c0c5657..20ac503 100644 --- a/lib/features/ai_tutor/presentation/pages/tutor_chat_page_simple.dart +++ b/lib/features/ai_tutor/presentation/pages/tutor_chat_page_simple.dart @@ -8,6 +8,7 @@ import '../../../../core/services/chat_memory_service.dart'; import '../../../../core/services/materials_rag_service.dart'; import '../../../../core/services/rag_ai_service.dart'; import '../../../../core/utils/logger.dart'; +import '../../../materials/presentation/pages/pdf_viewer_page.dart'; /// Simple AI Tutor chat interface page (for testing) class TutorChatPageSimple extends StatefulWidget { @@ -21,6 +22,7 @@ class _TutorChatPageSimpleState extends State with TickerProviderStateMixin { final TextEditingController _messageController = TextEditingController(); final ScrollController _scrollController = ScrollController(); + final FocusNode _messageFocusNode = FocusNode(); bool _isLoading = false; bool _materialsConfirmed = false; @@ -68,6 +70,7 @@ class _TutorChatPageSimpleState extends State void dispose() { _messageController.dispose(); _scrollController.dispose(); + _messageFocusNode.dispose(); super.dispose(); } @@ -674,12 +677,12 @@ class _TutorChatPageSimpleState extends State ), if (_selectedMaterialIds.isNotEmpty) ..._selectedMaterialIds.map((id) { - final name = - _availableMaterials.firstWhere( - (m) => m['id'] == id, - orElse: () => {'id': id, 'name': id}, - )['name'] ?? - id; + final material = _availableMaterials.firstWhere( + (m) => m['id'] == id, + orElse: () => {'id': id, 'name': id}, + ); + final name = material['name'] ?? id; + final url = material['url']; final cleanName = name .replaceAll('.pdf', '') .replaceAll('_', ' '); @@ -690,76 +693,98 @@ class _TutorChatPageSimpleState extends State final isLast = _selectedMaterialIds.length == 1; return Padding( padding: const EdgeInsets.only(left: 6), - child: Chip( - label: Text( - short, - style: const TextStyle( - fontSize: 11, - color: Colors.white, - fontWeight: FontWeight.w600, + child: InkWell( + onTap: url != null + ? () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + PdfViewerPage( + url: url, + fileName: name, + ), + ), + ); + } + : null, + borderRadius: BorderRadius.circular(20), + child: Chip( + label: Text( + short, + style: const TextStyle( + fontSize: 11, + color: Colors.white, + fontWeight: FontWeight.w600, + ), ), - ), - backgroundColor: chipBg, - deleteIconColor: Colors.white.withValues( - alpha: 0.85, - ), - deleteIcon: const Icon(Icons.close, size: 14), - onDeleted: () { - if (isLast) { - ScaffoldMessenger.of(context) - ..clearSnackBars() - ..showSnackBar( - SnackBar( - behavior: SnackBarBehavior.floating, - margin: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 12, - ), - backgroundColor: const Color( - 0xFFF68D2D, - ), - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(12), - ), - duration: const Duration( - seconds: 2, - ), - content: const Row( - children: [ - Icon( - Icons.warning_amber_rounded, - color: Colors.white, - size: 20, - ), - SizedBox(width: 10), - Expanded( - child: Text( - 'Tens de manter pelo menos um material selecionado.', - style: TextStyle( - color: Colors.white, - fontSize: 13, - fontWeight: - FontWeight.w500, + backgroundColor: chipBg, + deleteIconColor: Colors.white.withValues( + alpha: 0.85, + ), + deleteIcon: const Icon( + Icons.close, + size: 14, + ), + onDeleted: () { + if (isLast) { + ScaffoldMessenger.of(context) + ..clearSnackBars() + ..showSnackBar( + SnackBar( + behavior: + SnackBarBehavior.floating, + margin: + const EdgeInsets.symmetric( + horizontal: 20, + vertical: 12, + ), + backgroundColor: const Color( + 0xFFF68D2D, + ), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(12), + ), + duration: const Duration( + seconds: 2, + ), + content: const Row( + children: [ + Icon( + Icons.warning_amber_rounded, + color: Colors.white, + size: 20, + ), + SizedBox(width: 10), + Expanded( + child: Text( + 'Tens de manter pelo menos um material selecionado.', + style: TextStyle( + color: Colors.white, + fontSize: 13, + fontWeight: + FontWeight.w500, + ), ), ), - ), - ], + ], + ), ), - ), + ); + } else { + setState( + () => _selectedMaterialIds.remove(id), ); - } else { - setState( - () => _selectedMaterialIds.remove(id), - ); - } - }, - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - padding: const EdgeInsets.symmetric( - horizontal: 4, + } + }, + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + padding: const EdgeInsets.symmetric( + horizontal: 4, + ), + visualDensity: VisualDensity.compact, ), - visualDensity: VisualDensity.compact, ), ); }).toList(), @@ -785,6 +810,8 @@ class _TutorChatPageSimpleState extends State Expanded( child: TextField( controller: _messageController, + focusNode: _messageFocusNode, + autofocus: false, style: TextStyle( fontSize: 16, color: Theme.of(context).colorScheme.onSurface, @@ -1184,6 +1211,7 @@ class _TutorChatPageSimpleState extends State RAGAIService.clearLastContext(); } Navigator.of(dialogContext).pop(); + _messageFocusNode.unfocus(); }, style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder( diff --git a/lib/features/quiz/presentation/pages/quiz_list_page.dart b/lib/features/quiz/presentation/pages/quiz_list_page.dart index 378fbf6..ce66c81 100644 --- a/lib/features/quiz/presentation/pages/quiz_list_page.dart +++ b/lib/features/quiz/presentation/pages/quiz_list_page.dart @@ -2119,16 +2119,99 @@ class _QuizListPageState extends State ); } - final groups = _groupHistoryByDiscipline(); - final filteredItems = items.where((item) { - final cid = item['classId'] as String?; - return groups.containsKey(cid) && cid != null; - }).toList(); + // Handle selected discipline for AI-generated quizzes + if (_selectedHistoryDisciplineId != null) { + final groups = _groupHistoryByDiscipline(); + final disciplineItems = groups[_selectedHistoryDisciplineId] ?? []; + // Filter only AI-generated quizzes (teacherQuizId == null) + final aiDisciplineItems = disciplineItems + .where((q) => q['teacherQuizId'] == null) + .toList(); + final disciplineName = + _historyClassNames[_selectedHistoryDisciplineId] ?? + _selectedHistoryDisciplineId!; - if (filteredItems.isEmpty) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + IconButton( + icon: Icon(Icons.arrow_back, color: cs.onSurface), + onPressed: () => + setState(() => _selectedHistoryDisciplineId = null), + ), + const SizedBox(width: 4), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + disciplineName, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: cs.onSurface, + ), + ), + Text( + '${aiDisciplineItems.length} quiz${aiDisciplineItems.length != 1 ? 'zes' : ''}', + style: TextStyle( + fontSize: 14, + color: cs.onSurfaceVariant, + ), + ), + ], + ), + ), + ], + ), + ), + Expanded( + child: Stack( + children: [ + _buildHistoryList( + cs, + aiDisciplineItems, + isTeacherQuizzes: false, + ), + if (_isSelectionMode && _selectedQuizIds.isNotEmpty) + Positioned( + bottom: 16, + right: 16, + child: FloatingActionButton.extended( + onPressed: _deleteSelectedQuizzes, + backgroundColor: Colors.red, + icon: const Icon(Icons.delete), + label: Text('Eliminar (${_selectedQuizIds.length})'), + ), + ), + ], + ), + ), + ], + ); + } + + // Group only AI-generated quizzes by discipline + final aiItems = items.where((q) => q['teacherQuizId'] == null).toList(); + + // Filter groups to only include disciplines with AI-generated quizzes + final aiGroups = >>{}; + for (final item in aiItems) { + final cid = item['classId'] as String?; + final groupId = (cid != null && _historyClassNames.containsKey(cid)) + ? cid + : '__geral__'; + aiGroups.putIfAbsent(groupId, () => []).add(item); + } + + if (aiGroups.isEmpty) { return Stack( children: [ - _buildHistoryList(cs, items, isTeacherQuizzes: isTeacherQuizzes), + _buildHistoryList(cs, aiItems, isTeacherQuizzes: isTeacherQuizzes), if (_isSelectionMode && _selectedQuizIds.isNotEmpty) Positioned( bottom: 16, @@ -2144,7 +2227,7 @@ class _QuizListPageState extends State ); } - final realDisciplineIds = groups.keys + final realDisciplineIds = aiGroups.keys .where((k) => k != '__geral__' && _historyClassNames.containsKey(k)) .toList(); @@ -2176,7 +2259,7 @@ class _QuizListPageState extends State itemBuilder: (context, i) { final dId = realDisciplineIds[i]; final dName = _historyClassNames[dId] ?? dId; - final count = groups[dId]!.length; + final count = aiGroups[dId]!.length; return InkWell( borderRadius: BorderRadius.circular(16), onTap: () => setState(() => _selectedHistoryDisciplineId = dId),