La plupart des tutoriels IA en 2026 construisent des agents back-end — Claude, LangGraph, Mastra, Pydantic AI. Utile, mais ils s'arrêtent tous à un endpoint de chat. Le problème suivant est plus difficile : comment intégrer un assistant IA dans votre produit pour qu'il lise le même état que voit l'utilisateur et appelle les mêmes actions que vos boutons ?
C'est exactement ce que fait CopilotKit. C'est un framework React open source qui place un copilote à côté de votre interface, lui donne une vue typée de l'état de votre application, et lui permet de déclencher des fonctions que vous avez déjà écrites. À la fin de ce tutoriel, vous aurez une application Next.js 15 fonctionnelle où un copilote en barre latérale peut lire une liste de tâches, ajouter des tâches, les marquer comme terminées et afficher des cartes UI riches dans le chat — sans jamais quitter votre code.
Pourquoi CopilotKit plutôt qu'un widget de chat brut ? Un widget de chat répond à des questions. Un copilote opère l'application. CopilotKit fournit des hooks React typés (useCopilotReadable, useCopilotAction) pour que le LLM dispose d'un contexte structuré et d'une surface d'outils structurée — c'est la différence entre "ressemble à de l'IA" et "ressemble à un coéquipier".
Ce que vous allez apprendre
À la fin de ce tutoriel, vous saurez :
- Installer les paquets CopilotKit et brancher le provider
<CopilotKit>dans un projet Next.js 15 App Router - Construire la route runtime
/api/copilotkitavec l'adaptateur OpenAI - Partager l'état React avec le LLM via
useCopilotReadable - Définir des actions front-end typées avec
useCopilotActionpour que le copilote modifie l'état UI - Afficher de l'UI générative dans le chat (cartes personnalisées, pas seulement du texte)
- Mettre en place le
CopilotSidebaret le styliser aux couleurs de votre marque
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 20 ou supérieur (Node 18 ne fonctionnera pas — voir Dépannage)
- Une bonne connaissance de Next.js 15 (App Router, Server Components vs Client Components)
- Une clé API OpenAI (ou Anthropic, Groq, Azure — CopilotKit supporte plusieurs adaptateurs)
- Des bases en hooks React et TypeScript
Ce que vous allez construire
Nous construisons un gestionnaire de tâches intelligent — une page Next.js 15 avec une liste de tâches et une barre latérale CopilotKit. L'utilisateur peut parler au copilote pour ajouter des tâches, marquer des tâches comme terminées, filtrer par statut et demander un résumé de ce qu'il reste pour la journée. Le copilote affichera des cartes de tâches directement dans le chat plutôt que du texte brut.
Étape 1 : Mise en place du projet
Créez un nouveau projet Next.js 15 avec TypeScript et Tailwind :
npx create-next-app@latest copilot-tasks \
--typescript --tailwind --app --eslint --src-dir
cd copilot-tasksInstallez ensuite les trois paquets CopilotKit nécessaires. Le front a besoin de react-core et react-ui, et la route API a besoin de runtime :
npm install @copilotkit/react-core @copilotkit/react-ui @copilotkit/runtime
npm install openaiAjoutez votre clé OpenAI dans .env.local :
OPENAI_API_KEY=sk-proj-...Astuce : Ne committez jamais .env.local. Vérifiez que .gitignore l'exclut déjà (Next.js le configure par défaut).
Étape 2 : Créer la route API runtime
CopilotKit fournit un runtime qui relie votre app React au fournisseur LLM. Dans l'App Router, c'est un seul handler de route dans app/api/copilotkit/route.ts :
// src/app/api/copilotkit/route.ts
import {
CopilotRuntime,
OpenAIAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import OpenAI from "openai";
import { NextRequest } from "next/server";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const serviceAdapter = new OpenAIAdapter({ openai });
const runtime = new CopilotRuntime();
export const POST = async (req: NextRequest) => {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
runtime,
serviceAdapter,
endpoint: "/api/copilotkit",
});
return handleRequest(req);
};Trois points à noter :
OpenAIAdaptern'est qu'un parmi d'autres — vous pouvez le remplacer parAnthropicAdapter,GroqAdapterouLangChainAdaptersans toucher au reste du code.CopilotRuntimeest le cerveau central. Nous lui passerons plus tard des actions côté serveur.copilotRuntimeNextJSAppRouterEndpointretourne une fonctionhandleRequestqui parle déjà le contrat App Router — pas de plomberie de streaming manuelle.
Étape 3 : Monter le provider CopilotKit
Le provider <CopilotKit> doit envelopper tout arbre qui utilise les hooks copilote. Placez-le dans le layout racine, mais marquez l'enveloppe comme un client component :
// src/components/CopilotProvider.tsx
"use client";
import { CopilotKit } from "@copilotkit/react-core";
import "@copilotkit/react-ui/styles.css";
export function CopilotProvider({ children }: { children: React.ReactNode }) {
return (
<CopilotKit runtimeUrl="/api/copilotkit">
{children}
</CopilotKit>
);
}Branchez-le dans le layout racine (qui reste un Server Component) :
// src/app/layout.tsx
import { CopilotProvider } from "@/components/CopilotProvider";
import "./globals.css";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="fr">
<body>
<CopilotProvider>{children}</CopilotProvider>
</body>
</html>
);
}runtimeUrl est le chemin public de la route construite à l'étape 2. Tout ce qui se trouve dans <CopilotProvider> peut désormais utiliser useCopilotReadable et useCopilotAction.
Étape 4 : Construire l'UI de la liste de tâches
Avant d'ajouter de l'IA, on construit une liste de tâches React pilotée par l'état. Le copilote se branchera sur cet état exact — sans réécriture.
// src/app/page.tsx
"use client";
import { useState } from "react";
import { CopilotSidebar } from "@copilotkit/react-ui";
type Task = {
id: string;
title: string;
status: "todo" | "done";
};
export default function HomePage() {
const [tasks, setTasks] = useState<Task[]>([
{ id: "1", title: "Rédiger le brouillon du tutoriel", status: "todo" },
{ id: "2", title: "Relire la PR #42", status: "todo" },
]);
return (
<main className="mx-auto max-w-3xl p-8">
<h1 className="mb-6 text-3xl font-semibold">Tâches intelligentes</h1>
<ul className="space-y-2">
{tasks.map((task) => (
<li
key={task.id}
className="flex items-center justify-between rounded border p-3"
>
<span
className={
task.status === "done"
? "text-gray-400 line-through"
: "text-gray-900"
}
>
{task.title}
</span>
<span className="text-xs uppercase text-gray-500">
{task.status}
</span>
</li>
))}
</ul>
<CopilotSidebar
labels={{
title: "Copilote de tâches",
initial: "Bonjour ! Je peux ajouter, terminer ou résumer vos tâches.",
}}
/>
</main>
);
}Lancez npm run dev, ouvrez http://localhost:3000 et vous devriez voir la liste avec une barre latérale de chat à droite. Le copilote répondra — mais il ignore encore vos tâches. On corrige cela à l'étape suivante.
Étape 5 : Partager l'état avec useCopilotReadable
useCopilotReadable injecte n'importe quelle valeur React dans la fenêtre de contexte du copilote à chaque tour. Ajoutez-le à côté de useState :
import { useCopilotReadable } from "@copilotkit/react-core";
// dans HomePage()
useCopilotReadable({
description: "La liste de tâches actuelle de l'utilisateur, avec titre et statut",
value: tasks,
});C'est toute la plomberie nécessaire pour donner un accès en lecture au copilote. Rechargez la page et demandez : "Combien de tâches me reste-t-il ?" — il répondra correctement parce que tasks fait maintenant partie de chaque prompt.
Pourquoi décrire la valeur ? Le champ description est ce que le LLM voit dans son prompt système. Des descriptions vagues comme "data" produisent des réponses vagues. Des descriptions concrètes comme "la liste de tâches actuelle de l'utilisateur, avec titre et statut" produisent des appels d'outils précis.
Étape 6 : Définir des actions front-end avec useCopilotAction
Lire l'état n'est que la moitié du travail. Maintenant, on laisse le copilote modifier l'état. useCopilotAction est une fonction typée que le LLM peut appeler directement depuis le chat. Le framework gère le schéma JSON, le routage de l'outil et la réponse en streaming.
Ajoutez l'action addTask :
import { useCopilotAction } from "@copilotkit/react-core";
useCopilotAction({
name: "addTask",
description: "Ajouter une nouvelle tâche à la liste de l'utilisateur",
parameters: [
{
name: "title",
type: "string",
description: "Le titre de la nouvelle tâche",
required: true,
},
],
handler: async ({ title }) => {
const newTask: Task = {
id: crypto.randomUUID(),
title,
status: "todo",
};
setTasks((prev) => [...prev, newTask]);
return `Tâche ajoutée : ${title}`;
},
});Demandez maintenant au copilote : "Ajoute une tâche pour déployer la branche staging vendredi." Vous verrez le chat confirmer l'action, et la nouvelle tâche apparaît instantanément dans la liste. Sans rechargement, sans aller-retour API — l'action s'exécute dans l'arbre React.
Ajoutez une seconde action pour marquer les tâches comme terminées :
useCopilotAction({
name: "markTaskDone",
description: "Marquer une tâche comme terminée par son titre",
parameters: [
{
name: "title",
type: "string",
description: "Le titre (ou un fragment) de la tâche à terminer",
required: true,
},
],
handler: async ({ title }) => {
let matched: Task | undefined;
setTasks((prev) =>
prev.map((t) => {
if (
t.status === "todo" &&
t.title.toLowerCase().includes(title.toLowerCase())
) {
matched = t;
return { ...t, status: "done" };
}
return t;
}),
);
return matched
? `Terminée : ${matched.title}`
: `Aucune tâche correspondant à "${title}"`;
},
});Essayez : "Marque la relecture de PR comme terminée." Le copilote choisira la bonne tâche même sans le titre exact — c'est le useCopilotReadable de l'étape 5 qui rend ce filtrage flou possible.
Étape 7 : UI générative — afficher des cartes dans le chat
Les réponses en texte brut conviennent aux confirmations, mais le copilote peut aussi afficher du React arbitraire dans le chat. Passez une fonction render à votre action et la bulle de chat devient ce que vous retournez.
useCopilotAction({
name: "showTaskSummary",
description: "Afficher un résumé visuel des tâches en attente vs terminées",
parameters: [],
handler: async () => "résumé affiché",
render: () => {
const todo = tasks.filter((t) => t.status === "todo").length;
const done = tasks.filter((t) => t.status === "done").length;
return (
<div className="rounded-lg border bg-white p-4 shadow-sm">
<div className="mb-2 text-sm font-semibold text-gray-700">
Résumé des tâches
</div>
<div className="flex gap-4 text-sm">
<span className="text-blue-600">En attente : {todo}</span>
<span className="text-green-600">Terminées : {done}</span>
</div>
</div>
);
},
});Demandez : "Montre-moi un résumé." Au lieu d'un paragraphe, vous obtenez une carte stylisée intégrée au chat. Ce pattern monte en charge — vous pouvez afficher des graphiques Recharts, des cartes, des diffs de fichiers ou même des formulaires interactifs.
Étape 8 : Polir et thématiser la barre latérale
Les styles de la barre CopilotSidebar sont des variables CSS, donc vous pouvez correspondre à votre marque sans forker le composant. Ajoutez ceci dans globals.css :
:root {
--copilot-kit-primary-color: #0ea5e9;
--copilot-kit-contrast-color: #ffffff;
--copilot-kit-background-color: #ffffff;
--copilot-kit-secondary-color: #f4f4f5;
--copilot-kit-secondary-contrast-color: #18181b;
}Vous pouvez aussi passer un instructions à la barre pour donner au copilote une personnalité au niveau système :
<CopilotSidebar
instructions={`Tu es un copilote de productivité. Garde tes réponses sous trois phrases. Confirme toujours les actions destructives avant de les exécuter.`}
labels={{ title: "Copilote de tâches" }}
/>Tester votre implémentation
Essayez ces prompts contre le serveur de dev. Les quatre devraient fonctionner de bout en bout :
- "Qu'y a-t-il sur ma liste ?" — répondu via
useCopilotReadable - "Ajoute une tâche pour renouveler le certificat SSL." — appelle
addTask, la liste grandit - "Marque celle du SSL comme terminée." — appelle
markTaskDoneavec recherche floue - "Montre-moi un résumé." — affiche la carte UI générative
Si l'un d'eux échoue, ouvrez l'onglet Network. Chaque tour du copilote frappe POST /api/copilotkit. La réponse est un corps JSON streamé — s'il renvoie 500, votre OPENAI_API_KEY est manquante ou le nom du modèle est incorrect.
Dépannage
La barre s'affiche mais les messages ne font rien. Votre runtimeUrl ne correspond pas à la route. Vérifiez que runtimeUrl="/api/copilotkit" correspond au chemin du fichier app/api/copilotkit/route.ts.
Module not found: @copilotkit/react-ui/styles.css. Vous avez oublié d'importer la feuille de style dans CopilotProvider.tsx. Sans elle, la barre s'affiche sans style.
L'action se déclenche mais l'état ne se met jamais à jour. Vous avez probablement défini l'action en dehors du composant qui possède l'état. useCopilotAction doit s'exécuter dans le même arbre de composants que le useState qu'il modifie.
Erreur de build : Unexpected identifier 'assert'. Vous êtes sur Node 18. Passez à Node 20 avec nvm use 20.
Étapes suivantes
Vous avez maintenant un vrai copilote qui lit et écrit l'état de l'app. Extensions naturelles :
- Actions côté serveur : Passez
actions: [...]àCopilotRuntimepour exposer des mutations base de données que le LLM peut appeler sans passer par le navigateur. - Flux multi-agents : Combinez CopilotKit avec LangGraph pour des agents avec état et branches qui maintiennent la barre latérale sur plusieurs tours.
- Streaming Markdown : Remplacez les handlers de texte par des réponses streamées pour que les longues réponses s'affichent mot par mot.
- Auth + contexte par utilisateur : Lisez la session dans votre handler de route et injectez des readables limités à l'utilisateur (ID workspace, rôle) dans le runtime.
Pour des patterns IA plus larges, le tutoriel Claude Agent SDK couvre le côté agent back-end, et le tutoriel RAG agentique avec Vercel AI SDK couvre les copilotes augmentés par récupération.
Conclusion
CopilotKit comble l'écart entre "chatbot agrafé sur un site" et "IA qui opère le produit". Trois primitives portent toute l'expérience : le provider, useCopilotReadable pour l'état partagé, et useCopilotAction pour les outils typés. Le reste — la barre latérale, l'UI générative, les adaptateurs de modèles — c'est du polissage.
Livrez un copilote par surface de produit, gardez les readables spécifiques, gardez les actions étroites, et vos utilisateurs commenceront à traiter le chat comme un clavier plus rapide plutôt que comme un service d'aide.