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,674 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:go_router/go_router.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import '../../../../core/services/rag_service.dart';
import '../../../../core/services/auth_service.dart';
import '../../../../shared/presentation/widgets/custom_notification.dart';
import '../widgets/message_bubble.dart';
import '../widgets/chat_input.dart';
/// Main AI Tutor chat interface page
class TutorChatPage extends StatefulWidget {
const TutorChatPage({super.key});
@override
State<TutorChatPage> createState() => _TutorChatPageState();
}
class _TutorChatPageState extends State<TutorChatPage>
with TickerProviderStateMixin {
final TextEditingController _messageController = TextEditingController();
final ScrollController _scrollController = ScrollController();
late AnimationController _fadeController;
late AnimationController _slideController;
TutorMode _currentMode = TutorMode.explanation;
bool _isLoading = false;
List<Map<String, dynamic>> _messages = [];
bool _showHistory = false;
List<Map<String, dynamic>> _conversationHistory = [];
// Sample suggestions
final List<String> _suggestions = [
'O que é fotossíntese?',
'Como as plantas produzem energia?',
'Explica a cadeia alimentar',
'Qual a importância dos ecossistemas?',
];
@override
void initState() {
super.initState();
_fadeController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_slideController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_fadeController.forward();
_slideController.forward();
_loadConversationHistory();
_addWelcomeMessage();
}
@override
void dispose() {
_fadeController.dispose();
_slideController.dispose();
_messageController.dispose();
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF8F9FA),
appBar: _buildAppBar(context),
body: Row(
children: [
// Main chat area
Expanded(
child: Column(
children: [
// Messages area
Expanded(
child: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFFF8F9FA),
Color(0xFFE8F0FE),
Color(0xFFF8F9FA),
],
),
),
child: _buildMessagesArea(context),
),
),
// Input area
ChatInput(
controller: _messageController,
onSend: _handleSendMessage,
onModeChanged: _handleModeChanged,
currentMode: _currentMode,
isLoading: _isLoading,
suggestions: _suggestions,
onClear: _handleClearChat,
),
],
),
),
// History sidebar (desktop only)
if (MediaQuery.of(context).size.width > 768 && _showHistory)
_buildHistorySidebar(context),
],
),
);
}
PreferredSizeWidget _buildAppBar(BuildContext context) {
return AppBar(
backgroundColor: Colors.white,
elevation: 0,
title: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF82C9BD), Color(0xFF6BA5A0)],
),
borderRadius: BorderRadius.circular(20),
),
child: const Icon(Icons.school, color: Colors.white, size: 24),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'AI Study Assistant',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.grey[800],
),
),
Text(
'Seu tutor educacional inteligente',
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
],
),
],
),
actions: [
// History toggle (desktop)
if (MediaQuery.of(context).size.width > 768)
IconButton(
onPressed: () => setState(() => _showHistory = !_showHistory),
icon: Icon(
_showHistory ? Icons.history : Icons.history,
color: Colors.grey[700],
),
tooltip: 'Histórico de Conversas',
),
// Settings
IconButton(
onPressed: _showSettings,
icon: Icon(Icons.settings, color: Colors.grey[700]),
tooltip: 'Configurações',
),
// Logout
IconButton(
onPressed: _handleLogout,
icon: Icon(Icons.logout, color: Colors.grey[700]),
tooltip: 'Sair',
),
],
);
}
Widget _buildMessagesArea(BuildContext context) {
return ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.symmetric(vertical: 16.0),
itemCount: _messages.length,
itemBuilder: (context, index) {
final message = _messages[index];
return MessageBubble(
content: message['content'] as String,
isUser: message['isUser'] as bool,
timestamp: message['timestamp'] as DateTime,
sources: message['sources'] as List<SourceCitation>?,
confidence: message['confidence'] as double?,
onSourceTap: () => _showSourceDetails(message['sources']),
);
},
);
}
Widget _buildHistorySidebar(BuildContext context) {
return Container(
width: 300,
decoration: BoxDecoration(
color: Colors.white,
border: Border(left: BorderSide(color: Colors.grey[200]!)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(-5, 0),
),
],
),
child: Column(
children: [
// Header
Container(
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: Colors.grey[50],
border: Border(bottom: BorderSide(color: Colors.grey[200]!)),
),
child: Row(
children: [
Icon(Icons.history, color: Colors.grey[700]),
const SizedBox(width: 8),
Text(
'Histórico',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.grey[800],
),
),
const Spacer(),
IconButton(
onPressed: () => setState(() => _showHistory = false),
icon: Icon(Icons.close, color: Colors.grey[600]),
),
],
),
),
// History list
Expanded(
child: _conversationHistory.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.chat_outlined,
size: 48,
color: Colors.grey[400],
),
const SizedBox(height: 16),
Text(
'Nenhuma conversa anterior',
style: TextStyle(color: Colors.grey[600]),
),
],
),
)
: ListView.builder(
padding: const EdgeInsets.all(16.0),
itemCount: _conversationHistory.length,
itemBuilder: (context, index) {
final conversation = _conversationHistory[index];
return _buildHistoryItem(conversation);
},
),
),
],
),
).animate().slideX(
begin: 1.0,
end: 0.0,
duration: const Duration(milliseconds: 300),
);
}
Widget _buildHistoryItem(Map<String, dynamic> conversation) {
final query = conversation['query'] as String;
final timestamp = conversation['createdAt'] as Timestamp;
final date = timestamp.toDate();
return Container(
margin: const EdgeInsets.only(bottom: 8.0),
child: InkWell(
onTap: () => _loadConversation(conversation),
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.all(12.0),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[200]!),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
query,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.grey[800],
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
_formatDate(date),
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
],
),
),
),
);
}
void _addWelcomeMessage() {
final welcomeMessage = {
'content': '''Olá! Sou seu assistente educacional AI. Posso ajudar você a:
📚 **Explicar conceitos** de forma detalhada
🤔 **Fazer perguntas socráticas** para guiar seu aprendizado
🔍 **Explorar tópicos** de forma interativa
Escolha um modo de tutoria e faça sua pergunta sobre o conteúdo disponível!''',
'isUser': false,
'timestamp': DateTime.now(),
'sources': <SourceCitation>[],
'confidence': 1.0,
};
setState(() {
_messages.add(welcomeMessage);
});
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom();
});
}
Future<void> _handleSendMessage() async {
if (_messageController.text.trim().isEmpty) return;
final userMessage = _messageController.text.trim();
// Add user message
setState(() {
_messages.add({
'content': userMessage,
'isUser': true,
'timestamp': DateTime.now(),
'sources': <SourceCitation>[],
'confidence': null,
});
_isLoading = true;
});
_messageController.clear();
_scrollToBottom();
try {
// Process query through RAG pipeline
final response = await RAGService.processQuery(
userQuery: userMessage,
mode: _currentMode,
grade: 10, // Would get from user profile
);
// Add AI response
setState(() {
_messages.add({
'content': response.answer,
'isUser': false,
'timestamp': DateTime.now(),
'sources': response.sources,
'confidence': response.confidence,
});
_isLoading = false;
});
// Save conversation
await _saveConversation(userMessage, response);
} catch (e) {
setState(() {
_messages.add({
'content':
'Desculpe, ocorreu um erro ao processar a pergunta. Tente novamente.',
'isUser': false,
'timestamp': DateTime.now(),
'sources': <SourceCitation>[],
'confidence': 0.0,
});
_isLoading = false;
});
NotificationHelper.showError(context, message: e.toString());
}
_scrollToBottom();
}
void _handleModeChanged(TutorMode mode) {
setState(() {
_currentMode = mode;
});
}
void _handleClearChat() {
setState(() {
_messages.clear();
_isLoading = false;
});
_addWelcomeMessage();
}
void _handleLogout() async {
await AuthService.signOut();
if (mounted) {
context.go('/login');
}
}
void _showSettings() {
showModalBottomSheet(
context: context,
builder: (context) => _buildSettingsSheet(context),
);
}
void _showSourceDetails(List<SourceCitation>? sources) {
if (sources == null || sources.isEmpty) return;
showModalBottomSheet(
context: context,
builder: (context) => _buildSourceDetailsSheet(context, sources),
);
}
Future<void> _loadConversationHistory() async {
try {
final user = AuthService.currentUser;
if (user == null) return;
final history = await RAGService.getConversationHistory(
userId: user.uid,
limit: 20,
);
setState(() {
_conversationHistory = history;
});
} catch (e) {
print('Error loading conversation history: $e');
}
}
void _loadConversation(Map<String, dynamic> conversation) {
// Implementation for loading a specific conversation
print('Loading conversation: ${conversation['id']}');
}
Future<void> _saveConversation(String query, RAGResponse response) async {
try {
final user = AuthService.currentUser;
if (user == null) return;
await RAGService.saveConversation(
userId: user.uid,
query: query,
response: response,
);
} catch (e) {
print('Error saving conversation: $e');
}
}
void _scrollToBottom() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scrollController.hasClients) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
});
}
String _formatDate(DateTime date) {
final now = DateTime.now();
final difference = now.difference(date);
if (difference.inDays == 0) {
return 'Hoje ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}';
} else if (difference.inDays == 1) {
return 'Ontem ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}';
} else if (difference.inDays < 7) {
return '${difference.inDays} dias atrás';
} else {
return '${date.day}/${date.month}/${date.year}';
}
}
Widget _buildSettingsSheet(BuildContext context) {
return Container(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Configurações',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.grey[800],
),
),
const SizedBox(height: 20),
ListTile(
leading: Icon(Icons.notifications, color: Colors.grey[700]),
title: Text('Notificações'),
trailing: Switch(value: true, onChanged: (value) {}),
),
ListTile(
leading: Icon(Icons.dark_mode, color: Colors.grey[700]),
title: Text('Modo Escuro'),
trailing: Switch(value: false, onChanged: (value) {}),
),
ListTile(
leading: Icon(Icons.speed, color: Colors.grey[700]),
title: Text('Velocidade de Resposta'),
subtitle: Text('Normal'),
trailing: Icon(Icons.chevron_right),
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF82C9BD),
foregroundColor: Colors.white,
),
child: const Text('Fechar'),
),
),
],
),
);
}
Widget _buildSourceDetailsSheet(
BuildContext context,
List<SourceCitation> sources,
) {
return Container(
height: MediaQuery.of(context).size.height * 0.6,
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.source, color: Colors.grey[700]),
const SizedBox(width: 8),
Text(
'Fontes Citadas',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.grey[800],
),
),
const Spacer(),
IconButton(
onPressed: () => Navigator.pop(context),
icon: Icon(Icons.close),
),
],
),
const SizedBox(height: 20),
Expanded(
child: ListView.builder(
itemCount: sources.length,
itemBuilder: (context, index) {
final source = sources[index];
return Card(
margin: const EdgeInsets.only(bottom: 12.0),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
source.title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.grey[800],
),
),
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.category,
size: 16,
color: Colors.grey[600],
),
const SizedBox(width: 4),
Text('${source.subject}${source.concept}'),
],
),
const SizedBox(height: 4),
Row(
children: [
Icon(
Icons.menu_book,
size: 16,
color: Colors.grey[600],
),
const SizedBox(width: 4),
Text(
'Relevância: ${(source.relevance * 100).toInt()}%',
),
if (source.pageNumber != null) ...[
const SizedBox(width: 16),
Icon(
Icons.book,
size: 16,
color: Colors.grey[600],
),
const SizedBox(width: 4),
Text('Página ${source.pageNumber}'),
],
],
),
const SizedBox(height: 8),
Text(
source.excerpt,
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
fontStyle: FontStyle.italic,
),
),
],
),
),
);
},
),
),
],
),
);
}
}

View File

@@ -0,0 +1,612 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:go_router/go_router.dart';
import 'package:http/http.dart' as http;
import '../../../../core/services/auth_service.dart';
/// Simple AI Tutor chat interface page (for testing)
class TutorChatPageSimple extends StatefulWidget {
const TutorChatPageSimple({super.key});
@override
State<TutorChatPageSimple> createState() => _TutorChatPageSimpleState();
}
class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
with TickerProviderStateMixin {
final TextEditingController _messageController = TextEditingController();
final ScrollController _scrollController = ScrollController();
bool _isLoading = false;
List<Map<String, dynamic>> _messages = [];
@override
void initState() {
super.initState();
_addWelcomeMessage();
}
@override
void dispose() {
_messageController.dispose();
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
onPopInvoked: (didPop) {
if (didPop) return;
// Navigate back to dashboard instead of exiting app
if (mounted) {
context.go('/student-dashboard');
}
},
child: Scaffold(
backgroundColor: const Color(0xFFF8F9FA),
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
title: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF82C9BD), Color(0xFF6BA5A0)],
),
borderRadius: BorderRadius.circular(20),
),
child: const Icon(Icons.school, color: Colors.white, size: 24),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'AI Study Assistant',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.grey[800],
),
),
Text(
'Seu tutor educacional inteligente',
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
],
),
],
),
actions: [
IconButton(
onPressed: _handleLogout,
icon: Icon(Icons.logout, color: Colors.grey[700]),
tooltip: 'Sair',
),
],
),
body: Column(
children: [
// Messages area
Expanded(
child: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFFF8F9FA),
Color(0xFFE8F0FE),
Color(0xFFF8F9FA),
],
),
),
child: _buildMessagesArea(context),
),
),
// Input area
_buildInputArea(context),
],
),
),
);
}
Widget _buildMessagesArea(BuildContext context) {
return ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.symmetric(vertical: 16.0),
itemCount: _messages.length,
itemBuilder: (context, index) {
final message = _messages[index];
return _buildMessageBubble(message);
},
);
}
Widget _buildMessageBubble(Map<String, dynamic> message) {
final isUser = message['isUser'] as bool;
final content = message['content'] as String;
final timestamp = message['timestamp'] as DateTime;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
child: Column(
crossAxisAlignment: isUser
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: isUser
? MainAxisAlignment.end
: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isUser) ...[
_buildAvatar(context),
const SizedBox(width: 12),
],
Flexible(
child: Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.75,
),
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
gradient: isUser
? const LinearGradient(
colors: [Color(0xFF82C9BD), Color(0xFF6BA5A0)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
)
: LinearGradient(
colors: [
Colors.white.withOpacity(0.95),
Colors.white.withOpacity(0.9),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.only(
topLeft: const Radius.circular(20),
topRight: const Radius.circular(20),
bottomLeft: isUser
? const Radius.circular(20)
: const Radius.circular(4),
bottomRight: isUser
? const Radius.circular(4)
: const Radius.circular(20),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Text(
content,
style: TextStyle(
color: isUser
? Colors.white
: const Color(0xFF2D3748),
fontSize: 16,
height: 1.4,
fontWeight: isUser
? FontWeight.w500
: FontWeight.normal,
),
),
),
),
if (isUser) ...[
const SizedBox(width: 12),
_buildAvatar(context),
],
],
),
const SizedBox(height: 4),
Padding(
padding: EdgeInsets.only(
left: isUser ? 0 : 48,
right: isUser ? 48 : 0,
),
child: Text(
_formatTimestamp(timestamp),
style: TextStyle(fontSize: 11, color: Colors.grey[600]),
),
),
],
),
)
.animate()
.fadeIn(duration: const Duration(milliseconds: 300))
.slideY(
begin: isUser ? 0.1 : -0.1,
end: 0,
duration: const Duration(milliseconds: 400),
);
}
Widget _buildAvatar(BuildContext context) {
return Container(
width: 36,
height: 36,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF82C9BD), Color(0xFF6BA5A0)],
),
borderRadius: BorderRadius.circular(18),
boxShadow: [
BoxShadow(
color: const Color(0xFF82C9BD).withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: const Icon(Icons.person, color: Colors.white, size: 20),
);
}
Widget _buildInputArea(BuildContext context) {
// Get bottom padding to avoid system navigation bar
final bottomPadding = MediaQuery.of(context).padding.bottom;
return Container(
padding: EdgeInsets.only(
left: 16.0,
right: 16.0,
top: 16.0,
bottom: bottomPadding + 16.0, // Add system navigation bar padding
),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.95),
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 20,
offset: const Offset(0, -5),
),
],
),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: Colors.white,
border: Border.all(color: const Color(0xFFE2E8F0), width: 1),
),
child: Row(
children: [
// Text field
Expanded(
child: TextField(
controller: _messageController,
style: const TextStyle(
fontSize: 16,
color: Colors.white,
fontWeight: FontWeight.w500,
),
decoration: InputDecoration(
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
hintText: 'Faz a tua pergunta!',
hintStyle: TextStyle(
color: Colors.grey[400],
fontWeight: FontWeight.w400,
fontSize: 16,
),
),
onSubmitted: (_) => _handleSendMessage(),
textInputAction: TextInputAction.send,
onChanged: (value) {
setState(() {});
},
),
),
// Send button
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Container(
width: 44,
height: 44,
decoration: BoxDecoration(
gradient: _messageController.text.isNotEmpty
? const LinearGradient(
colors: [Color(0xFF82C9BD), Color(0xFF6BA5A0)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
)
: null,
color: _messageController.text.isNotEmpty
? null
: Colors.grey[300],
borderRadius: BorderRadius.circular(22),
boxShadow: _messageController.text.isNotEmpty
? [
BoxShadow(
color: const Color(0xFF82C9BD).withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
]
: null,
),
child: IconButton(
onPressed: _messageController.text.isNotEmpty && !_isLoading
? _handleSendMessage
: null,
icon: _isLoading
? SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white,
),
),
)
: Icon(Icons.send, color: Colors.white, size: 18),
),
),
),
],
),
),
);
}
void _addWelcomeMessage() {
final welcomeMessage = {
'content': '''Olá! Sou seu assistente educacional AI.
Bem-vindo ao TeachIT AI Tutor!
Funcionalidades disponíveis:
📚 Respostas baseadas em conteúdo educacional
🔍 Busca vetorial semântica
🤖 Integração com Ollama API
📖 Citações de fontes relevantes
🎯 Modo de aprendizado adaptativo
O sistema usa RAG (Retrieval-Augmented Generation) para fornecer respostas baseadas apenas no conteúdo educacional disponível.
Faça sua pergunta sobre qualquer assunto educacional!''',
'isUser': false,
'timestamp': DateTime.now(),
};
setState(() {
_messages.add(welcomeMessage);
});
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom();
});
}
Future<void> _handleSendMessage() async {
if (_messageController.text.trim().isEmpty) return;
final userMessage = _messageController.text.trim();
// Add user message
setState(() {
_messages.add({
'content': userMessage,
'isUser': true,
'timestamp': DateTime.now(),
});
_isLoading = true;
});
_messageController.clear();
_scrollToBottom();
try {
// Direct call to Ollama API based on working example
print('Processing query: $userMessage');
final url = Uri.parse('http://89.114.196.110:11434/api/chat');
final response = await http
.post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'model': 'qwen3-coder:30b',
'messages': [
{'role': 'user', 'content': userMessage},
],
'stream': false,
}),
)
.timeout(const Duration(seconds: 60));
print('API response status: ${response.statusCode}');
print('API response body: ${response.body}');
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final replyText = data['message']?['content'] ?? 'Sem resposta.';
final preview = replyText.length > 50
? replyText.substring(0, 50)
: replyText;
print('Ollama response received: $preview...');
setState(() {
_messages.add({
'content': replyText,
'isUser': false,
'timestamp': DateTime.now(),
});
_isLoading = false;
});
} else {
throw Exception('Erro HTTP ${response.statusCode}');
}
} catch (e) {
// Fallback to mock response if API fails
print('Ollama API error: $e');
print('Stack trace: ${StackTrace.current}');
final aiResponse = _generateMockResponse(userMessage);
setState(() {
_messages.add({
'content': aiResponse,
'isUser': false,
'timestamp': DateTime.now(),
});
_isLoading = false;
});
}
_scrollToBottom();
}
String _generateMockResponse(String userQuery) {
final responses = [
'Esta é uma resposta simulada para: "$userQuery". Na versão completa, esta resposta seria gerada pela API Ollama com base no conteúdo dos professores.',
'Recebi sua pergunta sobre "$userQuery". O sistema RAG completo buscaria conteúdo relevante no banco de dados e geraria uma resposta personalizada.',
'Sobre "$userQuery": A versão completa usaria embeddings vetoriais para encontrar o conteúdo mais relevante e fornecer uma resposta baseada apenas no material educacional.',
];
return responses[(userQuery.hashCode % responses.length)];
}
void _handleLogout() async {
await AuthService.signOut();
if (mounted) {
context.go('/login');
}
}
void _scrollToBottom() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scrollController.hasClients) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
});
}
String _formatTimestamp(DateTime timestamp) {
final now = DateTime.now();
final difference = now.difference(timestamp);
if (difference.inMinutes < 1) {
return 'Agora';
} else if (difference.inMinutes < 60) {
return '${difference.inMinutes} min atrás';
} else if (difference.inHours < 24) {
return '${difference.inHours} h atrás';
} else {
return '${difference.inDays} dias atrás';
}
}
}
/// Widget para texto com scrolling horizontal (Marquee)
class MarqueeText extends StatefulWidget {
final String text;
final TextStyle style;
final Duration duration;
final double scrollSpeed;
const MarqueeText({
super.key,
required this.text,
required this.style,
this.duration = const Duration(seconds: 8),
this.scrollSpeed = 50.0,
});
@override
State<MarqueeText> createState() => _MarqueeTextState();
}
class _MarqueeTextState extends State<MarqueeText>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
final GlobalKey _textKey = GlobalKey();
double _textWidth = 0;
double _containerWidth = 0;
@override
void initState() {
super.initState();
_controller = AnimationController(duration: widget.duration, vsync: this);
_animation = Tween<double>(begin: 0, end: 1).animate(_controller);
WidgetsBinding.instance.addPostFrameCallback((_) {
_measureText();
});
}
void _measureText() {
final RenderBox? textRenderBox =
_textKey.currentContext?.findRenderObject() as RenderBox?;
if (textRenderBox != null) {
_textWidth = textRenderBox.size.width;
final RenderBox? containerRenderBox =
context.findRenderObject() as RenderBox?;
if (containerRenderBox != null) {
_containerWidth = containerRenderBox.size.width;
if (_textWidth > _containerWidth) {
_controller.repeat();
}
}
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return LayoutBuilder(
builder: (context, constraints) {
if (_textWidth <= constraints.maxWidth) {
return Text(widget.text, style: widget.style, key: _textKey);
}
return Stack(
children: [
Positioned(
left: -(_textWidth - constraints.maxWidth) * _animation.value,
child: Text(widget.text, style: widget.style, key: _textKey),
),
],
);
},
);
},
);
}
}