|
@@ -0,0 +1,187 @@
|
|
|
|
|
+# 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
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 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
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+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
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 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
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 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`:
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+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
|
|
|
|
|
+
|
|
|
|
|
+- [MDN - AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController)
|
|
|
|
|
+- [MDN - Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)
|
|
|
|
|
+- [Sonner Toast](https://sonner.emilkowal.ski/)
|