From a55c0e0067aaa5ec7f3556eb61fa25f53e04683f Mon Sep 17 00:00:00 2001
From: David Woloszyn
Date: Tue, 1 Apr 2025 15:22:54 +1100
Subject: [PATCH] MDL-84806 tiny_aiplacement: Announce action results for
accessibility
The generatetext.js and associated templates have been adjusted to
match more closely with the generate image files. The generate image
way of displaying content was better suited to aria.
---
.../amd/build/generateimage.min.js | 2 +-
.../amd/build/generateimage.min.js.map | 2 +-
.../aiplacement/amd/build/generatetext.min.js | 2 +-
.../amd/build/generatetext.min.js.map | 2 +-
.../aiplacement/amd/src/generateimage.js | 12 +++--
.../aiplacement/amd/src/generatetext.js | 41 +++++++++++------
.../tiny/plugins/aiplacement/styles.css | 10 +++--
.../aiplacement/templates/image.mustache | 2 +-
.../aiplacement/templates/loading.mustache | 8 +---
.../templates/modalbodyerror.mustache | 2 +-
.../templates/modalbodytext.mustache | 14 +-----
.../aiplacement/templates/textarea.mustache | 44 +++++++++++++++++++
.../aiplacement/templates/textinsert.mustache | 8 ++--
13 files changed, 100 insertions(+), 49 deletions(-)
create mode 100644 lib/editor/tiny/plugins/aiplacement/templates/textarea.mustache
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}}}