From 26b8f1fbe533a196596bc966529901965f402230 Mon Sep 17 00:00:00 2001 From: Daniel Neis Araujo Date: Wed, 17 Sep 2025 16:53:55 -0300 Subject: [PATCH 1/4] Mobile support, first version --- classes/completion/chat.php | 2 + classes/external/answer.php | 147 ++++++++++++++++++++++++++++++++++++ classes/output/mobile.php | 105 ++++++++++++++++++++++++++ db/mobile.php | 46 +++++++++++ db/services.php | 37 +++++++++ mobile.js | 35 +++++++++ styles.css | 9 +++ templates/mobile.mustache | 50 ++++++++++++ version.php | 2 +- 9 files changed, 432 insertions(+), 1 deletion(-) create mode 100644 classes/external/answer.php create mode 100644 classes/output/mobile.php create mode 100644 db/mobile.php create mode 100644 db/services.php create mode 100644 mobile.js create mode 100644 templates/mobile.mustache 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..80f0a06 --- /dev/null +++ b/classes/output/mobile.php @@ -0,0 +1,105 @@ +. + +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, $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'); + + // 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]); + $content = new \stdClass; + $content->text = ' + + + '; + + $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', + 'contextid' => $context->id, + ]; + + 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..a456a4d --- /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' => 8, + ], + ], + ], + '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..727f025 --- /dev/null +++ b/mobile.js @@ -0,0 +1,35 @@ +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 newHtml = '

' + 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; +}; diff --git a/styles.css b/styles.css index 65bacc0..81372bc 100755 --- a/styles.css +++ b/styles.css @@ -92,6 +92,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 +135,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..7422ba3 --- /dev/null +++ b/templates/mobile.mustache @@ -0,0 +1,50 @@ +{{! + 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 + } +}} +{{=<% %>=}} +

<%#str%> openai_chat, block_openai_chat <%/str%>

+
+
+
+ +
+ <%#str%>askaquestion, 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'; From e42e29775ea9c9e19acd5133d87ccb90d134fd1b Mon Sep 17 00:00:00 2001 From: Daniel Neis Araujo Date: Tue, 30 Sep 2025 10:08:34 -0300 Subject: [PATCH 2/4] Mobile: refresh button --- mobile.js | 3 +++ templates/mobile.mustache | 11 +++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/mobile.js b/mobile.js index 727f025..2baa53b 100644 --- a/mobile.js +++ b/mobile.js @@ -33,3 +33,6 @@ this.update_history = function(msg) { 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/templates/mobile.mustache b/templates/mobile.mustache index 7422ba3..9a4df94 100644 --- a/templates/mobile.mustache +++ b/templates/mobile.mustache @@ -28,6 +28,7 @@ } }} {{=<% %>=}} +

<%#str%> openai_chat, block_openai_chat <%/str%>

@@ -42,9 +43,11 @@ (onSuccess)="this.answer($event)" (onError)="this.answer($event)" [useOtherDataForWS]="['message']" - ><%#str%>askaquestion, block_openai_chat<%/str%> + ><%#str%>askaquestion, block_openai_chat<%/str%> - + + <%#str%>new_chat, block_openai_chat <%/str%> + +
From 006e3416047b0517b8d9a0ca15512945c5698a1c Mon Sep 17 00:00:00 2001 From: Daniel Neis Araujo Date: Tue, 30 Sep 2025 10:30:15 -0300 Subject: [PATCH 3/4] Mobile: display assistant name --- classes/output/mobile.php | 19 +++---------------- mobile.js | 3 ++- templates/mobile.mustache | 1 + 3 files changed, 6 insertions(+), 17 deletions(-) diff --git a/classes/output/mobile.php b/classes/output/mobile.php index 80f0a06..3c46553 100644 --- a/classes/output/mobile.php +++ b/classes/output/mobile.php @@ -67,28 +67,15 @@ public static function get_block_content($args) { } $assistantname = format_string($assistantname, true, ['context' => $context]); $username = format_string($username, true, ['context' => $context]); - $content = new \stdClass; - $content->text = ' - - - '; $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, ]; diff --git a/mobile.js b/mobile.js index 2baa53b..c0559f1 100644 --- a/mobile.js +++ b/mobile.js @@ -24,7 +24,8 @@ this.update_history = function(msg) { let container = document.querySelector('#openai_chat_log'); container.scrollTop = lq.offsetTop; } - let newHtml = '

' + msg + '

'; + let assistantname = document.getElementById('openai_assistantname'); + let newHtml = '' + assistantname.innerHTML + '

' + msg + '

'; document.querySelector('.chat-loading').remove(); document.getElementById('openai_chat_log').insertAdjacentHTML('beforeend', newHtml); diff --git a/templates/mobile.mustache b/templates/mobile.mustache index 9a4df94..36371c3 100644 --- a/templates/mobile.mustache +++ b/templates/mobile.mustache @@ -50,4 +50,5 @@ <%#str%>new_chat, block_openai_chat <%/str%> + From 9a4fea270ddd9b0b37a210d2d961d0261369f2ea Mon Sep 17 00:00:00 2001 From: Daniel Neis Araujo Date: Mon, 6 Oct 2025 17:00:54 -0300 Subject: [PATCH 4/4] Use custom title and remove space between textarea and go button --- classes/output/mobile.php | 13 ++++++++++++- db/mobile.php | 2 +- styles.css | 3 ++- templates/mobile.mustache | 31 +++++++++++++++++++------------ 4 files changed, 34 insertions(+), 15 deletions(-) diff --git a/classes/output/mobile.php b/classes/output/mobile.php index 3c46553..3264f33 100644 --- a/classes/output/mobile.php +++ b/classes/output/mobile.php @@ -32,7 +32,7 @@ class mobile { * @param array $args Data provided by standard CoreBlockDelegate. */ public static function get_block_content($args) { - global $CFG, $OUTPUT; + global $CFG, $DB, $OUTPUT; $context = \core\context\block::instance($args['blockid']); @@ -60,6 +60,16 @@ public static function get_block_content($args) { $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; @@ -77,6 +87,7 @@ public static function get_block_content($args) { 'assistantname' => $assistantname, 'showlabelscss' => $showlabelscss, 'contextid' => $context->id, + 'title' => $title, ]; return [ diff --git a/db/mobile.php b/db/mobile.php index a456a4d..d69a212 100644 --- a/db/mobile.php +++ b/db/mobile.php @@ -35,7 +35,7 @@ ], 'styles' => [ 'url' => $CFG->wwwroot . '/blocks/openai_chat/styles.css', - 'version' => 8, + 'version' => 11, ], ], ], diff --git a/styles.css b/styles.css index 81372bc..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; } diff --git a/templates/mobile.mustache b/templates/mobile.mustache index 36371c3..2e7426c 100644 --- a/templates/mobile.mustache +++ b/templates/mobile.mustache @@ -29,22 +29,29 @@ }} {{=<% %>=}}
-

<%#str%> openai_chat, block_openai_chat <%/str%>

+

<% title %>

- + + + <%#str%>askaquestion, block_openai_chat<%/str%> +
- <%#str%>askaquestion, block_openai_chat<%/str%> - <%#str%>new_chat, block_openai_chat <%/str%>