| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572 |
- 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<Message[]>([]);
- 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<SuggestedPrompt[]>([]);
- const [showDynamicSuggestions, setShowDynamicSuggestions] = useState(false);
- const [inputDisabledForSuggestions, setInputDisabledForSuggestions] = useState(false);
- const [medicalAlertDetected, setMedicalAlertDetected] = useState<MedicalAlert | null>(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,
- };
- };;
|