Melhoria no funcionamento do histórico, Os nomes no dashboard do aluno carregam a primeira vez e ficam salvos para n ficarem sempre a carregar quando se volta ao dashboard, removi o butão de novo chat na interface de introdução da IA, mudei a aparencia dessa introdução e do histórico
This commit is contained in:
@@ -7,6 +7,47 @@
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
### 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
|
- **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`
|
- 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`)
|
- Acedida através do card "Upload Conteúdo" no dashboard do professor (usando `Navigator.push`)
|
||||||
|
|||||||
@@ -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
|
- ✅ **Foundation:** 100% Complete
|
||||||
|
|
||||||
- ✅ **UI/UX:** 95% Complete
|
- ✅ **UI/UX:** 99% Complete
|
||||||
|
|
||||||
- ✅ **Internationalization:** 100% Complete
|
- ✅ **Internationalization:** 100% Complete
|
||||||
|
|
||||||
- ✅ **Authentication:** 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
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
### **<EFBFBD> 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
|
- [x] Teacher materials upload page
|
||||||
- Tela dedicada: `lib/features/materials/presentation/pages/teacher_materials_page.dart`
|
- 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
|
## ⏳ PENDING FEATURES
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### **🤖 AI Tutor System (75%)**
|
### **🤖 AI Tutor System (98%)**
|
||||||
|
|
||||||
- [x] Chat interface design
|
- [x] Chat interface design
|
||||||
- [x] Message handling with source citations
|
- [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] Vector embeddings and similarity search
|
||||||
- [x] Content management system
|
- [x] Content management system
|
||||||
- [x] Conversation history
|
- [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
|
- [ ] Voice input support
|
||||||
- [ ] Multi-language support
|
- [ ] Multi-language support
|
||||||
- [ ] Advanced analytics
|
- [ ] Advanced analytics
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### **📝 Quiz System (0%)**
|
### **📝 Quiz System (90%)**
|
||||||
|
|
||||||
- [ ] Quiz creation interface
|
- [x] Quiz creation interface
|
||||||
|
- [x] Question types implementation (multiple choice)
|
||||||
- [ ] Question types implementation
|
- [x] Scoring system
|
||||||
|
- [x] Progress tracking
|
||||||
- [ ] Scoring system
|
- [x] Results display
|
||||||
|
- [x] Quiz categories
|
||||||
- [ ] Progress tracking
|
- [x] AI-powered quiz generation from materials
|
||||||
|
- [x] Teacher quiz management
|
||||||
- [ ] Results display
|
- [x] Student quiz taking interface
|
||||||
|
- [x] Quiz history and retry
|
||||||
- [ ] Quiz categories
|
- [ ] Advanced question types (fill in blank, true/false)
|
||||||
|
- [ ] Quiz sharing between classes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### **📊 Dashboard System (50%)**
|
### **📊 Dashboard System (95%)**
|
||||||
|
|
||||||
- [x] Student dashboard
|
- [x] Student dashboard
|
||||||
|
|
||||||
- [x] Teacher dashboard
|
- [x] Teacher dashboard
|
||||||
|
- [x] Analytics display
|
||||||
- [ ] Analytics display
|
- [x] Progress charts
|
||||||
|
- [x] Performance metrics
|
||||||
- [ ] Progress charts
|
|
||||||
|
|
||||||
- [ ] Performance metrics
|
|
||||||
|
|
||||||
- [x] Quick actions
|
- [x] Quick actions
|
||||||
|
- [x] Class management
|
||||||
|
- [x] Student enrollment
|
||||||
|
- [ ] Advanced data visualization
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### **🔍 RAG Engine (0%)**
|
### **🔍 RAG Engine (85%)**
|
||||||
|
|
||||||
- [ ] Vector database setup
|
|
||||||
|
|
||||||
- [ ] Document processing
|
|
||||||
|
|
||||||
- [ ] Search implementation
|
|
||||||
|
|
||||||
- [ ] Context retrieval
|
|
||||||
|
|
||||||
- [ ] Answer generation
|
|
||||||
|
|
||||||
|
- [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
|
- [ ] Performance optimization
|
||||||
|
- [ ] Advanced reranking
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### **📈 Analytics System (0%)**
|
### **📈 Analytics System (90%)**
|
||||||
|
|
||||||
- [ ] Learning progress tracking
|
|
||||||
|
|
||||||
- [ ] Usage statistics
|
|
||||||
|
|
||||||
- [ ] Performance metrics
|
|
||||||
|
|
||||||
|
- [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
|
- [ ] Export functionality
|
||||||
|
- [ ] Advanced data visualization
|
||||||
- [ ] Reporting dashboard
|
|
||||||
|
|
||||||
- [ ] 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
|
**Duration:** Current Week
|
||||||
|
|
||||||
**Goal:** Complete authentication flow
|
**Goal:** Finalize features and optimize performance
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### **Tasks:**
|
#### **Tasks:**
|
||||||
|
|
||||||
- [x] Fix login page design issues
|
- [x] Fix dashboard progress data flickering
|
||||||
|
- [x] Cache user name and stats
|
||||||
- [x] Improve animations and background
|
- [x] Smart back navigation in chat history
|
||||||
|
- [x] Material names display in history
|
||||||
- [x] Update language policy documentation
|
- [x] Filter conversations with user messages
|
||||||
|
- [x] Remove new chat button from intro screen
|
||||||
- [ ] Update signup page with Portuguese
|
- [x] Update documentation with actual progress
|
||||||
|
- [ ] Performance optimization
|
||||||
- [ ] Implement Firebase authentication
|
- [ ] Bug fixes and polish
|
||||||
|
|
||||||
- [ ] Add role-based routing
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### **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
|
- ✅ Basic UI/UX
|
||||||
|
|
||||||
@@ -307,35 +286,45 @@ This document tracks the overall progress of the AI Study Assistant project deve
|
|||||||
|
|
||||||
- ✅ Navigation flow
|
- ✅ 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)**
|
### **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
|
- ⏳ Mobile optimizations
|
||||||
|
|
||||||
@@ -383,15 +372,11 @@ This document tracks the overall progress of the AI Study Assistant project deve
|
|||||||
|
|
||||||
### **Development Metrics:**
|
### **Development Metrics:**
|
||||||
|
|
||||||
- **Total Files:** 45+ Dart files
|
- **Total Files:** 80+ Dart files
|
||||||
|
- **Lines of Code:** ~8,000+ lines
|
||||||
- **Lines of Code:** ~3,000+ lines
|
- **Dependencies:** 30+ packages
|
||||||
|
- **Build Time:** ~20 seconds
|
||||||
- **Dependencies:** 25+ packages
|
- **App Size:** ~35MB (debug)
|
||||||
|
|
||||||
- **Build Time:** ~15 seconds
|
|
||||||
|
|
||||||
- **App Size:** ~25MB (debug)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -417,6 +402,42 @@ This document tracks the overall progress of the AI Study Assistant project deve
|
|||||||
|
|
||||||
### **Last 24 Hours:**
|
### **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
|
- ✅ **Fixed Settings Profile Card UI** - profile_edit_page.dart
|
||||||
- Background: Changed from hardcoded white to Theme.of(context).colorScheme.surface
|
- 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
|
- 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
|
- ✅ **Samsung S928B (Android 16)** - Primary testing device
|
||||||
|
|
||||||
- ✅ **Windows Desktop** - Development environment
|
|
||||||
|
|
||||||
- ✅ **Chrome Browser** - Web testing
|
|
||||||
|
|
||||||
- ⏳ **iOS Devices** - Pending testing
|
- ⏳ **iOS Devices** - Pending testing
|
||||||
|
|
||||||
- ⏳ **Other Android** - 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**
|
**🔄 Auto-Update: Enabled**
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class ChatMemoryService {
|
|||||||
'updatedAt': FieldValue.serverTimestamp(),
|
'updatedAt': FieldValue.serverTimestamp(),
|
||||||
'selectedMaterialIds': selectedMaterialIds,
|
'selectedMaterialIds': selectedMaterialIds,
|
||||||
'messageCount': 0,
|
'messageCount': 0,
|
||||||
|
'hasUserMessage': false,
|
||||||
});
|
});
|
||||||
|
|
||||||
_currentConversationId = conversationRef.id;
|
_currentConversationId = conversationRef.id;
|
||||||
@@ -98,19 +99,24 @@ class ChatMemoryService {
|
|||||||
.orderBy('updatedAt', descending: true)
|
.orderBy('updatedAt', descending: true)
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
final conversations = snapshot.docs.map((doc) {
|
final conversations = snapshot.docs
|
||||||
final data = doc.data();
|
.map((doc) {
|
||||||
return {
|
final data = doc.data();
|
||||||
'id': doc.id,
|
return {
|
||||||
'title': data['title'] as String? ?? 'Sem título',
|
'id': doc.id,
|
||||||
'createdAt': data['createdAt'] as Timestamp?,
|
'title': data['title'] as String? ?? 'Sem título',
|
||||||
'updatedAt': data['updatedAt'] as Timestamp?,
|
'createdAt': data['createdAt'] as Timestamp?,
|
||||||
'selectedMaterialIds':
|
'updatedAt': data['updatedAt'] as Timestamp?,
|
||||||
(data['selectedMaterialIds'] as List<dynamic>?)?.cast<String>() ??
|
'selectedMaterialIds':
|
||||||
[],
|
(data['selectedMaterialIds'] as List<dynamic>?)
|
||||||
'messageCount': data['messageCount'] as int? ?? 0,
|
?.cast<String>() ??
|
||||||
};
|
[],
|
||||||
}).toList();
|
'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');
|
Logger.info('Retrieved ${conversations.length} conversations');
|
||||||
return conversations;
|
return conversations;
|
||||||
@@ -193,15 +199,22 @@ class ChatMemoryService {
|
|||||||
.add(messageData);
|
.add(messageData);
|
||||||
|
|
||||||
// Update conversation metadata
|
// Update conversation metadata
|
||||||
|
final updateData = <String, dynamic>{
|
||||||
|
'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
|
await _firestore
|
||||||
.collection('userChats')
|
.collection('userChats')
|
||||||
.doc(user.uid)
|
.doc(user.uid)
|
||||||
.collection('conversations')
|
.collection('conversations')
|
||||||
.doc(convId)
|
.doc(convId)
|
||||||
.update({
|
.update(updateData);
|
||||||
'updatedAt': FieldValue.serverTimestamp(),
|
|
||||||
'messageCount': FieldValue.increment(1),
|
|
||||||
});
|
|
||||||
|
|
||||||
Logger.info(
|
Logger.info(
|
||||||
'Message saved to Firestore: role=$role, conversation=$convId',
|
'Message saved to Firestore: role=$role, conversation=$convId',
|
||||||
@@ -281,6 +294,7 @@ class ChatMemoryService {
|
|||||||
(data?['selectedMaterialIds'] as List<dynamic>?)?.cast<String>() ??
|
(data?['selectedMaterialIds'] as List<dynamic>?)?.cast<String>() ??
|
||||||
[],
|
[],
|
||||||
'messageCount': data?['messageCount'] as int? ?? 0,
|
'messageCount': data?['messageCount'] as int? ?? 0,
|
||||||
|
'hasUserMessage': data?['hasUserMessage'] as bool? ?? false,
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger.error('Error getting conversation: $e');
|
Logger.error('Error getting conversation: $e');
|
||||||
|
|||||||
@@ -120,8 +120,10 @@ class _StudentAchievementsPageState extends State<StudentAchievementsPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
|
top: false,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
const SizedBox(height: 52),
|
||||||
// Content
|
// Content
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _loading
|
child: _loading
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter_animate/flutter_animate.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
import '../../../../core/services/chat_memory_service.dart';
|
import '../../../../core/services/chat_memory_service.dart';
|
||||||
|
import '../../../../core/theme/app_colors.dart';
|
||||||
import '../../../../core/utils/logger.dart';
|
import '../../../../core/utils/logger.dart';
|
||||||
|
|
||||||
class ChatHistoryPage extends StatefulWidget {
|
class ChatHistoryPage extends StatefulWidget {
|
||||||
@@ -20,6 +21,9 @@ class _ChatHistoryPageState extends State<ChatHistoryPage> {
|
|||||||
String _selectedDateFilter = 'all'; // all, today, yesterday, week, month
|
String _selectedDateFilter = 'all'; // all, today, yesterday, week, month
|
||||||
final Set<String> _selectedConversationIds = {};
|
final Set<String> _selectedConversationIds = {};
|
||||||
bool _isSelectionMode = false;
|
bool _isSelectionMode = false;
|
||||||
|
final Map<String, String> _materialNamesCache = {}; // materialId -> fileName
|
||||||
|
String? _source; // 'chat' or 'intro'
|
||||||
|
String? _previousConversationId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -28,6 +32,73 @@ class _ChatHistoryPageState extends State<ChatHistoryPage> {
|
|||||||
_searchController.addListener(_filterConversations);
|
_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<void> _loadMaterialNames(List<String> materialIds) async {
|
||||||
|
if (materialIds.isEmpty) return;
|
||||||
|
|
||||||
|
final uncachedIds = materialIds
|
||||||
|
.where((id) => !_materialNamesCache.containsKey(id))
|
||||||
|
.toList();
|
||||||
|
if (uncachedIds.isEmpty) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final batches = <Future<QuerySnapshot>>[];
|
||||||
|
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<String, dynamic>?;
|
||||||
|
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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_searchController.dispose();
|
_searchController.dispose();
|
||||||
@@ -38,6 +109,17 @@ class _ChatHistoryPageState extends State<ChatHistoryPage> {
|
|||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
try {
|
try {
|
||||||
final conversations = await ChatMemoryService.getConversations();
|
final conversations = await ChatMemoryService.getConversations();
|
||||||
|
|
||||||
|
// Collect all material IDs from all conversations
|
||||||
|
final allMaterialIds = <String>[];
|
||||||
|
for (final conv in conversations) {
|
||||||
|
final materialIds = conv['selectedMaterialIds'] as List<String>? ?? [];
|
||||||
|
allMaterialIds.addAll(materialIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load material names
|
||||||
|
await _loadMaterialNames(allMaterialIds);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_conversations = conversations;
|
_conversations = conversations;
|
||||||
@@ -269,159 +351,302 @@ class _ChatHistoryPageState extends State<ChatHistoryPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final cs = Theme.of(context).colorScheme;
|
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(
|
return PopScope(
|
||||||
backgroundColor: cs.surfaceContainerLowest,
|
canPop: false,
|
||||||
appBar: AppBar(
|
onPopInvokedWithResult: (didPop, result) {
|
||||||
backgroundColor: cs.surface,
|
if (didPop) return;
|
||||||
elevation: 0,
|
_handleBackNavigation();
|
||||||
leading: IconButton(
|
},
|
||||||
icon: Icon(Icons.arrow_back, color: cs.onSurface),
|
child: Scaffold(
|
||||||
onPressed: () => context.go('/student-dashboard'),
|
body: Container(
|
||||||
),
|
decoration: BoxDecoration(
|
||||||
title: Text(
|
gradient: LinearGradient(
|
||||||
_isSelectionMode
|
begin: Alignment.topCenter,
|
||||||
? '${_selectedConversationIds.length} selecionadas'
|
end: Alignment.bottomCenter,
|
||||||
: 'Histórico de Conversas',
|
colors: isDark
|
||||||
style: TextStyle(
|
? [
|
||||||
color: cs.onSurface,
|
cs.surfaceVariant,
|
||||||
fontSize: 18,
|
orangeAccent.withValues(alpha: 0.3),
|
||||||
fontWeight: FontWeight.bold,
|
cs.surfaceContainerLowest,
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
cs.primary.withValues(alpha: 0.08),
|
||||||
|
orangeAccent.withValues(alpha: 0.05),
|
||||||
|
cs.surfaceContainerLowest,
|
||||||
|
],
|
||||||
|
stops: const [0.0, 0.4, 1.0],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
child: SafeArea(
|
||||||
actions: [
|
top: false,
|
||||||
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: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Search bar
|
// Custom app bar
|
||||||
TextField(
|
Container(
|
||||||
controller: _searchController,
|
padding: const EdgeInsets.symmetric(
|
||||||
decoration: InputDecoration(
|
horizontal: 8,
|
||||||
hintText: 'Pesquisar conversas...',
|
vertical: 15,
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
),
|
decoration: BoxDecoration(
|
||||||
const SizedBox(height: 12),
|
gradient: LinearGradient(
|
||||||
// Date filter chips
|
colors: [cs.primary, orangeAccent],
|
||||||
SingleChildScrollView(
|
begin: Alignment.topLeft,
|
||||||
scrollDirection: Axis.horizontal,
|
end: Alignment.bottomRight,
|
||||||
child: Row(
|
),
|
||||||
children: [
|
boxShadow: [
|
||||||
_buildDateFilterChip('Todas', 'all', cs),
|
BoxShadow(
|
||||||
const SizedBox(width: 8),
|
color: orangeAccent.withValues(alpha: 0.2),
|
||||||
_buildDateFilterChip('Hoje', 'today', cs),
|
blurRadius: 8,
|
||||||
const SizedBox(width: 8),
|
offset: const Offset(0, 2),
|
||||||
_buildDateFilterChip('Ontem', 'yesterday', cs),
|
),
|
||||||
const SizedBox(width: 8),
|
|
||||||
_buildDateFilterChip('Última semana', 'week', cs),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
_buildDateFilterChip('Último mês', 'month', cs),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
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;
|
final isSelected = _selectedDateFilter == value;
|
||||||
return FilterChip(
|
return FilterChip(
|
||||||
label: Text(label),
|
label: Text(label),
|
||||||
@@ -433,13 +658,13 @@ class _ChatHistoryPageState extends State<ChatHistoryPage> {
|
|||||||
_filterConversations();
|
_filterConversations();
|
||||||
},
|
},
|
||||||
backgroundColor: cs.surface,
|
backgroundColor: cs.surface,
|
||||||
selectedColor: cs.primary.withValues(alpha: 0.2),
|
selectedColor: orangeAccent.withValues(alpha: 0.2),
|
||||||
labelStyle: TextStyle(
|
labelStyle: TextStyle(
|
||||||
color: isSelected ? cs.primary : cs.onSurface,
|
color: isSelected ? orangeAccent : cs.onSurface,
|
||||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||||
),
|
),
|
||||||
side: BorderSide(
|
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<ChatHistoryPage> {
|
|||||||
Widget _buildConversationCard(
|
Widget _buildConversationCard(
|
||||||
Map<String, dynamic> conversation,
|
Map<String, dynamic> conversation,
|
||||||
ColorScheme cs,
|
ColorScheme cs,
|
||||||
|
Color orangeAccent,
|
||||||
|
Color lightOrange,
|
||||||
) {
|
) {
|
||||||
final isSelected = _selectedConversationIds.contains(conversation['id']);
|
final isSelected = _selectedConversationIds.contains(conversation['id']);
|
||||||
|
|
||||||
@@ -476,22 +703,25 @@ class _ChatHistoryPageState extends State<ChatHistoryPage> {
|
|||||||
context.go('/ai-tutor/${conversation['id']}');
|
context.go('/ai-tutor/${conversation['id']}');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(20),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected ? cs.primary.withValues(alpha: 0.1) : cs.surface,
|
color: isSelected
|
||||||
borderRadius: BorderRadius.circular(16),
|
? orangeAccent.withValues(alpha: 0.1)
|
||||||
|
: cs.surface,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: isSelected
|
color: isSelected
|
||||||
? cs.primary
|
? orangeAccent
|
||||||
: cs.outline.withValues(alpha: 0.15),
|
: orangeAccent.withValues(alpha: 0.2),
|
||||||
|
width: isSelected ? 2 : 1.5,
|
||||||
),
|
),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: cs.shadow.withValues(alpha: 0.05),
|
color: orangeAccent.withValues(alpha: 0.08),
|
||||||
blurRadius: 8,
|
blurRadius: 12,
|
||||||
offset: const Offset(0, 2),
|
offset: const Offset(0, 4),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -507,6 +737,12 @@ class _ChatHistoryPageState extends State<ChatHistoryPage> {
|
|||||||
value: isSelected,
|
value: isSelected,
|
||||||
onChanged: (_) =>
|
onChanged: (_) =>
|
||||||
_toggleConversationSelection(conversation['id']),
|
_toggleConversationSelection(conversation['id']),
|
||||||
|
fillColor: WidgetStateProperty.resolveWith((states) {
|
||||||
|
if (states.contains(WidgetState.selected)) {
|
||||||
|
return orangeAccent;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
@@ -515,12 +751,18 @@ class _ChatHistoryPageState extends State<ChatHistoryPage> {
|
|||||||
height: 48,
|
height: 48,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
colors: [
|
colors: [cs.primary, orangeAccent],
|
||||||
cs.primary,
|
begin: Alignment.topLeft,
|
||||||
cs.primary.withValues(alpha: 0.7),
|
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(
|
child: const Icon(
|
||||||
Icons.chat_bubble,
|
Icons.chat_bubble,
|
||||||
@@ -540,7 +782,7 @@ class _ChatHistoryPageState extends State<ChatHistoryPage> {
|
|||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
@@ -590,26 +832,35 @@ class _ChatHistoryPageState extends State<ChatHistoryPage> {
|
|||||||
runSpacing: 6,
|
runSpacing: 6,
|
||||||
children: (conversation['selectedMaterialIds'] as List)
|
children: (conversation['selectedMaterialIds'] as List)
|
||||||
.take(3)
|
.take(3)
|
||||||
.map<Widget>(
|
.map<Widget>((id) {
|
||||||
(id) => Container(
|
final materialId = id as String;
|
||||||
|
final displayName =
|
||||||
|
_materialNamesCache[materialId] ?? materialId;
|
||||||
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 8,
|
horizontal: 8,
|
||||||
vertical: 4,
|
vertical: 4,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: cs.primary.withValues(alpha: 0.1),
|
color: lightOrange.withValues(alpha: 0.15),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(
|
||||||
|
color: lightOrange.withValues(alpha: 0.3),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
id.length > 15 ? '${id.substring(0, 15)}...' : id,
|
displayName.length > 20
|
||||||
|
? '${displayName.substring(0, 20)}...'
|
||||||
|
: displayName,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: cs.primary,
|
color: lightOrange,
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
)
|
})
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'package:cloud_firestore/cloud_firestore.dart';
|
|||||||
import '../../../../core/services/chat_memory_service.dart';
|
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/theme/app_colors.dart';
|
||||||
import '../../../../core/utils/logger.dart';
|
import '../../../../core/utils/logger.dart';
|
||||||
import '../../../materials/presentation/pages/pdf_viewer_page.dart';
|
import '../../../materials/presentation/pages/pdf_viewer_page.dart';
|
||||||
|
|
||||||
@@ -74,6 +75,7 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
|
|||||||
|
|
||||||
Future<void> _loadConversation(String conversationId) async {
|
Future<void> _loadConversation(String conversationId) async {
|
||||||
try {
|
try {
|
||||||
|
Logger.info('Loading conversation: $conversationId');
|
||||||
final conversation = await ChatMemoryService.getConversation(
|
final conversation = await ChatMemoryService.getConversation(
|
||||||
conversationId,
|
conversationId,
|
||||||
);
|
);
|
||||||
@@ -98,19 +100,32 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
|
|||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedMaterialIds = materialIds;
|
_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) {
|
} catch (e) {
|
||||||
Logger.error('Error loading material IDs: $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
|
// Load messages from Firestore with safe casting
|
||||||
final loadedMessages = messages.map((msg) {
|
final loadedMessages = messages.map((msg) {
|
||||||
try {
|
try {
|
||||||
return {
|
return <String, dynamic>{
|
||||||
'content': msg['content']?.toString() ?? '',
|
'content': msg['content']?.toString() ?? '',
|
||||||
'isUser': msg['role']?.toString() == 'user',
|
'isUser': msg['role']?.toString() == 'user',
|
||||||
'timestamp': msg['createdAt'] is Timestamp
|
'timestamp': msg['createdAt'] is Timestamp
|
||||||
@@ -120,7 +135,7 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger.error('Error mapping message: $e');
|
Logger.error('Error mapping message: $e');
|
||||||
// Return a safe fallback message
|
// Return a safe fallback message
|
||||||
return {
|
return <String, dynamic>{
|
||||||
'content': '[Mensagem indisponível]',
|
'content': '[Mensagem indisponível]',
|
||||||
'isUser': false,
|
'isUser': false,
|
||||||
'timestamp': DateTime.now(),
|
'timestamp': DateTime.now(),
|
||||||
@@ -130,9 +145,14 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
|
|||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_messages = loadedMessages;
|
_messages.clear();
|
||||||
|
_messages.addAll(loadedMessages);
|
||||||
|
_isLoading = false; // Ensure loading state is reset
|
||||||
});
|
});
|
||||||
_scrollToBottom();
|
_scrollToBottom();
|
||||||
|
Logger.info(
|
||||||
|
'Conversation loaded. _messages count: ${_messages.length}, _isLoading: $_isLoading, _materialsConfirmed: $_materialsConfirmed',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger.error('Error loading conversation: $e');
|
Logger.error('Error loading conversation: $e');
|
||||||
@@ -189,9 +209,11 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
|
top: false,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => context.go('/student-dashboard'),
|
onPressed: () => context.go('/student-dashboard'),
|
||||||
@@ -205,13 +227,15 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
|
|||||||
width: 38,
|
width: 38,
|
||||||
height: 38,
|
height: 38,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white.withValues(alpha: 0.2),
|
color: const Color(0xFFF9EEE8),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: Padding(
|
||||||
Icons.school,
|
padding: const EdgeInsets.all(6),
|
||||||
color: Colors.white,
|
child: Image.asset(
|
||||||
size: 22,
|
'assets/images/epvc.png',
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
@@ -282,49 +306,63 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
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(
|
icon: const Icon(
|
||||||
Icons.history,
|
Icons.history,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
if (_materialsConfirmed)
|
||||||
onPressed: () async {
|
IconButton(
|
||||||
final confirmed = await showDialog<bool>(
|
onPressed: () async {
|
||||||
context: context,
|
final confirmed = await showDialog<bool>(
|
||||||
builder: (context) => AlertDialog(
|
context: context,
|
||||||
title: const Text('Nova conversa'),
|
builder: (context) => AlertDialog(
|
||||||
content: const Text(
|
title: const Text('Nova conversa'),
|
||||||
'Isto vai limpar o chat atual e guardá-lo no histórico. Continuar?',
|
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'),
|
|
||||||
),
|
),
|
||||||
TextButton(
|
actions: [
|
||||||
onPressed: () => Navigator.pop(context, true),
|
TextButton(
|
||||||
style: TextButton.styleFrom(
|
onPressed: () =>
|
||||||
foregroundColor: Colors.red,
|
Navigator.pop(context, false),
|
||||||
|
child: const Text('Cancelar'),
|
||||||
),
|
),
|
||||||
child: const Text('Limpar'),
|
TextButton(
|
||||||
),
|
onPressed: () => Navigator.pop(context, true),
|
||||||
],
|
style: TextButton.styleFrom(
|
||||||
),
|
foregroundColor: Colors.red,
|
||||||
);
|
),
|
||||||
if (confirmed == true) {
|
child: const Text('Limpar'),
|
||||||
ChatMemoryService.setCurrentConversationId(null);
|
),
|
||||||
context.go('/ai-tutor');
|
],
|
||||||
}
|
),
|
||||||
},
|
);
|
||||||
icon: const Icon(
|
if (confirmed == true) {
|
||||||
Icons.add_comment,
|
ChatMemoryService.setCurrentConversationId(null);
|
||||||
color: Colors.white,
|
context.go('/ai-tutor');
|
||||||
size: 20,
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.add_comment,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
tooltip: 'Nova conversa',
|
||||||
),
|
),
|
||||||
tooltip: 'Nova conversa',
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -342,92 +380,300 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
|
|||||||
// ── Intro screen shown before any material is selected ──────────────────
|
// ── Intro screen shown before any material is selected ──────────────────
|
||||||
Widget _buildIntroScreen(BuildContext context) {
|
Widget _buildIntroScreen(BuildContext context) {
|
||||||
final cs = Theme.of(context).colorScheme;
|
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(
|
return SafeArea(
|
||||||
child: Padding(
|
top: false,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 28),
|
child: Container(
|
||||||
child: Column(
|
decoration: BoxDecoration(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
gradient: LinearGradient(
|
||||||
children: [
|
begin: Alignment.topCenter,
|
||||||
Container(
|
end: Alignment.bottomCenter,
|
||||||
width: 88,
|
colors: isDark
|
||||||
height: 88,
|
? [
|
||||||
decoration: BoxDecoration(
|
cs.surfaceVariant,
|
||||||
gradient: LinearGradient(
|
orangeAccent.withValues(alpha: 0.3),
|
||||||
colors: [cs.primary, cs.primary.withValues(alpha: 0.7)],
|
cs.surfaceContainerLowest,
|
||||||
begin: Alignment.topLeft,
|
]
|
||||||
end: Alignment.bottomRight,
|
: [
|
||||||
),
|
cs.primary.withValues(alpha: 0.08),
|
||||||
borderRadius: BorderRadius.circular(28),
|
orangeAccent.withValues(alpha: 0.05),
|
||||||
boxShadow: [
|
cs.surfaceContainerLowest,
|
||||||
BoxShadow(
|
],
|
||||||
color: cs.primary.withValues(alpha: 0.3),
|
stops: const [0.0, 0.4, 1.0],
|
||||||
blurRadius: 20,
|
),
|
||||||
offset: const Offset(0, 8),
|
),
|
||||||
),
|
child: LayoutBuilder(
|
||||||
],
|
builder: (context, constraints) {
|
||||||
),
|
return SingleChildScrollView(
|
||||||
child: const Icon(Icons.school, color: Colors.white, size: 44),
|
physics: const BouncingScrollPhysics(),
|
||||||
),
|
child: ConstrainedBox(
|
||||||
const SizedBox(height: 24),
|
constraints: BoxConstraints(minHeight: constraints.maxHeight),
|
||||||
Text(
|
child: IntrinsicHeight(
|
||||||
'Olá! Sou o Vico',
|
child: Padding(
|
||||||
style: TextStyle(
|
padding: const EdgeInsets.only(
|
||||||
fontSize: 26,
|
left: 24,
|
||||||
fontWeight: FontWeight.bold,
|
right: 24,
|
||||||
color: cs.onSurface,
|
bottom: 24,
|
||||||
),
|
top: 52,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
child: Column(
|
||||||
Text(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
'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.',
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
textAlign: TextAlign.center,
|
children: [
|
||||||
style: TextStyle(
|
const Spacer(flex: 2),
|
||||||
fontSize: 15,
|
// Avatar with animated-like glow (teal to orange gradient)
|
||||||
color: cs.onSurfaceVariant,
|
Container(
|
||||||
height: 1.55,
|
width: 140,
|
||||||
),
|
height: 140,
|
||||||
),
|
decoration: BoxDecoration(
|
||||||
const SizedBox(height: 36),
|
color: const Color(0xFFF9EEE8),
|
||||||
_availableMaterials.isEmpty
|
borderRadius: BorderRadius.circular(32),
|
||||||
? Column(
|
boxShadow: [
|
||||||
children: [
|
BoxShadow(
|
||||||
CircularProgressIndicator(color: cs.primary),
|
color: orangeAccent.withValues(alpha: 0.3),
|
||||||
const SizedBox(height: 16),
|
blurRadius: 24,
|
||||||
Text(
|
offset: const Offset(0, 10),
|
||||||
'A carregar materiais\u2026',
|
),
|
||||||
style: TextStyle(
|
],
|
||||||
fontSize: 13,
|
),
|
||||||
color: cs.onSurfaceVariant,
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/epvc.png',
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 28),
|
||||||
],
|
// Title with orange accent
|
||||||
)
|
RichText(
|
||||||
: SizedBox(
|
textAlign: TextAlign.center,
|
||||||
width: double.infinity,
|
text: TextSpan(
|
||||||
child: FilledButton.icon(
|
children: [
|
||||||
onPressed: () => _showMaterialsPicker(allowEmpty: true),
|
TextSpan(
|
||||||
icon: const Icon(Icons.folder_open_rounded),
|
text: 'Olá! Sou o ',
|
||||||
label: const Text(
|
style: TextStyle(
|
||||||
'Escolher materiais para estudar',
|
fontSize: 28,
|
||||||
style: TextStyle(fontSize: 15),
|
fontWeight: FontWeight.bold,
|
||||||
),
|
color: cs.onSurface,
|
||||||
style: FilledButton.styleFrom(
|
),
|
||||||
padding: const EdgeInsets.symmetric(
|
),
|
||||||
horizontal: 24,
|
TextSpan(
|
||||||
vertical: 16,
|
text: 'Vico',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: orangeAccent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
shape: RoundedRectangleBorder(
|
const SizedBox(height: 12),
|
||||||
borderRadius: BorderRadius.circular(14),
|
// 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) ──────────────────────────────────────────
|
// ── Chat body (messages + input) ──────────────────────────────────────────
|
||||||
Widget _buildChatBody(BuildContext context) {
|
Widget _buildChatBody(BuildContext context) {
|
||||||
final cs = Theme.of(context).colorScheme;
|
final cs = Theme.of(context).colorScheme;
|
||||||
@@ -665,9 +911,7 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
|
|||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: LinearGradient(
|
color: const Color(0xFFF9EEE8),
|
||||||
colors: [cs.primary, cs.primary.withValues(alpha: 0.7)],
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
@@ -677,7 +921,10 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
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<TutorChatPageSimple>
|
|||||||
onSubmitted: (_) => _handleSendMessage(),
|
onSubmitted: (_) => _handleSendMessage(),
|
||||||
textInputAction: TextInputAction.send,
|
textInputAction: TextInputAction.send,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
|
Logger.info(
|
||||||
|
'TextField changed. Text: "$value", isEmpty: ${value.isEmpty}',
|
||||||
|
);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -1457,6 +1707,9 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
|
|||||||
if (_messageController.text.trim().isEmpty) return;
|
if (_messageController.text.trim().isEmpty) return;
|
||||||
|
|
||||||
final userMessage = _messageController.text.trim();
|
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"
|
// Update conversation title if it's still the default "Nova conversa"
|
||||||
final currentId = ChatMemoryService.currentConversationId;
|
final currentId = ChatMemoryService.currentConversationId;
|
||||||
@@ -1473,7 +1726,7 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
|
|||||||
|
|
||||||
// Add user message
|
// Add user message
|
||||||
setState(() {
|
setState(() {
|
||||||
_messages.add({
|
_messages.add(<String, dynamic>{
|
||||||
'content': userMessage,
|
'content': userMessage,
|
||||||
'isUser': true,
|
'isUser': true,
|
||||||
'timestamp': DateTime.now(),
|
'timestamp': DateTime.now(),
|
||||||
@@ -1506,7 +1759,7 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
|
|||||||
Logger.info('Ollama response received: $preview...');
|
Logger.info('Ollama response received: $preview...');
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_messages.add({
|
_messages.add(<String, dynamic>{
|
||||||
'content': replyText,
|
'content': replyText,
|
||||||
'isUser': false,
|
'isUser': false,
|
||||||
'timestamp': DateTime.now(),
|
'timestamp': DateTime.now(),
|
||||||
@@ -1520,7 +1773,7 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
|
|||||||
'Desculpe, ocorreu um erro ao processar a pergunta. Tente novamente.';
|
'Desculpe, ocorreu um erro ao processar a pergunta. Tente novamente.';
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_messages.add({
|
_messages.add(<String, dynamic>{
|
||||||
'content': aiResponse,
|
'content': aiResponse,
|
||||||
'isUser': false,
|
'isUser': false,
|
||||||
'timestamp': DateTime.now(),
|
'timestamp': DateTime.now(),
|
||||||
|
|||||||
@@ -108,15 +108,22 @@ class _AnalyticsPageState extends State<AnalyticsPage>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
|
top: false,
|
||||||
bottom: false,
|
bottom: false,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Header
|
// Header
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.only(
|
||||||
|
left: 24,
|
||||||
|
right: 24,
|
||||||
|
bottom: 28,
|
||||||
|
top: 52,
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
|
|||||||
@@ -228,9 +228,15 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
|
top: false,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: SingleChildScrollView(
|
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(
|
child: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|||||||
@@ -38,8 +38,14 @@ class _RoleSelectionPageState extends State<RoleSelectionPage> {
|
|||||||
|
|
||||||
// Main content
|
// Main content
|
||||||
SafeArea(
|
SafeArea(
|
||||||
|
top: false,
|
||||||
child: Padding(
|
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(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const Spacer(flex: 1),
|
const Spacer(flex: 1),
|
||||||
|
|||||||
@@ -257,9 +257,15 @@ class _SignupPageState extends State<SignupPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
|
top: false,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: SingleChildScrollView(
|
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(
|
child: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|||||||
@@ -194,12 +194,19 @@ class _JoinClassPageState extends ConsumerState<JoinClassPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
|
top: false,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Custom AppBar
|
// Custom AppBar
|
||||||
Container(
|
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(
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
|
|||||||
@@ -11,6 +11,14 @@ import '../widgets/profile_section_widget.dart';
|
|||||||
class StudentDashboardPage extends StatefulWidget {
|
class StudentDashboardPage extends StatefulWidget {
|
||||||
const StudentDashboardPage({super.key});
|
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
|
@override
|
||||||
State<StudentDashboardPage> createState() => _StudentDashboardPageState();
|
State<StudentDashboardPage> createState() => _StudentDashboardPageState();
|
||||||
}
|
}
|
||||||
@@ -21,7 +29,12 @@ class _StudentDashboardPageState extends State<StudentDashboardPage> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_checkRoleAndLoadData();
|
// Use cached name if available, otherwise load data
|
||||||
|
if (StudentDashboardPage._cachedUserName != null) {
|
||||||
|
_userName = StudentDashboardPage._cachedUserName!;
|
||||||
|
} else {
|
||||||
|
_checkRoleAndLoadData();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _checkRoleAndLoadData() async {
|
Future<void> _checkRoleAndLoadData() async {
|
||||||
@@ -75,6 +88,7 @@ class _StudentDashboardPageState extends State<StudentDashboardPage> {
|
|||||||
print('DEBUG: Final displayName to use: "$displayName"');
|
print('DEBUG: Final displayName to use: "$displayName"');
|
||||||
setState(() {
|
setState(() {
|
||||||
_userName = displayName;
|
_userName = displayName;
|
||||||
|
StudentDashboardPage._cachedUserName = displayName;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,14 +113,21 @@ class _StudentDashboardPageState extends State<StudentDashboardPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
|
top: false,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Padding(
|
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(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Header with logout and settings
|
// Header with logout and settings
|
||||||
Row(
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|||||||
@@ -94,14 +94,21 @@ class _TeacherDashboardPageState extends State<TeacherDashboardPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
|
top: false,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Padding(
|
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(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Header with logout and settings
|
// Header with logout and settings
|
||||||
Row(
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|||||||
@@ -17,12 +17,22 @@ class ProgressHeroWidget extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ProgressHeroWidgetState extends State<ProgressHeroWidget> {
|
class _ProgressHeroWidgetState extends State<ProgressHeroWidget> {
|
||||||
|
UserStats? _cachedUserStats;
|
||||||
|
bool _isFirstLoad = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FutureBuilder<UserStats?>(
|
return FutureBuilder<UserStats?>(
|
||||||
future: _loadUserStats(),
|
future: _loadUserStats(),
|
||||||
builder: (context, snapshot) {
|
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();
|
return _buildLoadingState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,6 +41,10 @@ class _ProgressHeroWidgetState extends State<ProgressHeroWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final userStats = snapshot.data;
|
final userStats = snapshot.data;
|
||||||
|
if (userStats != null) {
|
||||||
|
_cachedUserStats = userStats;
|
||||||
|
_isFirstLoad = false;
|
||||||
|
}
|
||||||
return _buildContent(userStats);
|
return _buildContent(userStats);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -107,6 +107,21 @@ class QuickAccessWidget extends StatelessWidget {
|
|||||||
titleFontSize: _titleFontSize,
|
titleFontSize: _titleFontSize,
|
||||||
subtitleFontSize: _subtitleFontSize,
|
subtitleFontSize: _subtitleFontSize,
|
||||||
padding: _cardPadding,
|
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'),
|
onTap: () => context.go('/ai-tutor'),
|
||||||
)
|
)
|
||||||
.animate()
|
.animate()
|
||||||
@@ -227,6 +242,7 @@ class QuickAccessWidget extends StatelessWidget {
|
|||||||
subtitle: 'Assistente de estudos',
|
subtitle: 'Assistente de estudos',
|
||||||
icon: Icons.psychology,
|
icon: Icons.psychology,
|
||||||
useGradient: true,
|
useGradient: true,
|
||||||
|
useCustomIcon: true,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
context.go('/ai-tutor');
|
context.go('/ai-tutor');
|
||||||
@@ -330,29 +346,47 @@ class QuickAccessWidget extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: Container(
|
leading: item.useCustomIcon
|
||||||
padding: const EdgeInsets.all(10),
|
? Container(
|
||||||
decoration: BoxDecoration(
|
width: 44,
|
||||||
color: item.useGradient
|
height: 44,
|
||||||
? Theme.of(
|
decoration: BoxDecoration(
|
||||||
context,
|
color: const Color(0xFFF9EEE8),
|
||||||
).colorScheme.primary.withOpacity(0.1)
|
borderRadius: BorderRadius.circular(10),
|
||||||
: (item.iconColor ??
|
),
|
||||||
Theme.of(
|
child: Padding(
|
||||||
context,
|
padding: const EdgeInsets.all(8),
|
||||||
).colorScheme.secondary)
|
child: Image.asset(
|
||||||
.withOpacity(0.1),
|
'assets/images/epvc.png',
|
||||||
borderRadius: BorderRadius.circular(10),
|
fit: BoxFit.contain,
|
||||||
),
|
),
|
||||||
child: Icon(
|
),
|
||||||
item.icon,
|
)
|
||||||
color: item.useGradient
|
: Container(
|
||||||
? Theme.of(context).colorScheme.primary
|
padding: const EdgeInsets.all(10),
|
||||||
: (item.iconColor ??
|
decoration: BoxDecoration(
|
||||||
Theme.of(context).colorScheme.secondary),
|
color: item.useGradient
|
||||||
size: 24,
|
? 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(
|
title: Text(
|
||||||
item.title,
|
item.title,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@@ -395,6 +429,7 @@ class _QuickAccessItem {
|
|||||||
final IconData icon;
|
final IconData icon;
|
||||||
final bool useGradient;
|
final bool useGradient;
|
||||||
final Color? iconColor;
|
final Color? iconColor;
|
||||||
|
final bool useCustomIcon;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
|
|
||||||
_QuickAccessItem({
|
_QuickAccessItem({
|
||||||
@@ -403,6 +438,7 @@ class _QuickAccessItem {
|
|||||||
required this.icon,
|
required this.icon,
|
||||||
this.useGradient = false,
|
this.useGradient = false,
|
||||||
this.iconColor,
|
this.iconColor,
|
||||||
|
this.useCustomIcon = false,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -326,11 +326,18 @@ class _ContentManagementPageState extends State<ContentManagementPage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
SafeArea(
|
SafeArea(
|
||||||
|
top: false,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.only(
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
bottom: 20,
|
||||||
|
top: 52,
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.arrow_back, color: cs.onSurface),
|
icon: Icon(Icons.arrow_back, color: cs.onSurface),
|
||||||
|
|||||||
@@ -61,8 +61,14 @@ class _PdfViewerPageState extends State<PdfViewerPage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
SafeArea(
|
SafeArea(
|
||||||
|
top: false,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.only(
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
bottom: 20,
|
||||||
|
top: 52,
|
||||||
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: cs.surface.withOpacity(0.8),
|
color: cs.surface.withOpacity(0.8),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
@@ -74,6 +80,7 @@ class _PdfViewerPageState extends State<PdfViewerPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.arrow_back, color: cs.onSurface),
|
icon: Icon(Icons.arrow_back, color: cs.onSurface),
|
||||||
|
|||||||
@@ -200,117 +200,126 @@ class _TeacherMaterialsPageState extends State<TeacherMaterialsPage> {
|
|||||||
|
|
||||||
Widget _buildClassTab({required String classId}) {
|
Widget _buildClassTab({required String classId}) {
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
|
top: false,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
StreamBuilder<QuerySnapshot>(
|
Padding(
|
||||||
stream: _getMaterialsStream(classId),
|
padding: const EdgeInsets.only(top: 52.0),
|
||||||
builder: (context, snapshot) {
|
child: StreamBuilder<QuerySnapshot>(
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
stream: _getMaterialsStream(classId),
|
||||||
return const Center(
|
builder: (context, snapshot) {
|
||||||
child: CircularProgressIndicator(color: Color(0xFF82C9BD)),
|
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<String, dynamic>?;
|
|
||||||
final bData = b.data() as Map<String, dynamic>?;
|
|
||||||
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<String, dynamic>;
|
|
||||||
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,
|
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
);
|
|
||||||
},
|
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<String, dynamic>?;
|
||||||
|
final bData = b.data() as Map<String, dynamic>?;
|
||||||
|
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<String, dynamic>;
|
||||||
|
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(
|
Positioned(
|
||||||
right: 16,
|
right: 16,
|
||||||
|
|||||||
@@ -30,12 +30,19 @@ class HelpPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
|
top: false,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Custom AppBar
|
// Custom AppBar
|
||||||
Padding(
|
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(
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import '../../../../core/services/auth_service.dart';
|
import '../../../../core/services/auth_service.dart';
|
||||||
import '../../../../core/theme/app_colors.dart';
|
import '../../../../core/theme/app_colors.dart';
|
||||||
|
import '../../../dashboard/presentation/pages/student_dashboard_page.dart';
|
||||||
|
|
||||||
/// Profile edit page for settings
|
/// Profile edit page for settings
|
||||||
class ProfileEditPage extends ConsumerStatefulWidget {
|
class ProfileEditPage extends ConsumerStatefulWidget {
|
||||||
@@ -57,6 +58,9 @@ class _ProfileEditPageState extends ConsumerState<ProfileEditPage> {
|
|||||||
await user.updateDisplayName(_nameController.text);
|
await user.updateDisplayName(_nameController.text);
|
||||||
await user.reload();
|
await user.reload();
|
||||||
|
|
||||||
|
// Clear cached user name so dashboard will reload with new name
|
||||||
|
StudentDashboardPage.clearCachedUserName();
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
@@ -111,12 +115,19 @@ class _ProfileEditPageState extends ConsumerState<ProfileEditPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
|
top: false,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Custom AppBar
|
// Custom AppBar
|
||||||
Padding(
|
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(
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
||||||
|
|||||||
@@ -55,12 +55,19 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
|
top: false,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Custom AppBar
|
// Custom AppBar
|
||||||
Padding(
|
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(
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'core/theme/app_theme.dart';
|
import 'core/theme/app_theme.dart';
|
||||||
@@ -10,6 +11,17 @@ import 'l10n/app_localizations.dart';
|
|||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
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
|
// Initialize Firebase
|
||||||
await FirebaseService.initialize();
|
await FirebaseService.initialize();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user