CHAT_TIMEOUT_IMPLEMENTATION.md 6.3 KB

Sistema de Timeout y Retry para Chat

📋 Descripción

Sistema implementado para evitar que los usuarios queden atascados esperando indefinidamente cuando el API del chatbot demora demasiado en responder.

✨ Características

1. Timeout Automático (15 segundos)

  • Todas las peticiones al API de chat tienen un límite máximo de 15 segundos
  • Si la petición excede este tiempo, se cancela automáticamente usando AbortController
  • El usuario es notificado cuando esto ocurre

2. Retry Automático

  • Si una petición expira por timeout, el sistema reintenta automáticamente una vez
  • El mensaje del usuario se preserva y se reenvía
  • Se muestra una notificación informando al usuario del reintento

3. Mensajes Progresivos en el Loader

El componente DynamicLoader ahora incluye mensajes optimizados que se muestran durante la carga:

  • 0-4s: "Asistente escribiendo..."
  • 4-8s: "Analizando tu consulta..."
  • 8-12s: "Preparando tu respuesta..."
  • 12-20s: "Casi listo..."

Los mensajes están diseñados para cubrir hasta 20 segundos (tiempo máximo incluyendo márgenes).

🔧 Implementación Técnica

useChat.ts

// Configurar timeout de 15 segundos
const controller = new AbortController();
const timeoutId = setTimeout(() => {
  console.log("⏰ [CHAT] Timeout alcanzado, cancelando petición...");
  controller.abort();
}, 15000); // 15 segundos

const response = await fetch("/api/chat", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ message, messages, chatType }),
  signal: controller.signal, // 🔑 Señal de cancelación
});

// Limpiar timeout si la petición fue exitosa
clearTimeout(timeoutId);

Manejo del Error de Timeout

catch (error) {
  clearTimeout(timeoutId);
  
  // Detectar si el error fue por timeout
  const isTimeout = error instanceof Error && error.name === 'AbortError';
  
  // Si fue timeout y es el primer intento, reintentar
  if (isTimeout && retryCount === 0) {
    toast.info("Reintentando...", {
      description: "La petición tardó demasiado, reintentando automáticamente.",
    });
    
    // Reintentar después de 1 segundo
    setTimeout(() => sendMessage(messageToSend, 1), 1000);
    return;
  }
  
  // Mostrar mensaje de error personalizado
  const errorContent = isTimeout 
    ? "La petición tardó demasiado tiempo y fue cancelada automáticamente..."
    : "Lo siento, estoy teniendo problemas técnicos...";
}

📊 Flujo de Usuario

Usuario envía mensaje
    ↓
Petición al API inicia (timeout = 15s)
    ↓
¿Respuesta antes de 15s?
    ├─ SÍ → Mostrar respuesta ✅
    └─ NO → Cancelar petición
           ↓
           ¿Es el primer intento?
           ├─ SÍ → Mostrar toast "Reintentando..."
           │       Reintentar automáticamente (1 vez)
           │       ↓
           │       ¿Respuesta antes de 15s?
           │       ├─ SÍ → Mostrar respuesta ✅
           │       └─ NO → Mostrar mensaje de error ❌
           │
           └─ NO → Mostrar mensaje de error ❌

🎯 Beneficios

  1. Mejor UX: Los usuarios nunca quedan atascados indefinidamente
  2. Respuesta rápida: 15 segundos es un tiempo óptimo para mantener la atención del usuario
  3. Feedback claro: Mensajes progresivos informan al usuario del estado
  4. Resiliente: El sistema intenta recuperarse automáticamente
  5. Sin cambios en el backend: Solo cambios en el frontend
  6. Fácil de mantener: Usa APIs nativas del navegador (AbortController)
  7. UX moderna: Tiempos de espera competitivos con apps modernas (ChatGPT, Claude, etc.)

🔧 Configuración

Ajustar el tiempo de timeout

// En src/hooks/useChat.ts, línea ~183
const timeoutId = setTimeout(() => {
  controller.abort();
}, 15000); // 👈 Cambiar este valor (en milisegundos)
// 15000 = 15 segundos (actual - óptimo para UX)
// 10000 = 10 segundos (más agresivo)
// 20000 = 20 segundos (más tolerante)

Ajustar el número de reintentos

// En src/hooks/useChat.ts, línea ~401
if (isTimeout && retryCount === 0) { // 👈 Cambiar la condición
  // retryCount === 0: Solo 1 reintento
  // retryCount < 2: Hasta 2 reintentos
  // etc.
}

Personalizar mensajes del loader

Editar el array loadingStates en src/components/chatbot/DynamicLoader.tsx:

const loadingStates: LoadingState[] = [
  {
    message: "Tu mensaje personalizado...",
    icon: <Loader2 className="w-4 h-4 animate-spin" />,
    duration: 5 // segundos que dura este mensaje
  },
  // ... más estados
];

📝 Notas Técnicas

  • AbortController: API nativa del navegador para cancelar peticiones fetch
  • AbortError: Error específico que se lanza cuando se cancela una petición
  • Toast notifications: Usa la librería sonner para notificaciones
  • Estado del chat: Se preserva correctamente durante los reintentos

🚀 Próximas Mejoras

  1. Timeout adaptativo: Ajustar el timeout según la complejidad de la consulta
  2. Métricas: Registrar tiempos de respuesta promedio
  3. Indicador visual: Barra de progreso que muestre el tiempo transcurrido
  4. Configuración por usuario: Permitir que los usuarios ajusten el timeout

🐛 Troubleshooting

El timeout ocurre muy frecuentemente

  • Aumentar el valor de timeout (15000ms por defecto = 15 segundos)
  • Verificar la conexión del servidor
  • Revisar logs del API para identificar cuellos de botella
  • Considerar si el modelo de IA necesita optimización

Los reintentos no funcionan

  • Verificar que retryCount se pase correctamente
  • Revisar logs de consola para errores
  • Asegurar que el estado se limpie correctamente antes del reintento

El usuario ve el mensaje de timeout antes de tiempo

  • Los mensajes del loader están configurados para 20 segundos totales
  • El timeout está en 15 segundos para el primer intento
  • Con el reintento, el tiempo máximo total es ~31 segundos (15s + 1s delay + 15s)
  • Esto es ideal para mantener una UX ágil sin frustrar al usuario

📚 Referencias