This commit is contained in:
Shamim Rezaie
2025-02-26 17:28:00 +11:00
53 changed files with 1373 additions and 290 deletions
@@ -0,0 +1,6 @@
issueNumber: MDL-82942
notes:
aiplacement_courseassist:
- message: The `aiplacement_courseassist` templates and CSS have been modified.
These changes allow for multiple actions to be nested in a dropdown menu.
type: improved
+1 -1
View File
@@ -6,6 +6,6 @@ define("core_ai/helper",["exports"],(function(_exports){Object.defineProperty(_e
* @copyright 2024 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class{static replaceLineBreaks(text){const textWithBreaks=text.replace(/\n{2,}|\r\n/g,"<br/><br/>").replace(/\n/g,"<br/>");return"<p>".concat(textWithBreaks,"</p>")}},_exports.default}));
class{static replaceLineBreaks(text){const textWithBreaks=text.replace(/\n{2,}|\r\n/g,"<br/><br/>").replace(/\n/g,"<br/>");return"<p>".concat(textWithBreaks,"</p>")}static replaceMarkdown(text){return text.replace(/\*\*(.*?)\*\*/g,"<strong>$1</strong>")}static formatResponse(text){let formattedText=this.replaceLineBreaks(text);return formattedText=this.replaceMarkdown(formattedText),formattedText}},_exports.default}));
//# sourceMappingURL=helper.min.js.map
+1 -1
View File
@@ -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 <http://www.gnu.org/licenses/>.\n\n/**\n * The helper module or AI Subsystem.\n *\n * @module core_ai/helper\n * @copyright 2024 Huong Nguyen <[email protected]>\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 <br> and with </p><p> 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 </p><p> for paragraphs\n const textWithParagraphs = text.replace(/\\n{2,}|\\r\\n/g, '<br/><br/>');\n\n // Replace remaining single line breaks with <br> tags\n const textWithBreaks = textWithParagraphs.replace(/\\n/g, '<br/>');\n\n // Add opening and closing <p> tags to wrap the entire content\n return `<p>${textWithBreaks}</p>`;\n }\n}\n"],"names":["text","textWithBreaks","replace"],"mappings":";;;;;;;;+BA8B6BA,YAKfC,eAHqBD,KAAKE,QAAQ,eAAgB,cAGdA,QAAQ,MAAO,4BAG5CD"}
{"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 <http://www.gnu.org/licenses/>.\n\n/**\n * The helper module or AI Subsystem.\n *\n * @module core_ai/helper\n * @copyright 2024 Huong Nguyen <[email protected]>\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 <br> and with </p><p> 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 </p><p> for paragraphs\n const textWithParagraphs = text.replace(/\\n{2,}|\\r\\n/g, '<br/><br/>');\n\n // Replace remaining single line breaks with <br> tags\n const textWithBreaks = textWithParagraphs.replace(/\\n/g, '<br/>');\n\n // Add opening and closing <p> tags to wrap the entire content\n return `<p>${textWithBreaks}</p>`;\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, '<strong>$1</strong>');\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"}
+27
View File
@@ -38,4 +38,31 @@ export default class AIHelper {
// Add opening and closing <p> tags to wrap the entire content
return `<p>${textWithBreaks}</p>`;
}
/**
* 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, '<strong>$1</strong>');
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;
}
}
+66
View File
@@ -0,0 +1,66 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_ai\aiactions;
use core_ai\aiactions\responses\response_base;
/**
* Explain text class.
*
* @package core_ai
* @copyright 2024 David Woloszyn <[email protected]>
* @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.
*
* Its 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);
}
}
@@ -0,0 +1,90 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
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 <[email protected]>
* @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,
];
}
}
+147
View File
@@ -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);
}
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,3 +1,3 @@
define("aiplacement_courseassist/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={ELEMENTS:{AIDRAWER:"#ai-drawer",AIDRAWER_BODY:"#ai-drawer .ai-drawer-body",PAGE:"#page",MAIN_REGION:'[role="main"]'},ACTIONS:{SUMMARY:'[data-action="course-summarise"]',RETRY:'[data-action="course-summarise-retry"]',DECLINE:'.ai-policy-block [data-action="decline"]',ACCEPT:'.ai-policy-block [data-action="accept"]',REGENERATE:'[data-action="course-summarise-regenerate"]',CANCEL:'[data-action="course-summarise-cancel"]'}},_exports.default}));
define("aiplacement_courseassist/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={ELEMENTS:{AIDRAWER:"#ai-drawer",AIDRAWER_BODY:"#ai-drawer .ai-drawer-body",PAGE:"#page",MAIN_REGION:'[role="main"]',AIDRAWER_CLOSE:"#ai-drawer-close",RESPONSE:".course-assist-response"},ACTIONS:{SUMMARY:'.course-assist-controls [data-action="summarise"]',EXPLAIN:'.course-assist-controls [data-action="explain"]',RETRY:'.course-assist-controls [data-action="retry"]',DECLINE:'.ai-policy-block [data-action="decline"]',ACCEPT:'.ai-policy-block [data-action="accept"]',REGENERATE:'.course-assist-controls [data-action="regenerate"]',CANCEL:'.course-assist-controls [data-action="cancel"]'}},_exports.default}));
//# sourceMappingURL=selectors.min.js.map
@@ -1 +1 @@
{"version":3,"file":"selectors.min.js","sources":["../src/selectors.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 <http://www.gnu.org/licenses/>.\n\n/**\n * Define all of the selectors we will be using on the AI Course assistant.\n *\n * @module aiplacement_courseassist/selectors\n * @copyright 2024 Huong Nguyen <[email protected]>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default {\n ELEMENTS: {\n AIDRAWER: '#ai-drawer',\n AIDRAWER_BODY: '#ai-drawer .ai-drawer-body',\n PAGE: '#page',\n MAIN_REGION: '[role=\"main\"]',\n },\n ACTIONS: {\n SUMMARY: '[data-action=\"course-summarise\"]',\n RETRY: '[data-action=\"course-summarise-retry\"]',\n DECLINE: '.ai-policy-block [data-action=\"decline\"]',\n ACCEPT: '.ai-policy-block [data-action=\"accept\"]',\n REGENERATE: '[data-action=\"course-summarise-regenerate\"]',\n CANCEL: '[data-action=\"course-summarise-cancel\"]',\n }\n};\n"],"names":["ELEMENTS","AIDRAWER","AIDRAWER_BODY","PAGE","MAIN_REGION","ACTIONS","SUMMARY","RETRY","DECLINE","ACCEPT","REGENERATE","CANCEL"],"mappings":"oLAsBe,CACXA,SAAU,CACNC,SAAU,aACVC,cAAe,6BACfC,KAAM,QACNC,YAAa,iBAEjBC,QAAS,CACLC,QAAS,mCACTC,MAAO,yCACPC,QAAS,2CACTC,OAAQ,0CACRC,WAAY,8CACZC,OAAQ"}
{"version":3,"file":"selectors.min.js","sources":["../src/selectors.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 <http://www.gnu.org/licenses/>.\n\n/**\n * Define all of the selectors we will be using on the AI Course assistant.\n *\n * @module aiplacement_courseassist/selectors\n * @copyright 2024 Huong Nguyen <[email protected]>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default {\n ELEMENTS: {\n AIDRAWER: '#ai-drawer',\n AIDRAWER_BODY: '#ai-drawer .ai-drawer-body',\n PAGE: '#page',\n MAIN_REGION: '[role=\"main\"]',\n AIDRAWER_CLOSE: '#ai-drawer-close',\n RESPONSE: '.course-assist-response',\n },\n ACTIONS: {\n SUMMARY: '.course-assist-controls [data-action=\"summarise\"]',\n EXPLAIN: '.course-assist-controls [data-action=\"explain\"]',\n RETRY: '.course-assist-controls [data-action=\"retry\"]',\n DECLINE: '.ai-policy-block [data-action=\"decline\"]',\n ACCEPT: '.ai-policy-block [data-action=\"accept\"]',\n REGENERATE: '.course-assist-controls [data-action=\"regenerate\"]',\n CANCEL: '.course-assist-controls [data-action=\"cancel\"]',\n }\n};\n"],"names":["ELEMENTS","AIDRAWER","AIDRAWER_BODY","PAGE","MAIN_REGION","AIDRAWER_CLOSE","RESPONSE","ACTIONS","SUMMARY","EXPLAIN","RETRY","DECLINE","ACCEPT","REGENERATE","CANCEL"],"mappings":"oLAsBe,CACXA,SAAU,CACNC,SAAU,aACVC,cAAe,6BACfC,KAAM,QACNC,YAAa,gBACbC,eAAgB,mBAChBC,SAAU,2BAEdC,QAAS,CACLC,QAAS,oDACTC,QAAS,kDACTC,MAAO,gDACPC,QAAS,2CACTC,OAAQ,0CACRC,WAAY,qDACZC,OAAQ"}
+149 -55
View File
@@ -31,6 +31,7 @@ import AIHelper from 'core_ai/helper';
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';
const AICourseAssist = class {
@@ -57,6 +58,8 @@ const AICourseAssist = class {
this.aiDrawerElement = document.querySelector(Selectors.ELEMENTS.AIDRAWER);
this.aiDrawerBodyElement = document.querySelector(Selectors.ELEMENTS.AIDRAWER_BODY);
this.pageElement = document.querySelector(Selectors.ELEMENTS.PAGE);
this.lastAction = '';
this.responses = new Map();
this.registerEventListeners();
}
@@ -66,18 +69,39 @@ const AICourseAssist = class {
*/
registerEventListeners() {
document.addEventListener('click', async(e) => {
// Display summarise.
const summariseAction = e.target.closest(Selectors.ACTIONS.SUMMARY);
if (summariseAction) {
e.preventDefault();
this.toggleAIDrawer();
this.openAIDrawer();
this.lastAction = 'summarise';
const isPolicyAccepted = await this.isPolicyAccepted();
if (!isPolicyAccepted) {
// Display policy.
this.displayPolicy();
return;
}
// Display summary.
this.displaySummary();
this.displayAction(this.lastAction);
}
// Display explain.
const explainAction = e.target.closest(Selectors.ACTIONS.EXPLAIN);
if (explainAction) {
e.preventDefault();
this.openAIDrawer();
this.lastAction = 'explain';
const isPolicyAccepted = await this.isPolicyAccepted();
if (!isPolicyAccepted) {
// Display policy.
this.displayPolicy();
return;
}
this.displayAction(this.lastAction);
}
// Close AI drawer.
const closeAiDrawer = e.target.closest(Selectors.ELEMENTS.AIDRAWER_CLOSE);
if (closeAiDrawer) {
e.preventDefault();
this.closeAIDrawer();
}
});
@@ -95,11 +119,11 @@ const AICourseAssist = class {
registerPolicyEventListeners() {
const acceptAction = document.querySelector(Selectors.ACTIONS.ACCEPT);
const declineAction = document.querySelector(Selectors.ACTIONS.DECLINE);
if (acceptAction) {
if (acceptAction && this.lastAction.length) {
acceptAction.addEventListener('click', (e) => {
e.preventDefault();
this.acceptPolicy().then(() => {
return this.displaySummary();
return this.displayAction(this.lastAction);
}).catch(Notification.exception);
});
}
@@ -116,27 +140,34 @@ const AICourseAssist = class {
*/
registerErrorEventListeners() {
const retryAction = document.querySelector(Selectors.ACTIONS.RETRY);
if (retryAction) {
if (retryAction && this.lastAction.length) {
retryAction.addEventListener('click', (e) => {
e.preventDefault();
this.aiDrawerBodyElement.dataset.hasdata = '0';
this.displaySummary();
this.displayAction(this.lastAction);
});
}
}
/**
* Register event listeners for the response.
* Register event listeners for the responses.
*/
registerResponseEventListeners() {
const regenerateAction = document.querySelector(Selectors.ACTIONS.REGENERATE);
if (regenerateAction) {
regenerateAction.addEventListener('click', (e) => {
e.preventDefault();
this.aiDrawerBodyElement.dataset.hasdata = '0';
this.displaySummary();
});
}
// Get all regenerate action buttons (one per response in the AI drawer).
const regenerateActions = document.querySelectorAll(Selectors.ACTIONS.REGENERATE);
// Add event listeners for each regenerate action.
regenerateActions.forEach(regenerateAction => {
const responseElement = regenerateAction.closest(Selectors.ELEMENTS.RESPONSE);
if (regenerateAction && responseElement) {
// Get the action that this response is associated with.
const actionPerformed = responseElement.getAttribute('data-action-performed');
regenerateAction.addEventListener('click', (e) => {
e.preventDefault();
// Remove the old response before displaying the new one.
this.removeResponseFromStack(actionPerformed);
this.displayAction(actionPerformed);
});
}
});
}
registerLoadingEventListeners() {
@@ -146,6 +177,10 @@ const AICourseAssist = class {
e.preventDefault();
this.setRequestCancelled();
this.toggleAIDrawer();
this.removeResponseFromStack('loading');
// Refresh the response stack to avoid false indication of loading.
const responses = this.getResponseStack();
this.aiDrawerBodyElement.innerHTML = responses;
});
}
}
@@ -181,8 +216,6 @@ const AICourseAssist = class {
if (!this.pageElement.classList.contains('show-drawer-right')) {
this.addPadding();
}
// Disable the summary button.
this.disableSummaryButton();
}
/**
@@ -194,8 +227,6 @@ const AICourseAssist = class {
if (this.pageElement.classList.contains('show-drawer-right') && this.aiDrawerBodyElement.dataset.removepadding === '1') {
this.removePadding();
}
// Enable the summary button.
this.enableSummaryButton();
}
/**
@@ -226,24 +257,26 @@ const AICourseAssist = class {
}
/**
* Disable the summary button.
* Get important params related to the action.
* @param {string} action The action to use.
* @returns {object} The params to use for the action.
*/
disableSummaryButton() {
const summaryButton = document.querySelector(Selectors.ACTIONS.SUMMARY);
if (summaryButton) {
summaryButton.setAttribute('disabled', 1);
}
}
async getParamsForAction(action) {
let params = {};
/**
* Enable the summary button and focus on it.
*/
enableSummaryButton() {
const summaryButton = document.querySelector(Selectors.ACTIONS.SUMMARY);
if (summaryButton) {
summaryButton.removeAttribute('disabled');
summaryButton.focus();
switch (action) {
case 'summarise':
params.method = 'aiplacement_courseassist_summarise_text';
params.heading = await getString('aisummary', 'aiplacement_courseassist');
break;
case 'explain':
params.method = 'aiplacement_courseassist_explain_text';
params.heading = await getString('aiexplain', 'aiplacement_courseassist');
break;
}
return params;
}
/**
@@ -263,11 +296,12 @@ const AICourseAssist = class {
}
/**
* Check if the AI drawer has generated content or not.
* Check if the AI drawer has already generated content for a particular action.
* @param {string} action The action to check.
* @return {boolean} True if the AI drawer has generated content, false otherwise.
*/
hasGeneratedContent() {
return this.aiDrawerBodyElement.dataset.hasdata === '1';
hasGeneratedContent(action) {
return this.responses.has(action);
}
/**
@@ -286,23 +320,36 @@ const AICourseAssist = class {
*/
displayLoading() {
Templates.render('aiplacement_courseassist/loading', {}).then((html) => {
this.aiDrawerBodyElement.innerHTML = html;
this.addResponseToStack('loading', html);
const responses = this.getResponseStack();
this.aiDrawerBodyElement.innerHTML = responses;
this.registerLoadingEventListeners();
return;
}).then(() => {
this.removeResponseFromStack('loading');
return;
}).catch(Notification.exception);
}
/**
* Display the summary.
* Display the action result in the AI drawer.
* @param {string} action The action to display.
*/
async displaySummary() {
if (!this.hasGeneratedContent()) {
async displayAction(action) {
if (this.hasGeneratedContent(action)) {
// Scroll to generated content.
const existingReponse = document.querySelector('[data-action-performed="' + action + '"]');
if (existingReponse) {
this.aiDrawerBodyElement.scrollTop = existingReponse.offsetTop;
}
} else {
// Display loading spinner.
this.displayLoading();
// Clear the drawer content to prevent sending some unnecessary content.
// Clear the drawer to prevent including the previously generated response in the new response prompt.
this.aiDrawerBodyElement.innerHTML = '';
const params = await this.getParamsForAction(action);
const request = {
methodname: 'aiplacement_courseassist_summarise_text',
methodname: params.method,
args: {
contextid: this.contextId,
prompttext: this.getTextContent(),
@@ -315,9 +362,9 @@ const AICourseAssist = class {
return;
} else {
if (!this.isRequestCancelled()) {
// Replace double line breaks with <br> and with </p><p> for paragraphs.
const generatedContent = AIHelper.replaceLineBreaks(responseObj.generatedcontent);
this.displayResponse(generatedContent);
// Perform replacements on the generated context to ensure it is formatted correctly.
const generatedContent = AIHelper.formatResponse(responseObj.generatedcontent);
this.displayResponse(generatedContent, action);
return;
} else {
this.aiDrawerBodyElement.dataset.cancelled = '0';
@@ -331,13 +378,55 @@ const AICourseAssist = class {
}
/**
* Display the response.
* @param {String} content The content to display.
* Add the HTML response to the response stack.
* The stack will be used to display all responses in the AI drawer.
* @param {String} action The action key.
* @param {String} html The HTML to store.
*/
displayResponse(content) {
Templates.render('aiplacement_courseassist/response', {content: content}).then((html) => {
this.aiDrawerBodyElement.innerHTML = html;
this.aiDrawerBodyElement.dataset.hasdata = '1';
addResponseToStack(action, html) {
this.responses.set(action, html);
}
/**
* Remove a stored response, allowing for a regenerated one.
* @param {String} action The action key.
*/
removeResponseFromStack(action) {
if (this.responses.has(action)) {
this.responses.delete(action);
}
}
/**
* Return a stack of HTML responses.
* @return {String} HTML responses.
*/
getResponseStack() {
let stack = '';
// Reverse to get newest first.
const responses = [...this.responses.values()].reverse();
for (const response of responses) {
stack += response;
}
return stack;
}
/**
* Display the responses.
* @param {String} content The content to display.
* @param {String} action The action used.
*/
async displayResponse(content, action) {
const params = await this.getParamsForAction(action);
const args = {
content: content,
heading: params.heading,
action: action,
};
Templates.render('aiplacement_courseassist/response', args).then((html) => {
this.addResponseToStack(action, html);
const responses = this.getResponseStack();
this.aiDrawerBodyElement.innerHTML = responses;
this.registerResponseEventListeners();
return;
}).catch(Notification.exception);
@@ -348,9 +437,14 @@ const AICourseAssist = class {
*/
displayError() {
Templates.render('aiplacement_courseassist/error', {}).then((html) => {
this.aiDrawerBodyElement.innerHTML = html;
this.addResponseToStack('error', html);
const responses = this.getResponseStack();
this.aiDrawerBodyElement.innerHTML = responses;
this.registerErrorEventListeners();
return;
}).then(() => {
this.removeResponseFromStack('error');
return;
}).catch(Notification.exception);
}
@@ -26,13 +26,16 @@ export default {
AIDRAWER_BODY: '#ai-drawer .ai-drawer-body',
PAGE: '#page',
MAIN_REGION: '[role="main"]',
AIDRAWER_CLOSE: '#ai-drawer-close',
RESPONSE: '.course-assist-response',
},
ACTIONS: {
SUMMARY: '[data-action="course-summarise"]',
RETRY: '[data-action="course-summarise-retry"]',
SUMMARY: '.course-assist-controls [data-action="summarise"]',
EXPLAIN: '.course-assist-controls [data-action="explain"]',
RETRY: '.course-assist-controls [data-action="retry"]',
DECLINE: '.ai-policy-block [data-action="decline"]',
ACCEPT: '.ai-policy-block [data-action="accept"]',
REGENERATE: '[data-action="course-summarise-regenerate"]',
CANCEL: '[data-action="course-summarise-cancel"]',
REGENERATE: '.course-assist-controls [data-action="regenerate"]',
CANCEL: '.course-assist-controls [data-action="cancel"]',
}
};
@@ -0,0 +1,155 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace aiplacement_courseassist\external;
use aiplacement_courseassist\utils;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_value;
/**
* External API to call explain text action for this placement.
*
* @package aiplacement_courseassist
* @copyright 2024 David Woloszyn <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class explain_text extends external_api {
/**
* Explain text parameters.
*
* @return external_function_parameters
* @since Moodle 5.0
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'contextid' => new external_value(
PARAM_INT,
'The context ID',
VALUE_REQUIRED,
),
'prompttext' => new external_value(
PARAM_RAW,
'The prompt text for the AI service',
VALUE_REQUIRED,
),
]);
}
/**
* Explain text from the AI placement.
*
* @param int $contextid The context ID.
* @param string $prompttext The data encoded as a json array.
* @return array The generated content.
* @since Moodle 5.0
*/
public static function execute(
int $contextid,
string $prompttext,
): array {
global $USER;
// Parameter validation.
[
'contextid' => $contextid,
'prompttext' => $prompttext,
] = self::validate_parameters(self::execute_parameters(), [
'contextid' => $contextid,
'prompttext' => $prompttext,
]);
// Context validation and permission check.
// Get the context from the passed in ID.
$context = \context::instance_by_id($contextid);
// Check the user has permission to use the AI service.
self::validate_context($context);
if (!utils::is_course_assist_available($context)) {
throw new \moodle_exception('nocourseassist', 'aiplacement_courseassist');
}
// Prepare the action.
$action = new \core_ai\aiactions\explain_text(
contextid: $contextid,
userid: $USER->id,
prompttext: $prompttext,
);
// Send the action to the AI manager.
$manager = \core\di::get(\core_ai\manager::class);
$response = $manager->process_action($action);
// Return the response.
return [
'success' => $response->get_success(),
'generatedcontent' => $response->get_response_data()['generatedcontent'] ?? '',
'finishreason' => $response->get_response_data()['finishreason'] ?? '',
'errorcode' => $response->get_errorcode(),
'error' => $response->get_errormessage(),
'timecreated' => $response->get_timecreated(),
'prompttext' => $prompttext,
];
}
/**
* Generate content return value.
*
* @return external_function_parameters
* @since Moodle 5.0
*/
public static function execute_returns(): external_function_parameters {
return new external_function_parameters([
'success' => new external_value(
PARAM_BOOL,
'Was the request successful',
VALUE_REQUIRED
),
'timecreated' => new external_value(
PARAM_INT,
'The time the request was created',
VALUE_REQUIRED,
),
'prompttext' => new external_value(
PARAM_RAW,
'The prompt text for the AI service',
VALUE_REQUIRED,
),
'generatedcontent' => new external_value(
PARAM_RAW,
'The text generated by AI.',
VALUE_DEFAULT,
),
'finishreason' => new external_value(
PARAM_ALPHA,
'The reason generation was stopped',
VALUE_DEFAULT,
'stop',
),
'errorcode' => new external_value(
PARAM_INT,
'Error code if any',
VALUE_DEFAULT,
0,
),
'error' => new external_value(
PARAM_TEXT,
'Error message if any',
VALUE_DEFAULT,
'',
),
]);
}
}
@@ -37,11 +37,11 @@ class hook_callbacks {
}
/**
* Bootstrap the summarise button.
* Bootstrap the action buttons.
*
* @param after_http_headers $hook
*/
public static function after_http_headers(after_http_headers $hook): void {
\aiplacement_courseassist\output\assist_ui::load_summarise_button($hook);
\aiplacement_courseassist\output\assist_ui::action_buttons_handler($hook);
}
}
@@ -51,19 +51,30 @@ class assist_ui {
}
/**
* Bootstrap the summarise button.
* Determine if we should be loading a single button or a dropdown.
*
* @param after_http_headers $hook
*/
public static function load_summarise_button(after_http_headers $hook): void {
global $OUTPUT;
public static function action_buttons_handler(after_http_headers $hook): void {
global $PAGE, $OUTPUT;
// Preflight checks.
if (!self::preflight_checks()) {
return;
}
$html = $OUTPUT->render_from_template('aiplacement_courseassist/summarise_button', []);
$actions['actions'] = utils::get_actions_available($PAGE->context);
// No actions available.
if (empty($actions['actions'])) {
return;
}
if (count($actions['actions']) > 1) {
$actions['isdropdown'] = true;
}
$html = $OUTPUT->render_from_template('aiplacement_courseassist/actions', $actions);
$hook->add_html($html);
}
@@ -29,6 +29,7 @@ class placement extends \core_ai\placement {
public static function get_action_list(): array {
return [
\core_ai\aiactions\summarise_text::class,
\core_ai\aiactions\explain_text::class,
];
}
+45 -7
View File
@@ -16,6 +16,7 @@
namespace aiplacement_courseassist;
use core_ai\aiactions\explain_text;
use core_ai\aiactions\summarise_text;
use core_ai\manager;
@@ -40,16 +41,53 @@ class utils {
return false;
}
$aimanager = \core\di::get(manager::class);
$providers = $aimanager->get_providers_for_actions([summarise_text::class], true);
if (!has_capability('aiplacement/courseassist:summarise_text', $context)
|| !$aimanager->is_action_available(summarise_text::class)
|| !$aimanager->is_action_enabled('aiplacement_courseassist', summarise_text::class)
|| empty($providers[summarise_text::class])
) {
if (empty(self::get_actions_available($context))) {
return false;
}
return true;
}
/**
* Get all the actions available and return action data for template.
*
* @param \context $context The context.
* @return array Return the actions available with data.
*/
public static function get_actions_available(\context $context): array {
$actions = [];
$actionclasses = [
summarise_text::class,
explain_text::class,
];
$manager = \core\di::get(manager::class);
$providers = $manager->get_providers_for_actions($actionclasses, true);
// Summarise text.
if (has_capability('aiplacement/courseassist:summarise_text', $context)
&& $manager->is_action_available(summarise_text::class)
&& $manager->is_action_enabled('aiplacement_courseassist', summarise_text::class)
&& !empty($providers[summarise_text::class])
) {
$actions[] = [
'action' => 'summarise',
'buttontext' => get_string('summarise', 'aiplacement_courseassist'),
'title' => get_string('summarise_tooltips', 'aiplacement_courseassist'),
];
}
// Explain text.
if (has_capability('aiplacement/courseassist:explain_text', $context)
&& $manager->is_action_available(explain_text::class)
&& $manager->is_action_enabled('aiplacement_courseassist', explain_text::class)
&& !empty($providers[explain_text::class])
) {
$actions[] = [
'action' => 'explain',
'buttontext' => get_string('explain', 'aiplacement_courseassist'),
'title' => get_string('explain_tooltips', 'aiplacement_courseassist'),
];
}
return $actions;
}
}
+10
View File
@@ -34,4 +34,14 @@ $capabilities = [
'student' => CAP_ALLOW,
],
],
'aiplacement/courseassist:explain_text' => [
'captype' => 'write',
'contextlevel' => CONTEXT_COURSE,
'archetypes' => [
'manager' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW,
'teacher' => CAP_ALLOW,
'student' => CAP_ALLOW,
],
],
];
@@ -32,4 +32,11 @@ $functions = [
'ajax' => true,
'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE],
],
'aiplacement_courseassist_explain_text' => [
'classname' => 'aiplacement_courseassist\external\explain_text',
'description' => 'Explain text for the Course Assistance Placement',
'type' => 'write',
'ajax' => true,
'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE],
],
];
@@ -22,9 +22,14 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['aiexplain'] = 'AI explain';
$string['aifeatures'] = 'AI features';
$string['aisummary'] = 'AI summary';
$string['courseassist:explain_text'] = 'Explain text';
$string['courseassist:summarise_text'] = 'Summarise text';
$string['copy'] = 'Copy';
$string['explain'] = 'Explain';
$string['explain_tooltips'] = 'Create an AI-generated explanation of the page content';
$string['generatefailtitle'] = 'Something went wrong';
$string['generating'] = 'Generating your response';
$string['nocourseassist'] = 'An AI-generated summary is not available for this context.';
+20 -23
View File
@@ -38,40 +38,37 @@
scrollbar-color: #6a737b #f8f9fa;
}
#course-summarise-response {
.course-assist-response-content {
font-size: 0.875em;
}
.course-summarise-response-controls button .icon,
.course-summarise-response-watermark img.icon {
#ai-features .icon {
min-width: 16px;
}
.course-assist-response-header .icon {
width: 30px;
height: 30px;
}
.course-assist-controls .icon,
.ai-course-assist-sparkles-icon .icon {
margin-right: 0;
}
.ai-course-summarise-controls button.btn.btn-outline-secondary {
.course-assist-controls .ai-action {
color: unset;
}
.ai-course-summarise-controls button.btn.btn-outline-secondary span.ai-course-summarise-sparkles-icon {
/* Toggle visibility of sparkles icon on hover. */
.course-assist-controls .ai-action .ai-course-assist-sparkles-icon,
.course-assist-controls .ai-action:not([disabled]):hover .ai-course-assist-sparkles-icon.white,
.course-assist-controls.show .ai-action:not([disabled]) .ai-course-assist-sparkles-icon.white {
display: inline-block;
}
.ai-course-summarise-controls button.btn.btn-outline-secondary span.ai-course-summarise-sparkles-icon.white {
.course-assist-controls .ai-action .ai-course-assist-sparkles-icon.white,
.course-assist-controls .ai-action:not([disabled]):hover .ai-course-assist-sparkles-icon,
.course-assist-controls.show .ai-action:not([disabled]) .ai-course-assist-sparkles-icon {
display: none;
}
.ai-course-summarise-controls button.btn.btn-outline-secondary:not([disabled]):hover {
color: #fff;
}
.ai-course-summarise-controls button.btn.btn-outline-secondary:not([disabled]):hover span.ai-course-summarise-sparkles-icon {
display: none;
}
.ai-course-summarise-controls button.btn.btn-outline-secondary:not([disabled]):hover span.ai-course-summarise-sparkles-icon.white {
display: inline-block;
}
.ai-course-summarise-controls button img.icon {
width: auto;
vertical-align: sub;
}
@@ -0,0 +1,47 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template aiplacement_courseassist/action_button
* isdropdown - Is this a dropdown item
* actions - The action being performed
* buttontext - The text to display for the button
* title - The tooltip/title text to use
Template to display sparkles icon.
Example context (json):
{
"isdropdown": true,
"action": "explain",
"buttontext": "Explain",
"title": "Create an AI-generated explanation of the page content"
}
}}
<button
class="{{#isdropdown}}dropdown-item{{/isdropdown}}{{^isdropdown}}btn btn-outline-secondary{{/isdropdown}} ai-action"
type="button"
title="{{title}}"
data-action="{{action}}"
data-bs-toggle="tooltip"
data-bs-placement="right"
aria-controls="ai-drawer"
>
{{^isdropdown}}
{{> aiplacement_courseassist/sparkles_icon }}
{{/isdropdown}}
{{buttontext}}
</button>
@@ -0,0 +1,53 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template aiplacement_courseassist/actions
Template to display AI action in a dropdown or single action button for the course assist.
Context variables required for this template:
* isdropdown - If this is a dropdown, load the appropriate layout
* actions - List of actions to generate buttons for
Example context (json):
{
"isdropdown": true,
"actions": [
{
"action": "summarise",
"buttontext": "Summarise",
"title": "Create an AI-generated summary of the page content"
},
{
"action": "explain",
"buttontext": "Explain",
"title": "Create an AI-generated explanation of the page content"
}
]
}
}}
<div {{#isdropdown}}id="#ai-features"{{/isdropdown}}
class="course-assist-controls py-3">
{{#isdropdown}}
{{> aiplacement_courseassist/actions_dropdown }}
{{/isdropdown}}
{{^isdropdown}}
{{#actions}}
{{> aiplacement_courseassist/action_button }}
{{/actions}}
{{/isdropdown}}
</div>
@@ -0,0 +1,56 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template aiplacement_courseassist/actions_dropdown
Template to display AI action in a dropdown for the course assist.
Context variables required for this template:
* actions - List of actions to generate buttons for
Example context (json):
{
"actions": [
{
"action": "summarise",
"buttontext": "Summarise",
"title": "Create an AI-generated summary of the page content"
},
{
"action": "explain",
"buttontext": "Explain",
"title": "Create an AI-generated explanation of the page content"
}
]
}
}}
<button
class="btn btn-outline-secondary dropdown-toggle ai-action"
type="button"
title="{{#str}} aifeatures, aiplacement_courseassist {{/str}}"
data-bs-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>
{{> aiplacement_courseassist/sparkles_icon }}
{{#str}} aifeatures, aiplacement_courseassist {{/str}}
</button>
<div class="dropdown-menu">
{{#actions}}
{{> aiplacement_courseassist/action_button }}
{{/actions}}
</div>
@@ -22,25 +22,21 @@
Context variables required for this template:
* userid - User ID
* contextid - Context ID
* content - Content to display
Example context (json):
{
"userid": "1",
"contextid": "1",
"content": "<p>Content to display</p>"
"contextid": "1"
}
}}
<div class="ai-drawer" id="ai-drawer">
<div class="ai-drawer-header">
<button id="ai-drawer-close" class="btn ai-drawer-button" type="button" data-action="course-summarise">
<button id="ai-drawer-close" class="btn ai-drawer-button" type="button">
{{#pix}} e/cancel, core {{/pix}}
<span class="visually-hidden">{{#str}} closedrawer, core {{/str}}</span>
</button>
</div>
<div class="ai-drawer-body" data-hasdata="0" data-cancelled="0">
{{{content}}}
</div>
<div class="ai-drawer-body" data-cancelled="0"></div>
</div>
{{#js}}
require(['aiplacement_courseassist/placement'], function(AICourseAssist) {
@@ -28,13 +28,13 @@
}}
<section class="mb-3">
<div class="card-body p-3">
<h3 id="instance-9-header" class="h6 card-title d-inline">
<h3 class="h6 card-title d-inline">
{{#pix}} req, core {{/pix}}
{{#str}} generatefailtitle, aiplacement_courseassist {{/str}}
</h3>
<div class="card-text content mt-3">
<div class="d-block pt-3">
<button class="btn btn-sm btn-outline-secondary" data-action="course-summarise-retry">
<div class="d-block pt-3 course-assist-controls">
<button class="btn btn-sm btn-outline-secondary" data-action="retry">
{{#str}} tryagain, aiplacement_courseassist {{/str}}
</button>
</div>
@@ -28,13 +28,13 @@
}}
<section class="card mb-3">
<div class="card-body p-3">
<h3 id="instance-9-header" class="h6 card-title d-inline">
<h3 class="h6 card-title d-inline">
{{#pix}} i/loading, core {{/pix}}
{{#str}} generating, aiplacement_courseassist {{/str}}
</h3>
<div class="card-text content mt-3">
<div class="d-block pt-3">
<button class="btn btn-sm btn-outline-secondary" data-action="course-summarise-cancel">
<div class="d-block pt-3 course-assist-controls">
<button class="btn btn-sm btn-outline-secondary" data-action="cancel">
{{#str}} cancel, core {{/str}}
</button>
</div>
@@ -21,33 +21,42 @@
Context variables required for this template:
* content - Content to display
* heading - The heading to display
* action - The action being performed
Example context (json):
{
"content": "<p>Content to display</p>"
"content": "<p>Content to display</p>",
"heading": "AI Explain",
"action": "explain"
}
}}
<section class="card mb-3">
<section class="course-assist-response card mb-3" data-action-performed="{{action}}">
<div class="card-body p-3">
<h3 id="instance-9-header" class="h5 card-title d-inline">
{{#str}} aisummary, aiplacement_courseassist {{/str}}
<h3 class="course-assist-response-header h5 card-title d-inline">
<span class="ai-course-assist-sparkles-icon">
{{#pix}} sparkles, aiplacement_courseassist {{/pix}}
</span>
{{heading}}
</h3>
<div class="card-text content mt-3">
<div id="course-summarise-response" class="mb-3">
<div id="course-assist-response-content-{{uniqid}}" class="course-assist-response-content mb-3">
{{{content}}}
</div>
<div class="course-summarise-response-watermark">
<div class="course-assist-response-watermark">
<small class="text-muted">
{{#pix}} sparkles, aiplacement_courseassist {{/pix}}
<span class="ai-course-assist-sparkles-icon">
{{#pix}} sparkles, aiplacement_courseassist {{/pix}}
</span>
{{#str}} contentwatermark, core_ai {{/str}}
</small>
</div>
<div class="course-summarise-response-controls d-block pt-3">
<button class="btn btn-sm btn-outline-secondary" data-action="course-summarise-regenerate">
<div class="course-assist-controls d-block pt-3">
<button class="btn btn-sm btn-outline-secondary" data-action="regenerate">
{{#pix}} a/refresh, core {{/pix}}
{{#str}} regenerate, aiplacement_courseassist {{/str}}
</button>
<button class="btn btn-sm btn-outline-secondary" data-action="copytoclipboard" data-clipboard-target="#course-summarise-response">
<button class="btn btn-sm btn-outline-secondary" data-action="copytoclipboard" data-clipboard-target="#course-assist-response-content-{{uniqid}}">
{{#pix}} e/copy, core {{/pix}}
{{#str}} copy, aiplacement_courseassist {{/str}}
</button>
@@ -0,0 +1,31 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template aiplacement_courseassist/sparkles_icon
Template to display sparkles icon.
Example context (json):
{
}
}}
<span class="ai-course-assist-sparkles-icon">
{{#pix}} sparkles, aiplacement_courseassist {{/pix}}
</span>
<span class="ai-course-assist-sparkles-icon white">
{{#pix}} sparkles-white, aiplacement_courseassist {{/pix}}
</span>
@@ -1,46 +0,0 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template aiplacement_courseassist/summarise_button
Template to display summarise button for the course assist.
Context variables required for this template:
* none
Example context (json):
{
}
}}
<div class="ai-course-summarise-controls pt-3 pb-3">
<button class="btn btn-outline-secondary"
aria-controls="ai-drawer"
type="button"
data-action="course-summarise"
data-bs-toggle="tooltip"
data-bs-html="true"
title="{{#str}} summarise_tooltips, aiplacement_courseassist {{/str}}"
>
<span class="ai-course-summarise-sparkles-icon">
{{#pix}} sparkles, aiplacement_courseassist {{/pix}}
</span>
<span class="ai-course-summarise-sparkles-icon white">
{{#pix}} sparkles-white, aiplacement_courseassist {{/pix}}
</span>
{{#str}} summarise, aiplacement_courseassist {{/str}}
</button>
</div>
@@ -0,0 +1,84 @@
@core_ai @aiplacement_courseassist
Feature: AI course assist features
In order to use AI course assist features
As a user
I need to access a course and choose the desired AI feature
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | t1@example.com |
| teacher2 | Teacher | 2 | t2@example.com |
And the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | topics |
And the following "roles" exist:
| name | shortname | description | archetype |
| Custom editing teacher | custom1 | My custom role 1 | editingteacher |
| Custom teacher | custom2 | My custom role 2 | editingteacher |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | custom1 |
| teacher2 | C1 | custom2 |
And the following "activities" exist:
| activity | name | intro | introformat | course | content | contentformat | idnumber |
| page | PageName1 | PageDesc1 | 1 | C1 | PageContent | 1 | 1 |
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| aiplacement/courseassist:summarise_text | Prohibit | custom2 | Course | C1 |
And the following "core_ai > ai providers" exist:
| provider | name | enabled | apikey | orgid |
| aiprovider_openai | OpenAI API test | 1 | 123 | abc |
And the following config values are set as admin:
| enabled | 1 | aiplacement_courseassist |
Scenario: AI features dropdown is visible when more than one feature is enabled
When I am on the "PageName1" "page activity" page logged in as teacher1
Then "AI features" "button" should exist
# Check nested buttons exist too.
And "Summarise" "button" should exist
And "Explain" "button" should exist
Scenario: AI features dropdown is not visible when only one feature is enabled
Given I set the following action configuration for ai provider with name "OpenAI API test":
| action | enabled |
| explain_text | 0 |
| summarise_text | 1 |
When I am on the "PageName1" "page activity" page logged in as teacher1
Then "AI features" "button" should not exist
And "Explain" "button" should not exist
# Only the summarise button should exist.
And "Summarise" "button" should exist
Scenario: AI features are not available if placement is not enabled
Given the following config values are set as admin:
| enabled | | aiplacement_courseassist |
When I am on the "PageName1" "page activity" page logged in as teacher1
Then "AI features" "button" should not exist
Scenario: AI features are not available if provider action is not enabled
Given I set the following action configuration for ai provider with name "OpenAI API test":
| action | enabled |
| explain_text | 0 |
| summarise_text | 0 |
When I am on the "PageName1" "page activity" page logged in as teacher1
Then "AI features" "button" should not exist
Scenario: AI features are not available if placement action is not enabled
Given I set the following action configuration for ai provider with name "OpenAI API test":
| action | enabled |
| explain_text | 0 |
| summarise_text | 0 |
When I am on the "PageName1" "page activity" page logged in as teacher1
Then "AI features" "button" should not exist
Scenario: AI features are not available if the user does not have permission
When I am on the "PageName1" "page activity" page logged in as teacher2
Then "AI features" "button" should not exist
@javascript
Scenario: I can view the AI drawer contents using the AI features dropdown
Given I am on the "PageName1" "page activity" page logged in as teacher1
When I click on "AI features" "button"
And I click on "Summarise" "button"
Then I should see "Welcome to the new AI feature!" in the ".ai-drawer" "css_element"
@@ -1,103 +0,0 @@
@core_ai @aiplacement_courseassist
Feature: AI Course assist summarise
In order to generate a summary of a module using AI, as a teacher, I need to be able to use the AI course assist summarise feature
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | t1@example.com |
| teacher2 | Teacher | 2 | t2@example.com |
And the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | topics |
And the following "roles" exist:
| name | shortname | description | archetype |
| Custom editing teacher | custom1 | My custom role 1 | editingteacher |
| Custom teacher | custom2 | My custom role 2 | editingteacher |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | custom1 |
| teacher2 | C1 | custom2 |
And the following "activities" exist:
| activity | name | intro | introformat | course | content | contentformat | idnumber |
| page | PageName1 | PageDesc1 | 1 | C1 | PageContent | 1 | 1 |
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| aiplacement/courseassist:summarise_text | Prohibit | custom2 | Course | C1 |
And I log in as "admin"
And the following "core_ai > ai providers" exist:
|provider | name | enabled | apikey | orgid |
|aiprovider_openai| OpenAI API test | 1 | 123 | abc |
And I enable "courseassist" "aiplacement" plugin
@javascript
Scenario: Summarise text using AI is not available if placement is not enabled
Given I disable "courseassist" "aiplacement" plugin
When I am on the "PageName1" "page activity" page logged in as teacher1
Then "Summarise" "button" should not exist
And I enable "courseassist" "aiplacement" plugin
And I am on the "PageName1" "page activity" page logged in as teacher1
And "Summarise" "button" should exist
@javascript
Scenario: Summarise text using AI is not available if provider is not enabled
Given I "disable" the ai provider with name "OpenAI API test"
When I am on the "PageName1" "page activity" page logged in as teacher1
Then "Summarise" "button" should not exist
And I "enable" the ai provider with name "OpenAI API test"
And I am on the "PageName1" "page activity" page logged in as teacher1
And "Summarise" "button" should exist
@javascript
Scenario: Summarise text using AI is not available if provider action is not enabled
Given the following config values are set as admin:
| summarise_text | | aiprovider_openai |
And I set the following action configuration for ai provider with name "OpenAI API test":
| action | enabled |
| summarise_text | 0 |
When I am on the "PageName1" "page activity" page logged in as teacher1
Then "Summarise" "button" should not exist
And I set the following action configuration for ai provider with name "OpenAI API test":
| action | enabled |
| summarise_text | 1 |
And I am on the "PageName1" "page activity" page logged in as teacher1
And "Summarise" "button" should exist
@javascript
Scenario: Summarise text using AI is not available if placement action is not enabled
Given the following config values are set as admin:
| summarise_text | | aiplacement_courseassist |
When I am on the "PageName1" "page activity" page logged in as teacher1
Then "Summarise" "button" should not exist
And the following config values are set as admin:
| summarise_text | 1 | aiplacement_courseassist |
And I am on the "PageName1" "page activity" page logged in as teacher1
And "Summarise" "button" should exist
@javascript
Scenario: Summarise text using AI is not available if provider action is not enabled and placement action is enabled
Given the following config values are set as admin:
| summarise_text | | aiplacement_courseassist |
And I set the following action configuration for ai provider with name "OpenAI API test":
| action | enabled |
| summarise_text | 0 |
When I am on the "PageName1" "page activity" page logged in as teacher1
Then "Summarise" "button" should not exist
And the following config values are set as admin:
| summarise_text | 1 | aiplacement_courseassist |
And I am on the "PageName1" "page activity" page logged in as teacher1
And "Summarise" "button" should not exist
And I set the following action configuration for ai provider with name "OpenAI API test":
| action | enabled |
| summarise_text | 1 |
And I am on the "PageName1" "page activity" page logged in as teacher1
And "Summarise" "button" should exist
@javascript
Scenario: Summarise text using AI is not available if the user does not have permission
When I am on the "PageName1" "page activity" page logged in as teacher2
Then "Summarise" "button" should not exist
When I am on the "PageName1" "page activity" page logged in as teacher1
And "Summarise" "button" should exist
And I click on "Summarise" "button"
And I should see "Welcome to the new AI feature!" in the ".ai-drawer" "css_element"
+46 -4
View File
@@ -48,13 +48,15 @@ final class utils_test extends \advanced_testcase {
$this->setUser($user1);
$this->assertFalse(utils::is_course_assist_available($context));
// Plugin is not enabled.
$this->setUser($user1);
// Provider is enabled, but plugin is not enabled.
set_config('enabled', 1, 'aiprovider_openai');
set_config('apikey', '123', 'aiprovider_openai');
set_config('enabled', 0, 'aiplacement_courseassist');
$this->assertFalse(utils::is_course_assist_available($context));
// Plugin is enabled but user does not have capability.
assign_capability('aiplacement/courseassist:summarise_text', CAP_PROHIBIT, $teacherrole->id, $context);
assign_capability('aiplacement/courseassist:explain_text', CAP_PROHIBIT, $teacherrole->id, $context);
$this->setUser($user2);
set_config('enabled', 1, 'aiplacement_courseassist');
$this->assertFalse(utils::is_course_assist_available($context));
@@ -62,28 +64,68 @@ final class utils_test extends \advanced_testcase {
// Plugin is enabled, user has capability and placement action is not available.
$this->setUser($user1);
set_config('summarise_text', 0, 'aiplacement_courseassist');
set_config('explain_text', 0, 'aiplacement_courseassist');
$this->assertFalse(utils::is_course_assist_available($context));
// Plugin is enabled, user has capability and provider action is not available.
$this->setUser($user1);
set_config('summarise_text', 0, 'aiprovider_openai');
set_config('summarise_text', 1, 'aiplacement_courseassist');
set_config('explain_text', 0, 'aiprovider_openai');
set_config('explain_text', 1, 'aiplacement_courseassist');
$this->assertFalse(utils::is_course_assist_available($context));
// Plugin is enabled, user has capability, placement action is available and provider action is available.
$mockmanager = $this->createMock(\core_ai\manager::class);
$mockmanager = $this->createMock(manager::class);
$mockmanager->method('is_action_available')->willReturn(true);
$mockmanager->method('is_action_enabled')->willReturn(true);
$mockmanager->method('get_providers_for_actions')->willReturn([
summarise_text::class => ['aiprovider_openai'],
]);
\core\di::set(\core_ai\manager::class, function() use ($mockmanager) {
\core\di::set(manager::class, function() use ($mockmanager) {
return $mockmanager;
});
$this->setUser($user1);
set_config('summarise_text', 1, 'aiplacement_courseassist');
set_config('explain_text', 1, 'aiprovider_openai');
set_config('explain_text', 1, 'aiplacement_courseassist');
$this->assertTrue(utils::is_course_assist_available($context));
}
/**
* Test get_actions_available method.
*/
public function test_get_actions_available(): void {
global $DB;
$this->resetAfterTest();
$user1 = $this->getDataGenerator()->create_user();
$course = $this->getDataGenerator()->create_course();
$context = \context_course::instance($course->id);
$this->getDataGenerator()->enrol_user($user1->id, $course->id, 'manager');
$this->setUser($user1);
// Two actions enabled.
set_config('enabled', 1, 'aiprovider_openai');
set_config('apikey', '123', 'aiprovider_openai');
set_config('explain_text', 1, 'aiplacement_courseassist');
set_config('summarise_text', 1, 'aiplacement_courseassist');
$manager = \core\di::get(manager::class);
$manager->create_provider_instance(
classname: '\aiprovider_openai\provider',
name: 'dummy',
enabled: true,
config: ['apikey' => '123'],
);
$this->assertCount(2, utils::get_actions_available($context));
// One action enabled.
set_config('summarise_text', 0, 'aiplacement_courseassist');
$this->assertCount(1, utils::get_actions_available($context));
// No actions enabled.
set_config('explain_text', 0, 'aiplacement_courseassist');
$this->assertCount(0, utils::get_actions_available($context));
}
}
+1 -1
View File
@@ -25,6 +25,6 @@
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'aiplacement_courseassist';
$plugin->version = 2024121800;
$plugin->version = 2025012000;
$plugin->requires = 2024100100;
$plugin->maturity = MATURITY_STABLE;
@@ -0,0 +1,28 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace aiprovider_azureai;
/**
* Class process text explanation.
*
* @package aiprovider_azureai
* @copyright 2024 David Woloszyn <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class process_explain_text extends process_generate_text {
}
+3 -2
View File
@@ -37,6 +37,7 @@ class provider extends \core_ai\provider {
\core_ai\aiactions\generate_text::class,
\core_ai\aiactions\generate_image::class,
\core_ai\aiactions\summarise_text::class,
\core_ai\aiactions\explain_text::class,
];
}
@@ -74,7 +75,7 @@ class provider extends \core_ai\provider {
$actionname = substr($action, (strrpos($action, '\\') + 1));
$customdata['actionname'] = $actionname;
$customdata['action'] = $action;
if ($actionname === 'generate_text' || $actionname === 'summarise_text') {
if ($actionname === 'generate_text' || $actionname === 'summarise_text' || $actionname === 'explain_text') {
return new form\action_generate_text_form(customdata: $customdata);
} else if ($actionname === 'generate_image') {
return new form\action_generate_image_form(customdata: $customdata);
@@ -90,7 +91,7 @@ class provider extends \core_ai\provider {
'actionname' => $actionname,
'action' => $action,
];
if ($actionname === 'generate_text' || $actionname === 'summarise_text') {
if ($actionname === 'generate_text' || $actionname === 'summarise_text' || $actionname === 'explain_text') {
$mform = new form\action_generate_text_form(customdata: $customdata);
return $mform->get_defaults();
} else if ($actionname === 'generate_image') {
@@ -22,6 +22,11 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['action:explain_text:apiversion'] = 'API version';
$string['action:explain_text:deployment'] = 'Deployment ID';
$string['action:explain_text:deployment_help'] = 'The deployment ID that relates to the API endpoint the provider uses for this action.';
$string['action:explain_text:systeminstruction'] = 'System instruction';
$string['action:explain_text:systeminstruction_help'] = 'This instruction is sent to the AI model along with the user\'s prompt. Editing this instruction is not recommended unless absolutely required.';
$string['action:generate_image:apiversion'] = 'API version';
$string['action:generate_image:deployment'] = 'Deployment ID';
$string['action:generate_image:deployment_help'] = 'The deployment ID that relates to the API endpoint the provider uses for this action.';
+2 -1
View File
@@ -54,10 +54,11 @@ final class provider_test extends \advanced_testcase {
public function test_get_action_list(): void {
$actionlist = $this->provider->get_action_list();
$this->assertIsArray($actionlist);
$this->assertEquals(3, count($actionlist));
$this->assertEquals(4, count($actionlist));
$this->assertContains(\core_ai\aiactions\generate_text::class, $actionlist);
$this->assertContains(\core_ai\aiactions\generate_image::class, $actionlist);
$this->assertContains(\core_ai\aiactions\summarise_text::class, $actionlist);
$this->assertContains(\core_ai\aiactions\explain_text::class, $actionlist);
}
/**
@@ -0,0 +1,28 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace aiprovider_openai;
/**
* Class process text explanation.
*
* @package aiprovider_openai
* @copyright 2024 David Woloszyn <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class process_explain_text extends process_generate_text {
}
+3 -2
View File
@@ -37,6 +37,7 @@ class provider extends \core_ai\provider {
\core_ai\aiactions\generate_text::class,
\core_ai\aiactions\generate_image::class,
\core_ai\aiactions\summarise_text::class,
\core_ai\aiactions\explain_text::class,
];
}
@@ -79,7 +80,7 @@ class provider extends \core_ai\provider {
$actionname = substr($action, (strrpos($action, '\\') + 1));
$customdata['actionname'] = $actionname;
$customdata['action'] = $action;
if ($actionname === 'generate_text' || $actionname === 'summarise_text') {
if ($actionname === 'generate_text' || $actionname === 'summarise_text' || $actionname === 'explain_text') {
return new form\action_generate_text_form(customdata: $customdata);
} else if ($actionname === 'generate_image') {
return new form\action_generate_image_form(customdata: $customdata);
@@ -96,7 +97,7 @@ class provider extends \core_ai\provider {
'action' => $action,
'providername' => 'aiprovider_openai',
];
if ($actionname === 'generate_text' || $actionname === 'summarise_text') {
if ($actionname === 'generate_text' || $actionname === 'summarise_text' || $actionname === 'explain_text') {
$mform = new form\action_generate_text_form(customdata: $customdata);
return $mform->get_defaults();
} else if ($actionname === 'generate_image') {
@@ -22,6 +22,11 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['action:explain_text:endpoint'] = 'API endpoint';
$string['action:explain_text:model'] = 'AI model';
$string['action:explain_text:model_help'] = 'The model used to explain the provided text.';
$string['action:explain_text:systeminstruction'] = 'System instruction';
$string['action:explain_text:systeminstruction_help'] = 'This instruction is sent to the AI model along with the user\'s prompt. Editing this instruction is not recommended unless absolutely required.';
$string['action:generate_image:endpoint'] = 'API endpoint';
$string['action:generate_image:model'] = 'AI model';
$string['action:generate_image:model_help'] = 'The model used to generate images from user prompts.';
+2 -1
View File
@@ -53,10 +53,11 @@ final class provider_test extends \advanced_testcase {
public function test_get_action_list(): void {
$actionlist = $this->provider->get_action_list();
$this->assertIsArray($actionlist);
$this->assertCount(3, $actionlist);
$this->assertCount(4, $actionlist);
$this->assertContains(\core_ai\aiactions\generate_text::class, $actionlist);
$this->assertContains(\core_ai\aiactions\generate_image::class, $actionlist);
$this->assertContains(\core_ai\aiactions\summarise_text::class, $actionlist);
$this->assertContains(\core_ai\aiactions\explain_text::class, $actionlist);
}
/**
+1 -1
View File
@@ -28,7 +28,7 @@
}}
<section class="card mb-3">
<div class="card-body p-3 ai-policy-block">
<h3 id="instance-9-header" class="h5 card-title d-inline">
<h3 class="h5 card-title d-inline">
{{#str}} aiusagepolicy, core_ai {{/str}}
</h3>
<div class="card-text content mt-3">
@@ -0,0 +1,78 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_ai\aiactions\responses;
use core_ai\aiactions\responses\response_explain_text;
/**
* Test response_generate_image_test action methods.
*
* @package core_ai
* @copyright 2024 David Woloszyn <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \core_ai\aiactions\responses\response_explain_text
*/
final class response_explain_text_test extends \advanced_testcase {
/**
* Test get_basename.
*/
public function test_get_success(): void {
$actionresponse = new response_explain_text(
success: true,
);
$this->assertTrue($actionresponse->get_success());
$this->assertEquals('explain_text', $actionresponse->get_actionname());
}
/**
* Test constructor with error.
*/
public function test_construct_error(): void {
$this->expectException(\coding_exception::class);
$this->expectExceptionMessage('Error code and message must exist in an error response.');
new response_explain_text(
success: false,
);
}
/**
* Test set_response_data.
*/
public function test_set_response_data(): void {
$this->resetAfterTest();
$body = [
'id' => 'chatcmpl-123',
'fingerprint' => 'fp_44709d6fcb',
'generatedcontent' => 'This is the generated content',
'finishreason' => 'stop',
'prompttokens' => 9,
'completiontokens' => 12,
];
$actionresponse = new response_explain_text(
success: true,
);
$actionresponse->set_response_data($body);
$this->assertEquals($body['id'], $actionresponse->get_response_data()['id']);
$this->assertEquals($body['fingerprint'], $actionresponse->get_response_data()['fingerprint']);
$this->assertEquals($body['generatedcontent'], $actionresponse->get_response_data()['generatedcontent']);
$this->assertEquals($body['finishreason'], $actionresponse->get_response_data()['finishreason']);
$this->assertEquals($body['prompttokens'], $actionresponse->get_response_data()['prompttokens']);
$this->assertEquals($body['completiontokens'], $actionresponse->get_response_data()['completiontokens']);
}
}
+4
View File
@@ -19,6 +19,7 @@ namespace core_ai;
use core_ai\aiactions\generate_image;
use core_ai\aiactions\generate_text;
use core_ai\aiactions\summarise_text;
use core_ai\aiactions\explain_text;
use core_ai\aiactions\responses\response_generate_image;
/**
@@ -65,6 +66,7 @@ final class manager_test extends \advanced_testcase {
generate_text::class,
generate_image::class,
summarise_text::class,
explain_text::class,
], $actions);
}
@@ -262,6 +264,7 @@ final class manager_test extends \advanced_testcase {
$actions = [
generate_text::class,
summarise_text::class,
explain_text::class,
];
// Create two provider instances.
@@ -293,6 +296,7 @@ final class manager_test extends \advanced_testcase {
// Assert that there is only one provider for each action.
$this->assertCount(2, $providers[generate_text::class]);
$this->assertCount(2, $providers[summarise_text::class]);
$this->assertCount(2, $providers[explain_text::class]);
// Disable the generate text action for the Open AI provider.
$setresult = $manager->set_action_state(
+21
View File
@@ -24,6 +24,20 @@
*/
$string['acceptai'] = 'Accept and continue';
$string['action'] = 'Action';
$string['action_explain_text'] = 'Explain text';
$string['action_explain_text_desc'] = 'Explains the text content on a course page.';
$string['action_explain_text_instruction'] = 'You will receive a text input from the user. Your task is to explain the provided text. Follow these guidelines:
1. Elaborate: Expand on key ideas and concepts, ensuring the explanation adds meaningful depth and avoids restating the text verbatim.
2. Simplify: Break down complex terms or ideas into simpler components, making them easy to understand for a wide audience, including learners.
3. Provide Context: Explain why something happens, how it works, or what its purpose is. Include relevant examples or analogies to enhance understanding where appropriate.
4. Organise Logically: Structure your explanation to flow naturally, beginning with fundamental ideas before moving to finer details.
Important Instructions:
1. Return the summary in plain text only.
2. Do not include any markdown formatting, greetings, or platitudes.
3. Focus on clarity, conciseness, and accessibility.
Ensure the explanation is easy to read and effectively conveys the main points of the original text.';
$string['action_generate_image'] = 'Generate image';
$string['action_generate_image_desc'] = 'Generates an image based on a text prompt.';
$string['action_generate_text'] = 'Generate text';
@@ -85,6 +99,13 @@ $string['placementactionsettings'] = 'Actions';
$string['placementactionsettings_desc'] = 'The AI actions available for this placement.';
$string['placementsettings'] = 'Placement-specific settings';
$string['placementsettings_desc'] = 'These settings control how this AI placement connects to the AI service, and related operations.';
$string['privacy:metadata:ai_action_explain_text'] = 'A table storing the explain text requests made by users.';
$string['privacy:metadata:ai_action_explain_text:completiontoken'] = 'The completion tokens used to explain the text.';
$string['privacy:metadata:ai_action_explain_text:fingerprint'] = 'The unique hash representing the state/version of the model and content.';
$string['privacy:metadata:ai_action_explain_text:generatedcontent'] = 'The actual text generated by the AI model based on the input prompt.';
$string['privacy:metadata:ai_action_explain_text:prompt'] = 'The prompt for the explain text request.';
$string['privacy:metadata:ai_action_explain_text:prompttokens'] = 'The prompt tokens used to explain the text.';
$string['privacy:metadata:ai_action_explain_text:responseid'] = 'The ID of the response.';
$string['privacy:metadata:ai_action_generate_image'] = 'A table storing the image generation requests made by users.';
$string['privacy:metadata:ai_action_generate_image:aspectratio'] = 'The aspect ratio of the generated images.';
$string['privacy:metadata:ai_action_generate_image:numberimages'] = 'The number of images generated.';
+16 -1
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="lib/db" VERSION="20241124" COMMENT="XMLDB file for core Moodle tables"
<XMLDB PATH="lib/db" VERSION="20241205" COMMENT="XMLDB file for core Moodle tables"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
>
@@ -4973,6 +4973,21 @@
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="ai_action_explain_text" COMMENT="Stores specific data about explain text actions.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="prompt" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="The text from the user that was used to generate the text response"/>
<FIELD NAME="responseid" TYPE="char" LENGTH="128" NOTNULL="false" SEQUENCE="false" COMMENT="A unique identifier for the chat completion, returned by the AI."/>
<FIELD NAME="fingerprint" TYPE="char" LENGTH="128" NOTNULL="false" SEQUENCE="false" COMMENT="This fingerprint represents the backend configuration that the model runs with."/>
<FIELD NAME="generatedcontent" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="The contents of the generated message."/>
<FIELD NAME="finishreason" TYPE="char" LENGTH="128" NOTNULL="false" SEQUENCE="false" COMMENT="The reason the model stopped generating tokens."/>
<FIELD NAME="prompttokens" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Number of tokens in the prompt."/>
<FIELD NAME="completiontoken" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Number of tokens in the generated completion."/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="sms_messages" COMMENT="SMS Messages sent via the SMS subsystem">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+29
View File
@@ -1412,5 +1412,34 @@ function xmldb_main_upgrade($oldversion) {
upgrade_main_savepoint(true, 2025013100.01);
}
if ($oldversion < 2025022100.00) {
// Define table ai_action_explain_text to be created.
$table = new xmldb_table('ai_action_explain_text');
// Adding fields to table ai_action_explain_text.
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('prompt', XMLDB_TYPE_TEXT, null, null, null, null, null);
$table->add_field('responseid', XMLDB_TYPE_CHAR, '128', null, null, null, null);
$table->add_field('fingerprint', XMLDB_TYPE_CHAR, '128', null, null, null, null);
$table->add_field('generatedcontent', XMLDB_TYPE_TEXT, null, null, null, null, null);
$table->add_field('finishreason', XMLDB_TYPE_CHAR, '128', null, null, null, null);
$table->add_field('prompttokens', XMLDB_TYPE_INTEGER, '10', null, null, null, null);
$table->add_field('completiontoken', XMLDB_TYPE_INTEGER, '10', null, null, null, null);
// Adding keys to table ai_action_explain_text.
$table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
// Conditionally launch create table for ai_action_explain_text.
if (!$dbman->table_exists($table)) {
$dbman->create_table($table);
}
// Add explain action config to the AI providers.
upgrade_add_explain_action_to_ai_providers();
// Main savepoint reached.
upgrade_main_savepoint(true, 2025022100.00);
}
return true;
}
+40
View File
@@ -2026,3 +2026,43 @@ function upgrade_convert_ai_providers_to_instances() {
'action_summarise_text_systeminstruction'];
array_walk($openaisettings, static fn($setting) => unset_config($setting, 'aiprovider_openai'));
}
/**
* Upgrade script to get all current AI providers and update their action config to include explain.
*/
function upgrade_add_explain_action_to_ai_providers() {
global $DB;
$currentrecords = $DB->get_recordset('ai_providers');
foreach ($currentrecords as $currentrecord) {
$actionconfig = json_decode($currentrecord->actionconfig, true);
if ($currentrecord->provider === 'aiprovider_openai\provider') {
$explainconfig = [
'enabled' => true,
'settings' => [
'model' => 'gpt-4o',
'endpoint' => 'https://api.openai.com/v1/chat/completions',
'systeminstruction' => get_string('action_explain_text_instruction', 'core_ai'),
],
];
} else if ($currentrecord->provider === 'aiprovider_azureai\provider') {
$explainconfig = [
'enabled' => true,
'settings' => [
'deployment' => '',
'apiversion' => '2024-06-01',
'systeminstruction' => get_string('action_explain_text_instruction', 'core_ai'),
],
];
}
// Update the record with the changes.
if (!empty($explainconfig)) {
$actionconfig['core_ai\aiactions\explain_text'] = $explainconfig;
$currentrecord->actionconfig = json_encode($actionconfig);
$DB->update_record('ai_providers', $currentrecord);
}
}
$currentrecords->close();
}
@@ -1,3 +1,3 @@
define("tiny_aiplacement/generatetext",["exports","./textmodal","core/ajax","core/str","core/templates","core_ai/helper","./options","./textmark","./generatebase"],(function(_exports,_textmodal,_ajax,_str,_templates,_helper,_options,_textmark,_generatebase){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_textmodal=_interopRequireDefault(_textmodal),_ajax=_interopRequireDefault(_ajax),_templates=_interopRequireDefault(_templates),_helper=_interopRequireDefault(_helper),_textmark=_interopRequireDefault(_textmark),_generatebase=_interopRequireDefault(_generatebase);class GenerateText extends _generatebase.default{constructor(){var obj,key,value;super(...arguments),value={GENERATEBUTTON:()=>'[id="'.concat(this.editor.id,'_tiny_aiplacement_generatebutton"]'),PROMPTAREA:()=>'[id="'.concat(this.editor.id,'_tiny_aiplacement_textprompt"]'),RESPONSEWRAPPER:".tiny_aiplacement_textresponse",RESPONSEPLACEHOLDER:".tiny_aiplacement_textresponse_placeholder",GENERATEDRESPONSE:()=>'[id="'.concat(this.editor.id,'_tiny_aiplacement_textresponse"]'),INSERTBTN:'[data-action="inserter"]',BACKTBTN:'[data-action="back"]'},(key="SELECTORS")in(obj=this)?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value}getModalClass(){return _textmodal.default}handleContentModalClick(e,root){const actions={generate:()=>this.handleSubmit(root,e.target),inserter:()=>this.handleInsert(root,e.target),cancel:()=>this.modalObject.destroy(),back:()=>{this.modalObject.destroy(),this.displayContentModal()}},actionKey=Object.keys(actions).find((key=>e.target.closest('[data-action="'.concat(key,'"]'))));actionKey&&(e.preventDefault(),actions[actionKey]())}setupPromptArea(root){const generateBtn=root.querySelector(this.SELECTORS.GENERATEBUTTON()),promptArea=root.querySelector(this.SELECTORS.PROMPTAREA());promptArea.addEventListener("input",(()=>{generateBtn.disabled=""===promptArea.value.trim()}))}async handleSubmit(root,submitBtn){await this.displayLoading(root,submitBtn);const request={methodname:"aiplacement_editor_generate_text",args:this.getRequestArgs(root)};try{this.responseObj=await _ajax.default.call([request])[0],this.responseObj.error?this.handleGenerationError(root,submitBtn,""):(await this.displayGeneratedText(root),this.hideLoading(root,submitBtn))}catch(error){this.handleGenerationError(root,submitBtn,"")}}async handleInsert(root,submitBtn){await this.displayLoading(root,submitBtn);const generatedResponseDiv=root.querySelector(this.SELECTORS.GENERATEDRESPONSE()),wrappedEditedResponse=await _textmark.default.wrapEditedSections(this.responseObj.generatedcontent,generatedResponseDiv.value);this.responseObj.editedtext=_helper.default.replaceLineBreaks(wrappedEditedResponse);const formattedResponse=await _templates.default.render("tiny_aiplacement/textinsert",this.responseObj);this.editor.insertContent(formattedResponse),this.editor.execCommand("mceRepaint"),this.editor.windowManager.close(),this.modalObject.hide()}async handleGenerationError(root,submitBtn){let errorMessage=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"";errorMessage||(errorMessage=await(0,_str.getString)("errorgeneral","tiny_aiplacement")),this.modalObject.setBody(_templates.default.render("tiny_aiplacement/modalbodyerror",{errorMessage:errorMessage}));const backBtn=root.querySelector(this.SELECTORS.BACKTBTN),generateBtn=root.querySelector(this.SELECTORS.GENERATEBUTTON());backBtn.classList.remove("hidden"),generateBtn.classList.add("hidden"),this.hideLoading(root,submitBtn)}async displayGeneratedText(root){root.querySelector(this.SELECTORS.INSERTBTN).classList.remove("hidden");root.querySelector(this.SELECTORS.GENERATEDRESPONSE()).value=this.responseObj.generatedcontent;root.querySelector(this.SELECTORS.RESPONSEWRAPPER).classList.remove("hidden");root.querySelector(this.SELECTORS.RESPONSEPLACEHOLDER).classList.add("hidden")}getRequestArgs(root){return{contextid:(0,_options.getContextId)(this.editor),prompttext:root.querySelector(this.SELECTORS.PROMPTAREA()).value}}}return _exports.default=GenerateText,_exports.default}));
define("tiny_aiplacement/generatetext",["exports","./textmodal","core/ajax","core/str","core/templates","core_ai/helper","./options","./textmark","./generatebase"],(function(_exports,_textmodal,_ajax,_str,_templates,_helper,_options,_textmark,_generatebase){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_textmodal=_interopRequireDefault(_textmodal),_ajax=_interopRequireDefault(_ajax),_templates=_interopRequireDefault(_templates),_helper=_interopRequireDefault(_helper),_textmark=_interopRequireDefault(_textmark),_generatebase=_interopRequireDefault(_generatebase);class GenerateText extends _generatebase.default{constructor(){var obj,key,value;super(...arguments),value={GENERATEBUTTON:()=>'[id="'.concat(this.editor.id,'_tiny_aiplacement_generatebutton"]'),PROMPTAREA:()=>'[id="'.concat(this.editor.id,'_tiny_aiplacement_textprompt"]'),RESPONSEWRAPPER:".tiny_aiplacement_textresponse",RESPONSEPLACEHOLDER:".tiny_aiplacement_textresponse_placeholder",GENERATEDRESPONSE:()=>'[id="'.concat(this.editor.id,'_tiny_aiplacement_textresponse"]'),INSERTBTN:'[data-action="inserter"]',BACKTBTN:'[data-action="back"]'},(key="SELECTORS")in(obj=this)?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value}getModalClass(){return _textmodal.default}handleContentModalClick(e,root){const actions={generate:()=>this.handleSubmit(root,e.target),inserter:()=>this.handleInsert(root,e.target),cancel:()=>this.modalObject.destroy(),back:()=>{this.modalObject.destroy(),this.displayContentModal()}},actionKey=Object.keys(actions).find((key=>e.target.closest('[data-action="'.concat(key,'"]'))));actionKey&&(e.preventDefault(),actions[actionKey]())}setupPromptArea(root){const generateBtn=root.querySelector(this.SELECTORS.GENERATEBUTTON()),promptArea=root.querySelector(this.SELECTORS.PROMPTAREA());promptArea.addEventListener("input",(()=>{generateBtn.disabled=""===promptArea.value.trim()}))}async handleSubmit(root,submitBtn){await this.displayLoading(root,submitBtn);const request={methodname:"aiplacement_editor_generate_text",args:this.getRequestArgs(root)};try{this.responseObj=await _ajax.default.call([request])[0],this.responseObj.error?this.handleGenerationError(root,submitBtn,""):(await this.displayGeneratedText(root),this.hideLoading(root,submitBtn))}catch(error){this.handleGenerationError(root,submitBtn,"")}}async handleInsert(root,submitBtn){await this.displayLoading(root,submitBtn);const generatedResponseDiv=root.querySelector(this.SELECTORS.GENERATEDRESPONSE()),wrappedEditedResponse=await _textmark.default.wrapEditedSections(this.responseObj.generatedcontent,generatedResponseDiv.value);this.responseObj.editedtext=_helper.default.formatResponse(wrappedEditedResponse);const formattedResponse=await _templates.default.render("tiny_aiplacement/textinsert",this.responseObj);this.editor.insertContent(formattedResponse),this.editor.execCommand("mceRepaint"),this.editor.windowManager.close(),this.modalObject.hide()}async handleGenerationError(root,submitBtn){let errorMessage=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"";errorMessage||(errorMessage=await(0,_str.getString)("errorgeneral","tiny_aiplacement")),this.modalObject.setBody(_templates.default.render("tiny_aiplacement/modalbodyerror",{errorMessage:errorMessage}));const backBtn=root.querySelector(this.SELECTORS.BACKTBTN),generateBtn=root.querySelector(this.SELECTORS.GENERATEBUTTON());backBtn.classList.remove("hidden"),generateBtn.classList.add("hidden"),this.hideLoading(root,submitBtn)}async displayGeneratedText(root){root.querySelector(this.SELECTORS.INSERTBTN).classList.remove("hidden");root.querySelector(this.SELECTORS.GENERATEDRESPONSE()).value=this.responseObj.generatedcontent;root.querySelector(this.SELECTORS.RESPONSEWRAPPER).classList.remove("hidden");root.querySelector(this.SELECTORS.RESPONSEPLACEHOLDER).classList.add("hidden")}getRequestArgs(root){return{contextid:(0,_options.getContextId)(this.editor),prompttext:root.querySelector(this.SELECTORS.PROMPTAREA()).value}}}return _exports.default=GenerateText,_exports.default}));
//# sourceMappingURL=generatetext.min.js.map
File diff suppressed because one or more lines are too long
@@ -131,8 +131,8 @@ export default class GenerateText extends GenerateBase {
generatedResponseDiv.value)
;
// Replace double line breaks with <br> and with </p><p> for paragraphs.
this.responseObj.editedtext = AIHelper.replaceLineBreaks(wrappedEditedResponse);
// Replace double line breaks with <br> and with </p><p> for paragraphs and removed any markdown.
this.responseObj.editedtext = AIHelper.formatResponse(wrappedEditedResponse);
// Generate the HTML for the response.
const formattedResponse = await Templates.render('tiny_aiplacement/textinsert', this.responseObj);