chatbot nuevo

This commit is contained in:
Lizandro Guarnizo
2026-04-30 21:32:32 -05:00
parent 88a42ec629
commit f61e8005fc
24 changed files with 1724 additions and 0 deletions
+26
View File
@@ -0,0 +1,26 @@
<?php
namespace App\Http\Controllers;
class ChatController extends Controller
{
public function publicPage()
{
return view('chat-publico');
}
public function conversaciones()
{
return view('chat.conversaciones');
}
public function menus()
{
return view('chat.menus');
}
public function configuracion()
{
return view('chat.configuracion');
}
}
@@ -0,0 +1,35 @@
<?php
namespace App\Http\Controllers;
use App\Services\ChatBotEngine;
use Illuminate\Http\Request;
class TelegramWebhookController extends Controller
{
public function handle(Request $request)
{
$update = $request->all();
// Solo procesar mensajes de texto
if (! isset($update['message']['text'])) {
return response()->json(['ok' => true]);
}
$chatId = (string) $update['message']['chat']['id'];
$text = $update['message']['text'];
$nombre = trim(
($update['message']['from']['first_name'] ?? '') . ' ' .
($update['message']['from']['last_name'] ?? '')
);
$engine = new ChatBotEngine();
$replies = $engine->handle('telegram', $chatId, $text, $nombre);
foreach ($replies as $reply) {
$engine->enviarTelegram($chatId, $reply);
}
return response()->json(['ok' => true]);
}
}
+100
View File
@@ -0,0 +1,100 @@
<?php
namespace App\Http\Livewire\Chat;
use App\Models\ChatContact;
use App\Models\ChatConversation;
use App\Models\ChatMessage;
use App\Services\ChatBotEngine;
use Livewire\Component;
class PublicChat extends Component
{
public string $paso = 'telefono'; // 'telefono' | 'chat'
public string $telefono = '';
public string $nombre = '';
public string $input = '';
public array $mensajes = [];
public ?int $convId = null;
public string $estadoConv = 'bot';
public function iniciar()
{
$this->validate([
'telefono' => 'required|min:7|max:20',
'nombre' => 'nullable|max:80',
]);
$contact = ChatContact::firstOrCreate(
['canal' => 'web', 'canal_id' => $this->telefono],
['nombre' => $this->nombre ?: $this->telefono, 'telefono' => $this->telefono]
);
if ($this->nombre && $contact->nombre !== $this->nombre) {
$contact->update(['nombre' => $this->nombre]);
}
$conv = $contact->activeConversation();
if (! $conv) {
// Primera vez — el engine inicia el flujo con "hola"
$engine = new ChatBotEngine();
$engine->handle('web', $this->telefono, 'hola', $contact->nombre);
$contact->refresh();
$conv = $contact->activeConversation();
}
if ($conv) {
$this->convId = $conv->id;
$this->estadoConv = $conv->estado;
$this->cargarMensajes();
}
$this->paso = 'chat';
}
public function enviar()
{
if (! $this->convId || trim($this->input) === '') {
return;
}
$texto = trim($this->input);
$this->input = '';
$engine = new ChatBotEngine();
$engine->handle('web', $this->telefono, $texto, $this->nombre ?: $this->telefono);
$conv = ChatConversation::find($this->convId);
if ($conv) {
$this->estadoConv = $conv->fresh()->estado;
}
$this->cargarMensajes();
// scroll ya se dispara desde cargarMensajes()
}
public function cargarMensajes()
{
if (! $this->convId) {
return;
}
$this->mensajes = ChatMessage::where('conversation_id', $this->convId)
->orderBy('id')
->get()
->map(fn($m) => [
'tipo' => $m->tipo,
'contenido' => $m->contenido,
'created_at' => $m->created_at->format('H:i'),
])
->toArray();
$this->dispatchBrowserEvent('scroll-chat');
}
public function render()
{
return view('livewire.chat.public-chat');
}
}
@@ -0,0 +1,80 @@
<?php
namespace App\Http\Livewire\Chat;
use App\Models\ChatConfig;
use Jantinnerezo\LivewireAlert\LivewireAlert;
use Livewire\Component;
class ShowConfiguracionChat extends Component
{
use LivewireAlert;
public string $telegram_token = '';
public string $mensaje_bienvenida = '';
public string $mensaje_transferencia = '';
public string $webhookResult = '';
public function mount(): void
{
$this->telegram_token = ChatConfig::get('telegram_token', '');
$this->mensaje_bienvenida = ChatConfig::get('mensaje_bienvenida', 'Hola 👋 Bienvenido. Escribe tu consulta.');
$this->mensaje_transferencia = ChatConfig::get('mensaje_transferencia', 'Un agente se comunicará contigo en breve. Por favor espera.');
}
public function guardar(): void
{
$this->validate([
'mensaje_bienvenida' => 'required|string|max:500',
'mensaje_transferencia' => 'required|string|max:500',
]);
ChatConfig::set('telegram_token', $this->telegram_token);
ChatConfig::set('mensaje_bienvenida', $this->mensaje_bienvenida);
ChatConfig::set('mensaje_transferencia', $this->mensaje_transferencia);
$this->alert('success', 'Configuración guardada correctamente.');
}
public function registrarWebhook(): void
{
$token = trim($this->telegram_token);
if (! $token) {
$this->webhookResult = '⚠️ El token de Telegram está vacío.';
return;
}
// Guardar el token antes de registrar
ChatConfig::set('telegram_token', $token);
$webhookUrl = url('/chat/webhook/telegram');
$apiUrl = "https://api.telegram.org/bot{$token}/setWebhook";
$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => "Content-Type: application/x-www-form-urlencoded\r\n",
'content' => http_build_query(['url' => $webhookUrl]),
'timeout' => 10,
],
]);
$result = @file_get_contents($apiUrl, false, $context);
if ($result === false) {
$this->webhookResult = '❌ Error de conexión con la API de Telegram.';
return;
}
$data = json_decode($result, true);
$this->webhookResult = ($data['ok'] ?? false)
? '✅ Webhook registrado correctamente: ' . ($data['description'] ?? 'OK')
: '❌ Error: ' . ($data['description'] ?? 'respuesta desconocida');
}
public function render()
{
return view('livewire.chat.show-configuracion-chat');
}
}
@@ -0,0 +1,139 @@
<?php
namespace App\Http\Livewire\Chat;
use App\Models\ChatConversation;
use App\Models\ChatMessage;
use App\Services\ChatBotEngine;
use Livewire\Component;
class ShowConversaciones extends Component
{
public string $busqueda = '';
public string $filtroEstado = '';
public ?int $convSelecId = null;
public string $replyText = '';
public array $mensajes = [];
public function selectConv(int $convId): void
{
$this->convSelecId = $convId;
$this->replyText = '';
$this->cargarMensajes();
// Marcar mensajes entrantes como leídos
ChatMessage::where('conversation_id', $convId)
->where('tipo', 'usuario')
->where('leido', false)
->update(['leido' => true]);
ChatConversation::where('id', $convId)
->update(['mensajes_no_leidos' => 0]);
}
public function cargarMensajes(): void
{
if (! $this->convSelecId) {
$this->mensajes = [];
return;
}
$this->mensajes = ChatMessage::where('conversation_id', $this->convSelecId)
->orderBy('id')
->get()
->map(fn($m) => [
'id' => $m->id,
'tipo' => $m->tipo,
'contenido' => $m->contenido,
'created_at' => $m->created_at->format('d/m H:i'),
])
->toArray();
$this->dispatchBrowserEvent('scroll-admin');
}
public function sendReply(): void
{
$this->validate(['replyText' => 'required|string|max:4096']);
if (! $this->convSelecId) {
return;
}
$conv = ChatConversation::with('contact')->find($this->convSelecId);
if (! $conv) {
return;
}
ChatMessage::create([
'conversation_id' => $this->convSelecId,
'tipo' => 'agente',
'contenido' => $this->replyText,
'leido' => true,
]);
$conv->update(['ultimo_mensaje_at' => now()]);
// Enviar via Telegram si el canal es telegram
if ($conv->canal === 'telegram') {
$engine = new ChatBotEngine();
$engine->enviarTelegram($conv->contact->canal_id, $this->replyText);
}
$this->replyText = '';
$this->cargarMensajes();
}
public function cerrarConv(): void
{
if (! $this->convSelecId) {
return;
}
ChatConversation::where('id', $this->convSelecId)
->update(['estado' => 'cerrada']);
$this->cargarMensajes();
}
public function devolverAlBot(): void
{
if (! $this->convSelecId) {
return;
}
ChatConversation::where('id', $this->convSelecId)
->update(['estado' => 'bot', 'menu_actual_id' => null]);
}
public function tomarControl(): void
{
if (! $this->convSelecId) {
return;
}
ChatConversation::where('id', $this->convSelecId)
->update(['estado' => 'agente']);
}
public function render()
{
$conversaciones = ChatConversation::with('contact')
->when($this->filtroEstado, fn($q) => $q->where('estado', $this->filtroEstado))
->when($this->busqueda, function ($q) {
$q->whereHas('contact', function ($s) {
$s->where('nombre', 'like', "%{$this->busqueda}%")
->orWhere('telefono', 'like', "%{$this->busqueda}%")
->orWhere('canal_id', 'like', "%{$this->busqueda}%");
});
})
->orderByDesc('ultimo_mensaje_at')
->get();
$convSelec = $this->convSelecId
? ChatConversation::with('contact')->find($this->convSelecId)
: null;
return view('livewire.chat.show-conversaciones', compact('conversaciones', 'convSelec'));
}
}
+168
View File
@@ -0,0 +1,168 @@
<?php
namespace App\Http\Livewire\Chat;
use App\Models\ChatMenu;
use App\Models\ChatMenuOption;
use Livewire\Component;
class ShowMenusChat extends Component
{
// ── Formulario Menú ──────────────────────────────────
public bool $showMenuForm = false;
public ?int $editMenuId = null;
public string $menuNombre = '';
public string $menuMensaje = '';
public bool $menuEsRaiz = false;
public bool $menuActivo = true;
// ── Formulario Opción ─────────────────────────────────
public bool $showOpcionForm = false;
public ?int $editOpcionId = null;
public ?int $opcionMenuId = null;
public string $opcionClave = '';
public string $opcionEtiqueta = '';
public string $opcionAccion = 'respuesta'; // ir_menu | respuesta | agente
public ?int $opcionMenuDestId = null;
public string $opcionRespuesta = '';
public int $opcionOrden = 0;
// ── CRUD Menú ─────────────────────────────────────────
public function saveMenu(): void
{
$this->validate([
'menuNombre' => 'required|string|max:80',
'menuMensaje' => 'required|string|max:500',
]);
if ($this->menuEsRaiz) {
ChatMenu::where('es_raiz', true)->update(['es_raiz' => false]);
}
$data = [
'nombre' => $this->menuNombre,
'mensaje' => $this->menuMensaje,
'es_raiz' => $this->menuEsRaiz,
'activo' => $this->menuActivo,
];
if ($this->editMenuId) {
ChatMenu::where('id', $this->editMenuId)->update($data);
} else {
ChatMenu::create($data);
}
$this->closeMenuForm();
}
public function editMenu(int $id): void
{
$menu = ChatMenu::findOrFail($id);
$this->editMenuId = $id;
$this->menuNombre = $menu->nombre;
$this->menuMensaje = $menu->mensaje;
$this->menuEsRaiz = (bool) $menu->es_raiz;
$this->menuActivo = (bool) $menu->activo;
$this->showMenuForm = true;
}
public function deleteMenu(int $id): void
{
ChatMenu::destroy($id);
}
public function closeMenuForm(): void
{
$this->showMenuForm = false;
$this->editMenuId = null;
$this->menuNombre = '';
$this->menuMensaje = '';
$this->menuEsRaiz = false;
$this->menuActivo = true;
}
// ── CRUD Opción ───────────────────────────────────────
public function saveOpcion(): void
{
$this->validate([
'opcionClave' => 'required|string|max:20',
'opcionEtiqueta' => 'required|string|max:150',
'opcionAccion' => 'required|in:ir_menu,respuesta,agente',
'opcionMenuDestId' => 'nullable|exists:chat_menus,id',
'opcionRespuesta' => 'nullable|string|max:1000',
]);
$data = [
'menu_id' => $this->opcionMenuId,
'clave' => strtolower(trim($this->opcionClave)),
'etiqueta' => $this->opcionEtiqueta,
'accion' => $this->opcionAccion,
'menu_destino_id' => $this->opcionAccion === 'ir_menu' ? $this->opcionMenuDestId : null,
'respuesta_texto' => $this->opcionAccion === 'respuesta' ? $this->opcionRespuesta : null,
'orden' => $this->opcionOrden,
];
if ($this->editOpcionId) {
ChatMenuOption::where('id', $this->editOpcionId)->update($data);
} else {
ChatMenuOption::create($data);
}
$this->closeOpcionForm();
}
public function openOpcionForm(int $menuId): void
{
$this->opcionMenuId = $menuId;
$this->editOpcionId = null;
$this->opcionClave = '';
$this->opcionEtiqueta = '';
$this->opcionAccion = 'respuesta';
$this->opcionMenuDestId = null;
$this->opcionRespuesta = '';
$this->opcionOrden = 0;
$this->showOpcionForm = true;
}
public function editOpcion(int $id): void
{
$op = ChatMenuOption::findOrFail($id);
$this->editOpcionId = $id;
$this->opcionMenuId = $op->menu_id;
$this->opcionClave = $op->clave;
$this->opcionEtiqueta = $op->etiqueta;
$this->opcionAccion = $op->accion;
$this->opcionMenuDestId = $op->menu_destino_id;
$this->opcionRespuesta = $op->respuesta_texto ?? '';
$this->opcionOrden = $op->orden;
$this->showOpcionForm = true;
}
public function deleteOpcion(int $id): void
{
ChatMenuOption::destroy($id);
}
public function closeOpcionForm(): void
{
$this->showOpcionForm = false;
$this->editOpcionId = null;
$this->opcionMenuId = null;
$this->opcionClave = '';
$this->opcionEtiqueta = '';
$this->opcionAccion = 'respuesta';
$this->opcionMenuDestId = null;
$this->opcionRespuesta = '';
$this->opcionOrden = 0;
}
public function render()
{
$menus = ChatMenu::with('options.menuDestino')->orderByDesc('es_raiz')->orderBy('nombre')->get();
$todosMenus = ChatMenu::orderBy('nombre')->get();
return view('livewire.chat.show-menus-chat', compact('menus', 'todosMenus'));
}
}