Aller au contenu principal

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 useState et useEffect
  • 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


Pourquoi ajouter une question ouverte dans un quiz ?


Quel outil allons-nous utiliser pour analyser une réponse libre ?


Quelle méthode permet d’envoyer une requête HTTP en JavaScript ?


Quel est le rôle de `useState()` dans React ?


Que signifie le préfixe 'Bearer' dans les headers HTTP ?


Pourquoi est-il dangereux d'exposer une clé API dans le code côté client ?


Quelle est la meilleure façon d'utiliser une clé API dans une application Next.js ?


Quelle méthode empêche l'accès direct à une clé API depuis le navigateur ?


Que se passe-t-il si une clé API est exposée publiquement sur GitHub ?


Quelles sont les bonnes pratiques pour sécuriser une clé API ?


Quelles erreurs de sécurité sont souvent commises avec les clés API ?


Quelle est la conséquence d’avoir une variable d’environnement nommée `NEXT_PUBLIC_API_KEY` ?


Pourquoi utiliser une API route dans Next.js pour accéder à une clé API ?


Pourquoi ne faut-il pas utiliser `NEXT_PUBLIC_` pour une clé API privée ?


Quel fichier permet de créer une API route dans Next.js App Router ?


Quel est le rôle de `process.env.OPENROUTER_API_KEY` ?



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

  1. Vérifier si question.reponses est vide
  2. Si oui, afficher <QuestionOuverte /> avec la question passée en prop (question.intitule)
  3. 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 à handleClick si cette fonction accepte une valeur par défaut. Sinon, adapte en fonction de ce que ton hook useQuiz attend.


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 :

  1. Allez dans votre projet > Settings > Environment Variables

  2. Ajoutez une variable :

    • Name : OPENROUTER_API_KEY
    • Value : votre vraie clé
    • Environment : Production (et Preview si vous voulez tester avant)
  3. 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 handleQuestionOuverteSubmit est 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
Rappel de sécurité important

Il ne faut pas afficher la clé API côté client mais utiliser un backend sécurisé.