Inicio / Artículos

Diseñando una interfaz RAG transparente con React, Tailwind y Motion

Construir una interfaz frontal RAG que muestre las etapas de recuperación, citas interactivas, un panel de fuentes y acciones posteriores a la respuesta, utilizando React State, Tailwind CSS y Framer Motion.

Editor: REACTAPP.TOP11 min de lectura2544 palabras

Fuentes: How I’d Build a Modern RAG Interface with React, Tailwind CSS & Framer Motion

Un pipeline de generación mejorada mediante recuperación de información puede ser técnicamente excelente y, aun así, parecer un producto deficiente cuando la interfaz oculta lo que está haciendo el sistema. Un único cuadro de diálogo con un indicador de carga no le informa al usuario sobre qué documentos se buscaron, de dónde proviene la respuesta ni si aún se está generando. Esta guía muestra cómo estructurar una interfaz frontend en React alrededor de las etapas reales de una solicitud RAG, de modo que la recuperación de información, las fuentes, el flujo en tiempo real y las citaciones se conviertan en partes visibles e inspeccionables de la experiencia.

Diseño basado en las etapas de recuperación

El cambio clave consiste en tratar el producto como una secuencia que el usuario puede seguir, en lugar de como un “diálogo”: pregunta, recuperación de información, fuentes, generación, citaciones, exploración. Esa secuencia determina cómo se organiza la aplicación React.

En el caso de un asistente interno típico de conocimiento, la pantalla puede dividirse en tres áreas: una biblioteca de documentos a la izquierda, la conversación en el centro y las fuentes de la respuesta actual a la derecha.

---------------------------------------------------------
| Documents |          Conversation          | Sources |
|           |                                |         |
| SOPs      | User: How does onboarding work?| SOP.pdf |
| Policies  |                                |         |
| FAQs      | Searching knowledge...         | FAQ.pdf |
|           |                                |         |
|           | AI response...                 |         |
---------------------------------------------------------

Sigue siendo una interfaz de chat, pero la conversación es solo una parte del producto. Alrededor de ella, la aplicación también necesita exponer:

  • el conocimiento disponible para buscar
  • el estado actual de recuperación
  • las fuentes utilizadas para una respuesta
  • metadatos de documentos
  • citas
  • acciones posteriores
  • conversaciones anteriores

Incluir todo eso en un único componente de chat rápidamente se vuelve inmanejable.

Un árbol de componentes que se ajuste al producto

Una estructura viable sería esta:

RAGApplication
|
|-- Sidebar
|   |-- NewConversation
|   |-- ConversationHistory
|   `-- DocumentLibrary
|
|-- ChatWorkspace
|   |-- MessageList
|   |-- RetrievalStatus
|   |-- StreamingResponse
|   |-- CitationList
|   `-- PromptInput
|
`-- SourcePanel
    |-- SourceCard
    |-- DocumentPreview
    |-- RetrievedChunk
    `-- SourceMetadata

La división refleja las diferencias en el comportamiento de cada región. Los mensajes se acumulan como en una conversación, el indicador de recuperación es efímero, las respuestas llegan en flujo continuo, el panel de fuentes depende de la cita seleccionada, y la biblioteca funciona como un panel de control. Los componentes separados permiten gestionar el estado de cada uno de forma independiente y evitar que la respuesta en flujo vuelva a renderizar toda la pantalla con cada token.

Paso 1: Devolver suficientes datos desde la API

Las animaciones y el diseño vienen después. Primero, el backend debe proporcionar al frontend algo con lo que trabajar. Una respuesta que contenga solo el texto generado deja a la interfaz sin nada que mostrar aparte de ese texto:

{
  "answer": "..."
}

Un contrato más completo incluye un estado y la lista de fuentes que sirvieron de base para la respuesta, cada una con un identificador, un título, una referencia a sección y un fragmento breve:

{
  "answer": "Enterprise customers can request...",
  "status": "complete",
  "sources": [
    {
      "id": "doc-1",
      "title": "Refund Policy",
      "section": "3.2",
      "snippet": "Refund requests must be submitted..."
    },
    {
      "id": "doc-2",
      "title": "Enterprise Terms",
      "section": "5",
      "snippet": "Enterprise subscriptions..."
    }
  ]
}

Con esta estructura, React puede mostrar la respuesta, exhibir las fichas de citación, abrir las fuentes individuales y previsualizar el pasaje relevante, convirtiendo la evidencia en una interacción de primera categoría en lugar de algo secundario. Es importante utilizar un id estable: este se convierte en la clave de React para las tarjetas de fuentes y en el identificador para abrir un documento.

Paso 2: Considerar la recuperación de modelos como estado de interfaz explícito

Una solicitud RAG pasa por varias fases distintas, y resulta útil nombrarlas explícitamente:

idle
↓
retrieving
↓
sources_found
↓
generating
↓
complete

El punto de partida más sencillo en React son algunos elementos de estado relacionados con la fase actual, las fuentes y la respuesta:

const [status, setStatus] = useState("idle");
const [sources, setSources] = useState([]);
const [answer, setAnswer] = useState("");

Cuando el usuario envía una pregunta, el manejador avanza por esas fases a medida que la solicitud progresa:

async function handleAsk(question) {
  setStatus("retrieving");
  const result = await askKnowledgeBase(question);  setSources(result.sources);
  setStatus("generating");  setAnswer(result.answer);
  setStatus("complete");
}

Esto está deliberadamente simplificado. Dado que una sola llamada devuelve tanto las fuentes como la respuesta, generating se reemplaza por complete en el mismo paso, de modo que React agrupa las actualizaciones y el usuario nunca ve el estado intermedio (además, sources_found nunca se establece). Una implementación basada en streaming, por su parte, emitiría un evento de finalización de la recuperación, luego los tokens de respuesta y finalmente el estado de completado. El manejador también necesita un bloque try/catch y un estado error para que una solicitud fallida no permanezca en ejecución indefinidamente. Aun así, un estado explícito permite que la interfaz muestre con exactitud qué está sucediendo, y a medida que aumentan las fases, un useReducer con transiciones definidas mantiene la consistencia más fácilmente que varias llamadas a useState.

Paso 3: Reemplazar el spinner por un indicador de recuperación

Un simple mensaje de “Cargando...” no dice casi nada. Una aplicación RAG puede, en cambio, explicar el proceso que sigue. Primero, el usuario ve que se está buscando en la base de conocimientos:

Searching your knowledge base...

luego se indica cuántas pruebas se encontraron:

Found 4 relevant sources

y finalmente se muestra que se está redactando la respuesta:

Generating response...

Ese pequeño cambio explica el proceso y ayuda a establecer expectativas sobre el tiempo de espera. Un componente mínimo asocia cada estado con un mensaje correspondiente:

function RetrievalStatus({ status, sourceCount }) {
  if (status === "retrieving") {
    return <p>Searching your knowledge base...</p>;
  }
  if (status === "generating") {
    return <p>Found {sourceCount} sources. Generating response...</p>;
  }  return null;
}

Nada sofisticado, y ese es precisamente el objetivo: el componente devuelve null cuando no hay nada que informar, pero la aplicación parece actuar de manera más deliberada. Al colocar el mensaje en una región con aria-live="polite", los usuarios de lectores de pantalla también pueden escuchar las mismas actualizaciones.

Paso 4: Construir una jerarquía visual con Tailwind

Las pantallas RAG se llenan rápidamente con mensajes de usuarios y modelos, nombres y fragmentos de documentos, citas, advertencias, acciones y estado de recuperación. Las clases utilitarias de Tailwind permiten establecer rápidamente una jerarquía clara sin necesidad de un archivo de estilo personalizado extenso. Por ejemplo, una tarjeta de fuente puede utilizar un contenedor con borde y sombra ligera junto con un título en negrita:

<div className="rounded-xl border border-slate-200 bg-white p-4 shadow-sm">
  <p className="text-sm font-semibold text-slate-900">
    Refund Policy
  </p>

seguido por una etiqueta de sección en tono suave y un fragmento legible con un espaciado entre líneas generoso:

  <p className="mt-1 text-xs text-slate-500">
    Section 3.2
  </p>  <p className="mt-3 text-sm leading-6 text-slate-700">
    Refund requests must be submitted within...
  </p>
</div>

El estilo es intencionalmente sencillo. En las interfaces de IA, la claridad prevalece sobre la decoración, ya que el usuario desea conocer tres cosas: la respuesta, su origen y el siguiente paso posible. El énfasis visual debe seguir ese orden.

Paso 5: Hacer interactivas las citas

Muchas interfaces RAG se detienen en un número entre corchetes como [1]. Una cita es mucho más útil cuando permite al usuario examinar las pruebas detrás de una afirmación. Renderizar cada cita como un botón que registre la fuente seleccionada es una forma sencilla de lograrlo:

<button
  onClick={() => setSelectedSource(source)}
  className="rounded-full border px-3 py-1 text-sm"
>
  Refund Policy · 3.2
</button>

Al hacer clic en el chip se abre el panel de la fuente, que muestra el título del documento:

Refund Policy

junto con la sección, el pasaje citado y una forma de abrir el documento completo:

Section 3.2"Refund requests must be submitted within..."[Open document]

Las citas ahora realizan una función real, y la base de la respuesta se vuelve visible: los usuarios pueden verificar una afirmación con un solo clic en lugar de confiar ciegamente en ella. Un verdadero elemento button también permite acceder a las citas mediante el teclado.

Paso 6: Utilizar animaciones para explicar qué cambió

Framer Motion solo merece un lugar en este tipo de interfaz cuando la animación sirve para comunicar el estado. Nada debería moverse solo por el gusto de hacerlo. Un buen uso es permitir que las tarjetas de origen se desvanezcan y se deslicen ligeramente hacia arriba al completarse la recuperación. Comience importando el componente motion:

import { motion } from "framer-motion";

Luego, envuelva cada tarjeta en un motion.div que se anime de transparente y desplazado a completamente visible en un cuarto de segundo:

<motion.div
  initial={{ opacity: 0, y: 8 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: 0.25 }}
>
  <SourceCard source={source} />
</motion.div>

El panel de origen puede deslizarse desde un lado al seleccionar una cita, y deslizarse de vuelta al cerrarse:

<motion.aside
  initial={{ x: 40, opacity: 0 }}
  animate={{ x: 0, opacity: 1 }}
  exit={{ x: 40, opacity: 0 }}
>
  <SourcePanel source={selectedSource} />
</motion.aside>

Una advertencia: la animación de exit solo se ejecuta cuando el componente se renderiza dentro de AnimatePresence, lo que mantiene el elemento montado hasta que finaliza su animación de salida. Sin esto, React elimina el panel inmediatamente y la animación de deslizamiento nunca se muestra. Tenga también en cuenta que, en el momento de escribir esto, Framer Motion se distribuye como el paquete motion con un punto de entrada motion/react; consulte la documentación actual para conocer la ruta de importación que corresponda a su versión instalada.

El valor radica en el mensaje que transmite el movimiento: algo ha cambiado, ha llegado nueva información, y esta fuente pertenece a esta respuesta. Respete la preferencia reduced-motion para que nunca se convierta en una molestia.

Paso 7: Tratar la transmisión en tiempo real de manera diferente al cargado

La transmisión en tiempo real genera su propio problema de experiencia de usuario. Cuando el texto llega token por token, la interfaz debe hacer evidente que la respuesta aún se está escribiendo. Un pequeño cursor parpadeante después del texto cumple bien esta función. El componente muestra el contenido obtenido hasta el momento:

function StreamingAnswer({ content, isStreaming }) {
  return (
    <div className="prose max-w-none">
      {content}

y agrega el cursor solo mientras la transmisión está activa:

      {isStreaming && (
        <span className="ml-1 inline-block h-4 w-1 animate-pulse bg-slate-500" />
      )}
    </div>
  );
}

También es sensato limitar lo que el usuario puede hacer con una respuesta incompleta. Las acciones de lectura pueden permanecer disponibles, mientras que cualquier acción que guarde la respuesta en otro lugar debe esperar a que se complete:

Copy answer       ✓
View sources      ✓
Create task       disabled while streaming
Export            disabled while streaming

El objetivo es una comunicación honesta del estado, no la perfección estética: exportar una respuesta a medio escribir generaría un resultado defectuoso.

Paso 8: Hacer que el panel de origen forme parte del producto

Un panel de origen adecuado es una de las características más importantes que puede ofrecer una interfaz RAG. Considere todo el proceso:

User asks question
        ↓
RAG retrieves 4 chunks
        ↓
AI generates answer
        ↓
Citation cards appear
        ↓
User clicks citation
        ↓
Relevant document section opens

Como mínimo, el panel puede mostrar:

  • el título del documento
  • la sección
  • el pasaje recuperado
  • el tipo de documento
  • información sobre la relevancia
  • un enlace o botón para abrir el documento

Con el tiempo puede ampliarse para incluir:

  • resaltado del texto correspondiente
  • fragmentos adyacentes para contexto
  • una vista previa del documento
  • filtros de metadatos

Cada adición hace que el sistema sea menos una caja negra. Un buen panel también revela las debilidades en la recuperación de información, lo cual es una razón para invertir en la calidad de las fuentes desde el inicio de un proyecto RAG.

Paso 9: Ofrecer acciones después de la respuesta

Aquí es donde un chatbot RAG comienza a transformarse en una aplicación. Supongamos que alguien pregunta cómo el equipo integra a un nuevo cliente. El sistema obtiene las instrucciones para la integración y responde con una lista de verificación:

1. Create workspace
2. Request account access
3. Confirm scope
4. Assign team
5. Schedule kickoff

En lugar de detenerse allí, la interfaz puede ofrecer un enlace de regreso al documento original:

[View SOP]

además de acciones que reutilizan el resultado obtenido:

[Copy checklist][Create task list]

La acción relacionada con la lista de tareas es la más interesante. El valor de un sistema de IA suele residir menos en la respuesta que en lo siguiente que permite hacer, y aquí es donde las aplicaciones RAG comienzan a evolucionar hacia agentes. Nuestra guía para crear interfaces de usuario para agentes de IA de múltiples pasos aborda esa dirección.

La interacción completa, de principio a fin

Al combinar todo, una sola pregunta recorre toda la estructura. Comienza con el usuario preguntando si un cliente empresarial puede cancelar después de 30 días:

USER
"Can an enterprise client cancel after 30 days?"

A partir de ahí, React crea el estado de la consulta, la API RAG obtiene los documentos, la línea de estado informa sobre la búsqueda, aparece el recuento de fuentes, el LLM genera una respuesta fundamentada que se muestra en la pantalla, aparecen las fichas de citación y al hacer clic en una se desliza el panel de fuentes para mostrar el pasaje relevante:

        ↓REACT
Creates query state        ↓RAG API
Retrieves relevant documents        ↓UI STATUS
"Searching your knowledge base..."        ↓SOURCES
3 relevant sources found        ↓LLM
Generates grounded response        ↓STREAMING UI
Response appears progressively        ↓CITATIONS
Refund Policy · 3.2
Enterprise Terms · 5        ↓USER
Clicks citation        ↓MOTION
Source panel slides into view        ↓SOURCE
Relevant passage displayed

Imagínelo como dos capas que colaboran: una capa de inteligencia en el backend que obtiene y genera información, y una capa de experiencia en el frontend que hace que ese trabajo sea comprensible. Se construyen por separado pero deben diseñarse juntas, y por eso es importante el contrato del Paso 1.

Una pila tecnológica pragmática

Se puede crear una versión ligera de esta aplicación con:

  • Frontend: React, Tailwind CSS y Framer Motion.
  • Backend: FastAPI con Python.
  • Recuperación de datos: embeddings y una base de datos vectorial, gestionada con LangChain o un pequeño pipeline personalizado.
  • Modelo: cualquier API alojada adecuada, como OpenAI, Gemini o Claude.
  • Almacenamiento: PostgreSQL o Supabase, junto con Qdrant u otro almacén vectorial similar.

No es necesario adoptar todo esto desde el primer día. Para un proyecto de portafolio, basta con un punto de partida mínimo:

React
+
Tailwind
+
Framer Motion
+
FastAPI
+
one LLM
+
one vector store

La calidad de la recuperación de datos, el reordenamiento, la autenticación, el procesamiento de documentos y las acciones pueden mejorarse de forma gradual una vez que funcione el bucle principal.

El patrón a evitar

La versión que se debe evitar se parece a esta:

Upload PDF
↓
Chat box
↓
Spinner
↓
Answer

Eso demuestra que la pipeline funciona, pero muestra poca capacidad de pensamiento orientado al producto: no hay nada visible, verificable o accionable. Un objetivo más sólido combina varias capas:

Knowledge
+
Retrieval
+
Transparent states
+
Citations
+
Source exploration
+
Useful actions

Esa combinación demuestra tanto ingeniería de IA como una implementación sólida del frontend.

Por qué la capa de interfaz merece atención

El modelo es solo la mitad de un producto de IA. El resto es lo que siempre ha requerido un buen software: interacciones comprensibles, retroalimentación clara, estados predecibles, flujos de trabajo útiles y una arquitectura de información sólida. React, Tailwind y Motion no aportan inteligencia alguna, pero son ellos quienes deciden si alguien puede beneficiarse de ella, y ese trabajo en el frontend suele subestimarse.

Puntos clave

  • Diseñe primero la respuesta de la API; las fuentes con IDs, secciones y fragmentos estables son lo que hace posibles las citaciones y los paneles de fuentes.
  • Modela la solicitud como fases nombradas y planifica el streaming y los errores, en lugar de activar únicamente una bandera de carga.
  • Trata las citaciones como botones que abren la evidencia, y dedica atención real al panel de fuentes.
  • Utiliza animaciones solo para indicar cambios, envuelve los elementos que desaparecen en AnimatePresence, y respeta las preferencias de reducción de movimiento.
  • Controla las acciones que dependen de una respuesta finalizada, y ofrece los siguientes pasos una vez esté completa, porque ahí es cuando un chatbot se convierte en una aplicación.