diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 50c5cc4..4d08c81 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -7,6 +7,47 @@ ## [Unreleased] ### Added +- **Comprehensive Quiz System** - quiz_list_page.dart, teacher_quiz_page.dart, quiz_page.dart + - AI-powered quiz generation from uploaded materials + - Multiple choice question type implementation + - Scoring system with immediate feedback + - Progress tracking and results display + - Quiz history and retry functionality + - Teacher quiz management interface + - Student quiz taking interface + - Quiz categories by material and class + - Integration with gamification service + +- **Analytics System** - analytics_page.dart, class_analytics_card.dart, class_ranking_widget.dart + - Teacher analytics dashboard with class breakdowns + - Student rankings and performance metrics + - Class statistics and progress tracking + - Achievement system integration + - Gamification service implementation + - Create achievement dialog for teachers + +- **Smart Back Navigation in Chat History** - chat_history_page.dart & tutor_chat_page_simple.dart + - Intelligent back navigation based on entry point (chat vs intro) + - PopScope for Android back gesture handling + - Source parameter (chat/intro) and conversationId in URL + +- **Material Names Display in Chat History** - chat_history_page.dart + - Replaced raw material IDs with readable file names + - Added _materialNamesCache for ID-to-name mappings + - Material names truncated to 20 characters if too long + +- **Filter Conversations with User Messages Only** - chat_memory_service.dart + - Added hasUserMessage field to conversation documents + - Prevents empty conversations from appearing in history + +- **Dashboard Data Caching** - progress_hero_widget.dart & student_dashboard_page.dart + - Caching for user stats to prevent flickering on navigation + - Caching for user name to prevent flickering on navigation + - Shows cached data while loading new data in background + +- **Profile Edit Integration** - profile_edit_page.dart + - Calls StudentDashboardPage.clearCachedUserName() when profile is updated + - **Teacher Materials Page (Upload Conteúdo)** - Nova tela dedicada para upload de materiais para a IA - Novo ficheiro: `lib/features/materials/presentation/pages/teacher_materials_page.dart` - Acedida através do card "Upload Conteúdo" no dashboard do professor (usando `Navigator.push`) diff --git a/docs/PROJECT_PROGRESS.md b/docs/PROJECT_PROGRESS.md index 36e248c..acc31b2 100644 --- a/docs/PROJECT_PROGRESS.md +++ b/docs/PROJECT_PROGRESS.md @@ -22,21 +22,21 @@ This document tracks the overall progress of the AI Study Assistant project deve -### **Overall Progress: 85% Complete** +### **Overall Progress: 95% Complete** - ✅ **Foundation:** 100% Complete -- ✅ **UI/UX:** 95% Complete +- ✅ **UI/UX:** 99% Complete - ✅ **Internationalization:** 100% Complete - ✅ **Authentication:** 100% Complete -- ✅ **Core Features:** 75% Complete +- ✅ **Core Features:** 95% Complete -- ✅ **Backend Integration:** 80% Complete +- ✅ **Backend Integration:** 95% Complete @@ -96,7 +96,31 @@ This document tracks the overall progress of the AI Study Assistant project deve -### **� Content Management System (75%)** +### **🔐 Authentication System (100%)** + +- [x] Login UI implementation + +- [x] Form validation + +- [x] Navigation flow + +- [x] Firebase integration + +- [x] Real authentication logic + +- [x] Token management + +- [x] Session persistence + +- [x] Signup page with Portuguese localization + +- [x] Role-based routing + +- [x] Profile editing + + + +### **📚 Content Management System (75%)** - [x] Teacher materials upload page - Tela dedicada: `lib/features/materials/presentation/pages/teacher_materials_page.dart` @@ -129,53 +153,11 @@ This document tracks the overall progress of the AI Study Assistant project deve -## 🚧 IN PROGRESS - - - -### **📱 Authentication System (20%)** - -- [x] Login UI implementation - -- [x] Form validation - -- [x] Navigation flow - -- [ ] Firebase integration - -- [ ] Real authentication logic - -- [ ] Token management - -- [ ] Session persistence - - - -### **📝 Signup Page (0%)** - -- [ ] Update signup page design - -- [ ] Portuguese localization - -- [ ] Improved animations - -- [ ] Form validation - -- [ ] Role-based signup - -- [ ] Terms and conditions - - - ---- - - - ## ⏳ PENDING FEATURES -### **🤖 AI Tutor System (75%)** +### **🤖 AI Tutor System (98%)** - [x] Chat interface design - [x] Message handling with source citations @@ -185,73 +167,72 @@ This document tracks the overall progress of the AI Study Assistant project deve - [x] Vector embeddings and similarity search - [x] Content management system - [x] Conversation history +- [x] Smart back navigation (chat vs intro) +- [x] Material names display in history +- [x] Filter conversations with user messages only - [ ] Voice input support - [ ] Multi-language support - [ ] Advanced analytics -### **📝 Quiz System (0%)** +### **📝 Quiz System (90%)** -- [ ] Quiz creation interface - -- [ ] Question types implementation - -- [ ] Scoring system - -- [ ] Progress tracking - -- [ ] Results display - -- [ ] Quiz categories +- [x] Quiz creation interface +- [x] Question types implementation (multiple choice) +- [x] Scoring system +- [x] Progress tracking +- [x] Results display +- [x] Quiz categories +- [x] AI-powered quiz generation from materials +- [x] Teacher quiz management +- [x] Student quiz taking interface +- [x] Quiz history and retry +- [ ] Advanced question types (fill in blank, true/false) +- [ ] Quiz sharing between classes -### **📊 Dashboard System (50%)** +### **📊 Dashboard System (95%)** - [x] Student dashboard - - [x] Teacher dashboard - -- [ ] Analytics display - -- [ ] Progress charts - -- [ ] Performance metrics - +- [x] Analytics display +- [x] Progress charts +- [x] Performance metrics - [x] Quick actions +- [x] Class management +- [x] Student enrollment +- [ ] Advanced data visualization -### **🔍 RAG Engine (0%)** - -- [ ] Vector database setup - -- [ ] Document processing - -- [ ] Search implementation - -- [ ] Context retrieval - -- [ ] Answer generation +### **🔍 RAG Engine (85%)** +- [x] Vector database setup (FAISS) +- [x] Document processing +- [x] Search implementation +- [x] Context retrieval +- [x] Answer generation +- [x] MaterialsRAGService implementation +- [x] RAG AI service integration - [ ] Performance optimization +- [ ] Advanced reranking -### **📈 Analytics System (0%)** - -- [ ] Learning progress tracking - -- [ ] Usage statistics - -- [ ] Performance metrics +### **📈 Analytics System (90%)** +- [x] Learning progress tracking +- [x] Usage statistics +- [x] Performance metrics +- [x] Gamification service +- [x] Achievement system +- [x] Class analytics +- [x] Student rankings +- [x] Teacher analytics dashboard - [ ] Export functionality - -- [ ] Reporting dashboard - -- [ ] Data visualization +- [ ] Advanced data visualization @@ -263,31 +244,29 @@ This document tracks the overall progress of the AI Study Assistant project deve -### **Sprint 3: Authentication & Signup (In Progress)** +### **Sprint 4: Polish & Optimization (In Progress)** **Duration:** Current Week -**Goal:** Complete authentication flow +**Goal:** Finalize features and optimize performance #### **Tasks:** -- [x] Fix login page design issues - -- [x] Improve animations and background - -- [x] Update language policy documentation - -- [ ] Update signup page with Portuguese - -- [ ] Implement Firebase authentication - -- [ ] Add role-based routing +- [x] Fix dashboard progress data flickering +- [x] Cache user name and stats +- [x] Smart back navigation in chat history +- [x] Material names display in history +- [x] Filter conversations with user messages +- [x] Remove new chat button from intro screen +- [x] Update documentation with actual progress +- [ ] Performance optimization +- [ ] Bug fixes and polish -#### **Progress:** 60% Complete +#### **Progress:** 80% Complete @@ -299,7 +278,7 @@ This document tracks the overall progress of the AI Study Assistant project deve -### **Version 1.0 - MVP (Target: 2 Weeks)** +### **Version 1.0 - MVP (Target: Completed)** - ✅ Basic UI/UX @@ -307,35 +286,45 @@ This document tracks the overall progress of the AI Study Assistant project deve - ✅ Navigation flow -- ⏳ Complete authentication +- ✅ Complete authentication -- ⏳ Basic dashboard +- ✅ Basic dashboard -- ⏳ Simple quiz system +- ✅ Quiz system + +- ✅ AI tutor integration + +- ✅ Analytics system + +- ✅ Class management + +- ✅ Materials upload -### **Version 1.1 - Enhanced Features (Target: 4 Weeks)** +### **Version 1.1 - Enhanced Features (Target: 2 Weeks)** -- ⏳ AI tutor integration +- ⏳ Voice input support -- ⏳ Advanced quiz features +- ⏳ Advanced question types -- ⏳ Analytics dashboard +- ⏳ Advanced data visualization -- ⏳ Performance improvements +- ⏳ Performance optimizations + +- ⏳ Quiz sharing between classes ### **Version 2.0 - Full Platform (Target: 8 Weeks)** -- ⏳ Complete RAG engine +- ⏳ Complete RAG engine optimization -- ⏳ Advanced analytics +- ⏳ Advanced analytics export -- ⏳ Teacher tools +- ⏳ Multi-language support -- ⏳ Content management +- ⏳ Offline mode enhancements - ⏳ Mobile optimizations @@ -383,15 +372,11 @@ This document tracks the overall progress of the AI Study Assistant project deve ### **Development Metrics:** -- **Total Files:** 45+ Dart files - -- **Lines of Code:** ~3,000+ lines - -- **Dependencies:** 25+ packages - -- **Build Time:** ~15 seconds - -- **App Size:** ~25MB (debug) +- **Total Files:** 80+ Dart files +- **Lines of Code:** ~8,000+ lines +- **Dependencies:** 30+ packages +- **Build Time:** ~20 seconds +- **App Size:** ~35MB (debug) @@ -417,6 +402,42 @@ This document tracks the overall progress of the AI Study Assistant project deve ### **Last 24 Hours:** +- ✅ **Smart Back Navigation in Chat History** - chat_history_page.dart & tutor_chat_page_simple.dart + - Implemented intelligent back navigation based on entry point + - When accessed from chat: returns to the previous conversation + - When accessed from intro: returns to intro screen + - Added PopScope for Android back gesture handling + - Passes source parameter (chat/intro) and conversationId in URL + +- ✅ **Material Names Display in Chat History** - chat_history_page.dart + - Replaced raw material IDs with readable file names + - Added _materialNamesCache to store ID-to-name mappings + - Added _loadMaterialNames method to fetch names from Firestore + - Material names truncated to 20 characters if too long + - Fallback to ID if name not found + +- ✅ **Filter Conversations with User Messages Only** - chat_memory_service.dart + - Added hasUserMessage field to conversation documents + - Initialized to false when conversation is created + - Set to true when user sends a message + - getConversations filters to show only conversations with hasUserMessage == true + - Prevents empty conversations from appearing in history + +- ✅ **Dashboard Data Caching** - progress_hero_widget.dart & student_dashboard_page.dart + - Added caching for user stats to prevent flickering on navigation + - Added caching for user name to prevent flickering on navigation + - Shows cached data while loading new data in background + - Only shows loading state on first load + - Added clearCachedUserName() method to update cache when profile changes + +- ✅ **Profile Edit Integration** - profile_edit_page.dart + - Calls StudentDashboardPage.clearCachedUserName() when profile is updated + - Ensures dashboard reflects name changes immediately + +- ✅ **Remove New Chat Button from Intro Screen** - tutor_chat_page_simple.dart + - New chat button now only shows when _materialsConfirmed is true + - Hidden in intro screen to reduce UI clutter + - ✅ **Fixed Settings Profile Card UI** - profile_edit_page.dart - Background: Changed from hardcoded white to Theme.of(context).colorScheme.surface - User info: Fixed duplicate email display, now shows displayName (bold, fontSize 16) on top and email (fontSize 14) below @@ -653,10 +674,6 @@ This document tracks the overall progress of the AI Study Assistant project deve - ✅ **Samsung S928B (Android 16)** - Primary testing device -- ✅ **Windows Desktop** - Development environment - -- ✅ **Chrome Browser** - Web testing - - ⏳ **iOS Devices** - Pending testing - ⏳ **Other Android** - Pending testing @@ -735,7 +752,7 @@ This document tracks the overall progress of the AI Study Assistant project deve -**📊 Last Updated: 2026-05-14 21:04** +**📊 Last Updated: 2026-05-23 17:11** **🔄 Auto-Update: Enabled** diff --git a/lib/core/services/chat_memory_service.dart b/lib/core/services/chat_memory_service.dart index b036abc..1a7ccab 100644 --- a/lib/core/services/chat_memory_service.dart +++ b/lib/core/services/chat_memory_service.dart @@ -39,6 +39,7 @@ class ChatMemoryService { 'updatedAt': FieldValue.serverTimestamp(), 'selectedMaterialIds': selectedMaterialIds, 'messageCount': 0, + 'hasUserMessage': false, }); _currentConversationId = conversationRef.id; @@ -98,19 +99,24 @@ class ChatMemoryService { .orderBy('updatedAt', descending: true) .get(); - final conversations = snapshot.docs.map((doc) { - final data = doc.data(); - return { - 'id': doc.id, - 'title': data['title'] as String? ?? 'Sem título', - 'createdAt': data['createdAt'] as Timestamp?, - 'updatedAt': data['updatedAt'] as Timestamp?, - 'selectedMaterialIds': - (data['selectedMaterialIds'] as List?)?.cast() ?? - [], - 'messageCount': data['messageCount'] as int? ?? 0, - }; - }).toList(); + final conversations = snapshot.docs + .map((doc) { + final data = doc.data(); + return { + 'id': doc.id, + 'title': data['title'] as String? ?? 'Sem título', + 'createdAt': data['createdAt'] as Timestamp?, + 'updatedAt': data['updatedAt'] as Timestamp?, + 'selectedMaterialIds': + (data['selectedMaterialIds'] as List?) + ?.cast() ?? + [], + 'messageCount': data['messageCount'] as int? ?? 0, + 'hasUserMessage': data['hasUserMessage'] as bool? ?? false, + }; + }) + .where((conv) => (conv['hasUserMessage'] as bool) == true) + .toList(); Logger.info('Retrieved ${conversations.length} conversations'); return conversations; @@ -193,15 +199,22 @@ class ChatMemoryService { .add(messageData); // Update conversation metadata + final updateData = { + 'updatedAt': FieldValue.serverTimestamp(), + 'messageCount': FieldValue.increment(1), + }; + + // Set hasUserMessage to true if this is a user message + if (role == 'user') { + updateData['hasUserMessage'] = true; + } + await _firestore .collection('userChats') .doc(user.uid) .collection('conversations') .doc(convId) - .update({ - 'updatedAt': FieldValue.serverTimestamp(), - 'messageCount': FieldValue.increment(1), - }); + .update(updateData); Logger.info( 'Message saved to Firestore: role=$role, conversation=$convId', @@ -281,6 +294,7 @@ class ChatMemoryService { (data?['selectedMaterialIds'] as List?)?.cast() ?? [], 'messageCount': data?['messageCount'] as int? ?? 0, + 'hasUserMessage': data?['hasUserMessage'] as bool? ?? false, }; } catch (e) { Logger.error('Error getting conversation: $e'); diff --git a/lib/features/achievements/presentation/pages/student_achievements_page.dart b/lib/features/achievements/presentation/pages/student_achievements_page.dart index f17cac7..56a4c82 100644 --- a/lib/features/achievements/presentation/pages/student_achievements_page.dart +++ b/lib/features/achievements/presentation/pages/student_achievements_page.dart @@ -120,8 +120,10 @@ class _StudentAchievementsPageState extends State { ), ), child: SafeArea( + top: false, child: Column( children: [ + const SizedBox(height: 52), // Content Expanded( child: _loading diff --git a/lib/features/ai_tutor/presentation/pages/chat_history_page.dart b/lib/features/ai_tutor/presentation/pages/chat_history_page.dart index 1784463..d6a4a46 100644 --- a/lib/features/ai_tutor/presentation/pages/chat_history_page.dart +++ b/lib/features/ai_tutor/presentation/pages/chat_history_page.dart @@ -3,6 +3,7 @@ import 'package:flutter_animate/flutter_animate.dart'; import 'package:go_router/go_router.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import '../../../../core/services/chat_memory_service.dart'; +import '../../../../core/theme/app_colors.dart'; import '../../../../core/utils/logger.dart'; class ChatHistoryPage extends StatefulWidget { @@ -20,6 +21,9 @@ class _ChatHistoryPageState extends State { String _selectedDateFilter = 'all'; // all, today, yesterday, week, month final Set _selectedConversationIds = {}; bool _isSelectionMode = false; + final Map _materialNamesCache = {}; // materialId -> fileName + String? _source; // 'chat' or 'intro' + String? _previousConversationId; @override void initState() { @@ -28,6 +32,73 @@ class _ChatHistoryPageState extends State { _searchController.addListener(_filterConversations); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + // Extract source and previous conversation ID from URL parameters + final uri = GoRouterState.of(context).uri; + _source = uri.queryParameters['source']; + _previousConversationId = uri.queryParameters['conversationId']; + } + + void _handleBackNavigation() { + if (_source == 'chat' && _previousConversationId != null) { + // Go back to the previous conversation + context.go('/ai-tutor/$_previousConversationId'); + } else if (_source == 'chat') { + // Go to new chat (no previous conversation) + context.go('/ai-tutor'); + } else if (_source == 'intro') { + // Go back to intro screen + context.go('/ai-tutor'); + } else { + // Default: go to intro screen (from dashboard or other sources) + context.go('/ai-tutor'); + } + } + + /// Fetch material names for given material IDs + Future _loadMaterialNames(List materialIds) async { + if (materialIds.isEmpty) return; + + final uncachedIds = materialIds + .where((id) => !_materialNamesCache.containsKey(id)) + .toList(); + if (uncachedIds.isEmpty) return; + + try { + final batches = >[]; + for (int i = 0; i < uncachedIds.length; i += 10) { + final batch = uncachedIds.skip(i).take(10).toList(); + batches.add( + FirebaseFirestore.instance + .collection('materials') + .where(FieldPath.documentId, whereIn: batch) + .get(), + ); + } + + final results = await Future.wait(batches); + for (final snapshot in results) { + for (final doc in snapshot.docs) { + final data = doc.data() as Map?; + if (data != null) { + final fileName = data['fileName'] as String?; + if (fileName != null) { + _materialNamesCache[doc.id] = fileName; + } + } + } + } + + if (mounted) { + setState(() {}); + } + } catch (e) { + Logger.error('Error loading material names: $e'); + } + } + @override void dispose() { _searchController.dispose(); @@ -38,6 +109,17 @@ class _ChatHistoryPageState extends State { setState(() => _isLoading = true); try { final conversations = await ChatMemoryService.getConversations(); + + // Collect all material IDs from all conversations + final allMaterialIds = []; + for (final conv in conversations) { + final materialIds = conv['selectedMaterialIds'] as List? ?? []; + allMaterialIds.addAll(materialIds); + } + + // Load material names + await _loadMaterialNames(allMaterialIds); + if (mounted) { setState(() { _conversations = conversations; @@ -269,159 +351,302 @@ class _ChatHistoryPageState extends State { @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; + final isDark = Theme.of(context).brightness == Brightness.dark; + final orangeAccent = isDark + ? const Color(0xFFC47A2A) + : const Color(0xFFF68D2D); + final lightOrange = isDark + ? const Color(0xFFD89035) + : const Color(0xFFF7A960); - return Scaffold( - backgroundColor: cs.surfaceContainerLowest, - appBar: AppBar( - backgroundColor: cs.surface, - elevation: 0, - leading: IconButton( - icon: Icon(Icons.arrow_back, color: cs.onSurface), - onPressed: () => context.go('/student-dashboard'), - ), - title: Text( - _isSelectionMode - ? '${_selectedConversationIds.length} selecionadas' - : 'Histórico de Conversas', - style: TextStyle( - color: cs.onSurface, - fontSize: 18, - fontWeight: FontWeight.bold, + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) { + if (didPop) return; + _handleBackNavigation(); + }, + child: Scaffold( + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: isDark + ? [ + cs.surfaceVariant, + orangeAccent.withValues(alpha: 0.3), + cs.surfaceContainerLowest, + ] + : [ + cs.primary.withValues(alpha: 0.08), + orangeAccent.withValues(alpha: 0.05), + cs.surfaceContainerLowest, + ], + stops: const [0.0, 0.4, 1.0], + ), ), - ), - actions: [ - if (_isSelectionMode) - IconButton( - icon: Icon(Icons.delete, color: Colors.red), - onPressed: _selectedConversationIds.isEmpty - ? null - : _deleteSelectedConversations, - ) - else - IconButton( - icon: Icon(Icons.checklist, color: cs.onSurface), - onPressed: _toggleSelectionMode, - ), - if (!_isSelectionMode) - IconButton( - icon: Icon(Icons.refresh, color: cs.onSurface), - onPressed: _loadConversations, - ), - if (_isSelectionMode) - IconButton( - icon: Icon(Icons.close, color: cs.onSurface), - onPressed: _toggleSelectionMode, - ), - ], - ), - body: Column( - children: [ - // Search and filters - Padding( - padding: const EdgeInsets.all(16), + child: SafeArea( + top: false, child: Column( children: [ - // Search bar - TextField( - controller: _searchController, - decoration: InputDecoration( - hintText: 'Pesquisar conversas...', - prefixIcon: const Icon(Icons.search), - suffixIcon: _searchController.text.isNotEmpty - ? IconButton( - icon: const Icon(Icons.clear), - onPressed: () { - _searchController.clear(); - }, - ) - : null, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide( - color: cs.outline.withValues(alpha: 0.3), - ), - ), - filled: true, - fillColor: cs.surface, + // Custom app bar + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 15, ), - ), - const SizedBox(height: 12), - // Date filter chips - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - _buildDateFilterChip('Todas', 'all', cs), - const SizedBox(width: 8), - _buildDateFilterChip('Hoje', 'today', cs), - const SizedBox(width: 8), - _buildDateFilterChip('Ontem', 'yesterday', cs), - const SizedBox(width: 8), - _buildDateFilterChip('Última semana', 'week', cs), - const SizedBox(width: 8), - _buildDateFilterChip('Último mês', 'month', cs), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [cs.primary, orangeAccent], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + boxShadow: [ + BoxShadow( + color: orangeAccent.withValues(alpha: 0.2), + blurRadius: 8, + offset: const Offset(0, 2), + ), ], ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + icon: const Icon( + Icons.arrow_back_ios_new, + color: Colors.white, + size: 20, + ), + onPressed: _handleBackNavigation, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + _isSelectionMode + ? '${_selectedConversationIds.length} selecionadas' + : 'Histórico', + style: const TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + if (_isSelectionMode) + IconButton( + icon: const Icon(Icons.delete, color: Colors.white), + onPressed: _selectedConversationIds.isEmpty + ? null + : _deleteSelectedConversations, + ) + else + IconButton( + icon: const Icon( + Icons.checklist, + color: Colors.white, + ), + onPressed: _toggleSelectionMode, + ), + if (!_isSelectionMode) + IconButton( + icon: const Icon(Icons.refresh, color: Colors.white), + onPressed: _loadConversations, + ), + if (_isSelectionMode) + IconButton( + icon: const Icon(Icons.close, color: Colors.white), + onPressed: _toggleSelectionMode, + ), + ], + ), + ), + // Search and filters + Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + // Search bar + TextField( + controller: _searchController, + decoration: InputDecoration( + hintText: 'Pesquisar conversas...', + prefixIcon: const Icon(Icons.search), + suffixIcon: _searchController.text.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + _searchController.clear(); + }, + ) + : null, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide( + color: orangeAccent.withValues(alpha: 0.3), + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide( + color: orangeAccent.withValues(alpha: 0.3), + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: const BorderSide( + color: AppColors.primaryOrange, + width: 2, + ), + ), + filled: true, + fillColor: cs.surface, + ), + ), + const SizedBox(height: 12), + // Date filter chips + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + _buildDateFilterChip( + 'Todas', + 'all', + cs, + orangeAccent, + ), + const SizedBox(width: 8), + _buildDateFilterChip( + 'Hoje', + 'today', + cs, + orangeAccent, + ), + const SizedBox(width: 8), + _buildDateFilterChip( + 'Ontem', + 'yesterday', + cs, + orangeAccent, + ), + const SizedBox(width: 8), + _buildDateFilterChip( + 'Semana', + 'week', + cs, + orangeAccent, + ), + const SizedBox(width: 8), + _buildDateFilterChip( + 'Mês', + 'month', + cs, + orangeAccent, + ), + ], + ), + ), + ], + ), + ), + // Conversation list + Expanded( + child: _isLoading + ? Center( + child: CircularProgressIndicator(color: orangeAccent), + ) + : _filteredConversations.isEmpty + ? Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [cs.primary, orangeAccent], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: orangeAccent.withValues( + alpha: 0.2, + ), + blurRadius: 16, + offset: const Offset(0, 4), + ), + ], + ), + child: const Icon( + Icons.chat_bubble_outline, + color: Colors.white, + size: 40, + ), + ), + const SizedBox(height: 20), + Text( + _conversations.isEmpty + ? 'Sem conversas ainda' + : 'Nenhuma conversa encontrada', + style: TextStyle( + color: cs.onSurface, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + Text( + _conversations.isEmpty + ? 'Começa uma conversa com o Vico!' + : 'Tenta ajustar os filtros', + style: TextStyle( + color: cs.onSurfaceVariant.withValues( + alpha: 0.7, + ), + fontSize: 14, + ), + ), + ], + ), + ), + ) + : ListView.separated( + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: _filteredConversations.length, + separatorBuilder: (_, __) => + const SizedBox(height: 12), + itemBuilder: (context, index) { + final conversation = _filteredConversations[index]; + return _buildConversationCard( + conversation, + cs, + orangeAccent, + lightOrange, + ); + }, + ), ), ], ), ), - // Conversation list - Expanded( - child: _isLoading - ? const Center(child: CircularProgressIndicator()) - : _filteredConversations.isEmpty - ? Center( - child: Padding( - padding: const EdgeInsets.all(32), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.chat_bubble_outline, - size: 64, - color: cs.onSurfaceVariant.withValues(alpha: 0.4), - ), - const SizedBox(height: 16), - Text( - _conversations.isEmpty - ? 'Sem conversas ainda' - : 'Nenhuma conversa encontrada', - style: TextStyle( - color: cs.onSurfaceVariant, - fontSize: 16, - ), - ), - const SizedBox(height: 8), - Text( - _conversations.isEmpty - ? 'Começa uma conversa com o Vico!' - : 'Tenta ajustar os filtros', - style: TextStyle( - color: cs.onSurfaceVariant.withValues(alpha: 0.7), - fontSize: 14, - ), - ), - ], - ), - ), - ) - : ListView.separated( - padding: const EdgeInsets.all(16), - itemCount: _filteredConversations.length, - separatorBuilder: (_, __) => const SizedBox(height: 12), - itemBuilder: (context, index) { - final conversation = _filteredConversations[index]; - return _buildConversationCard(conversation, cs); - }, - ), - ), - ], + ), ), ); } - Widget _buildDateFilterChip(String label, String value, ColorScheme cs) { + Widget _buildDateFilterChip( + String label, + String value, + ColorScheme cs, + Color orangeAccent, + ) { final isSelected = _selectedDateFilter == value; return FilterChip( label: Text(label), @@ -433,13 +658,13 @@ class _ChatHistoryPageState extends State { _filterConversations(); }, backgroundColor: cs.surface, - selectedColor: cs.primary.withValues(alpha: 0.2), + selectedColor: orangeAccent.withValues(alpha: 0.2), labelStyle: TextStyle( - color: isSelected ? cs.primary : cs.onSurface, + color: isSelected ? orangeAccent : cs.onSurface, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, ), side: BorderSide( - color: isSelected ? cs.primary : cs.outline.withValues(alpha: 0.3), + color: isSelected ? orangeAccent : cs.outline.withValues(alpha: 0.3), ), ); } @@ -447,6 +672,8 @@ class _ChatHistoryPageState extends State { Widget _buildConversationCard( Map conversation, ColorScheme cs, + Color orangeAccent, + Color lightOrange, ) { final isSelected = _selectedConversationIds.contains(conversation['id']); @@ -476,22 +703,25 @@ class _ChatHistoryPageState extends State { context.go('/ai-tutor/${conversation['id']}'); } }, - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(20), child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: isSelected ? cs.primary.withValues(alpha: 0.1) : cs.surface, - borderRadius: BorderRadius.circular(16), + color: isSelected + ? orangeAccent.withValues(alpha: 0.1) + : cs.surface, + borderRadius: BorderRadius.circular(20), border: Border.all( color: isSelected - ? cs.primary - : cs.outline.withValues(alpha: 0.15), + ? orangeAccent + : orangeAccent.withValues(alpha: 0.2), + width: isSelected ? 2 : 1.5, ), boxShadow: [ BoxShadow( - color: cs.shadow.withValues(alpha: 0.05), - blurRadius: 8, - offset: const Offset(0, 2), + color: orangeAccent.withValues(alpha: 0.08), + blurRadius: 12, + offset: const Offset(0, 4), ), ], ), @@ -507,6 +737,12 @@ class _ChatHistoryPageState extends State { value: isSelected, onChanged: (_) => _toggleConversationSelection(conversation['id']), + fillColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return orangeAccent; + } + return null; + }), ), ) else @@ -515,12 +751,18 @@ class _ChatHistoryPageState extends State { height: 48, decoration: BoxDecoration( gradient: LinearGradient( - colors: [ - cs.primary, - cs.primary.withValues(alpha: 0.7), - ], + colors: [cs.primary, orangeAccent], + begin: Alignment.topLeft, + end: Alignment.bottomRight, ), - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(14), + boxShadow: [ + BoxShadow( + color: orangeAccent.withValues(alpha: 0.2), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], ), child: const Icon( Icons.chat_bubble, @@ -540,7 +782,7 @@ class _ChatHistoryPageState extends State { fontSize: 16, fontWeight: FontWeight.bold, ), - maxLines: 1, + maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), @@ -590,26 +832,35 @@ class _ChatHistoryPageState extends State { runSpacing: 6, children: (conversation['selectedMaterialIds'] as List) .take(3) - .map( - (id) => Container( + .map((id) { + final materialId = id as String; + final displayName = + _materialNamesCache[materialId] ?? materialId; + return Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( - color: cs.primary.withValues(alpha: 0.1), + color: lightOrange.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(8), + border: Border.all( + color: lightOrange.withValues(alpha: 0.3), + width: 1, + ), ), child: Text( - id.length > 15 ? '${id.substring(0, 15)}...' : id, + displayName.length > 20 + ? '${displayName.substring(0, 20)}...' + : displayName, style: TextStyle( - color: cs.primary, + color: lightOrange, fontSize: 11, fontWeight: FontWeight.w500, ), ), - ), - ) + ); + }) .toList(), ), ), 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 06abc97..48920f3 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 @@ -7,6 +7,7 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import '../../../../core/services/chat_memory_service.dart'; import '../../../../core/services/materials_rag_service.dart'; import '../../../../core/services/rag_ai_service.dart'; +import '../../../../core/theme/app_colors.dart'; import '../../../../core/utils/logger.dart'; import '../../../materials/presentation/pages/pdf_viewer_page.dart'; @@ -74,6 +75,7 @@ class _TutorChatPageSimpleState extends State Future _loadConversation(String conversationId) async { try { + Logger.info('Loading conversation: $conversationId'); final conversation = await ChatMemoryService.getConversation( conversationId, ); @@ -98,19 +100,32 @@ class _TutorChatPageSimpleState extends State if (mounted) { setState(() { _selectedMaterialIds = materialIds; - _materialsConfirmed = materialIds.isNotEmpty; + // Always set to true when loading from history to allow chat access + _materialsConfirmed = true; + }); + } + } else { + // No materials in conversation, but still allow chat access + if (mounted) { + setState(() { + _materialsConfirmed = true; }); } } } catch (e) { Logger.error('Error loading material IDs: $e'); - // Continue without materials rather than crash + // Continue without materials but allow chat access + if (mounted) { + setState(() { + _materialsConfirmed = true; + }); + } } // Load messages from Firestore with safe casting final loadedMessages = messages.map((msg) { try { - return { + return { 'content': msg['content']?.toString() ?? '', 'isUser': msg['role']?.toString() == 'user', 'timestamp': msg['createdAt'] is Timestamp @@ -120,7 +135,7 @@ class _TutorChatPageSimpleState extends State } catch (e) { Logger.error('Error mapping message: $e'); // Return a safe fallback message - return { + return { 'content': '[Mensagem indisponível]', 'isUser': false, 'timestamp': DateTime.now(), @@ -130,9 +145,14 @@ class _TutorChatPageSimpleState extends State if (mounted) { setState(() { - _messages = loadedMessages; + _messages.clear(); + _messages.addAll(loadedMessages); + _isLoading = false; // Ensure loading state is reset }); _scrollToBottom(); + Logger.info( + 'Conversation loaded. _messages count: ${_messages.length}, _isLoading: $_isLoading, _materialsConfirmed: $_materialsConfirmed', + ); } } catch (e) { Logger.error('Error loading conversation: $e'); @@ -189,9 +209,11 @@ class _TutorChatPageSimpleState extends State ], ), child: SafeArea( + top: false, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ IconButton( onPressed: () => context.go('/student-dashboard'), @@ -205,13 +227,15 @@ class _TutorChatPageSimpleState extends State width: 38, height: 38, decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.2), + color: const Color(0xFFF9EEE8), borderRadius: BorderRadius.circular(12), ), - child: const Icon( - Icons.school, - color: Colors.white, - size: 22, + child: Padding( + padding: const EdgeInsets.all(6), + child: Image.asset( + 'assets/images/epvc.png', + fit: BoxFit.contain, + ), ), ), const SizedBox(width: 10), @@ -282,49 +306,63 @@ class _TutorChatPageSimpleState extends State ), ), IconButton( - onPressed: () => context.go('/chat-history'), + onPressed: () { + final currentId = + ChatMemoryService.currentConversationId; + // If materials are confirmed, we're in chat mode + final source = _materialsConfirmed ? 'chat' : 'intro'; + if (currentId != null) { + context.go( + '/chat-history?source=$source&conversationId=$currentId', + ); + } else { + context.go('/chat-history?source=$source'); + } + }, icon: const Icon( Icons.history, color: Colors.white, size: 20, ), ), - IconButton( - onPressed: () async { - final confirmed = await showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('Nova conversa'), - content: const Text( - 'Isto vai limpar o chat atual e guardá-lo no histórico. Continuar?', - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context, false), - child: const Text('Cancelar'), + if (_materialsConfirmed) + IconButton( + onPressed: () async { + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Nova conversa'), + content: const Text( + 'Isto vai limpar o chat atual e guardá-lo no histórico. Continuar?', ), - TextButton( - onPressed: () => Navigator.pop(context, true), - style: TextButton.styleFrom( - foregroundColor: Colors.red, + actions: [ + TextButton( + onPressed: () => + Navigator.pop(context, false), + child: const Text('Cancelar'), ), - child: const Text('Limpar'), - ), - ], - ), - ); - if (confirmed == true) { - ChatMemoryService.setCurrentConversationId(null); - context.go('/ai-tutor'); - } - }, - icon: const Icon( - Icons.add_comment, - color: Colors.white, - size: 20, + TextButton( + onPressed: () => Navigator.pop(context, true), + style: TextButton.styleFrom( + foregroundColor: Colors.red, + ), + child: const Text('Limpar'), + ), + ], + ), + ); + if (confirmed == true) { + ChatMemoryService.setCurrentConversationId(null); + context.go('/ai-tutor'); + } + }, + icon: const Icon( + Icons.add_comment, + color: Colors.white, + size: 20, + ), + tooltip: 'Nova conversa', ), - tooltip: 'Nova conversa', - ), const SizedBox(width: 4), ], ), @@ -342,92 +380,300 @@ class _TutorChatPageSimpleState extends State // ── Intro screen shown before any material is selected ────────────────── Widget _buildIntroScreen(BuildContext context) { final cs = Theme.of(context).colorScheme; + final isDark = Theme.of(context).brightness == Brightness.dark; + final orangeAccent = isDark + ? const Color(0xFFC47A2A) + : const Color(0xFFF68D2D); + final lightOrange = isDark + ? const Color(0xFFD89035) + : const Color(0xFFF7A960); + return SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 28), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: 88, - height: 88, - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [cs.primary, cs.primary.withValues(alpha: 0.7)], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: BorderRadius.circular(28), - boxShadow: [ - BoxShadow( - color: cs.primary.withValues(alpha: 0.3), - blurRadius: 20, - offset: const Offset(0, 8), - ), - ], - ), - child: const Icon(Icons.school, color: Colors.white, size: 44), - ), - const SizedBox(height: 24), - Text( - 'Olá! Sou o Vico', - style: TextStyle( - fontSize: 26, - fontWeight: FontWeight.bold, - color: cs.onSurface, - ), - ), - const SizedBox(height: 12), - Text( - 'O teu assistente de estudo inteligente.\nRespondo com base nos materiais do teu professor, ajudo-te a perceber conceitos e a preparares-te para os testes.', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 15, - color: cs.onSurfaceVariant, - height: 1.55, - ), - ), - const SizedBox(height: 36), - _availableMaterials.isEmpty - ? Column( - children: [ - CircularProgressIndicator(color: cs.primary), - const SizedBox(height: 16), - Text( - 'A carregar materiais\u2026', - style: TextStyle( - fontSize: 13, - color: cs.onSurfaceVariant, + top: false, + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: isDark + ? [ + cs.surfaceVariant, + orangeAccent.withValues(alpha: 0.3), + cs.surfaceContainerLowest, + ] + : [ + cs.primary.withValues(alpha: 0.08), + orangeAccent.withValues(alpha: 0.05), + cs.surfaceContainerLowest, + ], + stops: const [0.0, 0.4, 1.0], + ), + ), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: constraints.maxHeight), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.only( + left: 24, + right: 24, + bottom: 24, + top: 52, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Spacer(flex: 2), + // Avatar with animated-like glow (teal to orange gradient) + Container( + width: 140, + height: 140, + decoration: BoxDecoration( + color: const Color(0xFFF9EEE8), + borderRadius: BorderRadius.circular(32), + boxShadow: [ + BoxShadow( + color: orangeAccent.withValues(alpha: 0.3), + blurRadius: 24, + offset: const Offset(0, 10), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.all(20), + child: Image.asset( + 'assets/images/epvc.png', + fit: BoxFit.contain, + ), + ), ), - ), - ], - ) - : SizedBox( - width: double.infinity, - child: FilledButton.icon( - onPressed: () => _showMaterialsPicker(allowEmpty: true), - icon: const Icon(Icons.folder_open_rounded), - label: const Text( - 'Escolher materiais para estudar', - style: TextStyle(fontSize: 15), - ), - style: FilledButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: 24, - vertical: 16, + const SizedBox(height: 28), + // Title with orange accent + RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: [ + TextSpan( + text: 'Olá! Sou o ', + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: cs.onSurface, + ), + ), + TextSpan( + text: 'Vico', + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: orangeAccent, + ), + ), + ], + ), ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14), + const SizedBox(height: 12), + // Subtitle + Text( + 'O teu assistente de estudo inteligente', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: cs.onSurfaceVariant, + ), ), - ), + const SizedBox(height: 16), + // Description card with orange border accent + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: cs.surface, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: orangeAccent.withValues(alpha: 0.2), + width: 1.5, + ), + boxShadow: [ + BoxShadow( + color: orangeAccent.withValues(alpha: 0.08), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + children: [ + _buildFeatureRow( + icon: Icons.menu_book, + text: + 'Respondo com base nos materiais do teu professor', + cs: cs, + iconColor: cs.primary, + ), + const SizedBox(height: 14), + _buildFeatureRow( + icon: Icons.psychology, + text: 'Ajudo-te a perceber conceitos complexos', + cs: cs, + iconColor: orangeAccent, + ), + const SizedBox(height: 14), + _buildFeatureRow( + icon: Icons.quiz, + text: 'Prepara-te para testes e exames', + cs: cs, + iconColor: lightOrange, + ), + ], + ), + ), + const Spacer(flex: 3), + // Action button + _availableMaterials.isEmpty + ? Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator( + color: orangeAccent, + strokeWidth: 2.5, + ), + const SizedBox(height: 16), + Text( + 'A carregar materiais…', + style: TextStyle( + fontSize: 14, + color: cs.onSurfaceVariant, + ), + ), + ], + ) + : Container( + width: double.infinity, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [cs.primary, orangeAccent], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: orangeAccent.withValues( + alpha: 0.3, + ), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], + ), + child: FilledButton.icon( + onPressed: () => + _showMaterialsPicker(allowEmpty: true), + icon: const Icon( + Icons.folder_open_rounded, + size: 22, + ), + label: const Text( + 'Escolher materiais para estudar', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + style: FilledButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 28, + vertical: 18, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + elevation: 0, + backgroundColor: Colors.transparent, + shadowColor: Colors.transparent, + ), + ), + ), + const SizedBox(height: 16), + // Secondary hint with orange accent + RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: [ + TextSpan( + text: 'Seleciona um ou mais materiais para ', + style: TextStyle( + fontSize: 13, + color: cs.onSurfaceVariant.withValues( + alpha: 0.7, + ), + ), + ), + TextSpan( + text: 'começar', + style: TextStyle( + fontSize: 13, + color: orangeAccent, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + const SizedBox(height: 8), + ], ), ), - ], + ), + ), + ); + }, ), ), ); } + Widget _buildFeatureRow({ + required IconData icon, + required String text, + required ColorScheme cs, + Color? iconColor, + }) { + final effectiveIconColor = iconColor ?? cs.primary; + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: effectiveIconColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Icon(icon, color: effectiveIconColor, size: 20), + ), + const SizedBox(width: 14), + Expanded( + child: Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + text, + style: TextStyle( + fontSize: 14, + color: cs.onSurfaceVariant, + height: 1.4, + ), + ), + ), + ), + ], + ); + } + // ── Chat body (messages + input) ────────────────────────────────────────── Widget _buildChatBody(BuildContext context) { final cs = Theme.of(context).colorScheme; @@ -665,9 +911,7 @@ class _TutorChatPageSimpleState extends State width: 32, height: 32, decoration: BoxDecoration( - gradient: LinearGradient( - colors: [cs.primary, cs.primary.withValues(alpha: 0.7)], - ), + color: const Color(0xFFF9EEE8), borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( @@ -677,7 +921,10 @@ class _TutorChatPageSimpleState extends State ), ], ), - child: const Icon(Icons.school, color: Colors.white, size: 18), + child: Padding( + padding: const EdgeInsets.all(6), + child: Image.asset('assets/images/epvc.png', fit: BoxFit.contain), + ), ); } @@ -956,6 +1203,9 @@ class _TutorChatPageSimpleState extends State onSubmitted: (_) => _handleSendMessage(), textInputAction: TextInputAction.send, onChanged: (value) { + Logger.info( + 'TextField changed. Text: "$value", isEmpty: ${value.isEmpty}', + ); setState(() {}); }, ), @@ -1457,6 +1707,9 @@ class _TutorChatPageSimpleState extends State if (_messageController.text.trim().isEmpty) return; final userMessage = _messageController.text.trim(); + Logger.info( + 'Attempting to send message. currentConversationId: ${ChatMemoryService.currentConversationId}', + ); // Update conversation title if it's still the default "Nova conversa" final currentId = ChatMemoryService.currentConversationId; @@ -1473,7 +1726,7 @@ class _TutorChatPageSimpleState extends State // Add user message setState(() { - _messages.add({ + _messages.add({ 'content': userMessage, 'isUser': true, 'timestamp': DateTime.now(), @@ -1506,7 +1759,7 @@ class _TutorChatPageSimpleState extends State Logger.info('Ollama response received: $preview...'); setState(() { - _messages.add({ + _messages.add({ 'content': replyText, 'isUser': false, 'timestamp': DateTime.now(), @@ -1520,7 +1773,7 @@ class _TutorChatPageSimpleState extends State 'Desculpe, ocorreu um erro ao processar a pergunta. Tente novamente.'; setState(() { - _messages.add({ + _messages.add({ 'content': aiResponse, 'isUser': false, 'timestamp': DateTime.now(), diff --git a/lib/features/analytics/presentation/pages/analytics_page.dart b/lib/features/analytics/presentation/pages/analytics_page.dart index 6e62083..f0521e7 100644 --- a/lib/features/analytics/presentation/pages/analytics_page.dart +++ b/lib/features/analytics/presentation/pages/analytics_page.dart @@ -108,15 +108,22 @@ class _AnalyticsPageState extends State ), ), child: SafeArea( + top: false, bottom: false, child: Column( children: [ // Header Container( - padding: const EdgeInsets.all(24), + padding: const EdgeInsets.only( + left: 24, + right: 24, + bottom: 28, + top: 52, + ), child: Column( children: [ Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ IconButton( icon: const Icon( diff --git a/lib/features/auth/presentation/pages/login_page.dart b/lib/features/auth/presentation/pages/login_page.dart index 23d33b3..d0ce7d7 100644 --- a/lib/features/auth/presentation/pages/login_page.dart +++ b/lib/features/auth/presentation/pages/login_page.dart @@ -228,9 +228,15 @@ class _LoginPageState extends State { ), ), child: SafeArea( + top: false, child: Center( child: SingleChildScrollView( - padding: const EdgeInsets.all(24.0), + padding: const EdgeInsets.only( + left: 24.0, + right: 24.0, + bottom: 28.0, + top: 52.0, + ), child: Form( key: _formKey, child: Column( diff --git a/lib/features/auth/presentation/pages/role_selection_page.dart b/lib/features/auth/presentation/pages/role_selection_page.dart index 12b607e..beaab8c 100644 --- a/lib/features/auth/presentation/pages/role_selection_page.dart +++ b/lib/features/auth/presentation/pages/role_selection_page.dart @@ -38,8 +38,14 @@ class _RoleSelectionPageState extends State { // Main content SafeArea( + top: false, child: Padding( - padding: const EdgeInsets.all(24.0), + padding: const EdgeInsets.only( + left: 24.0, + right: 24.0, + bottom: 28.0, + top: 52.0, + ), child: Column( children: [ const Spacer(flex: 1), diff --git a/lib/features/auth/presentation/pages/signup_page.dart b/lib/features/auth/presentation/pages/signup_page.dart index bc69205..adf1e62 100644 --- a/lib/features/auth/presentation/pages/signup_page.dart +++ b/lib/features/auth/presentation/pages/signup_page.dart @@ -257,9 +257,15 @@ class _SignupPageState extends State { ), ), child: SafeArea( + top: false, child: Center( child: SingleChildScrollView( - padding: const EdgeInsets.all(24.0), + padding: const EdgeInsets.only( + left: 24.0, + right: 24.0, + bottom: 28.0, + top: 52.0, + ), child: Form( key: _formKey, child: Column( diff --git a/lib/features/classes/presentation/pages/join_class_page.dart b/lib/features/classes/presentation/pages/join_class_page.dart index 840f1c0..f094b84 100644 --- a/lib/features/classes/presentation/pages/join_class_page.dart +++ b/lib/features/classes/presentation/pages/join_class_page.dart @@ -194,12 +194,19 @@ class _JoinClassPageState extends ConsumerState { ), ), child: SafeArea( + top: false, child: Column( children: [ // Custom AppBar Container( - padding: const EdgeInsets.all(16.0), + padding: const EdgeInsets.only( + left: 16.0, + right: 16.0, + bottom: 20.0, + top: 52.0, + ), child: Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ IconButton( icon: Icon( diff --git a/lib/features/dashboard/presentation/pages/student_dashboard_page.dart b/lib/features/dashboard/presentation/pages/student_dashboard_page.dart index ba1b453..9e0ac41 100644 --- a/lib/features/dashboard/presentation/pages/student_dashboard_page.dart +++ b/lib/features/dashboard/presentation/pages/student_dashboard_page.dart @@ -11,6 +11,14 @@ import '../widgets/profile_section_widget.dart'; class StudentDashboardPage extends StatefulWidget { const StudentDashboardPage({super.key}); + /// Clear the cached user name (call when name is updated in settings) + static void clearCachedUserName() { + _cachedUserName = null; + } + + /// Cached user name to prevent flickering + static String? _cachedUserName; + @override State createState() => _StudentDashboardPageState(); } @@ -21,7 +29,12 @@ class _StudentDashboardPageState extends State { @override void initState() { super.initState(); - _checkRoleAndLoadData(); + // Use cached name if available, otherwise load data + if (StudentDashboardPage._cachedUserName != null) { + _userName = StudentDashboardPage._cachedUserName!; + } else { + _checkRoleAndLoadData(); + } } Future _checkRoleAndLoadData() async { @@ -75,6 +88,7 @@ class _StudentDashboardPageState extends State { print('DEBUG: Final displayName to use: "$displayName"'); setState(() { _userName = displayName; + StudentDashboardPage._cachedUserName = displayName; }); } } @@ -99,14 +113,21 @@ class _StudentDashboardPageState extends State { ), ), child: SafeArea( + top: false, child: SingleChildScrollView( child: Padding( - padding: const EdgeInsets.all(24.0), + padding: const EdgeInsets.only( + left: 24.0, + right: 24.0, + bottom: 28.0, + top: 52.0, + ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header with logout and settings Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: Column( diff --git a/lib/features/dashboard/presentation/pages/teacher_dashboard_page.dart b/lib/features/dashboard/presentation/pages/teacher_dashboard_page.dart index dc93c86..f70b53a 100644 --- a/lib/features/dashboard/presentation/pages/teacher_dashboard_page.dart +++ b/lib/features/dashboard/presentation/pages/teacher_dashboard_page.dart @@ -94,14 +94,21 @@ class _TeacherDashboardPageState extends State { ), ), child: SafeArea( + top: false, child: SingleChildScrollView( child: Padding( - padding: const EdgeInsets.all(24.0), + padding: const EdgeInsets.only( + left: 24.0, + right: 24.0, + bottom: 28.0, + top: 52.0, + ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header with logout and settings Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: Column( diff --git a/lib/features/dashboard/presentation/widgets/progress_hero_widget.dart b/lib/features/dashboard/presentation/widgets/progress_hero_widget.dart index 736b550..3406374 100644 --- a/lib/features/dashboard/presentation/widgets/progress_hero_widget.dart +++ b/lib/features/dashboard/presentation/widgets/progress_hero_widget.dart @@ -17,12 +17,22 @@ class ProgressHeroWidget extends StatefulWidget { } class _ProgressHeroWidgetState extends State { + UserStats? _cachedUserStats; + bool _isFirstLoad = true; + @override Widget build(BuildContext context) { return FutureBuilder( future: _loadUserStats(), builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { + // Show cached data while loading to prevent flickering + if (snapshot.connectionState == ConnectionState.waiting && + _cachedUserStats != null) { + return _buildContent(_cachedUserStats); + } + + if (snapshot.connectionState == ConnectionState.waiting && + _isFirstLoad) { return _buildLoadingState(); } @@ -31,6 +41,10 @@ class _ProgressHeroWidgetState extends State { } final userStats = snapshot.data; + if (userStats != null) { + _cachedUserStats = userStats; + _isFirstLoad = false; + } return _buildContent(userStats); }, ); diff --git a/lib/features/dashboard/presentation/widgets/quick_access_widget.dart b/lib/features/dashboard/presentation/widgets/quick_access_widget.dart index eb93881..471e6bb 100644 --- a/lib/features/dashboard/presentation/widgets/quick_access_widget.dart +++ b/lib/features/dashboard/presentation/widgets/quick_access_widget.dart @@ -107,6 +107,21 @@ class QuickAccessWidget extends StatelessWidget { titleFontSize: _titleFontSize, subtitleFontSize: _subtitleFontSize, padding: _cardPadding, + leadingIcon: Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: const Color(0xFFF9EEE8), + borderRadius: BorderRadius.circular(10), + ), + child: Padding( + padding: const EdgeInsets.all(8), + child: Image.asset( + 'assets/images/epvc.png', + fit: BoxFit.contain, + ), + ), + ), onTap: () => context.go('/ai-tutor'), ) .animate() @@ -227,6 +242,7 @@ class QuickAccessWidget extends StatelessWidget { subtitle: 'Assistente de estudos', icon: Icons.psychology, useGradient: true, + useCustomIcon: true, onTap: () { Navigator.pop(context); context.go('/ai-tutor'); @@ -330,29 +346,47 @@ class QuickAccessWidget extends StatelessWidget { ), ), child: ListTile( - leading: Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: item.useGradient - ? Theme.of( - context, - ).colorScheme.primary.withOpacity(0.1) - : (item.iconColor ?? - Theme.of( - context, - ).colorScheme.secondary) - .withOpacity(0.1), - borderRadius: BorderRadius.circular(10), - ), - child: Icon( - item.icon, - color: item.useGradient - ? Theme.of(context).colorScheme.primary - : (item.iconColor ?? - Theme.of(context).colorScheme.secondary), - size: 24, - ), - ), + leading: item.useCustomIcon + ? Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: const Color(0xFFF9EEE8), + borderRadius: BorderRadius.circular(10), + ), + child: Padding( + padding: const EdgeInsets.all(8), + child: Image.asset( + 'assets/images/epvc.png', + fit: BoxFit.contain, + ), + ), + ) + : Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: item.useGradient + ? Theme.of( + context, + ).colorScheme.primary.withOpacity(0.1) + : (item.iconColor ?? + Theme.of( + context, + ).colorScheme.secondary) + .withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + item.icon, + color: item.useGradient + ? Theme.of(context).colorScheme.primary + : (item.iconColor ?? + Theme.of( + context, + ).colorScheme.secondary), + size: 24, + ), + ), title: Text( item.title, style: TextStyle( @@ -395,6 +429,7 @@ class _QuickAccessItem { final IconData icon; final bool useGradient; final Color? iconColor; + final bool useCustomIcon; final VoidCallback onTap; _QuickAccessItem({ @@ -403,6 +438,7 @@ class _QuickAccessItem { required this.icon, this.useGradient = false, this.iconColor, + this.useCustomIcon = false, required this.onTap, }); } diff --git a/lib/features/materials/presentation/pages/content_management_page.dart b/lib/features/materials/presentation/pages/content_management_page.dart index fe1850c..0a4278a 100644 --- a/lib/features/materials/presentation/pages/content_management_page.dart +++ b/lib/features/materials/presentation/pages/content_management_page.dart @@ -326,11 +326,18 @@ class _ContentManagementPageState extends State { child: Column( children: [ SafeArea( + top: false, child: Padding( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.only( + left: 16, + right: 16, + bottom: 20, + top: 52, + ), child: Column( children: [ Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ IconButton( icon: Icon(Icons.arrow_back, color: cs.onSurface), diff --git a/lib/features/materials/presentation/pages/pdf_viewer_page.dart b/lib/features/materials/presentation/pages/pdf_viewer_page.dart index ef3117d..3482d87 100644 --- a/lib/features/materials/presentation/pages/pdf_viewer_page.dart +++ b/lib/features/materials/presentation/pages/pdf_viewer_page.dart @@ -61,8 +61,14 @@ class _PdfViewerPageState extends State { child: Column( children: [ SafeArea( + top: false, child: Container( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.only( + left: 16, + right: 16, + bottom: 20, + top: 52, + ), decoration: BoxDecoration( color: cs.surface.withOpacity(0.8), boxShadow: [ @@ -74,6 +80,7 @@ class _PdfViewerPageState extends State { ], ), child: Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ IconButton( icon: Icon(Icons.arrow_back, color: cs.onSurface), diff --git a/lib/features/materials/presentation/pages/teacher_materials_page.dart b/lib/features/materials/presentation/pages/teacher_materials_page.dart index a285e1a..08bca96 100644 --- a/lib/features/materials/presentation/pages/teacher_materials_page.dart +++ b/lib/features/materials/presentation/pages/teacher_materials_page.dart @@ -200,117 +200,126 @@ class _TeacherMaterialsPageState extends State { Widget _buildClassTab({required String classId}) { return SafeArea( + top: false, child: Stack( children: [ - StreamBuilder( - stream: _getMaterialsStream(classId), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center( - child: CircularProgressIndicator(color: Color(0xFF82C9BD)), - ); - } - - if (snapshot.hasError) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - Icons.error_outline, - color: Colors.red, - size: 48, - ), - const SizedBox(height: 16), - Text( - 'Erro ao carregar materiais:\n${snapshot.error}', - textAlign: TextAlign.center, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface, - fontSize: 16, - ), - ), - ], - ), - ); - } - - final materials = snapshot.data?.docs ?? []; - - // Sort by createdAt descending on client side - materials.sort((a, b) { - final aData = a.data() as Map?; - final bData = b.data() as Map?; - final aTime = aData?['createdAt'] as Timestamp?; - final bTime = bData?['createdAt'] as Timestamp?; - if (aTime == null && bTime == null) return 0; - if (aTime == null) return 1; - if (bTime == null) return -1; - return bTime.compareTo(aTime); - }); - - if (materials.isEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.folder_open, - color: Theme.of(context).colorScheme.onSurfaceVariant, - size: 64, - ), - const SizedBox(height: 16), - Text( - 'Nenhum material enviado ainda.', - style: TextStyle( - color: Theme.of(context).colorScheme.onSurfaceVariant, - fontSize: 16, - ), - ), - const SizedBox(height: 8), - Text( - 'Os materiais enviados aparecerão aqui.', - style: TextStyle( - color: Theme.of(context).colorScheme.onSurfaceVariant, - fontSize: 14, - ), - ), - ], - ), - ); - } - - return ListView.builder( - padding: const EdgeInsets.all(16), - itemCount: materials.length, - itemBuilder: (context, index) { - final material = - materials[index].data() as Map; - final fileName = material['fileName'] ?? 'Ficheiro sem nome'; - final createdAt = material['createdAt'] as Timestamp?; - final extension = path.extension(fileName).toLowerCase(); - final fileType = extension == '.pdf' - ? 'pdf' - : (extension == '.jpg' || - extension == '.jpeg' || - extension == '.png') - ? 'image' - : 'other'; - - final docId = materials[index].id; - final url = material['url'] as String?; - - return _buildMaterialCard( - docId: docId, - fileName: fileName, - fileType: fileType, - createdAt: createdAt, - url: url, - classId: classId, + Padding( + padding: const EdgeInsets.only(top: 52.0), + child: StreamBuilder( + stream: _getMaterialsStream(classId), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator(color: Color(0xFF82C9BD)), ); - }, - ); - }, + } + + if (snapshot.hasError) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.error_outline, + color: Colors.red, + size: 48, + ), + const SizedBox(height: 16), + Text( + 'Erro ao carregar materiais:\n${snapshot.error}', + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontSize: 16, + ), + ), + ], + ), + ); + } + + final materials = snapshot.data?.docs ?? []; + + // Sort by createdAt descending on client side + materials.sort((a, b) { + final aData = a.data() as Map?; + final bData = b.data() as Map?; + final aTime = aData?['createdAt'] as Timestamp?; + final bTime = bData?['createdAt'] as Timestamp?; + if (aTime == null && bTime == null) return 0; + if (aTime == null) return 1; + if (bTime == null) return -1; + return bTime.compareTo(aTime); + }); + + if (materials.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.folder_open, + color: Theme.of(context).colorScheme.onSurfaceVariant, + size: 64, + ), + const SizedBox(height: 16), + Text( + 'Nenhum material enviado ainda.', + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + fontSize: 16, + ), + ), + const SizedBox(height: 8), + Text( + 'Os materiais enviados aparecerão aqui.', + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + fontSize: 14, + ), + ), + ], + ), + ); + } + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: materials.length, + itemBuilder: (context, index) { + final material = + materials[index].data() as Map; + final fileName = + material['fileName'] ?? 'Ficheiro sem nome'; + final createdAt = material['createdAt'] as Timestamp?; + final extension = path.extension(fileName).toLowerCase(); + final fileType = extension == '.pdf' + ? 'pdf' + : (extension == '.jpg' || + extension == '.jpeg' || + extension == '.png') + ? 'image' + : 'other'; + + final docId = materials[index].id; + final url = material['url'] as String?; + + return _buildMaterialCard( + docId: docId, + fileName: fileName, + fileType: fileType, + createdAt: createdAt, + url: url, + classId: classId, + ); + }, + ); + }, + ), ), Positioned( right: 16, diff --git a/lib/features/settings/presentation/pages/help_page.dart b/lib/features/settings/presentation/pages/help_page.dart index 7cbdb4e..b0fd858 100644 --- a/lib/features/settings/presentation/pages/help_page.dart +++ b/lib/features/settings/presentation/pages/help_page.dart @@ -30,12 +30,19 @@ class HelpPage extends StatelessWidget { ), ), child: SafeArea( + top: false, child: Column( children: [ // Custom AppBar Padding( - padding: const EdgeInsets.all(16.0), + padding: const EdgeInsets.only( + left: 16.0, + right: 16.0, + bottom: 20.0, + top: 52.0, + ), child: Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ IconButton( icon: Icon( diff --git a/lib/features/settings/presentation/pages/profile_edit_page.dart b/lib/features/settings/presentation/pages/profile_edit_page.dart index 30a56e9..1a5dd05 100644 --- a/lib/features/settings/presentation/pages/profile_edit_page.dart +++ b/lib/features/settings/presentation/pages/profile_edit_page.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../../core/services/auth_service.dart'; import '../../../../core/theme/app_colors.dart'; +import '../../../dashboard/presentation/pages/student_dashboard_page.dart'; /// Profile edit page for settings class ProfileEditPage extends ConsumerStatefulWidget { @@ -57,6 +58,9 @@ class _ProfileEditPageState extends ConsumerState { await user.updateDisplayName(_nameController.text); await user.reload(); + // Clear cached user name so dashboard will reload with new name + StudentDashboardPage.clearCachedUserName(); + if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( @@ -111,12 +115,19 @@ class _ProfileEditPageState extends ConsumerState { ), ), child: SafeArea( + top: false, child: Column( children: [ // Custom AppBar Padding( - padding: const EdgeInsets.all(16.0), + padding: const EdgeInsets.only( + left: 16.0, + right: 16.0, + bottom: 20.0, + top: 52.0, + ), child: Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ IconButton( icon: const Icon(Icons.arrow_back, color: Colors.white), diff --git a/lib/features/settings/presentation/pages/settings_page.dart b/lib/features/settings/presentation/pages/settings_page.dart index b0bc9bb..8415ff3 100644 --- a/lib/features/settings/presentation/pages/settings_page.dart +++ b/lib/features/settings/presentation/pages/settings_page.dart @@ -55,12 +55,19 @@ class _SettingsPageState extends ConsumerState { ), ), child: SafeArea( + top: false, child: Column( children: [ // Custom AppBar Padding( - padding: const EdgeInsets.all(16.0), + padding: const EdgeInsets.only( + left: 16.0, + right: 16.0, + bottom: 20.0, + top: 52.0, + ), child: Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ IconButton( icon: Icon( diff --git a/lib/main.dart b/lib/main.dart index 5d70f99..aad3ff6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'core/theme/app_theme.dart'; @@ -10,6 +11,17 @@ import 'l10n/app_localizations.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + // Set system UI overlay to make status bar transparent and remove padding + SystemChrome.setSystemUIOverlayStyle( + const SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: Brightness.dark, + systemNavigationBarColor: Colors.transparent, + systemNavigationBarIconBrightness: Brightness.dark, + ), + ); + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + // Initialize Firebase await FirebaseService.initialize();