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>
Tanto en Telegram como en el chat web, cuando el usuario escribe el monto
directamente (ej: "recargar 10.000"), el bot ahora va directo al paso de
método de pago sin volver a preguntar el monto.
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>
- PublicChat: valida ≥2 palabras al ingresar nombre_remitente en chat web
- PagoValidadorService: remitenteCoincide exige ≥2 palabras que coincidan en el correo
- TelegramBotService: processRemitenteNombre exige ≥2 palabras igual que el chat web
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>
Si el usuario tiene chat_user_id en sesión pero no chat_conv_id (sesión
expirada o servidor reiniciado), mount() ahora busca la conversación activa
del contacto en la BD o crea una nueva. Antes quedaba sin convId y el enviar
no hacía nada.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sin este índice, dos procesos PHP simultáneos podían pasar el check yaUsado()
ambos con false y marcar el mismo hash dos veces, aplicando la recarga doble.
Con UNIQUE, el segundo INSERT falla y el catch existente lo bloquea (falla cerrado).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Hora exacta: tolerancia 0 (timestamp comprobante = timestamp email)
- Remitente bloquea: si el email trae el nombre del remitente y no coincide, rechaza
- Nuevo paso antes de mostrar datos bancarios: bot pide nombre completo del titular
- Nombres aceptados se guardan en solicitudes_recarga.nombre_remitente
- Próxima recarga: muestra botones con nombres anteriores confirmados o ingresa uno nuevo
- Funciona igual en Telegram y web chat
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El timestamp del correo Bancolombia registra la hora exacta de la transacción,
igual que el comprobante. El retraso es en la llegada al inbox, no en el contenido.
±2 min cubre desfases menores sin permitir cruces entre transacciones cercanas.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Con return inmediato, si Usuario A usaba email_A ($20k), Usuario B
(también $20k) encontraba ese email ya_usado y se bloqueaba sin revisar
el email_B que le pertenecía.
Ahora: ya_usado → continue (sigue iterando); solo retorna ya_usado
al final del loop si todos los emails coincidentes estaban agotados
(caso real de comprobante reutilizado).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Usa el mismo modelo recarga que el flujo admin, con status='Confirmado'
y reference='breb-bot/chat-{solicitud_id}' para trazabilidad.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Antes: monto coincide → hash ya_usado → retorna "reutilizado" (falso positivo
si había un email viejo del mismo valor en la ventana IMAP de 120 min).
Ahora: monto + fecha + hora + llave deben coincidir primero; solo entonces
se verifica el hash — garantizando que es el mismo comprobante exacto.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sin solicitud activa el bot mostraba el análisis completo (Gemini + IMAP) antes de rechazar.
Ahora se valida primero: si no hay solicitud pendiente, se rechaza de inmediato sin gastar API.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- SolicitudRecarga: modelo + migración para atar cada intento de recarga al usuario (previene cruce de pagos entre usuarios)
- PublicChat: mostrarTransferencia crea solicitud, updatedFotoComprobante valida contra ella, auto-aplica sin botón extra
- Botón Reintentar en web chat cuando el correo aún no llega (pago_pendiente en flujoData)
- aplicarRecargaValidada corregido: error de sintaxis por fragmento duplicado eliminado
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- sin_coincidencia muestra mensaje explicativo + botón Reintentar
- datosPago guardado en estado para reintentar sin reenviar foto
- pay_retry callback: re-corre validación con datos guardados
- Si confirma: limpia pendiente y muestra botón aplicar recarga
- Si sigue sin llegar: mantiene botón Reintentar disponible
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Default de 5 min causaba que pagos enviados minutos después del correo
de Bancolombia quedaran fuera de la ventana si había otros correos recientes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Keywords: captura "no inicia sesión Netflix", "no abre", "error de acceso" → credenciales.listar
- GeminiAgent prompt: bot no pide pantallazos ni promete revisiones; problemas de acceso → credenciales.listar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Si el cliente tiene Telegram → mensaje por Telegram.
Si el cliente tiene chat web activo → mensaje en la conversación.
Ambos canales pueden coexistir.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cuando el servicio de un cliente vence mañana, además del email existente
se envía mensaje por Telegram si el cliente tiene el bot vinculado.
Busca en chat_contacts canal=telegram con el user_id del cliente.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Nuevo GeminiAgentService: entiende lenguaje natural, responde conversacionalmente
y extrae acción + parámetros (ej: servicio:"Netflix", monto:50000)
- handleFreeText usa GeminiAgentService en lugar de GeminiIntentService
- dispatchAction acepta accion_data: va directo a Netflix sin pasar por el listado
- viewServiceByName: busca servicio por nombre (ILIKE) y abre sus planes
- Keywords simplificadas: solo intents genéricos sin parámetros
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
'vamos a hacer una recarga' no era detectado por Gemini (thinkingBudget=0
+ frases coloquiales). Agrega detectarPorKeywords() con regex para las 6
acciones principales. Gemini queda como fallback para casos ambiguos.
También elimina asesor.solicitar del prompt de GeminiIntentService.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Con el asesor removido, el estado 'agente' en conversación bloqueaba
handleText y handleFreeText silenciosamente. Audio transcripto como
'hacer una recarga' no generaba respuesta por este return prematuro.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
mount() ahora solo requiere chat_user_id (no chat_conv_id) para mantener
al usuario logueado. limpiarChat() borra mensajes y convId pero preserva
userId, nombreUsuario, saldoUsuario y paso='chat'.
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>
- Eliminar botón del menú principal, pago confirmado, pago no confirmado
- callback 'agent' redirige al menú principal en vez de transferToAgent
- asesor.solicitar del intent también redirige al menú
- Limpiar textos 'contacta a un asesor' en mensajes de error
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El check 'agente' devolvía silenciosamente antes de leer el step,
descartando el texto '1000' del usuario. Si el bot está esperando
input específico (await_amount, await_email, etc.) tiene prioridad
sobre el estado agente de la conversación.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Si pagos_confirmados no existe o falla la BD, NO confirmar el pago (fail-closed).
Antes fallaba abierto: ignoraba el error y confirmaba igual, permitiendo
recargar con el mismo comprobante múltiples veces.
Agrega motivo error_sistema en Telegram para casos de error interno.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Si pagos_confirmados no existe en producción (migración pendiente), la excepción
escapaba hasta handlePhoto y mostraba "Error analizando el comprobante".
Safety catch permite que la confirmación proceda aunque la tabla no esté.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El toggle puede no estar guardado como '1' en BD aunque el panel lo muestre activo.
Verificar correo_imap_host es más confiable: si hay host → intentar IMAP.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Con thinkingBudget=0 el modelo no consume tokens en razonamiento interno,
evitando que el JSON de respuesta quede truncado (bug: '{"banco":"Nu",...').
maxOutputTokens 2048→8192 en Vision, 1024→2048 en Intent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Livewire checkbox con value="1" puede guardar '1','on','true' según el navegador.
PagoValidadorService ahora acepta cualquier valor truthy en vez de comparación estricta.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
preg_replace('/[^0-9]/', '', '1,000.00') = '100000' porque el .00 aporta 00.
Fix: primero quitar la parte decimal (/[.,]\d{1,2}$/) antes de limpiar separadores.
Aplicado en BancolombiaParser::normalizarValor() y como doble defensa en PagoValidadorService.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PagoValidadorService: add detailed Log::info at every step (emails count,
body preview, parser result, which filter fails) so failures are visible in laravel.log
- CorreoImapService::cleanBody: detect and decode base64-encoded BODY[1] before
QP decoding — critical fix when Bancolombia sends HTML part as base64
- BancolombiaParser: make esBancolombia() broader (includes 'valor','$','abono' etc.),
add extraerPrimero() helper, add multiple regex fallbacks for remitente/fecha/hora/valor
to handle different Bancolombia email HTML formats beyond the SMS text pattern
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Only email-confirmed payments get the apply button. Unconfirmed payments
show the failure reason and direct the user to an advisor.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When email validation fails (IMAP disabled, no emails, no match), show the
specific reason instead of a generic "No confirmado". Also add a manual
"Aplicar de todas formas" button so the user isn't stuck — they can still
apply the recharge after visually verifying the receipt.
Also show banco/fecha/hora in the Telegram analysis result for context.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove MercadoPago as a direct payment option for services and promotions.
If the user has enough balance: show only "Pagar con saldo".
If not: show current balance, amount missing, and a "Recargar saldo" button
that leads to the recharge flow (where MercadoPago and Bre-B remain available).
Applies to both TelegramBotService (startPurchase, viewPromo) and
PublicChat (iniciarCompra, verPromocion).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. PublicChat::comprarPromoConSaldo — double saldo subtraction: $saldo->update()
already updates the model in memory, so $this->saldoUsuario = $saldo->valor - precio
was subtracting twice. Fixed by computing $nuevoSaldo before the update.
Also wrapped in DB::transaction (was missing unlike pagarConSaldo).
2. TelegramBotService::listServices — showed all services without filtering by the
user's rol_id, so users could see services with no plans for their role.
Now uses whereHas('tarifas', rol_id) like PublicChat and the web.
3. TelegramBotService::payWithSaldo — no DB::transaction; if saldo update failed
after historial was created, user got a free service. Now wrapped atomically.
4. TelegramBotService::buyPromoSaldo — same missing transaction + fecha_final was
hardcoded to 30 days instead of promo->dias. Both fixed.
5. TelegramBotService::showCredentials — N+1 queries loading cuentas lazily per
historial. Added cuentas to the eager-load list.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three bugs fixed:
1. TelegramBotService queried Cuentas by tarifa_id + estado=activo which
never matched. Now uses CuentasHelper::contar/buscar — same date-range
+ historial-count logic as the web (pendiente for full accounts,
activo + slot check for per-screen accounts).
2. efectivePrice in TelegramBotService and PublicChat used the Preferencial
table (legacy) instead of Usuario_tarifa (used by the web admin panel).
CuentasHelper::precio checks Usuario_tarifa first, Preferencial as fallback.
3. payWithSaldo compared saldo against tarifa->valor (base price) instead of
the effective price stored in state. Also registered the purchase with the
wrong amount.
Extract shared logic to CuentasHelper (contar, buscar, precio) to avoid
duplication between TelegramBotService and PublicChat.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Create pagos_confirmados table to track consumed email receipts
- PagoValidadorService now validates: valor + fecha + hora (±10min) + llave Bancolombia
- Each matched email is hashed (sha1 body+date) and marked as used; a second
match returns estado=ya_usado instead of confirmado
- Add partial remitente name matching against the registered user's name
- GeminiVisionService prompt now extracts llave (@xxx) from the receipt image
- TelegramBotService shows distinct message for ya_usado state
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
gemini-3.5-flash uses thinking tokens that share the same budget as output
tokens. With maxOutputTokens=200 (vision) or 100 (intent), the model would
exhaust the budget on thinking (~288-1043 tokens) and truncate the actual
response to 8-11 tokens, producing garbage output like "040825".
Fix: 2048 for vision (needs full JSON), 1024 for intent, 512 for connection test.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
gemini-3.x models return thinking blocks with thought:true flag in parts[].
Filter those out so only actual response text is used.
Also extract the first {...} JSON object from the text so stray content
before or after (like '040825') doesn't break parsing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Rewrote extraerPago() to try v1 then v1beta for model compatibility
- Returns __error key with actual API message when both fail
- TelegramBotService shows that error in chat instead of generic message
- Also ensures model is read fresh from config (no hardcoded URL in constructor)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
gemini-2.5-flash and gemini-2.0-flash-lite are restricted/unavailable.
gemini-3.5-flash is confirmed working on this account.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The API returned: Unknown name "thinkingConfig": Cannot find field.
gemini-2.5-flash works with a plain generateContent request, no special config needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- GeminiIntentService/VisionService: auto-select v1 for 2.5+ models, v1beta for older
- probarGemini: tries v1 then v1beta, shows actual API error message, filters model
list to only those supporting generateContent so the user sees usable options
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Asterisks in the masked email (liza*****@domain) broke Telegram Markdown
parsing, causing the message to silently fail. Backticks (inline code)
render correctly regardless of the email content.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- processEmail: wrap DB lookup and ChatContact create/update in try-catch
so any exception sends an error message to the user instead of silently
failing and leaving state stuck at await_email
- sendOtp: inform user if mail sending fails so they know to wait/retry
- TelegramWebhookController: top-level try-catch returns 200 always so
Telegram never retries on server errors (retries caused duplicate state resets)
- ChatContact canal_id cast to string to avoid type mismatch on insert
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
thinkingConfig must be a top-level key in the request, NOT nested inside
generationConfig. This is required for gemini-2.5-flash and newer models.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
gemini-2.5-flash and newer require thinkingBudget:0 to disable thinking
mode, otherwise the API rejects the request or returns unexpected format.
Regex matches gemini-2.5-* and gemini-3.x-* automatically.
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>
gemini-1.5-flash was removed from the v1beta API. Updated all three
references (GeminiIntentService, GeminiVisionService, config test).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add language=es to Whisper query params so audio is always transcribed in Spanish regardless of accent or background noise
- Show "Procesando..." after echoing the transcript so user knows the AI is interpreting the intent
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>
lizandro.guarnizo (17 chars) was showing 'lizandro.' (9 chars including the dot)
because ceil(17/2)=9. Now always shows exactly min(4, len) chars: 'liza*************@domain'
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- STATE_TTL: 7200s → 315360000s (10 years) so sessions survive indefinitely
- Telegram: "salir"/"/salir" now shows inline confirmation keyboard instead of going straight to menu; "Cerrar sesión" button added to main menu; logout_ask/logout_confirm/confirmLogout() handler clears state and returns to email prompt
- Web chat: "Cerrar sesión" button in main menu; clicking it shows a confirmation message with "Sí, cerrar sesión" / "No, quedarme" buttons; only the confirmed action calls cerrarSesion()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Send response_format=json as URL query param (not form field)
- If JSON parse yields empty text, fall back to raw plain-text body
- Prevents false 'error' logs when Whisper returns plain text
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>
- Any message matching salir/menu/volver/cancelar/inicio resets flow and shows main menu
- Register /menu /salir /cancelar as official bot commands via setMyCommands
- Commands are registered automatically when saving the webhook from admin panel
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
apiCall() and downloadTelegramFile() were using file_get_contents() for
outgoing requests to api.telegram.org, which fails on restricted servers.
Now use Http:: (Guzzle) consistently, matching the rest of the project.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
file_get_contents fails on many hosts due to allow_url_fopen or SSL restrictions.
Http:: (Guzzle) is more reliable and shows a clearer error message on failure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add detectarIntencionKeyword() with regex patterns in Colombian Spanish
that runs before Gemini (instant, no API cost)
- Gemini is now only called for ambiguous text
- Improve Gemini prompt with real examples and informal Spanish
- Replace generic "no entendí" with actionable buttons
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Filter promos by TarifaPromo.rol_id (same as services)
- Fix visible filter: use string 'true' not boolean
- Get price from Usuario_promo or TarifaPromo instead of Promociones.precio
- Get utilidad from TarifaPromo for correct margin tracking
- Applied same fixes to TelegramBotService promo flows
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace tarifa_id+estado='ocupado' query with the same logic used in
ShowServicios: filter by servicio_id, date range, historiales_count
- Full-account plans (pantallas == completa): look for estado='pendiente'
accounts without assigned profiles
- Partial plans (screen sharing): look for estado='activo' accounts
with remaining slots (historiales_count <= pantallas_servicio - pantallas)
- Wrap purchase in DB::transaction to avoid race conditions
- Remove incorrect cuenta->update(['estado' => 'ocupado']) — availability
is now tracked via historial_cuentas count, not a flag
- Use effective price (Preferencial override) for saldo deduction
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>
- Web chat: 'Otro valor' button sets flujo=recarga.monto_custom,
next text message is parsed as the amount (min $1.000)
- Telegram: '✏️ Otro valor' button sets step=await_amount,
next message is parsed and routed to method selection
- Preset amounts in Telegram now show 2 per row for compactness
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>