357 lines
11 KiB
Dart
357 lines
11 KiB
Dart
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');
|
|
}
|
|
}
|
|
}
|