Files
App_Projeto/lib/chat_screen.dart
2026-03-13 17:47:25 +00:00

452 lines
15 KiB
Dart

import 'dart:ui';
import 'package:flutter/material.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
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;
Future<void> _handleSubmitted(String text) async {
if (text.trim().isEmpty) return;
_textController.clear();
setState(() {
_messages.insert(0, ChatMessage(text: text));
_isTyping = true;
});
try {
// Faz o pedido para o IP usando o formato compatível com OpenAI
final response = await http
.post(
Uri.parse('http://192.168.60.134:11434/v1/chat/completions'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'model': 'qwen3:4b', // Um modelo disponível no servidor
'messages': [
{'role': 'user', 'content': text},
],
'stream': false,
}),
)
.timeout(const Duration(seconds: 30));
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final reply =
data['choices'][0]['message']['content'] ??
'Sem resposta do modelo.';
if (mounted) {
setState(() {
_isTyping = false;
_messages.insert(0, ChatMessage(text: reply, isAssistant: true));
});
}
} else {
throw Exception(
'Erro no servidor: HTTP ${response.statusCode} - ${response.body}',
);
}
} catch (e) {
if (mounted) {
setState(() {
_isTyping = false;
_messages.insert(
0,
ChatMessage(
text:
"Não foi possível comunicar com o modelo.\nVerifique se o IP está acessível.\nDetalhes: $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)
Container(
margin: const EdgeInsets.only(right: 12),
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: const LinearGradient(
colors: [Color(0xFF8ad5c9), Color(0xFF57a7ed)], // Mint & Blue
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
boxShadow: [
BoxShadow(
color: const Color(0xFF8ad5c9).withOpacity(0.4),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: const CircleAvatar(
backgroundColor: Colors.transparent,
radius: 16,
child: Icon(Icons.auto_awesome, color: Colors.white, size: 18),
),
),
Flexible(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 20.0,
vertical: 14.0,
),
decoration: BoxDecoration(
gradient: isAssistant
? const LinearGradient(
colors: [
Color(0xFFF1F5F9),
Color(0xFFE2E8F0),
], // Light bubbles
begin: Alignment.topLeft,
end: Alignment.bottomRight,
)
: const LinearGradient(
colors: [
Color(0xFF8ad5c9),
Color(0xFF57a7ed),
], // Mint & Blue bubbles
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
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.2),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
border: Border.all(
color: isAssistant
? Colors.black.withOpacity(0.05)
: Colors.transparent,
width: 1,
),
),
child: Text(
message.text,
style: TextStyle(
color: isAssistant ? Colors.black87 : Colors.white,
fontSize: 15,
height: 1.4,
),
),
),
),
if (!isAssistant)
Container(
margin: const EdgeInsets.only(left: 12),
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: const LinearGradient(
colors: [
Color(0xFF57a7ed),
Color(0xFF3A8BD1),
], // User avatar gradient
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
boxShadow: [
BoxShadow(
color: const Color(0xFF57a7ed).withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: const CircleAvatar(
backgroundColor: Colors.transparent,
radius: 16,
child: Icon(Icons.person, 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: [
Container(
margin: const EdgeInsets.only(right: 12),
decoration: const BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [Color(0xFF8ad5c9), Color(0xFF57a7ed)], // Mint & Blue
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: const CircleAvatar(
backgroundColor: Colors.transparent,
radius: 16,
child: Icon(Icons.auto_awesome, color: Colors.white, size: 18),
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 12.0,
),
decoration: BoxDecoration(
color: const Color(
0xFFF1F5F9,
), // Typing bubble matching assistant light bubble
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
bottomLeft: Radius.circular(4),
bottomRight: Radius.circular(20),
),
border: Border.all(color: Colors.black.withOpacity(0.05)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildDot(0),
const SizedBox(width: 4),
_buildDot(1),
const SizedBox(width: 4),
_buildDot(2),
],
),
),
],
),
);
}
Widget _buildDot(int index) {
return TweenAnimationBuilder(
tween: Tween<double>(begin: 0, end: 1),
duration: const Duration(milliseconds: 600),
builder: (context, double value, child) {
return Opacity(
opacity: (value + (index * 0.3)) % 1.0,
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), // Light backdrop
border: Border(
top: BorderSide(
color: Colors.black.withOpacity(0.05),
width: 0.5,
),
),
),
child: SafeArea(
child: Row(
children: [
Expanded(
child: Container(
decoration: BoxDecoration(
color: const Color(
0xFFF1F5F9,
), // Light input box background
borderRadius: BorderRadius.circular(30.0),
border: Border.all(color: Colors.black.withOpacity(0.05)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: TextField(
controller: _textController,
onSubmitted: _handleSubmitted,
style: const TextStyle(color: Colors.black87),
decoration: InputDecoration(
hintText: "Message EPVChat!...",
hintStyle: TextStyle(
color: Colors.black.withOpacity(0.4),
),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(
horizontal: 24.0,
vertical: 14.0,
),
),
),
),
),
const SizedBox(width: 12),
Container(
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [
Color(0xFF8ad5c9),
Color(0xFF57a7ed),
], // Mint & Blue send button
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: const Color(0xFF8ad5c9).withOpacity(0.4),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: IconButton(
icon: const Icon(
Icons.send_rounded,
color: Colors.white,
size: 20,
),
onPressed: () => _handleSubmitted(_textController.text),
),
),
],
),
),
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight),
child: ClipRRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: AppBar(
title: ShaderMask(
shaderCallback: (bounds) => const LinearGradient(
colors: [
Color(0xFF8ad5c9),
Color(0xFF57a7ed),
], // Text gradient header
begin: Alignment.topLeft,
end: Alignment.bottomRight,
).createShader(bounds),
child: const Text(
'EPVChat!',
style: TextStyle(
fontWeight: FontWeight.w800,
fontSize: 22,
letterSpacing: -0.5,
),
),
),
backgroundColor: Colors.white.withOpacity(
0.7,
), // Light Header backgrop
elevation: 0,
centerTitle: true,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1.0),
child: Container(
color: Colors.black.withOpacity(0.05),
height: 1.0,
),
),
),
),
),
),
body: Container(
decoration: const BoxDecoration(
gradient: RadialGradient(
center: Alignment.topCenter,
radius: 1.5,
colors: [
Colors.white,
Color(0xFFF8FAFC), // Slate 50
],
),
),
child: SafeArea(
bottom: false,
child: Column(
children: [
Expanded(
child: ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.symmetric(vertical: 20.0),
reverse: true,
itemCount: _messages.length + (_isTyping ? 1 : 0),
itemBuilder: (_, int index) {
if (_isTyping && index == 0) {
return _buildTypingIndicator();
}
int messageIndex = _isTyping ? index - 1 : index;
return _buildMessage(_messages[messageIndex]);
},
),
),
_buildTextComposer(),
],
),
),
),
);
}
}