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:
2026-05-23 16:20:27 +01:00
parent 7ee262f4c7
commit 895ce64c6f
22 changed files with 1330 additions and 582 deletions

View File

@@ -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`)

View File

@@ -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**

View File

@@ -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');

View File

@@ -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

View File

@@ -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(),
), ),
), ),

View File

@@ -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(),

View File

@@ -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(

View File

@@ -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(

View File

@@ -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),

View File

@@ -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(

View File

@@ -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(

View File

@@ -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(

View File

@@ -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(

View File

@@ -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);
}, },
); );

View File

@@ -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,
}); });
} }

View File

@@ -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),

View File

@@ -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),

View File

@@ -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,

View File

@@ -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(

View File

@@ -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),

View File

@@ -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(

View File

@@ -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();