Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions classes/completion/chat.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
147 changes: 147 additions & 0 deletions classes/external/answer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
<?php

namespace block_openai_chat\external;

use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_multiple_structure;
use core_external\external_value;

/**
* External API class.
*
* @package block_recentlyaccesseditems
* @copyright 2018 Victor Deniz <victor@moodle.com>
* @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,
'',
),
]);
}
}
103 changes: 103 additions & 0 deletions classes/output/mobile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php
// This file is part of Moodle - https://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 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")
];
}
}
46 changes: 46 additions & 0 deletions db/mobile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php
// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.

/**
* 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' ],
],
],
];
37 changes: 37 additions & 0 deletions db/services.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?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/>.

/**
* 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),
),
);
39 changes: 39 additions & 0 deletions mobile.js
Original file line number Diff line number Diff line change
@@ -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',
'<div class="openai_message user"><span>' + question + '</span></div>' +
'<ion-item class="chat-loading">' +
'<ion-spinner name="crescent"></ion-spinner>' +
'</ion-item>'
);
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 = '<span style="color: gray; margin-bottom: .2em">' + assistantname.innerHTML + '</span><div class="openai_message bot"><span><p>' + msg + '</p></span></div>';

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 = ""
})
Loading