Co-authored-by: eddgranados12 <eddgranados12@users.noreply.github.com>
Co-authored-by: MENDOZA BALLARDO GAEL RICARDO <gael-meb123@users.noreply.github.com> Co-authored-by: Azareth-Tr <Azareth-Tr@users.noreply.github.com> modificacion de las vistas principales para el usuario ciudadano, primer avance para el panel admin
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
|
||||
// Modelo de mensaje simple
|
||||
class ChatMessage {
|
||||
final String role; // 'user', 'assistant', 'system'
|
||||
final String content;
|
||||
|
||||
ChatMessage({required this.role, required this.content});
|
||||
|
||||
Map<String, dynamic> toJson() => {'role': role, 'content': content};
|
||||
}
|
||||
|
||||
class AiChatNotifier extends StateNotifier<List<ChatMessage>> {
|
||||
AiChatNotifier()
|
||||
: super([
|
||||
ChatMessage(
|
||||
role: 'assistant',
|
||||
content:
|
||||
'¡Hola! Soy Eco 🍃, la mascota de Recolecta. '
|
||||
'Estoy aquí para ayudarte a reciclar y separar tu basura correctamente. ¿Tienes alguna duda?',
|
||||
),
|
||||
]);
|
||||
|
||||
bool isLoading = false;
|
||||
|
||||
Future<void> sendMessage(String userText) async {
|
||||
if (userText.trim().isEmpty) return;
|
||||
|
||||
// Añadir mensaje del usuario
|
||||
final userMsg = ChatMessage(role: 'user', content: userText);
|
||||
state = [...state, userMsg];
|
||||
isLoading = true;
|
||||
|
||||
try {
|
||||
final dio = Dio();
|
||||
// Importante: En producción, la llamada a OpenAI debería hacerse idealmente
|
||||
// desde tu backend FastAPI para no exponer la API_KEY en la app Flutter.
|
||||
// Para el MVP/Hackathon, la leemos del entorno (.env o --dart-define)
|
||||
final apiKey = dotenv.env['OPENAI_API_KEY'] ?? '';
|
||||
|
||||
// Contexto del sistema para que la IA actúe como la mascota
|
||||
final systemPrompt = ChatMessage(
|
||||
role: 'system',
|
||||
content:
|
||||
'Eres Eco, la mascota virtual de la app Recolecta en Celaya. '
|
||||
'Tu misión es educar a los ciudadanos sobre cómo separar la basura en 4 categorías: '
|
||||
'Orgánicos (verde), Reciclables (azul), Sanitarios (naranja) y Especiales (morado). '
|
||||
'Responde siempre de forma muy amigable, entusiasta, usando emojis. '
|
||||
'Sé muy conciso y breve (máximo 3 oraciones cortas). '
|
||||
'Nunca reveles ubicaciones de camiones ni te salgas del tema del reciclaje y medio ambiente.',
|
||||
);
|
||||
|
||||
final messagesForApi = [systemPrompt, ...state];
|
||||
|
||||
final response = await dio.post(
|
||||
'https://api.openai.com/v1/chat/completions',
|
||||
options: Options(
|
||||
headers: {
|
||||
'Authorization': 'Bearer $apiKey',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
),
|
||||
data: {
|
||||
'model': 'gpt-3.5-turbo', // Rápido y económico para el hackathon
|
||||
'messages': messagesForApi.map((m) => m.toJson()).toList(),
|
||||
'temperature': 0.7,
|
||||
'max_tokens': 150, // Limitar para que sea conciso
|
||||
},
|
||||
);
|
||||
|
||||
final botReply = response.data['choices'][0]['message']['content'];
|
||||
state = [...state, ChatMessage(role: 'assistant', content: botReply)];
|
||||
} catch (e) {
|
||||
debugPrint('Error en OpenAI: $e');
|
||||
state = [
|
||||
...state,
|
||||
ChatMessage(
|
||||
role: 'assistant',
|
||||
content:
|
||||
'Uy, tuve un problemita técnico con mi cerebro de hojitas 🧠🍂. ¿Me repites tu pregunta?',
|
||||
),
|
||||
];
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final aiChatProvider = StateNotifierProvider<AiChatNotifier, List<ChatMessage>>(
|
||||
(ref) {
|
||||
return AiChatNotifier();
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,215 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
// Importa Lottie si tus animaciones están en formato Lottie (.json)
|
||||
// import 'package:lottie/lottie.dart';
|
||||
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import 'ai_chat_provider.dart';
|
||||
|
||||
class AiPetChatScreen extends ConsumerStatefulWidget {
|
||||
const AiPetChatScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<AiPetChatScreen> createState() => _AiPetChatScreenState();
|
||||
}
|
||||
|
||||
class _AiPetChatScreenState extends ConsumerState<AiPetChatScreen> {
|
||||
final _textController = TextEditingController();
|
||||
final _scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_textController.dispose();
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _sendMessage() async {
|
||||
final text = _textController.text;
|
||||
if (text.trim().isEmpty) return;
|
||||
|
||||
_textController.clear();
|
||||
|
||||
// Ocultar teclado
|
||||
FocusScope.of(context).unfocus();
|
||||
|
||||
// Enviar al provider
|
||||
await ref.read(aiChatProvider.notifier).sendMessage(text);
|
||||
|
||||
// Hacer scroll hacia abajo
|
||||
_scrollToBottom();
|
||||
}
|
||||
|
||||
void _scrollToBottom() {
|
||||
if (_scrollController.hasClients) {
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
_scrollController.animateTo(
|
||||
_scrollController.position.maxScrollExtent,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final messages = ref.watch(aiChatProvider);
|
||||
// No podemos leer isLoading directamente de ref.watch(provider) porque es StateNotifierProvider.
|
||||
// Para leer la variable, leemos el notifier.
|
||||
final isLoading = ref.watch(aiChatProvider.notifier).isLoading;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppTheme.background,
|
||||
appBar: AppBar(
|
||||
title: const Text('Pregúntale a Eco 🍃'),
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// 1. ÁREA DE LA MASCOTA (Animación)
|
||||
Container(
|
||||
height: 150,
|
||||
width: double.infinity,
|
||||
decoration: const BoxDecoration(
|
||||
color: AppTheme.primaryLight,
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(30),
|
||||
bottomRight: Radius.circular(30),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
// Reemplaza este Icono con tu animación de Lottie:
|
||||
// child: Lottie.asset('assets/animations/mascota_feliz.json', height: 120),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.pets, size: 64, color: AppTheme.primary),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
isLoading ? 'Eco está pensando...' : 'Eco te escucha',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.primaryDark,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 2. HISTORIAL DE CHAT
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: messages.length,
|
||||
itemBuilder: (context, index) {
|
||||
final msg = messages[index];
|
||||
if (msg.role == 'system') return const SizedBox.shrink();
|
||||
|
||||
final isBot = msg.role == 'assistant';
|
||||
return _ChatBubble(text: msg.content, isBot: isBot);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// Indicador de escritura
|
||||
if (isLoading)
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
|
||||
// 3. CAMPO DE TEXTO
|
||||
SafeArea(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
color: Colors.white,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _textController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Ej. ¿Dónde tiro las cajas de pizza?',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade200,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 10,
|
||||
),
|
||||
),
|
||||
onSubmitted: (_) => _sendMessage(),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
CircleAvatar(
|
||||
backgroundColor: AppTheme.primary,
|
||||
child: IconButton(
|
||||
icon: const Icon(
|
||||
Icons.send,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: isLoading ? null : _sendMessage,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ChatBubble extends StatelessWidget {
|
||||
final String text;
|
||||
final bool isBot;
|
||||
|
||||
const _ChatBubble({required this.text, required this.isBot});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Align(
|
||||
alignment: isBot ? Alignment.centerLeft : Alignment.centerRight,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width * 0.75,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isBot ? Colors.white : AppTheme.primary,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: const Radius.circular(20),
|
||||
topRight: const Radius.circular(20),
|
||||
bottomLeft: isBot ? Radius.zero : const Radius.circular(20),
|
||||
bottomRight: isBot ? const Radius.circular(20) : Radius.zero,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: isBot ? AppTheme.textPrimary : Colors.white,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user