feat(widget): mostrar que el asistente está escribiendo

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 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-08-21 19:18:09 -05:00
co-authored by Claude Opus 5
parent 44686d409a
commit 9dba93e7f4
@@ -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();
});
}