Files
App_Projeto/lib/chat_screen.dart

325 lines
11 KiB
Dart

import 'dart:ui';
import 'package:flutter/material.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'dart:async';
class ChatScreen extends StatefulWidget {
const ChatScreen({super.key});
@override
State<ChatScreen> createState() => _ChatScreenState();
}
class ChatMessage {
final String text;
final bool isAssistant;
ChatMessage({required this.text, this.isAssistant = false});
}
class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
final List<ChatMessage> _messages = [];
final TextEditingController _textController = TextEditingController();
final ScrollController _scrollController = ScrollController();
bool _isTyping = false;
late AnimationController _typingController;
@override
void initState() {
super.initState();
_typingController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1200),
)..repeat();
Timer(const Duration(seconds: 2), () => _checkAvailableModels());
}
Future<void> _checkAvailableModels() async {
try {
final response = await http.get(
Uri.parse('http://89.114.196.110:11434/api/tags'),
).timeout(const Duration(seconds: 15));
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
print("Modelos ok");
}
} catch (e) {
print("Erro ao listar modelos: $e");
}
}
@override
void dispose() {
_typingController.dispose();
_textController.dispose();
_scrollController.dispose();
super.dispose();
}
// Função para limpar o chat
void _clearChat() {
setState(() {
_messages.clear();
_isTyping = false;
});
}
Future<void> _handleSubmitted(String text) async {
// BLOQUEIO: Se já estiver a escrever (isTyping), ignora o clique
if (text.trim().isEmpty || _isTyping) return;
_textController.clear();
setState(() {
_messages.insert(0, ChatMessage(text: text));
_isTyping = true; // Ativa o bloqueio
});
try {
final url = Uri.parse('http://89.114.196.110:11434/v1/chat/completions');
final response = await http
.post(
url,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: jsonEncode({
'model': 'tinyllama',
'messages': [{'role': 'user', 'content': text}],
'stream': false,
}),
)
.timeout(const Duration(seconds: 60));
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final reply = data['choices'][0]['message']['content'] ?? 'Sem resposta.';
if (mounted) {
setState(() {
_isTyping = false; // Liberta o bloqueio
_messages.insert(0, ChatMessage(text: reply, isAssistant: true));
});
}
} else {
throw Exception('Erro ${response.statusCode}');
}
} catch (e) {
if (mounted) {
setState(() {
_isTyping = false; // Liberta o bloqueio mesmo em caso de erro
_messages.insert(
0,
ChatMessage(text: "Erro: $e", isAssistant: true),
);
});
}
}
}
// ... (Mantenha _buildMessage, _buildAvatar, _buildTypingIndicator e _buildAnimatedDot iguais)
Widget _buildMessage(ChatMessage message) {
bool isAssistant = message.isAssistant;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0),
child: Row(
mainAxisAlignment: isAssistant ? MainAxisAlignment.start : MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (isAssistant) _buildAvatar(Icons.auto_awesome),
Flexible(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 14.0),
decoration: BoxDecoration(
gradient: isAssistant
? const LinearGradient(colors: [Color(0xFFF1F5F9), Color(0xFFE2E8F0)])
: const LinearGradient(colors: [Color(0xFF8ad5c9), Color(0xFF57a7ed)]),
borderRadius: BorderRadius.only(
topLeft: const Radius.circular(20),
topRight: const Radius.circular(20),
bottomLeft: Radius.circular(isAssistant ? 4 : 20),
bottomRight: Radius.circular(isAssistant ? 20 : 4),
),
),
child: Text(
message.text,
style: TextStyle(color: isAssistant ? Colors.black87 : Colors.white),
),
),
),
if (!isAssistant) _buildAvatar(Icons.person),
],
),
);
}
Widget _buildAvatar(IconData icon) {
return Container(
margin: EdgeInsets.only(left: icon == Icons.person ? 12 : 0, right: icon == Icons.auto_awesome ? 12 : 0),
decoration: const BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(colors: [Color(0xFF8ad5c9), Color(0xFF57a7ed)]),
),
child: CircleAvatar(backgroundColor: Colors.transparent, radius: 16, child: Icon(icon, color: Colors.white, size: 18)),
);
}
Widget _buildTypingIndicator() {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
_buildAvatar(Icons.auto_awesome),
Container(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
decoration: BoxDecoration(
color: const Color(0xFFF1F5F9),
borderRadius: const BorderRadius.only(topLeft: Radius.circular(20), topRight: Radius.circular(20), bottomLeft: Radius.circular(4), bottomRight: Radius.circular(20)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [_buildAnimatedDot(0), const SizedBox(width: 4), _buildAnimatedDot(1), const SizedBox(width: 4), _buildAnimatedDot(2)],
),
),
],
),
);
}
Widget _buildAnimatedDot(int index) {
return AnimatedBuilder(
animation: _typingController,
builder: (context, child) {
double value = (_typingController.value + (index * 0.15)) % 1.0;
double intensity = 1.0 - (value - 0.5).abs() * 2;
return Opacity(
opacity: 0.3 + (0.7 * intensity),
child: Transform.translate(offset: Offset(0, -3 * intensity), child: Container(width: 6, height: 6, decoration: const BoxDecoration(color: Colors.black54, shape: BoxShape.circle))),
);
},
);
}
Widget _buildTextComposer() {
return ClipRRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.8),
border: Border(top: BorderSide(color: Colors.black.withOpacity(0.05), width: 0.5)),
),
child: SafeArea(
child: Row(
children: [
// BOTÃO APAGAR CHAT
IconButton(
icon: const Icon(Icons.delete_sweep_rounded, color: Colors.redAccent),
onPressed: _isTyping ? null : _clearChat, // Desativa enquanto digita
),
Expanded(
child: Container(
decoration: BoxDecoration(
color: const Color(0xFFF1F5F9),
borderRadius: BorderRadius.circular(30.0),
border: Border.all(color: Colors.black.withOpacity(0.05)),
),
child: TextField(
controller: _textController,
enabled: !_isTyping, // Bloqueia o campo de texto
onSubmitted: _handleSubmitted,
decoration: InputDecoration(
hintText: _isTyping ? "Aguarde a resposta..." : "Mensagem EPVChat...",
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 14.0),
),
),
),
),
const SizedBox(width: 8),
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: _isTyping
? [Colors.grey, Colors.grey] // Cor de desativado
: [const Color(0xFF8ad5c9), const Color(0xFF57a7ed)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
shape: BoxShape.circle,
),
child: IconButton(
icon: const Icon(Icons.send_rounded, color: Colors.white),
onPressed: _isTyping ? null : () => _handleSubmitted(_textController.text),
),
),
],
),
),
),
),
);
}
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
return Scaffold(
backgroundColor: Colors.white,
body: Column(
children: [
Expanded(
child: ListView(
controller: _scrollController,
reverse: true, // Mantém a lógica de mensagens novas em baixo
children: [
// As mensagens vêm primeiro (no reverse: true, o topo da lista é o fundo do ecrã)
...(_isTyping ? [_buildTypingIndicator()] : []),
..._messages.map((msg) => _buildMessage(msg)),
// O LOGO E O SOMBREADO AGORA SÃO O ÚLTIMO ITEM DO LISTVIEW
// Quando o utilizador sobe o chat, eles sobem junto
Padding(
padding: const EdgeInsets.only(bottom: 50, top: 20),
child: Stack(
alignment: Alignment.center,
children: [
// O Sombreado Verde
Container(
height: screenWidth * 0.8,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [
const Color(0xFF8ad5c9).withOpacity(0.4),
const Color(0xFF8ad5c9).withOpacity(0.0),
],
),
),
),
// O Logo
Image.asset(
'assets/logo.png',
height: 170,
fit: BoxFit.contain,
),
],
),
),
],
),
),
_buildTextComposer(),
],
),
);
}
}