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'; import 'database_helper.dart'; class ChatScreen extends StatefulWidget { const ChatScreen({super.key}); @override State createState() => _ChatScreenState(); } class _ChatScreenState extends State with TickerProviderStateMixin { final List _messages = []; final List _sessions = []; ChatSession? _currentSession; final TextEditingController _textController = TextEditingController(); final ScrollController _scrollController = ScrollController(); final GlobalKey _scaffoldKey = GlobalKey(); bool _isTyping = false; late AnimationController _typingController; @override void initState() { super.initState(); _typingController = AnimationController( vsync: this, duration: const Duration(milliseconds: 1200), )..repeat(); _loadInitialData(); } Future _loadInitialData() async { await _loadSessions(); if (_sessions.isNotEmpty) { await _selectSession(_sessions.first); } else { await _createNewSession(); } } Future _loadSessions() async { final sessions = await DatabaseHelper.instance.getSessions(); if (mounted) { setState(() { _sessions.clear(); _sessions.addAll(sessions); }); } } Future _selectSession(ChatSession session) async { final messages = await DatabaseHelper.instance.getMessages(session.id!); if (mounted) { setState(() { _currentSession = session; _messages.clear(); _messages.addAll(messages.reversed); _isTyping = false; }); } if (_scaffoldKey.currentState?.isDrawerOpen ?? false) { Navigator.pop(context); } } Future _createNewSession() async { final title = "Chat ${DateTime.now().hour}:${DateTime.now().minute}"; final id = await DatabaseHelper.instance.createSession(title); await _loadSessions(); final newSession = _sessions.firstWhere((s) => s.id == id); await _selectSession(newSession); } Future _deleteSession(int id) async { await DatabaseHelper.instance.deleteSession(id); await _loadSessions(); if (_currentSession?.id == id) { if (_sessions.isNotEmpty) { await _selectSession(_sessions.first); } else { await _createNewSession(); } } else { setState(() {}); } } @override void dispose() { _typingController.dispose(); _textController.dispose(); _scrollController.dispose(); super.dispose(); } Future _handleSubmitted(String text) async { if (text.trim().isEmpty || _isTyping) return; // Se por algum motivo não houver sessão, cria uma antes de enviar if (_currentSession == null) { await _createNewSession(); } final userMsgText = text.trim(); _textController.clear(); final userMsg = ChatMessage( sessionId: _currentSession!.id!, text: userMsgText, isAssistant: false, timestamp: DateTime.now().toIso8601String(), ); await DatabaseHelper.instance.insertMessage(userMsg); setState(() { _messages.insert(0, userMsg); _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-coder:30b', 'messages': [{'role': 'user', 'content': userMsgText}], 'stream': false, }), ).timeout(const Duration(seconds: 60)); if (response.statusCode == 200) { final data = jsonDecode(response.body); final replyText = data['message']?['content'] ?? 'Sem resposta.'; final assistantMsg = ChatMessage( sessionId: _currentSession!.id!, text: replyText, isAssistant: true, timestamp: DateTime.now().toIso8601String(), ); await DatabaseHelper.instance.insertMessage(assistantMsg); if (mounted) { setState(() { _isTyping = false; _messages.insert(0, assistantMsg); }); } } else { throw Exception('Erro HTTP ${response.statusCode}'); } } catch (e) { if (mounted) { setState(() { _isTyping = false; _messages.insert(0, ChatMessage( sessionId: _currentSession!.id!, text: "Erro de ligação. Verifique se o servidor está online.", isAssistant: true, timestamp: DateTime.now().toIso8601String(), )); }); } } } 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), ), boxShadow: [ BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 5, offset: const Offset(0, 2)), ], ), 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), ), ) : Text(message.text, style: const TextStyle(color: Colors.white, fontSize: 15)), ), ), 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 Container( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), decoration: BoxDecoration( color: Colors.white, border: Border(top: BorderSide(color: Colors.black.withOpacity(0.05))), ), child: SafeArea( child: Row( children: [ Expanded( child: Container( decoration: BoxDecoration( color: const Color(0xFFF1F5F9), borderRadius: BorderRadius.circular(30.0), ), child: TextField( controller: _textController, enabled: !_isTyping, onSubmitted: _handleSubmitted, decoration: const InputDecoration( hintText: "Mensagem EPVChat...", border: InputBorder.none, contentPadding: EdgeInsets.symmetric(horizontal: 24.0, vertical: 14.0), ), ), ), ), const SizedBox(width: 12), Container( decoration: BoxDecoration( gradient: LinearGradient( colors: _isTyping ? [Colors.grey, Colors.grey] : [const Color(0xFF8ad5c9), const Color(0xFF57a7ed)], ), 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( key: _scaffoldKey, drawer: _buildSidebar(), backgroundColor: Colors.white, body: Stack( children: [ // Sombreado Mint Positioned( top: -screenWidth * 0.45, left: -screenWidth * 0.2, right: -screenWidth * 0.2, child: Container( height: screenWidth * 1.1, decoration: BoxDecoration( shape: BoxShape.circle, gradient: RadialGradient( center: Alignment.center, radius: 0.5, colors: [const Color(0xFF8ad5c9).withOpacity(0.6), const Color(0xFF8ad5c9).withOpacity(0.0)], stops: const [0.2, 1.0], ), ), ), ), Column( children: [ // Área do Topo (Menu e Logo) SafeArea( child: Container( height: 100, padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ IconButton( icon: const Icon(Icons.menu_rounded, color: Color(0xFF57a7ed), size: 32), onPressed: () => _scaffoldKey.currentState?.openDrawer(), ), Image.asset('assets/logo.png', height: 100, errorBuilder: (c,e,s) => const SizedBox(width: 100)), const SizedBox(width: 48), // Equilíbrio para o ícone do menu ], ), ), ), Expanded( child: ListView.builder( controller: _scrollController, padding: const EdgeInsets.symmetric(vertical: 20), reverse: true, itemCount: _messages.length + (_isTyping ? 1 : 0), itemBuilder: (context, index) { if (_isTyping && index == 0) return _buildTypingIndicator(); int msgIndex = _isTyping ? index - 1 : index; return _buildMessage(_messages[msgIndex]); }, ), ), _buildTextComposer(), ], ), ], ), ); } Widget _buildSidebar() { return Drawer( width: MediaQuery.of(context).size.width * 0.75, child: Column( children: [ DrawerHeader( decoration: const BoxDecoration( gradient: LinearGradient(colors: [Color(0xFF8ad5c9), Color(0xFF57a7ed)]), ), child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.auto_awesome, color: Colors.white, size: 40), const SizedBox(height: 10), const Text('Histórico de Chats', style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)), ], ), ), ), ListTile( leading: const Icon(Icons.add_comment_rounded, color: Color(0xFF57a7ed)), title: const Text('Nova Conversa', style: TextStyle(fontWeight: FontWeight.bold)), onTap: _createNewSession, ), const Divider(), Expanded( child: ListView.builder( padding: EdgeInsets.zero, itemCount: _sessions.length, itemBuilder: (context, index) { final session = _sessions[index]; final isSelected = _currentSession?.id == session.id; return Container( margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: isSelected ? const Color(0xFF8ad5c9).withOpacity(0.1) : Colors.transparent, borderRadius: BorderRadius.circular(12), ), child: ListTile( title: Text(session.title, maxLines: 1, overflow: TextOverflow.ellipsis), trailing: IconButton( icon: const Icon(Icons.delete_outline_rounded, color: Colors.redAccent), onPressed: () => _deleteSession(session.id!), ), onTap: () => _selectSession(session), ), ); }, ), ), ], ), ); } }