IA e pequenas coisas a funcionar

This commit is contained in:
2026-05-10 18:45:00 +01:00
parent 0f382e970b
commit 3475b57036
21 changed files with 4484 additions and 72 deletions

View File

@@ -0,0 +1,356 @@
import 'dart:io';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_storage/firebase_storage.dart';
import '../utils/logger.dart';
/// Service for managing educational content from teachers
class ContentService {
static final FirebaseAuth _auth = FirebaseAuth.instance;
static final FirebaseFirestore _firestore = FirebaseFirestore.instance;
static final FirebaseStorage _storage = FirebaseStorage.instance;
/// Upload and process content file
static Future<String> uploadContent({
required File file,
required String title,
required String subject,
required String concept,
required double difficulty,
required int grade,
Map<String, dynamic>? additionalMetadata,
}) async {
try {
final user = _auth.currentUser;
if (user == null) throw Exception('User not authenticated');
Logger.info('Starting content upload: $title');
// 1. Upload file to Firebase Storage
final fileName =
'${DateTime.now().millisecondsSinceEpoch}_${file.path.split('/').last}';
final storageRef = _storage.ref().child('content/${user.uid}/$fileName');
final uploadTask = await storageRef.putFile(file);
final downloadUrl = await uploadTask.ref.getDownloadURL();
Logger.info('File uploaded to storage: $downloadUrl');
// 2. Create content document in Firestore
final contentDoc = {
'title': title,
'subject': subject,
'concept': concept,
'difficulty': difficulty,
'grade': grade,
'teacherId': user.uid,
'teacherEmail': user.email,
'fileName': fileName,
'downloadUrl': downloadUrl,
'fileSize': await file.length(),
'uploadedAt': FieldValue.serverTimestamp(),
'status': 'processing',
'metadata': additionalMetadata ?? {},
'chunkCount': 0,
'totalChunks': 0,
};
final docRef = await _firestore.collection('content').add(contentDoc);
final contentId = docRef.id;
Logger.info('Content document created: $contentId');
// 3. Trigger content processing (this would be handled by a Cloud Function)
await _triggerContentProcessing(contentId, file.path, downloadUrl);
return contentId;
} catch (e) {
Logger.error('Error uploading content: $e');
throw Exception('Failed to upload content: $e');
}
}
/// Get content list for a teacher
static Future<List<Map<String, dynamic>>> getTeacherContent({
int limit = 20,
DocumentSnapshot? lastDocument,
}) async {
try {
final user = _auth.currentUser;
if (user == null) throw Exception('User not authenticated');
Query query = _firestore
.collection('content')
.where('teacherId', isEqualTo: user.uid)
.orderBy('uploadedAt', descending: true)
.limit(limit);
if (lastDocument != null) {
query = query.startAfterDocument(lastDocument);
}
final querySnapshot = await query.get();
return querySnapshot.docs
.map((doc) => {'id': doc.id, ...doc.data() as Map<String, dynamic>})
.toList();
} catch (e) {
Logger.error('Error getting teacher content: $e');
throw Exception('Failed to get content: $e');
}
}
/// Get content details
static Future<Map<String, dynamic>?> getContentDetails(
String contentId,
) async {
try {
final doc = await _firestore.collection('content').doc(contentId).get();
if (!doc.exists) return null;
return {'id': doc.id, ...doc.data() as Map<String, dynamic>};
} catch (e) {
Logger.error('Error getting content details: $e');
throw Exception('Failed to get content details: $e');
}
}
/// Get available content for students (filtered by school/grade)
static Future<List<Map<String, dynamic>>> getAvailableContent({
String? subject,
String? concept,
double? minDifficulty,
double? maxDifficulty,
int? grade,
int limit = 50,
}) async {
try {
final user = _auth.currentUser;
if (user == null) throw Exception('User not authenticated');
// Get user's grade from their profile
final userDoc = await _firestore.collection('users').doc(user.uid).get();
final userData = userDoc.data();
final userGrade = userData?['profile']?['gradeLevel'] ?? grade;
Query query = _firestore
.collection('content')
.where('status', isEqualTo: 'processed')
.where('grade', isEqualTo: userGrade)
.orderBy('uploadedAt', descending: true)
.limit(limit);
// Apply filters
if (subject != null) {
query = query.where('subject', isEqualTo: subject);
}
if (concept != null) {
query = query.where('concept', isEqualTo: concept);
}
if (minDifficulty != null) {
query = query.where(
'difficulty',
isGreaterThanOrEqualTo: minDifficulty,
);
}
if (maxDifficulty != null) {
query = query.where('difficulty', isLessThanOrEqualTo: maxDifficulty);
}
final querySnapshot = await query.get();
return querySnapshot.docs
.map((doc) => {'id': doc.id, ...doc.data() as Map<String, dynamic>})
.toList();
} catch (e) {
Logger.error('Error getting available content: $e');
throw Exception('Failed to get available content: $e');
}
}
/// Delete content
static Future<void> deleteContent(String contentId) async {
try {
final user = _auth.currentUser;
if (user == null) throw Exception('User not authenticated');
// Get content details
final content = await getContentDetails(contentId);
if (content == null) throw Exception('Content not found');
// Check ownership
if (content['teacherId'] != user.uid) {
throw Exception('Permission denied');
}
// Delete from Firestore
await _firestore.collection('content').doc(contentId).delete();
// Delete from Storage
if (content['fileName'] != null) {
final storageRef = _storage.ref().child(
'content/${user.uid}/${content['fileName']}',
);
await storageRef.delete();
}
// Delete associated chunks
final chunksSnapshot = await _firestore
.collection('contentChunks')
.where('contentId', isEqualTo: contentId)
.get();
final batch = _firestore.batch();
for (final doc in chunksSnapshot.docs) {
batch.delete(doc.reference);
}
await batch.commit();
Logger.info('Content deleted successfully: $contentId');
} catch (e) {
Logger.error('Error deleting content: $e');
throw Exception('Failed to delete content: $e');
}
}
/// Update content metadata
static Future<void> updateContentMetadata(
String contentId,
Map<String, dynamic> metadata,
) async {
try {
final user = _auth.currentUser;
if (user == null) throw Exception('User not authenticated');
// Check ownership
final content = await getContentDetails(contentId);
if (content == null) throw Exception('Content not found');
if (content['teacherId'] != user.uid) {
throw Exception('Permission denied');
}
await _firestore.collection('content').doc(contentId).update({
'metadata': metadata,
'updatedAt': FieldValue.serverTimestamp(),
});
Logger.info('Content metadata updated: $contentId');
} catch (e) {
Logger.error('Error updating content metadata: $e');
throw Exception('Failed to update content metadata: $e');
}
}
/// Get content statistics for a teacher
static Future<Map<String, dynamic>> getTeacherStats() async {
try {
final user = _auth.currentUser;
if (user == null) throw Exception('User not authenticated');
final contentSnapshot = await _firestore
.collection('content')
.where('teacherId', isEqualTo: user.uid)
.get();
final totalContent = contentSnapshot.docs.length;
final processedContent = contentSnapshot.docs
.where((doc) => doc['status'] == 'processed')
.length;
final totalChunks = contentSnapshot.docs.fold<int>(
0,
(sum, doc) => sum + (doc['chunkCount'] as int? ?? 0),
);
final subjects = <String, int>{};
for (final doc in contentSnapshot.docs) {
final subject = doc['subject'] as String? ?? 'Unknown';
subjects[subject] = (subjects[subject] ?? 0) + 1;
}
return {
'totalContent': totalContent,
'processedContent': processedContent,
'totalChunks': totalChunks,
'subjects': subjects,
'processingContent': totalContent - processedContent,
};
} catch (e) {
Logger.error('Error getting teacher stats: $e');
throw Exception('Failed to get teacher stats: $e');
}
}
/// Trigger content processing (would be handled by Cloud Function)
static Future<void> _triggerContentProcessing(
String contentId,
String filePath,
String downloadUrl,
) async {
try {
// This would typically trigger a Cloud Function
// For now, we'll update the status to indicate processing should start
await _firestore.collection('content').doc(contentId).update({
'status': 'processing_started',
'processingStartedAt': FieldValue.serverTimestamp(),
});
Logger.info('Content processing triggered: $contentId');
} catch (e) {
Logger.error('Error triggering content processing: $e');
throw Exception('Failed to trigger content processing: $e');
}
}
/// Search content by text
static Future<List<Map<String, dynamic>>> searchContent({
required String searchQuery,
String? subject,
int? grade,
int limit = 20,
}) async {
try {
final user = _auth.currentUser;
if (user == null) throw Exception('User not authenticated');
// Get user's grade if not specified
final userDoc = await _firestore.collection('users').doc(user.uid).get();
final userData = userDoc.data();
final userGrade = grade ?? userData?['profile']?['gradeLevel'];
// For now, we'll do a simple text search on title and concept
// In a full implementation, this would use the vector search
Query query = _firestore
.collection('content')
.where('status', isEqualTo: 'processed')
.where('grade', isEqualTo: userGrade)
.orderBy('uploadedAt', descending: true)
.limit(limit);
if (subject != null) {
query = query.where('subject', isEqualTo: subject);
}
final querySnapshot = await query.get();
final searchQueryLower = searchQuery.toLowerCase();
final results = querySnapshot.docs
.where((doc) {
final data = doc.data() as Map<String, dynamic>;
final title = (data['title'] as String? ?? '').toLowerCase();
final concept = (data['concept'] as String? ?? '').toLowerCase();
return title.contains(searchQueryLower) ||
concept.contains(searchQueryLower);
})
.map((doc) => {'id': doc.id, ...doc.data() as Map<String, dynamic>})
.toList();
return results;
} catch (e) {
Logger.error('Error searching content: $e');
throw Exception('Failed to search content: $e');
}
}
}