Tudo a funcionar, apenas falta a base de dados

This commit is contained in:
2026-03-16 20:10:36 +00:00
parent 7ca6cc23ca
commit c5db0717c5
3 changed files with 60 additions and 31 deletions

View File

@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'dart:async';
import 'package:flutter_markdown/flutter_markdown.dart';
class ChatScreen extends StatefulWidget {
const ChatScreen({super.key});
@@ -44,7 +45,12 @@ class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
print("Modelos ok");
print("--- MODELOS DISPONÍVEIS ---");
if (data['models'] != null) {
for (var m in data['models']) {
print("- ${m['name']}");
}
}
}
} catch (e) {
print("Erro ao listar modelos: $e");
@@ -59,7 +65,6 @@ class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
super.dispose();
}
// Função para limpar o chat
void _clearChat() {
setState(() {
_messages.clear();
@@ -68,51 +73,49 @@ class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
}
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
_isTyping = true;
});
try {
final url = Uri.parse('http://89.114.196.110:11434/v1/chat/completions');
final url = Uri.parse('http://89.114.196.110:11434/api/chat');
final response = await http
.post(
url,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: jsonEncode({
'model': 'tinyllama',
'model': 'qwen3:4b',
'messages': [{'role': 'user', 'content': text}],
'stream': false,
'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.';
final reply = data['message']?['content'] ?? 'Sem resposta.';
if (mounted) {
setState(() {
_isTyping = false; // Liberta o bloqueio
_isTyping = false;
_messages.insert(0, ChatMessage(text: reply, isAssistant: true));
});
}
} else {
throw Exception('Erro ${response.statusCode}');
throw Exception('Erro HTTP ${response.statusCode}: ${response.body}');
}
} catch (e) {
if (mounted) {
setState(() {
_isTyping = false; // Liberta o bloqueio mesmo em caso de erro
_isTyping = false;
_messages.insert(
0,
ChatMessage(text: "Erro: $e", isAssistant: true),
@@ -122,7 +125,6 @@ class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
}
}
// ... (Mantenha _buildMessage, _buildAvatar, _buildTypingIndicator e _buildAnimatedDot iguais)
Widget _buildMessage(ChatMessage message) {
bool isAssistant = message.isAssistant;
return Padding(
@@ -134,7 +136,7 @@ class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
if (isAssistant) _buildAvatar(Icons.auto_awesome),
Flexible(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 14.0),
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0),
decoration: BoxDecoration(
gradient: isAssistant
? const LinearGradient(colors: [Color(0xFFF1F5F9), Color(0xFFE2E8F0)])
@@ -146,10 +148,19 @@ class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
bottomRight: Radius.circular(isAssistant ? 20 : 4),
),
),
child: Text(
message.text,
style: TextStyle(color: isAssistant ? Colors.black87 : Colors.white),
),
child: isAssistant
? MarkdownBody(
data: message.text,
styleSheet: MarkdownStyleSheet(
p: const TextStyle(color: Colors.black87, fontSize: 15, height: 1.4),
strong: const TextStyle(color: Colors.black, fontWeight: FontWeight.bold),
listBullet: const TextStyle(color: Colors.black87),
),
)
: Text(
message.text,
style: const TextStyle(color: Colors.white, fontSize: 15, height: 1.4),
),
),
),
if (!isAssistant) _buildAvatar(Icons.person),
@@ -219,10 +230,9 @@ class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
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
onPressed: _isTyping ? null : _clearChat,
),
Expanded(
child: Container(
@@ -233,7 +243,7 @@ class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
),
child: TextField(
controller: _textController,
enabled: !_isTyping, // Bloqueia o campo de texto
enabled: !_isTyping,
onSubmitted: _handleSubmitted,
decoration: InputDecoration(
hintText: _isTyping ? "Aguarde a resposta..." : "Mensagem EPVChat...",
@@ -248,7 +258,7 @@ class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
decoration: BoxDecoration(
gradient: LinearGradient(
colors: _isTyping
? [Colors.grey, Colors.grey] // Cor de desativado
? [Colors.grey, Colors.grey]
: [const Color(0xFF8ad5c9), const Color(0xFF57a7ed)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
@@ -279,20 +289,15 @@ class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
Expanded(
child: ListView(
controller: _scrollController,
reverse: true, // Mantém a lógica de mensagens novas em baixo
reverse: true,
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(
@@ -305,7 +310,6 @@ class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
),
),
),
// O Logo
Image.asset(
'assets/logo.png',
height: 170,
@@ -322,4 +326,4 @@ class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
),
);
}
}
}