Aller au contenu principal

Widgets interactifs

Mis à jour le 29 juillet 2026

Widgets interactifs

Les messages texte ne suffisent pas toujours. Demandez à un agent la liste des produits d’une catégorie et il vous rendra un paragraphe où les prix se noient dans la phrase ; l’utilisateur relit trois fois pour comparer deux lignes. Quand votre agent retourne des données structurées — tableaux, graphiques, cartes produit — ChatKit permet d’afficher des widgets interactifs directement dans le chat, et l’utilisateur peut agir dessus sans quitter la conversation.

Le concept de widgets

Un widget est un composant visuel affiché dans un message de l’agent. Le principe tient en une phrase : quand l’agent retourne des données structurées via un tool, ChatKit peut les afficher sous forme de widget au lieu de texte brut. Deux moitiés se répondent donc — côté frontend vous déclarez les composants disponibles, côté backend vos tools signalent lequel utiliser. Ni l’un ni l’autre ne fonctionne seul.

Enregistrer un widget

Commencez par le frontend. Chaque widget est un composant React ordinaire qui reçoit un objet data et le met en forme ; vous les rassemblez ensuite dans un dictionnaire passé au ChatProvider, où la clé sert d’identifiant. L’exemple ci-dessous en déclare deux : un tableau générique, utile pour toute liste comparative, et une carte produit pour les réponses portant sur un article précis.

import { ChatProvider, ChatWindow, MessageInput } from "@openai/chatkit/react";

// Widget tableau de données
function TableauWidget({ data }) {
  return (
    <div style={{
      overflowX: "auto",
      margin: "8px 0",
      borderRadius: "8px",
      border: "1px solid #334155",
    }}>
      <table style={{ width: "100%", borderCollapse: "collapse", fontSize: "14px" }}>
        <thead>
          <tr style={{ background: "#1E293B" }}>
            {data.colonnes.map((col, i) => (
              <th key={i} style={{ padding: "10px 12px", textAlign: "left", color: "#94A3B8" }}>
                {col}
              </th>
            ))}
          </tr>
        </thead>
        <tbody>
          {data.lignes.map((ligne, i) => (
            <tr key={i} style={{ borderTop: "1px solid #1E293B" }}>
              {ligne.map((cell, j) => (
                <td key={j} style={{ padding: "10px 12px", color: "#F8FAFC" }}>
                  {cell}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

// Widget carte de produit
function ProduitWidget({ data }) {
  return (
    <div style={{
      display: "flex",
      gap: "16px",
      padding: "16px",
      background: "#1E293B",
      borderRadius: "12px",
      margin: "8px 0",
    }}>
      <div style={{ flex: 1 }}>
        <h3 style={{ margin: "0 0 8px", color: "#F8FAFC" }}>{data.nom}</h3>
        <p style={{ margin: "0 0 4px", color: "#94A3B8" }}>{data.description}</p>
        <span style={{
          color: "#3B82F6",
          fontSize: "20px",
          fontWeight: "bold",
        }}>
          {data.prix}
        </span>
      </div>
    </div>
  );
}

// Enregistrement des widgets
const widgets = {
  tableau: TableauWidget,
  produit: ProduitWidget,
};

function App() {
  return (
    <ChatProvider endpoint="/api/chat" widgets={widgets}>
      <ChatWindow />
      <MessageInput />
    </ChatProvider>
  );
}

Le overflowX: "auto" du tableau n’est pas décoratif : un chat vit souvent dans un panneau de 400 pixels, et sans ce réglage une colonne de trop casse la mise en page de toute la conversation. Le tableau défile alors dans sa propre boîte pendant que le fil de discussion reste intact.

Déclencher un widget depuis l’agent

Côté backend, le tool ne renvoie plus une phrase mais un objet JSON portant une clé __widget dont la valeur correspond à l’identifiant enregistré côté frontend. Le reste de l’objet devient le data du composant. Un tool de listing produira ainsi colonnes et lignes pour le tableau, tandis qu’un tool de détail produira nom, description et prix pour la carte.

import json
from agents import function_tool

@function_tool
def lister_produits(categorie: str) -> str:
    """Liste les produits d'une catégorie avec affichage enrichi."""
    produits = [
        {"nom": "Laptop Pro", "prix": "1 299€", "stock": 45},
        {"nom": "Tablet Air", "prix": "599€", "stock": 120},
    ]
    # Format spécial pour déclencher le widget "tableau"
    return json.dumps({
        "__widget": "tableau",
        "colonnes": ["Produit", "Prix", "Stock"],
        "lignes": [[p["nom"], p["prix"], p["stock"]] for p in produits],
    })

@function_tool
def details_produit(nom: str) -> str:
    """Affiche les détails d'un produit."""
    return json.dumps({
        "__widget": "produit",
        "nom": "Laptop Pro",
        "description": "Ordinateur portable haute performance, 16 Go RAM, SSD 1 To",
        "prix": "1 299€",
    })

Une clé mal orthographiée d’un côté ou de l’autre, et le widget ne s’affiche jamais : l’utilisateur voit du JSON brut dans la conversation. C’est le premier réflexe de débogage quand un widget « ne marche pas ».

Widget avec interactions

Un widget peut aussi rendre la main à l’utilisateur. En recevant la prop onAction, il expose des boutons qui relancent la conversation avec un contexte précis : ajouter au panier, demander plus de détails. L’utilisateur n’a plus à reformuler « oui, celui-là, le Laptop Pro » — le clic transporte déjà l’identifiant du produit.

function ProduitInteractifWidget({ data, onAction }) {
  return (
    <div style={{
      padding: "16px",
      background: "#1E293B",
      borderRadius: "12px",
      margin: "8px 0",
    }}>
      <h3 style={{ color: "#F8FAFC", margin: "0 0 8px" }}>{data.nom}</h3>
      <p style={{ color: "#94A3B8", margin: "0 0 12px" }}>{data.description}</p>
      <div style={{ display: "flex", gap: "8px" }}>
        <button
          onClick={() => onAction("ajouter_panier", { produit: data.nom })}
          style={{
            padding: "8px 16px",
            background: "#2563EB",
            color: "white",
            border: "none",
            borderRadius: "8px",
            cursor: "pointer",
          }}
        >
          Ajouter au panier
        </button>
        <button
          onClick={() => onAction("voir_details", { produit: data.nom })}
          style={{
            padding: "8px 16px",
            background: "transparent",
            color: "#3B82F6",
            border: "1px solid #3B82F6",
            borderRadius: "8px",
            cursor: "pointer",
          }}
        >
          Plus de détails
        </button>
      </div>
    </div>
  );
}

Quand l’utilisateur clique sur un bouton, onAction envoie un message à l’agent avec le contexte de l’action. Le contraste entre les deux boutons — fond plein pour l’action principale, contour seul pour l’action secondaire — n’est pas qu’une question de goût : il indique au lecteur laquelle des deux vous attendez de lui.

Widget graphique

Pour les données analytiques, intégrez une bibliothèque de graphiques. Le principe reste le même : un tool renvoie un titre et une série de valeurs, le widget les dessine. La version ci-dessous se contente de barres en CSS pur, ce qui suffit pour une tendance sur douze mois et évite d’embarquer une dépendance supplémentaire dans un panneau de chat.

// Avec recharts ou une autre bibliothèque
function GraphiqueWidget({ data }) {
  return (
    <div style={{
      padding: "16px",
      background: "#1E293B",
      borderRadius: "12px",
      margin: "8px 0",
    }}>
      <h4 style={{ color: "#F8FAFC", margin: "0 0 12px" }}>{data.titre}</h4>
      <div style={{ display: "flex", gap: "4px", alignItems: "flex-end", height: "120px" }}>
        {data.valeurs.map((v, i) => (
          <div key={i} style={{ flex: 1, textAlign: "center" }}>
            <div style={{
              height: `${(v.valeur / Math.max(...data.valeurs.map(x => x.valeur))) * 100}px`,
              background: "#3B82F6",
              borderRadius: "4px 4px 0 0",
              minHeight: "4px",
            }} />
            <span style={{ fontSize: "10px", color: "#94A3B8" }}>{v.label}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

La hauteur de chaque barre est calculée en proportion du maximum de la série : le graphique s’adapte donc aussi bien à des euros qu’à des pourcentages. Exercez-vous en ajoutant un troisième widget à ce trio — une carte de statut de commande, par exemple — et vérifiez qu’un tool existant peut le déclencher sans que vous touchiez au ChatWindow.

Points clés à retenir

  • Les widgets affichent des données structurées visuellement dans le chat
  • Enregistrez vos widgets dans le ChatProvider avec un identifiant unique
  • Les tools retournent un objet __widget pour déclencher l’affichage
  • Les widgets interactifs utilisent onAction pour communiquer avec l’agent
  • Combinez widgets et bibliothèques de graphiques pour des dashboards dans le chat