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:
shinra32
2026-05-23 03:13:46 -06:00
parent 0279ad05f4
commit 45ffba69b2
33 changed files with 2810 additions and 296 deletions

View File

@@ -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();
},
);

View File

@@ -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,
),
),
),
);
}
}