Tutorial: Rendre_un_exercice_interactif_complet

Rendre_un_exercice_interactif_complet

MathAlea permet de rendre un exercice interactif de deux façons :

  • en ligne : l'utilisateur saisit une réponse (dans un champ ou coche une case), celle-ci est vérifiée automatiquement et son score peut éventuellement être récupéré par son professeur.
  • sur papier : principalement sous forme de QCM ou de réponses numériques à coder mais pas que, les copies des élèves peuvent être scannées et corrigées automatiquement via ce qu'on appele communément AMC pour Auto Multiple Choice et dont vous pouvez trouver un guide dans le panneau de gauche.

Dans les deux cas, pour rendre un exercice interactif, il faut deux chose :

  1. Faire charger le nécessaire.
  2. Définir la correction et le feedback éventuel.

Remarque : Le moyen le plus simple de rendre un exercice interactif est d'utiliser MathLive, vous pouvez parcourir cette page pour voir les autres types d'interactivité ou y aller directement.

# 1.a Faire charger le nécessaire pour rendre un exercice en ligne interactif

Pour faire charger le nécessaire, il faut ajouter ces lignes juste après les import de début d'exercice :

export const interactifReady = true // pour définir qu'exercice peut s'afficher en mode interactif.
export const interactifType = 'typeInteractivite'

'typeInteractivite' peut être :

  • 'qcm' pour avoir un qcm. Exemple : 5L10-2
  • 'numerique' pour avoir une réponse numérique. Exemple : 5R22
  • 'mathLive'pour avoir un champ avec clavier et vérification d'égalité formelle. Exemple : 4C10-4
  • 'cliqueFigure' pour choisir une figure. Exemple : 6G10-3
  • 'listeDeroulante' permet d'avoir à choisir une réponse parmi différentes options d'une liste déroulante. Exemple : 6N43-4
  • 'custom' pour appeler la fonction this.correctionInteractive() définie dans l'exercice. Exemple : 6N11-2

Remarque : On peut utiliser this.interactif = false pour définir le mode dans lequel l'exercice va s'afficher par défaut (on le place avec les autres réglages par défaut de l'exercice entre Exercice.call(this) et this.nouvelleVersion = function)

# 1.b Faire charger le nécessaire pour rendre un exercice utilisable avec AMC

Pour faire charger le nécessaire, il faut ajouter ces lignes juste après les import de début d'exercice :

export const amcReady = true // pour définir que l'exercice peut servir à AMC
export const amcType = 'typeAMC'

'typeAMC' peut être l'une des valeurs suivantes :

  • 'qcmMono' : qcm avec une seule bonne réponse (évolution vers le bouton radio ?). Exemple : 6C10-2
  • 'qcmMult' : qcm avec possibilité de plusieurs bonnes réponses. Exemple : 6N43-2
  • 'AMCOpen' : question ouverte -> il n'y a pas d'interactivité, l'affichage est classique par contre on peut l'exporter vers AMC en question ouverte (ajout EE : avec la possibilité d'afficher ou pas le cadre de saisie qui est ainsi inutile en géométrie). Exemple : 6C10-5 (et 6G12-1 pour enlever le cadre)
  • 'AMCNum' : réponse numérique à entrer dans un formulaire texte. AmcNumeriqueChoice (voire attribut reponse). Exemple : 6C10
  • 'AMCOpenNum' : réponse identique au type 'AMCNum' mais AMC ajoute une zone pour une réponse ouverte. Exemple : 3G30
  • 'AMCOpenNum✖︎2' : identique à 'AMCOpenNum' avec deux réponses numériques (reponse et reponse2). Exemple : 4C21
  • 'AMCOpenNum✖︎3' : identique à 'AMCOpenNum' avec trois réponses numériques (reponse, reponse2 et reponse3). Exemple : 3L11-1
  • 'custom' : Ces exercices ne sont pas prédéfinis, ils partagent le bouton de validation puis appellent la méthode correctionInteractive() définie dans l'exercice. Ils ne sont pas compatibles avec AMC

# 2. Définir la correction et le feedback éventuel

La définition de la correction ainsi que celle du feedback éventuel se font via la variable this.autoCorrection

this.autoCorrection // doit contenir un tableau d'objets avec autant d'éléments qu'il y a de répétitions de l'énoncé (this.nbQuestions).
this.autoCorrection[0] // définit la première question
this.autoCorrection[1] // definit la deuxième question et ainsi de suite.

Selon les types, this.autoCorrection s'adapte :

#

  • types 'qcm' (Interactif), 'qcmMono' et 'qcmMult' (AMC) : (à la différence des deux autres, 'qcmMono' ne peut avoir qu'un seul statut à true)
this.autoCorrection[i] = {
  enonce: 'la question est posée ici',
  propositions: [
    {
      texte: 'ce qui est écrit à droite de la case à cocher',
      statut: // true ou false pour indiquer si c'est une bonne réponse (true),
      feedback: 'message' // qui s'affichera si la réponse est juste ou s'il n'y a qu'une erreur
    },
    {
      texte: 'deuxième proposition',
      statut: //true ou false,
      feedback: '...'
    },
    {

    } //.... autant de fois qu'il y a de propositions dans le qcm
  ],
  options: {
    ordered: true // (si les réponses doivent rester dans l'ordre ci-dessus, false s'il faut les mélanger),
    lastChoice: index // (en cas de mélange, l'index à partir duquel les propositions restent à leur place, souvent le dernier choix par défaut)
  }
}

#

  • type 'AMCOpen' (AMC) : ici un exemple pour une exercice ne produisant qu'une question (il y aura autant d'objets que this.nbQuestion > 1)
this.autoCorrection = [
  { 
    enonce: 'ici la question est posée'
    propositions: [
      { 
        texte: 'Ce qui apparaitra sur le corrigé',
        statut: 3 // (ici c'est le nombre de lignes du cadre pour la réponse de l'élève sur AMC)
        feedback: '',
        enonce: 'Texte écrit au dessus ou avant les cases à cocher' // EE : ce champ est facultatif et fonctionnel qu'en mode hybride (en mode normal, il n'y a pas d'intérêt)
        sanscadre : false // EE : ce champ est facultatif et permet (si true) de cacher le cadre et les lignes acceptant la réponse de l'élève
    
      }
    ]
  }
]

#

  • types 'numerique' (Interactif) , 'AMCNum', 'AMCOpenNum' et 'AMCOpenNum✖︎2' (AMC) : ('AMCOpenNum✖︎2' contient aussi un attribut reponse2 au fonctionnement identique à celui de l'attribut reponse ci-dessous)
this.autoCorrection[i] = {
  enonce: 'ici la question est posée',
  propositions: [
    {
      texte: 'ce qui est affiché dans le corrigé AMC',
      statut: nombreDeRéponsesNumériques,
      feedback: ''
      }
  ],
    reponse: {
      texte: 'le texte affiché au dessus du formulaire numerique dans AMC', //facultatif
      valeur: nombre, // obligatoire (la réponse numérique à comparer à celle de l'élève), NE PAS METTRE DE STRING à virgule ! 4.9 et non pas 4,9
      alignement: 'flushleft' // EE : ce champ est facultatif et n'est fonctionnel que pour l'hybride. Il permet de choisir où les cases sont disposées sur la feuille. Par défaut, c'est comme le texte pqui le précède. Pour mettre à gauche, au centre ou à droite, choisir parmi ('flushleft', 'center', 'flushright').
      param: {
        digits: 3, // obligatoire pour AMC (le nombre de chiffres pour AMC, si digits est mis à 0, alors il sera déterminé pour coller au nombre décimal demandé)
        decimals: 0, // facultatif. S'il n'est pas mis, il sera mis à 0 et sera déterminé automatiquement comme décrit ci-dessus
        signe: false, // (présence d'une case + ou - pour AMC)
        exposantNbChiffres: 0, // facultatif (présence de x10^ pour AMC si >0 c'est le nombre de chiffres pour l'exposant)
        exposantSigne: false, // (présence d'une case + ou - pour l'exposant précédent)
        approx: 0 // (0 = valeur exacte attendue, sinon valeur de tolérance... voir AMC)
      }
    }

#

  • type 'AMCHybride' (AMC) : Dans ce type, chaque question-réponse peut avoir un type différent. Il y a seul énoncé, une seule correction et plusieurs champs question-réponse (il faudra donc numéroter les questions dans l'énoncé).
this.autoCorrection[i] = {
  enonce: 'ici la (ou les) question(s) est(sont) posée(s)',
  enonceAvant: true, //EE : ce champ est facultatif et permet (si false) de supprimer l'énoncé ci-dessus avant la numérotation de la question. 
  propositions: [
    {
      type: type1, // on donne le type de la première question-réponse qcmMono, qcmMult, Num...
      propositions : [ // une ou plusieures(Qcms) 'propositions'
        {
          texte: // Facultatif. la proposition de Qcm ou ce qui est affiché dans le corrigé pour cette question quand ce n'est pas un Qcm ,
          statut: // true au false(Qcms)
          feedback: ''

        }
      ],
      reponse: { // utilisé si type = 'Num'
        texte: 'le texte affiché au dessus du formulaire numerique dans AMC', //facultatif
        valeur: nombre, // obligatoire (la réponse numérique à comparer à celle de l'élève), NE PAS METTRE DE STRING à virgule ! 4.9 et non pas 4,9
        param: {
          digits: 3, // obligatoire pour AMC (le nombre de chiffres pour AMC, si digits est mis à 0, alors il sera déterminé pour coller au nombre décimal demandé)
          decimals: 0, // facultatif. S'il n'est pas mis, il sera mis à 0 et sera déterminé automatiquement comme décrit ci-dessus
          signe: false, // (présence d'une case + ou - pour AMC)
          exposantNbChiffres: 0, // facultatif (présence de x10^ pour AMC si >0 c'est le nombre de chiffres pour l'exposant)
          exposantSigne: false, // (présence d'une case + ou - pour l'exposant précédent)
          approx: 0 // (0 = valeur exacte attendue, sinon valeur de tolérance... voir AMC)
        }
      }
      options: {ordered: false, lastChoice: false} // options pour Qcms
    },
    {
      type: type2, // on donne le type de la deuxième question-réponse qcmMono, qcmMult, Num...
      proposition : [ // une ou plusieures(Qcms) 'propositions'
        {
          texte: // la proposition de Qcm ou ce qui est affiché dans le corrigé pour cette question quand ce n'est pas un Qcm ,
          statut: // true au false(Qcms) ,
          feedback: ''
        }
      ],
      reponse: { // utilisé si type = 'Num'
        texte: 'le texte affiché au dessus du formulaire numerique dans AMC', //facultatif
        valeur: nombre, // obligatoire (la réponse numérique à comparer à celle de l'élève), NE PAS METTRE DE STRING à virgule ! 4.9 et non pas 4,9
        param: {
          digits: 3, // obligatoire pour AMC (le nombre de chiffres pour AMC, si digits est mis à 0, alors il sera déterminé pour coller au nombre décimal demandé)
          decimals: 0, // facultatif. S'il n'est pas mis, il sera mis à 0 et sera déterminé automatiquement comme décrit ci-dessus
          signe: false, // (présence d'une case + ou - pour AMC)
          exposantNbChiffres: 0, // facultatif (présence de x10^ pour AMC si >0 c'est le nombre de chiffres pour l'exposant)
          exposantSigne: false, // (présence d'une case + ou - pour l'exposant précédent)
          approx: 0 // (0 = valeur exacte attendue, sinon valeur de tolérance... voir AMC)
        }
      },
      options: {ordered: false, lastChoice: false} // options pour Qcms
    } // et ainsi de suite pour toutes les question-reponse
  ]
}

#

  • type 'listeDeroulante' (Interactif) : Ici, l'utilisateur devra sélectionner une réponse dans un menu déroulant dont les différentes options sont définies par la fonction choixDeroulant et dont les bonnes réponses sont définies par la fonction setReponse à importer toutes les deux de '../../modules/gestionInteractif.js' (voir ex. 6N43-4)
texte = 'Choisir une bonne réponse parmi ' + choixDeroulant(this, i, 0, [a, b, c, d]) // Si on veut avoir plusieurs menus déroulants dans la même question, il suffit d'incrémenter le troisième paramètre comme choixDeroulant(this, i, 1, [a, b, c, d]) (voir ex. 6N43-4)
texteCorr = `Les bonnes réponses sont ${a} et ${d}.`
setReponse(this, i, [a, d]) // S'il y a plusieurs menus déroulants, le troisième paramètre peut être une liste de listes comme setReponse(this, i, [[a, d], [c, d])

# Les fonctions

Pour gérer l'interactivité Rémi Angot a implémenté quelques fonctions dont l'appel permet de générer le code nécessaire facilement.

Ce sont toutes les deux des fonctions de gestionInteractif.js, si vous voulez faire appel à elles, il faut alors faire en début de fichier :

import { setReponse, propositionsQcm } from '../../modules/gestionInteractif.js'
function setReponse (this, i, a, {digits = 0, decimals = 0, signe = false, exposantNbChiffres = 0, exposantSigne = false, approx = 0} = {})

Cette fonction permet de fixer une réponse numérique à une exercice interactif/AMC de type 'numerique', 'mathLive', 'AMCNum' ou 'AMCOpenNum'.

(à développer une fonction setReponses() qui fixe les réponses des exercices de type 'AMCOpenNum✖︎2' ou 'AMCOpenNum✖︎3'.

Les trois premiers arguments sont obligatoires : l'exercice appelant (this), l'index de la question (i), une réponse numérique (a).

Si on attend une réponse négative, ne pas oublier de mettre le paramètre signe: true ! (qui est nécessaire pour la sortie AMC) Écrire par exemple setReponse(this, i, reponse, { signe: true })

Le quatrième est facultatif et ne sert que pour AMC (des valeurs par défaut seront mises garantissant un fonctionnement correct dans la plupart des cas : la fonction d'export AMC calculera le nombre de chiffres à coder à partir de la réponse).

Attention :

setReponse crée autant d'enregistrements dans autoCorrection qu'il y a de champs donc de questions. Or dans ce cas, la version AMC regroupe toutes les questions en une seule. Comme je récupérais le nombre d'enregistrements de autoCorrection pour ce paramètre, j'avais 4 à la place de 1. La solution est donc de n'effectuer le setReponse que si l'on n'est pas en contexte AMC

function propositionsQcm (this, i)

Cette fonction va, à chaque appel, retourner un objet { texte, texteCorr } qui contient les propositions faites pour le qcm avec leur case à cocher pour l'énoncé (texte) et pour la correction (texteCorr).

Si le texte est toujours utilisé, on préférera souvent la correction classique au texteCorr retourné par cette fonction (à réfléchir : pourquoi ne pas activer la correction classique avec le bouton 'correction détaillée' ?)

Attention :

Pour les interactifs de type qcm avec brassage des réponses (option ordered = false), il est très important de n'appeler qu'une seule fois la fonction propositionsQcm() !

Comme propositionsQcm() produit un objet {texte, texteCorr} à chaque appel, si on l'appelle 2 fois, on brasse 2 fois les propositions, et l'ordre des réponses (texteCorr et checkReponse) n'est pas le même que celui qui est affiché et donc celui sur lequel on clique.

Donc, ne surtout pas faire :

texte = enonce + propositionsQcm(this,i).texte
texteCorr = propositionsQcm(this,i).texteCorr

Mais il faut faire :

monQcm=propositionsQcm(this,i)
texte = enonce + monQcm.texte
texteCorr = monQcm.texteCorr

# MathLive

Nous n'avons pas encore parlé du type d'interactivité 'mathLive' qui est pourtant très pratique ! et pas très compliqué à mettre en place comme nous allons le voir :

# Dans le cas d'un exercice normal

Pour rendre un exercice interactif en utilisant MathLive, il suffit de :

  1. Placer en en-tête :
import { setReponse, ajouteChampTexteMathLive } from '../../modules/gestionInteractif.js'
export const interactifReady = true
export const interactifType = 'mathLive'
  1. mettre dans la boucle principale setReponse(this, i, maRéponse) avec maRéponse un string LaTeX ou une valeur numérique (donc sans texNombre ou des équivalents)
  2. faire texte += ajouteChampTexteMathLive(this, i) pour ajouter le champ de réponse.

Par défaut, on compare des expressions littérales ou des nombres. #

  • Pour comparer des textes sans traitement, on fait setReponse(this, i, '+', { formatInteractif: 'texte' }). La réponse doit être saisie sans les $ délimiteurs du LaTeX.
  • Pour comparer des fractions et attendre exactement une forme, on fait setReponse(this, i, '+', { formatInteractif: 'fraction' }) et la réponse doit être un objet fraction (créé avec new Fraction(a, b))
  • Pour comparer des fractions, on peut aussi faire setReponse(this, i, new Fraction(n, d), { formatInteractif: 'fractionPlusSimple' }) et la réponse doit être un objet fraction égale à la réponse mais avec un numérateur strictement inférieur (on compare les valeurs absolues).
  • Pour comparer des fractions, on peut aussi faire setReponse(this, i, new Fraction(n, d), { formatInteractif: 'fractionEgale' }) et la réponse doit être un objet fraction égale à la réponse.
  • Pour comparer des longueurs (ou des aires), on peut faire setReponse(this, i, new Grandeur(4, 'cm'), { formatInteractif: 'longueur' }) et personnaliser le champ texte avec ajouteChampTexteMathLive(this, i, 'longueur')

# Si une réponse correcte est considérée fausse

Le fonctionnement de MathLive peut parfois donner un résultat étonnant. Alors qu'on attend la réponse, "1h45min", verifieQuestionMathLive peut lui attendre "1h45\min" par exemple.

Si vous vous trouvez dans la situation où une réponse correcte est considérée fausse, voici la procédure à suivre :

  • Ouvrir l'inspecteur (CTRL+MAJ+C sur Firefox et Chrome, Command+Option+I sur Safari)
  • Sur l'onglet débugueur. Chercher dans l'onglet sources webpack/src/js/modules/gestionInteractifs.js
  • Mettre un point d'arrêt sur la ligne 95 juste après le let saisie = champTexte.value (clic droit sur 95 puis sur Ajouter un point d'arrêt)
  • Cliquer sur Actualiser
  • Saisir la réponse attendue dans le champ et valider la saisie
  • Mettre le curseur sur saisie pour visualiser la saisie qu'il a récupéré capture d'écran

Lien avec AMC : Si on a un setReponse(this,i, new Fraction(n,d),{formatInteractif: 'fraction'}), alors on peut mettre amcType = 'AMCNum' et ça passe automatiquement en un simili amcHybride avec 2 champs : un pour le numérateur, et un pour le dénominateur !

# Dans le cas d'un exercice simple fait pour utilisé dans une Course aux Nombre (this.typeExercice = 'simple')

Les exercices simples sont interactifs mathLive par défaut !

Il suffit de

  • Mettre votre énoncé dans this.question
  • Mettre votre correction dans this.correction
  • Mettre la réponse attendue dans this.reponse

Pour changer le format, il suffit de placer après le Exercice.call(this) :

  • this.formatInteractif = et de compléter avec un des formats vus ci-dessus : 'texte', 'fraction', 'fractionPlusSimple', 'fractionEgale', 'longueur' (voir /js/exercices/can/can6C15.js par exemple).
  • this.formatChampTexte = 'largeur10 inline' pour personnaliser le champTexte (10 % de la largeur sans retour à la ligne dans cet exemple)
  • this.optionsChampTexte = { texte: 'l = ', texteApres: ' cm'} permet d'avoir du texte avant et après le champTexte MathLive.

# Avoir deux champs de réponse sur une question, c'est possible !

Il suffit d'avoir un compteur indépendant du compteur i de la boucle qui augmente de 1 pour les questions à un champ de réponse et qui augmente de 2 pour les questions à deux champs de réponse.

Supposons qu'on nomme cet autre compteur j.

Au lieu de faire setReponse(this, i, maRéponse) et texte += ajouteChampTexteMathLive(this, i), il suffit de faire setReponse(this, j, maRéponse) et texte += ajouteChampTexteMathLive(this, j)

Le soucis, c'est que pour l'instant chaque formulaire rapporte un point au niveau du score. Il y aura donc des questions à 2 points et d'autres à 1 point.

# Remarques :

Pour une compatibilité entre les exercices interactifs en ligne et AMC,

il ne faut pas faire :

  • texte = propositionsQcm() ;
  • texte = ajouteChampTexteMathLive().

Mais il faut faire :

  • texte += propositionsQcm() ;
  • texte += ajouteChampTexteMathLive().

La raison se trouve ci-dessous :

Afin de ne pas se retrouver avec un code hors contexte, les fonctions propositionsQcm et ajouteChampTexteMathLive retournent des chaines vides lorsque le contexte est la sortie Latex ou le générateur AMC.

Il convient donc de ne pas utiliser l'affectation texte = ... mais la concaténation texte += ...

En effet, le texte initial de l'énoncé sert souvent tel quel pour les énoncés AMC. En cas d'affectation texte transmettrait une chaine vide comme énoncé pour AMC. Il en va de même pour l'utilisation de propositionsQcm() qui retourne un tableau avec deux chaines vides dans ce contexte de sortie AMC.

Si vous bricolez this.autoCorrection[i].reponse.valeur à la main pour AMC

Il faut intégrer que cette variable doit être un tableau dont on n'utilisera pour AMC que le premier élément.

Donc vous devez renseigner :

  • this.autoCorrection[i]reponse.valeur[0] <- Notez bien le [0] qui indique que c'est un tableau
  • this.autoCorrection[i]reponse.param.digits
  • this.autoCorrection[i]reponse.param.decimals

Sinon, vous pouvez laisser faire setReponse(this,i,reponses) en veillant à ce que le premier élément de réponses (ou le seul) soit un nombre décimal (6.543 par exemple). setReponse crée un tableau même si il n'y a qu'une valeur.

setReponse(this,i,reponse) renseigne le this.autoCorrection[i] pour une réponse de type numérique ou une réponse avec plusieurs valeurs en affectant la variable this.autoCorrection[i].reponse.valeur.

Cette variable est un tableau pour pouvoir en mode interactif tester plusieurs formes de réponse.

En mode AMCNum, on récupère la valeur décimale contenue dans this.autoCorrection[i].reponse.valeur[0].

Mais AMC a besoin de savoir combien de chiffres présenter au codage, combien de chiffres dans la partie décimale, si on le veut en notation scientifique, si il y a besoin d'un signe, si on tolère une marge d'erreur...

setReponse() ne fixe ces paramètres qui sont dans this.autoCorrection[i].reponse.param que s'ils sont passés en argument (il faut donc y penser). un setReponse(this, i, reponse, {digits: 6, decimals: 5, signe: true, exposantNbChiffres: 2, exopsantSigne: true, approx:1, formatInteractif: 'calcul'}) règle AMCNum pour un nombre dont la mantisse comporte 6 chiffres dont 5 après la virgule avec case signe +/- avec un codage en notation scientifique avec signe pour l'exposant et avec 2 chiffres pour l'exposant, la tolérance est de 1 sur le dernier chiffre significatif (valeur approchée par défaut ou par excès acceptée). Exemple de résultat possible : -124.924 seront tolérés -124.923, -124.924 et -124.925 (je crois car je ne sais pas trop quel comportement aurait AMC si le nombre envoyé comme premier paramètre était -123,9247 en précisant 6 chiffres, mais il me semble que AMC provoquerait un warning disant qu'il n'y a pas assez de chiffres pour coder la réponse 'number is to large'...)