Skip to content

Commit 26b8f1f

Browse files
committed
Mobile support, first version
1 parent ccddb13 commit 26b8f1f

File tree

9 files changed

+432
-1
lines changed

9 files changed

+432
-1
lines changed

classes/completion/chat.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
use block_openai_chat\completion;
2828
defined('MOODLE_INTERNAL') || die;
2929

30+
require_once($CFG->libdir.'/filelib.php');
31+
3032
class chat extends \block_openai_chat\completion {
3133

3234
public function __construct($model, $message, $history, $block_settings, $thread_id = null) {

classes/external/answer.php

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
<?php
2+
3+
namespace block_openai_chat\external;
4+
5+
use core_external\external_api;
6+
use core_external\external_function_parameters;
7+
use core_external\external_multiple_structure;
8+
use core_external\external_value;
9+
10+
/**
11+
* External API class.
12+
*
13+
* @package block_recentlyaccesseditems
14+
* @copyright 2018 Victor Deniz <victor@moodle.com>
15+
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
16+
*/
17+
class answer extends external_api {
18+
19+
/**
20+
* Returns description of method parameters
21+
* @return external_function_parameters
22+
*/
23+
public static function execute_parameters() {
24+
return new external_function_parameters(
25+
array(
26+
'contextid' => new external_value(PARAM_INT, 'the context of the block', VALUE_REQUIRED),
27+
'message' => new external_value(PARAM_RAW, 'The message from user', VALUE_REQUIRED),
28+
)
29+
);
30+
}
31+
32+
/**
33+
* Get last accessed items by the logged user (activities or resources).
34+
*
35+
* @param int $contextid Context Id of the block
36+
* @param string $message Message from user
37+
* @return array List of items
38+
*/
39+
public static function execute(int $contextid, string $message) {
40+
global $DB;
41+
42+
// Parameter validation.
43+
[
44+
'contextid' => $contextid,
45+
'message' => $message,
46+
] = self::validate_parameters(self::execute_parameters(), [
47+
'contextid' => $contextid,
48+
'message' => $message,
49+
]);
50+
// Context validation and permission check.
51+
// Get the context from the passed in ID.
52+
$context = \context::instance_by_id($contextid);
53+
54+
// Check the user has permission to use the AI service.
55+
self::validate_context($context);
56+
57+
$instance_record = $DB->get_record('block_instances', ['blockname' => 'openai_chat', 'id' => $context->instanceid], '*');
58+
$instance = block_instance('openai_chat', $instance_record);
59+
60+
$block_settings = [];
61+
$setting_names = [
62+
'sourceoftruth',
63+
'prompt',
64+
'instructions',
65+
'username',
66+
'assistantname',
67+
'apikey',
68+
'model',
69+
'temperature',
70+
'maxlength',
71+
'topp',
72+
'frequency',
73+
'presence',
74+
'assistant'
75+
];
76+
foreach ($setting_names as $setting) {
77+
if ($instance->config && property_exists($instance->config, $setting)) {
78+
$block_settings[$setting] = $instance->config->$setting ? $instance->config->$setting : "";
79+
} else {
80+
$block_settings[$setting] = "";
81+
}
82+
}
83+
84+
$engine_class;
85+
$model = get_config('block_openai_chat', 'model');
86+
$api_type = get_config('block_openai_chat', 'type');
87+
$engine_class = "\block_openai_chat\completion\\$api_type";
88+
89+
$completion = new $engine_class(...[$model, $message, $history, $block_settings, $thread_id]);
90+
$response = $completion->create_completion($context);
91+
92+
// Format the markdown of each completion message into HTML.
93+
$response = format_text($response["message"], FORMAT_MARKDOWN, ['context' => $context]);
94+
95+
// Return the response.
96+
return [
97+
'success' => true, // TODO
98+
'generatedcontent' => $response,
99+
'finishreason' => '', // TODO
100+
'error' => '', // TODO
101+
'timecreated' => 0, // TODO
102+
'message' => $message,
103+
];
104+
}
105+
106+
/**
107+
* Generate content return value.
108+
*
109+
* @return external_function_parameters
110+
*/
111+
public static function execute_returns(): external_function_parameters {
112+
return new external_function_parameters([
113+
'success' => new external_value(
114+
PARAM_BOOL,
115+
'Was the request successful',
116+
VALUE_REQUIRED
117+
),
118+
'timecreated' => new external_value(
119+
PARAM_INT,
120+
'The time the request was created',
121+
VALUE_REQUIRED,
122+
),
123+
'message' => new external_value(
124+
PARAM_RAW,
125+
'The prompt text for the AI service',
126+
VALUE_REQUIRED,
127+
),
128+
'generatedcontent' => new external_value(
129+
PARAM_RAW,
130+
'The text generated by AI.',
131+
VALUE_DEFAULT,
132+
),
133+
'finishreason' => new external_value(
134+
PARAM_ALPHA,
135+
'The reason generation was stopped',
136+
VALUE_DEFAULT,
137+
'stop',
138+
),
139+
'error' => new external_value(
140+
PARAM_TEXT,
141+
'Error message if any',
142+
VALUE_DEFAULT,
143+
'',
144+
),
145+
]);
146+
}
147+
}

classes/output/mobile.php

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
// This file is part of Moodle - https://moodle.org/
3+
//
4+
// Moodle is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// Moodle is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU General Public License
15+
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16+
17+
namespace block_openai_chat\output;
18+
19+
use context_course;
20+
21+
/**
22+
* Callbacks class for mobile app.
23+
*
24+
* @package block_openai_chat
25+
* @copyright 2025 Daniel Neis Araujo
26+
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27+
*/
28+
class mobile {
29+
30+
/**
31+
* Callback to render the block contents on mobile app.
32+
* @param array $args Data provided by standard CoreBlockDelegate.
33+
*/
34+
public static function get_block_content($args) {
35+
global $CFG, $OUTPUT;
36+
37+
$context = \core\context\block::instance($args['blockid']);
38+
39+
$config = get_config('block_openai_chat');
40+
41+
$persistconvo = get_config('block_openai_chat', 'persistconvo');
42+
if (!empty($config)) {
43+
$persistconvo = (property_exists($config, 'persistconvo') && get_config('block_openai_chat', 'allowinstancesettings')) ? $config->persistconvo : $persistconvo;
44+
}
45+
46+
// Determine if name labels should be shown.
47+
$showlabelscss = '';
48+
if (!empty($config) && empty($config->showlabels)) {
49+
$showlabelscss = '
50+
.openai_message:before {
51+
display: none;
52+
}
53+
.openai_message {
54+
margin-bottom: 0.5rem;
55+
}
56+
';
57+
}
58+
59+
// First, fetch the global settings for these (and the defaults if not set)
60+
$assistantname = get_config('block_openai_chat', 'assistantname') ? get_config('block_openai_chat', 'assistantname') : get_string('defaultassistantname', 'block_openai_chat');
61+
$username = get_config('block_openai_chat', 'username') ? get_config('block_openai_chat', 'username') : get_string('defaultusername', 'block_openai_chat');
62+
63+
// Then, override with local settings if available
64+
if (!empty($config)) {
65+
$assistantname = (property_exists($config, 'assistantname') && $config->assistantname) ? $config->assistantname : $assistantname;
66+
$username = (property_exists($config, 'username') && $config->username) ? $config->username : $username;
67+
}
68+
$assistantname = format_string($assistantname, true, ['context' => $context]);
69+
$username = format_string($username, true, ['context' => $context]);
70+
$content = new \stdClass;
71+
$content->text = '
72+
<script>
73+
var assistantName = "' . $assistantname . '";
74+
var userName = "' . $username . '";
75+
</script>
76+
77+
<style>
78+
' . $showlabelscss . '
79+
.openai_message.user:before {
80+
content: "' . $username . '";
81+
}
82+
.openai_message.bot:before {
83+
content: "' . $assistantname . '";
84+
}
85+
</style>';
86+
87+
$contextdata = [
88+
'logging_enabled' => get_config('block_openai_chat', 'logging'),
89+
'pix_popout' => '/blocks/openai_chat/pix/arrow-up-right-from-square.svg',
90+
'pix_arrow_right' => '/blocks/openai_chat/pix/arrow-right.svg',
91+
'pix_refresh' => '/blocks/openai_chat/pix/refresh.svg',
92+
'contextid' => $context->id,
93+
];
94+
95+
return [
96+
'templates' => [
97+
[
98+
'id' => 'main',
99+
'html' => $OUTPUT->render_from_template('block_openai_chat/mobile', $contextdata)
100+
]
101+
],
102+
'javascript' => file_get_contents("{$CFG->dirroot}/blocks/openai_chat/mobile.js")
103+
];
104+
}
105+
}

db/mobile.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
// This file is part of Moodle - https://moodle.org/
3+
//
4+
// Moodle is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// Moodle is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU General Public License
15+
// along with Moodle. If not, see <https://www.gnu.org/licenses/>.
16+
17+
/**
18+
* Mobile App addons definition.
19+
*
20+
* @package block_openai_chat
21+
* @copyright 2025 Daniel Neis Araujo
22+
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23+
*/
24+
25+
global $CFG;
26+
27+
$addons = [
28+
'block_openai_chat' => [
29+
'handlers' => [
30+
'completionlevels' => [
31+
'delegate' => 'CoreBlockDelegate',
32+
'method' => 'get_block_content',
33+
'displaydata' => [
34+
'class' => 'block block_openai_chat',
35+
],
36+
'styles' => [
37+
'url' => $CFG->wwwroot . '/blocks/openai_chat/styles.css',
38+
'version' => 8,
39+
],
40+
],
41+
],
42+
'lang' => [
43+
[ 'pluginname', 'block_openai_chat' ],
44+
],
45+
],
46+
];

db/services.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
// This file is part of Moodle - http://moodle.org/
3+
//
4+
// Moodle is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// Moodle is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU General Public License
15+
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16+
17+
/**
18+
* File description.
19+
*
20+
* @package block_openai_chat
21+
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22+
*/
23+
24+
defined('MOODLE_INTERNAL') || die();
25+
26+
$functions = array(
27+
28+
'block_openai_chat_answer' => array(
29+
'classpath' => 'blocks/openai_chat/classes/external/answer.php',
30+
'classname' => 'block_openai_chat\external\answer',
31+
'methodname' => 'execute',
32+
'description' => 'Get answer.',
33+
'type' => 'read',
34+
'ajax' => true,
35+
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
36+
),
37+
);

mobile.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
this.answer = function(result) {
2+
var msg = '';
3+
if (result.error == "") {
4+
msg = result.generatedcontent;
5+
} else {
6+
msg = 'error: ' + result.error;
7+
}
8+
this.update_history(msg);
9+
};
10+
11+
this.update_history = function(msg) {
12+
let question = document.getElementById('openai_input').value;
13+
if (question == '') {
14+
} else {
15+
document.getElementById('openai_input').value = '';
16+
document.getElementById('openai_chat_log').insertAdjacentHTML(
17+
'beforeend',
18+
'<div class="openai_message user"><span>' + question + '</span></div>' +
19+
'<ion-item class="chat-loading">' +
20+
'<ion-spinner name="crescent"></ion-spinner>' +
21+
'</ion-item>'
22+
);
23+
let lq = document.querySelector('#openai_chat_log > :last-child');
24+
let container = document.querySelector('#openai_chat_log');
25+
container.scrollTop = lq.offsetTop;
26+
}
27+
let newHtml = '<div class="openai_message bot"><span><p>' + msg + '</p></span></div>';
28+
29+
document.querySelector('.chat-loading').remove();
30+
document.getElementById('openai_chat_log').insertAdjacentHTML('beforeend', newHtml);
31+
32+
let container = document.querySelector('#openai_chat_log');
33+
let lastMessage = document.querySelector('#openai_chat_log div:last-child');
34+
container.scrollTop = lastMessage.offsetTop;
35+
};

0 commit comments

Comments
 (0)