Crea una interfaz de usuario en React para un chatbot con IA
Una interfaz interactiva en React para chatbots con streaming en tiempo real, respuestas en markdown y scroll automático
13 de septiembre de 2024
En el último artículo, "Crea un chatbot con IA alimentado por tus datos", exploramos cómo implementar Retrieval-Augmented Generation (RAG) para crear un chatbot con IA listo para producción que pudiera responder preguntas sobre tendencias tecnológicas, a partir de los últimos informes de instituciones como McKinsey, Deloitte, el Banco Mundial, el Foro Económico Mundial y la OCDE.
Sin embargo, quedaba pendiente una parte fundamental para completar esta aplicación full stack: la interfaz de usuario. En este artículo, vamos a crear un frontend en React para el chatbot de tendencias tecnológicas.
Este frontend está diseñado para ser flexible y fácil de adaptar a cualquier otro chatbot con IA, como por ejemplo un bot de atención al cliente o un asistente de programación. Solo necesitas conectarlo a una API que proporcione dos endpoints específicos para el chat (o adaptarlo a tus propios endpoints).
El frontend será una aplicación React sencilla, sin frameworks adicionales, utilizando Tailwind CSS para los estilos y Vite como herramienta de compilación (build tool). Al construir el frontend desde cero, verás que crear una interfaz de chatbot funcional requiere poco código y, además, es bastante sencillo
La implementación incluirá características como streaming de respuestas con Server-Sent Events, respuestas en formato markdown, diseño responsive optimizado para móviles y gestión de errores durante las interacciones. Y si quieres ampliarlo, puedes añadir funcionalidades más avanzadas como autenticación, historial de chats e incluso compartir conversaciones, para construir tu propia aplicación al estilo ChatGPT, totalmente adaptada a tus necesidades.
Al terminar este artículo, tendrás una interfaz de chatbot con IA totalmente funcional y personalizable con este aspecto:
Puedes acceder a una demo del chatbot aquí. Y el código completo de este proyecto (frontend y backend) está disponible en este repositorio de GitHub.
#Estructura del frontend
Esta es la estructura principal del frontend:
#Componente principal del chatbot
El componente principal de la aplicación frontend es el componente Chatbot
. Contiene el estado central de la aplicación y renderiza los subcomponentes necesarios. Esta es una versión simplificada del código del componente:
⚠️En general, nos vamos a centrar en la estructura y la lógica principal de los componentes. Para mantener los fragmentos de código más concisos y legibles, omitiremos las clases de Tailwind CSS, ya que no son cruciales para entender la funcionalidad del chatbot. Pero puedes consultar el código completo en el repositorio de GitHub.
Como puedes ver al inicio del componente, la aplicación tiene tres variables de estado principales:
chatId
: ID de la sesión de chat actual.messages
: Los mensajes del chat. Cada mensaje tiene las propiedadesrole
("user" o "assistant"),content
,loading
yerror
.newMessage
: El texto actual en el input del chat (antes de enviarse).
Observa también cómo estamos usando tanto useState
como useImmer
para la gestión del estado. Si tienes experiencia con React, sabrás que el estado nunca debe actualizarse directamente y todas las actualizaciones de estado deben realizarse de forma inmutable (creando un nuevo objeto o modificando una copia profunda del objeto). Esto puede hacer que el código sea más verboso y propenso a errores, sobre todo en estructuras de datos anidadas como el array messages
.
Immer es una librería ligera y muy práctica que simplifica las actualizaciones de estado mediante el hook useImmer
. Immer simplifica el código enormemente permitiéndote actualizar directamente un objeto temporal, y se ocupa de crear el siguiente estado de forma inmutable. Por ejemplo, puedes actualizar el último objeto en el array messages
directamente así:
La estructura JSX del componente Chatbot
es sencilla. Renderiza tres elementos:
- Un mensaje inicial de bienvenida (que se muestra si no hay mensajes).
- El componente
ChatMessages
para mostrar la conversación. - El componente
ChatInput
que permite al usuario escribir nuevos mensajes.
#Envío de nuevos mensajes y procesamiento de la respuesta
La funcionalidad principal del chatbot está en la función submitNewMessage
:
Veamos qué es lo que hace:
- Comprobamos que el mensaje no esté vacío y que no se esté procesando ya otra respuesta antes de continuar.
- Añadimos el mensaje del usuario al chat y creamos un mensaje provisional del asistente con la propiedad
loading
con valortrue
(útil para mostrar un spinner mientras carga). - Si no existe una sesión de chat, creamos una nueva usando la función
api.createChat
. - Luego usamos
api.sendChatMessage
para enviar el mensaje del usuario al backend, que devuelve un stream como respuesta. - Usamos la función
parseSSEStream
para convertir el stream de Server-Sent Events (SSE) en un iterador asíncrono de fragmentos de texto. Por cada nuevo fragmento de texto recibido, actualizamos el mensaje del asistente, creando el efecto de streaming en tiempo real. - Una vez que termina el streaming de la respuesta, actualizamos la propiedad
loading
del mensaje del asistente con valorfalse
. - Si hay algún error en el proceso, actualizamos la propiedad
error
del mensaje del asistente con valortrue
para mostrar un mensaje de error en la interfaz del chat.
El archivo api.js
contiene las dos funciones (createChat
y sendChatMessage
) que interactúan con los endpoints de la API del backend:
Como puedes ver en el código, ambas usan la API Fetch nativa del navegador. La función createChat
devuelve una respuesta JSON con el nuevo ID de chat, mientras que sendChatMessage
devuelve el body directamente ya que es una respuesta en streaming que necesitamos procesar de forma diferente.
Finalmente, echemos un vistazo a la función que usamos para procesar el stream SSE, aprovechando la librería eventsource-parser para simplificar la extracción de datos de los eventos:
La función aplica dos transformaciones al stream de entrada: TextDecoderStream()
convierte los bytes entrantes en texto, y EventSourceParserStream()
procesa los eventos SSE. Luego itera sobre los eventos y emite (con yield) los datos de cada evento (cada uno contiene un fragmento de texto de la respuesta del asistente).
Observa que la función es un generador asíncrono, por eso podemos iterar sobre los fragmentos de texto con un simple bucle for await...of
en la función submitNewMessage
.
#Visualización de los mensajes del chat
El componente ChatMessages
se encarga de renderizar el historial de mensajes. Echemos un vistazo a una versión simplificada del código (sin las clases de CSS):
Este componente implementa diferentes visualizaciones según el tipo de mensaje:
- Los mensajes del usuario se muestran con un icono de usuario.
- Los mensajes del asistente se renderizan usando el componente
Markdown
de la librería react-markdown. Esto es muy útil porque las respuestas de los modelos de lenguaje (LLM) a menudo vienen en formato Markdown, con texto enriquecido, párrafos, listas y otros elementos. - Cuando un mensaje del asistente está cargando y aún no tiene contenido, se muestra el componente
Spinner
. - Si hay algún error al procesar una respuesta del asistente, se muestra un icono de error y un mensaje debajo.
Para mejorar la experiencia de usuario, también implementamos scroll automático cuando se envían nuevos mensajes del asistente mediante un hook personalizado useAutoScroll
. Si tienes curiosidad por los detalles, puedes ver el código completo aquí. De forma resumida, así es cómo funciona el hook de auto-scrolling:
- Define y devuelve una referencia (ref)
scrollContentRef
que conectamos al elemento contenedor de los mensajes. Con esta ref y la API Resize Observer, podemos detectar cuándo el contenedor de mensajes cambia de tamaño (al recibir nuevos mensajes) y hacer scroll automático hasta el final. - Incluye un mecanismo para desactivar temporalmente el auto-scroll si el usuario hace scroll manual hacia arriba mientras se está generando la respuesta, de modo que pueda leer el historial sin interrupciones.
- El auto-scroll se reactiva cuando el usuario vuelve a hacer scroll hasta el final, o cuando el asistente comienza a generar un nuevo mensaje.
- Un detalle importante es que este hook asume que el documento completo (elemento HTML) es el elemento con la barra scroll, y utiliza por tanto
document.documentElement
para las mediciones y operaciones de scroll. En caso de usar un elemento distinto para la barra de scroll (por ejemplo, undiv
conoverflow: scroll
), deberás adaptar el hook para que use ese elemento en lugar del documento.
#Interfaz para escribir mensajes
La pieza final del frontend de chatbot es el componente ChatInput
, que permite a los usuarios escribir y enviar sus mensajes. Esta es una versión simplificada del código:
ChatInput
incluye un elemento textarea
para escribir los mensajes y un botón de envío. Este elemento textarea
incluye también una funcionalidad de redimensionado automático mediante el hook useAutosize
, que ajusta el tamaño del elemento de texto según su contenido. Puedes ver el código del hook aquí.
El componente también incluye la función handleKeyDown
, que permite enviar mensajes con la tecla Enter (sin Shift). Al mismo tiempo, preserva el comportamiento nativo del textarea
para añadir saltos de línea con Shift+Enter, permitiendo al usuario formatear adecuadamente mensajes más largos.
Con esto, damos por finalizada la implementación del chatbot con IA full stack. Puedes usar este proyecto como base y personalizarlo según tus necesidades.
Todas las técnicas que hemos cubierto en este artículo y el anterior - Retrieval-Augmented Generation, bases de datos vectoriales, búsqueda semántica, programación asíncrona, respuestas estructuradas, streaming en tiempo real con SSE, renderizado de markdown, scroll automático - son las piezas fundamentales para que puedas crear tus propios chatbots con IA.
¡Espero que te haya resultado útil!