diff --git a/lib/chat_screen.dart b/lib/chat_screen.dart index 21c4011..cd771d1 100644 --- a/lib/chat_screen.dart +++ b/lib/chat_screen.dart @@ -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 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 with TickerProviderStateMixin { super.dispose(); } - // Função para limpar o chat void _clearChat() { setState(() { _messages.clear(); @@ -68,51 +73,49 @@ class _ChatScreenState extends State with TickerProviderStateMixin { } Future _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 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 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 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 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 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 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 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 with TickerProviderStateMixin { ), ), ), - // O Logo Image.asset( 'assets/logo.png', height: 170, @@ -322,4 +326,4 @@ class _ChatScreenState extends State with TickerProviderStateMixin { ), ); } -} \ No newline at end of file +} diff --git a/pubspec.lock b/pubspec.lock index 61e67ac..c6994c1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" async: dependency: transitive description: @@ -70,6 +78,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + flutter_markdown: + dependency: "direct main" + description: + name: flutter_markdown + sha256: "08fb8315236099ff8e90cb87bb2b935e0a724a3af1623000a9cec930468e0f27" + url: "https://pub.dev" + source: hosted + version: "0.7.7+1" flutter_test: dependency: "direct dev" description: flutter @@ -123,6 +139,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" + markdown: + dependency: transitive + description: + name: markdown + sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1" + url: "https://pub.dev" + source: hosted + version: "7.3.0" matcher: dependency: transitive description: @@ -242,4 +266,4 @@ packages: version: "1.1.1" sdks: dart: ">=3.11.1 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index 791ffae..2f413e9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,6 +12,7 @@ dependencies: sdk: flutter cupertino_icons: ^1.0.8 http: ^1.6.0 + flutter_markdown: ^0.7.3 dev_dependencies: flutter_test: