Merge branch 'MDL-82942-main' of https://github.com/davewoloszyn/moodle
This commit is contained in:
@@ -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
|
||||
Vendored
+1
-1
@@ -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 @@
|
||||
{"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"}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* It’s responsible for performing any setup tasks,
|
||||
* such as getting additional data from the database etc.
|
||||
*
|
||||
* @param int $contextid The context id the action was created in.
|
||||
* @param int $userid The user id making the request.
|
||||
* @param string $prompttext The prompt text used to generate the image.
|
||||
*/
|
||||
public function __construct(
|
||||
int $contextid,
|
||||
/** @var int The user id requesting the action. */
|
||||
protected int $userid,
|
||||
/** @var string The prompt text used to generate the summary text */
|
||||
protected string $prompttext,
|
||||
) {
|
||||
parent::__construct($contextid);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function store(response_base $response): int {
|
||||
global $DB;
|
||||
|
||||
$responsearr = $response->get_response_data();
|
||||
|
||||
$record = new \stdClass();
|
||||
$record->prompt = $this->prompttext;
|
||||
$record->responseid = $responsearr['id']; // Can be null.
|
||||
$record->fingerprint = $responsearr['fingerprint']; // Can be null.
|
||||
$record->generatedcontent = $responsearr['generatedcontent']; // Can be null.
|
||||
$record->finishreason = $responsearr['finishreason']; // Can be null.
|
||||
$record->prompttokens = $responsearr['prompttokens']; // Can be null.
|
||||
$record->completiontoken = $responsearr['completiontokens']; // Can be null.
|
||||
|
||||
return $DB->insert_record($this->get_tablename(), $record);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
@@ -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"}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.';
|
||||
|
||||
@@ -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"
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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.';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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.';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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"/>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user