FINALMENTE ACABOU PAPAPAAAAA

This commit is contained in:
2026-05-17 23:01:22 +01:00
parent c979692fd9
commit 058bbaaea2
2 changed files with 960 additions and 40 deletions

View File

@@ -558,6 +558,76 @@ class MaterialsRAGService {
return scoredChunks.take(maxChunks).map((e) => e.key).toList();
}
/// Formatar texto extraído do PDF para melhor legibilidade
static String _formatPDFText(String text) {
if (text.isEmpty) return text;
String formatted = text;
// Corrigir quebras de linha excessivas
formatted = formatted.replaceAll(RegExp(r'\n{3,}'), '\n\n');
// Corrigir espaços excessivos
formatted = formatted.replaceAll(RegExp(r'[ \t]+'), ' ');
// Remover espaços no início/fim das linhas
formatted = formatted.split('\n').map((line) => line.trim()).join('\n');
// Corrigir parágrafos (linhas que terminam com ponto e seguem sem espaço)
formatted = formatted.replaceAllMapped(
RegExp(r'\.(\n)([A-ZÁÉÍÓÚÀÂÊÔÃÕÇ])'),
(match) => '.\n\n${match.group(2)}',
);
// Corrigir quebras de palavras com hífen no fim da linha
formatted = formatted.replaceAllMapped(
RegExp(r'([a-zA-Záéíóúàâêôãõç])-\n([a-zA-Záéíóúàâêôãõç])'),
(match) => '${match.group(1)}${match.group(2)}',
);
// Adicionar quebras de parágrafo para títulos (linhas em maiúsculas)
formatted = formatted.replaceAllMapped(
RegExp(r'\n([A-ZÁÉÍÓÚÀÂÊÔÃÕÇ][A-ZÁÉÍÓÚÀÂÊÔÃÕÇ\s]{10,})\n'),
(match) => '\n\n${match.group(1)}\n\n',
);
// Limpar quebras de linha no início e fim
formatted = formatted.trim();
return formatted;
}
/// Obter o texto completo de um PDF específico para pré-visualização
static Future<String> getFullPDFText(String fileName, String teacherId) async {
try {
// Remover extensão se existir
final cleanFileName = fileName.endsWith('.pdf') ? fileName : '$fileName.pdf';
// Usar cache do texto completo se disponível
final cacheKey = '${cleanFileName}_preview_v6';
if (_chunksCache.containsKey(cacheKey) && _chunksCache[cacheKey]!.isNotEmpty) {
final fullText = _chunksCache[cacheKey]!.first;
Logger.info('Using cached preview text for $cleanFileName: ${fullText.length} chars');
return fullText;
}
// Extrair texto completo
final rawText = await _extractFullText(cleanFileName, teacherId);
// Formatar texto para melhor legibilidade
final formattedText = _formatPDFText(rawText);
// Guardar em cache
_chunksCache[cacheKey] = [formattedText];
Logger.info('PDF "$cleanFileName" -> ${formattedText.length} chars extracted and formatted for preview');
return formattedText;
} catch (e) {
Logger.error('Error getting full PDF text for $fileName: $e');
return '';
}
}
/// Clear the chunks cache
static void clearCache() {
_chunksCache.clear();

View File

@@ -648,6 +648,324 @@ class _QuizListPageState extends State<QuizListPage>
dynamic _jsonDecode(String s) => jsonDecode(s);
Future<void> _previewPDF(Map<String, String> mat, String name) async {
final matId = mat['id']!;
final matName = name.replaceAll('.pdf', '').replaceAll('_', ' ');
// Mostrar loading
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
content: Row(
children: [
CircularProgressIndicator(),
const SizedBox(width: 16),
Text('A carregar PDF...'),
],
),
),
);
try {
// Obter o texto completo do PDF usando o método existente
final teacherId = mat['teacherId'];
if (teacherId == null) {
Navigator.of(context).pop();
_showSnack('Erro: não foi possível identificar o professor do material.');
return;
}
final fullText = await MaterialsRAGService.getFullPDFText(matName, teacherId);
Navigator.of(context).pop(); // Fechar loading
if (fullText.isEmpty) {
_showSnack('Não foi possível carregar o conteúdo do PDF.');
return;
}
// Mostrar o conteúdo em um diálogo scrollável melhorado
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => Container(
height: MediaQuery.of(context).size.height * 0.90,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 20,
offset: const Offset(0, -4),
),
],
),
child: Column(
children: [
// Handle bar
Container(
width: 40,
height: 4,
margin: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.4),
borderRadius: BorderRadius.circular(2),
),
),
// Header melhorado
Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(context).colorScheme.primary,
Theme.of(context).colorScheme.primary.withOpacity(0.8),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
),
child: Column(
children: [
Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white.withOpacity(0.3)),
),
child: Icon(Icons.picture_as_pdf, color: Colors.white, size: 24),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
matName,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
'Pré-visualização do conteúdo completo',
style: TextStyle(
fontSize: 13,
color: Colors.white.withOpacity(0.9),
),
),
],
),
),
Container(
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close, color: Colors.white, size: 20),
),
),
],
),
const SizedBox(height: 16),
// Stats bar
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white.withOpacity(0.2)),
),
child: Row(
children: [
Icon(Icons.description, color: Colors.white.withOpacity(0.9), size: 18),
const SizedBox(width: 8),
Expanded(
child: Text(
'${(fullText.length / 1000).toStringAsFixed(1)}K caracteres • ${fullText.split('\n').length} linhas',
style: TextStyle(
fontSize: 12,
color: Colors.white.withOpacity(0.9),
fontWeight: FontWeight.w500,
),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'PDF',
style: TextStyle(
fontSize: 10,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
],
),
),
// Content area melhorado
Expanded(
child: Container(
margin: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Theme.of(context).colorScheme.outline.withOpacity(0.1),
),
),
child: Column(
children: [
// Content header
Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
),
child: Row(
children: [
Icon(Icons.text_fields,
color: Theme.of(context).colorScheme.primary,
size: 18),
const SizedBox(width: 8),
Text(
'Conteúdo do Material',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurface,
),
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'Selecionável',
style: TextStyle(
fontSize: 10,
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
// Text content
Expanded(
child: Container(
padding: const EdgeInsets.all(20),
child: SingleChildScrollView(
child: SelectableText(
fullText,
style: TextStyle(
fontSize: 14,
height: 1.7,
color: Theme.of(context).colorScheme.onSurface,
fontFamily: 'Roboto',
letterSpacing: 0.3,
),
),
),
),
),
],
),
),
),
// Footer melhorado
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(24)),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(Icons.info_outline,
color: Theme.of(context).colorScheme.primary,
size: 16),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Texto extraído automaticamente do PDF',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
'Formatação otimizada para melhor legibilidade',
style: TextStyle(
fontSize: 11,
color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.8),
),
),
],
),
),
Icon(Icons.keyboard_arrow_up,
color: Theme.of(context).colorScheme.onSurfaceVariant,
size: 16),
],
),
),
],
),
),
);
} catch (e) {
Logger.error('Error previewing PDF: $e');
Navigator.of(context).pop(); // Fechar loading
_showSnack('Erro ao carregar o PDF. Tenta novamente.');
}
}
void _showInteractiveQuiz(
String title,
List<_QuizQuestion> questions, {
@@ -1030,55 +1348,587 @@ class _QuizListPageState extends State<QuizListPage>
bool isGenerating,
ColorScheme cs,
) {
final cleanName = name.replaceAll('.pdf', '').replaceAll('_', ' ');
return Container(
margin: const EdgeInsets.only(bottom: 4),
decoration: BoxDecoration(
color: cs.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: cs.outline.withOpacity(0.15)),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: cs.outline.withOpacity(0.08),
width: 1,
),
boxShadow: [
BoxShadow(
color: cs.shadow.withOpacity(0.05),
blurRadius: 8,
offset: const Offset(0, 2),
color: cs.shadow.withOpacity(0.04),
blurRadius: 12,
offset: const Offset(0, 4),
),
BoxShadow(
color: cs.primary.withOpacity(0.03),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
leading: Container(
width: 44,
height: 44,
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(20),
onTap: isGenerating ? null : () => _showMaterialOptions(mat, name, cs),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
// PDF Icon com design melhorado
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: cs.secondary.withOpacity(0.12),
borderRadius: BorderRadius.circular(10),
gradient: LinearGradient(
colors: [
cs.secondary.withOpacity(0.15),
cs.secondary.withOpacity(0.08),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
child: Icon(Icons.picture_as_pdf, color: cs.secondary, size: 22),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: cs.secondary.withOpacity(0.2),
width: 1,
),
title: Text(
name.replaceAll('.pdf', '').replaceAll('_', ' '),
),
child: Stack(
alignment: Alignment.center,
children: [
Icon(
Icons.picture_as_pdf,
color: cs.secondary,
size: 26,
),
if (!isGenerating)
Positioned(
bottom: 2,
right: 2,
child: Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: cs.primary,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.play_arrow,
color: Colors.white,
size: 10,
),
),
),
],
),
),
const SizedBox(width: 16),
// Content
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
cleanName,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 15,
color: cs.onSurface,
letterSpacing: 0.2,
),
),
const SizedBox(height: 6),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: cs.primaryContainer.withOpacity(0.5),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.touch_app,
size: 12,
color: cs.primary,
),
const SizedBox(width: 4),
Text(
isGenerating ? 'A gerar...' : 'Toca para opções',
style: TextStyle(
fontSize: 11,
color: cs.primary,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
),
// Action indicator
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: isGenerating
? cs.primary.withOpacity(0.1)
: cs.surfaceVariant.withOpacity(0.5),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isGenerating
? cs.primary.withOpacity(0.2)
: cs.outline.withOpacity(0.1),
),
),
child: isGenerating
? Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: cs.primary,
),
),
)
: Icon(
Icons.more_vert,
color: cs.onSurfaceVariant,
size: 20,
),
),
],
),
),
),
),
);
}
void _showMaterialOptions(Map<String, String> mat, String name, ColorScheme cs) {
final cleanName = name.replaceAll('.pdf', '').replaceAll('_', ' ');
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
isScrollControlled: true,
builder: (context) => Container(
decoration: BoxDecoration(
color: cs.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(28)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
blurRadius: 25,
offset: const Offset(0, -5),
),
],
),
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Handle bar melhorado
Container(
width: 48,
height: 5,
margin: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: cs.onSurfaceVariant.withOpacity(0.3),
borderRadius: BorderRadius.circular(3),
),
),
// Header melhorado com gradient
Container(
margin: const EdgeInsets.symmetric(horizontal: 24),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
cs.primary.withOpacity(0.05),
cs.secondary.withOpacity(0.05),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: cs.outline.withOpacity(0.1),
),
),
child: Row(
children: [
// PDF Icon animado
Container(
width: 52,
height: 52,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
cs.primary.withOpacity(0.2),
cs.secondary.withOpacity(0.15),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: cs.primary.withOpacity(0.3),
width: 1.5,
),
),
child: Stack(
alignment: Alignment.center,
children: [
Icon(
Icons.picture_as_pdf,
color: cs.primary,
size: 28,
),
Positioned(
top: 4,
right: 4,
child: Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: cs.secondary,
shape: BoxShape.circle,
),
child: Icon(
Icons.star,
color: Colors.white,
size: 8,
),
),
),
],
),
),
const SizedBox(width: 16),
// Title section
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
cleanName,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: cs.onSurface,
letterSpacing: 0.2,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 6),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: cs.primaryContainer.withOpacity(0.6),
borderRadius: BorderRadius.circular(20),
),
child: Text(
'Material de estudo',
style: TextStyle(
fontSize: 11,
color: cs.primary,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
],
),
),
const SizedBox(height: 24),
// Instructions
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Row(
children: [
Icon(
Icons.lightbulb_outline,
color: cs.tertiary,
size: 20,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'O que gostarias de fazer com este material?',
style: TextStyle(
fontSize: 15,
color: cs.onSurface,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
const SizedBox(height: 20),
// Options melhoradas
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
children: [
// Preview PDF option com design premium
_buildPremiumOptionTile(
icon: Icons.visibility_rounded,
title: 'Pré-visualizar PDF',
subtitle: 'Ler o conteúdo completo do material',
description: 'Texto formatado e otimizado para leitura',
color: cs.primary,
gradient: LinearGradient(
colors: [
cs.primary.withOpacity(0.1),
cs.primary.withOpacity(0.05),
],
),
onTap: () {
Navigator.of(context).pop();
_previewPDF(mat, name);
},
),
const SizedBox(height: 16),
// Generate Quiz option com design premium
_buildPremiumOptionTile(
icon: Icons.quiz_rounded,
title: 'Gerar Quiz',
subtitle: 'Criar um quiz personalizado',
description: 'Baseado em IA com perguntas inteligentes',
color: cs.secondary,
gradient: LinearGradient(
colors: [
cs.secondary.withOpacity(0.1),
cs.secondary.withOpacity(0.05),
],
),
onTap: () {
Navigator.of(context).pop();
_generateQuiz(mat);
},
),
],
),
),
const SizedBox(height: 32),
],
),
),
),
);
}
Widget _buildPremiumOptionTile({
required IconData icon,
required String title,
required String subtitle,
required String description,
required Color color,
required Gradient gradient,
required VoidCallback onTap,
}) {
final cs = Theme.of(context).colorScheme;
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(20),
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: gradient,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: color.withOpacity(0.2),
width: 1.5,
),
boxShadow: [
BoxShadow(
color: color.withOpacity(0.1),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Row(
children: [
// Icon container premium
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: color.withOpacity(0.15),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: color.withOpacity(0.3),
width: 1,
),
),
child: Stack(
alignment: Alignment.center,
children: [
Icon(icon, color: color, size: 28),
Positioned(
bottom: 2,
right: 2,
child: Container(
width: 14,
height: 14,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
child: Icon(
Icons.arrow_forward,
color: Colors.white,
size: 8,
),
),
),
],
),
),
const SizedBox(width: 16),
// Text content
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: cs.onSurface,
letterSpacing: 0.3,
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: TextStyle(
fontSize: 13,
color: cs.onSurfaceVariant,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 2),
Text(
description,
style: TextStyle(
fontSize: 11,
color: cs.onSurfaceVariant.withOpacity(0.8),
),
),
],
),
),
// Arrow indicator
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
Icons.chevron_right,
color: color,
size: 18,
),
),
],
),
),
);
}
Widget _buildOptionTile({
required IconData icon,
required String title,
required String subtitle,
required Color color,
required VoidCallback onTap,
}) {
final cs = Theme.of(context).colorScheme;
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: color.withOpacity(0.08),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withOpacity(0.2)),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: color.withOpacity(0.15),
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, color: color, size: 20),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
fontSize: 14,
color: cs.onSurface,
),
),
subtitle: Text(
'Toca para gerar um quiz',
style: TextStyle(fontSize: 12, color: cs.onSurfaceVariant),
const SizedBox(height: 2),
Text(
subtitle,
style: TextStyle(
fontSize: 12,
color: cs.onSurfaceVariant,
),
trailing: isGenerating
? SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2.5,
color: cs.primary,
),
)
: Icon(Icons.play_circle_outline, color: cs.primary, size: 28),
onTap: isGenerating ? null : () => _generateQuiz(mat),
],
),
),
Icon(Icons.chevron_right, color: cs.onSurfaceVariant, size: 20),
],
),
),
);
}