import 'dart:ui'; 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}); @override State createState() => _ChatScreenState(); } class ChatMessage { final String text; final bool isAssistant; ChatMessage({required this.text, this.isAssistant = false}); } class _ChatScreenState extends State with TickerProviderStateMixin { final List _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 _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 DISPONÍVEIS ---"); if (data['models'] != null) { for (var m in data['models']) { print("- ${m['name']}"); } } } } catch (e) { print("Erro ao listar modelos: $e"); } } @override void dispose() { _typingController.dispose(); _textController.dispose(); _scrollController.dispose(); super.dispose(); } void _clearChat() { setState(() { _messages.clear(); _isTyping = false; }); } Future _handleSubmitted(String text) async { if (text.trim().isEmpty || _isTyping) return; _textController.clear(); setState(() { _messages.insert(0, ChatMessage(text: text)); _isTyping = true; }); try { 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:4b', 'messages': [{'role': 'user', 'content': text}], 'stream': false, }), ) .timeout(const Duration(seconds: 60)); if (response.statusCode == 200) { final data = jsonDecode(response.body); final reply = data['message']?['content'] ?? 'Sem resposta.'; if (mounted) { setState(() { _isTyping = false; _messages.insert(0, ChatMessage(text: reply, isAssistant: true)); }); } } else { throw Exception('Erro HTTP ${response.statusCode}: ${response.body}'); } } catch (e) { if (mounted) { setState(() { _isTyping = false; _messages.insert( 0, ChatMessage(text: "Erro: $e", isAssistant: true), ); }); } } } 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: 16.0, vertical: 10.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: 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), ], ), ); } 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: [ IconButton( icon: const Icon(Icons.delete_sweep_rounded, color: Colors.redAccent), onPressed: _isTyping ? null : _clearChat, ), 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, 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] : [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, children: [ ...(_isTyping ? [_buildTypingIndicator()] : []), ..._messages.map((msg) => _buildMessage(msg)), Padding( padding: const EdgeInsets.only(bottom: 50, top: 20), child: Stack( alignment: Alignment.center, children: [ 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), ], ), ), ), Image.asset( 'assets/logo.png', height: 170, fit: BoxFit.contain, ), ], ), ), ], ), ), _buildTextComposer(), ], ), ); } }