Mudanças na aba de quiz
This commit is contained in:
@@ -21,7 +21,8 @@ class MaterialsRAGService {
|
||||
|
||||
/// Listar materiais disponíveis para o aluno autenticado
|
||||
/// Retorna apenas materiais cujo classId corresponde a uma turma onde o aluno está inscrito
|
||||
static Future<List<Map<String, String>>> getAvailableMaterialsForStudent() async {
|
||||
static Future<List<Map<String, String>>>
|
||||
getAvailableMaterialsForStudent() async {
|
||||
try {
|
||||
final user = _auth.currentUser;
|
||||
if (user == null) return [];
|
||||
@@ -62,7 +63,13 @@ class MaterialsRAGService {
|
||||
final classId = data['classId'] as String?;
|
||||
if (classId == null || enrolledClassIds.contains(classId)) {
|
||||
final fileName = data['fileName'] as String? ?? 'Material';
|
||||
result.add({'id': doc.id, 'name': fileName});
|
||||
final teacherId = data['teacherId'] as String?;
|
||||
result.add({
|
||||
'id': doc.id,
|
||||
'name': fileName,
|
||||
if (classId != null) 'classId': classId,
|
||||
if (teacherId != null) 'teacherId': teacherId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,9 +127,12 @@ class MaterialsRAGService {
|
||||
// Usar cache do texto completo se disponível (sufixo v2 invalida caches antigos)
|
||||
final cacheKey = '${fileName}_v6';
|
||||
String fullText;
|
||||
if (_chunksCache.containsKey(cacheKey) && _chunksCache[cacheKey]!.isNotEmpty) {
|
||||
if (_chunksCache.containsKey(cacheKey) &&
|
||||
_chunksCache[cacheKey]!.isNotEmpty) {
|
||||
fullText = _chunksCache[cacheKey]!.first;
|
||||
Logger.info('Using cached text for $fileName: ${fullText.length} chars');
|
||||
Logger.info(
|
||||
'Using cached text for $fileName: ${fullText.length} chars',
|
||||
);
|
||||
} else {
|
||||
try {
|
||||
final teacherId = data['teacherId'] as String?;
|
||||
@@ -164,7 +174,9 @@ class MaterialsRAGService {
|
||||
fullText = cleaned;
|
||||
// Guardar texto completo no cache com key versionada
|
||||
_chunksCache[cacheKey] = [fullText];
|
||||
Logger.info('PDF "$fileName" -> ${fullText.length} chars extracted');
|
||||
Logger.info(
|
||||
'PDF "$fileName" -> ${fullText.length} chars extracted',
|
||||
);
|
||||
} catch (e) {
|
||||
Logger.error('Error extracting text from $fileName: $e');
|
||||
continue;
|
||||
@@ -176,9 +188,15 @@ class MaterialsRAGService {
|
||||
final String context;
|
||||
if (fullText.length <= 10000) {
|
||||
context = fullText;
|
||||
Logger.info('Small PDF — sending full text (${fullText.length} chars)');
|
||||
Logger.info(
|
||||
'Small PDF — sending full text (${fullText.length} chars)',
|
||||
);
|
||||
} else {
|
||||
final windows = _extractKeywordWindows(fullText, userQuery, _maxRelevantChunks);
|
||||
final windows = _extractKeywordWindows(
|
||||
fullText,
|
||||
userQuery,
|
||||
_maxRelevantChunks,
|
||||
);
|
||||
context = windows.join('\n\n---\n\n');
|
||||
Logger.info('Large PDF — keyword windows: ${windows.length}');
|
||||
}
|
||||
@@ -205,7 +223,11 @@ class MaterialsRAGService {
|
||||
/// Método legacy - mantido para compatibilidade mas usa chunk retrieval
|
||||
@Deprecated('Use getRelevantChunks with userQuery instead')
|
||||
static Future<String> getMaterialsContext({int maxMaterials = 5}) async {
|
||||
return getRelevantChunks(userQuery: '', maxMaterials: maxMaterials, maxChunks: 3);
|
||||
return getRelevantChunks(
|
||||
userQuery: '',
|
||||
maxMaterials: maxMaterials,
|
||||
maxChunks: 3,
|
||||
);
|
||||
}
|
||||
|
||||
/// Get teacher IDs from student's enrolled classes
|
||||
@@ -239,11 +261,11 @@ class MaterialsRAGService {
|
||||
|
||||
// 3. Buscar turmas e extrair teacherIds
|
||||
final Set<String> teacherIds = {};
|
||||
|
||||
|
||||
// Firestore whereIn limit is 10, so process in batches if needed
|
||||
for (int i = 0; i < classIds.length; i += 10) {
|
||||
final batch = classIds.skip(i).take(10).toList();
|
||||
|
||||
|
||||
final classSnapshot = await _firestore
|
||||
.collection('classes')
|
||||
.where(FieldPath.documentId, whereIn: batch)
|
||||
@@ -273,7 +295,10 @@ class MaterialsRAGService {
|
||||
|
||||
/// Extrair texto real do PDF usando Firebase Storage SDK + syncfusion_flutter_pdf
|
||||
/// Usa getData() para descarregar o ficheiro completo (sem truncar a meio do stream)
|
||||
static Future<String> _extractFullText(String fileName, String teacherId) async {
|
||||
static Future<String> _extractFullText(
|
||||
String fileName,
|
||||
String teacherId,
|
||||
) async {
|
||||
PdfDocument? document;
|
||||
try {
|
||||
final ref = _storage
|
||||
@@ -306,12 +331,16 @@ class MaterialsRAGService {
|
||||
for (int i = startPage; i < totalPages; i++) {
|
||||
if (buffer.length >= _maxExtractedChars) break;
|
||||
try {
|
||||
final pageText = extractor.extractText(startPageIndex: i, endPageIndex: i).trim();
|
||||
final pageText = extractor
|
||||
.extractText(startPageIndex: i, endPageIndex: i)
|
||||
.trim();
|
||||
if (pageText.length < 80) continue;
|
||||
final lowerText = pageText.toLowerCase();
|
||||
final pipeCount = '|'.allMatches(pageText).length;
|
||||
final isStructurePage = pipeCount > 3 ||
|
||||
(lowerText.contains('table of contents') && pageText.length < 800) ||
|
||||
final isStructurePage =
|
||||
pipeCount > 3 ||
|
||||
(lowerText.contains('table of contents') &&
|
||||
pageText.length < 800) ||
|
||||
(lowerText.contains('copyright') && pageText.length < 400) ||
|
||||
(lowerText.contains('color insert') && pageText.length < 400) ||
|
||||
lowerText.contains('just light novels') ||
|
||||
@@ -355,8 +384,12 @@ class MaterialsRAGService {
|
||||
? fullText.substring(0, _maxExtractedChars)
|
||||
: fullText;
|
||||
|
||||
Logger.info('Extracted ${result.length} chars from $fileName (${document.pages.count} pages, ${form.fields.count} form fields)');
|
||||
Logger.info('Text preview: ${result.length > 200 ? result.substring(0, 200) : result}');
|
||||
Logger.info(
|
||||
'Extracted ${result.length} chars from $fileName (${document.pages.count} pages, ${form.fields.count} form fields)',
|
||||
);
|
||||
Logger.info(
|
||||
'Text preview: ${result.length > 200 ? result.substring(0, 200) : result}',
|
||||
);
|
||||
return result.trim();
|
||||
} catch (e) {
|
||||
Logger.error('Error extracting text from $fileName: $e');
|
||||
@@ -381,10 +414,9 @@ class MaterialsRAGService {
|
||||
|
||||
// Extrair keywords: palavras com >3 chars + nomes próprios (palavras com maiúscula, >2 chars)
|
||||
// Os nomes próprios são invariantes entre línguas (ex: "Claire", "Rae", "François")
|
||||
final properNouns = RegExp(r'\b[A-ZÁÉÍÓÚÀÂÊÔÃÕÇ][a-záéíóúàâêôãõç]{2,}\b')
|
||||
.allMatches(userQuery)
|
||||
.map((m) => m.group(0)!.toLowerCase())
|
||||
.toSet();
|
||||
final properNouns = RegExp(
|
||||
r'\b[A-ZÁÉÍÓÚÀÂÊÔÃÕÇ][a-záéíóúàâêôãõç]{2,}\b',
|
||||
).allMatches(userQuery).map((m) => m.group(0)!.toLowerCase()).toSet();
|
||||
final generalKeywords = userQuery
|
||||
.toLowerCase()
|
||||
.split(RegExp(r'[^\w]'))
|
||||
@@ -428,7 +460,9 @@ class MaterialsRAGService {
|
||||
lastEnd = end;
|
||||
}
|
||||
|
||||
Logger.info('Keyword windows found: ${windows.length} for query "$userQuery"');
|
||||
Logger.info(
|
||||
'Keyword windows found: ${windows.length} for query "$userQuery"',
|
||||
);
|
||||
return windows;
|
||||
}
|
||||
|
||||
@@ -436,15 +470,15 @@ class MaterialsRAGService {
|
||||
static List<String> _chunkText(String text, int chunkSize, int overlap) {
|
||||
final List<String> chunks = [];
|
||||
final int textLength = text.length;
|
||||
|
||||
|
||||
if (textLength <= chunkSize) {
|
||||
return [text];
|
||||
}
|
||||
|
||||
|
||||
int start = 0;
|
||||
while (start < textLength) {
|
||||
int end = start + chunkSize;
|
||||
|
||||
|
||||
if (end >= textLength) {
|
||||
end = textLength;
|
||||
} else {
|
||||
@@ -456,68 +490,70 @@ class MaterialsRAGService {
|
||||
end = start + chunkSize; // Forçar quebra se não encontrar espaço
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
chunks.add(text.substring(start, end).trim());
|
||||
|
||||
|
||||
// Avançar com overlap
|
||||
start = end - overlap;
|
||||
if (start >= end) break; // Prevenir loop infinito
|
||||
}
|
||||
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
/// Selecionar chunks mais relevantes usando keyword matching simples
|
||||
static List<String> _selectRelevantChunks(
|
||||
List<String> chunks,
|
||||
String userQuery,
|
||||
List<String> chunks,
|
||||
String userQuery,
|
||||
int maxChunks,
|
||||
) {
|
||||
if (userQuery.isEmpty || chunks.isEmpty) {
|
||||
// Se não há query, retornar primeiros chunks
|
||||
return chunks.take(maxChunks).toList();
|
||||
}
|
||||
|
||||
|
||||
// Extrair keywords da query (palavras com mais de 3 caracteres)
|
||||
final queryWords = userQuery
|
||||
.toLowerCase()
|
||||
.split(RegExp(r'[^\w]'))
|
||||
.where((w) => w.length > 3)
|
||||
.toSet();
|
||||
|
||||
|
||||
if (queryWords.isEmpty) {
|
||||
return chunks.take(maxChunks).toList();
|
||||
}
|
||||
|
||||
|
||||
// Calcular score para cada chunk
|
||||
final List<MapEntry<String, int>> scoredChunks = [];
|
||||
|
||||
|
||||
for (final chunk in chunks) {
|
||||
final chunkLower = chunk.toLowerCase();
|
||||
int score = 0;
|
||||
|
||||
|
||||
for (final word in queryWords) {
|
||||
// Contar ocorrências da palavra no chunk
|
||||
final matches = word.allMatches(chunkLower).length;
|
||||
score += matches * 10; // Peso por ocorrência
|
||||
|
||||
|
||||
// Bonus se a palavra estiver no início do chunk
|
||||
if (chunkLower.startsWith(word)) {
|
||||
score += 5;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Bonus por tamanho do chunk (preferir chunks mais completos)
|
||||
score += (chunk.length / 100).floor();
|
||||
|
||||
|
||||
scoredChunks.add(MapEntry(chunk, score));
|
||||
}
|
||||
|
||||
|
||||
// Ordenar por score decrescente
|
||||
scoredChunks.sort((a, b) => b.value.compareTo(a.value));
|
||||
|
||||
Logger.info('Top chunk scores: ${scoredChunks.take(3).map((e) => e.value).toList()}');
|
||||
|
||||
|
||||
Logger.info(
|
||||
'Top chunk scores: ${scoredChunks.take(3).map((e) => e.value).toList()}',
|
||||
);
|
||||
|
||||
// Retornar os N chunks mais relevantes
|
||||
return scoredChunks.take(maxChunks).map((e) => e.key).toList();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user