Accueil / Articles

Concevoir une interface RAG transparente avec React, Tailwind et Motion

Créez une interface utilisateur RAG qui affiche les étapes de récupération des données, des citations interactives, un panneau des sources ainsi que des actions après réponse, en utilisant React State, Tailwind CSS et Framer Motion.

Éditeur : REACTAPP.TOP11 min de lecture2544 mots

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

Un pipeline de génération améliorée par la recherche peut être techniquement excellent, mais donner l’impression d’un produit faible lorsque son interface cache ce que fait le système. Une simple boîte de dialogue avec un indicateur d’animation ne renseigne pas l’utilisateur sur les documents qui ont été recherchés, d’où provient la réponse, ni sur le fait que celle-ci est encore en cours d’élaboration. Cette présentation montre comment structurer une interface React autour des étapes réelles d’une demande RAG, afin que la recherche, les sources, le streaming et les citations deviennent des éléments visibles et inspectables de l’expérience utilisateur.

Conception centrée sur les étapes de recherche

Le changement clé consiste à considérer le produit comme une séquence que l’utilisateur peut suivre, plutôt que comme un simple « chat » : question, recherche, sources, génération, citations, exploration. C’est cette séquence qui guide l’organisation de l’application React.

Dans le cas d’un assistant à la connaissance interne typique, l’écran peut être divisé en trois zones : une bibliothèque de documents à gauche, la conversation au centre, et les sources relatives à la réponse actuelle à droite.

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

Il s’agit toujours d’une interface de chat, mais la conversation n’est qu’une partie du produit. Autour d’elle, l’application doit également permettre d’accéder à :

  • les connaissances pouvant être recherchées
  • l’état actuel de la recherche
  • les sources utilisées pour obtenir une réponse
  • les métadonnées des documents
  • les citations
  • les actions à entreprendre ensuite
  • les conversations antérieures

Intégrer tout cela dans un seul composant de chat devient rapidement difficile à gérer.

Un arbre de composants adapté au produit

Une structure fonctionnelle ressemble à ceci :

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

La segmentation montre à quel point chaque région se comporte différemment. Les messages s’accumulent comme dans une conversation, l’indicateur de récupération est de courte durée, les réponses arrivent en continu, le panneau source dépend de la citation sélectionnée, et la bibliothèque fonctionne comme un tableau de bord. Des composants séparés permettent de gérer l’état de chacun indépendamment et d’éviter que la réponse en flux ne force à re-render tout l’écran à chaque token.

Étape 1 : Retourner suffisamment de données depuis l’API

Les animations et le layout viendront plus tard. D’abord, le backend doit fournir au frontend quelque chose sur quoi travailler. Une réponse ne contenant que le texte généré ne laisse à l’interface quudit texte à afficher :

{
  "answer": "..."
}

Un contrat plus complet inclut un statut ainsi que la liste des sources qui ont servi de base à la réponse, chacune étant accompagnée d’un identifiant, d’un titre, d’une référence de section et d’un court extrait :

{
  "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..."
    }
  ]
}

Avec cette structure, React peut afficher la réponse, montrer des icônes de citation, ouvrir les sources individuelles et afficher un aperçu du passage pertinent, transformant ainsi les preuves en une interaction de premier plan plutôt qu’en un élément secondaire. Assurez-vous que l’id soit stable : il sert de clé React pour les cartes de sources et de point d’accès pour ouvrir un document.

Étape 2 : Modéliser la récupération des données comme un état UI explicite

Une requête RAG passe par plusieurs phases distinctes, et il est utile de les nommer explicitement :

idle
↓
retrieving
↓
sources_found
↓
generating
↓
complete

Le point de départ le plus simple en React consiste à définir quelques variables d’état pour la phase actuelle, les sources et la réponse :

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

Lorsque l’utilisateur soumet une question, le gestionnaire parcourt ces phases au fur et à mesure que la requête progresse :

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

Ceci est délibérément simplifié. Comme une seule appelée renvoie à la fois les sources et la réponse, generating est remplacé par complete au même moment, ce qui permet à React de regrouper les mises à jour ; l’utilisateur ne voit donc jamais l’état intermédiaire (et sources_found n’est jamais défini). Une implémentation en flux émettrait plutôt un événement indiquant que la récupération est terminée, suivi des tokens de réponse, puis un signal de fin. Le gestionnaire nécessite également une structure try/catch ainsi qu’un état error afin qu’une demande échouée ne reste pas en attente indéfiniment. Néanmoins, un état explicite permet à l’interface d’indiquer précisément ce qui se passe, et à mesure que le nombre de phases augmente, un useReducer avec des transitions définies reste plus facilement cohérent qu’un ensemble de calls useState.

Étape 3 : Remplacer l’indicateur de chargement par un indicateur de récupération

Un simple message « Chargement... » ne dit presque rien. Une application RAG peut quant à elle décrire son processus en temps réel. D’abord, l’utilisateur voit que la base de connaissances est consultée :

Searching your knowledge base...

ensuite, on lui indique combien de preuves ont été trouvées :

Found 4 relevant sources

et enfin, on lui montre que la réponse est en train d’être générée :

Generating response...

Ce petit changement explique le processus et donne une idée du temps d’attente. Un composant minimal convertit l’état en message :

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;
}

Rien de compliqué, et c’est justement l’idée : le composant renvoie null lorsqu’il n’y a rien à signaler, mais l’application semble alors plus réfléchie. En plaçant le message dans une zone aria-live="polite", les utilisateurs de lecteurs d’écran peuvent également entendre ces mises à jour.

Étape 4 : Créer une hiérarchie visuelle avec Tailwind

Les écrans RAG se remplissent rapidement de messages d’utilisateurs et de modèles, de noms de documents et d’extraits, de citations, d’avertissements, d’actions ainsi que d’états de récupération. Les classes utilitaires de Tailwind permettent d’établir rapidement une hiérarchie claire sans avoir recours à un grand fichier de style personnalisé. Une carte source, par exemple, peut utiliser un conteneur bordé et légèrement ombragé avec un titre en gras :

<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>

suivi d’une étiquette de section en couleur discrète et d’un extrait lisible disposé sur des lignes suffisamment espacées :

  <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>

Le style est intentionnellement sobre. La clarté prime sur la décoration dans les interfaces d’IA, car l’utilisateur souhaite connaître trois choses : la réponse, son origine et la prochaine étape possible. Le poids visuel doit respecter cet ordre.

Étape 5 : Rendre les citations interactives

De nombreuses interfaces RAG s’arrêtent à un numéro entre crochets, comme [1]. Une citation est bien plus utile lorsqu’elle permet à l’utilisateur d’examiner les preuves derrière une affirmation. Afficher chaque citation sous forme de bouton qui enregistre la source sélectionnée est un moyen simple d’y parvenir :

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

En cliquant sur ce bouton, le panneau de la source s’ouvre, affichant le titre du document :

Refund Policy

ainsi que la section concernée, le passage cité et un moyen d’ouvrir le document complet :

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

Les citations remplissent désormais une fonction réelle, et les fondements de la réponse deviennent visibles : les utilisateurs peuvent vérifier une affirmation d’un seul clic au lieu de la prendre à crédit. Un véritable élément button permet également d’accéder aux citations via le clavier.

Étape 6 : Utiliser des animations pour expliquer ce qui a changé

Framer Motion ne trouve sa place dans ce type d’interface que lorsque l’animation communique un état. Rien ne doit bouger pour le plaisir de bouger. Une bonne utilisation consiste à faire disparaître progressivement les cartes sources puis à les faire glisser légèrement vers le haut une fois la récupération terminée. Commencez par importer le composant motion:

import { motion } from "framer-motion";

Ensuite, enveloppez chaque carte dans un motion.div qui passe d’un état transparent et décalé à un état entièrement visible en un quart de seconde :

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

Le panneau des sources peut glisser depuis le côté lorsque une citation est sélectionnée, et revenir en arrière lorsqu’il se ferme :

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

Un point important : l’animation exit ne s’exécute que lorsque le composant est rendu à l’intérieur de AnimatePresence, ce qui maintient l’élément monté jusqu’à la fin de son animation de sortie. Sans cela, React supprime immédiatement le panneau et l’animation de glissement ne se déclenche jamais. Notez également qu’à l’époque où ce texte a été écrit, Framer Motion était distribué sous le nom de package motion avec un point d’entrée motion/react ; consultez la documentation actuelle pour connaître le chemin d’import adapté à votre version installée.

L’intérêt réside dans le message transmis par ce mouvement : quelque chose a changé, de nouvelles informations sont arrivées, et cette source appartient à cette réponse. Respectez la préférence reduced-motion afin qu’elle ne devienne jamais un problème.

Étape 7 : Traiter le streaming différemment du chargement

La diffusion en flux crée son propre problème d’expérience utilisateur. Lorsque le texte arrive token par token, l’interface doit faire en sorte qu’il soit évident que la réponse est encore en cours d’écriture. Un petit curseur clignotant après le texte permet bien de le faire. Le composant affiche donc le contenu jusqu’à présent :

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

et n’ajoute le curseur que tant que la diffusion en flux est active :

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

Il est également judicieux de limiter ce que l’utilisateur peut faire avec une réponse inachevée. Les actions de lecture peuvent rester disponibles, tandis que tout ce qui vise à enregistrer la réponse ailleurs doit attendre sa finalisation :

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

L’objectif est une communication honnête de l’état, et non la perfection : exporter une réponse inachevée produirait un résultat défectueux.

Étape 8 : Intégrer le panneau de source au produit

Un véritable panneau de source est l’une des fonctionnalités les plus puissantes que peut offrir une interface RAG. Pensez à l’ensemble du processus :

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

À titre minimum, le panneau peut afficher :

  • le titre du document
  • la section
  • le passage récupéré
  • le type de document
  • des informations sur la pertinence
  • un lien ou un bouton pour ouvrir le document

Au fil du temps, il peut inclure de plus en plus d’éléments tels que :

  • l’highlightage du texte correspondant
  • des extraits voisins pour donner du contexte
  • une vue préliminaire du document
  • des filtres de métadonnées

Chaque ajout rend le système moins opaque. Un bon panneau met également en évidence les faiblesses de la recherche, ce qui est l’une des raisons d’investir dès le début d’un projet RAG dans la qualité de la source.

Étape 9 : Proposer des actions après la réponse

C’est ici que le chatbot RAG commence à se transformer en application. Supposons qu’on demande comment l’équipe intègre un nouveau client. Le système récupère la procédure opérationnelle d’intégration et répond avec une liste de contrôle :

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

Plutôt que de s’arrêter là, l’interface peut proposer un lien vers le document source :

[View SOP]

ainsi que des actions qui réutilisent le résultat :

[Copy checklist][Create task list]

L’action liée à la liste de tâches est particulièrement intéressante. La valeur d’un système d’IA réside souvent moins dans la réponse elle-même que dans ce qu’il permet de faire ensuite, et c’est là que les applications RAG commencent à évoluer vers des agents. Notre guide sur la création d’interfaces utilisateur pour des agents IA à plusieurs étapes aborde cette direction.

L’interaction complète, du début à la fin

En résumé, une seule question circule à travers toute la pile technologique. Tout commence par la demande de l’utilisateur quant à la possibilité pour un client professionnel d’annuler après 30 jours :

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

Par la suite, React crée l’état de la requête, l’API RAG récupère les documents, la ligne d’état indique le résultat de la recherche, le compteur des sources s’affiche, le LLM génère une réponse fondée qui est affichée à l’écran, des fiches de citation apparaissent, et un clic sur l’une d’elles fait apparaître le panneau des sources avec le passage pertinent :

        ↓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

Pensez-y comme à deux couches qui coopèrent : une couche d’intelligence au backend chargée de récupérer et de générer des informations, et une couche d’expérience au frontend qui rend ce travail compréhensible. Elles sont développées séparément mais doivent être conçues ensemble, c’est pourquoi le contrat de l’Étape 1 est important.

Une pile technologique pragmatique

Une version légère de cette application peut être développée à l’aide de :

  • Frontend : React, Tailwind CSS et Framer Motion.
  • Backend : FastAPI avec Python.
  • Récupération des données : embeddings et une base de données vectorielle, gérés via LangChain ou un petit pipeline personnalisé.
  • Modèle : n’importe quelle API hébergée adaptée, telle que OpenAI, Gemini ou Claude.
  • Stockage : PostgreSQL ou Supabase, associés à Qdrant ou un stockage vectoriel similaire.

Il n’est pas nécessaire d’adopter tout cela dès le premier jour. Pour un projet de portfolio, un point de départ minimal suffit :

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

La qualité de la récupération des données, le réclassement, l’authentification, le traitement des documents ainsi que les actions associées peuvent être améliorés progressivement une fois que le cycle de base fonctionne.

Le schéma à éviter

La version à proscrire ressemble à ceci :

Upload PDF
↓
Chat box
↓
Spinner
↓
Answer

Cela prouve que le pipeline fonctionne, mais montre peu de réflexion sur le produit : rien n’est visible, vérifiable ou actionnable. Un objectif plus ambitieux combine plusieurs niveaux :

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

Cette combinaison démontre à la fois une ingénierie IA solide et une mise en œuvre frontend fiable.

Pourquoi la couche d’interface mérite de l’attention

Le modèle n’est que la moitié d’un produit IA. Le reste correspond à ce que tout bon logiciel a toujours exigé : des interactions compréhensibles, un retour d’information clair, des états prévisibles, des workflows utiles et une architecture de l’information solide. React, Tailwind et Motion ne contribuent en rien à l’intelligence du système, pourtant ils déterminent si quelqu’un peut en tirer profit, et le travail frontend est régulièrement sous-estimé.

Points clés

  • Concevez d’abord la réponse API ; des sources dotées d’ID stables, de sections et d’extrait permettent les citations ainsi que les panneaux de sources.
  • Modélisez la demande en phases nommées et planifiez le streaming ainsi que les erreurs, plutôt que de simplement activer/désactiver un seul indicateur de chargement.
  • Traitez les citations comme des boutons qui ouvrent les preuves, et accordez un véritable intérêt au panneau des sources.
  • Utilisez l’animation uniquement pour signaler un changement, enveloppez les éléments qui disparaissent dans AnimatePresence, et respectez les préférences de réduction des animations.
  • Restreignez les actions qui dépendent d’une réponse terminée, et proposez des étapes suivantes une fois celle-ci achevée, car c’est là que le chatbot devient une application.