diff --git a/.upgradenotes/MDL-85738-2025081910434216.yml b/.upgradenotes/MDL-85738-2025081910434216.yml
new file mode 100644
index 00000000000..fe6732c7804
--- /dev/null
+++ b/.upgradenotes/MDL-85738-2025081910434216.yml
@@ -0,0 +1,11 @@
+issueNumber: MDL-85738
+notes:
+ core_ai:
+ - message: >
+ - Added `get_enabled_actions_in_course_module` method in public/ai/classes/manager.php to get enabled AI actions in course module.
+ - Added `is_ai_tools_enabled_in_course` method in public/ai/classes/manager.php to check if AI tools is enabled in course.
+ - Added `is_action_enabled_in_context` method in public/ai/classes/manager.php to check if an action is enabled in a particular context.
+ - Added `get_ai_fields_from_course_module` method in public/ai/classes/manager.php to get the AI related fields from the course module.
+ - Added `is_html_editor_placement_available` method in public/ai/placement/editor/classes/utils.php to check if editor placement is enabled.
+ - Added `get_actions_available` method in public/ai/placement/editor/classes/utils.php to get available actions for editor placement.
+ type: improved
diff --git a/public/ai/classes/manager.php b/public/ai/classes/manager.php
index 8c3f4bfc546..ec1339726c4 100644
--- a/public/ai/classes/manager.php
+++ b/public/ai/classes/manager.php
@@ -28,6 +28,7 @@ use core\plugininfo\aiprovider as aiproviderplugin;
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class manager {
+
/**
* Create a new AI manager.
*
@@ -338,6 +339,36 @@ class manager {
}
}
+ /**
+ * Check if an action is enabled in a particular context.
+ *
+ * @param \context $context The context to use.
+ * @param string $actionclass The action class name to check.
+ * @return bool Return true enabled and allowed.
+ */
+ public function is_action_enabled_in_context(\context $context, string $actionclass): bool {
+ // Only check if we are in a supported context.
+ if (in_array($context->contextlevel, [CONTEXT_COURSE, CONTEXT_COURSECAT, CONTEXT_MODULE])) {
+ // Return false if AI tools is not enabled at the course level.
+ if (!self::is_ai_tools_enabled_in_course($context)) {
+ return false;
+ }
+
+ if ($context->contextlevel == CONTEXT_MODULE) {
+ // Detect if this is a newly created module (doesn't have any AI settings yet).
+ $record = self::get_ai_fields_from_course_module($context->instanceid);
+ if (is_null($record->enabledaiactions)) {
+ return true;
+ }
+ // Check if the action is one of our enabled ones.
+ $enabledactions = self::get_enabled_actions_in_course_module($record);
+ return in_array($actionclass, $enabledactions);
+ }
+ }
+
+ return true;
+ }
+
/**
* Check if an action is available.
* Action is available if it is enabled for at least one enabled provider.
@@ -684,4 +715,69 @@ class manager {
}
return true;
}
+
+ /**
+ * Get the AI related fields from the course module.
+ *
+ * @param int $id The course module to check.
+ * @return \stdClass Return AI related fields.
+ */
+ public static function get_ai_fields_from_course_module(int $id): \stdClass {
+ global $DB;
+
+ return $DB->get_record(
+ table: 'course_modules',
+ conditions: ['id' => $id],
+ fields: 'enableaitools, enabledaiactions',
+ );
+ }
+
+ /**
+ * Get the enabled actions in a course module context.
+ *
+ * @param \stdClass $record AI related fields from course module.
+ * @return array An array of enabled actions in the course module.
+ */
+ public static function get_enabled_actions_in_course_module(\stdClass $record): array {
+ $enabledaiactions = [];
+
+ if (is_null($record->enableaitools) || $record->enableaitools) {
+ // Get AI action settings and determine which ones are enabled.
+ if (!empty($record->enabledaiactions)) {
+ $enabledaiactions = array_keys(
+ array_filter((array) json_decode($record->enabledaiactions), function ($value): bool {
+ return $value == 1;
+ })
+ );
+ // Set to classname format.
+ foreach ($enabledaiactions as $key => $action) {
+ $enabledaiactions[$key] = "core_ai\\aiactions\\{$action}";
+ }
+ }
+ }
+
+ return $enabledaiactions;
+ }
+
+ /**
+ * Check if AI tools are enabled in the course.
+ *
+ * @param \context $context The context to check.
+ * @return bool True if AI tools are enabled in the course, false otherwise.
+ */
+ public static function is_ai_tools_enabled_in_course(\context $context): bool {
+ global $DB;
+
+ if (in_array($context->contextlevel, [CONTEXT_COURSE, CONTEXT_COURSECAT])) {
+ $courseid = $context->instanceid;
+ } else {
+ $courseid = $DB->get_field('course_modules', 'course', ['id' => $context->instanceid]);
+ }
+
+ $enableaitools = $DB->get_field('course', 'enableaitools', ['id' => $courseid]);
+ if (!is_null($enableaitools) && !$enableaitools) {
+ return false;
+ }
+ return true;
+ }
}
diff --git a/public/ai/placement/courseassist/amd/build/placement.min.js b/public/ai/placement/courseassist/amd/build/placement.min.js
index 730b0c31318..ee48616ed3b 100644
--- a/public/ai/placement/courseassist/amd/build/placement.min.js
+++ b/public/ai/placement/courseassist/amd/build/placement.min.js
@@ -1,3 +1,3 @@
-define("aiplacement_courseassist/placement",["exports","core/templates","core/ajax","core/copy_to_clipboard","core/notification","aiplacement_courseassist/selectors","core_ai/policy","core_ai/helper","core/drawer_events","core/pubsub","core_message/message_drawer_helper","core/str","core/local/aria/focuslock","core/pagehelpers"],(function(_exports,_templates,_ajax,_copy_to_clipboard,_notification,_selectors,_policy,_helper,_drawer_events,_pubsub,MessageDrawerHelper,_str,FocusLock,_pagehelpers){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}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,_templates=_interopRequireDefault(_templates),_ajax=_interopRequireDefault(_ajax),_notification=_interopRequireDefault(_notification),_selectors=_interopRequireDefault(_selectors),_policy=_interopRequireDefault(_policy),_helper=_interopRequireDefault(_helper),_drawer_events=_interopRequireDefault(_drawer_events),MessageDrawerHelper=_interopRequireWildcard(MessageDrawerHelper),FocusLock=_interopRequireWildcard(FocusLock);var _default=class{constructor(userId,contextId){_defineProperty(this,"userId",void 0),_defineProperty(this,"contextId",void 0),this.userId=userId,this.contextId=contextId,this.aiDrawerElement=document.querySelector(_selectors.default.ELEMENTS.AIDRAWER),this.aiDrawerBodyElement=document.querySelector(_selectors.default.ELEMENTS.AIDRAWER_BODY),this.pageElement=document.querySelector(_selectors.default.ELEMENTS.PAGE),this.jumpToElement=document.querySelector(_selectors.default.ELEMENTS.JUMPTO),this.actionElement=document.querySelector(_selectors.default.ELEMENTS.ACTION),this.aiDrawerCloseElement=this.aiDrawerElement.querySelector(_selectors.default.ELEMENTS.AIDRAWER_CLOSE),this.lastAction="",this.responses=new Map,this.isDrawerFocusLocked=!1,this.registerEventListeners()}registerEventListeners(){document.addEventListener("click",(async e=>{if(e.target.closest(_selectors.default.ACTIONS.SUMMARY)){e.preventDefault(),this.openAIDrawer(),this.lastAction="summarise",this.actionElement.focus();if(!await this.isPolicyAccepted())return void this.displayPolicy();this.displayAction(this.lastAction)}if(e.target.closest(_selectors.default.ACTIONS.EXPLAIN)){e.preventDefault(),this.openAIDrawer(),this.lastAction="explain",this.actionElement.focus();if(!await this.isPolicyAccepted())return void this.displayPolicy();this.displayAction(this.lastAction)}e.target.closest(_selectors.default.ELEMENTS.AIDRAWER_CLOSE)&&(e.preventDefault(),this.closeAIDrawer())})),document.addEventListener("keydown",(e=>{this.isAIDrawerOpen()&&"Escape"===e.key&&this.closeAIDrawer()})),(0,_pubsub.subscribe)(_drawer_events.default.DRAWER_SHOWN,(()=>{this.isAIDrawerOpen()&&this.closeAIDrawer()})),this.jumpToElement.addEventListener("focus",(()=>{this.aiDrawerCloseElement.focus()})),this.aiDrawerElement.addEventListener("focus",(()=>{this.actionElement.focus()})),this.actionElement.addEventListener("blur",(()=>{this.actionElement.classList.remove("active")}))}registerPolicyEventListeners(){const acceptAction=document.querySelector(_selectors.default.ACTIONS.ACCEPT),declineAction=document.querySelector(_selectors.default.ACTIONS.DECLINE);acceptAction&&this.lastAction.length&&acceptAction.addEventListener("click",(e=>{e.preventDefault(),this.acceptPolicy().then((()=>this.displayAction(this.lastAction))).catch(_notification.default.exception)})),declineAction&&declineAction.addEventListener("click",(e=>{e.preventDefault(),this.closeAIDrawer()}))}registerErrorEventListeners(){const retryAction=document.querySelector(_selectors.default.ACTIONS.RETRY);retryAction&&this.lastAction.length&&retryAction.addEventListener("click",(e=>{e.preventDefault(),this.displayAction(this.lastAction)}))}registerResponseEventListeners(){document.querySelectorAll(_selectors.default.ACTIONS.REGENERATE).forEach((regenerateAction=>{const responseElement=regenerateAction.closest(_selectors.default.ELEMENTS.RESPONSE);if(regenerateAction&&responseElement){const actionPerformed=responseElement.getAttribute("data-action-performed");regenerateAction.addEventListener("click",(e=>{e.preventDefault(),this.removeResponseFromStack(actionPerformed),this.displayAction(actionPerformed)}))}}))}registerLoadingEventListeners(){const cancelAction=document.querySelector(_selectors.default.ACTIONS.CANCEL);cancelAction&&cancelAction.addEventListener("click",(e=>{e.preventDefault(),this.setRequestCancelled(),this.toggleAIDrawer(),this.removeResponseFromStack("loading");const responses=this.getResponseStack();this.aiDrawerBodyElement.innerHTML=responses}))}isAIDrawerOpen(){return this.aiDrawerElement.classList.contains("show")}isRequestCancelled(){return"1"===this.aiDrawerBodyElement.dataset.cancelled}setRequestCancelled(){this.aiDrawerBodyElement.dataset.cancelled="1"}openAIDrawer(){MessageDrawerHelper.hide(),this.aiDrawerElement.classList.add("show"),this.aiDrawerElement.setAttribute("tabindex",0),this.aiDrawerBodyElement.setAttribute("aria-live","polite"),this.pageElement.classList.contains("show-drawer-right")||this.addPadding(),this.jumpToElement.setAttribute("tabindex",0),this.jumpToElement.focus(),(0,_pagehelpers.isSmall)()&&(FocusLock.trapFocus(this.aiDrawerElement),this.aiDrawerElement.setAttribute("aria-modal","true"),this.aiDrawerElement.setAttribute("role","dialog"),this.isDrawerFocusLocked=!0)}closeAIDrawer(){this.isDrawerFocusLocked&&(FocusLock.untrapFocus(),this.aiDrawerElement.removeAttribute("aria-modal"),this.aiDrawerElement.setAttribute("role","region")),this.aiDrawerElement.classList.remove("show"),this.aiDrawerElement.setAttribute("tabindex",-1),this.aiDrawerBodyElement.removeAttribute("aria-live"),this.pageElement.classList.contains("show-drawer-right")&&"1"===this.aiDrawerBodyElement.dataset.removepadding&&this.removePadding(),this.jumpToElement.setAttribute("tabindex",-1),this.actionElement.classList.add("active"),this.actionElement.focus()}toggleAIDrawer(){this.isAIDrawerOpen()?this.closeAIDrawer():this.openAIDrawer()}addPadding(){this.pageElement.classList.add("show-drawer-right"),this.aiDrawerBodyElement.dataset.removepadding="1"}removePadding(){this.pageElement.classList.remove("show-drawer-right"),this.aiDrawerBodyElement.dataset.removepadding="0"}async getParamsForAction(action){let params={};switch(action){case"summarise":params.method="aiplacement_courseassist_summarise_text",params.heading=await(0,_str.getString)("aisummary","aiplacement_courseassist");break;case"explain":params.method="aiplacement_courseassist_explain_text",params.heading=await(0,_str.getString)("aiexplain","aiplacement_courseassist")}return params}async isPolicyAccepted(){return await _policy.default.getPolicyStatus(this.userId)}acceptPolicy(){return _policy.default.acceptPolicy()}hasGeneratedContent(action){return this.responses.has(action)}displayPolicy(){_templates.default.render("core_ai/policyblock",{}).then((html=>{this.aiDrawerBodyElement.innerHTML=html,this.registerPolicyEventListeners()})).catch(_notification.default.exception)}displayLoading(){_templates.default.render("aiplacement_courseassist/loading",{}).then((html=>{this.addResponseToStack("loading",html);const responses=this.getResponseStack();this.aiDrawerBodyElement.innerHTML=responses,this.registerLoadingEventListeners()})).then((()=>{this.removeResponseFromStack("loading")})).catch(_notification.default.exception)}async displayAction(action){if(this.hasGeneratedContent(action)){const existingReponse=document.querySelector('[data-action-performed="'+action+'"]');existingReponse&&(this.aiDrawerBodyElement.scrollTop=existingReponse.offsetTop)}else{this.displayLoading(),this.aiDrawerBodyElement.innerHTML="";const request={methodname:(await this.getParamsForAction(action)).method,args:{contextid:this.contextId,prompttext:this.getTextContent()}};try{const responseObj=await _ajax.default.call([request])[0];if(responseObj.error)return void this.displayError(responseObj.error,responseObj.errormessage);if(!this.isRequestCancelled()){const generatedContent=_helper.default.formatResponse(responseObj.generatedcontent);return void this.displayResponse(generatedContent,action)}this.aiDrawerBodyElement.dataset.cancelled="0"}catch(error){window.console.log(error),this.displayError()}}}addResponseToStack(action,html){this.responses.set(action,html)}removeResponseFromStack(action){this.responses.has(action)&&this.responses.delete(action)}getResponseStack(){let stack="";const responses=[...this.responses.values()].reverse();for(const response of responses)stack+=response;return stack}async displayResponse(content,action){const args={content:content,heading:(await this.getParamsForAction(action)).heading,action:action};_templates.default.render("aiplacement_courseassist/response",args).then((html=>{this.addResponseToStack(action,html);const responses=this.getResponseStack();this.aiDrawerBodyElement.innerHTML=responses,this.registerResponseEventListeners()})).catch(_notification.default.exception)}async displayError(){let error=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"",errorMessage=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";error||(error=await(0,_str.getString)("error:defaultname","core_ai"),errorMessage=await(0,_str.getString)("error:defaultmessage","core_ai")),_templates.default.render("aiplacement_courseassist/error",{error:error,errorMessage:errorMessage}).then((html=>{this.addResponseToStack("error",html);const responses=this.getResponseStack();this.aiDrawerBodyElement.innerHTML=responses,this.registerErrorEventListeners()})).then((()=>{this.removeResponseFromStack("error")})).catch(_notification.default.exception)}getTextContent(){const mainRegion=document.querySelector(_selectors.default.ELEMENTS.MAIN_REGION);return mainRegion.innerText||mainRegion.textContent}};return _exports.default=_default,_exports.default}));
+define("aiplacement_courseassist/placement",["exports","core/templates","core/ajax","core/copy_to_clipboard","core/notification","aiplacement_courseassist/selectors","core_ai/policy","core_ai/helper","core/drawer_events","core/pubsub","core_message/message_drawer_helper","core/str","core/local/aria/focuslock","core/pagehelpers"],(function(_exports,_templates,_ajax,_copy_to_clipboard,_notification,_selectors,_policy,_helper,_drawer_events,_pubsub,MessageDrawerHelper,_str,FocusLock,_pagehelpers){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}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,_templates=_interopRequireDefault(_templates),_ajax=_interopRequireDefault(_ajax),_notification=_interopRequireDefault(_notification),_selectors=_interopRequireDefault(_selectors),_policy=_interopRequireDefault(_policy),_helper=_interopRequireDefault(_helper),_drawer_events=_interopRequireDefault(_drawer_events),MessageDrawerHelper=_interopRequireWildcard(MessageDrawerHelper),FocusLock=_interopRequireWildcard(FocusLock);var _default=class{constructor(userId,contextId){_defineProperty(this,"userId",void 0),_defineProperty(this,"contextId",void 0),this.userId=userId,this.contextId=contextId,this.aiDrawerElement=document.querySelector(_selectors.default.ELEMENTS.AIDRAWER),this.aiDrawerBodyElement=document.querySelector(_selectors.default.ELEMENTS.AIDRAWER_BODY),this.pageElement=document.querySelector(_selectors.default.ELEMENTS.PAGE),this.jumpToElement=document.querySelector(_selectors.default.ELEMENTS.JUMPTO),this.actionElement=document.querySelector(_selectors.default.ELEMENTS.ACTION),this.aiDrawerCloseElement=this.aiDrawerElement.querySelector(_selectors.default.ELEMENTS.AIDRAWER_CLOSE),this.lastAction="",this.responses=new Map,this.isDrawerFocusLocked=!1,this.registerEventListeners()}registerEventListeners(){document.addEventListener("click",(async e=>{if(e.target.closest(_selectors.default.ACTIONS.SUMMARY)){e.preventDefault(),this.openAIDrawer(),this.lastAction="summarise_text",this.actionElement.focus();if(!await this.isPolicyAccepted())return void this.displayPolicy();this.displayAction(this.lastAction)}if(e.target.closest(_selectors.default.ACTIONS.EXPLAIN)){e.preventDefault(),this.openAIDrawer(),this.lastAction="explain_text",this.actionElement.focus();if(!await this.isPolicyAccepted())return void this.displayPolicy();this.displayAction(this.lastAction)}e.target.closest(_selectors.default.ELEMENTS.AIDRAWER_CLOSE)&&(e.preventDefault(),this.closeAIDrawer())})),document.addEventListener("keydown",(e=>{this.isAIDrawerOpen()&&"Escape"===e.key&&this.closeAIDrawer()})),(0,_pubsub.subscribe)(_drawer_events.default.DRAWER_SHOWN,(()=>{this.isAIDrawerOpen()&&this.closeAIDrawer()})),this.jumpToElement&&this.jumpToElement.addEventListener("focus",(()=>{this.aiDrawerCloseElement.focus()})),this.aiDrawerElement.addEventListener("focus",(()=>{this.actionElement.focus()})),this.actionElement&&this.actionElement.addEventListener("blur",(()=>{this.actionElement.classList.remove("active")}))}registerPolicyEventListeners(){const acceptAction=document.querySelector(_selectors.default.ACTIONS.ACCEPT),declineAction=document.querySelector(_selectors.default.ACTIONS.DECLINE);acceptAction&&this.lastAction.length&&acceptAction.addEventListener("click",(e=>{e.preventDefault(),this.acceptPolicy().then((()=>this.displayAction(this.lastAction))).catch(_notification.default.exception)})),declineAction&&declineAction.addEventListener("click",(e=>{e.preventDefault(),this.closeAIDrawer()}))}registerErrorEventListeners(){const retryAction=document.querySelector(_selectors.default.ACTIONS.RETRY);retryAction&&this.lastAction.length&&retryAction.addEventListener("click",(e=>{e.preventDefault(),this.displayAction(this.lastAction)}))}registerResponseEventListeners(){document.querySelectorAll(_selectors.default.ACTIONS.REGENERATE).forEach((regenerateAction=>{const responseElement=regenerateAction.closest(_selectors.default.ELEMENTS.RESPONSE);if(regenerateAction&&responseElement){const actionPerformed=responseElement.getAttribute("data-action-performed");regenerateAction.addEventListener("click",(e=>{e.preventDefault(),this.removeResponseFromStack(actionPerformed),this.displayAction(actionPerformed)}))}}))}registerLoadingEventListeners(){const cancelAction=document.querySelector(_selectors.default.ACTIONS.CANCEL);cancelAction&&cancelAction.addEventListener("click",(e=>{e.preventDefault(),this.setRequestCancelled(),this.toggleAIDrawer(),this.removeResponseFromStack("loading");const responses=this.getResponseStack();this.aiDrawerBodyElement.innerHTML=responses}))}isAIDrawerOpen(){return this.aiDrawerElement.classList.contains("show")}isRequestCancelled(){return"1"===this.aiDrawerBodyElement.dataset.cancelled}setRequestCancelled(){this.aiDrawerBodyElement.dataset.cancelled="1"}openAIDrawer(){MessageDrawerHelper.hide(),this.aiDrawerElement.classList.add("show"),this.aiDrawerElement.setAttribute("tabindex",0),this.aiDrawerBodyElement.setAttribute("aria-live","polite"),this.pageElement.classList.contains("show-drawer-right")||this.addPadding(),this.jumpToElement.setAttribute("tabindex",0),this.jumpToElement.focus(),(0,_pagehelpers.isSmall)()&&(FocusLock.trapFocus(this.aiDrawerElement),this.aiDrawerElement.setAttribute("aria-modal","true"),this.aiDrawerElement.setAttribute("role","dialog"),this.isDrawerFocusLocked=!0)}closeAIDrawer(){this.isDrawerFocusLocked&&(FocusLock.untrapFocus(),this.aiDrawerElement.removeAttribute("aria-modal"),this.aiDrawerElement.setAttribute("role","region")),this.aiDrawerElement.classList.remove("show"),this.aiDrawerElement.setAttribute("tabindex",-1),this.aiDrawerBodyElement.removeAttribute("aria-live"),this.pageElement.classList.contains("show-drawer-right")&&"1"===this.aiDrawerBodyElement.dataset.removepadding&&this.removePadding(),this.jumpToElement.setAttribute("tabindex",-1),this.actionElement.classList.add("active"),this.actionElement.focus()}toggleAIDrawer(){this.isAIDrawerOpen()?this.closeAIDrawer():this.openAIDrawer()}addPadding(){this.pageElement.classList.add("show-drawer-right"),this.aiDrawerBodyElement.dataset.removepadding="1"}removePadding(){this.pageElement.classList.remove("show-drawer-right"),this.aiDrawerBodyElement.dataset.removepadding="0"}async getParamsForAction(action){let params={};switch(action){case"summarise_text":params.method="aiplacement_courseassist_summarise_text",params.heading=await(0,_str.getString)("aisummary","aiplacement_courseassist");break;case"explain_text":params.method="aiplacement_courseassist_explain_text",params.heading=await(0,_str.getString)("aiexplain","aiplacement_courseassist")}return params}async isPolicyAccepted(){return await _policy.default.getPolicyStatus(this.userId)}acceptPolicy(){return _policy.default.acceptPolicy()}hasGeneratedContent(action){return this.responses.has(action)}displayPolicy(){_templates.default.render("core_ai/policyblock",{}).then((html=>{this.aiDrawerBodyElement.innerHTML=html,this.registerPolicyEventListeners()})).catch(_notification.default.exception)}displayLoading(){_templates.default.render("aiplacement_courseassist/loading",{}).then((html=>{this.addResponseToStack("loading",html);const responses=this.getResponseStack();this.aiDrawerBodyElement.innerHTML=responses,this.registerLoadingEventListeners()})).then((()=>{this.removeResponseFromStack("loading")})).catch(_notification.default.exception)}async displayAction(action){if(this.hasGeneratedContent(action)){const existingReponse=document.querySelector('[data-action-performed="'+action+'"]');existingReponse&&(this.aiDrawerBodyElement.scrollTop=existingReponse.offsetTop)}else{this.displayLoading(),this.aiDrawerBodyElement.innerHTML="";const request={methodname:(await this.getParamsForAction(action)).method,args:{contextid:this.contextId,prompttext:this.getTextContent()}};try{const responseObj=await _ajax.default.call([request])[0];if(responseObj.error)return void this.displayError(responseObj.error,responseObj.errormessage);if(!this.isRequestCancelled()){const generatedContent=_helper.default.formatResponse(responseObj.generatedcontent);return void this.displayResponse(generatedContent,action)}this.aiDrawerBodyElement.dataset.cancelled="0"}catch(error){window.console.log(error),this.displayError()}}}addResponseToStack(action,html){this.responses.set(action,html)}removeResponseFromStack(action){this.responses.has(action)&&this.responses.delete(action)}getResponseStack(){let stack="";const responses=[...this.responses.values()].reverse();for(const response of responses)stack+=response;return stack}async displayResponse(content,action){const args={content:content,heading:(await this.getParamsForAction(action)).heading,action:action};_templates.default.render("aiplacement_courseassist/response",args).then((html=>{this.addResponseToStack(action,html);const responses=this.getResponseStack();this.aiDrawerBodyElement.innerHTML=responses,this.registerResponseEventListeners()})).catch(_notification.default.exception)}async displayError(){let error=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"",errorMessage=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";error||(error=await(0,_str.getString)("error:defaultname","core_ai"),errorMessage=await(0,_str.getString)("error:defaultmessage","core_ai")),_templates.default.render("aiplacement_courseassist/error",{error:error,errorMessage:errorMessage}).then((html=>{this.addResponseToStack("error",html);const responses=this.getResponseStack();this.aiDrawerBodyElement.innerHTML=responses,this.registerErrorEventListeners()})).then((()=>{this.removeResponseFromStack("error")})).catch(_notification.default.exception)}getTextContent(){const mainRegion=document.querySelector(_selectors.default.ELEMENTS.MAIN_REGION);return mainRegion.innerText||mainRegion.textContent}};return _exports.default=_default,_exports.default}));
//# sourceMappingURL=placement.min.js.map
\ No newline at end of file
diff --git a/public/ai/placement/courseassist/amd/build/placement.min.js.map b/public/ai/placement/courseassist/amd/build/placement.min.js.map
index 849e61e7515..a0dfdeee864 100644
--- a/public/ai/placement/courseassist/amd/build/placement.min.js.map
+++ b/public/ai/placement/courseassist/amd/build/placement.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"placement.min.js","sources":["../src/placement.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 * Module to load and render the tools for the AI assist plugin.\n *\n * @module aiplacement_courseassist/placement\n * @copyright 2024 Huong Nguyen \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Templates from 'core/templates';\nimport Ajax from 'core/ajax';\nimport 'core/copy_to_clipboard';\nimport Notification from 'core/notification';\nimport Selectors from 'aiplacement_courseassist/selectors';\nimport Policy from 'core_ai/policy';\nimport AIHelper from 'core_ai/helper';\nimport DrawerEvents from 'core/drawer_events';\nimport {subscribe} from 'core/pubsub';\nimport * as MessageDrawerHelper from 'core_message/message_drawer_helper';\nimport {getString} from 'core/str';\nimport * as FocusLock from 'core/local/aria/focuslock';\nimport {isSmall} from \"core/pagehelpers\";\n\nconst AICourseAssist = class {\n\n /**\n * The user ID.\n * @type {Integer}\n */\n userId;\n /**\n * The context ID.\n * @type {Integer}\n */\n contextId;\n\n /**\n * Constructor.\n * @param {Integer} userId The user ID.\n * @param {Integer} contextId The context ID.\n */\n constructor(userId, contextId) {\n this.userId = userId;\n this.contextId = contextId;\n\n this.aiDrawerElement = document.querySelector(Selectors.ELEMENTS.AIDRAWER);\n this.aiDrawerBodyElement = document.querySelector(Selectors.ELEMENTS.AIDRAWER_BODY);\n this.pageElement = document.querySelector(Selectors.ELEMENTS.PAGE);\n this.jumpToElement = document.querySelector(Selectors.ELEMENTS.JUMPTO);\n this.actionElement = document.querySelector(Selectors.ELEMENTS.ACTION);\n this.aiDrawerCloseElement = this.aiDrawerElement.querySelector(Selectors.ELEMENTS.AIDRAWER_CLOSE);\n this.lastAction = '';\n this.responses = new Map();\n this.isDrawerFocusLocked = false;\n\n this.registerEventListeners();\n }\n\n /**\n * Register event listeners.\n */\n registerEventListeners() {\n document.addEventListener('click', async(e) => {\n // Display summarise.\n const summariseAction = e.target.closest(Selectors.ACTIONS.SUMMARY);\n if (summariseAction) {\n e.preventDefault();\n this.openAIDrawer();\n this.lastAction = 'summarise';\n this.actionElement.focus();\n const isPolicyAccepted = await this.isPolicyAccepted();\n if (!isPolicyAccepted) {\n // Display policy.\n this.displayPolicy();\n return;\n }\n this.displayAction(this.lastAction);\n }\n // Display explain.\n const explainAction = e.target.closest(Selectors.ACTIONS.EXPLAIN);\n if (explainAction) {\n e.preventDefault();\n this.openAIDrawer();\n this.lastAction = 'explain';\n this.actionElement.focus();\n const isPolicyAccepted = await this.isPolicyAccepted();\n if (!isPolicyAccepted) {\n // Display policy.\n this.displayPolicy();\n return;\n }\n this.displayAction(this.lastAction);\n }\n // Close AI drawer.\n const closeAiDrawer = e.target.closest(Selectors.ELEMENTS.AIDRAWER_CLOSE);\n if (closeAiDrawer) {\n e.preventDefault();\n this.closeAIDrawer();\n }\n });\n\n document.addEventListener('keydown', e => {\n if (this.isAIDrawerOpen() && e.key === 'Escape') {\n this.closeAIDrawer();\n }\n });\n\n // Close AI drawer if message drawer is shown.\n subscribe(DrawerEvents.DRAWER_SHOWN, () => {\n if (this.isAIDrawerOpen()) {\n this.closeAIDrawer();\n }\n });\n\n // Focus on the AI drawer's close button when the jump-to element is focused.\n this.jumpToElement.addEventListener('focus', () => {\n this.aiDrawerCloseElement.focus();\n });\n\n // Focus on the action element when the AI drawer container receives focus.\n this.aiDrawerElement.addEventListener('focus', () => {\n this.actionElement.focus();\n });\n\n // Remove active from the action element when it loses focus.\n this.actionElement.addEventListener('blur', () => {\n this.actionElement.classList.remove('active');\n });\n }\n\n /**\n * Register event listeners for the policy.\n */\n registerPolicyEventListeners() {\n const acceptAction = document.querySelector(Selectors.ACTIONS.ACCEPT);\n const declineAction = document.querySelector(Selectors.ACTIONS.DECLINE);\n if (acceptAction && this.lastAction.length) {\n acceptAction.addEventListener('click', (e) => {\n e.preventDefault();\n this.acceptPolicy().then(() => {\n return this.displayAction(this.lastAction);\n }).catch(Notification.exception);\n });\n }\n if (declineAction) {\n declineAction.addEventListener('click', (e) => {\n e.preventDefault();\n this.closeAIDrawer();\n });\n }\n }\n\n /**\n * Register event listeners for the error.\n */\n registerErrorEventListeners() {\n const retryAction = document.querySelector(Selectors.ACTIONS.RETRY);\n if (retryAction && this.lastAction.length) {\n retryAction.addEventListener('click', (e) => {\n e.preventDefault();\n this.displayAction(this.lastAction);\n });\n }\n }\n\n /**\n * Register event listeners for the responses.\n */\n registerResponseEventListeners() {\n // Get all regenerate action buttons (one per response in the AI drawer).\n const regenerateActions = document.querySelectorAll(Selectors.ACTIONS.REGENERATE);\n // Add event listeners for each regenerate action.\n regenerateActions.forEach(regenerateAction => {\n const responseElement = regenerateAction.closest(Selectors.ELEMENTS.RESPONSE);\n if (regenerateAction && responseElement) {\n // Get the action that this response is associated with.\n const actionPerformed = responseElement.getAttribute('data-action-performed');\n regenerateAction.addEventListener('click', (e) => {\n e.preventDefault();\n // Remove the old response before displaying the new one.\n this.removeResponseFromStack(actionPerformed);\n this.displayAction(actionPerformed);\n });\n }\n });\n }\n\n registerLoadingEventListeners() {\n const cancelAction = document.querySelector(Selectors.ACTIONS.CANCEL);\n if (cancelAction) {\n cancelAction.addEventListener('click', (e) => {\n e.preventDefault();\n this.setRequestCancelled();\n this.toggleAIDrawer();\n this.removeResponseFromStack('loading');\n // Refresh the response stack to avoid false indication of loading.\n const responses = this.getResponseStack();\n this.aiDrawerBodyElement.innerHTML = responses;\n });\n }\n }\n\n /**\n * Check if the AI drawer is open.\n * @return {boolean} True if the AI drawer is open, false otherwise.\n */\n isAIDrawerOpen() {\n return this.aiDrawerElement.classList.contains('show');\n }\n\n /**\n * Check if the request is cancelled.\n * @return {boolean} True if the request is cancelled, false otherwise.\n */\n isRequestCancelled() {\n return this.aiDrawerBodyElement.dataset.cancelled === '1';\n }\n\n setRequestCancelled() {\n this.aiDrawerBodyElement.dataset.cancelled = '1';\n }\n\n /**\n * Open the AI drawer.\n */\n openAIDrawer() {\n // Close message drawer if it is shown.\n MessageDrawerHelper.hide();\n this.aiDrawerElement.classList.add('show');\n this.aiDrawerElement.setAttribute('tabindex', 0);\n this.aiDrawerBodyElement.setAttribute('aria-live', 'polite');\n if (!this.pageElement.classList.contains('show-drawer-right')) {\n this.addPadding();\n }\n this.jumpToElement.setAttribute('tabindex', 0);\n this.jumpToElement.focus();\n\n // If the AI drawer is opened on a small screen, we need to trap the focus tab within the AI drawer.\n if (isSmall()) {\n FocusLock.trapFocus(this.aiDrawerElement);\n this.aiDrawerElement.setAttribute('aria-modal', 'true');\n this.aiDrawerElement.setAttribute('role', 'dialog');\n this.isDrawerFocusLocked = true;\n }\n }\n\n /**\n * Close the AI drawer.\n */\n closeAIDrawer() {\n // Untrap focus if it was locked.\n if (this.isDrawerFocusLocked) {\n FocusLock.untrapFocus();\n this.aiDrawerElement.removeAttribute('aria-modal');\n this.aiDrawerElement.setAttribute('role', 'region');\n }\n\n this.aiDrawerElement.classList.remove('show');\n this.aiDrawerElement.setAttribute('tabindex', -1);\n this.aiDrawerBodyElement.removeAttribute('aria-live');\n if (this.pageElement.classList.contains('show-drawer-right') && this.aiDrawerBodyElement.dataset.removepadding === '1') {\n this.removePadding();\n }\n this.jumpToElement.setAttribute('tabindex', -1);\n\n // We can enforce a focus-visible state on the focus element using element.focus({focusVisible: true}).\n // Unfortunately, this feature isn't supported in all browsers, only Firefox provides support for it.\n // Therefore, we will apply the active class to the action element and set focus on it.\n // This action will make the action element appear focused.\n // When the action element loses focus,\n // we will remove the active class at {@see registerEventListeners()}\n this.actionElement.classList.add('active');\n this.actionElement.focus();\n }\n\n /**\n * Toggle the AI drawer.\n */\n toggleAIDrawer() {\n if (this.isAIDrawerOpen()) {\n this.closeAIDrawer();\n } else {\n this.openAIDrawer();\n }\n }\n\n /**\n * Add padding to the page to make space for the AI drawer.\n */\n addPadding() {\n this.pageElement.classList.add('show-drawer-right');\n this.aiDrawerBodyElement.dataset.removepadding = '1';\n }\n\n /**\n * Remove padding from the page.\n */\n removePadding() {\n this.pageElement.classList.remove('show-drawer-right');\n this.aiDrawerBodyElement.dataset.removepadding = '0';\n }\n\n /**\n * Get important params related to the action.\n * @param {string} action The action to use.\n * @returns {object} The params to use for the action.\n */\n async getParamsForAction(action) {\n let params = {};\n\n switch (action) {\n case 'summarise':\n params.method = 'aiplacement_courseassist_summarise_text';\n params.heading = await getString('aisummary', 'aiplacement_courseassist');\n break;\n\n case 'explain':\n params.method = 'aiplacement_courseassist_explain_text';\n params.heading = await getString('aiexplain', 'aiplacement_courseassist');\n break;\n }\n\n return params;\n }\n\n /**\n * Check if the policy is accepted.\n * @return {bool} True if the policy is accepted, false otherwise.\n */\n async isPolicyAccepted() {\n return await Policy.getPolicyStatus(this.userId);\n }\n\n /**\n * Accept the policy.\n * @return {Promise