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 07e9748f33a..1e7fa5ebc8a 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:()=>'[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))}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"),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{if(this.responseObj=await _ajax.default.call([request])[0],this.responseObj.error)this.handleGenerationError(root,submitBtn,"");else{await this.displayGeneratedImage(root),await this.hideLoading(root,submitBtn);root.querySelector(this.SELECTORS.IMAGECONTAINER()).focus()}}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.modalObject.setBody(await _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"),await this.hideLoading(root,submitBtn),backBtn.focus()}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"),imagElement.focus(),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"),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 157c8063800..80ffaaa2a7f 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: () => `[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 }\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,\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","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":"+0BA+BqBA,sBAAsBC,yFAC3B,CACRC,eAAgB,mBAAcC,KAAKC,OAAOC,yCAC1CC,WAAY,mBAAcH,KAAKC,OAAOC,sCACtCE,eAAgB,mBAAcJ,KAAKC,OAAOC,yCAC1CG,YAAa,2BACbC,UAAW,2BACXC,SAAU,uBACVC,eAAgB,mBAAcR,KAAKC,OAAOC,gEAGnC,MAEXO,uBACWC,oBASXC,wBAAwBC,EAAGC,YACjBC,QAAU,CACZC,SAAU,IAAMf,KAAKgB,aAAaH,KAAMD,EAAEK,QAC1CC,SAAU,IAAMlB,KAAKmB,eACrBC,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,oCACZC,KAHgB7C,KAAK8C,eAAejC,gBAO/BkC,kBAAoBC,cAAKC,KAAK,CAACN,UAAU,GAC1C3C,KAAK+C,YAAYG,WACZC,sBAAsBtC,KAAM4B,UAAW,WAEtCzC,KAAKoD,sBAAsBvC,WAC5BwC,YAAYxC,KAAM4B,YAE7B,MAAOS,YACAC,sBAAsBtC,KAAM4B,UAAW,gCAS1Ca,WAAa,IAAIC,oBAAavD,KAAKC,OAAQD,KAAKwD,SAAUxD,KAAKyD,kBAC/DH,WAAWI,uBACZrC,YAAYC,sCAUOT,KAAM4B,eAAWkB,oEAAe,GACnDA,eAEDA,mBAAqB,kBAAU,eAAgB,0BAE9CN,YAAYxC,KAAM4B,gBAClBpB,YAAYuC,QAAQC,mBAAUC,OAAO,kCAAmC,cAAiBH,sBACxFI,QAAUlD,KAAKqB,cAAclC,KAAKmC,UAAU5B,UAC5C0B,YAAcpB,KAAKqB,cAAclC,KAAKmC,UAAUpC,kBACtDgE,QAAQC,UAAUC,OAAO,UACzBhC,YAAY+B,UAAUE,IAAI,sCAQFrD,YAClBsD,sBAAwBtD,KAAKqB,cAAclC,KAAKmC,UAAU/B,kBAC1DgE,UAAYvD,KAAKqB,cAAclC,KAAKmC,UAAU7B,gBAE/CkD,SAAWxD,KAAK+C,YAAYsB,SAGjCF,sBAAsBG,gBAAkBT,mBAAUC,OAAO,yBAA0B,CAC/ES,IAAKvE,KAAK+C,YAAYsB,SACtBG,UAAWxE,KAAKC,OAAOC,GACvBuE,IAAKzE,KAAKyD,mBAERiB,YAAc7D,KAAKqB,cAAclC,KAAKmC,UAAU3B,yBAE/C,IAAImE,SAAQ,CAACC,QAASC,UACzBH,YAAYI,OAAS,KACjBV,UAAUJ,UAAUC,OAAO,UAC3BW,WAEJF,YAAYK,QAAW7B,QACnB2B,OAAO3B,WAUnBJ,eAAejC,YACLmE,WAAY,yBAAahF,KAAKC,QAC9BwD,WAAa5C,KAAKqB,cAAclC,KAAKmC,UAAUhC,cAAcoC,WAC9DkB,WAAaA,iBAKX,CACHwB,UAAWD,UACXE,WAAYzB,WACZ0B,YANgBnF,KAAKoF,sBAAsB,eAAgB,UAO3DC,QANiBrF,KAAKoF,sBAAsB,UAAW,YAOvDE,UAAW,GAUnBF,sBAAsBG,eAAWC,oEAAe,WACtCC,OAASC,SAASC,kBAAkBJ,eACrC,MAAMK,SAASH,UACZG,MAAMC,eACCD,MAAMrD,aAGdiD"}
\ 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 await this.hideLoading(root, submitBtn);\n // Focus the container for accessibility.\n const imageDisplayContainer = root.querySelector(this.SELECTORS.IMAGECONTAINER());\n imageDisplayContainer.focus();\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.modalObject.setBody(await 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 await this.hideLoading(root, submitBtn);\n // Focus the back button for accessibility.\n backBtn.focus();\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 imagElement.focus();\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,\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","focus","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 3e014a5f6bd..6d9b358ed2f 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","core_ai/helper","./options","./textmark","./generatebase"],(function(_exports,_textmodal,_ajax,_str,_templates,_helper,_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),_helper=_interopRequireDefault(_helper),_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=_helper.default.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}}}return _exports.default=GenerateText,_exports.default}));
+define("tiny_aiplacement/generatetext",["exports","./textmodal","core/ajax","core/str","core/templates","core_ai/helper","./options","./textmark","./generatebase"],(function(_exports,_textmodal,_ajax,_str,_templates,_helper,_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),_helper=_interopRequireDefault(_helper),_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"]'),TEXTCONTAINER:()=>'[id="'.concat(this.editor.id,'_tiny_aiplacement_generate_text"]'),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{if(this.responseObj=await _ajax.default.call([request])[0],this.responseObj.error)this.handleGenerationError(root,submitBtn,"");else{await this.displayGeneratedText(root),await this.hideLoading(root,submitBtn);root.querySelector(this.SELECTORS.TEXTCONTAINER()).focus()}}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=_helper.default.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(await _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"),await this.hideLoading(root,submitBtn),backBtn.focus()}async displayGeneratedText(root){const textDisplayContainer=root.querySelector(this.SELECTORS.TEXTCONTAINER()),insertBtn=root.querySelector(this.SELECTORS.INSERTBTN);textDisplayContainer.innerHTML=await _templates.default.render("tiny_aiplacement/textarea",{elementid:this.editor.id,text:this.responseObj.generatedcontent});const textareaElement=root.querySelector(this.SELECTORS.GENERATEDRESPONSE());return new Promise(((resolve,reject)=>{textareaElement.textLength>0?(insertBtn.classList.remove("hidden"),textareaElement.focus(),resolve()):reject()}))}getRequestArgs(root){return{contextid:(0,_options.getContextId)(this.editor),prompttext:root.querySelector(this.SELECTORS.PROMPTAREA()).value}}}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 4aaebd08fb9..7120713a9b0 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 AIHelper from 'core_ai/helper';\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 = AIHelper.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"],"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","AIHelper","replaceLineBreaks","formattedResponse","Templates","render","insertContent","execCommand","windowManager","close","hide","errorMessage","setBody","backBtn","classList","remove","add","contextid","prompttext"],"mappings":"krBAgCqBA,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,WAAaC,gBAASC,kBAAkBN,6BAGnDO,wBAA0BC,mBAAUC,OAAO,8BAA+BhE,KAAK+C,kBAGhF9C,OAAOgE,cAAcH,wBACrB7D,OAAOiE,YAAY,mBACnBjE,OAAOkE,cAAcC,aAGrB/C,YAAYgD,mCAUOxD,KAAM4B,eAAW6B,oEAAe,GACnDA,eAEDA,mBAAqB,kBAAU,eAAgB,0BAE9CjD,YAAYkD,QAAQR,mBAAUC,OAAO,kCAAmC,cAAiBM,sBACxFE,QAAU3D,KAAKqB,cAAclC,KAAKmC,UAAU3B,UAC5CyB,YAAcpB,KAAKqB,cAAclC,KAAKmC,UAAUpC,kBACtDyE,QAAQC,UAAUC,OAAO,UACzBzC,YAAYwC,UAAUE,IAAI,eACrBtB,YAAYxC,KAAM4B,sCAQA5B,MACLA,KAAKqB,cAAclC,KAAKmC,UAAU5B,WAC1CkE,UAAUC,OAAO,UAGE7D,KAAKqB,cAAclC,KAAKmC,UAAU7B,qBAC1CiC,MAAQvC,KAAK+C,YAAYW,iBACtB7C,KAAKqB,cAAclC,KAAKmC,UAAU/B,iBAC1CqE,UAAUC,OAAO,UACL7D,KAAKqB,cAAclC,KAAKmC,UAAU9B,qBAC1CoE,UAAUE,IAAI,UAQtC7B,eAAejC,YAIJ,CACH+D,WAJc,yBAAa5E,KAAKC,QAKhC4E,WAJehE,KAAKqB,cAAclC,KAAKmC,UAAUhC,cAAcoC"}
\ 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 AIHelper from 'core_ai/helper';\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 TEXTCONTAINER: () => `[id=\"${this.editor.id}_tiny_aiplacement_generate_text\"]`,\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 await this.hideLoading(root, submitBtn);\n // Focus the container for accessibility.\n const textDisplayContainer = root.querySelector(this.SELECTORS.TEXTCONTAINER());\n textDisplayContainer.focus();\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 = AIHelper.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(await 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 await this.hideLoading(root, submitBtn);\n // Focus the back button for accessibility.\n backBtn.focus();\n }\n\n /**\n * Display the generated text in the modal.\n *\n * @param {HTMLElement} root - The root element of the modal.\n */\n async displayGeneratedText(root) {\n const textDisplayContainer = root.querySelector(this.SELECTORS.TEXTCONTAINER());\n const insertBtn = root.querySelector(this.SELECTORS.INSERTBTN);\n\n // Render the textarea template and insert it into the modal.\n textDisplayContainer.innerHTML = await Templates.render('tiny_aiplacement/textarea', {\n elementid: this.editor.id,\n text: this.responseObj.generatedcontent,\n });\n const textareaElement = root.querySelector(this.SELECTORS.GENERATEDRESPONSE());\n\n return new Promise((resolve, reject) => {\n if (textareaElement.textLength > 0) {\n insertBtn.classList.remove('hidden');\n textareaElement.focus();\n resolve();\n } else {\n reject();\n }\n });\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"],"names":["GenerateText","GenerateBase","GENERATEBUTTON","this","editor","id","PROMPTAREA","TEXTCONTAINER","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","focus","generatedResponseDiv","wrappedEditedResponse","TinyAiTextMarker","wrapEditedSections","generatedcontent","editedtext","AIHelper","replaceLineBreaks","formattedResponse","Templates","render","insertContent","execCommand","windowManager","close","hide","errorMessage","setBody","backBtn","classList","remove","add","textDisplayContainer","insertBtn","innerHTML","elementid","text","textareaElement","Promise","resolve","reject","textLength","contextid","prompttext"],"mappings":"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"}
\ No newline at end of file
diff --git a/lib/editor/tiny/plugins/aiplacement/amd/src/generateimage.js b/lib/editor/tiny/plugins/aiplacement/amd/src/generateimage.js
index 24cf1436e47..bb2f8b7bfe2 100644
--- a/lib/editor/tiny/plugins/aiplacement/amd/src/generateimage.js
+++ b/lib/editor/tiny/plugins/aiplacement/amd/src/generateimage.js
@@ -105,7 +105,10 @@ export default class GenerateImage extends GenerateBase {
this.handleGenerationError(root, submitBtn, '');
} else {
await this.displayGeneratedImage(root);
- this.hideLoading(root, submitBtn);
+ await this.hideLoading(root, submitBtn);
+ // Focus the container for accessibility.
+ const imageDisplayContainer = root.querySelector(this.SELECTORS.IMAGECONTAINER());
+ imageDisplayContainer.focus();
}
} catch (error) {
this.handleGenerationError(root, submitBtn, '');
@@ -134,12 +137,14 @@ export default class GenerateImage extends GenerateBase {
// Get the default error message.
errorMessage = await getString('errorgeneral', 'tiny_aiplacement');
}
- this.hideLoading(root, submitBtn);
- this.modalObject.setBody(Templates.render('tiny_aiplacement/modalbodyerror', {'errorMessage': errorMessage}));
+ this.modalObject.setBody(await Templates.render('tiny_aiplacement/modalbodyerror', {'errorMessage': errorMessage}));
const backBtn = root.querySelector(this.SELECTORS.BACKTBTN);
const generateBtn = root.querySelector(this.SELECTORS.GENERATEBUTTON());
backBtn.classList.remove('hidden');
generateBtn.classList.add('hidden');
+ await this.hideLoading(root, submitBtn);
+ // Focus the back button for accessibility.
+ backBtn.focus();
}
/**
@@ -164,6 +169,7 @@ export default class GenerateImage extends GenerateBase {
return new Promise((resolve, reject) => {
imagElement.onload = () => {
insertBtn.classList.remove('hidden');
+ imagElement.focus();
resolve(); // Resolve the promise when the image is loaded.
};
imagElement.onerror = (error) => {
diff --git a/lib/editor/tiny/plugins/aiplacement/amd/src/generatetext.js b/lib/editor/tiny/plugins/aiplacement/amd/src/generatetext.js
index 7cdbce262fe..eeace74d55f 100644
--- a/lib/editor/tiny/plugins/aiplacement/amd/src/generatetext.js
+++ b/lib/editor/tiny/plugins/aiplacement/amd/src/generatetext.js
@@ -34,8 +34,7 @@ export default class GenerateText extends GenerateBase {
SELECTORS = {
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',
+ TEXTCONTAINER: () => `[id="${this.editor.id}_tiny_aiplacement_generate_text"]`,
GENERATEDRESPONSE: () => `[id="${this.editor.id}_tiny_aiplacement_textresponse"]`,
INSERTBTN: '[data-action="inserter"]',
BACKTBTN: '[data-action="back"]',
@@ -104,7 +103,10 @@ export default class GenerateText extends GenerateBase {
this.handleGenerationError(root, submitBtn, '');
} else {
await this.displayGeneratedText(root);
- this.hideLoading(root, submitBtn);
+ await this.hideLoading(root, submitBtn);
+ // Focus the container for accessibility.
+ const textDisplayContainer = root.querySelector(this.SELECTORS.TEXTCONTAINER());
+ textDisplayContainer.focus();
}
} catch (error) {
this.handleGenerationError(root, submitBtn, '');
@@ -158,30 +160,41 @@ export default class GenerateText extends GenerateBase {
// Get the default error message.
errorMessage = await getString('errorgeneral', 'tiny_aiplacement');
}
- this.modalObject.setBody(Templates.render('tiny_aiplacement/modalbodyerror', {'errorMessage': errorMessage}));
+ this.modalObject.setBody(await Templates.render('tiny_aiplacement/modalbodyerror', {'errorMessage': errorMessage}));
const backBtn = root.querySelector(this.SELECTORS.BACKTBTN);
const generateBtn = root.querySelector(this.SELECTORS.GENERATEBUTTON());
backBtn.classList.remove('hidden');
generateBtn.classList.add('hidden');
- this.hideLoading(root, submitBtn);
+ await this.hideLoading(root, submitBtn);
+ // Focus the back button for accessibility.
+ backBtn.focus();
}
/**
- * Display the generated image in the modal.
+ * Display the generated text in the modal.
*
* @param {HTMLElement} root - The root element of the modal.
*/
async displayGeneratedText(root) {
+ const textDisplayContainer = root.querySelector(this.SELECTORS.TEXTCONTAINER());
const insertBtn = root.querySelector(this.SELECTORS.INSERTBTN);
- insertBtn.classList.remove('hidden');
- // Add generated text to the modal.
- const generatedResponseDiv = root.querySelector(this.SELECTORS.GENERATEDRESPONSE());
- generatedResponseDiv.value = this.responseObj.generatedcontent;
- const responseWrapper = root.querySelector(this.SELECTORS.RESPONSEWRAPPER);
- responseWrapper.classList.remove('hidden');
- const responsePlaceholder = root.querySelector(this.SELECTORS.RESPONSEPLACEHOLDER);
- responsePlaceholder.classList.add('hidden');
+ // Render the textarea template and insert it into the modal.
+ textDisplayContainer.innerHTML = await Templates.render('tiny_aiplacement/textarea', {
+ elementid: this.editor.id,
+ text: this.responseObj.generatedcontent,
+ });
+ const textareaElement = root.querySelector(this.SELECTORS.GENERATEDRESPONSE());
+
+ return new Promise((resolve, reject) => {
+ if (textareaElement.textLength > 0) {
+ insertBtn.classList.remove('hidden');
+ textareaElement.focus();
+ resolve();
+ } else {
+ reject();
+ }
+ });
}
/**
diff --git a/lib/editor/tiny/plugins/aiplacement/styles.css b/lib/editor/tiny/plugins/aiplacement/styles.css
index 2e0a7cf4865..4c5b0f8f12c 100644
--- a/lib/editor/tiny/plugins/aiplacement/styles.css
+++ b/lib/editor/tiny/plugins/aiplacement/styles.css
@@ -41,7 +41,7 @@
}
.tiny-aiplacement-generate-image,
-.tiny_aiplacement_textresponse_placeholder {
+.tiny-aiplacement-generate-text {
border-radius: 15px;
border: 2px dashed #bbb;
padding: 20px;
@@ -55,7 +55,7 @@
margin-top: 0;
}
-.tiny_aiplacement_textresponse textarea:disabled {
+.tiny-aiplacement-generate-text textarea:disabled {
background-color: unset;
}
@@ -170,13 +170,15 @@
object-fit: contain;
}
-.tiny_aiplacement_textresponse_placeholder {
+.tiny-aiplacement-fa-text {
+ height: 100%;
+ width: 100%;
background-image: url('[[pix:tiny_aiplacement|sparkles-text]]');
background-repeat: no-repeat;
background-position: center;
background-color: rgba(255, 255, 255, 0.5);
background-blend-mode: lighten;
- background-size: 6em 6em;
+ background-size: 100px 100px;
}
.tiny-aiplacement-generate-footer button img {
diff --git a/lib/editor/tiny/plugins/aiplacement/templates/image.mustache b/lib/editor/tiny/plugins/aiplacement/templates/image.mustache
index 16527488180..0d3ffa392b3 100644
--- a/lib/editor/tiny/plugins/aiplacement/templates/image.mustache
+++ b/lib/editor/tiny/plugins/aiplacement/templates/image.mustache
@@ -35,4 +35,4 @@
"alt": "Example image"
}
}}
-
+
diff --git a/lib/editor/tiny/plugins/aiplacement/templates/loading.mustache b/lib/editor/tiny/plugins/aiplacement/templates/loading.mustache
index 9c6d2649175..0bdd6ff84cb 100644
--- a/lib/editor/tiny/plugins/aiplacement/templates/loading.mustache
+++ b/lib/editor/tiny/plugins/aiplacement/templates/loading.mustache
@@ -36,12 +36,8 @@
}}
-
-
- {{#str}} loading, tiny_aiplacement{{/str}}
-
-
-
+
+
{{#str}} loading, tiny_aiplacement{{/str}}
diff --git a/lib/editor/tiny/plugins/aiplacement/templates/modalbodyerror.mustache b/lib/editor/tiny/plugins/aiplacement/templates/modalbodyerror.mustache
index b3f7f4d96cf..ef06ef97620 100644
--- a/lib/editor/tiny/plugins/aiplacement/templates/modalbodyerror.mustache
+++ b/lib/editor/tiny/plugins/aiplacement/templates/modalbodyerror.mustache
@@ -34,7 +34,7 @@
}
}}
-
+
diff --git a/lib/editor/tiny/plugins/aiplacement/templates/modalbodytext.mustache b/lib/editor/tiny/plugins/aiplacement/templates/modalbodytext.mustache
index d53d2163fca..a3a7f04e131 100644
--- a/lib/editor/tiny/plugins/aiplacement/templates/modalbodytext.mustache
+++ b/lib/editor/tiny/plugins/aiplacement/templates/modalbodytext.mustache
@@ -48,18 +48,8 @@
rows="12">
diff --git a/lib/editor/tiny/plugins/aiplacement/templates/textarea.mustache b/lib/editor/tiny/plugins/aiplacement/templates/textarea.mustache
new file mode 100644
index 00000000000..bd15f1d4349
--- /dev/null
+++ b/lib/editor/tiny/plugins/aiplacement/templates/textarea.mustache
@@ -0,0 +1,44 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see
.
+}}
+{{!
+ @template tiny_aiplacement/textarea
+
+ Template for the generated text content within the Tiny Editor.
+
+ Classes required for JS:
+ * none
+
+ Data attributes required for JS:
+ * none
+
+ Context variables required for this template:
+ * elementid - The unique element id
+ * text - The generated text
+
+ Example context (json):
+ {
+ "elementid": "exampleId",
+ "text": "Example text"
+ }
+}}
+
diff --git a/lib/editor/tiny/plugins/aiplacement/templates/textinsert.mustache b/lib/editor/tiny/plugins/aiplacement/templates/textinsert.mustache
index b5a8d41a2b6..12eca9fbb27 100644
--- a/lib/editor/tiny/plugins/aiplacement/templates/textinsert.mustache
+++ b/lib/editor/tiny/plugins/aiplacement/templates/textinsert.mustache
@@ -31,13 +31,13 @@
{
"prompttext": "prompt text",
"timecreated": "1681901609",
- "generatedcontent": "returned AI content"
+ "editedtext": "returned AI content"
}
}}
+ data-generated="true"
+ data-prompt="{{prompttext}}"
+ data-time-created="{{timecreated}}">
{{{editedtext}}}