This commit is contained in:
2026-05-21 11:39:30 +01:00
parent 98dcd621c7
commit 2f411d08a4
3 changed files with 220 additions and 80 deletions

View File

@@ -1,5 +1,34 @@
package com.example.teachit 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 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
)
}
}
}

View File

@@ -8,6 +8,7 @@ import '../../../../core/services/chat_memory_service.dart';
import '../../../../core/services/materials_rag_service.dart'; import '../../../../core/services/materials_rag_service.dart';
import '../../../../core/services/rag_ai_service.dart'; import '../../../../core/services/rag_ai_service.dart';
import '../../../../core/utils/logger.dart'; import '../../../../core/utils/logger.dart';
import '../../../materials/presentation/pages/pdf_viewer_page.dart';
/// Simple AI Tutor chat interface page (for testing) /// Simple AI Tutor chat interface page (for testing)
class TutorChatPageSimple extends StatefulWidget { class TutorChatPageSimple extends StatefulWidget {
@@ -21,6 +22,7 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
with TickerProviderStateMixin { with TickerProviderStateMixin {
final TextEditingController _messageController = TextEditingController(); final TextEditingController _messageController = TextEditingController();
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
final FocusNode _messageFocusNode = FocusNode();
bool _isLoading = false; bool _isLoading = false;
bool _materialsConfirmed = false; bool _materialsConfirmed = false;
@@ -68,6 +70,7 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
void dispose() { void dispose() {
_messageController.dispose(); _messageController.dispose();
_scrollController.dispose(); _scrollController.dispose();
_messageFocusNode.dispose();
super.dispose(); super.dispose();
} }
@@ -674,12 +677,12 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
), ),
if (_selectedMaterialIds.isNotEmpty) if (_selectedMaterialIds.isNotEmpty)
..._selectedMaterialIds.map((id) { ..._selectedMaterialIds.map((id) {
final name = final material = _availableMaterials.firstWhere(
_availableMaterials.firstWhere( (m) => m['id'] == id,
(m) => m['id'] == id, orElse: () => {'id': id, 'name': id},
orElse: () => {'id': id, 'name': id}, );
)['name'] ?? final name = material['name'] ?? id;
id; final url = material['url'];
final cleanName = name final cleanName = name
.replaceAll('.pdf', '') .replaceAll('.pdf', '')
.replaceAll('_', ' '); .replaceAll('_', ' ');
@@ -690,76 +693,98 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
final isLast = _selectedMaterialIds.length == 1; final isLast = _selectedMaterialIds.length == 1;
return Padding( return Padding(
padding: const EdgeInsets.only(left: 6), padding: const EdgeInsets.only(left: 6),
child: Chip( child: InkWell(
label: Text( onTap: url != null
short, ? () {
style: const TextStyle( Navigator.push(
fontSize: 11, context,
color: Colors.white, MaterialPageRoute(
fontWeight: FontWeight.w600, 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,
backgroundColor: chipBg, deleteIconColor: Colors.white.withValues(
deleteIconColor: Colors.white.withValues( alpha: 0.85,
alpha: 0.85, ),
), deleteIcon: const Icon(
deleteIcon: const Icon(Icons.close, size: 14), Icons.close,
onDeleted: () { size: 14,
if (isLast) { ),
ScaffoldMessenger.of(context) onDeleted: () {
..clearSnackBars() if (isLast) {
..showSnackBar( ScaffoldMessenger.of(context)
SnackBar( ..clearSnackBars()
behavior: SnackBarBehavior.floating, ..showSnackBar(
margin: const EdgeInsets.symmetric( SnackBar(
horizontal: 20, behavior:
vertical: 12, SnackBarBehavior.floating,
), margin:
backgroundColor: const Color( const EdgeInsets.symmetric(
0xFFF68D2D, horizontal: 20,
), vertical: 12,
shape: RoundedRectangleBorder( ),
borderRadius: backgroundColor: const Color(
BorderRadius.circular(12), 0xFFF68D2D,
), ),
duration: const Duration( shape: RoundedRectangleBorder(
seconds: 2, borderRadius:
), BorderRadius.circular(12),
content: const Row( ),
children: [ duration: const Duration(
Icon( seconds: 2,
Icons.warning_amber_rounded, ),
color: Colors.white, content: const Row(
size: 20, children: [
), Icon(
SizedBox(width: 10), Icons.warning_amber_rounded,
Expanded( color: Colors.white,
child: Text( size: 20,
'Tens de manter pelo menos um material selecionado.', ),
style: TextStyle( SizedBox(width: 10),
color: Colors.white, Expanded(
fontSize: 13, child: Text(
fontWeight: 'Tens de manter pelo menos um material selecionado.',
FontWeight.w500, 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, visualDensity: VisualDensity.compact,
padding: const EdgeInsets.symmetric(
horizontal: 4,
), ),
visualDensity: VisualDensity.compact,
), ),
); );
}).toList(), }).toList(),
@@ -785,6 +810,8 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
Expanded( Expanded(
child: TextField( child: TextField(
controller: _messageController, controller: _messageController,
focusNode: _messageFocusNode,
autofocus: false,
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
@@ -1184,6 +1211,7 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
RAGAIService.clearLastContext(); RAGAIService.clearLastContext();
} }
Navigator.of(dialogContext).pop(); Navigator.of(dialogContext).pop();
_messageFocusNode.unfocus();
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(

View File

@@ -2119,16 +2119,99 @@ class _QuizListPageState extends State<QuizListPage>
); );
} }
final groups = _groupHistoryByDiscipline(); // Handle selected discipline for AI-generated quizzes
final filteredItems = items.where((item) { if (_selectedHistoryDisciplineId != null) {
final cid = item['classId'] as String?; final groups = _groupHistoryByDiscipline();
return groups.containsKey(cid) && cid != null; final disciplineItems = groups[_selectedHistoryDisciplineId] ?? [];
}).toList(); // 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 = <String, List<Map<String, dynamic>>>{};
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( return Stack(
children: [ children: [
_buildHistoryList(cs, items, isTeacherQuizzes: isTeacherQuizzes), _buildHistoryList(cs, aiItems, isTeacherQuizzes: isTeacherQuizzes),
if (_isSelectionMode && _selectedQuizIds.isNotEmpty) if (_isSelectionMode && _selectedQuizIds.isNotEmpty)
Positioned( Positioned(
bottom: 16, bottom: 16,
@@ -2144,7 +2227,7 @@ class _QuizListPageState extends State<QuizListPage>
); );
} }
final realDisciplineIds = groups.keys final realDisciplineIds = aiGroups.keys
.where((k) => k != '__geral__' && _historyClassNames.containsKey(k)) .where((k) => k != '__geral__' && _historyClassNames.containsKey(k))
.toList(); .toList();
@@ -2176,7 +2259,7 @@ class _QuizListPageState extends State<QuizListPage>
itemBuilder: (context, i) { itemBuilder: (context, i) {
final dId = realDisciplineIds[i]; final dId = realDisciplineIds[i];
final dName = _historyClassNames[dId] ?? dId; final dName = _historyClassNames[dId] ?? dId;
final count = groups[dId]!.length; final count = aiGroups[dId]!.length;
return InkWell( return InkWell(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
onTap: () => setState(() => _selectedHistoryDisciplineId = dId), onTap: () => setState(() => _selectedHistoryDisciplineId = dId),