modules/interactif/questionMathLive.js


import FractionEtendue from '../FractionEtendue.js'
import { number } from 'mathjs'
import Grandeur from '../Grandeur.js'
import { context } from '../context.js'
import { afficheScore } from '../gestionInteractif.js'
import { gestionCan } from './gestionCan.js'
import { sp, texteExposant } from '../outils.js'
import * as pkg from '@cortex-js/compute-engine'
const { ComputeEngine } = pkg

export function verifQuestionMathLive (exercice, i, writeResult = true) {
  const engine = new ComputeEngine()
  let saisieParsee, num, den, fSaisie, fReponse
  const formatInteractif = exercice.autoCorrection[i].reponse.param.formatInteractif || 'calcul'
  const precision = exercice.autoCorrection[i].reponse.param.precision || 0
  const spanReponseLigne = document.querySelector(`#resultatCheckEx${exercice.numeroExercice}Q${i}`)
  // On compare le texte avec la réponse attendue en supprimant les espaces pour les deux
  let reponse, saisie, nombreSaisi, grandeurSaisie, mantisseSaisie, expoSaisi, nombreAttendu, mantisseReponse, expoReponse
  let reponses = []
  let champTexte
  if (!Array.isArray(exercice.autoCorrection[i].reponse.valeur)) {
    reponses = [exercice.autoCorrection[i].reponse.valeur]
  } else {
    reponses = exercice.autoCorrection[i].reponse.valeur
  }
  try {
    // Ici on va s'occuper de ce champTexte qui pose tant de problèmes
    champTexte = document.getElementById(`champTexteEx${exercice.numeroExercice}Q${i}`)
    if (champTexte === {} || champTexte === undefined) champTexte = { value: '' }
    let resultat = 'KO'
    let statut = 'OK'
    let feedbackSaisie
    let feedbackCorrection
    let ii = 0
    while ((resultat === 'KO') && (ii < reponses.length)) {
      reponse = reponses[ii]
      switch (formatInteractif) {
        case 'Num':
          num = parseInt(champTexte.value.replace(',', '.'))
          if (isNaN(num) || num === undefined) num = 9999
          den = reponse.den
          fSaisie = new FractionEtendue(num, den)
          if (fSaisie.isEqual(reponse)) {
            resultat = 'OK'
          }
          break
        case 'Den':
          den = parseInt(champTexte.value.replace(',', '.'))
          if (isNaN(den) || den === undefined) den = 9999
          num = reponse.num
          fSaisie = new FractionEtendue(num, den)
          if (fSaisie.isEqual(reponse)) {
            resultat = 'OK'
          }
          break
        case 'calcul':
        // Le format par défaut
          saisie = champTexte.value.replaceAll(',', '.') // EE : Le All est nécessaire pour l'usage du clavier spécial 6ème
          // La réponse est transformée en chaine compatible avec engine.parse()
          reponse = reponse.toString().replaceAll(',', '.').replaceAll('dfrac', 'frac')
          saisie = saisie.replace(/\((\+?-?\d+)\)/, '$1') // Pour les nombres négatifs, supprime les parenthèses
          // console.log('saisie : ', saisie) // EE : NE PAS SUPPRIMER CAR UTILE POUR LE DEBUGGAGE
          // console.log('reponse : ', reponse) // EE : NE PAS SUPPRIMER CAR UTILE POUR LE DEBUGGAGE
          if (!isNaN(reponse)) {
            if (Number(saisie) === Number(reponse)) {
              resultat = 'OK'
            }
          } else if (engine.parse(reponse).canonical.isSame(engine.parse(saisie).canonical)) {
            resultat = 'OK'
          }
          break
        case 'formeDeveloppee':
          saisie = champTexte.value.replaceAll(',', '.')
          reponse = reponse.toString().replaceAll(',', '.').replaceAll('dfrac', 'frac')
          saisie = saisie.replace(/\((\+?-?\d+)\)/, '$1') // Pour les nombres négatifs, supprime les parenthèses
          if (!saisie.includes('times') && engine.parse(reponse).canonical.isSame(engine.parse(saisie).canonical)) {
            resultat = 'OK'
          }
          break
        case 'nombreDecimal':
          saisie = champTexte.value.replace(',', '.')
          // La réponse est ici arrondie en fonction de reponse.param.decimals
          reponse = Number(reponse.toString().replaceAll(',', '.')).toFixed(exercice.autoCorrection[i].reponse.param.decimals)
          saisie = saisie.replace(/\((\+?-?\d+)\)/, '$1') // Pour les nombres négatifs, supprime les parenthèses
          if (engine.parse(reponse).isSame(engine.parse(saisie))) {
            resultat = 'OK'
          }
          break
        case 'ecritureScientifique': // Le résultat, pour être considéré correct, devra être saisi en écriture scientifique
          saisie = champTexte.value.replace(',', '.')
          if (typeof reponse === 'string') {
            reponse = reponse.replace(',', '.').replace('{.}', '.')
          }
          if (engine.parse(reponse).canonical.isSame(engine.parse(saisie).canonical)) {
            saisie = saisie.split('\\times')
            if (number(saisie[0]) >= 1 && number(saisie[0]) < 10) { resultat = 'OK' }
          }
          break
        case 'texte':

          if (champTexte !== undefined) saisie = champTexte.value
          else saisie = ''
          // console.log({ saisie, reponse}) // EE : NE PAS SUPPRIMER CAR UTILE POUR LE DEBUGGAGE
          if (saisie === reponse) {
            resultat = 'OK'
          } else if (saisie.replaceAll('\\,', '') === reponse.replaceAll('\\,', '')) {
            feedbackCorrection = 'Attention aux espaces !'
          }
          break

        case 'ignorerCasse':
          saisie = champTexte.value
          if (saisie.toLowerCase().replaceAll('\\lparen', '(').replaceAll('\\rparen', ')').replaceAll('\\left(', '(').replaceAll('\\right)', ')') === reponse.toLowerCase()) {
            resultat = 'OK'
            // Pour les exercices de simplifications de fraction
          }
          break
        case 'fractionPlusSimple':
          saisie = champTexte.value.replace(',', '.')
          if (!isNaN(parseFloat(saisie))) {
            if (parseInt(saisie) === reponse.n) resultat = 'OK'
          } else {
            saisieParsee = engine.parse(saisie, { canonical: false })
            fReponse = engine.parse(reponse.texFSD.replace('dfrac', 'frac'), { canonical: false })
            if (saisieParsee.isEqual(fReponse) && saisieParsee.json[1] && saisieParsee.json[1] < fReponse.json[1] && Number.isInteger(saisieParsee.json[1])) resultat = 'OK'
          }
          break
        case 'fractionEgale': // Pour les exercices de calcul où on attend une fraction peu importe son écriture (3/4 ou 300/400 ou 30 000/40 000...)
        // Si l'utilisateur entre un nombre décimal n, on transforme en n/1
          saisie = champTexte.value.replace(',', '.') // On remplace la virgule éventuelle par un point.
          if (!isNaN(parseFloat(saisie))) {
            const newFraction = new FractionEtendue(parseFloat(saisie))
            saisieParsee = engine.parse(`${newFraction.toLatex().replace('dfrac', 'frac')}`).canonical
          } else {
            saisieParsee = engine.parse(saisie).canonical
          }
          fReponse = engine.parse(reponse.texFSD.replace('dfrac', 'frac')).canonical
          if (saisieParsee) {
            if (saisieParsee.isEqual(fReponse)) resultat = 'OK'
          }
          break
        case 'fraction': // Pour les exercices où l'on attend un écriture donnée d'une fraction
          saisie = champTexte.value.replace(',', '.')
          if (!isNaN(parseFloat(saisie))) {
            if (parseInt(saisie) === reponse.n) resultat = 'OK'
          } else {
            saisieParsee = engine.parse(saisie.replace('frac', 'dfrac').replace('ddfrac', 'dfrac'))
            fReponse = engine.parse(reponse.texFSD)
            if (saisieParsee.isEqual(fReponse)) resultat = 'OK'
          }
          break
        case 'unites': // Pour les exercices où l'on attend une mesure avec une unité au choix
          saisie = champTexte.value.replace('²', '^2').replace('³', '^3')
          grandeurSaisie = saisieToGrandeur(saisie)
          if (grandeurSaisie) {
            if (grandeurSaisie.estEgal(reponse)) resultat = 'OK'
            else if (precision && grandeurSaisie.estUneApproximation(reponse, precision)) feedbackCorrection = 'Erreur d\'arrondi.'
          } else {
            if ((saisie === '') || isNaN(parseFloat(saisie.replace(',', '.')))) {
              resultat = 'KO'
            } else {
              resultat = 'essaieEncoreAvecUneSeuleUnite'
            }
          }
          break
        case 'intervalleStrict':// Pour les exercice où la saisie doit être dans un intervalle
          saisie = champTexte.value.replace(',', '.')
          nombreSaisi = Number(saisie)
          if (saisie !== '' && nombreSaisi > exercice.autoCorrection[i].reponse.valeur[0] && nombreSaisi < exercice.autoCorrection[i].reponse.valeur[1]) resultat = 'OK'
          break
        case 'intervalle' :
          saisie = champTexte.value.replace(',', '.')
          nombreSaisi = Number(saisie)
          if (saisie !== '' && nombreSaisi >= exercice.autoCorrection[i].reponse.valeur[0] && nombreSaisi <= exercice.autoCorrection[i].reponse.valeur[1]) resultat = 'OK'
          break
        case 'puissance' :
          saisie = champTexte.value.replace(',', '.')
          // formatOK et formatKO sont deux variables globales,
          // sinon dans le cas où reponses est un tableau, la valeur n'est pas conservée d'un tour de boucle sur l'autre
          // eslint-disable-next-line no-var
          var formatOK, formatKO
          if (saisie.indexOf('^') !== -1) {
            nombreSaisi = saisie.split('^')
            mantisseSaisie = nombreSaisi[0]
            expoSaisi = nombreSaisi[1] ? nombreSaisi[1].replace(/[{}]/g, '') : ''
            nombreAttendu = reponse.split('^')
            mantisseReponse = nombreAttendu[0]
            expoReponse = nombreAttendu[1] ? nombreAttendu[1].replace(/[{}]/g, '') : ''
            if (mantisseReponse === mantisseSaisie && expoReponse === expoSaisi) {
              formatOK = true
            }
            // gérer le cas mantisse négative a et exposant impair e, -a^e est correct mais pas du format attendu
            // si la mantisse attendue est négative on nettoie la chaine des parenthèses
            if (parseInt(mantisseReponse.replace(/[()]/g, '')) < 0 && expoReponse % 2 === 1) {
              if ((saisie === `${mantisseReponse.replace(/[()]/g, '')}^{${expoReponse}}`) || (saisie === `${mantisseReponse.replace(/[()]/g, '')}^${expoReponse}`)) {
                formatKO = true
              }
            }
            // si l'exposant est négatif, il se peut qu'on ait une puissance au dénominateur
            if (parseInt(expoReponse) < 0) {
              // Si la mantisse est positive
              if ((saisie === `\\frac{1}{${parseInt(mantisseReponse)}^{${-expoReponse}}`) || (saisie === `\\frac{1}{${parseInt(mantisseReponse)}^${-expoReponse}}`)) {
                formatKO = true
              }
            }
          } else {
          // Dans tous ces cas on est sûr que le format n'est pas bon
          // Toutefois la valeur peut l'être donc on vérifie
            nombreSaisi = saisie
            nombreAttendu = reponse.split('^')
            mantisseReponse = nombreAttendu[0]
            expoReponse = nombreAttendu[1] ? nombreAttendu[1].replace(/[{}]/g, '') : ''
            if (parseInt(expoReponse) < 0) {
              // Si la mantisse est positive
              if (nombreSaisi === `\\frac{1}{${mantisseReponse ** (-expoReponse)}}`) {
                formatKO = true
              }
              // Si elle est négative, le signe - peut être devant la fraction ou au numérateur  ou au dénominateur
              if (parseInt(mantisseReponse.replace(/[()]/g, '')) < 0 && ((-expoReponse) % 2 === 1)) {
                if ((nombreSaisi === `-\\frac{1}{${((-1) * parseInt(mantisseReponse.replace(/[()]/g, ''))) ** (-expoReponse)}}`) || (nombreSaisi === `\\frac{-1}{${((-1) * parseInt(mantisseReponse.replace(/[()]/g, ''))) ** (-expoReponse)}}`) || (nombreSaisi === `\\frac{1}{-${((-1) * parseInt(mantisseReponse.replace(/[()]/g, ''))) ** (-expoReponse)}}`)) {
                  formatKO = true
                }
              }
            }
            if (parseInt(expoReponse) > 0) {
              if (nombreSaisi === `${mantisseReponse ** (expoReponse)}`) {
                formatKO = true
              }
            }
            if (parseInt(expoReponse) === 0) {
              if (nombreSaisi === '1') {
                formatKO = true
              }
            }
          }
          if (formatOK) {
            resultat = 'OK'
          }
          if (formatKO) {
            resultat = 'essaieEncorePuissance'
          }
          // if (mantisseReponse === mantisseSaisie && expoReponse === expoSaisi) {
          //   resultat = 'OK'
          // } else {
          //   resultat = 'KO'
          // }

          break
      }
      ii++
    }
    spanReponseLigne.innerHTML = ''
    if (resultat === 'OK' && writeResult) {
      spanReponseLigne.innerHTML = '😎'
      spanReponseLigne.style.fontSize = 'large'
      if (champTexte !== undefined) champTexte.readOnly = true
    } else if (resultat === 'essaieEncoreAvecUneSeuleUnite') {
      spanReponseLigne.innerHTML = '<em>Il faut saisir une valeur numérique et une seule unité (' +
    (reponse.uniteDeReference.indexOf('^') > 0
      ? reponse.uniteDeReference.split('^')[0] + texteExposant(reponse.uniteDeReference.split('^')[1])
      : reponse.uniteDeReference) +
    ' par exemple).</em>'
      spanReponseLigne.style.color = '#f15929'
      spanReponseLigne.style.fontWeight = 'bold'
      statut = 'wait'
    } else if (resultat === 'essaieEncorePuissance') {
      spanReponseLigne.innerHTML = '<br><em>Attention, la réponse est mathématiquement correcte mais n\'a pas le format demandé.</em>'
      spanReponseLigne.style.color = '#f15929'
      spanReponseLigne.style.fontWeight = 'bold'
    } else if (writeResult) {
      spanReponseLigne.innerHTML = '☹️'
      spanReponseLigne.style.fontSize = 'large'
      if (champTexte !== undefined) champTexte.readOnly = true
    }
    if (feedbackSaisie) spanReponseLigne.innerHTML += `<span style="margin-left: 10px">${feedbackSaisie}</span>`
    if (feedbackCorrection && writeResult) spanReponseLigne.innerHTML += `<span style="margin-left: 10px">${feedbackCorrection}</span>`
    return { resultat, statut }
  } catch (error) {
    window.notify(`Erreur dans verif QuestionMathLive : ${error} <br> Avec les métadonnées : `, { champTexteValue: champTexte._slotValue, exercice: exercice.id, i, autoCorrection: exercice.autoCorrection[i], formatInteractif, spanReponseLigne })
  }
}

function saisieToGrandeur (saisie) {
  if (saisie.indexOf('°') > 0) {
    const split = saisie.split('°')
    return new Grandeur(parseFloat(split[0].replace(',', '.')), '°')
  }
  if (saisie.split('operatorname').length !== 2) { return false } else {
    const split = saisie.split('\\operatorname{')
    const mesure = parseFloat(split[0].replace(',', '.'))
    if (split[1]) {
      const split2 = split[1].split('}')
      const unite = split2[0] + split2[1]
      return new Grandeur(mesure, unite)
    } else {
      return false
    }
  }
}

export function ajouteChampTexteMathLive (exercice, i, style = '', { texteApres = '', texte = '', tailleExtensible = false } = {}) {
  if (context.isHtml && exercice.interactif) {
    if (style === '') {
      return `<label>${texte}</label><math-field virtual-keyboard-mode=manual id="champTexteEx${exercice.numeroExercice}Q${i}"></math-field>${texteApres ? '<span>' + texteApres + '</span>' : ''}<span id="resultatCheckEx${exercice.numeroExercice}Q${i}"></span>`
    } else if (tailleExtensible) {
      return `<label>${sp()}${texte}${sp()}</label><table style="text-align:center;font-size: small;font-family:Arial,Times,serif;display:inline;height:1px;"><tr><td style="position: relative; top: 27px; left: 0px;padding:0px 0px 5px;margin:0px"><math-field virtual-keyboard-mode=manual id="champTexteEx${exercice.numeroExercice}Q${i}"></math-field>${texteApres ? '<span>' + texteApres + '</span>' : ''} </td></tr></table><span id="resultatCheckEx${exercice.numeroExercice}Q${i}"></span>`
    } else return `<label>${texte}</label><math-field virtual-keyboard-mode=manual class="${style}" id="champTexteEx${exercice.numeroExercice}Q${i}"></math-field>${texteApres ? '<span>' + texteApres + '</span>' : ''} <span id="resultatCheckEx${exercice.numeroExercice}Q${i}"></span>`
  } else {
    return ''
  }
}

/** Crée une fraction avec 1 ou 2 champs de réponse et autant de feedbacks.
 * Si seul le numérateur ou le dénominateur sont utilisés pour la fraction, l'autre est précisé.
 * numerateur = false signifie qu'il y a un champ de saisie pour le numérateur.
 * denominateur = 100 signifie que le dénominateur est déjà renseigné à 100.
 * Dans ce cas, on utilise le format Interactif correspondant : 'Num' ou 'Den'
 * Si les deux champs sont à saisir, on utilise deux réponses de formatInteractif 'calcul'.
 */
export function ajouteChampFractionMathLive (exercice, i, numerateur = false, denominateur = 100, style = '', { texte = '', texteApres = '' } = {}) {
  let code = ''
  if (context.isHtml && exercice.interactif) {
    code += `<label>${texte}</label>
             <table style="border-collapse:collapse;text-align:center;font-size: small;font-family:Arial,Times,serif;display:inline;"><tr>
             <td style="${!numerateur ? style : ''} ;padding:0px 0px 5px;margin:0px;border-bottom:1px solid #000;">`
    if (!numerateur) {
      code += `<math-field virtual-keyboard-mode=manual id="champTexteEx${exercice.numeroExercice}Q${i}"></math-field>
              </td><td><span id="resultatCheckEx${exercice.numeroExercice}Q${i}"></span>`
      i++
    } else {
      code += `${numerateur} `
    }
    code += `</td></tr><tr>
             <td width=50px style="padding:0px;margin:0px;">`
    if (!denominateur) {
      code += `<math-field virtual-keyboard-mode=manual id="champTexteEx${exercice.numeroExercice}Q${i}"></math-field>
              </td><td><span id="resultatCheckEx${exercice.numeroExercice}Q${i}"></span>`
    } else {
      code += `${denominateur}`
    }
    code += `</td></tr></table> ${texteApres ? '<span>' + texteApres + '</span>' : ''}`
    return code
  } else {
    return ''
  }
}

/**
 * Lorsque l'évènement 'exercicesAffiches' est lancé par mathalea.js
 * on vérifie la présence du bouton de validation d'id btnValidationEx{i} créé par listeQuestionsToContenu
 * et on y ajoute un listenner pour vérifier les réponses saisies dans les math-field
 * @param {object} exercice
 */
export function exerciceMathLive (exercice) {
  document.addEventListener('exercicesAffiches', () => {
    // On vérifie le type si jamais il a été changé après la création du listenner (voir 5R20)
    if (exercice.interactifType === 'mathLive') {
      if (context.vue === 'can') {
        gestionCan(exercice)
      }
      const button = document.querySelector(`#btnValidationEx${exercice.numeroExercice}-${exercice.id}`)
      if (button) {
        if (!button.hasMathaleaListener) {
          button.addEventListener('click', event => {
            let nbBonnesReponses = 0
            let nbMauvaisesReponses = 0
            let besoinDe2eEssai = false
            let resultat
            let wait = false
            for (const i in exercice.autoCorrection) {
              if (verifQuestionMathLive(exercice, i, false).statut === 'wait') wait = true
            }
            if (!wait) {
              for (const i in exercice.autoCorrection) {
                if (verifQuestionMathLive(exercice, i).statut) { resultat = verifQuestionMathLive(exercice, i).resultat }
                if (resultat === 'OK') {
                  nbBonnesReponses++
                } else if (resultat === 'wait') {
                  besoinDe2eEssai = true
                } else {
                  nbMauvaisesReponses++ // Il reste à gérer le 2e essai
                }
              }
              if (!besoinDe2eEssai) {
                button.classList.add('disabled')
                afficheScore(exercice, nbBonnesReponses, nbMauvaisesReponses)
              }
            }
          })
          button.hasMathaleaListener = true
        }
      }
    }
  })
}