34 |
35 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/styles.min.css:
--------------------------------------------------------------------------------
1 | h1,#controls{text-align:center;margin:1em;}main{width:80%;max-width:800px;margin:auto;padding:10px;border-radius:15px;border-color:rgb(228,228,228);background:linear-gradient(90deg,rgb(239,242,247) 0%,7.60286%,rgb(237,240,249) 15.2057%,20.7513%,rgb(235,239,248) 26.297%,27.6386%,rgb(235,239,248) 28.9803%,38.2826%,rgb(231,237,249) 47.585%,48.1216%,rgb(230,236,250) 48.6583%,53.1306%,rgb(228,236,249) 57.6029%,61.5385%,rgb(227,234,250) 65.4741%,68.7835%,rgb(222,234,250) 72.093%,75.7603%,rgb(219,230,248) 79.4275%,82.8265%,rgb(216,229,248) 86.2254%,87.8354%,rgb(213,228,249) 89.4454%,91.8605%,rgb(210,226,249) 94.2755%,95.4383%,rgb(209,225,248) 96.6011%,98.3005%,rgb(208,224,247) 100%);}#chat{min-height:350px;font-family:"Inter",sans-serif,Avenir,Helvetica,Arial;font-size:.9rem;position:relative;overflow:hidden;padding:10px;border-radius:5px;font-size:1em;}#controls{margin-top:2em;}#input-container{background-color:rgb(255,255,255);margin:auto;display:flex;border:1px solid rgba(0,0,0,.1);border-radius:5px;margin-top:.8em;margin-bottom:.8em;box-shadow:rgba(149,157,165,.2) 0 1px 12px;overflow-y:auto;max-height:200px;position:relative;}#user-input{outline:none;overflow-wrap:break-word;line-break:auto;width:100%;text-align:left;padding:5px;height:1em;}.placeholder{color:gray;}[contenteditable="true"]:empty:before{content:attr(placeholder);color:grey;font-style:italic;}.message{margin-top:.5em;margin-bottom:.5em;clear:both;margin-top:10px;margin-bottom:10px;padding:10px;width:80%;border-radius:10px;}.user-message{box-shadow:rgba(0,0,0,.12) 0 .3px .9px,rgba(0,0,0,.16) 0 1.6px 3.6px;color:rgb(255,255,255);background-color:rgb(0,132,255);float:right;}.user-message p{margin:0 5px;}.bot-message{float:left;margin-top:10px;margin-bottom:10px;}.bot-message>:first-child:before{content:"🗣️";margin-right:10px;font-size:2em;}footer{width:60%;max-width:600px;margin:auto;font-size:.9em;text-align:center;margin-top:3em;border-top:1px solid #bbb;padding-top:1em;}footer a{text-decoration:none;}footer a:hover{text-decoration:underline;}#chat img{display:block;margin:1em;}#chat li,#chat li p{margin-top:.25em;margin-bottom:.25em;}@media screen and (max-width:500px){body{font-size:1.05em!important}main{width:initial!important;max-width:initial!important}#chat{padding:5px!important}.message{padding:5px!important}.bot-message{width:90%!important}ul{padding-left:30px!important}ul ul{padding-left:25px!important}}
--------------------------------------------------------------------------------
/styles.css:
--------------------------------------------------------------------------------
1 | h1,
2 | #controls {
3 | text-align: center;
4 | margin: 1em;
5 | }
6 |
7 | main {
8 | width: 80%;
9 | max-width: 800px;
10 | margin: auto;
11 | padding: 10px;
12 | border-radius: 15px;
13 | border-color: rgb(228, 228, 228);
14 | background: linear-gradient(
15 | 90deg,
16 | rgb(239, 242, 247) 0%,
17 | 7.60286%,
18 | rgb(237, 240, 249) 15.2057%,
19 | 20.7513%,
20 | rgb(235, 239, 248) 26.297%,
21 | 27.6386%,
22 | rgb(235, 239, 248) 28.9803%,
23 | 38.2826%,
24 | rgb(231, 237, 249) 47.585%,
25 | 48.1216%,
26 | rgb(230, 236, 250) 48.6583%,
27 | 53.1306%,
28 | rgb(228, 236, 249) 57.6029%,
29 | 61.5385%,
30 | rgb(227, 234, 250) 65.4741%,
31 | 68.7835%,
32 | rgb(222, 234, 250) 72.093%,
33 | 75.7603%,
34 | rgb(219, 230, 248) 79.4275%,
35 | 82.8265%,
36 | rgb(216, 229, 248) 86.2254%,
37 | 87.8354%,
38 | rgb(213, 228, 249) 89.4454%,
39 | 91.8605%,
40 | rgb(210, 226, 249) 94.2755%,
41 | 95.4383%,
42 | rgb(209, 225, 248) 96.6011%,
43 | 98.3005%,
44 | rgb(208, 224, 247) 100%
45 | );
46 | }
47 |
48 | #chat {
49 | min-height: 350px;
50 | font-family: "Inter", sans-serif, Avenir, Helvetica, Arial;
51 | font-size: 0.9rem;
52 | position: relative;
53 | overflow: hidden;
54 | padding: 10px;
55 | border-radius: 5px;
56 | font-size: 1em;
57 | }
58 |
59 | #controls {
60 | margin-top: 2em;
61 | }
62 |
63 | #input-container {
64 | background-color: rgb(255, 255, 255);
65 | margin: auto;
66 | display: flex;
67 | border: 1px solid rgba(0, 0, 0, 0.1);
68 | border-radius: 5px;
69 | margin-top: 0.8em;
70 | margin-bottom: 0.8em;
71 | box-shadow: rgba(149, 157, 165, 0.2) 0px 1px 12px;
72 | overflow-y: auto;
73 | max-height: 200px;
74 | position: relative;
75 | }
76 |
77 | #user-input {
78 | outline: none;
79 | overflow-wrap: break-word;
80 | line-break: auto;
81 | width: 100%;
82 | text-align: left;
83 | padding: 5px;
84 | height: 1em;
85 | }
86 |
87 | .placeholder {
88 | color: gray;
89 | }
90 |
91 | [contenteditable="true"]:empty:before {
92 | content: attr(placeholder);
93 | color: grey;
94 | font-style: italic;
95 | }
96 |
97 | .message {
98 | margin-top: 0.5em;
99 | margin-bottom: 0.5em;
100 | clear: both;
101 | margin-top: 10px;
102 | margin-bottom: 10px;
103 | padding: 10px;
104 | width: 80%;
105 | border-radius: 10px;
106 | }
107 |
108 | .user-message {
109 | box-shadow: rgba(0, 0, 0, 0.12) 0px 0.3px 0.9px,
110 | rgba(0, 0, 0, 0.16) 0px 1.6px 3.6px;
111 | color: rgb(255, 255, 255);
112 | background-color: rgb(0, 132, 255);
113 | float: right;
114 | }
115 |
116 | .user-message p {
117 | margin: 0px 5px;
118 | }
119 |
120 | .bot-message {
121 | float: left;
122 | margin-top: 10px;
123 | margin-bottom: 10px;
124 | }
125 |
126 | .bot-message > :first-child:before {
127 | content: "🗣️";
128 | margin-right: 10px;
129 | font-size: 2em;
130 | }
131 |
132 | footer {
133 | width: 60%;
134 | max-width: 600px;
135 | margin: auto;
136 | font-size: 0.9em;
137 | text-align: center;
138 | margin-top: 3em;
139 | border-top: 1px solid #bbb;
140 | padding-top: 1em;
141 | }
142 |
143 | footer a {
144 | text-decoration: none;
145 | }
146 | footer a:hover {
147 | text-decoration: underline;
148 | }
149 |
150 | #chat img {
151 | display: block;
152 | margin: 1em;
153 | }
154 |
155 | #chat li,
156 | #chat li p {
157 | margin-top: 0.25em;
158 | margin-bottom: 0.25em;
159 | }
160 |
161 | @media screen and (max-width: 500px) {
162 | body {
163 | font-size: 1.05em !important;
164 | }
165 | main {
166 | width: initial !important;
167 | max-width: initial !important;
168 | }
169 | #chat {
170 | padding: 5px !important;
171 | }
172 | .message {
173 | padding: 5px !important;
174 | }
175 | .bot-message {
176 | width: 90% !important;
177 | }
178 | ul {
179 | padding-left: 30px !important;
180 | }
181 | ul ul {
182 | padding-left: 25px !important;
183 | }
184 | }
185 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ChatMD
2 |
3 | ChatMD est un chatbot, que vous pouvez configurer par vous-même en Markdown :
4 |
5 | - Créez un fichier en [markdown]((https://www.markdowntutorial.com/fr/)) et mettez-le en ligne : sur [CodiMD](https://codimd.apps.education.fr/), ou sur une [forge](https://forge.aeif.fr/)
6 | - Respectez la syntaxe de ChatMD pour définir votre chatbot
7 |
8 | Votre chatbot est alors prêt et visible à l'adresse suivante : [https://eyssette.forge.aeif.fr/chatMD/#URL](https://eyssette.forge.aeif.fr/chatMD/#URL) (Mettez l'url de votre fichier à la place de URL)
9 |
10 |
11 | ## Exemples
12 |
13 | Voici un modèle que vous pouvez récupérer pour construire votre chatbot : [modèle à récupérer](https://codimd.apps.education.fr/mBGbHStJSVOSrlGfGb981A?both)
14 |
15 | Voici quelques exemple de chatbot créés avec ChatMD :
16 |
17 | - [Méthode de la dissertation en philosophie](https://eyssette.forge.aeif.fr/chatMD/#https://eyssette.forge.aeif.fr/chatbot/dissertation-philosophie.md)
18 | - [Utilisation d'un microscope](https://eyssette.forge.aeif.fr/chatMD/#https://codimd.apps.education.fr/xGNHIJSeTVCk6FHas-_71g) : un chatbot créé à partir du travail de Guillaume Berthelot et de Jérémy Navoizat ([voir la source](https://codimd.apps.education.fr/xGNHIJSeTVCk6FHas-_71g))
19 |
20 |
21 | ## Syntaxe
22 |
23 | La syntaxe pour écrire un chatbot avec chatMD est la suivante :
24 |
25 | - On définit le titre du chatbot dans un titre de niveau 1
26 | - Le message initial est à mettre dans un bloc de citation après le titre du chatbot
27 | - Les titres de niveau 2 servent à identifier les réponses possibles du chatbot
28 | - Sous chaque titre de niveau 2 :
29 | - On indique avec une liste non ordonnée les mots clés ou expressions qui vont déclencher la réponse. On peut éventuellement s'en passer si on guide l'utilisateur avec un choix d'options (voir ci-dessous).
30 | - On écrit une réponse en Markdown.
31 | - [Optionnel] On indique avec une liste ordonnée les options possibles. Chaque élément de la liste doit être un lien en Mardown de la forme suivante : \`[intitulé de l'option](identifiant de l'option, qui doit correspondre à l'un des titres de niveau 2)\`.
32 |
33 | ## Options de configuration plus avancées
34 |
35 | On peut ajouter un en-tête yaml à son fichier Markdown.
36 | Par exemple :
37 |
38 | ```yaml
39 | clavier: false
40 | rechercheContenu: true
41 | gestionsGrosMots: true
42 | style: a{color:red}
43 | maths: true
44 | titresRéponses: ["### ", "#### "]
45 | ```
46 |
47 | - `clavier: false` désactive le champ d'entrée clavier si on souhaite simplement guider l'utilisateur avec les options proposées en fin de chaque réponse.
48 | - `rechercheContenu: true` permet d'ajouter une recherche de comparaison de l'entrée de l'utilisateur avec le contenu de chaque réponse
49 | - `gestionGrosMots: true` permet de détecter les gros mots envoyés par l'utilisateur et de formuler une réponse adéquate si l'utilisateur en utilise
50 | - `style: a{color:red}` permet d'ajouter des styles CSS personnalisés.
51 | - `maths: true` permet d'écrire des formules mathématiques en Latex avec la syntaxe `$Latex$` ou `$$Latex$$`
52 | - `titresRéponses: "### "` permet de changer l'identifiant des réponses du chatbot si on veut pouvoir structurer les réponses du chatbot dans son document
53 | - `titresRéponses: ["### ", "#### "]` permet de changer les identifiants possibles des réponses du chatbot si on veut pouvoir structurer les réponses du chatbot dans son document
54 |
55 | ## Crédits
56 |
57 | Chat MD est un outil libre et gratuit sous licence MIT.
58 |
59 | Il utilise d'autres logiciels libres :
60 | - [js-yaml](https://github.com/nodeca/js-yaml) pour la gestion des en-têtes yaml
61 | - [typed.js](https://github.com/mattboldt/typed.js) pour l'effet "machine à écrire"
62 | - [showdown](https://github.com/showdownjs/showdown) pour la conversion du markdown en html
63 | - [leo-profanity](https://github.com/jojoee/leo-profanity) et [french-badwords-list](https://github.com/darwiin/french-badwords-list/) pour la gestion des gros mots
--------------------------------------------------------------------------------
/script.min.js:
--------------------------------------------------------------------------------
1 | function createChatBot(e){const t=e.pop();let n=e.pop();document.getElementById("chatbot-name").textContent=t;const s=document.getElementById("chat"),o=document.getElementById("user-input"),r=document.getElementById("send-button");let a=null,i=Math.floor(Math.random()*defaultMessage.length),u=[];var l=new showdown.Converter({emoji:!0,parseImgDimensions:!0});function c(e,t){new Typed(t,{strings:[e],typeSpeed:1,startDelay:100,onBegin:()=>{o.focus()}})}function d(e,t){const n=document.createElement("div");n.classList.add("message"),n.classList.add(t?"user-message":"bot-message");let o=(r=e,l.makeHtml(r));var r;!0===yamlMaths?setTimeout((()=>{o=function(e){let t=(e=e.replace(/\$\$(.*?)\$\$/g,"\[$1\]").replace(/\$(.*?)\$/g,"\($1\)")).match(new RegExp(/\\[.*?\\]|\\(.*?\\)/g));if(t)for(let n of t){const t=!!n.includes("\[");let s=n.replace("\[","").replace("\]","");s=s.replace("\(","").replace("\)",""),stringWithLatex=katex.renderToString(s,{displayMode:t}),e=e.replace(n,stringWithLatex)}return e}(o),t?n.innerHTML=o:c(o,n),s.appendChild(n)}),100):(t?n.innerHTML=o:c(o,n),s.appendChild(n))}function p(e,t){if(0===e.length)return t.length;if(0===t.length)return e.length;const n=[];for(let e=0;e<=t.length;e++)n[e]=[e];for(let t=0;t<=e.length;t++)n[0][t]=t;for(let s=1;s<=t.length;s++)for(let o=1;o<=e.length;o++){const r=e[o-1]===t[s-1]?0:1;n[s][o]=Math.min(n[s-1][o]+1,n[s][o-1]+1,n[s-1][o-1]+r)}return n[t.length][e.length]}function m(e,t,n){const s=e.split(" ");for(const e of s){if(p(e,t)t[e]||e))}(s),s=s.split(/\s|'/).filter((e=>e.length>=5))||[];const o=[];for(const a of s){o.push({word:a,weight:5});const i=[0,0,0,0,.4,.6,.8],u=.2,l=10;function r(t,s){let o=i[s-1];o=0===t?o+u:o;const r=a.substring(t,t+s);return n&&e[n][0].toLowerCase().includes(r)&&(o+=l),{token:r,weight:o}}const c=a.length;if(c>=5)for(let d=0;d<=c-5;d++)o.push(r(d,5));if(c>=6)for(let p=0;p<=c-6;p++)o.push(r(p,6));if(c>=7)for(let m=0;m<=c-7;m++)o.push(r(m,7))}return o}(t,n),o={};for(const{token:e,weight:t}of s)e&&(o[e]=(o[e]||0)+t);return o}let v=[];if(yamlSearchInContent)for(let t=0;t'+o+"\n"}n+="",e+=n}else a=null;return e}r.addEventListener("click",(()=>{const t=o.innerText;""!==t.trim()&&(d(t,!0),setTimeout((()=>{!function(t){if(!0===yamldetectBadWords&&filterBadWords&&filterBadWords.check(t)){const e=Math.floor(Math.random()*badWordsMessage.length);return void d(badWordsMessage[e],!1)}let n,s,o,r=null,l=0,c=0,p=t.toLowerCase();if(a&&(s=a.map((e=>e[0].toLowerCase())),o=s.indexOf(p)),a&&o>-1)h(a[o][1]);else{for(let t=0;t4&&m(p,e,3)&&i++}0==a&&i>c&&(a++,c=i),a>l&&(r=o,l=a,n=t)}if(r&&l>.55){let t=Array.isArray(r)?r.join("\n\n"):r;t=y(t,e[n][3]),d(t,!1)}else{for(;u.includes(i);)i=Math.floor(Math.random()*defaultMessage.length);u.length>4&&u.shift(),u.push(i),d(defaultMessage[i],!1)}}}(t),window.scrollTo(0,document.body.scrollHeight)}),100),o.innerText="")})),o.addEventListener("keypress",(e=>{"Enter"===e.key&&(e.preventDefault(),r.click(),window.scrollTo(0,document.body.scrollHeight))})),o.focus(),o.addEventListener("focus",(function(){this.classList.remove("placeholder")})),o.addEventListener("blur",(function(){this.classList.add("placeholder")})),s.addEventListener("click",(function(e){const t=e.target;if("A"===t.tagName){const n=window.location.href,s=t.getAttribute("href");if(s.startsWith(n)&&window.open(s),s.startsWith("#")){e.preventDefault(),d(t.innerText,!0);h(s.substring(1)),window.scrollTo(0,document.body.scrollHeight)}}})),n=y(n[0].join("\n"),n[1]),d(n,!1),n=n.replace(/.*?\<\/span>/,"")}const defaultMessage=["Désolé, je ne comprends pas votre question.","Pardonnez-moi, mais je ne saisis pas votre demande.","Excusez-moi, je ne parviens pas à comprendre ce que vous demandez.","Je suis navré, mais je ne parviens pas à saisir votre question.","Malheureusement, je ne suis pas en mesure de comprendre votre question.","Je suis désolé, mais je ne saisis pas votre question.","Pardonnez-moi, mais je ne saisis pas le sens de votre question.","Je m'excuse, mais je ne parviens pas à saisir votre demande. Pouvez-vous reformuler votre question, s'il vous plaît ?","Je ne suis pas sûr de comprendre ce que vous demandez. Pouvez-vous expliquer davantage ?","Je ne peux pas répondre à votre question telle qu'elle est formulée. Pouvez-vous la poser différemment ?","Votre question ne semble pas correspondre à mes capacités actuelles. Pourriez-vous la reformuler autrement ?","Je n'ai malheureusement pas compris votre requête.","Je suis désolé, je ne suis pas capable de répondre.","Malheureusement, je ne peux pas répondre à votre question.","Malheureusement je n'arrive pas à comprendre votre requête.","Excusez-moi, je ne comprends pas votre requête.","Excusez-moi, je n'arrive pas à répondre à votre question.","Je ne parviens pas à répondre à votre requête. Veuillez m'excuser."],badWordsMessage=["Même si je ne suis qu'un chatbot, merci de vous adresser à moi avec un langage approprié","Je préférerais que nous restions courtois dans notre communication.","Les insultes ne sont pas nécessaires. Comment puis-je vous aider autrement ?","Essayons de garder une conversation respectueuse.","Je préfère une conversation respectueuse et productive.","Je vous encourage à reformuler votre question ou commentaire de manière respectueuse.","Les mots offensants ne sont pas nécessaires ici. Comment puis-je vous aider de manière constructive ?","Restons courtois dans nos échanges, s'il vous plaît.","Injures et grossièretés ne mènent nulle part. Comment puis-je vous assister ?","Je suis ouvert à la discussion, mais veuillez garder un langage respectueux.","Essayons de communiquer de manière civilisée !"];let md="\n---\ngestionGrosMots: true\n---\n# ChatMD\n\n> Bonjour, je suis ChatMD, un chatbot, que vous pouvez configurer par vous-même en Markdown :\n> \n> - Créez un fichier en Markdown et mettez-le en ligne : sur CodiMD, ou sur une forge\n> - Respectez la syntaxe de ChatMD pour définir votre chatbot\n> \n> Votre chatbot est alors prêt et visible à l'adresse suivante : [https://eyssette.forge.aeif.fr/chatMD/#URL](https://eyssette.forge.aeif.fr/chatMD/#URL) (Mettez l'url de votre fichier à la place de URL)\n> \n> 1. [Qu'est-ce que le Markdown ?](Markdown)\n> 2. [CodiMD, une forge : qu'est-ce que c'est ?](CodiMD et forge)\n> 3. [Quelle syntaxe faut-il respecter pour ChatMD ?](Syntaxe)\n> 4. [Tu peux me donner des exemples !](Exemples)\n> 5. [Quelles sont les options de configuration plus avancées ?](Options de configuration)\n> 6. [À quoi ça sert ?](À quoi ça sert ?)\n\n## Markdown\n- markdown\nLe Markdown est un format de balisage très léger qui permet d'écrire rapidement du texte formaté.\n\nPour découvrir le Markdown, vous pouvez suivre ce [tutoriel](https://www.markdowntutorial.com/fr/).\n\n## CodiMD et forge\n- codimd\n- codi\n- forge\n- en ligne\n\n[CodiMD](https://codimd.apps.education.fr/) est un outil pour écrire du Markdown en ligne et il est disponible avec vos identifiants académiques sur le [portail Apps Edu](https://portail.apps.education.fr/).\n\nUne forge est un outil plus complet qui permet d'héberger des fichiers texte et de les transformer en site web, en carte mentale, ou encore ici en chatbot ! ChatMD est présent sur la [Forge des Communs Numériques Éducatifs](https://forge.aeif.fr/) et vous pouvez aussi mettre vos fichiers sur cette forge.\n\n## Syntaxe\n- syntaxe\n- règles\n- comment\n\nLa syntaxe pour écrire un chatbot avec chatMD est la suivante, mais c'est peut-être plus simple de [voir des exemples](#Exemples) ou bien de [récupérer un modèle](https://codimd.apps.education.fr/mBGbHStJSVOSrlGfGb981A?both).\n\n- On définit le titre du chatbot dans un titre de niveau 1\n- Le message initial est à mettre dans un bloc de citation après le titre du chatbot\n- Les titres de niveau 2 servent à identifier les réponses possibles du chatbot\n- Sous chaque titre de niveau 2 : \n\t- On indique avec une liste non ordonnée les mots clés ou expressions qui vont déclencher la réponse. On peut éventuellement s'en passer si on guide l'utilisateur avec un choix d'options (voir ci-dessous).\n\t- On écrit une réponse en Markdown.\n\t- [Optionnel] On indique avec une liste ordonnée les options possibles. Chaque élément de la liste doit être un lien en Mardown de la forme suivante : `[intitulé de l'option](identifiant de l'option, qui doit correspondre à l'un des titres de niveau 2)`.\n\n1. [Voir aussi les options de configuration plus avancées](Options de configuration)\n\n## Options de configuration\n- yaml\n- en-tête\n- en-tête yaml\n- options\n- avancé\n\nOn peut ajouter un en-tête yaml à son fichier Markdown :\n\n- `clavier: false` désactive le champ d'entrée clavier si on souhaite simplement guider l'utilisateur avec les options proposées en fin de chaque réponse.\n- `rechercheContenu: true` permet d'ajouter une recherche de comparaison de l'entrée de l'utilisateur avec le contenu de chaque réponse\n- `style: a{color:red}` permet d'ajouter des styles CSS personnalisés.\n- `gestionGrosMots: true` permet de détecter les gros mots envoyés par l'utilisateur et de formuler une réponse adéquate si l'utilisateur en utilise\n- `maths: true` permet d'écrire des formules mathématiques en Latex avec la syntaxe `$Latex$` ou `$$Latex$$`\n- `titresRéponses: [\"### \", \"#### \"]` permet de changer les identifiants possibles des réponses du chatbot si on veut pouvoir structurer les réponses du chatbot dans son document\n\n## Exemples\n- exemple\n- donner un exemple\n- concret\n- concrètement\n- modèle\n- template\n\nVoici un modèle que vous pouvez récupérer pour construire votre chatbot : [modèle à récupérer](https://codimd.apps.education.fr/mBGbHStJSVOSrlGfGb981A?both)\n\nVoici quelques exemple de chatbot créés avec ChatMD : \n\n- [Méthode de la dissertation en philosophie](https://eyssette.forge.aeif.fr/chatMD/#https://eyssette.forge.aeif.fr/chatbot/dissertation-philosophie.md)\n- [Utilisation d'un microscope](https://eyssette.forge.aeif.fr/chatMD/#https://codimd.apps.education.fr/xGNHIJSeTVCk6FHas-_71g) : un chatbot créé à partir du travail de Guillaume Berthelot et de Jérémy Navoizat [voir la source](https://codimd.apps.education.fr/xGNHIJSeTVCk6FHas-_71g?both)\n\n## À quoi ça sert ?\n- à quoi ça sert ?\n- pourquoi\n- sert\n- intérêt\n- servir\n- objectif\n- but\n- en faire\n- tutoriel\n- histoire\n- méthod\n- révision\n- utilité\n- utile\n\nOn peut imaginer plusieurs usages de chatMD :\n- Tutoriel pour un outil informatique\n- Histoire dont vous êtes le héros\n- Guide méthodologique\n- Soutien pour la révision d'un cours\n- …\n\nOn peut faire travailler des élèves ensemble sur un CodiMD, ou bien travailler collaborativement entre collègues, en tant que prof ou dans le cadre d'une formation.\n\nSi vous avez trouvé des idées intéressantes, n'hésitez pas à les partager avec moi. Vous pouvez me contacter sur [Mastodon](https://scholar.social/@eyssette).\n\n## Merci\n- merci\n- remercier\n- remercie\n- félicitations\n- félicit\n- bravo\n- super\n- excellent\n- génial\n- wow\n- chouette\n- sympa\n\nMerci ! Si vous aimez ce travail, vous aimerez peut-être aussi les autres outils ou sites que je propose sur [mon site perso](https://eyssette.github.io).\n\n\n";const shortcuts=[["dissertation-philo","https://raw.githubusercontent.com/eyssette/chatbot/main/dissertation-philosophie.md"]];let chatData,filterBadWords,yamlStyle="",yamlUserInput=!0,yamlSearchInContent=!1,yamldetectBadWords=!1,yamlMaths=!1;function getMarkdownContent(){let e=window.location.hash.substring(1);""!==e?(e.startsWith("https://github.com")&&(e=e.replace("https://github.com","https://raw.githubusercontent.com"),e=e.replace("/blob/","/")),e.startsWith("https://codimd")&&-1===e.indexOf("download")&&(e=e.replace("?edit","").replace("?both","").replace("?view","")+"/download"),shortcut=shortcuts.find((t=>t[0]==e)),shortcut&&(e=shortcut[1]),fetch(e).then((e=>e.text())).then((e=>{md=e,chatData=parseMarkdown(md),createChatBot(chatData)})).catch((e=>console.error(e)))):createChatBot(parseMarkdown(md))}function loadScript(e){return new Promise(((t,n)=>{const s=document.createElement("script");s.src=e,s.onload=t,s.onerror=n,document.head.appendChild(s)}))}function loadCSS(e){return new Promise(((t,n)=>{const s=document.createElement("link");s.href=e,s.rel="stylesheet",s.onload=t,s.onerror=n,document.head.appendChild(s)}))}function startsWithAnyOf(e,t){for(const n of t)if(e.startsWith(n))return n}function parseMarkdown(e){let t=["## "];if(e.split("---").length>2)try{yamlData=jsyaml.load(e.split("---")[1]);for(const e in yamlData){if("maths"==e&&(yamlMaths=yamlData[e],!0===yamlMaths&&Promise.all([loadScript("https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"),loadCSS("https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css")])),"titresRéponses"!=e&&"responsesTitles"!=e||(t=yamlData[e],"string"==typeof t&&(t=[t])),"style"==e){yamlStyle=yamlData[e];const t=document.createElement("style");t.innerHTML=yamlStyle,document.body.appendChild(t)}if(("userInput"==e||"clavier"==e||"keyboard"==e)&&(yamlUserInput=yamlData[e],!1===yamlUserInput)){document.getElementById("controls").style.display="none"}"searchInContent"!=e&&"rechercheContenu"!=e||(yamlSearchInContent=yamlData[e]),"gestionGrosMots"!=e&&"detectBadWords"!=e||(yamldetectBadWords=yamlData[e],!0===yamldetectBadWords&&Promise.all([loadScript("https://cdn.jsdelivr.net/npm/leo-profanity"),loadScript("badWords-fr.js")]).then((()=>{filterBadWords=LeoProfanity,filterBadWords.add(badWordsFR)})).catch((e=>{console.error("Une erreur s'est produite lors du chargement des scripts :",e)})))}}catch(e){}const n=e.split("\n");let s=[],o=[""],r=!1,a=null,i=[],u=[],l=null;const c=/^\d{1,3}\.\s\[/;let d=!1,p=[],m=[];for(let e of n)if(e.startsWith("# "))o[0]=e.replace("# ","").trim();else if(e.startsWith(">")&&!r)if(e=e.replace(/^>\s?/,"").trim(),e.match(c)){const t=e.replace(/^\d+\.\s/,"").trim(),n=t.replace(/^\[.*?\]\(/,"").replace(/\)$/,""),s=t.replace(/\]\(.*/,"").replace(/^\[/,"");m.push([s,n])}else p.push(e);else if(startsWithAnyOf(e,t))r=!0,a&&s.push([a,i,u,l]),a=e.replace(startsWithAnyOf(e,t),"").trim(),i=[],l=null,d=!1,u=[];else if(e.startsWith("- ")&&!d)i.push(e.replace("- ","").trim());else if(e.match(c)){d=!1,l||(l=[]);const t=e.replace(/^\d+\.\s/,"").trim(),n=t.replace(/^\[.*?\]\(/,"").replace(/\)$/,""),s=t.replace(/\]\(.*/,"").replace(/^\[/,"");l.push([s,n])}else e.length>0&&(u.push(e+"\n"),d=!0);s.push([a,i,u.join("\n"),l]);const h=[p,m];return s.push(h),s.push(o),s}getMarkdownContent();
--------------------------------------------------------------------------------
/chatdata.js:
--------------------------------------------------------------------------------
1 | const defaultMessage = [
2 | "Désolé, je ne comprends pas votre question.",
3 | "Pardonnez-moi, mais je ne saisis pas votre demande.",
4 | "Excusez-moi, je ne parviens pas à comprendre ce que vous demandez.",
5 | "Je suis navré, mais je ne parviens pas à saisir votre question.",
6 | "Malheureusement, je ne suis pas en mesure de comprendre votre question.",
7 | "Je suis désolé, mais je ne saisis pas votre question.",
8 | "Pardonnez-moi, mais je ne saisis pas le sens de votre question.",
9 | "Je m'excuse, mais je ne parviens pas à saisir votre demande. Pouvez-vous reformuler votre question, s'il vous plaît ?",
10 | "Je ne suis pas sûr de comprendre ce que vous demandez. Pouvez-vous expliquer davantage ?",
11 | "Je ne peux pas répondre à votre question telle qu'elle est formulée. Pouvez-vous la poser différemment ?",
12 | "Votre question ne semble pas correspondre à mes capacités actuelles. Pourriez-vous la reformuler autrement ?",
13 | "Je n'ai malheureusement pas compris votre requête.",
14 | "Je suis désolé, je ne suis pas capable de répondre.",
15 | "Malheureusement, je ne peux pas répondre à votre question.",
16 | "Malheureusement je n'arrive pas à comprendre votre requête.",
17 | "Excusez-moi, je ne comprends pas votre requête.",
18 | "Excusez-moi, je n'arrive pas à répondre à votre question.",
19 | "Je ne parviens pas à répondre à votre requête. Veuillez m'excuser.",
20 | ];
21 |
22 | const badWordsMessage = [
23 | "Même si je ne suis qu'un chatbot, merci de vous adresser à moi avec un langage approprié",
24 | "Je préférerais que nous restions courtois dans notre communication.",
25 | "Les insultes ne sont pas nécessaires. Comment puis-je vous aider autrement ?",
26 | "Essayons de garder une conversation respectueuse.",
27 | "Je préfère une conversation respectueuse et productive.",
28 | "Je vous encourage à reformuler votre question ou commentaire de manière respectueuse.",
29 | "Les mots offensants ne sont pas nécessaires ici. Comment puis-je vous aider de manière constructive ?",
30 | "Restons courtois dans nos échanges, s'il vous plaît.",
31 | "Injures et grossièretés ne mènent nulle part. Comment puis-je vous assister ?",
32 | "Je suis ouvert à la discussion, mais veuillez garder un langage respectueux.",
33 | "Essayons de communiquer de manière civilisée !",
34 | ];
35 |
36 | let md = `
37 | ---
38 | gestionGrosMots: true
39 | ---
40 | # ChatMD
41 |
42 | > Bonjour, je suis ChatMD, un chatbot, que vous pouvez configurer par vous-même en Markdown :
43 | >
44 | > - Créez un fichier en Markdown et mettez-le en ligne : sur CodiMD, ou sur une forge
45 | > - Respectez la syntaxe de ChatMD pour définir votre chatbot
46 | >
47 | > Votre chatbot est alors prêt et visible à l'adresse suivante : [https://eyssette.forge.aeif.fr/chatMD/#URL](https://eyssette.forge.aeif.fr/chatMD/#URL) (Mettez l'url de votre fichier à la place de URL)
48 | >
49 | > 1. [Qu'est-ce que le Markdown ?](Markdown)
50 | > 2. [CodiMD, une forge : qu'est-ce que c'est ?](CodiMD et forge)
51 | > 3. [Quelle syntaxe faut-il respecter pour ChatMD ?](Syntaxe)
52 | > 4. [Tu peux me donner des exemples !](Exemples)
53 | > 5. [Quelles sont les options de configuration plus avancées ?](Options de configuration)
54 | > 6. [À quoi ça sert ?](À quoi ça sert ?)
55 |
56 | ## Markdown
57 | - markdown
58 | Le Markdown est un format de balisage très léger qui permet d'écrire rapidement du texte formaté.
59 |
60 | Pour découvrir le Markdown, vous pouvez suivre ce [tutoriel](https://www.markdowntutorial.com/fr/).
61 |
62 | ## CodiMD et forge
63 | - codimd
64 | - codi
65 | - forge
66 | - en ligne
67 |
68 | [CodiMD](https://codimd.apps.education.fr/) est un outil pour écrire du Markdown en ligne et il est disponible avec vos identifiants académiques sur le [portail Apps Edu](https://portail.apps.education.fr/).
69 |
70 | Une forge est un outil plus complet qui permet d'héberger des fichiers texte et de les transformer en site web, en carte mentale, ou encore ici en chatbot ! ChatMD est présent sur la [Forge des Communs Numériques Éducatifs](https://forge.aeif.fr/) et vous pouvez aussi mettre vos fichiers sur cette forge.
71 |
72 | ## Syntaxe
73 | - syntaxe
74 | - règles
75 | - comment
76 |
77 | La syntaxe pour écrire un chatbot avec chatMD est la suivante, mais c'est peut-être plus simple de [voir des exemples](#Exemples) ou bien de [récupérer un modèle](https://codimd.apps.education.fr/mBGbHStJSVOSrlGfGb981A?both).
78 |
79 | - On définit le titre du chatbot dans un titre de niveau 1
80 | - Le message initial est à mettre dans un bloc de citation après le titre du chatbot
81 | - Les titres de niveau 2 servent à identifier les réponses possibles du chatbot
82 | - Sous chaque titre de niveau 2 :
83 | - On indique avec une liste non ordonnée les mots clés ou expressions qui vont déclencher la réponse. On peut éventuellement s'en passer si on guide l'utilisateur avec un choix d'options (voir ci-dessous).
84 | - On écrit une réponse en Markdown.
85 | - [Optionnel] On indique avec une liste ordonnée les options possibles. Chaque élément de la liste doit être un lien en Mardown de la forme suivante : \`[intitulé de l'option](identifiant de l'option, qui doit correspondre à l'un des titres de niveau 2)\`.
86 |
87 | 1. [Voir aussi les options de configuration plus avancées](Options de configuration)
88 |
89 | ## Options de configuration
90 | - yaml
91 | - en-tête
92 | - en-tête yaml
93 | - options
94 | - avancé
95 |
96 | On peut ajouter un en-tête yaml à son fichier Markdown :
97 |
98 | - \`clavier: false\` désactive le champ d'entrée clavier si on souhaite simplement guider l'utilisateur avec les options proposées en fin de chaque réponse.
99 | - \`rechercheContenu: true\` permet d'ajouter une recherche de comparaison de l'entrée de l'utilisateur avec le contenu de chaque réponse
100 | - \`style: a{color:red}\` permet d'ajouter des styles CSS personnalisés.
101 | - \`gestionGrosMots: true\` permet de détecter les gros mots envoyés par l'utilisateur et de formuler une réponse adéquate si l'utilisateur en utilise
102 | - \`maths: true\` permet d'écrire des formules mathématiques en Latex avec la syntaxe \`$Latex$\` ou \`$$Latex$$\`
103 | - \`titresRéponses: ["### ", "#### "]\` permet de changer les identifiants possibles des réponses du chatbot si on veut pouvoir structurer les réponses du chatbot dans son document
104 |
105 | ## Exemples
106 | - exemple
107 | - donner un exemple
108 | - concret
109 | - concrètement
110 | - modèle
111 | - template
112 |
113 | Voici un modèle que vous pouvez récupérer pour construire votre chatbot : [modèle à récupérer](https://codimd.apps.education.fr/mBGbHStJSVOSrlGfGb981A?both)
114 |
115 | Voici quelques exemple de chatbot créés avec ChatMD :
116 |
117 | - [Méthode de la dissertation en philosophie](https://eyssette.forge.aeif.fr/chatMD/#https://eyssette.forge.aeif.fr/chatbot/dissertation-philosophie.md)
118 | - [Utilisation d'un microscope](https://eyssette.forge.aeif.fr/chatMD/#https://codimd.apps.education.fr/xGNHIJSeTVCk6FHas-_71g) : un chatbot créé à partir du travail de Guillaume Berthelot et de Jérémy Navoizat [voir la source](https://codimd.apps.education.fr/xGNHIJSeTVCk6FHas-_71g?both)
119 |
120 | ## À quoi ça sert ?
121 | - à quoi ça sert ?
122 | - pourquoi
123 | - sert
124 | - intérêt
125 | - servir
126 | - objectif
127 | - but
128 | - en faire
129 | - tutoriel
130 | - histoire
131 | - méthod
132 | - révision
133 | - utilité
134 | - utile
135 |
136 | On peut imaginer plusieurs usages de chatMD :
137 | - Tutoriel pour un outil informatique
138 | - Histoire dont vous êtes le héros
139 | - Guide méthodologique
140 | - Soutien pour la révision d'un cours
141 | - …
142 |
143 | On peut faire travailler des élèves ensemble sur un CodiMD, ou bien travailler collaborativement entre collègues, en tant que prof ou dans le cadre d'une formation.
144 |
145 | Si vous avez trouvé des idées intéressantes, n'hésitez pas à les partager avec moi. Vous pouvez me contacter sur [Mastodon](https://scholar.social/@eyssette).
146 |
147 | ## Merci
148 | - merci
149 | - remercier
150 | - remercie
151 | - félicitations
152 | - félicit
153 | - bravo
154 | - super
155 | - excellent
156 | - génial
157 | - wow
158 | - chouette
159 | - sympa
160 |
161 | Merci ! Si vous aimez ce travail, vous aimerez peut-être aussi les autres outils ou sites que je propose sur [mon site perso](https://eyssette.github.io).
162 |
163 |
164 | `;
165 |
166 | const shortcuts = [
167 | ["dissertation-philo","https://raw.githubusercontent.com/eyssette/chatbot/main/dissertation-philosophie.md"]
168 | ];
169 |
170 | let yamlStyle = "";
171 | let yamlUserInput = true;
172 | let yamlSearchInContent = false;
173 | let yamldetectBadWords = false;
174 | let yamlMaths = false;
175 |
176 | let chatData;
177 | let filterBadWords;
178 |
179 | function getMarkdownContent() {
180 | // Récupération du markdown externe
181 | let urlMD = window.location.hash.substring(1); // Récupère l'URL du hashtag sans le #
182 | if (urlMD !== "") {
183 | // Gestion des fichiers hébergés sur github
184 | if (urlMD.startsWith("https://github.com")) {
185 | urlMD = urlMD.replace(
186 | "https://github.com",
187 | "https://raw.githubusercontent.com"
188 | );
189 | urlMD = urlMD.replace("/blob/", "/");
190 | }
191 | // Gestion des fichiers hébergés sur codiMD
192 | if (
193 | urlMD.startsWith("https://codimd") &&
194 | urlMD.indexOf("download") === -1
195 | ) {
196 | urlMD =
197 | urlMD.replace("?edit", "").replace("?both", "").replace("?view", "") +
198 | "/download";
199 | }
200 | // Vérification de la présence d'un raccourci
201 | shortcut = shortcuts.find(element => element[0]==urlMD)
202 | if (shortcut) {
203 | urlMD = shortcut[1]
204 | }
205 | // Récupération du contenu du fichier
206 | fetch(urlMD)
207 | .then((response) => response.text())
208 | .then((data) => {
209 | md = data;
210 | chatData = parseMarkdown(md);
211 | /* console.log(chatData); */
212 | createChatBot(chatData);
213 | })
214 | .catch((error) => console.error(error));
215 | } else {
216 | createChatBot(parseMarkdown(md));
217 | }
218 | }
219 |
220 | getMarkdownContent();
221 |
222 | function loadScript(src) {
223 | // Fonction pour charger des scripts
224 | return new Promise((resolve, reject) => {
225 | const script = document.createElement("script");
226 | script.src = src;
227 | script.onload = resolve;
228 | script.onerror = reject;
229 | document.head.appendChild(script);
230 | });
231 | }
232 | function loadCSS(src) {
233 | // Fonction pour charger des CSS
234 | return new Promise((resolve, reject) => {
235 | const styleElement = document.createElement("link");
236 | styleElement.href = src;
237 | styleElement.rel = "stylesheet";
238 | styleElement.onload = resolve;
239 | styleElement.onerror = reject;
240 | document.head.appendChild(styleElement);
241 | });
242 | }
243 |
244 | function startsWithAnyOf(string,array) {
245 | // Vérifie si une variable texte commence par un élément d'un tableau
246 | for (const element of array) {
247 | if (string.startsWith(element)) {
248 | return element
249 | break;
250 | }
251 | }
252 | }
253 |
254 | function parseMarkdown(markdownContent) {
255 | let responsesTitles = ["## "]; // Par défaut les titres des réponses sont définis par des titres en markdown niveau 2
256 | if (markdownContent.split("---").length > 2) {
257 | try {
258 | yamlData = jsyaml.load(markdownContent.split("---")[1]);
259 | for (const property in yamlData) {
260 | if (property == "maths") {
261 | yamlMaths = yamlData[property];
262 | if (yamlMaths === true) {
263 | Promise.all([
264 | loadScript(
265 | "https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"
266 | ),
267 | loadCSS(
268 | "https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css"
269 | ),
270 | ]);
271 | }
272 | }
273 | if (property == "titresRéponses" || property == "responsesTitles") {
274 | responsesTitles = yamlData[property];
275 | if (typeof responsesTitles === 'string') {
276 | // Cas où le yaml pour les titres des réponses ne contient pas un tableau, mais un seul élément
277 | responsesTitles = [responsesTitles];
278 | }
279 | }
280 | if (property == "style") {
281 | yamlStyle = yamlData[property];
282 | const styleElement = document.createElement("style");
283 | styleElement.innerHTML = yamlStyle;
284 | document.body.appendChild(styleElement);
285 | }
286 | if (
287 | property == "userInput" ||
288 | property == "clavier" ||
289 | property == "keyboard"
290 | ) {
291 | yamlUserInput = yamlData[property];
292 | if (yamlUserInput === false) {
293 | const controls = document.getElementById("controls");
294 | controls.style.display = "none";
295 | }
296 | }
297 | if (property == "searchInContent" || property == "rechercheContenu") {
298 | yamlSearchInContent = yamlData[property];
299 | }
300 | if (property == "gestionGrosMots" || property == "detectBadWords") {
301 | yamldetectBadWords = yamlData[property];
302 | if (yamldetectBadWords === true) {
303 | Promise.all([
304 | loadScript("https://cdn.jsdelivr.net/npm/leo-profanity"),
305 | loadScript("badWords-fr.js"),
306 | ])
307 | .then(() => {
308 | // Les deux scripts sont chargés et prêts à être utilisés
309 | filterBadWords = LeoProfanity;
310 | filterBadWords.add(badWordsFR);
311 | })
312 | .catch((error) => {
313 | console.error(
314 | "Une erreur s'est produite lors du chargement des scripts :",
315 | error
316 | );
317 | });
318 | }
319 | }
320 | }
321 | } catch (e) {}
322 | }
323 | const lines = markdownContent.split("\n");
324 | let chatbotData = [];
325 | let chatbotTitle = [""];
326 |
327 | let initialMessageComputed = false;
328 | let currentH2Title = null;
329 | let currentLiItems = [];
330 | let content = [];
331 | let lastOrderedList = null;
332 | const regexOrderedList = /^\d{1,3}\.\s\[/;
333 | let listParsed = false;
334 | let initialMessageContent = [];
335 | let initialMessageOptions = [];
336 |
337 | for (let line of lines) {
338 | // On parcourt le contenu du fichier ligne par ligne
339 | if (line.startsWith("# ")) {
340 | // Récupération du titre du chatbot
341 | chatbotTitle[0] = line.replace("# ", "").trim();
342 | } else if (line.startsWith(">") && !initialMessageComputed) {
343 | // Récupération du message initial du chatbot, défini par un bloc citation
344 | line = line.replace(/^>\s?/, "").trim();
345 | if (line.match(regexOrderedList)) {
346 | // Récupération des options dans le message initial, s'il y en a
347 | const listContent = line.replace(/^\d+\.\s/, "").trim();
348 | const link = listContent.replace(/^\[.*?\]\(/, "").replace(/\)$/, "");
349 | const text = listContent.replace(/\]\(.*/, "").replace(/^\[/, "");
350 | initialMessageOptions.push([text, link]);
351 | } else {
352 | initialMessageContent.push(line);
353 | }
354 | } else if (startsWithAnyOf(line,responsesTitles)) {
355 | // Gestion des identifiants de réponse, et début de traitement du contenu de chaque réponse
356 | initialMessageComputed = true;
357 | if (currentH2Title) {
358 | chatbotData.push([
359 | currentH2Title,
360 | currentLiItems,
361 | content,
362 | lastOrderedList,
363 | ]);
364 | }
365 | currentH2Title = line.replace(startsWithAnyOf(line,responsesTitles), "").trim(); // Titre h2
366 | currentLiItems = [];
367 | lastOrderedList = null;
368 | listParsed = false;
369 | content = [];
370 | } else if (line.startsWith("- ") && !listParsed) {
371 | // Gestion des listes
372 | currentLiItems.push(line.replace("- ", "").trim());
373 | } else if (line.match(regexOrderedList)) {
374 | // Cas des listes ordonnées
375 | listParsed = false;
376 | if (!lastOrderedList) {
377 | lastOrderedList = [];
378 | }
379 | const listContent = line.replace(/^\d+\.\s/, "").trim();
380 | const link = listContent.replace(/^\[.*?\]\(/, "").replace(/\)$/, "");
381 | const text = listContent.replace(/\]\(.*/, "").replace(/^\[/, "");
382 | lastOrderedList.push([text, link]);
383 | /* lastOrderedList.push(listContent); */
384 | } else if (line.length > 0) {
385 | // Gestion du reste du contenu
386 | content.push(line + "\n");
387 | listParsed = true;
388 | }
389 | }
390 | chatbotData.push([
391 | currentH2Title,
392 | currentLiItems,
393 | content.join("\n"),
394 | lastOrderedList,
395 | ]);
396 |
397 | const initialMessage = [initialMessageContent, initialMessageOptions];
398 | chatbotData.push(initialMessage);
399 | chatbotData.push(chatbotTitle);
400 |
401 | return chatbotData;
402 | }
403 |
--------------------------------------------------------------------------------
/chatbot.js:
--------------------------------------------------------------------------------
1 | function createChatBot(chatData) {
2 | const chatbotName = chatData.pop();
3 | let initialMessage = chatData.pop();
4 | document.getElementById("chatbot-name").textContent = chatbotName;
5 |
6 | const chatContainer = document.getElementById("chat");
7 | const userInput = document.getElementById("user-input");
8 | const sendButton = document.getElementById("send-button");
9 | let optionsLastResponse = null;
10 | let randomDefaultMessageIndex = Math.floor(
11 | Math.random() * defaultMessage.length
12 | );
13 | let randomDefaultMessageIndexLastChoice = [];
14 |
15 | // Gestion du markdown dans les réponses du chatbot
16 | var converter = new showdown.Converter({
17 | emoji: true,
18 | parseImgDimensions: true,
19 | });
20 | function markdownToHTML(text) {
21 | const html = converter.makeHtml(text);
22 | return html;
23 | }
24 |
25 | // Effet machine à écrire
26 | function typeWriter(content, element) {
27 | var typed = new Typed(element, {
28 | strings: [content],
29 | typeSpeed: 1,
30 | startDelay: 100,
31 | onBegin: () => {userInput.focus();},
32 | });
33 | }
34 |
35 | function convertLatexExpressions(string) {
36 | string = string
37 | .replace(/\$\$(.*?)\$\$/g, "\[$1\]")
38 | .replace(/\$(.*?)\$/g, "\($1\)");
39 | let expressionsLatex = string.match(
40 | new RegExp(/\\[.*?\\]|\\(.*?\\)/g)
41 | );
42 | if (expressionsLatex) {
43 | // On n'utilise Katex que s'il y a des expressions en Latex dans le Markdown
44 | for (let expressionLatex of expressionsLatex) {
45 | // On vérifie si le mode d'affichage de l'expression (inline ou block)
46 | const inlineMaths = expressionLatex.includes("\[") ? true : false;
47 | // On récupère la formule mathématique
48 | let mathInExpressionLatex = expressionLatex
49 | .replace("\[", "")
50 | .replace("\]", "");
51 | mathInExpressionLatex = mathInExpressionLatex
52 | .replace("\(", "")
53 | .replace("\)", "");
54 | // On convertit la formule mathématique en HTML avec Katex
55 | stringWithLatex = katex.renderToString(mathInExpressionLatex, {
56 | displayMode: inlineMaths,
57 | });
58 | string = string.replace(expressionLatex, stringWithLatex);
59 | }
60 | }
61 | return string;
62 | }
63 |
64 | // Création du message par le bot ou l'utilisateur
65 | function createChatMessage(message, isUser) {
66 | const chatMessage = document.createElement("div");
67 | chatMessage.classList.add("message");
68 | chatMessage.classList.add(isUser ? "user-message" : "bot-message");
69 | let html = markdownToHTML(message);
70 | if (yamlMaths === true) {
71 | // S'il y a des maths, on doit gérer le Latex avant d'afficher le message
72 | setTimeout(() => {
73 | html = convertLatexExpressions(html);
74 | // Effet machine à écrire : seulement quand c'est le chatbot qui répond, sinon affichage direct
75 | if (isUser) {
76 | chatMessage.innerHTML = html;
77 | } else {
78 | typeWriter(html, chatMessage);
79 | }
80 | chatContainer.appendChild(chatMessage);
81 | }, 100);
82 | } else {
83 | // Effet machine à écrire : seulement quand c'est le chatbot qui répond, sinon affichage direct
84 | if (isUser) {
85 | chatMessage.innerHTML = html;
86 | } else {
87 | typeWriter(html, chatMessage);
88 | }
89 | chatContainer.appendChild(chatMessage);
90 | }
91 | }
92 |
93 | function levenshteinDistance(a, b) {
94 | /* Fonction pour calculer une similarité plutôt que d'en rester à une identité stricte */
95 | if (a.length === 0) return b.length;
96 | if (b.length === 0) return a.length;
97 |
98 | const matrix = [];
99 | for (let i = 0; i <= b.length; i++) {
100 | matrix[i] = [i];
101 | }
102 |
103 | for (let j = 0; j <= a.length; j++) {
104 | matrix[0][j] = j;
105 | }
106 |
107 | for (let i = 1; i <= b.length; i++) {
108 | for (let j = 1; j <= a.length; j++) {
109 | const cost = a[j - 1] === b[i - 1] ? 0 : 1;
110 | matrix[i][j] = Math.min(
111 | matrix[i - 1][j] + 1,
112 | matrix[i][j - 1] + 1,
113 | matrix[i - 1][j - 1] + cost
114 | );
115 | }
116 | }
117 |
118 | return matrix[b.length][a.length];
119 | }
120 |
121 | function hasLevenshteinDistanceLessThan(string, keyWord, distance) {
122 | // Teste la présence d'un mot dans une chaîne de caractère qui a une distance de Levenshstein inférieure à une distance donnée
123 |
124 | const words = string.split(" ");
125 | // On parcourt les mots
126 |
127 | for (const word of words) {
128 | // On calcule la distance de Levenshtein entre le mot et le mot cible
129 | const distanceLevenshtein = levenshteinDistance(word, keyWord);
130 |
131 | // Si la distance est inférieure à la distance donnée, on renvoie vrai
132 | if (distanceLevenshtein < distance) {
133 | return true;
134 | }
135 | }
136 |
137 | // Si on n'a pas trouvé de mot avec une distance inférieure à la distance donnée, on renvoie faux
138 | return false;
139 | }
140 |
141 | const LEVENSHTEIN_THRESHOLD = 3; // Seuil de similarité
142 | const MATCH_SCORE_IDENTITY = 5; // Pour régler le fait de privilégier l'identité d'un mot à la simple similarité
143 | const BESTMATCH_THRESHOLD = 0.55 // Seuil pour que le bestMatch soit pertinent
144 |
145 | function responseToSelectedOption(optionLink) {
146 | // Gestion de la réponse à envoyer si on sélectionne une des options proposées
147 | if (optionLink != "") {
148 | for (let i = 0; i < chatData.length; i++) {
149 | const title = chatData[i][0];
150 | if (optionLink == title) {
151 | let response = chatData[i][2];
152 | const options = chatData[i][3];
153 | response = Array.isArray(response) ? response.join("\n\n") : response;
154 | optionsLastResponse = options;
155 | response = gestionOptions(response, options);
156 | createChatMessage(response, false);
157 | }
158 | }
159 | } else {
160 | createChatMessage(initialMessage, false);
161 | }
162 | }
163 |
164 | function removeAccents(str) {
165 | const accentMap = {
166 | à: 'a',
167 | â: 'a',
168 | é: 'e',
169 | è: 'e',
170 | ê: 'e',
171 | ë: 'e',
172 | î: 'i',
173 | ï: 'i',
174 | ô: 'o',
175 | ö: 'o',
176 | û: 'u',
177 | ü: 'u',
178 | ÿ: 'y',
179 | ç: 'c',
180 | À: 'A',
181 | Â: 'A',
182 | É: 'E',
183 | È: 'E',
184 | Ê: 'E',
185 | Ë: 'E',
186 | Î: 'I',
187 | Ï: 'I',
188 | Ô: 'O',
189 | Ö: 'O',
190 | Û: 'U',
191 | Ü: 'U',
192 | Ÿ: 'Y',
193 | Ç: 'C',
194 | };
195 |
196 | return str.replace(/[àâéèêëîïôöûüÿçÀÂÉÈÊËÎÏÔÖÛÜŸÇ]/g, (match) => accentMap[match] || match);
197 | }
198 |
199 | function tokenize(text, indexChatBotResponse) {
200 | // Fonction pour diviser une chaîne de caractères en tokens, éventuellement en prenant en compte l'index de la réponse du Chatbot (pour prendre en compte différement les tokens présents dans le titre de la réponse)
201 |
202 | // On garde d'abord seulement les mots d'au moins 5 caractères et on remplace les lettres accentuées par l'équivalent sans accent
203 | let words = text.toLowerCase()
204 | words = words.replace(/,|\.|\:|\?|\!|\(|\)|\[|\||\/\]/g,"");
205 | words = removeAccents(words);
206 | words = words.split(/\s|'/).filter(word => word.length >= 5) || [];
207 | const tokens = [];
208 |
209 | // On va créer des tokens avec à chaque fois un poids associé
210 | for (const word of words) {
211 | // Premier type de token : le mot en entier ; poids le plus important
212 | tokens.push({word, weight: 5});
213 |
214 | // Ensuite on intègre des tokens de 5, 6 et 7 caractères consécutifs pour détecter des racines communes
215 | // Plus le token est long, plus le poids du token est important
216 | const weights = [0, 0, 0, 0, 0.4, 0.6, 0.8];
217 | // Si le token correspond au début du mot, le poids est plus important
218 | const bonusStart = 0.2;
219 | // Si le token est présent dans le titre, le poids est très important
220 | const bonusInTitle = 10;
221 |
222 | function weightedToken(index, tokenDimension) {
223 | let weight = weights[tokenDimension-1]; // Poids en fonction de la taille du token
224 | weight = index === 0 ? weight+bonusStart : weight; // Bonus si le token est en début du mot
225 | const token = word.substring(index, index + tokenDimension);
226 | if (indexChatBotResponse) {
227 | const titleResponse = chatData[indexChatBotResponse][0].toLowerCase();
228 | // Bonus si le token est dans le titre
229 | if (titleResponse.includes(token)) {
230 | weight = weight + bonusInTitle;
231 | }
232 | }
233 | return {token, weight: weight}
234 | }
235 |
236 | const wordLength = word.length;
237 |
238 | if (wordLength >= 5) {
239 | for (let i = 0; i <= wordLength - 5; i++) {
240 | tokens.push(weightedToken(i,5));
241 | }
242 | }
243 | if (wordLength >= 6) {
244 | for (let i = 0; i <= wordLength - 6; i++) {
245 | tokens.push(weightedToken(i,6));
246 | }
247 | }
248 | if (wordLength >= 7) {
249 | for (let i = 0; i <= wordLength - 7; i++) {
250 | tokens.push(weightedToken(i,7));
251 | }
252 | }
253 | }
254 | return tokens;
255 | }
256 |
257 | function createVector(text, indexChatBotResponse) {
258 | // Fonction pour créer un vecteur pour chaque texte en prenant en compte le poids de chaque token et éventuellement l'index de la réponse du chatbot
259 | const index = indexChatBotResponse ? indexChatBotResponse : false
260 | const tokens = tokenize(text, indexChatBotResponse);
261 | const vec = {};
262 | for (const {token, weight} of tokens) {
263 | if (token) {
264 | vec[token] = (vec[token] || 0) + weight;
265 | }
266 | }
267 | return vec;
268 | }
269 |
270 | let vectorChatBotResponses = [];
271 | // On précalcule les vecteurs des réponses du chatbot
272 | if (yamlSearchInContent) {
273 | for (let i = 0; i < chatData.length; i++) {
274 | const responses = chatData[i][2];
275 | let response = Array.isArray(responses) ? responses.join(" ").toLowerCase() : responses.toLowerCase();
276 | response = chatData[i][0] + ' ' + response
277 | const vectorResponse = createVector(response, i)
278 | vectorChatBotResponses.push(vectorResponse)
279 | }
280 | }
281 |
282 | function cosineSimilarity(str, vector) {
283 | // Calcul de similarité entre une chaîne de caractère (ce sera le message de l'utilisateur) et une autre chaîne de caractère déjà transformée en vecteur (c'est le vecteur de la réponse du chatbot)
284 |
285 | // Calcule le produit scalaire de deux vecteurs
286 | function dotProduct(vec1, vec2) {
287 | const commonWords = new Set([...Object.keys(vec1), ...Object.keys(vec2)]);
288 | let dot = 0;
289 | for (const word of commonWords) {
290 | dot += (vec1[word] || 0) * (vec2[word] || 0);
291 | }
292 | return dot;
293 | }
294 |
295 | // Calcule la magnitude d'un vecteur
296 | function magnitude(vec) {
297 | let sum = 0;
298 | for (const word in vec) {
299 | sum += vec[word] ** 2;
300 | }
301 | return Math.sqrt(sum);
302 | }
303 |
304 | // Crée les vecteurs pour la chaîne de caractère (qui correspondra au message de l'utilisateur)
305 | const vectorString = createVector(str);
306 |
307 | // Calcule la similarité cosinus
308 | const dot = dotProduct(vectorString, vector);
309 | const mag1 = magnitude(vectorString);
310 | const mag2 = magnitude(vector);
311 |
312 | if (mag1 === 0 || mag2 === 0) {
313 | return 0; // Évitez la division par zéro
314 | } else {
315 | return dot / (mag1 * mag2);
316 | }
317 | }
318 |
319 | function chatbotResponse(userInputText) {
320 | // Choix de la réponse que le chatbot va envoyer
321 |
322 | if(yamldetectBadWords === true && filterBadWords) {
323 | if (filterBadWords.check(userInputText)) {
324 | const randomBadWordsMessageIndex = Math.floor(
325 | Math.random() * badWordsMessage.length
326 | );
327 | createChatMessage(badWordsMessage[randomBadWordsMessageIndex],false);
328 | return;
329 | }
330 | }
331 |
332 | let bestMatch = null;
333 | let bestMatchScore = 0;
334 | let bestDistanceScore = 0;
335 | let userInputTextToLowerCase = userInputText.toLowerCase();
336 | let indexBestMatch;
337 |
338 | let optionsLastResponseKeysToLowerCase;
339 | let indexLastResponseKeyMatch;
340 | if (optionsLastResponse) {
341 | // On va comparer le message de l'utilisateur aux dernières options proposées s'il y en a une
342 | optionsLastResponseKeysToLowerCase = optionsLastResponse.map(
343 | (element) => {
344 | return element[0].toLowerCase();
345 | }
346 | );
347 | indexLastResponseKeyMatch = optionsLastResponseKeysToLowerCase.indexOf(
348 | userInputTextToLowerCase
349 | );
350 | }
351 |
352 | if (optionsLastResponse && indexLastResponseKeyMatch > -1) {
353 | // Si le message de l'utilisateur correspond à une des options proposées, on renvoie directement vers cette option
354 | const optionLink = optionsLastResponse[indexLastResponseKeyMatch][1];
355 | responseToSelectedOption(optionLink);
356 | } else {
357 | /* Sinon, on cherche la meilleure réponse possible en testant l'identité ou la similarité entre les mots ou expressions clés de chaque réponse possible et le message envoyé */
358 | for (let i = 0; i < chatData.length; i++) {
359 | const keywords = chatData[i][1];
360 | const responses = chatData[i][2];
361 | let matchScore = 0;
362 | let distanceScore = 0;
363 | let distance = 0;
364 | if (yamlSearchInContent) {
365 | const cosSim = cosineSimilarity(userInputTextToLowerCase,vectorChatBotResponses[i]);
366 | matchScore = matchScore + cosSim + 0.5;
367 | }
368 | for (let keyword of keywords) {
369 | let keywordToLowerCase = keyword.toLowerCase();
370 | if (userInputTextToLowerCase.includes(keywordToLowerCase)) {
371 | // Test de l'identité stricte
372 | // En cas d'identité stricte, on monte le score d'une valeur plus importante que 1 (définie par MATCH_SCORE_IDENTITY)
373 | matchScore = matchScore + MATCH_SCORE_IDENTITY;
374 | // On privilégie les correspondances sur les keywords plus longs
375 | matchScore = matchScore + keywordToLowerCase.length/10;
376 | } else if (userInputTextToLowerCase.length > 4) {
377 | // Sinon : test de la similarité (seulement si le message de l'utilisateur n'est pas très court)
378 | if (hasLevenshteinDistanceLessThan(userInputTextToLowerCase, keyword, LEVENSHTEIN_THRESHOLD)) {
379 | distanceScore++;
380 | }
381 | }
382 | }
383 | if (matchScore == 0) {
384 | // En cas de simple similarité : on monte quand même le score, mais d'une unité seulement
385 | if (distanceScore > bestDistanceScore) {
386 | matchScore++;
387 | bestDistanceScore = distanceScore;
388 | }
389 | }
390 | if (matchScore > bestMatchScore) {
391 | bestMatch = responses;
392 | bestMatchScore = matchScore;
393 | indexBestMatch = i;
394 | }
395 | }
396 | if (bestMatch && bestMatchScore > BESTMATCH_THRESHOLD) {
397 | // On envoie le meilleur choix s'il en existe un
398 | let selectedResponse = Array.isArray(bestMatch)
399 | ? bestMatch.join("\n\n")
400 | : bestMatch;
401 | const options = chatData[indexBestMatch][3];
402 | selectedResponse = gestionOptions(selectedResponse, options);
403 | createChatMessage(selectedResponse, false);
404 | } else {
405 | // En cas de correspondance non trouvée, on envoie un message par défaut (sélectionné au hasard dans la liste définie par defaultMessage)
406 | // On fait en sorte que le message par défaut envoyé ne soit pas le même que les derniers messages par défaut envoyés
407 | while (
408 | randomDefaultMessageIndexLastChoice.includes(
409 | randomDefaultMessageIndex
410 | )
411 | ) {
412 | randomDefaultMessageIndex = Math.floor(
413 | Math.random() * defaultMessage.length
414 | );
415 | }
416 | if (randomDefaultMessageIndexLastChoice.length > 4) {
417 | randomDefaultMessageIndexLastChoice.shift();
418 | }
419 | randomDefaultMessageIndexLastChoice.push(randomDefaultMessageIndex);
420 | createChatMessage(defaultMessage[randomDefaultMessageIndex], false);
421 | }
422 | }
423 | }
424 |
425 | function gestionOptions(response, options) {
426 | if (options) {
427 | optionsLastResponse = options;
428 | // Gestion du cas où il y a un choix possible entre différentes options après la réponse du chatbot
429 | let messageOptions = "\n