Files
AppRecoleccion/celaya_limpia/lib/screens/citizen/chatbot_screen.dart

271 lines
11 KiB
Dart

import 'package:flutter/material.dart';
import '../../core/app_colors.dart';
// ── Árbol de respuestas predefinidas ──────────────────────────────────────
class _ChatNode {
final String text;
final List<_ChatOption> options;
final bool isAnswer;
const _ChatNode(this.text, this.options, {this.isAnswer = false});
}
class _ChatOption {
final String label;
final _ChatNode next;
const _ChatOption(this.label, this.next);
}
final _chatTree = _ChatNode('Hola, soy el asistente de Celaya Limpia. ¿En que te puedo ayudar?', [
_ChatOption('Separacion de residuos', _ChatNode('¿Que quieres saber sobre separacion?', [
_ChatOption('Como separo mi basura', _ChatNode(
'Separa tus residuos en 3 grupos:\n\n'
'ORGANICOS (bolsa verde):\nRestos de comida, cascara de huevo, pasto, hojas.\n\n'
'INORGANICOS reciclables (bolsa azul):\nPET, latas, carton limpio, vidrio.\n\n'
'NO reciclables (bolsa negra):\nPanales, papel sanitario, colillas, chicles.',
[], isAnswer: true)),
_ChatOption('Que NO debo mezclar', _ChatNode(
'NUNCA mezcles:\n\n'
'- Pilas o baterias con basura comun\n'
'- Aceite de cocina (contamina el agua)\n'
'- Medicamentos vencidos\n'
'- Jeringas o material medico\n'
'- Electronicos con basura doméstica\n\n'
'Estos requieren manejo especial.',
[], isAnswer: true)),
_ChatOption('Que hago con el aceite', _ChatNode(
'El aceite de cocina usado NO va a la basura ni al drenaje.\n\n'
'1. Dejalo enfriar completamente\n'
'2. Guardalo en botella de PET cerrada\n'
'3. Llevalo a los puntos de acopio del Ayuntamiento de Celaya\n\n'
'El aceite reciclado se convierte en biodiesel.',
[], isAnswer: true)),
])),
_ChatOption('Residuos especiales', _ChatNode('¿Que tipo de residuo especial tienes?', [
_ChatOption('Donde dejo electronicos', _ChatNode(
'Los aparatos electronicos (celulares, computadoras, focos ahorradores) '
'son residuos RAEE.\n\n'
'Puntos de acopio en Celaya:\n'
'- Tiendas de electronica\n'
'- Centros comerciales con contenedores especiales\n'
'- Eventos de recoleccion del municipio\n\n'
'NUNCA los tires a la basura comun.',
[], isAnswer: true)),
_ChatOption('Que hago con medicamentos', _ChatNode(
'Los medicamentos vencidos son residuos peligrosos.\n\n'
'- Llevalos a farmacias que tengan programa de devolucion\n'
'- Algunos hospitales los reciben\n'
'- Nunca los tires al drenaje ni a la basura comun\n\n'
'Contaminar el agua con medicamentos afecta a toda la comunidad.',
[], isAnswer: true)),
_ChatOption('Que hago con pilas y baterias', _ChatNode(
'Las pilas y baterias contienen metales pesados toxicos.\n\n'
'Depositalas en:\n'
'- Supermercados (contenedores naranjas)\n'
'- Tiendas de electronica\n'
'- Oficinas del Ayuntamiento de Celaya\n\n'
'1 pila puede contaminar 600,000 litros de agua.',
[], isAnswer: true)),
])),
_ChatOption('Sobre el servicio de recoleccion', _ChatNode('¿Que necesitas saber?', [
_ChatOption('Cuando debo sacar la basura', _ChatNode(
'Celaya Limpia te notificara:\n\n'
'1. Cuando el camion salga del relleno sanitario\n'
'2. Cuando este a 30 minutos\n'
'3. A 15 minutos: este es el momento de sacar tus bolsas\n\n'
'NO saques la basura antes del aviso de 15 minutos. '
'Atrae fauna nociva y obstruye la acera.',
[], isAnswer: true)),
_ChatOption('El camion no paso', _ChatNode(
'Si el camion no paso en tu horario habitual:\n\n'
'1. Revisa las alertas en la app (puede haber un retraso o incidente)\n'
'2. Guarda tu basura bien cerrada\n'
'3. Reporta la incidencia desde la seccion "Reportar"\n\n'
'El administrador revisara tu reporte y te mantendra informado.',
[], isAnswer: true)),
_ChatOption('Como califico el servicio', _ChatNode(
'Despues de que el camion pase por tu zona, '
'la app te mostrara una notificacion para calificar.\n\n'
'Puedes dar de 1 a 5 estrellas y dejar un comentario.\n\n'
'Tus calificaciones ayudan al Ayuntamiento a identificar '
'colonias con problemas y mejorar el servicio.',
[], isAnswer: true)),
])),
_ChatOption('Denuncia o emergencia', _ChatNode(
'Para situaciones urgentes:\n\n'
'- Reporte de incidencias: usa la seccion "Reportar" en la app\n'
'- Emergencias: llama al 911\n'
'- Ayuntamiento de Celaya: (461) 614-8000\n'
'- SEMARNAT Guanajuato: (477) 717-2600\n\n'
'Para basura clandestina o tiraderos ilegales, reportalo al municipio.',
[], isAnswer: true)),
]);
// ── Pantalla del chatbot ──────────────────────────────────────────────────
class ChatbotScreen extends StatefulWidget {
const ChatbotScreen({super.key});
@override State<ChatbotScreen> createState() => _ChatbotScreenState();
}
class _ChatbotScreenState extends State<ChatbotScreen> {
final List<_Message> _messages = [];
_ChatNode _current = _chatTree;
final _scroll = ScrollController();
@override
void initState() {
super.initState();
// Mensaje inicial
_messages.add(_Message(text: _chatTree.text, isBot: true));
}
void _handleOption(_ChatOption option) {
setState(() {
// Mensaje del usuario
_messages.add(_Message(text: option.label, isBot: false));
// Ir al siguiente nodo
_current = option.next;
_messages.add(_Message(text: _current.text, isBot: true,
isAnswer: _current.isAnswer));
});
Future.delayed(const Duration(milliseconds: 100), () {
_scroll.animateTo(_scroll.position.maxScrollExtent,
duration: const Duration(milliseconds: 300), curve: Curves.easeOut);
});
}
void _reset() {
setState(() {
_messages.clear();
_current = _chatTree;
_messages.add(_Message(text: _chatTree.text, isBot: true));
});
}
@override
Widget build(BuildContext context) => Scaffold(
backgroundColor: AppColors.grisFondo,
appBar: AppBar(
backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
title: const Row(children: [
CircleAvatar(radius: 14,
backgroundColor: Colors.white24,
child: Icon(Icons.smart_toy, color: AppColors.dorado, size: 18)),
SizedBox(width: 8),
Text('Asistente Celaya Limpia'),
]),
bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
child: Container(height: 4, color: AppColors.dorado)),
actions: [
IconButton(icon: const Icon(Icons.refresh), tooltip: 'Reiniciar',
onPressed: _reset),
],
),
body: Column(children: [
// Mensajes
Expanded(
child: ListView.builder(
controller: _scroll,
padding: const EdgeInsets.all(12),
itemCount: _messages.length,
itemBuilder: (_, i) => _MessageBubble(msg: _messages[i]),
),
),
// Opciones del nodo actual
if (_current.options.isNotEmpty)
Container(
color: Colors.white,
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Selecciona una opcion:',
style: TextStyle(fontSize: 11, color: AppColors.grisTexto,
fontWeight: FontWeight.w500)),
const SizedBox(height: 8),
Wrap(spacing: 8, runSpacing: 8,
children: _current.options.map((opt) =>
ActionChip(
label: Text(opt.label, style: const TextStyle(fontSize: 12)),
backgroundColor: AppColors.guindaPrimary.withOpacity(0.1),
side: const BorderSide(color: AppColors.guindaPrimary),
labelStyle: const TextStyle(color: AppColors.guindaPrimary),
onPressed: () => _handleOption(opt),
)).toList()),
],
),
)
else
// Botón de reiniciar al llegar a una respuesta final
Container(
color: Colors.white,
padding: const EdgeInsets.all(12),
child: SizedBox(width: double.infinity,
child: OutlinedButton.icon(
onPressed: _reset,
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.guindaPrimary,
side: const BorderSide(color: AppColors.guindaPrimary)),
icon: const Icon(Icons.arrow_back, size: 16),
label: const Text('Hacer otra pregunta'))),
),
]),
);
@override void dispose() { _scroll.dispose(); super.dispose(); }
}
class _Message {
final String text;
final bool isBot;
final bool isAnswer;
const _Message({required this.text, required this.isBot, this.isAnswer = false});
}
class _MessageBubble extends StatelessWidget {
final _Message msg;
const _MessageBubble({super.key, required this.msg});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
mainAxisAlignment: msg.isBot ? MainAxisAlignment.start : MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (msg.isBot) ...[
CircleAvatar(radius: 16,
backgroundColor: AppColors.guindaPrimary,
child: const Icon(Icons.smart_toy, color: Colors.white, size: 16)),
const SizedBox(width: 8),
],
Flexible(child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: msg.isBot
? (msg.isAnswer ? Colors.green.shade50 : Colors.white)
: AppColors.guindaPrimary,
borderRadius: BorderRadius.only(
topLeft: const Radius.circular(16),
topRight: const Radius.circular(16),
bottomLeft: Radius.circular(msg.isBot ? 4 : 16),
bottomRight: Radius.circular(msg.isBot ? 16 : 4),
),
border: msg.isBot ? Border.all(
color: msg.isAnswer
? Colors.green.shade200
: Colors.grey.shade200) : null,
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.06),
blurRadius: 4, offset: const Offset(0, 2))],
),
child: Text(msg.text,
style: TextStyle(fontSize: 13, height: 1.5,
color: msg.isBot ? AppColors.negroTexto : Colors.white)),
)),
if (!msg.isBot) const SizedBox(width: 8),
],
),
);
}
}