Refactor: implemented robust structured JSON parser for Moodle questions and DOM scope detection

This commit is contained in:
2026-04-11 19:52:39 +02:00
parent 921e2bba4e
commit 6ce2c47cb4
24 changed files with 1841 additions and 96 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+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>
+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>
+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>
+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
+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>
+51 -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,13 @@ 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.
Always output strict JSON according to the requested schema block.
`.trim();
const SYSTEM_INSTRUCTION_MESSAGE = {
@@ -37,7 +34,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 +44,7 @@ async function getContent(
!isGPTModelGreaterOrEqualTo4(config.model) ||
imagesElements.length === 0
) {
return question;
return textContent;
}
const contentWithImages: ChatCompletionUserMessageParam['content'] = [];
@@ -67,7 +65,7 @@ async function getContent(
contentWithImages.push({
type: 'text',
text: question
text: textContent
});
return contentWithImages;
@@ -126,34 +124,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;
+46 -14
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 { fixeO } from '../utils/fixe-o';
import { QuestionSchemas } from './utils/question-schemas';
import { MoodleQuestionType, MoodleQuestionResponse } from '../types/question-types';
/**
* Get the response from chatGPT api
@@ -29,27 +30,58 @@ async function getChatGPTResponse(
dangerouslyAllowBrowser: true
});
const req = await client.chat.completions.create(
fixeO(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;
max_completion_tokens: config.maxTokens || 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 // Maximum length of the response,
};
if (targetSchema) {
requestPayload.response_format = {
type: 'json_object'
};
if (requestPayload.messages.length > 0 && requestPayload.messages[0].role === 'system') {
requestPayload.messages[0].content += `\n\nYou MUST respond in JSON strictly adhering to the following schema. Do NOT wrap the JSON in markdown code blocks. Output raw JSON only.\n\n${JSON.stringify(targetSchema, null, 2)}`;
}
}
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) {
try {
const cleanedResponse = rawResponse
.replace(/^```(json)?[\s\S]*?\n([\s\S]*?)```$/g, '$2')
.replace(/^```(json)?|```$/gm, '')
.trim();
structuredResponse = JSON.parse(cleanedResponse);
} catch (e) {
console.error('Failed to parse structured JSON from GPT', 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
};
}
+99
View File
@@ -0,0 +1,99 @@
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')) {
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')) {
return {
question_type: MoodleQuestionType.NUMERICAL,
question_text: normalizedQuestionText
};
}
if (container.classList.contains('essay')) {
return {
question_type: MoodleQuestionType.ESSAY,
question_text: normalizedQuestionText
};
}
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));
@@ -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;
+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;
+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;
@@ -0,0 +1,98 @@
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.UNKNOWN]: undefined
};
+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;
+96
View File
@@ -0,0 +1,96 @@
export enum MoodleQuestionType {
SINGLE_CHOICE = 'single_choice',
MULTIPLE_CHOICE = 'multiple_choice',
TRUE_FALSE = 'true_false',
SHORT_TEXT = 'short_text',
NUMERICAL = 'numerical',
ESSAY = 'essay',
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;
}
export type MoodleQuestionQuery =
| SingleChoiceQuery
| MultipleChoiceQuery
| TrueFalseQuery
| ShortTextQuery
| NumericalQuery
| EssayQuery;
// ==== 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 type MoodleQuestionResponse =
| SingleChoiceResponse
| MultipleChoiceResponse
| TrueFalseResponse
| ShortTextResponse
| NumericalResponse
| EssayResponse;
+2 -1
View File
@@ -2,7 +2,8 @@
"compilerOptions": {
"strict": true,
"baseUrl": "src",
"module": "esnext",
"module": "ESNext",
"moduleResolution": "Bundler",
"target": "ES6",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,