")}static replaceMarkdown(text){return text.replace(/\*\*(.*?)\*\*/g,"$1")}static formatResponse(text){let formattedText=this.replaceLineBreaks(text);return formattedText=this.replaceMarkdown(formattedText),formattedText}},_exports.default}));
//# sourceMappingURL=helper.min.js.map
\ No newline at end of file
diff --git a/ai/amd/build/helper.min.js.map b/ai/amd/build/helper.min.js.map
index 5c7f7680b51..d5beb920885 100644
--- a/ai/amd/build/helper.min.js.map
+++ b/ai/amd/build/helper.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"helper.min.js","sources":["../src/helper.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 * The helper module or AI Subsystem.\n *\n * @module core_ai/helper\n * @copyright 2024 Huong Nguyen \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class AIHelper {\n /**\n * Replace double line breaks with and with
for paragraphs.\n * This is to handle the difference in response from the AI to what is expected by the editor.\n *\n * @param {String} text The text to replace.\n * @returns {String}\n */\n static replaceLineBreaks(text) {\n // Replace double line breaks with
for paragraphs\n const textWithParagraphs = text.replace(/\\n{2,}|\\r\\n/g, '
');\n\n // Replace remaining single line breaks with tags\n const textWithBreaks = textWithParagraphs.replace(/\\n/g, ' ');\n\n // Add opening and closing
tags to wrap the entire content\n return `
${textWithBreaks}
`;\n }\n}\n"],"names":["text","textWithBreaks","replace"],"mappings":";;;;;;;;+BA8B6BA,YAKfC,eAHqBD,KAAKE,QAAQ,eAAgB,cAGdA,QAAQ,MAAO,4BAG5CD"}
\ No newline at end of file
+{"version":3,"file":"helper.min.js","sources":["../src/helper.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 * The helper module or AI Subsystem.\n *\n * @module core_ai/helper\n * @copyright 2024 Huong Nguyen \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class AIHelper {\n /**\n * Replace double line breaks with and with
for paragraphs.\n * This is to handle the difference in response from the AI to what is expected by the editor.\n *\n * @param {String} text The text to replace.\n * @returns {String}\n */\n static replaceLineBreaks(text) {\n // Replace double line breaks with
for paragraphs\n const textWithParagraphs = text.replace(/\\n{2,}|\\r\\n/g, '
');\n\n // Replace remaining single line breaks with tags\n const textWithBreaks = textWithParagraphs.replace(/\\n/g, ' ');\n\n // Add opening and closing
tags to wrap the entire content\n return `
${textWithBreaks}
`;\n }\n\n /**\n * Replace markdown formatting.\n * Even when asked not to, AI models will sometimes return markdown.\n *\n * @param {String} text The text to replace.\n * @returns {String}\n */\n static replaceMarkdown(text) {\n // Replace markdown bold formatting HTML equivalent.\n const textWithMarkdown = text.replace(/\\*\\*(.*?)\\*\\*/g, '$1');\n\n return textWithMarkdown;\n }\n\n /**\n * Format the response provided by the AI model.\n *\n * @param {String} text The text to format.\n * @returns {String}\n */\n static formatResponse(text) {\n let formattedText = this.replaceLineBreaks(text) ;\n formattedText = this.replaceMarkdown(formattedText);\n\n return formattedText;\n }\n}\n"],"names":["text","textWithBreaks","replace","formattedText","this","replaceLineBreaks","replaceMarkdown"],"mappings":";;;;;;;;+BA8B6BA,YAKfC,eAHqBD,KAAKE,QAAQ,eAAgB,cAGdA,QAAQ,MAAO,4BAG5CD,8CAUMD,aAEMA,KAAKE,QAAQ,iBAAkB,6CAWtCF,UACdG,cAAgBC,KAAKC,kBAAkBL,aAC3CG,cAAgBC,KAAKE,gBAAgBH,eAE9BA"}
\ No newline at end of file
diff --git a/ai/amd/src/helper.js b/ai/amd/src/helper.js
index ac27d473e48..64389a62e75 100644
--- a/ai/amd/src/helper.js
+++ b/ai/amd/src/helper.js
@@ -38,4 +38,31 @@ export default class AIHelper {
// Add opening and closing
tags to wrap the entire content
return `
${textWithBreaks}
`;
}
+
+ /**
+ * Replace markdown formatting.
+ * Even when asked not to, AI models will sometimes return markdown.
+ *
+ * @param {String} text The text to replace.
+ * @returns {String}
+ */
+ static replaceMarkdown(text) {
+ // Replace markdown bold formatting HTML equivalent.
+ const textWithMarkdown = text.replace(/\*\*(.*?)\*\*/g, '$1');
+
+ return textWithMarkdown;
+ }
+
+ /**
+ * Format the response provided by the AI model.
+ *
+ * @param {String} text The text to format.
+ * @returns {String}
+ */
+ static formatResponse(text) {
+ let formattedText = this.replaceLineBreaks(text) ;
+ formattedText = this.replaceMarkdown(formattedText);
+
+ return formattedText;
+ }
}
diff --git a/ai/classes/aiactions/explain_text.php b/ai/classes/aiactions/explain_text.php
new file mode 100644
index 00000000000..7e6deea48ac
--- /dev/null
+++ b/ai/classes/aiactions/explain_text.php
@@ -0,0 +1,66 @@
+.
+
+namespace core_ai\aiactions;
+
+use core_ai\aiactions\responses\response_base;
+
+/**
+ * Explain text class.
+ *
+ * @package core_ai
+ * @copyright 2024 David Woloszyn
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class explain_text extends base {
+ /**
+ * Create a new instance of the explain_text action.
+ *
+ * It’s responsible for performing any setup tasks,
+ * such as getting additional data from the database etc.
+ *
+ * @param int $contextid The context id the action was created in.
+ * @param int $userid The user id making the request.
+ * @param string $prompttext The prompt text used to generate the image.
+ */
+ public function __construct(
+ int $contextid,
+ /** @var int The user id requesting the action. */
+ protected int $userid,
+ /** @var string The prompt text used to generate the summary text */
+ protected string $prompttext,
+ ) {
+ parent::__construct($contextid);
+ }
+
+ #[\Override]
+ public function store(response_base $response): int {
+ global $DB;
+
+ $responsearr = $response->get_response_data();
+
+ $record = new \stdClass();
+ $record->prompt = $this->prompttext;
+ $record->responseid = $responsearr['id']; // Can be null.
+ $record->fingerprint = $responsearr['fingerprint']; // Can be null.
+ $record->generatedcontent = $responsearr['generatedcontent']; // Can be null.
+ $record->finishreason = $responsearr['finishreason']; // Can be null.
+ $record->prompttokens = $responsearr['prompttokens']; // Can be null.
+ $record->completiontoken = $responsearr['completiontokens']; // Can be null.
+
+ return $DB->insert_record($this->get_tablename(), $record);
+ }
+}
diff --git a/ai/classes/aiactions/responses/response_explain_text.php b/ai/classes/aiactions/responses/response_explain_text.php
new file mode 100644
index 00000000000..8fca94c4a73
--- /dev/null
+++ b/ai/classes/aiactions/responses/response_explain_text.php
@@ -0,0 +1,90 @@
+.
+
+namespace core_ai\aiactions\responses;
+
+/**
+ * Explain text action response class.
+ *
+ * Any method that processes an action must return an instance of this class.
+ *
+ * @package core_ai
+ * @copyright 2024 David Woloszyn
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class response_explain_text extends response_base {
+ /** @var string|null A unique identifier for the chat completion, returned by the AI. */
+ private ?string $id = null;
+
+ /** @var string|null This fingerprint represents the backend configuration that the model runs with. */
+ private ?string $fingerprint = null;
+
+ /** @var string|null The contents of the generated message. */
+ private ?string $generatedcontent = null;
+
+ /** @var string|null The reason the model stopped generating tokens. */
+ private ?string $finishreason = null;
+
+ /** @var string|null Number of tokens in the prompt. */
+ private ?string $prompttokens = null;
+
+ /** @var string|null Number of tokens in the generated completion. */
+ private ?string $completiontokens = null;
+
+ /**
+ * Constructor.
+ *
+ * @param bool $success The success status of the action.
+ * @param int $errorcode Error code. Must exist if success is false.
+ * @param string $errormessage Error message. Must exist if success is false
+ */
+ public function __construct(
+ bool $success,
+ int $errorcode = 0,
+ string $errormessage = '',
+ ) {
+ parent::__construct(
+ success: $success,
+ actionname: 'explain_text',
+ errorcode: $errorcode,
+ errormessage: $errormessage,
+ );
+ }
+
+ #[\Override]
+ public function set_response_data(array $response): void {
+ $this->id = $response['id'] ?? null;
+ $this->fingerprint = $response['fingerprint'] ?? null;
+ $this->generatedcontent = $response['generatedcontent'] ?? null;
+ $this->finishreason = $response['finishreason'] ?? null;
+ $this->prompttokens = $response['prompttokens'] ?? null;
+ $this->completiontokens = $response['completiontokens'] ?? null;
+ $this->model = $response['model'] ?? null;
+ }
+
+ #[\Override]
+ public function get_response_data(): array {
+ return [
+ 'id' => $this->id,
+ 'fingerprint' => $this->fingerprint,
+ 'generatedcontent' => $this->generatedcontent,
+ 'finishreason' => $this->finishreason,
+ 'prompttokens' => $this->prompttokens,
+ 'completiontokens' => $this->completiontokens,
+ 'model' => $this->model,
+ ];
+ }
+}
diff --git a/ai/classes/privacy/provider.php b/ai/classes/privacy/provider.php
index dc60ee17f4e..921b70332e1 100644
--- a/ai/classes/privacy/provider.php
+++ b/ai/classes/privacy/provider.php
@@ -83,6 +83,14 @@ class provider implements
'prompttokens' => 'privacy:metadata:ai_action_summarise_text:prompttokens',
'completiontoken' => 'privacy:metadata:ai_action_summarise_text:completiontoken',
], 'privacy:metadata:ai_action_summarise_text');
+ $collection->add_database_table('ai_action_explain_text', [
+ 'prompt' => 'privacy:metadata:ai_action_explain_text:prompt',
+ 'responseid' => 'privacy:metadata:ai_action_explain_text:responseid',
+ 'fingerprint' => 'privacy:metadata:ai_action_explain_text:fingerprint',
+ 'generatedcontent' => 'privacy:metadata:ai_action_explain_text:generatedcontent',
+ 'prompttokens' => 'privacy:metadata:ai_action_explain_text:prompttokens',
+ 'completiontoken' => 'privacy:metadata:ai_action_explain_text:completiontoken',
+ ], 'privacy:metadata:ai_action_explain_text');
return $collection;
}
@@ -137,6 +145,17 @@ class provider implements
AND aar.userid = :userid";
$contextlist->add_from_sql($sql, ['userid' => $userid]);
+ // AI action explain text.
+ $sql = "SELECT DISTINCT ctx.id
+ FROM {context} ctx
+ JOIN {ai_action_register} aar
+ ON aar.contextid = ctx.id
+ JOIN {ai_action_explain_text} aaet
+ ON aaet.id = aar.actionid
+ WHERE aar.actionname = 'explain_text'
+ AND aar.userid = :userid";
+ $contextlist->add_from_sql($sql, ['userid' => $userid]);
+
return $contextlist;
}
@@ -308,6 +327,50 @@ class provider implements
writer::with_context($context)->export_related_data($subcontexts, $name, $details);
}
$textsummarisedetails->close();
+
+ // AI action explain text.
+ $sql = "SELECT aar.actionname, aar.success, aar.provider, aar.timecreated, aar.timecompleted, aar.contextid,
+ aaet.prompt, aaet.responseid, aaet.fingerprint, aaet.generatedcontent,
+ aaet.prompttokens, aaet.completiontoken, aar.model
+ FROM {ai_action_register} aar
+ JOIN {ai_action_explain_text} aaet
+ ON aar.actionid = aaet.id
+ JOIN {context} ctx
+ ON aar.contextid = ctx.id
+ WHERE aar.actionname = 'explain_text'
+ AND aar.userid = :userid
+ AND ctx.id " . $contextsql;
+ $params = [
+ 'userid' => $userid,
+ ];
+ $params += $contextparams;
+ $textexplaindetails = $DB->get_recordset_sql($sql, $params);
+ foreach ($textexplaindetails as $textexplaindetail) {
+ $subcontexts = [
+ get_string('ai', 'core_ai'),
+ get_string('action_explain_text', 'core_ai'),
+ date('c', $textexplaindetail->timecreated),
+ ];
+ $details = (object) [
+ 'actionname' => $textexplaindetail->actionname,
+ 'contextid' => $textexplaindetail->contextid,
+ 'prompt' => $textexplaindetail->prompt,
+ 'responseid' => $textexplaindetail->responseid,
+ 'fingerprint' => $textexplaindetail->fingerprint,
+ 'generatedcontent' => $textexplaindetail->generatedcontent,
+ 'prompttokens' => $textexplaindetail->prompttokens,
+ 'completiontoken' => $textexplaindetail->completiontoken,
+ 'model' => $textexplaindetail->model,
+ 'success' => transform::yesno($textexplaindetail->success),
+ 'provider' => $textexplaindetail->provider,
+ 'timecreated' => transform::datetime($textexplaindetail->timecreated),
+ 'timecompleted' => transform::datetime($textexplaindetail->timecompleted),
+ ];
+ $name = 'action_explain_text';
+ $context = \context::instance_by_id($textexplaindetail->contextid);
+ writer::with_context($context)->export_related_data($subcontexts, $name, $details);
+ }
+ $textexplaindetails->close();
}
/**
@@ -402,6 +465,30 @@ class provider implements
WHERE id " . $aastidsql;
$DB->execute($sql, $aastidparams);
}
+
+ // AI action explain text.
+ $sql = "SELECT DISTINCT aaet.id
+ FROM {ai_action_register} aar
+ JOIN {ai_action_explain_text} aaet
+ ON aar.actionid = aaet.id
+ JOIN {context} ctx
+ ON aar.contextid = ctx.id
+ WHERE aar.actionname = 'explain_text'
+ AND ctx.id = :contextid";
+ $params = [
+ 'contextid' => $context->id,
+ ];
+ $aaetids = $DB->get_records_sql_menu($sql, $params);
+ if ($aaetids) {
+ [$aaetidsql, $aaetidparams] = $DB->get_in_or_equal(array_keys($aaetids), SQL_PARAMS_NAMED);
+ $sql = "UPDATE {ai_action_explain_text}
+ SET prompt = '',
+ responseid = '',
+ fingerprint = '',
+ generatedcontent = ''
+ WHERE id " . $aaetidsql;
+ $DB->execute($sql, $aaetidparams);
+ }
}
/**
@@ -513,6 +600,33 @@ class provider implements
WHERE id " . $aastidsql;
$DB->execute($sql, $aastidparams);
}
+
+ // AI action explain text.
+ $sql = "SELECT DISTINCT aaet.id AS textexplainid
+ FROM {ai_action_register} aar
+ JOIN {ai_action_explain_text} aaet
+ ON aar.actionid = aaet.id
+ JOIN {context} ctx
+ ON aar.contextid = ctx.id
+ WHERE aar.actionname = 'explain_text'
+ AND aar.userid = :userid
+ AND ctx.id " . $contextsql;
+ $textexplaindetails = $DB->get_recordset_sql($sql, $params);
+ $aaetids = [];
+ foreach ($textexplaindetails as $textexplaindetail) {
+ $aaetids[] = $textexplaindetail->textexplainid;
+ }
+ $textexplaindetails->close();
+ if ($aaetids) {
+ [$aaetidsql, $aaetidparams] = $DB->get_in_or_equal($aaetids, SQL_PARAMS_NAMED);
+ $sql = "UPDATE {ai_action_explain_text}
+ SET prompt = '',
+ responseid = '',
+ fingerprint = '',
+ generatedcontent = ''
+ WHERE id " . $aaetidsql;
+ $DB->execute($sql, $aaetidparams);
+ }
}
/**
@@ -563,6 +677,17 @@ class provider implements
WHERE aar.actionname = 'summarise_text'
AND aar.contextid = :contextid";
$userlist->add_from_sql('userid', $sql, ['contextid' => $context->id]);
+
+ // AI action explain text.
+ $sql = "SELECT DISTINCT aar.userid
+ FROM {context} ctx
+ JOIN {ai_action_register} aar
+ ON aar.contextid = ctx.id
+ JOIN {ai_action_explain_text} aaet
+ ON aaet.id = aar.actionid
+ WHERE aar.actionname = 'explain_text'
+ AND aar.contextid = :contextid";
+ $userlist->add_from_sql('userid', $sql, ['contextid' => $context->id]);
}
/**
@@ -658,5 +783,27 @@ class provider implements
WHERE id " . $aastidsql;
$DB->execute($sql, $aastidparams);
}
+
+ // AI action explain text.
+ $sql = "SELECT DISTINCT aaet.id
+ FROM {ai_action_register} aar
+ JOIN {ai_action_explain_text} aaet
+ ON aar.actionid = aaet.id
+ JOIN {context} ctx
+ ON aar.contextid = ctx.id
+ WHERE aar.actionname = 'explain_text'
+ AND ctx.id = :contextid
+ AND aar.userid " . $useridssql;
+ $aaetids = $DB->get_records_sql_menu($sql, $params);
+ if ($aaetids) {
+ [$aaetidsql, $aaetidparams] = $DB->get_in_or_equal(array_keys($aaetids), SQL_PARAMS_NAMED);
+ $sql = "UPDATE {ai_action_explain_text}
+ SET prompt = '',
+ responseid = '',
+ fingerprint = '',
+ generatedcontent = ''
+ WHERE id " . $aaetidsql;
+ $DB->execute($sql, $aaetidparams);
+ }
}
}
diff --git a/ai/placement/courseassist/amd/build/placement.min.js b/ai/placement/courseassist/amd/build/placement.min.js
index 8f9b6056f53..4161580ed17 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"],(function(_exports,_templates,_ajax,_copy_to_clipboard,_notification,_selectors,_policy,_helper,_drawer_events,_pubsub,MessageDrawerHelper){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.registerEventListeners()}registerEventListeners(){document.addEventListener("click",(async e=>{if(e.target.closest(_selectors.default.ACTIONS.SUMMARY)){e.preventDefault(),this.toggleAIDrawer();if(!await this.isPolicyAccepted())return void this.displayPolicy();this.displaySummary()}})),(0,_pubsub.subscribe)(_drawer_events.default.DRAWER_SHOWN,(()=>{this.isAIDrawerOpen()&&this.closeAIDrawer()}))}registerPolicyEventListeners(){const acceptAction=document.querySelector(_selectors.default.ACTIONS.ACCEPT),declineAction=document.querySelector(_selectors.default.ACTIONS.DECLINE);acceptAction&&acceptAction.addEventListener("click",(e=>{e.preventDefault(),this.acceptPolicy().then((()=>this.displaySummary())).catch(_notification.default.exception)})),declineAction&&declineAction.addEventListener("click",(e=>{e.preventDefault(),this.closeAIDrawer()}))}registerErrorEventListeners(){const retryAction=document.querySelector(_selectors.default.ACTIONS.RETRY);retryAction&&retryAction.addEventListener("click",(e=>{e.preventDefault(),this.aiDrawerBodyElement.dataset.hasdata="0",this.displaySummary()}))}registerResponseEventListeners(){const regenerateAction=document.querySelector(_selectors.default.ACTIONS.REGENERATE);regenerateAction&®enerateAction.addEventListener("click",(e=>{e.preventDefault(),this.aiDrawerBodyElement.dataset.hasdata="0",this.displaySummary()}))}registerLoadingEventListeners(){const cancelAction=document.querySelector(_selectors.default.ACTIONS.CANCEL);cancelAction&&cancelAction.addEventListener("click",(e=>{e.preventDefault(),this.setRequestCancelled(),this.toggleAIDrawer()}))}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.aiDrawerBodyElement.setAttribute("aria-live","polite"),this.pageElement.classList.contains("show-drawer-right")||this.addPadding(),this.disableSummaryButton()}closeAIDrawer(){this.aiDrawerElement.classList.remove("show"),this.aiDrawerBodyElement.removeAttribute("aria-live"),this.pageElement.classList.contains("show-drawer-right")&&"1"===this.aiDrawerBodyElement.dataset.removepadding&&this.removePadding(),this.enableSummaryButton()}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"}disableSummaryButton(){const summaryButton=document.querySelector(_selectors.default.ACTIONS.SUMMARY);summaryButton&&summaryButton.setAttribute("disabled",1)}enableSummaryButton(){const summaryButton=document.querySelector(_selectors.default.ACTIONS.SUMMARY);summaryButton&&(summaryButton.removeAttribute("disabled"),summaryButton.focus())}async isPolicyAccepted(){return await _policy.default.getPolicyStatus(this.userId)}acceptPolicy(){return _policy.default.acceptPolicy()}hasGeneratedContent(){return"1"===this.aiDrawerBodyElement.dataset.hasdata}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.aiDrawerBodyElement.innerHTML=html,this.registerLoadingEventListeners()})).catch(_notification.default.exception)}async displaySummary(){if(!this.hasGeneratedContent()){this.displayLoading(),this.aiDrawerBodyElement.innerHTML="";const request={methodname:"aiplacement_courseassist_summarise_text",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.replaceLineBreaks(responseObj.generatedcontent);return void this.displayResponse(generatedContent)}this.aiDrawerBodyElement.dataset.cancelled="0"}catch(error){window.console.log(error),this.displayError()}}}displayResponse(content){_templates.default.render("aiplacement_courseassist/response",{content:content}).then((html=>{this.aiDrawerBodyElement.innerHTML=html,this.aiDrawerBodyElement.dataset.hasdata="1",this.registerResponseEventListeners()})).catch(_notification.default.exception)}displayError(){_templates.default.render("aiplacement_courseassist/error",{}).then((html=>{this.aiDrawerBodyElement.innerHTML=html,this.registerErrorEventListeners()})).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"],(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.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";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";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()}))}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.aiDrawerBodyElement.setAttribute("aria-live","polite"),this.pageElement.classList.contains("show-drawer-right")||this.addPadding()}closeAIDrawer(){this.aiDrawerElement.classList.remove("show"),this.aiDrawerBodyElement.removeAttribute("aria-live"),this.pageElement.classList.contains("show-drawer-right")&&"1"===this.aiDrawerBodyElement.dataset.removepadding&&this.removePadding()}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 b9ee162e85c..cb91c023e1b 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';\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\n this.registerEventListeners();\n }\n\n /**\n * Register event listeners.\n */\n registerEventListeners() {\n document.addEventListener('click', async(e) => {\n const summariseAction = e.target.closest(Selectors.ACTIONS.SUMMARY);\n if (summariseAction) {\n e.preventDefault();\n this.toggleAIDrawer();\n const isPolicyAccepted = await this.isPolicyAccepted();\n if (!isPolicyAccepted) {\n // Display policy.\n this.displayPolicy();\n return;\n }\n // Display summary.\n this.displaySummary();\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\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) {\n acceptAction.addEventListener('click', (e) => {\n e.preventDefault();\n this.acceptPolicy().then(() => {\n return this.displaySummary();\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) {\n retryAction.addEventListener('click', (e) => {\n e.preventDefault();\n this.aiDrawerBodyElement.dataset.hasdata = '0';\n this.displaySummary();\n });\n }\n }\n\n /**\n * Register event listeners for the response.\n */\n registerResponseEventListeners() {\n const regenerateAction = document.querySelector(Selectors.ACTIONS.REGENERATE);\n if (regenerateAction) {\n regenerateAction.addEventListener('click', (e) => {\n e.preventDefault();\n this.aiDrawerBodyElement.dataset.hasdata = '0';\n this.displaySummary();\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 });\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.aiDrawerBodyElement.setAttribute('aria-live', 'polite');\n if (!this.pageElement.classList.contains('show-drawer-right')) {\n this.addPadding();\n }\n // Disable the summary button.\n this.disableSummaryButton();\n }\n\n /**\n * Close the AI drawer.\n */\n closeAIDrawer() {\n this.aiDrawerElement.classList.remove('show');\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 // Enable the summary button.\n this.enableSummaryButton();\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 * Disable the summary button.\n */\n disableSummaryButton() {\n const summaryButton = document.querySelector(Selectors.ACTIONS.SUMMARY);\n if (summaryButton) {\n summaryButton.setAttribute('disabled', 1);\n }\n }\n\n /**\n * Enable the summary button and focus on it.\n */\n enableSummaryButton() {\n const summaryButton = document.querySelector(Selectors.ACTIONS.SUMMARY);\n if (summaryButton) {\n summaryButton.removeAttribute('disabled');\n summaryButton.focus();\n }\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