27 Commits

Author SHA1 Message Date
blackicedbear 8b45a5d5ce Migrate popup UI to Preact and Tailwind CSS 2026-05-05 19:55:09 +02:00
blackicedbear d36949b42f Enhance clipboard mode to format answers based on question type 2026-05-05 19:12:54 +02:00
blackicedbear e7f00359a1 Rebrand to SparkAssist and update version to 2.0.0 2026-04-30 22:04:30 +02:00
blackicedbear 03bc4e64e4 refactor: update clipboard mode for JSON and remove question-to-answer mode 2026-04-28 19:53:39 +02:00
blackicedbear 46c5b756a5 feat(popup): validate model using structured JSON output
Replace the fire-and-forget model ping with a structured output
test that enforces a JSON schema (reply, success, data.number).
The parsed response is validated against the expected types before
showing a success message, providing stronger proof that the
selected model both responds and supports structured outputs.
2026-04-25 20:11:02 +02:00
blackicedbear b0873f3ed3 Refactor: Use native json_schema structured output, remove manual schema injection 2026-04-18 02:02:16 +02:00
blackicedbear c3bc3bbcdd Feat: Discern Essay format types and load initial text templates 2026-04-18 01:39:20 +02:00
blackicedbear 9cab0155b1 Feat: Add Configurable Timeout, internal Project ID fields, and patch model testing payload 2026-04-12 19:20:47 +02:00
blackicedbear a3e828a00e Feat: Add support for Ordering questions 2026-04-12 12:57:42 +02:00
blackicedbear 1e44cf4129 Docs: Update README with new supported question types and JSON parsing engine 2026-04-11 22:17:53 +02:00
blackicedbear 72be91d796 Refactor: Add support for Match, Gapselect, DDwtos and Calculated Moodle question types 2026-04-11 21:20:46 +02:00
blackicedbear 6ce2c47cb4 Refactor: implemented robust structured JSON parser for Moodle questions and DOM scope detection 2026-04-11 19:52:39 +02:00
yoannchb-pro 921e2bba4e Merge branch 'dev' 2025-10-01 09:30:19 +02:00
yoannchb-pro 3591aceba1 v1.1.5 2025-10-01 09:28:48 +02:00
yoannchb-pro 17aa8b49e0 Merge pull request #61 from yoannchb-pro/dev
Dev
2025-07-30 11:14:23 -04:00
yoannchb-pro a2849da20c v1.1.4 2025-07-30 11:12:44 -04:00
yoannchb-pro 54c9373e33 Merge pull request #58 from LuckyForce/main
Solves #57 for o3
2025-07-30 10:52:03 -04:00
Adrian Schauer e793cbd0b2 #57 addtion of o3 2025-07-10 17:01:10 +02:00
yoannchb-pro fffc0d55d6 Merge pull request #54 from yoannchb-pro/dev
v1.1.3
2025-03-19 09:28:49 -04:00
yoannchb-pro e561227b78 updated doc 2025-03-19 09:27:19 -04:00
yoannchb-pro 677e870635 gpt version msg 2025-03-19 09:09:35 -04:00
yoannchb-pro 6e69830a2e v1.1.2 -> v1.1.3 2025-03-19 09:07:52 -04:00
yoannchb-pro f01785256c Updated dependencies 2025-03-19 09:05:08 -04:00
yoannchb-pro f2e1ec8ed6 Merge pull request #46 from dmunozv04/main
Add baseURL and max Tokens config
2025-03-19 08:53:03 -04:00
yoannchb-pro 98f22e9056 Merge pull request #53 from yoannchb-pro/main
Merge main into dev
2025-03-19 08:51:25 -04:00
yoannchb-pro 79a75fee89 Create FUNDING.yml 2025-02-03 14:29:50 -05:00
dmunozv04 476559188f Add baseURL and max Tokens config 2025-02-03 20:21:29 +01:00
77 changed files with 8238 additions and 2728 deletions
-21
View File
@@ -1,21 +0,0 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'prettier'
],
ignorePatterns: ['node_modules/'],
overrides: [
{
files: ['extension/popup/*.js', 'src/**/*.ts'],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'no-constant-condition': 'off'
}
}
]
};
+1
View File
@@ -0,0 +1 @@
buy_me_a_coffee: yoannchbpro
+15
View File
@@ -1,5 +1,20 @@
# 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
+32 -5
View File
@@ -2,7 +2,7 @@
href="https://www.flaticon.com/free-icons/mortarboard" target="_blank" rel="noopener noreferrer"
title="Mortarboard icons created by itim2101 - Flaticon" ><img src="./extension/icon.png" alt="Mortarboard icons created by itim2101 - Flaticon" width="150" style="display:block; margin:auto;"></a></p>
# MoodleGPT 1.1.2
# 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.
@@ -12,15 +12,17 @@ Find the extension on the Chrome Webstore right [here](https://chrome.google.com
## Summary
- [MoodleGPT 1.1.2](#moodlegpt-112)
- [SparkAssist 2.0.0](#sparkassist-200)
- [Chrome Webstore](#chrome-webstore)
- [Summary](#summary)
- [Disclaimer !](#disclaimer-)
- [Donate](#donate)
- [Update](#update)
- [Set up](#set-up)
- [Mode](#mode)
- [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)
@@ -61,6 +63,17 @@ See the [changelog](./CHANGELOG.md) to see every updates !
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">
@@ -72,7 +85,7 @@ Go to <b>"Manage my extensions"</b> on your browser, then click on <b>"Load unpa
- <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
## Options
<p align="center">
<img src="./assets/settings.png" alt="Popup" width="300">
@@ -114,10 +127,24 @@ Person 2 | Yann  | 19/01/2000 | no
## Supported questions type
### Select
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)
![Calculated](./assets/equations.gif)
### Select (Missing Words / Gapselect)
![Select](./assets/select.gif)
### 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
![Order](./assets/order.gif)
+19 -1
View File
@@ -1 +1,19 @@
# TODO
# 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.
+32
View File
@@ -0,0 +1,32 @@
const js = require('@eslint/js');
const tsParser = require('@typescript-eslint/parser');
const tsPlugin = require('@typescript-eslint/eslint-plugin');
const prettierConfig = require('eslint-config-prettier');
const tseslint = require('typescript-eslint');
module.exports = [
{
ignores: ['**/node_modules/*', '**/dist/*', '**/*.js']
},
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ['**/*.ts'],
languageOptions: {
parser: tsParser,
ecmaVersion: 'latest',
sourceType: 'module'
},
plugins: {
'@typescript-eslint': tsPlugin
},
rules: {
...tsPlugin.configs['eslint-recommended'].rules,
...tsPlugin.configs.recommended.rules,
...prettierConfig.rules,
'@typescript-eslint/no-explicit-any': 'off'
}
}
];
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 480 KiB

+3 -5
View File
@@ -1,21 +1,19 @@
{
"manifest_version": 3,
"name": "MoodleGPT",
"version": "1.1.2",
"description": "Hidden chat-gpt for your moodle quiz",
"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:///*"],
+10 -143
View File
@@ -2,158 +2,25 @@
<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>
<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"
integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
</head>
<body>
<main>
<div class="line center" style="margin-bottom: 0.5rem">
<a
target="_blank"
rel="noopener noreferrer"
href="https://www.flaticon.com/free-icons/mortarboard"
title="Mortarboard icons created by itim2101 - Flaticon"
>
<img src="../icon.png" alt="icon" />
</a>
<div class="col center title">
<h1>MoodleGPT</h1>
<p id="version"></p>
</div>
</div>
<!-- SETTINGS -->
<div class="settings" id="settings">
<div class="line center">
<label for="apiKey" class="textLabel">Api Key<span class="required">*</span>:</label>
<input id="apiKey" type="text" />
</div>
<div class="line center">
<label for="model" class="textLabel">GPT Model<span class="required">*</span>:</label>
<input type="text" id="model" list="models" />
<datalist id="models"></datalist>
<i id="check-model" title="Test" style="cursor: pointer" class="fa-solid fa-play"></i>
</div>
</div>
<!-- ADVANCED SETTINGS -->
<div class="settings" id="advanced-settings" style="display: none">
<div class="line center">
<label for="code" class="textLabel">Code:</label>
<input id="code" type="text" />
</div>
</div>
<!-- SWITCH SETTINGS MODE -->
<div class="line center mt">
<a id="switch-settings" href="#">Advanced settings</a>
</div>
<div class="line center-y mt">
<i class="fa-solid fa-robot"></i>
<p>Mode:</p>
</div>
<div class="line">
<ul id="mode" class="line center">
<li><button value="autocomplete">autocomplete</button></li>
<li>
<button value="clipboard" class="not-selected">clipboard</button>
</li>
<li>
<button value="question-to-answer" class="not-selected">question to answer</button>
</li>
</ul>
</div>
<div class="line mt center-y">
<i class="fa-solid fa-gear"></i>
<p>Options:</p>
</div>
<div class="line center" style="gap: 2rem">
<div class="col">
<div class="line">
<input id="logs" type="checkbox" />
<label for="logs">Console logs</label>
</div>
<div class="line">
<input id="title" type="checkbox" checked />
<label for="title">Title indication</label>
</div>
<div class="line">
<input id="cursor" type="checkbox" checked />
<label for="cursor">Cursor indication</label>
</div>
<div class="line">
<input id="timeout" type="checkbox" checked />
<label for="timeout">Request timeout</label>
</div>
</div>
<div class="col">
<div class="line">
<input id="typing" type="checkbox" />
<label for="typing">Typing effect</label>
</div>
<div class="line">
<input id="mouseover" type="checkbox" />
<label for="mouseover">Mouseover effect</label>
</div>
<div class="line">
<input id="infinite" type="checkbox" />
<label for="infinite">Infinite try</label>
</div>
<div class="line">
<input id="history" type="checkbox" />
<label for="history">Save history</label>
</div>
<!-- This option is only showed if the current version of the model support it -->
<div class="line" id="includeImages-line" style="display: none">
<input id="includeImages" type="checkbox" />
<label for="includeImages">Include images</label>
</div>
</div>
</div>
<p id="message">{Message}</p>
<div class="line center">
<button class="save">Save</button>
</div>
<div class="line center">
<a
class="donate"
href="https://www.buymeacoffee.com/yoannchbpro"
target="_blank"
rel="noopener noreferrer"
>
Donate
</a>
<a
href="https://github.com/yoannchb-pro/MoodleGPT"
target="_blank"
rel="noopener noreferrer"
>
See the documentation
</a>
<a
href="https://github.com/yoannchb-pro/MoodleGPT/issues"
target="_blank"
rel="noopener noreferrer"
>
Need Help
</a>
</div>
</main>
<div id="root"></div>
</body>
</html>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1031 -133
View File
File diff suppressed because it is too large Load Diff
+78
View File
@@ -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, wed 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 ChatGPTs 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.
+2079 -1907
View File
File diff suppressed because it is too large Load Diff
+27 -16
View File
@@ -1,11 +1,12 @@
{
"name": "moodlegpt",
"version": "1.1.2",
"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": "npm run prettier && npm run lint && npm run fastBuild",
"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",
"lint": "eslint . --ext .ts,.tsx",
"prettier": "prettier --write ."
},
"repository": {
@@ -26,19 +27,29 @@
},
"homepage": "https://github.com/yoannchb-pro/MoodleGPT#readme",
"devDependencies": {
"@rollup/plugin-commonjs": "^28.0.2",
"@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.2",
"@types/chrome": "^0.0.294",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"openai": "^4.78.1",
"prettier": "^3.2.5",
"rollup": "^4.13.0",
"@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.4.2"
"tailwindcss": "^3.4.19",
"typescript": "^5.8.3",
"typescript-eslint": "^8.38.0"
},
"dependencies": {
"preact": "^10.29.1"
}
}
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};
+311
View File
@@ -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&amp;qubaid=124&amp;qid=1621&amp;slot=10&amp;checksum=7baed22ee2b738ef71503b27cd982b7d&amp;sesskey=iGNCxptJmv&amp;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&amp;returnurl=%2Fmod%2Fquiz%2Fattempt.php%3Fattempt%3D88%26cmid%3D1210%23question-124-10&amp;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&amp;qubaid=124&amp;qid=1620&amp;slot=9&amp;checksum=6f196762f980a0b2c9f887ed255c8a24&amp;sesskey=iGNCxptJmv&amp;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&amp;returnurl=%2Fmod%2Fquiz%2Fattempt.php%3Fattempt%3D88%26cmid%3D1210%23question-124-9&amp;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>&nbsp;</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&amp;qubaid=124&amp;qid=1618&amp;slot=8&amp;checksum=b7d586e320edea342963eb06ad05faa0&amp;sesskey=iGNCxptJmv&amp;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&amp;returnurl=%2Fmod%2Fquiz%2Fattempt.php%3Fattempt%3D88%26cmid%3D1210%23question-124-8&amp;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.&nbsp;
</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>&nbsp;</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>
+232
View File
@@ -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&amp;qubaid=124&amp;qid=1642&amp;slot=16&amp;checksum=2b637f444fcfdda7191b7f0620cd7533&amp;sesskey=xrcdi6NRF8&amp;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&amp;returnurl=%2Fmod%2Fquiz%2Fattempt.php%3Fattempt%3D88%26cmid%3D1210%23question-124-16&amp;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"
>&nbsp;<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"
>&nbsp;<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"
>&nbsp;<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"
>&nbsp;<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"
>&nbsp;<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>
+753
View File
@@ -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&amp;qubaid=125&amp;qid=3703&amp;slot=6&amp;checksum=3aadede34be020a7858cc02258ea5877&amp;sesskey=MRh4T7j2fU&amp;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&amp;returnurl=%2Fmod%2Fquiz%2Fattempt.php%3Fattempt%3D89%26cmid%3D1655%23question-125-6&amp;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='&lt;!DOCTYPE html&gt;&lt;html&gt;&lt;head&gt;&lt;meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /&gt;&lt;/head&gt;&lt;body id="tinymce" class="mce-content-body " data-id="q125:6_answer_id" aria-label="Rich text area. Press ALT-0 for help."&gt;&lt;br&gt;&lt;/body&gt;&lt;/html&gt;'
></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&amp;utm_source=tiny&amp;utm_medium=referral&amp;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>
+66
View File
@@ -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&amp;qubaid=125&amp;qid=3704&amp;slot=7&amp;checksum=4717bc6928ebf1e9a66ab68e7e595899&amp;sesskey=MRh4T7j2fU&amp;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&amp;returnurl=%2Fmod%2Fquiz%2Fattempt.php%3Fattempt%3D89%26cmid%3D1655%23question-125-7&amp;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>
+176
View File
@@ -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&amp;qubaid=124&amp;qid=1581&amp;slot=3&amp;checksum=35d5356ab101c7fac69fb355c4f95306&amp;sesskey=xrcdi6NRF8&amp;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&amp;returnurl=%2Fmod%2Fquiz%2Fattempt.php%3Fattempt%3D88%26cmid%3D1210%23question-124-3&amp;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>
+121
View File
@@ -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&amp;qubaid=125&amp;qid=3700&amp;slot=2&amp;checksum=c534dfa95146a052b3bdd2d1dd744a46&amp;sesskey=MRh4T7j2fU&amp;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&amp;returnurl=%2Fmod%2Fquiz%2Fattempt.php%3Fattempt%3D89%26cmid%3D1655%23question-125-2&amp;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>&nbsp;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>
&nbsp;Seeking support from friends, mentors, or professional resources when facing
difficulties.&nbsp;
</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>
+63
View File
@@ -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&amp;qubaid=125&amp;qid=3702&amp;slot=5&amp;checksum=2c2029ed58598b4af63b0db74bebcaab&amp;sesskey=MRh4T7j2fU&amp;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&amp;returnurl=%2Fmod%2Fquiz%2Fattempt.php%3Fattempt%3D89%26cmid%3D1655%23question-125-5&amp;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>
+821
View File
@@ -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&amp;qubaid=124&amp;qid=1773&amp;slot=18&amp;checksum=5dbbc1b24217228df046bd34749543fb&amp;sesskey=djxMUQR7fp&amp;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&amp;returnurl=%2Fmod%2Fquiz%2Fattempt.php%3Fattempt%3D88%26cmid%3D1210%23question-124-18&amp;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&amp;qubaid=124&amp;qid=1600&amp;slot=19&amp;checksum=2379a29476cf85083f91b4d6d99526ea&amp;sesskey=djxMUQR7fp&amp;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&amp;returnurl=%2Fmod%2Fquiz%2Fattempt.php%3Fattempt%3D88%26cmid%3D1210%23question-124-19&amp;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&amp;qubaid=124&amp;qid=1774&amp;slot=20&amp;checksum=5bec9067451e2e2635cdb8165c63bac5&amp;sesskey=djxMUQR7fp&amp;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&amp;returnurl=%2Fmod%2Fquiz%2Fattempt.php%3Fattempt%3D88%26cmid%3D1210%23question-124-20&amp;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&nbsp; 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>
+52
View File
@@ -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
+184
View File
@@ -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&amp;qubaid=124&amp;qid=1643&amp;slot=17&amp;checksum=c75ebf63f5c7bfcfaceaba8e41013eed&amp;sesskey=xrcdi6NRF8&amp;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&amp;returnurl=%2Fmod%2Fquiz%2Fattempt.php%3Fattempt%3D88%26cmid%3D1210%23question-124-17&amp;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.&nbsp;</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="">&nbsp;</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 &nbsp;<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="">&nbsp;</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="">&nbsp;</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 &nbsp;<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="">&nbsp;</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="">&nbsp;</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="">&nbsp;</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="">&nbsp;</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>
+64
View File
@@ -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&amp;qubaid=125&amp;qid=3701&amp;slot=4&amp;checksum=e963ee069c13bab0bb84c37d94edf69b&amp;sesskey=MRh4T7j2fU&amp;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&amp;returnurl=%2Fmod%2Fquiz%2Fattempt.php%3Fattempt%3D89%26cmid%3D1655%23question-125-4&amp;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>
+120
View File
@@ -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&amp;qubaid=125&amp;qid=3699&amp;slot=1&amp;checksum=e0f716344fc5402f32878bcbf0aa8342&amp;sesskey=MRh4T7j2fU&amp;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&amp;returnurl=%2Fmod%2Fquiz%2Fattempt.php%3Fattempt%3D89%26cmid%3D1655%23&amp;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 peoples 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>
+73
View File
@@ -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&amp;qubaid=125&amp;qid=3698&amp;slot=3&amp;checksum=57228a123cfb296aa6bd9357997dfbed&amp;sesskey=MRh4T7j2fU&amp;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&amp;returnurl=%2Fmod%2Fquiz%2Fattempt.php%3Fattempt%3D89%26cmid%3D1655%23question-125-3&amp;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>
+11 -3
View File
@@ -1,7 +1,7 @@
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 = [
@@ -17,13 +17,21 @@ module.exports = [
},
{
input: './src/popup/index.ts',
input: './src/popup/index.tsx',
output: {
file: './extension/popup/popup.js',
format: 'umd',
sourcemap: true
},
onwarn() {},
plugins: [nodeResolve(), ts(config), terser()]
plugins: [
replace({
'process.env.NODE_ENV': JSON.stringify('production'),
preventAssignment: true
}),
nodeResolve(),
ts(config),
terser()
]
}
];
+3 -1
View File
@@ -58,7 +58,9 @@ function setUpMoodleGpt(config: Config) {
const inputTypeQuery = ['checkbox', 'radio', 'text', 'number']
.map(e => `input[type="${e}"]`)
.join(',');
const inputQuery = inputTypeQuery + ', textarea, select, [contenteditable], .qtype_essay_editor';
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
+2
View File
@@ -20,6 +20,8 @@ function createAndNormalizeQuestion(questionContainer: HTMLElement) {
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');
+50 -33
View File
@@ -2,6 +2,8 @@ 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 = {
@@ -11,18 +13,12 @@ type History = {
history: ChatCompletionMessageParam[];
};
const INSTRUCTION: string = `
Act as a quiz solver for the best notation with the following rules:
- If no answer(s) are given, answer the statement as usual without following the other rules, providing the most detailed, complete and precise explanation.
- But for the calculation provide this format 'result: <result of the equation>'
- For 'put in order' questions, maintain the answer in the order as presented in the question but assocy the correct order to it by usin this format '<order>:<answer 1>\n<order>:<answer 2>', ignore other rules.
- Always reply in the format: '<answer 1>\n<answer 2>\n...'.
- Retain only the correct answer(s).
- Maintain the same order for the answers as in the text.
- Retain all text from the answer with its description, content or definition.
- Only provide answers that exactly match the given answer in the text.
- The question always has the correct answer(s), so you should always provide an answer.
- Always respond in the same language as the user's question.
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 = {
@@ -37,7 +33,8 @@ const SYSTEM_INSTRUCTION_MESSAGE = {
async function getContent(
config: Config,
questionElement: HTMLElement,
question: string
// We provide the structured JSON if parsed, otherwise fallback to normalized text string
textContent: string
): Promise<ChatCompletionUserMessageParam['content']> {
const imagesElements = questionElement.querySelectorAll('img');
@@ -46,7 +43,7 @@ async function getContent(
!isGPTModelGreaterOrEqualTo4(config.model) ||
imagesElements.length === 0
) {
return question;
return textContent;
}
const contentWithImages: ChatCompletionUserMessageParam['content'] = [];
@@ -67,7 +64,7 @@ async function getContent(
contentWithImages.push({
type: 'text',
text: question
text: textContent
});
return contentWithImages;
@@ -126,34 +123,54 @@ async function getContentWithHistory(
): Promise<{
messages: [typeof SYSTEM_INSTRUCTION_MESSAGE, ...ChatCompletionMessageParam[]];
saveResponse?: (response: string) => void;
query: MoodleQuestionQuery | null;
}> {
const content = await getContent(config, questionElement, question);
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 };
if (!config.history) return { messages: [SYSTEM_INSTRUCTION_MESSAGE, message] };
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);
}
}
};
};
let history: History;
if (!config.history) {
return buildResult([]);
}
const pastHistory: History | null = loadPastHistory();
const newHistory: History = createNewHistory();
if (pastHistory === null || !areHistoryFromSameQuiz(pastHistory, newHistory)) {
history = newHistory;
return buildResult(newHistory.history);
} else {
history = pastHistory;
return buildResult(pastHistory.history);
}
return {
messages: [SYSTEM_INSTRUCTION_MESSAGE, ...history.history, message],
saveResponse(response: string) {
// Register the conversation
if (config.history) {
history.history.push(message);
history.history.push({ role: 'assistant', content: response });
sessionStorage.moodleGPTHistory = JSON.stringify(history);
}
}
};
}
export default getContentWithHistory;
+47 -20
View File
@@ -1,9 +1,10 @@
import type Config from '../types/config';
import type GPTAnswer from '../types/gpt-answer';
import normalizeText from 'background/utils/normalize-text';
import getContentWithHistory from './get-content-with-history';
import OpenAI from 'openai';
import { fixeO1 } from '../utils/fixe-o1';
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
@@ -17,41 +18,67 @@ async function getChatGPTResponse(
question: string
): Promise<GPTAnswer> {
const controller = new AbortController();
const timeoutControler = setTimeout(() => controller.abort(), 20 * 1000);
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
// 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 req = await client.chat.completions.create(
fixeO1(config.model, {
model: config.model,
messages: contentHandler.messages,
const questionType = contentHandler.query
? contentHandler.query.question_type
: MoodleQuestionType.UNKNOWN;
const targetSchema: MoodleQuestionResponse =
questionType !== MoodleQuestionType.UNKNOWN ? QuestionSchemas[questionType] : undefined;
temperature: 0.1, // Controls the randomness of the generated responses, with lower values producing more deterministic and predictable outputs. With set to 0.1 instead of 0 for more creativity.
top_p: 0.6, // Determines the diversity of the generated responses
presence_penalty: 0, // Encourages the model to introduce new concepts by penalizing words that have already appeared in the text.
max_tokens: 2000 // Maximum length of the response,
}),
{ signal: config.timeout ? controller.signal : null }
);
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 response = req.choices[0].message.content ?? '';
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(response);
if (typeof contentHandler.saveResponse === 'function') {
contentHandler.saveResponse(rawResponse);
}
return {
question,
response,
normalizedResponse: normalizeText(response)
questionQuery: contentHandler.query,
response: structuredResponse,
rawResponse: rawResponse
};
}
@@ -8,6 +8,10 @@ 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;
@@ -31,6 +35,10 @@ function autoCompleteMode(props: Props) {
handleContentEditable,
handleTextbox,
handleNumber,
handleMatch,
handleGapSelect,
handleDragDropText,
handleOrdering,
handleSelect,
handleRadio,
handleCheckbox
@@ -1,32 +0,0 @@
import type GPTAnswer from '../../types/gpt-answer';
type Props = {
questionElement: HTMLElement;
gptAnswer: GPTAnswer;
removeListener: () => void;
};
/**
* Question to answer mode:
* Simply turn the question into the answer by clicking on it
* @param props
*/
function questionToAnswerMode(props: Props) {
const questionElement = props.questionElement;
props.removeListener();
const questionBackup = questionElement.innerHTML ?? '';
questionElement.innerHTML = props.gptAnswer.response;
questionElement.style.whiteSpace = 'pre-wrap';
// To go back to the question / answer
questionElement.addEventListener('click', function () {
const contentIsResponse = questionElement.innerHTML === props.gptAnswer.response;
questionElement.style.whiteSpace = contentIsResponse ? 'initial' : 'pre-wrap';
questionElement.innerHTML = contentIsResponse ? questionBackup : props.gptAnswer.response;
});
}
export default questionToAnswerMode;
+223
View File
@@ -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;
}
+8 -3
View File
@@ -29,18 +29,23 @@ function handleAtto(
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 >= gptAnswer.response.length) {
if (event.key === 'Backspace' || index >= answerText.length) {
iframe.contentWindow!.removeEventListener('keydown', eventHandler);
return;
}
// Append text one character at a time
const textNode = document.createTextNode(gptAnswer.response.charAt(index++));
const textNode = document.createTextNode(answerText.charAt(index++));
textContainer.appendChild(textNode);
// Move the cursor after the last character
@@ -58,7 +63,7 @@ function handleAtto(
iframe.contentWindow.addEventListener('keydown', eventHandler);
} else {
textContainer.textContent += gptAnswer.response;
textContainer.textContent += answerText;
}
return true;
+40 -16
View File
@@ -3,6 +3,7 @@ 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
@@ -22,29 +23,52 @@ function handleCheckbox(
return false;
}
const corrects = gptAnswer.normalizedResponse.split('\n');
const possibleAnswers = Array.from(inputList)
.map(inp => ({
element: inp as HTMLInputElement,
value: normalizeText(inp?.parentElement?.textContent ?? '')
}))
.filter(obj => obj.value !== '');
// Find the best answers elements
const correctElements: Set<HTMLInputElement> = new Set();
for (const correct of corrects) {
const bestAnswer = pickBestReponse(correct, possibleAnswers);
if (config.logs && bestAnswer.value) {
Logs.bestAnswer(bestAnswer.value, bestAnswer.similarity);
// 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');
correctElements.add(bestAnswer.element as HTMLInputElement);
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 element of possibleAnswers.map(e => e.element)) {
for (const inp of Array.from(inputList)) {
const element = inp as HTMLInputElement;
const needAction =
(element.checked && !correctElements.has(element)) ||
(!element.checked && correctElements.has(element));
+121 -1
View File
@@ -1,6 +1,7 @@
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
@@ -9,7 +10,126 @@ import titleIndications from 'background/utils/title-indications';
*/
function handleClipboard(config: Config, gptAnswer: GPTAnswer) {
if (config.title) titleIndications('Copied to clipboard');
navigator.clipboard.writeText(gptAnswer.response);
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;
@@ -27,18 +27,23 @@ function handleContentEditable(
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 >= gptAnswer.response.length) {
if (event.key === 'Backspace' || index >= answerText.length) {
input.removeEventListener('keydown', eventHandler);
return;
}
input.textContent = gptAnswer.response.slice(0, ++index);
input.textContent = answerText.slice(0, ++index);
// Put the cursor at the end of the typed text
input.focus();
@@ -54,7 +59,7 @@ function handleContentEditable(
input.addEventListener('keydown', eventHandler);
} else {
input.textContent = gptAnswer.response;
input.textContent = answerText;
}
return true;
+44
View File
@@ -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;
}
+36
View File
@@ -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;
}
+5 -1
View File
@@ -22,7 +22,11 @@ function handleNumber(
return false;
}
const number = gptAnswer.normalizedResponse.match(/\d+([,.]\d+)?/gi)?.[0]?.replace(',', '.');
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;
+52
View File
@@ -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;
}
+55 -16
View File
@@ -3,6 +3,11 @@ 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
@@ -22,26 +27,60 @@ function handleRadio(
return false;
}
const possibleAnswers = Array.from(inputList)
.map(inp => ({
element: inp,
value: normalizeText(inp?.parentElement?.textContent ?? '')
}))
.filter(obj => obj.value !== '');
let correctInput: HTMLInputElement | null = null;
const bestAnswer = pickBestReponse(gptAnswer.normalizedResponse, possibleAnswers);
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;
if (config.logs && bestAnswer.value) {
Logs.bestAnswer(bestAnswer.value, bestAnswer.similarity);
// 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;
}
const correctInput = bestAnswer.element as HTMLInputElement;
if (config.mouseover) {
correctInput.addEventListener('mouseover', () => correctInput.click(), {
once: true
});
} else {
correctInput.click();
if (correctInput) {
if (config.mouseover) {
correctInput.addEventListener('mouseover', () => (correctInput as HTMLInputElement).click(), {
once: true
});
} else {
correctInput.click();
}
}
return true;
+6 -1
View File
@@ -18,7 +18,12 @@ function handleSelect(
): boolean {
if (inputList.length === 0 || inputList[0].tagName !== 'SELECT') return false;
const corrects = gptAnswer.normalizedResponse.split('\n');
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);
+8 -3
View File
@@ -22,23 +22,28 @@ function handleTextbox(
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 >= gptAnswer.response.length) {
if ((<KeyboardEvent>event).key === 'Backspace' || index >= answerText.length) {
input.removeEventListener('keydown', eventHandler);
return;
}
input.value = gptAnswer.response.slice(0, ++index);
input.value = answerText.slice(0, ++index);
};
input.addEventListener('keydown', eventHandler);
} else {
input.value = gptAnswer.response;
input.value = answerText;
}
return true;
-8
View File
@@ -3,7 +3,6 @@ import Logs from 'background/utils/logs';
import getChatGPTResponse from './get-response';
import createAndNormalizeQuestion from './create-question';
import clipboardMode from './modes/clipboard';
import questionToAnswerMode from './modes/question-to-answer';
import autoCompleteMode from './modes/autocomplete';
type Props = {
@@ -56,13 +55,6 @@ async function reply(props: Props): Promise<void> {
removeListener: props.removeListener
});
break;
case 'question-to-answer':
questionToAnswerMode({
gptAnswer,
questionElement: props.questionElement,
removeListener: props.removeListener
});
break;
case 'autocomplete':
autoCompleteMode({
config: props.config,
@@ -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
};
+5 -1
View File
@@ -11,7 +11,11 @@ type Config = {
timeout?: boolean;
history?: boolean;
includeImages?: boolean;
mode?: 'autocomplete' | 'question-to-answer' | 'clipboard';
mode?: 'autocomplete' | 'clipboard';
baseURL?: string;
projectId?: string;
maxTokens?: number;
timeoutValue?: number;
};
export default Config;
+5 -3
View File
@@ -1,7 +1,9 @@
import { MoodleQuestionQuery, MoodleQuestionResponse } from './question-types';
type GPTAnswer = {
question: string;
response: string;
normalizedResponse: string;
questionQuery: MoodleQuestionQuery | null;
response: MoodleQuestionResponse | null;
rawResponse: string; // Keep the original just in case or for logging/unknown
};
export default GPTAnswer;
+155
View File
@@ -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;
@@ -6,13 +6,8 @@ import { ChatCompletionCreateParamsNonStreaming } from 'openai/resources/chat/co
* @param data
* @returns
*/
export function fixeO1(model: string, data: ChatCompletionCreateParamsNonStreaming) {
if (!model.startsWith('o1')) return data;
if (data.max_tokens) {
data.max_completion_tokens = data.max_tokens;
delete data.max_tokens;
}
export function fixeO(model: string, data: ChatCompletionCreateParamsNonStreaming) {
if (model.search(/^o\d+/gi) === -1) return data;
if (data.temperature) delete data.temperature;
-1
View File
@@ -22,7 +22,6 @@ class Logs {
static response(gptAnswer: GPTAnswer) {
console.log('Original:\n' + gptAnswer.response);
console.log('Normalized:\n' + gptAnswer.normalizedResponse);
}
}
@@ -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>
);
}
+112
View File
@@ -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>
);
}
+70
View File
@@ -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>
);
}
+46
View File
@@ -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>
</>
);
}
+47
View File
@@ -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>
);
}
+86
View File
@@ -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>
);
}
-15
View File
@@ -1,15 +0,0 @@
export const globalData = { actualMode: 'autocomplete' };
export const inputsCheckbox = [
'logs',
'title',
'cursor',
'typing',
'mouseover',
'infinite',
'timeout',
'history',
'includeImages'
];
export const mode = document.querySelector('#mode')!;
export const modes = mode.querySelectorAll('button')!;
-78
View File
@@ -1,78 +0,0 @@
import OpenAI from 'openai';
import { isCurrentVersionSupportingImages, showMessage } from './utils';
const apiKeySelector: HTMLInputElement = document.querySelector('#apiKey')!;
const inputModel: HTMLInputElement = document.querySelector('#model')!;
const modelsList: HTMLElement = document.querySelector('#models')!;
const imagesIntegrationLine: HTMLInputElement = document.querySelector('#includeImages-line')!;
/**
* Check if the gpt version is at least 4 to show the option 'Include images'
*/
export function checkCanIncludeImages() {
const version = inputModel.value;
if (isCurrentVersionSupportingImages(version)) {
imagesIntegrationLine.style.display = 'flex';
} else {
imagesIntegrationLine.style.display = 'none';
}
}
inputModel.addEventListener('input', checkCanIncludeImages);
// We populate the datalist of the chatgpt model
export async function populateDatalistWithGptVersions() {
const apiKey = apiKeySelector.value?.trim();
if (!apiKey) return;
inputModel.innerHTML = '';
try {
const client = new OpenAI({
apiKey,
dangerouslyAllowBrowser: true
});
const rep = await client.models.list();
const models = rep.data.filter(
model =>
model.id.startsWith('gpt') || model.id.startsWith('o1') || model.id.startsWith('chatgpt')
);
models.sort((a, b) => b.id.localeCompare(a.id)); // we sort the model to get the best chatgpt version first
for (const model of models) {
const opt = document.createElement('option');
opt.value = model.id;
opt.textContent = model.id;
modelsList.appendChild(opt);
}
checkCanIncludeImages();
} catch (err: any) {
console.error(err);
showMessage({ msg: err, isError: true });
}
}
inputModel.addEventListener('focus', populateDatalistWithGptVersions);
export async function checkModel() {
const model = inputModel.value?.trim();
const apiKey = apiKeySelector.value?.trim();
try {
const client = new OpenAI({ apiKey, dangerouslyAllowBrowser: true });
await client.chat.completions.create({
model,
messages: [{ role: 'user', content: 'reply just pong' }]
});
showMessage({ msg: 'The model is valid!' });
} catch (err: any) {
showMessage({ msg: err, isError: true });
}
}
const checkModelBtn: HTMLElement = document.querySelector('#check-model')!;
checkModelBtn.addEventListener('click', checkModel);
+48
View File
@@ -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 };
}
+113
View File
@@ -0,0 +1,113 @@
import { useState } from 'preact/hooks';
import OpenAI from 'openai';
export function useModel(apiKey?: string, baseURL?: string, projectId?: string) {
const [models, setModels] = useState<string[]>([]);
const [error, setError] = useState<string | null>(null);
const isCurrentVersionSupportingImages = (version?: string) => {
if (!version) return false;
const versionNumber = version.match(/gpt-(\d+)/);
if (!versionNumber?.[1]) {
return false;
}
return Number(versionNumber[1]) >= 4;
};
const fetchModels = async () => {
if (!apiKey) return;
try {
const client = new OpenAI({
apiKey,
baseURL: baseURL || undefined,
project: projectId || undefined,
dangerouslyAllowBrowser: true
});
const rep = await client.models.list();
const filteredModels = rep.data.filter(
model =>
model.id.startsWith('gpt') ||
model.id.search(/^o\d+/gi) !== -1 ||
model.id.startsWith('chatgpt')
);
filteredModels.sort((a, b) => b.id.localeCompare(a.id));
setModels(filteredModels.map(m => m.id));
setError(null);
} catch (err: any) {
console.error(err);
setError(err.message || String(err));
}
};
const validateModel = async (model: string, maxTokens?: number) => {
if (!apiKey || !model) return { success: false, error: 'API Key and Model are required' };
try {
const client = new OpenAI({
apiKey,
baseURL: baseURL || undefined,
project: projectId || undefined,
dangerouslyAllowBrowser: true
});
const completion = await client.chat.completions.create({
model,
messages: [
{
role: 'user',
content:
'reply just pong, set success to true, and provide a random number between 1 and 100.'
}
],
max_completion_tokens: maxTokens || 2000,
response_format: {
type: 'json_schema',
json_schema: {
name: 'model_test',
strict: true,
schema: {
type: 'object',
properties: {
reply: { type: 'string', description: 'The text reply' },
success: { type: 'boolean', description: 'Always true' },
data: {
type: 'object',
properties: {
number: { type: 'integer' }
},
required: ['number'],
additionalProperties: false
}
},
required: ['reply', 'success', 'data'],
additionalProperties: false
}
}
}
});
const content = completion.choices[0]?.message?.content;
if (!content) {
throw new Error('No content returned from the model.');
}
const parsed = JSON.parse(content);
if (
typeof parsed.reply !== 'string' ||
typeof parsed.success !== 'boolean' ||
typeof parsed.data !== 'object' ||
typeof parsed.data.number !== 'number'
) {
throw new Error('Model did not follow the JSON schema correctly.');
}
return { success: true, message: 'The model is valid and supports structured outputs!' };
} catch (err: any) {
return { success: false, error: err.message || String(err) };
}
};
return { models, fetchModels, validateModel, error, isCurrentVersionSupportingImages };
}
-87
View File
@@ -1,87 +0,0 @@
import { globalData, inputsCheckbox, modes } from './data';
import { checkCanIncludeImages } from './gpt-version';
import { handleModeChange } from './mode-handler';
import './version';
import './settings';
import { showMessage } from './utils';
const saveBtn = document.querySelector('.save')!;
// inputs id
const inputsText = ['apiKey', 'code', 'model'];
// Save the configuration
saveBtn.addEventListener('click', function () {
const [apiKey, code, model] = inputsText.map(selector =>
(document.querySelector('#' + selector) as HTMLInputElement).value.trim()
);
const [logs, title, cursor, typing, mouseover, infinite, timeout, history, includeImages] =
inputsCheckbox.map(selector => {
const element: HTMLInputElement = document.querySelector('#' + selector)!;
return element.checked && element.parentElement!.style.display !== 'none';
});
if (!apiKey || !model) {
showMessage({ msg: 'Please complete all the form', isError: true });
return;
}
if (code.length > 0 && code.length < 2) {
showMessage({
msg: 'The code should at least contain 2 characters',
isError: true
});
return;
}
chrome.storage.sync.set({
moodleGPT: {
apiKey,
code,
model,
logs,
title,
cursor,
typing,
mouseover,
infinite,
timeout,
history,
includeImages,
mode: globalData.actualMode
}
});
showMessage({ msg: 'Configuration saved' });
});
// we load back the configuration
chrome.storage.sync.get(['moodleGPT']).then(function (storage) {
const config = storage.moodleGPT;
if (config) {
if (config.mode) {
globalData.actualMode = config.mode;
for (const mode of modes) {
if (mode.value === config.mode) {
mode.classList.remove('not-selected');
} else {
mode.classList.add('not-selected');
}
}
}
inputsText.forEach(key =>
config[key]
? ((document.querySelector('#' + key) as HTMLInputElement).value = config[key])
: null
);
inputsCheckbox.forEach(
key => ((document.querySelector('#' + key) as HTMLInputElement).checked = config[key] || '')
);
}
handleModeChange();
checkCanIncludeImages();
});
+7
View File
@@ -0,0 +1,7 @@
import { render } from 'preact';
import { App } from './components/App';
const root = document.getElementById('root');
if (root) {
render(<App />, root);
}
-43
View File
@@ -1,43 +0,0 @@
import { globalData, inputsCheckbox, modes } from './data';
// input to don't take in consideration
const toExcludes = ['includeImages'];
// inputs id that need to be disabled for a specific mode
const disabledForThisMode: Record<string, string[]> = {
autocomplete: [],
clipboard: ['typing', 'mouseover'],
'question-to-answer': ['typing', 'infinite', 'mouseover']
};
/**
* Handle when a mode change to show specific input or to hide them
*/
export function handleModeChange() {
const needDisable = disabledForThisMode[globalData.actualMode];
const dontNeedDisable = inputsCheckbox.filter(
input => !needDisable.includes(input) && !toExcludes.includes(input)
);
for (const id of needDisable) {
document.querySelector('#' + id)!.parentElement!.style.display = 'none';
}
for (const id of dontNeedDisable) {
document.querySelector('#' + id)!.parentElement!.style.display = '';
}
}
// Mode hanlder
for (const button of modes) {
button.addEventListener('click', function () {
const value = button.value;
globalData.actualMode = value;
for (const mode of modes) {
if (mode.value !== value) {
mode.classList.add('not-selected');
} else {
mode.classList.remove('not-selected');
}
}
handleModeChange();
});
}
-21
View File
@@ -1,21 +0,0 @@
const settings: HTMLElement = document.querySelector('#settings')!;
const advencedSettings: HTMLElement = document.querySelector('#advanced-settings')!;
const switchSettings: HTMLLinkElement = document.querySelector('#switch-settings')!;
export function switchSettingsMode() {
const isAdvancedSettings = advencedSettings.style.display === 'flex';
if (isAdvancedSettings) {
settings.style.display = 'flex';
advencedSettings.style.display = 'none';
switchSettings.textContent = 'Advanced settings';
} else {
settings.style.display = 'none';
advencedSettings.style.display = 'flex';
switchSettings.textContent = 'Go back to settings';
}
}
switchSettings.addEventListener('click', function (event) {
event.preventDefault();
switchSettingsMode();
});
+61
View File
@@ -0,0 +1,61 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
body {
width: 380px;
min-height: 500px;
}
}
@layer components {
.toggle-switch {
position: relative;
display: inline-block;
width: 36px;
height: 20px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: theme('colors.input.bg');
border: 1px solid theme('colors.input.border');
transition: 0.4s;
border-radius: 20px;
}
.slider:before {
position: absolute;
content: '';
height: 14px;
width: 14px;
left: 2px;
bottom: 2px;
background-color: theme('colors.text.secondary');
transition: 0.4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: theme('colors.primary.DEFAULT');
border-color: theme('colors.primary.DEFAULT');
}
input:checked + .slider:before {
transform: translateX(16px);
background-color: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
}
-31
View File
@@ -1,31 +0,0 @@
/**
* Show message into the popup
*/
export function showMessage({
msg,
isError,
isInfinite
}: {
msg: string;
isError?: boolean;
isInfinite?: boolean;
}) {
const message: HTMLElement = document.querySelector('#message')!;
message.style.color = isError ? 'red' : 'limegreen';
message.textContent = msg;
message.style.display = 'block';
if (!isInfinite) setTimeout(() => (message.style.display = 'none'), 5000);
}
/**
* Check if the current model support images integrations
* @param {string} version
* @returns
*/
export function isCurrentVersionSupportingImages(version: string) {
const versionNumber = version.match(/gpt-(\d+)/);
if (!versionNumber?.[1]) {
return false;
}
return Number(versionNumber[1]) >= 4;
}
-61
View File
@@ -1,61 +0,0 @@
const CURRENT_VERSION = '1.1.2';
const versionDisplay = document.querySelector('#version')!;
/**
* Get the last version from the github
* @returns
*/
export async function getLastVersion(): Promise<string> {
const req = await fetch(
'https://raw.githubusercontent.com/yoannchb-pro/MoodleGPT/main/package.json'
);
const rep = await req.json();
return rep.version;
}
/**
* Display the version or an update message
* @param {string} version
* @param {boolean} isCurrent
* @returns
*/
export function setVersion(version: string, isCurrent = true) {
if (isCurrent) {
versionDisplay.textContent = 'v' + version;
return;
}
const link = document.createElement('a');
link.href = 'https://github.com/yoannchb-pro/MoodleGPT';
link.rel = 'noopener noreferrer';
link.target = '_blank';
link.textContent = 'v' + version;
versionDisplay.appendChild(link);
versionDisplay.appendChild(document.createTextNode(' is now available !'));
}
/**
* Check if the extension neeed an update or not
*/
export async function notifyUpdate() {
const lastVersion = await getLastVersion().catch(err => {
console.error(err);
return CURRENT_VERSION;
});
const lastVertionSplitted = lastVersion.split('.');
const currentVersionSplitted = CURRENT_VERSION.split('.');
const minVersionLength = Math.min(lastVertionSplitted.length, currentVersionSplitted.length);
for (let i = 0; i < minVersionLength; ++i) {
if (parseInt(lastVertionSplitted[i]) > parseInt(currentVersionSplitted[i])) {
return setVersion(lastVersion, false);
} else if (parseInt(currentVersionSplitted[i]) > parseInt(lastVertionSplitted[i])) {
return setVersion(CURRENT_VERSION);
}
}
setVersion(CURRENT_VERSION);
}
notifyUpdate();
+55
View File
@@ -0,0 +1,55 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/popup/**/*.{js,jsx,ts,tsx}', './extension/popup/index.html'],
theme: {
extend: {
colors: {
primary: {
DEFAULT: '#6366f1',
hover: '#4f46e5'
},
gradient: {
start: '#0f172a',
end: '#1e1b4b'
},
panel: {
bg: 'rgba(255, 255, 255, 0.05)',
border: 'rgba(255, 255, 255, 0.1)'
},
text: {
primary: '#f8fafc',
secondary: '#94a3b8'
},
input: {
bg: 'rgba(0, 0, 0, 0.2)',
border: 'rgba(255, 255, 255, 0.15)',
focus: '#818cf8'
},
success: '#10b981',
error: '#ef4444'
},
fontFamily: {
sans: [
'Inter',
'-apple-system',
'BlinkMacSystemFont',
'Segoe UI',
'Roboto',
'Helvetica',
'Arial',
'sans-serif'
]
},
animation: {
float: 'float 3s ease-in-out infinite'
},
keyframes: {
float: {
'0%, 100%': { transform: 'translateY(0)' },
'50%': { transform: 'translateY(-4px)' }
}
}
}
},
plugins: []
};
+5 -4
View File
@@ -3,17 +3,18 @@
"strict": true,
"baseUrl": "src",
"module": "ESNext",
"moduleResolution": "Bundler",
"target": "ES6",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"target": "ES6",
"noImplicitAny": true,
"moduleResolution": "node",
"sourceMap": true,
"outDir": "extension",
"resolveJsonModule": true,
"types": ["node", "chrome"],
"typeRoots": ["node_modules/@types"],
"strictBindCallApply": true
"strictBindCallApply": true,
"jsx": "react-jsx",
"jsxImportSource": "preact"
},
"include": ["src/**/*"]
}