// src/components/Chatbot.tsx import React, { useState, useEffect, useRef } from 'react'; import ReactMarkdown from 'react-markdown'; import rehypeSanitize from 'rehype-sanitize'; import './Chatbot.css'; interface Message { text: string; sender: 'user' | 'bot'; } const MAX_CHARS = 200; // Constante para la clave del localStorage const CHAT_HISTORY_KEY = 'chatbot-history'; const Chatbot: React.FC = () => { const [isOpen, setIsOpen] = useState(false); const [messages, setMessages] = useState(() => { try { // 1. Intentamos obtener el historial guardado. const savedHistory = localStorage.getItem(CHAT_HISTORY_KEY); if (savedHistory) { // 2. Si existe, lo parseamos y lo devolvemos para usarlo como estado inicial. return JSON.parse(savedHistory); } } catch (error) { console.error("No se pudo cargar el historial del chat desde localStorage:", error); } // 3. Si no hay nada guardado o hay un error, devolvemos el estado por defecto. return [{ text: '¡Hola! Soy tu asistente virtual. ¿En qué puedo ayudarte hoy?', sender: 'bot' }]; }); const [inputValue, setInputValue] = useState(''); const [isLoading, setIsLoading] = useState(false); const messagesEndRef = useRef(null); // Añadimos un useEffect para guardar los mensajes. useEffect(() => { try { // Cada vez que el array de 'messages' cambie, lo guardamos en localStorage. localStorage.setItem(CHAT_HISTORY_KEY, JSON.stringify(messages)); } catch (error) { console.error("No se pudo guardar el historial del chat en localStorage:", error); } }, [messages]); useEffect(() => { // Solo intentamos hacer scroll si la ventana del chat está abierta. if (isOpen) { // Usamos un pequeño retardo para asegurar que el navegador haya renderizado // completamente la ventana antes de intentar hacer el scroll. // Esto previene problemas si hay animaciones CSS. setTimeout(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, 50); } }, [messages, isOpen]); const toggleChat = () => setIsOpen(!isOpen); const handleInputChange = (event: React.ChangeEvent) => { setInputValue(event.target.value); }; const handleSendMessage = async (event: React.FormEvent) => { event.preventDefault(); if (inputValue.trim() === '' || isLoading) return; const userMessage: Message = { text: inputValue, sender: 'user' }; setMessages(prev => [...prev, userMessage]); const messageToSend = inputValue; setInputValue(''); setIsLoading(true); const botMessagePlaceholder: Message = { text: '', sender: 'bot' }; setMessages(prev => [...prev, botMessagePlaceholder]); try { const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/chat/stream-message`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: messageToSend }), }); if (!response.ok || !response.body) { throw new Error('Error en la respuesta del servidor.'); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let accumulatedResponse = ''; // Variable para acumular el texto crudo const readStream = async () => { while (true) { const { done, value } = await reader.read(); if (done) { // El stream ha terminado, no hacemos nada más aquí. break; } // Acumulamos la respuesta cruda que viene del backend accumulatedResponse += decoder.decode(value); try { // Intentamos limpiar la respuesta acumulada // 1. Parseamos como si fuera un array JSON const parsedArray = JSON.parse(accumulatedResponse // Añadimos un corchete de cierre por si el stream se corta a la mitad .replace(/,$/, '') + ']'); // 2. Unimos los fragmentos del array en un solo texto const cleanText = Array.isArray(parsedArray) ? parsedArray.join('') : accumulatedResponse; // 3. Actualizamos el estado con el texto limpio setMessages(prev => { const lastMessage = prev[prev.length - 1]; const updatedLastMessage = { ...lastMessage, text: cleanText }; return [...prev.slice(0, -1), updatedLastMessage]; }); } catch (e) { // Si hay un error de parseo (porque el JSON aún no está completo), // mostramos el texto sin los caracteres iniciales/finales. const partiallyCleanedText = accumulatedResponse .replace(/^\[?"|"?,"?|"?\]$/g, '') .replace(/","/g, ''); setMessages(prev => { const lastMessage = prev[prev.length - 1]; const updatedLastMessage = { ...lastMessage, text: partiallyCleanedText }; return [...prev.slice(0, -1), updatedLastMessage]; }); } } }; await readStream(); } catch (error) { console.error("Error al conectar con la API de streaming:", error); const errorText = error instanceof Error ? error.message : 'Lo siento, no pude conectarme.'; setMessages(prev => { const lastMessage = prev[prev.length - 1]; const updatedLastMessage = { ...lastMessage, text: errorText }; return [...prev.slice(0, -1), updatedLastMessage]; }); } finally { setIsLoading(false); } }; return ( <>
{isOpen ? 'X' : '💬'}
{isOpen && (
Asistente Virtual - El Día
{messages.map((msg, index) => (
{msg.text.replace(/\\n/g, "\n")}
))}
{inputValue.length} / {MAX_CHARS}
)} ); }; export default Chatbot;