diff --git a/classes/completion/chat.php b/classes/completion/chat.php index 99b20ad..d107cd1 100755 --- a/classes/completion/chat.php +++ b/classes/completion/chat.php @@ -27,6 +27,8 @@ use block_openai_chat\completion; defined('MOODLE_INTERNAL') || die; +require_once($CFG->libdir.'/filelib.php'); + class chat extends \block_openai_chat\completion { public function __construct($model, $message, $history, $block_settings, $thread_id = null) { diff --git a/classes/external/answer.php b/classes/external/answer.php new file mode 100644 index 0000000..81ca09d --- /dev/null +++ b/classes/external/answer.php @@ -0,0 +1,147 @@ + + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class answer extends external_api { + + /** + * Returns description of method parameters + * @return external_function_parameters + */ + public static function execute_parameters() { + return new external_function_parameters( + array( + 'contextid' => new external_value(PARAM_INT, 'the context of the block', VALUE_REQUIRED), + 'message' => new external_value(PARAM_RAW, 'The message from user', VALUE_REQUIRED), + ) + ); + } + + /** + * Get last accessed items by the logged user (activities or resources). + * + * @param int $contextid Context Id of the block + * @param string $message Message from user + * @return array List of items + */ + public static function execute(int $contextid, string $message) { + global $DB; + + // Parameter validation. + [ + 'contextid' => $contextid, + 'message' => $message, + ] = self::validate_parameters(self::execute_parameters(), [ + 'contextid' => $contextid, + 'message' => $message, + ]); + // 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); + + $instance_record = $DB->get_record('block_instances', ['blockname' => 'openai_chat', 'id' => $context->instanceid], '*'); + $instance = block_instance('openai_chat', $instance_record); + + $block_settings = []; + $setting_names = [ + 'sourceoftruth', + 'prompt', + 'instructions', + 'username', + 'assistantname', + 'apikey', + 'model', + 'temperature', + 'maxlength', + 'topp', + 'frequency', + 'presence', + 'assistant' + ]; + foreach ($setting_names as $setting) { + if ($instance->config && property_exists($instance->config, $setting)) { + $block_settings[$setting] = $instance->config->$setting ? $instance->config->$setting : ""; + } else { + $block_settings[$setting] = ""; + } + } + + $engine_class; + $model = get_config('block_openai_chat', 'model'); + $api_type = get_config('block_openai_chat', 'type'); + $engine_class = "\block_openai_chat\completion\\$api_type"; + + $completion = new $engine_class(...[$model, $message, $history, $block_settings, $thread_id]); + $response = $completion->create_completion($context); + + // Format the markdown of each completion message into HTML. + $response = format_text($response["message"], FORMAT_MARKDOWN, ['context' => $context]); + + // Return the response. + return [ + 'success' => true, // TODO + 'generatedcontent' => $response, + 'finishreason' => '', // TODO + 'error' => '', // TODO + 'timecreated' => 0, // TODO + 'message' => $message, + ]; + } + + /** + * Generate content return value. + * + * @return external_function_parameters + */ + 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, + ), + 'message' => 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', + ), + 'error' => new external_value( + PARAM_TEXT, + 'Error message if any', + VALUE_DEFAULT, + '', + ), + ]); + } +} diff --git a/classes/output/mobile.php b/classes/output/mobile.php new file mode 100644 index 0000000..3264f33 --- /dev/null +++ b/classes/output/mobile.php @@ -0,0 +1,103 @@ +. + +namespace block_openai_chat\output; + +use context_course; + +/** + * Callbacks class for mobile app. + * + * @package block_openai_chat + * @copyright 2025 Daniel Neis Araujo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mobile { + + /** + * Callback to render the block contents on mobile app. + * @param array $args Data provided by standard CoreBlockDelegate. + */ + public static function get_block_content($args) { + global $CFG, $DB, $OUTPUT; + + $context = \core\context\block::instance($args['blockid']); + + $config = get_config('block_openai_chat'); + + $persistconvo = get_config('block_openai_chat', 'persistconvo'); + if (!empty($config)) { + $persistconvo = (property_exists($config, 'persistconvo') && get_config('block_openai_chat', 'allowinstancesettings')) ? $config->persistconvo : $persistconvo; + } + + // Determine if name labels should be shown. + $showlabelscss = ''; + if (!empty($config) && empty($config->showlabels)) { + $showlabelscss = ' + .openai_message:before { + display: none; + } + .openai_message { + margin-bottom: 0.5rem; + } + '; + } + + // First, fetch the global settings for these (and the defaults if not set) + $assistantname = get_config('block_openai_chat', 'assistantname') ? get_config('block_openai_chat', 'assistantname') : get_string('defaultassistantname', 'block_openai_chat'); + $username = get_config('block_openai_chat', 'username') ? get_config('block_openai_chat', 'username') : get_string('defaultusername', 'block_openai_chat'); + + $title = get_string('openai_chat', 'block_openai_chat'); + $configdata = $DB->get_field('block_instances', 'configdata', ['id' => $context->instanceid]); + if (!empty($configdata)) { + $cfg = base64_decode($configdata); + $cfg = unserialize_object($cfg); + if (!empty($cfg->title)) { + $title = $cfg->title; + } + } + + // Then, override with local settings if available + if (!empty($config)) { + $assistantname = (property_exists($config, 'assistantname') && $config->assistantname) ? $config->assistantname : $assistantname; + $username = (property_exists($config, 'username') && $config->username) ? $config->username : $username; + } + $assistantname = format_string($assistantname, true, ['context' => $context]); + $username = format_string($username, true, ['context' => $context]); + + $contextdata = [ + 'logging_enabled' => get_config('block_openai_chat', 'logging'), + 'pix_popout' => '/blocks/openai_chat/pix/arrow-up-right-from-square.svg', + 'pix_arrow_right' => '/blocks/openai_chat/pix/arrow-right.svg', + 'pix_refresh' => '/blocks/openai_chat/pix/refresh.svg', + 'username' => $username, + 'assistantname' => $assistantname, + 'showlabelscss' => $showlabelscss, + 'contextid' => $context->id, + 'title' => $title, + ]; + + return [ + 'templates' => [ + [ + 'id' => 'main', + 'html' => $OUTPUT->render_from_template('block_openai_chat/mobile', $contextdata) + ] + ], + 'javascript' => file_get_contents("{$CFG->dirroot}/blocks/openai_chat/mobile.js") + ]; + } +} diff --git a/db/mobile.php b/db/mobile.php new file mode 100644 index 0000000..d69a212 --- /dev/null +++ b/db/mobile.php @@ -0,0 +1,46 @@ +. + +/** + * Mobile App addons definition. + * + * @package block_openai_chat + * @copyright 2025 Daniel Neis Araujo + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +global $CFG; + +$addons = [ + 'block_openai_chat' => [ + 'handlers' => [ + 'completionlevels' => [ + 'delegate' => 'CoreBlockDelegate', + 'method' => 'get_block_content', + 'displaydata' => [ + 'class' => 'block block_openai_chat', + ], + 'styles' => [ + 'url' => $CFG->wwwroot . '/blocks/openai_chat/styles.css', + 'version' => 11, + ], + ], + ], + 'lang' => [ + [ 'pluginname', 'block_openai_chat' ], + ], + ], +]; diff --git a/db/services.php b/db/services.php new file mode 100644 index 0000000..71f7ac7 --- /dev/null +++ b/db/services.php @@ -0,0 +1,37 @@ +. + +/** + * File description. + * + * @package block_openai_chat + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$functions = array( + + 'block_openai_chat_answer' => array( + 'classpath' => 'blocks/openai_chat/classes/external/answer.php', + 'classname' => 'block_openai_chat\external\answer', + 'methodname' => 'execute', + 'description' => 'Get answer.', + 'type' => 'read', + 'ajax' => true, + 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), + ), +); diff --git a/mobile.js b/mobile.js new file mode 100644 index 0000000..c0559f1 --- /dev/null +++ b/mobile.js @@ -0,0 +1,39 @@ +this.answer = function(result) { + var msg = ''; + if (result.error == "") { + msg = result.generatedcontent; + } else { + msg = 'error: ' + result.error; + } + this.update_history(msg); +}; + +this.update_history = function(msg) { + let question = document.getElementById('openai_input').value; + if (question == '') { + } else { + document.getElementById('openai_input').value = ''; + document.getElementById('openai_chat_log').insertAdjacentHTML( + 'beforeend', + '
' + question + '
' + + '' + + '' + + '' + ); + let lq = document.querySelector('#openai_chat_log > :last-child'); + let container = document.querySelector('#openai_chat_log'); + container.scrollTop = lq.offsetTop; + } + let assistantname = document.getElementById('openai_assistantname'); + let newHtml = '' + assistantname.innerHTML + '

' + msg + '

'; + + document.querySelector('.chat-loading').remove(); + document.getElementById('openai_chat_log').insertAdjacentHTML('beforeend', newHtml); + + let container = document.querySelector('#openai_chat_log'); + let lastMessage = document.querySelector('#openai_chat_log div:last-child'); + container.scrollTop = lastMessage.offsetTop; +}; +document.querySelector(`.block_openai_chat #refresh`).addEventListener('click', e => { + document.querySelector(`#openai_chat_log`).innerHTML = "" +}) diff --git a/styles.css b/styles.css index 65bacc0..5e68cd6 100755 --- a/styles.css +++ b/styles.css @@ -28,7 +28,8 @@ filter: brightness(0.8); } -.block_openai_chat #control_bar #go { +.block_openai_chat #control_bar #go, +#control_bar #go button.button-native { border-radius: 0 0.5rem 0.5rem 0; } @@ -92,6 +93,10 @@ transition: background 0.4s ease; } +ng-component textarea#openai_input { + height: 1em; +} + .block_openai_chat #openai_input.error { border: 1px solid red; } @@ -131,6 +136,11 @@ color: var(--white); } +ion-card.block_openai_chat .openai_message.bot { + background-color: #0f6cbf !important; + color: white !important; +} + .block_openai_chat .openai_message.loading { animation: block_openai_chat_thinking 1s ease infinite; } diff --git a/templates/mobile.mustache b/templates/mobile.mustache new file mode 100644 index 0000000..2e7426c --- /dev/null +++ b/templates/mobile.mustache @@ -0,0 +1,61 @@ +{{! + 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 . +}} +{{! + @template block_openai_chat/mobile + + This template renders the block on mobile. + + Example context (json): + { + "logging_enabled": boolean, true if logging is enabled in the plugin settings + "pix_popout": relative url of the popout pix to the root of moodle + "pix_arrow_right": relative url of the arrow right pix to the root of moodle + "pix_refresh": relative url of the refresh pix to the root of moodle + } +}} +{{=<% %>=}} +
+

<% title %>

+
+
+
+ + + <%#str%>askaquestion, block_openai_chat<%/str%> + +
+ + <%#str%>new_chat, block_openai_chat <%/str%> + +
+ +
diff --git a/version.php b/version.php index 9deb834..5c3876e 100755 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'block_openai_chat'; -$plugin->version = 2025021700; +$plugin->version = 2025091800; $plugin->requires = 2022041600; $plugin->maturity = MATURITY_STABLE; $plugin->release = '3.0.1';