118 Commits

Author SHA1 Message Date
yoannchb-pro 921e2bba4e Merge branch 'dev' 2025-10-01 09:30:19 +02:00
yoannchb-pro 3591aceba1 v1.1.5 2025-10-01 09:28:48 +02:00
yoannchb-pro 17aa8b49e0 Merge pull request #61 from yoannchb-pro/dev
Dev
2025-07-30 11:14:23 -04:00
yoannchb-pro a2849da20c v1.1.4 2025-07-30 11:12:44 -04:00
yoannchb-pro 54c9373e33 Merge pull request #58 from LuckyForce/main
Solves #57 for o3
2025-07-30 10:52:03 -04:00
Adrian Schauer e793cbd0b2 #57 addtion of o3 2025-07-10 17:01:10 +02:00
yoannchb-pro fffc0d55d6 Merge pull request #54 from yoannchb-pro/dev
v1.1.3
2025-03-19 09:28:49 -04:00
yoannchb-pro e561227b78 updated doc 2025-03-19 09:27:19 -04:00
yoannchb-pro 677e870635 gpt version msg 2025-03-19 09:09:35 -04:00
yoannchb-pro 6e69830a2e v1.1.2 -> v1.1.3 2025-03-19 09:07:52 -04:00
yoannchb-pro f01785256c Updated dependencies 2025-03-19 09:05:08 -04:00
yoannchb-pro f2e1ec8ed6 Merge pull request #46 from dmunozv04/main
Add baseURL and max Tokens config
2025-03-19 08:53:03 -04:00
yoannchb-pro 98f22e9056 Merge pull request #53 from yoannchb-pro/main
Merge main into dev
2025-03-19 08:51:25 -04:00
yoannchb-pro 79a75fee89 Create FUNDING.yml 2025-02-03 14:29:50 -05:00
dmunozv04 476559188f Add baseURL and max Tokens config 2025-02-03 20:21:29 +01:00
yoannchb-pro 16a82fe3d8 v1.1.2 2025-01-14 12:23:13 -05:00
yoannchb-pro ea57a315b7 Merge pull request #36 from yoannchb-pro/dev
Dev
2024-05-05 14:05:38 -04:00
yoannchb-pro 8303dffe99 Merge branch 'main' into dev 2024-05-05 14:01:19 -04:00
yoannchb-pro ac2ca56a54 Fixed bug and added atto handler 2024-05-05 14:00:47 -04:00
yoannchb-pro 3dcc6dc6eb build 2024-05-05 14:00:32 -04:00
yoannchb-pro f5679dd825 doc update 2024-05-05 14:00:17 -04:00
yoannchb-pro dcfe8f3320 removed get visible text because not working 2024-05-04 00:55:26 -04:00
yoannchb-pro ea5cd3763d Update TODO.md 2024-05-04 00:38:18 -04:00
yoannchb-pro 71f43590db code from 3 to 2 chars 2024-05-04 00:33:46 -04:00
yoannchb-pro eb58ac44ca fixed backspace for text input, number input and contenteditable. Also moved to v1.1.1 2024-04-19 15:17:00 -04:00
yoannchb-pro b60b3106a0 Update TODO.md 2024-03-25 20:23:03 -04:00
yoannchb-pro 292d00e664 fixing issues templates 2024-03-21 20:10:28 -04:00
yoannchb-pro 252e48fa95 Merge pull request #33 from yoannchb-pro/dev
v1.1.0
2024-03-21 20:05:14 -04:00
yoannchb-pro 4ee6b6a745 fixing assets size 2024-03-21 20:04:19 -04:00
yoannchb-pro bda60ce813 fixed table css 2024-03-21 19:57:38 -04:00
yoannchb-pro 32370b8bbc Update TODO.md 2024-03-21 19:53:22 -04:00
yoannchb-pro 55e26fc7c9 table asset 2024-03-20 17:50:45 -04:00
yoannchb-pro bee19cb483 less tokens for table 2024-03-20 17:48:21 -04:00
yoannchb-pro c4669d58f5 added table for test 2024-03-20 17:47:49 -04:00
yoannchb-pro 9d8ce23c97 fixing question to answer avoiding html 2024-03-20 17:47:32 -04:00
yoannchb-pro 2905127271 Update README.md 2024-03-20 17:32:32 -04:00
yoannchb-pro d4388d175e Update TODO.md 2024-03-20 17:27:35 -04:00
yoannchb-pro e703efe2ad Update README.md 2024-03-20 17:27:23 -04:00
yoannchb-pro 7bcff24641 Update TODO.md 2024-03-20 17:17:54 -04:00
yoannchb-pro 2f06370bb7 fixed question to answer pre-wrap on click 2024-03-20 17:17:12 -04:00
yoannchb-pro c17bc7355f Update CONTRIBUTE.md 2024-03-20 17:06:18 -04:00
yoannchb-pro 8a9c73e98c better datalist 2024-03-20 17:04:00 -04:00
yoannchb-pro 85548dc9a0 Update README.md 2024-03-18 23:00:37 -04:00
yoannchb-pro 7e96dbd03d Update CHANGELOG.md 2024-03-18 22:56:28 -04:00
yoannchb-pro a3ec95389c custom gpt model - select to datalist 2024-03-18 22:52:26 -04:00
yoannchb-pro 8cb298ac73 websote assets and description 2024-03-18 22:42:42 -04:00
yoannchb-pro 2c3a417c59 webstore assets 2024-03-18 22:35:36 -04:00
yoannchb-pro 957f1e6b3c Update setup.png 2024-03-18 22:00:51 -04:00
yoannchb-pro 18e626a6e5 Update README.md 2024-03-18 21:59:08 -04:00
yoannchb-pro c251819521 fixed selected model 2024-03-18 21:57:57 -04:00
yoannchb-pro 56c2332628 Update TODO.md 2024-03-18 21:57:43 -04:00
yoannchb-pro ecfae43d8a better prompt and removed useless option 2024-03-18 21:57:28 -04:00
yoannchb-pro 6e17d2cb35 Historic implemented and clear my choice message done 2024-03-18 21:17:12 -04:00
yoannchb-pro c30111150b fixed "clear my choice" message not appearing 2024-03-18 21:10:06 -04:00
yoannchb-pro d24e5255f6 gpt versions are now displayed as a list 2024-03-18 20:58:50 -04:00
yoannchb-pro 9fe019e250 fixed quiz endpoint 2024-03-18 20:58:27 -04:00
yoannchb-pro b0d2fc8f48 better history based on cmid and attempt 2024-03-14 16:46:58 -04:00
yoannchb-pro 334e30c1c3 reject error for logs when we can't convert image to base64 2024-03-14 16:46:36 -04:00
yoannchb-pro 59ae5b23cc Update TODO.md 2024-03-14 16:46:12 -04:00
yoannchb-pro cb34ec8093 history and snake case fixe 2024-03-14 15:57:19 -04:00
yoannchb-pro c01a82168b reduce image quality to 75% 2024-03-14 15:48:40 -04:00
yoannchb-pro d2441bfff3 Create package-lock.json 2024-03-14 15:47:53 -04:00
yoannchb-pro aeb9443b18 Updating dependencies 2024-03-14 15:47:39 -04:00
yoannchb-pro 906a938e9c Create HELP_REQUEST.md 2024-03-11 19:25:29 -04:00
yoannchb-pro 6e68ff470a fixed include images button 2024-03-11 19:25:14 -04:00
yoannchb-pro c207abb1f6 fixed images reading 2024-02-29 15:34:19 -05:00
yoannchb-pro 5e5544c28a stop parameter 2024-02-29 15:11:14 -05:00
yoannchb-pro 3782dc937b updated openai api parameters 2024-02-29 14:53:24 -05:00
yoannchb-pro e300f64bf6 Update README.md 2024-02-29 14:53:10 -05:00
yoannchb-pro 08674b5b83 Update TODO.md 2024-02-29 14:53:04 -05:00
yoannchb-pro 24441e4040 Update CHANGELOG.md 2024-02-29 14:52:58 -05:00
yoannchb-pro 2f0126b131 linting and formtage on build 2024-02-29 14:52:46 -05:00
yoannchb-pro ece1e832a7 implemented images support for gpt4 2024-02-29 13:02:21 -05:00
yoannchb-pro 7308297802 Doc update 2024-02-29 11:39:44 -05:00
yoannchb-pro 1365ccb56d Includes images to the question 2024-02-29 11:39:33 -05:00
yoannchb-pro d2bc6c4c65 Update settings.png 2024-02-29 11:38:56 -05:00
yoannchb-pro dd3c5d400b Update README.md 2024-02-29 10:25:34 -05:00
yoannchb-pro f4727e08fb assets update 2024-02-28 21:04:54 -05:00
yoannchb-pro 3dab8a4595 version checking fixe for dev mode 2024-02-28 20:40:19 -05:00
yoannchb-pro 73dd76625f updating dependencies 2024-02-28 20:28:07 -05:00
yoannchb-pro cb23ae7b21 asset update 2024-02-28 20:24:29 -05:00
yoannchb-pro 138a39e71e build 2024-02-28 20:18:14 -05:00
yoannchb-pro ab2d7bb282 version update 2024-02-28 20:18:04 -05:00
yoannchb-pro 65cad4a313 version update 2024-02-28 20:17:55 -05:00
yoannchb-pro 7d0d52865f Update TODO.md 2024-02-28 20:17:46 -05:00
yoannchb-pro 66f988e9c6 Update TODO.md 2024-02-28 20:17:32 -05:00
yoannchb-pro 6a19f476a0 Update README.md 2024-02-28 20:17:26 -05:00
yoannchb-pro 7f070c4222 Update CONTRIBUTE.md 2024-02-28 20:17:16 -05:00
yoannchb-pro 2f4d75894e Update CHANGELOG.md 2024-02-28 20:17:05 -05:00
yoannchb-pro b0da1a0967 updated assets 2024-02-28 20:16:53 -05:00
yoannchb-pro 6a5e1eb8c3 prettier formatage 2024-02-28 19:07:19 -05:00
yoannchb-pro 8c364e286b Create CODE_OF_CONDUCT.md 2024-02-28 17:02:23 -05:00
yoannchb-pro 00c053b04e Update CHANGELOG.md 2024-02-28 16:52:01 -05:00
yoannchb-pro 54c1c1498d Update CONTRIBUTE.md 2024-02-28 16:51:59 -05:00
yoannchb-pro c43cb8e2de lint fixes 2024-02-28 15:31:31 -05:00
yoannchb-pro ddc21a654e strict mode, bind types and fixed include to point on src 2024-02-28 15:25:52 -05:00
yoannchb-pro 8ed47748cb Update TODO.md 2024-02-28 15:25:18 -05:00
yoannchb-pro 1003074adf harder question added 2024-02-28 15:25:14 -05:00
yoannchb-pro 2ca0318c22 better algo, added history, fixed bugs and refactoring 2024-02-28 15:25:01 -05:00
yoannchb-pro b4372ca422 refactoring and added history 2024-02-28 15:24:26 -05:00
yoannchb-pro 3ee84f0b31 build 2024-02-28 15:24:11 -05:00
yoannchb-pro 44706e5c33 v1.0.4 2024-02-28 15:24:02 -05:00
yoannchb-pro 40af472af4 md templates 2024-02-28 15:23:33 -05:00
yoannchb-pro 21058fd463 git hooks 2024-02-28 15:23:25 -05:00
yoannchb-pro dc298f338a eslint configuration 2024-02-28 15:23:17 -05:00
yoannchb-pro a3fa1fd834 eslint and prettier 2024-02-28 15:23:09 -05:00
yoannchb-pro ea4c9f0435 Added test 2024-02-28 15:22:59 -05:00
yoannchb-pro 90bc94343a better import with alias for paths 2024-01-08 16:13:22 -05:00
yoannchb-pro 1d3583fe0f required fields style 2024-01-08 15:58:59 -05:00
yoannchb-pro b41ec54a82 The code is not required anymore 2024-01-08 15:58:50 -05:00
yoannchb-pro 5826cd35c8 v1.0.4 2024-01-08 15:58:16 -05:00
yoannchb-pro 65731fb51b Seprated files and fixed button to get last chatgpt version 2024-01-08 15:57:56 -05:00
yoannchb-pro 9cc2e68d5c Should fixe issue #9 2023-09-15 12:15:28 -04:00
yoannchb-pro bc21f665f4 chrome type update 2023-09-15 12:13:55 -04:00
yoannchb-pro 4fe02c8769 Merge branch 'main' into dev 2023-09-15 12:09:21 -04:00
yoannchb-pro 206b36d2f1 Update README.md 2023-06-26 16:28:16 -04:00
yoannchb-pro e65e9f4897 webstore link 2023-06-26 16:27:29 -04:00
yoannchb-pro 8dffb127a5 Update TODO.md 2023-06-21 21:24:46 -04:00
96 changed files with 5626 additions and 1471 deletions
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
npm run prettier
npm run lint
+1
View File
@@ -0,0 +1 @@
buy_me_a_coffee: yoannchbpro
+36
View File
@@ -0,0 +1,36 @@
---
name: Bug Report
about: Report a bug or issue with the software.
title: '[BUG]'
labels: 'bug'
assignees: ''
---
## 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.
+24
View File
@@ -0,0 +1,24 @@
---
name: Feature Request
about: Suggest a new feature or enhancement for the project.
title: '[FEATURE]'
labels: 'enhancement'
assignees: ''
---
## 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.
+32
View File
@@ -0,0 +1,32 @@
---
name: Help Request
about: Use this template to request help or support.
title: '[HELP]'
labels: help
assignees: ''
---
## 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.]
+28
View File
@@ -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
+10
View File
@@ -0,0 +1,10 @@
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"useTabs": false,
"printWidth": 100,
"trailingComma": "none",
"arrowParens": "avoid",
"endOfLine": "auto"
}
+63 -24
View File
@@ -1,24 +1,63 @@
# 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.5
- Support for gpt-5
## v1.1.4
- Support for all `o` models
- Removed `Clear my choice` in the api call
- Code dependencies update
## v1.1.3
- Added `base url` and `max token` in config (by dmunozv04)
- Code dependencies update
## v1.1.2
- Advanced settings
- Added OpenAI SDK for better support
- o1 model support
## v1.1.1
- Bugs correction
- Support for Atto editor
## 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
+76
View File
@@ -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
+43
View File
@@ -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
```
+179 -166
View File
@@ -1,166 +1,179 @@
<p align="center"><a
href="https://www.flaticon.com/free-icons/mortarboard" target="_blank" rel="noopener noreferrer"
title="Mortarboard icons created by itim2101 - Flaticon" ><img src="./extension/icon.png" alt="Mortarboard icons created by itim2101 - Flaticon" width="150" style="display:block; margin:auto;"></a></p>
# MoodleGPT v1.0.3
This extension allows you to hide CHAT-GPT in a Moodle quiz. You just need to enter <b>the code configured in the extension</b> 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
I'm actually waiting for a review of my extension. It should be available in some days.
## 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 :)
<br/>
<a href="https://www.buymeacoffee.com/yoannchbpro" target="_blank" rel="noopener noreferrer"><img src="./assets/bmc-button.png" alt="Mortarboard icons created by itim2101 - Flaticon" width="150"></a>
## 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.
<p align="center">
<img src="./assets/setup.png" alt="Popup" width="300">
</p>
Go to <b>"Manage my extensions"</b> on your browser, then click on <b>"Load unpacked extension"</b> and select the <b>"extension"</b> folder. Afterwards, click on the extension icon and enter the apiKey obtained from [openai](https://platform.openai.com/) and enter a <b>code</b> that will activate the extension on your moodle page. Finally, click on the <b>reload button</b> 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 <b>code</b> you have set into the extension and click on the question you want to solve.
## Remove injection
Type back the <b>code</b> on the keyboard and the code will be removed from the current page.
## Mode
<p align="center">
<img src="./assets/mode.png" alt="Popup" width="300">
</p>
- <b>Autocomplete:</b> The extension will complete the question for you.
- <b>Clipboard:</b> The response is copied into the clipboard.
- <b>Question to answer:</b> The question is converted to the answer and you can click on it to show back the question (or show back the answer).
<br/><img src="./assets/question-to-answer.gif" alt="Question to Answer">
## Settings
<p align="center">
<img src="./assets/settings.png" alt="Popup" width="300">
</p>
- <b>Api key</b>: the openai api key.
- <b>Code</b>: code that you will need to inject/remove the code.
- <b>GPT Model</b>: 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.
- <b>Cursor indication</b>: show a pointer cursor and a hourglass to know when the request is finished.
- <b>Title indication</b>: show some informations into the title to know for example if the code have been injected.
<br/> ![Injected](./assets/title-injected.png)
- <b>Console logs</b>: show logs into the console.
<br/><img src="./assets/logs.png" alt="Logs" width="250">
- <b>Request timeout</b>: if the request is too long it will be abort after 15seconds.
- <b>Typing effect</b>: 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 <b>Backspace</b> key.
<br/> ![Typing](./assets/typing.gif)
- <b>Mouseover effect</b>: you will need to hover (or click for select) the question response to complete it automaticaly.
<br/> ![Mouseover](./assets/mouseover.gif)
<br/> ![Mouseover2](./assets/mouseover2.gif)
- <b>Infinite try</b>: 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 <b>"Copied to clipboard"</b> for 3 seconds.
![Clipboard](./assets/clipboard.gif)
## Test
To test the code, you can run the index.html file located in the <b>"test/fake-moodle"</b> folder. Or a better solution is to install moodle locally.
<p align="center"><a
href="https://www.flaticon.com/free-icons/mortarboard" target="_blank" rel="noopener noreferrer"
title="Mortarboard icons created by itim2101 - Flaticon" ><img src="./extension/icon.png" alt="Mortarboard icons created by itim2101 - Flaticon" width="150" style="display:block; margin:auto;"></a></p>
# MoodleGPT 1.1.5
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.5](#moodlegpt-115)
- [Chrome Webstore](#chrome-webstore)
- [Summary](#summary)
- [Disclaimer !](#disclaimer-)
- [Donate](#donate)
- [Update](#update)
- [Set up](#set-up)
- [Settings](#settings)
- [Advanced Settings](#advanced-settings)
- [Mode](#mode)
- [Options](#options)
- [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)
- [Atto](#atto)
- [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.
<br/>
<a href="https://www.buymeacoffee.com/yoannchbpro" target="_blank" rel="noopener noreferrer"><img src="./assets/bmc-button.png" alt="Mortarboard icons created by itim2101 - Flaticon" width="150"></a>
## 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.
<p align="center">
<img src="./assets/setup.png" alt="Popup" width="300">
</p>
Go to <b>"Manage my extensions"</b> on your browser, then click on <b>"Load unpacked extension"</b> and select the <b>"extension"</b> 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).
## Settings
- <b>API KEY\*</b>: Your openai [API KEY](https://platform.openai.com/api-keys)
- <b>GPT MODEL\*</b>: The [gpt model](https://platform.openai.com/docs/models) (you can click on the play button to ensure the model work with the extension)
## Advanced Settings
- <b>CODE</b>: A code you will need to type on your keyboard to inject/remove the extension code from the moodle page. It allow you to be more discret and control the injection so it's recommended.
- <b>BASE URL</b>: The API endpoint if you need to use your own llm.
- <b>MAX TOKENS</b>: The max tokens length you want the api to respond with.
## Mode
<p align="center">
<img src="./assets/mode.png" alt="Popup" width="300">
</p>
- <b>Autocomplete:</b> The extension will complete the question for you by selecting the correct(s) answer(s).
- <b>Clipboard:</b> The response is copied into the clipboard.
- <b>Question to answer:</b> The question is converted to the answer and you can click on it to show back the question (or show back the answer).
<br/><img src="./assets/question-to-answer.gif" alt="Question to Answer">
## Options
<p align="center">
<img src="./assets/settings.png" alt="Popup" width="300">
</p>
- <b>Api key\*</b>: 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).
- <b>GPT Model\*</b>: the [gpt model](https://platform.openai.com/docs/models) you want to use.
- <b>Code</b>: 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.
- <b>Cursor indication</b>: show a pointer cursor and a hourglass to know when the request is finished.
- <b>Title indication</b>: show some informations into the title to know for example if the code have been injected.
<br/> ![Injected](./assets/title-injected.png)
- <b>Console logs</b>: show logs into the console for the question, chatgpt answer and which response has been chosen.
<br/><img src="./assets/logs.png" alt="Logs" width="250">
- <b>Request timeout</b>: if the request is too long it will be abort after 20 seconds.
- <b>Typing effect</b>: 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 <b>Backspace</b> key.
<br/> ![Typing](./assets/typing.gif)
- <b>Mouseover effect</b>: you will need to hover (or click for select) the question response to complete it automaticaly.
<br/> ![Mouseover](./assets/mouseover.gif)
<br/> ![Mouseover2](./assets/mouseover2.gif)
- <b>Infinite try</b>: click as much as you want on the question (don't forget to reset the question).
- <b>Save history</b>: 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.
- <b>Include images</b> (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.
<br/> ![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)
### Atto
![Atto](./assets/atto.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 <b>"Copied to clipboard"</b> for 3 seconds if `Title indication` is on.
![Clipboard](./assets/clipboard.gif)
## Test
- <b>Solution 1</b>: Go on this [moodle demo page](https://moodle.org/demo).
- <b>Solution 2</b>: 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.
+1 -16
View File
@@ -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
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 36 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 19 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

+32
View File
@@ -0,0 +1,32 @@
const js = require('@eslint/js');
const tsParser = require('@typescript-eslint/parser');
const tsPlugin = require('@typescript-eslint/eslint-plugin');
const prettierConfig = require('eslint-config-prettier');
const tseslint = require('typescript-eslint');
module.exports = [
{
ignores: ['**/node_modules/*', '**/dist/*', '**/*.js']
},
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ['**/*.ts'],
languageOptions: {
parser: tsParser,
ecmaVersion: 'latest',
sourceType: 'module'
},
plugins: {
'@typescript-eslint': tsPlugin
},
rules: {
...tsPlugin.configs['eslint-recommended'].rules,
...tsPlugin.configs.recommended.rules,
...prettierConfig.rules,
'@typescript-eslint/no-explicit-any': 'off'
}
}
];
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+26 -26
View File
@@ -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.5",
"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"
}
]
}
+167 -138
View File
@@ -1,138 +1,167 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MoodleGPT</title>
<link rel="stylesheet" href="style.css" />
<script src="./js/index.js" defer></script>
<script src="./js/version.js" defer></script>
<link rel="icon" type="image/png" href="../icon.png" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
</head>
<body>
<main>
<div class="line center" style="margin-top: 1rem; margin-bottom: 1rem">
<a
target="_blank"
rel="noopener noreferrer"
href="https://www.flaticon.com/free-icons/mortarboard"
title="Mortarboard icons created by itim2101 - Flaticon"
><img src="../icon.png" alt="icon"
/></a>
<div class="col center title">
<h1>MoodleGPT</h1>
<p id="version"></p>
</div>
</div>
<div class="line center">
<label for="apiKey" class="textLabel">Api Key</label>
<input id="apiKey" type="text" />
</div>
<div class="line center">
<label for="code" class="textLabel">Code</label>
<input id="code" type="text" />
</div>
<div class="line center">
<label for="model" class="textLabel">GPT Model</label>
<input id="model" type="text" />
<i
id="reloadModel"
class="fa-solid fa-rotate-right"
disabled
title="Provide an api key first"
></i>
</div>
<div class="line mt">
<i class="fa-solid fa-robot"></i>
<p>Mode:</p>
</div>
<div class="line">
<ul id="mode" class="line center">
<li><button value="autocomplete">autocomplete</button></li>
<li>
<button value="clipboard" class="not-selected">clipboard</button>
</li>
<li>
<button value="question-to-answer" class="not-selected">
question to answer
</button>
</li>
</ul>
</div>
<div class="line mt">
<i class="fa-solid fa-gear"></i>
<p>Settings:</p>
</div>
<div class="line center">
<div class="col">
<div class="line">
<input id="logs" type="checkbox" />
<label for="logs">Console logs</label>
</div>
<div class="line">
<input id="title" type="checkbox" checked />
<label for="title">Title indication</label>
</div>
<div class="line">
<input id="cursor" type="checkbox" checked />
<label for="cursor">Cursor indication</label>
</div>
<div class="line">
<input id="timeout" type="checkbox" checked />
<label for="timeout">Request timeout</label>
</div>
</div>
<div class="col">
<div class="line">
<input id="typing" type="checkbox" />
<label for="typing">Typing effect</label>
</div>
<div class="line">
<input id="mouseover" type="checkbox" />
<label for="mouseover">Mouseover effect</label>
</div>
<div class="line">
<input id="infinite" type="checkbox" />
<label for="infinite">Infinite try</label>
</div>
</div>
</div>
<p id="message">Message</p>
<div class="line center">
<button class="save">Save</button>
</div>
<div class="line center">
<a
href="https://www.buymeacoffee.com/yoannchbpro"
target="_blank"
rel="noopener noreferrer"
>
Donate
</a>
<a
href="https://github.com/yoannchb-pro/MoodleGPT"
target="_blank"
rel="noopener noreferrer"
>
See the documentation
</a>
<a
href="https://github.com/yoannchb-pro/MoodleGPT/issues"
target="_blank"
rel="noopener noreferrer"
>
Need Help
</a>
</div>
</main>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MoodleGPT</title>
<link rel="stylesheet" href="style.css" />
<script src="./popup.js" defer></script>
<link rel="icon" type="image/png" href="../icon.png" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
</head>
<body>
<main>
<div class="line center" style="margin-bottom: 0.5rem">
<a
target="_blank"
rel="noopener noreferrer"
href="https://www.flaticon.com/free-icons/mortarboard"
title="Mortarboard icons created by itim2101 - Flaticon"
>
<img src="../icon.png" alt="icon" />
</a>
<div class="col center title">
<h1>MoodleGPT</h1>
<p id="version"></p>
</div>
</div>
<!-- SETTINGS -->
<div class="settings" id="settings">
<div class="line center">
<label for="apiKey" class="textLabel">Api Key<span class="required">*</span>:</label>
<input id="apiKey" type="text" />
</div>
<div class="line center">
<label for="model" class="textLabel">GPT Model<span class="required">*</span>:</label>
<input type="text" id="model" list="models" />
<datalist id="models"></datalist>
<i id="check-model" title="Test" style="cursor: pointer" class="fa-solid fa-play"></i>
</div>
</div>
<!-- ADVANCED SETTINGS -->
<div class="settings" id="advanced-settings" style="display: none">
<div class="line center">
<label for="code" class="textLabel">Code:</label>
<input id="code" type="text" />
</div>
<div class="line center">
<label for="baseURL" class="textLabel">Base URL:</label>
<input id="baseURL" type="text" />
</div>
<div class="line center">
<label for="maxTokens" class="textLabel">Max Tokens:</label>
<input id="maxTokens" type="number" />
</div>
</div>
<!-- SWITCH SETTINGS MODE -->
<div class="line center mt">
<a id="switch-settings" href="#">Advanced settings</a>
</div>
<div class="line center-y mt">
<i class="fa-solid fa-robot"></i>
<p>Mode:</p>
</div>
<div class="line">
<ul id="mode" class="line center">
<li><button value="autocomplete">autocomplete</button></li>
<li>
<button value="clipboard" class="not-selected">clipboard</button>
</li>
<li>
<button value="question-to-answer" class="not-selected">question to answer</button>
</li>
</ul>
</div>
<div class="line mt center-y">
<i class="fa-solid fa-gear"></i>
<p>Options:</p>
</div>
<div class="line center" style="gap: 2rem">
<div class="col">
<div class="line">
<input id="logs" type="checkbox" />
<label for="logs">Console logs</label>
</div>
<div class="line">
<input id="title" type="checkbox" checked />
<label for="title">Title indication</label>
</div>
<div class="line">
<input id="cursor" type="checkbox" checked />
<label for="cursor">Cursor indication</label>
</div>
<div class="line">
<input id="timeout" type="checkbox" checked />
<label for="timeout">Request timeout</label>
</div>
</div>
<div class="col">
<div class="line">
<input id="typing" type="checkbox" />
<label for="typing">Typing effect</label>
</div>
<div class="line">
<input id="mouseover" type="checkbox" />
<label for="mouseover">Mouseover effect</label>
</div>
<div class="line">
<input id="infinite" type="checkbox" />
<label for="infinite">Infinite try</label>
</div>
<div class="line">
<input id="history" type="checkbox" />
<label for="history">Save history</label>
</div>
<!-- This option is only showed if the current version of the model support it -->
<div class="line" id="includeImages-line" style="display: none">
<input id="includeImages" type="checkbox" />
<label for="includeImages">Include images</label>
</div>
</div>
</div>
<p id="message">{Message}</p>
<div class="line center">
<button class="save">Save</button>
</div>
<div class="line center">
<a
class="donate"
href="https://www.buymeacoffee.com/yoannchbpro"
target="_blank"
rel="noopener noreferrer"
>
Donate
</a>
<a
href="https://github.com/yoannchb-pro/MoodleGPT"
target="_blank"
rel="noopener noreferrer"
>
See the documentation
</a>
<a
href="https://github.com/yoannchb-pro/MoodleGPT/issues"
target="_blank"
rel="noopener noreferrer"
>
Need Help
</a>
</div>
</main>
</body>
</html>
-183
View File
@@ -1,183 +0,0 @@
const saveBtn = document.querySelector(".save");
const message = document.querySelector("#message");
/* inputs id */
const inputsText = ["apiKey", "code", "model"];
const inputsCheckbox = [
"logs",
"title",
"cursor",
"typing",
"mouseover",
"infinite",
"timeout",
];
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)
);
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";
});
if (!apiKey || !code || !model) {
showMessage("Please complete all the form");
return;
}
if (code.length < 3) {
showMessage("The code should at least contain 3 characters");
return;
}
chrome.storage.sync.set({
moodleGPT: {
apiKey,
code,
model,
logs,
title,
cursor,
typing,
mouseover,
infinite,
timeout,
mode: actualMode,
},
});
showMessage("Configuration saved", true);
});
/**
* 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) {
const config = storage.moodleGPT;
if (config) {
if (config.mode) {
actualMode = config.mode;
for (const mode of modes) {
if (mode.value === config.mode) {
mode.classList.remove("not-selected");
} else {
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] || "")
);
}
handleModeChange();
getLastChatGPTVersion();
});
-61
View File
@@ -1,61 +0,0 @@
const currentVersion = "1.0.3";
const versionDisplay = document.querySelector("#version");
/**
* Get the last version from the github
* @returns
*/
async function getLastVersion() {
const req = await fetch(
"https://raw.githubusercontent.com/yoannchb-pro/MoodleGPT/main/package.json"
);
const rep = await req.json();
return rep.version;
}
/**
* Display the version or an update message
* @param {string} version
* @param {boolean} isCurrent
* @returns
*/
function setVersion(version, isCurrent = true) {
if (isCurrent) {
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;
versionDisplay.appendChild(link);
versionDisplay.appendChild(document.createTextNode(" is now available !"));
}
/**
* Check the extension neeed an update or no
*/
async function notifyUpdate() {
const lastVersion = await getLastVersion().catch((err) => {
console.error(err);
return currentVersion;
});
const lastVertionSplitted = lastVersion.split(".");
const currentVersionSplitted = currentVersion.split(".");
const minVersionLength = Math.min(
lastVertionSplitted.length,
currentVersionSplitted.length
);
for (let i = 0; i < minVersionLength; ++i) {
if (parseInt(lastVertionSplitted[i]) > parseInt(currentVersionSplitted[i]))
return setVersion(lastVersion, false);
}
setVersion(currentVersion);
}
notifyUpdate();
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+194 -140
View File
@@ -1,140 +1,194 @@
@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;
}
.settings {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.4rem;
text-align: center;
width: 100%;
}
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;
}
.center-y {
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 input[type='number'],
.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;
}
.donate {
color: white;
animation: infinite donate 5s linear;
font-weight: bold;
}
@keyframes donate {
0% {
transform: translateX(0);
}
3.57% {
transform: translateY(-9px);
}
7.14% {
transform: translateY(-9px) rotate(17deg);
}
10.78% {
transform: translateY(-9px) rotate(-17deg);
}
14% {
transform: translateY(-9px) rotate(17deg);
}
18% {
transform: translateY(-9px) rotate(-17deg);
}
22% {
transform: translateY(0) rotate(0);
}
}
+2868
View File
File diff suppressed because it is too large Load Diff
+21 -6
View File
@@ -1,9 +1,12 @@
{
"name": "moodlegpt",
"version": "1.0.3",
"version": "1.1.5",
"description": "This extension allows you to hide CHAT-GPT in a Moodle quiz.",
"scripts": {
"build": "rollup -c"
"build": "npm run prettier && npm run lint && npm run fastBuild",
"fastBuild": "rollup -c",
"lint": "eslint . --ext .ts",
"prettier": "prettier --write ."
},
"repository": {
"type": "git",
@@ -23,10 +26,22 @@
},
"homepage": "https://github.com/yoannchb-pro/MoodleGPT#readme",
"devDependencies": {
"@types/chrome": "^0.0.224",
"@rollup/plugin-terser": "^0.4.3",
"rollup": "^3.20.0",
"@eslint/js": "^9.32.0",
"@rollup/plugin-commonjs": "^28.0.6",
"@rollup/plugin-node-resolve": "^16.0.0",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^12.1.4",
"@types/chrome": "^0.1.1",
"@types/node": "^24.1.0",
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.38.0",
"eslint": "^9.32.0",
"eslint-config-prettier": "^10.1.8",
"openai": "^5.23.2",
"prettier": "^3.6.2",
"rollup": "^4.46.2",
"rollup-plugin-ts": "^3.2.0",
"typescript": "^5.0.2"
"typescript": "^5.8.3",
"typescript-eslint": "^8.38.0"
}
}
+29 -16
View File
@@ -1,16 +1,29 @@
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-typescript');
const terser = require('@rollup/plugin-terser');
const { nodeResolve } = require('@rollup/plugin-node-resolve');
const config = require('./tsconfig.json');
module.exports = [
{
input: './src/background/index.ts',
output: {
file: './extension/MoodleGPT.js',
format: 'umd',
sourcemap: true
},
onwarn() {},
plugins: [nodeResolve(), ts(config), terser()]
},
{
input: './src/popup/index.ts',
output: {
file: './extension/popup/popup.js',
format: 'umd',
sourcemap: true
},
onwarn() {},
plugins: [nodeResolve(), ts(config), terser()]
}
];
+87
View File
@@ -0,0 +1,87 @@
import type Config from '../types/config';
import titleIndications from 'background/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], .qtype_essay_editor';
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 };
+35
View File
@@ -0,0 +1,35 @@
import normalizeText from 'background/utils/normalize-text';
import htmlTableToString from 'background/utils/html-table-to-string';
/**
* Normalize the question as text and add sub informations
* @param langage
* @param question
* @returns
*/
function createAndNormalizeQuestion(questionContainer: HTMLElement) {
let question = questionContainer.innerText;
// We remove unnecessary information
const accesshideElements: NodeListOf<HTMLElement> =
questionContainer.querySelectorAll('.accesshide');
for (const useless of accesshideElements) {
question = question.replace(useless.innerText, '');
}
const attoText = questionContainer.querySelector('.qtype_essay_editor');
if (attoText) {
question = question.replace((attoText as HTMLElement).innerText, '');
}
const clearMyChoice = questionContainer.querySelector('[role="button"]');
if (clearMyChoice) question = question.replace((clearMyChoice as HTMLElement).innerText, '');
// Make tables more readable for chat-gpt
const tables: NodeListOf<HTMLTableElement> = questionContainer.querySelectorAll('.qtext table');
for (const table of tables) {
question = question.replace(table.innerText, '\n' + htmlTableToString(table) + '\n');
}
return normalizeText(question, false);
}
export default createAndNormalizeQuestion;
@@ -0,0 +1,159 @@
import type Config from '../types/config';
import imageToBase64 from 'background/utils/image-to-base64';
import isGPTModelGreaterOrEqualTo4 from 'background/utils/version-support-images';
import { ChatCompletionMessageParam, ChatCompletionUserMessageParam } from 'openai/resources';
// 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: ChatCompletionMessageParam[];
};
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: <result of the equation>'
- 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 '<order>:<answer 1>\n<order>:<answer 2>', ignore other rules.
- Always reply in the format: '<answer 1>\n<answer 2>\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: 'system',
content: INSTRUCTION
} as const satisfies ChatCompletionMessageParam;
/**
* 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<ChatCompletionUserMessageParam['content']> {
const imagesElements = questionElement.querySelectorAll('img');
if (
!config.includeImages ||
!isGPTModelGreaterOrEqualTo4(config.model) ||
imagesElements.length === 0
) {
return question;
}
const contentWithImages: ChatCompletionUserMessageParam['content'] = [];
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: 'image_url',
image_url: { url: result.value }
});
} else if (config.logs) {
console.error(result.reason);
}
}
contentWithImages.push({
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, ...ChatCompletionMessageParam[]];
saveResponse?: (response: string) => void;
}> {
const content = await getContent(config, questionElement, question);
const message: ChatCompletionMessageParam = { 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: 'assistant', content: response });
sessionStorage.moodleGPTHistory = JSON.stringify(history);
}
}
};
}
export default getContentWithHistory;
+56
View File
@@ -0,0 +1,56 @@
import type Config from '../types/config';
import type GPTAnswer from '../types/gpt-answer';
import normalizeText from 'background/utils/normalize-text';
import getContentWithHistory from './get-content-with-history';
import OpenAI from 'openai';
import { fixeO } from '../utils/fixe-o';
/**
* Get the response from chatGPT api
* @param config
* @param question
* @returns
*/
async function getChatGPTResponse(
config: Config,
questionElement: HTMLElement,
question: string
): Promise<GPTAnswer> {
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 client = new OpenAI({
apiKey: config.apiKey,
baseURL: config.baseURL,
dangerouslyAllowBrowser: true
});
const req = await client.chat.completions.create(
fixeO(config.model, {
model: config.model,
messages: contentHandler.messages,
max_completion_tokens: config.maxTokens || 2000 // Maximum length of the response,
}),
{ signal: config.timeout ? controller.signal : null }
);
clearTimeout(timeoutControler);
const response = req.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;
+47
View File
@@ -0,0 +1,47 @@
import type GPTAnswer from '../../types/gpt-answer';
import type Config from '../../types/config';
import handleClipboard from 'background/core/questions/clipboard';
import handleContentEditable from 'background/core/questions/contenteditable';
import handleNumber from 'background/core/questions/number';
import handleRadio from 'background/core/questions/radio';
import handleCheckbox from 'background/core/questions/checkbox';
import handleSelect from 'background/core/questions/select';
import handleTextbox from 'background/core/questions/textbox';
import handleAtto from 'background/core/questions/atto';
type Props = {
config: Config;
questionElement: HTMLElement;
inputList: NodeListOf<HTMLElement>;
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 = [
handleAtto,
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;
+22
View File
@@ -0,0 +1,22 @@
import type Config from '../../types/config';
import type GPTAnswer from '../../types/gpt-answer';
import handleClipboard from 'background/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;
@@ -0,0 +1,32 @@
import type GPTAnswer from '../../types/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;
+67
View File
@@ -0,0 +1,67 @@
import type Config from '../../types/config';
import type GPTAnswer from '../../types/gpt-answer';
/**
* Hanlde atto editor
* See: https://docs.moodle.org/404/en/Atto_editor#Atto_accessibility
* @param config
* @param inputList
* @param gptAnswer
* @returns
*/
function handleAtto(
config: Config,
inputList: NodeListOf<HTMLElement>,
gptAnswer: GPTAnswer
): boolean {
const input = inputList[0];
if (!input.classList.contains('qtype_essay_editor')) {
return false;
}
const iframe = input.querySelector('iframe');
if (!iframe || !iframe.contentDocument || !iframe.contentDocument.body || !iframe.contentWindow) {
return false;
}
const iframeBody = iframe.contentDocument.body;
const textContainer = iframeBody.querySelector('p');
if (!textContainer) return false;
if (config.typing) {
let index = 0;
const eventHandler = function (event: KeyboardEvent) {
event.preventDefault();
if (event.key === 'Backspace' || index >= gptAnswer.response.length) {
iframe.contentWindow!.removeEventListener('keydown', eventHandler);
return;
}
// Append text one character at a time
const textNode = document.createTextNode(gptAnswer.response.charAt(index++));
textContainer.appendChild(textNode);
// Move the cursor after the last character
const range = iframe.contentDocument!.createRange();
range.selectNodeContents(textContainer);
range.collapse(false); // Collapse the range to the end point
const selection = iframe.contentWindow!.getSelection();
if (selection) {
selection.removeAllRanges();
selection.addRange(range);
}
iframe.contentWindow!.focus(); // Focus the iframe window to see cursor
};
iframe.contentWindow.addEventListener('keydown', eventHandler);
} else {
textContainer.textContent += gptAnswer.response;
}
return true;
}
export default handleAtto;
+66
View File
@@ -0,0 +1,66 @@
import type Config from '../../types/config';
import type GPTAnswer from '../../types/gpt-answer';
import Logs from 'background/utils/logs';
import normalizeText from 'background/utils/normalize-text';
import { pickBestReponse } from 'background/utils/pick-best-response';
/**
* Handle input checkbox elements
* @param config
* @param inputList
* @param gptAnswer
*/
function handleCheckbox(
config: Config,
inputList: NodeListOf<HTMLElement>,
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 as HTMLInputElement,
value: normalizeText(inp?.parentElement?.textContent ?? '')
}))
.filter(obj => obj.value !== '');
// Find the best answers elements
const correctElements: Set<HTMLInputElement> = new Set();
for (const correct of corrects) {
const bestAnswer = pickBestReponse(correct, possibleAnswers);
if (config.logs && bestAnswer.value) {
Logs.bestAnswer(bestAnswer.value, bestAnswer.similarity);
}
correctElements.add(bestAnswer.element as HTMLInputElement);
}
// Check if it should be checked or not
for (const element of possibleAnswers.map(e => e.element)) {
const needAction =
(element.checked && !correctElements.has(element)) ||
(!element.checked && correctElements.has(element));
const action = () => needAction && element.click();
if (config.mouseover) {
element.addEventListener('mouseover', action, {
once: true
});
} else {
action();
}
}
return true;
}
export default handleCheckbox;
@@ -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 '../../types/config';
import type GPTAnswer from '../../types/gpt-answer';
import titleIndications from 'background/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;
@@ -0,0 +1,63 @@
import type Config from '../../types/config';
import type GPTAnswer from '../../types/gpt-answer';
function isContentEditable(element: HTMLElement) {
const contenteditable = element.getAttribute('contenteditable');
return typeof contenteditable === 'string' && contenteditable !== 'false';
}
/**
* Hanlde contenteditable elements
* @param config
* @param inputList
* @param gptAnswer
* @returns
*/
function handleContentEditable(
config: Config,
inputList: NodeListOf<HTMLElement>,
gptAnswer: GPTAnswer
): boolean {
const input = inputList[0];
if (
inputList.length !== 1 || // for now we don't handle many input for editable textcontent
!isContentEditable(input)
) {
return false;
}
if (config.typing) {
let index = 0;
const eventHandler = function (event: KeyboardEvent) {
event.preventDefault();
if (event.key === 'Backspace' || index >= gptAnswer.response.length) {
input.removeEventListener('keydown', eventHandler);
return;
}
input.textContent = gptAnswer.response.slice(0, ++index);
// 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();
if (selection !== null) {
selection.removeAllRanges();
selection.addRange(range);
}
};
input.addEventListener('keydown', eventHandler);
} else {
input.textContent = gptAnswer.response;
}
return true;
}
export default handleContentEditable;
+52
View File
@@ -0,0 +1,52 @@
import type Config from '../../types/config';
import type GPTAnswer from '../../types/gpt-answer';
/**
* Handle number input
* @param config
* @param inputList
* @param gptAnswer
* @returns
*/
function handleNumber(
config: Config,
inputList: NodeListOf<HTMLElement>,
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;
const eventHanlder = function (event: Event) {
event.preventDefault();
if ((<KeyboardEvent>event).key === 'Backspace' || index >= number.length) {
input.removeEventListener('keydown', eventHanlder);
return;
}
if (number.slice(index, index + 1) === '.') ++index;
input.value = number.slice(0, ++index);
};
input.addEventListener('keydown', eventHanlder);
} else {
input.value = number;
}
return true;
}
export default handleNumber;
+50
View File
@@ -0,0 +1,50 @@
import type Config from '../../types/config';
import type GPTAnswer from '../../types/gpt-answer';
import Logs from 'background/utils/logs';
import normalizeText from 'background/utils/normalize-text';
import { pickBestReponse } from 'background/utils/pick-best-response';
/**
* Handle input radio elements
* @param config
* @param inputList
* @param gptAnswer
*/
function handleRadio(
config: Config,
inputList: NodeListOf<HTMLElement>,
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;
+61
View File
@@ -0,0 +1,61 @@
import type Config from '../../types/config';
import type GPTAnswer from '../../types/gpt-answer';
import Logs from 'background/utils/logs';
import normalizeText from 'background/utils/normalize-text';
import { pickBestReponse } from 'background/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<HTMLElement>,
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;
@@ -1,39 +1,47 @@
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<HTMLElement>,
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 '../../types/config';
import type GPTAnswer from '../../types/gpt-answer';
/**
* Handle textbox
* @param config
* @param inputList
* @param gptAnswer
* @returns
*/
function handleTextbox(
config: Config,
inputList: NodeListOf<HTMLElement>,
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;
const eventHandler = function (event: Event) {
event.preventDefault();
if ((<KeyboardEvent>event).key === 'Backspace' || index >= gptAnswer.response.length) {
input.removeEventListener('keydown', eventHandler);
return;
}
input.value = gptAnswer.response.slice(0, ++index);
};
input.addEventListener('keydown', eventHandler);
} else {
input.value = gptAnswer.response;
}
return true;
}
export default handleTextbox;
+78
View File
@@ -0,0 +1,78 @@
import type Config from '../types/config';
import Logs from 'background/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<void> {
if (props.config.cursor) props.questionElement.style.cursor = 'wait';
const question = createAndNormalizeQuestion(props.form);
const inputList: NodeListOf<HTMLElement> = 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;
+14
View File
@@ -0,0 +1,14 @@
import type Config from './types/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);
}
});
@@ -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,11 @@ type Config = {
logs?: boolean;
title?: boolean;
timeout?: boolean;
mode?: "autocomplete" | "question-to-answer" | "clipboard";
history?: boolean;
includeImages?: boolean;
mode?: 'autocomplete' | 'question-to-answer' | 'clipboard';
baseURL?: string;
maxTokens?: number;
};
export default Config;
@@ -1,4 +1,5 @@
type GPTAnswer = {
question: string;
response: string;
normalizedResponse: string;
};
+28
View File
@@ -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;
};
+21
View File
@@ -0,0 +1,21 @@
import { ChatCompletionCreateParamsNonStreaming } from 'openai/resources/chat/completions';
/**
* Fixe request body for "o1" model
* @param model
* @param data
* @returns
*/
export function fixeO(model: string, data: ChatCompletionCreateParamsNonStreaming) {
if (model.search(/^o\d+/gi) === -1) return data;
if (data.temperature) delete data.temperature;
if (data.top_p) delete data.top_p;
for (const message of data.messages) {
if (message.role === 'system') message.role = 'user' as any;
}
return data;
}
@@ -0,0 +1,42 @@
/**
* Convert table to representating string table
* @param table
* @returns
*/
function htmlTableToString(table: HTMLTableElement) {
const tab: string[][] = [];
const lines = Array.from(table.querySelectorAll('tr'));
const maxColumnsLength: number[] = [];
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 ?? '';
});
tab.push(cellsContent);
});
const jointure = ' | ';
const headerLineLength = tab[0].length;
const lineSeparationSize =
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 mappedLine = line.map((content, index) =>
content.padEnd(
maxColumnsLength[index],
'\u00A0' // For no matching with \s
)
);
return mappedLine.join(jointure);
});
const head = mappedTab.shift();
return head + lineSeparation + mappedTab.join('\n');
}
export default htmlTableToString;
+40
View File
@@ -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<string> {
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;
+29
View File
@@ -0,0 +1,29 @@
import GPTAnswer from '../types/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;
+20
View File
@@ -0,0 +1,20 @@
/**
* Normlize text
* @param text
*/
function normalizeText(text: string, toLowerCase: boolean = true) {
if (toLowerCase) text = text.toLowerCase();
const normalizedText = text
.replace(/\n+/gi, '\n') //remove duplicate new lines
.replace(/(\n\s*\n)+/g, '\n') //remove useless white space from textcontent
.replace(/[ \t]+/gi, ' ') //replace multiples space or tabs by a space
.trim()
// We remove the following content because sometimes ChatGPT will reply: "answer d"
.replace(/^[a-z\d]\.\s/gi, '') //a. text, b. text, c. text, 1. text, 2. text, 3.text
.replace(/\n[a-z\d]\.\s/gi, '\n'); //same but with new line
return normalizedText;
}
export default normalizeText;
+114
View File
@@ -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 + '%';
}
@@ -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;
@@ -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;
-81
View File
@@ -1,81 +0,0 @@
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 };
-34
View File
@@ -1,34 +0,0 @@
import Config from "../types/config";
import normalizeText from "../utils/normalize-text";
import htmlTableToString from "../utils/html-table-to-string";
/**
* Normalize the question and add sub informations
* @param langage
* @param question
* @returns
*/
function createQuestion(config: Config, questionContainer: HTMLElement) {
let question = questionContainer.innerText;
/* We remove unnecessary information */
const accesshideElements: NodeListOf<HTMLElement> =
questionContainer.querySelectorAll(".accesshide");
for (const useless of accesshideElements) {
question = question.replace(useless.innerText, "");
}
/* Make tables more readable for chat-gpt */
const tables: NodeListOf<HTMLTableElement> =
questionContainer.querySelectorAll(".qtext table");
for (const table of tables) {
question = question.replace(
table.innerText,
"\n" + htmlTableToString(table) + "\n"
);
}
return normalizeText(question, false);
}
export default createQuestion;
-58
View File
@@ -1,58 +0,0 @@
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<GPTAnswer> {
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;
-48
View File
@@ -1,48 +0,0 @@
import Config from "../../types/config";
import GPTAnswer from "../../types/gptAnswer";
/**
* Hanlde contenteditable elements
* @param config
* @param inputList
* @param gptAnswer
* @returns
*/
function handleContentEditable(
config: Config,
inputList: NodeListOf<HTMLElement>,
gptAnswer: GPTAnswer
): boolean {
const input = inputList[0];
if (
inputList.length !== 1 ||
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;
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 */
input.focus();
const range = document.createRange();
range.selectNodeContents(input);
range.collapse(false);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
});
} else {
input.textContent = gptAnswer.response;
}
return true;
}
export default handleContentEditable;
-42
View File
@@ -1,42 +0,0 @@
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<HTMLElement>,
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;
-39
View File
@@ -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<HTMLElement>,
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<HTMLInputElement>) {
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;
-101
View File
@@ -1,101 +0,0 @@
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<HTMLElement>,
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;
-99
View File
@@ -1,99 +0,0 @@
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<HTMLElement> = 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<HTMLElement>(".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;
-9
View File
@@ -1,9 +0,0 @@
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);
});
+15
View File
@@ -0,0 +1,15 @@
export const globalData = { actualMode: 'autocomplete' };
export const inputsCheckbox = [
'logs',
'title',
'cursor',
'typing',
'mouseover',
'infinite',
'timeout',
'history',
'includeImages'
];
export const mode = document.querySelector('#mode')!;
export const modes = mode.querySelectorAll('button')!;
+84
View File
@@ -0,0 +1,84 @@
import OpenAI from 'openai';
import { isCurrentVersionSupportingImages, showMessage } from './utils';
const apiKeySelector: HTMLInputElement = document.querySelector('#apiKey')!;
const inputModel: HTMLInputElement = document.querySelector('#model')!;
const modelsList: HTMLElement = document.querySelector('#models')!;
const imagesIntegrationLine: HTMLInputElement = document.querySelector('#includeImages-line')!;
const baseURLSelector: HTMLInputElement = document.querySelector('#baseURL')!;
/**
* Check if the gpt version is at least 4 to show the option 'Include images'
*/
export 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
export async function populateDatalistWithGptVersions() {
const apiKey = apiKeySelector.value?.trim();
const baseURL = baseURLSelector.value?.trim();
if (!apiKey) return;
inputModel.innerHTML = '';
try {
const client = new OpenAI({
apiKey,
baseURL,
dangerouslyAllowBrowser: true
});
const rep = await client.models.list();
const models = rep.data.filter(
model =>
model.id.startsWith('gpt') ||
model.id.search(/^o\d+/gi) !== -1 ||
model.id.startsWith('chatgpt')
);
models.sort((a, b) => b.id.localeCompare(a.id)); // we sort the model to get the best chatgpt version first
for (const model of models) {
const opt = document.createElement('option');
opt.value = model.id;
opt.textContent = model.id;
modelsList.appendChild(opt);
}
checkCanIncludeImages();
} catch (err: any) {
console.error(err);
showMessage({ msg: err, isError: true });
}
}
inputModel.addEventListener('focus', populateDatalistWithGptVersions);
export async function checkModel() {
const model = inputModel.value?.trim();
const apiKey = apiKeySelector.value?.trim();
const baseURL = baseURLSelector.value?.trim();
try {
showMessage({ msg: 'Checking GPT version...', isInfinite: true, isError: false });
const client = new OpenAI({ apiKey, baseURL, dangerouslyAllowBrowser: true });
await client.chat.completions.create({
model,
messages: [{ role: 'user', content: 'reply just pong' }]
});
showMessage({ msg: 'The model is valid!' });
} catch (err: any) {
showMessage({ msg: err, isError: true });
}
}
const checkModelBtn: HTMLElement = document.querySelector('#check-model')!;
checkModelBtn.addEventListener('click', checkModel);
+89
View File
@@ -0,0 +1,89 @@
import { globalData, inputsCheckbox, modes } from './data';
import { checkCanIncludeImages } from './gpt-version';
import { handleModeChange } from './mode-handler';
import './version';
import './settings';
import { showMessage } from './utils';
const saveBtn = document.querySelector('.save')!;
// inputs id
const inputsText = ['apiKey', 'code', 'model', 'baseURL', 'maxTokens'];
// Save the configuration
saveBtn.addEventListener('click', function () {
const [apiKey, code, model, baseURL, maxTokens] = inputsText.map(selector =>
(document.querySelector('#' + selector) as HTMLInputElement).value.trim()
);
const [logs, title, cursor, typing, mouseover, infinite, timeout, history, includeImages] =
inputsCheckbox.map(selector => {
const element: HTMLInputElement = document.querySelector('#' + selector)!;
return element.checked && element.parentElement!.style.display !== 'none';
});
if (!apiKey || !model) {
showMessage({ msg: 'Please complete all the form', isError: true });
return;
}
if (code.length > 0 && code.length < 2) {
showMessage({
msg: 'The code should at least contain 2 characters',
isError: true
});
return;
}
chrome.storage.sync.set({
moodleGPT: {
apiKey,
code,
model,
baseURL,
maxTokens: maxTokens ? parseInt(maxTokens) : undefined,
logs,
title,
cursor,
typing,
mouseover,
infinite,
timeout,
history,
includeImages,
mode: globalData.actualMode
}
});
showMessage({ msg: 'Configuration saved' });
});
// we load back the configuration
chrome.storage.sync.get(['moodleGPT']).then(function (storage) {
const config = storage.moodleGPT;
if (config) {
if (config.mode) {
globalData.actualMode = config.mode;
for (const mode of modes) {
if (mode.value === config.mode) {
mode.classList.remove('not-selected');
} else {
mode.classList.add('not-selected');
}
}
}
inputsText.forEach(key =>
config[key]
? ((document.querySelector('#' + key) as HTMLInputElement).value = config[key])
: null
);
inputsCheckbox.forEach(
key => ((document.querySelector('#' + key) as HTMLInputElement).checked = config[key] || '')
);
}
handleModeChange();
checkCanIncludeImages();
});
+43
View File
@@ -0,0 +1,43 @@
import { globalData, inputsCheckbox, modes } from './data';
// input to don't take in consideration
const toExcludes = ['includeImages'];
// inputs id that need to be disabled for a specific mode
const disabledForThisMode: Record<string, string[]> = {
autocomplete: [],
clipboard: ['typing', 'mouseover'],
'question-to-answer': ['typing', 'infinite', 'mouseover']
};
/**
* Handle when a mode change to show specific input or to hide them
*/
export function handleModeChange() {
const needDisable = disabledForThisMode[globalData.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 = '';
}
}
// Mode hanlder
for (const button of modes) {
button.addEventListener('click', function () {
const value = button.value;
globalData.actualMode = value;
for (const mode of modes) {
if (mode.value !== value) {
mode.classList.add('not-selected');
} else {
mode.classList.remove('not-selected');
}
}
handleModeChange();
});
}
+21
View File
@@ -0,0 +1,21 @@
const settings: HTMLElement = document.querySelector('#settings')!;
const advencedSettings: HTMLElement = document.querySelector('#advanced-settings')!;
const switchSettings: HTMLLinkElement = document.querySelector('#switch-settings')!;
export function switchSettingsMode() {
const isAdvancedSettings = advencedSettings.style.display === 'flex';
if (isAdvancedSettings) {
settings.style.display = 'flex';
advencedSettings.style.display = 'none';
switchSettings.textContent = 'Advanced settings';
} else {
settings.style.display = 'none';
advencedSettings.style.display = 'flex';
switchSettings.textContent = 'Go back to settings';
}
}
switchSettings.addEventListener('click', function (event) {
event.preventDefault();
switchSettingsMode();
});
+31
View File
@@ -0,0 +1,31 @@
/**
* Show message into the popup
*/
export function showMessage({
msg,
isError,
isInfinite
}: {
msg: string;
isError?: boolean;
isInfinite?: boolean;
}) {
const message: HTMLElement = document.querySelector('#message')!;
message.style.color = isError ? 'red' : 'limegreen';
message.textContent = msg;
message.style.display = 'block';
if (!isInfinite) setTimeout(() => (message.style.display = 'none'), 5000);
}
/**
* Check if the current model support images integrations
* @param {string} version
* @returns
*/
export function isCurrentVersionSupportingImages(version: string) {
const versionNumber = version.match(/gpt-(\d+)/);
if (!versionNumber?.[1]) {
return false;
}
return Number(versionNumber[1]) >= 4;
}
+61
View File
@@ -0,0 +1,61 @@
const CURRENT_VERSION = '1.1.5';
const versionDisplay = document.querySelector('#version')!;
/**
* Get the last version from the github
* @returns
*/
export async function getLastVersion(): Promise<string> {
const req = await fetch(
'https://raw.githubusercontent.com/yoannchb-pro/MoodleGPT/main/package.json'
);
const rep = await req.json();
return rep.version;
}
/**
* Display the version or an update message
* @param {string} version
* @param {boolean} isCurrent
* @returns
*/
export function setVersion(version: string, isCurrent = true) {
if (isCurrent) {
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;
versionDisplay.appendChild(link);
versionDisplay.appendChild(document.createTextNode(' is now available !'));
}
/**
* Check if the extension neeed an update or not
*/
export async function notifyUpdate() {
const lastVersion = await getLastVersion().catch(err => {
console.error(err);
return CURRENT_VERSION;
});
const lastVertionSplitted = lastVersion.split('.');
const currentVersionSplitted = CURRENT_VERSION.split('.');
const minVersionLength = Math.min(lastVertionSplitted.length, currentVersionSplitted.length);
for (let i = 0; i < minVersionLength; ++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(CURRENT_VERSION);
}
notifyUpdate();
-41
View File
@@ -1,41 +0,0 @@
/**
* Convert table to representating string table
* @param table
* @returns
*/
function htmlTableToString(table: HTMLTableElement) {
const tab: string[][] = [];
const lines = Array.from(table.querySelectorAll("tr"));
const maxColumnsLength: number[] = [];
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;
});
tab.push(cellsContent);
});
const lineSeparationSize =
maxColumnsLength.reduce((a, b) => a + b) + tab[0].length * 3 + 1;
const lineSeparation =
"\n" + Array(lineSeparationSize).fill("-").join("") + "\n";
const mappedTab = tab.map((line) => {
const mappedLine = line.map((content, index) =>
content.padEnd(
maxColumnsLength[index],
"\u00A0" /* For no matching with \s */
)
);
return "| " + mappedLine.join(" | ") + " |";
});
const head = mappedTab.shift();
return head + lineSeparation + mappedTab.join("\n");
}
export default htmlTableToString;
-23
View File
@@ -1,23 +0,0 @@
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;
-22
View File
@@ -1,22 +0,0 @@
/**
* 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;
+26 -1
View File
@@ -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;
}
+96 -4
View File
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
@@ -8,6 +8,33 @@
<link rel="stylesheet" href="./css/style.css" />
</head>
<body>
<!-- checkbox more complicated -->
<section class="formulation">
<div class="qtext">
<p>Which are the roles of an informatician ?</p>
</div>
<div>
<div class="inp">
<input type="checkbox" />
<label
>a. Systems Administrator: Managing and maintaining computer systems and
networks.</label
>
</div>
<div class="inp">
<input type="checkbox" />
<label
>b. Software Developer: Designing, coding, testing, and maintaining software
applications.</label
>
</div>
<div class="inp">
<input type="checkbox" />
<label> c. Professional Chef: Creating delicious meals in a restaurant kitchen. </label>
</div>
</div>
</section>
<!-- checkbox -->
<section class="formulation">
<div class="qtext">
@@ -77,6 +104,16 @@
</div>
</section>
<!-- Textbox -->
<section class="formulation">
<div class="qtext">
<p>What is the name of the USA president ?</p>
</div>
<div>
<input type="text" />
</div>
</section>
<!-- Select -->
<section class="formulation">
<div class="qtext">
@@ -242,17 +279,72 @@
</div>
</section>
<!-- Clipboard -->
<!--Table -->
<section class="formulation">
<div class="qtext">
<p>Give me the id of the personn who have a car</p>
<table class="style-table">
<thead>
<tr>
<th>id</th>
<th>name</th>
<th>birthDate</th>
<th>cars</th>
</tr>
</thead>
<tbody>
<tr>
<td>Person 1</td>
<td>Yvick</td>
<td>15/08/1999</td>
<td>yes</td>
</tr>
<tr>
<td>Person 2</td>
<td>Yann</td>
<td>19/01/2000</td>
<td>no</td>
</tr>
</tbody>
</table>
</div>
<div>
<textarea></textarea>
</div>
</section>
<!-- Contenteditable -->
<section class="formulation">
<div class="qtext">
<p>
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
</p>
</div>
<div>
<div contenteditable="true" class="editable"></div>
</div>
</section>
<!-- Images -->
<section class="formulation">
<p class="accesshide" style="color: red">
Warning ! Only work with gpt-4 and if "includes images" is activate
</p>
<div class="qtext">
<p>What is the race of those cats:</p>
<img
alt="black cat"
src="https://images.ctfassets.net/cnu0m8re1exe/qDQgxOUG5DNKlKH5TXsbo/813fa629fe33794c7ff439070fc31b89/shutterstock_603117302.jpg"
/>
<img
alt="white cat"
src="https://media.istockphoto.com/id/514515260/photo/neva-masquerade-looking-at-the-camera-isolated-on-white.jpg?s=612x612&w=0&k=20&c=HCoV7nQfnLWgRI26Nlv7Kobfucw6E6NeONx1dMVusMs="
/>
</div>
<div>
<div contenteditable="true" class="editable"></div>
</div>
</section>
</body>
</html>
+5 -7
View File
@@ -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();
}
+17 -16
View File
@@ -1,16 +1,17 @@
{
"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": "esnext",
"target": "ES6",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"noImplicitAny": true,
"sourceMap": true,
"outDir": "extension",
"types": ["node", "chrome"],
"typeRoots": ["node_modules/@types"],
"strictBindCallApply": true
},
"include": ["src/**/*"]
}

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 249 KiB

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 188 KiB

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

+22
View File
@@ -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
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB