diff --git a/ai/placement/courseassist/amd/build/placement.min.js b/ai/placement/courseassist/amd/build/placement.min.js index 44190ab2205..14d10d6ba79 100644 --- a/ai/placement/courseassist/amd/build/placement.min.js +++ b/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"],(function(_exports,_templates,_ajax,_copy_to_clipboard,_notification,_selectors,_policy,_helper,_drawer_events,_pubsub,MessageDrawerHelper,_str){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 _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=function(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]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(MessageDrawerHelper);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.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())})),(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()}closeAIDrawer(){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();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)}displayError(){_templates.default.render("aiplacement_courseassist/error",{}).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",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();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)}displayError(){_templates.default.render("aiplacement_courseassist/error",{}).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/ai/placement/courseassist/amd/build/placement.min.js.map b/ai/placement/courseassist/amd/build/placement.min.js.map index 5d5fef385e1..1dd661fc44e 100644 --- a/ai/placement/courseassist/amd/build/placement.min.js.map +++ b/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';\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\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 // 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\n /**\n * Close the AI drawer.\n */\n closeAIDrawer() {\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}\n */\n acceptPolicy() {\n return Policy.acceptPolicy();\n }\n\n /**\n * Check if the AI drawer has already generated content for a particular action.\n * @param {string} action The action to check.\n * @return {boolean} True if the AI drawer has generated content, false otherwise.\n */\n hasGeneratedContent(action) {\n return this.responses.has(action);\n }\n\n /**\n * Display the policy.\n */\n displayPolicy() {\n Templates.render('core_ai/policyblock', {}).then((html) => {\n this.aiDrawerBodyElement.innerHTML = html;\n this.registerPolicyEventListeners();\n return;\n }).catch(Notification.exception);\n }\n\n /**\n * Display the loading spinner.\n */\n displayLoading() {\n Templates.render('aiplacement_courseassist/loading', {}).then((html) => {\n this.addResponseToStack('loading', html);\n const responses = this.getResponseStack();\n this.aiDrawerBodyElement.innerHTML = responses;\n this.registerLoadingEventListeners();\n return;\n }).then(() => {\n this.removeResponseFromStack('loading');\n return;\n }).catch(Notification.exception);\n }\n\n /**\n * Display the action result in the AI drawer.\n * @param {string} action The action to display.\n */\n async displayAction(action) {\n if (this.hasGeneratedContent(action)) {\n // Scroll to generated content.\n const existingReponse = document.querySelector('[data-action-performed=\"' + action + '\"]');\n if (existingReponse) {\n this.aiDrawerBodyElement.scrollTop = existingReponse.offsetTop;\n }\n } else {\n // Display loading spinner.\n this.displayLoading();\n // Clear the drawer to prevent including the previously generated response in the new response prompt.\n this.aiDrawerBodyElement.innerHTML = '';\n const params = await this.getParamsForAction(action);\n const request = {\n methodname: params.method,\n args: {\n contextid: this.contextId,\n prompttext: this.getTextContent(),\n }\n };\n try {\n const responseObj = await Ajax.call([request])[0];\n if (responseObj.error) {\n this.displayError();\n return;\n } else {\n if (!this.isRequestCancelled()) {\n // Perform replacements on the generated context to ensure it is formatted correctly.\n const generatedContent = AIHelper.formatResponse(responseObj.generatedcontent);\n this.displayResponse(generatedContent, action);\n return;\n } else {\n this.aiDrawerBodyElement.dataset.cancelled = '0';\n }\n }\n } catch (error) {\n window.console.log(error);\n this.displayError();\n }\n }\n }\n\n /**\n * Add the HTML response to the response stack.\n * The stack will be used to display all responses in the AI drawer.\n * @param {String} action The action key.\n * @param {String} html The HTML to store.\n */\n addResponseToStack(action, html) {\n this.responses.set(action, html);\n }\n\n /**\n * Remove a stored response, allowing for a regenerated one.\n * @param {String} action The action key.\n */\n removeResponseFromStack(action) {\n if (this.responses.has(action)) {\n this.responses.delete(action);\n }\n }\n\n /**\n * Return a stack of HTML responses.\n * @return {String} HTML responses.\n */\n getResponseStack() {\n let stack = '';\n // Reverse to get newest first.\n const responses = [...this.responses.values()].reverse();\n for (const response of responses) {\n stack += response;\n }\n return stack;\n }\n\n /**\n * Display the responses.\n * @param {String} content The content to display.\n * @param {String} action The action used.\n */\n async displayResponse(content, action) {\n const params = await this.getParamsForAction(action);\n const args = {\n content: content,\n heading: params.heading,\n action: action,\n };\n Templates.render('aiplacement_courseassist/response', args).then((html) => {\n this.addResponseToStack(action, html);\n const responses = this.getResponseStack();\n this.aiDrawerBodyElement.innerHTML = responses;\n this.registerResponseEventListeners();\n return;\n }).catch(Notification.exception);\n }\n\n /**\n * Display the error.\n */\n displayError() {\n Templates.render('aiplacement_courseassist/error', {}).then((html) => {\n this.addResponseToStack('error', html);\n const responses = this.getResponseStack();\n this.aiDrawerBodyElement.innerHTML = responses;\n this.registerErrorEventListeners();\n return;\n }).then(() => {\n this.removeResponseFromStack('error');\n return;\n }).catch(Notification.exception);\n }\n\n /**\n * Get the text content of the main region.\n * @return {String} The text content.\n */\n getTextContent() {\n const mainRegion = document.querySelector(Selectors.ELEMENTS.MAIN_REGION);\n return mainRegion.innerText || mainRegion.textContent;\n }\n};\n\nexport default AICourseAssist;\n"],"names":["constructor","userId","contextId","aiDrawerElement","document","querySelector","Selectors","ELEMENTS","AIDRAWER","aiDrawerBodyElement","AIDRAWER_BODY","pageElement","PAGE","jumpToElement","JUMPTO","actionElement","ACTION","aiDrawerCloseElement","this","AIDRAWER_CLOSE","lastAction","responses","Map","registerEventListeners","addEventListener","async","e","target","closest","ACTIONS","SUMMARY","preventDefault","openAIDrawer","focus","isPolicyAccepted","displayPolicy","displayAction","EXPLAIN","closeAIDrawer","DrawerEvents","DRAWER_SHOWN","isAIDrawerOpen","classList","remove","registerPolicyEventListeners","acceptAction","ACCEPT","declineAction","DECLINE","length","acceptPolicy","then","catch","Notification","exception","registerErrorEventListeners","retryAction","RETRY","registerResponseEventListeners","querySelectorAll","REGENERATE","forEach","regenerateAction","responseElement","RESPONSE","actionPerformed","getAttribute","removeResponseFromStack","registerLoadingEventListeners","cancelAction","CANCEL","setRequestCancelled","toggleAIDrawer","getResponseStack","innerHTML","contains","isRequestCancelled","dataset","cancelled","MessageDrawerHelper","hide","add","setAttribute","addPadding","removeAttribute","removepadding","removePadding","action","params","method","heading","Policy","getPolicyStatus","hasGeneratedContent","has","render","html","displayLoading","addResponseToStack","existingReponse","scrollTop","offsetTop","request","methodname","getParamsForAction","args","contextid","prompttext","getTextContent","responseObj","Ajax","call","error","displayError","generatedContent","AIHelper","formatResponse","generatedcontent","displayResponse","window","console","log","set","delete","stack","values","reverse","response","content","mainRegion","MAIN_REGION","innerText","textContent"],"mappings":"mgEAmCuB,MAkBnBA,YAAYC,OAAQC,+FACXD,OAASA,YACTC,UAAYA,eAEZC,gBAAkBC,SAASC,cAAcC,mBAAUC,SAASC,eAC5DC,oBAAsBL,SAASC,cAAcC,mBAAUC,SAASG,oBAChEC,YAAcP,SAASC,cAAcC,mBAAUC,SAASK,WACxDC,cAAgBT,SAASC,cAAcC,mBAAUC,SAASO,aAC1DC,cAAgBX,SAASC,cAAcC,mBAAUC,SAASS,aAC1DC,qBAAuBC,KAAKf,gBAAgBE,cAAcC,mBAAUC,SAASY,qBAC7EC,WAAa,QACbC,UAAY,IAAIC,SAEhBC,yBAMTA,yBACInB,SAASoB,iBAAiB,SAASC,MAAAA,OAEPC,EAAEC,OAAOC,QAAQtB,mBAAUuB,QAAQC,SACtC,CACjBJ,EAAEK,sBACGC,oBACAZ,WAAa,iBACbL,cAAckB,kBACYf,KAAKgB,oCAG3BC,qBAGJC,cAAclB,KAAKE,eAGNM,EAAEC,OAAOC,QAAQtB,mBAAUuB,QAAQQ,SACtC,CACfX,EAAEK,sBACGC,oBACAZ,WAAa,eACbL,cAAckB,kBACYf,KAAKgB,oCAG3BC,qBAGJC,cAAclB,KAAKE,YAGNM,EAAEC,OAAOC,QAAQtB,mBAAUC,SAASY,kBAEtDO,EAAEK,sBACGO,0CAKHC,uBAAaC,cAAc,KAC7BtB,KAAKuB,uBACAH,wBAKRzB,cAAcW,iBAAiB,SAAS,UACpCP,qBAAqBgB,gBAIzB9B,gBAAgBqB,iBAAiB,SAAS,UACtCT,cAAckB,gBAIlBlB,cAAcS,iBAAiB,QAAQ,UACnCT,cAAc2B,UAAUC,OAAO,aAO5CC,qCACUC,aAAezC,SAASC,cAAcC,mBAAUuB,QAAQiB,QACxDC,cAAgB3C,SAASC,cAAcC,mBAAUuB,QAAQmB,SAC3DH,cAAgB3B,KAAKE,WAAW6B,QAChCJ,aAAarB,iBAAiB,SAAUE,IACpCA,EAAEK,sBACGmB,eAAeC,MAAK,IACdjC,KAAKkB,cAAclB,KAAKE,cAChCgC,MAAMC,sBAAaC,cAG1BP,eACAA,cAAcvB,iBAAiB,SAAUE,IACrCA,EAAEK,sBACGO,mBAQjBiB,oCACUC,YAAcpD,SAASC,cAAcC,mBAAUuB,QAAQ4B,OACzDD,aAAetC,KAAKE,WAAW6B,QAC/BO,YAAYhC,iBAAiB,SAAUE,IACnCA,EAAEK,sBACGK,cAAclB,KAAKE,eAQpCsC,iCAE8BtD,SAASuD,iBAAiBrD,mBAAUuB,QAAQ+B,YAEpDC,SAAQC,yBAChBC,gBAAkBD,iBAAiBlC,QAAQtB,mBAAUC,SAASyD,aAChEF,kBAAoBC,gBAAiB,OAE/BE,gBAAkBF,gBAAgBG,aAAa,yBACrDJ,iBAAiBtC,iBAAiB,SAAUE,IACxCA,EAAEK,sBAEGoC,wBAAwBF,sBACxB7B,cAAc6B,wBAMnCG,sCACUC,aAAejE,SAASC,cAAcC,mBAAUuB,QAAQyC,QAC1DD,cACAA,aAAa7C,iBAAiB,SAAUE,IACpCA,EAAEK,sBACGwC,2BACAC,sBACAL,wBAAwB,iBAEvB9C,UAAYH,KAAKuD,wBAClBhE,oBAAoBiE,UAAYrD,aASjDoB,wBACWvB,KAAKf,gBAAgBuC,UAAUiC,SAAS,QAOnDC,2BAC0D,MAA/C1D,KAAKT,oBAAoBoE,QAAQC,UAG5CP,2BACS9D,oBAAoBoE,QAAQC,UAAY,IAMjD9C,eAEI+C,oBAAoBC,YACf7E,gBAAgBuC,UAAUuC,IAAI,aAC9B9E,gBAAgB+E,aAAa,WAAY,QACzCzE,oBAAoByE,aAAa,YAAa,UAC9ChE,KAAKP,YAAY+B,UAAUiC,SAAS,2BAChCQ,kBAEJtE,cAAcqE,aAAa,WAAY,QACvCrE,cAAcoB,QAMvBK,qBACSnC,gBAAgBuC,UAAUC,OAAO,aACjCxC,gBAAgB+E,aAAa,YAAa,QAC1CzE,oBAAoB2E,gBAAgB,aACrClE,KAAKP,YAAY+B,UAAUiC,SAAS,sBAA2E,MAAnDzD,KAAKT,oBAAoBoE,QAAQQ,oBACxFC,qBAEJzE,cAAcqE,aAAa,YAAa,QAQxCnE,cAAc2B,UAAUuC,IAAI,eAC5BlE,cAAckB,QAMvBuC,iBACQtD,KAAKuB,sBACAH,qBAEAN,eAObmD,kBACSxE,YAAY+B,UAAUuC,IAAI,0BAC1BxE,oBAAoBoE,QAAQQ,cAAgB,IAMrDC,qBACS3E,YAAY+B,UAAUC,OAAO,0BAC7BlC,oBAAoBoE,QAAQQ,cAAgB,6BAQ5BE,YACjBC,OAAS,UAELD,YACC,YACDC,OAAOC,OAAS,0CAChBD,OAAOE,cAAgB,kBAAU,YAAa,sCAG7C,UACDF,OAAOC,OAAS,wCAChBD,OAAOE,cAAgB,kBAAU,YAAa,mCAI/CF,6CAQMG,gBAAOC,gBAAgB1E,KAAKjB,QAO7CiD,sBACWyC,gBAAOzC,eAQlB2C,oBAAoBN,eACTrE,KAAKG,UAAUyE,IAAIP,QAM9BpD,mCACc4D,OAAO,sBAAuB,IAAI5C,MAAM6C,YACzCvF,oBAAoBiE,UAAYsB,UAChCpD,kCAENQ,MAAMC,sBAAaC,WAM1B2C,oCACcF,OAAO,mCAAoC,IAAI5C,MAAM6C,YACtDE,mBAAmB,UAAWF,YAC7B3E,UAAYH,KAAKuD,wBAClBhE,oBAAoBiE,UAAYrD,eAChC+C,mCAENjB,MAAK,UACCgB,wBAAwB,cAE9Bf,MAAMC,sBAAaC,+BAONiC,WACZrE,KAAK2E,oBAAoBN,QAAS,OAE5BY,gBAAkB/F,SAASC,cAAc,2BAA6BkF,OAAS,MACjFY,uBACK1F,oBAAoB2F,UAAYD,gBAAgBE,eAEtD,MAEEJ,sBAEAxF,oBAAoBiE,UAAY,SAE/B4B,QAAU,CACZC,kBAFiBrF,KAAKsF,mBAAmBjB,SAEtBE,OACnBgB,KAAM,CACFC,UAAWxF,KAAKhB,UAChByG,WAAYzF,KAAK0F,6BAIfC,kBAAoBC,cAAKC,KAAK,CAACT,UAAU,MAC3CO,YAAYG,uBACPC,mBAGA/F,KAAK0D,qBAAsB,OAEtBsC,iBAAmBC,gBAASC,eAAeP,YAAYQ,mCACxDC,gBAAgBJ,iBAAkB3B,aAGlC9E,oBAAoBoE,QAAQC,UAAY,IAGvD,MAAOkC,OACLO,OAAOC,QAAQC,IAAIT,YACdC,iBAWjBf,mBAAmBX,OAAQS,WAClB3E,UAAUqG,IAAInC,OAAQS,MAO/B7B,wBAAwBoB,QAChBrE,KAAKG,UAAUyE,IAAIP,cACdlE,UAAUsG,OAAOpC,QAQ9Bd,uBACQmD,MAAQ,SAENvG,UAAY,IAAIH,KAAKG,UAAUwG,UAAUC,cAC1C,MAAMC,YAAY1G,UACnBuG,OAASG,gBAENH,4BAQWI,QAASzC,cAErBkB,KAAO,CACTuB,QAASA,QACTtC,eAHiBxE,KAAKsF,mBAAmBjB,SAGzBG,QAChBH,OAAQA,2BAEFQ,OAAO,oCAAqCU,MAAMtD,MAAM6C,YACzDE,mBAAmBX,OAAQS,YAC1B3E,UAAYH,KAAKuD,wBAClBhE,oBAAoBiE,UAAYrD,eAChCqC,oCAENN,MAAMC,sBAAaC,WAM1B2D,kCACclB,OAAO,iCAAkC,IAAI5C,MAAM6C,YACpDE,mBAAmB,QAASF,YAC3B3E,UAAYH,KAAKuD,wBAClBhE,oBAAoBiE,UAAYrD,eAChCkC,iCAENJ,MAAK,UACCgB,wBAAwB,YAE9Bf,MAAMC,sBAAaC,WAO1BsD,uBACUqB,WAAa7H,SAASC,cAAcC,mBAAUC,SAAS2H,oBACtDD,WAAWE,WAAaF,WAAWG"} \ No newline at end of file +{"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}\n */\n acceptPolicy() {\n return Policy.acceptPolicy();\n }\n\n /**\n * Check if the AI drawer has already generated content for a particular action.\n * @param {string} action The action to check.\n * @return {boolean} True if the AI drawer has generated content, false otherwise.\n */\n hasGeneratedContent(action) {\n return this.responses.has(action);\n }\n\n /**\n * Display the policy.\n */\n displayPolicy() {\n Templates.render('core_ai/policyblock', {}).then((html) => {\n this.aiDrawerBodyElement.innerHTML = html;\n this.registerPolicyEventListeners();\n return;\n }).catch(Notification.exception);\n }\n\n /**\n * Display the loading spinner.\n */\n displayLoading() {\n Templates.render('aiplacement_courseassist/loading', {}).then((html) => {\n this.addResponseToStack('loading', html);\n const responses = this.getResponseStack();\n this.aiDrawerBodyElement.innerHTML = responses;\n this.registerLoadingEventListeners();\n return;\n }).then(() => {\n this.removeResponseFromStack('loading');\n return;\n }).catch(Notification.exception);\n }\n\n /**\n * Display the action result in the AI drawer.\n * @param {string} action The action to display.\n */\n async displayAction(action) {\n if (this.hasGeneratedContent(action)) {\n // Scroll to generated content.\n const existingReponse = document.querySelector('[data-action-performed=\"' + action + '\"]');\n if (existingReponse) {\n this.aiDrawerBodyElement.scrollTop = existingReponse.offsetTop;\n }\n } else {\n // Display loading spinner.\n this.displayLoading();\n // Clear the drawer to prevent including the previously generated response in the new response prompt.\n this.aiDrawerBodyElement.innerHTML = '';\n const params = await this.getParamsForAction(action);\n const request = {\n methodname: params.method,\n args: {\n contextid: this.contextId,\n prompttext: this.getTextContent(),\n }\n };\n try {\n const responseObj = await Ajax.call([request])[0];\n if (responseObj.error) {\n this.displayError();\n return;\n } else {\n if (!this.isRequestCancelled()) {\n // Perform replacements on the generated context to ensure it is formatted correctly.\n const generatedContent = AIHelper.formatResponse(responseObj.generatedcontent);\n this.displayResponse(generatedContent, action);\n return;\n } else {\n this.aiDrawerBodyElement.dataset.cancelled = '0';\n }\n }\n } catch (error) {\n window.console.log(error);\n this.displayError();\n }\n }\n }\n\n /**\n * Add the HTML response to the response stack.\n * The stack will be used to display all responses in the AI drawer.\n * @param {String} action The action key.\n * @param {String} html The HTML to store.\n */\n addResponseToStack(action, html) {\n this.responses.set(action, html);\n }\n\n /**\n * Remove a stored response, allowing for a regenerated one.\n * @param {String} action The action key.\n */\n removeResponseFromStack(action) {\n if (this.responses.has(action)) {\n this.responses.delete(action);\n }\n }\n\n /**\n * Return a stack of HTML responses.\n * @return {String} HTML responses.\n */\n getResponseStack() {\n let stack = '';\n // Reverse to get newest first.\n const responses = [...this.responses.values()].reverse();\n for (const response of responses) {\n stack += response;\n }\n return stack;\n }\n\n /**\n * Display the responses.\n * @param {String} content The content to display.\n * @param {String} action The action used.\n */\n async displayResponse(content, action) {\n const params = await this.getParamsForAction(action);\n const args = {\n content: content,\n heading: params.heading,\n action: action,\n };\n Templates.render('aiplacement_courseassist/response', args).then((html) => {\n this.addResponseToStack(action, html);\n const responses = this.getResponseStack();\n this.aiDrawerBodyElement.innerHTML = responses;\n this.registerResponseEventListeners();\n return;\n }).catch(Notification.exception);\n }\n\n /**\n * Display the error.\n */\n displayError() {\n Templates.render('aiplacement_courseassist/error', {}).then((html) => {\n this.addResponseToStack('error', html);\n const responses = this.getResponseStack();\n this.aiDrawerBodyElement.innerHTML = responses;\n this.registerErrorEventListeners();\n return;\n }).then(() => {\n this.removeResponseFromStack('error');\n return;\n }).catch(Notification.exception);\n }\n\n /**\n * Get the text content of the main region.\n * @return {String} The text content.\n */\n getTextContent() {\n const mainRegion = document.querySelector(Selectors.ELEMENTS.MAIN_REGION);\n return mainRegion.innerText || mainRegion.textContent;\n }\n};\n\nexport default AICourseAssist;\n"],"names":["constructor","userId","contextId","aiDrawerElement","document","querySelector","Selectors","ELEMENTS","AIDRAWER","aiDrawerBodyElement","AIDRAWER_BODY","pageElement","PAGE","jumpToElement","JUMPTO","actionElement","ACTION","aiDrawerCloseElement","this","AIDRAWER_CLOSE","lastAction","responses","Map","isDrawerFocusLocked","registerEventListeners","addEventListener","async","e","target","closest","ACTIONS","SUMMARY","preventDefault","openAIDrawer","focus","isPolicyAccepted","displayPolicy","displayAction","EXPLAIN","closeAIDrawer","isAIDrawerOpen","key","DrawerEvents","DRAWER_SHOWN","classList","remove","registerPolicyEventListeners","acceptAction","ACCEPT","declineAction","DECLINE","length","acceptPolicy","then","catch","Notification","exception","registerErrorEventListeners","retryAction","RETRY","registerResponseEventListeners","querySelectorAll","REGENERATE","forEach","regenerateAction","responseElement","RESPONSE","actionPerformed","getAttribute","removeResponseFromStack","registerLoadingEventListeners","cancelAction","CANCEL","setRequestCancelled","toggleAIDrawer","getResponseStack","innerHTML","contains","isRequestCancelled","dataset","cancelled","MessageDrawerHelper","hide","add","setAttribute","addPadding","FocusLock","trapFocus","untrapFocus","removeAttribute","removepadding","removePadding","action","params","method","heading","Policy","getPolicyStatus","hasGeneratedContent","has","render","html","displayLoading","addResponseToStack","existingReponse","scrollTop","offsetTop","request","methodname","getParamsForAction","args","contextid","prompttext","getTextContent","responseObj","Ajax","call","error","displayError","generatedContent","AIHelper","formatResponse","generatedcontent","displayResponse","window","console","log","set","delete","stack","values","reverse","response","content","mainRegion","MAIN_REGION","innerText","textContent"],"mappings":"qqEAqCuB,MAkBnBA,YAAYC,OAAQC,+FACXD,OAASA,YACTC,UAAYA,eAEZC,gBAAkBC,SAASC,cAAcC,mBAAUC,SAASC,eAC5DC,oBAAsBL,SAASC,cAAcC,mBAAUC,SAASG,oBAChEC,YAAcP,SAASC,cAAcC,mBAAUC,SAASK,WACxDC,cAAgBT,SAASC,cAAcC,mBAAUC,SAASO,aAC1DC,cAAgBX,SAASC,cAAcC,mBAAUC,SAASS,aAC1DC,qBAAuBC,KAAKf,gBAAgBE,cAAcC,mBAAUC,SAASY,qBAC7EC,WAAa,QACbC,UAAY,IAAIC,SAChBC,qBAAsB,OAEtBC,yBAMTA,yBACIpB,SAASqB,iBAAiB,SAASC,MAAAA,OAEPC,EAAEC,OAAOC,QAAQvB,mBAAUwB,QAAQC,SACtC,CACjBJ,EAAEK,sBACGC,oBACAb,WAAa,iBACbL,cAAcmB,kBACYhB,KAAKiB,oCAG3BC,qBAGJC,cAAcnB,KAAKE,eAGNO,EAAEC,OAAOC,QAAQvB,mBAAUwB,QAAQQ,SACtC,CACfX,EAAEK,sBACGC,oBACAb,WAAa,eACbL,cAAcmB,kBACYhB,KAAKiB,oCAG3BC,qBAGJC,cAAcnB,KAAKE,YAGNO,EAAEC,OAAOC,QAAQvB,mBAAUC,SAASY,kBAEtDQ,EAAEK,sBACGO,oBAIbnC,SAASqB,iBAAiB,WAAWE,IAC7BT,KAAKsB,kBAA8B,WAAVb,EAAEc,UACtBF,yCAKHG,uBAAaC,cAAc,KAC7BzB,KAAKsB,uBACAD,wBAKR1B,cAAcY,iBAAiB,SAAS,UACpCR,qBAAqBiB,gBAIzB/B,gBAAgBsB,iBAAiB,SAAS,UACtCV,cAAcmB,gBAIlBnB,cAAcU,iBAAiB,QAAQ,UACnCV,cAAc6B,UAAUC,OAAO,aAO5CC,qCACUC,aAAe3C,SAASC,cAAcC,mBAAUwB,QAAQkB,QACxDC,cAAgB7C,SAASC,cAAcC,mBAAUwB,QAAQoB,SAC3DH,cAAgB7B,KAAKE,WAAW+B,QAChCJ,aAAatB,iBAAiB,SAAUE,IACpCA,EAAEK,sBACGoB,eAAeC,MAAK,IACdnC,KAAKmB,cAAcnB,KAAKE,cAChCkC,MAAMC,sBAAaC,cAG1BP,eACAA,cAAcxB,iBAAiB,SAAUE,IACrCA,EAAEK,sBACGO,mBAQjBkB,oCACUC,YAActD,SAASC,cAAcC,mBAAUwB,QAAQ6B,OACzDD,aAAexC,KAAKE,WAAW+B,QAC/BO,YAAYjC,iBAAiB,SAAUE,IACnCA,EAAEK,sBACGK,cAAcnB,KAAKE,eAQpCwC,iCAE8BxD,SAASyD,iBAAiBvD,mBAAUwB,QAAQgC,YAEpDC,SAAQC,yBAChBC,gBAAkBD,iBAAiBnC,QAAQvB,mBAAUC,SAAS2D,aAChEF,kBAAoBC,gBAAiB,OAE/BE,gBAAkBF,gBAAgBG,aAAa,yBACrDJ,iBAAiBvC,iBAAiB,SAAUE,IACxCA,EAAEK,sBAEGqC,wBAAwBF,sBACxB9B,cAAc8B,wBAMnCG,sCACUC,aAAenE,SAASC,cAAcC,mBAAUwB,QAAQ0C,QAC1DD,cACAA,aAAa9C,iBAAiB,SAAUE,IACpCA,EAAEK,sBACGyC,2BACAC,sBACAL,wBAAwB,iBAEvBhD,UAAYH,KAAKyD,wBAClBlE,oBAAoBmE,UAAYvD,aASjDmB,wBACWtB,KAAKf,gBAAgByC,UAAUiC,SAAS,QAOnDC,2BAC0D,MAA/C5D,KAAKT,oBAAoBsE,QAAQC,UAG5CP,2BACShE,oBAAoBsE,QAAQC,UAAY,IAMjD/C,eAEIgD,oBAAoBC,YACf/E,gBAAgByC,UAAUuC,IAAI,aAC9BhF,gBAAgBiF,aAAa,WAAY,QACzC3E,oBAAoB2E,aAAa,YAAa,UAC9ClE,KAAKP,YAAYiC,UAAUiC,SAAS,2BAChCQ,kBAEJxE,cAAcuE,aAAa,WAAY,QACvCvE,cAAcqB,SAGf,4BACAoD,UAAUC,UAAUrE,KAAKf,sBACpBA,gBAAgBiF,aAAa,aAAc,aAC3CjF,gBAAgBiF,aAAa,OAAQ,eACrC7D,qBAAsB,GAOnCgB,gBAEQrB,KAAKK,sBACL+D,UAAUE,mBACLrF,gBAAgBsF,gBAAgB,mBAChCtF,gBAAgBiF,aAAa,OAAQ,gBAGzCjF,gBAAgByC,UAAUC,OAAO,aACjC1C,gBAAgBiF,aAAa,YAAa,QAC1C3E,oBAAoBgF,gBAAgB,aACrCvE,KAAKP,YAAYiC,UAAUiC,SAAS,sBAA2E,MAAnD3D,KAAKT,oBAAoBsE,QAAQW,oBACxFC,qBAEJ9E,cAAcuE,aAAa,YAAa,QAQxCrE,cAAc6B,UAAUuC,IAAI,eAC5BpE,cAAcmB,QAMvBwC,iBACQxD,KAAKsB,sBACAD,qBAEAN,eAOboD,kBACS1E,YAAYiC,UAAUuC,IAAI,0BAC1B1E,oBAAoBsE,QAAQW,cAAgB,IAMrDC,qBACShF,YAAYiC,UAAUC,OAAO,0BAC7BpC,oBAAoBsE,QAAQW,cAAgB,6BAQ5BE,YACjBC,OAAS,UAELD,YACC,YACDC,OAAOC,OAAS,0CAChBD,OAAOE,cAAgB,kBAAU,YAAa,sCAG7C,UACDF,OAAOC,OAAS,wCAChBD,OAAOE,cAAgB,kBAAU,YAAa,mCAI/CF,6CAQMG,gBAAOC,gBAAgB/E,KAAKjB,QAO7CmD,sBACW4C,gBAAO5C,eAQlB8C,oBAAoBN,eACT1E,KAAKG,UAAU8E,IAAIP,QAM9BxD,mCACcgE,OAAO,sBAAuB,IAAI/C,MAAMgD,YACzC5F,oBAAoBmE,UAAYyB,UAChCvD,kCAENQ,MAAMC,sBAAaC,WAM1B8C,oCACcF,OAAO,mCAAoC,IAAI/C,MAAMgD,YACtDE,mBAAmB,UAAWF,YAC7BhF,UAAYH,KAAKyD,wBAClBlE,oBAAoBmE,UAAYvD,eAChCiD,mCAENjB,MAAK,UACCgB,wBAAwB,cAE9Bf,MAAMC,sBAAaC,+BAONoC,WACZ1E,KAAKgF,oBAAoBN,QAAS,OAE5BY,gBAAkBpG,SAASC,cAAc,2BAA6BuF,OAAS,MACjFY,uBACK/F,oBAAoBgG,UAAYD,gBAAgBE,eAEtD,MAEEJ,sBAEA7F,oBAAoBmE,UAAY,SAE/B+B,QAAU,CACZC,kBAFiB1F,KAAK2F,mBAAmBjB,SAEtBE,OACnBgB,KAAM,CACFC,UAAW7F,KAAKhB,UAChB8G,WAAY9F,KAAK+F,6BAIfC,kBAAoBC,cAAKC,KAAK,CAACT,UAAU,MAC3CO,YAAYG,uBACPC,mBAGApG,KAAK4D,qBAAsB,OAEtByC,iBAAmBC,gBAASC,eAAeP,YAAYQ,mCACxDC,gBAAgBJ,iBAAkB3B,aAGlCnF,oBAAoBsE,QAAQC,UAAY,IAGvD,MAAOqC,OACLO,OAAOC,QAAQC,IAAIT,YACdC,iBAWjBf,mBAAmBX,OAAQS,WAClBhF,UAAU0G,IAAInC,OAAQS,MAO/BhC,wBAAwBuB,QAChB1E,KAAKG,UAAU8E,IAAIP,cACdvE,UAAU2G,OAAOpC,QAQ9BjB,uBACQsD,MAAQ,SAEN5G,UAAY,IAAIH,KAAKG,UAAU6G,UAAUC,cAC1C,MAAMC,YAAY/G,UACnB4G,OAASG,gBAENH,4BAQWI,QAASzC,cAErBkB,KAAO,CACTuB,QAASA,QACTtC,eAHiB7E,KAAK2F,mBAAmBjB,SAGzBG,QAChBH,OAAQA,2BAEFQ,OAAO,oCAAqCU,MAAMzD,MAAMgD,YACzDE,mBAAmBX,OAAQS,YAC1BhF,UAAYH,KAAKyD,wBAClBlE,oBAAoBmE,UAAYvD,eAChCuC,oCAENN,MAAMC,sBAAaC,WAM1B8D,kCACclB,OAAO,iCAAkC,IAAI/C,MAAMgD,YACpDE,mBAAmB,QAASF,YAC3BhF,UAAYH,KAAKyD,wBAClBlE,oBAAoBmE,UAAYvD,eAChCoC,iCAENJ,MAAK,UACCgB,wBAAwB,YAE9Bf,MAAMC,sBAAaC,WAO1ByD,uBACUqB,WAAalI,SAASC,cAAcC,mBAAUC,SAASgI,oBACtDD,WAAWE,WAAaF,WAAWG"} \ No newline at end of file diff --git a/ai/placement/courseassist/amd/src/placement.js b/ai/placement/courseassist/amd/src/placement.js index 812d73c9120..425711e5b91 100644 --- a/ai/placement/courseassist/amd/src/placement.js +++ b/ai/placement/courseassist/amd/src/placement.js @@ -32,6 +32,8 @@ import DrawerEvents from 'core/drawer_events'; import {subscribe} from 'core/pubsub'; import * as MessageDrawerHelper from 'core_message/message_drawer_helper'; import {getString} from 'core/str'; +import * as FocusLock from 'core/local/aria/focuslock'; +import {isSmall} from "core/pagehelpers"; const AICourseAssist = class { @@ -63,6 +65,7 @@ const AICourseAssist = class { this.aiDrawerCloseElement = this.aiDrawerElement.querySelector(Selectors.ELEMENTS.AIDRAWER_CLOSE); this.lastAction = ''; this.responses = new Map(); + this.isDrawerFocusLocked = false; this.registerEventListeners(); } @@ -110,6 +113,12 @@ const AICourseAssist = class { } }); + document.addEventListener('keydown', e => { + if (this.isAIDrawerOpen() && e.key === 'Escape') { + this.closeAIDrawer(); + } + }); + // Close AI drawer if message drawer is shown. subscribe(DrawerEvents.DRAWER_SHOWN, () => { if (this.isAIDrawerOpen()) { @@ -239,12 +248,27 @@ const AICourseAssist = class { } this.jumpToElement.setAttribute('tabindex', 0); this.jumpToElement.focus(); + + // If the AI drawer is opened on a small screen, we need to trap the focus tab within the AI drawer. + if (isSmall()) { + FocusLock.trapFocus(this.aiDrawerElement); + this.aiDrawerElement.setAttribute('aria-modal', 'true'); + this.aiDrawerElement.setAttribute('role', 'dialog'); + this.isDrawerFocusLocked = true; + } } /** * Close the AI drawer. */ closeAIDrawer() { + // Untrap focus if it was locked. + if (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'); diff --git a/ai/placement/courseassist/lang/en/aiplacement_courseassist.php b/ai/placement/courseassist/lang/en/aiplacement_courseassist.php index 5d56733de8f..a5435658a44 100644 --- a/ai/placement/courseassist/lang/en/aiplacement_courseassist.php +++ b/ai/placement/courseassist/lang/en/aiplacement_courseassist.php @@ -22,6 +22,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$string['aidrawerlabel'] = 'AI-generated content drawer'; $string['aiexplain'] = 'AI explain'; $string['aifeatures'] = 'AI features'; $string['aisummary'] = 'AI summary'; diff --git a/ai/placement/courseassist/templates/drawer.mustache b/ai/placement/courseassist/templates/drawer.mustache index cc7c0756f27..69c0daf4ef3 100644 --- a/ai/placement/courseassist/templates/drawer.mustache +++ b/ai/placement/courseassist/templates/drawer.mustache @@ -29,7 +29,7 @@ "contextid": "1" } }} -
+