Compare commits
130 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b45a5d5ce | |||
| d36949b42f | |||
| e7f00359a1 | |||
| 03bc4e64e4 | |||
| 46c5b756a5 | |||
| b0873f3ed3 | |||
| c3bc3bbcdd | |||
| 9cab0155b1 | |||
| a3e828a00e | |||
| 1e44cf4129 | |||
| 72be91d796 | |||
| 6ce2c47cb4 | |||
| 921e2bba4e | |||
| 3591aceba1 | |||
| 17aa8b49e0 | |||
| a2849da20c | |||
| 54c9373e33 | |||
| e793cbd0b2 | |||
| fffc0d55d6 | |||
| e561227b78 | |||
| 677e870635 | |||
| 6e69830a2e | |||
| f01785256c | |||
| f2e1ec8ed6 | |||
| 98f22e9056 | |||
| 79a75fee89 | |||
| 476559188f | |||
| 16a82fe3d8 | |||
| ea57a315b7 | |||
| 8303dffe99 | |||
| ac2ca56a54 | |||
| 3dcc6dc6eb | |||
| f5679dd825 | |||
| dcfe8f3320 | |||
| ea5cd3763d | |||
| 71f43590db | |||
| eb58ac44ca | |||
| b60b3106a0 | |||
| 292d00e664 | |||
| 252e48fa95 | |||
| 4ee6b6a745 | |||
| bda60ce813 | |||
| 32370b8bbc | |||
| 55e26fc7c9 | |||
| bee19cb483 | |||
| c4669d58f5 | |||
| 9d8ce23c97 | |||
| 2905127271 | |||
| d4388d175e | |||
| e703efe2ad | |||
| 7bcff24641 | |||
| 2f06370bb7 | |||
| c17bc7355f | |||
| 8a9c73e98c | |||
| 85548dc9a0 | |||
| 7e96dbd03d | |||
| a3ec95389c | |||
| 8cb298ac73 | |||
| 2c3a417c59 | |||
| 957f1e6b3c | |||
| 18e626a6e5 | |||
| c251819521 | |||
| 56c2332628 | |||
| ecfae43d8a | |||
| 6e17d2cb35 | |||
| c30111150b | |||
| d24e5255f6 | |||
| 9fe019e250 | |||
| b0d2fc8f48 | |||
| 334e30c1c3 | |||
| 59ae5b23cc | |||
| cb34ec8093 | |||
| c01a82168b | |||
| d2441bfff3 | |||
| aeb9443b18 | |||
| 906a938e9c | |||
| 6e68ff470a | |||
| c207abb1f6 | |||
| 5e5544c28a | |||
| 3782dc937b | |||
| e300f64bf6 | |||
| 08674b5b83 | |||
| 24441e4040 | |||
| 2f0126b131 | |||
| ece1e832a7 | |||
| 7308297802 | |||
| 1365ccb56d | |||
| d2bc6c4c65 | |||
| dd3c5d400b | |||
| f4727e08fb | |||
| 3dab8a4595 | |||
| 73dd76625f | |||
| cb23ae7b21 | |||
| 138a39e71e | |||
| ab2d7bb282 | |||
| 65cad4a313 | |||
| 7d0d52865f | |||
| 66f988e9c6 | |||
| 6a19f476a0 | |||
| 7f070c4222 | |||
| 2f4d75894e | |||
| b0da1a0967 | |||
| 6a5e1eb8c3 | |||
| 8c364e286b | |||
| 00c053b04e | |||
| 54c1c1498d | |||
| c43cb8e2de | |||
| ddc21a654e | |||
| 8ed47748cb | |||
| 1003074adf | |||
| 2ca0318c22 | |||
| b4372ca422 | |||
| 3ee84f0b31 | |||
| 44706e5c33 | |||
| 40af472af4 | |||
| 21058fd463 | |||
| dc298f338a | |||
| a3fa1fd834 | |||
| ea4c9f0435 | |||
| 90bc94343a | |||
| 1d3583fe0f | |||
| b41ec54a82 | |||
| 5826cd35c8 | |||
| 65731fb51b | |||
| 9cc2e68d5c | |||
| bc21f665f4 | |||
| 4fe02c8769 | |||
| 206b36d2f1 | |||
| e65e9f4897 | |||
| 8dffb127a5 |
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
npm run prettier
|
||||
npm run lint
|
||||
@@ -0,0 +1 @@
|
||||
buy_me_a_coffee: yoannchbpro
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.]
|
||||
@@ -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
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"printWidth": 100,
|
||||
"trailingComma": "none",
|
||||
"arrowParens": "avoid",
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
```
|
||||
@@ -1,166 +1,193 @@
|
||||
<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/> 
|
||||
- <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/> 
|
||||
- <b>Mouseover effect</b>: you will need to hover (or click for select) the question response to complete it automaticaly.
|
||||
<br/> 
|
||||
<br/> 
|
||||
|
||||
- <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
|
||||
|
||||

|
||||
|
||||
### Put in order question
|
||||
|
||||

|
||||
|
||||
### Resolve equation
|
||||
|
||||

|
||||
|
||||
### One response (radio button)
|
||||
|
||||

|
||||
|
||||
### Multiples responses (checkbox)
|
||||
|
||||

|
||||
|
||||
### True or false
|
||||
|
||||

|
||||
|
||||
### Number
|
||||
|
||||

|
||||
|
||||
### Text
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
## 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>
|
||||
|
||||
# SparkAssist 2.0.0
|
||||
|
||||
This extension allows you to hide CHAT-GPT in a Moodle quiz. You just need to click on the question you want to solve, and CHAT-GPT will automatically provide the answer. However, one needs to be careful because as we know, CHAT-GPT can make errors especially in calculations.
|
||||
|
||||
## Chrome Webstore
|
||||
|
||||
Find the extension on the Chrome Webstore right [here](https://chrome.google.com/webstore/detail/moodlegpt/fgiepdkoifhpcgdhbiikpgdapjdoemko)
|
||||
|
||||
## Summary
|
||||
|
||||
- [SparkAssist 2.0.0](#sparkassist-200)
|
||||
- [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/> 
|
||||
- <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/> 
|
||||
- <b>Mouseover effect</b>: you will need to hover (or click for select) the question response to complete it automaticaly.
|
||||
<br/> 
|
||||
<br/> 
|
||||
- <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/> 
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Supported questions type
|
||||
|
||||
MoodleGPT now utilizes strict strongly-typed JSON schema generation. Complex question logic is mapped directly from the Moodle DOM structure to the LLM backend for perfect, index-matched interaction.
|
||||
|
||||
### Calculated Questions (Numerical / Multi)
|
||||
|
||||

|
||||
|
||||
### Select (Missing Words / Gapselect)
|
||||
|
||||

|
||||
|
||||
### Match Questions
|
||||
|
||||
Extracts options seamlessly per row.
|
||||
|
||||
### Drag and Drop Into Text
|
||||
|
||||
Under the hood matching using native `.placeinput` tags for accurate invisible placement dropping.
|
||||
|
||||
### Put in order question
|
||||
|
||||

|
||||
|
||||
### Resolve equation
|
||||
|
||||

|
||||
|
||||
### One response (radio button)
|
||||
|
||||

|
||||
|
||||
### Multiples responses (checkbox)
|
||||
|
||||

|
||||
|
||||
### True or false
|
||||
|
||||

|
||||
|
||||
### Number
|
||||
|
||||

|
||||
|
||||
### Text
|
||||
|
||||

|
||||
|
||||
### Atto
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
## 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 +1,19 @@
|
||||
# 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
|
||||
# MoodleGPT TODO List
|
||||
|
||||
Based on the open GitHub issues, the following features and improvements are planned:
|
||||
|
||||
## Configuration & Settings
|
||||
|
||||
- [x] **Configurable Request Timeout** (#70): Add a setting to manually configure the API request timeout, preventing hangups on deeply nested or slow LLM requests.
|
||||
- [x] **OpenAI Project ID Support** (#55): Add a field under Advanced Settings to provide an OpenAI `project_id`, enabling compatibility with organizational level API keys.
|
||||
- [ ] **Toggleable Visual Indications** (#71): Add an option to completely disable the cursor hijack and loading animations to prevent unwanted visual behaviors during strictly proctored quizzes.
|
||||
- [ ] **Custom LMS Hostname Whitelisting** (#71): Implement internal configuration logic that seamlessly whitelists custom university domains (e.g., `*.oes.kz`) to ensure extension logic runs on rebranded Moodle portals.
|
||||
|
||||
## Context & Advanced AI Integration
|
||||
|
||||
- [ ] **Document Context Injection** (#39, #42): Allow users to upload or attach PDFs/lecture notes to be embedded in the AI's prompt resolution context to strictly enforce the correct syllabus answers.
|
||||
- [ ] **ChatGPT Assistant Integrations** (#37): Implement support for routing requests to customized pre-trained ChatGPT Assistants using specific Assistant IDs instead of relying entirely on standard ChatCompletion routes.
|
||||
|
||||
## Cross-Platform
|
||||
|
||||
- [ ] **Firefox Port** (#50): Migrate and package the Chrome extension codebase for native Firefox compatibility, adapting to standard `browser.*` APIs.
|
||||
|
||||
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 69 KiB |
@@ -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'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 480 KiB |
@@ -1,26 +1,24 @@
|
||||
{
|
||||
"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": "SparkAssist",
|
||||
"version": "2.0.0",
|
||||
"description": "An AI study assistant for your quizzes",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,138 +1,26 @@
|
||||
<!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 name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>SparkAssist</title>
|
||||
<!-- Modern Typography -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<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"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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();
|
||||
@@ -0,0 +1,78 @@
|
||||
[HELP] Add host and desactivate the cursor indication #71
|
||||
Help Request
|
||||
|
||||
Issue Summary
|
||||
Host name change due to private proctoring system integration and unwanted visual behavior in tests.
|
||||
|
||||
Describe the problem
|
||||
Hello!
|
||||
Our university is integrating a private proctoring system, and because of this, the host name is changing — it now ends with .oes.kz.
|
||||
How can we properly add or register this new host in the LMS so that everything continues to work correctly?
|
||||
|
||||
Additionally, we’d like to remove the visual behavior when hovering over a test question. Currently, when you hover, the cursor changes, and when you click, a loading animation appears. Is there a way to disable this effect?
|
||||
|
||||
Environment
|
||||
• Operating System: Windows 10
|
||||
• Browser: Google Chrome
|
||||
• Version: [e.g. 130.0.6723.70]
|
||||
• Platform: LMS
|
||||
|
||||
[FEATURE] Add "Request timeout" configuration option #70
|
||||
Help Request
|
||||
|
||||
Issue Summary
|
||||
|
||||
[Request was abort.]
|
||||
|
||||
Describe the problem
|
||||
|
||||
[I have no idea what happen maybe request is take too long time.it happen after using gpt 5 mode which not heppen on gpt4 before. can we have timeout setting feature so we can manually set it]
|
||||
|
||||
Environment
|
||||
|
||||
Operating System: [W11]
|
||||
Browser: [Chrome]
|
||||
Version: [1.1.5]
|
||||
Any other relevant information
|
||||
|
||||
Additional Information
|
||||
|
||||
[Any additional information that may be helpful in diagnosing the issue.]
|
||||
|
||||
[FEATURE] Add Project ID under Advanced Settings #55
|
||||
Add Project ID under Advanced Settings
|
||||
|
||||
To use organization API keys it is required to provide the project id in the request. I never used openai APIs before and I was getting 401 - Incorrect API key provided.
|
||||
|
||||
I fixed changing this line in MoodleGPT.js :
|
||||
|
||||
project: s = Be("OPENAI_PROJECT_ID") ?? <my-project-id>
|
||||
|
||||
and then loading the extension manually.
|
||||
|
||||
It would be very nice to add an optional project id under the advanced settings section.
|
||||
|
||||
[FEATURE] Firefox port? #50
|
||||
Feature Request
|
||||
|
||||
Description of the Feature
|
||||
Would be really good if the extension was available for Firefox too.
|
||||
|
||||
Additional Information
|
||||
It wouldn't require too much code modification, since Firefox supports chrome. APIs natively too.
|
||||
|
||||
[FEATURE] Preprompt for test with existing documents #42
|
||||
Hello, could there be an option to maybe choose a chatgpt chat you have made already or do I need to make a separate API and enter the files in there maybe?
|
||||
|
||||
TLDR: I have specific exams for what correct answers only come from there. For this I would usually make a chatgpt chat that had all the documents pre uploaded in there. But if I want to use the plugin, it takes new chats. Any way to work this in or do I need to generate a specific API for this?
|
||||
|
||||
[FEATURE] Add a document with the extension #39
|
||||
Hello, quick question: is it possible to provide my PDF of lecture notes in advance, to prepare and optimise ChatGPT’s answers to the multiple-choice questions?
|
||||
For example, for my ‘Strategic Communication’ exam, I provide my PDF of about 30 pages of notes, so that the answers are faithful to what we covered in class.
|
||||
Thanks and have a good day
|
||||
Leo
|
||||
|
||||
[FEATURE] ChatGPT Assistants #37
|
||||
Feature Request
|
||||
|
||||
It would be really helpful if ChatGPT's assistants would also be supported in the extension. It should be only an option to select and use assistants which you have made for a specific topic to give more exact results.
|
||||
@@ -1,9 +1,13 @@
|
||||
{
|
||||
"name": "moodlegpt",
|
||||
"version": "1.0.3",
|
||||
"description": "This extension allows you to hide CHAT-GPT in a Moodle quiz.",
|
||||
"name": "sparkassist",
|
||||
"version": "2.0.0",
|
||||
"description": "An AI study assistant for your quizzes.",
|
||||
"scripts": {
|
||||
"build": "rollup -c"
|
||||
"build": "npm run prettier && npm run lint && npm run build:css && npm run fastBuild",
|
||||
"build:css": "tailwindcss -i ./src/popup/style.css -o ./extension/popup/style.css",
|
||||
"fastBuild": "rollup -c",
|
||||
"lint": "eslint . --ext .ts,.tsx",
|
||||
"prettier": "prettier --write ."
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -23,10 +27,29 @@
|
||||
},
|
||||
"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-replace": "^6.0.3",
|
||||
"@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",
|
||||
"autoprefixer": "^10.5.0",
|
||||
"eslint": "^9.32.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"openai": "^5.23.2",
|
||||
"postcss": "^8.5.14",
|
||||
"prettier": "^3.6.2",
|
||||
"rollup": "^4.46.2",
|
||||
"rollup-plugin-ts": "^3.2.0",
|
||||
"typescript": "^5.0.2"
|
||||
"tailwindcss": "^3.4.19",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.38.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"preact": "^10.29.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,311 @@
|
||||
<div id="question-124-10" class="que calculatedsimple interactive notyetanswered">
|
||||
<div class="info">
|
||||
<h3 class="no">Question <span class="qno">9</span></h3>
|
||||
<div class="state">Tries remaining: 3</div>
|
||||
<div class="grade">Marked out of 1.00</div>
|
||||
<div class="questionflag editable" id="yui_3_18_1_1_1775934842056_72">
|
||||
<input type="hidden" name="q124:10_:flagged" value="0" /><input
|
||||
type="hidden"
|
||||
value="qaid=801&qubaid=124&qid=1621&slot=10&checksum=7baed22ee2b738ef71503b27cd982b7d&sesskey=iGNCxptJmv&newstate="
|
||||
class="questionflagpostdata"
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
class="questionflagvalue"
|
||||
id="q124:10_:flaggedcheckbox"
|
||||
name="q124:10_:flagged"
|
||||
value="0"
|
||||
/><a
|
||||
tabindex="0"
|
||||
class="aabtn"
|
||||
role="button"
|
||||
aria-pressed="false"
|
||||
aria-label="Flagged"
|
||||
title="Flag this question for future reference"
|
||||
><img
|
||||
class="questionflagimage"
|
||||
src="https://school.moodledemo.net/theme/image.php/boost/core/1775930536/i/unflagged"
|
||||
alt=""
|
||||
/>Flag question</a
|
||||
>
|
||||
</div>
|
||||
<div class="editquestion">
|
||||
<a
|
||||
href="https://school.moodledemo.net/question/bank/editquestion/question.php?cmid=1210&returnurl=%2Fmod%2Fquiz%2Fattempt.php%3Fattempt%3D88%26cmid%3D1210%23question-124-10&id=1621"
|
||||
><i class="icon fa fa-pen fa-fw iconsmall" title="Edit" role="img" aria-label="Edit"></i
|
||||
>Edit question</a
|
||||
>
|
||||
</div>
|
||||
<span class="badge bg-primary text-light">v1 (latest)</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="formulation clearfix">
|
||||
<h4 class="accesshide">Question text</h4>
|
||||
<input type="hidden" name="q124:10_:sequencecheck" value="1" />
|
||||
<div class="qtext" style="cursor: pointer">
|
||||
<div class="clearfix">
|
||||
<p>
|
||||
This is an example of a simple calculated question. It's similar to the calculated
|
||||
question but with fewer settings
|
||||
<img
|
||||
class="icon emoticon"
|
||||
alt="smile"
|
||||
title="smile"
|
||||
src="https://school.moodledemo.net/theme/image.php/boost/core/1775930536/s/smiley"
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
You won a cool Moodle cooler box at a Moodle Moot and wonder how much it will hold. What
|
||||
is the volume of your cooler box if its height is 6.4 its length is 9.9 and its width is
|
||||
7.0?
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ablock d-flex flex-wrap align-items-center">
|
||||
<label for="q124:10_answer">Answer: <span class="visually-hidden">Question 9</span></label
|
||||
><span class="answer"
|
||||
><input
|
||||
type="text"
|
||||
name="q124:10_answer"
|
||||
id="q124:10_answer"
|
||||
size="30"
|
||||
class="form-control d-inline"
|
||||
/></span>
|
||||
</div>
|
||||
<div class="im-controls">
|
||||
<button
|
||||
type="submit"
|
||||
id="q124:10_-submit"
|
||||
name="q124:10_-submit"
|
||||
value="1"
|
||||
class="submit btn btn-secondary"
|
||||
data-savescrollposition="true"
|
||||
>
|
||||
Check <span class="visually-hidden">Question 9</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="question-124-9" class="que calculatedmulti interactive notyetanswered">
|
||||
<div class="info">
|
||||
<h3 class="no">Question <span class="qno">8</span></h3>
|
||||
<div class="state">Tries remaining: 3</div>
|
||||
<div class="grade">Marked out of 1.00</div>
|
||||
<div class="questionflag editable" id="yui_3_18_1_1_1775934842056_66">
|
||||
<input type="hidden" name="q124:9_:flagged" value="0" /><input
|
||||
type="hidden"
|
||||
value="qaid=800&qubaid=124&qid=1620&slot=9&checksum=6f196762f980a0b2c9f887ed255c8a24&sesskey=iGNCxptJmv&newstate="
|
||||
class="questionflagpostdata"
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
class="questionflagvalue"
|
||||
id="q124:9_:flaggedcheckbox"
|
||||
name="q124:9_:flagged"
|
||||
value="0"
|
||||
/><a
|
||||
tabindex="0"
|
||||
class="aabtn"
|
||||
role="button"
|
||||
aria-pressed="false"
|
||||
aria-label="Flagged"
|
||||
title="Flag this question for future reference"
|
||||
><img
|
||||
class="questionflagimage"
|
||||
src="https://school.moodledemo.net/theme/image.php/boost/core/1775930536/i/unflagged"
|
||||
alt=""
|
||||
/>Flag question</a
|
||||
>
|
||||
</div>
|
||||
<div class="editquestion">
|
||||
<a
|
||||
href="https://school.moodledemo.net/question/bank/editquestion/question.php?cmid=1210&returnurl=%2Fmod%2Fquiz%2Fattempt.php%3Fattempt%3D88%26cmid%3D1210%23question-124-9&id=1620"
|
||||
><i class="icon fa fa-pen fa-fw iconsmall" title="Edit" role="img" aria-label="Edit"></i
|
||||
>Edit question</a
|
||||
>
|
||||
</div>
|
||||
<span class="badge bg-primary text-light">v2 (latest)</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="formulation clearfix">
|
||||
<h4 class="accesshide">Question text</h4>
|
||||
<input type="hidden" name="q124:9_:sequencecheck" value="1" />
|
||||
<div class="qtext" style="cursor: pointer">
|
||||
<div class="clearfix">
|
||||
<p>
|
||||
This is an example of a calculated multi-choice question. It is like the calculated
|
||||
question in that the numbers used may vary with each student and question, but the
|
||||
equation remains the same but it is different from the calculated question in that there
|
||||
is a choice of answers available.
|
||||
</p>
|
||||
<p>
|
||||
You are making a banner for your organisation's Moodle User Group meeting. Its length is
|
||||
14 and its height is 8 What is its area?
|
||||
</p>
|
||||
<p> </p>
|
||||
</div>
|
||||
</div>
|
||||
<fieldset class="ablock no-overflow visual-scroll-x">
|
||||
<legend class="prompt h6 fw-normal visually-hidden">
|
||||
<span class="visually-hidden">Question 8</span> Answer
|
||||
</legend>
|
||||
<div class="answer">
|
||||
<div class="r0">
|
||||
<input
|
||||
type="radio"
|
||||
name="q124:9_answer"
|
||||
value="0"
|
||||
id="q124:9_answer0"
|
||||
aria-labelledby="q124:9_answer0_label"
|
||||
/>
|
||||
<div class="d-flex w-auto" id="q124:9_answer0_label" data-region="answer-label">
|
||||
<div class="flex-fill ms-1">6</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="r1">
|
||||
<input
|
||||
type="radio"
|
||||
name="q124:9_answer"
|
||||
value="1"
|
||||
id="q124:9_answer1"
|
||||
aria-labelledby="q124:9_answer1_label"
|
||||
/>
|
||||
<div class="d-flex w-auto" id="q124:9_answer1_label" data-region="answer-label">
|
||||
<div class="flex-fill ms-1">112</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="r0">
|
||||
<input
|
||||
type="radio"
|
||||
name="q124:9_answer"
|
||||
value="2"
|
||||
id="q124:9_answer2"
|
||||
aria-labelledby="q124:9_answer2_label"
|
||||
/>
|
||||
<div class="d-flex w-auto" id="q124:9_answer2_label" data-region="answer-label">
|
||||
<div class="flex-fill ms-1">22</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="q124:9_clearchoice"
|
||||
class="qtype_multichoice_clearchoice visually-hidden"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="q124:9_answer"
|
||||
id="q124:9_answer-1"
|
||||
value="-1"
|
||||
class="visually-hidden"
|
||||
aria-hidden="true"
|
||||
checked="checked"
|
||||
/><label for="q124:9_answer-1"
|
||||
><a tabindex="-1" role="button" class="btn btn-link ms-3 mt-n1" href="#"
|
||||
>Clear my choice</a
|
||||
></label
|
||||
>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="im-controls">
|
||||
<button
|
||||
type="submit"
|
||||
id="q124:9_-submit"
|
||||
name="q124:9_-submit"
|
||||
value="1"
|
||||
class="submit btn btn-secondary"
|
||||
data-savescrollposition="true"
|
||||
>
|
||||
Check <span class="visually-hidden">Question 8</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="question-124-8" class="que calculated interactive notyetanswered">
|
||||
<div class="info">
|
||||
<h3 class="no">Question <span class="qno">7</span></h3>
|
||||
<div class="state">Tries remaining: 2</div>
|
||||
<div class="grade">Marked out of 1.00</div>
|
||||
<div class="questionflag editable" id="yui_3_18_1_1_1775934842056_60">
|
||||
<input type="hidden" name="q124:8_:flagged" value="0" /><input
|
||||
type="hidden"
|
||||
value="qaid=799&qubaid=124&qid=1618&slot=8&checksum=b7d586e320edea342963eb06ad05faa0&sesskey=iGNCxptJmv&newstate="
|
||||
class="questionflagpostdata"
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
class="questionflagvalue"
|
||||
id="q124:8_:flaggedcheckbox"
|
||||
name="q124:8_:flagged"
|
||||
value="0"
|
||||
/><a
|
||||
tabindex="0"
|
||||
class="aabtn"
|
||||
role="button"
|
||||
aria-pressed="false"
|
||||
aria-label="Flagged"
|
||||
title="Flag this question for future reference"
|
||||
><img
|
||||
class="questionflagimage"
|
||||
src="https://school.moodledemo.net/theme/image.php/boost/core/1775930536/i/unflagged"
|
||||
alt=""
|
||||
/>Flag question</a
|
||||
>
|
||||
</div>
|
||||
<div class="editquestion">
|
||||
<a
|
||||
href="https://school.moodledemo.net/question/bank/editquestion/question.php?cmid=1210&returnurl=%2Fmod%2Fquiz%2Fattempt.php%3Fattempt%3D88%26cmid%3D1210%23question-124-8&id=1618"
|
||||
><i class="icon fa fa-pen fa-fw iconsmall" title="Edit" role="img" aria-label="Edit"></i
|
||||
>Edit question</a
|
||||
>
|
||||
</div>
|
||||
<span class="badge bg-primary text-light">v1 (latest)</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="formulation clearfix">
|
||||
<h4 class="accesshide">Question text</h4>
|
||||
<input type="hidden" name="q124:8_:sequencecheck" value="1" />
|
||||
<div class="qtext" style="cursor: pointer">
|
||||
<div class="clearfix">
|
||||
<p>
|
||||
This is an example of a calculated question. The numbers will vary but the formula stays
|
||||
the same.
|
||||
</p>
|
||||
<p>
|
||||
You're doing some training in your organisation. In your group you have 3 complete
|
||||
beginners and 14 relatively experienced Moodle users. How many are there in your group
|
||||
in total?
|
||||
</p>
|
||||
<p><span> </span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ablock d-flex flex-wrap align-items-center">
|
||||
<label for="q124:8_answer">Answer: <span class="visually-hidden">Question 7</span></label
|
||||
><span class="answer"
|
||||
><input
|
||||
type="text"
|
||||
name="q124:8_answer"
|
||||
id="q124:8_answer"
|
||||
size="30"
|
||||
class="form-control d-inline"
|
||||
/></span>
|
||||
</div>
|
||||
<div class="im-controls">
|
||||
<button
|
||||
type="submit"
|
||||
id="q124:8_-submit"
|
||||
name="q124:8_-submit"
|
||||
value="1"
|
||||
class="submit btn btn-secondary"
|
||||
data-savescrollposition="true"
|
||||
>
|
||||
Check <span class="visually-hidden">Question 7</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,232 @@
|
||||
<div id="question-124-16" class="que ddwtos interactivecountback notyetanswered">
|
||||
<div class="info">
|
||||
<h3 class="no">Question <span class="qno">15</span></h3>
|
||||
<div class="state">Tries remaining: 1</div>
|
||||
<div class="grade">Marked out of 5.00</div>
|
||||
<div class="questionflag editable" id="yui_3_18_1_1_1775933031480_108">
|
||||
<input type="hidden" name="q124:16_:flagged" value="0" /><input
|
||||
type="hidden"
|
||||
value="qaid=814&qubaid=124&qid=1642&slot=16&checksum=2b637f444fcfdda7191b7f0620cd7533&sesskey=xrcdi6NRF8&newstate="
|
||||
class="questionflagpostdata"
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
class="questionflagvalue"
|
||||
id="q124:16_:flaggedcheckbox"
|
||||
name="q124:16_:flagged"
|
||||
value="0"
|
||||
/><a
|
||||
tabindex="0"
|
||||
class="aabtn"
|
||||
role="button"
|
||||
aria-pressed="false"
|
||||
aria-label="Flagged"
|
||||
title="Flag this question for future reference"
|
||||
><img
|
||||
class="questionflagimage"
|
||||
src="https://school.moodledemo.net/theme/image.php/boost/core/1775926936/i/unflagged"
|
||||
alt=""
|
||||
/>Flag question</a
|
||||
>
|
||||
</div>
|
||||
<div class="editquestion">
|
||||
<a
|
||||
href="https://school.moodledemo.net/question/bank/editquestion/question.php?cmid=1210&returnurl=%2Fmod%2Fquiz%2Fattempt.php%3Fattempt%3D88%26cmid%3D1210%23question-124-16&id=1642"
|
||||
><i class="icon fa fa-pen fa-fw iconsmall" title="Edit" role="img" aria-label="Edit"></i
|
||||
>Edit question</a
|
||||
>
|
||||
</div>
|
||||
<span class="badge bg-primary text-light">v1 (latest)</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="formulation clearfix">
|
||||
<h4 class="accesshide">Question text</h4>
|
||||
<input type="hidden" name="q124:16_:sequencecheck" value="1" />
|
||||
<div class="qtext" style="cursor: pointer">
|
||||
<p>
|
||||
<i
|
||||
>Drag the capital city onto the correct country.There are a few extra just to confuse
|
||||
you!</i
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<i
|
||||
>This is an example of a
|
||||
<a href="https://docs.moodle.org/en/Drag_and_drop_into_text"
|
||||
>Drag and drop into text question type.</a
|
||||
></i
|
||||
>
|
||||
</p>
|
||||
<hr />
|
||||
<p>
|
||||
The capital of England is
|
||||
<span
|
||||
class="place1 drop active group1"
|
||||
tabindex="0"
|
||||
style="width: 78px; height: 27px; line-height: 27px"
|
||||
> <span class="accesshide">Blank 1 Question 15</span></span
|
||||
>
|
||||
which is fairly easy. But do you know that the capital of Wales is
|
||||
<span
|
||||
class="place2 drop active group1"
|
||||
tabindex="0"
|
||||
style="width: 78px; height: 27px; line-height: 27px"
|
||||
> <span class="accesshide">Blank 2 Question 15</span></span
|
||||
>
|
||||
?
|
||||
</p>
|
||||
<p>
|
||||
Scotland's capital city is the very beautiful
|
||||
<span
|
||||
class="place3 drop active group1"
|
||||
tabindex="0"
|
||||
style="width: 78px; height: 27px; line-height: 27px"
|
||||
> <span class="accesshide">Blank 3 Question 15</span></span
|
||||
>
|
||||
while the Republic of Ireland has
|
||||
<span
|
||||
class="place4 drop active group1"
|
||||
tabindex="0"
|
||||
style="width: 78px; height: 27px; line-height: 27px"
|
||||
> <span class="accesshide">Blank 4 Question 15</span></span
|
||||
>
|
||||
as its capital and Northern Ireland's capital is
|
||||
<span
|
||||
class="place5 drop active group1"
|
||||
tabindex="0"
|
||||
style="width: 78px; height: 27px; line-height: 27px"
|
||||
> <span class="accesshide">Blank 5 Question 15</span></span
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<div class="answercontainer">
|
||||
<div class="user-select-none draggrouphomes1">
|
||||
<span
|
||||
class="draghome choice1 group1 dragplaceholder"
|
||||
style="width: 80px; height: 29px; line-height: 27px"
|
||||
>Glasgow</span
|
||||
><span
|
||||
class="draghome user-select-none choice1 group1 unplaced"
|
||||
style="width: 80px; height: 29px; line-height: 27px"
|
||||
>Glasgow</span
|
||||
>
|
||||
<span
|
||||
class="draghome choice2 group1 dragplaceholder"
|
||||
style="width: 80px; height: 29px; line-height: 27px"
|
||||
>Dublin</span
|
||||
><span
|
||||
class="draghome user-select-none choice2 group1 unplaced"
|
||||
style="width: 80px; height: 29px; line-height: 27px"
|
||||
>Dublin</span
|
||||
>
|
||||
<span
|
||||
class="draghome choice3 group1 dragplaceholder"
|
||||
style="width: 80px; height: 29px; line-height: 27px"
|
||||
>London</span
|
||||
><span
|
||||
class="draghome user-select-none choice3 group1 unplaced"
|
||||
style="width: 80px; height: 29px; line-height: 27px"
|
||||
>London</span
|
||||
>
|
||||
<span
|
||||
class="draghome choice4 group1 dragplaceholder"
|
||||
style="width: 80px; height: 29px; line-height: 27px"
|
||||
>Cardiff</span
|
||||
><span
|
||||
class="draghome user-select-none choice4 group1 unplaced"
|
||||
style="width: 80px; height: 29px; line-height: 27px"
|
||||
>Cardiff</span
|
||||
>
|
||||
<span
|
||||
class="draghome choice5 group1 dragplaceholder"
|
||||
style="width: 80px; height: 29px; line-height: 27px"
|
||||
>Cork</span
|
||||
><span
|
||||
class="draghome user-select-none choice5 group1 unplaced"
|
||||
style="width: 80px; height: 29px; line-height: 27px"
|
||||
>Cork</span
|
||||
>
|
||||
<span
|
||||
class="draghome choice6 group1 dragplaceholder"
|
||||
style="width: 80px; height: 29px; line-height: 27px"
|
||||
>Belfast</span
|
||||
><span
|
||||
class="draghome user-select-none choice6 group1 unplaced"
|
||||
style="width: 80px; height: 29px; line-height: 27px"
|
||||
>Belfast</span
|
||||
>
|
||||
<span
|
||||
class="draghome choice7 group1 dragplaceholder"
|
||||
style="width: 80px; height: 29px; line-height: 27px"
|
||||
>Derry</span
|
||||
><span
|
||||
class="draghome user-select-none choice7 group1 unplaced"
|
||||
style="width: 80px; height: 29px; line-height: 27px"
|
||||
>Derry</span
|
||||
>
|
||||
<span
|
||||
class="draghome choice8 group1 dragplaceholder"
|
||||
style="width: 80px; height: 29px; line-height: 27px"
|
||||
>Edinburgh</span
|
||||
><span
|
||||
class="draghome user-select-none choice8 group1 unplaced"
|
||||
style="width: 80px; height: 29px; line-height: 27px"
|
||||
>Edinburgh</span
|
||||
>
|
||||
<span
|
||||
class="draghome choice9 group1 dragplaceholder"
|
||||
style="width: 80px; height: 29px; line-height: 27px"
|
||||
>Swansea</span
|
||||
><span
|
||||
class="draghome user-select-none choice9 group1 unplaced"
|
||||
style="width: 80px; height: 29px; line-height: 27px"
|
||||
>Swansea</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="hidden"
|
||||
id="q124_16_p1"
|
||||
class="placeinput place1 group1"
|
||||
name="q124:16_p1"
|
||||
value="0"
|
||||
/><input
|
||||
type="hidden"
|
||||
id="q124_16_p2"
|
||||
class="placeinput place2 group1"
|
||||
name="q124:16_p2"
|
||||
value="0"
|
||||
/><input
|
||||
type="hidden"
|
||||
id="q124_16_p3"
|
||||
class="placeinput place3 group1"
|
||||
name="q124:16_p3"
|
||||
value="0"
|
||||
/><input
|
||||
type="hidden"
|
||||
id="q124_16_p4"
|
||||
class="placeinput place4 group1"
|
||||
name="q124:16_p4"
|
||||
value="0"
|
||||
/><input
|
||||
type="hidden"
|
||||
id="q124_16_p5"
|
||||
class="placeinput place5 group1"
|
||||
name="q124:16_p5"
|
||||
value="0"
|
||||
/>
|
||||
<div class="im-controls">
|
||||
<button
|
||||
type="submit"
|
||||
id="q124:16_-submit"
|
||||
name="q124:16_-submit"
|
||||
value="1"
|
||||
class="submit btn btn-secondary"
|
||||
data-savescrollposition="true"
|
||||
>
|
||||
Check <span class="visually-hidden">Question 15</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,753 @@
|
||||
<div id="question-125-6" class="que essay manualgraded notyetanswered">
|
||||
<div class="info">
|
||||
<h3 class="no">Question <span class="qno">6</span></h3>
|
||||
<div class="state">Not yet answered</div>
|
||||
<div class="grade">Marked out of 1.00</div>
|
||||
<div class="questionflag editable" id="yui_3_18_1_1_1775909302600_48">
|
||||
<input type="hidden" name="q125:6_:flagged" value="0" /><input
|
||||
type="hidden"
|
||||
value="qaid=803&qubaid=125&qid=3703&slot=6&checksum=3aadede34be020a7858cc02258ea5877&sesskey=MRh4T7j2fU&newstate="
|
||||
class="questionflagpostdata"
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
class="questionflagvalue"
|
||||
id="q125:6_:flaggedcheckbox"
|
||||
name="q125:6_:flagged"
|
||||
value="0"
|
||||
/><a
|
||||
tabindex="0"
|
||||
class="aabtn"
|
||||
role="button"
|
||||
aria-pressed="false"
|
||||
aria-label="Flagged"
|
||||
title="Flag this question for future reference"
|
||||
><img
|
||||
class="questionflagimage"
|
||||
src="https://school.moodledemo.net/theme/image.php/boost/core/1775905339/i/unflagged"
|
||||
alt=""
|
||||
/>Flag question</a
|
||||
>
|
||||
</div>
|
||||
<div class="editquestion">
|
||||
<a
|
||||
href="https://school.moodledemo.net/question/bank/editquestion/question.php?cmid=1655&returnurl=%2Fmod%2Fquiz%2Fattempt.php%3Fattempt%3D89%26cmid%3D1655%23question-125-6&id=3703"
|
||||
><i class="icon fa fa-pen fa-fw iconsmall" title="Edit" role="img" aria-label="Edit"></i
|
||||
>Edit question</a
|
||||
>
|
||||
</div>
|
||||
<span class="badge bg-primary text-light">v1 (latest)</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="formulation clearfix">
|
||||
<h4 class="accesshide">Question text</h4>
|
||||
<input type="hidden" name="q125:6_:sequencecheck" value="1" />
|
||||
<div class="qtext" style="cursor: pointer">
|
||||
<div class="clearfix">
|
||||
<p>What is resilience?</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ablock">
|
||||
<div class="answer">
|
||||
<label class="visually-hidden" for="q125:6_answer_id">Answer text Question 6</label>
|
||||
<div class="qtype_essay_editor qtype_essay_response">
|
||||
<div>
|
||||
<textarea
|
||||
id="q125:6_answer_id"
|
||||
name="q125:6_answer"
|
||||
rows="10"
|
||||
cols="60"
|
||||
class="form-control"
|
||||
style="display: none"
|
||||
aria-hidden="true"
|
||||
data-fieldtype="editor"
|
||||
></textarea>
|
||||
<div
|
||||
role="application"
|
||||
class="tox tox-tinymce"
|
||||
aria-disabled="false"
|
||||
style="visibility: hidden; height: 237px"
|
||||
>
|
||||
<div class="tox-editor-container">
|
||||
<div data-alloy-vertical-dir="toptobottom" class="tox-editor-header">
|
||||
<div role="menubar" data-alloy-tabstop="true" class="tox-menubar">
|
||||
<button
|
||||
aria-haspopup="true"
|
||||
role="menuitem"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
data-alloy-tabstop="true"
|
||||
unselectable="on"
|
||||
class="tox-mbtn tox-mbtn--select"
|
||||
aria-expanded="false"
|
||||
style="user-select: none; width: 39.4688px"
|
||||
>
|
||||
<span class="tox-mbtn__select-label">Edit</span>
|
||||
<div class="tox-mbtn__select-chevron">
|
||||
<svg width="10" height="10" focusable="false">
|
||||
<path
|
||||
d="M8.7 2.2c.3-.3.8-.3 1 0 .4.4.4.9 0 1.2L5.7 7.8c-.3.3-.9.3-1.2 0L.2 3.4a.8.8 0 0 1 0-1.2c.3-.3.8-.3 1.1 0L5 6l3.7-3.8Z"
|
||||
fill-rule="nonzero"
|
||||
></path>
|
||||
</svg>
|
||||
</div></button
|
||||
><button
|
||||
aria-haspopup="true"
|
||||
role="menuitem"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
data-alloy-tabstop="true"
|
||||
unselectable="on"
|
||||
class="tox-mbtn tox-mbtn--select"
|
||||
style="user-select: none; width: 45.5312px"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="tox-mbtn__select-label">View</span>
|
||||
<div class="tox-mbtn__select-chevron">
|
||||
<svg width="10" height="10" focusable="false">
|
||||
<path
|
||||
d="M8.7 2.2c.3-.3.8-.3 1 0 .4.4.4.9 0 1.2L5.7 7.8c-.3.3-.9.3-1.2 0L.2 3.4a.8.8 0 0 1 0-1.2c.3-.3.8-.3 1.1 0L5 6l3.7-3.8Z"
|
||||
fill-rule="nonzero"
|
||||
></path>
|
||||
</svg>
|
||||
</div></button
|
||||
><button
|
||||
aria-haspopup="true"
|
||||
role="menuitem"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
data-alloy-tabstop="true"
|
||||
unselectable="on"
|
||||
class="tox-mbtn tox-mbtn--select"
|
||||
style="user-select: none; width: 50.5312px"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="tox-mbtn__select-label">Insert</span>
|
||||
<div class="tox-mbtn__select-chevron">
|
||||
<svg width="10" height="10" focusable="false">
|
||||
<path
|
||||
d="M8.7 2.2c.3-.3.8-.3 1 0 .4.4.4.9 0 1.2L5.7 7.8c-.3.3-.9.3-1.2 0L.2 3.4a.8.8 0 0 1 0-1.2c.3-.3.8-.3 1.1 0L5 6l3.7-3.8Z"
|
||||
fill-rule="nonzero"
|
||||
></path>
|
||||
</svg>
|
||||
</div></button
|
||||
><button
|
||||
aria-haspopup="true"
|
||||
role="menuitem"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
data-alloy-tabstop="true"
|
||||
unselectable="on"
|
||||
class="tox-mbtn tox-mbtn--select"
|
||||
style="user-select: none; width: 59.8438px"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="tox-mbtn__select-label">Format</span>
|
||||
<div class="tox-mbtn__select-chevron">
|
||||
<svg width="10" height="10" focusable="false">
|
||||
<path
|
||||
d="M8.7 2.2c.3-.3.8-.3 1 0 .4.4.4.9 0 1.2L5.7 7.8c-.3.3-.9.3-1.2 0L.2 3.4a.8.8 0 0 1 0-1.2c.3-.3.8-.3 1.1 0L5 6l3.7-3.8Z"
|
||||
fill-rule="nonzero"
|
||||
></path>
|
||||
</svg>
|
||||
</div></button
|
||||
><button
|
||||
aria-haspopup="true"
|
||||
role="menuitem"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
data-alloy-tabstop="true"
|
||||
unselectable="on"
|
||||
class="tox-mbtn tox-mbtn--select"
|
||||
style="user-select: none; width: 47.7188px"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="tox-mbtn__select-label">Tools</span>
|
||||
<div class="tox-mbtn__select-chevron">
|
||||
<svg width="10" height="10" focusable="false">
|
||||
<path
|
||||
d="M8.7 2.2c.3-.3.8-.3 1 0 .4.4.4.9 0 1.2L5.7 7.8c-.3.3-.9.3-1.2 0L.2 3.4a.8.8 0 0 1 0-1.2c.3-.3.8-.3 1.1 0L5 6l3.7-3.8Z"
|
||||
fill-rule="nonzero"
|
||||
></path>
|
||||
</svg>
|
||||
</div></button
|
||||
><button
|
||||
aria-haspopup="true"
|
||||
role="menuitem"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
data-alloy-tabstop="true"
|
||||
unselectable="on"
|
||||
class="tox-mbtn tox-mbtn--select"
|
||||
style="user-select: none; width: 47.8281px"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="tox-mbtn__select-label">Table</span>
|
||||
<div class="tox-mbtn__select-chevron">
|
||||
<svg width="10" height="10" focusable="false">
|
||||
<path
|
||||
d="M8.7 2.2c.3-.3.8-.3 1 0 .4.4.4.9 0 1.2L5.7 7.8c-.3.3-.9.3-1.2 0L.2 3.4a.8.8 0 0 1 0-1.2c.3-.3.8-.3 1.1 0L5 6l3.7-3.8Z"
|
||||
fill-rule="nonzero"
|
||||
></path>
|
||||
</svg>
|
||||
</div></button
|
||||
><button
|
||||
aria-haspopup="true"
|
||||
role="menuitem"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
data-alloy-tabstop="true"
|
||||
unselectable="on"
|
||||
class="tox-mbtn tox-mbtn--select"
|
||||
style="user-select: none; width: 44.8906px"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="tox-mbtn__select-label">Help</span>
|
||||
<div class="tox-mbtn__select-chevron">
|
||||
<svg width="10" height="10" focusable="false">
|
||||
<path
|
||||
d="M8.7 2.2c.3-.3.8-.3 1 0 .4.4.4.9 0 1.2L5.7 7.8c-.3.3-.9.3-1.2 0L.2 3.4a.8.8 0 0 1 0-1.2c.3-.3.8-.3 1.1 0L5 6l3.7-3.8Z"
|
||||
fill-rule="nonzero"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div role="group" class="tox-toolbar-overlord" aria-disabled="false">
|
||||
<div role="group" class="tox-toolbar__primary">
|
||||
<div
|
||||
aria-label="history"
|
||||
role="toolbar"
|
||||
data-alloy-tabstop="true"
|
||||
class="tox-toolbar__group"
|
||||
>
|
||||
<button
|
||||
aria-label="Undo"
|
||||
data-mce-name="undo"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
class="tox-tbtn tox-tbtn--disabled"
|
||||
aria-disabled="true"
|
||||
style="width: 34px"
|
||||
>
|
||||
<span class="tox-icon tox-tbtn__icon-wrap"
|
||||
><svg width="24" height="24" focusable="false">
|
||||
<path
|
||||
d="M6.4 8H12c3.7 0 6.2 2 6.8 5.1.6 2.7-.4 5.6-2.3 6.8a1 1 0 0 1-1-1.8c1.1-.6 1.8-2.7 1.4-4.6-.5-2.1-2.1-3.5-4.9-3.5H6.4l3.3 3.3a1 1 0 1 1-1.4 1.4l-5-5a1 1 0 0 1 0-1.4l5-5a1 1 0 0 1 1.4 1.4L6.4 8Z"
|
||||
fill-rule="nonzero"
|
||||
></path></svg
|
||||
></span></button
|
||||
><button
|
||||
aria-label="Redo"
|
||||
data-mce-name="redo"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
class="tox-tbtn tox-tbtn--disabled"
|
||||
aria-disabled="true"
|
||||
style="width: 34px"
|
||||
>
|
||||
<span class="tox-icon tox-tbtn__icon-wrap"
|
||||
><svg width="24" height="24" focusable="false">
|
||||
<path
|
||||
d="M17.6 10H12c-2.8 0-4.4 1.4-4.9 3.5-.4 2 .3 4 1.4 4.6a1 1 0 1 1-1 1.8c-2-1.2-2.9-4.1-2.3-6.8.6-3 3-5.1 6.8-5.1h5.6l-3.3-3.3a1 1 0 1 1 1.4-1.4l5 5a1 1 0 0 1 0 1.4l-5 5a1 1 0 0 1-1.4-1.4l3.3-3.3Z"
|
||||
fill-rule="nonzero"
|
||||
></path></svg
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
aria-label="formatting"
|
||||
role="toolbar"
|
||||
data-alloy-tabstop="true"
|
||||
class="tox-toolbar__group"
|
||||
>
|
||||
<button
|
||||
aria-label="Bold"
|
||||
data-mce-name="bold"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
class="tox-tbtn"
|
||||
aria-disabled="false"
|
||||
aria-pressed="false"
|
||||
style="width: 34px"
|
||||
>
|
||||
<span class="tox-icon tox-tbtn__icon-wrap"
|
||||
><svg width="24" height="24" focusable="false">
|
||||
<path
|
||||
d="M7.8 19c-.3 0-.5 0-.6-.2l-.2-.5V5.7c0-.2 0-.4.2-.5l.6-.2h5c1.5 0 2.7.3 3.5 1 .7.6 1.1 1.4 1.1 2.5a3 3 0 0 1-.6 1.9c-.4.6-1 1-1.6 1.2.4.1.9.3 1.3.6s.8.7 1 1.2c.4.4.5 1 .5 1.6 0 1.3-.4 2.3-1.3 3-.8.7-2.1 1-3.8 1H7.8Zm5-8.3c.6 0 1.2-.1 1.6-.5.4-.3.6-.7.6-1.3 0-1.1-.8-1.7-2.3-1.7H9.3v3.5h3.4Zm.5 6c.7 0 1.3-.1 1.7-.4.4-.4.6-.9.6-1.5s-.2-1-.7-1.4c-.4-.3-1-.4-2-.4H9.4v3.8h4Z"
|
||||
fill-rule="evenodd"
|
||||
></path></svg
|
||||
></span></button
|
||||
><button
|
||||
aria-label="Italic"
|
||||
data-mce-name="italic"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
class="tox-tbtn"
|
||||
aria-disabled="false"
|
||||
aria-pressed="false"
|
||||
style="width: 34px"
|
||||
>
|
||||
<span class="tox-icon tox-tbtn__icon-wrap"
|
||||
><svg width="24" height="24" focusable="false">
|
||||
<path
|
||||
d="m16.7 4.7-.1.9h-.3c-.6 0-1 0-1.4.3-.3.3-.4.6-.5 1.1l-2.1 9.8v.6c0 .5.4.8 1.4.8h.2l-.2.8H8l.2-.8h.2c1.1 0 1.8-.5 2-1.5l2-9.8.1-.5c0-.6-.4-.8-1.4-.8h-.3l.2-.9h5.8Z"
|
||||
fill-rule="evenodd"
|
||||
></path></svg
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
aria-label="content"
|
||||
role="toolbar"
|
||||
data-alloy-tabstop="true"
|
||||
class="tox-toolbar__group"
|
||||
>
|
||||
<button
|
||||
aria-label="Insert H5P content"
|
||||
data-mce-name="tiny_h5p"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
class="tox-tbtn"
|
||||
aria-disabled="false"
|
||||
aria-pressed="false"
|
||||
style="width: 34px"
|
||||
>
|
||||
<span class="tox-icon tox-tbtn__icon-wrap"
|
||||
><svg
|
||||
data-buttonsource="moodle"
|
||||
width="24"
|
||||
height="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
focusable="false"
|
||||
>
|
||||
<image
|
||||
href="https://school.moodledemo.net/theme/image.php/boost/tiny_h5p/1775905339/icon"
|
||||
width="24"
|
||||
height="24"
|
||||
></image></svg
|
||||
></span></button
|
||||
><button
|
||||
aria-label="Link"
|
||||
data-mce-name="tiny_link_link"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
class="tox-tbtn"
|
||||
aria-disabled="false"
|
||||
aria-pressed="false"
|
||||
style="width: 34px"
|
||||
>
|
||||
<span class="tox-icon tox-tbtn__icon-wrap"
|
||||
><svg width="24" height="24" focusable="false">
|
||||
<path
|
||||
d="M6.2 12.3a1 1 0 0 1 1.4 1.4l-2 2a2 2 0 1 0 2.6 2.8l4.8-4.8a1 1 0 0 0 0-1.4 1 1 0 1 1 1.4-1.3 2.9 2.9 0 0 1 0 4L9.6 20a3.9 3.9 0 0 1-5.5-5.5l2-2Zm11.6-.6a1 1 0 0 1-1.4-1.4l2-2a2 2 0 1 0-2.6-2.8L11 10.3a1 1 0 0 0 0 1.4A1 1 0 1 1 9.6 13a2.9 2.9 0 0 1 0-4L14.4 4a3.9 3.9 0 0 1 5.5 5.5l-2 2Z"
|
||||
fill-rule="nonzero"
|
||||
></path></svg
|
||||
></span></button
|
||||
><button
|
||||
aria-label="Unlink"
|
||||
data-mce-name="tiny_link_unlink"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
class="tox-tbtn"
|
||||
aria-disabled="false"
|
||||
aria-pressed="false"
|
||||
style="width: 34px"
|
||||
>
|
||||
<span class="tox-icon tox-tbtn__icon-wrap"
|
||||
><svg width="24" height="24" focusable="false">
|
||||
<path
|
||||
d="M6.2 12.3a1 1 0 0 1 1.4 1.4l-2 2a2 2 0 1 0 2.6 2.8l4.8-4.8a1 1 0 0 0 0-1.4 1 1 0 1 1 1.4-1.3 2.9 2.9 0 0 1 0 4L9.6 20a3.9 3.9 0 0 1-5.5-5.5l2-2Zm11.6-.6a1 1 0 0 1-1.4-1.4l2.1-2a2 2 0 1 0-2.7-2.8L11 10.3a1 1 0 0 0 0 1.4A1 1 0 1 1 9.6 13a2.9 2.9 0 0 1 0-4L14.4 4a3.9 3.9 0 0 1 5.5 5.5l-2 2ZM7.6 6.3a.8.8 0 0 1-1 1.1L3.3 4.2a.7.7 0 1 1 1-1l3.2 3.1ZM5.1 8.6a.8.8 0 0 1 0 1.5H3a.8.8 0 0 1 0-1.5H5Zm5-3.5a.8.8 0 0 1-1.5 0V3a.8.8 0 0 1 1.5 0V5Zm6 11.8a.8.8 0 0 1 1-1l3.2 3.2a.8.8 0 0 1-1 1L16 17Zm-2.2 2a.8.8 0 0 1 1.5 0V21a.8.8 0 0 1-1.5 0V19Zm5-3.5a.7.7 0 1 1 0-1.5H21a.8.8 0 0 1 0 1.5H19Z"
|
||||
fill-rule="nonzero"
|
||||
></path></svg
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
aria-label="view"
|
||||
role="toolbar"
|
||||
data-alloy-tabstop="true"
|
||||
class="tox-toolbar__group"
|
||||
>
|
||||
<button
|
||||
aria-label="Fullscreen"
|
||||
data-mce-name="fullscreen"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
class="tox-tbtn"
|
||||
aria-disabled="false"
|
||||
aria-pressed="false"
|
||||
style="width: 34px"
|
||||
>
|
||||
<span class="tox-icon tox-tbtn__icon-wrap"
|
||||
><svg width="24" height="24" focusable="false">
|
||||
<path
|
||||
d="m15.3 10-1.2-1.3 2.9-3h-2.3a.9.9 0 1 1 0-1.7H19c.5 0 .9.4.9.9v4.4a.9.9 0 1 1-1.8 0V7l-2.9 3Zm0 4 3 3v-2.3a.9.9 0 1 1 1.7 0V19c0 .5-.4.9-.9.9h-4.4a.9.9 0 1 1 0-1.8H17l-3-2.9 1.3-1.2ZM10 15.4l-2.9 3h2.3a.9.9 0 1 1 0 1.7H5a.9.9 0 0 1-.9-.9v-4.4a.9.9 0 1 1 1.8 0V17l2.9-3 1.2 1.3ZM8.7 10 5.7 7v2.3a.9.9 0 0 1-1.7 0V5c0-.5.4-.9.9-.9h4.4a.9.9 0 0 1 0 1.8H7l3 2.9-1.3 1.2Z"
|
||||
fill-rule="nonzero"
|
||||
></path></svg
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
aria-label="alignment"
|
||||
role="toolbar"
|
||||
data-alloy-tabstop="true"
|
||||
class="tox-toolbar__group"
|
||||
>
|
||||
<button
|
||||
aria-label="Align left"
|
||||
data-mce-name="alignleft"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
class="tox-tbtn"
|
||||
aria-disabled="false"
|
||||
aria-pressed="false"
|
||||
style="width: 34px"
|
||||
>
|
||||
<span class="tox-icon tox-tbtn__icon-wrap"
|
||||
><svg width="24" height="24" focusable="false">
|
||||
<path
|
||||
d="M5 5h14c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 1 1 0-2Zm0 4h8c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 1 1 0-2Zm0 8h8c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 0 1 0-2Zm0-4h14c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 0 1 0-2Z"
|
||||
fill-rule="evenodd"
|
||||
></path></svg
|
||||
></span></button
|
||||
><button
|
||||
aria-label="Align centre"
|
||||
data-mce-name="aligncenter"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
class="tox-tbtn"
|
||||
aria-disabled="false"
|
||||
aria-pressed="false"
|
||||
style="width: 34px"
|
||||
>
|
||||
<span class="tox-icon tox-tbtn__icon-wrap"
|
||||
><svg width="24" height="24" focusable="false">
|
||||
<path
|
||||
d="M5 5h14c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 1 1 0-2Zm3 4h8c.6 0 1 .4 1 1s-.4 1-1 1H8a1 1 0 1 1 0-2Zm0 8h8c.6 0 1 .4 1 1s-.4 1-1 1H8a1 1 0 0 1 0-2Zm-3-4h14c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 0 1 0-2Z"
|
||||
fill-rule="evenodd"
|
||||
></path></svg
|
||||
></span></button
|
||||
><button
|
||||
aria-label="Align right"
|
||||
data-mce-name="alignright"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
class="tox-tbtn"
|
||||
aria-disabled="false"
|
||||
aria-pressed="false"
|
||||
style="width: 34px"
|
||||
>
|
||||
<span class="tox-icon tox-tbtn__icon-wrap"
|
||||
><svg width="24" height="24" focusable="false">
|
||||
<path
|
||||
d="M5 5h14c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 1 1 0-2Zm6 4h8c.6 0 1 .4 1 1s-.4 1-1 1h-8a1 1 0 0 1 0-2Zm0 8h8c.6 0 1 .4 1 1s-.4 1-1 1h-8a1 1 0 0 1 0-2Zm-6-4h14c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 0 1 0-2Z"
|
||||
fill-rule="evenodd"
|
||||
></path></svg
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
aria-label="directionality"
|
||||
role="toolbar"
|
||||
data-alloy-tabstop="true"
|
||||
class="tox-toolbar__group"
|
||||
>
|
||||
<button
|
||||
aria-label="Left to right"
|
||||
data-mce-name="ltr"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
class="tox-tbtn tox-tbtn--enabled"
|
||||
aria-disabled="false"
|
||||
aria-pressed="true"
|
||||
style="width: 34px"
|
||||
>
|
||||
<span class="tox-icon tox-tbtn__icon-wrap"
|
||||
><svg width="24" height="24" focusable="false">
|
||||
<path
|
||||
d="M11 5h7a1 1 0 0 1 0 2h-1v11a1 1 0 0 1-2 0V7h-2v11a1 1 0 0 1-2 0v-6c-.5 0-1 0-1.4-.3A3.4 3.4 0 0 1 7.8 10a3.3 3.3 0 0 1 0-2.8 3.4 3.4 0 0 1 1.8-1.8L11 5ZM4.4 16.2 6.2 15l-1.8-1.2a1 1 0 0 1 1.2-1.6l3 2a1 1 0 0 1 0 1.6l-3 2a1 1 0 1 1-1.2-1.6Z"
|
||||
fill-rule="evenodd"
|
||||
></path></svg
|
||||
></span></button
|
||||
><button
|
||||
aria-label="Right to left"
|
||||
data-mce-name="rtl"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
class="tox-tbtn"
|
||||
aria-disabled="false"
|
||||
aria-pressed="false"
|
||||
style="width: 34px"
|
||||
>
|
||||
<span class="tox-icon tox-tbtn__icon-wrap"
|
||||
><svg width="24" height="24" focusable="false">
|
||||
<path
|
||||
d="M8 5h8v2h-2v12h-2V7h-2v12H8v-7c-.5 0-1 0-1.4-.3A3.4 3.4 0 0 1 4.8 10a3.3 3.3 0 0 1 0-2.8 3.4 3.4 0 0 1 1.8-1.8L8 5Zm12 11.2a1 1 0 1 1-1 1.6l-3-2a1 1 0 0 1 0-1.6l3-2a1 1 0 1 1 1 1.6L18.4 15l1.8 1.2Z"
|
||||
fill-rule="evenodd"
|
||||
></path></svg
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
<div role="toolbar" data-alloy-tabstop="true" class="tox-toolbar__group">
|
||||
<button
|
||||
aria-label="Reveal or hide additional toolbar items"
|
||||
data-mce-name="overflow-button"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
data-alloy-tabstop="true"
|
||||
class="tox-tbtn"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="tox-icon tox-tbtn__icon-wrap"
|
||||
><svg width="24" height="24" focusable="false">
|
||||
<path
|
||||
d="M6 10a2 2 0 0 0-2 2c0 1.1.9 2 2 2a2 2 0 0 0 2-2 2 2 0 0 0-2-2Zm12 0a2 2 0 0 0-2 2c0 1.1.9 2 2 2a2 2 0 0 0 2-2 2 2 0 0 0-2-2Zm-6 0a2 2 0 0 0-2 2c0 1.1.9 2 2 2a2 2 0 0 0 2-2 2 2 0 0 0-2-2Z"
|
||||
fill-rule="nonzero"
|
||||
></path></svg
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
role="group"
|
||||
class="tox-toolbar__overflow tox-toolbar__overflow--closed"
|
||||
style="height: 0px"
|
||||
>
|
||||
<div
|
||||
aria-label="indentation"
|
||||
role="toolbar"
|
||||
data-alloy-tabstop="true"
|
||||
class="tox-toolbar__group"
|
||||
>
|
||||
<button
|
||||
aria-label="Decrease indent"
|
||||
data-mce-name="outdent"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
class="tox-tbtn tox-tbtn--disabled"
|
||||
aria-disabled="true"
|
||||
style="width: 34px"
|
||||
>
|
||||
<span class="tox-icon tox-tbtn__icon-wrap"
|
||||
><svg width="24" height="24" focusable="false">
|
||||
<path
|
||||
d="M7 5h12c.6 0 1 .4 1 1s-.4 1-1 1H7a1 1 0 1 1 0-2Zm5 4h7c.6 0 1 .4 1 1s-.4 1-1 1h-7a1 1 0 0 1 0-2Zm0 4h7c.6 0 1 .4 1 1s-.4 1-1 1h-7a1 1 0 0 1 0-2Zm-5 4h12a1 1 0 0 1 0 2H7a1 1 0 0 1 0-2Zm1.6-3.8a1 1 0 0 1-1.2 1.6l-3-2a1 1 0 0 1 0-1.6l3-2a1 1 0 0 1 1.2 1.6L6.8 12l1.8 1.2Z"
|
||||
fill-rule="evenodd"
|
||||
></path></svg
|
||||
></span></button
|
||||
><button
|
||||
aria-label="Increase indent"
|
||||
data-mce-name="indent"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
class="tox-tbtn"
|
||||
aria-disabled="false"
|
||||
style="width: 34px"
|
||||
>
|
||||
<span class="tox-icon tox-tbtn__icon-wrap"
|
||||
><svg width="24" height="24" focusable="false">
|
||||
<path
|
||||
d="M7 5h12c.6 0 1 .4 1 1s-.4 1-1 1H7a1 1 0 1 1 0-2Zm5 4h7c.6 0 1 .4 1 1s-.4 1-1 1h-7a1 1 0 0 1 0-2Zm0 4h7c.6 0 1 .4 1 1s-.4 1-1 1h-7a1 1 0 0 1 0-2Zm-5 4h12a1 1 0 0 1 0 2H7a1 1 0 0 1 0-2Zm-2.6-3.8L6.2 12l-1.8-1.2a1 1 0 0 1 1.2-1.6l3 2a1 1 0 0 1 0 1.6l-3 2a1 1 0 1 1-1.2-1.6Z"
|
||||
fill-rule="evenodd"
|
||||
></path></svg
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
aria-label="lists"
|
||||
role="toolbar"
|
||||
data-alloy-tabstop="true"
|
||||
class="tox-toolbar__group"
|
||||
>
|
||||
<button
|
||||
aria-label="Bullet list"
|
||||
data-mce-name="bullist"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
class="tox-tbtn"
|
||||
aria-disabled="false"
|
||||
aria-pressed="false"
|
||||
style="width: 34px"
|
||||
>
|
||||
<span class="tox-icon tox-tbtn__icon-wrap"
|
||||
><svg width="24" height="24" focusable="false">
|
||||
<path
|
||||
d="M11 5h8c.6 0 1 .4 1 1s-.4 1-1 1h-8a1 1 0 0 1 0-2Zm0 6h8c.6 0 1 .4 1 1s-.4 1-1 1h-8a1 1 0 0 1 0-2Zm0 6h8c.6 0 1 .4 1 1s-.4 1-1 1h-8a1 1 0 0 1 0-2ZM4.5 6c0-.4.1-.8.4-1 .3-.4.7-.5 1.1-.5.4 0 .8.1 1 .4.4.3.5.7.5 1.1 0 .4-.1.8-.4 1-.3.4-.7.5-1.1.5-.4 0-.8-.1-1-.4-.4-.3-.5-.7-.5-1.1Zm0 6c0-.4.1-.8.4-1 .3-.4.7-.5 1.1-.5.4 0 .8.1 1 .4.4.3.5.7.5 1.1 0 .4-.1.8-.4 1-.3.4-.7.5-1.1.5-.4 0-.8-.1-1-.4-.4-.3-.5-.7-.5-1.1Zm0 6c0-.4.1-.8.4-1 .3-.4.7-.5 1.1-.5.4 0 .8.1 1 .4.4.3.5.7.5 1.1 0 .4-.1.8-.4 1-.3.4-.7.5-1.1.5-.4 0-.8-.1-1-.4-.4-.3-.5-.7-.5-1.1Z"
|
||||
fill-rule="evenodd"
|
||||
></path></svg
|
||||
></span></button
|
||||
><button
|
||||
aria-label="Numbered list"
|
||||
data-mce-name="numlist"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
class="tox-tbtn"
|
||||
aria-disabled="false"
|
||||
aria-pressed="false"
|
||||
style="width: 34px"
|
||||
>
|
||||
<span class="tox-icon tox-tbtn__icon-wrap"
|
||||
><svg width="24" height="24" focusable="false">
|
||||
<path
|
||||
d="M10 17h8c.6 0 1 .4 1 1s-.4 1-1 1h-8a1 1 0 0 1 0-2Zm0-6h8c.6 0 1 .4 1 1s-.4 1-1 1h-8a1 1 0 0 1 0-2Zm0-6h8c.6 0 1 .4 1 1s-.4 1-1 1h-8a1 1 0 1 1 0-2ZM6 4v3.5c0 .3-.2.5-.5.5a.5.5 0 0 1-.5-.5V5h-.5a.5.5 0 0 1 0-1H6Zm-1 8.8.2.2h1.3c.3 0 .5.2.5.5s-.2.5-.5.5H4.9a1 1 0 0 1-.9-1V13c0-.4.3-.8.6-1l1.2-.4.2-.3a.2.2 0 0 0-.2-.2H4.5a.5.5 0 0 1-.5-.5c0-.3.2-.5.5-.5h1.6c.5 0 .9.4.9 1v.1c0 .4-.3.8-.6 1l-1.2.4-.2.3ZM7 17v2c0 .6-.4 1-1 1H4.5a.5.5 0 0 1 0-1h1.2c.2 0 .3-.1.3-.3 0-.2-.1-.3-.3-.3H4.4a.4.4 0 1 1 0-.8h1.3c.2 0 .3-.1.3-.3 0-.2-.1-.3-.3-.3H4.5a.5.5 0 1 1 0-1H6c.6 0 1 .4 1 1Z"
|
||||
fill-rule="evenodd"
|
||||
></path></svg
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
aria-label="advanced"
|
||||
role="toolbar"
|
||||
data-alloy-tabstop="true"
|
||||
class="tox-toolbar__group"
|
||||
>
|
||||
<button
|
||||
aria-label="Equation editor"
|
||||
data-mce-name="tiny_equation"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
class="tox-tbtn"
|
||||
aria-disabled="false"
|
||||
aria-pressed="false"
|
||||
style="width: 34px"
|
||||
>
|
||||
<span class="tox-icon tox-tbtn__icon-wrap"
|
||||
><svg
|
||||
data-buttonsource="moodle"
|
||||
width="24"
|
||||
height="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
focusable="false"
|
||||
>
|
||||
<image
|
||||
href="https://school.moodledemo.net/theme/image.php/boost/tiny_equation/1775905339/icon"
|
||||
width="24"
|
||||
height="24"
|
||||
></image></svg
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tox-anchorbar"></div>
|
||||
</div>
|
||||
<div class="tox-sidebar-wrap" style="height: 225px">
|
||||
<div class="tox-edit-area">
|
||||
<iframe
|
||||
id="q125:6_answer_id_ifr"
|
||||
frameborder="0"
|
||||
allowtransparency="true"
|
||||
title="Rich text area"
|
||||
class="tox-edit-area__iframe"
|
||||
srcdoc='<!DOCTYPE html><html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /></head><body id="tinymce" class="mce-content-body " data-id="q125:6_answer_id" aria-label="Rich text area. Press ALT-0 for help."><br></body></html>'
|
||||
></iframe>
|
||||
</div>
|
||||
<div role="presentation" class="tox-sidebar">
|
||||
<div
|
||||
data-alloy-tabstop="true"
|
||||
tabindex="-1"
|
||||
class="tox-sidebar__slider tox-sidebar--sliding-closed"
|
||||
style="width: 0px"
|
||||
>
|
||||
<div class="tox-sidebar__pane-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tox-bottom-anchorbar"></div>
|
||||
</div>
|
||||
<div aria-hidden="true" class="tox-view-wrap" style="display: none">
|
||||
<div class="tox-view-wrap__slot-container"></div>
|
||||
</div>
|
||||
<div class="tox-statusbar">
|
||||
<div
|
||||
class="tox-statusbar__text-container tox-statusbar__text-container--flex-start"
|
||||
>
|
||||
<div
|
||||
role="navigation"
|
||||
data-alloy-tabstop="true"
|
||||
class="tox-statusbar__path"
|
||||
aria-disabled="false"
|
||||
>
|
||||
<div
|
||||
data-index="0"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
class="tox-statusbar__path-item"
|
||||
aria-disabled="false"
|
||||
>
|
||||
p
|
||||
</div>
|
||||
</div>
|
||||
<div class="tox-statusbar__right-container">
|
||||
<button
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
data-alloy-tabstop="true"
|
||||
class="tox-statusbar__wordcount"
|
||||
>
|
||||
0 words</button
|
||||
><span class="tox-statusbar__branding"
|
||||
><a
|
||||
href="https://www.tiny.cloud/powered-by-tiny?utm_campaign=poweredby&utm_source=tiny&utm_medium=referral&utm_content=v7"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
aria-label="Build with TinyMCE"
|
||||
tabindex="-1"
|
||||
>Build with
|
||||
<svg
|
||||
height="16"
|
||||
viewBox="0 0 80 16"
|
||||
width="80"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g opacity=".8">
|
||||
<path
|
||||
d="m80 3.537v-2.202h-7.976v11.585h7.976v-2.25h-5.474v-2.621h4.812v-2.069h-4.812v-2.443zm-10.647 6.929c-.493.217-1.13.337-1.864.337s-1.276-.156-1.805-.47a3.732 3.732 0 0 1 -1.3-1.298c-.324-.554-.48-1.191-.48-1.877s.156-1.335.48-1.877a3.635 3.635 0 0 1 1.3-1.299 3.466 3.466 0 0 1 1.805-.481c.65 0 .914.06 1.263.18.36.12.698.277.986.47.289.192.578.384.842.6l.12.085v-2.586l-.023-.024c-.385-.35-.855-.614-1.384-.818-.53-.205-1.155-.313-1.877-.313-.721 0-1.6.144-2.333.445a5.773 5.773 0 0 0 -1.937 1.251 5.929 5.929 0 0 0 -1.324 1.9c-.324.735-.48 1.565-.48 2.455s.156 1.72.48 2.454c.325.734.758 1.383 1.324 1.913.553.53 1.215.938 1.937 1.25a6.286 6.286 0 0 0 2.333.434c.819 0 1.384-.108 1.961-.313.59-.216 1.083-.505 1.468-.866l.024-.024v-2.49l-.12.096c-.41.337-.878.626-1.396.866zm-14.869-4.15-4.8-5.04-.024-.025h-.902v11.67h2.502v-6.847l2.827 3.08.385.409.397-.41 2.791-3.067v6.845h2.502v-11.679h-.902l-4.788 5.052z"
|
||||
></path>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="m15.543 5.137c0-3.032-2.466-5.113-4.957-5.137-.36 0-.745.024-1.094.096-.157.024-3.85.758-3.85.758-3.032.602-4.62 2.466-4.704 4.788-.024.89-.024 4.27-.024 4.27.036 3.165 2.406 5.138 5.017 5.126.337 0 1.119-.109 1.287-.145.144-.024.385-.084.746-.144.661-.12 1.684-.325 3.067-.602 2.37-.409 4.103-2.009 4.44-4.33.156-1.023.084-4.692.084-4.692zm-3.213 3.308-2.346.457v2.31l-5.859 1.143v-5.75l2.346-.458v3.441l3.513-.686v-3.44l-3.513.685v-2.297l5.859-1.143v5.75zm20.09-3.296-.083-1.023h-2.13v8.794h2.346v-4.884c0-1.107.95-1.985 2.057-1.997 1.095 0 1.901.89 1.901 1.997v4.884h2.346v-5.245c-.012-2.105-1.588-3.777-3.67-3.765a3.764 3.764 0 0 0 -2.778 1.25l.012-.011zm-6.014-4.102 2.346-.458v2.298l-2.346.457z"
|
||||
fill-rule="evenodd"
|
||||
></path>
|
||||
<path d="m28.752 4.126h-2.346v8.794h2.346z"></path>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="m43.777 15.483 4.043-11.357h-2.418l-1.54 4.355-.445 1.324-.36-1.324-1.54-4.355h-2.418l3.151 8.794-1.083 3.08zm-21.028-5.51c0 .722.541 1.034.878 1.034s.638-.048.95-.144l.518 1.708c-.217.145-.879.518-2.13.518a2.565 2.565 0 0 1 -2.562-2.587c-.024-1.082-.024-2.49 0-4.21h-1.54v-2.142h1.54v-1.912l2.346-.458v2.37h2.201v2.142h-2.2v3.693-.012z"
|
||||
fill-rule="evenodd"
|
||||
></path>
|
||||
</g></svg></a
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Press the Up and Down arrow keys to resize the editor."
|
||||
data-mce-name="resize-handle"
|
||||
data-alloy-tabstop="true"
|
||||
tabindex="-1"
|
||||
class="tox-statusbar__resize-handle"
|
||||
>
|
||||
<svg width="10" height="10" focusable="false">
|
||||
<g fill-rule="nonzero">
|
||||
<path
|
||||
d="M8.1 1.1A.5.5 0 1 1 9 2l-7 7A.5.5 0 1 1 1 8l7-7ZM8.1 5.1A.5.5 0 1 1 9 6l-3 3A.5.5 0 1 1 5 8l3-3Z"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div aria-hidden="true" class="tox-throbber" style="display: none"></div>
|
||||
</div>
|
||||
<div
|
||||
class="tox tox-silver-sink tox-silver-popup-sink tox-tinymce-aux"
|
||||
style="position: relative"
|
||||
></div>
|
||||
</div>
|
||||
<div><input type="hidden" name="q125:6_answerformat" value="1" /></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="attachments"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,66 @@
|
||||
<div id="question-125-7" class="que essay manualgraded notyetanswered">
|
||||
<div class="info">
|
||||
<h3 class="no">Question <span class="qno">7</span></h3>
|
||||
<div class="state">Not yet answered</div>
|
||||
<div class="grade">Marked out of 1.00</div>
|
||||
<div class="questionflag editable" id="yui_3_18_1_1_1775909302600_54">
|
||||
<input type="hidden" name="q125:7_:flagged" value="0" /><input
|
||||
type="hidden"
|
||||
value="qaid=804&qubaid=125&qid=3704&slot=7&checksum=4717bc6928ebf1e9a66ab68e7e595899&sesskey=MRh4T7j2fU&newstate="
|
||||
class="questionflagpostdata"
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
class="questionflagvalue"
|
||||
id="q125:7_:flaggedcheckbox"
|
||||
name="q125:7_:flagged"
|
||||
value="0"
|
||||
/><a
|
||||
tabindex="0"
|
||||
class="aabtn"
|
||||
role="button"
|
||||
aria-pressed="false"
|
||||
aria-label="Flagged"
|
||||
title="Flag this question for future reference"
|
||||
><img
|
||||
class="questionflagimage"
|
||||
src="https://school.moodledemo.net/theme/image.php/boost/core/1775905339/i/unflagged"
|
||||
alt=""
|
||||
/>Flag question</a
|
||||
>
|
||||
</div>
|
||||
<div class="editquestion">
|
||||
<a
|
||||
href="https://school.moodledemo.net/question/bank/editquestion/question.php?cmid=1655&returnurl=%2Fmod%2Fquiz%2Fattempt.php%3Fattempt%3D89%26cmid%3D1655%23question-125-7&id=3704"
|
||||
><i class="icon fa fa-pen fa-fw iconsmall" title="Edit" role="img" aria-label="Edit"></i
|
||||
>Edit question</a
|
||||
>
|
||||
</div>
|
||||
<span class="badge bg-primary text-light">v1 (latest)</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="formulation clearfix">
|
||||
<h4 class="accesshide">Question text</h4>
|
||||
<input type="hidden" name="q125:7_:sequencecheck" value="1" />
|
||||
<div class="qtext" style="cursor: pointer">
|
||||
<div class="clearfix">
|
||||
<p>What is resilience?</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ablock">
|
||||
<div class="answer">
|
||||
<label class="visually-hidden" for="q125:7_answer_id">Answer text Question 7</label
|
||||
><textarea
|
||||
name="q125:7_answer"
|
||||
id="q125:7_answer_id"
|
||||
class="qtype_essay_plain qtype_essay_response form-control"
|
||||
rows="10"
|
||||
cols="60"
|
||||
></textarea
|
||||
><input type="hidden" name="q125:7_answerformat" value="2" />
|
||||
</div>
|
||||
<div class="attachments"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,176 @@
|
||||
<div id="question-124-3" class="que match interactivecountback notyetanswered">
|
||||
<div class="info">
|
||||
<h3 class="no">Question <span class="qno">2</span></h3>
|
||||
<div class="state">Tries remaining: 3</div>
|
||||
<div class="grade">Marked out of 1.00</div>
|
||||
<div class="questionflag editable" id="yui_3_18_1_1_1775932863373_30">
|
||||
<input type="hidden" name="q124:3_:flagged" value="0" /><input
|
||||
type="hidden"
|
||||
value="qaid=801&qubaid=124&qid=1581&slot=3&checksum=35d5356ab101c7fac69fb355c4f95306&sesskey=xrcdi6NRF8&newstate="
|
||||
class="questionflagpostdata"
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
class="questionflagvalue"
|
||||
id="q124:3_:flaggedcheckbox"
|
||||
name="q124:3_:flagged"
|
||||
value="0"
|
||||
/><a
|
||||
tabindex="0"
|
||||
class="aabtn"
|
||||
role="button"
|
||||
aria-pressed="false"
|
||||
aria-label="Flagged"
|
||||
title="Flag this question for future reference"
|
||||
><img
|
||||
class="questionflagimage"
|
||||
src="https://school.moodledemo.net/theme/image.php/boost/core/1775926936/i/unflagged"
|
||||
alt=""
|
||||
/>Flag question</a
|
||||
>
|
||||
</div>
|
||||
<div class="editquestion">
|
||||
<a
|
||||
href="https://school.moodledemo.net/question/bank/editquestion/question.php?cmid=1210&returnurl=%2Fmod%2Fquiz%2Fattempt.php%3Fattempt%3D88%26cmid%3D1210%23question-124-3&id=1581"
|
||||
><i class="icon fa fa-pen fa-fw iconsmall" title="Edit" role="img" aria-label="Edit"></i
|
||||
>Edit question</a
|
||||
>
|
||||
</div>
|
||||
<span class="badge bg-primary text-light">v3 (latest)</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="formulation clearfix">
|
||||
<h4 class="accesshide">Question text</h4>
|
||||
<input type="hidden" name="q124:3_:sequencecheck" value="1" />
|
||||
<div class="qtext" id="q124:3_qtext" style="cursor: pointer">
|
||||
<div class="clearfix">
|
||||
<p>This is an example of a matching question type.</p>
|
||||
<p>Match the Moodle features to their functionality:</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ablock">
|
||||
<table class="answer" role="presentation">
|
||||
<tbody role="presentation">
|
||||
<tr class="r0" role="presentation">
|
||||
<td class="text" id="q124:3_sub0_itemtext" role="presentation">
|
||||
<p>Wiki</p>
|
||||
</td>
|
||||
<td class="control" role="presentation">
|
||||
<label class="visually-hidden" for="menuq124:3_sub0">Answer 1 Question 2</label
|
||||
><select
|
||||
class="select form-select form-select d-inline-block ms-1"
|
||||
aria-describedby="q124:3_qtext q124:3_sub0_itemtext"
|
||||
id="menuq124:3_sub0"
|
||||
name="q124:3_sub0"
|
||||
>
|
||||
<option selected="selected" value="0">Choose...</option>
|
||||
<option value="1">quick poll</option>
|
||||
<option value="2">collaborative editing</option>
|
||||
<option value="3">Peer assessment</option>
|
||||
<option value="4">interactive content</option>
|
||||
<option value="5">branching activities</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="r1" role="presentation">
|
||||
<td class="text" id="q124:3_sub1_itemtext" role="presentation">
|
||||
<p>Lesson</p>
|
||||
</td>
|
||||
<td class="control" role="presentation">
|
||||
<label class="visually-hidden" for="menuq124:3_sub1">Answer 2 Question 2</label
|
||||
><select
|
||||
class="select form-select form-select d-inline-block ms-1"
|
||||
aria-describedby="q124:3_sub1_itemtext"
|
||||
id="menuq124:3_sub1"
|
||||
name="q124:3_sub1"
|
||||
>
|
||||
<option selected="selected" value="0">Choose...</option>
|
||||
<option value="1">quick poll</option>
|
||||
<option value="2">collaborative editing</option>
|
||||
<option value="3">Peer assessment</option>
|
||||
<option value="4">interactive content</option>
|
||||
<option value="5">branching activities</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="r0" role="presentation">
|
||||
<td class="text" id="q124:3_sub2_itemtext" role="presentation">
|
||||
<p>H5P</p>
|
||||
</td>
|
||||
<td class="control" role="presentation">
|
||||
<label class="visually-hidden" for="menuq124:3_sub2">Answer 3 Question 2</label
|
||||
><select
|
||||
class="select form-select form-select d-inline-block ms-1"
|
||||
aria-describedby="q124:3_sub2_itemtext"
|
||||
id="menuq124:3_sub2"
|
||||
name="q124:3_sub2"
|
||||
>
|
||||
<option selected="selected" value="0">Choose...</option>
|
||||
<option value="1">quick poll</option>
|
||||
<option value="2">collaborative editing</option>
|
||||
<option value="3">Peer assessment</option>
|
||||
<option value="4">interactive content</option>
|
||||
<option value="5">branching activities</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="r1" role="presentation">
|
||||
<td class="text" id="q124:3_sub3_itemtext" role="presentation">
|
||||
<p>Workshop</p>
|
||||
</td>
|
||||
<td class="control" role="presentation">
|
||||
<label class="visually-hidden" for="menuq124:3_sub3">Answer 4 Question 2</label
|
||||
><select
|
||||
class="select form-select form-select d-inline-block ms-1"
|
||||
aria-describedby="q124:3_sub3_itemtext"
|
||||
id="menuq124:3_sub3"
|
||||
name="q124:3_sub3"
|
||||
>
|
||||
<option selected="selected" value="0">Choose...</option>
|
||||
<option value="1">quick poll</option>
|
||||
<option value="2">collaborative editing</option>
|
||||
<option value="3">Peer assessment</option>
|
||||
<option value="4">interactive content</option>
|
||||
<option value="5">branching activities</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="r0" role="presentation">
|
||||
<td class="text" id="q124:3_sub4_itemtext" role="presentation">
|
||||
<p>Choice</p>
|
||||
</td>
|
||||
<td class="control" role="presentation">
|
||||
<label class="visually-hidden" for="menuq124:3_sub4">Answer 5 Question 2</label
|
||||
><select
|
||||
class="select form-select form-select d-inline-block ms-1"
|
||||
aria-describedby="q124:3_sub4_itemtext"
|
||||
id="menuq124:3_sub4"
|
||||
name="q124:3_sub4"
|
||||
>
|
||||
<option selected="selected" value="0">Choose...</option>
|
||||
<option value="1">quick poll</option>
|
||||
<option value="2">collaborative editing</option>
|
||||
<option value="3">Peer assessment</option>
|
||||
<option value="4">interactive content</option>
|
||||
<option value="5">branching activities</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="im-controls">
|
||||
<button
|
||||
type="submit"
|
||||
id="q124:3_-submit"
|
||||
name="q124:3_-submit"
|
||||
value="1"
|
||||
class="submit btn btn-secondary"
|
||||
data-savescrollposition="true"
|
||||
>
|
||||
Check <span class="visually-hidden">Question 2</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,121 @@
|
||||
<div id="question-125-2" class="que multichoice deferredfeedback notyetanswered">
|
||||
<div class="info">
|
||||
<h3 class="no">Question <span class="qno">2</span></h3>
|
||||
<div class="state">Not yet answered</div>
|
||||
<div class="grade">Marked out of 1.00</div>
|
||||
<div class="questionflag editable" id="yui_3_18_1_1_1775909302600_24">
|
||||
<input type="hidden" name="q125:2_:flagged" value="0" /><input
|
||||
type="hidden"
|
||||
value="qaid=799&qubaid=125&qid=3700&slot=2&checksum=c534dfa95146a052b3bdd2d1dd744a46&sesskey=MRh4T7j2fU&newstate="
|
||||
class="questionflagpostdata"
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
class="questionflagvalue"
|
||||
id="q125:2_:flaggedcheckbox"
|
||||
name="q125:2_:flagged"
|
||||
value="0"
|
||||
/><a
|
||||
tabindex="0"
|
||||
class="aabtn"
|
||||
role="button"
|
||||
aria-pressed="false"
|
||||
aria-label="Flagged"
|
||||
title="Flag this question for future reference"
|
||||
><img
|
||||
class="questionflagimage"
|
||||
src="https://school.moodledemo.net/theme/image.php/boost/core/1775905339/i/unflagged"
|
||||
alt=""
|
||||
/>Flag question</a
|
||||
>
|
||||
</div>
|
||||
<div class="editquestion">
|
||||
<a
|
||||
href="https://school.moodledemo.net/question/bank/editquestion/question.php?cmid=1655&returnurl=%2Fmod%2Fquiz%2Fattempt.php%3Fattempt%3D89%26cmid%3D1655%23question-125-2&id=3700"
|
||||
><i class="icon fa fa-pen fa-fw iconsmall" title="Edit" role="img" aria-label="Edit"></i
|
||||
>Edit question</a
|
||||
>
|
||||
</div>
|
||||
<span class="badge bg-primary text-light">v2 (latest)</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="formulation clearfix">
|
||||
<h4 class="accesshide">Question text</h4>
|
||||
<input type="hidden" name="q125:2_:sequencecheck" value="1" />
|
||||
<div class="qtext" style="cursor: pointer">
|
||||
<div class="clearfix">
|
||||
<p>Which of the following actions can help you develop resilience?</p>
|
||||
</div>
|
||||
</div>
|
||||
<fieldset class="ablock no-overflow visual-scroll-x">
|
||||
<legend class="prompt h6 fw-normal">
|
||||
<span class="visually-hidden">Question 2</span> Select one or more:
|
||||
</legend>
|
||||
<div class="answer">
|
||||
<div class="r0">
|
||||
<input type="hidden" name="q125:2_choice0" value="0" /><input
|
||||
type="checkbox"
|
||||
name="q125:2_choice0"
|
||||
value="1"
|
||||
id="q125:2_choice0"
|
||||
aria-labelledby="q125:2_choice0_label"
|
||||
/>
|
||||
<div class="d-flex w-auto" id="q125:2_choice0_label" data-region="answer-label">
|
||||
<div class="flex-fill ms-1">
|
||||
<p> Ignoring emotions and pushing through hardships without reflection.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="r1">
|
||||
<input type="hidden" name="q125:2_choice1" value="0" /><input
|
||||
type="checkbox"
|
||||
name="q125:2_choice1"
|
||||
value="1"
|
||||
id="q125:2_choice1"
|
||||
aria-labelledby="q125:2_choice1_label"
|
||||
/>
|
||||
<div class="d-flex w-auto" id="q125:2_choice1_label" data-region="answer-label">
|
||||
<div class="flex-fill ms-1">
|
||||
<p>
|
||||
Practising self-care, such as maintaining a healthy sleep routine and regular
|
||||
exercise
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="r0">
|
||||
<input type="hidden" name="q125:2_choice2" value="0" /><input
|
||||
type="checkbox"
|
||||
name="q125:2_choice2"
|
||||
value="1"
|
||||
id="q125:2_choice2"
|
||||
aria-labelledby="q125:2_choice2_label"
|
||||
/>
|
||||
<div class="d-flex w-auto" id="q125:2_choice2_label" data-region="answer-label">
|
||||
<div class="flex-fill ms-1">
|
||||
<p>
|
||||
Seeking support from friends, mentors, or professional resources when facing
|
||||
difficulties.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="r1">
|
||||
<input type="hidden" name="q125:2_choice3" value="0" /><input
|
||||
type="checkbox"
|
||||
name="q125:2_choice3"
|
||||
value="1"
|
||||
id="q125:2_choice3"
|
||||
aria-labelledby="q125:2_choice3_label"
|
||||
/>
|
||||
<div class="d-flex w-auto" id="q125:2_choice3_label" data-region="answer-label">
|
||||
<div class="flex-fill ms-1">
|
||||
<p>Avoiding challenges and stressful situations to prevent failure</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,63 @@
|
||||
<div id="question-125-5" class="que numerical deferredfeedback notyetanswered">
|
||||
<div class="info">
|
||||
<h3 class="no">Question <span class="qno">5</span></h3>
|
||||
<div class="state">Not yet answered</div>
|
||||
<div class="grade">Marked out of 1.00</div>
|
||||
<div class="questionflag editable" id="yui_3_18_1_1_1775909302600_42">
|
||||
<input type="hidden" name="q125:5_:flagged" value="0" /><input
|
||||
type="hidden"
|
||||
value="qaid=802&qubaid=125&qid=3702&slot=5&checksum=2c2029ed58598b4af63b0db74bebcaab&sesskey=MRh4T7j2fU&newstate="
|
||||
class="questionflagpostdata"
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
class="questionflagvalue"
|
||||
id="q125:5_:flaggedcheckbox"
|
||||
name="q125:5_:flagged"
|
||||
value="0"
|
||||
/><a
|
||||
tabindex="0"
|
||||
class="aabtn"
|
||||
role="button"
|
||||
aria-pressed="false"
|
||||
aria-label="Flagged"
|
||||
title="Flag this question for future reference"
|
||||
><img
|
||||
class="questionflagimage"
|
||||
src="https://school.moodledemo.net/theme/image.php/boost/core/1775905339/i/unflagged"
|
||||
alt=""
|
||||
/>Flag question</a
|
||||
>
|
||||
</div>
|
||||
<div class="editquestion">
|
||||
<a
|
||||
href="https://school.moodledemo.net/question/bank/editquestion/question.php?cmid=1655&returnurl=%2Fmod%2Fquiz%2Fattempt.php%3Fattempt%3D89%26cmid%3D1655%23question-125-5&id=3702"
|
||||
><i class="icon fa fa-pen fa-fw iconsmall" title="Edit" role="img" aria-label="Edit"></i
|
||||
>Edit question</a
|
||||
>
|
||||
</div>
|
||||
<span class="badge bg-primary text-light">v1 (latest)</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="formulation clearfix">
|
||||
<h4 class="accesshide">Question text</h4>
|
||||
<input type="hidden" name="q125:5_:sequencecheck" value="1" />
|
||||
<div class="qtext" style="cursor: pointer">
|
||||
<div class="clearfix">
|
||||
<p>What is 2 + 2?</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ablock d-flex flex-wrap align-items-center">
|
||||
<label for="q125:5_answer">Answer: <span class="visually-hidden">Question 5</span></label
|
||||
><span class="answer"
|
||||
><input
|
||||
type="text"
|
||||
name="q125:5_answer"
|
||||
id="q125:5_answer"
|
||||
size="30"
|
||||
class="form-control d-inline"
|
||||
/></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,821 @@
|
||||
<div id="question-124-18" class="que ordering interactive notyetanswered">
|
||||
<div class="info">
|
||||
<h3 class="no">Question <span class="qno">17</span></h3>
|
||||
<div class="state">Tries remaining: 1</div>
|
||||
<div class="grade">Marked out of 1.00</div>
|
||||
<div class="questionflag editable" id="yui_3_18_1_1_1775990511189_120">
|
||||
<input type="hidden" name="q124:18_:flagged" value="0" /><input
|
||||
type="hidden"
|
||||
value="qaid=810&qubaid=124&qid=1773&slot=18&checksum=5dbbc1b24217228df046bd34749543fb&sesskey=djxMUQR7fp&newstate="
|
||||
class="questionflagpostdata"
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
class="questionflagvalue"
|
||||
id="q124:18_:flaggedcheckbox"
|
||||
name="q124:18_:flagged"
|
||||
value="0"
|
||||
/><a
|
||||
tabindex="0"
|
||||
class="aabtn"
|
||||
role="button"
|
||||
aria-pressed="false"
|
||||
aria-label="Flagged"
|
||||
title="Flag this question for future reference"
|
||||
><img
|
||||
class="questionflagimage"
|
||||
src="https://school.moodledemo.net/theme/image.php/boost/core/1775984535/i/unflagged"
|
||||
alt=""
|
||||
/>Flag question</a
|
||||
>
|
||||
</div>
|
||||
<div class="editquestion">
|
||||
<a
|
||||
href="https://school.moodledemo.net/question/bank/editquestion/question.php?cmid=1210&returnurl=%2Fmod%2Fquiz%2Fattempt.php%3Fattempt%3D88%26cmid%3D1210%23question-124-18&id=1773"
|
||||
><i class="icon fa fa-pen fa-fw iconsmall" title="Edit" role="img" aria-label="Edit"></i
|
||||
>Edit question</a
|
||||
>
|
||||
</div>
|
||||
<span class="badge bg-primary text-light">v4 (latest)</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="formulation clearfix">
|
||||
<h4 class="accesshide">Question text</h4>
|
||||
<input type="hidden" name="q124:18_:sequencecheck" value="1" />
|
||||
<div class="qtext" style="cursor: pointer">
|
||||
<div class="clearfix">
|
||||
<p>
|
||||
Put this dialogue into the correct order. (This is an example of the Ordering question
|
||||
type.)
|
||||
</p>
|
||||
</div>
|
||||
<div class="ablock" id="id_ablock_1773">
|
||||
<div class="answer ordering">
|
||||
<div
|
||||
aria-live="polite"
|
||||
class="visually-hidden"
|
||||
id="id_sortable_1773-announcement"
|
||||
></div>
|
||||
<ul class="sortablelist vertical numberingnone active" id="id_sortable_1773">
|
||||
<li class="p-2 sortableitem" id="ordering_item_c88215f9a7ea9937bc26558b5ceae12d">
|
||||
<div class="d-flex">
|
||||
<div class="d-flex p-2 grip">
|
||||
<i class="fas fa-grip-vertical" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div
|
||||
class="d-flex align-items-center flex-grow-1 px-2"
|
||||
id="ordering_item_c88215f9a7ea9937bc26558b5ceae12d-text"
|
||||
data-itemcontent=""
|
||||
>
|
||||
Probably! I travelled from Kenya to Kamchatka!
|
||||
</div>
|
||||
<div class="d-flex px-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-icon"
|
||||
data-action="move-backward"
|
||||
aria-label="Move up"
|
||||
aria-describedby="ordering_item_c88215f9a7ea9937bc26558b5ceae12d-text"
|
||||
>
|
||||
<i class="icon fa fa-chevron-up" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-icon"
|
||||
data-action="move-forward"
|
||||
aria-label="Move down"
|
||||
aria-describedby="ordering_item_c88215f9a7ea9937bc26558b5ceae12d-text"
|
||||
>
|
||||
<i class="icon fa fa-chevron-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="p-2 sortableitem" id="ordering_item_3a7a3b22fc58e71418c2d218e0f4407b">
|
||||
<div class="d-flex">
|
||||
<div class="d-flex p-2 grip">
|
||||
<i class="fas fa-grip-vertical" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div
|
||||
class="d-flex align-items-center flex-grow-1 px-2"
|
||||
id="ordering_item_3a7a3b22fc58e71418c2d218e0f4407b-text"
|
||||
data-itemcontent=""
|
||||
>
|
||||
Hello John! How are you?
|
||||
</div>
|
||||
<div class="d-flex px-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-icon"
|
||||
data-action="move-backward"
|
||||
aria-label="Move up"
|
||||
aria-describedby="ordering_item_3a7a3b22fc58e71418c2d218e0f4407b-text"
|
||||
>
|
||||
<i class="icon fa fa-chevron-up" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-icon"
|
||||
data-action="move-forward"
|
||||
aria-label="Move down"
|
||||
aria-describedby="ordering_item_3a7a3b22fc58e71418c2d218e0f4407b-text"
|
||||
>
|
||||
<i class="icon fa fa-chevron-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="p-2 sortableitem" id="ordering_item_2388d00649281f3ad808246e6002be9b">
|
||||
<div class="d-flex">
|
||||
<div class="d-flex p-2 grip">
|
||||
<i class="fas fa-grip-vertical" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div
|
||||
class="d-flex align-items-center flex-grow-1 px-2"
|
||||
id="ordering_item_2388d00649281f3ad808246e6002be9b-text"
|
||||
data-itemcontent=""
|
||||
>
|
||||
Really? Why is that?
|
||||
</div>
|
||||
<div class="d-flex px-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-icon"
|
||||
data-action="move-backward"
|
||||
aria-label="Move up"
|
||||
aria-describedby="ordering_item_2388d00649281f3ad808246e6002be9b-text"
|
||||
>
|
||||
<i class="icon fa fa-chevron-up" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-icon"
|
||||
data-action="move-forward"
|
||||
aria-label="Move down"
|
||||
aria-describedby="ordering_item_2388d00649281f3ad808246e6002be9b-text"
|
||||
>
|
||||
<i class="icon fa fa-chevron-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="p-2 sortableitem" id="ordering_item_38c0bd413bd7de3368655abc8e0390c9">
|
||||
<div class="d-flex">
|
||||
<div class="d-flex p-2 grip">
|
||||
<i class="fas fa-grip-vertical" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div
|
||||
class="d-flex align-items-center flex-grow-1 px-2"
|
||||
id="ordering_item_38c0bd413bd7de3368655abc8e0390c9-text"
|
||||
data-itemcontent=""
|
||||
>
|
||||
Oh you probably have jet lag!
|
||||
</div>
|
||||
<div class="d-flex px-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-icon"
|
||||
data-action="move-backward"
|
||||
aria-label="Move up"
|
||||
aria-describedby="ordering_item_38c0bd413bd7de3368655abc8e0390c9-text"
|
||||
>
|
||||
<i class="icon fa fa-chevron-up" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-icon"
|
||||
data-action="move-forward"
|
||||
aria-label="Move down"
|
||||
aria-describedby="ordering_item_38c0bd413bd7de3368655abc8e0390c9-text"
|
||||
>
|
||||
<i class="icon fa fa-chevron-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="p-2 sortableitem" id="ordering_item_2d618912cb4b8d79f8ebb8ec7fc14f11">
|
||||
<div class="d-flex">
|
||||
<div class="d-flex p-2 grip">
|
||||
<i class="fas fa-grip-vertical" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div
|
||||
class="d-flex align-items-center flex-grow-1 px-2"
|
||||
id="ordering_item_2d618912cb4b8d79f8ebb8ec7fc14f11-text"
|
||||
data-itemcontent=""
|
||||
>
|
||||
Well actually I'm a bit tired.
|
||||
</div>
|
||||
<div class="d-flex px-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-icon"
|
||||
data-action="move-backward"
|
||||
aria-label="Move up"
|
||||
aria-describedby="ordering_item_2d618912cb4b8d79f8ebb8ec7fc14f11-text"
|
||||
>
|
||||
<i class="icon fa fa-chevron-up" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-icon"
|
||||
data-action="move-forward"
|
||||
aria-label="Move down"
|
||||
aria-describedby="ordering_item_2d618912cb4b8d79f8ebb8ec7fc14f11-text"
|
||||
>
|
||||
<i class="icon fa fa-chevron-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="p-2 sortableitem" id="ordering_item_7d2f2c6d1bdd6eea3fadf2cbe5a8b818">
|
||||
<div class="d-flex">
|
||||
<div class="d-flex p-2 grip">
|
||||
<i class="fas fa-grip-vertical" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div
|
||||
class="d-flex align-items-center flex-grow-1 px-2"
|
||||
id="ordering_item_7d2f2c6d1bdd6eea3fadf2cbe5a8b818-text"
|
||||
data-itemcontent=""
|
||||
>
|
||||
I've just been on a long plane journey!
|
||||
</div>
|
||||
<div class="d-flex px-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-icon"
|
||||
data-action="move-backward"
|
||||
aria-label="Move up"
|
||||
aria-describedby="ordering_item_7d2f2c6d1bdd6eea3fadf2cbe5a8b818-text"
|
||||
>
|
||||
<i class="icon fa fa-chevron-up" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-icon"
|
||||
data-action="move-forward"
|
||||
aria-label="Move down"
|
||||
aria-describedby="ordering_item_7d2f2c6d1bdd6eea3fadf2cbe5a8b818-text"
|
||||
>
|
||||
<i class="icon fa fa-chevron-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="p-2 sortableitem" id="ordering_item_5fab152120918e055fde5d4c068c28c0">
|
||||
<div class="d-flex">
|
||||
<div class="d-flex p-2 grip">
|
||||
<i class="fas fa-grip-vertical" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div
|
||||
class="d-flex align-items-center flex-grow-1 px-2"
|
||||
id="ordering_item_5fab152120918e055fde5d4c068c28c0-text"
|
||||
data-itemcontent=""
|
||||
>
|
||||
Fine thanks. And you?
|
||||
</div>
|
||||
<div class="d-flex px-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-icon"
|
||||
data-action="move-backward"
|
||||
aria-label="Move up"
|
||||
aria-describedby="ordering_item_5fab152120918e055fde5d4c068c28c0-text"
|
||||
>
|
||||
<i class="icon fa fa-chevron-up" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-icon"
|
||||
data-action="move-forward"
|
||||
aria-label="Move down"
|
||||
aria-describedby="ordering_item_5fab152120918e055fde5d4c068c28c0-text"
|
||||
>
|
||||
<i class="icon fa fa-chevron-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="p-2 sortableitem" id="ordering_item_ef71eac9e78d47f09b0aafe8f7f72cb9">
|
||||
<div class="d-flex">
|
||||
<div class="d-flex p-2 grip">
|
||||
<i class="fas fa-grip-vertical" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div
|
||||
class="d-flex align-items-center flex-grow-1 px-2"
|
||||
id="ordering_item_ef71eac9e78d47f09b0aafe8f7f72cb9-text"
|
||||
data-itemcontent=""
|
||||
>
|
||||
Fascinating! Tell me all about it.
|
||||
</div>
|
||||
<div class="d-flex px-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-icon"
|
||||
data-action="move-backward"
|
||||
aria-label="Move up"
|
||||
aria-describedby="ordering_item_ef71eac9e78d47f09b0aafe8f7f72cb9-text"
|
||||
>
|
||||
<i class="icon fa fa-chevron-up" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-icon"
|
||||
data-action="move-forward"
|
||||
aria-label="Move down"
|
||||
aria-describedby="ordering_item_ef71eac9e78d47f09b0aafe8f7f72cb9-text"
|
||||
>
|
||||
<i class="icon fa fa-chevron-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
name="q124:18_response_1773"
|
||||
id="id_q124_18_response_1773"
|
||||
type="hidden"
|
||||
value="ordering_item_c88215f9a7ea9937bc26558b5ceae12d,ordering_item_3a7a3b22fc58e71418c2d218e0f4407b,ordering_item_2388d00649281f3ad808246e6002be9b,ordering_item_38c0bd413bd7de3368655abc8e0390c9,ordering_item_2d618912cb4b8d79f8ebb8ec7fc14f11,ordering_item_7d2f2c6d1bdd6eea3fadf2cbe5a8b818,ordering_item_5fab152120918e055fde5d4c068c28c0,ordering_item_ef71eac9e78d47f09b0aafe8f7f72cb9"
|
||||
/>
|
||||
</div>
|
||||
<div class="im-controls">
|
||||
<button
|
||||
type="submit"
|
||||
id="q124:18_-submit"
|
||||
name="q124:18_-submit"
|
||||
value="1"
|
||||
class="submit btn btn-secondary"
|
||||
data-savescrollposition="true"
|
||||
>
|
||||
Check <span class="visually-hidden">Question 17</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="question-124-19" class="que ordering interactive notyetanswered">
|
||||
<div class="info">
|
||||
<h3 class="no">Question <span class="qno">18</span></h3>
|
||||
<div class="state">Tries remaining: 1</div>
|
||||
<div class="grade">Marked out of 1.00</div>
|
||||
<div class="questionflag editable" id="yui_3_18_1_1_1775990511189_126">
|
||||
<input type="hidden" name="q124:19_:flagged" value="0" /><input
|
||||
type="hidden"
|
||||
value="qaid=811&qubaid=124&qid=1600&slot=19&checksum=2379a29476cf85083f91b4d6d99526ea&sesskey=djxMUQR7fp&newstate="
|
||||
class="questionflagpostdata"
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
class="questionflagvalue"
|
||||
id="q124:19_:flaggedcheckbox"
|
||||
name="q124:19_:flagged"
|
||||
value="0"
|
||||
/><a
|
||||
tabindex="0"
|
||||
class="aabtn"
|
||||
role="button"
|
||||
aria-pressed="false"
|
||||
aria-label="Flagged"
|
||||
title="Flag this question for future reference"
|
||||
><img
|
||||
class="questionflagimage"
|
||||
src="https://school.moodledemo.net/theme/image.php/boost/core/1775984535/i/unflagged"
|
||||
alt=""
|
||||
/>Flag question</a
|
||||
>
|
||||
</div>
|
||||
<div class="editquestion">
|
||||
<a
|
||||
href="https://school.moodledemo.net/question/bank/editquestion/question.php?cmid=1210&returnurl=%2Fmod%2Fquiz%2Fattempt.php%3Fattempt%3D88%26cmid%3D1210%23question-124-19&id=1600"
|
||||
><i class="icon fa fa-pen fa-fw iconsmall" title="Edit" role="img" aria-label="Edit"></i
|
||||
>Edit question</a
|
||||
>
|
||||
</div>
|
||||
<span class="badge bg-primary text-light">v4 (latest)</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="formulation clearfix">
|
||||
<h4 class="accesshide">Question text</h4>
|
||||
<input type="hidden" name="q124:19_:sequencecheck" value="1" />
|
||||
<div class="qtext" style="cursor: pointer">
|
||||
<div class="clearfix">
|
||||
<p>
|
||||
Simply put the numbers in order. This is just an example question to demo the Ordering
|
||||
question type.
|
||||
</p>
|
||||
</div>
|
||||
<div class="ablock" id="id_ablock_1600">
|
||||
<div class="answer ordering">
|
||||
<div
|
||||
aria-live="polite"
|
||||
class="visually-hidden"
|
||||
id="id_sortable_1600-announcement"
|
||||
></div>
|
||||
<ul class="sortablelist vertical numberingnone active" id="id_sortable_1600">
|
||||
<li class="p-2 sortableitem" id="ordering_item_d4de8dbf3a0e2caf66e32325032220b1">
|
||||
<div class="d-flex">
|
||||
<div class="d-flex p-2 grip">
|
||||
<i class="fas fa-grip-vertical" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div
|
||||
class="d-flex align-items-center flex-grow-1 px-2"
|
||||
id="ordering_item_d4de8dbf3a0e2caf66e32325032220b1-text"
|
||||
data-itemcontent=""
|
||||
>
|
||||
two
|
||||
</div>
|
||||
<div class="d-flex px-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-icon"
|
||||
data-action="move-backward"
|
||||
aria-label="Move up"
|
||||
aria-describedby="ordering_item_d4de8dbf3a0e2caf66e32325032220b1-text"
|
||||
>
|
||||
<i class="icon fa fa-chevron-up" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-icon"
|
||||
data-action="move-forward"
|
||||
aria-label="Move down"
|
||||
aria-describedby="ordering_item_d4de8dbf3a0e2caf66e32325032220b1-text"
|
||||
>
|
||||
<i class="icon fa fa-chevron-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="p-2 sortableitem" id="ordering_item_f0936bab1853836dba0fc6b25ce6d315">
|
||||
<div class="d-flex">
|
||||
<div class="d-flex p-2 grip">
|
||||
<i class="fas fa-grip-vertical" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div
|
||||
class="d-flex align-items-center flex-grow-1 px-2"
|
||||
id="ordering_item_f0936bab1853836dba0fc6b25ce6d315-text"
|
||||
data-itemcontent=""
|
||||
>
|
||||
one
|
||||
</div>
|
||||
<div class="d-flex px-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-icon"
|
||||
data-action="move-backward"
|
||||
aria-label="Move up"
|
||||
aria-describedby="ordering_item_f0936bab1853836dba0fc6b25ce6d315-text"
|
||||
>
|
||||
<i class="icon fa fa-chevron-up" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-icon"
|
||||
data-action="move-forward"
|
||||
aria-label="Move down"
|
||||
aria-describedby="ordering_item_f0936bab1853836dba0fc6b25ce6d315-text"
|
||||
>
|
||||
<i class="icon fa fa-chevron-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="p-2 sortableitem" id="ordering_item_5e9a9d795d65eef14aabda148d71f4df">
|
||||
<div class="d-flex">
|
||||
<div class="d-flex p-2 grip">
|
||||
<i class="fas fa-grip-vertical" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div
|
||||
class="d-flex align-items-center flex-grow-1 px-2"
|
||||
id="ordering_item_5e9a9d795d65eef14aabda148d71f4df-text"
|
||||
data-itemcontent=""
|
||||
>
|
||||
three
|
||||
</div>
|
||||
<div class="d-flex px-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-icon"
|
||||
data-action="move-backward"
|
||||
aria-label="Move up"
|
||||
aria-describedby="ordering_item_5e9a9d795d65eef14aabda148d71f4df-text"
|
||||
>
|
||||
<i class="icon fa fa-chevron-up" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-icon"
|
||||
data-action="move-forward"
|
||||
aria-label="Move down"
|
||||
aria-describedby="ordering_item_5e9a9d795d65eef14aabda148d71f4df-text"
|
||||
>
|
||||
<i class="icon fa fa-chevron-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="p-2 sortableitem" id="ordering_item_da1838db9eb87b5e696fe927959fb532">
|
||||
<div class="d-flex">
|
||||
<div class="d-flex p-2 grip">
|
||||
<i class="fas fa-grip-vertical" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div
|
||||
class="d-flex align-items-center flex-grow-1 px-2"
|
||||
id="ordering_item_da1838db9eb87b5e696fe927959fb532-text"
|
||||
data-itemcontent=""
|
||||
>
|
||||
four
|
||||
</div>
|
||||
<div class="d-flex px-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-icon"
|
||||
data-action="move-backward"
|
||||
aria-label="Move up"
|
||||
aria-describedby="ordering_item_da1838db9eb87b5e696fe927959fb532-text"
|
||||
>
|
||||
<i class="icon fa fa-chevron-up" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-icon"
|
||||
data-action="move-forward"
|
||||
aria-label="Move down"
|
||||
aria-describedby="ordering_item_da1838db9eb87b5e696fe927959fb532-text"
|
||||
>
|
||||
<i class="icon fa fa-chevron-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
name="q124:19_response_1600"
|
||||
id="id_q124_19_response_1600"
|
||||
type="hidden"
|
||||
value="ordering_item_d4de8dbf3a0e2caf66e32325032220b1,ordering_item_f0936bab1853836dba0fc6b25ce6d315,ordering_item_5e9a9d795d65eef14aabda148d71f4df,ordering_item_da1838db9eb87b5e696fe927959fb532"
|
||||
/>
|
||||
</div>
|
||||
<div class="im-controls">
|
||||
<button
|
||||
type="submit"
|
||||
id="q124:19_-submit"
|
||||
name="q124:19_-submit"
|
||||
value="1"
|
||||
class="submit btn btn-secondary"
|
||||
data-savescrollposition="true"
|
||||
>
|
||||
Check <span class="visually-hidden">Question 18</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="question-124-20" class="que ordering interactive notyetanswered">
|
||||
<div class="info">
|
||||
<h3 class="no">Question <span class="qno">19</span></h3>
|
||||
<div class="state">Tries remaining: 1</div>
|
||||
<div class="grade">Marked out of 1.00</div>
|
||||
<div class="questionflag editable" id="yui_3_18_1_1_1775990511189_132">
|
||||
<input type="hidden" name="q124:20_:flagged" value="0" /><input
|
||||
type="hidden"
|
||||
value="qaid=812&qubaid=124&qid=1774&slot=20&checksum=5bec9067451e2e2635cdb8165c63bac5&sesskey=djxMUQR7fp&newstate="
|
||||
class="questionflagpostdata"
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
class="questionflagvalue"
|
||||
id="q124:20_:flaggedcheckbox"
|
||||
name="q124:20_:flagged"
|
||||
value="0"
|
||||
/><a
|
||||
tabindex="0"
|
||||
class="aabtn"
|
||||
role="button"
|
||||
aria-pressed="false"
|
||||
aria-label="Flagged"
|
||||
title="Flag this question for future reference"
|
||||
><img
|
||||
class="questionflagimage"
|
||||
src="https://school.moodledemo.net/theme/image.php/boost/core/1775984535/i/unflagged"
|
||||
alt=""
|
||||
/>Flag question</a
|
||||
>
|
||||
</div>
|
||||
<div class="editquestion">
|
||||
<a
|
||||
href="https://school.moodledemo.net/question/bank/editquestion/question.php?cmid=1210&returnurl=%2Fmod%2Fquiz%2Fattempt.php%3Fattempt%3D88%26cmid%3D1210%23question-124-20&id=1774"
|
||||
><i class="icon fa fa-pen fa-fw iconsmall" title="Edit" role="img" aria-label="Edit"></i
|
||||
>Edit question</a
|
||||
>
|
||||
</div>
|
||||
<span class="badge bg-primary text-light">v4 (latest)</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="formulation clearfix">
|
||||
<h4 class="accesshide">Question text</h4>
|
||||
<input type="hidden" name="q124:20_:sequencecheck" value="1" />
|
||||
<div class="qtext" style="cursor: pointer">
|
||||
<div class="clearfix">
|
||||
<p>
|
||||
Order these country placing the most egalitarian at the top and the most
|
||||
hierarchical at the bottom. (This is an example of the Ordering question type.)
|
||||
</p>
|
||||
</div>
|
||||
<div class="ablock" id="id_ablock_1774">
|
||||
<div class="answer ordering">
|
||||
<div
|
||||
aria-live="polite"
|
||||
class="visually-hidden"
|
||||
id="id_sortable_1774-announcement"
|
||||
></div>
|
||||
<ul class="sortablelist vertical numberingnone active" id="id_sortable_1774">
|
||||
<li class="p-2 sortableitem" id="ordering_item_ab895ff6c0b0bf4a844756fba4bbfedd">
|
||||
<div class="d-flex">
|
||||
<div class="d-flex p-2 grip">
|
||||
<i class="fas fa-grip-vertical" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div
|
||||
class="d-flex align-items-center flex-grow-1 px-2"
|
||||
id="ordering_item_ab895ff6c0b0bf4a844756fba4bbfedd-text"
|
||||
data-itemcontent=""
|
||||
>
|
||||
<img
|
||||
class="img-fluid"
|
||||
src="https://school.moodledemo.net/pluginfile.php/2345/question/answer/124/20/4043/france.png"
|
||||
alt="France"
|
||||
width="100"
|
||||
height="69"
|
||||
/>
|
||||
</div>
|
||||
<div class="d-flex px-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-icon"
|
||||
data-action="move-backward"
|
||||
aria-label="Move up"
|
||||
aria-describedby="ordering_item_ab895ff6c0b0bf4a844756fba4bbfedd-text"
|
||||
>
|
||||
<i class="icon fa fa-chevron-up" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-icon"
|
||||
data-action="move-forward"
|
||||
aria-label="Move down"
|
||||
aria-describedby="ordering_item_ab895ff6c0b0bf4a844756fba4bbfedd-text"
|
||||
>
|
||||
<i class="icon fa fa-chevron-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="p-2 sortableitem" id="ordering_item_889db43eeca4114cde398376ce578f58">
|
||||
<div class="d-flex">
|
||||
<div class="d-flex p-2 grip">
|
||||
<i class="fas fa-grip-vertical" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div
|
||||
class="d-flex align-items-center flex-grow-1 px-2"
|
||||
id="ordering_item_889db43eeca4114cde398376ce578f58-text"
|
||||
data-itemcontent=""
|
||||
>
|
||||
<img
|
||||
src="https://school.moodledemo.net/pluginfile.php/2345/question/answer/124/20/4041/sweden.png"
|
||||
alt="Sweden"
|
||||
width="100"
|
||||
height="65"
|
||||
/>
|
||||
</div>
|
||||
<div class="d-flex px-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-icon"
|
||||
data-action="move-backward"
|
||||
aria-label="Move up"
|
||||
aria-describedby="ordering_item_889db43eeca4114cde398376ce578f58-text"
|
||||
>
|
||||
<i class="icon fa fa-chevron-up" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-icon"
|
||||
data-action="move-forward"
|
||||
aria-label="Move down"
|
||||
aria-describedby="ordering_item_889db43eeca4114cde398376ce578f58-text"
|
||||
>
|
||||
<i class="icon fa fa-chevron-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="p-2 sortableitem" id="ordering_item_a294555e0023a4fcec1f6aabfb4fb096">
|
||||
<div class="d-flex">
|
||||
<div class="d-flex p-2 grip">
|
||||
<i class="fas fa-grip-vertical" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div
|
||||
class="d-flex align-items-center flex-grow-1 px-2"
|
||||
id="ordering_item_a294555e0023a4fcec1f6aabfb4fb096-text"
|
||||
data-itemcontent=""
|
||||
>
|
||||
<img
|
||||
class="img-fluid"
|
||||
src="https://school.moodledemo.net/pluginfile.php/2345/question/answer/124/20/4044/nigeria.png"
|
||||
alt="Nigeria"
|
||||
width="100"
|
||||
height="67"
|
||||
/>
|
||||
</div>
|
||||
<div class="d-flex px-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-icon"
|
||||
data-action="move-backward"
|
||||
aria-label="Move up"
|
||||
aria-describedby="ordering_item_a294555e0023a4fcec1f6aabfb4fb096-text"
|
||||
>
|
||||
<i class="icon fa fa-chevron-up" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-icon"
|
||||
data-action="move-forward"
|
||||
aria-label="Move down"
|
||||
aria-describedby="ordering_item_a294555e0023a4fcec1f6aabfb4fb096-text"
|
||||
>
|
||||
<i class="icon fa fa-chevron-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="p-2 sortableitem" id="ordering_item_c95f3ab27166c65cc44720f41d43db2e">
|
||||
<div class="d-flex">
|
||||
<div class="d-flex p-2 grip">
|
||||
<i class="fas fa-grip-vertical" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div
|
||||
class="d-flex align-items-center flex-grow-1 px-2"
|
||||
id="ordering_item_c95f3ab27166c65cc44720f41d43db2e-text"
|
||||
data-itemcontent=""
|
||||
>
|
||||
<img
|
||||
class="img-fluid"
|
||||
src="https://school.moodledemo.net/pluginfile.php/2345/question/answer/124/20/4042/canada.png"
|
||||
alt="Canada"
|
||||
width="100"
|
||||
height="67"
|
||||
/>
|
||||
</div>
|
||||
<div class="d-flex px-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-icon"
|
||||
data-action="move-backward"
|
||||
aria-label="Move up"
|
||||
aria-describedby="ordering_item_c95f3ab27166c65cc44720f41d43db2e-text"
|
||||
>
|
||||
<i class="icon fa fa-chevron-up" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-icon"
|
||||
data-action="move-forward"
|
||||
aria-label="Move down"
|
||||
aria-describedby="ordering_item_c95f3ab27166c65cc44720f41d43db2e-text"
|
||||
>
|
||||
<i class="icon fa fa-chevron-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
name="q124:20_response_1774"
|
||||
id="id_q124_20_response_1774"
|
||||
type="hidden"
|
||||
value="ordering_item_ab895ff6c0b0bf4a844756fba4bbfedd,ordering_item_889db43eeca4114cde398376ce578f58,ordering_item_a294555e0023a4fcec1f6aabfb4fb096,ordering_item_c95f3ab27166c65cc44720f41d43db2e"
|
||||
/>
|
||||
</div>
|
||||
<div class="im-controls">
|
||||
<button
|
||||
type="submit"
|
||||
id="q124:20_-submit"
|
||||
name="q124:20_-submit"
|
||||
value="1"
|
||||
class="submit btn btn-secondary"
|
||||
data-savescrollposition="true"
|
||||
>
|
||||
Check <span class="visually-hidden">Question 19</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,52 @@
|
||||
From moodle docs: https://docs.moodle.org/501/en/Question_types
|
||||
|
||||
Standard question types
|
||||
Calculated
|
||||
Calculated questions offer a way to create individual numerical questions by the use of wildcards that are substituted with individual values when the quiz is taken. More on the Calculated question type
|
||||
|
||||
Calculated multi-choice
|
||||
Calculated multichoice questions are like multichoice questions with the additional property that the elements to select can include formula results from numeric values that are selected randomly from a set when the quiz is taken. They use the same wildcards than Calculated questions and their wildcards can be shared with other Calculated multichoice or regular Calculated questions.
|
||||
|
||||
The main difference is that the formula is included in the answer choice as {=...} i.e. if you calculate the surface of a rectangle {={l}*{w}}.
|
||||
|
||||
More on the Calculated Multi-Choice question type.
|
||||
|
||||
Calculated simple
|
||||
Simple calculated questions offer a way to create individual numerical questions whose response is the result of a numerical formula which contain variable numerical values by the use of wildcards (i.e. {x} , {y}) that are substituted with random values when the quiz is taken.
|
||||
|
||||
The simple calculated questions offers the most used features of the calculated question with a much simpler creation interface. More on the Simple Calculated question type.
|
||||
|
||||
Drag and drop into text
|
||||
Students select missing words or phrases and add them to text by dragging boxes to the correct location. Items may be grouped and used more than once. More on the Drag and drop into text question type.
|
||||
|
||||
Essay
|
||||
This allows students to write at length on a particular subject and must be manually graded.
|
||||
|
||||
It is possible for a teacher to create a template to scaffold the student's answer in order to give them extra support. The template is then reproduced in the text editor when the student starts to answer the question. See YouTube video Essay scaffold with the Moodle quiz It is also possible to include grading information for teachers marking the essay to refer to as they assess the essays,
|
||||
|
||||
Matching
|
||||
A list of sub-questions is provided, along with a list of answers. The respondent must "match" the correct answers with each question. More on the Matching question type
|
||||
|
||||
Embedded Answers (Cloze Test / Gap Fill)
|
||||
These very flexible questions consist of a passage of text (in Moodle format) that has various answers embedded within it, including multiple choice, short answers and numerical answers. More on the Embedded Answers question type
|
||||
|
||||
Multiple choice
|
||||
With the Multiple Choice question type you can create single-answer and multiple-answer questions, include pictures, sound or other media in the question and/or answer options (by inserting HTML) and weight individual answers.
|
||||
|
||||
Ordering
|
||||
The ordering question type displays several items (words, phrases or images) in a random order which are to be dragged into the correct sequential order. See Ordering question type for more information.
|
||||
|
||||
Short Answer
|
||||
In response to a question (that may include an image), the respondent types a word or phrase. There may several possible correct answers, with different grades. Answers may or may not be sensitive to case. More on the Short Answer question type
|
||||
|
||||
Numerical
|
||||
From the student perspective, a numerical question looks just like a short-answer question. The difference is that numerical answers are allowed to have an accepted error. This allows a continuous range of answers to be set. More on the Numerical question type
|
||||
|
||||
Random short-answer matching
|
||||
From the student perspective, this looks just like a Matching question. The difference is that the sub-questions are drawn randomly from Short Answer questions in the current category. More on the Random Short-Answer Matching question type
|
||||
|
||||
Select missing words
|
||||
Students select a missing word or phrase from a dropdown menu. Items may be grouped and used more than once. More on the Select missing words question type
|
||||
|
||||
True/False
|
||||
In response to a question (that may include an image), the respondent selects from two options: True or False. More on the True/False question type
|
||||
@@ -0,0 +1,184 @@
|
||||
<div id="question-124-17" class="que gapselect interactivecountback notyetanswered">
|
||||
<div class="info">
|
||||
<h3 class="no">Question <span class="qno">16</span></h3>
|
||||
<div class="state">Tries remaining: 1</div>
|
||||
<div class="grade">Marked out of 7.00</div>
|
||||
<div class="questionflag editable" id="yui_3_18_1_1_1775933031480_114">
|
||||
<input type="hidden" name="q124:17_:flagged" value="0" /><input
|
||||
type="hidden"
|
||||
value="qaid=815&qubaid=124&qid=1643&slot=17&checksum=c75ebf63f5c7bfcfaceaba8e41013eed&sesskey=xrcdi6NRF8&newstate="
|
||||
class="questionflagpostdata"
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
class="questionflagvalue"
|
||||
id="q124:17_:flaggedcheckbox"
|
||||
name="q124:17_:flagged"
|
||||
value="0"
|
||||
/><a
|
||||
tabindex="0"
|
||||
class="aabtn"
|
||||
role="button"
|
||||
aria-pressed="false"
|
||||
aria-label="Flagged"
|
||||
title="Flag this question for future reference"
|
||||
><img
|
||||
class="questionflagimage"
|
||||
src="https://school.moodledemo.net/theme/image.php/boost/core/1775926936/i/unflagged"
|
||||
alt=""
|
||||
/>Flag question</a
|
||||
>
|
||||
</div>
|
||||
<div class="editquestion">
|
||||
<a
|
||||
href="https://school.moodledemo.net/question/bank/editquestion/question.php?cmid=1210&returnurl=%2Fmod%2Fquiz%2Fattempt.php%3Fattempt%3D88%26cmid%3D1210%23question-124-17&id=1643"
|
||||
><i class="icon fa fa-pen fa-fw iconsmall" title="Edit" role="img" aria-label="Edit"></i
|
||||
>Edit question</a
|
||||
>
|
||||
</div>
|
||||
<span class="badge bg-primary text-light">v1 (latest)</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="formulation clearfix">
|
||||
<h4 class="accesshide">Question text</h4>
|
||||
<input type="hidden" name="q124:17_:sequencecheck" value="1" />
|
||||
<div class="qtext" style="cursor: pointer">
|
||||
<p>
|
||||
Do you know your Great Britain from your United Kingdom and your British Isles? Find out
|
||||
here!
|
||||
</p>
|
||||
<p>
|
||||
This question uses the
|
||||
<a href="https://docs.moodle.org/en/Select_missing_words_question_type"
|
||||
>Select missing words question type. </a
|
||||
>
|
||||
</p>
|
||||
<p>****************************************************</p>
|
||||
<p>
|
||||
Scotland, England and
|
||||
<span class="control group1"
|
||||
><label class="visually-hidden" for="q124_17_p1">Blank 1 Question 16</label
|
||||
><select
|
||||
id="q124_17_p1"
|
||||
class="select form-select form-select d-inline-block place1"
|
||||
name="q124:17_p1"
|
||||
>
|
||||
<option value=""> </option>
|
||||
<option value="1">Cumberland</option>
|
||||
<option value="2">Northern Ireland</option>
|
||||
<option value="3">the Isle of Man</option>
|
||||
<option value="4">the Republic of Ireland</option>
|
||||
<option value="5">Wales</option>
|
||||
</select>
|
||||
</span>
|
||||
together form <span class="control group2"
|
||||
><label class="visually-hidden" for="q124_17_p2">Blank 2 Question 16</label
|
||||
><select
|
||||
id="q124_17_p2"
|
||||
class="select form-select form-select d-inline-block place2"
|
||||
name="q124:17_p2"
|
||||
>
|
||||
<option value=""> </option>
|
||||
<option value="1">the United Kingdom</option>
|
||||
<option value="2">Great Britain</option>
|
||||
<option value="3">the British Isles</option>
|
||||
<option value="4">the Commonwealth</option>
|
||||
</select>
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
Together with
|
||||
<span class="control group1"
|
||||
><label class="visually-hidden" for="q124_17_p3">Blank 3 Question 16</label
|
||||
><select
|
||||
id="q124_17_p3"
|
||||
class="select form-select form-select d-inline-block place3"
|
||||
name="q124:17_p3"
|
||||
>
|
||||
<option value=""> </option>
|
||||
<option value="1">Cumberland</option>
|
||||
<option value="2">Northern Ireland</option>
|
||||
<option value="3">the Isle of Man</option>
|
||||
<option value="4">the Republic of Ireland</option>
|
||||
<option value="5">Wales</option>
|
||||
</select>
|
||||
</span>
|
||||
they also make up <span class="control group2"
|
||||
><label class="visually-hidden" for="q124_17_p4">Blank 4 Question 16</label
|
||||
><select
|
||||
id="q124_17_p4"
|
||||
class="select form-select form-select d-inline-block place4"
|
||||
name="q124:17_p4"
|
||||
>
|
||||
<option value=""> </option>
|
||||
<option value="1">the United Kingdom</option>
|
||||
<option value="2">Great Britain</option>
|
||||
<option value="3">the British Isles</option>
|
||||
<option value="4">the Commonwealth</option>
|
||||
</select>
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
So what are the British Isles? They are
|
||||
<span class="control group2"
|
||||
><label class="visually-hidden" for="q124_17_p5">Blank 5 Question 16</label
|
||||
><select
|
||||
id="q124_17_p5"
|
||||
class="select form-select form-select d-inline-block place5"
|
||||
name="q124:17_p5"
|
||||
>
|
||||
<option value=""> </option>
|
||||
<option value="1">the United Kingdom</option>
|
||||
<option value="2">Great Britain</option>
|
||||
<option value="3">the British Isles</option>
|
||||
<option value="4">the Commonwealth</option>
|
||||
</select>
|
||||
</span>
|
||||
and
|
||||
<span class="control group1"
|
||||
><label class="visually-hidden" for="q124_17_p6">Blank 6 Question 16</label
|
||||
><select
|
||||
id="q124_17_p6"
|
||||
class="select form-select form-select d-inline-block place6"
|
||||
name="q124:17_p6"
|
||||
>
|
||||
<option value=""> </option>
|
||||
<option value="1">Cumberland</option>
|
||||
<option value="2">Northern Ireland</option>
|
||||
<option value="3">the Isle of Man</option>
|
||||
<option value="4">the Republic of Ireland</option>
|
||||
<option value="5">Wales</option>
|
||||
</select> </span
|
||||
>, together with many other islands and dependencies such as
|
||||
<span class="control group1"
|
||||
><label class="visually-hidden" for="q124_17_p7">Blank 7 Question 16</label
|
||||
><select
|
||||
id="q124_17_p7"
|
||||
class="select form-select form-select d-inline-block place7"
|
||||
name="q124:17_p7"
|
||||
>
|
||||
<option value=""> </option>
|
||||
<option value="1">Cumberland</option>
|
||||
<option value="2">Northern Ireland</option>
|
||||
<option value="3">the Isle of Man</option>
|
||||
<option value="4">the Republic of Ireland</option>
|
||||
<option value="5">Wales</option>
|
||||
</select> </span
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="im-controls">
|
||||
<button
|
||||
type="submit"
|
||||
id="q124:17_-submit"
|
||||
name="q124:17_-submit"
|
||||
value="1"
|
||||
class="submit btn btn-secondary"
|
||||
data-savescrollposition="true"
|
||||
>
|
||||
Check <span class="visually-hidden">Question 16</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,64 @@
|
||||
<div id="question-125-4" class="que shortanswer deferredfeedback notyetanswered">
|
||||
<div class="info">
|
||||
<h3 class="no">Question <span class="qno">4</span></h3>
|
||||
<div class="state">Not yet answered</div>
|
||||
<div class="grade">Marked out of 1.00</div>
|
||||
<div class="questionflag editable" id="yui_3_18_1_1_1775909302600_36">
|
||||
<input type="hidden" name="q125:4_:flagged" value="0" /><input
|
||||
type="hidden"
|
||||
value="qaid=801&qubaid=125&qid=3701&slot=4&checksum=e963ee069c13bab0bb84c37d94edf69b&sesskey=MRh4T7j2fU&newstate="
|
||||
class="questionflagpostdata"
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
class="questionflagvalue"
|
||||
id="q125:4_:flaggedcheckbox"
|
||||
name="q125:4_:flagged"
|
||||
value="0"
|
||||
/><a
|
||||
tabindex="0"
|
||||
class="aabtn"
|
||||
role="button"
|
||||
aria-pressed="false"
|
||||
aria-label="Flagged"
|
||||
title="Flag this question for future reference"
|
||||
><img
|
||||
class="questionflagimage"
|
||||
src="https://school.moodledemo.net/theme/image.php/boost/core/1775905339/i/unflagged"
|
||||
alt=""
|
||||
/>Flag question</a
|
||||
>
|
||||
</div>
|
||||
<div class="editquestion">
|
||||
<a
|
||||
href="https://school.moodledemo.net/question/bank/editquestion/question.php?cmid=1655&returnurl=%2Fmod%2Fquiz%2Fattempt.php%3Fattempt%3D89%26cmid%3D1655%23question-125-4&id=3701"
|
||||
><i class="icon fa fa-pen fa-fw iconsmall" title="Edit" role="img" aria-label="Edit"></i
|
||||
>Edit question</a
|
||||
>
|
||||
</div>
|
||||
<span class="badge bg-primary text-light">v1 (latest)</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="formulation clearfix">
|
||||
<h4 class="accesshide">Question text</h4>
|
||||
<input type="hidden" name="q125:4_:sequencecheck" value="1" />
|
||||
<div class="qtext" style="cursor: pointer">
|
||||
<div class="clearfix">
|
||||
<p>How is a post on Twitter called?</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ablock d-flex flex-wrap align-items-center">
|
||||
<label for="q125:4_answer"
|
||||
>Answer: <span class="visually-hidden">Question 4</span
|
||||
><span class="answer"
|
||||
><input
|
||||
type="text"
|
||||
name="q125:4_answer"
|
||||
id="q125:4_answer"
|
||||
size="80"
|
||||
class="form-control d-inline" /></span
|
||||
></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,120 @@
|
||||
<div id="question-125-1" class="que multichoice deferredfeedback notyetanswered">
|
||||
<div class="info">
|
||||
<h3 class="no">Question <span class="qno">1</span></h3>
|
||||
<div class="state">Not yet answered</div>
|
||||
<div class="grade">Marked out of 1.00</div>
|
||||
<div class="questionflag editable" id="yui_3_18_1_1_1775909302600_18">
|
||||
<input type="hidden" name="q125:1_:flagged" value="0" /><input
|
||||
type="hidden"
|
||||
value="qaid=798&qubaid=125&qid=3699&slot=1&checksum=e0f716344fc5402f32878bcbf0aa8342&sesskey=MRh4T7j2fU&newstate="
|
||||
class="questionflagpostdata"
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
class="questionflagvalue"
|
||||
id="q125:1_:flaggedcheckbox"
|
||||
name="q125:1_:flagged"
|
||||
value="0"
|
||||
/><a
|
||||
tabindex="0"
|
||||
class="aabtn"
|
||||
role="button"
|
||||
aria-pressed="false"
|
||||
aria-label="Flagged"
|
||||
title="Flag this question for future reference"
|
||||
><img
|
||||
class="questionflagimage"
|
||||
src="https://school.moodledemo.net/theme/image.php/boost/core/1775905339/i/unflagged"
|
||||
alt=""
|
||||
/>Flag question</a
|
||||
>
|
||||
</div>
|
||||
<div class="editquestion">
|
||||
<a
|
||||
href="https://school.moodledemo.net/question/bank/editquestion/question.php?cmid=1655&returnurl=%2Fmod%2Fquiz%2Fattempt.php%3Fattempt%3D89%26cmid%3D1655%23&id=3699"
|
||||
><i class="icon fa fa-pen fa-fw iconsmall" title="Edit" role="img" aria-label="Edit"></i
|
||||
>Edit question</a
|
||||
>
|
||||
</div>
|
||||
<span class="badge bg-primary text-light">v2 (latest)</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="formulation clearfix">
|
||||
<h4 class="accesshide">Question text</h4>
|
||||
<input type="hidden" name="q125:1_:sequencecheck" value="1" />
|
||||
<div class="qtext" style="cursor: pointer">
|
||||
<div class="clearfix">
|
||||
<p>What is resilience?</p>
|
||||
</div>
|
||||
</div>
|
||||
<fieldset class="ablock no-overflow visual-scroll-x">
|
||||
<legend class="prompt h6 fw-normal visually-hidden">
|
||||
<span class="visually-hidden">Question 1</span> Answer
|
||||
</legend>
|
||||
<div class="answer">
|
||||
<div class="r0">
|
||||
<input
|
||||
type="radio"
|
||||
name="q125:1_answer"
|
||||
value="0"
|
||||
id="q125:1_answer0"
|
||||
aria-labelledby="q125:1_answer0_label"
|
||||
/>
|
||||
<div class="d-flex w-auto" id="q125:1_answer0_label" data-region="answer-label">
|
||||
<div class="flex-fill ms-1">
|
||||
<p>The ability to recover from difficulties and adapt to challenging situations</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="r1">
|
||||
<input
|
||||
type="radio"
|
||||
name="q125:1_answer"
|
||||
value="1"
|
||||
id="q125:1_answer1"
|
||||
aria-labelledby="q125:1_answer1_label"
|
||||
/>
|
||||
<div class="d-flex w-auto" id="q125:1_answer1_label" data-region="answer-label">
|
||||
<div class="flex-fill ms-1">
|
||||
<p>The skill of controlling other people’s emotions during a crisis</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="r0">
|
||||
<input
|
||||
type="radio"
|
||||
name="q125:1_answer"
|
||||
value="2"
|
||||
id="q125:1_answer2"
|
||||
aria-labelledby="q125:1_answer2_label"
|
||||
/>
|
||||
<div class="d-flex w-auto" id="q125:1_answer2_label" data-region="answer-label">
|
||||
<div class="flex-fill ms-1">
|
||||
<p>The ability to avoid stressful situations entirely.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="q125:1_clearchoice"
|
||||
class="qtype_multichoice_clearchoice visually-hidden"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="q125:1_answer"
|
||||
id="q125:1_answer-1"
|
||||
value="-1"
|
||||
class="visually-hidden"
|
||||
aria-hidden="true"
|
||||
checked="checked"
|
||||
/><label for="q125:1_answer-1"
|
||||
><a tabindex="-1" role="button" class="btn btn-link ms-3 mt-n1" href="#"
|
||||
>Clear my choice</a
|
||||
></label
|
||||
>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,73 @@
|
||||
<div id="question-125-3" class="que truefalse deferredfeedback notyetanswered">
|
||||
<div class="info">
|
||||
<h3 class="no">Question <span class="qno">3</span></h3>
|
||||
<div class="state">Not yet answered</div>
|
||||
<div class="grade">Marked out of 1.00</div>
|
||||
<div class="questionflag editable" id="yui_3_18_1_1_1775909302600_30">
|
||||
<input type="hidden" name="q125:3_:flagged" value="0" /><input
|
||||
type="hidden"
|
||||
value="qaid=800&qubaid=125&qid=3698&slot=3&checksum=57228a123cfb296aa6bd9357997dfbed&sesskey=MRh4T7j2fU&newstate="
|
||||
class="questionflagpostdata"
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
class="questionflagvalue"
|
||||
id="q125:3_:flaggedcheckbox"
|
||||
name="q125:3_:flagged"
|
||||
value="0"
|
||||
/><a
|
||||
tabindex="0"
|
||||
class="aabtn"
|
||||
role="button"
|
||||
aria-pressed="false"
|
||||
aria-label="Flagged"
|
||||
title="Flag this question for future reference"
|
||||
><img
|
||||
class="questionflagimage"
|
||||
src="https://school.moodledemo.net/theme/image.php/boost/core/1775905339/i/unflagged"
|
||||
alt=""
|
||||
/>Flag question</a
|
||||
>
|
||||
</div>
|
||||
<div class="editquestion">
|
||||
<a
|
||||
href="https://school.moodledemo.net/question/bank/editquestion/question.php?cmid=1655&returnurl=%2Fmod%2Fquiz%2Fattempt.php%3Fattempt%3D89%26cmid%3D1655%23question-125-3&id=3698"
|
||||
><i class="icon fa fa-pen fa-fw iconsmall" title="Edit" role="img" aria-label="Edit"></i
|
||||
>Edit question</a
|
||||
>
|
||||
</div>
|
||||
<span class="badge bg-primary text-light">v1 (latest)</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="formulation clearfix">
|
||||
<h4 class="accesshide">Question text</h4>
|
||||
<input type="hidden" name="q125:3_:sequencecheck" value="1" />
|
||||
<div class="qtext" style="cursor: pointer">
|
||||
<div class="clearfix">
|
||||
<p>Is resilience important?</p>
|
||||
</div>
|
||||
</div>
|
||||
<fieldset class="ablock">
|
||||
<legend class="prompt h6 fw-normal visually-hidden">
|
||||
<span class="visually-hidden">Question 3</span> Answer
|
||||
</legend>
|
||||
<div class="answer">
|
||||
<div class="r0">
|
||||
<input type="radio" name="q125:3_answer" value="1" id="q125:3_answertrue" /><label
|
||||
for="q125:3_answertrue"
|
||||
class="ms-1"
|
||||
>True</label
|
||||
>
|
||||
</div>
|
||||
<div class="r1">
|
||||
<input type="radio" name="q125:3_answer" value="0" id="q125:3_answerfalse" /><label
|
||||
for="q125:3_answerfalse"
|
||||
class="ms-1"
|
||||
>False</label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,16 +1,37 @@
|
||||
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 replace = require('@rollup/plugin-replace');
|
||||
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.tsx',
|
||||
output: {
|
||||
file: './extension/popup/popup.js',
|
||||
format: 'umd',
|
||||
sourcemap: true
|
||||
},
|
||||
onwarn() {},
|
||||
plugins: [
|
||||
replace({
|
||||
'process.env.NODE_ENV': JSON.stringify('production'),
|
||||
preventAssignment: true
|
||||
}),
|
||||
nodeResolve(),
|
||||
ts(config),
|
||||
terser()
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
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, .placeinput, .sortablelist';
|
||||
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 };
|
||||
@@ -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,176 @@
|
||||
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';
|
||||
import { parseMoodleQuestion } from './parse-question';
|
||||
import { MoodleQuestionQuery /* MoodleQuestionType */ } from '../types/question-types';
|
||||
|
||||
// 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 = `
|
||||
You are an expert quiz solver.
|
||||
Please solve the provided question based on its type and provide the correct result.
|
||||
- For choice questions, output the exact index(es) of the correct answer(s).
|
||||
- For text/numerical questions, provide the exact wording or number.
|
||||
- For essay questions, provide a highly detailed and complete response, adapting exactly to the requested 'format' (HTML vs plain text) and building upon any 'initial_text' template if supplied.
|
||||
`.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,
|
||||
// We provide the structured JSON if parsed, otherwise fallback to normalized text string
|
||||
textContent: string
|
||||
): Promise<ChatCompletionUserMessageParam['content']> {
|
||||
const imagesElements = questionElement.querySelectorAll('img');
|
||||
|
||||
if (
|
||||
!config.includeImages ||
|
||||
!isGPTModelGreaterOrEqualTo4(config.model) ||
|
||||
imagesElements.length === 0
|
||||
) {
|
||||
return textContent;
|
||||
}
|
||||
|
||||
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: textContent
|
||||
});
|
||||
|
||||
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;
|
||||
query: MoodleQuestionQuery | null;
|
||||
}> {
|
||||
const parsedQuery = parseMoodleQuestion(questionElement, question);
|
||||
const textContent = parsedQuery ? JSON.stringify(parsedQuery, null, 2) : question;
|
||||
|
||||
const content = await getContent(config, questionElement, textContent);
|
||||
const message: ChatCompletionMessageParam = { role: 'user', content };
|
||||
|
||||
const buildResult = (historyMsg: ChatCompletionMessageParam[]) => {
|
||||
const historyObj = { history: historyMsg };
|
||||
return {
|
||||
messages: [SYSTEM_INSTRUCTION_MESSAGE, ...historyMsg, message] as [
|
||||
typeof SYSTEM_INSTRUCTION_MESSAGE,
|
||||
...ChatCompletionMessageParam[]
|
||||
],
|
||||
query: parsedQuery,
|
||||
saveResponse(response: string) {
|
||||
if (config.history) {
|
||||
historyObj.history.push(message);
|
||||
historyObj.history.push({ role: 'assistant', content: response });
|
||||
// Note we probably need the full 'history' object here to stringify it:
|
||||
// We will recreate it or reuse the loaded one
|
||||
let historyToSave: History;
|
||||
const pastHistory: History | null = loadPastHistory();
|
||||
const newHistory: History = createNewHistory();
|
||||
if (pastHistory === null || !areHistoryFromSameQuiz(pastHistory, newHistory)) {
|
||||
historyToSave = newHistory;
|
||||
} else {
|
||||
historyToSave = pastHistory;
|
||||
}
|
||||
historyToSave.history = historyObj.history;
|
||||
sessionStorage.moodleGPTHistory = JSON.stringify(historyToSave);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
if (!config.history) {
|
||||
return buildResult([]);
|
||||
}
|
||||
|
||||
const pastHistory: History | null = loadPastHistory();
|
||||
const newHistory: History = createNewHistory();
|
||||
if (pastHistory === null || !areHistoryFromSameQuiz(pastHistory, newHistory)) {
|
||||
return buildResult(newHistory.history);
|
||||
} else {
|
||||
return buildResult(pastHistory.history);
|
||||
}
|
||||
}
|
||||
|
||||
export default getContentWithHistory;
|
||||
@@ -0,0 +1,85 @@
|
||||
import type Config from '../types/config';
|
||||
import type GPTAnswer from '../types/gpt-answer';
|
||||
import getContentWithHistory from './get-content-with-history';
|
||||
import OpenAI from 'openai';
|
||||
import { fixeO } from '../utils/fixe-o';
|
||||
import { QuestionSchemas } from './utils/question-schemas';
|
||||
import { MoodleQuestionType, MoodleQuestionResponse } from '../types/question-types';
|
||||
|
||||
/**
|
||||
* 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(), (config.timeoutValue || 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,
|
||||
project: config.projectId,
|
||||
dangerouslyAllowBrowser: true
|
||||
});
|
||||
|
||||
const questionType = contentHandler.query
|
||||
? contentHandler.query.question_type
|
||||
: MoodleQuestionType.UNKNOWN;
|
||||
const targetSchema: MoodleQuestionResponse =
|
||||
questionType !== MoodleQuestionType.UNKNOWN ? QuestionSchemas[questionType] : undefined;
|
||||
|
||||
const requestPayload: any = {
|
||||
model: config.model,
|
||||
messages: contentHandler.messages.map(msg => ({ ...msg })),
|
||||
max_completion_tokens: config.maxTokens || 2000
|
||||
};
|
||||
|
||||
// Use the modern json_schema structured output when a schema is available.
|
||||
// The model is guaranteed to return schema-valid JSON — no prompt hacks needed.
|
||||
if (targetSchema) {
|
||||
requestPayload.response_format = {
|
||||
type: 'json_schema',
|
||||
json_schema: targetSchema
|
||||
};
|
||||
}
|
||||
|
||||
const req = await client.chat.completions.create(fixeO(config.model, requestPayload), {
|
||||
signal: config.timeout ? controller.signal : null
|
||||
});
|
||||
|
||||
clearTimeout(timeoutControler);
|
||||
|
||||
const rawResponse = req.choices[0].message.content ?? '';
|
||||
|
||||
let structuredResponse: MoodleQuestionResponse | null = null;
|
||||
if (targetSchema && rawResponse) {
|
||||
try {
|
||||
structuredResponse = JSON.parse(rawResponse);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse structured JSON response', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Save the response into the history
|
||||
if (typeof contentHandler.saveResponse === 'function') {
|
||||
contentHandler.saveResponse(rawResponse);
|
||||
}
|
||||
|
||||
return {
|
||||
questionQuery: contentHandler.query,
|
||||
response: structuredResponse,
|
||||
rawResponse: rawResponse
|
||||
};
|
||||
}
|
||||
|
||||
export default getChatGPTResponse;
|
||||
@@ -0,0 +1,55 @@
|
||||
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';
|
||||
import handleMatch from 'background/core/questions/match';
|
||||
import handleGapSelect from 'background/core/questions/gapselect';
|
||||
import handleDragDropText from 'background/core/questions/ddwtos';
|
||||
import handleOrdering from 'background/core/questions/ordering';
|
||||
|
||||
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,
|
||||
handleMatch,
|
||||
handleGapSelect,
|
||||
handleDragDropText,
|
||||
handleOrdering,
|
||||
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;
|
||||
@@ -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,223 @@
|
||||
import { MoodleQuestionQuery, MoodleQuestionType, AnswerOption } from '../types/question-types';
|
||||
import normalizeText from 'background/utils/normalize-text';
|
||||
|
||||
/**
|
||||
* Extracts options from a multichoice question.
|
||||
*/
|
||||
function extractOptions(questionElement: HTMLElement, inputSelector: string): AnswerOption[] {
|
||||
const options: AnswerOption[] = [];
|
||||
const inputs = questionElement.querySelectorAll<HTMLInputElement>(inputSelector);
|
||||
|
||||
inputs.forEach((input, index) => {
|
||||
// some inputs have value "-1", which is standard moodle for "clear choice"
|
||||
if (input.value === '-1') return;
|
||||
|
||||
// Try finding the label by ID
|
||||
let text = '';
|
||||
const labelEl = questionElement.querySelector(`#${input.id.replace(/:/g, '\\:')}_label`);
|
||||
if (labelEl) {
|
||||
text = labelEl.textContent ?? '';
|
||||
} else {
|
||||
text = input.parentElement?.textContent ?? '';
|
||||
}
|
||||
|
||||
text = normalizeText(text.replace('Clear my choice', ''));
|
||||
if (text) {
|
||||
options.push({
|
||||
index,
|
||||
text
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the question DOM element to determine the strict question type and structural query
|
||||
* @param questionElement The question container element
|
||||
* @param normalizedQuestionText The full text of the question
|
||||
* @returns MoodleQuestionQuery object or null if parsing fails
|
||||
*/
|
||||
export function parseMoodleQuestion(
|
||||
questionElement: HTMLElement,
|
||||
normalizedQuestionText: string
|
||||
): MoodleQuestionQuery | null {
|
||||
const container =
|
||||
questionElement.closest('.que') || questionElement.closest('.formulation') || questionElement;
|
||||
|
||||
if (
|
||||
container.classList.contains('multichoice') ||
|
||||
container.classList.contains('calculatedmulti')
|
||||
) {
|
||||
const checkboxes = container.querySelectorAll<HTMLInputElement>(
|
||||
'.answer input[type="checkbox"]'
|
||||
);
|
||||
const radios = container.querySelectorAll<HTMLInputElement>('.answer input[type="radio"]');
|
||||
|
||||
if (checkboxes.length > 0) {
|
||||
return {
|
||||
question_type: MoodleQuestionType.MULTIPLE_CHOICE,
|
||||
question_text: normalizedQuestionText,
|
||||
answer_options: extractOptions(container as HTMLElement, '.answer input[type="checkbox"]')
|
||||
};
|
||||
} else if (radios.length > 0) {
|
||||
return {
|
||||
question_type: MoodleQuestionType.SINGLE_CHOICE,
|
||||
question_text: normalizedQuestionText,
|
||||
answer_options: extractOptions(container as HTMLElement, '.answer input[type="radio"]')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (container.classList.contains('truefalse')) {
|
||||
return {
|
||||
question_type: MoodleQuestionType.TRUE_FALSE,
|
||||
question_text: normalizedQuestionText
|
||||
};
|
||||
}
|
||||
|
||||
if (container.classList.contains('shortanswer')) {
|
||||
return {
|
||||
question_type: MoodleQuestionType.SHORT_TEXT,
|
||||
question_text: normalizedQuestionText
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
container.classList.contains('numerical') ||
|
||||
container.classList.contains('calculated') ||
|
||||
container.classList.contains('calculatedsimple')
|
||||
) {
|
||||
return {
|
||||
question_type: MoodleQuestionType.NUMERICAL,
|
||||
question_text: normalizedQuestionText
|
||||
};
|
||||
}
|
||||
|
||||
if (container.classList.contains('essay')) {
|
||||
let format: 'plain_text' | 'html' = 'plain_text';
|
||||
let initial_text = '';
|
||||
|
||||
const editorDiv = container.querySelector('.qtype_essay_editor');
|
||||
if (editorDiv) format = 'html';
|
||||
|
||||
const textarea = container.querySelector<HTMLTextAreaElement>('textarea');
|
||||
if (textarea) {
|
||||
initial_text = textarea.value || textarea.textContent || '';
|
||||
initial_text = normalizeText(initial_text);
|
||||
}
|
||||
|
||||
const payload: MoodleQuestionQuery = {
|
||||
question_type: MoodleQuestionType.ESSAY,
|
||||
question_text: normalizedQuestionText,
|
||||
format
|
||||
};
|
||||
if (initial_text) {
|
||||
(payload as any).initial_text = initial_text;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
if (container.classList.contains('match')) {
|
||||
const subQuestions: { index: number; text: string }[] = [];
|
||||
const firstSelectOptions: { index: number; text: string }[] = [];
|
||||
|
||||
const rows = container.querySelectorAll('tr');
|
||||
let subIndex = 0;
|
||||
rows.forEach(row => {
|
||||
const textCell = row.querySelector('.text');
|
||||
const select = row.querySelector('select');
|
||||
if (textCell && select) {
|
||||
subQuestions.push({ index: subIndex, text: normalizeText(textCell.textContent || '') });
|
||||
if (subIndex === 0) {
|
||||
const options = select.querySelectorAll('option');
|
||||
let optionIndexCounter = 0;
|
||||
Array.from(options)
|
||||
.slice(1)
|
||||
.forEach(opt => {
|
||||
firstSelectOptions.push({
|
||||
index: optionIndexCounter++,
|
||||
text: normalizeText(opt.textContent || '')
|
||||
});
|
||||
});
|
||||
}
|
||||
subIndex++;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
question_type: MoodleQuestionType.MATCH,
|
||||
question_text: normalizedQuestionText,
|
||||
sub_questions: subQuestions,
|
||||
options: firstSelectOptions
|
||||
};
|
||||
}
|
||||
|
||||
if (container.classList.contains('gapselect')) {
|
||||
const selects = container.querySelectorAll('select');
|
||||
const dropDowns: { index: number; options: { index: number; text: string }[] }[] = [];
|
||||
|
||||
selects.forEach((select, dropIndex) => {
|
||||
const options = select.querySelectorAll('option');
|
||||
const optArr: { index: number; text: string }[] = [];
|
||||
let optionIndexCounter = 0;
|
||||
Array.from(options)
|
||||
.slice(1)
|
||||
.forEach(opt => {
|
||||
optArr.push({ index: optionIndexCounter++, text: normalizeText(opt.textContent || '') });
|
||||
});
|
||||
dropDowns.push({ index: dropIndex, options: optArr });
|
||||
});
|
||||
|
||||
return {
|
||||
question_type: MoodleQuestionType.SELECT_MISSING_WORD,
|
||||
question_text: normalizedQuestionText,
|
||||
drop_downs: dropDowns
|
||||
};
|
||||
}
|
||||
|
||||
if (container.classList.contains('ddwtos')) {
|
||||
const dragContainer = container.querySelector('.answercontainer');
|
||||
const draggables: { index: number; text: string }[] = [];
|
||||
if (dragContainer) {
|
||||
const dragHomes = dragContainer.querySelectorAll('.draghome.unplaced');
|
||||
dragHomes.forEach((drag, idx) => {
|
||||
draggables.push({ index: idx, text: normalizeText(drag.textContent || '') });
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
question_type: MoodleQuestionType.DRAG_DROP_TEXT,
|
||||
question_text: normalizedQuestionText,
|
||||
draggables
|
||||
};
|
||||
}
|
||||
|
||||
if (container.classList.contains('ordering')) {
|
||||
const itemsContainer = container.querySelector('.sortablelist');
|
||||
const orderingItems: { index: number; text: string }[] = [];
|
||||
if (itemsContainer) {
|
||||
const items = itemsContainer.querySelectorAll('.sortableitem');
|
||||
items.forEach((item, idx) => {
|
||||
// the textcontent holds the answer string or possibly img alt depending on the question contents
|
||||
let text = item.textContent?.trim() || '';
|
||||
// If it's an image-only answering scenario, text will be empty, grab the alt of first img
|
||||
if (text === '') {
|
||||
const img = item.querySelector('img');
|
||||
if (img) text = img.getAttribute('alt') || '';
|
||||
}
|
||||
orderingItems.push({ index: idx, text: normalizeText(text) });
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
question_type: MoodleQuestionType.ORDERING,
|
||||
question_text: normalizedQuestionText,
|
||||
items: orderingItems
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
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;
|
||||
|
||||
const answerText =
|
||||
gptAnswer.response && 'correct_answer' in gptAnswer.response
|
||||
? String((gptAnswer.response as any).correct_answer)
|
||||
: gptAnswer.rawResponse;
|
||||
|
||||
if (config.typing) {
|
||||
let index = 0;
|
||||
const eventHandler = function (event: KeyboardEvent) {
|
||||
event.preventDefault();
|
||||
|
||||
if (event.key === 'Backspace' || index >= answerText.length) {
|
||||
iframe.contentWindow!.removeEventListener('keydown', eventHandler);
|
||||
return;
|
||||
}
|
||||
|
||||
// Append text one character at a time
|
||||
const textNode = document.createTextNode(answerText.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 += answerText;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export default handleAtto;
|
||||
@@ -0,0 +1,90 @@
|
||||
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';
|
||||
import { MoodleQuestionType, MultipleChoiceResponse } from '../../types/question-types';
|
||||
|
||||
/**
|
||||
* 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 correctElements: Set<HTMLInputElement> = new Set();
|
||||
|
||||
// New structured mode
|
||||
if (
|
||||
gptAnswer.response &&
|
||||
gptAnswer.response.question_type === MoodleQuestionType.MULTIPLE_CHOICE
|
||||
) {
|
||||
const response = gptAnswer.response as MultipleChoiceResponse;
|
||||
const correctIndexes = new Set(response.correct_answer.indexes);
|
||||
|
||||
Array.from(inputList).forEach((inp, index) => {
|
||||
const element = inp as HTMLInputElement;
|
||||
if (correctIndexes.has(index)) {
|
||||
correctElements.add(element);
|
||||
}
|
||||
});
|
||||
|
||||
if (config.logs) {
|
||||
console.log('Using strict mode multiple choice selection:', response.correct_answer.indexes);
|
||||
}
|
||||
}
|
||||
// Fallback to fuzzy text matching if structural failed
|
||||
else {
|
||||
const corrects = gptAnswer.rawResponse.split('\n');
|
||||
|
||||
const possibleAnswers = Array.from(inputList)
|
||||
.map(inp => ({
|
||||
element: inp as HTMLInputElement,
|
||||
value: normalizeText(inp?.parentElement?.textContent ?? '')
|
||||
}))
|
||||
.filter(obj => obj.value !== '');
|
||||
|
||||
for (const correct of corrects) {
|
||||
const bestAnswer = pickBestReponse(correct, possibleAnswers);
|
||||
|
||||
if (config.logs && bestAnswer.value) {
|
||||
Logs.bestAnswer(bestAnswer.value, bestAnswer.similarity);
|
||||
}
|
||||
|
||||
correctElements.add(bestAnswer.element as HTMLInputElement);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it should be checked or not
|
||||
for (const inp of Array.from(inputList)) {
|
||||
const element = inp as HTMLInputElement;
|
||||
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;
|
||||
@@ -0,0 +1,135 @@
|
||||
import type Config from '../../types/config';
|
||||
import type GPTAnswer from '../../types/gpt-answer';
|
||||
import titleIndications from 'background/utils/title-indications';
|
||||
import { MoodleQuestionType } from '../../types/question-types';
|
||||
|
||||
/**
|
||||
* 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');
|
||||
|
||||
let textToCopy = '';
|
||||
|
||||
if (gptAnswer.response && gptAnswer.questionQuery) {
|
||||
const q = gptAnswer.questionQuery;
|
||||
const r = gptAnswer.response;
|
||||
|
||||
if (q.question_type === r.question_type) {
|
||||
switch (r.question_type) {
|
||||
case MoodleQuestionType.SINGLE_CHOICE: {
|
||||
const query = q as Extract<typeof q, { question_type: MoodleQuestionType.SINGLE_CHOICE }>;
|
||||
const resp = r as Extract<typeof r, { question_type: MoodleQuestionType.SINGLE_CHOICE }>;
|
||||
const opt = query.answer_options.find(o => o.index === resp.correct_answer.index);
|
||||
textToCopy = opt ? opt.text : '';
|
||||
break;
|
||||
}
|
||||
case MoodleQuestionType.MULTIPLE_CHOICE: {
|
||||
const query = q as Extract<
|
||||
typeof q,
|
||||
{ question_type: MoodleQuestionType.MULTIPLE_CHOICE }
|
||||
>;
|
||||
const resp = r as Extract<
|
||||
typeof r,
|
||||
{ question_type: MoodleQuestionType.MULTIPLE_CHOICE }
|
||||
>;
|
||||
const texts = resp.correct_answer.indexes
|
||||
.map(idx => {
|
||||
const opt = query.answer_options.find(o => o.index === idx);
|
||||
return opt ? opt.text : '';
|
||||
})
|
||||
.filter(Boolean);
|
||||
textToCopy = texts.join('\n');
|
||||
break;
|
||||
}
|
||||
case MoodleQuestionType.TRUE_FALSE: {
|
||||
const resp = r as Extract<typeof r, { question_type: MoodleQuestionType.TRUE_FALSE }>;
|
||||
textToCopy = resp.correct_answer ? 'True' : 'False';
|
||||
break;
|
||||
}
|
||||
case MoodleQuestionType.SHORT_TEXT:
|
||||
case MoodleQuestionType.ESSAY: {
|
||||
const resp = r as Extract<
|
||||
typeof r,
|
||||
{ question_type: MoodleQuestionType.SHORT_TEXT | MoodleQuestionType.ESSAY }
|
||||
>;
|
||||
textToCopy = resp.correct_answer;
|
||||
break;
|
||||
}
|
||||
case MoodleQuestionType.NUMERICAL: {
|
||||
const resp = r as Extract<typeof r, { question_type: MoodleQuestionType.NUMERICAL }>;
|
||||
textToCopy = resp.correct_answer.toString();
|
||||
break;
|
||||
}
|
||||
case MoodleQuestionType.MATCH: {
|
||||
const query = q as Extract<typeof q, { question_type: MoodleQuestionType.MATCH }>;
|
||||
const resp = r as Extract<typeof r, { question_type: MoodleQuestionType.MATCH }>;
|
||||
const matches = resp.correct_answers.map(ans => {
|
||||
const sub = query.sub_questions.find(s => s.index === ans.sub_question_index);
|
||||
const opt = query.options.find(o => o.index === ans.option_index);
|
||||
return `${sub?.text || `Sub ${ans.sub_question_index}`} -> ${opt?.text || `Option ${ans.option_index}`}`;
|
||||
});
|
||||
textToCopy = matches.join('\n');
|
||||
break;
|
||||
}
|
||||
case MoodleQuestionType.SELECT_MISSING_WORD: {
|
||||
const query = q as Extract<
|
||||
typeof q,
|
||||
{ question_type: MoodleQuestionType.SELECT_MISSING_WORD }
|
||||
>;
|
||||
const resp = r as Extract<
|
||||
typeof r,
|
||||
{ question_type: MoodleQuestionType.SELECT_MISSING_WORD }
|
||||
>;
|
||||
const answers = resp.correct_answers.map(ans => {
|
||||
const dd = query.drop_downs.find(d => d.index === ans.drop_down_index);
|
||||
const opt = dd?.options.find(o => o.index === ans.option_index);
|
||||
return `Blank ${ans.drop_down_index}: ${opt?.text || `Option ${ans.option_index}`}`;
|
||||
});
|
||||
textToCopy = answers.join('\n');
|
||||
break;
|
||||
}
|
||||
case MoodleQuestionType.DRAG_DROP_TEXT: {
|
||||
const query = q as Extract<
|
||||
typeof q,
|
||||
{ question_type: MoodleQuestionType.DRAG_DROP_TEXT }
|
||||
>;
|
||||
const resp = r as Extract<typeof r, { question_type: MoodleQuestionType.DRAG_DROP_TEXT }>;
|
||||
const answers = resp.correct_answers.map(ans => {
|
||||
const drag = query.draggables.find(d => d.index === ans.draggable_index);
|
||||
return `Blank ${ans.blank_index}: ${drag?.text || `Draggable ${ans.draggable_index}`}`;
|
||||
});
|
||||
textToCopy = answers.join('\n');
|
||||
break;
|
||||
}
|
||||
case MoodleQuestionType.ORDERING: {
|
||||
const query = q as Extract<typeof q, { question_type: MoodleQuestionType.ORDERING }>;
|
||||
const resp = r as Extract<typeof r, { question_type: MoodleQuestionType.ORDERING }>;
|
||||
const texts = resp.correct_order.indexes.map((idx, i) => {
|
||||
const item = query.items.find(o => o.index === idx);
|
||||
return `${i + 1}. ${item?.text || `Item ${idx}`}`;
|
||||
});
|
||||
textToCopy = texts.join('\n');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback if empty or types didn't match / no response
|
||||
if (!textToCopy) {
|
||||
if (gptAnswer.response) {
|
||||
textToCopy = JSON.stringify(gptAnswer.response, null, 2);
|
||||
} else if (gptAnswer.rawResponse) {
|
||||
textToCopy = gptAnswer.rawResponse;
|
||||
} else {
|
||||
textToCopy = '';
|
||||
}
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(textToCopy);
|
||||
}
|
||||
|
||||
export default handleClipboard;
|
||||
@@ -0,0 +1,68 @@
|
||||
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;
|
||||
}
|
||||
|
||||
const answerText =
|
||||
gptAnswer.response && 'correct_answer' in gptAnswer.response
|
||||
? String((gptAnswer.response as any).correct_answer)
|
||||
: gptAnswer.rawResponse;
|
||||
|
||||
if (config.typing) {
|
||||
let index = 0;
|
||||
|
||||
const eventHandler = function (event: KeyboardEvent) {
|
||||
event.preventDefault();
|
||||
|
||||
if (event.key === 'Backspace' || index >= answerText.length) {
|
||||
input.removeEventListener('keydown', eventHandler);
|
||||
return;
|
||||
}
|
||||
|
||||
input.textContent = answerText.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 = answerText;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export default handleContentEditable;
|
||||
@@ -0,0 +1,44 @@
|
||||
import type Config from '../../types/config';
|
||||
import type GPTAnswer from '../../types/gpt-answer';
|
||||
import type { DragDropTextResponse } from '../../types/question-types';
|
||||
import { MoodleQuestionType } from '../../types/question-types';
|
||||
|
||||
export default function handleDragDropText(
|
||||
config: Config,
|
||||
inputList: NodeListOf<HTMLElement>,
|
||||
gptAnswer: GPTAnswer
|
||||
): boolean {
|
||||
if (!gptAnswer.response || gptAnswer.response.question_type !== MoodleQuestionType.DRAG_DROP_TEXT)
|
||||
return false;
|
||||
|
||||
const response = gptAnswer.response as DragDropTextResponse;
|
||||
const hiddenInputs = Array.from(inputList).filter(el =>
|
||||
el.classList.contains('placeinput')
|
||||
) as HTMLInputElement[];
|
||||
|
||||
for (const answer of response.correct_answers) {
|
||||
const hiddenInput = hiddenInputs[answer.blank_index];
|
||||
if (!hiddenInput) continue;
|
||||
|
||||
const container = hiddenInput.closest('.que');
|
||||
if (!container) continue;
|
||||
|
||||
const dragHomes = container.querySelectorAll('.draghome.unplaced');
|
||||
const targetDrag = dragHomes[answer.draggable_index];
|
||||
if (!targetDrag) continue;
|
||||
|
||||
const choiceMatch = targetDrag.className.match(/choice(\d+)/);
|
||||
if (!choiceMatch) continue;
|
||||
|
||||
const choiceValue = choiceMatch[1];
|
||||
hiddenInput.value = choiceValue;
|
||||
|
||||
// visually update the drag drop UI string
|
||||
const blankSpan = container.querySelector(`.place${answer.blank_index + 1}.drop`);
|
||||
if (blankSpan) {
|
||||
blankSpan.textContent = targetDrag.textContent;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import type Config from '../../types/config';
|
||||
import type GPTAnswer from '../../types/gpt-answer';
|
||||
import type { SelectMissingWordResponse } from '../../types/question-types';
|
||||
import { MoodleQuestionType } from '../../types/question-types';
|
||||
|
||||
export default function handleGapSelect(
|
||||
config: Config,
|
||||
inputList: NodeListOf<HTMLElement>,
|
||||
gptAnswer: GPTAnswer
|
||||
): boolean {
|
||||
if (
|
||||
!gptAnswer.response ||
|
||||
gptAnswer.response.question_type !== MoodleQuestionType.SELECT_MISSING_WORD
|
||||
)
|
||||
return false;
|
||||
|
||||
const response = gptAnswer.response as SelectMissingWordResponse;
|
||||
const selects = Array.from(inputList).filter(
|
||||
el => el.tagName === 'SELECT'
|
||||
) as HTMLSelectElement[];
|
||||
|
||||
for (const answer of response.correct_answers) {
|
||||
const selectEl = selects[answer.drop_down_index];
|
||||
if (!selectEl) continue;
|
||||
|
||||
const options = selectEl.querySelectorAll('option');
|
||||
const correctOption = options[answer.option_index + 1];
|
||||
|
||||
if (correctOption) {
|
||||
if (config.mouseover) {
|
||||
selectEl.addEventListener('click', () => (correctOption.selected = true), { once: true });
|
||||
} else {
|
||||
correctOption.selected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import type Config from '../../types/config';
|
||||
import type GPTAnswer from '../../types/gpt-answer';
|
||||
import type { MatchResponse } from '../../types/question-types';
|
||||
import { MoodleQuestionType } from '../../types/question-types';
|
||||
|
||||
export default function handleMatch(
|
||||
config: Config,
|
||||
inputList: NodeListOf<HTMLElement>,
|
||||
gptAnswer: GPTAnswer
|
||||
): boolean {
|
||||
if (!gptAnswer.response || gptAnswer.response.question_type !== MoodleQuestionType.MATCH)
|
||||
return false;
|
||||
|
||||
const response = gptAnswer.response as MatchResponse;
|
||||
const selects = Array.from(inputList).filter(
|
||||
el => el.tagName === 'SELECT'
|
||||
) as HTMLSelectElement[];
|
||||
|
||||
for (const answer of response.correct_answers) {
|
||||
const selectEl = selects[answer.sub_question_index];
|
||||
if (!selectEl) continue;
|
||||
|
||||
const options = selectEl.querySelectorAll('option');
|
||||
const correctOption = options[answer.option_index + 1]; // + 1 because index 0 is "Choose..."
|
||||
|
||||
if (correctOption) {
|
||||
if (config.mouseover) {
|
||||
selectEl.addEventListener('click', () => (correctOption.selected = true), { once: true });
|
||||
} else {
|
||||
correctOption.selected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
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 rawNumberStr =
|
||||
gptAnswer.response && 'correct_answer' in gptAnswer.response
|
||||
? String((gptAnswer.response as any).correct_answer)
|
||||
: gptAnswer.rawResponse;
|
||||
const number = rawNumberStr.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;
|
||||
@@ -0,0 +1,52 @@
|
||||
import type Config from '../../types/config';
|
||||
import type GPTAnswer from '../../types/gpt-answer';
|
||||
import type { OrderingResponse } from '../../types/question-types';
|
||||
import { MoodleQuestionType } from '../../types/question-types';
|
||||
|
||||
export default function handleOrdering(
|
||||
config: Config,
|
||||
inputList: NodeListOf<HTMLElement>,
|
||||
gptAnswer: GPTAnswer
|
||||
): boolean {
|
||||
if (!gptAnswer.response || gptAnswer.response.question_type !== MoodleQuestionType.ORDERING)
|
||||
return false;
|
||||
|
||||
const response = gptAnswer.response as OrderingResponse;
|
||||
|
||||
// The sortable list is present within inputList because we added .sortablelist to code-listener.ts
|
||||
const sortableList = Array.from(inputList).find(el => el.classList.contains('sortablelist'));
|
||||
if (!sortableList) return true;
|
||||
|
||||
const container = sortableList.closest('.que');
|
||||
if (!container) return true;
|
||||
|
||||
const items = Array.from(sortableList.querySelectorAll('li.sortableitem'));
|
||||
|
||||
// Form final array of element IDs to place in hidden input
|
||||
const correctIdsList: string[] = [];
|
||||
const orderedItems: HTMLElement[] = [];
|
||||
|
||||
for (const index of response.correct_order.indexes) {
|
||||
const item = items[index] as HTMLElement;
|
||||
if (!item) continue;
|
||||
correctIdsList.push(item.id);
|
||||
orderedItems.push(item);
|
||||
}
|
||||
|
||||
// Find the hidden input
|
||||
const hiddenInput = container.querySelector(
|
||||
'input[type="hidden"][name*="_response_"]'
|
||||
) as HTMLInputElement;
|
||||
if (hiddenInput) {
|
||||
hiddenInput.value = correctIdsList.join(',');
|
||||
}
|
||||
|
||||
// Visually reorder elements in DOM if mouseover auto-completion is disabled (meaning auto populate directly)
|
||||
// Even if mouseover is enabled, moodle drag/drop handles are too complex to easily simulate,
|
||||
// so typically visual repopulation on ordering is acceptable.
|
||||
orderedItems.forEach(item => {
|
||||
sortableList.appendChild(item); // appendChild physically moves an already existing dom element
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
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';
|
||||
import {
|
||||
MoodleQuestionType,
|
||||
SingleChoiceResponse,
|
||||
TrueFalseResponse
|
||||
} from '../../types/question-types';
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
let correctInput: HTMLInputElement | null = null;
|
||||
|
||||
if (gptAnswer.response && gptAnswer.response.question_type === MoodleQuestionType.SINGLE_CHOICE) {
|
||||
const res = gptAnswer.response as SingleChoiceResponse;
|
||||
const index = res.correct_answer.index;
|
||||
if (index >= 0 && index < inputList.length) {
|
||||
correctInput = inputList[index] as HTMLInputElement;
|
||||
}
|
||||
} else if (
|
||||
gptAnswer.response &&
|
||||
gptAnswer.response.question_type === MoodleQuestionType.TRUE_FALSE
|
||||
) {
|
||||
const res = gptAnswer.response as TrueFalseResponse;
|
||||
// In Moodle true/false typically true is index 0 and false is index 1 or vice-versa.
|
||||
// The query extracted options though... wait! True/false doesn't use `extractOptions`!
|
||||
// So we need to match text "true" or "false", or 1/0.
|
||||
const isTrue = res.correct_answer === true;
|
||||
|
||||
// Quick fallback fuzzy to "true" or "false" if we don't know the exact indices
|
||||
// True/false has radio options, so we can search by text.
|
||||
const possibleAnswers = Array.from(inputList)
|
||||
.map(inp => ({
|
||||
element: inp,
|
||||
value: normalizeText(inp?.parentElement?.textContent ?? '')
|
||||
}))
|
||||
.filter(obj => obj.value !== '');
|
||||
|
||||
const bestAnswer = pickBestReponse(isTrue ? 'true' : 'false', possibleAnswers);
|
||||
correctInput = bestAnswer.element as HTMLInputElement;
|
||||
} else {
|
||||
// Fallback parsing
|
||||
const possibleAnswers = Array.from(inputList)
|
||||
.map(inp => ({
|
||||
element: inp,
|
||||
value: normalizeText(inp?.parentElement?.textContent ?? '')
|
||||
}))
|
||||
.filter(obj => obj.value !== '');
|
||||
|
||||
const bestAnswer = pickBestReponse(gptAnswer.rawResponse, possibleAnswers);
|
||||
|
||||
if (config.logs && bestAnswer.value) {
|
||||
Logs.bestAnswer(bestAnswer.value, bestAnswer.similarity);
|
||||
}
|
||||
correctInput = bestAnswer.element as HTMLInputElement;
|
||||
}
|
||||
|
||||
if (correctInput) {
|
||||
if (config.mouseover) {
|
||||
correctInput.addEventListener('mouseover', () => (correctInput as HTMLInputElement).click(), {
|
||||
once: true
|
||||
});
|
||||
} else {
|
||||
correctInput.click();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export default handleRadio;
|
||||
@@ -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 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 rawResponse =
|
||||
gptAnswer.response && 'correct_answer' in gptAnswer.response
|
||||
? String((gptAnswer.response as any).correct_answer)
|
||||
: gptAnswer.rawResponse;
|
||||
|
||||
const corrects = rawResponse.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;
|
||||
@@ -0,0 +1,52 @@
|
||||
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;
|
||||
}
|
||||
|
||||
const answerText =
|
||||
gptAnswer.response && 'correct_answer' in gptAnswer.response
|
||||
? String((gptAnswer.response as any).correct_answer)
|
||||
: gptAnswer.rawResponse;
|
||||
|
||||
if (config.typing) {
|
||||
let index = 0;
|
||||
|
||||
const eventHandler = function (event: Event) {
|
||||
event.preventDefault();
|
||||
|
||||
if ((<KeyboardEvent>event).key === 'Backspace' || index >= answerText.length) {
|
||||
input.removeEventListener('keydown', eventHandler);
|
||||
return;
|
||||
}
|
||||
|
||||
input.value = answerText.slice(0, ++index);
|
||||
};
|
||||
|
||||
input.addEventListener('keydown', eventHandler);
|
||||
} else {
|
||||
input.value = answerText;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export default handleTextbox;
|
||||
@@ -0,0 +1,70 @@
|
||||
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 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 'autocomplete':
|
||||
autoCompleteMode({
|
||||
config: props.config,
|
||||
gptAnswer,
|
||||
inputList,
|
||||
questionElement: props.questionElement,
|
||||
removeListener: props.removeListener
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export default reply;
|
||||
@@ -0,0 +1,193 @@
|
||||
import { MoodleQuestionType } from '../../types/question-types';
|
||||
|
||||
export const QuestionSchemas: Record<MoodleQuestionType, any> = {
|
||||
[MoodleQuestionType.SINGLE_CHOICE]: {
|
||||
name: 'single_choice_response',
|
||||
strict: true,
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
question_type: { type: 'string', enum: [MoodleQuestionType.SINGLE_CHOICE] },
|
||||
correct_answer: {
|
||||
type: 'object',
|
||||
properties: { index: { type: 'integer' } },
|
||||
required: ['index'],
|
||||
additionalProperties: false
|
||||
}
|
||||
},
|
||||
required: ['question_type', 'correct_answer'],
|
||||
additionalProperties: false
|
||||
}
|
||||
},
|
||||
[MoodleQuestionType.MULTIPLE_CHOICE]: {
|
||||
name: 'multiple_choice_response',
|
||||
strict: true,
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
question_type: { type: 'string', enum: [MoodleQuestionType.MULTIPLE_CHOICE] },
|
||||
correct_answer: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
indexes: {
|
||||
type: 'array',
|
||||
items: { type: 'integer' }
|
||||
}
|
||||
},
|
||||
required: ['indexes'],
|
||||
additionalProperties: false
|
||||
}
|
||||
},
|
||||
required: ['question_type', 'correct_answer'],
|
||||
additionalProperties: false
|
||||
}
|
||||
},
|
||||
[MoodleQuestionType.TRUE_FALSE]: {
|
||||
name: 'true_false_response',
|
||||
strict: true,
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
question_type: { type: 'string', enum: [MoodleQuestionType.TRUE_FALSE] },
|
||||
correct_answer: { type: 'boolean' }
|
||||
},
|
||||
required: ['question_type', 'correct_answer'],
|
||||
additionalProperties: false
|
||||
}
|
||||
},
|
||||
[MoodleQuestionType.SHORT_TEXT]: {
|
||||
name: 'short_text_response',
|
||||
strict: true,
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
question_type: { type: 'string', enum: [MoodleQuestionType.SHORT_TEXT] },
|
||||
correct_answer: { type: 'string' }
|
||||
},
|
||||
required: ['question_type', 'correct_answer'],
|
||||
additionalProperties: false
|
||||
}
|
||||
},
|
||||
[MoodleQuestionType.NUMERICAL]: {
|
||||
name: 'numerical_response',
|
||||
strict: true,
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
question_type: { type: 'string', enum: [MoodleQuestionType.NUMERICAL] },
|
||||
correct_answer: { type: 'number' }
|
||||
},
|
||||
required: ['question_type', 'correct_answer'],
|
||||
additionalProperties: false
|
||||
}
|
||||
},
|
||||
[MoodleQuestionType.ESSAY]: {
|
||||
name: 'essay_response',
|
||||
strict: true,
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
question_type: { type: 'string', enum: [MoodleQuestionType.ESSAY] },
|
||||
correct_answer: { type: 'string' }
|
||||
},
|
||||
required: ['question_type', 'correct_answer'],
|
||||
additionalProperties: false
|
||||
}
|
||||
},
|
||||
[MoodleQuestionType.MATCH]: {
|
||||
name: 'match_response',
|
||||
strict: true,
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
question_type: { type: 'string', enum: [MoodleQuestionType.MATCH] },
|
||||
correct_answers: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
sub_question_index: { type: 'integer' },
|
||||
option_index: { type: 'integer' }
|
||||
},
|
||||
required: ['sub_question_index', 'option_index'],
|
||||
additionalProperties: false
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['question_type', 'correct_answers'],
|
||||
additionalProperties: false
|
||||
}
|
||||
},
|
||||
[MoodleQuestionType.SELECT_MISSING_WORD]: {
|
||||
name: 'select_missing_word_response',
|
||||
strict: true,
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
question_type: { type: 'string', enum: [MoodleQuestionType.SELECT_MISSING_WORD] },
|
||||
correct_answers: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
drop_down_index: { type: 'integer' },
|
||||
option_index: { type: 'integer' }
|
||||
},
|
||||
required: ['drop_down_index', 'option_index'],
|
||||
additionalProperties: false
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['question_type', 'correct_answers'],
|
||||
additionalProperties: false
|
||||
}
|
||||
},
|
||||
[MoodleQuestionType.DRAG_DROP_TEXT]: {
|
||||
name: 'drag_drop_text_response',
|
||||
strict: true,
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
question_type: { type: 'string', enum: [MoodleQuestionType.DRAG_DROP_TEXT] },
|
||||
correct_answers: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
blank_index: { type: 'integer' },
|
||||
draggable_index: { type: 'integer' }
|
||||
},
|
||||
required: ['blank_index', 'draggable_index'],
|
||||
additionalProperties: false
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['question_type', 'correct_answers'],
|
||||
additionalProperties: false
|
||||
}
|
||||
},
|
||||
[MoodleQuestionType.ORDERING]: {
|
||||
name: 'ordering_response',
|
||||
strict: true,
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
question_type: { type: 'string', enum: [MoodleQuestionType.ORDERING] },
|
||||
correct_order: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
indexes: {
|
||||
type: 'array',
|
||||
items: { type: 'integer' }
|
||||
}
|
||||
},
|
||||
required: ['indexes'],
|
||||
additionalProperties: false
|
||||
}
|
||||
},
|
||||
required: ['question_type', 'correct_order'],
|
||||
additionalProperties: false
|
||||
}
|
||||
},
|
||||
[MoodleQuestionType.UNKNOWN]: undefined
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
type Config = {
|
||||
apiKey: string;
|
||||
model: string;
|
||||
code?: string;
|
||||
infinite?: boolean;
|
||||
typing?: boolean;
|
||||
mouseover?: boolean;
|
||||
cursor?: boolean;
|
||||
logs?: boolean;
|
||||
title?: boolean;
|
||||
timeout?: boolean;
|
||||
history?: boolean;
|
||||
includeImages?: boolean;
|
||||
mode?: 'autocomplete' | 'clipboard';
|
||||
baseURL?: string;
|
||||
projectId?: string;
|
||||
maxTokens?: number;
|
||||
timeoutValue?: number;
|
||||
};
|
||||
|
||||
export default Config;
|
||||
@@ -0,0 +1,9 @@
|
||||
import { MoodleQuestionQuery, MoodleQuestionResponse } from './question-types';
|
||||
|
||||
type GPTAnswer = {
|
||||
questionQuery: MoodleQuestionQuery | null;
|
||||
response: MoodleQuestionResponse | null;
|
||||
rawResponse: string; // Keep the original just in case or for logging/unknown
|
||||
};
|
||||
|
||||
export default GPTAnswer;
|
||||
@@ -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;
|
||||
};
|
||||
@@ -0,0 +1,155 @@
|
||||
export enum MoodleQuestionType {
|
||||
SINGLE_CHOICE = 'single_choice',
|
||||
MULTIPLE_CHOICE = 'multiple_choice',
|
||||
TRUE_FALSE = 'true_false',
|
||||
SHORT_TEXT = 'short_text',
|
||||
NUMERICAL = 'numerical',
|
||||
ESSAY = 'essay',
|
||||
MATCH = 'match',
|
||||
SELECT_MISSING_WORD = 'select_missing_word',
|
||||
DRAG_DROP_TEXT = 'drag_drop_text',
|
||||
ORDERING = 'ordering',
|
||||
UNKNOWN = 'unknown'
|
||||
}
|
||||
|
||||
export interface AnswerOption {
|
||||
index: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
// ==== Queries sent to LLM ====
|
||||
|
||||
export interface SingleChoiceQuery {
|
||||
question_type: MoodleQuestionType.SINGLE_CHOICE;
|
||||
question_text: string;
|
||||
answer_options: AnswerOption[];
|
||||
}
|
||||
|
||||
export interface MultipleChoiceQuery {
|
||||
question_type: MoodleQuestionType.MULTIPLE_CHOICE;
|
||||
question_text: string;
|
||||
answer_options: AnswerOption[];
|
||||
}
|
||||
|
||||
export interface TrueFalseQuery {
|
||||
question_type: MoodleQuestionType.TRUE_FALSE;
|
||||
question_text: string;
|
||||
}
|
||||
|
||||
export interface ShortTextQuery {
|
||||
question_type: MoodleQuestionType.SHORT_TEXT;
|
||||
question_text: string;
|
||||
}
|
||||
|
||||
export interface NumericalQuery {
|
||||
question_type: MoodleQuestionType.NUMERICAL;
|
||||
question_text: string;
|
||||
}
|
||||
|
||||
export interface EssayQuery {
|
||||
question_type: MoodleQuestionType.ESSAY;
|
||||
question_text: string;
|
||||
format: 'plain_text' | 'html';
|
||||
initial_text?: string;
|
||||
}
|
||||
|
||||
export interface MatchQuery {
|
||||
question_type: MoodleQuestionType.MATCH;
|
||||
question_text: string;
|
||||
sub_questions: { index: number; text: string }[];
|
||||
options: { index: number; text: string }[];
|
||||
}
|
||||
|
||||
export interface SelectMissingWordQuery {
|
||||
question_type: MoodleQuestionType.SELECT_MISSING_WORD;
|
||||
question_text: string;
|
||||
drop_downs: { index: number; options: { index: number; text: string }[] }[];
|
||||
}
|
||||
|
||||
export interface DragDropTextQuery {
|
||||
question_type: MoodleQuestionType.DRAG_DROP_TEXT;
|
||||
question_text: string;
|
||||
draggables: { index: number; text: string }[];
|
||||
}
|
||||
|
||||
export interface OrderingQuery {
|
||||
question_type: MoodleQuestionType.ORDERING;
|
||||
question_text: string;
|
||||
items: { index: number; text: string }[];
|
||||
}
|
||||
|
||||
export type MoodleQuestionQuery =
|
||||
| SingleChoiceQuery
|
||||
| MultipleChoiceQuery
|
||||
| TrueFalseQuery
|
||||
| ShortTextQuery
|
||||
| NumericalQuery
|
||||
| EssayQuery
|
||||
| MatchQuery
|
||||
| SelectMissingWordQuery
|
||||
| DragDropTextQuery
|
||||
| OrderingQuery;
|
||||
|
||||
// ==== Expected LLM Responses ====
|
||||
|
||||
export interface SingleChoiceResponse {
|
||||
question_type: MoodleQuestionType.SINGLE_CHOICE;
|
||||
correct_answer: { index: number };
|
||||
}
|
||||
|
||||
export interface MultipleChoiceResponse {
|
||||
question_type: MoodleQuestionType.MULTIPLE_CHOICE;
|
||||
correct_answer: { indexes: number[] };
|
||||
}
|
||||
|
||||
export interface TrueFalseResponse {
|
||||
question_type: MoodleQuestionType.TRUE_FALSE;
|
||||
correct_answer: boolean;
|
||||
}
|
||||
|
||||
export interface ShortTextResponse {
|
||||
question_type: MoodleQuestionType.SHORT_TEXT;
|
||||
correct_answer: string;
|
||||
}
|
||||
|
||||
export interface NumericalResponse {
|
||||
question_type: MoodleQuestionType.NUMERICAL;
|
||||
correct_answer: number;
|
||||
}
|
||||
|
||||
export interface EssayResponse {
|
||||
question_type: MoodleQuestionType.ESSAY;
|
||||
correct_answer: string;
|
||||
}
|
||||
|
||||
export interface MatchResponse {
|
||||
question_type: MoodleQuestionType.MATCH;
|
||||
correct_answers: { sub_question_index: number; option_index: number }[];
|
||||
}
|
||||
|
||||
export interface SelectMissingWordResponse {
|
||||
question_type: MoodleQuestionType.SELECT_MISSING_WORD;
|
||||
correct_answers: { drop_down_index: number; option_index: number }[];
|
||||
}
|
||||
|
||||
export interface DragDropTextResponse {
|
||||
question_type: MoodleQuestionType.DRAG_DROP_TEXT;
|
||||
correct_answers: { blank_index: number; draggable_index: number }[];
|
||||
}
|
||||
|
||||
export interface OrderingResponse {
|
||||
question_type: MoodleQuestionType.ORDERING;
|
||||
correct_order: { indexes: number[] };
|
||||
}
|
||||
|
||||
export type MoodleQuestionResponse =
|
||||
| SingleChoiceResponse
|
||||
| MultipleChoiceResponse
|
||||
| TrueFalseResponse
|
||||
| ShortTextResponse
|
||||
| NumericalResponse
|
||||
| EssayResponse
|
||||
| MatchResponse
|
||||
| SelectMissingWordResponse
|
||||
| DragDropTextResponse
|
||||
| OrderingResponse;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,28 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
export default Logs;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1,15 +0,0 @@
|
||||
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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1,39 +0,0 @@
|
||||
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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
import { MoodleGPTConfig } from '../hooks/useConfig';
|
||||
|
||||
interface Props {
|
||||
config: MoodleGPTConfig;
|
||||
onChange: (key: keyof MoodleGPTConfig, value: MoodleGPTConfig[keyof MoodleGPTConfig]) => void;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
export function AdvancedSettingsPanel({ config, onChange, visible }: Props) {
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
class="bg-panel-bg backdrop-blur-md border border-panel-border rounded-2xl p-4 shadow-[0_8px_32px_0_rgba(0,0,0,0.3)] flex flex-col gap-4"
|
||||
id="advanced-settings"
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label htmlFor="code" class="text-sm font-medium text-text-secondary">
|
||||
Activation Code
|
||||
</label>
|
||||
<input
|
||||
id="code"
|
||||
type="text"
|
||||
placeholder="Secret key..."
|
||||
class="w-full px-3 py-2.5 bg-input-bg border border-input-border rounded-lg text-text-primary text-sm transition-all focus:outline-none focus:border-input-focus focus:ring-2 focus:ring-primary/20 box-border"
|
||||
value={config.code || ''}
|
||||
onInput={e => onChange('code', (e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label htmlFor="baseURL" class="text-sm font-medium text-text-secondary">
|
||||
Base URL
|
||||
</label>
|
||||
<input
|
||||
id="baseURL"
|
||||
type="text"
|
||||
placeholder="https://api.openai.com/v1"
|
||||
class="w-full px-3 py-2.5 bg-input-bg border border-input-border rounded-lg text-text-primary text-sm transition-all focus:outline-none focus:border-input-focus focus:ring-2 focus:ring-primary/20 box-border"
|
||||
value={config.baseURL || ''}
|
||||
onInput={e => onChange('baseURL', (e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label htmlFor="projectId" class="text-sm font-medium text-text-secondary">
|
||||
Project ID
|
||||
</label>
|
||||
<input
|
||||
id="projectId"
|
||||
type="text"
|
||||
placeholder="proj_..."
|
||||
class="w-full px-3 py-2.5 bg-input-bg border border-input-border rounded-lg text-text-primary text-sm transition-all focus:outline-none focus:border-input-focus focus:ring-2 focus:ring-primary/20 box-border"
|
||||
value={config.projectId || ''}
|
||||
onInput={e => onChange('projectId', (e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label htmlFor="maxTokens" class="text-sm font-medium text-text-secondary">
|
||||
Max Tokens
|
||||
</label>
|
||||
<input
|
||||
id="maxTokens"
|
||||
type="number"
|
||||
class="w-full px-3 py-2.5 bg-input-bg border border-input-border rounded-lg text-text-primary text-sm transition-all focus:outline-none focus:border-input-focus focus:ring-2 focus:ring-primary/20 box-border"
|
||||
value={config.maxTokens || ''}
|
||||
onInput={e => {
|
||||
const val = (e.target as HTMLInputElement).value;
|
||||
onChange('maxTokens', val ? parseInt(val) : undefined);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label htmlFor="timeoutValue" class="text-sm font-medium text-text-secondary">
|
||||
Timeout (s)
|
||||
</label>
|
||||
<input
|
||||
id="timeoutValue"
|
||||
type="number"
|
||||
placeholder="20"
|
||||
class="w-full px-3 py-2.5 bg-input-bg border border-input-border rounded-lg text-text-primary text-sm transition-all focus:outline-none focus:border-input-focus focus:ring-2 focus:ring-primary/20 box-border"
|
||||
value={config.timeoutValue || ''}
|
||||
onInput={e => {
|
||||
const val = (e.target as HTMLInputElement).value;
|
||||
onChange('timeoutValue', val ? parseInt(val) : undefined);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { useState } from 'preact/hooks';
|
||||
import { Header } from './Header';
|
||||
import { SettingsPanel } from './SettingsPanel';
|
||||
import { AdvancedSettingsPanel } from './AdvancedSettingsPanel';
|
||||
import { OptionsGrid } from './OptionsGrid';
|
||||
import { OperatingMode } from './OperatingMode';
|
||||
import { useConfig, MoodleGPTConfig } from '../hooks/useConfig';
|
||||
|
||||
export function App() {
|
||||
const { config, loading, saveConfig, setConfig } = useConfig();
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
const [message, setMessage] = useState<{ text: string; isError: boolean } | null>(null);
|
||||
|
||||
if (loading) return null;
|
||||
|
||||
const handleConfigChange = (
|
||||
key: keyof MoodleGPTConfig,
|
||||
value: MoodleGPTConfig[keyof MoodleGPTConfig]
|
||||
) => {
|
||||
setConfig(prev => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
const showMessage = (msg: string, isError = false) => {
|
||||
setMessage({ text: msg, isError });
|
||||
setTimeout(() => setMessage(null), 5000);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!config.apiKey || !config.model) {
|
||||
showMessage('Please complete all the form', true);
|
||||
return;
|
||||
}
|
||||
if (config.code && config.code.length > 0 && config.code.length < 2) {
|
||||
showMessage('The code should at least contain 2 characters', true);
|
||||
return;
|
||||
}
|
||||
await saveConfig(config);
|
||||
showMessage('Configuration saved');
|
||||
};
|
||||
|
||||
return (
|
||||
<main class="p-6 flex flex-col gap-5 bg-gradient-to-br from-gradient-start to-gradient-end text-text-primary min-h-screen font-sans antialiased overflow-x-hidden">
|
||||
<Header />
|
||||
|
||||
<SettingsPanel
|
||||
config={config}
|
||||
onChange={handleConfigChange}
|
||||
showMessage={showMessage}
|
||||
visible={!showAdvanced}
|
||||
/>
|
||||
|
||||
<AdvancedSettingsPanel config={config} onChange={handleConfigChange} visible={showAdvanced} />
|
||||
|
||||
<a
|
||||
href="#"
|
||||
class="block text-center text-sm text-text-secondary no-underline transition-colors hover:text-text-primary"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
setShowAdvanced(!showAdvanced);
|
||||
}}
|
||||
>
|
||||
{showAdvanced ? 'Go back to settings' : 'Show Advanced Settings'}
|
||||
</a>
|
||||
|
||||
<OperatingMode
|
||||
mode={config.mode || 'autocomplete'}
|
||||
onChange={m => handleConfigChange('mode', m)}
|
||||
/>
|
||||
|
||||
<div class="flex items-center gap-2 text-sm font-semibold text-text-primary my-2">
|
||||
<i class="fa-solid fa-sliders text-primary"></i>
|
||||
<span>Options</span>
|
||||
</div>
|
||||
|
||||
<OptionsGrid config={config} onChange={handleConfigChange} />
|
||||
|
||||
{message && (
|
||||
<p
|
||||
class={`text-center text-sm font-medium m-0 min-h-[18px] ${message.isError ? 'text-error' : 'text-success'}`}
|
||||
>
|
||||
{message.text}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
class="w-full p-3 bg-gradient-to-br from-primary to-primary-hover text-white border-none rounded-xl text-base font-semibold cursor-pointer transition-all hover:-translate-y-0.5 hover:shadow-[0_4px_12px_rgba(99,102,241,0.4)] active:translate-y-0 mt-2"
|
||||
onClick={handleSave}
|
||||
>
|
||||
Save Preferences
|
||||
</button>
|
||||
|
||||
<div class="flex justify-center gap-6 mt-2">
|
||||
<a
|
||||
class="text-sm font-medium no-underline transition-colors text-amber-400 hover:text-amber-300"
|
||||
href="https://www.buymeacoffee.com/yoannchbpro"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Support
|
||||
</a>
|
||||
<a
|
||||
class="text-sm font-medium no-underline transition-colors text-text-secondary hover:text-text-primary"
|
||||
href="https://github.com/yoannchb-pro/MoodleGPT"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Docs
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { useEffect, useState } from 'preact/hooks';
|
||||
|
||||
export function Header() {
|
||||
const [version, setVersion] = useState('2.0.0');
|
||||
const [hasUpdate, setHasUpdate] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkVersion = async () => {
|
||||
try {
|
||||
const req = await fetch(
|
||||
'https://raw.githubusercontent.com/yoannchb-pro/MoodleGPT/main/package.json'
|
||||
);
|
||||
const rep = await req.json();
|
||||
const lastVersion = rep.version;
|
||||
|
||||
const lastVertionSplitted = lastVersion.split('.');
|
||||
const currentVersionSplitted = version.split('.');
|
||||
const minVersionLength = Math.min(
|
||||
lastVertionSplitted.length,
|
||||
currentVersionSplitted.length
|
||||
);
|
||||
|
||||
for (let i = 0; i < minVersionLength; ++i) {
|
||||
if (parseInt(lastVertionSplitted[i]) > parseInt(currentVersionSplitted[i])) {
|
||||
setVersion(lastVersion);
|
||||
setHasUpdate(true);
|
||||
return;
|
||||
} else if (parseInt(currentVersionSplitted[i]) > parseInt(lastVertionSplitted[i])) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
checkVersion();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div class="flex items-center gap-4 mb-2">
|
||||
<img
|
||||
src="../icon.png"
|
||||
alt="SparkAssist Logo"
|
||||
class="w-12 h-12 drop-shadow-md animate-float"
|
||||
/>
|
||||
<div>
|
||||
<h1 class="m-0 text-2xl font-bold bg-gradient-to-r from-indigo-300 to-indigo-400 bg-clip-text text-transparent">
|
||||
SparkAssist
|
||||
</h1>
|
||||
<p class="m-0 mt-1 text-xs text-text-secondary">
|
||||
{hasUpdate ? (
|
||||
<>
|
||||
<a
|
||||
href="https://github.com/yoannchb-pro/MoodleGPT"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-sky-400 no-underline font-medium hover:text-sky-300 transition-colors"
|
||||
>
|
||||
v{version}
|
||||
</a>{' '}
|
||||
is now available !
|
||||
</>
|
||||
) : (
|
||||
`v${version}`
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
interface Props {
|
||||
mode: 'autocomplete' | 'clipboard';
|
||||
onChange: (mode: 'autocomplete' | 'clipboard') => void;
|
||||
}
|
||||
|
||||
export function OperatingMode({ mode, onChange }: Props) {
|
||||
return (
|
||||
<>
|
||||
<div class="flex items-center gap-2 text-sm font-semibold text-text-primary my-2">
|
||||
<i class="fa-solid fa-bolt text-primary"></i>
|
||||
<span>Operating Mode</span>
|
||||
</div>
|
||||
<ul
|
||||
id="mode"
|
||||
class="list-none p-0 m-0 flex bg-input-bg rounded-xl border border-input-border overflow-hidden"
|
||||
>
|
||||
<li class="flex-1">
|
||||
<button
|
||||
value="autocomplete"
|
||||
class={`w-full p-2.5 border-none font-sans text-sm font-semibold cursor-pointer transition-colors duration-300 ${
|
||||
mode === 'autocomplete'
|
||||
? 'bg-primary text-text-primary shadow-[0_2px_8px_rgba(99,102,241,0.4)]'
|
||||
: 'bg-transparent text-text-secondary hover:bg-white/5 hover:text-text-primary'
|
||||
}`}
|
||||
onClick={() => onChange('autocomplete')}
|
||||
>
|
||||
autocomplete
|
||||
</button>
|
||||
</li>
|
||||
<li class="flex-1">
|
||||
<button
|
||||
value="clipboard"
|
||||
class={`w-full p-2.5 border-none font-sans text-sm font-semibold cursor-pointer transition-colors duration-300 ${
|
||||
mode === 'clipboard'
|
||||
? 'bg-primary text-text-primary shadow-[0_2px_8px_rgba(99,102,241,0.4)]'
|
||||
: 'bg-transparent text-text-secondary hover:bg-white/5 hover:text-text-primary'
|
||||
}`}
|
||||
onClick={() => onChange('clipboard')}
|
||||
>
|
||||
clipboard
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { MoodleGPTConfig } from '../hooks/useConfig';
|
||||
import { useModel } from '../hooks/useModel';
|
||||
|
||||
interface Props {
|
||||
config: MoodleGPTConfig;
|
||||
onChange: (key: keyof MoodleGPTConfig, value: MoodleGPTConfig[keyof MoodleGPTConfig]) => void;
|
||||
}
|
||||
|
||||
export function OptionsGrid({ config, onChange }: Props) {
|
||||
const { isCurrentVersionSupportingImages } = useModel();
|
||||
|
||||
const toggleRow = (id: keyof MoodleGPTConfig, label: string, visible = true) => {
|
||||
if (!visible) return null;
|
||||
return (
|
||||
<div class="flex justify-between items-center">
|
||||
<label htmlFor={id} class="text-sm text-text-primary">
|
||||
{label}
|
||||
</label>
|
||||
<label class="toggle-switch">
|
||||
<input
|
||||
id={id}
|
||||
type="checkbox"
|
||||
checked={!!config[id]}
|
||||
onChange={e => onChange(id, (e.target as HTMLInputElement).checked)}
|
||||
/>
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const isClipboard = config.mode === 'clipboard';
|
||||
|
||||
return (
|
||||
<div class="bg-panel-bg backdrop-blur-md border border-panel-border rounded-2xl p-4 shadow-[0_8px_32px_0_rgba(0,0,0,0.3)] grid grid-cols-2 gap-4">
|
||||
{toggleRow('logs', 'Console logs')}
|
||||
{toggleRow('title', 'Title hint')}
|
||||
{toggleRow('cursor', 'Cursor hint')}
|
||||
{toggleRow('timeout', 'Timeout')}
|
||||
{toggleRow('typing', 'Typing effect', !isClipboard)}
|
||||
{toggleRow('mouseover', 'Hover effect', !isClipboard)}
|
||||
{toggleRow('infinite', 'Infinite try')}
|
||||
{toggleRow('history', 'History')}
|
||||
{toggleRow('includeImages', 'Images (GPT-4)', isCurrentVersionSupportingImages(config.model))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { useModel } from '../hooks/useModel';
|
||||
import { MoodleGPTConfig } from '../hooks/useConfig';
|
||||
import { useState } from 'preact/hooks';
|
||||
|
||||
interface Props {
|
||||
config: MoodleGPTConfig;
|
||||
onChange: (key: keyof MoodleGPTConfig, value: MoodleGPTConfig[keyof MoodleGPTConfig]) => void;
|
||||
showMessage: (msg: string, isError?: boolean) => void;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
export function SettingsPanel({ config, onChange, showMessage, visible }: Props) {
|
||||
if (!visible) return null;
|
||||
const { models, fetchModels, validateModel } = useModel(
|
||||
config.apiKey,
|
||||
config.baseURL,
|
||||
config.projectId
|
||||
);
|
||||
const [testing, setTesting] = useState(false);
|
||||
|
||||
const handleTest = async () => {
|
||||
if (!config.model) {
|
||||
showMessage('Please select a model first', true);
|
||||
return;
|
||||
}
|
||||
setTesting(true);
|
||||
showMessage('Checking GPT version...', false);
|
||||
const result = await validateModel(config.model, config.maxTokens);
|
||||
setTesting(false);
|
||||
if (result.success) {
|
||||
showMessage(result.message || 'Valid model');
|
||||
} else {
|
||||
showMessage(result.error || 'Invalid model', true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
class="bg-panel-bg backdrop-blur-md border border-panel-border rounded-2xl p-4 shadow-[0_8px_32px_0_rgba(0,0,0,0.3)]"
|
||||
id="settings"
|
||||
>
|
||||
<div class="flex flex-col gap-2 mb-4">
|
||||
<label htmlFor="apiKey" class="text-sm font-medium text-text-secondary">
|
||||
Api Key<span class="text-error ml-1">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="apiKey"
|
||||
type="text"
|
||||
placeholder="sk-..."
|
||||
class="w-full px-3 py-2.5 bg-input-bg border border-input-border rounded-lg text-text-primary text-sm transition-all focus:outline-none focus:border-input-focus focus:ring-2 focus:ring-primary/20 box-border"
|
||||
value={config.apiKey || ''}
|
||||
onInput={e => onChange('apiKey', (e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label htmlFor="model" class="text-sm font-medium text-text-secondary">
|
||||
GPT Model<span class="text-error ml-1">*</span>
|
||||
</label>
|
||||
<div class="relative flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
id="model"
|
||||
list="models"
|
||||
placeholder="gpt-4o"
|
||||
class="w-full pl-3 pr-9 py-2.5 bg-input-bg border border-input-border rounded-lg text-text-primary text-sm transition-all focus:outline-none focus:border-input-focus focus:ring-2 focus:ring-primary/20 box-border"
|
||||
value={config.model || ''}
|
||||
onInput={e => onChange('model', (e.target as HTMLInputElement).value)}
|
||||
onFocus={fetchModels}
|
||||
/>
|
||||
<datalist id="models">
|
||||
{models.map(m => (
|
||||
<option key={m} value={m}>
|
||||
{m}
|
||||
</option>
|
||||
))}
|
||||
</datalist>
|
||||
<i
|
||||
class={`fa-solid ${testing ? 'fa-spinner fa-spin' : 'fa-play'} absolute right-3 text-primary text-sm transition-all hover:scale-110 hover:text-primary-hover cursor-pointer`}
|
||||
onClick={handleTest}
|
||||
title="Test"
|
||||
></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useState, useEffect } from 'preact/hooks';
|
||||
|
||||
export interface MoodleGPTConfig {
|
||||
apiKey?: string;
|
||||
code?: string;
|
||||
model?: string;
|
||||
baseURL?: string;
|
||||
maxTokens?: number;
|
||||
projectId?: string;
|
||||
timeoutValue?: number;
|
||||
logs?: boolean;
|
||||
title?: boolean;
|
||||
cursor?: boolean;
|
||||
typing?: boolean;
|
||||
mouseover?: boolean;
|
||||
infinite?: boolean;
|
||||
timeout?: boolean;
|
||||
history?: boolean;
|
||||
includeImages?: boolean;
|
||||
mode?: 'autocomplete' | 'clipboard';
|
||||
}
|
||||
|
||||
export function useConfig() {
|
||||
const [config, setConfig] = useState<MoodleGPTConfig>({
|
||||
mode: 'autocomplete',
|
||||
title: true,
|
||||
cursor: true,
|
||||
timeout: true
|
||||
});
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
chrome.storage.sync.get(['moodleGPT']).then(storage => {
|
||||
if (storage.moodleGPT) {
|
||||
setConfig(prev => ({ ...prev, ...storage.moodleGPT }));
|
||||
}
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const saveConfig = async (newConfig: MoodleGPTConfig) => {
|
||||
const updated = { ...config, ...newConfig };
|
||||
setConfig(updated);
|
||||
await chrome.storage.sync.set({ moodleGPT: updated });
|
||||
};
|
||||
|
||||
return { config, loading, saveConfig, setConfig };
|
||||
}
|
||||