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
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
)
}
}
}

View File

@@ -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<TutorChatPageSimple>
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<TutorChatPageSimple>
void dispose() {
_messageController.dispose();
_scrollController.dispose();
_messageFocusNode.dispose();
super.dispose();
}
@@ -674,12 +677,12 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
),
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<TutorChatPageSimple>
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<TutorChatPageSimple>
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<TutorChatPageSimple>
RAGAIService.clearLastContext();
}
Navigator.of(dialogContext).pop();
_messageFocusNode.unfocus();
},
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(

View File

@@ -2119,16 +2119,99 @@ class _QuizListPageState extends State<QuizListPage>
);
}
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 = <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(
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<QuizListPage>
);
}
final realDisciplineIds = groups.keys
final realDisciplineIds = aiGroups.keys
.where((k) => k != '__geral__' && _historyClassNames.containsKey(k))
.toList();
@@ -2176,7 +2259,7 @@ class _QuizListPageState extends State<QuizListPage>
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),