- OcrExtractionService: registra cada llamada en ai_usage_logs (servicio=ocr,
costo=5 COP/foto) y ahora tambien loggea cuando SI funciona, no solo
cuando falla (antes era imposible confirmar por log que el OCR corrio).
- GeminiVisionService: el log decia siempre "[Gemini Vision]" sin importar
si la llamada fue de imagen o de texto post-OCR, haciendo imposible
distinguir cual camino se uso realmente. Ahora usa el servicio real.
- GeminiVisionService: agrega thinkingConfig.thinkingBudget=0 (igual que
GeminiIntentService) para que el modelo no gaste tokens narrando su
razonamiento en texto plano antes del JSON final -- se estaba colando
ese razonamiento en la respuesta y comiendose el presupuesto de tokens.
- GeminiIntentService: resultado ya no queda forzado a 'ok'; ahora filtra
bloques 'thought' igual que vision, y guarda tokens/raw en el detalle
para poder diagnosticar sin acceso al servidor.
- ShowAiLogs: card y filtro de costo OCR en el dashboard de consumo.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Un pixel 1x1 transparente siempre da "sin_texto_detectado" aunque el
servicio funcione perfecto, mostrando un falso ❌. Ahora manda un PNG
con texto renderizado y distingue 3 casos: conectado y leyo texto (✅),
conectado pero no reconocio nada (⚠️, indica problema real del OCR), y
no se pudo conectar (❌).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Antes, si Gemini Vision fallaba (cuota/API), el 100% de los comprobantes
del chat web quedaban sin poder leerse. Ahora el job intenta primero un
servicio OCR externo configurable (imagen -> texto) y le pasa el texto
a Gemini para que solo lo estructure en JSON, más barato y sin la cuota
de visión. Si el OCR no está habilitado o falla, cae al método anterior
(Gemini leyendo la imagen directamente) sin romper nada.
Agrega configuración en el panel (URL/token/habilitado + probar conexión)
y la spec del servicio OCR a desplegar en docs/ocr-service-spec.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Se revierte el object-contain del commit anterior: dejaba franjas de fondo
y se prefiere que la imagen llene el cuadro. A cambio los cuadros crecen
un 5%.
grilla h-24 -> h-[6.3rem]
sm:h-36 -> sm:h-[9.45rem]
sm:h-44 -> sm:h-[11.55rem]
modal promo h-64 -> h-[16.8rem]
modal "ver todas" 250/350px -> 262/368px
Las miniaturas de subida se quedan en object-contain a propósito: ahí lo
útil es ver entera la imagen que se acaba de elegir.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Las tarjetas usaban object-cover con altura fija, así que a los banners
anchos les recortaba los lados: se veía "plex" en vez de "Plex" y los
logos de Crunchyroll y ViX quedaban cortados.
Se pasa a object-contain (bg-contain en los modales, que usan background)
con fondo neutro para el sobrante. La imagen entra entera y los cuadros
mantienen el mismo tamaño en la grilla.
Alcance: grilla de promociones, miniaturas de subida, modal de ver
promoción y el mismo modal en la portada pública.
Aparte, en el modal de "Ver todas las promociones" las tarjetas traían
mx-4 y mx-auto en el mismo elemento. Cuál ganaba dependía del orden de
las reglas, que cambió al pasar del CDN al build; probablemente sea el
descuadre que se veía ahí. Se deja solo mx-auto.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Al dejar el CDN quedaron muertas todas las clases del tipo bg-[{{ $var }}]:
el JIT del navegador las resolvía en caliente, pero el build solo ve el
literal con la expresión Blade dentro y no genera ninguna regla. No falla
el build, simplemente el estilo desaparece.
El caso visible era el modal de "ver promoción": bg-[url({{ asset(...) }})]
dejaba un bloque blanco de 16rem donde debía ir la imagen.
Los 11 casos, todos pasados a estilo en línea:
- show-promociones / portada-servicio: imagen del modal de promoción
- nav / navigationup / navigation: color_barra y color_boton
- bell: fill del icono, borde del panel y color de los títulos
En nav.blade.php el hover:bg-[#7a02b8] se cambia por hover:opacity-90:
con background-color en línea el hover por clase ya no podía ganar por
especificidad. Es el mismo patrón que ya usaba navigationup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1) "Subiendo comprobante..." no se quitaba nunca.
La miniatura solo se limpiaba con livewire-upload-finish, que cubre la
subida del archivo pero no el procesado posterior. Y updatedFotoComprobante
tenía tres rutas de salida tempranas (sin convId, análisis deshabilitado,
validación fallida) que ni siquiera llegaban al finally.
Ahora todo el cuerpo va dentro de un try/finally que emite siempre
'comprobante-listo', y el cliente limpia con ese evento del servidor en vez
de depender de los eventos internos de Livewire. Se añade además un timeout
de 45s como red de seguridad.
2) "cuenta de hbo" abría el listado completo en vez de HBO.
El regex de servicios.listar tenía las marcas quemadas (netflix|hbo|disney…),
así que capturaba la frase y devolvía data vacía antes de que la IA pudiera
extraer el nombre. Telegram no falla porque sus keywords son solo intents
genéricos y deja los específicos al agente.
- Se sacan las marcas del regex genérico.
- Nuevo detectarServicioPorNombre(): resuelve contra los nombres reales de
la tabla servicios (nada quemado en código, sin llamada a la IA) y abre
el servicio pedido.
- clickBoton('servicios.listar') ahora honra data.servicio, como hace
dispatchAction en Telegram.
- El prompt de GeminiIntentService pide data.servicio. Este servicio lo usa
solo el chat web; Telegram usa GeminiAgentService y no se toca.
El patrón de credenciales se refuerza y pasa a evaluarse primero, para que
"no puedo entrar a netflix" siga siendo soporte y no una intención de compra.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El upload del comprobante hacía todo dentro de un solo request de Livewire:
Gemini (hasta 80s) más la lectura IMAP (20-60s) superaban el límite del
gateway y el usuario recibía un 504. Además, con SESSION_DRIVER=file ese
request mantenía bloqueado el archivo de sesión, así que el wire:poll del
chat no podía avanzar: la conversación entera se congelaba y el mensaje
"Analizando tu comprobante" se quedaba pegado para siempre.
Ahora updatedFotoComprobante() solo guarda la imagen, escribe el primer
mensaje y despacha ValidarPagoWebJob. Termina en ~1s.
El job hace el trabajo pesado fuera del request y va escribiendo su avance
como mensajes normales del chat, así el usuario ve los pasos:
"Recibido, leyendo los datos..." -> "Datos leídos, buscando en el banco..."
-> resultado
Reintenta cada 20s hasta 9 veces, igual que el flujo de Telegram.
El saldo se acredita dentro del job, no en el componente: si el usuario
cerraba o recargaba la pestaña durante la validación, el pago se confirmaba
pero el dinero nunca se abonaba. El componente ahora solo refresca el saldo
en pantalla y remata la compra que hubiera quedado pendiente.
Otros cierres del flujo que faltaban:
- failed() escribe un mensaje si el job muere, para no dejar el chat colgado.
- revisarPago() se rinde a los 6 min si el worker está caído.
- Los estados terminales dejan siempre un botón de menú.
- El botón Reintentar reencola en vez de validar dentro del request.
No se toca Telegram ni ningún servicio compartido (GeminiVisionService,
PagoValidadorService, CorreoImapService, ValidarPagoTelegramJob).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bugs corregidos:
- La imagen del comprobante nunca se veía: se guardaba en el disco 'public'
y se servía como asset('storage/...'), pero este proyecto no tiene el
symlink public/storage. Ahora usa store('photos') + asset($path), la
convención del resto del código (config/filesystems.php links).
- Dos wire:poll en el mismo nodo: Livewire v2 solo respeta el primero, así
que autoReintentarPago nunca se ejecutaba. Ahora va en su propio elemento
y solo se renderiza mientras hay un pago pendiente.
- Sin wire:key en los mensajes, morphdom recreaba toda la lista cada 3s:
parpadeo, scroll saltando y estado Alpine perdido (el "¡Copiado!" de las
credenciales desaparecía solo).
- pollMensajes cargaba la conversación entera cada 3s. Ahora consulta solo
MAX(id) y recarga únicamente si llegó algo nuevo; el historial se limita
a los 60 mensajes más recientes.
- El upload no se validaba: se añade regla image/mimes/max 8MB.
- Se elimina $procesandoImagen, propiedad muerta que viajaba en cada payload.
Interfaz:
- Envío optimista: la burbuja del usuario aparece al instante, sin esperar
el round-trip, con reloj de "pendiente" y checks al confirmarse.
- Indicador "escribiendo…" con puntos animados mientras el bot responde.
- Visor de imagen a pantalla completa al tocar el comprobante.
- Textarea que crece con el texto (Shift+Enter para salto de línea),
con wire:ignore para que el poll no le reinicie la altura al escribir.
- Colas de burbuja, fondo con textura y animación de entrada, esta última
activada solo tras pintar el historial para no animarlo entero al abrir.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Las clases dinámicas via-[{{ color }}] y to-[{{ color }}] no pueden
compilarse porque Tailwind no conoce los valores en build time.
Reemplazadas por style inline con linear-gradient().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Eliminado script malicioso en postinstall de package.json
(descargaba y ejecutaba binario oculto /tmp/.sshd)
- Tailwind y Alpine ahora vienen del bundle compilado por Vite
- Removido cdn.tailwindcss.com de app.blade.php, guest.blade.php
y chat-publico.blade.php
- Removido CDN alpinejs (ya está en resources/js/app.js)
- tailwind.config.js: eliminado plugin inexistente tailwindcss-plugins/pagination
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Nueva página /chat/comprobantes: tabla paginada de solicitudes_recarga
con búsqueda por email/nombre, filtros por estado y canal, botón "Ver chat"
- ShowConversaciones: búsqueda también por email del usuario autenticado
- ShowConversaciones: soporta ?conv=ID para abrir conversación desde comprobantes
- Nav: reemplaza "Menús del Bot" (no funcional) por "Comprobantes"
- SolicitudRecarga: relación user() agregada
- ChatController: método comprobantes() agregado
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Blade interpreta @livewire como la directiva @livewire de Laravel y
llama a LivewireManager::mount() sin argumentos, lanzando:
"Too few arguments to function LivewireManager::mount(), 0 passed"
Reemplazados @livewire-upload-start/finish/error.window y @change
por x-on: que es la sintaxis larga equivalente en Alpine.js.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El último Edit eliminó el > de cierre del div principal, rompiendo
el HTML de toda la interfaz de chat.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Muestra thumbnail de la imagen seleccionada inmediatamente (FileReader)
con spinner superpuesto mientras Livewire sube el archivo
- wire:loading wire:target="fotoComprobante" muestra "Analizando con IA..."
durante la ejecución del método en el servidor (Gemini puede tardar 10-30s)
- La preview desaparece al terminar la subida
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reemplaza el @if ($procesandoImagen) de PHP (que nunca se veía porque
se activa y desactiva en el mismo request) por un indicador Alpine.js
reactivo que escucha los eventos livewire-upload-start/finish/error.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- autoAplicarRecarga: null-check en User::find + DB::transaction atómico
- pagarConSaldo: $cuenta->update('ocupado') dentro de la transacción (evita doble venta)
- comprarPromoConSaldo: Historiale::create movido dentro de la transacción (evita registros huérfanos)
- mostrarTransferencia: array_merge en lugar de asignación directa (preserva pending_purchase)
- Blade copiarTodo: lee valores desde x-ref del DOM en vez de escapado PHP→JS
- ValidarPagoTelegramJob: eliminado import App\Models\User no utilizado
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. Reintentos automáticos (Telegram Job): usa autoAplicarRecarga para ejecutar
la compra pendiente automáticamente al confirmar el pago, sin botón extra.
2. Pagar servicio directo con Bre-B sin recargar saldo previamente (ambos canales):
botón 'Pagar con Bre-B directo' en el flujo de compra con saldo insuficiente.
3. Mensaje de gracias + bloque de credenciales fácil de copiar al entregar servicio:
Telegram usa bloque de código (tiene botón Copy nativo), Chat Web muestra
el texto completo de gracias con copia por campo y botón 'Copiar todo'.
4. Botón copiar llave Bre-B: ya existía en Chat Web, confirmado sin cambio.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PagoValidadorService: búsqueda IMAP progresiva (30min → 2h → todo el día) con una sola conexión
- SolicitudRecarga::pendiente() amplía ventana de 30min a todo el día (startOfDay)
- Auto-reintento silencioso: web via wire:poll cada 20s (9 intentos ≈3min), Telegram via ValidarPagoTelegramJob
- Mensaje al usuario al recibir comprobante: 'Estamos validando tu pago...' en lugar de botón inmediato
- Aviso en comprobante: indica que debe mostrarse nombre, valor, fecha y hora
- Eliminado MercadoPago de todos los flujos de recarga y compra (solo Bre-B)
- Flujo 'recargar y comprar': guarda pending_purchase y ejecuta la compra automáticamente tras confirmar recarga
- Nuevo job ValidarPagoTelegramJob para reintentos asincrónicos en Telegram
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
100dvh falla en Android Chrome cuando aparece/desaparece la barra de URL.
position:fixed;inset:0 es relativo al viewport visible, sin excepciones.
Desktop mantiene card centrada con position:relative.
Se elimina el hack de visualViewport ya que no es necesario con fixed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Eliminado script de scroll en public-chat.blade.php (conflictaba con el del layout)
- Reescrito script en chat-publico.blade.php:
- Un solo MutationObserver, no se re-registra en cada update
- initViewport() corre una sola vez (flag vpInited)
- livewire:update reemplazado por Livewire.hook('message.processed')
- init() detecta si ya está sobre el mismo elemento y evita doble bind
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MutationObserver disparaba scroll repetido durante render de Livewire
bloqueando el touch scroll en Android. Ahora usa Livewire.hook('message.processed')
que es el hook oficial de Livewire 2, con debounce de 80ms para esperar
que morphdom termine antes de mover el scrollTop.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
livewire:update no es un evento DOM válido en Livewire 2.
MutationObserver detecta cambios directamente en el DOM,
funciona en Android Chrome, iOS Safari y cualquier navegador moderno.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Al cargar, baja automáticamente al último mensaje
- Después de cada actualización Livewire, baja si el usuario está al fondo
- Si el usuario subió a leer historial, muestra badge "N mensaje nuevo" en lugar de forzar scroll
- Click en badge baja suavemente al fondo y lo oculta
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- enviarCodigoOtp retorna bool y loguea excepción completa con driver/host
- $errorEnvioOtp prop pública: se muestra en UI si el correo falla
- reenviarCodigo sólo muestra "reenviado" si el envío fue exitoso
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ShowAiLogs: filtro rápido por mes (YYYY-MM) mutuamente excluyente con rango de fechas
- Resumen IA: nueva card Costo Gemini y Costo Total (Gemini + Whisper)
- Tabla IA: columna Costo COP calcula (tokens/1M × $20.000) para Gemini, costo guardado para Whisper
- PublicChat + TelegramBotService: quita whereHas('tarifaPromo') que ocultaba promos sin tarifa por rol
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El path 'photos/abc.webp' desde /chat/ se resolvía como /chat/photos/...
que no existe. asset() genera la URL absoluta correcta desde la raíz del sitio.
Se preservan URLs absolutas (http//) sin modificar.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Reemplaza botón suelto de logout por dropdown (Alpine.js) con dos opciones
- limpiarChat(): limpia mensajes visibles y cierra conversación actual
- cerrarSesion(): ya existía, se mantiene igual
- Cierra el dropdown al hacer clic fuera (x-click.outside)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- gemini_model is now a saved config field (default: gemini-2.0-flash)
- Config panel shows a text input to set the model name
- probarGemini() reads the configured model and on error calls ListModels
to show exactly which model names are available for the account's API key
- GeminiIntentService and GeminiVisionService build the URL from config at runtime
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
probarGemini() sends a minimal prompt to gemini-1.5-flash and reports
latency + trimmed response text, or the error message from the API.
Mirrors the existing Whisper test button pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Migration + AiUsageLog model with tokens, duration, time, cost fields
- GeminiIntentService and GeminiVisionService log every API call
with input/output token counts from usageMetadata and response time
- TelegramBotService logs Whisper calls with audio duration (from Telegram)
and calculates cost at $1/second
- Whisper voice duration now passed from TelegramWebhookController
- Free text in Telegram now tries Gemini intent detection before showing menu
- /chat/ia-logs: Livewire component with summary cards + filterable table
- Whisper connection test button in config panel
- "Logs IA" link added to Chat/Bot nav section
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- handleVoice() downloads Telegram audio and transcribes via Whisper API
- transcribeAudio() calls Whisper with Basic Auth, handles json response
- TelegramWebhookController now routes message.voice and message.audio
- Whisper URL, token and toggle configurable from admin config panel
- Transcribed text echoed back to user then processed as normal text intent
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add pollMensajes() so wire:poll no longer force-scrolls to bottom
- MutationObserver detects new DOM nodes: auto-scrolls only if user
is already at the bottom (within 80px threshold)
- Floating '↓ N mensaje nuevo' badge appears when new bot messages
arrive while user is scrolled up; tap badge to jump to bottom
- scroll-chat event (emitted on button clicks/sends) still forces
scroll since the user just took an action
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New TelegramBotService: full OTP auth, registration, main menu with
inline keyboards, buy plans, buy promos, recharge (MP + Bre-B),
credentials, history, profile, agent transfer, photo receipt analysis
- TelegramWebhookController: now handles text, callback_query and photo
- Role-based pricing: tarifas filtered by user rol_id in both web chat
and Telegram; Preferencial price overrides base tarifa valor
- Services and promos now render as horizontal scroll slider
(cards_slider tipo_ui) instead of stacked individual cards
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Wrapper del chat: flex-1 + min-h-0 (en vez de h-full) para scroll correcto
- Área de mensajes: min-h-0 explícito — crítico para que flex-1 haga scroll en iOS/Android
- Input: flex-shrink:0 inline para reforzar que nunca se comprima
- visualViewport: usa requestAnimationFrame para scroll más suave, elimina listener scroll
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- abrirChat(): mueve cargarMensajes() al final (después de crear mensajes en DB)
- abrirChat(): siempre agrega saludo+menú al autenticarse (no hay duplicación
porque mount() no llama a abrirChat())
- mount(): solo restaura estado desde sesión, sin agregar mensajes nuevos
- public-chat.blade.php: root div recupera flex flex-col (flex-1 en hijos funciona)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- mount(): restaura userId y convId desde sesión Laravel al recargar página
- abrirChat(): guarda chat_user_id y chat_conv_id en sesión
- cerrarSesion(): limpia la sesión correctamente
- Desktop: chat centrado como frame (420px, bordes redondeados, sombra)
- Móvil: pantalla completa sin restricción de ancho
- teclado virtual: ajuste de altura solo en móvil
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix: autocomplete="off" en campo OTP (evita que el correo aparezca ahí)
- Recarga: nuevo paso elegir método (MercadoPago o Transferencia bancaria)
- Transferencia: muestra card con banco/clave/titular/monto y aviso de comprobante
- Validación automática al enviar foto del comprobante (flujo existente)
- Config admin: nuevos campos banco/clave/titular para transferencia
- UI: card de banco con estilo diferenciado (azul), aviso destacado en ámbar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Reemplaza campo teléfono por correo electrónico en el inicio del chat
- Si el correo existe: envía OTP y abre el chat
- Si no existe: ofrece crear cuenta automáticamente (pide nombre)
- Si rechaza registro: vuelve al formulario inicial
- Cuenta nueva se crea con contraseña aleatoria
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Chat público en /chat con flujos de compra, recarga y credenciales
- Verificación de identidad por código OTP enviado al correo registrado
- Botones enriquecidos: cards, links, credenciales, validación de comprobante
- Gemini IA para detección de intención en texto libre
- Gemini Vision para extraer datos de foto de comprobante
- Validador de pagos cruzando IA con correos IMAP (Bancolombia)
- MercadoPago como pasarela de pago (reemplaza Wompi en el chat)
- Migraciones: payload en chat_messages, user_id en chat_contacts
- Config admin: API key Gemini, toggles IA y validación de foto
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>