diff --git a/amd/build/spellcheck.min.js b/amd/build/spellcheck.min.js index 8c7860e..3fa7313 100644 --- a/amd/build/spellcheck.min.js +++ b/amd/build/spellcheck.min.js @@ -6,6 +6,6 @@ define("qtype_aitext/spellcheck",["exports","qtype_aitext/diff","core_form/modal * @copyright 2024, ISB Bayern * @author Dr. Peter Mayer * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */(Diff),_modalform=(obj=_modalform)&&obj.__esModule?obj:{default:obj};_exports.init=(cmid,readonlyareaselector,spellcheckeditbuttonselector)=>{renderDiff(readonlyareaselector),document.querySelector(spellcheckeditbuttonselector)&&document.querySelector(spellcheckeditbuttonselector).addEventListener("click",(async event=>{event.preventDefault(),await showModalForm(cmid,readonlyareaselector)}))};const renderDiff=readonlyareaselector=>{const studentanswer=document.querySelector(readonlyareaselector).innerHTML,spellcheck=document.querySelector(readonlyareaselector).dataset.spellcheck;let span=null;const diff=Diff.diffChars(studentanswer,spellcheck),fragment=document.createElement("div");let fullspellcheck="";diff.forEach((part=>{part.value=part.value.replace(/ /g," ");const parser=new DOMParser;part.value=parser.parseFromString(part.value,"text/html");const cls=part.added?"qtype_aitext_spellcheck_new":part.removed?"qtype_aitext_spellcheck_wrong":"";part.added||part.removed?(span=document.createElement("span"),span.classList=cls,span.appendChild(part.value.documentElement),fullspellcheck+=span.outerHTML):fullspellcheck+=part.value.documentElement.textContent})),fragment.innerHTML=fullspellcheck,document.querySelector(readonlyareaselector).replaceChildren(fragment)};_exports.renderDiff=renderDiff;const showModalForm=async(cmid,readonlyareaselector)=>{const attemptstepid=document.querySelector(readonlyareaselector).dataset.spellcheckattemptstepid,answerstepid=document.querySelector(readonlyareaselector).dataset.spellcheckattemptstepanswerid,title=await(0,_str.getString)("spellcheckedit","qtype_aitext"),modalForm=new _modalform.default({formClass:"qtype_aitext\\form\\edit_spellchek",args:{attemptstepid:attemptstepid,answerstepid:answerstepid,cmid:cmid},modalConfig:{title:title}});modalForm.addEventListener(modalForm.events.FORM_SUBMITTED,reloadpage),await modalForm.show()};_exports.showModalForm=showModalForm;const reloadpage=()=>{location.reload()}})); + */(Diff),_modalform=(obj=_modalform)&&obj.__esModule?obj:{default:obj};_exports.init=(readonlyareaselector,spellcheckeditbuttonselector)=>{renderDiff(readonlyareaselector),document.querySelector(spellcheckeditbuttonselector)&&document.querySelector(spellcheckeditbuttonselector).addEventListener("click",(async event=>{event.preventDefault(),await showModalForm(readonlyareaselector)}))};const renderDiff=readonlyareaselector=>{const studentanswer=document.querySelector(readonlyareaselector).innerHTML,spellcheck=document.querySelector(readonlyareaselector).dataset.spellcheck;let span=null;const diff=Diff.diffChars(studentanswer,spellcheck),fragment=document.createElement("div");let fullspellcheck="";diff.forEach((part=>{part.value=part.value.replace(/ /g," ");const parser=new DOMParser;part.value=parser.parseFromString(part.value,"text/html");const cls=part.added?"qtype_aitext_spellcheck_new":part.removed?"qtype_aitext_spellcheck_wrong":"";part.added||part.removed?(span=document.createElement("span"),span.classList=cls,span.appendChild(part.value.documentElement),fullspellcheck+=span.outerHTML):fullspellcheck+=part.value.documentElement.textContent})),fragment.innerHTML=fullspellcheck,document.querySelector(readonlyareaselector).replaceChildren(fragment)};_exports.renderDiff=renderDiff;const showModalForm=async readonlyareaselector=>{const attemptstepid=document.querySelector(readonlyareaselector).dataset.spellcheckattemptstepid,answerstepid=document.querySelector(readonlyareaselector).dataset.spellcheckattemptstepanswerid,title=await(0,_str.getString)("spellcheckedit","qtype_aitext"),modalForm=new _modalform.default({formClass:"qtype_aitext\\form\\edit_spellcheck",args:{attemptstepid:attemptstepid,answerstepid:answerstepid},modalConfig:{title:title}});modalForm.addEventListener(modalForm.events.FORM_SUBMITTED,reloadpage),await modalForm.show()};_exports.showModalForm=showModalForm;const reloadpage=()=>{location.reload()}})); //# sourceMappingURL=spellcheck.min.js.map \ No newline at end of file diff --git a/amd/build/spellcheck.min.js.map b/amd/build/spellcheck.min.js.map index f2478c9..cbc2565 100644 --- a/amd/build/spellcheck.min.js.map +++ b/amd/build/spellcheck.min.js.map @@ -1 +1 @@ -{"version":3,"file":"spellcheck.min.js","sources":["../src/spellcheck.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Generates the spellcheck diff view.\n *\n * @module qtype_aitext/spellcheck\n * @copyright 2024, ISB Bayern\n * @author Dr. Peter Mayer\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as Diff from 'qtype_aitext/diff';\nimport ModalForm from 'core_form/modalform';\nimport {getString as getString} from 'core/str';\n\n/**\n * Init the module.\n *\n * @param {int} cmid the course module id of the quiz.\n * @param {string} readonlyareaselector the selector for the readonly area to apply the spellchecking\n * @param {string} spellcheckeditbuttonselector the selector for the spell check edit button\n */\nexport const init = (cmid, readonlyareaselector, spellcheckeditbuttonselector) => {\n renderDiff(readonlyareaselector);\n\n if (!document.querySelector(spellcheckeditbuttonselector)) {\n return;\n }\n document.querySelector(spellcheckeditbuttonselector).addEventListener('click',\n async(event) => {\n event.preventDefault();\n await showModalForm(cmid, readonlyareaselector);\n });\n};\n\n/**\n * Render the spell check highlighting.\n *\n * @param {string} readonlyareaselector the selector for the readonly area to apply the spell check diff to\n */\nexport const renderDiff = (readonlyareaselector) => {\n const studentanswer = document.querySelector(readonlyareaselector).innerHTML;\n const spellcheck = document.querySelector(readonlyareaselector).dataset.spellcheck;\n let span = null;\n\n const diff = Diff.diffChars(studentanswer, spellcheck);\n const fragment = document.createElement('div');\n\n let fullspellcheck = '';\n\n diff.forEach(part => {\n // We need to replace the whitespaces, because otherwise they will be removed by\n // calling parseFromString of the DOMParser.\n part.value = part.value.replace(/ /g, ' ');\n const parser = new DOMParser();\n part.value = parser.parseFromString(part.value, 'text/html');\n const cls = part.added ? 'qtype_aitext_spellcheck_new' :\n part.removed ? 'qtype_aitext_spellcheck_wrong' : '';\n if (part.added || part.removed) {\n span = document.createElement('span');\n span.classList = cls;\n span.appendChild(part.value.documentElement);\n fullspellcheck += span.outerHTML;\n } else {\n fullspellcheck += part.value.documentElement.textContent;\n }\n });\n\n fragment.innerHTML = fullspellcheck;\n document.querySelector(readonlyareaselector).replaceChildren(fragment);\n};\n\n/**\n * Show the dynamic spellcheck form.\n *\n * @param {int} cmid the course module id of the quiz\n * @param {string} readonlyareaselector the selector for the readonly area\n */\nexport const showModalForm = async(cmid, readonlyareaselector) => {\n const attemptstepid = document.querySelector(readonlyareaselector).dataset.spellcheckattemptstepid;\n const answerstepid = document.querySelector(readonlyareaselector).dataset.spellcheckattemptstepanswerid;\n const title = await getString('spellcheckedit', 'qtype_aitext');\n const modalForm = new ModalForm({\n formClass: \"qtype_aitext\\\\form\\\\edit_spellchek\",\n args: {\n attemptstepid,\n answerstepid,\n cmid\n },\n modalConfig: {title},\n });\n modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, reloadpage);\n await modalForm.show();\n};\n\n/**\n * Reload the page.\n *\n * This is not nice, but easy :-) .\n */\nconst reloadpage = () => {\n location.reload();\n};\n"],"names":["cmid","readonlyareaselector","spellcheckeditbuttonselector","renderDiff","document","querySelector","addEventListener","async","event","preventDefault","showModalForm","studentanswer","innerHTML","spellcheck","dataset","span","diff","Diff","diffChars","fragment","createElement","fullspellcheck","forEach","part","value","replace","parser","DOMParser","parseFromString","cls","added","removed","classList","appendChild","documentElement","outerHTML","textContent","replaceChildren","attemptstepid","spellcheckattemptstepid","answerstepid","spellcheckattemptstepanswerid","title","modalForm","ModalForm","formClass","args","modalConfig","events","FORM_SUBMITTED","reloadpage","show","location","reload"],"mappings":";;;;;;;;wFAmCoB,CAACA,KAAMC,qBAAsBC,gCAC7CC,WAAWF,sBAENG,SAASC,cAAcH,+BAG5BE,SAASC,cAAcH,8BAA8BI,iBAAiB,SAClEC,MAAAA,QACIC,MAAMC,uBACAC,cAAcV,KAAMC,gCASzBE,WAAcF,6BACjBU,cAAgBP,SAASC,cAAcJ,sBAAsBW,UAC7DC,WAAaT,SAASC,cAAcJ,sBAAsBa,QAAQD,eACpEE,KAAO,WAELC,KAAOC,KAAKC,UAAUP,cAAeE,YACrCM,SAAWf,SAASgB,cAAc,WAEpCC,eAAiB,GAErBL,KAAKM,SAAQC,OAGTA,KAAKC,MAAQD,KAAKC,MAAMC,QAAQ,KAAM,gBAChCC,OAAS,IAAIC,UACnBJ,KAAKC,MAAQE,OAAOE,gBAAgBL,KAAKC,MAAO,mBAC1CK,IAAMN,KAAKO,MAAQ,8BACrBP,KAAKQ,QAAU,gCAAkC,GACjDR,KAAKO,OAASP,KAAKQ,SACnBhB,KAAOX,SAASgB,cAAc,QAC9BL,KAAKiB,UAAYH,IACjBd,KAAKkB,YAAYV,KAAKC,MAAMU,iBAC5Bb,gBAAkBN,KAAKoB,WAEvBd,gBAAkBE,KAAKC,MAAMU,gBAAgBE,eAIrDjB,SAASP,UAAYS,eACrBjB,SAASC,cAAcJ,sBAAsBoC,gBAAgBlB,gDASpDT,cAAgBH,MAAMP,KAAMC,8BAC/BqC,cAAgBlC,SAASC,cAAcJ,sBAAsBa,QAAQyB,wBACrEC,aAAepC,SAASC,cAAcJ,sBAAsBa,QAAQ2B,8BACpEC,YAAc,kBAAU,iBAAkB,gBAC1CC,UAAY,IAAIC,mBAAU,CAC5BC,UAAW,qCACXC,KAAM,CACFR,cAAAA,cACAE,aAAAA,aACAxC,KAAAA,MAEJ+C,YAAa,CAACL,MAAAA,SAElBC,UAAUrC,iBAAiBqC,UAAUK,OAAOC,eAAgBC,kBACtDP,UAAUQ,mDAQdD,WAAa,KACfE,SAASC"} \ No newline at end of file +{"version":3,"file":"spellcheck.min.js","sources":["../src/spellcheck.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Generates the spellcheck diff view.\n *\n * @module qtype_aitext/spellcheck\n * @copyright 2024, ISB Bayern\n * @author Dr. Peter Mayer\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as Diff from 'qtype_aitext/diff';\nimport ModalForm from 'core_form/modalform';\nimport {getString as getString} from 'core/str';\n\n/**\n * Init the module.\n *\n * @param {string} readonlyareaselector the selector for the readonly area to apply the spellchecking\n * @param {string} spellcheckeditbuttonselector the selector for the spell check edit button\n */\nexport const init = (readonlyareaselector, spellcheckeditbuttonselector) => {\n renderDiff(readonlyareaselector);\n\n if (!document.querySelector(spellcheckeditbuttonselector)) {\n return;\n }\n document.querySelector(spellcheckeditbuttonselector).addEventListener('click',\n async(event) => {\n event.preventDefault();\n await showModalForm(readonlyareaselector);\n });\n};\n\n/**\n * Render the spell check highlighting.\n *\n * @param {string} readonlyareaselector the selector for the readonly area to apply the spell check diff to\n */\nexport const renderDiff = (readonlyareaselector) => {\n const studentanswer = document.querySelector(readonlyareaselector).innerHTML;\n const spellcheck = document.querySelector(readonlyareaselector).dataset.spellcheck;\n let span = null;\n\n const diff = Diff.diffChars(studentanswer, spellcheck);\n const fragment = document.createElement('div');\n\n let fullspellcheck = '';\n\n diff.forEach(part => {\n // We need to replace the whitespaces, because otherwise they will be removed by\n // calling parseFromString of the DOMParser.\n part.value = part.value.replace(/ /g, ' ');\n const parser = new DOMParser();\n part.value = parser.parseFromString(part.value, 'text/html');\n const cls = part.added ? 'qtype_aitext_spellcheck_new' :\n part.removed ? 'qtype_aitext_spellcheck_wrong' : '';\n if (part.added || part.removed) {\n span = document.createElement('span');\n span.classList = cls;\n span.appendChild(part.value.documentElement);\n fullspellcheck += span.outerHTML;\n } else {\n fullspellcheck += part.value.documentElement.textContent;\n }\n });\n\n fragment.innerHTML = fullspellcheck;\n document.querySelector(readonlyareaselector).replaceChildren(fragment);\n};\n\n/**\n * Show the dynamic spellcheck form.\n *\n * @param {string} readonlyareaselector the selector for the readonly area\n */\nexport const showModalForm = async(readonlyareaselector) => {\n const attemptstepid = document.querySelector(readonlyareaselector).dataset.spellcheckattemptstepid;\n const answerstepid = document.querySelector(readonlyareaselector).dataset.spellcheckattemptstepanswerid;\n const title = await getString('spellcheckedit', 'qtype_aitext');\n const modalForm = new ModalForm({\n formClass: \"qtype_aitext\\\\form\\\\edit_spellcheck\",\n args: {\n attemptstepid,\n answerstepid\n },\n modalConfig: {title},\n });\n modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, reloadpage);\n await modalForm.show();\n};\n\n/**\n * Reload the page.\n *\n * This is not nice, but easy :-) .\n */\nconst reloadpage = () => {\n location.reload();\n};\n"],"names":["readonlyareaselector","spellcheckeditbuttonselector","renderDiff","document","querySelector","addEventListener","async","event","preventDefault","showModalForm","studentanswer","innerHTML","spellcheck","dataset","span","diff","Diff","diffChars","fragment","createElement","fullspellcheck","forEach","part","value","replace","parser","DOMParser","parseFromString","cls","added","removed","classList","appendChild","documentElement","outerHTML","textContent","replaceChildren","attemptstepid","spellcheckattemptstepid","answerstepid","spellcheckattemptstepanswerid","title","modalForm","ModalForm","formClass","args","modalConfig","events","FORM_SUBMITTED","reloadpage","show","location","reload"],"mappings":";;;;;;;;wFAkCoB,CAACA,qBAAsBC,gCACvCC,WAAWF,sBAENG,SAASC,cAAcH,+BAG5BE,SAASC,cAAcH,8BAA8BI,iBAAiB,SAClEC,MAAAA,QACIC,MAAMC,uBACAC,cAAcT,gCASnBE,WAAcF,6BACjBU,cAAgBP,SAASC,cAAcJ,sBAAsBW,UAC7DC,WAAaT,SAASC,cAAcJ,sBAAsBa,QAAQD,eACpEE,KAAO,WAELC,KAAOC,KAAKC,UAAUP,cAAeE,YACrCM,SAAWf,SAASgB,cAAc,WAEpCC,eAAiB,GAErBL,KAAKM,SAAQC,OAGTA,KAAKC,MAAQD,KAAKC,MAAMC,QAAQ,KAAM,gBAChCC,OAAS,IAAIC,UACnBJ,KAAKC,MAAQE,OAAOE,gBAAgBL,KAAKC,MAAO,mBAC1CK,IAAMN,KAAKO,MAAQ,8BACrBP,KAAKQ,QAAU,gCAAkC,GACjDR,KAAKO,OAASP,KAAKQ,SACnBhB,KAAOX,SAASgB,cAAc,QAC9BL,KAAKiB,UAAYH,IACjBd,KAAKkB,YAAYV,KAAKC,MAAMU,iBAC5Bb,gBAAkBN,KAAKoB,WAEvBd,gBAAkBE,KAAKC,MAAMU,gBAAgBE,eAIrDjB,SAASP,UAAYS,eACrBjB,SAASC,cAAcJ,sBAAsBoC,gBAAgBlB,gDAQpDT,cAAgBH,MAAAA,6BACnB+B,cAAgBlC,SAASC,cAAcJ,sBAAsBa,QAAQyB,wBACrEC,aAAepC,SAASC,cAAcJ,sBAAsBa,QAAQ2B,8BACpEC,YAAc,kBAAU,iBAAkB,gBAC1CC,UAAY,IAAIC,mBAAU,CAC5BC,UAAW,sCACXC,KAAM,CACFR,cAAAA,cACAE,aAAAA,cAEJO,YAAa,CAACL,MAAAA,SAElBC,UAAUrC,iBAAiBqC,UAAUK,OAAOC,eAAgBC,kBACtDP,UAAUQ,mDAQdD,WAAa,KACfE,SAASC"} \ No newline at end of file diff --git a/amd/src/spellcheck.js b/amd/src/spellcheck.js index 6639a5a..a46c1f2 100644 --- a/amd/src/spellcheck.js +++ b/amd/src/spellcheck.js @@ -29,11 +29,10 @@ import {getString as getString} from 'core/str'; /** * Init the module. * - * @param {int} cmid the course module id of the quiz. * @param {string} readonlyareaselector the selector for the readonly area to apply the spellchecking * @param {string} spellcheckeditbuttonselector the selector for the spell check edit button */ -export const init = (cmid, readonlyareaselector, spellcheckeditbuttonselector) => { +export const init = (readonlyareaselector, spellcheckeditbuttonselector) => { renderDiff(readonlyareaselector); if (!document.querySelector(spellcheckeditbuttonselector)) { @@ -42,7 +41,7 @@ export const init = (cmid, readonlyareaselector, spellcheckeditbuttonselector) = document.querySelector(spellcheckeditbuttonselector).addEventListener('click', async(event) => { event.preventDefault(); - await showModalForm(cmid, readonlyareaselector); + await showModalForm(readonlyareaselector); }); }; @@ -86,19 +85,17 @@ export const renderDiff = (readonlyareaselector) => { /** * Show the dynamic spellcheck form. * - * @param {int} cmid the course module id of the quiz * @param {string} readonlyareaselector the selector for the readonly area */ -export const showModalForm = async(cmid, readonlyareaselector) => { +export const showModalForm = async(readonlyareaselector) => { const attemptstepid = document.querySelector(readonlyareaselector).dataset.spellcheckattemptstepid; const answerstepid = document.querySelector(readonlyareaselector).dataset.spellcheckattemptstepanswerid; const title = await getString('spellcheckedit', 'qtype_aitext'); const modalForm = new ModalForm({ - formClass: "qtype_aitext\\form\\edit_spellchek", + formClass: "qtype_aitext\\form\\edit_spellcheck", args: { attemptstepid, - answerstepid, - cmid + answerstepid }, modalConfig: {title}, }); diff --git a/classes/external.php b/classes/external.php index 3c9e807..a405e14 100644 --- a/classes/external.php +++ b/classes/external.php @@ -72,7 +72,7 @@ public static function fetch_ai_grade($response, $defaultmark, $prompt, $markssc if (!empty($response) && !empty($prompt) && $defaultmark > 0) { $fullaiprompt = $aiquestion->build_full_ai_prompt($response, $prompt, $defaultmark, $marksscheme); $feedback = $aiquestion->perform_request($fullaiprompt); - $contentobject = json_decode($feedback); + $contentobject = $aiquestion->process_feedback($feedback); } else { $contentobject = (object)["feedback" => get_string('err_parammissing', 'qtype_aitext'), "marks" => 0]; } diff --git a/classes/form/edit_spellchek.php b/classes/form/edit_spellcheck.php similarity index 76% rename from classes/form/edit_spellchek.php rename to classes/form/edit_spellcheck.php index 380db99..83b40e2 100644 --- a/classes/form/edit_spellchek.php +++ b/classes/form/edit_spellcheck.php @@ -29,7 +29,10 @@ * @author Dr. Peter Mayer * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class edit_spellchek extends dynamic_form { +class edit_spellcheck extends dynamic_form { + + /** @var context|null Variable to store the context because it is expensive to retrieve. */ + private ?context $context = null; /** * Define the form @@ -38,9 +41,6 @@ public function definition() { $mform = &$this->_form; - $mform->addElement('hidden', 'cmid'); - $mform->setType('cmid', PARAM_INT); - $mform->addElement('hidden', 'attemptstepid'); $mform->setType('attemptstepid', PARAM_INT); @@ -66,13 +66,8 @@ public function definition() { * @return context */ protected function get_context_for_dynamic_submission(): context { - $cmid = $this->optional_param('cmid', null, PARAM_INT); - if (empty($cmid)) { - $cmid = $this->_ajaxformdata['cmid']; - } - // Verify cm exists. - [, $cm] = get_course_and_cm_from_cmid($cmid); - return context_module::instance($cm->id); + $attemptstepid = $this->_ajaxformdata['attemptstepid']; + return $this->get_context_from_attemptstepid($attemptstepid); } /** @@ -81,13 +76,26 @@ protected function get_context_for_dynamic_submission(): context { * @throws \moodle_exception User does not have capability to access the form */ protected function check_access_for_dynamic_submission(): void { + global $USER; $context = $this->get_context_for_dynamic_submission(); + + if ($context->contextlevel === CONTEXT_USER) { + // This will happen in preview mode. + // In preview mode we just check if the user context belongs to the current user. + if (intval($context->instanceid) !== intval($USER->id)) { + throw new \moodle_exception('nocapabilitytousethisservice'); + } + return; + } + // We usually end up with a course module context otherwise. Even if not we just check for + // decent capabilities to edit the result of the AI. if ( !has_capability('mod/quiz:grade', $context) && !has_capability('mod/quiz:regrade', $context) ) { throw new \moodle_exception('nocapabilitytousethisservice'); } + } /** @@ -130,7 +138,6 @@ public function set_data_for_dynamic_submission(): void { 'test' => $spellcheckrecord->value, 'spellcheck_editor' => ['text' => $spellcheckrecord->value, 'format' => FORMAT_HTML, 'itemid' => $draftitemid], 'attemptstepid' => $this->optional_param('attemptstepid', 0, PARAM_INT), - 'cmid' => $this->optional_param('cmid', 0, PARAM_INT), 'student_answer' => $answerrecord->value, ]); } @@ -147,4 +154,16 @@ protected function get_page_url_for_dynamic_submission(): moodle_url { ]; return new moodle_url('/mod/quiz/review.php', $params); } + + private function get_context_from_attemptstepid(int $attemptstepid) { + global $DB; + if (!is_null($this->context)) { + return $this->context; + } + $attemptstep = $DB->get_record('question_attempt_steps', ['id' => $attemptstepid]); + $attempt = $DB->get_record('question_attempts', ['id' => $attemptstep->questionattemptid]); + $questionusage = $DB->get_record('question_usages', ['id' => $attempt->questionusageid]); + $this->context = context::instance_by_id($questionusage->contextid); + return $this->context; + } } diff --git a/question.php b/question.php index f25e57d..01cc092 100755 --- a/question.php +++ b/question.php @@ -147,8 +147,6 @@ public function apply_attempt_state(question_attempt_step $step) { * Call the llm using either the 4.5 core api or the backend provided by * local_ai_manager (mebis) or tool_aimanager * - * See "uselocalaimanager" admin setting. - * * @param string $prompt * @param string $purpose */ @@ -341,7 +339,8 @@ protected function llm_translate(string $text): string { $cache = cache::make('qtype_aitext', 'stringdata'); if (($translation = $cache->get(current_language().'_'.$text)) === false) { - $prompt = 'translate "'.$text .'" into '.current_language(); + $prompt = 'translate "'.$text .'" into '.current_language() . + 'Only return the exact text, do not wrap it in other text.'; $translation = $this->perform_request($prompt, 'translate'); $translation = trim($translation, '"'); $cache->set(current_language().'_'.$text, $translation); diff --git a/questiontype.php b/questiontype.php index 2fd86f5..0d712d2 100755 --- a/questiontype.php +++ b/questiontype.php @@ -388,7 +388,7 @@ protected function export_errorcmid($cmid) { */ public function menu_name() { if (class_exists('\local_ai_manager\local\tenant')) { - if (get_config('qtype_aitext', 'uselocalaimanager')) { + if (get_config('qtype_aitext', 'backend') === 'local_ai_manager') { $tenant = \core\di::get(\local_ai_manager\local\tenant::class); return $tenant->is_tenant_allowed() ? parent::menu_name() : ''; } diff --git a/renderer.php b/renderer.php index 10ee294..2902cc6 100755 --- a/renderer.php +++ b/renderer.php @@ -23,7 +23,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -use qtype_aitext\form\edit_spellchek; +use qtype_aitext\form\edit_spellcheck; defined('MOODLE_INTERNAL') || die(); /** @@ -93,7 +93,7 @@ public function formulation_and_controls(question_attempt $qa, } $result = ''; - if (get_config('qtype_aitext', 'uselocalaimanager')) { + if (get_config('qtype_aitext', 'backend') === 'local_ai_manager') { $uniqid = uniqid(); $result .= html_writer::tag('div', '', ['data-content' => 'local_ai_manager_infobox', 'data-boxid' => $uniqid]); @@ -114,7 +114,7 @@ public function formulation_and_controls(question_attempt $qa, } $result .= html_writer::tag('div', $files, ['class' => 'attachments']); $result .= html_writer::end_tag('div'); - if (get_config('qtype_aitext', 'uselocalaimanager')) { + if (get_config('qtype_aitext', 'backend') === 'local_ai_manager') { $result .= html_writer::tag('div', '', ['data-content' => 'local_ai_manager_warningbox', 'data-boxid' => $uniqid]); $this->page->requires->js_call_amd('local_ai_manager/warningbox', 'renderWarningBox', @@ -247,7 +247,6 @@ public function files_input(question_attempt $qa, $numallowed, return $output; } - } @@ -273,7 +272,7 @@ public function set_displayoptions(question_display_options $displayoptions): vo } /** - * Render the students respone when the question is in read-only mode. + * Render the students response when the question is in read-only mode. * * @param string $name the variable name this input edits. * @param question_attempt $qa the question attempt being display. @@ -282,56 +281,9 @@ public function set_displayoptions(question_display_options $displayoptions): vo * @param object $context the context teh output belongs to. * @return string html to display the response. */ - abstract public function response_area_read_only($name, question_attempt $qa, - question_attempt_step $step, $lines, $context); - - /** - * Render the students respone when the question is in read-only mode. - * @param string $name the variable name this input edits. - * @param question_attempt $qa the question attempt being display. - * @param question_attempt_step $step the current step. - * @param int $lines approximate size of input box to display. - * @param object $context the context teh output belongs to. - * @return string html to display the response for editing. - */ - abstract public function response_area_input($name, question_attempt $qa, - question_attempt_step $step, $lines, $context); - - /** - * Specific class name to add to the input element. - * - * @return string - */ - abstract protected function class_name(); -} - -/** - * Where the student use the HTML editor - * - * @author Marcus Green 2024 building on work by the UK OU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class qtype_aitext_format_editor_renderer extends qtype_aitext_format_renderer_base { - /** - * Specific class name to add to the input element. - * - * @return string - */ - protected function class_name() { - return 'qtype_aitext_editor'; - } - /** - * Return a read only version of the response areay. Typically for after - * a quesiton has been answered and the response cannot be modified. - * @param string $name - * @param question_attempt $qa - * @param question_attempt_step $step - * @param int $lines number of lines in the editor - * @param object $context - * @return string - * @throws coding_exception - */ public function response_area_read_only($name, $qa, $step, $lines, $context) { + global $USER; + $question = $qa->get_question(); $uniqid = uniqid(); $readonlyareaid = 'aitext_readonly_area' . $uniqid; @@ -340,7 +292,7 @@ public function response_area_read_only($name, $qa, $step, $lines, $context) { if ($question->spellcheck) { $this->page->requires->js_call_amd('qtype_aitext/diff'); $this->page->requires->js_call_amd('qtype_aitext/spellcheck', 'init', - [$this->get_page()->cm->id, '#' . $readonlyareaid, '#' . $spellcheckeditbuttonid]); + ['#' . $readonlyareaid, '#' . $spellcheckeditbuttonid]); $stepspellcheck = $qa->get_last_step_with_qt_var('-spellcheckresponse'); $stepanswer = $qa->get_last_step_with_qt_var('answer'); } @@ -350,12 +302,12 @@ public function response_area_read_only($name, $qa, $step, $lines, $context) { $output = html_writer::tag('h4', $responselabel, ['id' => $labelbyid, 'class' => 'sr-only']); $divoptions = [ - 'id' => $readonlyareaid, - 'role' => 'textbox', - 'aria-readonly' => 'true', - 'aria-labelledby' => $labelbyid, - 'class' => $this->class_name() . ' qtype_aitext_response readonly', - 'style' => 'min-height: ' . ($lines * 1.25) . 'em;', + 'id' => $readonlyareaid, + 'role' => 'textbox', + 'aria-readonly' => 'true', + 'aria-labelledby' => $labelbyid, + 'class' => $this->class_name() . ' qtype_aitext_response readonly', + 'style' => 'min-height: ' . ($lines * 1.25) . 'em;', ]; if ($qa->get_question()->spellcheck) { @@ -368,27 +320,66 @@ public function response_area_read_only($name, $qa, $step, $lines, $context) { $output .= html_writer::tag('div', $this->prepare_response($name, $qa, $step, $context), $divoptions); if ( - $qa->get_question()->spellcheck && - ( - has_capability('mod/quiz:grade', $context) || - has_capability('mod/quiz:regrade', $context) - ) + $qa->get_question()->spellcheck && + ( + has_capability('mod/quiz:grade', $context) || + has_capability('mod/quiz:regrade', $context) || + ($context->contextlevel === CONTEXT_USER && intval($USER->id) === intval($context->instanceid)) + ) ) { $btnoptions = ['id' => $spellcheckeditbuttonid, 'class' => 'btn btn-link']; $output .= html_writer::tag( - 'button', - $this->output->pix_icon( - 'i/edit', - get_string('spellcheckedit', 'qtype_aitext'), - 'moodle' - ) . " " . get_string('spellcheckedit', 'qtype_aitext'), - $btnoptions + 'button', + $this->output->pix_icon( + 'i/edit', + get_string('spellcheckedit', 'qtype_aitext'), + 'moodle' + ) . " " . get_string('spellcheckedit', 'qtype_aitext'), + $btnoptions ); } + // Height $lines * 1.25 because that is a typical line-height on web pages. + // That seems to give results that look OK. return $output; } + /** + * Render the students respone when the question is in read-only mode. + * @param string $name the variable name this input edits. + * @param question_attempt $qa the question attempt being display. + * @param question_attempt_step $step the current step. + * @param int $lines approximate size of input box to display. + * @param object $context the context teh output belongs to. + * @return string html to display the response for editing. + */ + abstract public function response_area_input($name, question_attempt $qa, + question_attempt_step $step, $lines, $context); + + /** + * Specific class name to add to the input element. + * + * @return string + */ + abstract protected function class_name(); +} + +/** + * Where the student use the HTML editor + * + * @author Marcus Green 2024 building on work by the UK OU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qtype_aitext_format_editor_renderer extends qtype_aitext_format_renderer_base { + /** + * Specific class name to add to the input element. + * + * @return string + */ + protected function class_name() { + return 'qtype_aitext_editor'; + } + /** * Where the student types in their response * @@ -684,76 +675,6 @@ protected function textarea($response, $lines, $attributes) { protected function class_name() { return 'qtype_aitext_plain'; } - /** - * Read only version of response (typically after submission) - * @param string $name - * @param question_attempt $qa - * @param question_attempt_step $step - * @param int $lines - * @param object $context - * @return string - * @throws coding_exception - */ - public function response_area_read_only($name, $qa, $step, $lines, $context) { - // CARE: This is basically duplicating response_area_read_only from qtype_aitext_format_editor_renderer. - $question = $qa->get_question(); - $uniqid = uniqid(); - $readonlyareaid = 'aitext_readonly_area' . $uniqid; - $spellcheckeditbuttonid = 'aitext_spellcheckedit' . $uniqid; - - if ($question->spellcheck) { - $this->page->requires->js_call_amd('qtype_aitext/diff'); - $this->page->requires->js_call_amd('qtype_aitext/spellcheck', 'init', - [$this->get_page()->cm->id, '#' . $readonlyareaid, '#' . $spellcheckeditbuttonid]); - $stepspellcheck = $qa->get_last_step_with_qt_var('-spellcheckresponse'); - $stepanswer = $qa->get_last_step_with_qt_var('answer'); - } - // Lib to display the spellcheck diff. - $labelbyid = $qa->get_qt_field_name($name) . '_label'; - $responselabel = $this->displayoptions->add_question_identifier_to_label(get_string('answertext', 'qtype_aitext')); - $output = html_writer::tag('h4', $responselabel, ['id' => $labelbyid, 'class' => 'sr-only']); - - $divoptions = [ - 'id' => $readonlyareaid, - 'role' => 'textbox', - 'aria-readonly' => 'true', - 'aria-labelledby' => $labelbyid, - 'class' => $this->class_name() . ' qtype_aitext_response readonly', - 'style' => 'min-height: ' . ($lines * 1.25) . 'em;', - ]; - - if ($qa->get_question()->spellcheck) { - $divoptions['data-spellcheck'] = $this->prepare_response('-spellcheckresponse', $qa, $stepspellcheck, $context); - $divoptions['data-spellcheckattemptstepid'] = $stepspellcheck->get_id(); - $divoptions['data-spellcheckattemptstepanswerid'] = $stepanswer->get_id(); - $divoptions['data-answer'] = $this->prepare_response($name, $qa, $step, $context); - } - - $output .= html_writer::tag('div', $this->prepare_response($name, $qa, $step, $context), $divoptions); - - if ( - $qa->get_question()->spellcheck && - ( - has_capability('mod/quiz:grade', $context) || - has_capability('mod/quiz:regrade', $context) - ) - ) { - $btnoptions = ['id' => $spellcheckeditbuttonid, 'class' => 'btn btn-link']; - $output .= html_writer::tag( - 'button', - $this->output->pix_icon( - 'i/edit', - get_string('spellcheckedit', 'qtype_aitext'), - 'moodle' - ) . " " . get_string('spellcheckedit', 'qtype_aitext'), - $btnoptions - ); - } - // Height $lines * 1.25 because that is a typical line-height on web pages. - // That seems to give results that look OK. - - return $output; - } /** * Text area for response to be keyed in diff --git a/settings.php b/settings.php index 9db359d..51bb114 100644 --- a/settings.php +++ b/settings.php @@ -66,12 +66,6 @@ new lang_string('responseformat_setting', 'qtype_aitext'), 0, ['plain' => 'plain', 'editor' => 'editor', 'monospaced' => 'monospaced'] )); - $settings->add(new admin_setting_configcheckbox( - 'qtype_aitext/uselocalaimanager', - new lang_string('use_local_ai_manager', 'qtype_aitext'), - new lang_string('use_local_ai_manager_setting', 'qtype_aitext'), - 0 - )); // Define the choices for the radio buttons. $backends = [ 'local_ai_manager' => get_string('localaimanager', 'qtype_aitext'),