Basculer le menu
Changer de menu des préférences
Basculer le menu personnel
Non connecté(e)
Votre adresse IP sera visible au public si vous faites des modifications.

Note : après avoir publié vos modifications, il se peut que vous deviez forcer le rechargement complet du cache de votre navigateur pour voir les changements.

  • Firefox / Safari : maintenez la touche Maj (Shift) en cliquant sur le bouton Actualiser ou appuyez sur Ctrl + F5 ou Ctrl + R (⌘ + R sur un Mac).
  • Google Chrome : appuyez sur Ctrl + Maj + R (⌘ + Shift + R sur un Mac).
  •  Edge : maintenez la touche Ctrl en cliquant sur le bouton Actualiser ou pressez Ctrl + F5.
/* =====================================================================
   Assistant Microguide — recherche-réponse pour microguide.ca
   =====================================================================
   Fonctionne ENTIÈREMENT dans le navigateur du visiteur :
     - il lit le wiki par son API publique (api.php) ;
     - il bâtit son index tout seul, puis le garde en cache 24 h ;
     - il répond en recopiant mot pour mot le texte et les tableaux du wiki,
       avec le lien de la page.
   Aucun serveur à louer, aucune clé d'API, aucun service externe, aucune
   dépendance. Rien ne sort du wiki.

   INSTALLATION : coller ce fichier au complet dans MediaWiki:Common.js
   (Thomas234 a le droit « interface-admin », donc l'accès à cette page).

   POUR L'ENLEVER : vider MediaWiki:Common.js de ce bloc. Rien d'autre
   n'a été modifié sur le wiki.

   Réglage facultatif, avant ce bloc :
     window.MICROGUIDE_BASE = "https://microguide.ca";  // sinon : site courant
   ===================================================================== */

/* ---------- 1/4 vocabulaire ---------- */
// Vocabulaire de l'assistant microguide.
//
// GLOSSAIRE : français québécois, abréviations cliniques et noms de rue ->
// mots réellement écrits dans le wiki. Appliqué sans IA, donc sans erreur
// possible (principe repris du glossaire du bibliothécaire).
//
// INTENTIONS : quelle SORTE de renseignement la question demande, pour
// privilégier la bonne section (« quoi donner » -> section Traitements).

window.MICROGUIDE_GLOSSAIRE = {
  "atb": [
    "antibiotique",
    "antibiotiques"
  ],
  "abx": [
    "antibiotique",
    "antibiotiques"
  ],
  "anti biotique": [
    "antibiotique"
  ],
  "traiter": [
    "traitement",
    "traitements"
  ],
  "traite": [
    "traitement"
  ],
  "soigner": [
    "traitement"
  ],
  "donner": [
    "traitement"
  ],
  "prescrire": [
    "traitement"
  ],
  "couvrir": [
    "couverture",
    "traitement"
  ],
  "couvre": [
    "couverture"
  ],
  "tuer": [
    "traitement",
    "sensibilite"
  ],
  "marche": [
    "efficace",
    "traitement"
  ],
  "fonctionne": [
    "efficace",
    "traitement"
  ],
  "pogner": [
    "infection",
    "acquisition"
  ],
  "attraper": [
    "infection",
    "acquisition"
  ],
  "poumon": [
    "pulmonaire",
    "pneumonie"
  ],
  "poumons": [
    "pulmonaire",
    "pneumonie"
  ],
  "pipi": [
    "urinaire"
  ],
  "urine": [
    "urinaire"
  ],
  "vessie": [
    "cystite",
    "urinaire"
  ],
  "rein": [
    "pyelonephrite",
    "urinaire"
  ],
  "reins": [
    "pyelonephrite",
    "urinaire"
  ],
  "sang": [
    "bacteriemie",
    "sepsis",
    "hemoculture"
  ],
  "coeur": [
    "endocardite"
  ],
  "peau": [
    "cutanee",
    "tissus mous",
    "cellulite"
  ],
  "os": [
    "osteomyelite",
    "osteoarticulaire"
  ],
  "cerveau": [
    "meningite"
  ],
  "ventre": [
    "intra abdominale",
    "digestif"
  ],
  "diarrhee": [
    "digestif",
    "colite"
  ],
  "oreille": [
    "otite"
  ],
  "gorge": [
    "pharyngite",
    "orl"
  ],
  "yeux": [
    "keratite",
    "ophtalmologique"
  ],
  "oeil": [
    "keratite",
    "ophtalmologique"
  ],
  "bibitte": [
    "bacterie"
  ],
  "germe": [
    "bacterie"
  ],
  "microbe": [
    "bacterie"
  ],
  "resistant": [
    "resistance",
    "resistances"
  ],
  "resiste": [
    "resistance"
  ],
  "dose": [
    "posologie",
    "dose"
  ],
  "poso": [
    "posologie"
  ],
  "grossesse": [
    "grossesse",
    "enceinte"
  ],
  "enceinte": [
    "grossesse"
  ],
  "allergie": [
    "allergie",
    "allergique"
  ],
  "penicilline": [
    "penicilline",
    "penicillines"
  ],
  "pseudo": [
    "pseudomonas"
  ],
  "staph": [
    "staphylococcus",
    "staphyloccocus"
  ],
  "strep": [
    "streptococcus",
    "streptocoque"
  ],
  "pneumo": [
    "pneumoniae",
    "pneumocoque"
  ],
  "sarm": [
    "sarm",
    "meticilline",
    "resistant"
  ],
  "e coli": [
    "escherichia",
    "coli"
  ],
  "ecoli": [
    "escherichia",
    "coli"
  ],
  "c diff": [
    "clostridioides",
    "difficile"
  ],
  "cdiff": [
    "clostridioides",
    "difficile"
  ],
  "klebs": [
    "klebsiella"
  ],
  "entero": [
    "enterocoque",
    "enterococcus",
    "enterobacter"
  ],
  "bgn": [
    "gram negatif"
  ],
  "bgp": [
    "gram positif"
  ],
  "cocci": [
    "cocci",
    "coque"
  ],
  "labo": [
    "laboratoire"
  ],
  "gram": [
    "gram"
  ],
  "vanco": [
    "vancomycine"
  ],
  "cipro": [
    "ciprofloxacine"
  ],
  "levo": [
    "levofloxacine"
  ],
  "moxi": [
    "moxifloxacine"
  ],
  "ceftri": [
    "ceftriaxone"
  ],
  "cefta": [
    "ceftazidime"
  ],
  "pip tazo": [
    "piperacilline",
    "tazobactam"
  ],
  "piptazo": [
    "piperacilline",
    "tazobactam"
  ],
  "tazo": [
    "tazobactam",
    "piperacilline"
  ],
  "mero": [
    "meropeneme",
    "meropenem"
  ],
  "erta": [
    "ertapenem"
  ],
  "clinda": [
    "clindamycine"
  ],
  "flagyl": [
    "metronidazole"
  ],
  "metro": [
    "metronidazole"
  ],
  "doxy": [
    "doxycycline"
  ],
  "azithro": [
    "azithromycine"
  ],
  "clav": [
    "clavulanate",
    "amoxicilline"
  ],
  "amox": [
    "amoxicilline"
  ],
  "cloxa": [
    "cloxacilline"
  ],
  "cefaz": [
    "cefazoline",
    "cefazolin"
  ],
  "keflex": [
    "cephalexine"
  ],
  "genta": [
    "gentamicine"
  ],
  "tobra": [
    "tobramycine"
  ],
  "septra": [
    "triclotrimoxazole",
    "tmp",
    "smx"
  ],
  "tmp smx": [
    "triclotrimoxazole"
  ],
  "fkp": [
    "fibrose kystique",
    "fkp"
  ],
  "mpoc": [
    "mpoc"
  ],
  "si": [
    "soins intensifs"
  ],
  "usi": [
    "soins intensifs"
  ]
};

window.MICROGUIDE_INTENTIONS = {
  "traitement": {
    "declencheurs": [
      "traiter",
      "traite",
      "traitement",
      "atb",
      "abx",
      "antibiotique",
      "antibiotiques",
      "soigner",
      "donner",
      "prescrire",
      "couvrir",
      "couverture",
      "efficace",
      "quoi donner",
      "tuer",
      "marche",
      "fonctionne",
      "choix",
      "empirique",
      "per os",
      "po",
      "iv"
    ],
    "titres": [
      "traitement",
      "traitements",
      "efficace",
      "efficaces",
      "spectre",
      "couverture",
      "sensibilite",
      "posologie"
    ]
  },
  "resistance": {
    "declencheurs": [
      "resistance",
      "resistances",
      "resistant",
      "resiste",
      "mecanisme",
      "mecanismes",
      "blse",
      "carbapenemase",
      "sarm",
      "ervr"
    ],
    "titres": [
      "resistance",
      "resistances",
      "mecanisme",
      "mecanismes"
    ]
  },
  "clinique": {
    "declencheurs": [
      "infection",
      "infections",
      "cause",
      "causes",
      "clinique",
      "maladie",
      "symptome",
      "symptomes",
      "manifestation",
      "manifestations",
      "tableau",
      "pneumonie",
      "cellulite",
      "endocardite",
      "meningite",
      "urinaire",
      "osteomyelite",
      "abces",
      "bacteriemie",
      "sepsis",
      "otite",
      "keratite"
    ],
    "titres": [
      "infection",
      "infections",
      "clinique",
      "manifestations",
      "principales"
    ]
  },
  "epidemiologie": {
    "declencheurs": [
      "epidemiologie",
      "reservoir",
      "ou vit",
      "transmission",
      "attraper",
      "pogner",
      "acquisition",
      "colonisation",
      "colonisant",
      "risque",
      "nosocomial",
      "nosocomiale",
      "communautaire",
      "porteur"
    ],
    "titres": [
      "epidemiologie",
      "reservoir",
      "transmission",
      "acquisition",
      "specificite"
    ]
  },
  "laboratoire": {
    "declencheurs": [
      "laboratoire",
      "labo",
      "identification",
      "identifier",
      "culture",
      "gram",
      "microscope",
      "coloration",
      "catalase",
      "coagulase",
      "oxydase",
      "lactose",
      "milieu",
      "gelose",
      "hemoculture",
      "tsi"
    ],
    "titres": [
      "laboratoire",
      "laboratoires",
      "microscope",
      "identification",
      "culture",
      "caracteristiques",
      "tests"
    ]
  }
};

/* ---------- 2/4 construction de l'index ---------- */
// Construit l'index de microguide.ca DANS LE NAVIGATEUR, à partir de l'API
// du wiki. Aucun fichier à maintenir : l'index se refait tout seul, donc il
// est toujours à jour avec le wiki.
//
// Deux appels suffisent : le contenu de toutes les pages, puis l'URL des
// images. Environ 130 Ko, gardés en cache local pour 24 h.

(function (root) {
  "use strict";

  var MOTS_VIDES = ("a au aux avec ce ces dans de des du elle en et eux il ils je la le les leur lui ma mais " +
    "me meme mes moi mon ne nos notre nous on ou par pas pour qu que qui sa se ses son sur ta te tes toi ton " +
    "tu un une vos votre vous y est sont etre ete suis es sommes etes ont ai as avons avez avait etait sera " +
    "seront c d j l m n s t quel quelle quels quelles quoi comment pourquoi quand combien cet cette plus " +
    "moins tres bien aussi alors donc car si comme tout tous toute toutes autre autres faire fait fais peut " +
    "peux peuvent doit dois doivent").split(" ");

  function normalise(s) {
    return (s || "").toLowerCase().normalize("NFD")
      .replace(/[̀-ͯ]/g, "")
      .replace(/[-'’/]/g, " ");
  }

  var VIDES = new Set(MOTS_VIDES);

  function mots(s) {
    return (normalise(s).match(/[a-z0-9]+/g) || []).filter(function (w) {
      return w.length > 1 && !VIDES.has(w);
    });
  }

  // ------------------------------------------------ nettoyage du wikitext

  function cellules(bloc) {
    var out = [];
    bloc.split("\n").forEach(function (l) {
      l = l.trim();
      if (l[0] === "!" || l[0] === "|") {
        l = l.replace(/^[!|]+/, "");
        if (l.indexOf("|") >= 0 && l.indexOf("||") < 0) l = l.replace(/^[^|]*\|(?!\|)/, "");
        l.split("||").forEach(function (c) {
          c = c.replace(/^[a-z\-]+\s*=\s*"[^"]*"\s*\|/, "").trim();
          if (c && !/^[-\s|]*$/.test(c)) out.push(c);
        });
      }
    });
    return out.join("\n");
  }

  function nettoyer(t) {
    t = t.replace(/<!--[\s\S]*?-->/g, "");
    t = t.replace(/<ref[^>]*\/>/g, "").replace(/<ref[\s\S]*?<\/ref>/g, "");
    for (var i = 0; i < 5; i++) {
      var t2 = t.replace(/\{\{[^{}]*\}\}/g, "");
      if (t2 === t) break;
      t = t2;
    }
    t = t.replace(/\{\|[\s\S]*?\|\}/g, function (m) { return cellules(m); });
    t = t.replace(/\[\[(?:Fichier|File|Image)\s*:[^\]]*\]\]/gi, "");
    t = t.replace(/\[\[([^\]|]*)\|([^\]]*)\]\]/g, "$2").replace(/\[\[([^\]]*)\]\]/g, "$1");
    t = t.replace(/\[https?:\/\/\S+\s+([^\]]*)\]/g, "$1").replace(/\[https?:\/\/\S+\]/g, "");
    t = t.replace(/<br\s*\/?>/gi, "\n");
    t = t.replace(/<\/?(div|span|p|big|small|center|u|b|i|sub|sup|font|blockquote)[^>]*>/gi, "");
    t = t.replace(/<[^>]+>/g, " ");
    t = t.replace(/'''/g, "").replace(/''/g, "");
    t = t.replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">");
    t = t.replace(/[ \t]+/g, " ").replace(/\n\s*\n\s*\n+/g, "\n\n");
    return t.trim();
  }

  // ------------------------------------------------ découpage en sections

  function decouper(wikitext) {
    var lignes = wikitext.split("\n");
    var sections = [], cur = { titre: "", lignes: [] };
    lignes.forEach(function (ligne) {
      var m = ligne.match(/^\s*(={2,6})\s*(.+?)\s*\1\s*$/);
      if (m) {
        if (cur.lignes.length) sections.push(cur);
        var brut = m[2];
        // un titre qui n'est qu'une image n'est pas un titre
        if (/^\s*\[\[(?:Fichier|File|Image)\s*:/i.test(brut)) brut = "";
        cur = { titre: nettoyer(brut) || brut.replace(/^['= ]+|['= ]+$/g, ""), lignes: [] };
      } else {
        cur.lignes.push(ligne);
      }
    });
    if (cur.lignes.length) sections.push(cur);

    return sections.map(function (s) {
      var brut = s.lignes.join("\n");
      var imgs = [];
      var re = /\[\[(?:Fichier|File|Image)\s*:\s*([^|\]]+)/gi, m;
      while ((m = re.exec(brut))) imgs.push(m[1].trim().replace(/_/g, " "));
      var texte = nettoyer(brut);
      var puces = [], prose = [];
      texte.split("\n").forEach(function (l) {
        l = l.trim();
        if (!l) return;
        if (/^[\*\#•·–-]\s*/.test(l)) puces.push(l.replace(/^[\*\#•·–-]+\s*/, "").trim());
        else prose.push(l);
      });
      return { titre: s.titre, puces: puces, prose: prose, images: imgs };
    }).filter(function (s) {
      return s.puces.length || s.prose.length || s.images.length;
    });
  }

  // ------------------------------------------------ appels à l'API

  function api(params, base) {
    var u = (base || "") + "/api.php?origin=*&format=json&formatversion=2&" +
      Object.keys(params).map(function (k) {
        return encodeURIComponent(k) + "=" + encodeURIComponent(params[k]);
      }).join("&");
    return fetch(u, { credentials: "omit" }).then(function (r) {
      if (!r.ok) throw new Error("API " + r.status);
      return r.json();
    });
  }

  function toutesLesPages(base) {
    var pages = [];
    function tour(cont) {
      var p = {
        action: "query", generator: "allpages", gapnamespace: 0, gaplimit: "max",
        prop: "revisions", rvprop: "content|timestamp", rvslots: "main"
      };
      if (cont) for (var k in cont) p[k] = cont[k];
      return api(p, base).then(function (d) {
        ((d.query && d.query.pages) || []).forEach(function (pg) {
          var rev = pg.revisions && pg.revisions[0];
          var txt = rev && rev.slots && rev.slots.main && rev.slots.main.content;
          if (txt) pages.push({ titre: pg.title, wikitext: txt });
        });
        if (d.continue) return tour(d.continue);
        return pages;
      });
    }
    return tour(null);
  }

  function toutesLesImages(base) {
    var map = {};
    function tour(cont) {
      var p = { action: "query", list: "allimages", ailimit: "max" };
      if (cont) for (var k in cont) p[k] = cont[k];
      return api(p, base).then(function (d) {
        ((d.query && d.query.allimages) || []).forEach(function (im) {
          map[im.name.replace(/_/g, " ")] = im.url;
        });
        if (d.continue) return tour(d.continue);
        return map;
      });
    }
    return tour(null);
  }

  // ------------------------------------------------ assemblage de l'index

  function assembler(pagesBrutes, imgMap, base) {
    var pages = [], sections = [], vus = {};

    // Le wiki contient des redirections et des pages-coquilles (titres mal
    // orthographiés, pages vides) qui, sans ça, passent devant les vraies.
    pagesBrutes = pagesBrutes.filter(function (p) {
      if (/^\s*#\s*REDIRECT/i.test(p.wikitext)) return false;
      return nettoyer(p.wikitext).replace(/\s/g, "").length >= 80;
    });
    // à titre égal, on garde la page la plus fournie
    pagesBrutes.sort(function (a, b) { return b.wikitext.length - a.wikitext.length; });

    pagesBrutes.forEach(function (p) {
      if (vus[p.titre]) return;            // redirections vers le même titre
      var secs = decouper(p.wikitext);
      if (!secs.length) return;
      vus[p.titre] = 1;

      var pid = pages.length;
      var imgsPage = [];
      secs.forEach(function (sec, k) {
        sec.images.forEach(function (n) {
          if (imgMap[n]) {
            imgsPage.push({
              nom: n, url: imgMap[n],
              section: sec.titre || p.titre,
              entete: k === 0 && !sec.titre
            });
          }
        });
      });

      pages.push({
        id: pid, titre: p.titre,
        url: base + "/index.php/" + encodeURI(p.titre.replace(/ /g, "_")),
        images: imgsPage
      });

      secs.forEach(function (s) {
        var plat = s.prose.concat(s.puces).join(" ");
        var images = s.images.filter(function (n) { return imgMap[n]; })
          .map(function (n) { return { nom: n, url: imgMap[n] }; });
        if (plat.length < 15 && !images.length) return;
        sections.push({
          page: pid, titre: s.titre || p.titre,
          puces: s.puces, prose: s.prose, images: images,
          // le titre compte double : une section « Couverture » doit se
          // trouver même si son contenu n'est qu'un tableau en image
          mots: mots((s.titre || "") + " " + (s.titre || "") + " " + plat)
        });
      });
    });

    var df = {};
    sections.forEach(function (s) {
      new Set(s.mots).forEach(function (w) { df[w] = (df[w] || 0) + 1; });
    });
    sections.forEach(function (s) {
      var tf = {};
      s.mots.forEach(function (w) { tf[w] = (tf[w] || 0) + 1; });
      s.tf = tf; s.len = s.mots.length; delete s.mots;
    });

    return {
      pages: pages, sections: sections, df: df, N: sections.length,
      wiki: base, mots_vides: MOTS_VIDES,
      glossaire: root.MICROGUIDE_GLOSSAIRE || {},
      intentions: root.MICROGUIDE_INTENTIONS || {},
      bati: Date.now()
    };
  }

  // ------------------------------------------------ cache local 24 h

  var CLE = "microguide-index-v1";

  function duCache() {
    try {
      var b = localStorage.getItem(CLE);
      if (!b) return null;
      var d = JSON.parse(b);
      if (!d.bati || Date.now() - d.bati > 864e5) return null;
      d.glossaire = root.MICROGUIDE_GLOSSAIRE || {};
      d.intentions = root.MICROGUIDE_INTENTIONS || {};
      return d;
    } catch (e) { return null; }
  }

  function auCache(d) {
    try { localStorage.setItem(CLE, JSON.stringify(d)); } catch (e) { /* quota : tant pis */ }
  }

  // Construit (ou relit) l'index. Renvoie une promesse.
  function charger(base) {
    base = base || "";
    var c = duCache();
    if (c) return Promise.resolve(c);
    return Promise.all([toutesLesPages(base), toutesLesImages(base)])
      .then(function (r) {
        var ix = assembler(r[0], r[1], base);
        auCache(ix);
        return ix;
      });
  }

  root.MicroguideIndexeur = {
    charger: charger, assembler: assembler, decouper: decouper,
    nettoyer: nettoyer, mots: mots, viderCache: function () {
      try { localStorage.removeItem(CLE); } catch (e) {}
    }
  };
})(typeof module !== "undefined" ? module.exports : window);

/* ---------- 3/4 moteur de réponse ---------- */
// Moteur de réponse de l'assistant microguide — AUCUNE INVENTION POSSIBLE.
//
// La réponse est composée uniquement de phrases et de puces recopiées
// telles quelles depuis les pages du wiki, avec le titre de la section et
// le lien. Il n'y a pas de modèle de langue qui réécrit : le texte affiché
// existe mot pour mot sur microguide.ca.
//
// Principe repris du bibliothécaire de Thomas : les passages sources SONT
// la réponse.

(function (root) {
  "use strict";

  function normalise(s) {
    return (s || "")
      .toLowerCase()
      .normalize("NFD")
      .replace(/[̀-ͯ]/g, "");
  }

  function Moteur(index) {
    this.ix = index;
    this.vides = new Set(index.mots_vides || []);
    this.gloss = index.glossaire || {};
    // longueur moyenne des sections, pour BM25
    var t = 0;
    for (var i = 0; i < index.sections.length; i++) t += index.sections[i].len;
    this.avgdl = t / Math.max(1, index.sections.length);
  }

  Moteur.prototype.mots = function (s) {
    var self = this;
    return (normalise(s).match(/[a-z0-9]+/g) || []).filter(function (w) {
      return w.length > 1 && !self.vides.has(w);
    });
  };

  // Ajoute le vocabulaire québécois / les abréviations cliniques
  Moteur.prototype.etendre = function (q) {
    var n = normalise(q);
    var base = this.mots(q);
    var ajouts = [];
    for (var cle in this.gloss) {
      var re = new RegExp("\\b" + cle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\b");
      if (re.test(n)) {
        var vals = this.gloss[cle];
        for (var i = 0; i < vals.length; i++) {
          ajouts = ajouts.concat(this.mots(vals[i]));
        }
      }
    }
    // les mots de la question pèsent plus que les ajouts du glossaire
    var poids = {};
    base.forEach(function (w) { poids[w] = Math.max(poids[w] || 0, 1); });
    ajouts.forEach(function (w) { poids[w] = Math.max(poids[w] || 0, 0.6); });
    return poids;
  };

  // Quelle sorte de renseignement la question demande-t-elle ?
  // « quoi donner pour un pseudomonas » -> traitement, donc on privilégie
  // les sections « Traitements » plutôt qu'une autre qui parle aussi d'ATB.
  Moteur.prototype.intentions = function (question) {
    var n = normalise(question), out = [];
    var defs = this.ix.intentions || {};
    for (var nom in defs) {
      var d = defs[nom], touche = false;
      for (var i = 0; i < d.declencheurs.length; i++) {
        var t = d.declencheurs[i];
        var re = new RegExp("\\b" + t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
        if (re.test(n)) { touche = true; break; }
      }
      if (touche) out.push({ nom: nom, titres: d.titres });
    }
    return out;
  };

  // BM25 sur les sections
  Moteur.prototype.chercher = function (question, nb) {
    var ix = this.ix, self = this;
    var poids = this.etendre(question);
    var inten = this.intentions(question);
    var k1 = 1.4, b = 0.72, N = ix.N;
    var res = [];

    // Quelle page la question NOMME-t-elle ? « le SARM ça se traite comment »
    // doit répondre avec la page du SARM, pas avec une autre page qui parle
    // aussi de traitement. On mesure la correspondance avec le titre de page,
    // pondérée par la rareté des mots (un mot rare identifie mieux).
    var entite = [];
    for (var pi = 0; pi < ix.pages.length; pi++) {
      var mt = self.mots(ix.pages[pi].titre), e = 0;
      var vusMot = {};
      for (var mi = 0; mi < mt.length; mi++) {
        var mw = mt[mi];
        if (vusMot[mw]) continue;
        vusMot[mw] = 1;
        if (poids[mw] >= 0.6) {
          var dfp = ix.df[mw] || 1;
          e += Math.log(1 + (N - dfp + 0.5) / (dfp + 0.5));
        }
      }
      entite[pi] = e;
    }
    var entiteMax = Math.max.apply(null, entite.concat([0]));

    for (var i = 0; i < ix.sections.length; i++) {
      var s = ix.sections[i], score = 0, touches = 0;
      for (var w in poids) {
        var f = s.tf[w];
        if (!f) continue;
        var df = ix.df[w] || 1;
        var idf = Math.log(1 + (N - df + 0.5) / (df + 0.5));
        score += poids[w] * idf * (f * (k1 + 1)) / (f + k1 * (1 - b + b * s.len / self.avgdl));
        touches++;
      }
      if (!score) continue;
      // bonus quand le titre de la page ou de la section correspond
      var tp = normalise(ix.pages[s.page].titre);
      var ts = normalise(s.titre);
      for (var w2 in poids) {
        if (poids[w2] < 1) continue;
        if (tp.indexOf(w2) >= 0) score += 2.2 * poids[w2];
        if (ts.indexOf(w2) >= 0) score += 1.5 * poids[w2];
      }
      // la section répond-elle à la sorte de question posée ?
      var bonusIntention = 0;
      for (var a = 0; a < inten.length; a++) {
        var titres = inten[a].titres;
        for (var c = 0; c < titres.length; c++) {
          if (ts.indexOf(titres[c]) >= 0) {
            bonusIntention = Math.max(bonusIntention, 1);
            break;
          }
        }
      }
      if (inten.length) {
        // multiplicatif : une section hors sujet garde son score, la bonne
        // section passe devant sans écraser la pertinence des mots.
        score *= bonusIntention ? 1.9 : 0.85;
      }
      // la page nommée par la question passe devant les autres
      if (entiteMax > 0 && entite[s.page] > 0) {
        score *= 1 + 1.6 * (entite[s.page] / entiteMax);
      }
      res.push({ i: i, score: score, touches: touches });
    }
    res.sort(function (a, b) { return b.score - a.score; });
    return res.slice(0, nb || 8);
  };

  // Compose la réponse : toujours un texte, toujours du verbatim + source.
  Moteur.prototype.repondre = function (question) {
    var ix = this.ix;
    var hits = this.chercher(question, 10);
    var inten = this.intentions(question);

    if (!hits.length) {
      return {
        etat: "rien",
        intro: "Je n'ai rien trouvé sur microguide.ca pour cette question. " +
               "Le wiki ne semble pas couvrir ce sujet — il n'y a donc rien que je puisse te citer.",
        blocs: [],
        suggestions: ix.pages.slice(0, 0)
      };
    }

    // on regroupe par page, en gardant l'ordre de pertinence
    var parPage = {}, ordre = [];
    hits.forEach(function (h) {
      var s = ix.sections[h.i];
      if (!parPage[s.page]) { parPage[s.page] = []; ordre.push(s.page); }
      parPage[s.page].push({ s: s, score: h.score });
    });

    var meilleur = hits[0].score;
    var blocs = [];
    // page principale : jusqu'à 3 sections ; puis 2 pages d'appoint, 1 section
    ordre.slice(0, 3).forEach(function (pid, rang) {
      var page = ix.pages[pid];
      var secs = parPage[pid]
        .filter(function (x) { return x.score >= meilleur * (rang === 0 ? 0.28 : 0.45); })
        .slice(0, rang === 0 ? 3 : 1);
      if (!secs.length) return;
      // Images à afficher : celles dont la section correspond à la sorte de
      // question posée (un tableau de couverture pour une question de
      // traitement), plus la fiche d'en-tête de la page quand c'est la page
      // principale. Les images résument souvent mieux que le texte.
      var titresRetenus = {};
      secs.forEach(function (x) { titresRetenus[normalise(x.s.titre)] = 1; });
      var images = (page.images || []).filter(function (im) {
        var ns = normalise(im.section);
        if (titresRetenus[ns]) return true;
        for (var a = 0; a < inten.length; a++) {
          var titres = inten[a].titres;
          for (var c = 0; c < titres.length; c++) {
            if (ns.indexOf(titres[c]) >= 0) return true;
          }
        }
        return rang === 0 && im.entete;
      }).slice(0, 3);

      blocs.push({
        page: page.titre,
        url: page.url,
        images: images,
        sections: secs.map(function (x) {
          return { titre: x.s.titre, prose: x.s.prose, puces: x.s.puces };
        })
      });
    });

    var faible = meilleur < 6;
    var p0 = blocs.length ? blocs[0].page : null;
    var intro;
    if (faible) {
      intro = "Je ne trouve pas de passage qui répond clairement à cette question sur microguide.ca. " +
              "Voici ce qui s'en rapproche le plus, recopié tel quel — à vérifier, ça ne répond " +
              "peut-être pas à ce que tu cherches.";
    } else {
      intro = "D'après " + (blocs.length > 1 ? "les pages du wiki" : "la page « " + p0 + " »") +
              ", voici ce qui est écrit sur microguide.ca, recopié mot pour mot :";
    }

    return { etat: faible ? "faible" : "ok", intro: intro, blocs: blocs };
  };

  // Rendu texte (pour les essais en ligne de commande)
  Moteur.prototype.texte = function (question) {
    var r = this.repondre(question);
    var out = ["Q: " + question, "", r.intro, ""];
    r.blocs.forEach(function (b) {
      out.push("── " + b.page + "  <" + b.url + ">");
      (b.images || []).forEach(function (im) {
        out.push("   [IMAGE] " + im.nom + "  (§ " + im.section + ")");
      });
      b.sections.forEach(function (s) {
        out.push("   § " + s.titre);
        s.prose.forEach(function (p) { out.push("     " + p); });
        s.puces.forEach(function (p) { out.push("     • " + p); });
      });
      out.push("");
    });
    return out.join("\n");
  };

  root.MicroguideMoteur = Moteur;
})(typeof module !== "undefined" ? module.exports : window);

/* ---------- 4/4 bulle ---------- */
// Bulle de l'assistant microguide : lanceur en bas à droite, visible sur
// toutes les pages du wiki. Tout tourne dans le navigateur du visiteur, à
// partir de l'API du wiki : aucun serveur, aucune clé, aucun service externe.

(function () {
  "use strict";

  if (window.__microguideAssistant) return;   // déjà chargé
  window.__microguideAssistant = true;

  var BASE = (window.MICROGUIDE_BASE !== undefined)
    ? window.MICROGUIDE_BASE
    : (location.origin || "");

  var CSS = [
    '.mga-launch{position:fixed;z-index:9000;right:calc(18px + env(safe-area-inset-right,0px));',
    'bottom:calc(18px + env(safe-area-inset-bottom,0px));display:flex;align-items:center;gap:9px;',
    'background:#3f7490;color:#fff;border:none;font:600 14.5px/1 inherit;padding:13px 19px;',
    'border-radius:30px;cursor:pointer;box-shadow:0 6px 22px -6px rgba(20,40,55,.5)}',
    '.mga-launch:hover{filter:brightness(1.08)}',
    '.mga-launch svg{width:18px;height:18px;flex:0 0 auto}',
    '.mga-panel{position:fixed;z-index:9001;right:calc(18px + env(safe-area-inset-right,0px));',
    'bottom:calc(18px + env(safe-area-inset-bottom,0px));',
    'width:min(440px,calc(100vw - 32px));max-height:min(680px,calc(100vh - 36px));',
    'background:#fff;color:#1c2228;border:1px solid #dcd8d0;border-radius:12px;',
    'box-shadow:0 18px 50px -14px rgba(15,30,42,.45);display:flex;flex-direction:column;',
    'overflow:hidden;font:400 15px/1.6 "IBM Plex Sans",system-ui,sans-serif}',
    '.mga-head{display:flex;align-items:center;gap:10px;padding:13px 15px;border-bottom:1px solid #dcd8d0}',
    '.mga-head .t{font-weight:600;font-size:14.5px;flex:1;min-width:0}',
    '.mga-head .t em{display:block;font-style:normal;font-size:11px;color:#8995a0;font-weight:400}',
    '.mga-ico{background:none;border:none;color:#8995a0;cursor:pointer;font-size:19px;line-height:1;padding:4px 6px;border-radius:5px}',
    '.mga-ico:hover{color:#1c2228;background:#f4f1ea}',
    '.mga-log{flex:1;overflow-y:auto;padding:15px;display:flex;flex-direction:column;gap:17px;min-height:0}',
    '.mga-intro{font-size:13.5px;color:#55616b}',
    '.mga-intro p{margin:0 0 9px}',
    '.mga-rule{border-left:3px solid #3f7490;background:#e4eff6;padding:9px 12px;color:#1c2228}',
    '.mga-samples{display:flex;flex-direction:column;gap:6px;margin-top:11px}',
    '.mga-samples button{text-align:left;background:none;border:1px solid #dcd8d0;color:#1c2228;',
    'font:400 13px/1.5 inherit;padding:8px 11px;border-radius:6px;cursor:pointer}',
    '.mga-samples button:hover{border-color:#3f7490;background:#e4eff6}',
    '.mga-q{align-self:flex-end;max-width:88%;background:#3f7490;color:#fff;padding:9px 13px;',
    'border-radius:12px 12px 3px 12px;font-size:14px}',
    '.mga-a{font-size:14px}',
    '.mga-lead{color:#55616b;margin:0 0 12px}',
    '.mga-lead.weak{background:#faf0dd;border-left:3px solid #8a5a1c;padding:9px 12px;color:#1c2228}',
    '.mga-src{border-top:1px solid #dcd8d0;padding-top:11px;margin-top:13px}',
    '.mga-src:first-of-type{border-top:none;padding-top:0;margin-top:0}',
    '.mga-srch{display:flex;align-items:baseline;gap:7px;flex-wrap:wrap;margin-bottom:8px}',
    '.mga-srch .lbl{font-family:"IBM Plex Mono",monospace;font-size:10px;letter-spacing:.1em;',
    'text-transform:uppercase;color:#8995a0}',
    '.mga-srch a{color:#3f7490;font-weight:600;font-size:14px;text-decoration:none;border-bottom:1px solid transparent}',
    '.mga-srch a:hover{border-bottom-color:#3f7490}',
    '.mga-sec{margin:0 0 11px}',
    '.mga-sect{font-family:"IBM Plex Mono",monospace;font-size:10.5px;letter-spacing:.07em;',
    'text-transform:uppercase;color:#3f7490;margin-bottom:5px}',
    '.mga-quote{font-family:"IBM Plex Serif",Georgia,serif;font-size:14px;background:#f4f1ea;',
    'border-left:2px solid #dcd8d0;padding:9px 12px;color:#1c2228}',
    '.mga-quote p{margin:0 0 6px}.mga-quote p:last-child{margin-bottom:0}',
    '.mga-quote ul{margin:5px 0 0;padding-left:19px}.mga-quote li{margin:2px 0}',
    '.mga-fig{margin:0 0 11px}',
    '.mga-fig img{display:block;width:100%;height:auto;border:1px solid #dcd8d0;border-radius:4px;background:#fff}',
    '.mga-fig figcaption{font-family:"IBM Plex Mono",monospace;font-size:10.5px;color:#8995a0;margin-top:4px}',
    '.mga-none{background:#faf0dd;border-left:3px solid #8a5a1c;padding:11px 13px;font-size:13.5px}',
    '.mga-ask{border-top:1px solid #dcd8d0;padding:11px;display:flex;gap:8px}',
    '.mga-ask input{flex:1;min-width:0;background:#fbfaf7;color:#1c2228;border:1px solid #dcd8d0;',
    'border-radius:7px;padding:10px 12px;font:400 14px/1.4 inherit}',
    '.mga-ask input:focus{outline:2px solid #3f7490;outline-offset:-1px;border-color:#3f7490}',
    '.mga-ask button{background:#3f7490;color:#fff;border:none;border-radius:7px;padding:0 15px;',
    'font:600 14px/1 inherit;cursor:pointer}',
    '.mga-foot{padding:0 13px 11px;font-family:"IBM Plex Mono",monospace;font-size:11px;color:#8995a0}',
    '@media (max-width:460px){.mga-panel{right:8px;left:8px;width:auto}}'
  ].join("");

  var SAMPLES = [
    "quels antibiotiques pour traiter un pseudomonas ?",
    "la couverture de la ceftriaxone",
    "comment on traite un c diff",
    "le SARM ça se traite comment",
    "c'est quoi la posologie de la gentamicine"
  ];

  function el(tag, cls, txt) {
    var n = document.createElement(tag);
    if (cls) n.className = cls;
    if (txt != null) n.textContent = txt;
    return n;
  }

  var moteur = null, chargement = null, log, intro, champ, panel, launch;

  function construire() {
    var style = el("style");
    style.textContent = CSS;
    document.head.appendChild(style);

    launch = el("button", "mga-launch");
    launch.setAttribute("aria-label", "Ouvrir l'assistant");
    launch.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" ' +
      'stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 11.5a8.4 8.4 0 0 1-.9 ' +
      '3.8A8.5 8.5 0 0 1 12.5 20a8.4 8.4 0 0 1-3.8-.9L3 21l1.9-5.7A8.4 8.4 0 0 1 4 11.5a8.5 8.5 0 0 1 ' +
      '4.7-7.6A8.4 8.4 0 0 1 12.5 3h.5a8.5 8.5 0 0 1 8 8z"/></svg>';
    launch.appendChild(document.createTextNode("Demander au guide"));
    document.body.appendChild(launch);

    panel = el("section", "mga-panel");
    panel.hidden = true;
    panel.setAttribute("aria-label", "Assistant Microguide");

    var head = el("div", "mga-head");
    var t = el("div", "t", "Assistant Microguide");
    t.appendChild(el("em", null, "répond uniquement avec le contenu du wiki"));
    head.appendChild(t);
    var bReset = el("button", "mga-ico", "⟲");
    bReset.title = "Effacer";
    var bClose = el("button", "mga-ico", "✕");
    bClose.title = "Fermer";
    head.appendChild(bReset);
    head.appendChild(bClose);
    panel.appendChild(head);

    log = el("div", "mga-log");
    intro = el("div", "mga-intro");
    intro.appendChild(el("p", null, "Pose ta question en français, comme tu la dirais à l'oral."));
    intro.appendChild(el("div", "mga-rule",
      "Je ne rédige rien de moi-même. Je recopie les passages du wiki mot pour mot, avec le " +
      "lien de la page et le tableau quand il y en a un. Si le wiki ne le dit pas, je ne l'invente pas."));
    var samples = el("div", "mga-samples");
    SAMPLES.forEach(function (q) {
      var b = el("button", null, q);
      b.addEventListener("click", function () { demander(q); });
      samples.appendChild(b);
    });
    intro.appendChild(samples);
    log.appendChild(intro);
    panel.appendChild(log);

    var form = el("form", "mga-ask");
    champ = el("input");
    champ.type = "text";
    champ.placeholder = "Ta question…";
    champ.autocomplete = "off";
    champ.setAttribute("aria-label", "Ta question");
    var env = el("button", null, "Chercher");
    env.type = "submit";
    form.appendChild(champ);
    form.appendChild(env);
    form.addEventListener("submit", function (e) {
      e.preventDefault();
      demander(champ.value);
    });
    panel.appendChild(form);

    var foot = el("div", "mga-foot", "index rebâti depuis le wiki");
    panel.appendChild(foot);
    document.body.appendChild(panel);

    launch.addEventListener("click", ouvrir);
    bClose.addEventListener("click", fermer);
    bReset.addEventListener("click", function () {
      Array.prototype.slice.call(log.children).forEach(function (n) {
        if (n !== intro) n.remove();
      });
      intro.hidden = false;
      champ.value = "";
      champ.focus();
    });
    document.addEventListener("keydown", function (e) {
      if (e.key === "Escape" && !panel.hidden) fermer();
    });
    panel.__foot = foot;
  }

  function ouvrir() {
    panel.hidden = false;
    launch.hidden = true;
    precharger();
    setTimeout(function () { champ.focus(); }, 30);
  }
  function fermer() {
    panel.hidden = true;
    launch.hidden = false;
  }

  // L'index se bâtit au premier usage, pas au chargement de la page : la
  // navigation normale du wiki n'est donc jamais ralentie.
  function precharger() {
    if (moteur || chargement) return chargement;
    chargement = window.MicroguideIndexeur.charger(BASE).then(function (ix) {
      ix.glossaire = window.MICROGUIDE_GLOSSAIRE || {};
      ix.intentions = window.MICROGUIDE_INTENTIONS || {};
      moteur = new window.MicroguideMoteur(ix);
      if (panel.__foot) {
        panel.__foot.textContent = ix.pages.length + " pages · " + ix.sections.length + " sections";
      }
      return moteur;
    });
    return chargement;
  }

  function rendreSection(s) {
    var wrap = el("div", "mga-sec");
    wrap.appendChild(el("div", "mga-sect", s.titre));
    var box = el("div", "mga-quote");
    s.prose.forEach(function (p) { box.appendChild(el("p", null, p)); });
    if (s.puces.length) {
      var ul = el("ul");
      s.puces.forEach(function (p) { ul.appendChild(el("li", null, p)); });
      box.appendChild(ul);
    }
    wrap.appendChild(box);
    return wrap;
  }

  function rendreReponse(r) {
    var box = el("div", "mga-a");
    box.appendChild(el("p", "mga-lead" + (r.etat === "faible" ? " weak" : ""), r.intro));
    if (!r.blocs.length) {
      box.appendChild(el("div", "mga-none",
        "Aucune page du wiki ne traite de ça. Je préfère te le dire plutôt que de te donner " +
        "une réponse que je n'ai pas lue quelque part."));
      return box;
    }
    r.blocs.forEach(function (b) {
      var src = el("div", "mga-src");
      var h = el("div", "mga-srch");
      h.appendChild(el("span", "lbl", "Page"));
      var a = el("a", null, b.page);
      a.href = b.url;
      h.appendChild(a);
      src.appendChild(h);
      (b.images || []).forEach(function (im) {
        var fig = el("figure", "mga-fig");
        var img = document.createElement("img");
        img.src = im.url;
        img.alt = im.section + " — " + b.page;
        img.loading = "lazy";
        fig.appendChild(img);
        fig.appendChild(el("figcaption", null, "Tableau du wiki — " + im.section));
        src.appendChild(fig);
      });
      b.sections.forEach(function (s) { src.appendChild(rendreSection(s)); });
      box.appendChild(src);
    });
    return box;
  }

  function demander(q) {
    q = (q || "").trim();
    if (!q) return;
    intro.hidden = true;
    log.appendChild(el("div", "mga-q", q));
    var attente = el("p", "mga-lead", "Je cherche dans le wiki…");
    log.appendChild(attente);
    log.scrollTop = log.scrollHeight;
    champ.value = "";

    precharger().then(function (m) {
      attente.remove();
      log.appendChild(rendreReponse(m.repondre(q)));
      log.scrollTop = log.scrollHeight;
    })["catch"](function (e) {
      attente.remove();
      log.appendChild(el("div", "mga-none",
        "Je n'ai pas pu lire le wiki (" + (e && e.message ? e.message : "erreur") +
        "). Réessaie dans un moment."));
    });
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", construire);
  } else {
    construire();
  }
})();