Utilisateur:Thomas234/common.js
De Microguide.ca
Autres actions
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(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/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();
}
})();