From 636cacdcc69031eccc62b27af012fb5175fa18c8 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 02373a6fa33..eeee1a56bf3 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 revisedPrompt=this.responseObj.revisedprompt,altTextToUse=revisedPrompt||this.promptText,mediaImage=new _mediaimage.default(this.editor,this.imageURL,altTextToUse);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 revisedPrompt=this.responseObj.revisedprompt,altTextToUse=revisedPrompt||this.promptText,mediaImage=new _mediaimage.default(this.editor,this.imageURL,altTextToUse);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 f7c7778d043..76ed6dadae9 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 // Use the revised prompt for the image alt text if it is available in the response.\n const revisedPrompt = this.responseObj.revisedprompt;\n const altTextToUse = revisedPrompt ? revisedPrompt : this.promptText;\n const mediaImage = new AiMediaImage(this.editor, this.imageURL, altTextToUse);\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","revisedPrompt","revisedprompt","altTextToUse","promptText","mediaImage","AiMediaImage","imageURL","displayDialogue","errorMessage","setBody","Templates","render","backBtn","classList","remove","add","imageDisplayContainer","insertBtn","drafturl","innerHTML","url","elementid","alt","imagElement","Promise","resolve","reject","onload","onerror","contextId","contextid","prompttext","aspectratio","getSelectedRadioValue","quality","numimages","radioName","defaultValue","radios","document","getElementsByName","radio","checked"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"generateimage.min.js","sources":["../src/generateimage.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Tiny AI generate images.\n *\n * @module tiny_aiplacement/generateimage\n * @copyright 2024 Matt Porritt \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ImageModal from 'tiny_aiplacement/imagemodal';\nimport Ajax from 'core/ajax';\nimport {getString} from 'core/str';\nimport Templates from 'core/templates';\nimport AiMediaImage from './mediaimage';\nimport {getContextId} from 'tiny_aiplacement/options';\nimport GenerateBase from 'tiny_aiplacement/generatebase';\n\nexport default class GenerateImage extends GenerateBase {\n SELECTORS = {\n GENERATEBUTTON: () => `[id=\"${this.editor.id}_tiny_aiplacement_generatebutton\"]`,\n PROMPTAREA: () => `[id=\"${this.editor.id}_tiny_aiplacement_imageprompt\"]`,\n IMAGECONTAINER: () => `[id=\"${this.editor.id}_tiny_aiplacement_generate_image\"]`,\n GENERATEBTN: '[data-action=\"generate\"]',\n INSERTBTN: '[data-action=\"inserter\"]',\n BACKTBTN: '[data-action=\"back\"]',\n GENERATEDIMAGE: () => `[id=\"${this.editor.id}_tiny_generated_image\"]`,\n };\n\n imageURL = null;\n\n getModalClass() {\n return ImageModal;\n }\n\n /**\n * Handle click events within the image modal.\n *\n * @param {Event} e - The click event object.\n * @param {HTMLElement} root - The root element of the modal.\n */\n handleContentModalClick(e, root) {\n const actions = {\n generate: () => this.handleSubmit(root, e.target),\n inserter: () => this.handleInsert(),\n cancel: () => this.modalObject.destroy(),\n back: () => {\n this.modalObject.destroy();\n this.displayContentModal();\n },\n };\n\n const actionKey = Object.keys(actions).find(key => e.target.closest(`[data-action=\"${key}\"]`));\n if (actionKey) {\n e.preventDefault();\n actions[actionKey]();\n }\n }\n\n /**\n * Set up the prompt area in the modal, adding necessary event listeners.\n *\n * @param {HTMLElement} root - The root element of the modal.\n */\n setupPromptArea(root) {\n const generateBtn = root.querySelector(this.SELECTORS.GENERATEBUTTON());\n const promptArea = root.querySelector(this.SELECTORS.PROMPTAREA());\n\n promptArea.addEventListener('input', () => {\n generateBtn.disabled = promptArea.value.trim() === '';\n });\n }\n\n /**\n * Handle the submit action.\n *\n * @param {Object} root The root element of the modal.\n * @param {Object} submitBtn The submit button element.\n */\n async handleSubmit(root, submitBtn) {\n await this.displayLoading(root, submitBtn);\n\n const displayArgs = this.getDisplayArgs(root);\n const request = {\n methodname: 'aiplacement_editor_generate_image',\n args: displayArgs\n };\n\n try {\n this.responseObj = await Ajax.call([request])[0];\n if (this.responseObj.error) {\n this.handleGenerationError(root, submitBtn, '');\n } else {\n await this.displayGeneratedImage(root);\n 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 // Use the revised prompt for the image alt text if it is available in the response.\n const revisedPrompt = this.responseObj.revisedprompt;\n const altTextToUse = revisedPrompt ? revisedPrompt : this.promptText;\n const mediaImage = new AiMediaImage(this.editor, this.imageURL, altTextToUse);\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","revisedPrompt","revisedprompt","altTextToUse","promptText","mediaImage","AiMediaImage","imageURL","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 8290c73b9af..090e46784e9 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.formatResponse(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.formatResponse(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 5ae01e10564..41a3803793a 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 and removed any markdown.\n this.responseObj.editedtext = AIHelper.formatResponse(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","formatResponse","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,eAAeN,6BAGhDO,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 and removed any markdown.\n this.responseObj.editedtext = AIHelper.formatResponse(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","formatResponse","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 6bca26f04fb..09b66133219 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, ''); @@ -137,12 +140,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(); } /** @@ -167,6 +172,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 484f796f040..eaad345c130 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" } }} -{{alt}} +{{alt}} diff --git a/lib/editor/tiny/plugins/aiplacement/templates/loading.mustache b/lib/editor/tiny/plugins/aiplacement/templates/loading.mustache index 013218e654e..0bdd6ff84cb 100644 --- a/lib/editor/tiny/plugins/aiplacement/templates/loading.mustache +++ b/lib/editor/tiny/plugins/aiplacement/templates/loading.mustache @@ -36,12 +36,8 @@ }}