Ver Fonte

why is the api not answering wth

Matthew Trejo há 1 mês atrás
pai
commit
289dfe6aca

+ 187 - 0
docs/CHAT_TIMEOUT_IMPLEMENTATION.md

@@ -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/)

+ 6 - 21
src/components/chatbot/DynamicLoader.tsx

@@ -11,37 +11,22 @@ const loadingStates: LoadingState[] = [
   {
     message: "Asistente escribiendo...",
     icon: <Loader2 className="w-4 h-4 animate-spin" />,
-    duration: 5
+    duration: 4
   },
   {
     message: "Analizando tu consulta...",
     icon: <Bot className="w-4 h-4 animate-pulse" />,
-    duration: 5
+    duration: 4
   },
   {
-    message: "Estoy pensando en la mejor respuesta...",
-    icon: <Clock className="w-4 h-4 animate-bounce" />,
-    duration: 5
-  },
-  {
-    message: "Revisando información médica...",
+    message: "Preparando tu respuesta...",
     icon: <Wifi className="w-4 h-4 animate-pulse" />,
-    duration: 5
-  },
-  {
-    message: "Preparando recomendaciones personalizadas...",
-    icon: <Bot className="w-4 h-4 animate-spin" />,
-    duration: 5
+    duration: 4
   },
   {
-    message: "Casi listo, organizando la información...",
+    message: "Casi listo...",
     icon: <Coffee className="w-4 h-4 animate-bounce" />,
-    duration: 10
-  },
-  {
-    message: "Gracias por tu paciencia, estoy finalizando tu respuesta...",
-    icon: <Bot className="w-4 h-4 animate-pulse" />,
-    duration: 15
+    duration: 8
   }
 ];
 

+ 50 - 4
src/hooks/useChat.ts

@@ -1,5 +1,6 @@
 import { useState, useEffect, useCallback } from "react";
 import { notifications } from "@/lib/notifications";
+import { toast } from "sonner";
 import { generateReportFromMessages } from "@/utils/reports";
 import { Message, ChatState, ChatResponse, SuggestedPrompt, MedicalAlert } from "@/components/chatbot/types";
 
@@ -157,7 +158,7 @@ export const useChat = ({ chatType }: UseChatProps) => {
     isLoading,
   ]);
 
-  const sendMessage = async (customMessage?: string) => {
+  const sendMessage = async (customMessage?: string, retryCount = 0) => {
     const messageToSend = customMessage || inputMessage.trim();
     if (!messageToSend || isLimitReached || isLoading) return;
 
@@ -180,6 +181,13 @@ export const useChat = ({ chatType }: UseChatProps) => {
     const startTime = Date.now();
     console.log("⏱️ [CHAT] Iniciando petición a API en:", new Date().toISOString());
 
+    // 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
+
     try {
       // Llamada a OpenRouter API con STREAMING
       console.log("📡 [CHAT] Enviando petición a /api/chat...");
@@ -193,8 +201,12 @@ export const useChat = ({ chatType }: UseChatProps) => {
           messages: [...messages, userMessage],
           chatType,
         }),
+        signal: controller.signal, // Agregar señal de cancelación
       });
       
+      // Limpiar timeout si la petición fue exitosa
+      clearTimeout(timeoutId);
+      
       const responseTime = Date.now() - startTime;
       console.log("✅ [CHAT] Conexión establecida en:", responseTime, "ms");
       console.log("📋 [CHAT] Status de respuesta:", response.status, response.statusText);
@@ -370,19 +382,53 @@ export const useChat = ({ chatType }: UseChatProps) => {
       }
     } catch (error) {
       const totalTime = Date.now() - startTime;
+      
+      // Limpiar timeout
+      clearTimeout(timeoutId);
+      
+      // Detectar si el error fue por timeout
+      const isTimeout = error instanceof Error && error.name === 'AbortError';
+      
       console.error("💥 [CHAT] Error enviando mensaje después de", totalTime, "ms:", error);
-      console.error("🔍 [CHAT] Tipo de error:", error instanceof TypeError ? 'Network/Fetch Error' : 'Other Error');
+      console.error("🔍 [CHAT] Tipo de error:", error instanceof TypeError ? 'Network/Fetch Error' : isTimeout ? 'Timeout Error' : 'Other Error');
       console.error("📋 [CHAT] Detalles del error:", {
         name: error instanceof Error ? error.name : 'Unknown',
         message: error instanceof Error ? error.message : String(error),
         stack: error instanceof Error ? error.stack : 'No stack trace'
       });
       
+      // Si fue timeout y es el primer intento, reintentar automáticamente
+      if (isTimeout && retryCount === 0) {
+        console.log("🔄 [CHAT] Timeout detectado, reintentando automáticamente...");
+        
+        // Remover el mensaje del usuario anterior antes de reintentar
+        setMessages((prev) => prev.slice(0, -1));
+        setMessageCount((prev) => prev - 1);
+        setIsLoading(false);
+        
+        // Notificar al usuario
+        toast.info("Reintentando...", {
+          description: "La petición tardó demasiado, reintentando automáticamente.",
+        });
+        
+        // Reintentar después de un pequeño delay
+        setTimeout(() => {
+          sendMessage(messageToSend, 1);
+        }, 1000);
+        
+        return;
+      }
+      
+      // Mensaje personalizado según el tipo de error
+      let errorContent = "Lo siento, estoy teniendo problemas técnicos. Por favor, intenta de nuevo en unos momentos.";
+      if (isTimeout) {
+        errorContent = "La petición tardó demasiado tiempo y fue cancelada automáticamente. Por favor, intenta enviar tu mensaje nuevamente.";
+      }
+      
       // Respuesta de fallback en caso de error
       const fallbackMessage: Message = {
         role: "assistant",
-        content:
-          "Lo siento, estoy teniendo problemas técnicos. Por favor, intenta de nuevo en unos momentos.",
+        content: errorContent,
         timestamp: new Date(),
         medicalAlert: "NO_AGENDAR",
         suggestions: [