IA e pequenas coisas a funcionar
This commit is contained in:
415
lib/features/ai_tutor/presentation/widgets/chat_input.dart
Normal file
415
lib/features/ai_tutor/presentation/widgets/chat_input.dart
Normal file
@@ -0,0 +1,415 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import '../../../../core/services/rag_service.dart';
|
||||
|
||||
/// Enhanced chat input widget with suggestions and mode selection
|
||||
class ChatInput extends StatefulWidget {
|
||||
final TextEditingController controller;
|
||||
final VoidCallback onSend;
|
||||
final ValueChanged<TutorMode>? onModeChanged;
|
||||
final TutorMode currentMode;
|
||||
final bool isLoading;
|
||||
final List<String> suggestions;
|
||||
final VoidCallback? onClear;
|
||||
|
||||
const ChatInput({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.onSend,
|
||||
this.onModeChanged,
|
||||
this.currentMode = TutorMode.explanation,
|
||||
this.isLoading = false,
|
||||
this.suggestions = const [],
|
||||
this.onClear,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ChatInput> createState() => _ChatInputState();
|
||||
}
|
||||
|
||||
class _ChatInputState extends State<ChatInput> {
|
||||
bool _showSuggestions = false;
|
||||
bool _isExpanded = false;
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_focusNode.addListener(_onFocusChange);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_focusNode.removeListener(_onFocusChange);
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onFocusChange() {
|
||||
setState(() {
|
||||
_showSuggestions = _focusNode.hasFocus && widget.suggestions.isNotEmpty;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
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: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Mode selector
|
||||
_buildModeSelector(context),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Input field with send button
|
||||
_buildInputField(context),
|
||||
|
||||
// Suggestions
|
||||
if (_showSuggestions) ...[
|
||||
const SizedBox(height: 8),
|
||||
_buildSuggestions(context),
|
||||
],
|
||||
],
|
||||
),
|
||||
).animate().slideY(begin: 1.0, end: 0.0, duration: const Duration(milliseconds: 300));
|
||||
}
|
||||
|
||||
Widget _buildModeSelector(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.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(
|
||||
'Modo de Tutoria',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: TutorMode.values.map((mode) => _buildModeButton(mode)).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildModeButton(TutorMode mode) {
|
||||
final isSelected = widget.currentMode == mode;
|
||||
final modeInfo = _getModeInfo(mode);
|
||||
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: InkWell(
|
||||
onTap: () => widget.onModeChanged?.call(mode),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0),
|
||||
decoration: BoxDecoration(
|
||||
gradient: isSelected
|
||||
? LinearGradient(
|
||||
colors: [
|
||||
modeInfo['color'] as Color,
|
||||
modeInfo['colorDark'] as Color,
|
||||
],
|
||||
)
|
||||
: null,
|
||||
color: isSelected ? null : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: isSelected ? Colors.transparent : Colors.grey[300]!,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
modeInfo['icon'] as IconData,
|
||||
size: 20,
|
||||
color: isSelected ? Colors.white : Colors.grey[600],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
modeInfo['label'] as String,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isSelected ? Colors.white : Colors.grey[600],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInputField(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Colors.grey[100]!,
|
||||
Colors.grey[50]!,
|
||||
],
|
||||
),
|
||||
border: Border.all(
|
||||
color: Colors.grey[300]!,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Text field
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: widget.controller,
|
||||
focusNode: _focusNode,
|
||||
maxLines: _isExpanded ? 5 : 1,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Color(0xFF2D3748),
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Faça sua pergunta sobre o conteúdo...',
|
||||
hintStyle: TextStyle(
|
||||
color: Colors.grey[500],
|
||||
),
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
onSubmitted: (_) => _handleSend(),
|
||||
),
|
||||
),
|
||||
|
||||
// Action buttons
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Expand/Collapse button
|
||||
if (widget.controller.text.isNotEmpty) ...[
|
||||
IconButton(
|
||||
onPressed: () => setState(() => _isExpanded = !_isExpanded),
|
||||
icon: Icon(
|
||||
_isExpanded ? Icons.compress : Icons.expand,
|
||||
color: Colors.grey[600],
|
||||
size: 20,
|
||||
),
|
||||
tooltip: _isExpanded ? 'Reduzir' : 'Expandir',
|
||||
),
|
||||
],
|
||||
|
||||
// Clear button
|
||||
if (widget.controller.text.isNotEmpty)
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
widget.controller.clear();
|
||||
widget.onClear?.call();
|
||||
setState(() {
|
||||
_isExpanded = false;
|
||||
_showSuggestions = false;
|
||||
});
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.clear,
|
||||
color: Colors.grey[600],
|
||||
size: 20,
|
||||
),
|
||||
tooltip: 'Limpar',
|
||||
),
|
||||
|
||||
// Send button
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
gradient: widget.controller.text.isNotEmpty
|
||||
? const LinearGradient(
|
||||
colors: [Color(0xFF82C9BD), Color(0xFF6BA5A0)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
)
|
||||
: null,
|
||||
color: widget.controller.text.isNotEmpty ? null : Colors.grey[300],
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: widget.controller.text.isNotEmpty
|
||||
? [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF82C9BD).withOpacity(0.3),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: widget.controller.text.isNotEmpty && !widget.isLoading
|
||||
? _handleSend
|
||||
: null,
|
||||
icon: widget.isLoading
|
||||
? SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Colors.white,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
Icons.send,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSuggestions(BuildContext context) {
|
||||
return 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: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.lightbulb,
|
||||
size: 16,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'Sugestões',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
children: widget.suggestions.take(6).map((suggestion) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
widget.controller.text = suggestion;
|
||||
_focusNode.requestFocus();
|
||||
},
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
const Color(0xFF82C9BD).withOpacity(0.1),
|
||||
const Color(0xFF6BA5A0).withOpacity(0.1),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: const Color(0xFF82C9BD).withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
suggestion,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: const Color(0xFF82C9BD),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
).animate().fadeIn(duration: const Duration(milliseconds: 200));
|
||||
}
|
||||
|
||||
Map<String, dynamic> _getModeInfo(TutorMode mode) {
|
||||
switch (mode) {
|
||||
case TutorMode.explanation:
|
||||
return {
|
||||
'label': 'Explicação',
|
||||
'icon': Icons.school,
|
||||
'color': const Color(0xFF82C9BD),
|
||||
'colorDark': const Color(0xFF6BA5A0),
|
||||
};
|
||||
case TutorMode.tutor:
|
||||
return {
|
||||
'label': 'Tutor',
|
||||
'icon': Icons.psychology,
|
||||
'color': const Color(0xFFF68D2D),
|
||||
'colorDark': const Color(0xFFE67E22),
|
||||
};
|
||||
case TutorMode.exploration:
|
||||
return {
|
||||
'label': 'Exploração',
|
||||
'icon': Icons.explore,
|
||||
'color': const Color(0xFF9C27B0),
|
||||
'colorDark': const Color(0xFF7B1FA2),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
void _handleSend() {
|
||||
if (widget.controller.text.trim().isNotEmpty && !widget.isLoading) {
|
||||
widget.onSend();
|
||||
setState(() {
|
||||
_isExpanded = false;
|
||||
_showSuggestions = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
367
lib/features/ai_tutor/presentation/widgets/message_bubble.dart
Normal file
367
lib/features/ai_tutor/presentation/widgets/message_bubble.dart
Normal file
@@ -0,0 +1,367 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import '../../../../core/services/rag_service.dart';
|
||||
|
||||
/// Widget for displaying chat messages with source citations
|
||||
class MessageBubble extends StatelessWidget {
|
||||
final String content;
|
||||
final bool isUser;
|
||||
final DateTime timestamp;
|
||||
final List<SourceCitation>? sources;
|
||||
final double? confidence;
|
||||
final bool showSources;
|
||||
final VoidCallback? onSourceTap;
|
||||
|
||||
const MessageBubble({
|
||||
super.key,
|
||||
required this.content,
|
||||
required this.isUser,
|
||||
required this.timestamp,
|
||||
this.sources,
|
||||
this.confidence,
|
||||
this.showSources = true,
|
||||
this.onSourceTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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: Column(
|
||||
crossAxisAlignment: isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildMessageBubble(context),
|
||||
if (!isUser && showSources && sources != null && sources!.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
_buildSourceCitations(context),
|
||||
],
|
||||
if (!isUser && confidence != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
_buildConfidenceIndicator(context),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isUser) ...[
|
||||
const SizedBox(width: 12),
|
||||
_buildAvatar(context),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
_buildTimestamp(context),
|
||||
],
|
||||
),
|
||||
).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: isUser
|
||||
? const LinearGradient(
|
||||
colors: [Color(0xFF82C9BD), Color(0xFF6BA5A0)],
|
||||
)
|
||||
: const LinearGradient(
|
||||
colors: [Color(0xFFF68D2D), Color(0xFFE67E22)],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: (isUser ? const Color(0xFF82C9BD) : const Color(0xFFF68D2D))
|
||||
.withOpacity(0.3),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
isUser ? Icons.person : Icons.school,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMessageBubble(BuildContext context) {
|
||||
return 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: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
content,
|
||||
style: TextStyle(
|
||||
color: isUser ? Colors.white : const Color(0xFF2D3748),
|
||||
fontSize: 16,
|
||||
height: 1.4,
|
||||
fontWeight: isUser ? FontWeight.w500 : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSourceCitations(BuildContext context) {
|
||||
return 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: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.source,
|
||||
size: 16,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'Fontes',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
'${sources!.length} ${sources!.length == 1 ? 'fonte' : 'fontes'}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...sources!.take(3).map((source) => _buildSourceItem(context, source)),
|
||||
if (sources!.length > 3) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'+${sources!.length - 3} mais fontes...',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey[600],
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSourceItem(BuildContext context, SourceCitation source) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 6.0),
|
||||
child: InkWell(
|
||||
onTap: () => onSourceTap?.call(),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Colors.grey[200]!,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.menu_book,
|
||||
size: 14,
|
||||
color: const Color(0xFF82C9BD),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
source.title,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey[800],
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF82C9BD).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Text(
|
||||
'${(source.relevance * 100).toInt()}%',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: const Color(0xFF82C9BD),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.category,
|
||||
size: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${source.subject} • ${source.concept}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
if (source.pageNumber != null) ...[
|
||||
const Spacer(),
|
||||
Icon(
|
||||
Icons.book,
|
||||
size: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
Text(
|
||||
'p. ${source.pageNumber}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildConfidenceIndicator(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.verified,
|
||||
size: 14,
|
||||
color: _getConfidenceColor(confidence!),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Confiança: ${(confidence! * 100).toInt()}%',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: _getConfidenceColor(confidence!),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Color _getConfidenceColor(double confidence) {
|
||||
if (confidence >= 0.8) return Colors.green;
|
||||
if (confidence >= 0.6) return Colors.orange;
|
||||
return Colors.red;
|
||||
}
|
||||
|
||||
Widget _buildTimestamp(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: isUser ? 0 : 48,
|
||||
right: isUser ? 48 : 0,
|
||||
),
|
||||
child: Text(
|
||||
_formatTimestamp(timestamp),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user