diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..2f4fec9 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,18 @@ +module.exports = { + root: true, + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:@typescript-eslint/recommended', + 'prettier' + ], + ignorePatterns: ['node_modules/'], + overrides: [ + { + files: ['extension/popup/*.js', 'src/**/*.ts'], + rules: {} + } + ] +}; diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100644 index 0000000..3e0eb2c --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,3 @@ +#!/bin/bash +npm run prettier +npm run lint \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.md b/.github/ISSUE_TEMPLATE/BUG_REPORT.md new file mode 100644 index 0000000..bda7baf --- /dev/null +++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.md @@ -0,0 +1,28 @@ +## Bug Report + +**Description of the Bug** +A clear and concise description of the bug. + +**Steps to Reproduce** + +1. Step 1 +2. Step 2 +3. ... + +**Expected Behavior** +A clear and concise description of what you expected to happen. + +**Actual Behavior** +A clear and concise description of what actually happens. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Tried Solutions** +Have you tried anything to solve the problem? If yes, what? + +**Additional Context** +Add any other context about the problem here. + +**Additional Information** +Add any other information you think is relevant. diff --git a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md new file mode 100644 index 0000000..3a24437 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md @@ -0,0 +1,16 @@ +## Feature Request + +**Description of the Feature** +A clear and concise description of the feature you'd like to see added. + +**Motivation** +An explanation of the motivation behind this feature. How would it improve the user experience or utility of the software? + +**Screenshot (Optional)** +If possible, add a screenshot or illustration of the feature you're proposing. + +**Additional Context** +Add any other context or information about the feature that you think is relevant. + +**Additional Information** +Add any other information you think is relevant. diff --git a/.github/ISSUE_TEMPLATE/HELP_REQUEST.md b/.github/ISSUE_TEMPLATE/HELP_REQUEST.md new file mode 100644 index 0000000..66eddb6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/HELP_REQUEST.md @@ -0,0 +1,24 @@ +## Help Request + +**Issue Summary** + +[Summarize the issue you are facing concisely.] + +**Describe the problem** + +[Describe what is the problem] + +**Environment** + +- Operating System: [Your OS] +- Browser: [Your Browser] +- Version: [Version Number] +- Any other relevant information + +### Screenshots + +[Include any relevant screenshots if they can help illustrate the issue.] + +### Additional Information + +[Any additional information that may be helpful in diagnosing the issue.] diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..1849f5e --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,28 @@ +## Pull Request + +**Description** +A brief description of the changes in this pull request. + +**Related Issue** + +- Fixes # (issue number) +- Addresses # (issue number) + +**Changes Made** +Describe the changes you've made. Provide a high-level overview of what has been done in this pull request. + +**Screenshots (if applicable)** +Add screenshots or GIFs to demonstrate the changes, if applicable. + +**Checklist** + +- [ ] I have tested the changes locally. +- [ ] I have updated the documentation accordingly. +- [ ] I have added necessary comments to the code. +- [ ] All tests pass successfully. + +**Additional Notes** +Add any additional notes or comments about the pull request here. + +**Reviewer(s)** +@username1, @username2 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 2581e32..d78c3d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,24 +1,37 @@ -# CHANGELOG - -## 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.1.0 + +- Bugs correction +- Adjusted the abort timeout to 20seconds +- `code` is not required anymore +- Issue [#9](https://github.com/yoannchb-pro/MoodleGPT/issues/9) resolved +- GPT model autocompletation +- Better algorithm to find the correct answer (levenshtein distance) +- Better ChatGPT prompt +- Added `history` to the options/configuration +- Added `include images` to the option/configuration +- `gpt-4` support + +## 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/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..b16e98b --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +- The use of sexualized language or imagery and unwelcome sexual attention or + advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic + address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/CONTRIBUTE.md b/CONTRIBUTE.md new file mode 100644 index 0000000..3f2738b --- /dev/null +++ b/CONTRIBUTE.md @@ -0,0 +1,43 @@ +# CONTRIBUTE + +When contributing to this repository, please first discuss the change you wish to make via issue, +email, or any other method with the owners of this repository before making a change. + +Please note we have a code of conduct, please follow it in all your interactions with the project. + +## Code of Conduct + +See the code of conduct [here](./CODE_OF_CONDUCT.md) + +## Pull Request Process + +1. Update the README.md with details of changes to the extension and update the screens. +2. Build the project. +3. Select `dev` as the target branch to prepare the futur version. +4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you + do not have permission to do that, you may request the second reviewer to merge it for you. + +## Setup the project + +``` +node: 18.13.0 +npm: 8.19.2 +``` + +### 1. Install dependencies + +``` +$ npm install +``` + +### 2. Setup Git hooks + +``` +$ git config --local core.hooksPath .githooks/ +``` + +### 3. Build project + +``` +$ npm run build +``` diff --git a/README.md b/README.md index d27055c..1a5318d 100644 --- a/README.md +++ b/README.md @@ -1,166 +1,161 @@ -

Mortarboard icons created by itim2101 - Flaticon

- -# MoodleGPT v1.0.3 - -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 v1.0.3](#moodlegpt-v103) - - [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 - -To test the code, you can run the index.html file located in the "test/fake-moodle" folder. Or a better solution is to install moodle locally. +

Mortarboard icons created by itim2101 - Flaticon

+ +# MoodleGPT 1.1.0 + +This extension allows you to hide CHAT-GPT in a Moodle quiz. You just need to 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 1.1.0](#moodlegpt-110) + - [Chrome Webstore](#chrome-webstore) + - [Summary](#summary) + - [Disclaimer !](#disclaimer-) + - [Donate](#donate) + - [Update](#update) + - [Set up](#set-up) + - [Mode](#mode) + - [Settings](#settings) + - [Internal other features](#internal-other-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 autocompleted ?](#what-about-if-the-question-cant-be-autocompleted-) + - [Test](#test) + - [Beta version with advanced features](#beta-version-with-advanced-features) + +## 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 :). I'm alone working on this project and I'm still a student. +
+Mortarboard icons created by itim2101 - Flaticon + +## Update + +See the [changelog](./CHANGELOG.md) to see every updates ! + +## Set up + +> NOTE: This extension only works on Chromium-based browsers like Edge, Chrome, etc. + +

+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 api](https://platform.openai.com/api-keys). Finally, select a [gpt model](https://platform.openai.com/docs/models) (ensure it work with completion api). + +## Mode + +

+Popup +

+ +- Autocomplete: The extension will complete the question for you by selecting the correct(s) answer(s). +- 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](https://platform.openai.com/api-keys) from your account (Note you have to put credits by entering a credit card onto your account). +- GPT Model\*: the [gpt model](https://platform.openai.com/docs/models) you want to use. +- Code: a code to be more discret for injecting/removing the extension from the page. Simply type your code you entered into the configuration on the keyboard when you are on your moodle quiz and the extension will be inject. If you want to remove the injection just simply type back the code on your keyboard. +- 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 for the question, chatgpt answer and which response has been chosen. +
Logs +- Request timeout: if the request is too long it will be abort after 20 seconds. +- 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). +- Save history: allows you to create a conversation with ChatGPT by saving the previous question with its answer. However, note that it can consume a significant number of tokens. +- Include images (only work with gpt-4): allows you to include the images from the question to be send to the chatgpt api. The quality is reduced to 75% to use less tokens. However, note that it can consume a significant number of tokens. +
![Images](./assets/images.gif) + +## Internal other 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 +``` + +![Table](./assets/table.gif) + +## 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 autocompleted ? + +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 if `Title indication` is on. + +![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. + +## Beta version with advanced features + +If you're interested in accessing advanced features ahead of their official release, please consider downloading the extension from the [dev branch](https://github.com/yoannchb-pro/MoodleGPT/tree/dev). However, please be aware that this branch is under development and may contain bugs. If you encounter any issues, don't hesitate to contact me or create an issue on GitHub. Your feedback is invaluable in helping us improve the extension. diff --git a/TODO.md b/TODO.md index 0ba115e..4640904 100644 --- a/TODO.md +++ b/TODO.md @@ -1,16 +1 @@ -# TODO - -## Priority: 1 - -- [ ] Fixe put in order -- [ ] Make some tests - -## Priority: 2 - -## Priority: 3 (because hard to make) - -- [ ] Increment question when there is statement (hard because it is often on another page) -- [ ] Support math equation from image stocked in the `data-mathml` attribute -- [ ] Try something to understand images like (image -> ascii or may be using other AI ?) -- [ ] Support multiple input type in a question -- [ ] Support drag and drop quiz +# TODO diff --git a/assets/images.gif b/assets/images.gif new file mode 100644 index 0000000..84fc686 Binary files /dev/null and b/assets/images.gif differ diff --git a/assets/logs.png b/assets/logs.png index 612d7f7..918914d 100644 Binary files a/assets/logs.png and b/assets/logs.png differ diff --git a/assets/popup.png b/assets/popup.png index 673dcab..601e817 100644 Binary files a/assets/popup.png and b/assets/popup.png differ diff --git a/assets/settings.png b/assets/settings.png index d85e836..568e261 100644 Binary files a/assets/settings.png and b/assets/settings.png differ diff --git a/assets/setup.png b/assets/setup.png index d326e0e..6e68157 100644 Binary files a/assets/setup.png and b/assets/setup.png differ diff --git a/assets/table.gif b/assets/table.gif new file mode 100644 index 0000000..f26bf2d Binary files /dev/null and b/assets/table.gif differ diff --git a/extension/MoodleGPT.js b/extension/MoodleGPT.js index a44c165..cd2a9d2 100644 --- a/extension/MoodleGPT.js +++ b/extension/MoodleGPT.js @@ -1,2 +1,2 @@ -!function(e){"function"==typeof define&&define.amd?define(e):e()}((function(){"use strict";function e(e){const t=document.title;document.title=e,setTimeout((()=>document.title=t),3e3)}function t(e,t,n,o){return new(n||(n=Promise))((function(r,s){function i(e){try{c(o.next(e))}catch(e){s(e)}}function l(e){try{c(o.throw(e))}catch(e){s(e)}}function c(e){var t;e.done?r(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(i,l)}c((o=o.apply(e,t||[])).next())}))}class n{static question(e){console.log("%c[QUESTION]: %s","color: cyan",e)}static responseTry(e,t){const n="color: "+(t?"green":"red");console.log("%c[CHECKING]: %s",n,e)}static array(e){console.log("[CORRECTS] ",e)}static response(e){console.log("Original:\n"+e.response),console.log("Normalized:\n"+e.normalizedResponse)}}function o(e,t=!0){let n=e.replace(/\n+/gi,"\n").replace(/(\n\s*\n)+/g,"\n").replace(/[ \t]+/gi," ");return t&&(n=n.toLowerCase()),n.trim().replace(/^[a-z\d]\.\s/gi,"").replace(/\n[a-z\d]\.\s/gi,"\n")}function r(e){const t=[],n=Array.from(e.querySelectorAll("tr")),o=[];n.map((e=>{const n=Array.from(e.querySelectorAll("td, th")).map(((e,t)=>{var n;const r=null===(n=e.textContent)||void 0===n?void 0:n.trim();return o[t]=Math.max(o[t]||0,r.length||0),r}));t.push(n)}));const r=o.reduce(((e,t)=>e+t))+3*t[0].length+1,s="\n"+Array(r).fill("-").join("")+"\n",i=t.map((e=>"| "+e.map(((e,t)=>e.padEnd(o[t]," "))).join(" | ")+" |"));return i.shift()+s+i.join("\n")}function s(e,t,r){const s=null==t?void 0:t[0];if(!s||"checkbox"!==s.type&&"radio"!==s.type)return!1;for(const s of t){const t=o(s.parentNode.textContent),i=r.normalizedResponse.includes(t);e.logs&&n.responseTry(t,i),i&&(e.mouseover?s.addEventListener("mouseover",(()=>s.checked=!0),{once:!0}):s.checked=!0)}return!0}function i(e,t,r){if(0===t.length||"SELECT"!==t[0].tagName)return!1;let s=r.normalizedResponse.split("\n");e.logs&&n.array(s),s.length===2*t.length&&(s=s.filter(((e,t)=>t%2==1)));for(let r=0;r{const o=t.includes(r);return e.logs&&n.responseTry(r,o),o}));if(-1!==l){e.mouseover?i[l+1].closest("select").addEventListener("click",(function(){i[l+1].selected="selected"}),{once:!0}):i[l+1].selected="selected";break}}if(e.logs&&n.responseTry(l,c),c){e.mouseover?t.closest("select").addEventListener("click",(()=>t.selected=!0),{once:!0}):t.selected=!0;break}}}return!0}function l(e,t,n){const o=t[0];if(1!==t.length||"TEXTAREA"!==o.tagName&&"text"!==o.type)return!1;if(e.typing){let e=0;o.addEventListener("keydown",(function(t){"Backspace"===t.key&&(e=n.response.length+1),e>n.response.length||(t.preventDefault(),o.value=n.response.slice(0,++e))}))}else o.value=n.response;return!0}function c(t,n){t.title&&e("Copied to clipboard"),navigator.clipboard.writeText(n.response)}function a(e,t,n){var o,r;const s=t[0];if(1!==t.length||"number"!==s.type)return!1;const i=null===(r=null===(o=n.normalizedResponse.match(/\d+([,\.]\d+)?/gi))||void 0===o?void 0:o[0])||void 0===r?void 0:r.replace(",",".");if(!i)return!1;if(e.typing){let e=0;s.addEventListener("keydown",(function(t){"Backspace"===t.key&&(e=i.length+1),e>i.length||(t.preventDefault(),"."===i.slice(e,e+1)&&++e,s.value=i.slice(0,++e))}))}else s.value=i;return!0}function u(e,t,n){const o=t[0];if(1!==t.length||"true"!==o.getAttribute("contenteditable"))return!1;if(e.typing){let e=0;o.addEventListener("keydown",(function(t){if("Backspace"===t.key&&(e=n.response.length+1),e>n.response.length)return;t.preventDefault(),o.textContent=n.response.slice(0,++e),o.focus();const r=document.createRange();r.selectNodeContents(o),r.collapse(!1);const s=window.getSelection();s.removeAllRanges(),s.addRange(r)}))}else o.textContent=n.response;return!0}function d(e,d,f,p){return t(this,void 0,void 0,(function*(){e.cursor&&(d.style.cursor="wait");const h=function(e,t){let n=t.innerText;const s=t.querySelectorAll(".accesshide");for(const e of s)n=n.replace(e.innerText,"");const i=t.querySelectorAll(".qtext table");for(const e of i)n=n.replace(e.innerText,"\n"+r(e)+"\n");return o(n,!1)}(0,f),g=f.querySelectorAll(p),y=yield function(e,n){return t(this,void 0,void 0,(function*(){const t=new AbortController,r=setTimeout((()=>t.abort()),15e3),s=yield fetch("https://api.openai.com/v1/chat/completions",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e.apiKey}`},signal:e.timeout?t.signal:null,body:JSON.stringify({model:e.model,messages:[{role:"system",content:"\nFollow those rules:\n- Sometimes there won't be a question, so just answer the statement as you normally would without following the other rules and give the most detailled and complete answer with explication.\n- For put in order question just give the good order separate by new line\n- Your goal is to understand the statement and to reply to each question by giving only the answer.\n- You will keep the same order for the answers like in the text. \n- You will separate all the answer with new lines and only show the correctes one.\n- You will only give the answers for each question and omit the questions, statement, title or other informations from the response.\n- You will only give answer with exactly the same text as the gived answers.\n- The question always have the good answer so you should always give an answer to the question.\n- You will always respond in the same langage as the user question."},{role:"user",content:n}],temperature:.8,top_p:1,presence_penalty:1,stop:null})});clearTimeout(r);const i=(yield s.json()).choices[0].message.content;return{response:i,normalizedResponse:o(i)}}))}(e,h).catch((e=>({error:e}))),v="object"==typeof y&&"error"in y;if(e.cursor&&(d.style.cursor=e.infinite||v?"pointer":"initial"),v)return void console.error(y.error);if(e.logs&&(n.question(h),n.response(y)),"clipboard"===e.mode)return e.infinite||m(d),c(e,y);if("question-to-answer"===e.mode){m(d);const e=f.querySelector(".qtext"),t=e.textContent;return e.textContent=y.response,e.style.whiteSpace="pre-wrap",void e.addEventListener("click",(function(){const n=e.textContent===t;e.style.whiteSpace=n?"pre-wrap":null,e.textContent=n?y.response:t}))}e.infinite||m(d);const w=[u,l,a,i,s];for(const t of w)if(t(e,g,y))return;c(e,y)}))}const f=[],p=[];function h(t){document.body.addEventListener("keydown",(function(n){f.push(n.key),f.length>t.code.length&&f.shift(),f.join("")===t.code&&(f.length=0,function(t){if(p.length>0){for(const e of p)t.cursor&&(e.element.style.cursor="initial"),e.element.removeEventListener("click",e.fn);return t.title&&e("Removed"),void(p.length=0)}const n=["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");t.cursor&&(o.style.cursor="pointer");const r=d.bind(null,t,o,e,n);p.push({element:o,fn:r}),o.addEventListener("click",r)}t.title&&e("Injected")}(t))}))}function m(e){const t=p.findIndex((t=>t.element===e));if(-1!==t){const e=p.splice(t,1)[0];e.element.removeEventListener("click",e.fn)}}chrome.storage.sync.get(["moodleGPT"]).then((function(e){const t=e.moodleGPT;if(!t)throw new Error("Please configure MoodleGPT into the extension");h(t)}))})); +!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){n&&(e=e.toLowerCase());return 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,l;!function(e){e.SYSTEM="system",e.USER="user",e.ASSISTANT="assistant"}(s||(s={})),function(e){e.TEXT="text",e.IMAGE="image_url"}(l||(l={}));const c="\nAct as a quiz solver for the best notation with the following 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- But for the calculation provide this format 'result: '\n- For 'put in order' questions, maintain the answer in the order as presented in the question but assocy the correct order to it by usin this format ':\n:', ignore other rules.\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(),a={role:s.SYSTEM,content:c};function u(e,t,o){return n(this,void 0,void 0,(function*(){const n=t.querySelectorAll("img");if(!e.includeImages||!function(e){const n=e.match(/gpt-(\d+)/);return!!(null==n?void 0:n[1])&&Number(n[1])>=4}(e.model)||0===n.length)return o;const r=[],i=Array.from(n).map((e=>function(e,n=.75){return new Promise(((t,o)=>{const r=document.createElement("canvas"),i=r.getContext("2d");if(!i)return o("Can't get the canvas context, ensure your navigator support canvas"),void r.remove();const s=new Image;s.crossOrigin="Anonymous",s.onload=()=>{r.width=s.width,r.height=s.height,i.drawImage(s,0,0);const e=r.toDataURL("image/png",n);t(e),r.remove()},s.onerror=e=>{o(e),r.remove()},s.src=e.src}))}(e))),s=yield Promise.allSettled(i);for(const n of s)"fulfilled"===n.status?r.push({type:l.IMAGE,image_url:{url:n.value}}):e.logs&&console.error(n.reason);return r.push({type:l.TEXT,text:o}),r}))}function f(e,t,o){return n(this,void 0,void 0,(function*(){const n=yield u(e,t,o),r={role:s.USER,content:n};if(!e.history)return{messages:[a,r]};let i;const l=JSON.parse(null!==(c=sessionStorage.moodleGPTHistory)&&void 0!==c?c:"null");var c;const f=function(){var e,n;const t=new URLSearchParams(document.location.search);return{host:document.location.host,cmid:null!==(e=t.get("cmid"))&&void 0!==e?e:"",attempt:null!==(n=t.get("attempt"))&&void 0!==n?n:"",history:[]}}();return i=null!==l&&function(e,n){const t=["host","cmid","attempt"];for(const o of t)if(e[o]!==n[o])return!1;return!0}(l,f)?l:f,{messages:[a,...i.history,r],saveResponse(n){e.history&&(i.history.push(r),i.history.push({role:s.ASSISTANT,content:n}),sessionStorage.moodleGPTHistory=JSON.stringify(i))}}}))}function m(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=n[0].length,i=o.reduce(((e,n)=>e+n),0)+3*(r-1),s="\n"+Array(i).fill("-").join("")+"\n",l=n.map((e=>e.map(((e,n)=>e.padEnd(o[n]," "))).join(" | ")));return l.shift()+s+l.join("\n")}function d(n,t){n.title&&e("Copied to clipboard"),navigator.clipboard.writeText(t.response)}function p(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 h(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 g(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.click()),{once:!0}):a.click(),!0}function v(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.click()),{once:!0}):i.click()}return!0}function y(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 w(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 A(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"+m(e)+"\n");return i(n,!1)}(e.form),o=e.form.querySelectorAll(e.inputQuery),s=yield function(e,t,o){return n(this,void 0,void 0,(function*(){const n=new AbortController,r=setTimeout((()=>n.abort()),2e4),s=yield f(e,t,o),l=yield fetch("https://api.openai.com/v1/chat/completions",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e.apiKey}`},signal:e.timeout?n.signal:null,body:JSON.stringify({model:e.model,messages:s.messages,temperature:.1,top_p:.6,presence_penalty:0,max_tokens:2e3})});clearTimeout(r);const c=(yield l.json()).choices[0].message.content;return"function"==typeof s.saveResponse&&s.saveResponse(c),{question:o,response:c,normalizedResponse:i(c)}}))}(e.config,e.questionElement,t).catch((e=>({error:e}))),l="object"==typeof s&&"error"in s;if(e.config.cursor&&(e.questionElement.style.cursor=e.config.infinite||l?"pointer":"initial"),l)console.error(s.error);else switch(e.config.logs&&(r.question(t),r.response(s)),e.config.mode){case"clipboard":!function(e){e.config.infinite||e.removeListener(),d(e.config,e.gptAnswer)}({config:e.config,questionElement:e.questionElement,gptAnswer:s,removeListener:e.removeListener});break;case"question-to-answer":!function(e){var n;const t=e.questionElement;e.removeListener();const o=null!==(n=t.innerHTML)&&void 0!==n?n:"";t.innerHTML=e.gptAnswer.response,t.style.whiteSpace="pre-wrap",t.addEventListener("click",(function(){const n=t.innerHTML===e.gptAnswer.response;t.style.whiteSpace=n?"initial":"pre-wrap",t.innerHTML=n?o:e.gptAnswer.response}))}({gptAnswer:s,questionElement:e.questionElement,removeListener:e.removeListener});break;case"autocomplete":!function(e){e.config.infinite||e.removeListener();const n=[p,w,h,y,g,v];for(const t of n)if(t(e.config,e.inputList,e.gptAnswer))return;d(e.config,e.gptAnswer)}({config:e.config,gptAnswer:s,inputList:o,questionElement:e.questionElement,removeListener:e.removeListener})}}))}const E=[],S=[];function T(e){const n=S.findIndex((n=>n.element===e));if(-1!==n){const e=S.splice(n,1)[0];e.element.removeEventListener("click",e.fn)}}function q(n){if(S.length>0){for(const e of S)n.cursor&&(e.element.style.cursor="initial"),e.element.removeEventListener("click",e.fn);return n.title&&e("Removed"),void(S.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=A.bind(null,{config:n,questionElement:o,form:e,inputQuery:t,removeListener:()=>T(o)});S.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){E.push(n.key),E.length>e.code.length&&E.shift(),E.join("")===e.code&&(E.length=0,q(e))}))}(n):q(n)}))})); //# sourceMappingURL=MoodleGPT.js.map diff --git a/extension/MoodleGPT.js.map b/extension/MoodleGPT.js.map index e43d28b..655f9fe 100644 --- a/extension/MoodleGPT.js.map +++ b/extension/MoodleGPT.js.map @@ -1 +1 @@ -{"version":3,"file":"MoodleGPT.js","sources":["../src/utils/title-indications.ts","../node_modules/tslib/tslib.es6.js","../src/utils/logs.ts","../src/utils/normalize-text.ts","../src/utils/html-table-to-string.ts","../src/core/questions/radio-checkbox.ts","../src/core/questions/select.ts","../src/core/questions/textbox.ts","../src/core/questions/clipboard.ts","../src/core/questions/number.ts","../src/core/questions/contenteditable.ts","../src/core/reply.ts","../src/core/create-question.ts","../src/core/get-response.ts","../src/core/code-listener.ts","../src/index.ts"],"sourcesContent":["/**\r\n * Show some informations into the document title and remove it after 3000ms\r\n * @param text\r\n */\r\nfunction titleIndications(text: string) {\r\n const backTitle = document.title;\r\n document.title = text;\r\n setTimeout(() => (document.title = backTitle), 3000);\r\n}\r\n\r\nexport default titleIndications;\r\n","/******************************************************************************\r\nCopyright (c) Microsoft Corporation.\r\n\r\nPermission to use, copy, modify, and/or distribute this software for any\r\npurpose with or without fee is hereby granted.\r\n\r\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\r\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\r\nAND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\r\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\r\nLOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\r\nOTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\r\nPERFORMANCE OF THIS SOFTWARE.\r\n***************************************************************************** */\r\n/* global Reflect, Promise */\r\n\r\nvar extendStatics = function(d, b) {\r\n extendStatics = Object.setPrototypeOf ||\r\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\r\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\r\n return extendStatics(d, b);\r\n};\r\n\r\nexport function __extends(d, b) {\r\n if (typeof b !== \"function\" && b !== null)\r\n throw new TypeError(\"Class extends value \" + String(b) + \" is not a constructor or null\");\r\n extendStatics(d, b);\r\n function __() { this.constructor = d; }\r\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\r\n}\r\n\r\nexport var __assign = function() {\r\n __assign = Object.assign || function __assign(t) {\r\n for (var s, i = 1, n = arguments.length; i < n; i++) {\r\n s = arguments[i];\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];\r\n }\r\n return t;\r\n }\r\n return __assign.apply(this, arguments);\r\n}\r\n\r\nexport function __rest(s, e) {\r\n var t = {};\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)\r\n t[p] = s[p];\r\n if (s != null && typeof Object.getOwnPropertySymbols === \"function\")\r\n for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {\r\n if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))\r\n t[p[i]] = s[p[i]];\r\n }\r\n return t;\r\n}\r\n\r\nexport function __decorate(decorators, target, key, desc) {\r\n var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;\r\n if (typeof Reflect === \"object\" && typeof Reflect.decorate === \"function\") r = Reflect.decorate(decorators, target, key, desc);\r\n else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;\r\n return c > 3 && r && Object.defineProperty(target, key, r), r;\r\n}\r\n\r\nexport function __param(paramIndex, decorator) {\r\n return function (target, key) { decorator(target, key, paramIndex); }\r\n}\r\n\r\nexport function __esDecorate(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {\r\n function accept(f) { if (f !== void 0 && typeof f !== \"function\") throw new TypeError(\"Function expected\"); return f; }\r\n var kind = contextIn.kind, key = kind === \"getter\" ? \"get\" : kind === \"setter\" ? \"set\" : \"value\";\r\n var target = !descriptorIn && ctor ? contextIn[\"static\"] ? ctor : ctor.prototype : null;\r\n var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});\r\n var _, done = false;\r\n for (var i = decorators.length - 1; i >= 0; i--) {\r\n var context = {};\r\n for (var p in contextIn) context[p] = p === \"access\" ? {} : contextIn[p];\r\n for (var p in contextIn.access) context.access[p] = contextIn.access[p];\r\n context.addInitializer = function (f) { if (done) throw new TypeError(\"Cannot add initializers after decoration has completed\"); extraInitializers.push(accept(f || null)); };\r\n var result = (0, decorators[i])(kind === \"accessor\" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);\r\n if (kind === \"accessor\") {\r\n if (result === void 0) continue;\r\n if (result === null || typeof result !== \"object\") throw new TypeError(\"Object expected\");\r\n if (_ = accept(result.get)) descriptor.get = _;\r\n if (_ = accept(result.set)) descriptor.set = _;\r\n if (_ = accept(result.init)) initializers.push(_);\r\n }\r\n else if (_ = accept(result)) {\r\n if (kind === \"field\") initializers.push(_);\r\n else descriptor[key] = _;\r\n }\r\n }\r\n if (target) Object.defineProperty(target, contextIn.name, descriptor);\r\n done = true;\r\n};\r\n\r\nexport function __runInitializers(thisArg, initializers, value) {\r\n var useValue = arguments.length > 2;\r\n for (var i = 0; i < initializers.length; i++) {\r\n value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);\r\n }\r\n return useValue ? value : void 0;\r\n};\r\n\r\nexport function __propKey(x) {\r\n return typeof x === \"symbol\" ? x : \"\".concat(x);\r\n};\r\n\r\nexport function __setFunctionName(f, name, prefix) {\r\n if (typeof name === \"symbol\") name = name.description ? \"[\".concat(name.description, \"]\") : \"\";\r\n return Object.defineProperty(f, \"name\", { configurable: true, value: prefix ? \"\".concat(prefix, \" \", name) : name });\r\n};\r\n\r\nexport function __metadata(metadataKey, metadataValue) {\r\n if (typeof Reflect === \"object\" && typeof Reflect.metadata === \"function\") return Reflect.metadata(metadataKey, metadataValue);\r\n}\r\n\r\nexport function __awaiter(thisArg, _arguments, P, generator) {\r\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\r\n return new (P || (P = Promise))(function (resolve, reject) {\r\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\r\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\r\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\r\n step((generator = generator.apply(thisArg, _arguments || [])).next());\r\n });\r\n}\r\n\r\nexport function __generator(thisArg, body) {\r\n var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;\r\n return g = { next: verb(0), \"throw\": verb(1), \"return\": verb(2) }, typeof Symbol === \"function\" && (g[Symbol.iterator] = function() { return this; }), g;\r\n function verb(n) { return function (v) { return step([n, v]); }; }\r\n function step(op) {\r\n if (f) throw new TypeError(\"Generator is already executing.\");\r\n while (g && (g = 0, op[0] && (_ = 0)), _) try {\r\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\r\n if (y = 0, t) op = [op[0] & 2, t.value];\r\n switch (op[0]) {\r\n case 0: case 1: t = op; break;\r\n case 4: _.label++; return { value: op[1], done: false };\r\n case 5: _.label++; y = op[1]; op = [0]; continue;\r\n case 7: op = _.ops.pop(); _.trys.pop(); continue;\r\n default:\r\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\r\n if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }\r\n if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }\r\n if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }\r\n if (t[2]) _.ops.pop();\r\n _.trys.pop(); continue;\r\n }\r\n op = body.call(thisArg, _);\r\n } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }\r\n if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };\r\n }\r\n}\r\n\r\nexport var __createBinding = Object.create ? (function(o, m, k, k2) {\r\n if (k2 === undefined) k2 = k;\r\n var desc = Object.getOwnPropertyDescriptor(m, k);\r\n if (!desc || (\"get\" in desc ? !m.__esModule : desc.writable || desc.configurable)) {\r\n desc = { enumerable: true, get: function() { return m[k]; } };\r\n }\r\n Object.defineProperty(o, k2, desc);\r\n}) : (function(o, m, k, k2) {\r\n if (k2 === undefined) k2 = k;\r\n o[k2] = m[k];\r\n});\r\n\r\nexport function __exportStar(m, o) {\r\n for (var p in m) if (p !== \"default\" && !Object.prototype.hasOwnProperty.call(o, p)) __createBinding(o, m, p);\r\n}\r\n\r\nexport function __values(o) {\r\n var s = typeof Symbol === \"function\" && Symbol.iterator, m = s && o[s], i = 0;\r\n if (m) return m.call(o);\r\n if (o && typeof o.length === \"number\") return {\r\n next: function () {\r\n if (o && i >= o.length) o = void 0;\r\n return { value: o && o[i++], done: !o };\r\n }\r\n };\r\n throw new TypeError(s ? \"Object is not iterable.\" : \"Symbol.iterator is not defined.\");\r\n}\r\n\r\nexport function __read(o, n) {\r\n var m = typeof Symbol === \"function\" && o[Symbol.iterator];\r\n if (!m) return o;\r\n var i = m.call(o), r, ar = [], e;\r\n try {\r\n while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);\r\n }\r\n catch (error) { e = { error: error }; }\r\n finally {\r\n try {\r\n if (r && !r.done && (m = i[\"return\"])) m.call(i);\r\n }\r\n finally { if (e) throw e.error; }\r\n }\r\n return ar;\r\n}\r\n\r\n/** @deprecated */\r\nexport function __spread() {\r\n for (var ar = [], i = 0; i < arguments.length; i++)\r\n ar = ar.concat(__read(arguments[i]));\r\n return ar;\r\n}\r\n\r\n/** @deprecated */\r\nexport function __spreadArrays() {\r\n for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;\r\n for (var r = Array(s), k = 0, i = 0; i < il; i++)\r\n for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)\r\n r[k] = a[j];\r\n return r;\r\n}\r\n\r\nexport function __spreadArray(to, from, pack) {\r\n if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {\r\n if (ar || !(i in from)) {\r\n if (!ar) ar = Array.prototype.slice.call(from, 0, i);\r\n ar[i] = from[i];\r\n }\r\n }\r\n return to.concat(ar || Array.prototype.slice.call(from));\r\n}\r\n\r\nexport function __await(v) {\r\n return this instanceof __await ? (this.v = v, this) : new __await(v);\r\n}\r\n\r\nexport function __asyncGenerator(thisArg, _arguments, generator) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var g = generator.apply(thisArg, _arguments || []), i, q = [];\r\n return i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i;\r\n function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; }\r\n function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }\r\n function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }\r\n function fulfill(value) { resume(\"next\", value); }\r\n function reject(value) { resume(\"throw\", value); }\r\n function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }\r\n}\r\n\r\nexport function __asyncDelegator(o) {\r\n var i, p;\r\n return i = {}, verb(\"next\"), verb(\"throw\", function (e) { throw e; }), verb(\"return\"), i[Symbol.iterator] = function () { return this; }, i;\r\n function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: false } : f ? f(v) : v; } : f; }\r\n}\r\n\r\nexport function __asyncValues(o) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var m = o[Symbol.asyncIterator], i;\r\n return m ? m.call(o) : (o = typeof __values === \"function\" ? __values(o) : o[Symbol.iterator](), i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i);\r\n function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }\r\n function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }\r\n}\r\n\r\nexport function __makeTemplateObject(cooked, raw) {\r\n if (Object.defineProperty) { Object.defineProperty(cooked, \"raw\", { value: raw }); } else { cooked.raw = raw; }\r\n return cooked;\r\n};\r\n\r\nvar __setModuleDefault = Object.create ? (function(o, v) {\r\n Object.defineProperty(o, \"default\", { enumerable: true, value: v });\r\n}) : function(o, v) {\r\n o[\"default\"] = v;\r\n};\r\n\r\nexport function __importStar(mod) {\r\n if (mod && mod.__esModule) return mod;\r\n var result = {};\r\n if (mod != null) for (var k in mod) if (k !== \"default\" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);\r\n __setModuleDefault(result, mod);\r\n return result;\r\n}\r\n\r\nexport function __importDefault(mod) {\r\n return (mod && mod.__esModule) ? mod : { default: mod };\r\n}\r\n\r\nexport function __classPrivateFieldGet(receiver, state, kind, f) {\r\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a getter\");\r\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot read private member from an object whose class did not declare it\");\r\n return kind === \"m\" ? f : kind === \"a\" ? f.call(receiver) : f ? f.value : state.get(receiver);\r\n}\r\n\r\nexport function __classPrivateFieldSet(receiver, state, value, kind, f) {\r\n if (kind === \"m\") throw new TypeError(\"Private method is not writable\");\r\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a setter\");\r\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot write private member to an object whose class did not declare it\");\r\n return (kind === \"a\" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;\r\n}\r\n\r\nexport function __classPrivateFieldIn(state, receiver) {\r\n if (receiver === null || (typeof receiver !== \"object\" && typeof receiver !== \"function\")) throw new TypeError(\"Cannot use 'in' operator on non-object\");\r\n return typeof state === \"function\" ? receiver === state : state.has(receiver);\r\n}\r\n","import GPTAnswer from \"../types/gptAnswer\";\r\nclass Logs {\r\n static question(text: string) {\r\n const css = \"color: cyan\";\r\n console.log(\"%c[QUESTION]: %s\", css, text);\r\n }\r\n\r\n static responseTry(text: string, valide: boolean) {\r\n const css = \"color: \" + (valide ? \"green\" : \"red\");\r\n console.log(\"%c[CHECKING]: %s\", css, text);\r\n }\r\n\r\n static array(arr: unknown[]) {\r\n console.log(\"[CORRECTS] \", arr);\r\n }\r\n\r\n static response(gptAnswer: GPTAnswer) {\r\n console.log(\"Original:\\n\" + gptAnswer.response);\r\n console.log(\"Normalized:\\n\" + gptAnswer.normalizedResponse);\r\n }\r\n}\r\n\r\nexport default Logs;\r\n","/**\r\n * Normlize text\r\n * @param text\r\n */\r\nfunction normalizeText(text: string, toLowerCase: boolean = true) {\r\n let normalizedText = text\r\n .replace(/\\n+/gi, \"\\n\") //remove duplicate new lines\r\n .replace(/(\\n\\s*\\n)+/g, \"\\n\") //remove useless white sapce from textcontent\r\n .replace(/[ \\t]+/gi, \" \"); //replace multiples space or tabs by a space\r\n\r\n if (toLowerCase) normalizedText = normalizedText.toLowerCase();\r\n\r\n return (\r\n normalizedText\r\n .trim()\r\n /* We remove that because sometimes ChatGPT will reply: \"answer d\" */\r\n .replace(/^[a-z\\d]\\.\\s/gi, \"\") //a. text, b. text, c. text, 1. text, 2. text, 3.text\r\n .replace(/\\n[a-z\\d]\\.\\s/gi, \"\\n\") //same but with new line\r\n );\r\n}\r\n\r\nexport default normalizeText;\r\n","/**\r\n * Convert table to representating string table\r\n * @param table\r\n * @returns\r\n */\r\nfunction htmlTableToString(table: HTMLTableElement) {\r\n const tab: string[][] = [];\r\n const lines = Array.from(table.querySelectorAll(\"tr\"));\r\n const maxColumnsLength: number[] = [];\r\n lines.map((line) => {\r\n const cells = Array.from(line.querySelectorAll(\"td, th\"));\r\n const cellsContent = cells.map((cell, index) => {\r\n const content = cell.textContent?.trim();\r\n maxColumnsLength[index] = Math.max(\r\n maxColumnsLength[index] || 0,\r\n content.length || 0\r\n );\r\n return content;\r\n });\r\n tab.push(cellsContent);\r\n });\r\n\r\n const lineSeparationSize =\r\n maxColumnsLength.reduce((a, b) => a + b) + tab[0].length * 3 + 1;\r\n const lineSeparation =\r\n \"\\n\" + Array(lineSeparationSize).fill(\"-\").join(\"\") + \"\\n\";\r\n\r\n const mappedTab = tab.map((line) => {\r\n const mappedLine = line.map((content, index) =>\r\n content.padEnd(\r\n maxColumnsLength[index],\r\n \"\\u00A0\" /* For no matching with \\s */\r\n )\r\n );\r\n return \"| \" + mappedLine.join(\" | \") + \" |\";\r\n });\r\n const head = mappedTab.shift();\r\n return head + lineSeparation + mappedTab.join(\"\\n\");\r\n}\r\n\r\nexport default htmlTableToString;\r\n","import Config from \"../../types/config\";\r\nimport GPTAnswer from \"../../types/gptAnswer\";\r\nimport Logs from \"../../utils/logs\";\r\nimport normalizeText from \"../../utils/normalize-text\";\r\n\r\n/**\r\n * Handle checkbox and input elements\r\n * @param config\r\n * @param inputList\r\n * @param gptAnswer\r\n */\r\nfunction handleRadioAndCheckbox(\r\n config: Config,\r\n inputList: NodeListOf,\r\n gptAnswer: GPTAnswer\r\n): boolean {\r\n const input = inputList?.[0] as HTMLInputElement;\r\n\r\n if (!input || (input.type !== \"checkbox\" && input.type !== \"radio\"))\r\n return false;\r\n\r\n for (const input of inputList as NodeListOf) {\r\n const content = normalizeText(input.parentNode.textContent);\r\n const valide = gptAnswer.normalizedResponse.includes(content);\r\n if (config.logs) Logs.responseTry(content, valide);\r\n if (valide) {\r\n if (config.mouseover) {\r\n input.addEventListener(\"mouseover\", () => (input.checked = true), {\r\n once: true,\r\n });\r\n } else {\r\n input.checked = true;\r\n }\r\n }\r\n }\r\n return true;\r\n}\r\n\r\nexport default handleRadioAndCheckbox;\r\n","import Config from \"../../types/config\";\r\nimport GPTAnswer from \"../../types/gptAnswer\";\r\nimport Logs from \"../../utils/logs\";\r\nimport normalizeText from \"../../utils/normalize-text\";\r\n\r\n/**\r\n * Handle select elements (and put in order select)\r\n * @param config\r\n * @param inputList\r\n * @param gptAnswer\r\n * @returns\r\n */\r\nfunction handleSelect(\r\n config: Config,\r\n inputList: NodeListOf,\r\n gptAnswer: GPTAnswer\r\n): boolean {\r\n if (inputList.length === 0 || inputList[0].tagName !== \"SELECT\") return false;\r\n\r\n let correct = gptAnswer.normalizedResponse.split(\"\\n\");\r\n\r\n if (config.logs) Logs.array(correct);\r\n\r\n /**\r\n * Sometimes ChatGPT give the question so we should remove them\r\n * Example:\r\n * 5*5\r\n * 25\r\n * 10+10\r\n * 20\r\n * 20-10\r\n * 10\r\n *\r\n * And we only want to keep answers\r\n * 25\r\n * 20\r\n * 10\r\n */\r\n if (correct.length === inputList.length * 2) {\r\n correct = correct.filter((answer, index) => index % 2 === 1);\r\n }\r\n\r\n for (let j = 0; j < inputList.length; ++j) {\r\n const options = inputList[j].querySelectorAll(\"option\");\r\n\r\n for (const option of options) {\r\n const content = normalizeText(option.textContent);\r\n const valide = correct[j].includes(content);\r\n\r\n /* Handle put in order question */\r\n if (!/[^\\d]+/gi.test(content)) {\r\n const elementTitle = (option.parentNode as HTMLElement)\r\n .closest(\"tr\")\r\n .querySelector(\".text\");\r\n const content = normalizeText(elementTitle.textContent);\r\n\r\n const indexCorrectAnswer = correct.findIndex((answer) => {\r\n const valide = answer.includes(content);\r\n if (config.logs) Logs.responseTry(content, valide);\r\n return valide;\r\n });\r\n\r\n if (indexCorrectAnswer !== -1) {\r\n //we do + 1 because we skip the first option: Choose...\r\n if (config.mouseover) {\r\n options[indexCorrectAnswer + 1].closest(\"select\").addEventListener(\r\n \"click\",\r\n function () {\r\n options[indexCorrectAnswer + 1].selected = \"selected\" as any;\r\n },\r\n { once: true }\r\n );\r\n } else {\r\n options[indexCorrectAnswer + 1].selected = \"selected\" as any;\r\n }\r\n break;\r\n }\r\n }\r\n /* End */\r\n\r\n if (config.logs) Logs.responseTry(content, valide);\r\n\r\n if (valide) {\r\n if (config.mouseover) {\r\n option\r\n .closest(\"select\")\r\n .addEventListener(\"click\", () => (option.selected = true), {\r\n once: true,\r\n });\r\n } else {\r\n option.selected = true;\r\n }\r\n break;\r\n }\r\n }\r\n }\r\n\r\n return true;\r\n}\r\n\r\nexport default handleSelect;\r\n","import Config from \"../../types/config\";\r\nimport GPTAnswer from \"../../types/gptAnswer\";\r\n\r\n/**\r\n * Handle textbox\r\n * @param config\r\n * @param inputList\r\n * @param gptAnswer\r\n * @returns\r\n */\r\nfunction handleTextbox(\r\n config: Config,\r\n inputList: NodeListOf,\r\n gptAnswer: GPTAnswer\r\n): boolean {\r\n const input = inputList[0] as HTMLInputElement | HTMLTextAreaElement;\r\n\r\n if (\r\n inputList.length !== 1 ||\r\n (input.tagName !== \"TEXTAREA\" && input.type !== \"text\")\r\n )\r\n return false;\r\n\r\n if (config.typing) {\r\n let index = 0;\r\n input.addEventListener(\"keydown\", function (event: KeyboardEvent) {\r\n if (event.key === \"Backspace\") index = gptAnswer.response.length + 1;\r\n if (index > gptAnswer.response.length) return;\r\n event.preventDefault();\r\n input.value = gptAnswer.response.slice(0, ++index);\r\n });\r\n } else {\r\n input.value = gptAnswer.response;\r\n }\r\n\r\n return true;\r\n}\r\n\r\nexport default handleTextbox;\r\n","import Config from \"../../types/config\";\r\nimport GPTAnswer from \"../../types/gptAnswer\";\r\nimport titleIndications from \"../../utils/title-indications\";\r\n\r\n/**\r\n * Copy the response in the clipboard if we can automaticaly fill the question\r\n * @param config\r\n * @param gptAnswer\r\n */\r\nfunction handleClipboard(config: Config, gptAnswer: GPTAnswer) {\r\n if (config.title) titleIndications(\"Copied to clipboard\");\r\n navigator.clipboard.writeText(gptAnswer.response);\r\n}\r\n\r\nexport default handleClipboard;\r\n","import Config from \"../../types/config\";\r\nimport GPTAnswer from \"../../types/gptAnswer\";\r\n\r\n/**\r\n * Handle number input\r\n * @param config\r\n * @param inputList\r\n * @param gptAnswer\r\n * @returns\r\n */\r\nfunction handleNumber(\r\n config: Config,\r\n inputList: NodeListOf,\r\n gptAnswer: GPTAnswer\r\n): boolean {\r\n const input = inputList[0] as HTMLInputElement | HTMLTextAreaElement;\r\n\r\n if (inputList.length !== 1 || input.type !== \"number\") return false;\r\n\r\n const number = gptAnswer.normalizedResponse\r\n .match(/\\d+([,\\.]\\d+)?/gi)?.[0]\r\n ?.replace(\",\", \".\");\r\n\r\n if (!number) return false;\r\n\r\n if (config.typing) {\r\n let index = 0;\r\n input.addEventListener(\"keydown\", function (event: KeyboardEvent) {\r\n if (event.key === \"Backspace\") index = number.length + 1;\r\n if (index > number.length) return;\r\n event.preventDefault();\r\n if (number.slice(index, index + 1) === \".\") ++index;\r\n input.value = number.slice(0, ++index);\r\n });\r\n } else {\r\n input.value = number;\r\n }\r\n\r\n return true;\r\n}\r\n\r\nexport default handleNumber;\r\n","import Config from \"../../types/config\";\r\nimport GPTAnswer from \"../../types/gptAnswer\";\r\n\r\n/**\r\n * Hanlde contenteditable elements\r\n * @param config\r\n * @param inputList\r\n * @param gptAnswer\r\n * @returns\r\n */\r\nfunction handleContentEditable(\r\n config: Config,\r\n inputList: NodeListOf,\r\n gptAnswer: GPTAnswer\r\n): boolean {\r\n const input = inputList[0];\r\n\r\n if (\r\n inputList.length !== 1 ||\r\n input.getAttribute(\"contenteditable\") !== \"true\"\r\n )\r\n return false;\r\n\r\n if (config.typing) {\r\n let index = 0;\r\n input.addEventListener(\"keydown\", function (event: KeyboardEvent) {\r\n if (event.key === \"Backspace\") index = gptAnswer.response.length + 1;\r\n if (index > gptAnswer.response.length) return;\r\n event.preventDefault();\r\n input.textContent = gptAnswer.response.slice(0, ++index);\r\n\r\n /* Put the cursor at the end of the typed text */\r\n input.focus();\r\n const range = document.createRange();\r\n range.selectNodeContents(input);\r\n range.collapse(false);\r\n const selection = window.getSelection();\r\n selection.removeAllRanges();\r\n selection.addRange(range);\r\n });\r\n } else {\r\n input.textContent = gptAnswer.response;\r\n }\r\n\r\n return true;\r\n}\r\n\r\nexport default handleContentEditable;\r\n","import Config from \"../types/config\";\r\nimport Logs from \"../utils/logs\";\r\nimport getChatGPTResponse from \"./get-response\";\r\nimport createQuestion from \"./create-question\";\r\nimport handleRadioAndCheckbox from \"./questions/radio-checkbox\";\r\nimport handleSelect from \"./questions/select\";\r\nimport handleTextbox from \"./questions/textbox\";\r\nimport handleClipboard from \"./questions/clipboard\";\r\nimport handleNumber from \"./questions/number\";\r\nimport handleContentEditable from \"./questions/contenteditable\";\r\nimport { removeListener } from \"./code-listener\";\r\n\r\n/**\r\n * Reply to the question\r\n * @param config\r\n * @param hiddenButton\r\n * @param form\r\n * @param query\r\n * @returns\r\n */\r\nasync function reply(\r\n config: Config,\r\n hiddenButton: HTMLElement,\r\n form: HTMLElement,\r\n query: string\r\n) {\r\n if (config.cursor) hiddenButton.style.cursor = \"wait\";\r\n\r\n const question = createQuestion(config, form);\r\n const inputList: NodeListOf = form.querySelectorAll(query);\r\n\r\n const gptAnswer = await getChatGPTResponse(config, question).catch(\r\n (error) => ({\r\n error,\r\n })\r\n );\r\n\r\n const haveError = typeof gptAnswer === \"object\" && \"error\" in gptAnswer;\r\n\r\n if (config.cursor)\r\n hiddenButton.style.cursor =\r\n config.infinite || haveError ? \"pointer\" : \"initial\";\r\n\r\n if (haveError) {\r\n console.error(gptAnswer.error);\r\n return;\r\n }\r\n\r\n if (config.logs) {\r\n Logs.question(question);\r\n Logs.response(gptAnswer);\r\n }\r\n\r\n /* Handle clipboard mode */\r\n if (config.mode === \"clipboard\") {\r\n if (!config.infinite) removeListener(hiddenButton);\r\n return handleClipboard(config, gptAnswer);\r\n }\r\n\r\n /* Handle question to answer mode */\r\n if (config.mode === \"question-to-answer\") {\r\n removeListener(hiddenButton);\r\n\r\n const questionContainer = form.querySelector(\".qtext\");\r\n const questionBackup = questionContainer.textContent;\r\n\r\n questionContainer.textContent = gptAnswer.response;\r\n questionContainer.style.whiteSpace = \"pre-wrap\";\r\n\r\n questionContainer.addEventListener(\"click\", function () {\r\n const isNotResponse = questionContainer.textContent === questionBackup;\r\n questionContainer.style.whiteSpace = isNotResponse ? \"pre-wrap\" : null;\r\n questionContainer.textContent = isNotResponse\r\n ? gptAnswer.response\r\n : questionBackup;\r\n });\r\n return;\r\n }\r\n\r\n /* Better then set once on the event because if there is an error the user can click an other time on the question */\r\n if (!config.infinite) removeListener(hiddenButton);\r\n\r\n const handlers = [\r\n handleContentEditable,\r\n handleTextbox,\r\n handleNumber,\r\n handleSelect,\r\n handleRadioAndCheckbox,\r\n ];\r\n\r\n for (const handler of handlers) {\r\n if (handler(config, inputList, gptAnswer)) return;\r\n }\r\n\r\n /* In the case we can't auto complete the question */\r\n handleClipboard(config, gptAnswer);\r\n}\r\n\r\nexport default reply;\r\n","import Config from \"../types/config\";\r\nimport normalizeText from \"../utils/normalize-text\";\r\nimport htmlTableToString from \"../utils/html-table-to-string\";\r\n\r\n/**\r\n * Normalize the question and add sub informations\r\n * @param langage\r\n * @param question\r\n * @returns\r\n */\r\nfunction createQuestion(config: Config, questionContainer: HTMLElement) {\r\n let question = questionContainer.innerText;\r\n\r\n /* We remove unnecessary information */\r\n const accesshideElements: NodeListOf =\r\n questionContainer.querySelectorAll(\".accesshide\");\r\n for (const useless of accesshideElements) {\r\n question = question.replace(useless.innerText, \"\");\r\n }\r\n\r\n /* Make tables more readable for chat-gpt */\r\n const tables: NodeListOf =\r\n questionContainer.querySelectorAll(\".qtext table\");\r\n for (const table of tables) {\r\n question = question.replace(\r\n table.innerText,\r\n \"\\n\" + htmlTableToString(table) + \"\\n\"\r\n );\r\n }\r\n\r\n return normalizeText(question, false);\r\n}\r\n\r\nexport default createQuestion;\r\n","import Config from \"../types/config\";\r\nimport GPTAnswer from \"../types/gptAnswer\";\r\nimport normalizeText from \"../utils/normalize-text\";\r\n\r\n/**\r\n * Get the response from chatGPT api\r\n * @param config\r\n * @param question\r\n * @returns\r\n */\r\nasync function getChatGPTResponse(\r\n config: Config,\r\n question: string\r\n): Promise {\r\n const controller = new AbortController();\r\n const timeoutControler = setTimeout(() => controller.abort(), 15000);\r\n const req = await fetch(\"https://api.openai.com/v1/chat/completions\", {\r\n method: \"POST\",\r\n headers: {\r\n \"Content-Type\": \"application/json\",\r\n Authorization: `Bearer ${config.apiKey}`,\r\n },\r\n signal: config.timeout ? controller.signal : null,\r\n body: JSON.stringify({\r\n model: config.model,\r\n messages: [\r\n {\r\n role: \"system\",\r\n content: `\r\nFollow those rules:\r\n- Sometimes there won't be a question, so just answer the statement as you normally would without following the other rules and give the most detailled and complete answer with explication.\r\n- For put in order question just give the good order separate by new line\r\n- Your goal is to understand the statement and to reply to each question by giving only the answer.\r\n- You will keep the same order for the answers like in the text. \r\n- You will separate all the answer with new lines and only show the correctes one.\r\n- You will only give the answers for each question and omit the questions, statement, title or other informations from the response.\r\n- You will only give answer with exactly the same text as the gived answers.\r\n- The question always have the good answer so you should always give an answer to the question.\r\n- You will always respond in the same langage as the user question.`,\r\n },\r\n { role: \"user\", content: question },\r\n ],\r\n temperature: 0.8,\r\n top_p: 1.0,\r\n presence_penalty: 1.0,\r\n stop: null,\r\n }),\r\n });\r\n clearTimeout(timeoutControler);\r\n const rep = await req.json();\r\n const response = rep.choices[0].message.content;\r\n return {\r\n response,\r\n normalizedResponse: normalizeText(response),\r\n };\r\n}\r\n\r\nexport default getChatGPTResponse;\r\n","import Config from \"../types/config\";\r\nimport titleIndications from \"../utils/title-indications\";\r\nimport reply from \"./reply\";\r\n\r\nconst pressedKeys: string[] = [];\r\nconst listeners: {\r\n element: HTMLElement;\r\n fn: (this: HTMLElement, ev: MouseEvent) => any;\r\n}[] = [];\r\n\r\n/**\r\n * Create a listener on the keyboard to inject the code\r\n * @param config\r\n */\r\nfunction codeListener(config: Config) {\r\n document.body.addEventListener(\"keydown\", function (event) {\r\n pressedKeys.push(event.key);\r\n if (pressedKeys.length > config.code.length) pressedKeys.shift();\r\n if (pressedKeys.join(\"\") === config.code) {\r\n pressedKeys.length = 0;\r\n setUpMoodleGpt(config);\r\n }\r\n });\r\n}\r\n\r\n/**\r\n * Setup moodleGPT into the page (remove/injection)\r\n * @param config\r\n * @returns\r\n */\r\nfunction setUpMoodleGpt(config: Config) {\r\n /* Removing events */\r\n if (listeners.length > 0) {\r\n for (const listener of listeners) {\r\n if (config.cursor) listener.element.style.cursor = \"initial\";\r\n listener.element.removeEventListener(\"click\", listener.fn);\r\n }\r\n if (config.title) titleIndications(\"Removed\");\r\n listeners.length = 0;\r\n return;\r\n }\r\n\r\n /* Code injection */\r\n const inputQuery = [\"checkbox\", \"radio\", \"text\", \"number\"]\r\n .map((e) => `input[type=\"${e}\"]`)\r\n .join(\",\");\r\n const query = inputQuery + \", textarea, select, [contenteditable]\";\r\n const forms = document.querySelectorAll(\".formulation\");\r\n\r\n for (const form of forms) {\r\n const hiddenButton: HTMLElement = form.querySelector(\".qtext\");\r\n\r\n if (config.cursor) hiddenButton.style.cursor = \"pointer\";\r\n\r\n const injectionFunction = reply.bind(\r\n null,\r\n config,\r\n hiddenButton,\r\n form,\r\n query\r\n );\r\n listeners.push({ element: hiddenButton, fn: injectionFunction });\r\n hiddenButton.addEventListener(\"click\", injectionFunction);\r\n }\r\n\r\n if (config.title) titleIndications(\"Injected\");\r\n}\r\n\r\n/**\r\n * Remove the event listener on a specific question\r\n * @param element\r\n */\r\nfunction removeListener(element: HTMLElement) {\r\n const index = listeners.findIndex((listener) => listener.element === element);\r\n if (index !== -1) {\r\n const listener = listeners.splice(index, 1)[0];\r\n listener.element.removeEventListener(\"click\", listener.fn);\r\n }\r\n}\r\n\r\nexport { codeListener, removeListener };\r\n","import { codeListener } from \"./core/code-listener\";\r\n\r\nchrome.storage.sync.get([\"moodleGPT\"]).then(function (storage) {\r\n const config = storage.moodleGPT;\r\n\r\n if (!config) throw new Error(\"Please configure MoodleGPT into the extension\");\r\n\r\n codeListener(config);\r\n});\r\n"],"names":["titleIndications","text","backTitle","document","title","setTimeout","__awaiter","thisArg","_arguments","P","generator","Promise","resolve","reject","fulfilled","value","step","next","e","rejected","result","done","then","apply","Logs","static","console","log","valide","css","arr","gptAnswer","response","normalizedResponse","normalizeText","toLowerCase","normalizedText","replace","trim","htmlTableToString","table","tab","lines","Array","from","querySelectorAll","maxColumnsLength","map","line","cellsContent","cell","index","content","_a","textContent","Math","max","length","push","lineSeparationSize","reduce","a","b","lineSeparation","fill","join","mappedTab","padEnd","shift","handleRadioAndCheckbox","config","inputList","input","type","parentNode","includes","logs","responseTry","mouseover","addEventListener","checked","once","handleSelect","tagName","correct","split","array","filter","answer","j","options","option","test","closest","querySelector","indexCorrectAnswer","findIndex","selected","handleTextbox","typing","event","key","preventDefault","slice","handleClipboard","navigator","clipboard","writeText","handleNumber","number","_b","match","handleContentEditable","getAttribute","focus","range","createRange","selectNodeContents","collapse","selection","window","getSelection","removeAllRanges","addRange","reply","hiddenButton","form","query","cursor","style","question","questionContainer","innerText","accesshideElements","useless","tables","createQuestion","controller","AbortController","timeoutControler","abort","req","fetch","method","headers","Authorization","apiKey","signal","timeout","body","JSON","stringify","model","messages","role","temperature","top_p","presence_penalty","stop","clearTimeout","json","choices","message","getChatGPTResponse","catch","error","haveError","infinite","mode","removeListener","questionBackup","whiteSpace","isNotResponse","handlers","handler","pressedKeys","listeners","codeListener","code","listener","element","removeEventListener","fn","forms","injectionFunction","bind","setUpMoodleGpt","splice","chrome","storage","sync","get","moodleGPT","Error"],"mappings":"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","x_google_ignoreList":[1]} \ No newline at end of file +{"version":3,"file":"MoodleGPT.js","sources":["../src/utils/title-indications.ts","../node_modules/tslib/tslib.es6.js","../src/utils/pick-best-response.ts","../src/utils/logs.ts","../src/utils/normalize-text.ts","../src/types/message.ts","../src/core/get-content-with-history.ts","../src/utils/version-support-images.ts","../src/utils/image-to-base64.ts","../src/utils/html-table-to-string.ts","../src/core/questions/clipboard.ts","../src/core/questions/contenteditable.ts","../src/core/questions/number.ts","../src/core/questions/radio.ts","../src/core/questions/checkbox.ts","../src/core/questions/select.ts","../src/core/questions/textbox.ts","../src/core/reply.ts","../src/core/create-question.ts","../src/core/get-response.ts","../src/core/modes/clipboard.ts","../src/core/modes/question-to-answer.ts","../src/core/modes/autocomplete.ts","../src/core/code-listener.ts","../src/index.ts"],"sourcesContent":["/**\n * Show some informations into the document title and remove it after 3000ms\n * @param text\n */\nfunction titleIndications(text: string) {\n const backTitle = document.title;\n document.title = text;\n setTimeout(() => (document.title = backTitle), 3000);\n}\n\nexport default titleIndications;\n","/******************************************************************************\r\nCopyright (c) Microsoft Corporation.\r\n\r\nPermission to use, copy, modify, and/or distribute this software for any\r\npurpose with or without fee is hereby granted.\r\n\r\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\r\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\r\nAND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\r\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\r\nLOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\r\nOTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\r\nPERFORMANCE OF THIS SOFTWARE.\r\n***************************************************************************** */\r\n/* global Reflect, Promise, SuppressedError, Symbol */\r\n\r\nvar extendStatics = function(d, b) {\r\n extendStatics = Object.setPrototypeOf ||\r\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\r\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\r\n return extendStatics(d, b);\r\n};\r\n\r\nexport function __extends(d, b) {\r\n if (typeof b !== \"function\" && b !== null)\r\n throw new TypeError(\"Class extends value \" + String(b) + \" is not a constructor or null\");\r\n extendStatics(d, b);\r\n function __() { this.constructor = d; }\r\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\r\n}\r\n\r\nexport var __assign = function() {\r\n __assign = Object.assign || function __assign(t) {\r\n for (var s, i = 1, n = arguments.length; i < n; i++) {\r\n s = arguments[i];\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];\r\n }\r\n return t;\r\n }\r\n return __assign.apply(this, arguments);\r\n}\r\n\r\nexport function __rest(s, e) {\r\n var t = {};\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)\r\n t[p] = s[p];\r\n if (s != null && typeof Object.getOwnPropertySymbols === \"function\")\r\n for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {\r\n if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))\r\n t[p[i]] = s[p[i]];\r\n }\r\n return t;\r\n}\r\n\r\nexport function __decorate(decorators, target, key, desc) {\r\n var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;\r\n if (typeof Reflect === \"object\" && typeof Reflect.decorate === \"function\") r = Reflect.decorate(decorators, target, key, desc);\r\n else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;\r\n return c > 3 && r && Object.defineProperty(target, key, r), r;\r\n}\r\n\r\nexport function __param(paramIndex, decorator) {\r\n return function (target, key) { decorator(target, key, paramIndex); }\r\n}\r\n\r\nexport function __esDecorate(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {\r\n function accept(f) { if (f !== void 0 && typeof f !== \"function\") throw new TypeError(\"Function expected\"); return f; }\r\n var kind = contextIn.kind, key = kind === \"getter\" ? \"get\" : kind === \"setter\" ? \"set\" : \"value\";\r\n var target = !descriptorIn && ctor ? contextIn[\"static\"] ? ctor : ctor.prototype : null;\r\n var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});\r\n var _, done = false;\r\n for (var i = decorators.length - 1; i >= 0; i--) {\r\n var context = {};\r\n for (var p in contextIn) context[p] = p === \"access\" ? {} : contextIn[p];\r\n for (var p in contextIn.access) context.access[p] = contextIn.access[p];\r\n context.addInitializer = function (f) { if (done) throw new TypeError(\"Cannot add initializers after decoration has completed\"); extraInitializers.push(accept(f || null)); };\r\n var result = (0, decorators[i])(kind === \"accessor\" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);\r\n if (kind === \"accessor\") {\r\n if (result === void 0) continue;\r\n if (result === null || typeof result !== \"object\") throw new TypeError(\"Object expected\");\r\n if (_ = accept(result.get)) descriptor.get = _;\r\n if (_ = accept(result.set)) descriptor.set = _;\r\n if (_ = accept(result.init)) initializers.unshift(_);\r\n }\r\n else if (_ = accept(result)) {\r\n if (kind === \"field\") initializers.unshift(_);\r\n else descriptor[key] = _;\r\n }\r\n }\r\n if (target) Object.defineProperty(target, contextIn.name, descriptor);\r\n done = true;\r\n};\r\n\r\nexport function __runInitializers(thisArg, initializers, value) {\r\n var useValue = arguments.length > 2;\r\n for (var i = 0; i < initializers.length; i++) {\r\n value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);\r\n }\r\n return useValue ? value : void 0;\r\n};\r\n\r\nexport function __propKey(x) {\r\n return typeof x === \"symbol\" ? x : \"\".concat(x);\r\n};\r\n\r\nexport function __setFunctionName(f, name, prefix) {\r\n if (typeof name === \"symbol\") name = name.description ? \"[\".concat(name.description, \"]\") : \"\";\r\n return Object.defineProperty(f, \"name\", { configurable: true, value: prefix ? \"\".concat(prefix, \" \", name) : name });\r\n};\r\n\r\nexport function __metadata(metadataKey, metadataValue) {\r\n if (typeof Reflect === \"object\" && typeof Reflect.metadata === \"function\") return Reflect.metadata(metadataKey, metadataValue);\r\n}\r\n\r\nexport function __awaiter(thisArg, _arguments, P, generator) {\r\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\r\n return new (P || (P = Promise))(function (resolve, reject) {\r\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\r\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\r\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\r\n step((generator = generator.apply(thisArg, _arguments || [])).next());\r\n });\r\n}\r\n\r\nexport function __generator(thisArg, body) {\r\n var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;\r\n return g = { next: verb(0), \"throw\": verb(1), \"return\": verb(2) }, typeof Symbol === \"function\" && (g[Symbol.iterator] = function() { return this; }), g;\r\n function verb(n) { return function (v) { return step([n, v]); }; }\r\n function step(op) {\r\n if (f) throw new TypeError(\"Generator is already executing.\");\r\n while (g && (g = 0, op[0] && (_ = 0)), _) try {\r\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\r\n if (y = 0, t) op = [op[0] & 2, t.value];\r\n switch (op[0]) {\r\n case 0: case 1: t = op; break;\r\n case 4: _.label++; return { value: op[1], done: false };\r\n case 5: _.label++; y = op[1]; op = [0]; continue;\r\n case 7: op = _.ops.pop(); _.trys.pop(); continue;\r\n default:\r\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\r\n if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }\r\n if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }\r\n if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }\r\n if (t[2]) _.ops.pop();\r\n _.trys.pop(); continue;\r\n }\r\n op = body.call(thisArg, _);\r\n } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }\r\n if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };\r\n }\r\n}\r\n\r\nexport var __createBinding = Object.create ? (function(o, m, k, k2) {\r\n if (k2 === undefined) k2 = k;\r\n var desc = Object.getOwnPropertyDescriptor(m, k);\r\n if (!desc || (\"get\" in desc ? !m.__esModule : desc.writable || desc.configurable)) {\r\n desc = { enumerable: true, get: function() { return m[k]; } };\r\n }\r\n Object.defineProperty(o, k2, desc);\r\n}) : (function(o, m, k, k2) {\r\n if (k2 === undefined) k2 = k;\r\n o[k2] = m[k];\r\n});\r\n\r\nexport function __exportStar(m, o) {\r\n for (var p in m) if (p !== \"default\" && !Object.prototype.hasOwnProperty.call(o, p)) __createBinding(o, m, p);\r\n}\r\n\r\nexport function __values(o) {\r\n var s = typeof Symbol === \"function\" && Symbol.iterator, m = s && o[s], i = 0;\r\n if (m) return m.call(o);\r\n if (o && typeof o.length === \"number\") return {\r\n next: function () {\r\n if (o && i >= o.length) o = void 0;\r\n return { value: o && o[i++], done: !o };\r\n }\r\n };\r\n throw new TypeError(s ? \"Object is not iterable.\" : \"Symbol.iterator is not defined.\");\r\n}\r\n\r\nexport function __read(o, n) {\r\n var m = typeof Symbol === \"function\" && o[Symbol.iterator];\r\n if (!m) return o;\r\n var i = m.call(o), r, ar = [], e;\r\n try {\r\n while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);\r\n }\r\n catch (error) { e = { error: error }; }\r\n finally {\r\n try {\r\n if (r && !r.done && (m = i[\"return\"])) m.call(i);\r\n }\r\n finally { if (e) throw e.error; }\r\n }\r\n return ar;\r\n}\r\n\r\n/** @deprecated */\r\nexport function __spread() {\r\n for (var ar = [], i = 0; i < arguments.length; i++)\r\n ar = ar.concat(__read(arguments[i]));\r\n return ar;\r\n}\r\n\r\n/** @deprecated */\r\nexport function __spreadArrays() {\r\n for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;\r\n for (var r = Array(s), k = 0, i = 0; i < il; i++)\r\n for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)\r\n r[k] = a[j];\r\n return r;\r\n}\r\n\r\nexport function __spreadArray(to, from, pack) {\r\n if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {\r\n if (ar || !(i in from)) {\r\n if (!ar) ar = Array.prototype.slice.call(from, 0, i);\r\n ar[i] = from[i];\r\n }\r\n }\r\n return to.concat(ar || Array.prototype.slice.call(from));\r\n}\r\n\r\nexport function __await(v) {\r\n return this instanceof __await ? (this.v = v, this) : new __await(v);\r\n}\r\n\r\nexport function __asyncGenerator(thisArg, _arguments, generator) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var g = generator.apply(thisArg, _arguments || []), i, q = [];\r\n return i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i;\r\n function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; }\r\n function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }\r\n function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }\r\n function fulfill(value) { resume(\"next\", value); }\r\n function reject(value) { resume(\"throw\", value); }\r\n function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }\r\n}\r\n\r\nexport function __asyncDelegator(o) {\r\n var i, p;\r\n return i = {}, verb(\"next\"), verb(\"throw\", function (e) { throw e; }), verb(\"return\"), i[Symbol.iterator] = function () { return this; }, i;\r\n function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: false } : f ? f(v) : v; } : f; }\r\n}\r\n\r\nexport function __asyncValues(o) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var m = o[Symbol.asyncIterator], i;\r\n return m ? m.call(o) : (o = typeof __values === \"function\" ? __values(o) : o[Symbol.iterator](), i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i);\r\n function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }\r\n function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }\r\n}\r\n\r\nexport function __makeTemplateObject(cooked, raw) {\r\n if (Object.defineProperty) { Object.defineProperty(cooked, \"raw\", { value: raw }); } else { cooked.raw = raw; }\r\n return cooked;\r\n};\r\n\r\nvar __setModuleDefault = Object.create ? (function(o, v) {\r\n Object.defineProperty(o, \"default\", { enumerable: true, value: v });\r\n}) : function(o, v) {\r\n o[\"default\"] = v;\r\n};\r\n\r\nexport function __importStar(mod) {\r\n if (mod && mod.__esModule) return mod;\r\n var result = {};\r\n if (mod != null) for (var k in mod) if (k !== \"default\" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);\r\n __setModuleDefault(result, mod);\r\n return result;\r\n}\r\n\r\nexport function __importDefault(mod) {\r\n return (mod && mod.__esModule) ? mod : { default: mod };\r\n}\r\n\r\nexport function __classPrivateFieldGet(receiver, state, kind, f) {\r\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a getter\");\r\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot read private member from an object whose class did not declare it\");\r\n return kind === \"m\" ? f : kind === \"a\" ? f.call(receiver) : f ? f.value : state.get(receiver);\r\n}\r\n\r\nexport function __classPrivateFieldSet(receiver, state, value, kind, f) {\r\n if (kind === \"m\") throw new TypeError(\"Private method is not writable\");\r\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a setter\");\r\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot write private member to an object whose class did not declare it\");\r\n return (kind === \"a\" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;\r\n}\r\n\r\nexport function __classPrivateFieldIn(state, receiver) {\r\n if (receiver === null || (typeof receiver !== \"object\" && typeof receiver !== \"function\")) throw new TypeError(\"Cannot use 'in' operator on non-object\");\r\n return typeof state === \"function\" ? receiver === state : state.has(receiver);\r\n}\r\n\r\nexport function __addDisposableResource(env, value, async) {\r\n if (value !== null && value !== void 0) {\r\n if (typeof value !== \"object\" && typeof value !== \"function\") throw new TypeError(\"Object expected.\");\r\n var dispose;\r\n if (async) {\r\n if (!Symbol.asyncDispose) throw new TypeError(\"Symbol.asyncDispose is not defined.\");\r\n dispose = value[Symbol.asyncDispose];\r\n }\r\n if (dispose === void 0) {\r\n if (!Symbol.dispose) throw new TypeError(\"Symbol.dispose is not defined.\");\r\n dispose = value[Symbol.dispose];\r\n }\r\n if (typeof dispose !== \"function\") throw new TypeError(\"Object not disposable.\");\r\n env.stack.push({ value: value, dispose: dispose, async: async });\r\n }\r\n else if (async) {\r\n env.stack.push({ async: true });\r\n }\r\n return value;\r\n}\r\n\r\nvar _SuppressedError = typeof SuppressedError === \"function\" ? SuppressedError : function (error, suppressed, message) {\r\n var e = new Error(message);\r\n return e.name = \"SuppressedError\", e.error = error, e.suppressed = suppressed, e;\r\n};\r\n\r\nexport function __disposeResources(env) {\r\n function fail(e) {\r\n env.error = env.hasError ? new _SuppressedError(e, env.error, \"An error was suppressed during disposal.\") : e;\r\n env.hasError = true;\r\n }\r\n function next() {\r\n while (env.stack.length) {\r\n var rec = env.stack.pop();\r\n try {\r\n var result = rec.dispose && rec.dispose.call(rec.value);\r\n if (rec.async) return Promise.resolve(result).then(next, function(e) { fail(e); return next(); });\r\n }\r\n catch (e) {\r\n fail(e);\r\n }\r\n }\r\n if (env.hasError) throw env.error;\r\n }\r\n return next();\r\n}\r\n\r\nexport default {\r\n __extends: __extends,\r\n __assign: __assign,\r\n __rest: __rest,\r\n __decorate: __decorate,\r\n __param: __param,\r\n __metadata: __metadata,\r\n __awaiter: __awaiter,\r\n __generator: __generator,\r\n __createBinding: __createBinding,\r\n __exportStar: __exportStar,\r\n __values: __values,\r\n __read: __read,\r\n __spread: __spread,\r\n __spreadArrays: __spreadArrays,\r\n __spreadArray: __spreadArray,\r\n __await: __await,\r\n __asyncGenerator: __asyncGenerator,\r\n __asyncDelegator: __asyncDelegator,\r\n __asyncValues: __asyncValues,\r\n __makeTemplateObject: __makeTemplateObject,\r\n __importStar: __importStar,\r\n __importDefault: __importDefault,\r\n __classPrivateFieldGet: __classPrivateFieldGet,\r\n __classPrivateFieldSet: __classPrivateFieldSet,\r\n __classPrivateFieldIn: __classPrivateFieldIn,\r\n __addDisposableResource: __addDisposableResource,\r\n __disposeResources: __disposeResources,\r\n};\r\n","type BestResponse = {\n similarity: number;\n value: string | null;\n element: HTMLElement | null;\n};\n\ntype ResponsesBySimilarity = {\n similarity: number;\n value: string;\n element: HTMLElement;\n};\n\n/**\n * Calculate the levenshtein distance between two sentence\n * @param str1\n * @param str2\n * @returns\n */\nfunction levenshteinDistance(str1: string, str2: string) {\n if (str1.length === 0) return str2.length;\n if (str2.length === 0) return str1.length;\n\n const matrix: number[][] = [];\n const str1WithoutSpaces = str1.replace(/\\s+/, '');\n const str2WithoutSpaces = str2.replace(/\\s+/, '');\n\n for (let i = 0; i <= str1WithoutSpaces.length; ++i) {\n matrix.push([i]);\n for (let j = 1; j <= str2WithoutSpaces.length; ++j) {\n matrix[i][j] =\n i === 0\n ? j\n : Math.min(\n matrix[i - 1][j] + 1,\n matrix[i][j - 1] + 1,\n matrix[i - 1][j - 1] + (str1WithoutSpaces[i - 1] === str2WithoutSpaces[j - 1] ? 0 : 1)\n );\n }\n }\n\n return matrix[str1WithoutSpaces.length][str2WithoutSpaces.length];\n}\n\n/**\n * Calculate the similarity between two sentences from 0 to 1 (best)\n * @param str1\n * @param str2\n * @returns\n */\nfunction sentenceSimilarity(str1: string, str2: string) {\n const longerLength = str1.length > str2.length ? str1.length : str2.length;\n if (longerLength === 0) return 1;\n return (longerLength - levenshteinDistance(str1, str2)) / longerLength;\n}\n\n/**\n * Pick the best sentence that correspond to the answer\n * @param arr\n * @param answer\n * @returns\n */\nexport function pickBestReponse(\n answer: string,\n arr: { element: HTMLElement; value: string }[]\n): BestResponse {\n let bestResponse: BestResponse = {\n element: null,\n similarity: 0,\n value: null\n };\n for (const obj of arr) {\n const similarity = sentenceSimilarity(obj.value, answer);\n if (similarity === 1) {\n return { element: obj.element, value: obj.value, similarity };\n }\n if (similarity > bestResponse.similarity) {\n bestResponse = { element: obj.element, value: obj.value, similarity };\n }\n }\n return bestResponse;\n}\n\n/**\n * Return the sentences sorted by score with a score superior or equal to what is asked\n * @param answer\n * @param arr\n * @param score\n * @returns\n */\nexport function pickResponsesWithSimilarityGreaterThan(\n answer: string,\n arr: { element: HTMLElement; value: string }[],\n score: number\n): ResponsesBySimilarity[] {\n const responses: ResponsesBySimilarity[] = [];\n for (const obj of arr) {\n const similarity = sentenceSimilarity(obj.value, answer);\n if (similarity >= score)\n responses.push({\n similarity,\n value: obj.value,\n element: obj.element\n });\n }\n return responses.sort((a, b) => a.similarity - b.similarity);\n}\n\n/**\n * Convert a number to a readable string pourcentage\n * @param similarity\n */\nexport function toPourcentage(similarity: number): string {\n return Math.round(similarity * 100 * 100) / 100 + '%';\n}\n","import GPTAnswer from '@typing/gpt-answer';\nimport { toPourcentage } from './pick-best-response';\n\nclass Logs {\n static question(text: string) {\n const css = 'color: cyan';\n console.log('%c[QUESTION]: %s', css, text);\n }\n\n static bestAnswer(answer: string, similarity: number) {\n const css = 'color: green';\n console.log(\n '%c[BEST ANSWER]: %s',\n css,\n `\"${answer}\" with a similarity of ${toPourcentage(similarity)}`\n );\n }\n\n static array(arr: unknown[]) {\n console.log('[CORRECTS] ', arr);\n }\n\n static response(gptAnswer: GPTAnswer) {\n console.log('Original:\\n' + gptAnswer.response);\n console.log('Normalized:\\n' + gptAnswer.normalizedResponse);\n }\n}\n\nexport default Logs;\n","/**\n * Normlize text\n * @param text\n */\nfunction normalizeText(text: string, toLowerCase: boolean = true) {\n if (toLowerCase) text = text.toLowerCase();\n\n const normalizedText = text\n .replace(/\\n+/gi, '\\n') //remove duplicate new lines\n .replace(/(\\n\\s*\\n)+/g, '\\n') //remove useless white space from textcontent\n .replace(/[ \\t]+/gi, ' ') //replace multiples space or tabs by a space\n .trim()\n // We remove the following content because sometimes ChatGPT will reply: \"answer d\"\n .replace(/^[a-z\\d]\\.\\s/gi, '') //a. text, b. text, c. text, 1. text, 2. text, 3.text\n .replace(/\\n[a-z\\d]\\.\\s/gi, '\\n'); //same but with new line\n\n return normalizedText;\n}\n\nexport default normalizeText;\n","export enum ROLE {\r\n SYSTEM = 'system',\r\n USER = 'user',\r\n ASSISTANT = 'assistant'\r\n}\r\n\r\nexport enum CONTENT_TYPE {\r\n TEXT = 'text',\r\n IMAGE = 'image_url'\r\n}\r\n\r\nexport type MessageContent =\r\n | string\r\n | Array<\r\n | {\r\n type: CONTENT_TYPE.TEXT;\r\n text: string;\r\n }\r\n | {\r\n type: CONTENT_TYPE.IMAGE;\r\n image_url: { url: string };\r\n }\r\n >;\r\n\r\nexport type Message = {\r\n role: ROLE;\r\n content: MessageContent;\r\n};\r\n","import type Config from '@typing/config';\r\nimport { ROLE, CONTENT_TYPE, type MessageContent, type Message } from '@typing/message';\r\nimport imageToBase64 from '@utils/image-to-base64';\r\nimport isGPTModelGreaterOrEqualTo4 from '@utils/version-support-images';\r\n\r\n// The attempt and the cmid allow us to identify a quiz\r\ntype History = {\r\n host: string;\r\n cmid: string; // The id of the quiz\r\n attempt: string; // The attempt of the current quiz\r\n history: { role: ROLE; content: MessageContent }[];\r\n};\r\n\r\nconst INSTRUCTION: string = `\r\nAct as a quiz solver for the best notation with the following rules:\r\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. \r\n- But for the calculation provide this format 'result: '\r\n- For 'put in order' questions, maintain the answer in the order as presented in the question but assocy the correct order to it by usin this format ':\\n:', ignore other rules.\r\n- Always reply in the format: '\\n\\n...'.\r\n- Retain only the correct answer(s).\r\n- Maintain the same order for the answers as in the text.\r\n- Retain all text from the answer with its description, content or definition.\r\n- Only provide answers that exactly match the given answer in the text.\r\n- The question always has the correct answer(s), so you should always provide an answer.\r\n- Always respond in the same language as the user's question.\r\n`.trim();\r\n\r\nconst SYSTEM_INSTRUCTION_MESSAGE = {\r\n role: ROLE.SYSTEM,\r\n content: INSTRUCTION\r\n} as const satisfies Message;\r\n\r\n/**\r\n * Get the content to send to ChatGPT API (it allows to includes images if supported)\r\n * @param config\r\n */\r\nasync function getContent(\r\n config: Config,\r\n questionElement: HTMLElement,\r\n question: string\r\n): Promise {\r\n const imagesElements = questionElement.querySelectorAll('img');\r\n\r\n if (\r\n !config.includeImages ||\r\n !isGPTModelGreaterOrEqualTo4(config.model) ||\r\n imagesElements.length === 0\r\n ) {\r\n return question;\r\n }\r\n\r\n const contentWithImages: MessageContent = [];\r\n\r\n const base64Images = Array.from(imagesElements).map(imgEl => imageToBase64(imgEl));\r\n const base64ImagesResolved = await Promise.allSettled(base64Images);\r\n\r\n for (const result of base64ImagesResolved) {\r\n if (result.status === 'fulfilled') {\r\n contentWithImages.push({\r\n type: CONTENT_TYPE.IMAGE,\r\n image_url: { url: result.value }\r\n });\r\n } else if (config.logs) {\r\n console.error(result.reason);\r\n }\r\n }\r\n\r\n contentWithImages.push({\r\n type: CONTENT_TYPE.TEXT,\r\n text: question\r\n });\r\n\r\n return contentWithImages;\r\n}\r\n\r\n/**\r\n * Create a new history object from the current page\r\n * @returns\r\n */\r\nfunction createNewHistory(): History {\r\n const urlParams = new URLSearchParams(document.location.search);\r\n\r\n return {\r\n host: document.location.host,\r\n cmid: urlParams.get('cmid') ?? '',\r\n attempt: urlParams.get('attempt') ?? '',\r\n history: []\r\n };\r\n}\r\n\r\n/**\r\n * Load the past history from the session storage otherwise return the default history object\r\n * @returns\r\n */\r\nfunction loadPastHistory(): History | null {\r\n return JSON.parse(sessionStorage.moodleGPTHistory ?? 'null');\r\n}\r\n\r\n/**\r\n * Check if two history are from the same origin\r\n * @param a\r\n * @param b\r\n * @returns\r\n */\r\nfunction areHistoryFromSameQuiz(a: History, b: History): boolean {\r\n const KEYS_TO_COMPARE: (keyof History)[] = ['host', 'cmid', 'attempt'];\r\n\r\n for (const key of KEYS_TO_COMPARE) {\r\n if (a[key] !== b[key]) return false;\r\n }\r\n\r\n return true;\r\n}\r\n\r\n/**\r\n * Return the content to send to chatgpt api with history if needed\r\n * @param config\r\n * @param questionElement\r\n * @param question\r\n * @returns\r\n */\r\nasync function getContentWithHistory(\r\n config: Config,\r\n questionElement: HTMLElement,\r\n question: string\r\n): Promise<{\r\n messages: [typeof SYSTEM_INSTRUCTION_MESSAGE, ...Message[]];\r\n saveResponse?: (response: string) => void;\r\n}> {\r\n const content = await getContent(config, questionElement, question);\r\n const message = { role: ROLE.USER, content };\r\n\r\n if (!config.history) return { messages: [SYSTEM_INSTRUCTION_MESSAGE, message] };\r\n\r\n let history: History;\r\n\r\n const pastHistory: History | null = loadPastHistory();\r\n const newHistory: History = createNewHistory();\r\n\r\n if (pastHistory === null || !areHistoryFromSameQuiz(pastHistory, newHistory)) {\r\n history = newHistory;\r\n } else {\r\n history = pastHistory;\r\n }\r\n\r\n return {\r\n messages: [SYSTEM_INSTRUCTION_MESSAGE, ...history.history, message],\r\n saveResponse(response: string) {\r\n // Register the conversation\r\n if (config.history) {\r\n history.history.push(message);\r\n history.history.push({ role: ROLE.ASSISTANT, content: response });\r\n sessionStorage.moodleGPTHistory = JSON.stringify(history);\r\n }\r\n }\r\n };\r\n}\r\n\r\nexport default getContentWithHistory;\r\n","/**\r\n * Check if the current ChatGPT version is greater or equal to 4\r\n * @param version\r\n * @returns\r\n */\r\nfunction isGPTModelGreaterOrEqualTo4(version: string): boolean {\r\n const versionNumber = version.match(/gpt-(\\d+)/);\r\n if (!versionNumber?.[1]) {\r\n return false;\r\n }\r\n return Number(versionNumber[1]) >= 4;\r\n}\r\n\r\nexport default isGPTModelGreaterOrEqualTo4;\r\n","/**\r\n * Convert an image html element into a base64 image string\r\n * @param imageElement\r\n * @param quality (default: 0.75 -> 75%)\r\n * @returns\r\n */\r\nfunction imageToBase64(imageElement: HTMLImageElement, quality = 0.75): Promise {\r\n return new Promise((resolve, reject) => {\r\n const canvas = document.createElement('canvas');\r\n const ctx = canvas.getContext('2d');\r\n\r\n if (!ctx) {\r\n reject(\"Can't get the canvas context, ensure your navigator support canvas\");\r\n canvas.remove();\r\n return;\r\n }\r\n\r\n const img = new Image();\r\n img.crossOrigin = 'Anonymous';\r\n img.onload = () => {\r\n canvas.width = img.width;\r\n canvas.height = img.height;\r\n ctx.drawImage(img, 0, 0);\r\n\r\n const base64 = canvas.toDataURL('image/png', quality);\r\n resolve(base64);\r\n\r\n canvas.remove();\r\n };\r\n\r\n img.onerror = err => {\r\n reject(err);\r\n canvas.remove();\r\n };\r\n\r\n img.src = imageElement.src;\r\n });\r\n}\r\n\r\nexport default imageToBase64;\r\n","/**\n * Convert table to representating string table\n * @param table\n * @returns\n */\nfunction htmlTableToString(table: HTMLTableElement) {\n const tab: string[][] = [];\n const lines = Array.from(table.querySelectorAll('tr'));\n const maxColumnsLength: number[] = [];\n\n lines.map(line => {\n const cells = Array.from(line.querySelectorAll('td, th'));\n const cellsContent = cells.map((cell, index) => {\n const content = cell.textContent?.trim();\n maxColumnsLength[index] = Math.max(maxColumnsLength[index] || 0, content?.length || 0);\n return content ?? '';\n });\n tab.push(cellsContent);\n });\n\n const jointure = ' | ';\n const headerLineLength = tab[0].length;\n const lineSeparationSize =\n maxColumnsLength.reduce((a, b) => a + b, 0) + (headerLineLength - 1) * jointure.length;\n const lineSeparation = '\\n' + Array(lineSeparationSize).fill('-').join('') + '\\n';\n\n const mappedTab = tab.map(line => {\n const mappedLine = line.map((content, index) =>\n content.padEnd(\n maxColumnsLength[index],\n '\\u00A0' // For no matching with \\s\n )\n );\n return mappedLine.join(jointure);\n });\n\n const head = mappedTab.shift();\n\n return head + lineSeparation + mappedTab.join('\\n');\n}\n\nexport default htmlTableToString;\n","import type Config from '@typing/config';\nimport type GPTAnswer from '@typing/gpt-answer';\nimport titleIndications from '@utils/title-indications';\n\n/**\n * Copy the response in the clipboard if we can automaticaly fill the question\n * @param config\n * @param gptAnswer\n */\nfunction handleClipboard(config: Config, gptAnswer: GPTAnswer) {\n if (config.title) titleIndications('Copied to clipboard');\n navigator.clipboard.writeText(gptAnswer.response);\n}\n\nexport default handleClipboard;\n","import type Config from '@typing/config';\nimport type GPTAnswer from '@typing/gpt-answer';\n\n/**\n * Hanlde contenteditable elements\n * @param config\n * @param inputList\n * @param gptAnswer\n * @returns\n */\nfunction handleContentEditable(\n config: Config,\n inputList: NodeListOf,\n gptAnswer: GPTAnswer\n): boolean {\n const input = inputList[0];\n\n if (\n inputList.length !== 1 || // for now we don't handle many input for editable textcontent\n input.getAttribute('contenteditable') !== 'true'\n ) {\n return false;\n }\n\n if (config.typing) {\n let index = 0;\n input.addEventListener('keydown', function (event: KeyboardEvent) {\n if (event.key === 'Backspace') index = gptAnswer.response.length + 1;\n if (index > gptAnswer.response.length) return;\n event.preventDefault();\n input.textContent = gptAnswer.response.slice(0, ++index);\n\n // Put the cursor at the end of the typed text\n input.focus();\n const range = document.createRange();\n range.selectNodeContents(input);\n range.collapse(false);\n const selection = window.getSelection();\n if (selection !== null) {\n selection.removeAllRanges();\n selection.addRange(range);\n }\n });\n } else {\n input.textContent = gptAnswer.response;\n }\n\n return true;\n}\n\nexport default handleContentEditable;\n","import type Config from '@typing/config';\nimport type GPTAnswer from '@typing/gpt-answer';\n\n/**\n * Handle number input\n * @param config\n * @param inputList\n * @param gptAnswer\n * @returns\n */\nfunction handleNumber(\n config: Config,\n inputList: NodeListOf,\n gptAnswer: GPTAnswer\n): boolean {\n const input = inputList[0] as HTMLInputElement | HTMLTextAreaElement;\n\n if (\n inputList.length !== 1 || // for now we don't handle many input number\n input.type !== 'number'\n ) {\n return false;\n }\n\n const number = gptAnswer.normalizedResponse.match(/\\d+([,.]\\d+)?/gi)?.[0]?.replace(',', '.');\n\n if (number === undefined) return false;\n\n if (config.typing) {\n let index = 0;\n input.addEventListener('keydown', function (event: Event) {\n event.preventDefault();\n if ((event).key === 'Backspace') index = number.length + 1;\n if (index > number.length) return;\n if (number.slice(index, index + 1) === '.') ++index;\n input.value = number.slice(0, ++index);\n });\n } else {\n input.value = number;\n }\n\n return true;\n}\n\nexport default handleNumber;\n","import type Config from '@typing/config';\nimport type GPTAnswer from '@typing/gpt-answer';\nimport Logs from '@utils/logs';\nimport normalizeText from '@utils/normalize-text';\nimport { pickBestReponse } from '@utils/pick-best-response';\n\n/**\n * Handle input radio elements\n * @param config\n * @param inputList\n * @param gptAnswer\n */\nfunction handleRadio(\n config: Config,\n inputList: NodeListOf,\n gptAnswer: GPTAnswer\n): boolean {\n const firstInput = inputList?.[0] as HTMLInputElement;\n\n // Handle the case the input is not a radio\n if (!firstInput || firstInput.type !== 'radio') {\n return false;\n }\n\n const possibleAnswers = Array.from(inputList)\n .map(inp => ({\n element: inp,\n value: normalizeText(inp?.parentElement?.textContent ?? '')\n }))\n .filter(obj => obj.value !== '');\n\n const bestAnswer = pickBestReponse(gptAnswer.normalizedResponse, possibleAnswers);\n\n if (config.logs && bestAnswer.value) {\n Logs.bestAnswer(bestAnswer.value, bestAnswer.similarity);\n }\n\n const correctInput = bestAnswer.element as HTMLInputElement;\n if (config.mouseover) {\n correctInput.addEventListener('mouseover', () => correctInput.click(), {\n once: true\n });\n } else {\n correctInput.click();\n }\n\n return true;\n}\n\nexport default handleRadio;\n","import type Config from '@typing/config';\nimport type GPTAnswer from '@typing/gpt-answer';\nimport Logs from '@utils/logs';\nimport normalizeText from '@utils/normalize-text';\nimport { pickBestReponse } from '@utils/pick-best-response';\n\n/**\n * Handle input checkbox elements\n * @param config\n * @param inputList\n * @param gptAnswer\n */\nfunction handleCheckbox(\n config: Config,\n inputList: NodeListOf,\n gptAnswer: GPTAnswer\n): boolean {\n const firstInput = inputList?.[0] as HTMLInputElement;\n\n // Handle the case the input is not a checkbox\n if (!firstInput || firstInput.type !== 'checkbox') {\n return false;\n }\n\n const corrects = gptAnswer.normalizedResponse.split('\\n');\n\n const possibleAnswers = Array.from(inputList)\n .map(inp => ({\n element: inp,\n value: normalizeText(inp?.parentElement?.textContent ?? '')\n }))\n .filter(obj => obj.value !== '');\n\n for (const correct of corrects) {\n const bestAnswer = pickBestReponse(correct, possibleAnswers);\n\n if (config.logs && bestAnswer.value) {\n Logs.bestAnswer(bestAnswer.value, bestAnswer.similarity);\n }\n\n const correctInput = bestAnswer.element as HTMLInputElement;\n if (config.mouseover) {\n correctInput.addEventListener('mouseover', () => correctInput.click(), {\n once: true\n });\n } else {\n correctInput.click();\n }\n }\n\n return true;\n}\n\nexport default handleCheckbox;\n","import type Config from '@typing/config';\nimport type GPTAnswer from '@typing/gpt-answer';\nimport Logs from '@utils/logs';\nimport normalizeText from '@utils/normalize-text';\nimport { pickBestReponse } from '@utils/pick-best-response';\n\n/**\n * Handle select elements (and put in order select)\n * @param config\n * @param inputList\n * @param gptAnswer\n * @returns\n */\nfunction handleSelect(\n config: Config,\n inputList: NodeListOf,\n gptAnswer: GPTAnswer\n): boolean {\n if (inputList.length === 0 || inputList[0].tagName !== 'SELECT') return false;\n\n const corrects = gptAnswer.normalizedResponse.split('\\n');\n\n if (config.logs) Logs.array(corrects);\n\n for (let i = 0; i < inputList.length; ++i) {\n if (!corrects[i]) break;\n\n const options = inputList[i].querySelectorAll('option');\n\n const possibleAnswers = Array.from(options)\n .slice(1) // We remove the first option which correspond to \"Choose...\"\n .map(opt => ({\n element: opt,\n value: normalizeText(opt.textContent ?? '')\n }))\n .filter(obj => obj.value !== '');\n\n const bestAnswer = pickBestReponse(corrects[i], possibleAnswers);\n\n if (config.logs && bestAnswer.value) {\n Logs.bestAnswer(bestAnswer.value, bestAnswer.similarity);\n }\n\n const correctOption = bestAnswer.element as HTMLOptionElement;\n const currentSelect = correctOption.closest('select');\n\n if (currentSelect === null) continue;\n\n if (config.mouseover) {\n currentSelect.addEventListener('click', () => (correctOption.selected = true), {\n once: true\n });\n } else {\n correctOption.selected = true;\n }\n }\n\n return true;\n}\n\nexport default handleSelect;\n","import type Config from '@typing/config';\nimport type GPTAnswer from '@typing/gpt-answer';\n\n/**\n * Handle textbox\n * @param config\n * @param inputList\n * @param gptAnswer\n * @returns\n */\nfunction handleTextbox(\n config: Config,\n inputList: NodeListOf,\n gptAnswer: GPTAnswer\n): boolean {\n const input = inputList[0] as HTMLInputElement | HTMLTextAreaElement;\n\n if (\n inputList.length !== 1 || // for now we don't handle many input text\n (input.tagName !== 'TEXTAREA' && input.type !== 'text')\n ) {\n return false;\n }\n\n if (config.typing) {\n let index = 0;\n input.addEventListener('keydown', function (event: Event) {\n event.preventDefault();\n if ((event).key === 'Backspace') {\n index = gptAnswer.response.length + 1;\n }\n if (index > gptAnswer.response.length) return;\n input.value = gptAnswer.response.slice(0, ++index);\n });\n } else {\n input.value = gptAnswer.response;\n }\n\n return true;\n}\n\nexport default handleTextbox;\n","import type Config from '@typing/config';\nimport Logs from '@utils/logs';\nimport getChatGPTResponse from './get-response';\nimport createAndNormalizeQuestion from './create-question';\nimport clipboardMode from './modes/clipboard';\nimport questionToAnswerMode from './modes/question-to-answer';\nimport autoCompleteMode from './modes/autocomplete';\n\ntype Props = {\n config: Config;\n questionElement: HTMLElement;\n form: HTMLElement;\n inputQuery: string;\n removeListener: () => void;\n};\n\n/**\n * Reply to the question\n * @param props\n * @returns\n */\nasync function reply(props: Props): Promise {\n if (props.config.cursor) props.questionElement.style.cursor = 'wait';\n\n const question = createAndNormalizeQuestion(props.form);\n const inputList: NodeListOf = props.form.querySelectorAll(props.inputQuery);\n\n const gptAnswer = await getChatGPTResponse(props.config, props.questionElement, question).catch(\n error => ({\n error\n })\n );\n\n const haveError = typeof gptAnswer === 'object' && 'error' in gptAnswer;\n\n if (props.config.cursor) {\n props.questionElement.style.cursor = props.config.infinite || haveError ? 'pointer' : 'initial';\n }\n\n if (haveError) {\n console.error(gptAnswer.error);\n return;\n }\n\n if (props.config.logs) {\n Logs.question(question);\n Logs.response(gptAnswer);\n }\n\n switch (props.config.mode) {\n case 'clipboard':\n clipboardMode({\n config: props.config,\n questionElement: props.questionElement,\n gptAnswer,\n removeListener: props.removeListener\n });\n break;\n case 'question-to-answer':\n questionToAnswerMode({\n gptAnswer,\n questionElement: props.questionElement,\n removeListener: props.removeListener\n });\n break;\n case 'autocomplete':\n autoCompleteMode({\n config: props.config,\n gptAnswer,\n inputList,\n questionElement: props.questionElement,\n removeListener: props.removeListener\n });\n break;\n }\n}\n\nexport default reply;\n","import normalizeText from '@utils/normalize-text';\nimport htmlTableToString from '@utils/html-table-to-string';\n\n/**\n * Normalize the question as text and add sub informations\n * @param langage\n * @param question\n * @returns\n */\nfunction createAndNormalizeQuestion(questionContainer: HTMLElement) {\n let question = questionContainer.innerText;\n\n // We remove unnecessary information\n const accesshideElements: NodeListOf =\n questionContainer.querySelectorAll('.accesshide');\n for (const useless of accesshideElements) {\n question = question.replace(useless.innerText, '');\n }\n\n // Make tables more readable for chat-gpt\n const tables: NodeListOf = questionContainer.querySelectorAll('.qtext table');\n for (const table of tables) {\n question = question.replace(table.innerText, '\\n' + htmlTableToString(table) + '\\n');\n }\n\n return normalizeText(question, false);\n}\n\nexport default createAndNormalizeQuestion;\n","import type Config from '@typing/config';\nimport type GPTAnswer from '@typing/gpt-answer';\nimport normalizeText from '@utils/normalize-text';\nimport getContentWithHistory from './get-content-with-history';\n\n/**\n * Get the response from chatGPT api\n * @param config\n * @param question\n * @returns\n */\nasync function getChatGPTResponse(\n config: Config,\n questionElement: HTMLElement,\n question: string\n): Promise {\n const controller = new AbortController();\n const timeoutControler = setTimeout(() => controller.abort(), 20 * 1000);\n\n // Get the content to send to chatgpt\n // Including the instructions to the AI, the images as base64 if needed, the question and the past conversation if history is set to true\n const contentHandler = await getContentWithHistory(config, questionElement, question);\n\n const req = await fetch('https://api.openai.com/v1/chat/completions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${config.apiKey}`\n },\n signal: config.timeout ? controller.signal : null,\n body: JSON.stringify({\n model: config.model,\n messages: contentHandler.messages,\n\n temperature: 0.1, // Controls the randomness of the generated responses, with lower values producing more deterministic and predictable outputs. With set to 0.1 instead of 0 for more creativity.\n top_p: 0.6, // Determines the diversity of the generated responses\n presence_penalty: 0, // Encourages the model to introduce new concepts by penalizing words that have already appeared in the text.\n max_tokens: 2000 // Maximum length of the response\n })\n });\n\n clearTimeout(timeoutControler);\n\n const rep = await req.json();\n const response = rep.choices[0].message.content;\n\n // Save the response into the history\n if (typeof contentHandler.saveResponse === 'function') contentHandler.saveResponse(response);\n\n return {\n question,\n response,\n normalizedResponse: normalizeText(response)\n };\n}\n\nexport default getChatGPTResponse;\n","import type Config from '@typing/config';\nimport type GPTAnswer from '@typing/gpt-answer';\nimport handleClipboard from '@core/questions/clipboard';\n\ntype Props = {\n config: Config;\n questionElement: HTMLElement;\n gptAnswer: GPTAnswer;\n removeListener: () => void;\n};\n\n/**\n * Clipboard mode:\n * Simply copy the answer into the clipboard\n * @param props\n */\nfunction clipboardMode(props: Props) {\n if (!props.config.infinite) props.removeListener();\n handleClipboard(props.config, props.gptAnswer);\n}\n\nexport default clipboardMode;\n","import type GPTAnswer from '@typing/gpt-answer';\n\ntype Props = {\n questionElement: HTMLElement;\n gptAnswer: GPTAnswer;\n removeListener: () => void;\n};\n\n/**\n * Question to answer mode:\n * Simply turn the question into the answer by clicking on it\n * @param props\n */\nfunction questionToAnswerMode(props: Props) {\n const questionElement = props.questionElement;\n\n props.removeListener();\n\n const questionBackup = questionElement.innerHTML ?? '';\n questionElement.innerHTML = props.gptAnswer.response;\n questionElement.style.whiteSpace = 'pre-wrap';\n\n // To go back to the question / answer\n questionElement.addEventListener('click', function () {\n const contentIsResponse = questionElement.innerHTML === props.gptAnswer.response;\n\n questionElement.style.whiteSpace = contentIsResponse ? 'initial' : 'pre-wrap';\n questionElement.innerHTML = contentIsResponse ? questionBackup : props.gptAnswer.response;\n });\n}\n\nexport default questionToAnswerMode;\n","import type GPTAnswer from '@typing/gpt-answer';\nimport type Config from '@typing/config';\nimport handleClipboard from '@core/questions/clipboard';\nimport handleContentEditable from '@core/questions/contenteditable';\nimport handleNumber from '@core/questions/number';\nimport handleRadio from '@core/questions/radio';\nimport handleCheckbox from '@core/questions/checkbox';\nimport handleSelect from '@core/questions/select';\nimport handleTextbox from '@core/questions/textbox';\n\ntype Props = {\n config: Config;\n questionElement: HTMLElement;\n inputList: NodeListOf;\n gptAnswer: GPTAnswer;\n removeListener: () => void;\n};\n\n/**\n * Autocomplete mode:\n * Autocomplete the question by checking the good answer\n * @param props\n * @returns\n */\nfunction autoCompleteMode(props: Props) {\n if (!props.config.infinite) props.removeListener();\n\n const handlers = [\n handleContentEditable,\n handleTextbox,\n handleNumber,\n handleSelect,\n handleRadio,\n handleCheckbox\n ];\n\n for (const handler of handlers) {\n if (handler(props.config, props.inputList, props.gptAnswer)) return;\n }\n\n // In the case we can't auto complete the question\n handleClipboard(props.config, props.gptAnswer);\n}\n\nexport default autoCompleteMode;\n","import type Config from '@typing/config';\nimport titleIndications from '@utils/title-indications';\nimport reply from './reply';\n\ntype Listener = {\n element: HTMLElement;\n fn: (this: HTMLElement, ev: MouseEvent) => void;\n};\n\nconst pressedKeys: string[] = [];\nconst listeners: Listener[] = [];\n\n/**\n * Create a listener on the keyboard to inject the code\n * @param config\n */\nfunction codeListener(config: Config) {\n document.body.addEventListener('keydown', function (event) {\n pressedKeys.push(event.key);\n if (pressedKeys.length > config.code!.length) pressedKeys.shift();\n if (pressedKeys.join('') === config.code) {\n pressedKeys.length = 0;\n setUpMoodleGpt(config);\n }\n });\n}\n\n/**\n * Remove the event listener on a specific question\n * @param element\n */\nfunction removeListener(element: HTMLElement) {\n const index = listeners.findIndex(listener => listener.element === element);\n if (index !== -1) {\n const listener = listeners.splice(index, 1)[0];\n listener.element.removeEventListener('click', listener.fn);\n }\n}\n\n/**\n * Setup moodleGPT into the page (remove/injection)\n * @param config\n * @returns\n */\nfunction setUpMoodleGpt(config: Config) {\n // Removing events if there are already declared\n if (listeners.length > 0) {\n for (const listener of listeners) {\n if (config.cursor) listener.element.style.cursor = 'initial';\n listener.element.removeEventListener('click', listener.fn);\n }\n if (config.title) titleIndications('Removed');\n listeners.length = 0;\n return;\n }\n\n // Query to find inputs and forms\n const inputTypeQuery = ['checkbox', 'radio', 'text', 'number']\n .map(e => `input[type=\"${e}\"]`)\n .join(',');\n const inputQuery = inputTypeQuery + ', textarea, select, [contenteditable]';\n const forms = document.querySelectorAll('.formulation');\n\n // For each form we inject a function on the queqtion\n for (const form of forms) {\n const questionElement: HTMLElement | null = form.querySelector('.qtext');\n\n if (questionElement === null) continue;\n\n if (config.cursor) questionElement.style.cursor = 'pointer';\n\n const injectionFunction = reply.bind(null, {\n config,\n questionElement,\n form: form as HTMLElement,\n inputQuery,\n removeListener: () => removeListener(questionElement)\n });\n\n listeners.push({ element: questionElement, fn: injectionFunction });\n questionElement.addEventListener('click', injectionFunction);\n }\n\n if (config.title) titleIndications('Injected');\n}\n\nexport { codeListener, removeListener, setUpMoodleGpt };\n","import type Config from '@typing/config';\nimport { codeListener, setUpMoodleGpt } from './core/code-listener';\n\nchrome.storage.sync.get(['moodleGPT']).then(function (storage) {\n const config: Config = storage.moodleGPT;\n\n if (!config) throw new Error('Please configure MoodleGPT into the extension');\n\n if (config.code) {\n codeListener(config);\n } else {\n setUpMoodleGpt(config);\n }\n});\n"],"names":["titleIndications","text","backTitle","document","title","setTimeout","__awaiter","thisArg","_arguments","P","generator","Promise","resolve","reject","fulfilled","value","step","next","e","rejected","result","done","then","apply","sentenceSimilarity","str1","str2","longerLength","length","matrix","str1WithoutSpaces","replace","str2WithoutSpaces","i","push","j","Math","min","levenshteinDistance","pickBestReponse","answer","arr","bestResponse","element","similarity","obj","SuppressedError","Logs","question","console","log","bestAnswer","round","toPourcentage","array","response","gptAnswer","normalizedResponse","normalizeText","toLowerCase","trim","ROLE","CONTENT_TYPE","INSTRUCTION","SYSTEM_INSTRUCTION_MESSAGE","role","SYSTEM","content","getContent","config","questionElement","imagesElements","querySelectorAll","includeImages","version","versionNumber","match","Number","isGPTModelGreaterOrEqualTo4","model","contentWithImages","base64Images","Array","from","map","imgEl","imageElement","quality","canvas","createElement","ctx","getContext","remove","img","Image","crossOrigin","onload","width","height","drawImage","base64","toDataURL","onerror","err","src","imageToBase64","base64ImagesResolved","allSettled","status","type","IMAGE","image_url","url","logs","error","reason","TEXT","getContentWithHistory","message","USER","history","messages","pastHistory","JSON","parse","_a","sessionStorage","moodleGPTHistory","newHistory","urlParams","URLSearchParams","location","search","host","cmid","get","attempt","_b","createNewHistory","a","b","KEYS_TO_COMPARE","key","areHistoryFromSameQuiz","saveResponse","ASSISTANT","stringify","htmlTableToString","table","tab","lines","maxColumnsLength","line","cellsContent","cell","index","textContent","max","headerLineLength","lineSeparationSize","reduce","jointure","lineSeparation","fill","join","mappedTab","padEnd","shift","handleClipboard","navigator","clipboard","writeText","handleContentEditable","inputList","input","getAttribute","typing","addEventListener","event","preventDefault","slice","focus","range","createRange","selectNodeContents","collapse","selection","window","getSelection","removeAllRanges","addRange","handleNumber","number","undefined","handleRadio","firstInput","possibleAnswers","inp","parentElement","filter","correctInput","mouseover","click","once","handleCheckbox","corrects","split","correct","handleSelect","tagName","options","opt","correctOption","currentSelect","closest","selected","handleTextbox","reply","props","cursor","style","questionContainer","innerText","accesshideElements","useless","tables","createAndNormalizeQuestion","form","inputQuery","controller","AbortController","timeoutControler","abort","contentHandler","req","fetch","method","headers","Authorization","apiKey","signal","timeout","body","temperature","top_p","presence_penalty","max_tokens","clearTimeout","json","choices","getChatGPTResponse","catch","haveError","infinite","mode","removeListener","clipboardMode","questionBackup","innerHTML","whiteSpace","contentIsResponse","questionToAnswerMode","handlers","handler","autoCompleteMode","pressedKeys","listeners","findIndex","listener","splice","removeEventListener","fn","setUpMoodleGpt","forms","querySelector","injectionFunction","bind","chrome","storage","sync","moodleGPT","Error","code","codeListener"],"mappings":"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","x_google_ignoreList":[1]} \ No newline at end of file diff --git a/extension/manifest.json b/extension/manifest.json index 511db99..cd82460 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -1,26 +1,26 @@ -{ - "manifest_version": 3, - "name": "MoodleGPT", - "version": "1.0.3", - "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.1.0", + "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/attempt.php*", "*://*/mod/quiz/attempt.php*", "file:///*"], + "js": ["MoodleGPT.js"], + "run_at": "document_end" + } + ] +} diff --git a/extension/popup/index.html b/extension/popup/index.html index caa943c..3871c91 100644 --- a/extension/popup/index.html +++ b/extension/popup/index.html @@ -1,138 +1,149 @@ - - - - - - - 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 new file mode 100644 index 0000000..703f1ec --- /dev/null +++ b/extension/popup/js/gptVersion.js @@ -0,0 +1,54 @@ +'use strict'; + +const apiKeySelector = document.querySelector('#apiKey'); +const inputModel = document.querySelector('#model'); +const modelsList = document.querySelector('#models'); +const imagesIntegrationLine = document.querySelector('#includeImages-line'); + +/** + * Check if the gpt version is at least 4 to show the option 'Include images' + */ +function checkCanIncludeImages() { + const version = inputModel.value; + if (isCurrentVersionSupportingImages(version)) { + imagesIntegrationLine.style.display = 'flex'; + } else { + imagesIntegrationLine.style.display = 'none'; + } +} + +inputModel.addEventListener('input', checkCanIncludeImages); + +// We populate the datalist of the chatgpt model +async function populateDatalistWithGptVersions() { + const apiKey = apiKeySelector.value?.trim(); + + if (!apiKey) return; + + inputModel.innerHTML = ''; + + try { + const req = await fetch('https://api.openai.com/v1/models', { + headers: { + Authorization: `Bearer ${apiKey}` + } + }); + const rep = await req.json(); + rep.data.sort((a, b) => b.id.localeCompare(a.id)); // we sort the model to get the best chatgpt version first + const models = rep.data.filter(model => model.id.startsWith('gpt')); + + for (const model of models) { + const opt = document.createElement('option'); + opt.value = model.id; + opt.textContent = model.id; + modelsList.appendChild(opt); + } + + checkCanIncludeImages(); + } catch (err) { + console.error(err); + showMessage({ msg: 'Failed to fetch last ChatGPT versions', error: true }); + } +} + +inputModel.addEventListener('focus', populateDatalistWithGptVersions); diff --git a/extension/popup/js/index.js b/extension/popup/js/index.js index 304002e..5b69638 100644 --- a/extension/popup/js/index.js +++ b/extension/popup/js/index.js @@ -1,90 +1,42 @@ -const saveBtn = document.querySelector(".save"); -const message = document.querySelector("#message"); +'use strict'; -/* inputs id */ -const inputsText = ["apiKey", "code", "model"]; +const saveBtn = document.querySelector('.save'); + +// inputs id +const inputsText = ['apiKey', 'code', 'model']; const inputsCheckbox = [ - "logs", - "title", - "cursor", - "typing", - "mouseover", - "infinite", - "timeout", + 'logs', + 'title', + 'cursor', + 'typing', + 'mouseover', + 'infinite', + 'timeout', + 'history', + 'includeImages' ]; -const mode = document.querySelector("#mode"); -const modes = mode.querySelectorAll("button"); -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"], -}; - -/** - * Show message into the popup - * @param {string} messageTxt - * @param {boolean} valide - */ -function showMessage(messageTxt, valide) { - message.style.color = valide ? "limegreen" : "red"; - message.textContent = messageTxt; - message.style.display = "block"; - setTimeout(() => (message.style.display = "none"), 5000); -} - -/** - * Handle when a mode change to show specific input - */ -function handleModeChange() { - const needDisable = disabledForThisMode[actualMode]; - const dontNeedDisable = inputsCheckbox.filter( - (input) => !needDisable.includes(input) +// Save the configuration +saveBtn.addEventListener('click', function () { + const [apiKey, code, model] = inputsText.map(selector => + document.querySelector('#' + selector).value.trim() ); - for (const id of needDisable) { - document.querySelector("#" + id).parentElement.style.display = "none"; - } - for (const id of dontNeedDisable) { - document.querySelector("#" + id).parentElement.style.display = null; - } -} - -/* Mode handler */ -modes.forEach((button) => { - button.addEventListener("click", function () { - const value = button.value; - actualMode = value; - for (const mode of modes) { - if (mode.value !== value) { - mode.classList.add("not-selected"); - } else { - mode.classList.remove("not-selected"); - } - } - handleModeChange(); - }); -}); - -/* Save the configuration */ -saveBtn.addEventListener("click", function () { - const [apiKey, code, model] = inputsText.map((selector) => - document.querySelector("#" + selector).value.trim() - ); - const [logs, title, cursor, typing, mouseover, infinite, timeout] = - 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, includeImages] = + inputsCheckbox.map(selector => { + const element = document.querySelector('#' + selector); + return element.checked && element.parentElement.style.display !== 'none'; }); - if (!apiKey || !code || !model) { - showMessage("Please complete all the form"); + if (!apiKey || !model) { + showMessage({ msg: 'Please complete all the form', error: true }); return; } - if (code.length < 3) { - showMessage("The code should at least contain 3 characters"); + if (code.length > 0 && code.length < 3) { + showMessage({ + msg: 'The code should at least contain 3 characters', + error: true + }); return; } @@ -100,60 +52,17 @@ saveBtn.addEventListener("click", function () { mouseover, infinite, timeout, - mode: actualMode, - }, + history, + includeImages, + mode: actualMode + } }); - showMessage("Configuration saved", true); + showMessage({ msg: 'Configuration saved' }); }); -/** - * Get the last ChatGPT version - */ -function getLastChatGPTVersion() { - const apiKeySelector = document.querySelector("#apiKey"); - const reloadModel = document.querySelector("#reloadModel"); - - let apiKey = apiKeySelector.value; - - function checkFiledApiKey() { - if (apiKey) { - reloadModel.removeAttribute("disabled"); - reloadModel.setAttribute("title", "Get last ChatGPT version"); - return; - } - - reloadModel.setAttribute("disabled", true); - reloadModel.setAttribute("title", "Provide an api key first"); - } - - checkFiledApiKey(); - - apiKeySelector.addEventListener("input", function () { - apiKey = apiKeySelector.value.trim(); - checkFiledApiKey(); - }); - - reloadModel.addEventListener("click", async function () { - if (!apiKey) return; - try { - const req = await fetch("https://api.openai.com/v1/models", { - headers: { - Authorization: `Bearer ${apiKey}`, - }, - }); - const rep = await req.json(); - const model = rep.data.find((model) => model.id.includes("gpt")); - document.querySelector("#model").value = model.root; - } catch (err) { - console.error(err); - showMessage("Failed to fetch last ChatGPT version"); - } - }); -} - -/* we load back the configuration */ -chrome.storage.sync.get(["moodleGPT"]).then(function (storage) { +// we load back the configuration +chrome.storage.sync.get(['moodleGPT']).then(function (storage) { const config = storage.moodleGPT; if (config) { @@ -161,23 +70,19 @@ 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(); - getLastChatGPTVersion(); + checkCanIncludeImages(); }); diff --git a/extension/popup/js/modeHandler.js b/extension/popup/js/modeHandler.js new file mode 100644 index 0000000..60dccac --- /dev/null +++ b/extension/popup/js/modeHandler.js @@ -0,0 +1,48 @@ +'use strict'; + +const mode = document.querySelector('#mode'); +const modes = mode.querySelectorAll('button'); + +let actualMode = 'autocomplete'; + +// input to don't take in consideration +const toExcludes = ['includeImages']; + +// inputs id that need to be disabled for a specific mode +const disabledForThisMode = { + autocomplete: [], + clipboard: ['typing', 'mouseover'], + 'question-to-answer': ['typing', 'infinite', 'mouseover'] +}; + +/** + * Handle when a mode change to show specific input or to hide them + */ +function handleModeChange() { + const needDisable = disabledForThisMode[actualMode]; + const dontNeedDisable = inputsCheckbox.filter( + input => !needDisable.includes(input) && !toExcludes.includes(input) + ); + for (const id of needDisable) { + document.querySelector('#' + id).parentElement.style.display = 'none'; + } + for (const id of dontNeedDisable) { + document.querySelector('#' + id).parentElement.style.display = null; + } +} + +// Mode hanlder +for (const button of modes) { + button.addEventListener('click', function () { + const value = button.value; + actualMode = value; + for (const mode of modes) { + if (mode.value !== value) { + mode.classList.add('not-selected'); + } else { + mode.classList.remove('not-selected'); + } + } + handleModeChange(); + }); +} diff --git a/extension/popup/js/utils.js b/extension/popup/js/utils.js new file mode 100644 index 0000000..0c71661 --- /dev/null +++ b/extension/popup/js/utils.js @@ -0,0 +1,25 @@ +'use strict'; + +/** + * Show message into the popup + */ +function showMessage({ msg, error, infinite }) { + 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); +} + +/** + * Check if the current model support images integrations + * @param {string} version + * @returns + */ +function isCurrentVersionSupportingImages(version) { + const versionNumber = version.match(/gpt-(\d+)/); + if (!versionNumber?.[1]) { + return false; + } + return Number(versionNumber[1]) >= 4; +} diff --git a/extension/popup/js/version.js b/extension/popup/js/version.js index dd745a7..dc9991d 100644 --- a/extension/popup/js/version.js +++ b/extension/popup/js/version.js @@ -1,5 +1,7 @@ -const currentVersion = "1.0.3"; -const versionDisplay = document.querySelector("#version"); +'use strict'; + +const CURRENT_VERSION = '1.1.0'; +const versionDisplay = document.querySelector('#version'); /** * Get the last version from the github @@ -7,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; @@ -21,41 +23,41 @@ 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 the extension neeed an update or no + * 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 currentVersion; + return CURRENT_VERSION; }); - const lastVertionSplitted = lastVersion.split("."); - const currentVersionSplitted = currentVersion.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])) + if (parseInt(lastVertionSplitted[i]) > parseInt(currentVersionSplitted[i])) { return setVersion(lastVersion, false); + } else if (parseInt(currentVersionSplitted[i]) > parseInt(lastVertionSplitted[i])) { + return setVersion(CURRENT_VERSION); + } } - setVersion(currentVersion); + setVersion(CURRENT_VERSION); } notifyUpdate(); diff --git a/extension/popup/style.css b/extension/popup/style.css index 93df10a..d44b084 100644 --- a/extension/popup/style.css +++ b/extension/popup/style.css @@ -1,140 +1,150 @@ -@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: 6rem; - text-transform: uppercase; -} - -.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'], +.line select { + flex: 1 1; + border: thin solid var(--color); + padding: 0.3rem 0.5rem; + border-radius: 0.2rem; + outline-color: transparent; + background-color: transparent; +} + +.line option { + color: #000; +} + +.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-bottom: -0.5rem; +} + +#reloadModel { + cursor: pointer; +} + +#reloadModel[disabled] { + cursor: not-allowed; + opacity: 0.75; +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a1018a0 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3411 @@ +{ + "name": "moodlegpt", + "version": "1.1.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "moodlegpt", + "version": "1.1.0", + "license": "MIT", + "devDependencies": { + "@rollup/plugin-terser": "^0.4.4", + "@types/chrome": "^0.0.263", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "prettier": "^3.2.5", + "rollup": "^4.13.0", + "rollup-plugin-ts": "^3.2.0", + "typescript": "^5.4.2" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.2", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.19", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mdn/browser-compat-data": { + "version": "5.3.16", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rollup/plugin-terser": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "dev": true, + "dependencies": { + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz", + "integrity": "sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/chrome": { + "version": "0.0.263", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.263.tgz", + "integrity": "sha512-As0vzv99ov3M6ZR7R6VzhMWFZXkPMrFrCEXXVrMN576Cm70fTkj7Df2CF+qEo170JepX50pd11cX6O4DSAtl2Q==", + "dev": true, + "dependencies": { + "@types/filesystem": "*", + "@types/har-format": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/filesystem": { + "version": "0.0.33", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/filewriter": "*" + } + }, + "node_modules/@types/filewriter": { + "version": "0.0.30", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/har-format": { + "version": "1.2.12", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "17.0.45", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/object-path": { + "version": "0.11.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/semver": { + "version": "7.5.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ua-parser-js": { + "version": "0.7.37", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.2.0.tgz", + "integrity": "sha512-mdekAHOqS9UjlmyF/LSs6AIEvfceV749GFxoBAjwAv0nkevfKHWQFDMcBZWUiIC5ft6ePWivXoS36aKQ0Cy3sw==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "7.2.0", + "@typescript-eslint/type-utils": "7.2.0", + "@typescript-eslint/utils": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.2.0.tgz", + "integrity": "sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "7.2.0", + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/typescript-estree": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.2.0.tgz", + "integrity": "sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.2.0.tgz", + "integrity": "sha512-xHi51adBHo9O9330J8GQYQwrKBqbIPJGZZVQTHHmy200hvkLZFWJIFtAG/7IYTWUyun6DE6w5InDReePJYJlJA==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "7.2.0", + "@typescript-eslint/utils": "7.2.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.2.0.tgz", + "integrity": "sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.2.0.tgz", + "integrity": "sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.2.0.tgz", + "integrity": "sha512-YfHpnMAGb1Eekpm3XRK8hcMwGLGsnT6L+7b2XyRv6ouDuJU1tZir1GS2i0+VXRatMwSI1/UfcyPe53ADkU+IuA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "7.2.0", + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/typescript-estree": "7.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.2.0.tgz", + "integrity": "sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.2.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "dev": true, + "license": "ISC" + }, + "node_modules/@wessberg/stringutil": { + "version": "1.0.19", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/acorn": { + "version": "8.10.0", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.21.10", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001517", + "electron-to-chromium": "^1.4.477", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.11" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/browserslist-generator": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@mdn/browser-compat-data": "^5.3.7", + "@types/object-path": "^0.11.1", + "@types/semver": "^7.5.0", + "@types/ua-parser-js": "^0.7.36", + "browserslist": "^4.21.10", + "caniuse-lite": "^1.0.30001518", + "isbot": "^3.6.13", + "object-path": "^0.11.8", + "semver": "^7.5.4", + "ua-parser-js": "^1.0.35" + }, + "engines": { + "node": ">=16.15.1", + "npm": ">=7.0.0", + "pnpm": ">=3.2.0", + "yarn": ">=1.13" + }, + "funding": { + "type": "github", + "url": "https://github.com/wessberg/browserslist-generator?sponsor=1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.20.3", + "dev": true, + "license": "MIT" + }, + "node_modules/compatfactory": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "helpertypes": "^0.0.19" + }, + "engines": { + "node": ">=14.9.0" + }, + "peerDependencies": { + "typescript": ">=3.x || >= 4.x || >= 5.x" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crosspath": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^17.0.36" + }, + "engines": { + "node": ">=14.9.0" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.522", + "dev": true, + "license": "ISC" + }, + "node_modules/escalade": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.17.1", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "dev": true, + "license": "ISC" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/glob": { + "version": "7.2.3", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/helpertypes": { + "version": "0.0.19", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/ignore": { + "version": "5.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "dev": true, + "license": "ISC" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isbot": { + "version": "3.6.13", + "dev": true, + "license": "Unlicense", + "engines": { + "node": ">=12" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/magic-string": { + "version": "0.30.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.13", + "dev": true, + "license": "MIT" + }, + "node_modules/object-path": { + "version": "0.11.8", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.2.5", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.0.tgz", + "integrity": "sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.13.0", + "@rollup/rollup-android-arm64": "4.13.0", + "@rollup/rollup-darwin-arm64": "4.13.0", + "@rollup/rollup-darwin-x64": "4.13.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.13.0", + "@rollup/rollup-linux-arm64-gnu": "4.13.0", + "@rollup/rollup-linux-arm64-musl": "4.13.0", + "@rollup/rollup-linux-riscv64-gnu": "4.13.0", + "@rollup/rollup-linux-x64-gnu": "4.13.0", + "@rollup/rollup-linux-x64-musl": "4.13.0", + "@rollup/rollup-win32-arm64-msvc": "4.13.0", + "@rollup/rollup-win32-ia32-msvc": "4.13.0", + "@rollup/rollup-win32-x64-msvc": "4.13.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup-plugin-ts": { + "version": "3.4.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.2", + "@wessberg/stringutil": "^1.0.19", + "ansi-colors": "^4.1.3", + "browserslist": "^4.21.10", + "browserslist-generator": "^2.1.0", + "compatfactory": "^3.0.0", + "crosspath": "^2.0.0", + "magic-string": "^0.30.2", + "ts-clone-node": "^3.0.0", + "tslib": "^2.6.1" + }, + "engines": { + "node": ">=16.15.1", + "npm": ">=7.0.0", + "pnpm": ">=3.2.0", + "yarn": ">=1.13" + }, + "funding": { + "type": "github", + "url": "https://github.com/wessberg/rollup-plugin-ts?sponsor=1" + }, + "peerDependencies": { + "@babel/core": ">=7.x", + "@babel/plugin-transform-runtime": ">=7.x", + "@babel/preset-env": ">=7.x", + "@babel/preset-typescript": ">=7.x", + "@babel/runtime": ">=7.x", + "@swc/core": ">=1.x", + "@swc/helpers": ">=0.2", + "rollup": ">=1.x || >=2.x || >=3.x", + "typescript": ">=3.2.x || >= 4.x || >= 5.x" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@babel/plugin-transform-runtime": { + "optional": true + }, + "@babel/preset-env": { + "optional": true + }, + "@babel/preset-typescript": { + "optional": true + }, + "@babel/runtime": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.5.4", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/smob": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/terser": { + "version": "5.19.4", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-clone-node": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "compatfactory": "^3.0.0" + }, + "engines": { + "node": ">=14.9.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/wessberg/ts-clone-node?sponsor=1" + }, + "peerDependencies": { + "typescript": "^3.x || ^4.x || ^5.x" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", + "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ua-parser-js": { + "version": "1.0.36", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.11", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "dev": true, + "license": "ISC" + }, + "node_modules/yallist": { + "version": "4.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "dev": true + }, + "@eslint-community/eslint-utils": { + "version": "4.4.0", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.3.0" + } + }, + "@eslint-community/regexpp": { + "version": "4.10.0", + "dev": true + }, + "@eslint/eslintrc": { + "version": "2.1.4", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "minimatch": { + "version": "3.1.2", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "@eslint/js": { + "version": "8.57.0", + "dev": true + }, + "@humanwhocodes/config-array": { + "version": "0.11.14", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "minimatch": { + "version": "3.1.2", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "dev": true + }, + "@humanwhocodes/object-schema": { + "version": "2.0.2", + "dev": true + }, + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.1", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "dev": true + }, + "@jridgewell/source-map": { + "version": "0.3.5", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.19", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "@mdn/browser-compat-data": { + "version": "5.3.16", + "dev": true + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@rollup/plugin-terser": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "dev": true, + "requires": { + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" + } + }, + "@rollup/pluginutils": { + "version": "5.1.0", + "dev": true, + "requires": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + } + }, + "@rollup/rollup-win32-x64-msvc": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz", + "integrity": "sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==", + "dev": true, + "optional": true + }, + "@types/chrome": { + "version": "0.0.263", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.263.tgz", + "integrity": "sha512-As0vzv99ov3M6ZR7R6VzhMWFZXkPMrFrCEXXVrMN576Cm70fTkj7Df2CF+qEo170JepX50pd11cX6O4DSAtl2Q==", + "dev": true, + "requires": { + "@types/filesystem": "*", + "@types/har-format": "*" + } + }, + "@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "@types/filesystem": { + "version": "0.0.33", + "dev": true, + "requires": { + "@types/filewriter": "*" + } + }, + "@types/filewriter": { + "version": "0.0.30", + "dev": true + }, + "@types/har-format": { + "version": "1.2.12", + "dev": true + }, + "@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "@types/node": { + "version": "17.0.45", + "dev": true + }, + "@types/object-path": { + "version": "0.11.2", + "dev": true + }, + "@types/semver": { + "version": "7.5.2", + "dev": true + }, + "@types/ua-parser-js": { + "version": "0.7.37", + "dev": true + }, + "@typescript-eslint/eslint-plugin": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.2.0.tgz", + "integrity": "sha512-mdekAHOqS9UjlmyF/LSs6AIEvfceV749GFxoBAjwAv0nkevfKHWQFDMcBZWUiIC5ft6ePWivXoS36aKQ0Cy3sw==", + "dev": true, + "requires": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "7.2.0", + "@typescript-eslint/type-utils": "7.2.0", + "@typescript-eslint/utils": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + } + }, + "@typescript-eslint/parser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.2.0.tgz", + "integrity": "sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "7.2.0", + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/typescript-estree": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", + "debug": "^4.3.4" + } + }, + "@typescript-eslint/scope-manager": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.2.0.tgz", + "integrity": "sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0" + } + }, + "@typescript-eslint/type-utils": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.2.0.tgz", + "integrity": "sha512-xHi51adBHo9O9330J8GQYQwrKBqbIPJGZZVQTHHmy200hvkLZFWJIFtAG/7IYTWUyun6DE6w5InDReePJYJlJA==", + "dev": true, + "requires": { + "@typescript-eslint/typescript-estree": "7.2.0", + "@typescript-eslint/utils": "7.2.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + } + }, + "@typescript-eslint/types": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.2.0.tgz", + "integrity": "sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.2.0.tgz", + "integrity": "sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + } + }, + "@typescript-eslint/utils": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.2.0.tgz", + "integrity": "sha512-YfHpnMAGb1Eekpm3XRK8hcMwGLGsnT6L+7b2XyRv6ouDuJU1tZir1GS2i0+VXRatMwSI1/UfcyPe53ADkU+IuA==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "7.2.0", + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/typescript-estree": "7.2.0", + "semver": "^7.5.4" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.2.0.tgz", + "integrity": "sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==", + "dev": true, + "requires": { + "@typescript-eslint/types": "7.2.0", + "eslint-visitor-keys": "^3.4.1" + } + }, + "@ungap/structured-clone": { + "version": "1.2.0", + "dev": true + }, + "@wessberg/stringutil": { + "version": "1.0.19", + "dev": true + }, + "acorn": { + "version": "8.10.0", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.2", + "dev": true, + "requires": {} + }, + "ajv": { + "version": "6.12.6", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-colors": { + "version": "4.1.3", + "dev": true + }, + "ansi-regex": { + "version": "5.0.1", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "argparse": { + "version": "2.0.1", + "dev": true + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "balanced-match": { + "version": "1.0.2", + "dev": true + }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browserslist": { + "version": "4.21.10", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001517", + "electron-to-chromium": "^1.4.477", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.11" + } + }, + "browserslist-generator": { + "version": "2.1.0", + "dev": true, + "requires": { + "@mdn/browser-compat-data": "^5.3.7", + "@types/object-path": "^0.11.1", + "@types/semver": "^7.5.0", + "@types/ua-parser-js": "^0.7.36", + "browserslist": "^4.21.10", + "caniuse-lite": "^1.0.30001518", + "isbot": "^3.6.13", + "object-path": "^0.11.8", + "semver": "^7.5.4", + "ua-parser-js": "^1.0.35" + } + }, + "buffer-from": { + "version": "1.1.2", + "dev": true + }, + "callsites": { + "version": "3.1.0", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "dev": true + }, + "commander": { + "version": "2.20.3", + "dev": true + }, + "compatfactory": { + "version": "3.0.0", + "dev": true, + "requires": { + "helpertypes": "^0.0.19" + } + }, + "concat-map": { + "version": "0.0.1", + "dev": true + }, + "cross-spawn": { + "version": "7.0.3", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "crosspath": { + "version": "2.0.0", + "dev": true, + "requires": { + "@types/node": "^17.0.36" + } + }, + "debug": { + "version": "4.3.4", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "deep-is": { + "version": "0.1.4", + "dev": true + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "doctrine": { + "version": "3.0.0", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "electron-to-chromium": { + "version": "1.4.522", + "dev": true + }, + "escalade": { + "version": "3.1.1", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "dev": true + }, + "eslint": { + "version": "8.57.0", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "minimatch": { + "version": "3.1.2", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "eslint-config-prettier": { + "version": "9.1.0", + "dev": true, + "requires": {} + }, + "eslint-scope": { + "version": "7.2.2", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "eslint-visitor-keys": { + "version": "3.4.3", + "dev": true + }, + "espree": { + "version": "9.6.1", + "dev": true, + "requires": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + } + }, + "esquery": { + "version": "1.5.0", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + } + }, + "esrecurse": { + "version": "4.3.0", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "dev": true + }, + "estree-walker": { + "version": "2.0.2", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "dev": true + }, + "fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "dev": true + }, + "fastq": { + "version": "1.17.1", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "5.0.0", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "3.2.0", + "dev": true, + "requires": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.3.1", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "dev": true + }, + "glob": { + "version": "7.2.3", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "minimatch": { + "version": "3.1.2", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "glob-parent": { + "version": "6.0.2", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "globals": { + "version": "13.24.0", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, + "graphemer": { + "version": "1.4.0", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "dev": true + }, + "helpertypes": { + "version": "0.0.19", + "dev": true + }, + "ignore": { + "version": "5.3.1", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-path-inside": { + "version": "3.0.3", + "dev": true + }, + "isbot": { + "version": "3.6.13", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "json-buffer": { + "version": "3.0.1", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true + }, + "keyv": { + "version": "4.5.4", + "dev": true, + "requires": { + "json-buffer": "3.0.1" + } + }, + "levn": { + "version": "0.4.1", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "locate-path": { + "version": "6.0.0", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash.merge": { + "version": "4.6.2", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "magic-string": { + "version": "0.30.3", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "ms": { + "version": "2.1.2", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "dev": true + }, + "node-releases": { + "version": "2.0.13", + "dev": true + }, + "object-path": { + "version": "0.11.8", + "dev": true + }, + "once": { + "version": "1.4.0", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.9.3", + "dev": true, + "requires": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + } + }, + "p-limit": { + "version": "3.1.0", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "parent-module": { + "version": "1.0.1", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "path-exists": { + "version": "4.0.0", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "dev": true + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "dev": true + }, + "prelude-ls": { + "version": "1.2.1", + "dev": true + }, + "prettier": { + "version": "3.2.5", + "dev": true + }, + "punycode": { + "version": "2.3.1", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "resolve-from": { + "version": "4.0.0", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "rollup": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.0.tgz", + "integrity": "sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==", + "dev": true, + "requires": { + "@rollup/rollup-android-arm-eabi": "4.13.0", + "@rollup/rollup-android-arm64": "4.13.0", + "@rollup/rollup-darwin-arm64": "4.13.0", + "@rollup/rollup-darwin-x64": "4.13.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.13.0", + "@rollup/rollup-linux-arm64-gnu": "4.13.0", + "@rollup/rollup-linux-arm64-musl": "4.13.0", + "@rollup/rollup-linux-riscv64-gnu": "4.13.0", + "@rollup/rollup-linux-x64-gnu": "4.13.0", + "@rollup/rollup-linux-x64-musl": "4.13.0", + "@rollup/rollup-win32-arm64-msvc": "4.13.0", + "@rollup/rollup-win32-ia32-msvc": "4.13.0", + "@rollup/rollup-win32-x64-msvc": "4.13.0", + "@types/estree": "1.0.5", + "fsevents": "~2.3.2" + } + }, + "rollup-plugin-ts": { + "version": "3.4.5", + "dev": true, + "requires": { + "@rollup/pluginutils": "^5.0.2", + "@wessberg/stringutil": "^1.0.19", + "ansi-colors": "^4.1.3", + "browserslist": "^4.21.10", + "browserslist-generator": "^2.1.0", + "compatfactory": "^3.0.0", + "crosspath": "^2.0.0", + "magic-string": "^0.30.2", + "ts-clone-node": "^3.0.0", + "tslib": "^2.6.1" + } + }, + "run-parallel": { + "version": "1.2.0", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "safe-buffer": { + "version": "5.2.1", + "dev": true + }, + "semver": { + "version": "7.5.4", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "serialize-javascript": { + "version": "6.0.1", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "shebang-command": { + "version": "2.0.0", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "smob": { + "version": "1.4.0", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "dev": true + }, + "source-map-support": { + "version": "0.5.21", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "strip-ansi": { + "version": "6.0.1", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "terser": { + "version": "5.19.4", + "dev": true, + "requires": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + } + }, + "text-table": { + "version": "0.2.0", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "ts-api-utils": { + "version": "1.2.1", + "dev": true, + "requires": {} + }, + "ts-clone-node": { + "version": "3.0.0", + "dev": true, + "requires": { + "compatfactory": "^3.0.0" + } + }, + "tslib": { + "version": "2.6.2", + "dev": true + }, + "type-check": { + "version": "0.4.0", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.20.2", + "dev": true + }, + "typescript": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", + "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", + "dev": true + }, + "ua-parser-js": { + "version": "1.0.36", + "dev": true + }, + "update-browserslist-db": { + "version": "1.0.11", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, + "uri-js": { + "version": "4.4.1", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "which": { + "version": "2.0.2", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "dev": true + } + } +} diff --git a/package.json b/package.json index 0e25c09..977f49f 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,11 @@ { "name": "moodlegpt", - "version": "1.0.3", + "version": "1.1.0", "description": "This extension allows you to hide CHAT-GPT in a Moodle quiz.", "scripts": { - "build": "rollup -c" + "build": "npm run prettier && npm run lint && rollup -c", + "lint": "eslint . --ext .ts", + "prettier": "prettier --write ." }, "repository": { "type": "git", @@ -23,10 +25,15 @@ }, "homepage": "https://github.com/yoannchb-pro/MoodleGPT#readme", "devDependencies": { - "@types/chrome": "^0.0.224", - "@rollup/plugin-terser": "^0.4.3", - "rollup": "^3.20.0", + "@rollup/plugin-terser": "^0.4.4", + "@types/chrome": "^0.0.263", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "prettier": "^3.2.5", + "rollup": "^4.13.0", "rollup-plugin-ts": "^3.2.0", - "typescript": "^5.0.2" + "typescript": "^5.4.2" } } 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 eb96584..688a7ac 100644 --- a/src/core/code-listener.ts +++ b/src/core/code-listener.ts @@ -1,81 +1,87 @@ -import Config from "../types/config"; -import titleIndications from "../utils/title-indications"; -import reply from "./reply"; - -const pressedKeys: string[] = []; -const listeners: { - element: HTMLElement; - fn: (this: HTMLElement, ev: MouseEvent) => any; -}[] = []; - -/** - * 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); - } - }); -} - -/** - * Setup moodleGPT into the page (remove/injection) - * @param config - * @returns - */ -function setUpMoodleGpt(config: Config) { - /* Removing events */ - 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; - } - - /* Code injection */ - const inputQuery = ["checkbox", "radio", "text", "number"] - .map((e) => `input[type="${e}"]`) - .join(","); - const query = inputQuery + ", textarea, select, [contenteditable]"; - const forms = document.querySelectorAll(".formulation"); - - for (const form of forms) { - const hiddenButton: HTMLElement = form.querySelector(".qtext"); - - if (config.cursor) hiddenButton.style.cursor = "pointer"; - - const injectionFunction = reply.bind( - null, - config, - hiddenButton, - form, - query - ); - listeners.push({ element: hiddenButton, fn: injectionFunction }); - hiddenButton.addEventListener("click", injectionFunction); - } - - if (config.title) titleIndications("Injected"); -} - -/** - * 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); - } -} - -export { codeListener, removeListener }; +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 d94abf0..a765410 100644 --- a/src/core/create-question.ts +++ b/src/core/create-question.ts @@ -1,34 +1,29 @@ -import Config from "../types/config"; -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 and add sub informations + * Normalize the question as text and add sub informations * @param langage * @param question * @returns */ -function createQuestion(config: Config, questionContainer: HTMLElement) { +function createAndNormalizeQuestion(questionContainer: HTMLElement) { let question = questionContainer.innerText; - /* We remove unnecessary information */ + // 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"); + // Make tables more readable for chat-gpt + 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); } -export default createQuestion; +export default createAndNormalizeQuestion; diff --git a/src/core/get-content-with-history.ts b/src/core/get-content-with-history.ts new file mode 100644 index 0000000..b636838 --- /dev/null +++ b/src/core/get-content-with-history.ts @@ -0,0 +1,159 @@ +import type Config from '@typing/config'; +import { ROLE, CONTENT_TYPE, type MessageContent, type Message } from '@typing/message'; +import imageToBase64 from '@utils/image-to-base64'; +import isGPTModelGreaterOrEqualTo4 from '@utils/version-support-images'; + +// The attempt and the cmid allow us to identify a quiz +type History = { + host: string; + cmid: string; // The id of the quiz + attempt: string; // The attempt of the current quiz + history: { role: ROLE; content: MessageContent }[]; +}; + +const INSTRUCTION: string = ` +Act as a quiz solver for the best notation with the following 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. +- But for the calculation provide this format 'result: ' +- For 'put in order' questions, maintain the answer in the order as presented in the question but assocy the correct order to it by usin this format ':\n:', ignore other rules. +- 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 SYSTEM_INSTRUCTION_MESSAGE = { + role: ROLE.SYSTEM, + content: INSTRUCTION +} as const satisfies Message; + +/** + * Get the content to send to ChatGPT API (it allows to includes images if supported) + * @param config + */ +async function getContent( + config: Config, + questionElement: HTMLElement, + question: string +): Promise { + const imagesElements = questionElement.querySelectorAll('img'); + + if ( + !config.includeImages || + !isGPTModelGreaterOrEqualTo4(config.model) || + imagesElements.length === 0 + ) { + return question; + } + + const contentWithImages: MessageContent = []; + + const base64Images = Array.from(imagesElements).map(imgEl => imageToBase64(imgEl)); + const base64ImagesResolved = await Promise.allSettled(base64Images); + + for (const result of base64ImagesResolved) { + if (result.status === 'fulfilled') { + contentWithImages.push({ + type: CONTENT_TYPE.IMAGE, + image_url: { url: result.value } + }); + } else if (config.logs) { + console.error(result.reason); + } + } + + contentWithImages.push({ + type: CONTENT_TYPE.TEXT, + text: question + }); + + return contentWithImages; +} + +/** + * Create a new history object from the current page + * @returns + */ +function createNewHistory(): History { + const urlParams = new URLSearchParams(document.location.search); + + return { + host: document.location.host, + cmid: urlParams.get('cmid') ?? '', + attempt: urlParams.get('attempt') ?? '', + history: [] + }; +} + +/** + * Load the past history from the session storage otherwise return the default history object + * @returns + */ +function loadPastHistory(): History | null { + return JSON.parse(sessionStorage.moodleGPTHistory ?? 'null'); +} + +/** + * Check if two history are from the same origin + * @param a + * @param b + * @returns + */ +function areHistoryFromSameQuiz(a: History, b: History): boolean { + const KEYS_TO_COMPARE: (keyof History)[] = ['host', 'cmid', 'attempt']; + + for (const key of KEYS_TO_COMPARE) { + if (a[key] !== b[key]) return false; + } + + return true; +} + +/** + * Return the content to send to chatgpt api with history if needed + * @param config + * @param questionElement + * @param question + * @returns + */ +async function getContentWithHistory( + config: Config, + questionElement: HTMLElement, + question: string +): Promise<{ + messages: [typeof SYSTEM_INSTRUCTION_MESSAGE, ...Message[]]; + saveResponse?: (response: string) => void; +}> { + const content = await getContent(config, questionElement, question); + const message = { role: ROLE.USER, content }; + + if (!config.history) return { messages: [SYSTEM_INSTRUCTION_MESSAGE, message] }; + + let history: History; + + const pastHistory: History | null = loadPastHistory(); + const newHistory: History = createNewHistory(); + + if (pastHistory === null || !areHistoryFromSameQuiz(pastHistory, newHistory)) { + history = newHistory; + } else { + history = pastHistory; + } + + return { + messages: [SYSTEM_INSTRUCTION_MESSAGE, ...history.history, message], + saveResponse(response: string) { + // Register the conversation + if (config.history) { + history.history.push(message); + history.history.push({ role: ROLE.ASSISTANT, content: response }); + sessionStorage.moodleGPTHistory = JSON.stringify(history); + } + } + }; +} + +export default getContentWithHistory; diff --git a/src/core/get-response.ts b/src/core/get-response.ts index d5dcc7b..f1f19b1 100644 --- a/src/core/get-response.ts +++ b/src/core/get-response.ts @@ -1,58 +1,57 @@ -import Config from "../types/config"; -import GPTAnswer from "../types/gptAnswer"; -import normalizeText from "../utils/normalize-text"; - -/** - * Get the response from chatGPT api - * @param config - * @param question - * @returns - */ -async function getChatGPTResponse( - config: Config, - question: string -): Promise { - const controller = new AbortController(); - const timeoutControler = setTimeout(() => controller.abort(), 15000); - 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: [ - { - role: "system", - content: ` -Follow those rules: -- Sometimes there won't be a question, so just answer the statement as you normally would without following the other rules and give the most detailled and complete answer with explication. -- For put in order question just give the good order separate by new line -- Your goal is to understand the statement and to reply to each question by giving only the answer. -- You will keep the same order for the answers like in the text. -- You will separate all the answer with new lines and only show the correctes one. -- You will only give the answers for each question and omit the questions, statement, title or other informations from the response. -- You will only give answer with exactly the same text as the gived answers. -- The question always have the good answer so you should always give an answer to the question. -- You will always respond in the same langage as the user question.`, - }, - { role: "user", content: question }, - ], - 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; - return { - response, - normalizedResponse: normalizeText(response), - }; -} - -export default getChatGPTResponse; +import type Config from '@typing/config'; +import type GPTAnswer from '@typing/gpt-answer'; +import normalizeText from '@utils/normalize-text'; +import getContentWithHistory from './get-content-with-history'; + +/** + * Get the response from chatGPT api + * @param config + * @param question + * @returns + */ +async function getChatGPTResponse( + config: Config, + questionElement: HTMLElement, + question: string +): Promise { + const controller = new AbortController(); + const timeoutControler = setTimeout(() => controller.abort(), 20 * 1000); + + // Get the content to send to chatgpt + // Including the instructions to the AI, the images as base64 if needed, the question and the past conversation if history is set to true + const contentHandler = await getContentWithHistory(config, questionElement, 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: contentHandler.messages, + + temperature: 0.1, // Controls the randomness of the generated responses, with lower values producing more deterministic and predictable outputs. With set to 0.1 instead of 0 for more creativity. + top_p: 0.6, // Determines the diversity of the generated responses + presence_penalty: 0, // Encourages the model to introduce new concepts by penalizing words that have already appeared in the text. + max_tokens: 2000 // Maximum length of the response + }) + }); + + clearTimeout(timeoutControler); + + const rep = await req.json(); + const response = rep.choices[0].message.content; + + // Save the response into the history + if (typeof contentHandler.saveResponse === 'function') contentHandler.saveResponse(response); + + return { + question, + response, + normalizedResponse: normalizeText(response) + }; +} + +export default getChatGPTResponse; diff --git a/src/core/modes/autocomplete.ts b/src/core/modes/autocomplete.ts new file mode 100644 index 0000000..5f8f9a2 --- /dev/null +++ b/src/core/modes/autocomplete.ts @@ -0,0 +1,45 @@ +import type GPTAnswer from '@typing/gpt-answer'; +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; + questionElement: HTMLElement; + inputList: NodeListOf; + gptAnswer: GPTAnswer; + removeListener: () => void; +}; + +/** + * Autocomplete mode: + * Autocomplete the question by checking the good answer + * @param props + * @returns + */ +function autoCompleteMode(props: Props) { + if (!props.config.infinite) props.removeListener(); + + const handlers = [ + handleContentEditable, + handleTextbox, + handleNumber, + handleSelect, + handleRadio, + handleCheckbox + ]; + + for (const handler of handlers) { + if (handler(props.config, props.inputList, props.gptAnswer)) return; + } + + // In the case we can't auto complete the question + handleClipboard(props.config, props.gptAnswer); +} + +export default autoCompleteMode; diff --git a/src/core/modes/clipboard.ts b/src/core/modes/clipboard.ts new file mode 100644 index 0000000..a8a1090 --- /dev/null +++ b/src/core/modes/clipboard.ts @@ -0,0 +1,22 @@ +import type Config from '@typing/config'; +import type GPTAnswer from '@typing/gpt-answer'; +import handleClipboard from '@core/questions/clipboard'; + +type Props = { + config: Config; + questionElement: HTMLElement; + gptAnswer: GPTAnswer; + removeListener: () => void; +}; + +/** + * Clipboard mode: + * Simply copy the answer into the clipboard + * @param props + */ +function clipboardMode(props: Props) { + if (!props.config.infinite) props.removeListener(); + handleClipboard(props.config, props.gptAnswer); +} + +export default clipboardMode; diff --git a/src/core/modes/question-to-answer.ts b/src/core/modes/question-to-answer.ts new file mode 100644 index 0000000..5a3a8ca --- /dev/null +++ b/src/core/modes/question-to-answer.ts @@ -0,0 +1,32 @@ +import type GPTAnswer from '@typing/gpt-answer'; + +type Props = { + questionElement: HTMLElement; + gptAnswer: GPTAnswer; + removeListener: () => void; +}; + +/** + * Question to answer mode: + * Simply turn the question into the answer by clicking on it + * @param props + */ +function questionToAnswerMode(props: Props) { + const questionElement = props.questionElement; + + props.removeListener(); + + const questionBackup = questionElement.innerHTML ?? ''; + questionElement.innerHTML = props.gptAnswer.response; + questionElement.style.whiteSpace = 'pre-wrap'; + + // To go back to the question / answer + questionElement.addEventListener('click', function () { + const contentIsResponse = questionElement.innerHTML === props.gptAnswer.response; + + questionElement.style.whiteSpace = contentIsResponse ? 'initial' : 'pre-wrap'; + questionElement.innerHTML = contentIsResponse ? questionBackup : props.gptAnswer.response; + }); +} + +export default questionToAnswerMode; diff --git a/src/core/questions/checkbox.ts b/src/core/questions/checkbox.ts new file mode 100644 index 0000000..f6c7ed3 --- /dev/null +++ b/src/core/questions/checkbox.ts @@ -0,0 +1,54 @@ +import type Config from '@typing/config'; +import type GPTAnswer from '@typing/gpt-answer'; +import Logs from '@utils/logs'; +import normalizeText from '@utils/normalize-text'; +import { pickBestReponse } from '@utils/pick-best-response'; + +/** + * Handle input checkbox elements + * @param config + * @param inputList + * @param gptAnswer + */ +function handleCheckbox( + config: Config, + inputList: NodeListOf, + gptAnswer: GPTAnswer +): boolean { + const firstInput = inputList?.[0] as HTMLInputElement; + + // Handle the case the input is not a checkbox + if (!firstInput || firstInput.type !== 'checkbox') { + return false; + } + + const corrects = gptAnswer.normalizedResponse.split('\n'); + + const possibleAnswers = Array.from(inputList) + .map(inp => ({ + element: inp, + value: normalizeText(inp?.parentElement?.textContent ?? '') + })) + .filter(obj => obj.value !== ''); + + for (const correct of corrects) { + const bestAnswer = pickBestReponse(correct, possibleAnswers); + + if (config.logs && bestAnswer.value) { + Logs.bestAnswer(bestAnswer.value, bestAnswer.similarity); + } + + const correctInput = bestAnswer.element as HTMLInputElement; + if (config.mouseover) { + correctInput.addEventListener('mouseover', () => correctInput.click(), { + once: true + }); + } else { + correctInput.click(); + } + } + + return true; +} + +export default handleCheckbox; diff --git a/src/core/questions/clipboard.ts b/src/core/questions/clipboard.ts index 563bbe5..7bf3b04 100644 --- a/src/core/questions/clipboard.ts +++ b/src/core/questions/clipboard.ts @@ -1,15 +1,15 @@ -import Config from "../../types/config"; -import GPTAnswer from "../../types/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/gpt-answer'; +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 ea8b661..188ffc7 100644 --- a/src/core/questions/contenteditable.ts +++ b/src/core/questions/contenteditable.ts @@ -1,5 +1,5 @@ -import Config from "../../types/config"; -import GPTAnswer from "../../types/gptAnswer"; +import type Config from '@typing/config'; +import type GPTAnswer from '@typing/gpt-answer'; /** * Hanlde contenteditable elements @@ -16,27 +16,30 @@ function handleContentEditable( const input = inputList[0]; if ( - inputList.length !== 1 || - input.getAttribute("contenteditable") !== "true" - ) + inputList.length !== 1 || // for now we don't handle many input for editable textcontent + 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); - /* Put the cursor at the end of the typed text */ + // Put the cursor at the end of the typed text input.focus(); const range = document.createRange(); range.selectNodeContents(input); range.collapse(false); const selection = window.getSelection(); - selection.removeAllRanges(); - selection.addRange(range); + if (selection !== null) { + selection.removeAllRanges(); + selection.addRange(range); + } }); } else { input.textContent = gptAnswer.response; diff --git a/src/core/questions/number.ts b/src/core/questions/number.ts index 3171928..c145d2c 100644 --- a/src/core/questions/number.ts +++ b/src/core/questions/number.ts @@ -1,42 +1,45 @@ -import Config from "../../types/config"; -import GPTAnswer from "../../types/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 || input.type !== "number") return false; - - const number = gptAnswer.normalizedResponse - .match(/\d+([,\.]\d+)?/gi)?.[0] - ?.replace(",", "."); - - if (!number) return false; - - if (config.typing) { - let index = 0; - input.addEventListener("keydown", function (event: KeyboardEvent) { - if (event.key === "Backspace") index = number.length + 1; - if (index > number.length) return; - event.preventDefault(); - 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/gpt-answer'; + +/** + * 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-checkbox.ts b/src/core/questions/radio-checkbox.ts deleted file mode 100644 index ea9de1f..0000000 --- a/src/core/questions/radio-checkbox.ts +++ /dev/null @@ -1,39 +0,0 @@ -import Config from "../../types/config"; -import GPTAnswer from "../../types/gptAnswer"; -import Logs from "../../utils/logs"; -import normalizeText from "../../utils/normalize-text"; - -/** - * Handle checkbox and input elements - * @param config - * @param inputList - * @param gptAnswer - */ -function handleRadioAndCheckbox( - config: Config, - inputList: NodeListOf, - gptAnswer: GPTAnswer -): boolean { - const input = inputList?.[0] as HTMLInputElement; - - if (!input || (input.type !== "checkbox" && input.type !== "radio")) - return false; - - for (const input of inputList as NodeListOf) { - const content = normalizeText(input.parentNode.textContent); - const valide = gptAnswer.normalizedResponse.includes(content); - if (config.logs) Logs.responseTry(content, valide); - if (valide) { - if (config.mouseover) { - input.addEventListener("mouseover", () => (input.checked = true), { - once: true, - }); - } else { - input.checked = true; - } - } - } - return true; -} - -export default handleRadioAndCheckbox; diff --git a/src/core/questions/radio.ts b/src/core/questions/radio.ts new file mode 100644 index 0000000..d27fef2 --- /dev/null +++ b/src/core/questions/radio.ts @@ -0,0 +1,50 @@ +import type Config from '@typing/config'; +import type GPTAnswer from '@typing/gpt-answer'; +import Logs from '@utils/logs'; +import normalizeText from '@utils/normalize-text'; +import { pickBestReponse } from '@utils/pick-best-response'; + +/** + * Handle input radio elements + * @param config + * @param inputList + * @param gptAnswer + */ +function handleRadio( + config: Config, + inputList: NodeListOf, + gptAnswer: GPTAnswer +): boolean { + const firstInput = inputList?.[0] as HTMLInputElement; + + // Handle the case the input is not a radio + if (!firstInput || firstInput.type !== 'radio') { + return false; + } + + const possibleAnswers = Array.from(inputList) + .map(inp => ({ + element: inp, + value: normalizeText(inp?.parentElement?.textContent ?? '') + })) + .filter(obj => obj.value !== ''); + + const bestAnswer = pickBestReponse(gptAnswer.normalizedResponse, possibleAnswers); + + if (config.logs && bestAnswer.value) { + Logs.bestAnswer(bestAnswer.value, bestAnswer.similarity); + } + + const correctInput = bestAnswer.element as HTMLInputElement; + if (config.mouseover) { + correctInput.addEventListener('mouseover', () => correctInput.click(), { + once: true + }); + } else { + correctInput.click(); + } + + return true; +} + +export default handleRadio; diff --git a/src/core/questions/select.ts b/src/core/questions/select.ts index 0ebcbb2..6f9f902 100644 --- a/src/core/questions/select.ts +++ b/src/core/questions/select.ts @@ -1,101 +1,61 @@ -import Config from "../../types/config"; -import GPTAnswer from "../../types/gptAnswer"; -import Logs from "../../utils/logs"; -import normalizeText from "../../utils/normalize-text"; - -/** - * 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; - - let correct = gptAnswer.normalizedResponse.split("\n"); - - if (config.logs) Logs.array(correct); - - /** - * Sometimes ChatGPT give the question so we should remove them - * Example: - * 5*5 - * 25 - * 10+10 - * 20 - * 20-10 - * 10 - * - * And we only want to keep answers - * 25 - * 20 - * 10 - */ - if (correct.length === inputList.length * 2) { - correct = correct.filter((answer, index) => index % 2 === 1); - } - - for (let j = 0; j < inputList.length; ++j) { - const options = inputList[j].querySelectorAll("option"); - - for (const option of options) { - const content = normalizeText(option.textContent); - const valide = correct[j].includes(content); - - /* Handle put in order question */ - if (!/[^\d]+/gi.test(content)) { - const elementTitle = (option.parentNode as HTMLElement) - .closest("tr") - .querySelector(".text"); - const content = normalizeText(elementTitle.textContent); - - const indexCorrectAnswer = correct.findIndex((answer) => { - const valide = answer.includes(content); - if (config.logs) Logs.responseTry(content, valide); - return valide; - }); - - if (indexCorrectAnswer !== -1) { - //we do + 1 because we skip the first option: Choose... - if (config.mouseover) { - options[indexCorrectAnswer + 1].closest("select").addEventListener( - "click", - function () { - options[indexCorrectAnswer + 1].selected = "selected" as any; - }, - { once: true } - ); - } else { - options[indexCorrectAnswer + 1].selected = "selected" as any; - } - break; - } - } - /* End */ - - if (config.logs) Logs.responseTry(content, valide); - - if (valide) { - if (config.mouseover) { - option - .closest("select") - .addEventListener("click", () => (option.selected = true), { - once: true, - }); - } else { - option.selected = true; - } - break; - } - } - } - - return true; -} - -export default handleSelect; +import type Config from '@typing/config'; +import type GPTAnswer from '@typing/gpt-answer'; +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) + .slice(1) // We remove the first option which correspond to "Choose..." + .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 8712b45..3accaaa 100644 --- a/src/core/questions/textbox.ts +++ b/src/core/questions/textbox.ts @@ -1,39 +1,42 @@ -import Config from "../../types/config"; -import GPTAnswer from "../../types/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 || - (input.tagName !== "TEXTAREA" && input.type !== "text") - ) - return false; - - if (config.typing) { - let index = 0; - input.addEventListener("keydown", function (event: KeyboardEvent) { - if (event.key === "Backspace") index = gptAnswer.response.length + 1; - if (index > gptAnswer.response.length) return; - event.preventDefault(); - 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/gpt-answer'; + +/** + * 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 44ae04b..f00122a 100644 --- a/src/core/reply.ts +++ b/src/core/reply.ts @@ -1,99 +1,78 @@ -import Config from "../types/config"; -import Logs from "../utils/logs"; -import getChatGPTResponse from "./get-response"; -import createQuestion from "./create-question"; -import handleRadioAndCheckbox from "./questions/radio-checkbox"; -import handleSelect from "./questions/select"; -import handleTextbox from "./questions/textbox"; -import handleClipboard from "./questions/clipboard"; -import handleNumber from "./questions/number"; -import handleContentEditable from "./questions/contenteditable"; -import { removeListener } from "./code-listener"; - -/** - * Reply to the question - * @param config - * @param hiddenButton - * @param form - * @param query - * @returns - */ -async function reply( - config: Config, - hiddenButton: HTMLElement, - form: HTMLElement, - query: string -) { - if (config.cursor) hiddenButton.style.cursor = "wait"; - - const question = createQuestion(config, form); - const inputList: NodeListOf = form.querySelectorAll(query); - - const gptAnswer = await getChatGPTResponse(config, question).catch( - (error) => ({ - error, - }) - ); - - const haveError = typeof gptAnswer === "object" && "error" in gptAnswer; - - if (config.cursor) - hiddenButton.style.cursor = - config.infinite || haveError ? "pointer" : "initial"; - - if (haveError) { - console.error(gptAnswer.error); - return; - } - - if (config.logs) { - Logs.question(question); - Logs.response(gptAnswer); - } - - /* Handle clipboard mode */ - if (config.mode === "clipboard") { - if (!config.infinite) removeListener(hiddenButton); - return handleClipboard(config, gptAnswer); - } - - /* Handle question to answer mode */ - if (config.mode === "question-to-answer") { - removeListener(hiddenButton); - - const questionContainer = form.querySelector(".qtext"); - const questionBackup = questionContainer.textContent; - - questionContainer.textContent = gptAnswer.response; - questionContainer.style.whiteSpace = "pre-wrap"; - - questionContainer.addEventListener("click", function () { - const isNotResponse = questionContainer.textContent === questionBackup; - questionContainer.style.whiteSpace = isNotResponse ? "pre-wrap" : null; - questionContainer.textContent = isNotResponse - ? gptAnswer.response - : questionBackup; - }); - return; - } - - /* Better then set once on the event because if there is an error the user can click an other time on the question */ - if (!config.infinite) removeListener(hiddenButton); - - const handlers = [ - handleContentEditable, - handleTextbox, - handleNumber, - handleSelect, - handleRadioAndCheckbox, - ]; - - for (const handler of handlers) { - if (handler(config, inputList, gptAnswer)) return; - } - - /* In the case we can't auto complete the question */ - handleClipboard(config, gptAnswer); -} - -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, props.questionElement, 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 fe3c403..fa5c8ae 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,14 @@ -import { codeListener } from "./core/code-listener"; - -chrome.storage.sync.get(["moodleGPT"]).then(function (storage) { - const config = storage.moodleGPT; - - if (!config) throw new Error("Please configure MoodleGPT into the extension"); - - codeListener(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 c2d101f..cf31c87 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -1,7 +1,7 @@ type Config = { apiKey: string; - code: string; - model?: string; + model: string; + code?: string; infinite?: boolean; typing?: boolean; mouseover?: boolean; @@ -9,7 +9,9 @@ type Config = { logs?: boolean; title?: boolean; timeout?: boolean; - mode?: "autocomplete" | "question-to-answer" | "clipboard"; + history?: boolean; + includeImages?: boolean; + mode?: 'autocomplete' | 'question-to-answer' | 'clipboard'; }; export default Config; diff --git a/src/types/gptAnswer.ts b/src/types/gpt-answer.ts similarity index 83% rename from src/types/gptAnswer.ts rename to src/types/gpt-answer.ts index 4742310..32a7694 100644 --- a/src/types/gptAnswer.ts +++ b/src/types/gpt-answer.ts @@ -1,4 +1,5 @@ type GPTAnswer = { + question: string; response: string; normalizedResponse: string; }; diff --git a/src/types/message.ts b/src/types/message.ts new file mode 100644 index 0000000..165e73f --- /dev/null +++ b/src/types/message.ts @@ -0,0 +1,28 @@ +export enum ROLE { + SYSTEM = 'system', + USER = 'user', + ASSISTANT = 'assistant' +} + +export enum CONTENT_TYPE { + TEXT = 'text', + IMAGE = 'image_url' +} + +export type MessageContent = + | string + | Array< + | { + type: CONTENT_TYPE.TEXT; + text: string; + } + | { + type: CONTENT_TYPE.IMAGE; + image_url: { url: string }; + } + >; + +export type Message = { + role: ROLE; + content: MessageContent; +}; diff --git a/src/utils/html-table-to-string.ts b/src/utils/html-table-to-string.ts index 0d23c7c..6578181 100644 --- a/src/utils/html-table-to-string.ts +++ b/src/utils/html-table-to-string.ts @@ -5,37 +5,38 @@ */ 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 jointure = ' | '; + const headerLineLength = tab[0].length; const lineSeparationSize = - maxColumnsLength.reduce((a, b) => a + b) + tab[0].length * 3 + 1; - const lineSeparation = - "\n" + Array(lineSeparationSize).fill("-").join("") + "\n"; + maxColumnsLength.reduce((a, b) => a + b, 0) + (headerLineLength - 1) * jointure.length; + 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(jointure); }); + const head = mappedTab.shift(); - return head + lineSeparation + mappedTab.join("\n"); + + return head + lineSeparation + mappedTab.join('\n'); } export default htmlTableToString; diff --git a/src/utils/image-to-base64.ts b/src/utils/image-to-base64.ts new file mode 100644 index 0000000..6b7f498 --- /dev/null +++ b/src/utils/image-to-base64.ts @@ -0,0 +1,40 @@ +/** + * Convert an image html element into a base64 image string + * @param imageElement + * @param quality (default: 0.75 -> 75%) + * @returns + */ +function imageToBase64(imageElement: HTMLImageElement, quality = 0.75): Promise { + return new Promise((resolve, reject) => { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + if (!ctx) { + reject("Can't get the canvas context, ensure your navigator support canvas"); + canvas.remove(); + return; + } + + const img = new Image(); + img.crossOrigin = 'Anonymous'; + img.onload = () => { + canvas.width = img.width; + canvas.height = img.height; + ctx.drawImage(img, 0, 0); + + const base64 = canvas.toDataURL('image/png', quality); + resolve(base64); + + canvas.remove(); + }; + + img.onerror = err => { + reject(err); + canvas.remove(); + }; + + img.src = imageElement.src; + }); +} + +export default imageToBase64; diff --git a/src/utils/logs.ts b/src/utils/logs.ts index 41cacde..30a0b0e 100644 --- a/src/utils/logs.ts +++ b/src/utils/logs.ts @@ -1,23 +1,29 @@ -import GPTAnswer from "../types/gptAnswer"; -class Logs { - static question(text: string) { - const css = "color: cyan"; - console.log("%c[QUESTION]: %s", css, text); - } - - static responseTry(text: string, valide: boolean) { - const css = "color: " + (valide ? "green" : "red"); - console.log("%c[CHECKING]: %s", css, text); - } - - 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/gpt-answer'; +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 dd314d3..2945ab3 100644 --- a/src/utils/normalize-text.ts +++ b/src/utils/normalize-text.ts @@ -1,22 +1,20 @@ -/** - * Normlize text - * @param text - */ -function normalizeText(text: string, toLowerCase: boolean = true) { - let normalizedText = text - .replace(/\n+/gi, "\n") //remove duplicate new lines - .replace(/(\n\s*\n)+/g, "\n") //remove useless white sapce from textcontent - .replace(/[ \t]+/gi, " "); //replace multiples space or tabs by a space - - if (toLowerCase) normalizedText = normalizedText.toLowerCase(); - - return ( - normalizedText - .trim() - /* We remove that 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 - ); -} - -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 new file mode 100644 index 0000000..e523952 --- /dev/null +++ b/src/utils/pick-best-response.ts @@ -0,0 +1,114 @@ +type BestResponse = { + similarity: number; + value: string | null; + element: HTMLElement | null; +}; + +type ResponsesBySimilarity = { + similarity: number; + value: string; + element: HTMLElement; +}; + +/** + * Calculate the levenshtein distance between two sentence + * @param str1 + * @param str2 + * @returns + */ +function levenshteinDistance(str1: string, str2: string) { + if (str1.length === 0) return str2.length; + if (str2.length === 0) return str1.length; + + const matrix: number[][] = []; + const str1WithoutSpaces = str1.replace(/\s+/, ''); + const str2WithoutSpaces = str2.replace(/\s+/, ''); + + for (let i = 0; i <= str1WithoutSpaces.length; ++i) { + matrix.push([i]); + for (let j = 1; j <= str2WithoutSpaces.length; ++j) { + matrix[i][j] = + i === 0 + ? j + : 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) + ); + } + } + + return matrix[str1WithoutSpaces.length][str2WithoutSpaces.length]; +} + +/** + * Calculate the similarity between two sentences from 0 to 1 (best) + * @param str1 + * @param str2 + * @returns + */ +function sentenceSimilarity(str1: string, str2: string) { + const longerLength = str1.length > str2.length ? str1.length : str2.length; + if (longerLength === 0) return 1; + return (longerLength - levenshteinDistance(str1, str2)) / longerLength; +} + +/** + * Pick the best sentence that correspond to the answer + * @param arr + * @param answer + * @returns + */ +export function pickBestReponse( + answer: string, + arr: { element: HTMLElement; value: string }[] +): BestResponse { + let bestResponse: BestResponse = { + element: null, + similarity: 0, + value: null + }; + for (const obj of arr) { + const similarity = sentenceSimilarity(obj.value, answer); + if (similarity === 1) { + return { element: obj.element, value: obj.value, similarity }; + } + if (similarity > bestResponse.similarity) { + bestResponse = { element: obj.element, value: obj.value, similarity }; + } + } + return bestResponse; +} + +/** + * Return the sentences sorted by score with a score superior or equal to what is asked + * @param answer + * @param arr + * @param score + * @returns + */ +export function pickResponsesWithSimilarityGreaterThan( + answer: string, + arr: { element: HTMLElement; value: string }[], + score: number +): ResponsesBySimilarity[] { + const responses: ResponsesBySimilarity[] = []; + for (const obj of arr) { + const similarity = sentenceSimilarity(obj.value, answer); + if (similarity >= score) + responses.push({ + similarity, + value: obj.value, + element: obj.element + }); + } + return responses.sort((a, b) => a.similarity - b.similarity); +} + +/** + * Convert a number to a readable string pourcentage + * @param similarity + */ +export function toPourcentage(similarity: number): string { + 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/src/utils/version-support-images.ts b/src/utils/version-support-images.ts new file mode 100644 index 0000000..98f1d0a --- /dev/null +++ b/src/utils/version-support-images.ts @@ -0,0 +1,14 @@ +/** + * Check if the current ChatGPT version is greater or equal to 4 + * @param version + * @returns + */ +function isGPTModelGreaterOrEqualTo4(version: string): boolean { + const versionNumber = version.match(/gpt-(\d+)/); + if (!versionNumber?.[1]) { + return false; + } + return Number(versionNumber[1]) >= 4; +} + +export default isGPTModelGreaterOrEqualTo4; diff --git a/test/fake-moodle/css/style.css b/test/fake-moodle/css/style.css index bc8746f..0a36ffc 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 { @@ -65,3 +65,28 @@ textarea { white-space: pre-wrap; overflow-y: auto; } + +img { + height: 5rem; +} + +.style-table { + width: 100%; + border-collapse: collapse; + margin-bottom: 20px; +} + +.style-table th, +.style-table td { + border: 1px solid #ddd; + padding: 8px; + text-align: left; +} + +.style-table th { + background-color: #f2f2f2; +} + +.style-table tr:nth-child(even) { + background-color: #f2f2f2; +} diff --git a/test/fake-moodle/index.html b/test/fake-moodle/index.html index 9f8bef2..6053122 100644 --- a/test/fake-moodle/index.html +++ b/test/fake-moodle/index.html @@ -1,4 +1,4 @@ - + @@ -8,6 +8,33 @@ + +
+
+

Which are the roles of an informatician ?

+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
@@ -242,17 +269,72 @@
- + +
+
+

Give me the id of the personn who have a car

+ + + + + + + + + + + + + + + + + + + + + + + +
idnamebirthDatecars
Person 1Yvick15/08/1999yes
Person 2Yann19/01/2000no
+
+
+ +
+
+ +

- 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

+ + +
+

+ Warning ! Only work with gpt-4 and if "includes images" is activate +

+
+

What is the race of those cats:

+ black cat + white cat +
+
+
+
+
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 c7d476b..e6bdc50 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,16 +1,25 @@ -{ - "compilerOptions": { - "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"] - }, - "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/**/*"] +} diff --git a/webstore-assets/banner1.jpg b/webstore-assets/v1/banner1.jpg similarity index 100% rename from webstore-assets/banner1.jpg rename to webstore-assets/v1/banner1.jpg diff --git a/webstore-assets/banner2.png b/webstore-assets/v1/banner2.png similarity index 100% rename from webstore-assets/banner2.png rename to webstore-assets/v1/banner2.png diff --git a/webstore-assets/banner3.jpg b/webstore-assets/v1/banner3.jpg similarity index 100% rename from webstore-assets/banner3.jpg rename to webstore-assets/v1/banner3.jpg diff --git a/webstore-assets/banner4.png b/webstore-assets/v1/banner4.png similarity index 100% rename from webstore-assets/banner4.png rename to webstore-assets/v1/banner4.png diff --git a/webstore-assets/description.txt b/webstore-assets/v1/description.txt similarity index 100% rename from webstore-assets/description.txt rename to webstore-assets/v1/description.txt diff --git a/webstore-assets/icon-128x128.jpg b/webstore-assets/v1/icon-128x128.jpg similarity index 100% rename from webstore-assets/icon-128x128.jpg rename to webstore-assets/v1/icon-128x128.jpg diff --git a/webstore-assets/v2/banner1.jpg b/webstore-assets/v2/banner1.jpg new file mode 100644 index 0000000..69990f0 Binary files /dev/null and b/webstore-assets/v2/banner1.jpg differ diff --git a/webstore-assets/v2/banner2.png b/webstore-assets/v2/banner2.png new file mode 100644 index 0000000..b160a72 Binary files /dev/null and b/webstore-assets/v2/banner2.png differ diff --git a/webstore-assets/v2/banner3.jpg b/webstore-assets/v2/banner3.jpg new file mode 100644 index 0000000..b04c130 Binary files /dev/null and b/webstore-assets/v2/banner3.jpg differ diff --git a/webstore-assets/v2/banner4.jpg b/webstore-assets/v2/banner4.jpg new file mode 100644 index 0000000..571649d Binary files /dev/null and b/webstore-assets/v2/banner4.jpg differ diff --git a/webstore-assets/v2/description.txt b/webstore-assets/v2/description.txt new file mode 100644 index 0000000..628275c --- /dev/null +++ b/webstore-assets/v2/description.txt @@ -0,0 +1,22 @@ +MoodleGPT: Transform Your Moodle Quiz Experience + +MoodleGPT is a revolutionary Chrome extension designed to elevate your Moodle quiz-taking experience to new heights. By seamlessly integrating the power of CHAT-GPT, MoodleGPT provides you with an invisible assistant that effortlessly guides you through quiz questions, delivering answers with precision and speed. + +Key Features: +--- +- Versatile Question Support: Whether it's Select, Put in Order, Resolve Equation, One Response, Multiple Responses, True or False, Number, or Text questions, MoodleGPT has you covered. +- Dynamic Modes: Choose from Autocomplete, Clipboard, or Question to Answer modes to interact with answers in a way that suits your style. +- Simple Setup: Load the extension, enter your OpenAI API key, choose a model, and you're ready to conquer your Moodle quizzes. +- Customizable Settings: Tailor the extension to your preferences, including the choice of GPT model, cursor indication, title indication, typing/mouseover effect, include images, create conversation and more. + +Enhance Your Quiz-Taking Experience: +--- +MoodleGPT streamlines the quiz-taking process, providing you with accurate answers and saving you valuable time. It's the perfect tool for students and educators looking to optimize their Moodle experience. + +Support Us: +--- +If you find MoodleGPT helpful, consider supporting the project with a donation. Your support helps us continue improving and adding new features. + +Github: https://github.com/yoannchb-pro/MoodleGPT +Donation: https://www.buymeacoffee.com/yoannchbpro +Icon credits: Mortarboard icons created by itim2101 - Flaticon \ No newline at end of file diff --git a/webstore-assets/v2/icon-128x128-black.jpg b/webstore-assets/v2/icon-128x128-black.jpg new file mode 100644 index 0000000..e3d431d Binary files /dev/null and b/webstore-assets/v2/icon-128x128-black.jpg differ diff --git a/webstore-assets/v2/little_promotion.jpg b/webstore-assets/v2/little_promotion.jpg new file mode 100644 index 0000000..37e0ae6 Binary files /dev/null and b/webstore-assets/v2/little_promotion.jpg differ