271 lines
11 KiB
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),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|