diff --git a/lib/editor/tiny/plugins/aiplacement/amd/build/generatebase.min.js b/lib/editor/tiny/plugins/aiplacement/amd/build/generatebase.min.js
index 0e03509cbce..538b2084ad0 100644
--- a/lib/editor/tiny/plugins/aiplacement/amd/build/generatebase.min.js
+++ b/lib/editor/tiny/plugins/aiplacement/amd/build/generatebase.min.js
@@ -1,3 +1,3 @@
-define("tiny_aiplacement/generatebase",["exports","tiny_aiplacement/loading","core/str","tiny_aiplacement/options","core_ai/policy","core_ai/policymodal","core/custom_interaction_events","./options"],(function(_exports,_loading,_str,_options,_policy,_policymodal,_custom_interaction_events,_options2){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_policy=_interopRequireDefault(_policy),_policymodal=_interopRequireDefault(_policymodal),_custom_interaction_events=_interopRequireDefault(_custom_interaction_events);return _exports.default=class{constructor(editor){var obj,key,value;value=void 0,(key="modalObject")in(obj=this)?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,this.editor=editor,this.userid=(0,_options.getUserId)(editor),this.contextid=(0,_options.getContextId)(editor),this.responseObj=null}async displayContentModal(){if(_policy.default.preconfigurePolicyState(this.userid,(0,_options2.isPolicyAgreed)(this.editor)),await _policy.default.getPolicyStatus(this.userid))this.modalObject=await this.setupModal();else{const policyModal=await _policymodal.default.create();policyModal.getModal().on(_custom_interaction_events.default.events.activate,policyModal.getActionSelector("save"),(()=>{this.displayContentModal()}))}}getModalClass(){throw new Error("Method 'getModalClass' must be implemented.")}async setupModal(){const modal=this.getModalClass().create({templateContext:{elementid:this.editor.id}});return this.addContentEventListeners(modal),modal}async addContentEventListeners(modal){const root=(await modal).getRoot()[0];root.addEventListener("click",(e=>{this.handleContentModalClick(e,root)})),this.setupPromptArea(root),this.hideLoadingSpinner(root)}handleContentModalClick(){throw new Error("Method handleContentModalClick must be implemented.")}hideLoadingSpinner(root){const loadingSpinnerDiv=root.querySelector("#".concat(this.editor.id,"_tiny_aiplacement_spinner"));loadingSpinnerDiv.classList.add("hidden"),loadingSpinnerDiv.classList.remove("tiny-aiplacement-loading-spinner-container")}async displayLoading(root,submitBtn){let removeClass=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;const loadingSpinnerDiv=root.querySelector("#".concat(this.editor.id,"_tiny_aiplacement_spinner")),overlayDiv=root.querySelector("#".concat(this.editor.id,"_tiny_aiplacement_overlay")),blurDiv=root.querySelector("#".concat(this.editor.id,"_tiny_aiplacement_blur")),loadingTextDiv=root.querySelector("#".concat(this.editor.id,"_tiny_aiplacement_loading_text")),actionButtons=root.querySelectorAll(".tiny-aiplacement-generate-footer button");(0,_loading.loadingMessages)(loadingTextDiv),removeClass&&loadingSpinnerDiv.classList.remove(removeClass),loadingSpinnerDiv.classList.remove("hidden"),overlayDiv.classList.remove("hidden"),blurDiv.classList.add("tiny-aiplacement-blur"),submitBtn.innerHTML=await(0,_str.getString)("generating","tiny_aiplacement"),actionButtons&&actionButtons.forEach((button=>{button.disabled=!0}))}async hideLoading(root,submitBtn){const loadingSpinnerDiv=root.querySelector("#".concat(this.editor.id,"_tiny_aiplacement_spinner")),overlayDiv=root.querySelector("#".concat(this.editor.id,"_tiny_aiplacement_overlay")),blurDiv=root.querySelector("#".concat(this.editor.id,"_tiny_aiplacement_blur")),actionButtons=root.querySelectorAll(".tiny-aiplacement-generate-footer button");loadingSpinnerDiv&&loadingSpinnerDiv.classList.add("hidden"),overlayDiv&&overlayDiv.classList.add("hidden"),blurDiv&&blurDiv.classList.remove("tiny-aiplacement-blur"),submitBtn.innerHTML=await(0,_str.getString)("regenerate","tiny_aiplacement"),actionButtons&&actionButtons.forEach((button=>{button.disabled=!1}))}},_exports.default}));
+define("tiny_aiplacement/generatebase",["exports","tiny_aiplacement/loading","core/str","tiny_aiplacement/options","core_ai/policy","core_ai/policymodal","core/custom_interaction_events","./options"],(function(_exports,_loading,_str,_options,_policy,_policymodal,_custom_interaction_events,_options2){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_policy=_interopRequireDefault(_policy),_policymodal=_interopRequireDefault(_policymodal),_custom_interaction_events=_interopRequireDefault(_custom_interaction_events);return _exports.default=class{constructor(editor){var obj,key,value;value=void 0,(key="modalObject")in(obj=this)?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,this.editor=editor,this.userid=(0,_options.getUserId)(editor),this.contextid=(0,_options.getContextId)(editor),this.responseObj=null}async displayContentModal(){if(_policy.default.preconfigurePolicyState(this.userid,(0,_options2.isPolicyAgreed)(this.editor)),await _policy.default.getPolicyStatus(this.userid))this.modalObject=await this.setupModal();else{const policyModal=await _policymodal.default.create();policyModal.getModal().on(_custom_interaction_events.default.events.activate,policyModal.getActionSelector("save"),(()=>{this.displayContentModal()}))}}getModalClass(){throw new Error("Method 'getModalClass' must be implemented.")}async setupModal(){const modal=this.getModalClass().create({templateContext:{elementid:this.editor.id}});return this.addContentEventListeners(modal),modal}async addContentEventListeners(modal){const root=(await modal).getRoot()[0];root.addEventListener("click",(e=>{this.handleContentModalClick(e,root)})),this.setupPromptArea(root),this.hideLoadingSpinner(root)}handleContentModalClick(){throw new Error("Method handleContentModalClick must be implemented.")}hideLoadingSpinner(root){const loadingSpinnerDiv=root.querySelector('[id="'.concat(this.editor.id,'_tiny_aiplacement_spinner"]'));loadingSpinnerDiv.classList.add("hidden"),loadingSpinnerDiv.classList.remove("tiny-aiplacement-loading-spinner-container")}async displayLoading(root,submitBtn){let removeClass=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;const loadingSpinnerDiv=root.querySelector('[id="'.concat(this.editor.id,'_tiny_aiplacement_spinner"]')),overlayDiv=root.querySelector('[id="'.concat(this.editor.id,'_tiny_aiplacement_overlay"]')),blurDiv=root.querySelector('[id="'.concat(this.editor.id,'_tiny_aiplacement_blur"]')),loadingTextDiv=root.querySelector('[id="'.concat(this.editor.id,'_tiny_aiplacement_loading_text"]')),actionButtons=root.querySelectorAll(".tiny-aiplacement-generate-footer button");(0,_loading.loadingMessages)(loadingTextDiv),removeClass&&loadingSpinnerDiv.classList.remove(removeClass),loadingSpinnerDiv.classList.remove("hidden"),overlayDiv.classList.remove("hidden"),blurDiv.classList.add("tiny-aiplacement-blur"),submitBtn.innerHTML=await(0,_str.getString)("generating","tiny_aiplacement"),actionButtons&&actionButtons.forEach((button=>{button.disabled=!0}))}async hideLoading(root,submitBtn){const loadingSpinnerDiv=root.querySelector('[id="'.concat(this.editor.id,'_tiny_aiplacement_spinner"]')),overlayDiv=root.querySelector('[id="'.concat(this.editor.id,'_tiny_aiplacement_overlay"]')),blurDiv=root.querySelector('[id="'.concat(this.editor.id,'_tiny_aiplacement_blur"]')),actionButtons=root.querySelectorAll(".tiny-aiplacement-generate-footer button");loadingSpinnerDiv&&loadingSpinnerDiv.classList.add("hidden"),overlayDiv&&overlayDiv.classList.add("hidden"),blurDiv&&blurDiv.classList.remove("tiny-aiplacement-blur"),submitBtn.innerHTML=await(0,_str.getString)("regenerate","tiny_aiplacement"),actionButtons&&actionButtons.forEach((button=>{button.disabled=!1}))}},_exports.default}));
//# sourceMappingURL=generatebase.min.js.map
\ No newline at end of file
diff --git a/lib/editor/tiny/plugins/aiplacement/amd/build/generatebase.min.js.map b/lib/editor/tiny/plugins/aiplacement/amd/build/generatebase.min.js.map
index 114fbab3670..151595ea6be 100644
--- a/lib/editor/tiny/plugins/aiplacement/amd/build/generatebase.min.js.map
+++ b/lib/editor/tiny/plugins/aiplacement/amd/build/generatebase.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"generatebase.min.js","sources":["../src/generatebase.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 * Tiny AI base generate class.\n *\n * @module tiny_aiplacement/generatebase\n * @copyright 2024 Matt Porritt \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {loadingMessages} from 'tiny_aiplacement/loading';\nimport {getString} from 'core/str';\nimport {\n getContextId,\n getUserId,\n} from 'tiny_aiplacement/options';\nimport Policy from 'core_ai/policy';\nimport PolicyModal from 'core_ai/policymodal';\nimport CustomEvents from 'core/custom_interaction_events';\nimport {isPolicyAgreed} from './options';\n\nexport default class GenerateBase {\n modalObject;\n\n /**\n * Class constructor.\n *\n * @param {TinyMCE.editor} editor The tinyMCE editor instance.\n */\n constructor(editor) {\n this.editor = editor;\n this.userid = getUserId(editor);\n this.contextid = getContextId(editor);\n this.responseObj = null;\n }\n\n /**\n * Display the modal when the AI button is clicked.\n *\n */\n async displayContentModal() {\n Policy.preconfigurePolicyState(this.userid, isPolicyAgreed(this.editor));\n if (!await Policy.getPolicyStatus(this.userid)) {\n const policyModal = await PolicyModal.create();\n policyModal.getModal().on(CustomEvents.events.activate, policyModal.getActionSelector('save'), () => {\n this.displayContentModal();\n });\n return;\n }\n\n this.modalObject = await this.setupModal();\n }\n\n getModalClass() {\n throw new Error(\"Method 'getModalClass' must be implemented.\");\n }\n\n\n /**\n * Set up the base text generation modal with default body content.\n *\n * @returns {TextModal} The image modal object.\n */\n async setupModal() {\n const modal = this.getModalClass().create({\n templateContext: {\n elementid: this.editor.id,\n },\n });\n\n this.addContentEventListeners(modal);\n\n return modal;\n }\n\n /**\n * Add event listeners for the text modal.\n *\n * @param {Modal} modal\n */\n async addContentEventListeners(modal) {\n const modalRoot = (await modal).getRoot();\n const root = modalRoot[0];\n\n root.addEventListener('click', (e) => {\n this.handleContentModalClick(e, root);\n });\n\n this.setupPromptArea(root);\n this.hideLoadingSpinner(root);\n }\n\n handleContentModalClick() {\n throw new Error('Method handleContentModalClick must be implemented.');\n }\n\n /**\n * Hide the loading spinner.\n *\n * @param {Object} root The root element of the modal.\n */\n hideLoadingSpinner(root) {\n const loadingSpinnerDiv = root.querySelector(`#${this.editor.id}_tiny_aiplacement_spinner`);\n loadingSpinnerDiv.classList.add('hidden');\n loadingSpinnerDiv.classList.remove('tiny-aiplacement-loading-spinner-container');\n }\n\n /**\n * Display the loading state in the modal.\n *\n * @param {HTMLElement} root - The root element of the modal.\n * @param {HTMLElement} submitBtn - The submit button element.\n * @param {String|null} removeClass - The class to be removed from the loading spinner div, if any.\n */\n async displayLoading(root, submitBtn, removeClass = null) {\n const loadingSpinnerDiv = root.querySelector(`#${this.editor.id}_tiny_aiplacement_spinner`);\n const overlayDiv = root.querySelector(`#${this.editor.id}_tiny_aiplacement_overlay`);\n const blurDiv = root.querySelector(`#${this.editor.id}_tiny_aiplacement_blur`);\n const loadingTextDiv = root.querySelector(`#${this.editor.id}_tiny_aiplacement_loading_text`);\n const actionButtons = root.querySelectorAll('.tiny-aiplacement-generate-footer button');\n\n loadingMessages(loadingTextDiv);\n\n if (removeClass) {\n loadingSpinnerDiv.classList.remove(removeClass);\n }\n\n loadingSpinnerDiv.classList.remove('hidden');\n overlayDiv.classList.remove('hidden');\n blurDiv.classList.add('tiny-aiplacement-blur');\n submitBtn.innerHTML = await getString('generating', 'tiny_aiplacement');\n\n if (actionButtons) {\n actionButtons.forEach((button) => {\n button.disabled = true;\n });\n }\n }\n\n /**\n * Hide the loading action in the modal.\n *\n * @param {Object} root The root element of the modal.\n * @param {Object} submitBtn The submit button element.\n */\n async hideLoading(root, submitBtn) {\n const loadingSpinnerDiv = root.querySelector(`#${this.editor.id}_tiny_aiplacement_spinner`);\n const overlayDiv = root.querySelector(`#${this.editor.id}_tiny_aiplacement_overlay`);\n const blurDiv = root.querySelector(`#${this.editor.id}_tiny_aiplacement_blur`);\n const actionButtons = root.querySelectorAll('.tiny-aiplacement-generate-footer button');\n if (loadingSpinnerDiv) {\n loadingSpinnerDiv.classList.add('hidden');\n }\n if (overlayDiv) {\n overlayDiv.classList.add('hidden');\n }\n if (blurDiv) {\n blurDiv.classList.remove('tiny-aiplacement-blur');\n }\n submitBtn.innerHTML = await getString('regenerate', 'tiny_aiplacement');\n\n if (actionButtons) {\n actionButtons.forEach((button) => {\n button.disabled = false;\n });\n }\n }\n}\n"],"names":["constructor","editor","userid","contextid","responseObj","preconfigurePolicyState","this","Policy","getPolicyStatus","modalObject","setupModal","policyModal","PolicyModal","create","getModal","on","CustomEvents","events","activate","getActionSelector","displayContentModal","getModalClass","Error","modal","templateContext","elementid","id","addContentEventListeners","root","getRoot","addEventListener","e","handleContentModalClick","setupPromptArea","hideLoadingSpinner","loadingSpinnerDiv","querySelector","classList","add","remove","submitBtn","removeClass","overlayDiv","blurDiv","loadingTextDiv","actionButtons","querySelectorAll","innerHTML","forEach","button","disabled"],"mappings":"qpBA0CIA,YAAYC,kLACHA,OAASA,YACTC,QAAS,sBAAUD,aACnBE,WAAY,yBAAaF,aACzBG,YAAc,oDAQZC,wBAAwBC,KAAKJ,QAAQ,4BAAeI,KAAKL,eACrDM,gBAAOC,gBAAgBF,KAAKJ,aAQlCO,kBAAoBH,KAAKI,wBAPpBC,kBAAoBC,qBAAYC,SACtCF,YAAYG,WAAWC,GAAGC,mCAAaC,OAAOC,SAAUP,YAAYQ,kBAAkB,SAAS,UACtFC,0BAQjBC,sBACU,IAAIC,MAAM,wEAUVC,MAAQjB,KAAKe,gBAAgBR,OAAO,CACtCW,gBAAiB,CACbC,UAAWnB,KAAKL,OAAOyB,kBAI1BC,yBAAyBJ,OAEvBA,qCAQoBA,aAErBK,YADmBL,OAAOM,UACT,GAEvBD,KAAKE,iBAAiB,SAAUC,SACvBC,wBAAwBD,EAAGH,cAG/BK,gBAAgBL,WAChBM,mBAAmBN,MAG5BI,gCACU,IAAIV,MAAM,uDAQpBY,mBAAmBN,YACTO,kBAAoBP,KAAKQ,yBAAkB9B,KAAKL,OAAOyB,iCAC7DS,kBAAkBE,UAAUC,IAAI,UAChCH,kBAAkBE,UAAUE,OAAO,mEAUlBX,KAAMY,eAAWC,mEAAc,WAC1CN,kBAAoBP,KAAKQ,yBAAkB9B,KAAKL,OAAOyB,iCACvDgB,WAAad,KAAKQ,yBAAkB9B,KAAKL,OAAOyB,iCAChDiB,QAAUf,KAAKQ,yBAAkB9B,KAAKL,OAAOyB,8BAC7CkB,eAAiBhB,KAAKQ,yBAAkB9B,KAAKL,OAAOyB,sCACpDmB,cAAgBjB,KAAKkB,iBAAiB,yEAE5BF,gBAEZH,aACAN,kBAAkBE,UAAUE,OAAOE,aAGvCN,kBAAkBE,UAAUE,OAAO,UACnCG,WAAWL,UAAUE,OAAO,UAC5BI,QAAQN,UAAUC,IAAI,yBACtBE,UAAUO,gBAAkB,kBAAU,aAAc,oBAEhDF,eACAA,cAAcG,SAASC,SACnBA,OAAOC,UAAW,uBAWZtB,KAAMY,iBACdL,kBAAoBP,KAAKQ,yBAAkB9B,KAAKL,OAAOyB,iCACvDgB,WAAad,KAAKQ,yBAAkB9B,KAAKL,OAAOyB,iCAChDiB,QAAUf,KAAKQ,yBAAkB9B,KAAKL,OAAOyB,8BAC7CmB,cAAgBjB,KAAKkB,iBAAiB,4CACxCX,mBACAA,kBAAkBE,UAAUC,IAAI,UAEhCI,YACAA,WAAWL,UAAUC,IAAI,UAEzBK,SACAA,QAAQN,UAAUE,OAAO,yBAE7BC,UAAUO,gBAAkB,kBAAU,aAAc,oBAEhDF,eACAA,cAAcG,SAASC,SACnBA,OAAOC,UAAW"}
\ No newline at end of file
+{"version":3,"file":"generatebase.min.js","sources":["../src/generatebase.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 * Tiny AI base generate class.\n *\n * @module tiny_aiplacement/generatebase\n * @copyright 2024 Matt Porritt \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {loadingMessages} from 'tiny_aiplacement/loading';\nimport {getString} from 'core/str';\nimport {\n getContextId,\n getUserId,\n} from 'tiny_aiplacement/options';\nimport Policy from 'core_ai/policy';\nimport PolicyModal from 'core_ai/policymodal';\nimport CustomEvents from 'core/custom_interaction_events';\nimport {isPolicyAgreed} from './options';\n\nexport default class GenerateBase {\n modalObject;\n\n /**\n * Class constructor.\n *\n * @param {TinyMCE.editor} editor The tinyMCE editor instance.\n */\n constructor(editor) {\n this.editor = editor;\n this.userid = getUserId(editor);\n this.contextid = getContextId(editor);\n this.responseObj = null;\n }\n\n /**\n * Display the modal when the AI button is clicked.\n *\n */\n async displayContentModal() {\n Policy.preconfigurePolicyState(this.userid, isPolicyAgreed(this.editor));\n if (!await Policy.getPolicyStatus(this.userid)) {\n const policyModal = await PolicyModal.create();\n policyModal.getModal().on(CustomEvents.events.activate, policyModal.getActionSelector('save'), () => {\n this.displayContentModal();\n });\n return;\n }\n\n this.modalObject = await this.setupModal();\n }\n\n getModalClass() {\n throw new Error(\"Method 'getModalClass' must be implemented.\");\n }\n\n\n /**\n * Set up the base text generation modal with default body content.\n *\n * @returns {TextModal} The image modal object.\n */\n async setupModal() {\n const modal = this.getModalClass().create({\n templateContext: {\n elementid: this.editor.id,\n },\n });\n\n this.addContentEventListeners(modal);\n\n return modal;\n }\n\n /**\n * Add event listeners for the text modal.\n *\n * @param {Modal} modal\n */\n async addContentEventListeners(modal) {\n const modalRoot = (await modal).getRoot();\n const root = modalRoot[0];\n\n root.addEventListener('click', (e) => {\n this.handleContentModalClick(e, root);\n });\n\n this.setupPromptArea(root);\n this.hideLoadingSpinner(root);\n }\n\n handleContentModalClick() {\n throw new Error('Method handleContentModalClick must be implemented.');\n }\n\n /**\n * Hide the loading spinner.\n *\n * @param {Object} root The root element of the modal.\n */\n hideLoadingSpinner(root) {\n const loadingSpinnerDiv = root.querySelector(`[id=\"${this.editor.id}_tiny_aiplacement_spinner\"]`);\n loadingSpinnerDiv.classList.add('hidden');\n loadingSpinnerDiv.classList.remove('tiny-aiplacement-loading-spinner-container');\n }\n\n /**\n * Display the loading state in the modal.\n *\n * @param {HTMLElement} root - The root element of the modal.\n * @param {HTMLElement} submitBtn - The submit button element.\n * @param {String|null} removeClass - The class to be removed from the loading spinner div, if any.\n */\n async displayLoading(root, submitBtn, removeClass = null) {\n const loadingSpinnerDiv = root.querySelector(`[id=\"${this.editor.id}_tiny_aiplacement_spinner\"]`);\n const overlayDiv = root.querySelector(`[id=\"${this.editor.id}_tiny_aiplacement_overlay\"]`);\n const blurDiv = root.querySelector(`[id=\"${this.editor.id}_tiny_aiplacement_blur\"]`);\n const loadingTextDiv = root.querySelector(`[id=\"${this.editor.id}_tiny_aiplacement_loading_text\"]`);\n const actionButtons = root.querySelectorAll('.tiny-aiplacement-generate-footer button');\n\n loadingMessages(loadingTextDiv);\n\n if (removeClass) {\n loadingSpinnerDiv.classList.remove(removeClass);\n }\n\n loadingSpinnerDiv.classList.remove('hidden');\n overlayDiv.classList.remove('hidden');\n blurDiv.classList.add('tiny-aiplacement-blur');\n submitBtn.innerHTML = await getString('generating', 'tiny_aiplacement');\n\n if (actionButtons) {\n actionButtons.forEach((button) => {\n button.disabled = true;\n });\n }\n }\n\n /**\n * Hide the loading action in the modal.\n *\n * @param {Object} root The root element of the modal.\n * @param {Object} submitBtn The submit button element.\n */\n async hideLoading(root, submitBtn) {\n const loadingSpinnerDiv = root.querySelector(`[id=\"${this.editor.id}_tiny_aiplacement_spinner\"]`);\n const overlayDiv = root.querySelector(`[id=\"${this.editor.id}_tiny_aiplacement_overlay\"]`);\n const blurDiv = root.querySelector(`[id=\"${this.editor.id}_tiny_aiplacement_blur\"]`);\n const actionButtons = root.querySelectorAll('.tiny-aiplacement-generate-footer button');\n if (loadingSpinnerDiv) {\n loadingSpinnerDiv.classList.add('hidden');\n }\n if (overlayDiv) {\n overlayDiv.classList.add('hidden');\n }\n if (blurDiv) {\n blurDiv.classList.remove('tiny-aiplacement-blur');\n }\n submitBtn.innerHTML = await getString('regenerate', 'tiny_aiplacement');\n\n if (actionButtons) {\n actionButtons.forEach((button) => {\n button.disabled = false;\n });\n }\n }\n}\n"],"names":["constructor","editor","userid","contextid","responseObj","preconfigurePolicyState","this","Policy","getPolicyStatus","modalObject","setupModal","policyModal","PolicyModal","create","getModal","on","CustomEvents","events","activate","getActionSelector","displayContentModal","getModalClass","Error","modal","templateContext","elementid","id","addContentEventListeners","root","getRoot","addEventListener","e","handleContentModalClick","setupPromptArea","hideLoadingSpinner","loadingSpinnerDiv","querySelector","classList","add","remove","submitBtn","removeClass","overlayDiv","blurDiv","loadingTextDiv","actionButtons","querySelectorAll","innerHTML","forEach","button","disabled"],"mappings":"qpBA0CIA,YAAYC,kLACHA,OAASA,YACTC,QAAS,sBAAUD,aACnBE,WAAY,yBAAaF,aACzBG,YAAc,oDAQZC,wBAAwBC,KAAKJ,QAAQ,4BAAeI,KAAKL,eACrDM,gBAAOC,gBAAgBF,KAAKJ,aAQlCO,kBAAoBH,KAAKI,wBAPpBC,kBAAoBC,qBAAYC,SACtCF,YAAYG,WAAWC,GAAGC,mCAAaC,OAAOC,SAAUP,YAAYQ,kBAAkB,SAAS,UACtFC,0BAQjBC,sBACU,IAAIC,MAAM,wEAUVC,MAAQjB,KAAKe,gBAAgBR,OAAO,CACtCW,gBAAiB,CACbC,UAAWnB,KAAKL,OAAOyB,kBAI1BC,yBAAyBJ,OAEvBA,qCAQoBA,aAErBK,YADmBL,OAAOM,UACT,GAEvBD,KAAKE,iBAAiB,SAAUC,SACvBC,wBAAwBD,EAAGH,cAG/BK,gBAAgBL,WAChBM,mBAAmBN,MAG5BI,gCACU,IAAIV,MAAM,uDAQpBY,mBAAmBN,YACTO,kBAAoBP,KAAKQ,6BAAsB9B,KAAKL,OAAOyB,mCACjES,kBAAkBE,UAAUC,IAAI,UAChCH,kBAAkBE,UAAUE,OAAO,mEAUlBX,KAAMY,eAAWC,mEAAc,WAC1CN,kBAAoBP,KAAKQ,6BAAsB9B,KAAKL,OAAOyB,mCAC3DgB,WAAad,KAAKQ,6BAAsB9B,KAAKL,OAAOyB,mCACpDiB,QAAUf,KAAKQ,6BAAsB9B,KAAKL,OAAOyB,gCACjDkB,eAAiBhB,KAAKQ,6BAAsB9B,KAAKL,OAAOyB,wCACxDmB,cAAgBjB,KAAKkB,iBAAiB,yEAE5BF,gBAEZH,aACAN,kBAAkBE,UAAUE,OAAOE,aAGvCN,kBAAkBE,UAAUE,OAAO,UACnCG,WAAWL,UAAUE,OAAO,UAC5BI,QAAQN,UAAUC,IAAI,yBACtBE,UAAUO,gBAAkB,kBAAU,aAAc,oBAEhDF,eACAA,cAAcG,SAASC,SACnBA,OAAOC,UAAW,uBAWZtB,KAAMY,iBACdL,kBAAoBP,KAAKQ,6BAAsB9B,KAAKL,OAAOyB,mCAC3DgB,WAAad,KAAKQ,6BAAsB9B,KAAKL,OAAOyB,mCACpDiB,QAAUf,KAAKQ,6BAAsB9B,KAAKL,OAAOyB,gCACjDmB,cAAgBjB,KAAKkB,iBAAiB,4CACxCX,mBACAA,kBAAkBE,UAAUC,IAAI,UAEhCI,YACAA,WAAWL,UAAUC,IAAI,UAEzBK,SACAA,QAAQN,UAAUE,OAAO,yBAE7BC,UAAUO,gBAAkB,kBAAU,aAAc,oBAEhDF,eACAA,cAAcG,SAASC,SACnBA,OAAOC,UAAW"}
\ No newline at end of file
diff --git a/lib/editor/tiny/plugins/aiplacement/amd/build/generateimage.min.js b/lib/editor/tiny/plugins/aiplacement/amd/build/generateimage.min.js
index 8fc5c743464..68fae4d05d7 100644
--- a/lib/editor/tiny/plugins/aiplacement/amd/build/generateimage.min.js
+++ b/lib/editor/tiny/plugins/aiplacement/amd/build/generateimage.min.js
@@ -1,3 +1,3 @@
-define("tiny_aiplacement/generateimage",["exports","tiny_aiplacement/imagemodal","core/ajax","core/str","core/templates","./mediaimage","tiny_aiplacement/options","tiny_aiplacement/generatebase"],(function(_exports,_imagemodal,_ajax,_str,_templates,_mediaimage,_options,_generatebase){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_imagemodal=_interopRequireDefault(_imagemodal),_ajax=_interopRequireDefault(_ajax),_templates=_interopRequireDefault(_templates),_mediaimage=_interopRequireDefault(_mediaimage),_generatebase=_interopRequireDefault(_generatebase);class GenerateImage extends _generatebase.default{constructor(){super(...arguments),_defineProperty(this,"SELECTORS",{GENERATEBUTTON:()=>"#".concat(this.editor.id,"_tiny_aiplacement_generatebutton"),PROMPTAREA:()=>"#".concat(this.editor.id,"_tiny_aiplacement_imageprompt"),IMAGECONTAINER:()=>"#".concat(this.editor.id,"_tiny_aiplacement_generate_image"),GENERATEBTN:'[data-action="generate"]',INSERTBTN:'[data-action="inserter"]',BACKTBTN:'[data-action="back"]',GENERATEDIMAGE:()=>"#".concat(this.editor.id,"_tiny_generated_image")}),_defineProperty(this,"imageURL",null)}getModalClass(){return _imagemodal.default}handleContentModalClick(e,root){const actions={generate:()=>this.handleSubmit(root,e.target),inserter:()=>this.handleInsert(),cancel:()=>this.modalObject.destroy(),back:()=>{this.modalObject.destroy(),this.displayContentModal()}},actionKey=Object.keys(actions).find((key=>e.target.closest('[data-action="'.concat(key,'"]'))));actionKey&&(e.preventDefault(),actions[actionKey]())}setupPromptArea(root){const generateBtn=root.querySelector(this.SELECTORS.GENERATEBUTTON()),promptArea=root.querySelector(this.SELECTORS.PROMPTAREA());promptArea.addEventListener("input",(()=>{generateBtn.disabled=""===promptArea.value.trim()}))}async handleSubmit(root,submitBtn){await this.displayLoading(root,submitBtn);const request={methodname:"aiplacement_editor_generate_image",args:this.getDisplayArgs(root)};try{this.responseObj=await _ajax.default.call([request])[0],this.responseObj.error?this.handleGenerationError(root,submitBtn,""):(await this.displayGeneratedImage(root),this.hideLoading(root,submitBtn),window.console.log(this.responseObj))}catch(error){this.handleGenerationError(root,submitBtn,"")}}async handleInsert(){const mediaImage=new _mediaimage.default(this.editor,this.imageURL,this.promptText);await mediaImage.displayDialogue(),this.modalObject.destroy()}async handleGenerationError(root,submitBtn){let errorMessage=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"";errorMessage||(errorMessage=await(0,_str.getString)("errorgeneral","tiny_aiplacement")),this.hideLoading(root,submitBtn),this.modalObject.setBody(_templates.default.render("tiny_aiplacement/modalbodyerror",{errorMessage:errorMessage}));const backBtn=root.querySelector(this.SELECTORS.BACKTBTN),generateBtn=root.querySelector(this.SELECTORS.GENERATEBUTTON());backBtn.classList.remove("hidden"),generateBtn.classList.add("hidden")}async displayGeneratedImage(root){const imageDisplayContainer=root.querySelector(this.SELECTORS.IMAGECONTAINER()),insertBtn=root.querySelector(this.SELECTORS.INSERTBTN);this.imageURL=this.responseObj.drafturl,imageDisplayContainer.innerHTML=await _templates.default.render("tiny_aiplacement/image",{url:this.responseObj.drafturl,elementid:this.editor.id,alt:this.promptText});const imagElement=root.querySelector(this.SELECTORS.GENERATEDIMAGE());return new Promise(((resolve,reject)=>{imagElement.onload=()=>{insertBtn.classList.remove("hidden"),resolve()},imagElement.onerror=error=>{reject(error)}}))}getDisplayArgs(root){const contextId=(0,_options.getContextId)(this.editor),promptText=root.querySelector(this.SELECTORS.PROMPTAREA()).value;this.promptText=promptText;return{contextid:contextId,prompttext:promptText,aspectratio:this.getSelectedRadioValue("aspect-ratio","square"),quality:this.getSelectedRadioValue("quality","standard")?"hd":"standard",numimages:1}}getSelectedRadioValue(radioName){let defaultValue=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;const radios=document.getElementsByName(radioName);for(const radio of radios)if(radio.checked)return radio.value;return defaultValue}}return _exports.default=GenerateImage,_exports.default}));
+define("tiny_aiplacement/generateimage",["exports","tiny_aiplacement/imagemodal","core/ajax","core/str","core/templates","./mediaimage","tiny_aiplacement/options","tiny_aiplacement/generatebase"],(function(_exports,_imagemodal,_ajax,_str,_templates,_mediaimage,_options,_generatebase){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_imagemodal=_interopRequireDefault(_imagemodal),_ajax=_interopRequireDefault(_ajax),_templates=_interopRequireDefault(_templates),_mediaimage=_interopRequireDefault(_mediaimage),_generatebase=_interopRequireDefault(_generatebase);class GenerateImage extends _generatebase.default{constructor(){super(...arguments),_defineProperty(this,"SELECTORS",{GENERATEBUTTON:()=>'[id="'.concat(this.editor.id,'_tiny_aiplacement_generatebutton"]'),PROMPTAREA:()=>'[id="'.concat(this.editor.id,'_tiny_aiplacement_imageprompt"]'),IMAGECONTAINER:()=>'[id="'.concat(this.editor.id,'_tiny_aiplacement_generate_image"]'),GENERATEBTN:'[data-action="generate"]',INSERTBTN:'[data-action="inserter"]',BACKTBTN:'[data-action="back"]',GENERATEDIMAGE:()=>'[id="'.concat(this.editor.id,'_tiny_generated_image"]')}),_defineProperty(this,"imageURL",null)}getModalClass(){return _imagemodal.default}handleContentModalClick(e,root){const actions={generate:()=>this.handleSubmit(root,e.target),inserter:()=>this.handleInsert(),cancel:()=>this.modalObject.destroy(),back:()=>{this.modalObject.destroy(),this.displayContentModal()}},actionKey=Object.keys(actions).find((key=>e.target.closest('[data-action="'.concat(key,'"]'))));actionKey&&(e.preventDefault(),actions[actionKey]())}setupPromptArea(root){const generateBtn=root.querySelector(this.SELECTORS.GENERATEBUTTON()),promptArea=root.querySelector(this.SELECTORS.PROMPTAREA());promptArea.addEventListener("input",(()=>{generateBtn.disabled=""===promptArea.value.trim()}))}async handleSubmit(root,submitBtn){await this.displayLoading(root,submitBtn);const request={methodname:"aiplacement_editor_generate_image",args:this.getDisplayArgs(root)};try{this.responseObj=await _ajax.default.call([request])[0],this.responseObj.error?this.handleGenerationError(root,submitBtn,""):(await this.displayGeneratedImage(root),this.hideLoading(root,submitBtn),window.console.log(this.responseObj))}catch(error){this.handleGenerationError(root,submitBtn,"")}}async handleInsert(){const mediaImage=new _mediaimage.default(this.editor,this.imageURL,this.promptText);await mediaImage.displayDialogue(),this.modalObject.destroy()}async handleGenerationError(root,submitBtn){let errorMessage=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"";errorMessage||(errorMessage=await(0,_str.getString)("errorgeneral","tiny_aiplacement")),this.hideLoading(root,submitBtn),this.modalObject.setBody(_templates.default.render("tiny_aiplacement/modalbodyerror",{errorMessage:errorMessage}));const backBtn=root.querySelector(this.SELECTORS.BACKTBTN),generateBtn=root.querySelector(this.SELECTORS.GENERATEBUTTON());backBtn.classList.remove("hidden"),generateBtn.classList.add("hidden")}async displayGeneratedImage(root){const imageDisplayContainer=root.querySelector(this.SELECTORS.IMAGECONTAINER()),insertBtn=root.querySelector(this.SELECTORS.INSERTBTN);this.imageURL=this.responseObj.drafturl,imageDisplayContainer.innerHTML=await _templates.default.render("tiny_aiplacement/image",{url:this.responseObj.drafturl,elementid:this.editor.id,alt:this.promptText});const imagElement=root.querySelector(this.SELECTORS.GENERATEDIMAGE());return new Promise(((resolve,reject)=>{imagElement.onload=()=>{insertBtn.classList.remove("hidden"),resolve()},imagElement.onerror=error=>{reject(error)}}))}getDisplayArgs(root){const contextId=(0,_options.getContextId)(this.editor),promptText=root.querySelector(this.SELECTORS.PROMPTAREA()).value;this.promptText=promptText;return{contextid:contextId,prompttext:promptText,aspectratio:this.getSelectedRadioValue("aspect-ratio","square"),quality:this.getSelectedRadioValue("quality","standard")?"hd":"standard",numimages:1}}getSelectedRadioValue(radioName){let defaultValue=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;const radios=document.getElementsByName(radioName);for(const radio of radios)if(radio.checked)return radio.value;return defaultValue}}return _exports.default=GenerateImage,_exports.default}));
//# sourceMappingURL=generateimage.min.js.map
\ No newline at end of file
diff --git a/lib/editor/tiny/plugins/aiplacement/amd/build/generateimage.min.js.map b/lib/editor/tiny/plugins/aiplacement/amd/build/generateimage.min.js.map
index 62d988304b9..ce4c7404007 100644
--- a/lib/editor/tiny/plugins/aiplacement/amd/build/generateimage.min.js.map
+++ b/lib/editor/tiny/plugins/aiplacement/amd/build/generateimage.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"generateimage.min.js","sources":["../src/generateimage.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 * Tiny AI generate images.\n *\n * @module tiny_aiplacement/generateimage\n * @copyright 2024 Matt Porritt \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ImageModal from 'tiny_aiplacement/imagemodal';\nimport Ajax from 'core/ajax';\nimport {getString} from 'core/str';\nimport Templates from 'core/templates';\nimport AiMediaImage from './mediaimage';\nimport {getContextId} from 'tiny_aiplacement/options';\nimport GenerateBase from 'tiny_aiplacement/generatebase';\n\nexport default class GenerateImage extends GenerateBase {\n SELECTORS = {\n GENERATEBUTTON: () => `#${this.editor.id}_tiny_aiplacement_generatebutton`,\n PROMPTAREA: () => `#${this.editor.id}_tiny_aiplacement_imageprompt`,\n IMAGECONTAINER: () => `#${this.editor.id}_tiny_aiplacement_generate_image`,\n GENERATEBTN: '[data-action=\"generate\"]',\n INSERTBTN: '[data-action=\"inserter\"]',\n BACKTBTN: '[data-action=\"back\"]',\n GENERATEDIMAGE: () => `#${this.editor.id}_tiny_generated_image`,\n };\n\n imageURL = null;\n\n getModalClass() {\n return ImageModal;\n }\n\n /**\n * Handle click events within the image modal.\n *\n * @param {Event} e - The click event object.\n * @param {HTMLElement} root - The root element of the modal.\n */\n handleContentModalClick(e, root) {\n const actions = {\n generate: () => this.handleSubmit(root, e.target),\n inserter: () => this.handleInsert(),\n cancel: () => this.modalObject.destroy(),\n back: () => {\n this.modalObject.destroy();\n this.displayContentModal();\n },\n };\n\n const actionKey = Object.keys(actions).find(key => e.target.closest(`[data-action=\"${key}\"]`));\n if (actionKey) {\n e.preventDefault();\n actions[actionKey]();\n }\n }\n\n /**\n * Set up the prompt area in the modal, adding necessary event listeners.\n *\n * @param {HTMLElement} root - The root element of the modal.\n */\n setupPromptArea(root) {\n const generateBtn = root.querySelector(this.SELECTORS.GENERATEBUTTON());\n const promptArea = root.querySelector(this.SELECTORS.PROMPTAREA());\n\n promptArea.addEventListener('input', () => {\n generateBtn.disabled = promptArea.value.trim() === '';\n });\n }\n\n /**\n * Handle the submit action.\n *\n * @param {Object} root The root element of the modal.\n * @param {Object} submitBtn The submit button element.\n */\n async handleSubmit(root, submitBtn) {\n await this.displayLoading(root, submitBtn);\n\n const displayArgs = this.getDisplayArgs(root);\n const request = {\n methodname: 'aiplacement_editor_generate_image',\n args: displayArgs\n };\n\n try {\n this.responseObj = await Ajax.call([request])[0];\n if (this.responseObj.error) {\n this.handleGenerationError(root, submitBtn, '');\n } else {\n await this.displayGeneratedImage(root);\n this.hideLoading(root, submitBtn);\n window.console.log(this.responseObj);\n }\n } catch (error) {\n this.handleGenerationError(root, submitBtn, '');\n }\n }\n\n /**\n * Handle the insert action.\n *\n */\n async handleInsert() {\n const mediaImage = new AiMediaImage(this.editor, this.imageURL, this.promptText);\n await mediaImage.displayDialogue();\n this.modalObject.destroy();\n }\n\n /**\n * Handle a generation error.\n *\n * @param {Object} root The root element of the modal.\n * @param {Object} submitBtn The submit button element.\n * @param {String} errorMessage The error message to display.\n */\n async handleGenerationError(root, submitBtn, errorMessage = '') {\n if (!errorMessage) {\n // Get the default error message.\n errorMessage = await getString('errorgeneral', 'tiny_aiplacement');\n }\n this.hideLoading(root, submitBtn);\n this.modalObject.setBody(Templates.render('tiny_aiplacement/modalbodyerror', {'errorMessage': errorMessage}));\n const backBtn = root.querySelector(this.SELECTORS.BACKTBTN);\n const generateBtn = root.querySelector(this.SELECTORS.GENERATEBUTTON());\n backBtn.classList.remove('hidden');\n generateBtn.classList.add('hidden');\n }\n\n /**\n * Display the generated image in the modal.\n *\n * @param {HTMLElement} root - The root element of the modal.\n */\n async displayGeneratedImage(root) {\n const imageDisplayContainer = root.querySelector(this.SELECTORS.IMAGECONTAINER());\n const insertBtn = root.querySelector(this.SELECTORS.INSERTBTN);\n // Set the draft URL as it's used elsewhere.\n this.imageURL = this.responseObj.drafturl;\n\n // Render the image template and insert it into the modal.\n imageDisplayContainer.innerHTML = await Templates.render('tiny_aiplacement/image', {\n url: this.responseObj.drafturl,\n elementid: this.editor.id,\n alt: this.promptText,\n });\n const imagElement = root.querySelector(this.SELECTORS.GENERATEDIMAGE());\n\n return new Promise((resolve, reject) => {\n imagElement.onload = () => {\n insertBtn.classList.remove('hidden');\n resolve(); // Resolve the promise when the image is loaded.\n };\n imagElement.onerror = (error) => {\n reject(error); // Reject the promise if there is an error loading the image.\n };\n });\n }\n\n /**\n * Get the display args for the image.\n *\n * @param {Object} root The root element of the modal.\n */\n getDisplayArgs(root) {\n const contextId = getContextId(this.editor);\n const promptText = root.querySelector(this.SELECTORS.PROMPTAREA()).value;\n this.promptText = promptText;\n\n const aspectRatio = this.getSelectedRadioValue('aspect-ratio', 'square');\n const imageQuality = this.getSelectedRadioValue('quality', 'standard');\n\n return {\n contextid: contextId,\n prompttext: promptText,\n aspectratio: aspectRatio,\n quality: imageQuality ? 'hd' : 'standard',\n numimages: 1\n };\n }\n\n /**\n * Get the value of the selected radio button.\n *\n * @param {String} radioName The name of the radio button group.\n * @param {String} defaultValue The default value of the radio button.\n */\n getSelectedRadioValue(radioName, defaultValue = null) {\n const radios = document.getElementsByName(radioName);\n for (const radio of radios) {\n if (radio.checked) {\n return radio.value;\n }\n }\n return defaultValue;\n }\n}\n"],"names":["GenerateImage","GenerateBase","GENERATEBUTTON","this","editor","id","PROMPTAREA","IMAGECONTAINER","GENERATEBTN","INSERTBTN","BACKTBTN","GENERATEDIMAGE","getModalClass","ImageModal","handleContentModalClick","e","root","actions","generate","handleSubmit","target","inserter","handleInsert","cancel","modalObject","destroy","back","displayContentModal","actionKey","Object","keys","find","key","closest","preventDefault","setupPromptArea","generateBtn","querySelector","SELECTORS","promptArea","addEventListener","disabled","value","trim","submitBtn","displayLoading","request","methodname","args","getDisplayArgs","responseObj","Ajax","call","error","handleGenerationError","displayGeneratedImage","hideLoading","window","console","log","mediaImage","AiMediaImage","imageURL","promptText","displayDialogue","errorMessage","setBody","Templates","render","backBtn","classList","remove","add","imageDisplayContainer","insertBtn","drafturl","innerHTML","url","elementid","alt","imagElement","Promise","resolve","reject","onload","onerror","contextId","contextid","prompttext","aspectratio","getSelectedRadioValue","quality","numimages","radioName","defaultValue","radios","document","getElementsByName","radio","checked"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"generateimage.min.js","sources":["../src/generateimage.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 * Tiny AI generate images.\n *\n * @module tiny_aiplacement/generateimage\n * @copyright 2024 Matt Porritt \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ImageModal from 'tiny_aiplacement/imagemodal';\nimport Ajax from 'core/ajax';\nimport {getString} from 'core/str';\nimport Templates from 'core/templates';\nimport AiMediaImage from './mediaimage';\nimport {getContextId} from 'tiny_aiplacement/options';\nimport GenerateBase from 'tiny_aiplacement/generatebase';\n\nexport default class GenerateImage extends GenerateBase {\n SELECTORS = {\n GENERATEBUTTON: () => `[id=\"${this.editor.id}_tiny_aiplacement_generatebutton\"]`,\n PROMPTAREA: () => `[id=\"${this.editor.id}_tiny_aiplacement_imageprompt\"]`,\n IMAGECONTAINER: () => `[id=\"${this.editor.id}_tiny_aiplacement_generate_image\"]`,\n GENERATEBTN: '[data-action=\"generate\"]',\n INSERTBTN: '[data-action=\"inserter\"]',\n BACKTBTN: '[data-action=\"back\"]',\n GENERATEDIMAGE: () => `[id=\"${this.editor.id}_tiny_generated_image\"]`,\n };\n\n imageURL = null;\n\n getModalClass() {\n return ImageModal;\n }\n\n /**\n * Handle click events within the image modal.\n *\n * @param {Event} e - The click event object.\n * @param {HTMLElement} root - The root element of the modal.\n */\n handleContentModalClick(e, root) {\n const actions = {\n generate: () => this.handleSubmit(root, e.target),\n inserter: () => this.handleInsert(),\n cancel: () => this.modalObject.destroy(),\n back: () => {\n this.modalObject.destroy();\n this.displayContentModal();\n },\n };\n\n const actionKey = Object.keys(actions).find(key => e.target.closest(`[data-action=\"${key}\"]`));\n if (actionKey) {\n e.preventDefault();\n actions[actionKey]();\n }\n }\n\n /**\n * Set up the prompt area in the modal, adding necessary event listeners.\n *\n * @param {HTMLElement} root - The root element of the modal.\n */\n setupPromptArea(root) {\n const generateBtn = root.querySelector(this.SELECTORS.GENERATEBUTTON());\n const promptArea = root.querySelector(this.SELECTORS.PROMPTAREA());\n\n promptArea.addEventListener('input', () => {\n generateBtn.disabled = promptArea.value.trim() === '';\n });\n }\n\n /**\n * Handle the submit action.\n *\n * @param {Object} root The root element of the modal.\n * @param {Object} submitBtn The submit button element.\n */\n async handleSubmit(root, submitBtn) {\n await this.displayLoading(root, submitBtn);\n\n const displayArgs = this.getDisplayArgs(root);\n const request = {\n methodname: 'aiplacement_editor_generate_image',\n args: displayArgs\n };\n\n try {\n this.responseObj = await Ajax.call([request])[0];\n if (this.responseObj.error) {\n this.handleGenerationError(root, submitBtn, '');\n } else {\n await this.displayGeneratedImage(root);\n this.hideLoading(root, submitBtn);\n window.console.log(this.responseObj);\n }\n } catch (error) {\n this.handleGenerationError(root, submitBtn, '');\n }\n }\n\n /**\n * Handle the insert action.\n *\n */\n async handleInsert() {\n const mediaImage = new AiMediaImage(this.editor, this.imageURL, this.promptText);\n await mediaImage.displayDialogue();\n this.modalObject.destroy();\n }\n\n /**\n * Handle a generation error.\n *\n * @param {Object} root The root element of the modal.\n * @param {Object} submitBtn The submit button element.\n * @param {String} errorMessage The error message to display.\n */\n async handleGenerationError(root, submitBtn, errorMessage = '') {\n if (!errorMessage) {\n // Get the default error message.\n errorMessage = await getString('errorgeneral', 'tiny_aiplacement');\n }\n this.hideLoading(root, submitBtn);\n this.modalObject.setBody(Templates.render('tiny_aiplacement/modalbodyerror', {'errorMessage': errorMessage}));\n const backBtn = root.querySelector(this.SELECTORS.BACKTBTN);\n const generateBtn = root.querySelector(this.SELECTORS.GENERATEBUTTON());\n backBtn.classList.remove('hidden');\n generateBtn.classList.add('hidden');\n }\n\n /**\n * Display the generated image in the modal.\n *\n * @param {HTMLElement} root - The root element of the modal.\n */\n async displayGeneratedImage(root) {\n const imageDisplayContainer = root.querySelector(this.SELECTORS.IMAGECONTAINER());\n const insertBtn = root.querySelector(this.SELECTORS.INSERTBTN);\n // Set the draft URL as it's used elsewhere.\n this.imageURL = this.responseObj.drafturl;\n\n // Render the image template and insert it into the modal.\n imageDisplayContainer.innerHTML = await Templates.render('tiny_aiplacement/image', {\n url: this.responseObj.drafturl,\n elementid: this.editor.id,\n alt: this.promptText,\n });\n const imagElement = root.querySelector(this.SELECTORS.GENERATEDIMAGE());\n\n return new Promise((resolve, reject) => {\n imagElement.onload = () => {\n insertBtn.classList.remove('hidden');\n resolve(); // Resolve the promise when the image is loaded.\n };\n imagElement.onerror = (error) => {\n reject(error); // Reject the promise if there is an error loading the image.\n };\n });\n }\n\n /**\n * Get the display args for the image.\n *\n * @param {Object} root The root element of the modal.\n */\n getDisplayArgs(root) {\n const contextId = getContextId(this.editor);\n const promptText = root.querySelector(this.SELECTORS.PROMPTAREA()).value;\n this.promptText = promptText;\n\n const aspectRatio = this.getSelectedRadioValue('aspect-ratio', 'square');\n const imageQuality = this.getSelectedRadioValue('quality', 'standard');\n\n return {\n contextid: contextId,\n prompttext: promptText,\n aspectratio: aspectRatio,\n quality: imageQuality ? 'hd' : 'standard',\n numimages: 1\n };\n }\n\n /**\n * Get the value of the selected radio button.\n *\n * @param {String} radioName The name of the radio button group.\n * @param {String} defaultValue The default value of the radio button.\n */\n getSelectedRadioValue(radioName, defaultValue = null) {\n const radios = document.getElementsByName(radioName);\n for (const radio of radios) {\n if (radio.checked) {\n return radio.value;\n }\n }\n return defaultValue;\n }\n}\n"],"names":["GenerateImage","GenerateBase","GENERATEBUTTON","this","editor","id","PROMPTAREA","IMAGECONTAINER","GENERATEBTN","INSERTBTN","BACKTBTN","GENERATEDIMAGE","getModalClass","ImageModal","handleContentModalClick","e","root","actions","generate","handleSubmit","target","inserter","handleInsert","cancel","modalObject","destroy","back","displayContentModal","actionKey","Object","keys","find","key","closest","preventDefault","setupPromptArea","generateBtn","querySelector","SELECTORS","promptArea","addEventListener","disabled","value","trim","submitBtn","displayLoading","request","methodname","args","getDisplayArgs","responseObj","Ajax","call","error","handleGenerationError","displayGeneratedImage","hideLoading","window","console","log","mediaImage","AiMediaImage","imageURL","promptText","displayDialogue","errorMessage","setBody","Templates","render","backBtn","classList","remove","add","imageDisplayContainer","insertBtn","drafturl","innerHTML","url","elementid","alt","imagElement","Promise","resolve","reject","onload","onerror","contextId","contextid","prompttext","aspectratio","getSelectedRadioValue","quality","numimages","radioName","defaultValue","radios","document","getElementsByName","radio","checked"],"mappings":"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"}
\ No newline at end of file
diff --git a/lib/editor/tiny/plugins/aiplacement/amd/build/generatetext.min.js b/lib/editor/tiny/plugins/aiplacement/amd/build/generatetext.min.js
index 91ea47a9625..f99cb03fbe7 100644
--- a/lib/editor/tiny/plugins/aiplacement/amd/build/generatetext.min.js
+++ b/lib/editor/tiny/plugins/aiplacement/amd/build/generatetext.min.js
@@ -1,3 +1,3 @@
-define("tiny_aiplacement/generatetext",["exports","./textmodal","core/ajax","core/str","core/templates","./options","./textmark","./generatebase"],(function(_exports,_textmodal,_ajax,_str,_templates,_options,_textmark,_generatebase){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_textmodal=_interopRequireDefault(_textmodal),_ajax=_interopRequireDefault(_ajax),_templates=_interopRequireDefault(_templates),_textmark=_interopRequireDefault(_textmark),_generatebase=_interopRequireDefault(_generatebase);class GenerateText extends _generatebase.default{constructor(){var obj,key,value;super(...arguments),value={GENERATEBUTTON:()=>"#".concat(this.editor.id,"_tiny_aiplacement_generatebutton"),PROMPTAREA:()=>"#".concat(this.editor.id,"_tiny_aiplacement_textprompt"),RESPONSEWRAPPER:".tiny_aiplacement_textresponse",RESPONSEPLACEHOLDER:".tiny_aiplacement_textresponse_placeholder",GENERATEDRESPONSE:()=>"#".concat(this.editor.id,"_tiny_aiplacement_textresponse"),INSERTBTN:'[data-action="inserter"]',BACKTBTN:'[data-action="back"]'},(key="SELECTORS")in(obj=this)?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value}getModalClass(){return _textmodal.default}handleContentModalClick(e,root){const actions={generate:()=>this.handleSubmit(root,e.target),inserter:()=>this.handleInsert(root,e.target),cancel:()=>this.modalObject.destroy(),back:()=>{this.modalObject.destroy(),this.displayContentModal()}},actionKey=Object.keys(actions).find((key=>e.target.closest('[data-action="'.concat(key,'"]'))));actionKey&&(e.preventDefault(),actions[actionKey]())}setupPromptArea(root){const generateBtn=root.querySelector(this.SELECTORS.GENERATEBUTTON()),promptArea=root.querySelector(this.SELECTORS.PROMPTAREA());promptArea.addEventListener("input",(()=>{generateBtn.disabled=""===promptArea.value.trim()}))}async handleSubmit(root,submitBtn){await this.displayLoading(root,submitBtn);const request={methodname:"aiplacement_editor_generate_text",args:this.getRequestArgs(root)};try{this.responseObj=await _ajax.default.call([request])[0],this.responseObj.error?this.handleGenerationError(root,submitBtn,""):(await this.displayGeneratedText(root),this.hideLoading(root,submitBtn))}catch(error){this.handleGenerationError(root,submitBtn,"")}}async handleInsert(root,submitBtn){await this.displayLoading(root,submitBtn);const generatedResponseDiv=root.querySelector(this.SELECTORS.GENERATEDRESPONSE()),wrappedEditedResponse=await _textmark.default.wrapEditedSections(this.responseObj.generatedcontent,generatedResponseDiv.value);this.responseObj.editedtext=this.replaceLineBreaks(wrappedEditedResponse);const formattedResponse=await _templates.default.render("tiny_aiplacement/textinsert",this.responseObj);this.editor.insertContent(formattedResponse),this.editor.execCommand("mceRepaint"),this.editor.windowManager.close(),this.modalObject.hide()}async handleGenerationError(root,submitBtn){let errorMessage=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"";errorMessage||(errorMessage=await(0,_str.getString)("errorgeneral","tiny_aiplacement")),this.modalObject.setBody(_templates.default.render("tiny_aiplacement/modalbodyerror",{errorMessage:errorMessage}));const backBtn=root.querySelector(this.SELECTORS.BACKTBTN),generateBtn=root.querySelector(this.SELECTORS.GENERATEBUTTON());backBtn.classList.remove("hidden"),generateBtn.classList.add("hidden"),this.hideLoading(root,submitBtn)}async displayGeneratedText(root){root.querySelector(this.SELECTORS.INSERTBTN).classList.remove("hidden");root.querySelector(this.SELECTORS.GENERATEDRESPONSE()).value=this.responseObj.generatedcontent;root.querySelector(this.SELECTORS.RESPONSEWRAPPER).classList.remove("hidden");root.querySelector(this.SELECTORS.RESPONSEPLACEHOLDER).classList.add("hidden")}getRequestArgs(root){return{contextid:(0,_options.getContextId)(this.editor),prompttext:root.querySelector(this.SELECTORS.PROMPTAREA()).value}}replaceLineBreaks(text){const textWithBreaks=text.replace(/\n{2,}|\r\n/g,"
").replace(/\n/g,"
");return"".concat(textWithBreaks,"
")}}return _exports.default=GenerateText,_exports.default}));
+define("tiny_aiplacement/generatetext",["exports","./textmodal","core/ajax","core/str","core/templates","./options","./textmark","./generatebase"],(function(_exports,_textmodal,_ajax,_str,_templates,_options,_textmark,_generatebase){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_textmodal=_interopRequireDefault(_textmodal),_ajax=_interopRequireDefault(_ajax),_templates=_interopRequireDefault(_templates),_textmark=_interopRequireDefault(_textmark),_generatebase=_interopRequireDefault(_generatebase);class GenerateText extends _generatebase.default{constructor(){var obj,key,value;super(...arguments),value={GENERATEBUTTON:()=>'[id="'.concat(this.editor.id,'_tiny_aiplacement_generatebutton"]'),PROMPTAREA:()=>'[id="'.concat(this.editor.id,'_tiny_aiplacement_textprompt"]'),RESPONSEWRAPPER:".tiny_aiplacement_textresponse",RESPONSEPLACEHOLDER:".tiny_aiplacement_textresponse_placeholder",GENERATEDRESPONSE:()=>'[id="'.concat(this.editor.id,'_tiny_aiplacement_textresponse"]'),INSERTBTN:'[data-action="inserter"]',BACKTBTN:'[data-action="back"]'},(key="SELECTORS")in(obj=this)?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value}getModalClass(){return _textmodal.default}handleContentModalClick(e,root){const actions={generate:()=>this.handleSubmit(root,e.target),inserter:()=>this.handleInsert(root,e.target),cancel:()=>this.modalObject.destroy(),back:()=>{this.modalObject.destroy(),this.displayContentModal()}},actionKey=Object.keys(actions).find((key=>e.target.closest('[data-action="'.concat(key,'"]'))));actionKey&&(e.preventDefault(),actions[actionKey]())}setupPromptArea(root){const generateBtn=root.querySelector(this.SELECTORS.GENERATEBUTTON()),promptArea=root.querySelector(this.SELECTORS.PROMPTAREA());promptArea.addEventListener("input",(()=>{generateBtn.disabled=""===promptArea.value.trim()}))}async handleSubmit(root,submitBtn){await this.displayLoading(root,submitBtn);const request={methodname:"aiplacement_editor_generate_text",args:this.getRequestArgs(root)};try{this.responseObj=await _ajax.default.call([request])[0],this.responseObj.error?this.handleGenerationError(root,submitBtn,""):(await this.displayGeneratedText(root),this.hideLoading(root,submitBtn))}catch(error){this.handleGenerationError(root,submitBtn,"")}}async handleInsert(root,submitBtn){await this.displayLoading(root,submitBtn);const generatedResponseDiv=root.querySelector(this.SELECTORS.GENERATEDRESPONSE()),wrappedEditedResponse=await _textmark.default.wrapEditedSections(this.responseObj.generatedcontent,generatedResponseDiv.value);this.responseObj.editedtext=this.replaceLineBreaks(wrappedEditedResponse);const formattedResponse=await _templates.default.render("tiny_aiplacement/textinsert",this.responseObj);this.editor.insertContent(formattedResponse),this.editor.execCommand("mceRepaint"),this.editor.windowManager.close(),this.modalObject.hide()}async handleGenerationError(root,submitBtn){let errorMessage=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"";errorMessage||(errorMessage=await(0,_str.getString)("errorgeneral","tiny_aiplacement")),this.modalObject.setBody(_templates.default.render("tiny_aiplacement/modalbodyerror",{errorMessage:errorMessage}));const backBtn=root.querySelector(this.SELECTORS.BACKTBTN),generateBtn=root.querySelector(this.SELECTORS.GENERATEBUTTON());backBtn.classList.remove("hidden"),generateBtn.classList.add("hidden"),this.hideLoading(root,submitBtn)}async displayGeneratedText(root){root.querySelector(this.SELECTORS.INSERTBTN).classList.remove("hidden");root.querySelector(this.SELECTORS.GENERATEDRESPONSE()).value=this.responseObj.generatedcontent;root.querySelector(this.SELECTORS.RESPONSEWRAPPER).classList.remove("hidden");root.querySelector(this.SELECTORS.RESPONSEPLACEHOLDER).classList.add("hidden")}getRequestArgs(root){return{contextid:(0,_options.getContextId)(this.editor),prompttext:root.querySelector(this.SELECTORS.PROMPTAREA()).value}}replaceLineBreaks(text){const textWithBreaks=text.replace(/\n{2,}|\r\n/g,"
").replace(/\n/g,"
");return"".concat(textWithBreaks,"
")}}return _exports.default=GenerateText,_exports.default}));
//# sourceMappingURL=generatetext.min.js.map
\ No newline at end of file
diff --git a/lib/editor/tiny/plugins/aiplacement/amd/build/generatetext.min.js.map b/lib/editor/tiny/plugins/aiplacement/amd/build/generatetext.min.js.map
index c5449209513..59729634072 100644
--- a/lib/editor/tiny/plugins/aiplacement/amd/build/generatetext.min.js.map
+++ b/lib/editor/tiny/plugins/aiplacement/amd/build/generatetext.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"generatetext.min.js","sources":["../src/generatetext.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 * Tiny AI generate text.\n *\n * @module tiny_aiplacement/generatetext\n * @copyright 2024 Matt Porritt \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport TextModal from './textmodal';\nimport Ajax from 'core/ajax';\nimport {getString} from 'core/str';\nimport Templates from 'core/templates';\nimport {getContextId} from './options';\nimport TinyAiTextMarker from './textmark';\nimport GenerateBase from './generatebase';\n\nexport default class GenerateText extends GenerateBase {\n SELECTORS = {\n GENERATEBUTTON: () => `#${this.editor.id}_tiny_aiplacement_generatebutton`,\n PROMPTAREA: () => `#${this.editor.id}_tiny_aiplacement_textprompt`,\n RESPONSEWRAPPER: '.tiny_aiplacement_textresponse',\n RESPONSEPLACEHOLDER: '.tiny_aiplacement_textresponse_placeholder',\n GENERATEDRESPONSE: () => `#${this.editor.id}_tiny_aiplacement_textresponse`,\n INSERTBTN: '[data-action=\"inserter\"]',\n BACKTBTN: '[data-action=\"back\"]',\n };\n\n getModalClass() {\n return TextModal;\n }\n\n /**\n * Handle click events within the text modal.\n *\n * @param {Event} e - The click event object.\n * @param {HTMLElement} root - The root element of the modal.\n */\n handleContentModalClick(e, root) {\n const actions = {\n generate: () => this.handleSubmit(root, e.target),\n inserter: () => this.handleInsert(root, e.target),\n cancel: () => this.modalObject.destroy(),\n back: () => {\n this.modalObject.destroy();\n this.displayContentModal();\n },\n };\n\n const actionKey = Object.keys(actions).find(key => e.target.closest(`[data-action=\"${key}\"]`));\n if (actionKey) {\n e.preventDefault();\n actions[actionKey]();\n }\n }\n\n /**\n * Set up the prompt area in the modal, adding necessary event listeners.\n *\n * @param {HTMLElement} root - The root element of the modal.\n */\n setupPromptArea(root) {\n const generateBtn = root.querySelector(this.SELECTORS.GENERATEBUTTON());\n const promptArea = root.querySelector(this.SELECTORS.PROMPTAREA());\n\n promptArea.addEventListener('input', () => {\n generateBtn.disabled = promptArea.value.trim() === '';\n });\n }\n\n /**\n * Handle the submit action.\n *\n * @param {Object} root The root element of the modal.\n * @param {Object} submitBtn The submit button element.\n */\n async handleSubmit(root, submitBtn) {\n await this.displayLoading(root, submitBtn);\n\n const requestArgs = this.getRequestArgs(root);\n const request = {\n methodname: 'aiplacement_editor_generate_text',\n args: requestArgs\n };\n\n try {\n this.responseObj = await Ajax.call([request])[0];\n if (this.responseObj.error) {\n this.handleGenerationError(root, submitBtn, '');\n } else {\n await this.displayGeneratedText(root);\n this.hideLoading(root, submitBtn);\n }\n } catch (error) {\n this.handleGenerationError(root, submitBtn, '');\n }\n }\n\n /**\n * Handle the insert action.\n *\n * @param {Object} root The root element of the modal.\n * @param {HTMLElement} submitBtn - The submit button element.\n */\n async handleInsert(root, submitBtn) {\n await this.displayLoading(root, submitBtn);\n\n // Update the generated response with the content from the form.\n // In case the user has edited the response.\n const generatedResponseDiv = root.querySelector(this.SELECTORS.GENERATEDRESPONSE());\n\n // Wrap the edited sections in the response with tags.\n // This is so we can differentiate between the edited sections and the generated content.\n const wrappedEditedResponse = await TinyAiTextMarker.wrapEditedSections(\n this.responseObj.generatedcontent,\n generatedResponseDiv.value)\n ;\n\n // Replace double line breaks with
and with
for paragraphs.\n this.responseObj.editedtext = this.replaceLineBreaks(wrappedEditedResponse);\n\n // Generate the HTML for the response.\n const formattedResponse = await Templates.render('tiny_aiplacement/textinsert', this.responseObj);\n\n // Insert the response into the editor.\n this.editor.insertContent(formattedResponse);\n this.editor.execCommand('mceRepaint');\n this.editor.windowManager.close();\n\n // Close the modal and return to the editor.\n this.modalObject.hide();\n }\n\n /**\n * Handle a generation error.\n *\n * @param {Object} root The root element of the modal.\n * @param {Object} submitBtn The submit button element.\n * @param {String} errorMessage The error message to display.\n */\n async handleGenerationError(root, submitBtn, errorMessage = '') {\n if (!errorMessage) {\n // Get the default error message.\n errorMessage = await getString('errorgeneral', 'tiny_aiplacement');\n }\n this.modalObject.setBody(Templates.render('tiny_aiplacement/modalbodyerror', {'errorMessage': errorMessage}));\n const backBtn = root.querySelector(this.SELECTORS.BACKTBTN);\n const generateBtn = root.querySelector(this.SELECTORS.GENERATEBUTTON());\n backBtn.classList.remove('hidden');\n generateBtn.classList.add('hidden');\n this.hideLoading(root, submitBtn);\n }\n\n /**\n * Display the generated image in the modal.\n *\n * @param {HTMLElement} root - The root element of the modal.\n */\n async displayGeneratedText(root) {\n const insertBtn = root.querySelector(this.SELECTORS.INSERTBTN);\n insertBtn.classList.remove('hidden');\n\n // Add generated text to the modal.\n const generatedResponseDiv = root.querySelector(this.SELECTORS.GENERATEDRESPONSE());\n generatedResponseDiv.value = this.responseObj.generatedcontent;\n const responseWrapper = root.querySelector(this.SELECTORS.RESPONSEWRAPPER);\n responseWrapper.classList.remove('hidden');\n const responsePlaceholder = root.querySelector(this.SELECTORS.RESPONSEPLACEHOLDER);\n responsePlaceholder.classList.add('hidden');\n }\n\n /**\n * Get the request args for the generated text.\n *\n * @param {Object} root The root element of the modal.\n */\n getRequestArgs(root) {\n const contextId = getContextId(this.editor);\n const promptText = root.querySelector(this.SELECTORS.PROMPTAREA()).value;\n\n return {\n contextid: contextId,\n prompttext: promptText\n };\n }\n\n /**\n * Replace double line breaks with
and with
for paragraphs.\n * This is to handle the difference in response from the AI to what is expected by the editor.\n *\n * @param {String} text The text to replace.\n * @returns {String}\n */\n replaceLineBreaks(text) {\n // Replace double line breaks with
for paragraphs\n const textWithParagraphs = text.replace(/\\n{2,}|\\r\\n/g, '
');\n\n // Replace remaining single line breaks with
tags\n const textWithBreaks = textWithParagraphs.replace(/\\n/g, '
');\n\n // Add opening and closing
tags to wrap the entire content\n return `
${textWithBreaks}
`;\n }\n}\n"],"names":["GenerateText","GenerateBase","GENERATEBUTTON","this","editor","id","PROMPTAREA","RESPONSEWRAPPER","RESPONSEPLACEHOLDER","GENERATEDRESPONSE","INSERTBTN","BACKTBTN","getModalClass","TextModal","handleContentModalClick","e","root","actions","generate","handleSubmit","target","inserter","handleInsert","cancel","modalObject","destroy","back","displayContentModal","actionKey","Object","keys","find","key","closest","preventDefault","setupPromptArea","generateBtn","querySelector","SELECTORS","promptArea","addEventListener","disabled","value","trim","submitBtn","displayLoading","request","methodname","args","getRequestArgs","responseObj","Ajax","call","error","handleGenerationError","displayGeneratedText","hideLoading","generatedResponseDiv","wrappedEditedResponse","TinyAiTextMarker","wrapEditedSections","generatedcontent","editedtext","replaceLineBreaks","formattedResponse","Templates","render","insertContent","execCommand","windowManager","close","hide","errorMessage","setBody","backBtn","classList","remove","add","contextid","prompttext","text","textWithBreaks","replace"],"mappings":"inBA+BqBA,qBAAqBC,gFAC1B,CACRC,eAAgB,eAAUC,KAAKC,OAAOC,uCACtCC,WAAY,eAAUH,KAAKC,OAAOC,mCAClCE,gBAAiB,iCACjBC,oBAAqB,6CACrBC,kBAAmB,eAAUN,KAAKC,OAAOC,qCACzCK,UAAW,2BACXC,SAAU,4JAGdC,uBACWC,mBASXC,wBAAwBC,EAAGC,YACjBC,QAAU,CACZC,SAAU,IAAMf,KAAKgB,aAAaH,KAAMD,EAAEK,QAC1CC,SAAU,IAAMlB,KAAKmB,aAAaN,KAAMD,EAAEK,QAC1CG,OAAQ,IAAMpB,KAAKqB,YAAYC,UAC/BC,KAAM,UACGF,YAAYC,eACZE,wBAIPC,UAAYC,OAAOC,KAAKb,SAASc,MAAKC,KAAOjB,EAAEK,OAAOa,gCAAyBD,aACjFJ,YACAb,EAAEmB,iBACFjB,QAAQW,cAShBO,gBAAgBnB,YACNoB,YAAcpB,KAAKqB,cAAclC,KAAKmC,UAAUpC,kBAChDqC,WAAavB,KAAKqB,cAAclC,KAAKmC,UAAUhC,cAErDiC,WAAWC,iBAAiB,SAAS,KACjCJ,YAAYK,SAAuC,KAA5BF,WAAWG,MAAMC,6BAU7B3B,KAAM4B,iBACfzC,KAAK0C,eAAe7B,KAAM4B,iBAG1BE,QAAU,CACZC,WAAY,mCACZC,KAHgB7C,KAAK8C,eAAejC,gBAO/BkC,kBAAoBC,cAAKC,KAAK,CAACN,UAAU,GAC1C3C,KAAK+C,YAAYG,WACZC,sBAAsBtC,KAAM4B,UAAW,WAEtCzC,KAAKoD,qBAAqBvC,WAC3BwC,YAAYxC,KAAM4B,YAE7B,MAAOS,YACAC,sBAAsBtC,KAAM4B,UAAW,wBAUjC5B,KAAM4B,iBACfzC,KAAK0C,eAAe7B,KAAM4B,iBAI1Ba,qBAAuBzC,KAAKqB,cAAclC,KAAKmC,UAAU7B,qBAIzDiD,4BAA8BC,kBAAiBC,mBACjDzD,KAAK+C,YAAYW,iBACjBJ,qBAAqBf,YAIpBQ,YAAYY,WAAa3D,KAAK4D,kBAAkBL,6BAG/CM,wBAA0BC,mBAAUC,OAAO,8BAA+B/D,KAAK+C,kBAGhF9C,OAAO+D,cAAcH,wBACrB5D,OAAOgE,YAAY,mBACnBhE,OAAOiE,cAAcC,aAGrB9C,YAAY+C,mCAUOvD,KAAM4B,eAAW4B,oEAAe,GACnDA,eAEDA,mBAAqB,kBAAU,eAAgB,0BAE9ChD,YAAYiD,QAAQR,mBAAUC,OAAO,kCAAmC,cAAiBM,sBACxFE,QAAU1D,KAAKqB,cAAclC,KAAKmC,UAAU3B,UAC5CyB,YAAcpB,KAAKqB,cAAclC,KAAKmC,UAAUpC,kBACtDwE,QAAQC,UAAUC,OAAO,UACzBxC,YAAYuC,UAAUE,IAAI,eACrBrB,YAAYxC,KAAM4B,sCAQA5B,MACLA,KAAKqB,cAAclC,KAAKmC,UAAU5B,WAC1CiE,UAAUC,OAAO,UAGE5D,KAAKqB,cAAclC,KAAKmC,UAAU7B,qBAC1CiC,MAAQvC,KAAK+C,YAAYW,iBACtB7C,KAAKqB,cAAclC,KAAKmC,UAAU/B,iBAC1CoE,UAAUC,OAAO,UACL5D,KAAKqB,cAAclC,KAAKmC,UAAU9B,qBAC1CmE,UAAUE,IAAI,UAQtC5B,eAAejC,YAIJ,CACH8D,WAJc,yBAAa3E,KAAKC,QAKhC2E,WAJe/D,KAAKqB,cAAclC,KAAKmC,UAAUhC,cAAcoC,OAevEqB,kBAAkBiB,YAKRC,eAHqBD,KAAKE,QAAQ,eAAgB,cAGdA,QAAQ,MAAO,4BAG5CD"}
\ No newline at end of file
+{"version":3,"file":"generatetext.min.js","sources":["../src/generatetext.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 * Tiny AI generate text.\n *\n * @module tiny_aiplacement/generatetext\n * @copyright 2024 Matt Porritt \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport TextModal from './textmodal';\nimport Ajax from 'core/ajax';\nimport {getString} from 'core/str';\nimport Templates from 'core/templates';\nimport {getContextId} from './options';\nimport TinyAiTextMarker from './textmark';\nimport GenerateBase from './generatebase';\n\nexport default class GenerateText extends GenerateBase {\n SELECTORS = {\n GENERATEBUTTON: () => `[id=\"${this.editor.id}_tiny_aiplacement_generatebutton\"]`,\n PROMPTAREA: () => `[id=\"${this.editor.id}_tiny_aiplacement_textprompt\"]`,\n RESPONSEWRAPPER: '.tiny_aiplacement_textresponse',\n RESPONSEPLACEHOLDER: '.tiny_aiplacement_textresponse_placeholder',\n GENERATEDRESPONSE: () => `[id=\"${this.editor.id}_tiny_aiplacement_textresponse\"]`,\n INSERTBTN: '[data-action=\"inserter\"]',\n BACKTBTN: '[data-action=\"back\"]',\n };\n\n getModalClass() {\n return TextModal;\n }\n\n /**\n * Handle click events within the text modal.\n *\n * @param {Event} e - The click event object.\n * @param {HTMLElement} root - The root element of the modal.\n */\n handleContentModalClick(e, root) {\n const actions = {\n generate: () => this.handleSubmit(root, e.target),\n inserter: () => this.handleInsert(root, e.target),\n cancel: () => this.modalObject.destroy(),\n back: () => {\n this.modalObject.destroy();\n this.displayContentModal();\n },\n };\n\n const actionKey = Object.keys(actions).find(key => e.target.closest(`[data-action=\"${key}\"]`));\n if (actionKey) {\n e.preventDefault();\n actions[actionKey]();\n }\n }\n\n /**\n * Set up the prompt area in the modal, adding necessary event listeners.\n *\n * @param {HTMLElement} root - The root element of the modal.\n */\n setupPromptArea(root) {\n const generateBtn = root.querySelector(this.SELECTORS.GENERATEBUTTON());\n const promptArea = root.querySelector(this.SELECTORS.PROMPTAREA());\n\n promptArea.addEventListener('input', () => {\n generateBtn.disabled = promptArea.value.trim() === '';\n });\n }\n\n /**\n * Handle the submit action.\n *\n * @param {Object} root The root element of the modal.\n * @param {Object} submitBtn The submit button element.\n */\n async handleSubmit(root, submitBtn) {\n await this.displayLoading(root, submitBtn);\n\n const requestArgs = this.getRequestArgs(root);\n const request = {\n methodname: 'aiplacement_editor_generate_text',\n args: requestArgs\n };\n\n try {\n this.responseObj = await Ajax.call([request])[0];\n if (this.responseObj.error) {\n this.handleGenerationError(root, submitBtn, '');\n } else {\n await this.displayGeneratedText(root);\n this.hideLoading(root, submitBtn);\n }\n } catch (error) {\n this.handleGenerationError(root, submitBtn, '');\n }\n }\n\n /**\n * Handle the insert action.\n *\n * @param {Object} root The root element of the modal.\n * @param {HTMLElement} submitBtn - The submit button element.\n */\n async handleInsert(root, submitBtn) {\n await this.displayLoading(root, submitBtn);\n\n // Update the generated response with the content from the form.\n // In case the user has edited the response.\n const generatedResponseDiv = root.querySelector(this.SELECTORS.GENERATEDRESPONSE());\n\n // Wrap the edited sections in the response with tags.\n // This is so we can differentiate between the edited sections and the generated content.\n const wrappedEditedResponse = await TinyAiTextMarker.wrapEditedSections(\n this.responseObj.generatedcontent,\n generatedResponseDiv.value)\n ;\n\n // Replace double line breaks with
and with for paragraphs.\n this.responseObj.editedtext = this.replaceLineBreaks(wrappedEditedResponse);\n\n // Generate the HTML for the response.\n const formattedResponse = await Templates.render('tiny_aiplacement/textinsert', this.responseObj);\n\n // Insert the response into the editor.\n this.editor.insertContent(formattedResponse);\n this.editor.execCommand('mceRepaint');\n this.editor.windowManager.close();\n\n // Close the modal and return to the editor.\n this.modalObject.hide();\n }\n\n /**\n * Handle a generation error.\n *\n * @param {Object} root The root element of the modal.\n * @param {Object} submitBtn The submit button element.\n * @param {String} errorMessage The error message to display.\n */\n async handleGenerationError(root, submitBtn, errorMessage = '') {\n if (!errorMessage) {\n // Get the default error message.\n errorMessage = await getString('errorgeneral', 'tiny_aiplacement');\n }\n this.modalObject.setBody(Templates.render('tiny_aiplacement/modalbodyerror', {'errorMessage': errorMessage}));\n const backBtn = root.querySelector(this.SELECTORS.BACKTBTN);\n const generateBtn = root.querySelector(this.SELECTORS.GENERATEBUTTON());\n backBtn.classList.remove('hidden');\n generateBtn.classList.add('hidden');\n this.hideLoading(root, submitBtn);\n }\n\n /**\n * Display the generated image in the modal.\n *\n * @param {HTMLElement} root - The root element of the modal.\n */\n async displayGeneratedText(root) {\n const insertBtn = root.querySelector(this.SELECTORS.INSERTBTN);\n insertBtn.classList.remove('hidden');\n\n // Add generated text to the modal.\n const generatedResponseDiv = root.querySelector(this.SELECTORS.GENERATEDRESPONSE());\n generatedResponseDiv.value = this.responseObj.generatedcontent;\n const responseWrapper = root.querySelector(this.SELECTORS.RESPONSEWRAPPER);\n responseWrapper.classList.remove('hidden');\n const responsePlaceholder = root.querySelector(this.SELECTORS.RESPONSEPLACEHOLDER);\n responsePlaceholder.classList.add('hidden');\n }\n\n /**\n * Get the request args for the generated text.\n *\n * @param {Object} root The root element of the modal.\n */\n getRequestArgs(root) {\n const contextId = getContextId(this.editor);\n const promptText = root.querySelector(this.SELECTORS.PROMPTAREA()).value;\n\n return {\n contextid: contextId,\n prompttext: promptText\n };\n }\n\n /**\n * Replace double line breaks with
and with
for paragraphs.\n * This is to handle the difference in response from the AI to what is expected by the editor.\n *\n * @param {String} text The text to replace.\n * @returns {String}\n */\n replaceLineBreaks(text) {\n // Replace double line breaks with
for paragraphs\n const textWithParagraphs = text.replace(/\\n{2,}|\\r\\n/g, '
');\n\n // Replace remaining single line breaks with
tags\n const textWithBreaks = textWithParagraphs.replace(/\\n/g, '
');\n\n // Add opening and closing
tags to wrap the entire content\n return `
${textWithBreaks}
`;\n }\n}\n"],"names":["GenerateText","GenerateBase","GENERATEBUTTON","this","editor","id","PROMPTAREA","RESPONSEWRAPPER","RESPONSEPLACEHOLDER","GENERATEDRESPONSE","INSERTBTN","BACKTBTN","getModalClass","TextModal","handleContentModalClick","e","root","actions","generate","handleSubmit","target","inserter","handleInsert","cancel","modalObject","destroy","back","displayContentModal","actionKey","Object","keys","find","key","closest","preventDefault","setupPromptArea","generateBtn","querySelector","SELECTORS","promptArea","addEventListener","disabled","value","trim","submitBtn","displayLoading","request","methodname","args","getRequestArgs","responseObj","Ajax","call","error","handleGenerationError","displayGeneratedText","hideLoading","generatedResponseDiv","wrappedEditedResponse","TinyAiTextMarker","wrapEditedSections","generatedcontent","editedtext","replaceLineBreaks","formattedResponse","Templates","render","insertContent","execCommand","windowManager","close","hide","errorMessage","setBody","backBtn","classList","remove","add","contextid","prompttext","text","textWithBreaks","replace"],"mappings":"inBA+BqBA,qBAAqBC,gFAC1B,CACRC,eAAgB,mBAAcC,KAAKC,OAAOC,yCAC1CC,WAAY,mBAAcH,KAAKC,OAAOC,qCACtCE,gBAAiB,iCACjBC,oBAAqB,6CACrBC,kBAAmB,mBAAcN,KAAKC,OAAOC,uCAC7CK,UAAW,2BACXC,SAAU,4JAGdC,uBACWC,mBASXC,wBAAwBC,EAAGC,YACjBC,QAAU,CACZC,SAAU,IAAMf,KAAKgB,aAAaH,KAAMD,EAAEK,QAC1CC,SAAU,IAAMlB,KAAKmB,aAAaN,KAAMD,EAAEK,QAC1CG,OAAQ,IAAMpB,KAAKqB,YAAYC,UAC/BC,KAAM,UACGF,YAAYC,eACZE,wBAIPC,UAAYC,OAAOC,KAAKb,SAASc,MAAKC,KAAOjB,EAAEK,OAAOa,gCAAyBD,aACjFJ,YACAb,EAAEmB,iBACFjB,QAAQW,cAShBO,gBAAgBnB,YACNoB,YAAcpB,KAAKqB,cAAclC,KAAKmC,UAAUpC,kBAChDqC,WAAavB,KAAKqB,cAAclC,KAAKmC,UAAUhC,cAErDiC,WAAWC,iBAAiB,SAAS,KACjCJ,YAAYK,SAAuC,KAA5BF,WAAWG,MAAMC,6BAU7B3B,KAAM4B,iBACfzC,KAAK0C,eAAe7B,KAAM4B,iBAG1BE,QAAU,CACZC,WAAY,mCACZC,KAHgB7C,KAAK8C,eAAejC,gBAO/BkC,kBAAoBC,cAAKC,KAAK,CAACN,UAAU,GAC1C3C,KAAK+C,YAAYG,WACZC,sBAAsBtC,KAAM4B,UAAW,WAEtCzC,KAAKoD,qBAAqBvC,WAC3BwC,YAAYxC,KAAM4B,YAE7B,MAAOS,YACAC,sBAAsBtC,KAAM4B,UAAW,wBAUjC5B,KAAM4B,iBACfzC,KAAK0C,eAAe7B,KAAM4B,iBAI1Ba,qBAAuBzC,KAAKqB,cAAclC,KAAKmC,UAAU7B,qBAIzDiD,4BAA8BC,kBAAiBC,mBACjDzD,KAAK+C,YAAYW,iBACjBJ,qBAAqBf,YAIpBQ,YAAYY,WAAa3D,KAAK4D,kBAAkBL,6BAG/CM,wBAA0BC,mBAAUC,OAAO,8BAA+B/D,KAAK+C,kBAGhF9C,OAAO+D,cAAcH,wBACrB5D,OAAOgE,YAAY,mBACnBhE,OAAOiE,cAAcC,aAGrB9C,YAAY+C,mCAUOvD,KAAM4B,eAAW4B,oEAAe,GACnDA,eAEDA,mBAAqB,kBAAU,eAAgB,0BAE9ChD,YAAYiD,QAAQR,mBAAUC,OAAO,kCAAmC,cAAiBM,sBACxFE,QAAU1D,KAAKqB,cAAclC,KAAKmC,UAAU3B,UAC5CyB,YAAcpB,KAAKqB,cAAclC,KAAKmC,UAAUpC,kBACtDwE,QAAQC,UAAUC,OAAO,UACzBxC,YAAYuC,UAAUE,IAAI,eACrBrB,YAAYxC,KAAM4B,sCAQA5B,MACLA,KAAKqB,cAAclC,KAAKmC,UAAU5B,WAC1CiE,UAAUC,OAAO,UAGE5D,KAAKqB,cAAclC,KAAKmC,UAAU7B,qBAC1CiC,MAAQvC,KAAK+C,YAAYW,iBACtB7C,KAAKqB,cAAclC,KAAKmC,UAAU/B,iBAC1CoE,UAAUC,OAAO,UACL5D,KAAKqB,cAAclC,KAAKmC,UAAU9B,qBAC1CmE,UAAUE,IAAI,UAQtC5B,eAAejC,YAIJ,CACH8D,WAJc,yBAAa3E,KAAKC,QAKhC2E,WAJe/D,KAAKqB,cAAclC,KAAKmC,UAAUhC,cAAcoC,OAevEqB,kBAAkBiB,YAKRC,eAHqBD,KAAKE,QAAQ,eAAgB,cAGdA,QAAQ,MAAO,4BAG5CD"}
\ No newline at end of file
diff --git a/lib/editor/tiny/plugins/aiplacement/amd/src/generatebase.js b/lib/editor/tiny/plugins/aiplacement/amd/src/generatebase.js
index 1c4532959fe..81ce58224db 100644
--- a/lib/editor/tiny/plugins/aiplacement/amd/src/generatebase.js
+++ b/lib/editor/tiny/plugins/aiplacement/amd/src/generatebase.js
@@ -113,7 +113,7 @@ export default class GenerateBase {
* @param {Object} root The root element of the modal.
*/
hideLoadingSpinner(root) {
- const loadingSpinnerDiv = root.querySelector(`#${this.editor.id}_tiny_aiplacement_spinner`);
+ const loadingSpinnerDiv = root.querySelector(`[id="${this.editor.id}_tiny_aiplacement_spinner"]`);
loadingSpinnerDiv.classList.add('hidden');
loadingSpinnerDiv.classList.remove('tiny-aiplacement-loading-spinner-container');
}
@@ -126,10 +126,10 @@ export default class GenerateBase {
* @param {String|null} removeClass - The class to be removed from the loading spinner div, if any.
*/
async displayLoading(root, submitBtn, removeClass = null) {
- const loadingSpinnerDiv = root.querySelector(`#${this.editor.id}_tiny_aiplacement_spinner`);
- const overlayDiv = root.querySelector(`#${this.editor.id}_tiny_aiplacement_overlay`);
- const blurDiv = root.querySelector(`#${this.editor.id}_tiny_aiplacement_blur`);
- const loadingTextDiv = root.querySelector(`#${this.editor.id}_tiny_aiplacement_loading_text`);
+ const loadingSpinnerDiv = root.querySelector(`[id="${this.editor.id}_tiny_aiplacement_spinner"]`);
+ const overlayDiv = root.querySelector(`[id="${this.editor.id}_tiny_aiplacement_overlay"]`);
+ const blurDiv = root.querySelector(`[id="${this.editor.id}_tiny_aiplacement_blur"]`);
+ const loadingTextDiv = root.querySelector(`[id="${this.editor.id}_tiny_aiplacement_loading_text"]`);
const actionButtons = root.querySelectorAll('.tiny-aiplacement-generate-footer button');
loadingMessages(loadingTextDiv);
@@ -157,9 +157,9 @@ export default class GenerateBase {
* @param {Object} submitBtn The submit button element.
*/
async hideLoading(root, submitBtn) {
- const loadingSpinnerDiv = root.querySelector(`#${this.editor.id}_tiny_aiplacement_spinner`);
- const overlayDiv = root.querySelector(`#${this.editor.id}_tiny_aiplacement_overlay`);
- const blurDiv = root.querySelector(`#${this.editor.id}_tiny_aiplacement_blur`);
+ const loadingSpinnerDiv = root.querySelector(`[id="${this.editor.id}_tiny_aiplacement_spinner"]`);
+ const overlayDiv = root.querySelector(`[id="${this.editor.id}_tiny_aiplacement_overlay"]`);
+ const blurDiv = root.querySelector(`[id="${this.editor.id}_tiny_aiplacement_blur"]`);
const actionButtons = root.querySelectorAll('.tiny-aiplacement-generate-footer button');
if (loadingSpinnerDiv) {
loadingSpinnerDiv.classList.add('hidden');
diff --git a/lib/editor/tiny/plugins/aiplacement/amd/src/generateimage.js b/lib/editor/tiny/plugins/aiplacement/amd/src/generateimage.js
index ddfc9bc6931..0329dc30d4c 100644
--- a/lib/editor/tiny/plugins/aiplacement/amd/src/generateimage.js
+++ b/lib/editor/tiny/plugins/aiplacement/amd/src/generateimage.js
@@ -31,13 +31,13 @@ import GenerateBase from 'tiny_aiplacement/generatebase';
export default class GenerateImage extends GenerateBase {
SELECTORS = {
- GENERATEBUTTON: () => `#${this.editor.id}_tiny_aiplacement_generatebutton`,
- PROMPTAREA: () => `#${this.editor.id}_tiny_aiplacement_imageprompt`,
- IMAGECONTAINER: () => `#${this.editor.id}_tiny_aiplacement_generate_image`,
+ GENERATEBUTTON: () => `[id="${this.editor.id}_tiny_aiplacement_generatebutton"]`,
+ PROMPTAREA: () => `[id="${this.editor.id}_tiny_aiplacement_imageprompt"]`,
+ IMAGECONTAINER: () => `[id="${this.editor.id}_tiny_aiplacement_generate_image"]`,
GENERATEBTN: '[data-action="generate"]',
INSERTBTN: '[data-action="inserter"]',
BACKTBTN: '[data-action="back"]',
- GENERATEDIMAGE: () => `#${this.editor.id}_tiny_generated_image`,
+ GENERATEDIMAGE: () => `[id="${this.editor.id}_tiny_generated_image"]`,
};
imageURL = null;
diff --git a/lib/editor/tiny/plugins/aiplacement/amd/src/generatetext.js b/lib/editor/tiny/plugins/aiplacement/amd/src/generatetext.js
index 0372d344474..64dbc5b9ec6 100644
--- a/lib/editor/tiny/plugins/aiplacement/amd/src/generatetext.js
+++ b/lib/editor/tiny/plugins/aiplacement/amd/src/generatetext.js
@@ -31,11 +31,11 @@ import GenerateBase from './generatebase';
export default class GenerateText extends GenerateBase {
SELECTORS = {
- GENERATEBUTTON: () => `#${this.editor.id}_tiny_aiplacement_generatebutton`,
- PROMPTAREA: () => `#${this.editor.id}_tiny_aiplacement_textprompt`,
+ GENERATEBUTTON: () => `[id="${this.editor.id}_tiny_aiplacement_generatebutton"]`,
+ PROMPTAREA: () => `[id="${this.editor.id}_tiny_aiplacement_textprompt"]`,
RESPONSEWRAPPER: '.tiny_aiplacement_textresponse',
RESPONSEPLACEHOLDER: '.tiny_aiplacement_textresponse_placeholder',
- GENERATEDRESPONSE: () => `#${this.editor.id}_tiny_aiplacement_textresponse`,
+ GENERATEDRESPONSE: () => `[id="${this.editor.id}_tiny_aiplacement_textresponse"]`,
INSERTBTN: '[data-action="inserter"]',
BACKTBTN: '[data-action="back"]',
};
diff --git a/lib/editor/tiny/plugins/aiplacement/classes/plugininfo.php b/lib/editor/tiny/plugins/aiplacement/classes/plugininfo.php
index 1e5a3633d6e..bd36643e9c9 100644
--- a/lib/editor/tiny/plugins/aiplacement/classes/plugininfo.php
+++ b/lib/editor/tiny/plugins/aiplacement/classes/plugininfo.php
@@ -49,7 +49,7 @@ class plugininfo extends plugin implements plugin_with_buttons, plugin_with_menu
array $fpoptions,
?editor $editor = null
): bool {
- return in_array(true, self::get_allowed_actions($context));
+ return in_array(true, self::get_allowed_actions($context, $options));
}
#[\Override]
@@ -72,7 +72,7 @@ class plugininfo extends plugin implements plugin_with_buttons, plugin_with_menu
global $USER;
$userid = (int) $USER->id;
- $allowedactions = self::get_allowed_actions($context);
+ $allowedactions = self::get_allowed_actions($context, $options);
return array_merge([
'contextid' => $context->id,
@@ -85,9 +85,10 @@ class plugininfo extends plugin implements plugin_with_buttons, plugin_with_menu
* Get the allowed actions for the plugin.
*
* @param context $context The context that the editor is used within
+ * @param array $options The options passed in when requesting the editor
* @return array The allowed actions.
*/
- private static function get_allowed_actions(context $context): array {
+ private static function get_allowed_actions(context $context, array $options): array {
[$plugintype, $pluginname] = explode('_', \core_component::normalize_componentname('aiplacement_editor'), 2);
$manager = \core_plugin_manager::resolve_plugininfo_class($plugintype);
$allowedactions = [];
@@ -99,7 +100,14 @@ class plugininfo extends plugin implements plugin_with_buttons, plugin_with_menu
&& manager::is_action_enabled('aiplacement_editor', $action)
&& !empty($providers[$providerclass])
) {
- $allowedactions[$action] = true;
+ if ($action == 'generate_image') {
+ // For generate image, we need to check if the user has the capability to upload files.
+ $canhavefiles = !empty($options['maxfiles']);
+ $canhaveexternalfiles = !empty($options['return_types']) && ($options['return_types'] & FILE_EXTERNAL);
+ $allowedactions[$action] = $canhavefiles || $canhaveexternalfiles;
+ } else {
+ $allowedactions[$action] = true;
+ }
} else {
$allowedactions[$action] = false;
}