From 9dba93e7f43715aff692d1d0c5995b8003610d38 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Fri, 21 Aug 2026 19:18:09 -0500 Subject: [PATCH] =?UTF-8?q?feat(widget):=20mostrar=20que=20el=20asistente?= =?UTF-8?q?=20est=C3=A1=20escribiendo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Entre que el visitante manda y el agente contesta pasan varios segundos —más si tiene que buscar en la base de conocimiento— y el widget se quedaba mudo. Sin señal de vida, la reacción normal es pensar que se colgó y volver a mandar. Ahora aparecen los tres puntos que cualquiera reconoce de WhatsApp, y el campo se bloquea mientras responde: mandar tres mensajes seguidos desordena el hilo y multiplica el consumo sin que nadie lo pida. La limpieza va en un then final y no en el de éxito: si quedara ahí, un error de red dejaría los puntitos animándose para siempre y el campo bloqueado, que es peor que no haber puesto nada. Probado con la respuesta normal y con un fetch que falla. El indicador se inserta siempre al final, así que un mensaje que llegue mientras tanto no lo deja en el medio del hilo. Co-Authored-By: Claude Opus 5 --- .../api/umind_widget_controller.go | 54 ++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/rest/controllers/api/umind_widget_controller.go b/rest/controllers/api/umind_widget_controller.go index 9ccd58f..24c4708 100644 --- a/rest/controllers/api/umind_widget_controller.go +++ b/rest/controllers/api/umind_widget_controller.go @@ -139,6 +139,16 @@ const umindWidgetJS = `(function () { '.umind-msg-user{color:#fff;margin-left:auto;border-bottom-right-radius:4px;}', '.umind-msg-bot a{color:var(--umind-color);text-decoration:underline;word-break:break-all;}', '.umind-msg-user a{color:#fff;text-decoration:underline;word-break:break-all;}', + // "Escribiendo": los tres puntos que cualquiera reconoce de WhatsApp. Sin + // esto, entre que la persona manda y el agente contesta —varios segundos + // si tiene que buscar en el conocimiento— el widget se queda mudo y parece + // colgado. + '.umind-escribiendo{display:flex;gap:4px;align-items:center;width:fit-content;margin-bottom:10px;padding:11px 13px;background:#fff;border:1px solid #ececec;border-radius:13px;border-bottom-left-radius:4px;}', + '.umind-escribiendo i{width:6px;height:6px;border-radius:50%;background:#b6b6b6;display:block;animation:umind-punto 1.3s infinite;}', + '.umind-escribiendo i:nth-child(2){animation-delay:.18s;}', + '.umind-escribiendo i:nth-child(3){animation-delay:.36s;}', + '@keyframes umind-punto{0%,60%,100%{opacity:.3;transform:translateY(0);}30%{opacity:1;transform:translateY(-3px);}}', + '@media (prefers-reduced-motion:reduce){.umind-escribiendo i{animation:none;opacity:.6;}}', '.umind-inputbar{display:flex;align-items:center;border-top:1px solid #eee;padding:8px;gap:6px;background:#fff;flex-shrink:0;}', '.umind-inputbar input{flex:1;border:1px solid #e5e5e5;border-radius:20px;padding:9px 14px;font-size:13px;outline:none;}', '.umind-inputbar button{border:none;color:#fff;width:34px;height:34px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;}', @@ -225,13 +235,38 @@ const umindWidgetJS = `(function () { } } + // El indicador es un elemento suelto que se agrega y se saca: así no hay que + // llevar estado ni limpiar nada si la petición falla a mitad de camino. + var escribiendoEl = null; + + function mostrarEscribiendo() { + if (escribiendoEl) return; + escribiendoEl = document.createElement('div'); + escribiendoEl.className = 'umind-escribiendo'; + escribiendoEl.setAttribute('aria-label', 'El asistente está escribiendo'); + for (var i = 0; i < 3; i++) escribiendoEl.appendChild(document.createElement('i')); + msgsEl.appendChild(escribiendoEl); + msgsEl.scrollTop = msgsEl.scrollHeight; + } + + function ocultarEscribiendo() { + if (!escribiendoEl) return; + if (escribiendoEl.parentNode) escribiendoEl.parentNode.removeChild(escribiendoEl); + escribiendoEl = null; + } + function agregarMensaje(texto, quien) { var burbuja = document.createElement('div'); pintarConEnlaces(burbuja, texto == null ? '' : texto); var esUser = quien === 'user'; burbuja.className = 'umind-msg ' + (esUser ? 'umind-msg-user' : 'umind-msg-bot'); if (esUser) burbuja.style.background = color; - msgsEl.appendChild(burbuja); + // Antes del indicador, para que los puntitos queden siempre al final. + if (escribiendoEl && escribiendoEl.parentNode === msgsEl) { + msgsEl.insertBefore(burbuja, escribiendoEl); + } else { + msgsEl.appendChild(burbuja); + } msgsEl.scrollTop = msgsEl.scrollHeight; } @@ -274,6 +309,13 @@ const umindWidgetJS = `(function () { if (!texto) return; inputEl.value = ''; agregarMensaje(texto, 'user'); + mostrarEscribiendo(); + // Se bloquea el envío mientras responde: mandar tres mensajes seguidos + // desordena el hilo y multiplica el consumo sin que nadie lo pida. + inputEl.disabled = true; + sendEl.disabled = true; + sendEl.style.opacity = '.5'; + fetch(apiBase + '/mensaje', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -290,6 +332,16 @@ const umindWidgetJS = `(function () { ? 'El asistente no está disponible en este momento.' : 'Error de conexión, intenta de nuevo.', 'bot'); + }) + .then(function () { + // Pase lo que pase: si esto quedara solo en el .then de éxito, un + // error dejaría los puntitos animándose para siempre y el campo de + // texto bloqueado. + ocultarEscribiendo(); + inputEl.disabled = false; + sendEl.disabled = false; + sendEl.style.opacity = ''; + inputEl.focus(); }); }