import { useState, useEffect, useCallback } from "react"; import { notifications } from "@/lib/notifications"; import { generateReportFromMessages } from "@/utils/reports"; import { Message, ChatState, ChatResponse, SuggestedPrompt, MedicalAlert } from "@/components/chatbot/types"; export const MAX_MESSAGES = 5; interface UseChatProps { chatType: "medical" | "psychological"; } export const useChat = ({ chatType }: UseChatProps) => { const [messages, setMessages] = useState([]); const [messageCount, setMessageCount] = useState(0); const [isLimitReached, setIsLimitReached] = useState(false); const [isGeneratingReport, setIsGeneratingReport] = useState(false); const [generatedReport, setGeneratedReport] = useState(""); const [inputMessage, setInputMessage] = useState(""); const [isLoading, setIsLoading] = useState(false); const [reportGenerated, setReportGenerated] = useState(false); const [showSuggestions, setShowSuggestions] = useState(true); const [showCompletedBanner, setShowCompletedBanner] = useState(false); const [currentSuggestions, setCurrentSuggestions] = useState([]); const [showDynamicSuggestions, setShowDynamicSuggestions] = useState(false); const [inputDisabledForSuggestions, setInputDisabledForSuggestions] = useState(false); const [medicalAlertDetected, setMedicalAlertDetected] = useState(null); const [isSchedulingFromAlert, setIsSchedulingFromAlert] = useState(false); const [showMedicalAlertBanner, setShowMedicalAlertBanner] = useState(false); const [crisisDetected, setCrisisDetected] = useState(false); const [showCrisisBanner, setShowCrisisBanner] = useState(false); const isPsychological = chatType === "psychological"; const remainingMessages = isPsychological ? Infinity : Math.max(0, MAX_MESSAGES - messageCount); const isLastMessage = !isPsychological && remainingMessages === 1; // Cargar estado desde localStorage al iniciar useEffect(() => { const storageKey = `chatState_${chatType}`; const savedState = localStorage.getItem(storageKey); if (savedState) { try { const parsed: ChatState = JSON.parse(savedState); // Convertir timestamps de string a Date const messagesWithDates = (parsed.messages || []).map(msg => ({ ...msg, timestamp: msg.timestamp ? new Date(msg.timestamp) : undefined })); setMessages(messagesWithDates); setMessageCount(parsed.messageCount || 0); setIsLimitReached(parsed.isLimitReached || false); setReportGenerated(parsed.reportGenerated || false); setCurrentSuggestions(parsed.currentSuggestions || []); // Si hay mensajes guardados, ocultar sugerencias iniciales if (messagesWithDates && messagesWithDates.length > 0) { setShowSuggestions(false); // Mostrar sugerencias dinámicas si el último mensaje es del asistente y no está en streaming const lastMessage = messagesWithDates[messagesWithDates.length - 1]; if (lastMessage?.role === "assistant" && !lastMessage.isStreaming && lastMessage.suggestions) { setCurrentSuggestions(lastMessage.suggestions); setShowDynamicSuggestions(true); } } else { // Si no hay mensajes, asegurarse de que las sugerencias estén visibles setShowSuggestions(true); } } catch (error) { console.error("Error loading chat state:", error); // En caso de error, asegurar que las sugerencias estén visibles setShowSuggestions(true); } } }, [chatType]); // Guardar estado en localStorage cuando cambie useEffect(() => { const storageKey = `chatState_${chatType}`; const stateToSave: ChatState = { messages, messageCount, isLimitReached, reportGenerated, currentSuggestions, }; localStorage.setItem(storageKey, JSON.stringify(stateToSave)); }, [messages, messageCount, isLimitReached, reportGenerated, currentSuggestions, chatType]); // Función para generar reporte usando useCallback para evitar re-renderizados const generateMedicalReport = useCallback(async () => { if (isGeneratingReport || reportGenerated) return; setIsGeneratingReport(true); try { // Mostrar toast de generación notifications.records.generating(); // Generar reporte const report = generateReportFromMessages(messages); setGeneratedReport(report); setReportGenerated(true); // Guardar en la base de datos const response = await fetch("/api/chat/report", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ content: report, messages: messages, chatType, }), }); if (response.ok) { notifications.records.saved(); } else { notifications.records.saveError(); } } catch (error) { console.error("Error generando reporte:", error); notifications.records.generateError(); } finally { setIsGeneratingReport(false); } }, [messages, isGeneratingReport, reportGenerated]); // Efecto para generar reporte cuando se alcanza el límite (solo para chat médico) useEffect(() => { if ( !isPsychological && messageCount >= MAX_MESSAGES && !isLimitReached && !reportGenerated && !isLoading ) { // Solo generar reporte si el último mensaje es del asistente y no está en streaming if ( messages.length > 0 && messages[messages.length - 1].role === "assistant" && !messages[messages.length - 1].isStreaming ) { // Esperar un poco antes de mostrar el banner para que el usuario vea la respuesta setTimeout(() => { setShowCompletedBanner(true); generateMedicalReport(); }, 2000); // 2 segundos de delay } } }, [ isPsychological, messageCount, isLimitReached, reportGenerated, generateMedicalReport, messages, isLoading, ]); const sendMessage = async (customMessage?: string) => { const messageToSend = customMessage || inputMessage.trim(); if (!messageToSend || isLimitReached || isLoading) return; console.log("🚀 [CHAT] Iniciando envío de mensaje:", messageToSend); console.log("📊 [CHAT] Estado actual - Mensajes:", messageCount, "Límite alcanzado:", isLimitReached); const userMessage: Message = { role: "user", content: messageToSend, timestamp: new Date(), }; setMessages((prev) => [...prev, userMessage]); setMessageCount((prev) => prev + 1); setInputMessage(""); setIsLoading(true); setShowSuggestions(false); // Ocultar sugerencias iniciales después del primer mensaje setShowDynamicSuggestions(false); // Ocultar sugerencias dinámicas cuando el usuario envía un mensaje setInputDisabledForSuggestions(false); // Habilitar input inmediatamente cuando el usuario envía un mensaje const startTime = Date.now(); console.log("⏱️ [CHAT] Iniciando petición a API en:", new Date().toISOString()); try { // Llamada a OpenRouter API con STREAMING console.log("📡 [CHAT] Enviando petición a /api/chat..."); const response = await fetch("/api/chat", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ message: messageToSend, messages: [...messages, userMessage], chatType, }), }); const responseTime = Date.now() - startTime; console.log("✅ [CHAT] Conexión establecida en:", responseTime, "ms"); console.log("📋 [CHAT] Status de respuesta:", response.status, response.statusText); if (response.ok) { console.log("🔄 [CHAT] Procesando stream en tiempo real..."); // Crear mensaje del asistente vacío para ir llenándolo const assistantMessage: Message = { role: "assistant", content: "", timestamp: new Date(), isStreaming: true, }; setMessages((prev) => [...prev, assistantMessage]); console.log("🎬 [CHAT] Iniciando recepción de stream..."); // Procesar el stream const reader = response.body?.getReader(); const decoder = new TextDecoder(); let buffer = ""; let fullContent = ""; let metadata: { medicalAlert?: string; suggestions?: SuggestedPrompt[]; crisisDetected?: boolean } = {}; if (!reader) { throw new Error("No se pudo obtener el reader del stream"); } try { while (true) { const { done, value } = await reader.read(); if (done) { console.log("✅ [CHAT] Stream completado"); break; } // Decodificar el chunk buffer += decoder.decode(value, { stream: true }); // Procesar eventos SSE (formato: data: {json}\n\n) const lines = buffer.split('\n\n'); buffer = lines.pop() || ""; // Guardar la última línea incompleta for (const line of lines) { if (line.startsWith('data: ')) { const data = line.slice(6); // Remover "data: " if (data === '[DONE]') { console.log("� [CHAT] Recibida señal de finalización"); break; } try { const parsed = JSON.parse(data); if (parsed.type === 'content') { // Acumular contenido y actualizar en tiempo real fullContent += parsed.content; setMessages((prev) => prev.map((msg, index) => index === prev.length - 1 && msg.role === "assistant" ? { ...msg, content: fullContent } : msg ) ); } else if (parsed.type === 'metadata') { // Guardar metadatos metadata = { medicalAlert: parsed.medicalAlert, suggestions: parsed.suggestions, crisisDetected: parsed.crisisDetected }; console.log("📦 [CHAT] Metadatos recibidos:", metadata); } } catch (parseError) { console.warn("⚠️ [CHAT] Error parseando evento SSE:", parseError); } } } } } finally { reader.releaseLock(); } const totalTime = Date.now() - startTime; console.log("✅ [CHAT] Respuesta completa procesada en:", totalTime, "ms"); // Detectar crisis en chat psicológico if (isPsychological && metadata.crisisDetected) { console.log("🚨 [CHAT] Crisis detectada en chat psicológico"); setCrisisDetected(true); setShowCrisisBanner(true); } // Detectar alerta médica if (metadata.medicalAlert && metadata.medicalAlert !== "NO_AGENDAR" && !medicalAlertDetected) { console.log("🚨 [CHAT] Alerta médica detectada:", metadata.medicalAlert); setMedicalAlertDetected(metadata.medicalAlert as MedicalAlert); setShowMedicalAlertBanner(true); } // Marcar mensaje como completado y agregar metadatos setMessages((prev) => prev.map((msg, index) => index === prev.length - 1 && msg.role === "assistant" ? { ...msg, isStreaming: false, medicalAlert: metadata.medicalAlert as MedicalAlert | undefined, suggestions: metadata.suggestions } : msg ) ); // Mostrar sugerencias dinámicas después del streaming si no es el último mensaje // En chat psicológico no mostrar sugerencias dinámicas if (!isPsychological && messageCount + 1 < MAX_MESSAGES && metadata.suggestions) { setCurrentSuggestions(metadata.suggestions); setShowDynamicSuggestions(true); setInputDisabledForSuggestions(true); // Habilitar el input después de 3 segundos para dar tiempo a leer las sugerencias setTimeout(() => { setInputDisabledForSuggestions(false); }, 3000); } } else { console.log("❌ [CHAT] Error en respuesta de API - Status:", response.status); const errorText = await response.text(); console.log("📄 [CHAT] Contenido del error:", errorText); // Respuesta de fallback si la API falla const fallbackMessage: Message = { role: "assistant", content: `Gracias por tu consulta sobre "${messageToSend}". Como asistente médico virtual, puedo ayudarte con información general sobre salud, pero recuerda que no puedo reemplazar la consulta con un profesional médico. ¿Hay algo específico que te gustaría saber?`, timestamp: new Date(), medicalAlert: "NO_AGENDAR", suggestions: [ { title: "Síntomas de Gripe", emoji: "🤒", prompt: "¿Cuáles son los síntomas más comunes de la gripe y cómo diferenciarla de un resfriado?" }, { title: "Fortalecer Inmunidad", emoji: "🛡️", prompt: "¿Qué alimentos y hábitos me ayudan a fortalecer mi sistema inmunológico?" }, { title: "Cuándo Consultar", emoji: "🩺", prompt: "¿Cuándo debo consultar a un médico por síntomas de gripe o resfriado?" } ], }; setMessages((prev) => [...prev, fallbackMessage]); // Mostrar sugerencias dinámicas si no es el último mensaje // En chat psicológico no mostrar sugerencias dinámicas if (!isPsychological && messageCount + 1 < MAX_MESSAGES) { setCurrentSuggestions(fallbackMessage.suggestions || []); setShowDynamicSuggestions(true); setInputDisabledForSuggestions(true); // Habilitar el input después de 3 segundos setTimeout(() => { setInputDisabledForSuggestions(false); }, 3000); } } } catch (error) { const totalTime = Date.now() - startTime; 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] 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' }); // 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.", timestamp: new Date(), medicalAlert: "NO_AGENDAR", suggestions: [ { title: "Reintentar Consulta", emoji: "🔄", prompt: "¿Puedes intentar responder mi pregunta anterior sobre salud?" }, { title: "Síntomas Comunes", emoji: "🩺", prompt: "¿Cuáles son los síntomas más comunes que debo vigilar en mi salud?" }, { title: "Emergencias Médicas", emoji: "🚨", prompt: "¿Cuándo debo considerar que un síntoma es una emergencia médica?" } ], }; setMessages((prev) => [...prev, fallbackMessage]); // Mostrar sugerencias dinámicas si no es el último mensaje // En chat psicológico no mostrar sugerencias dinámicas if (!isPsychological && messageCount + 1 < MAX_MESSAGES) { setCurrentSuggestions(fallbackMessage.suggestions || []); setShowDynamicSuggestions(true); setInputDisabledForSuggestions(true); // Habilitar el input después de 3 segundos setTimeout(() => { setInputDisabledForSuggestions(false); }, 3000); } } finally { const totalTime = Date.now() - startTime; console.log("🏁 [CHAT] Procesamiento completado en:", totalTime, "ms"); setIsLoading(false); } }; const resetChat = () => { setMessages([]); setMessageCount(0); setIsLimitReached(false); setIsGeneratingReport(false); setGeneratedReport(""); setInputMessage(""); setReportGenerated(false); setShowSuggestions(true); // Mostrar sugerencias iniciales al resetear setShowCompletedBanner(false); setCurrentSuggestions([]); setShowDynamicSuggestions(false); setInputDisabledForSuggestions(false); setMedicalAlertDetected(null); setShowMedicalAlertBanner(false); setIsSchedulingFromAlert(false); setCrisisDetected(false); setShowCrisisBanner(false); // Limpiar localStorage const storageKey = `chatState_${chatType}`; localStorage.removeItem(storageKey); notifications.chat.newConsultation(); }; const dismissCompletedBanner = () => { setShowCompletedBanner(false); }; const handleResetWithReport = async () => { if (messages.length === 0) { resetChat(); return; } try { // Generar reporte con la conversación actual const currentReport = generateReportFromMessages(messages); // Guardar el reporte en la base de datos const response = await fetch("/api/chat/report", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ content: currentReport, messages: messages, }), }); if (response.ok) { notifications.records.savedNewConsultation(); resetChat(); } else { notifications.records.saveError(); } } catch (error) { console.error("Error al generar reporte:", error); notifications.records.generateError(); } }; const handleScheduleFromAlert = async (onSuccess: (recordId: string) => void) => { console.log("🚀 [useChat] handleScheduleFromAlert iniciado"); setIsSchedulingFromAlert(true); try { // Generar reporte con la conversación actual const currentReport = generateReportFromMessages(messages); console.log("📝 [useChat] Reporte generado, longitud:", currentReport.length); // Guardar el reporte en la base de datos const response = await fetch("/api/chat/report", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ content: currentReport, messages: messages, }), }); if (response.ok) { const data = await response.json(); const recordId = data.id; // ID del Record creado console.log("✅ [useChat] Record creado exitosamente, ID:", recordId); // Callback con el recordId para abrir el modal de cita onSuccess(recordId); notifications.records.saved(); } else { console.error("❌ [useChat] Error al guardar reporte:", response.status); notifications.records.saveError(); } } catch (error) { console.error("❌ [useChat] Error al generar reporte para cita:", error); notifications.records.generateError(); } finally { setIsSchedulingFromAlert(false); } }; const dismissMedicalAlertBanner = () => { setShowMedicalAlertBanner(false); }; const dismissCrisisBanner = () => { setShowCrisisBanner(false); }; return { messages, messageCount, isLimitReached, isGeneratingReport, generatedReport, inputMessage, setInputMessage, isLoading, reportGenerated, showSuggestions, showCompletedBanner, remainingMessages, isLastMessage, sendMessage, resetChat, handleResetWithReport, dismissCompletedBanner, setGeneratedReport, currentSuggestions, showDynamicSuggestions, setShowDynamicSuggestions, inputDisabledForSuggestions, setInputDisabledForSuggestions, medicalAlertDetected, showMedicalAlertBanner, isSchedulingFromAlert, handleScheduleFromAlert, dismissMedicalAlertBanner, crisisDetected, showCrisisBanner, dismissCrisisBanner, }; };;