diff --git a/.eslintrc.js b/.eslintrc.js index 39f743d..2f4fec9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,18 +1,18 @@ module.exports = { root: true, - parser: "@typescript-eslint/parser", - plugins: ["@typescript-eslint"], + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], extends: [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended", - "prettier", + 'eslint:recommended', + 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:@typescript-eslint/recommended', + 'prettier' ], - ignorePatterns: ["node_modules/"], + ignorePatterns: ['node_modules/'], overrides: [ { - files: ["extension/popup/*.js", "src/**/*.ts"], - rules: {}, - }, - ], + files: ['extension/popup/*.js', 'src/**/*.ts'], + rules: {} + } + ] }; diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..24b36d7 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,10 @@ +{ + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "useTabs": false, + "printWidth": 100, + "trailingComma": "none", + "arrowParens": "avoid", + "endOfLine": "auto" +} diff --git a/CHANGELOG.md b/CHANGELOG.md index a95951e..ab221b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,33 +1,33 @@ -# CHANGELOG - -## v1.0.4 - -- `code` is not required anymore -- Issue [#9](https://github.com/yoannchb-pro/MoodleGPT/issues/9) resolved -- GPT version button fixed -- Better algorithm to find the correct answer (levenshtein distance) -- Better ChatGPT prompt -- Added `history` to the options/configuration - -## v1.0.3 - -- Removed the option `table formating` because it will now set to true by default -- Adjusted the abort timeout to 15seconds -- If an error occur the user can now click back on the question -- `Textbox, question to answser mode and clipboard mode` is not formatted anymore -- Fixed many bugs -- Write AI system instructions - -## v1.0.2 - -- Added `mode` - -## v1.0.1 - -- Removed langage -- Added a button next to model to get the last ChatGPT version -- Added update message - -## v1.0.0 - -- Initial commit +# CHANGELOG + +## v1.0.4 + +- `code` is not required anymore +- Issue [#9](https://github.com/yoannchb-pro/MoodleGPT/issues/9) resolved +- GPT version button fixed +- Better algorithm to find the correct answer (levenshtein distance) +- Better ChatGPT prompt +- Added `history` to the options/configuration + +## v1.0.3 + +- Removed the option `table formating` because it will now set to true by default +- Adjusted the abort timeout to 15seconds +- If an error occur the user can now click back on the question +- `Textbox, question to answser mode and clipboard mode` is not formatted anymore +- Fixed many bugs +- Write AI system instructions + +## v1.0.2 + +- Added `mode` + +## v1.0.1 + +- Removed langage +- Added a button next to model to get the last ChatGPT version +- Added update message + +## v1.0.0 + +- Initial commit diff --git a/README.md b/README.md index af44f4a..662f5ef 100644 --- a/README.md +++ b/README.md @@ -1,167 +1,167 @@ -

Mortarboard icons created by itim2101 - Flaticon

- -# MoodleGPT - -This extension allows you to hide CHAT-GPT in a Moodle quiz. You just need to enter the code configured in the extension on the keyboard and then click on the question you want to solve, and CHAT-GPT will automatically provide the answer. However, one needs to be careful because as we know, CHAT-GPT can make errors especially in calculations. - -## Chrome Webstore - -Find the extension on the Chrome Webstore right [here](https://chrome.google.com/webstore/detail/moodlegpt/fgiepdkoifhpcgdhbiikpgdapjdoemko) - -## Summary - -- [MoodleGPT](#moodlegpt) - - [Chrome Webstore](#chrome-webstore) - - [Summary](#summary) - - [Disclaimer !](#disclaimer-) - - [Donate](#donate) - - [Update](#update) - - [MoodleGPT don't complete my quiz ?](#moodlegpt-dont-complete-my-quiz-) - - [Set up](#set-up) - - [Inject the code into the moodle](#inject-the-code-into-the-moodle) - - [Remove injection](#remove-injection) - - [Mode](#mode) - - [Settings](#settings) - - [Internal Features](#internal-features) - - [Support table](#support-table) - - [Supported questions type](#supported-questions-type) - - [Select](#select) - - [Put in order question](#put-in-order-question) - - [Resolve equation](#resolve-equation) - - [One response (radio button)](#one-response-radio-button) - - [Multiples responses (checkbox)](#multiples-responses-checkbox) - - [True or false](#true-or-false) - - [Number](#number) - - [Text](#text) - - [What about if the question can't be completed ?](#what-about-if-the-question-cant-be-completed-) - - [Test](#test) - -## Disclaimer ! - -I hereby declare that I am not responsible for any misuse or illegal activities carried out using my program. The code is provided for educational and research purposes only, and any use of it outside of these purposes is at the user's own risk. - -## Donate - -Will be a pleasure if you want to support this project :) -
-Mortarboard icons created by itim2101 - Flaticon - -## Update - -See [changelog](./CHANGELOG.md) - -## MoodleGPT don't complete my quiz ? - -If MoodleGPT cannot complete one of your moodle quiz please provide the html code of the page. It will help us to add it in the futur version of MoodleGPT ! Check the [TODO](./TODO.md) to see what is comming in the futur version. - -## Set up - -> NOTE: This extension only works on Chromium-based browsers like Edge, Chrome, etc. Unfortunately, Firefox requires a click on the extension, which is not very discreet. - -

-Popup -

- -Go to "Manage my extensions" on your browser, then click on "Load unpacked extension" and select the "extension" folder. Afterwards, click on the extension icon and enter the apiKey obtained from [openai](https://platform.openai.com/) and enter a code that will activate the extension on your moodle page. Finally, click on the reload button next to model (it should give you the last ChatGPT version, otherwise enter it by your self) and click on the save button (The extension need to be configured before entering the moodle quiz). - -## Inject the code into the moodle - -You just need to enter on the keyboard the code you have set into the extension and click on the question you want to solve. - -## Remove injection - -Type back the code on the keyboard and the code will be removed from the current page. - -## Mode - -

-Popup -

- -- Autocomplete: The extension will complete the question for you. -- Clipboard: The response is copied into the clipboard. -- Question to answer: The question is converted to the answer and you can click on it to show back the question (or show back the answer). -
Question to Answer - -## Settings - -

-Popup -

- -- Api key: the openai api key. -- Code: code that you will need to inject/remove the code. -- GPT Model: the gpt model you want to use. You can click on the reload button to get the latest version of available gpt model for your account but you need to enter the api key first. -- Cursor indication: show a pointer cursor and a hourglass to know when the request is finished. -- Title indication: show some informations into the title to know for example if the code have been injected. -
![Injected](./assets/title-injected.png) -- Console logs: show logs into the console. -
Logs -- Request timeout: if the request is too long it will be abort after 15seconds. -- Typing effect: create a typing effect for text. Type any text and it will be replaced by the correct one. If you want to stop it press Backspace key. -
![Typing](./assets/typing.gif) -- Mouseover effect: you will need to hover (or click for select) the question response to complete it automaticaly. -
![Mouseover](./assets/mouseover.gif) -
![Mouseover2](./assets/mouseover2.gif) - -- Infinite try: click as much as you want on the question (don't forget to reset the question). - -## Internal Features - -### Support table - -Table are formated from the question to make it more readable for CHAT-GPT. Example of formatted table output: - -``` -| id | name | birthDate | cars | ----------------------------------------- -| Person 1 | Yvick | 15/08/1999 | yes | -| Person 2 | Yann | 19/01/2000 | no | -``` - -## Supported questions type - -### Select - -![Select](./assets/select.gif) - -### Put in order question - -![Order](./assets/order.gif) - -### Resolve equation - -![Equations](./assets/equations.gif) - -### One response (radio button) - -![Radio](./assets/radio.gif) - -### Multiples responses (checkbox) - -![Checkbox](./assets/checkbox.gif) - -### True or false - -![True-false](./assets/true-false.gif) - -### Number - -![Number](./assets/number.gif) - -### Text - -![Text](./assets/text.gif) - -## What about if the question can't be completed ? - -To know if the answer has been copied to the clipboard, you can look at the title of the page which will become "Copied to clipboard" for 3 seconds. - -![Clipboard](./assets/clipboard.gif) - -## Test - -- Solution 1: Go on [this moodle test page](https://school.moodledemo.net/login/index.php) (username: `student`, password: `moodle`) and choose any quiz. -- Solution 2: Run the `index.html` file located in the `test/fake-moodle` folder. +

Mortarboard icons created by itim2101 - Flaticon

+ +# MoodleGPT + +This extension allows you to hide CHAT-GPT in a Moodle quiz. You just need to enter the code configured in the extension on the keyboard and then click on the question you want to solve, and CHAT-GPT will automatically provide the answer. However, one needs to be careful because as we know, CHAT-GPT can make errors especially in calculations. + +## Chrome Webstore + +Find the extension on the Chrome Webstore right [here](https://chrome.google.com/webstore/detail/moodlegpt/fgiepdkoifhpcgdhbiikpgdapjdoemko) + +## Summary + +- [MoodleGPT](#moodlegpt) + - [Chrome Webstore](#chrome-webstore) + - [Summary](#summary) + - [Disclaimer !](#disclaimer-) + - [Donate](#donate) + - [Update](#update) + - [MoodleGPT don't complete my quiz ?](#moodlegpt-dont-complete-my-quiz-) + - [Set up](#set-up) + - [Inject the code into the moodle](#inject-the-code-into-the-moodle) + - [Remove injection](#remove-injection) + - [Mode](#mode) + - [Settings](#settings) + - [Internal Features](#internal-features) + - [Support table](#support-table) + - [Supported questions type](#supported-questions-type) + - [Select](#select) + - [Put in order question](#put-in-order-question) + - [Resolve equation](#resolve-equation) + - [One response (radio button)](#one-response-radio-button) + - [Multiples responses (checkbox)](#multiples-responses-checkbox) + - [True or false](#true-or-false) + - [Number](#number) + - [Text](#text) + - [What about if the question can't be completed ?](#what-about-if-the-question-cant-be-completed-) + - [Test](#test) + +## Disclaimer ! + +I hereby declare that I am not responsible for any misuse or illegal activities carried out using my program. The code is provided for educational and research purposes only, and any use of it outside of these purposes is at the user's own risk. + +## Donate + +Will be a pleasure if you want to support this project :) +
+Mortarboard icons created by itim2101 - Flaticon + +## Update + +See [changelog](./CHANGELOG.md) + +## MoodleGPT don't complete my quiz ? + +If MoodleGPT cannot complete one of your moodle quiz please provide the html code of the page. It will help us to add it in the futur version of MoodleGPT ! Check the [TODO](./TODO.md) to see what is comming in the futur version. + +## Set up + +> NOTE: This extension only works on Chromium-based browsers like Edge, Chrome, etc. Unfortunately, Firefox requires a click on the extension, which is not very discreet. + +

+Popup +

+ +Go to "Manage my extensions" on your browser, then click on "Load unpacked extension" and select the "extension" folder. Afterwards, click on the extension icon and enter the apiKey obtained from [openai](https://platform.openai.com/) and enter a code that will activate the extension on your moodle page. Finally, click on the reload button next to model (it should give you the last ChatGPT version, otherwise enter it by your self) and click on the save button (The extension need to be configured before entering the moodle quiz). + +## Inject the code into the moodle + +You just need to enter on the keyboard the code you have set into the extension and click on the question you want to solve. + +## Remove injection + +Type back the code on the keyboard and the code will be removed from the current page. + +## Mode + +

+Popup +

+ +- Autocomplete: The extension will complete the question for you. +- Clipboard: The response is copied into the clipboard. +- Question to answer: The question is converted to the answer and you can click on it to show back the question (or show back the answer). +
Question to Answer + +## Settings + +

+Popup +

+ +- Api key: the openai api key. +- Code: code that you will need to inject/remove the code. +- GPT Model: the gpt model you want to use. You can click on the reload button to get the latest version of available gpt model for your account but you need to enter the api key first. +- Cursor indication: show a pointer cursor and a hourglass to know when the request is finished. +- Title indication: show some informations into the title to know for example if the code have been injected. +
![Injected](./assets/title-injected.png) +- Console logs: show logs into the console. +
Logs +- Request timeout: if the request is too long it will be abort after 15seconds. +- Typing effect: create a typing effect for text. Type any text and it will be replaced by the correct one. If you want to stop it press Backspace key. +
![Typing](./assets/typing.gif) +- Mouseover effect: you will need to hover (or click for select) the question response to complete it automaticaly. +
![Mouseover](./assets/mouseover.gif) +
![Mouseover2](./assets/mouseover2.gif) + +- Infinite try: click as much as you want on the question (don't forget to reset the question). + +## Internal Features + +### Support table + +Table are formated from the question to make it more readable for CHAT-GPT. Example of formatted table output: + +``` +| id | name | birthDate | cars | +---------------------------------------- +| Person 1 | Yvick | 15/08/1999 | yes | +| Person 2 | Yann | 19/01/2000 | no | +``` + +## Supported questions type + +### Select + +![Select](./assets/select.gif) + +### Put in order question + +![Order](./assets/order.gif) + +### Resolve equation + +![Equations](./assets/equations.gif) + +### One response (radio button) + +![Radio](./assets/radio.gif) + +### Multiples responses (checkbox) + +![Checkbox](./assets/checkbox.gif) + +### True or false + +![True-false](./assets/true-false.gif) + +### Number + +![Number](./assets/number.gif) + +### Text + +![Text](./assets/text.gif) + +## What about if the question can't be completed ? + +To know if the answer has been copied to the clipboard, you can look at the title of the page which will become "Copied to clipboard" for 3 seconds. + +![Clipboard](./assets/clipboard.gif) + +## Test + +- Solution 1: Go on [this moodle test page](https://school.moodledemo.net/login/index.php) (username: `student`, password: `moodle`) and choose any quiz. +- Solution 2: Run the `index.html` file located in the `test/fake-moodle` folder. diff --git a/TODO.md b/TODO.md index 804306b..0e08c01 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,7 @@ -# TODO - -- Historic for questions (implemented but need testing) -- Contributing.md -- Better prompt (Fixe put in order question, Fixe calculation question) -- Support math equation from image stocked in the `data-mathml` attribute -- Better assets +# TODO + +- Historic for questions (implemented but need testing) +- Contributing.md / Fixe readme.md 'MoodleGPT don't complete my quiz ?' +- Better prompt (Fixe put in order question, Fixe calculation question) +- Support math equation from image stocked in the `data-mathml` attribute +- Better assets diff --git a/extension/MoodleGPT.js b/extension/MoodleGPT.js index f5cd786..1c4959d 100644 --- a/extension/MoodleGPT.js +++ b/extension/MoodleGPT.js @@ -1,2 +1,419 @@ -!function(e){"function"==typeof define&&define.amd?define(e):e()}((function(){"use strict";function e(e){const n=document.title;document.title=e,setTimeout((()=>document.title=n),3e3)}function n(e,n,t,o){return new(t||(t=Promise))((function(r,i){function s(e){try{c(o.next(e))}catch(e){i(e)}}function l(e){try{c(o.throw(e))}catch(e){i(e)}}function c(e){var n;e.done?r(e.value):(n=e.value,n instanceof t?n:new t((function(e){e(n)}))).then(s,l)}c((o=o.apply(e,n||[])).next())}))}function t(e,n){const t=e.length>n.length?e.length:n.length;return 0===t?1:(t-function(e,n){if(0===e.length)return n.length;if(0===n.length)return e.length;const t=[],o=e.replace(/\s+/,""),r=n.replace(/\s+/,"");for(let e=0;e<=o.length;++e){t.push([e]);for(let n=1;n<=r.length;++n)t[e][n]=0===e?n:Math.min(t[e-1][n]+1,t[e][n-1]+1,t[e-1][n-1]+(o[e-1]===r[n-1]?0:1))}return t[o.length][r.length]}(e,n))/t}function o(e,n){let o={element:null,similarity:0,value:null};for(const r of n){const n=t(r.value,e);if(1===n)return{element:r.element,value:r.value,similarity:n};n>o.similarity&&(o={element:r.element,value:r.value,similarity:n})}return o}"function"==typeof SuppressedError&&SuppressedError;class r{static question(e){console.log("%c[QUESTION]: %s","color: cyan",e)}static bestAnswer(e,n){console.log("%c[BEST ANSWER]: %s","color: green",`"${e}" with a similarity of ${function(e){return Math.round(100*e*100)/100+"%"}(n)}`)}static array(e){console.log("[CORRECTS] ",e)}static response(e){console.log("Original:\n"+e.response),console.log("Normalized:\n"+e.normalizedResponse)}}function i(e,n=!0){return n&&(e=e.toLowerCase()),e.replace(/\n+/gi,"\n").replace(/(\n\s*\n)+/g,"\n").replace(/[ \t]+/gi," ").trim().replace(/^[a-z\d]\.\s/gi,"").replace(/\n[a-z\d]\.\s/gi,"\n")}var s;!function(e){e.SYSTEM="system",e.USER="user",e.ASSISTANT="assistant"}(s||(s={}));const l="\nAct as a quiz solver for the best notation with the following rules:\n- When asked for the result of an equation, provide only the result without any other information and skip the other rules.\n- If no answer(s) are given, answer the statement as usual without following the other rules, providing the most detailed, complete and precise explanation.\n- For 'put in order' questions, provide the position of the answer separated by a new line (e.g., '1\n3\n2') and ignore other rules.- Always reply in this format: '\n\n...'\n- Always reply in the format: '\n\n...'.\n- Retain only the correct answer(s).\n- Maintain the same order for the answers as in the text.\n- Retain all text from the answer with its description, content or definition.\n- Only provide answers that exactly match the given answer in the text.\n- The question always has the correct answer(s), so you should always provide an answer.\n- Always respond in the same language as the user's question.\n".trim(),c={url:null,system:{role:s.SYSTEM,content:l},history:[]};function a(e){const n=[],t=Array.from(e.querySelectorAll("tr")),o=[];t.map((e=>{const t=Array.from(e.querySelectorAll("td, th")).map(((e,n)=>{var t;const r=null===(t=e.textContent)||void 0===t?void 0:t.trim();return o[n]=Math.max(o[n]||0,(null==r?void 0:r.length)||0),null!=r?r:""}));n.push(t)}));const r=o.reduce(((e,n)=>e+n))+3*n[0].length+1,i="\n"+Array(r).fill("-").join("")+"\n",s=n.map((e=>"| "+e.map(((e,n)=>e.padEnd(o[n]," "))).join(" | ")+" |"));return s.shift()+i+s.join("\n")}function u(n,t){n.title&&e("Copied to clipboard"),navigator.clipboard.writeText(t.response)}function f(e,n,t){const o=n[0];if(1!==n.length||"true"!==o.getAttribute("contenteditable"))return!1;if(e.typing){let e=0;o.addEventListener("keydown",(function(n){if("Backspace"===n.key&&(e=t.response.length+1),e>t.response.length)return;n.preventDefault(),o.textContent=t.response.slice(0,++e),o.focus();const r=document.createRange();r.selectNodeContents(o),r.collapse(!1);const i=window.getSelection();null!==i&&(i.removeAllRanges(),i.addRange(r))}))}else o.textContent=t.response;return!0}function d(e,n,t){var o,r;const i=n[0];if(1!==n.length||"number"!==i.type)return!1;const s=null===(r=null===(o=t.normalizedResponse.match(/\d+([,\.]\d+)?/gi))||void 0===o?void 0:o[0])||void 0===r?void 0:r.replace(",",".");if(void 0===s)return!1;if(e.typing){let e=0;i.addEventListener("keydown",(function(n){n.preventDefault(),"Backspace"===n.key&&(e=s.length+1),e>s.length||("."===s.slice(e,e+1)&&++e,i.value=s.slice(0,++e))}))}else i.value=s;return!0}function m(e,n,t){const s=null==n?void 0:n[0];if(!s||"radio"!==s.type)return!1;const l=Array.from(n).map((e=>{var n,t;return{element:e,value:i(null!==(t=null===(n=null==e?void 0:e.parentElement)||void 0===n?void 0:n.textContent)&&void 0!==t?t:"")}})).filter((e=>""!==e.value)),c=o(t.normalizedResponse,l);e.logs&&c.value&&r.bestAnswer(c.value,c.similarity);const a=c.element;return e.mouseover?a.addEventListener("mouseover",(()=>a.checked=!0),{once:!0}):a.checked=!0,!0}function p(e,n,t){const s=null==n?void 0:n[0];if(!s||"checkbox"!==s.type)return!1;const l=t.normalizedResponse.split("\n"),c=Array.from(n).map((e=>{var n,t;return{element:e,value:i(null!==(t=null===(n=null==e?void 0:e.parentElement)||void 0===n?void 0:n.textContent)&&void 0!==t?t:"")}})).filter((e=>""!==e.value));for(const n of l){const t=o(n,c);e.logs&&t.value&&r.bestAnswer(t.value,t.similarity);const i=t.element;e.mouseover?i.addEventListener("mouseover",(()=>i.checked=!0),{once:!0}):i.checked=!0}return!0}function h(e,n,t){if(0===n.length||"SELECT"!==n[0].tagName)return!1;const s=t.normalizedResponse.split("\n");e.logs&&r.array(s);for(let t=0;t{var n;return{element:e,value:i(null!==(n=e.textContent)&&void 0!==n?n:"")}})).filter((e=>""!==e.value)),a=o(s[t],c);e.logs&&a.value&&r.bestAnswer(a.value,a.similarity);const u=a.element,f=u.closest("select");null!==f&&(e.mouseover?f.addEventListener("click",(()=>u.selected=!0),{once:!0}):u.selected=!0)}return!0}function g(e,n,t){const o=n[0];if(1!==n.length||"TEXTAREA"!==o.tagName&&"text"!==o.type)return!1;if(e.typing){let e=0;o.addEventListener("keydown",(function(n){n.preventDefault(),"Backspace"===n.key&&(e=t.response.length+1),e>t.response.length||(o.value=t.response.slice(0,++e))}))}else o.value=t.response;return!0}function v(e){return n(this,void 0,void 0,(function*(){e.config.cursor&&(e.questionElement.style.cursor="wait");const t=function(e){let n=e.innerText;const t=e.querySelectorAll(".accesshide");for(const e of t)n=n.replace(e.innerText,"");const o=e.querySelectorAll(".qtext table");for(const e of o)n=n.replace(e.innerText,"\n"+a(e)+"\n");return i(n,!1)}(e.form),o=e.form.querySelectorAll(e.inputQuery),l=yield function(e,t){return n(this,void 0,void 0,(function*(){const n=location.hostname+location.pathname;e.history&&c.url===n||(c.url=n,c.history=[]);const o=new AbortController,r=setTimeout((()=>o.abort()),15e3),l={role:s.USER,content:t},a=yield fetch("https://api.openai.com/v1/chat/completions",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e.apiKey}`},signal:e.timeout?o.signal:null,body:JSON.stringify({model:e.model,messages:[c.system,...c.history,l],temperature:.8,top_p:1,presence_penalty:1,stop:null})});clearTimeout(r);const u=(yield a.json()).choices[0].message.content;return e.history&&(c.history.push(l),c.history.push({role:s.ASSISTANT,content:u})),{question:t,response:u,normalizedResponse:i(u)}}))}(e.config,t).catch((e=>({error:e}))),v="object"==typeof l&&"error"in l;if(e.config.cursor&&(e.questionElement.style.cursor=e.config.infinite||v?"pointer":"initial"),v)console.error(l.error);else switch(e.config.logs&&(r.question(t),r.response(l)),e.config.mode){case"clipboard":!function(e){e.config.infinite||e.removeListener(),u(e.config,e.gptAnswer)}({config:e.config,questionElement:e.questionElement,gptAnswer:l,removeListener:e.removeListener});break;case"question-to-answer":!function(e){const n=e.questionElement;e.removeListener();const t=n.textContent;n.textContent=e.gptAnswer.response,n.style.whiteSpace="pre-wrap",n.addEventListener("click",(function(){n.style.whiteSpace="",n.textContent=t}))}({gptAnswer:l,questionElement:e.questionElement,removeListener:e.removeListener});break;case"autocomplete":!function(e){e.config.infinite||e.removeListener();const n=[f,g,d,h,m,p];for(const t of n)if(t(e.config,e.inputList,e.gptAnswer))return;u(e.config,e.gptAnswer)}({config:e.config,gptAnswer:l,inputList:o,questionElement:e.questionElement,removeListener:e.removeListener})}}))}const y=[],w=[];function A(e){const n=w.findIndex((n=>n.element===e));if(-1!==n){const e=w.splice(n,1)[0];e.element.removeEventListener("click",e.fn)}}function E(n){if(w.length>0){for(const e of w)n.cursor&&(e.element.style.cursor="initial"),e.element.removeEventListener("click",e.fn);return n.title&&e("Removed"),void(w.length=0)}const t=["checkbox","radio","text","number"].map((e=>`input[type="${e}"]`)).join(",")+", textarea, select, [contenteditable]",o=document.querySelectorAll(".formulation");for(const e of o){const o=e.querySelector(".qtext");if(null===o)continue;n.cursor&&(o.style.cursor="pointer");const r=v.bind(null,{config:n,questionElement:o,form:e,inputQuery:t,removeListener:()=>A(o)});w.push({element:o,fn:r}),o.addEventListener("click",r)}n.title&&e("Injected")}chrome.storage.sync.get(["moodleGPT"]).then((function(e){const n=e.moodleGPT;if(!n)throw new Error("Please configure MoodleGPT into the extension");n.code?function(e){document.body.addEventListener("keydown",(function(n){y.push(n.key),y.length>e.code.length&&y.shift(),y.join("")===e.code&&(y.length=0,E(e))}))}(n):E(n)}))})); +!(function (e) { + 'function' == typeof define && define.amd ? define(e) : e(); +})(function () { + 'use strict'; + function e(e) { + const n = document.title; + (document.title = e), setTimeout(() => (document.title = n), 3e3); + } + function n(e, n, t, o) { + return new (t || (t = Promise))(function (r, i) { + function s(e) { + try { + c(o.next(e)); + } catch (e) { + i(e); + } + } + function l(e) { + try { + c(o.throw(e)); + } catch (e) { + i(e); + } + } + function c(e) { + var n; + e.done + ? r(e.value) + : ((n = e.value), + n instanceof t + ? n + : new t(function (e) { + e(n); + })).then(s, l); + } + c((o = o.apply(e, n || [])).next()); + }); + } + function t(e, n) { + const t = e.length > n.length ? e.length : n.length; + return 0 === t + ? 1 + : (t - + (function (e, n) { + if (0 === e.length) return n.length; + if (0 === n.length) return e.length; + const t = [], + o = e.replace(/\s+/, ''), + r = n.replace(/\s+/, ''); + for (let e = 0; e <= o.length; ++e) { + t.push([e]); + for (let n = 1; n <= r.length; ++n) + t[e][n] = + 0 === e + ? n + : Math.min( + t[e - 1][n] + 1, + t[e][n - 1] + 1, + t[e - 1][n - 1] + (o[e - 1] === r[n - 1] ? 0 : 1) + ); + } + return t[o.length][r.length]; + })(e, n)) / + t; + } + function o(e, n) { + let o = { element: null, similarity: 0, value: null }; + for (const r of n) { + const n = t(r.value, e); + if (1 === n) return { element: r.element, value: r.value, similarity: n }; + n > o.similarity && (o = { element: r.element, value: r.value, similarity: n }); + } + return o; + } + 'function' == typeof SuppressedError && SuppressedError; + class r { + static question(e) { + console.log('%c[QUESTION]: %s', 'color: cyan', e); + } + static bestAnswer(e, n) { + console.log( + '%c[BEST ANSWER]: %s', + 'color: green', + `"${e}" with a similarity of ${(function (e) { + return Math.round(100 * e * 100) / 100 + '%'; + })(n)}` + ); + } + static array(e) { + console.log('[CORRECTS] ', e); + } + static response(e) { + console.log('Original:\n' + e.response), console.log('Normalized:\n' + e.normalizedResponse); + } + } + function i(e, n = !0) { + return ( + n && (e = e.toLowerCase()), + e + .replace(/\n+/gi, '\n') + .replace(/(\n\s*\n)+/g, '\n') + .replace(/[ \t]+/gi, ' ') + .trim() + .replace(/^[a-z\d]\.\s/gi, '') + .replace(/\n[a-z\d]\.\s/gi, '\n') + ); + } + var s; + !(function (e) { + (e.SYSTEM = 'system'), (e.USER = 'user'), (e.ASSISTANT = 'assistant'); + })(s || (s = {})); + const l = + "\nAct as a quiz solver for the best notation with the following rules:\n- When asked for the result of an equation, provide only the result without any other information and skip the other rules.\n- If no answer(s) are given, answer the statement as usual without following the other rules, providing the most detailed, complete and precise explanation.\n- For 'put in order' questions, provide the position of the answer separated by a new line (e.g., '1\n3\n2') and ignore other rules.- Always reply in this format: '\n\n...'\n- Always reply in the format: '\n\n...'.\n- Retain only the correct answer(s).\n- Maintain the same order for the answers as in the text.\n- Retain all text from the answer with its description, content or definition.\n- Only provide answers that exactly match the given answer in the text.\n- The question always has the correct answer(s), so you should always provide an answer.\n- Always respond in the same language as the user's question.\n".trim(), + c = { url: null, system: { role: s.SYSTEM, content: l }, history: [] }; + function a(e) { + const n = [], + t = Array.from(e.querySelectorAll('tr')), + o = []; + t.map(e => { + const t = Array.from(e.querySelectorAll('td, th')).map((e, n) => { + var t; + const r = null === (t = e.textContent) || void 0 === t ? void 0 : t.trim(); + return ( + (o[n] = Math.max(o[n] || 0, (null == r ? void 0 : r.length) || 0)), null != r ? r : '' + ); + }); + n.push(t); + }); + const r = o.reduce((e, n) => e + n) + 3 * n[0].length + 1, + i = '\n' + Array(r).fill('-').join('') + '\n', + s = n.map(e => '| ' + e.map((e, n) => e.padEnd(o[n], ' ')).join(' | ') + ' |'); + return s.shift() + i + s.join('\n'); + } + function u(n, t) { + n.title && e('Copied to clipboard'), navigator.clipboard.writeText(t.response); + } + function f(e, n, t) { + const o = n[0]; + if (1 !== n.length || 'true' !== o.getAttribute('contenteditable')) return !1; + if (e.typing) { + let e = 0; + o.addEventListener('keydown', function (n) { + if (('Backspace' === n.key && (e = t.response.length + 1), e > t.response.length)) return; + n.preventDefault(), (o.textContent = t.response.slice(0, ++e)), o.focus(); + const r = document.createRange(); + r.selectNodeContents(o), r.collapse(!1); + const i = window.getSelection(); + null !== i && (i.removeAllRanges(), i.addRange(r)); + }); + } else o.textContent = t.response; + return !0; + } + function d(e, n, t) { + var o, r; + const i = n[0]; + if (1 !== n.length || 'number' !== i.type) return !1; + const s = + null === + (r = + null === (o = t.normalizedResponse.match(/\d+([,\.]\d+)?/gi)) || void 0 === o + ? void 0 + : o[0]) || void 0 === r + ? void 0 + : r.replace(',', '.'); + if (void 0 === s) return !1; + if (e.typing) { + let e = 0; + i.addEventListener('keydown', function (n) { + n.preventDefault(), + 'Backspace' === n.key && (e = s.length + 1), + e > s.length || ('.' === s.slice(e, e + 1) && ++e, (i.value = s.slice(0, ++e))); + }); + } else i.value = s; + return !0; + } + function m(e, n, t) { + const s = null == n ? void 0 : n[0]; + if (!s || 'radio' !== s.type) return !1; + const l = Array.from(n) + .map(e => { + var n, t; + return { + element: e, + value: i( + null !== + (t = + null === (n = null == e ? void 0 : e.parentElement) || void 0 === n + ? void 0 + : n.textContent) && void 0 !== t + ? t + : '' + ) + }; + }) + .filter(e => '' !== e.value), + c = o(t.normalizedResponse, l); + e.logs && c.value && r.bestAnswer(c.value, c.similarity); + const a = c.element; + return ( + e.mouseover + ? a.addEventListener('mouseover', () => (a.checked = !0), { once: !0 }) + : (a.checked = !0), + !0 + ); + } + function p(e, n, t) { + const s = null == n ? void 0 : n[0]; + if (!s || 'checkbox' !== s.type) return !1; + const l = t.normalizedResponse.split('\n'), + c = Array.from(n) + .map(e => { + var n, t; + return { + element: e, + value: i( + null !== + (t = + null === (n = null == e ? void 0 : e.parentElement) || void 0 === n + ? void 0 + : n.textContent) && void 0 !== t + ? t + : '' + ) + }; + }) + .filter(e => '' !== e.value); + for (const n of l) { + const t = o(n, c); + e.logs && t.value && r.bestAnswer(t.value, t.similarity); + const i = t.element; + e.mouseover + ? i.addEventListener('mouseover', () => (i.checked = !0), { once: !0 }) + : (i.checked = !0); + } + return !0; + } + function h(e, n, t) { + if (0 === n.length || 'SELECT' !== n[0].tagName) return !1; + const s = t.normalizedResponse.split('\n'); + e.logs && r.array(s); + for (let t = 0; t < n.length && s[t]; ++t) { + const l = n[t].querySelectorAll('option'), + c = Array.from(l) + .map(e => { + var n; + return { + element: e, + value: i(null !== (n = e.textContent) && void 0 !== n ? n : '') + }; + }) + .filter(e => '' !== e.value), + a = o(s[t], c); + e.logs && a.value && r.bestAnswer(a.value, a.similarity); + const u = a.element, + f = u.closest('select'); + null !== f && + (e.mouseover + ? f.addEventListener('click', () => (u.selected = !0), { once: !0 }) + : (u.selected = !0)); + } + return !0; + } + function g(e, n, t) { + const o = n[0]; + if (1 !== n.length || ('TEXTAREA' !== o.tagName && 'text' !== o.type)) return !1; + if (e.typing) { + let e = 0; + o.addEventListener('keydown', function (n) { + n.preventDefault(), + 'Backspace' === n.key && (e = t.response.length + 1), + e > t.response.length || (o.value = t.response.slice(0, ++e)); + }); + } else o.value = t.response; + return !0; + } + function v(e) { + return n(this, void 0, void 0, function* () { + e.config.cursor && (e.questionElement.style.cursor = 'wait'); + const t = (function (e) { + let n = e.innerText; + const t = e.querySelectorAll('.accesshide'); + for (const e of t) n = n.replace(e.innerText, ''); + const o = e.querySelectorAll('.qtext table'); + for (const e of o) n = n.replace(e.innerText, '\n' + a(e) + '\n'); + return i(n, !1); + })(e.form), + o = e.form.querySelectorAll(e.inputQuery), + l = yield (function (e, t) { + return n(this, void 0, void 0, function* () { + const n = location.hostname + location.pathname; + (e.history && c.url === n) || ((c.url = n), (c.history = [])); + const o = new AbortController(), + r = setTimeout(() => o.abort(), 15e3), + l = { role: s.USER, content: t }, + a = yield fetch('https://api.openai.com/v1/chat/completions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${e.apiKey}` + }, + signal: e.timeout ? o.signal : null, + body: JSON.stringify({ + model: e.model, + messages: [c.system, ...c.history, l], + temperature: 0.8, + top_p: 1, + presence_penalty: 1, + stop: null + }) + }); + clearTimeout(r); + const u = (yield a.json()).choices[0].message.content; + return ( + e.history && (c.history.push(l), c.history.push({ role: s.ASSISTANT, content: u })), + { question: t, response: u, normalizedResponse: i(u) } + ); + }); + })(e.config, t).catch(e => ({ error: e })), + v = 'object' == typeof l && 'error' in l; + if ( + (e.config.cursor && + (e.questionElement.style.cursor = e.config.infinite || v ? 'pointer' : 'initial'), + v) + ) + console.error(l.error); + else + switch ((e.config.logs && (r.question(t), r.response(l)), e.config.mode)) { + case 'clipboard': + !(function (e) { + e.config.infinite || e.removeListener(), u(e.config, e.gptAnswer); + })({ + config: e.config, + questionElement: e.questionElement, + gptAnswer: l, + removeListener: e.removeListener + }); + break; + case 'question-to-answer': + !(function (e) { + const n = e.questionElement; + e.removeListener(); + const t = n.textContent; + (n.textContent = e.gptAnswer.response), + (n.style.whiteSpace = 'pre-wrap'), + n.addEventListener('click', function () { + (n.style.whiteSpace = ''), (n.textContent = t); + }); + })({ + gptAnswer: l, + questionElement: e.questionElement, + removeListener: e.removeListener + }); + break; + case 'autocomplete': + !(function (e) { + e.config.infinite || e.removeListener(); + const n = [f, g, d, h, m, p]; + for (const t of n) if (t(e.config, e.inputList, e.gptAnswer)) return; + u(e.config, e.gptAnswer); + })({ + config: e.config, + gptAnswer: l, + inputList: o, + questionElement: e.questionElement, + removeListener: e.removeListener + }); + } + }); + } + const y = [], + w = []; + function A(e) { + const n = w.findIndex(n => n.element === e); + if (-1 !== n) { + const e = w.splice(n, 1)[0]; + e.element.removeEventListener('click', e.fn); + } + } + function E(n) { + if (w.length > 0) { + for (const e of w) + n.cursor && (e.element.style.cursor = 'initial'), + e.element.removeEventListener('click', e.fn); + return n.title && e('Removed'), void (w.length = 0); + } + const t = + ['checkbox', 'radio', 'text', 'number'].map(e => `input[type="${e}"]`).join(',') + + ', textarea, select, [contenteditable]', + o = document.querySelectorAll('.formulation'); + for (const e of o) { + const o = e.querySelector('.qtext'); + if (null === o) continue; + n.cursor && (o.style.cursor = 'pointer'); + const r = v.bind(null, { + config: n, + questionElement: o, + form: e, + inputQuery: t, + removeListener: () => A(o) + }); + w.push({ element: o, fn: r }), o.addEventListener('click', r); + } + n.title && e('Injected'); + } + chrome.storage.sync.get(['moodleGPT']).then(function (e) { + const n = e.moodleGPT; + if (!n) throw new Error('Please configure MoodleGPT into the extension'); + n.code + ? (function (e) { + document.body.addEventListener('keydown', function (n) { + y.push(n.key), + y.length > e.code.length && y.shift(), + y.join('') === e.code && ((y.length = 0), E(e)); + }); + })(n) + : E(n); + }); +}); //# sourceMappingURL=MoodleGPT.js.map diff --git a/extension/manifest.json b/extension/manifest.json index 7053f8a..b0fe3eb 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -1,26 +1,26 @@ -{ - "manifest_version": 3, - "name": "MoodleGPT", - "version": "1.0.4", - "description": "Hidden chat-gpt for your moodle quiz", - "permissions": ["storage"], - "action": { - "default_icon": "icon.png", - "default_popup": "./popup/index.html" - }, - - "icons": { - "16": "icon.png", - "32": "icon.png", - "48": "icon.png", - "128": "icon.png" - }, - - "content_scripts": [ - { - "matches": ["*://*/**/mod/quiz/*", "*://*/mod/quiz/*", "file:///*"], - "js": ["MoodleGPT.js"], - "run_at": "document_end" - } - ] -} +{ + "manifest_version": 3, + "name": "MoodleGPT", + "version": "1.0.4", + "description": "Hidden chat-gpt for your moodle quiz", + "permissions": ["storage"], + "action": { + "default_icon": "icon.png", + "default_popup": "./popup/index.html" + }, + + "icons": { + "16": "icon.png", + "32": "icon.png", + "48": "icon.png", + "128": "icon.png" + }, + + "content_scripts": [ + { + "matches": ["*://*/**/mod/quiz/*", "*://*/mod/quiz/*", "file:///*"], + "js": ["MoodleGPT.js"], + "run_at": "document_end" + } + ] +} diff --git a/extension/popup/index.html b/extension/popup/index.html index 0494b8c..ca94616 100644 --- a/extension/popup/index.html +++ b/extension/popup/index.html @@ -1,154 +1,148 @@ - - - - - - - MoodleGPT - - - - - - - - - - - - - - - -
-
- icon -
-

MoodleGPT

-

-
-
-
- - -
-
- - - -
-
- - -
-
- -

Mode:

-
-
-
    -
  • -
  • - -
  • -
  • - -
  • -
-
-
- -

Settings:

-
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-

Message

-
- -
- -
- - + + + + + + + MoodleGPT + + + + + + + + + + + + + + + +
+
+ icon +
+

MoodleGPT

+

+
+
+
+ + +
+
+ + + +
+
+ + +
+
+ +

Mode:

+
+
+
    +
  • +
  • + +
  • +
  • + +
  • +
+
+
+ +

Settings:

+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+

Message

+
+ +
+ +
+ + diff --git a/extension/popup/js/gptVersion.js b/extension/popup/js/gptVersion.js index 312df3e..9cb7c0a 100644 --- a/extension/popup/js/gptVersion.js +++ b/extension/popup/js/gptVersion.js @@ -1,49 +1,49 @@ -"use strict"; +'use strict'; /** * Get the last ChatGPT version */ function getLastChatGPTVersion() { - const apiKeySelector = document.querySelector("#apiKey"); - const reloadModel = document.querySelector("#reloadModel"); + const apiKeySelector = document.querySelector('#apiKey'); + const reloadModel = document.querySelector('#reloadModel'); let apiKey = apiKeySelector.value; // If the api key is set we enable the button to get the last chatgpt version function checkFiledApiKey() { if (apiKey) { - reloadModel.removeAttribute("disabled"); - reloadModel.setAttribute("title", "Get last ChatGPT version"); + reloadModel.removeAttribute('disabled'); + reloadModel.setAttribute('title', 'Get last ChatGPT version'); return; } - reloadModel.setAttribute("disabled", true); - reloadModel.setAttribute("title", "Provide an api key first"); + reloadModel.setAttribute('disabled', true); + reloadModel.setAttribute('title', 'Provide an api key first'); } checkFiledApiKey(); // Check if the api key is set - apiKeySelector.addEventListener("input", function () { + apiKeySelector.addEventListener('input', function () { apiKey = apiKeySelector.value.trim(); checkFiledApiKey(); }); // Event listener to handle a click on the relaod icon button - reloadModel.addEventListener("click", async function () { + reloadModel.addEventListener('click', async function () { if (!apiKey) return; try { - const req = await fetch("https://api.openai.com/v1/models", { + const req = await fetch('https://api.openai.com/v1/models', { headers: { - Authorization: `Bearer ${apiKey}`, - }, + Authorization: `Bearer ${apiKey}` + } }); const rep = await req.json(); - const model = rep.data.find((model) => model.id.startsWith("gpt")); - document.querySelector("#model").value = model.id; + const model = rep.data.find(model => model.id.startsWith('gpt')); + document.querySelector('#model').value = model.id; } catch (err) { console.error(err); - showMessage({ msg: "Failed to fetch last ChatGPT version", error: true }); + showMessage({ msg: 'Failed to fetch last ChatGPT version', error: true }); } }); } diff --git a/extension/popup/js/index.js b/extension/popup/js/index.js index 421f95f..caad3df 100644 --- a/extension/popup/js/index.js +++ b/extension/popup/js/index.js @@ -1,38 +1,39 @@ -const saveBtn = document.querySelector(".save"); +const saveBtn = document.querySelector('.save'); /* inputs id */ -const inputsText = ["apiKey", "code", "model"]; +const inputsText = ['apiKey', 'code', 'model']; const inputsCheckbox = [ - "logs", - "title", - "cursor", - "typing", - "mouseover", - "infinite", - "timeout", - "history", + 'logs', + 'title', + 'cursor', + 'typing', + 'mouseover', + 'infinite', + 'timeout', + 'history' ]; /* Save the configuration */ -saveBtn.addEventListener("click", function () { - const [apiKey, code, model] = inputsText.map((selector) => - document.querySelector("#" + selector).value.trim() +saveBtn.addEventListener('click', function () { + const [apiKey, code, model] = inputsText.map(selector => + document.querySelector('#' + selector).value.trim() + ); + const [logs, title, cursor, typing, mouseover, infinite, timeout, history] = inputsCheckbox.map( + selector => { + const element = document.querySelector('#' + selector); + return element.checked && element.parentElement.style.display !== 'none'; + } ); - const [logs, title, cursor, typing, mouseover, infinite, timeout, history] = - inputsCheckbox.map((selector) => { - const element = document.querySelector("#" + selector); - return element.checked && element.parentElement.style.display !== "none"; - }); if (!apiKey || !model) { - showMessage({ msg: "Please complete all the form", error: true }); + showMessage({ msg: 'Please complete all the form', error: true }); return; } if (code.length > 0 && code.length < 3) { showMessage({ - msg: "The code should at least contain 3 characters", - error: true, + msg: 'The code should at least contain 3 characters', + error: true }); return; } @@ -50,15 +51,15 @@ saveBtn.addEventListener("click", function () { infinite, timeout, history, - mode: actualMode, - }, + mode: actualMode + } }); - showMessage({ msg: "Configuration saved" }); + showMessage({ msg: 'Configuration saved' }); }); /* we load back the configuration */ -chrome.storage.sync.get(["moodleGPT"]).then(function (storage) { +chrome.storage.sync.get(['moodleGPT']).then(function (storage) { const config = storage.moodleGPT; if (config) { @@ -66,21 +67,17 @@ chrome.storage.sync.get(["moodleGPT"]).then(function (storage) { actualMode = config.mode; for (const mode of modes) { if (mode.value === config.mode) { - mode.classList.remove("not-selected"); + mode.classList.remove('not-selected'); } else { - mode.classList.add("not-selected"); + mode.classList.add('not-selected'); } } } - inputsText.forEach((key) => - config[key] - ? (document.querySelector("#" + key).value = config[key]) - : null - ); - inputsCheckbox.forEach( - (key) => (document.querySelector("#" + key).checked = config[key] || "") + inputsText.forEach(key => + config[key] ? (document.querySelector('#' + key).value = config[key]) : null ); + inputsCheckbox.forEach(key => (document.querySelector('#' + key).checked = config[key] || '')); } handleModeChange(); diff --git a/extension/popup/js/modeHandler.js b/extension/popup/js/modeHandler.js index 705dd5f..1d77676 100644 --- a/extension/popup/js/modeHandler.js +++ b/extension/popup/js/modeHandler.js @@ -1,15 +1,15 @@ -"use strict"; +'use strict'; -const mode = document.querySelector("#mode"); -const modes = mode.querySelectorAll("button"); +const mode = document.querySelector('#mode'); +const modes = mode.querySelectorAll('button'); -let actualMode = "autocomplete"; +let actualMode = 'autocomplete'; /* inputs id that need to be disabled for a specific mode */ const disabledForThisMode = { autocomplete: [], - clipboard: ["typing", "mouseover"], - "question-to-answer": ["typing", "infinite", "mouseover"], + clipboard: ['typing', 'mouseover'], + 'question-to-answer': ['typing', 'infinite', 'mouseover'] }; /** @@ -17,27 +17,25 @@ const disabledForThisMode = { */ function handleModeChange() { const needDisable = disabledForThisMode[actualMode]; - const dontNeedDisable = inputsCheckbox.filter( - (input) => !needDisable.includes(input) - ); + const dontNeedDisable = inputsCheckbox.filter(input => !needDisable.includes(input)); for (const id of needDisable) { - document.querySelector("#" + id).parentElement.style.display = "none"; + document.querySelector('#' + id).parentElement.style.display = 'none'; } for (const id of dontNeedDisable) { - document.querySelector("#" + id).parentElement.style.display = null; + document.querySelector('#' + id).parentElement.style.display = null; } } /* Mode handler */ for (const button of modes) { - button.addEventListener("click", function () { + button.addEventListener('click', function () { const value = button.value; actualMode = value; for (const mode of modes) { if (mode.value !== value) { - mode.classList.add("not-selected"); + mode.classList.add('not-selected'); } else { - mode.classList.remove("not-selected"); + mode.classList.remove('not-selected'); } } handleModeChange(); diff --git a/extension/popup/js/utils.js b/extension/popup/js/utils.js index 5b69d3a..17a42cb 100644 --- a/extension/popup/js/utils.js +++ b/extension/popup/js/utils.js @@ -1,12 +1,12 @@ -"use strict"; +'use strict'; /** * Show message into the popup */ function showMessage({ msg, error, infinite }) { - const message = document.querySelector("#message"); - message.style.color = error ? "red" : "limegreen"; + const message = document.querySelector('#message'); + message.style.color = error ? 'red' : 'limegreen'; message.textContent = msg; - message.style.display = "block"; - if (!infinite) setTimeout(() => (message.style.display = "none"), 5000); + message.style.display = 'block'; + if (!infinite) setTimeout(() => (message.style.display = 'none'), 5000); } diff --git a/extension/popup/js/version.js b/extension/popup/js/version.js index 3f5ee5b..095e30e 100644 --- a/extension/popup/js/version.js +++ b/extension/popup/js/version.js @@ -1,7 +1,7 @@ -"use strict"; +'use strict'; -const CURRENT_VERSION = "1.0.4"; -const versionDisplay = document.querySelector("#version"); +const CURRENT_VERSION = '1.0.4'; +const versionDisplay = document.querySelector('#version'); /** * Get the last version from the github @@ -9,7 +9,7 @@ const versionDisplay = document.querySelector("#version"); */ async function getLastVersion() { const req = await fetch( - "https://raw.githubusercontent.com/yoannchb-pro/MoodleGPT/main/package.json" + 'https://raw.githubusercontent.com/yoannchb-pro/MoodleGPT/main/package.json' ); const rep = await req.json(); return rep.version; @@ -23,34 +23,31 @@ async function getLastVersion() { */ function setVersion(version, isCurrent = true) { if (isCurrent) { - versionDisplay.textContent = "v" + version; + versionDisplay.textContent = 'v' + version; return; } - const link = document.createElement("a"); - link.href = "https://github.com/yoannchb-pro/MoodleGPT"; - link.rel = "noopener noreferrer"; - link.target = "_blank"; - link.textContent = "v" + version; + const link = document.createElement('a'); + link.href = 'https://github.com/yoannchb-pro/MoodleGPT'; + link.rel = 'noopener noreferrer'; + link.target = '_blank'; + link.textContent = 'v' + version; versionDisplay.appendChild(link); - versionDisplay.appendChild(document.createTextNode(" is now available !")); + versionDisplay.appendChild(document.createTextNode(' is now available !')); } /** * Check if the extension neeed an update or not */ async function notifyUpdate() { - const lastVersion = await getLastVersion().catch((err) => { + const lastVersion = await getLastVersion().catch(err => { console.error(err); return CURRENT_VERSION; }); - const lastVertionSplitted = lastVersion.split("."); - const currentVersionSplitted = CURRENT_VERSION.split("."); - const minVersionLength = Math.min( - lastVertionSplitted.length, - currentVersionSplitted.length - ); + const lastVertionSplitted = lastVersion.split('.'); + const currentVersionSplitted = CURRENT_VERSION.split('.'); + const minVersionLength = Math.min(lastVertionSplitted.length, currentVersionSplitted.length); for (let i = 0; i < minVersionLength; ++i) { if (parseInt(lastVertionSplitted[i]) > parseInt(currentVersionSplitted[i])) diff --git a/extension/popup/style.css b/extension/popup/style.css index 1629eae..fa48bbb 100644 --- a/extension/popup/style.css +++ b/extension/popup/style.css @@ -1,146 +1,146 @@ -@font-face { - font-family: Segeo UI; - src: url(../../fonts/Segoe\ UI.ttf); -} - -:root { - --bg-color: #121212; - --color: #fff; - --btn-color: #7f39fb; -} - -* { - box-sizing: border-box; - padding: 0; - margin: 0; - font-family: "Segeo UI", sans-serif; - color: var(--color); -} - -body { - min-height: 100vh; - background-color: var(--bg-color); - display: flex; - justify-content: center; - align-items: center; -} - -main { - display: flex; - flex-direction: column; - align-items: center; - padding: 0.75rem; - gap: 0.4rem; - text-align: center; - width: 22rem; -} - -img { - width: 5rem; -} - -a { - color: var(--btn-color); - margin: 0; -} - -.line { - display: flex; - flex-direction: row; - width: 100%; - gap: 0.5rem; -} - -.center { - justify-content: center; - align-items: center; -} - -.line .textLabel { - width: 5rem; - text-align: left; - text-transform: uppercase; -} - -.line .textLabel .required { - color: var(--btn-color); - font-weight: bold; -} - -.line input[type="text"], -.line input[type="password"] { - flex: 1 1; - border: thin solid var(--color); - padding: 0.3rem 0.5rem; - border-radius: 0.2rem; - outline-color: transparent; - background-color: transparent; -} - -.line input[type="checkbox"] { - accent-color: var(--btn-color); -} - -.col { - display: flex; - flex-direction: column; - text-align: left; -} - -.save { - border: none; - background-color: var(--btn-color); - padding: 0.5rem 2rem; - margin-top: 1rem; - cursor: pointer; - border-radius: 0.2rem; - font-size: 1.5rem; - margin-bottom: 0.75rem; -} - -.mt { - margin-top: 1rem; -} - -.not-selected { - opacity: 0.4; -} - -#mode li { - list-style: none; - flex-grow: 1; -} - -#mode { - flex-wrap: wrap; -} - -#mode button { - background-color: var(--btn-color); - border: none; - text-align: center; - padding: 0.3rem 0.75rem; - cursor: pointer; - width: 100%; - border-radius: 0.5rem; - text-transform: uppercase; -} - -#version { - font-size: 0.75rem; -} - -#message { - display: none; - margin-top: 0.75rem; - margin-bottom: -0.25rem; -} - -#reloadModel { - cursor: pointer; -} - -#reloadModel[disabled] { - cursor: not-allowed; - opacity: 0.75; -} +@font-face { + font-family: Segeo UI; + src: url(../../fonts/Segoe\ UI.ttf); +} + +:root { + --bg-color: #121212; + --color: #fff; + --btn-color: #7f39fb; +} + +* { + box-sizing: border-box; + padding: 0; + margin: 0; + font-family: 'Segeo UI', sans-serif; + color: var(--color); +} + +body { + min-height: 100vh; + background-color: var(--bg-color); + display: flex; + justify-content: center; + align-items: center; +} + +main { + display: flex; + flex-direction: column; + align-items: center; + padding: 0.75rem; + gap: 0.4rem; + text-align: center; + width: 22rem; +} + +img { + width: 5rem; +} + +a { + color: var(--btn-color); + margin: 0; +} + +.line { + display: flex; + flex-direction: row; + width: 100%; + gap: 0.5rem; +} + +.center { + justify-content: center; + align-items: center; +} + +.line .textLabel { + width: 5rem; + text-align: left; + text-transform: uppercase; +} + +.line .textLabel .required { + color: var(--btn-color); + font-weight: bold; +} + +.line input[type='text'], +.line input[type='password'] { + flex: 1 1; + border: thin solid var(--color); + padding: 0.3rem 0.5rem; + border-radius: 0.2rem; + outline-color: transparent; + background-color: transparent; +} + +.line input[type='checkbox'] { + accent-color: var(--btn-color); +} + +.col { + display: flex; + flex-direction: column; + text-align: left; +} + +.save { + border: none; + background-color: var(--btn-color); + padding: 0.5rem 2rem; + margin-top: 1rem; + cursor: pointer; + border-radius: 0.2rem; + font-size: 1.5rem; + margin-bottom: 0.75rem; +} + +.mt { + margin-top: 1rem; +} + +.not-selected { + opacity: 0.4; +} + +#mode li { + list-style: none; + flex-grow: 1; +} + +#mode { + flex-wrap: wrap; +} + +#mode button { + background-color: var(--btn-color); + border: none; + text-align: center; + padding: 0.3rem 0.75rem; + cursor: pointer; + width: 100%; + border-radius: 0.5rem; + text-transform: uppercase; +} + +#version { + font-size: 0.75rem; +} + +#message { + display: none; + margin-top: 0.75rem; + margin-bottom: -0.25rem; +} + +#reloadModel { + cursor: pointer; +} + +#reloadModel[disabled] { + cursor: not-allowed; + opacity: 0.75; +} diff --git a/rollup.config.js b/rollup.config.js index ea4a70e..d32d7c0 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,16 +1,16 @@ -const ts = require("rollup-plugin-ts"); -const terser = require("@rollup/plugin-terser"); - -const config = require("./tsconfig.json"); - -module.exports = { - input: "./src/index.ts", - output: [ - { - file: "./extension/MoodleGPT.js", - format: "umd", - sourcemap: true, - }, - ], - plugins: [ts(config), terser()], -}; +const ts = require('rollup-plugin-ts'); +const terser = require('@rollup/plugin-terser'); + +const config = require('./tsconfig.json'); + +module.exports = { + input: './src/index.ts', + output: [ + { + file: './extension/MoodleGPT.js', + format: 'umd', + sourcemap: true + } + ], + plugins: [ts(config), terser()] +}; diff --git a/src/core/code-listener.ts b/src/core/code-listener.ts index 76284ba..688a7ac 100644 --- a/src/core/code-listener.ts +++ b/src/core/code-listener.ts @@ -1,87 +1,87 @@ -import type Config from "@typing/config"; -import titleIndications from "@utils/title-indications"; -import reply from "./reply"; - -type Listener = { - element: HTMLElement; - fn: (this: HTMLElement, ev: MouseEvent) => void; -}; - -const pressedKeys: string[] = []; -const listeners: Listener[] = []; - -/** - * Create a listener on the keyboard to inject the code - * @param config - */ -function codeListener(config: Config) { - document.body.addEventListener("keydown", function (event) { - pressedKeys.push(event.key); - if (pressedKeys.length > config.code!.length) pressedKeys.shift(); - if (pressedKeys.join("") === config.code) { - pressedKeys.length = 0; - setUpMoodleGpt(config); - } - }); -} - -/** - * Remove the event listener on a specific question - * @param element - */ -function removeListener(element: HTMLElement) { - const index = listeners.findIndex((listener) => listener.element === element); - if (index !== -1) { - const listener = listeners.splice(index, 1)[0]; - listener.element.removeEventListener("click", listener.fn); - } -} - -/** - * Setup moodleGPT into the page (remove/injection) - * @param config - * @returns - */ -function setUpMoodleGpt(config: Config) { - // Removing events if there are already declared - if (listeners.length > 0) { - for (const listener of listeners) { - if (config.cursor) listener.element.style.cursor = "initial"; - listener.element.removeEventListener("click", listener.fn); - } - if (config.title) titleIndications("Removed"); - listeners.length = 0; - return; - } - - // Query to find inputs and forms - const inputTypeQuery = ["checkbox", "radio", "text", "number"] - .map((e) => `input[type="${e}"]`) - .join(","); - const inputQuery = inputTypeQuery + ", textarea, select, [contenteditable]"; - const forms = document.querySelectorAll(".formulation"); - - // For each form we inject a function on the queqtion - for (const form of forms) { - const questionElement: HTMLElement | null = form.querySelector(".qtext"); - - if (questionElement === null) continue; - - if (config.cursor) questionElement.style.cursor = "pointer"; - - const injectionFunction = reply.bind(null, { - config, - questionElement, - form: form as HTMLElement, - inputQuery, - removeListener: () => removeListener(questionElement), - }); - - listeners.push({ element: questionElement, fn: injectionFunction }); - questionElement.addEventListener("click", injectionFunction); - } - - if (config.title) titleIndications("Injected"); -} - -export { codeListener, removeListener, setUpMoodleGpt }; +import type Config from '@typing/config'; +import titleIndications from '@utils/title-indications'; +import reply from './reply'; + +type Listener = { + element: HTMLElement; + fn: (this: HTMLElement, ev: MouseEvent) => void; +}; + +const pressedKeys: string[] = []; +const listeners: Listener[] = []; + +/** + * Create a listener on the keyboard to inject the code + * @param config + */ +function codeListener(config: Config) { + document.body.addEventListener('keydown', function (event) { + pressedKeys.push(event.key); + if (pressedKeys.length > config.code!.length) pressedKeys.shift(); + if (pressedKeys.join('') === config.code) { + pressedKeys.length = 0; + setUpMoodleGpt(config); + } + }); +} + +/** + * Remove the event listener on a specific question + * @param element + */ +function removeListener(element: HTMLElement) { + const index = listeners.findIndex(listener => listener.element === element); + if (index !== -1) { + const listener = listeners.splice(index, 1)[0]; + listener.element.removeEventListener('click', listener.fn); + } +} + +/** + * Setup moodleGPT into the page (remove/injection) + * @param config + * @returns + */ +function setUpMoodleGpt(config: Config) { + // Removing events if there are already declared + if (listeners.length > 0) { + for (const listener of listeners) { + if (config.cursor) listener.element.style.cursor = 'initial'; + listener.element.removeEventListener('click', listener.fn); + } + if (config.title) titleIndications('Removed'); + listeners.length = 0; + return; + } + + // Query to find inputs and forms + const inputTypeQuery = ['checkbox', 'radio', 'text', 'number'] + .map(e => `input[type="${e}"]`) + .join(','); + const inputQuery = inputTypeQuery + ', textarea, select, [contenteditable]'; + const forms = document.querySelectorAll('.formulation'); + + // For each form we inject a function on the queqtion + for (const form of forms) { + const questionElement: HTMLElement | null = form.querySelector('.qtext'); + + if (questionElement === null) continue; + + if (config.cursor) questionElement.style.cursor = 'pointer'; + + const injectionFunction = reply.bind(null, { + config, + questionElement, + form: form as HTMLElement, + inputQuery, + removeListener: () => removeListener(questionElement) + }); + + listeners.push({ element: questionElement, fn: injectionFunction }); + questionElement.addEventListener('click', injectionFunction); + } + + if (config.title) titleIndications('Injected'); +} + +export { codeListener, removeListener, setUpMoodleGpt }; diff --git a/src/core/create-question.ts b/src/core/create-question.ts index fbd68a5..a765410 100644 --- a/src/core/create-question.ts +++ b/src/core/create-question.ts @@ -1,5 +1,5 @@ -import normalizeText from "@utils/normalize-text"; -import htmlTableToString from "@utils/html-table-to-string"; +import normalizeText from '@utils/normalize-text'; +import htmlTableToString from '@utils/html-table-to-string'; /** * Normalize the question as text and add sub informations @@ -12,19 +12,15 @@ function createAndNormalizeQuestion(questionContainer: HTMLElement) { // We remove unnecessary information const accesshideElements: NodeListOf = - questionContainer.querySelectorAll(".accesshide"); + questionContainer.querySelectorAll('.accesshide'); for (const useless of accesshideElements) { - question = question.replace(useless.innerText, ""); + question = question.replace(useless.innerText, ''); } // Make tables more readable for chat-gpt - const tables: NodeListOf = - questionContainer.querySelectorAll(".qtext table"); + const tables: NodeListOf = questionContainer.querySelectorAll('.qtext table'); for (const table of tables) { - question = question.replace( - table.innerText, - "\n" + htmlTableToString(table) + "\n" - ); + question = question.replace(table.innerText, '\n' + htmlTableToString(table) + '\n'); } return normalizeText(question, false); diff --git a/src/core/get-response.ts b/src/core/get-response.ts index 007e154..9a32435 100644 --- a/src/core/get-response.ts +++ b/src/core/get-response.ts @@ -1,98 +1,95 @@ -import type Config from "@typing/config"; -import type GPTAnswer from "@typing/gptAnswer"; -import normalizeText from "@utils/normalize-text"; - -type History = { - url: string | null; - system: { role: ROLE; content: string }; - history: { role: ROLE; content: string }[]; -}; - -enum ROLE { - SYSTEM = "system", - USER = "user", - ASSISTANT = "assistant", -} - -const INSTRUCTION: string = ` -Act as a quiz solver for the best notation with the following rules: -- When asked for the result of an equation, provide only the result without any other information and skip the other rules. -- If no answer(s) are given, answer the statement as usual without following the other rules, providing the most detailed, complete and precise explanation. -- For 'put in order' questions, provide the position of the answer separated by a new line (e.g., '1\n3\n2') and ignore other rules.- Always reply in this format: '\n\n...' -- Always reply in the format: '\n\n...'. -- Retain only the correct answer(s). -- Maintain the same order for the answers as in the text. -- Retain all text from the answer with its description, content or definition. -- Only provide answers that exactly match the given answer in the text. -- The question always has the correct answer(s), so you should always provide an answer. -- Always respond in the same language as the user's question. -`.trim(); - -const history: History = { - url: null, - system: { - role: ROLE.SYSTEM, - content: INSTRUCTION, - }, - history: [], -}; - -/** - * Get the response from chatGPT api - * @param config - * @param question - * @returns - */ -async function getChatGPTResponse( - config: Config, - question: string -): Promise { - const URL = location.hostname + location.pathname; - - // We reset the history when we enter a new moodle quiz or when it's desactivate - if (!config.history || history.url !== URL) { - history.url = URL; - history.history = []; - } - - const controller = new AbortController(); - const timeoutControler = setTimeout(() => controller.abort(), 15 * 1000); - - const message = { role: ROLE.USER, content: question }; - - const req = await fetch("https://api.openai.com/v1/chat/completions", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${config.apiKey}`, - }, - signal: config.timeout ? controller.signal : null, - body: JSON.stringify({ - model: config.model, - messages: [history.system, ...history.history, message], - temperature: 0.8, - top_p: 1.0, - presence_penalty: 1.0, - stop: null, - }), - }); - - clearTimeout(timeoutControler); - - const rep = await req.json(); - const response = rep.choices[0].message.content; - - // Register the conversation - if (config.history) { - history.history.push(message); - history.history.push({ role: ROLE.ASSISTANT, content: response }); - } - - return { - question, - response, - normalizedResponse: normalizeText(response), - }; -} - -export default getChatGPTResponse; +import type Config from '@typing/config'; +import type GPTAnswer from '@typing/gptAnswer'; +import normalizeText from '@utils/normalize-text'; + +type History = { + url: string | null; + system: { role: ROLE; content: string }; + history: { role: ROLE; content: string }[]; +}; + +enum ROLE { + SYSTEM = 'system', + USER = 'user', + ASSISTANT = 'assistant' +} + +const INSTRUCTION: string = ` +Act as a quiz solver for the best notation with the following rules: +- When asked for the result of an equation, provide only the result without any other information and skip the other rules. +- If no answer(s) are given, answer the statement as usual without following the other rules, providing the most detailed, complete and precise explanation. +- For 'put in order' questions, provide the position of the answer separated by a new line (e.g., '1\n3\n2') and ignore other rules.- Always reply in this format: '\n\n...' +- Always reply in the format: '\n\n...'. +- Retain only the correct answer(s). +- Maintain the same order for the answers as in the text. +- Retain all text from the answer with its description, content or definition. +- Only provide answers that exactly match the given answer in the text. +- The question always has the correct answer(s), so you should always provide an answer. +- Always respond in the same language as the user's question. +`.trim(); + +const history: History = { + url: null, + system: { + role: ROLE.SYSTEM, + content: INSTRUCTION + }, + history: [] +}; + +/** + * Get the response from chatGPT api + * @param config + * @param question + * @returns + */ +async function getChatGPTResponse(config: Config, question: string): Promise { + const URL = location.hostname + location.pathname; + + // We reset the history when we enter a new moodle quiz or when it's desactivate + if (!config.history || history.url !== URL) { + history.url = URL; + history.history = []; + } + + const controller = new AbortController(); + const timeoutControler = setTimeout(() => controller.abort(), 15 * 1000); + + const message = { role: ROLE.USER, content: question }; + + const req = await fetch('https://api.openai.com/v1/chat/completions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${config.apiKey}` + }, + signal: config.timeout ? controller.signal : null, + body: JSON.stringify({ + model: config.model, + messages: [history.system, ...history.history, message], + temperature: 0.8, + top_p: 1.0, + presence_penalty: 1.0, + stop: null + }) + }); + + clearTimeout(timeoutControler); + + const rep = await req.json(); + const response = rep.choices[0].message.content; + + // Register the conversation + if (config.history) { + history.history.push(message); + history.history.push({ role: ROLE.ASSISTANT, content: response }); + } + + return { + question, + response, + normalizedResponse: normalizeText(response) + }; +} + +export default getChatGPTResponse; diff --git a/src/core/modes/autocomplete.ts b/src/core/modes/autocomplete.ts index d3f384a..63f7f32 100644 --- a/src/core/modes/autocomplete.ts +++ b/src/core/modes/autocomplete.ts @@ -1,12 +1,12 @@ -import type GPTAnswer from "@typing/gptAnswer"; -import type Config from "@typing/config"; -import handleClipboard from "@core/questions/clipboard"; -import handleContentEditable from "@core/questions/contenteditable"; -import handleNumber from "@core/questions/number"; -import handleRadio from "@core/questions/radio"; -import handleCheckbox from "@core/questions/checkbox"; -import handleSelect from "@core/questions/select"; -import handleTextbox from "@core/questions/textbox"; +import type GPTAnswer from '@typing/gptAnswer'; +import type Config from '@typing/config'; +import handleClipboard from '@core/questions/clipboard'; +import handleContentEditable from '@core/questions/contenteditable'; +import handleNumber from '@core/questions/number'; +import handleRadio from '@core/questions/radio'; +import handleCheckbox from '@core/questions/checkbox'; +import handleSelect from '@core/questions/select'; +import handleTextbox from '@core/questions/textbox'; type Props = { config: Config; @@ -31,7 +31,7 @@ function autoCompleteMode(props: Props) { handleNumber, handleSelect, handleRadio, - handleCheckbox, + handleCheckbox ]; for (const handler of handlers) { diff --git a/src/core/modes/clipboard.ts b/src/core/modes/clipboard.ts index 0c37151..782b6d3 100644 --- a/src/core/modes/clipboard.ts +++ b/src/core/modes/clipboard.ts @@ -1,6 +1,6 @@ -import type Config from "@typing/config"; -import type GPTAnswer from "@typing/gptAnswer"; -import handleClipboard from "@core/questions/clipboard"; +import type Config from '@typing/config'; +import type GPTAnswer from '@typing/gptAnswer'; +import handleClipboard from '@core/questions/clipboard'; type Props = { config: Config; diff --git a/src/core/modes/question-to-answer.ts b/src/core/modes/question-to-answer.ts index 1e4a33d..9250010 100644 --- a/src/core/modes/question-to-answer.ts +++ b/src/core/modes/question-to-answer.ts @@ -1,4 +1,4 @@ -import type GPTAnswer from "@typing/gptAnswer"; +import type GPTAnswer from '@typing/gptAnswer'; type Props = { questionElement: HTMLElement; @@ -21,15 +21,13 @@ function questionToAnswerMode(props: Props) { questionElement.textContent = props.gptAnswer.response; // Format the content - questionElement.style.whiteSpace = "pre-wrap"; + questionElement.style.whiteSpace = 'pre-wrap'; // To go back to the question / answer let contentIsResponse = true; - questionElement.addEventListener("click", function () { - questionElement.style.whiteSpace = contentIsResponse ? "" : "pre-warp"; - questionElement.textContent = contentIsResponse - ? questionBackup - : props.gptAnswer.response; + questionElement.addEventListener('click', function () { + questionElement.style.whiteSpace = contentIsResponse ? '' : 'pre-warp'; + questionElement.textContent = contentIsResponse ? questionBackup : props.gptAnswer.response; contentIsResponse = !contentIsResponse; }); diff --git a/src/core/questions/checkbox.ts b/src/core/questions/checkbox.ts index 3c3150b..5446e71 100644 --- a/src/core/questions/checkbox.ts +++ b/src/core/questions/checkbox.ts @@ -1,8 +1,8 @@ -import type Config from "@typing/config"; -import type GPTAnswer from "@typing/gptAnswer"; -import Logs from "@utils/logs"; -import normalizeText from "@utils/normalize-text"; -import { pickBestReponse } from "@utils/pick-best-response"; +import type Config from '@typing/config'; +import type GPTAnswer from '@typing/gptAnswer'; +import Logs from '@utils/logs'; +import normalizeText from '@utils/normalize-text'; +import { pickBestReponse } from '@utils/pick-best-response'; /** * Handle input checkbox elements @@ -18,18 +18,18 @@ function handleCheckbox( const firstInput = inputList?.[0] as HTMLInputElement; // Handle the case the input is not a checkbox - if (!firstInput || firstInput.type !== "checkbox") { + if (!firstInput || firstInput.type !== 'checkbox') { return false; } - const corrects = gptAnswer.normalizedResponse.split("\n"); + const corrects = gptAnswer.normalizedResponse.split('\n'); const possibleAnswers = Array.from(inputList) - .map((inp) => ({ + .map(inp => ({ element: inp, - value: normalizeText(inp?.parentElement?.textContent ?? ""), + value: normalizeText(inp?.parentElement?.textContent ?? '') })) - .filter((obj) => obj.value !== ""); + .filter(obj => obj.value !== ''); for (const correct of corrects) { const bestAnswer = pickBestReponse(correct, possibleAnswers); @@ -40,13 +40,9 @@ function handleCheckbox( const correctInput = bestAnswer.element as HTMLInputElement; if (config.mouseover) { - correctInput.addEventListener( - "mouseover", - () => (correctInput.checked = true), - { - once: true, - } - ); + correctInput.addEventListener('mouseover', () => (correctInput.checked = true), { + once: true + }); } else { correctInput.checked = true; } diff --git a/src/core/questions/clipboard.ts b/src/core/questions/clipboard.ts index c661953..f5af75d 100644 --- a/src/core/questions/clipboard.ts +++ b/src/core/questions/clipboard.ts @@ -1,15 +1,15 @@ -import type Config from "@typing/config"; -import type GPTAnswer from "@typing/gptAnswer"; -import titleIndications from "@utils/title-indications"; - -/** - * Copy the response in the clipboard if we can automaticaly fill the question - * @param config - * @param gptAnswer - */ -function handleClipboard(config: Config, gptAnswer: GPTAnswer) { - if (config.title) titleIndications("Copied to clipboard"); - navigator.clipboard.writeText(gptAnswer.response); -} - -export default handleClipboard; +import type Config from '@typing/config'; +import type GPTAnswer from '@typing/gptAnswer'; +import titleIndications from '@utils/title-indications'; + +/** + * Copy the response in the clipboard if we can automaticaly fill the question + * @param config + * @param gptAnswer + */ +function handleClipboard(config: Config, gptAnswer: GPTAnswer) { + if (config.title) titleIndications('Copied to clipboard'); + navigator.clipboard.writeText(gptAnswer.response); +} + +export default handleClipboard; diff --git a/src/core/questions/contenteditable.ts b/src/core/questions/contenteditable.ts index 742d68b..f10c750 100644 --- a/src/core/questions/contenteditable.ts +++ b/src/core/questions/contenteditable.ts @@ -1,5 +1,5 @@ -import type Config from "@typing/config"; -import type GPTAnswer from "@typing/gptAnswer"; +import type Config from '@typing/config'; +import type GPTAnswer from '@typing/gptAnswer'; /** * Hanlde contenteditable elements @@ -17,15 +17,15 @@ function handleContentEditable( if ( inputList.length !== 1 || // for now we don't handle many input for editable textcontent - input.getAttribute("contenteditable") !== "true" + input.getAttribute('contenteditable') !== 'true' ) { return false; } if (config.typing) { let index = 0; - input.addEventListener("keydown", function (event: KeyboardEvent) { - if (event.key === "Backspace") index = gptAnswer.response.length + 1; + input.addEventListener('keydown', function (event: KeyboardEvent) { + if (event.key === 'Backspace') index = gptAnswer.response.length + 1; if (index > gptAnswer.response.length) return; event.preventDefault(); input.textContent = gptAnswer.response.slice(0, ++index); diff --git a/src/core/questions/number.ts b/src/core/questions/number.ts index 2134904..5e52599 100644 --- a/src/core/questions/number.ts +++ b/src/core/questions/number.ts @@ -1,47 +1,45 @@ -import type Config from "@typing/config"; -import type GPTAnswer from "@typing/gptAnswer"; - -/** - * Handle number input - * @param config - * @param inputList - * @param gptAnswer - * @returns - */ -function handleNumber( - config: Config, - inputList: NodeListOf, - gptAnswer: GPTAnswer -): boolean { - const input = inputList[0] as HTMLInputElement | HTMLTextAreaElement; - - if ( - inputList.length !== 1 || // for now we don't handle many input number - input.type !== "number" - ) { - return false; - } - - const number = gptAnswer.normalizedResponse - .match(/\d+([,.]\d+)?/gi)?.[0] - ?.replace(",", "."); - - if (number === undefined) return false; - - if (config.typing) { - let index = 0; - input.addEventListener("keydown", function (event: Event) { - event.preventDefault(); - if ((event).key === "Backspace") index = number.length + 1; - if (index > number.length) return; - if (number.slice(index, index + 1) === ".") ++index; - input.value = number.slice(0, ++index); - }); - } else { - input.value = number; - } - - return true; -} - -export default handleNumber; +import type Config from '@typing/config'; +import type GPTAnswer from '@typing/gptAnswer'; + +/** + * Handle number input + * @param config + * @param inputList + * @param gptAnswer + * @returns + */ +function handleNumber( + config: Config, + inputList: NodeListOf, + gptAnswer: GPTAnswer +): boolean { + const input = inputList[0] as HTMLInputElement | HTMLTextAreaElement; + + if ( + inputList.length !== 1 || // for now we don't handle many input number + input.type !== 'number' + ) { + return false; + } + + const number = gptAnswer.normalizedResponse.match(/\d+([,.]\d+)?/gi)?.[0]?.replace(',', '.'); + + if (number === undefined) return false; + + if (config.typing) { + let index = 0; + input.addEventListener('keydown', function (event: Event) { + event.preventDefault(); + if ((event).key === 'Backspace') index = number.length + 1; + if (index > number.length) return; + if (number.slice(index, index + 1) === '.') ++index; + input.value = number.slice(0, ++index); + }); + } else { + input.value = number; + } + + return true; +} + +export default handleNumber; diff --git a/src/core/questions/radio.ts b/src/core/questions/radio.ts index 67b5d8d..e903dad 100644 --- a/src/core/questions/radio.ts +++ b/src/core/questions/radio.ts @@ -1,8 +1,8 @@ -import type Config from "@typing/config"; -import type GPTAnswer from "@typing/gptAnswer"; -import Logs from "@utils/logs"; -import normalizeText from "@utils/normalize-text"; -import { pickBestReponse } from "@utils/pick-best-response"; +import type Config from '@typing/config'; +import type GPTAnswer from '@typing/gptAnswer'; +import Logs from '@utils/logs'; +import normalizeText from '@utils/normalize-text'; +import { pickBestReponse } from '@utils/pick-best-response'; /** * Handle input radio elements @@ -18,21 +18,18 @@ function handleRadio( const firstInput = inputList?.[0] as HTMLInputElement; // Handle the case the input is not a radio - if (!firstInput || firstInput.type !== "radio") { + if (!firstInput || firstInput.type !== 'radio') { return false; } const possibleAnswers = Array.from(inputList) - .map((inp) => ({ + .map(inp => ({ element: inp, - value: normalizeText(inp?.parentElement?.textContent ?? ""), + value: normalizeText(inp?.parentElement?.textContent ?? '') })) - .filter((obj) => obj.value !== ""); + .filter(obj => obj.value !== ''); - const bestAnswer = pickBestReponse( - gptAnswer.normalizedResponse, - possibleAnswers - ); + const bestAnswer = pickBestReponse(gptAnswer.normalizedResponse, possibleAnswers); if (config.logs && bestAnswer.value) { Logs.bestAnswer(bestAnswer.value, bestAnswer.similarity); @@ -40,13 +37,9 @@ function handleRadio( const correctInput = bestAnswer.element as HTMLInputElement; if (config.mouseover) { - correctInput.addEventListener( - "mouseover", - () => (correctInput.checked = true), - { - once: true, - } - ); + correctInput.addEventListener('mouseover', () => (correctInput.checked = true), { + once: true + }); } else { correctInput.checked = true; } diff --git a/src/core/questions/select.ts b/src/core/questions/select.ts index f0e3e58..49cf762 100644 --- a/src/core/questions/select.ts +++ b/src/core/questions/select.ts @@ -1,64 +1,60 @@ -import type Config from "@typing/config"; -import type GPTAnswer from "@typing/gptAnswer"; -import Logs from "@utils/logs"; -import normalizeText from "@utils/normalize-text"; -import { pickBestReponse } from "@utils/pick-best-response"; - -/** - * Handle select elements (and put in order select) - * @param config - * @param inputList - * @param gptAnswer - * @returns - */ -function handleSelect( - config: Config, - inputList: NodeListOf, - gptAnswer: GPTAnswer -): boolean { - if (inputList.length === 0 || inputList[0].tagName !== "SELECT") return false; - - const corrects = gptAnswer.normalizedResponse.split("\n"); - - if (config.logs) Logs.array(corrects); - - for (let i = 0; i < inputList.length; ++i) { - if (!corrects[i]) break; - - const options = inputList[i].querySelectorAll("option"); - - const possibleAnswers = Array.from(options) - .map((opt) => ({ - element: opt, - value: normalizeText(opt.textContent ?? ""), - })) - .filter((obj) => obj.value !== ""); - - const bestAnswer = pickBestReponse(corrects[i], possibleAnswers); - - if (config.logs && bestAnswer.value) { - Logs.bestAnswer(bestAnswer.value, bestAnswer.similarity); - } - - const correctOption = bestAnswer.element as HTMLOptionElement; - const currentSelect = correctOption.closest("select"); - - if (currentSelect === null) continue; - - if (config.mouseover) { - currentSelect.addEventListener( - "click", - () => (correctOption.selected = true), - { - once: true, - } - ); - } else { - correctOption.selected = true; - } - } - - return true; -} - -export default handleSelect; +import type Config from '@typing/config'; +import type GPTAnswer from '@typing/gptAnswer'; +import Logs from '@utils/logs'; +import normalizeText from '@utils/normalize-text'; +import { pickBestReponse } from '@utils/pick-best-response'; + +/** + * Handle select elements (and put in order select) + * @param config + * @param inputList + * @param gptAnswer + * @returns + */ +function handleSelect( + config: Config, + inputList: NodeListOf, + gptAnswer: GPTAnswer +): boolean { + if (inputList.length === 0 || inputList[0].tagName !== 'SELECT') return false; + + const corrects = gptAnswer.normalizedResponse.split('\n'); + + if (config.logs) Logs.array(corrects); + + for (let i = 0; i < inputList.length; ++i) { + if (!corrects[i]) break; + + const options = inputList[i].querySelectorAll('option'); + + const possibleAnswers = Array.from(options) + .map(opt => ({ + element: opt, + value: normalizeText(opt.textContent ?? '') + })) + .filter(obj => obj.value !== ''); + + const bestAnswer = pickBestReponse(corrects[i], possibleAnswers); + + if (config.logs && bestAnswer.value) { + Logs.bestAnswer(bestAnswer.value, bestAnswer.similarity); + } + + const correctOption = bestAnswer.element as HTMLOptionElement; + const currentSelect = correctOption.closest('select'); + + if (currentSelect === null) continue; + + if (config.mouseover) { + currentSelect.addEventListener('click', () => (correctOption.selected = true), { + once: true + }); + } else { + correctOption.selected = true; + } + } + + return true; +} + +export default handleSelect; diff --git a/src/core/questions/textbox.ts b/src/core/questions/textbox.ts index b817fa0..810c883 100644 --- a/src/core/questions/textbox.ts +++ b/src/core/questions/textbox.ts @@ -1,42 +1,42 @@ -import type Config from "@typing/config"; -import type GPTAnswer from "@typing/gptAnswer"; - -/** - * Handle textbox - * @param config - * @param inputList - * @param gptAnswer - * @returns - */ -function handleTextbox( - config: Config, - inputList: NodeListOf, - gptAnswer: GPTAnswer -): boolean { - const input = inputList[0] as HTMLInputElement | HTMLTextAreaElement; - - if ( - inputList.length !== 1 || // for now we don't handle many input text - (input.tagName !== "TEXTAREA" && input.type !== "text") - ) { - return false; - } - - if (config.typing) { - let index = 0; - input.addEventListener("keydown", function (event: Event) { - event.preventDefault(); - if ((event).key === "Backspace") { - index = gptAnswer.response.length + 1; - } - if (index > gptAnswer.response.length) return; - input.value = gptAnswer.response.slice(0, ++index); - }); - } else { - input.value = gptAnswer.response; - } - - return true; -} - -export default handleTextbox; +import type Config from '@typing/config'; +import type GPTAnswer from '@typing/gptAnswer'; + +/** + * Handle textbox + * @param config + * @param inputList + * @param gptAnswer + * @returns + */ +function handleTextbox( + config: Config, + inputList: NodeListOf, + gptAnswer: GPTAnswer +): boolean { + const input = inputList[0] as HTMLInputElement | HTMLTextAreaElement; + + if ( + inputList.length !== 1 || // for now we don't handle many input text + (input.tagName !== 'TEXTAREA' && input.type !== 'text') + ) { + return false; + } + + if (config.typing) { + let index = 0; + input.addEventListener('keydown', function (event: Event) { + event.preventDefault(); + if ((event).key === 'Backspace') { + index = gptAnswer.response.length + 1; + } + if (index > gptAnswer.response.length) return; + input.value = gptAnswer.response.slice(0, ++index); + }); + } else { + input.value = gptAnswer.response; + } + + return true; +} + +export default handleTextbox; diff --git a/src/core/reply.ts b/src/core/reply.ts index f7aba91..84e619e 100644 --- a/src/core/reply.ts +++ b/src/core/reply.ts @@ -1,81 +1,76 @@ -import type Config from "@typing/config"; -import Logs from "@utils/logs"; -import getChatGPTResponse from "./get-response"; -import createAndNormalizeQuestion from "./create-question"; -import clipboardMode from "./modes/clipboard"; -import questionToAnswerMode from "./modes/question-to-answer"; -import autoCompleteMode from "./modes/autocomplete"; - -type Props = { - config: Config; - questionElement: HTMLElement; - form: HTMLElement; - inputQuery: string; - removeListener: () => void; -}; - -/** - * Reply to the question - * @param props - * @returns - */ -async function reply(props: Props): Promise { - if (props.config.cursor) props.questionElement.style.cursor = "wait"; - - const question = createAndNormalizeQuestion(props.form); - const inputList: NodeListOf = props.form.querySelectorAll( - props.inputQuery - ); - - const gptAnswer = await getChatGPTResponse(props.config, question).catch( - (error) => ({ - error, - }) - ); - - const haveError = typeof gptAnswer === "object" && "error" in gptAnswer; - - if (props.config.cursor) { - props.questionElement.style.cursor = - props.config.infinite || haveError ? "pointer" : "initial"; - } - - if (haveError) { - console.error(gptAnswer.error); - return; - } - - if (props.config.logs) { - Logs.question(question); - Logs.response(gptAnswer); - } - - switch (props.config.mode) { - case "clipboard": - clipboardMode({ - config: props.config, - questionElement: props.questionElement, - gptAnswer, - removeListener: props.removeListener, - }); - break; - case "question-to-answer": - questionToAnswerMode({ - gptAnswer, - questionElement: props.questionElement, - removeListener: props.removeListener, - }); - break; - case "autocomplete": - autoCompleteMode({ - config: props.config, - gptAnswer, - inputList, - questionElement: props.questionElement, - removeListener: props.removeListener, - }); - break; - } -} - -export default reply; +import type Config from '@typing/config'; +import Logs from '@utils/logs'; +import getChatGPTResponse from './get-response'; +import createAndNormalizeQuestion from './create-question'; +import clipboardMode from './modes/clipboard'; +import questionToAnswerMode from './modes/question-to-answer'; +import autoCompleteMode from './modes/autocomplete'; + +type Props = { + config: Config; + questionElement: HTMLElement; + form: HTMLElement; + inputQuery: string; + removeListener: () => void; +}; + +/** + * Reply to the question + * @param props + * @returns + */ +async function reply(props: Props): Promise { + if (props.config.cursor) props.questionElement.style.cursor = 'wait'; + + const question = createAndNormalizeQuestion(props.form); + const inputList: NodeListOf = props.form.querySelectorAll(props.inputQuery); + + const gptAnswer = await getChatGPTResponse(props.config, question).catch(error => ({ + error + })); + + const haveError = typeof gptAnswer === 'object' && 'error' in gptAnswer; + + if (props.config.cursor) { + props.questionElement.style.cursor = props.config.infinite || haveError ? 'pointer' : 'initial'; + } + + if (haveError) { + console.error(gptAnswer.error); + return; + } + + if (props.config.logs) { + Logs.question(question); + Logs.response(gptAnswer); + } + + switch (props.config.mode) { + case 'clipboard': + clipboardMode({ + config: props.config, + questionElement: props.questionElement, + gptAnswer, + removeListener: props.removeListener + }); + break; + case 'question-to-answer': + questionToAnswerMode({ + gptAnswer, + questionElement: props.questionElement, + removeListener: props.removeListener + }); + break; + case 'autocomplete': + autoCompleteMode({ + config: props.config, + gptAnswer, + inputList, + questionElement: props.questionElement, + removeListener: props.removeListener + }); + break; + } +} + +export default reply; diff --git a/src/index.ts b/src/index.ts index 804df57..fa5c8ae 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,14 +1,14 @@ -import type Config from "@typing/config"; -import { codeListener, setUpMoodleGpt } from "./core/code-listener"; - -chrome.storage.sync.get(["moodleGPT"]).then(function (storage) { - const config: Config = storage.moodleGPT; - - if (!config) throw new Error("Please configure MoodleGPT into the extension"); - - if (config.code) { - codeListener(config); - } else { - setUpMoodleGpt(config); - } -}); +import type Config from '@typing/config'; +import { codeListener, setUpMoodleGpt } from './core/code-listener'; + +chrome.storage.sync.get(['moodleGPT']).then(function (storage) { + const config: Config = storage.moodleGPT; + + if (!config) throw new Error('Please configure MoodleGPT into the extension'); + + if (config.code) { + codeListener(config); + } else { + setUpMoodleGpt(config); + } +}); diff --git a/src/types/config.ts b/src/types/config.ts index c317e68..ab8205a 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -10,7 +10,7 @@ type Config = { title?: boolean; timeout?: boolean; history?: boolean; - mode?: "autocomplete" | "question-to-answer" | "clipboard"; + mode?: 'autocomplete' | 'question-to-answer' | 'clipboard'; }; export default Config; diff --git a/src/utils/html-table-to-string.ts b/src/utils/html-table-to-string.ts index c06ae6d..507e794 100644 --- a/src/utils/html-table-to-string.ts +++ b/src/utils/html-table-to-string.ts @@ -5,37 +5,32 @@ */ function htmlTableToString(table: HTMLTableElement) { const tab: string[][] = []; - const lines = Array.from(table.querySelectorAll("tr")); + const lines = Array.from(table.querySelectorAll('tr')); const maxColumnsLength: number[] = []; - lines.map((line) => { - const cells = Array.from(line.querySelectorAll("td, th")); + lines.map(line => { + const cells = Array.from(line.querySelectorAll('td, th')); const cellsContent = cells.map((cell, index) => { const content = cell.textContent?.trim(); - maxColumnsLength[index] = Math.max( - maxColumnsLength[index] || 0, - content?.length || 0 - ); - return content ?? ""; + maxColumnsLength[index] = Math.max(maxColumnsLength[index] || 0, content?.length || 0); + return content ?? ''; }); tab.push(cellsContent); }); - const lineSeparationSize = - maxColumnsLength.reduce((a, b) => a + b) + tab[0].length * 3 + 1; - const lineSeparation = - "\n" + Array(lineSeparationSize).fill("-").join("") + "\n"; + const lineSeparationSize = maxColumnsLength.reduce((a, b) => a + b) + tab[0].length * 3 + 1; + const lineSeparation = '\n' + Array(lineSeparationSize).fill('-').join('') + '\n'; - const mappedTab = tab.map((line) => { + const mappedTab = tab.map(line => { const mappedLine = line.map((content, index) => content.padEnd( maxColumnsLength[index], - "\u00A0" // For no matching with \s + '\u00A0' // For no matching with \s ) ); - return "| " + mappedLine.join(" | ") + " |"; + return '| ' + mappedLine.join(' | ') + ' |'; }); const head = mappedTab.shift(); - return head + lineSeparation + mappedTab.join("\n"); + return head + lineSeparation + mappedTab.join('\n'); } export default htmlTableToString; diff --git a/src/utils/logs.ts b/src/utils/logs.ts index 0483ddb..eb4a9d1 100644 --- a/src/utils/logs.ts +++ b/src/utils/logs.ts @@ -1,29 +1,29 @@ -import GPTAnswer from "@typing/gptAnswer"; -import { toPourcentage } from "./pick-best-response"; - -class Logs { - static question(text: string) { - const css = "color: cyan"; - console.log("%c[QUESTION]: %s", css, text); - } - - static bestAnswer(answer: string, similarity: number) { - const css = "color: green"; - console.log( - "%c[BEST ANSWER]: %s", - css, - `"${answer}" with a similarity of ${toPourcentage(similarity)}` - ); - } - - static array(arr: unknown[]) { - console.log("[CORRECTS] ", arr); - } - - static response(gptAnswer: GPTAnswer) { - console.log("Original:\n" + gptAnswer.response); - console.log("Normalized:\n" + gptAnswer.normalizedResponse); - } -} - -export default Logs; +import GPTAnswer from '@typing/gptAnswer'; +import { toPourcentage } from './pick-best-response'; + +class Logs { + static question(text: string) { + const css = 'color: cyan'; + console.log('%c[QUESTION]: %s', css, text); + } + + static bestAnswer(answer: string, similarity: number) { + const css = 'color: green'; + console.log( + '%c[BEST ANSWER]: %s', + css, + `"${answer}" with a similarity of ${toPourcentage(similarity)}` + ); + } + + static array(arr: unknown[]) { + console.log('[CORRECTS] ', arr); + } + + static response(gptAnswer: GPTAnswer) { + console.log('Original:\n' + gptAnswer.response); + console.log('Normalized:\n' + gptAnswer.normalizedResponse); + } +} + +export default Logs; diff --git a/src/utils/normalize-text.ts b/src/utils/normalize-text.ts index a5321a4..2945ab3 100644 --- a/src/utils/normalize-text.ts +++ b/src/utils/normalize-text.ts @@ -1,20 +1,20 @@ -/** - * Normlize text - * @param text - */ -function normalizeText(text: string, toLowerCase: boolean = true) { - if (toLowerCase) text = text.toLowerCase(); - - const normalizedText = text - .replace(/\n+/gi, "\n") //remove duplicate new lines - .replace(/(\n\s*\n)+/g, "\n") //remove useless white space from textcontent - .replace(/[ \t]+/gi, " ") //replace multiples space or tabs by a space - .trim() - // We remove the following content because sometimes ChatGPT will reply: "answer d" - .replace(/^[a-z\d]\.\s/gi, "") //a. text, b. text, c. text, 1. text, 2. text, 3.text - .replace(/\n[a-z\d]\.\s/gi, "\n"); //same but with new line - - return normalizedText; -} - -export default normalizeText; +/** + * Normlize text + * @param text + */ +function normalizeText(text: string, toLowerCase: boolean = true) { + if (toLowerCase) text = text.toLowerCase(); + + const normalizedText = text + .replace(/\n+/gi, '\n') //remove duplicate new lines + .replace(/(\n\s*\n)+/g, '\n') //remove useless white space from textcontent + .replace(/[ \t]+/gi, ' ') //replace multiples space or tabs by a space + .trim() + // We remove the following content because sometimes ChatGPT will reply: "answer d" + .replace(/^[a-z\d]\.\s/gi, '') //a. text, b. text, c. text, 1. text, 2. text, 3.text + .replace(/\n[a-z\d]\.\s/gi, '\n'); //same but with new line + + return normalizedText; +} + +export default normalizeText; diff --git a/src/utils/pick-best-response.ts b/src/utils/pick-best-response.ts index 7956419..e523952 100644 --- a/src/utils/pick-best-response.ts +++ b/src/utils/pick-best-response.ts @@ -21,8 +21,8 @@ function levenshteinDistance(str1: string, str2: string) { if (str2.length === 0) return str1.length; const matrix: number[][] = []; - const str1WithoutSpaces = str1.replace(/\s+/, ""); - const str2WithoutSpaces = str2.replace(/\s+/, ""); + const str1WithoutSpaces = str1.replace(/\s+/, ''); + const str2WithoutSpaces = str2.replace(/\s+/, ''); for (let i = 0; i <= str1WithoutSpaces.length; ++i) { matrix.push([i]); @@ -33,8 +33,7 @@ function levenshteinDistance(str1: string, str2: string) { : Math.min( matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, - matrix[i - 1][j - 1] + - (str1WithoutSpaces[i - 1] === str2WithoutSpaces[j - 1] ? 0 : 1) + matrix[i - 1][j - 1] + (str1WithoutSpaces[i - 1] === str2WithoutSpaces[j - 1] ? 0 : 1) ); } } @@ -67,7 +66,7 @@ export function pickBestReponse( let bestResponse: BestResponse = { element: null, similarity: 0, - value: null, + value: null }; for (const obj of arr) { const similarity = sentenceSimilarity(obj.value, answer); @@ -100,7 +99,7 @@ export function pickResponsesWithSimilarityGreaterThan( responses.push({ similarity, value: obj.value, - element: obj.element, + element: obj.element }); } return responses.sort((a, b) => a.similarity - b.similarity); @@ -111,5 +110,5 @@ export function pickResponsesWithSimilarityGreaterThan( * @param similarity */ export function toPourcentage(similarity: number): string { - return Math.round(similarity * 100 * 100) / 100 + "%"; + return Math.round(similarity * 100 * 100) / 100 + '%'; } diff --git a/src/utils/title-indications.ts b/src/utils/title-indications.ts index 65c7506..32b94dd 100644 --- a/src/utils/title-indications.ts +++ b/src/utils/title-indications.ts @@ -1,11 +1,11 @@ -/** - * Show some informations into the document title and remove it after 3000ms - * @param text - */ -function titleIndications(text: string) { - const backTitle = document.title; - document.title = text; - setTimeout(() => (document.title = backTitle), 3000); -} - -export default titleIndications; +/** + * Show some informations into the document title and remove it after 3000ms + * @param text + */ +function titleIndications(text: string) { + const backTitle = document.title; + document.title = text; + setTimeout(() => (document.title = backTitle), 3000); +} + +export default titleIndications; diff --git a/test/fake-moodle/css/style.css b/test/fake-moodle/css/style.css index bc8746f..3fe7434 100644 --- a/test/fake-moodle/css/style.css +++ b/test/fake-moodle/css/style.css @@ -7,7 +7,7 @@ box-sizing: border-box; padding: 0; margin: 0; - font-family: "Segeo UI"; + font-family: 'Segeo UI'; } body { diff --git a/test/fake-moodle/index.html b/test/fake-moodle/index.html index 4b333fe..93abd99 100644 --- a/test/fake-moodle/index.html +++ b/test/fake-moodle/index.html @@ -1,4 +1,4 @@ - + @@ -17,23 +17,20 @@
a. Systems Administrator: Managing and maintaining computer systems and + networks.
b. Software Developer: Designing, coding, testing, and maintaining software + applications.
- +
@@ -276,8 +273,8 @@

- Gives a "reverseWorld" function in javascript which takes as a - parameter a word and flips it in the opposite direction + Gives a "reverseWorld" function in javascript which takes as a parameter a word and flips + it in the opposite direction

diff --git a/test/reset-moodle-inputs/reset.js b/test/reset-moodle-inputs/reset.js index 2ba4d86..9675c46 100644 --- a/test/reset-moodle-inputs/reset.js +++ b/test/reset-moodle-inputs/reset.js @@ -1,21 +1,19 @@ /* Reset real moodle inputs to try in real env */ -for (const option of document.querySelectorAll("option")) { +for (const option of document.querySelectorAll('option')) { option.selected = false; option.disabled = false; - option.closest("select").disabled = false; + option.closest('select').disabled = false; } -for (const input of document.querySelectorAll( - 'input[type="radio"], input[type="checkbox"]' -)) { +for (const input of document.querySelectorAll('input[type="radio"], input[type="checkbox"]')) { input.checked = false; input.disabled = false; } -for (const icon of document.querySelectorAll(".text-danger, .text-success")) { +for (const icon of document.querySelectorAll('.text-danger, .text-success')) { icon.remove(); } -for (const feedback of document.querySelectorAll(".specificfeedback")) { +for (const feedback of document.querySelectorAll('.specificfeedback')) { feedback.remove(); } diff --git a/tsconfig.json b/tsconfig.json index 3be788f..e6bdc50 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,25 +1,25 @@ -{ - "compilerOptions": { - "strict": true, - "baseUrl": "src", - "module": "CommonJS", - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "target": "ES6", - "noImplicitAny": true, - "moduleResolution": "node", - "sourceMap": true, - "outDir": "extension", - "resolveJsonModule": true, - "types": ["node", "chrome"], - "typeRoots": ["node_modules/@types"], - "strictBindCallApply": true, - "paths": { - "@typing/*": ["types/*"], - "@utils/*": ["utils/*"], - "@core/*": ["core/*"], - "@questions/*": ["core/question/*"] - } - }, - "include": ["src/**/*"] -} +{ + "compilerOptions": { + "strict": true, + "baseUrl": "src", + "module": "CommonJS", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "target": "ES6", + "noImplicitAny": true, + "moduleResolution": "node", + "sourceMap": true, + "outDir": "extension", + "resolveJsonModule": true, + "types": ["node", "chrome"], + "typeRoots": ["node_modules/@types"], + "strictBindCallApply": true, + "paths": { + "@typing/*": ["types/*"], + "@utils/*": ["utils/*"], + "@core/*": ["core/*"], + "@questions/*": ["core/question/*"] + } + }, + "include": ["src/**/*"] +}