IA e pequenas coisas a funcionar
This commit is contained in:
356
lib/core/services/content_service.dart
Normal file
356
lib/core/services/content_service.dart
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user