Utiliser l'IA
Utiliser l’intelligence artificielle pour analyser une réponse libre à une question de cybersécurité
Notions théoriques
Pourquoi une question ouverte ?
Jusqu’à présent, notre quiz Cyber permettait de répondre à des questions à choix multiples (QCM).
Ce format est très pratique pour :
- Corriger automatiquement
- Donner un score rapide
- Proposer une expérience fluide
Mais il ne permet pas de vérifier la compréhension réelle de l’élève.
C’est pourquoi nous allons ajouter une question ouverte, où l’utilisateur doit écrire une réponse avec ses propres mots.
Comment corriger automatiquement ?
Corriger une réponse ouverte est plus complexe qu’un QCM.
Il faut :
- Analyser le sens de la réponse
- Déterminer si elle est correcte ou non
- Fournir un feedback personnalisé
Pour cela, nous allons faire appel à un modèle d’intelligence artificielle via une API.
Quelle API utiliser ?
Nous allons utiliser OpenRouter, une alternative gratuite à OpenAI.
- OpenRouter permet d’accéder à des modèles comme GPT-3.5 ou GPT-4
- Il est gratuit dans sa version de base
- Il fonctionne avec une clé API que vous pouvez obtenir facilement
Comment fonctionne l’API OpenRouter ?
L’API d’OpenRouter fonctionne comme celle d’OpenAI :
- Vous envoyez un prompt (texte d’instruction)
- Vous recevez une réponse générée par l’IA
Nous allons créer un prompt du type :
"Voici une question de cybersécurité. Évalue la réponse de l'utilisateur et dis-moi si elle est correcte. Puis donne un commentaire personnalisé."
Objectif pédagogique
Grâce à cette fonctionnalité, vous allez :
- Comprendre comment utiliser une API externe avec
fetch - Vous perfectionner dans la création de composants React avec
useStateetuseEffect - Découvrir le traitement de texte intelligent avec l’IA
Exemple pratique
Voici un exemple de composant React qui utilise OpenRouter pour analyser une réponse libre :
"use client";
import { useState } from "react";
export default function QuestionOuverte() {
const [reponse, setReponse] = useState("");
const [resultat, setResultat] = useState("");
async function analyserReponse() {
const prompt = `
Tu es un professeur de cybersécurité.
Voici la question : "Qu'est-ce qu'une attaque de phishing ?"
Voici la réponse de l'élève : "${reponse}"
Ta mission : dis si c'est correct ou non, et donne un commentaire personnalisé.
Réponds en une phrase.
`;
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": "Bearer VOTRE_CLE_API_OPENROUTER",
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "mistralai/mistral-7b-instruct", // ou "openai/gpt-3.5-turbo"
messages: [{ role: "user", content: prompt }]
})
});
const data = await response.json();
setResultat(data.choices[0].message.content);
}
return (
<div className="max-w-xl mx-auto mt-10">
<h2 className="text-2xl font-bold mb-4">Question ouverte</h2>
<p className="mb-2">Qu'est-ce qu'une attaque de phishing ?</p>
<textarea
className="w-full border p-2 rounded"
rows={4}
value={reponse}
onChange={(e) => setReponse(e.target.value)}
/>
<button
onClick={analyserReponse}
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded"
>
Envoyer ma réponse
</button>
{resultat && (
<div className="mt-6 p-4 bg-gray-100 rounded border">
<strong>Analyse de l'IA :</strong>
<p>{resultat}</p>
</div>
)}
</div>
);
}
Quelques méthodes à connaître
1. fetch()
fetch()pour appeler une API externe
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", { ... });
2. JSON.stringify()
JSON.stringify()pour envoyer un objet JSON
3. Authorization: Bearer ...
Authorization: Bearer <jeton>pour s’authentifier avec une clé d'API.
Le préfixe Bearer dans l'en-tête HTTP Authorization indique que le schéma d'authentification utilisé est un token d'accès de type Bearer.
Que signifie le terme Bearer en informatique ?
- Bearer signifie "porteur" en anglais.
En informatique, un bearer token est un jeton d'accès qui accorde des droits à quiconque le possède (le "porteur"), sans nécessiter de preuve supplémentaire de possession (comme une clé cryptographique).
Toute personne ou entité présentant ce jeton peut l'utiliser pour accéder aux ressources protégées.
Voici comment l'utiliser dans une requête HTTP :
Authorization: Bearer <jeton>
Exemple :
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.xxxxx
- Très répandu dans les API REST modernes (ex. : Google APIs, GitHub, etc.).
- Le token est généralement obtenu après une authentification (login), et transmis ensuite pour chaque requête protégée.
- Sécurité importante : Les bearer tokens doivent toujours être transmis via HTTPS (TLS), car quiconque intercepte le jeton peut l'utiliser.
Bonne pratique : sécuriser la clé API
Il ne faut jamais afficher une clé API côté client, car le code exécuté dans le navigateur est visible par tous.
Une clé exposée peut être copiée et utilisée par des personnes malveillantes, entraînant des frais ou des abus.
Pour protéger cette clé, on doit l’utiliser uniquement dans du code exécuté côté serveur (comme une API route dans Next.js).
Test de mémorisation/compréhension
TP pour réfléchir et résoudre des problèmes
Objectif
Créer un composant QuestionOuverte qui :
- Affiche une question ouverte
- Permet à l’utilisateur d’écrire une réponse
- Envoie la réponse à une API route côté serveur
- Cette API appelle OpenRouter (GPT-3.5) pour analyser la réponse
- Affiche un commentaire personnalisé généré par l’IA
1. Créer un compte chez OpenRouter
Voici comment créer un compte gratuit sur https://openrouter.ai :
Phase n°1 – Accès à la plateforme
Rendez-vous sur https://openrouter.ai puis cliquez sur le bouton « Sign up » pour lancer la création du compte.
Phase n°2 – Saisie des informations d’identification
Entrez votre adresse email dans le champ dédié. Choisissez un mot de passe robuste (au moins 8 caractères, incluant majuscules, minuscules, chiffres et symboles). Validez les conditions d’utilisation et la politique de confidentialité, puis cliquez sur « Continue ».
Phase n°3 – Vérification de l’email
Un mail de confirmation vous est envoyé. Ouvrez-le et cliquez sur le lien de validation afin d’activer votre compte.
Phase n°4 – Connexion et configuration initiale
Retournez sur le site et connectez-vous avec l’email et le mot de passe que vous venez de créer. Remplissez les informations de profil (nom, etc...) puis validez.
2. Créer le composant QuestionOuverte
Créez le fichier suivant :
/components/QuestionOuverte.tsx
Et collez ce code :
// components/QuestionOuverte.tsx
"use client";
import { useState } from "react";
type Props = {
questionTexte: string;
onSubmit: (resultatIA: string, reponseUtilisateur: string) => void;
};
export default function QuestionOuverte({ questionTexte, onSubmit }: Props) {
const [reponse, setReponse] = useState("");
const [loading, setLoading] = useState(false);
const [erreur, setErreur] = useState("");
async function analyserReponse() {
setLoading(true);
setErreur("");
try {
const prompt = `
Tu es un professeur de cybersécurité.
Voici la question : "${questionTexte}"
Voici la réponse de l'élève : "${reponse}"
Ta mission : dis si c'est correct ou non, et donne un commentaire personnalisé.
Réponds en une phrase.
`;
const res = await fetch("/api/openrouter", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ prompt })
});
const data = await res.json();
const resultatIA = data.choices?.[0]?.message?.content || "Analyse indisponible.";
// Appel de la fonction de rappel
onSubmit(resultatIA, reponse);
} catch (error) {
setErreur("Erreur lors de l'analyse de la réponse.");
} finally {
setLoading(false);
}
}
return (
<div className="max-w-xl mx-auto mt-10">
<h2 className="text-2xl font-bold mb-4">Question ouverte</h2>
<p className="mb-2">{questionTexte}</p>
<textarea
className="w-full border p-2 rounded"
rows={4}
value={reponse}
onChange={(e) => setReponse(e.target.value)}
placeholder="Écris ta réponse ici..."
/>
<button
onClick={analyserReponse}
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded"
disabled={loading || reponse.trim() === ""}
>
{loading ? "Analyse en cours..." : "Envoyer ma réponse"}
</button>
{erreur && (
<p className="text-red-600 mt-4">{erreur}</p>
)}
</div>
);
}
3. Créer une API route sécurisée
Créez le fichier suivant :
/app/api/openrouter/route.ts
Et collez ce code :
import { NextResponse } from "next/server";
export async function POST(req: Request) {
const { prompt } = await req.json();
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.OPENROUTER_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "openai/gpt-3.5-turbo",
messages: [{ role: "user", content: prompt }]
})
});
const data = await response.json();
return NextResponse.json(data);
}
4. Ajouter la clé API dans .env.local
Ajoutez la variable OPENROUTER_API_KEY dans votre fichier .env.local (à la racine de votre projet) :
OPENROUTER_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx
⚠️ Ne mettez pas
NEXT_PUBLIC_devant cette variable, sinon elle sera exposée côté client.
5. Modifier app/page.tsx
Nous voulons afficher le composant <QuestionOuverte /> à la place de <QuizCard /> lorsque la variable question ne contient pas de réponses (par exemple, pour une question ouverte plutôt qu’un QCM) :
- Vérifier si
question.reponsesest vide - Si oui, afficher
<QuestionOuverte />avec la question passée en prop (question.intitule) - Sinon, afficher
<QuizCard />comme actuellement
Notre variable question ressemble à ceci :
{
id: 1,
intitule: "Qu'est-ce qu'une attaque de phishing ?",
reponses: [
{ id: 1, texte: "..." },
{ id: 2, texte: "..." }
]
}
Donc si question.reponses.length === 0, c’est une question ouverte
et on affiche un composant <QuestionOuverte /> qui permet à l’utilisateur de soumettre une réponse ouverte.
Cette réponse doit ensuite être traitée par une fonction handleQuestionOuverteSubmit qui déclenche le passage à la question suivante (comme le fait handleClick pour les QCM).
Modification n°1 : Importer le composant
Ajoute en haut du fichier app/page.tsx :
import QuestionOuverte from "@/components/QuestionOuverte";
Modification n°2 : Ajouter une fonction handleQuestionOuverteSubmit
Toujours dans app/page.tsx, ajoute cette fonction dans le composant Home, au même niveau que handleClick :
function handleQuestionOuverteSubmit(reponse: string) {
// Optionnel : envoyer la réponse à une API ou l'analyser ici
// Ensuite, déclencher la logique de passage à la question suivante
handleClick(null); // ou handleClick("ouvert") selon ta logique
}
Remarque : ici on passe
nullàhandleClicksi cette fonction accepte une valeur par défaut. Sinon, adapte en fonction de ce que ton hookuseQuizattend.
Modification n°3 : Modifier le rendu conditionnel
Remplace ce bloc dans le return de Home :
{questions.length > 0 && question ? (
<QuizCard
question={question}
afficherExplication={afficherExplication}
explication={explication}
onAnswerClick={handleClick}
/>
) : (
<p className="text-center mt-6">Chargement de la question...</p>
)}
Par ce bloc conditionnel :
{questions.length > 0 && question ? (
question.reponses?.length > 0 ? (
<QuizCard
question={question}
afficherExplication={afficherExplication}
explication={explication}
onAnswerClick={handleClick}
/>
) : (
<QuestionOuverte
questionTexte={question.intitule}
onSubmit={handleQuestionOuverteSubmit}
/>
)
) : (
<p className="text-center mt-6">Chargement de la question...</p>
)}
Exemple de code complet mis à jour dans app/page.tsx pour intégrer QuestionOuverte
"use client";
import { useEffect, useState } from "react";
import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert";
import FormulaireJoueur from "@/components/FormulaireJoueur";
import Score from "@/components/Score";
import QuizCard from "@/components/QuizCard";
import QuizResult from "@/components/QuizResult";
import QuestionOuverte from "@/components/QuestionOuverte";
import { useQuiz } from "../hooks/useQuiz";
import { quizService } from "../services/quizService";
export default function Home() {
const [joueurNom, setJoueurNom] = useState("");
const [joueurPret, setJoueurPret] = useState(false);
const [analyseIA, setAnalyseIA] = useState("");
const {
questions,
question,
questionIndex,
explication,
afficherExplication,
score,
debut,
quizTermine,
handleClick
} = useQuiz();
useEffect(() => {
if (joueurPret) {
const userId = localStorage.getItem("supabase_user_id");
if (userId) {
quizService.getJoueur(userId)
.then(data => {
if (data) {
setJoueurNom(data.pseudo);
}
})
.catch(error => {
console.error("Erreur lors de la récupération du joueur :", error);
});
}
}
}, [joueurPret]);
function handleQuestionOuverteSubmit(resultatIA: string, reponseUtilisateur: string) {
// Optionnel : enregistrer la réponse et l'analyse dans une base de données
console.log("Réponse utilisateur :", reponseUtilisateur);
console.log("Analyse IA :", resultatIA);
// Afficher l'explication IA (si nécessaire)
setAnalyseIA(resultatIA);
// Passer à la question suivante
handleClick(null); // ou passer une valeur spécifique selon ton implémentation
}
if (quizTermine) {
return (
<QuizResult
score={score}
totalQuestions={questions.length}
debut={debut}
joueurNom={joueurNom}
/>
);
}
return (
<div>
{!joueurPret ? (
<div>
<Alert className="bg-blue-50 border-blue-300 text-blue-800 max-w-xl mx-auto mt-6">
<AlertTitle className="text-xl font-semibold">Bienvenue sur CyberQuiz</AlertTitle>
<AlertDescription>
Un quiz pour tester vos connaissances en cybersécurité.
</AlertDescription>
</Alert>
<FormulaireJoueur onJoueurCree={() => setJoueurPret(true)} />
</div>
) : (
<div>
<Alert className="bg-green-50 border-green-300 text-green-800 max-w-xl mx-auto mt-6">
<AlertTitle className="text-xl font-semibold">
Bienvenue {joueurNom} !
</AlertTitle>
<AlertDescription>
Préparez-vous à tester vos connaissances en cybersécurité.
</AlertDescription>
</Alert>
<Score actuel={score} total={questions.length} />
{questions.length > 0 && question ? (
question.reponses?.length > 0 ? (
<QuizCard
question={question}
afficherExplication={afficherExplication}
explication={explication}
onAnswerClick={handleClick}
/>
) : (
<QuestionOuverte
questionTexte={question.intitule}
onSubmit={handleQuestionOuverteSubmit}
/>
)
) : (
<p className="text-center mt-6">Chargement de la question...</p>
)}
{analyseIA && (
<div className="max-w-xl mx-auto mt-6 p-4 bg-gray-100 border rounded">
<strong>Analyse de l'IA :</strong>
<p>{analyseIA}</p>
</div>
)}
</div>
)}
</div>
);
}
6. Déployer sur Vercel
Lorsque vous déployez sur https://vercel.com :
-
Allez dans votre projet > Settings > Environment Variables
-
Ajoutez une variable :
- Name :
OPENROUTER_API_KEY - Value : votre vraie clé
- Environment :
Production(etPreviewsi vous voulez tester avant)
- Name :
-
Redéployez votre projet
Résultat attendu
- Si la question contient des réponses (QCM), le composant
<QuizCard />est affiché - Si la question est ouverte (
reponses.length === 0), le composant<QuestionOuverte />est affiché - L’utilisateur écrit une réponse libre
- Lorsque l’utilisateur soumet sa réponse ouverte, la fonction
handleQuestionOuverteSubmitest appelée - L’IA analyse la réponse via une API route sécurisée
- Un commentaire personnalisé s’affiche
- Cette fonction déclenche
handleClick()pour passer à la question suivante
Il ne faut pas afficher la clé API côté client mais utiliser un backend sécurisé.