Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
TobiGa committed Jan 24, 2025
1 parent 0c3ce55 commit 45e1929
Show file tree
Hide file tree
Showing 14 changed files with 460 additions and 8 deletions.
2 changes: 1 addition & 1 deletion amd/build/dialog.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion amd/build/dialog.min.js.map

Large diffs are not rendered by default.

50 changes: 46 additions & 4 deletions amd/src/dialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as externalServices from 'block_ai_chat/webservices';
import Templates from 'core/templates';
import {alert as displayAlert, exception as displayException, deleteCancelPromise} from 'core/notification';
import ModalEvents from 'core/modal_events';
import ModalForm from 'core_form/modalform';
import * as helper from 'block_ai_chat/helper';
import * as manager from 'block_ai_chat/ai_manager';
import {getString} from 'core/str';
Expand All @@ -25,6 +26,8 @@ let strHistory;
let strNewDialog;
let strToday;
let strYesterday;
let strDefinePersona;
let personaForm = {};
let badge;
let viewmode;
let modalopen = false;
Expand All @@ -38,7 +41,7 @@ let conversation = {
let allConversations = [];
// Userid.
let userid = 0;
// Course context id.
// Block context id.
let contextid = 0;
// First load.
let firstLoad = true;
Expand Down Expand Up @@ -92,6 +95,7 @@ export const init = async(params) => {
contextid = params.contextid;
strNewDialog = params.new;
strHistory = params.history;
strDefinePersona = params.persona;
badge = params.badge;
// Disable bdage.
badge = false;
Expand All @@ -101,7 +105,7 @@ export const init = async(params) => {
tenantConfig = aiConfig;
chatConfig = aiConfig.purposes.find(p => p.purpose === "chat");

// Build modal.
// Build chat dialog modal.
modal = await DialogModal.create({
templateContext: {
title: strNewDialog,
Expand Down Expand Up @@ -142,6 +146,42 @@ export const init = async(params) => {
if (window.innerWidth <= 576) {
setView(VIEW_OPENFULL);
}

// Add a dynamic form to add a systemprompt/persona to a block instance.
personaForm = new ModalForm({
// Set formclass, depending on component.
formClass: "block_ai_chat\\form\\persona_form",
args: {
contextid: contextid,
},
modalConfig: {title: strDefinePersona},
});

// In addition to menu, attach listener to the block prompt button to call the persona modal.
let promptbutton = document.getElementById('ai_chat_prompt');
if (promptbutton) {
promptbutton.addEventListener('mousedown', async(e) => {
e.preventDefault();
personaForm.show();
setTimeout(
() => {
const inputprompts = document.querySelector('input[name="prompts"]');
const prompts = JSON.parse(inputprompts.value);
const select = document.querySelector('select[name="name"]');
const textarea = document.querySelector('textarea[name="prompt"]');
select.addEventListener('change', (event) => {
let selectedValue = event.target.value;
if (typeof prompts[selectedValue] !== 'undefined') {
textarea.value = prompts[selectedValue];
} else {
textarea.value = '';
}
});
}, 1200
);
});
}

};

/**
Expand Down Expand Up @@ -173,7 +213,6 @@ async function showModal() {
await getConversations();

// Show conversation.
// Todo - Evtl. noch firstload verschönern, spinner für header und content z.b.
showConversation();

// Get conversationcontext message limit.
Expand All @@ -194,6 +233,10 @@ async function showModal() {
btnShowHistory.addEventListener('click', () => {
showHistory();
});
const btnDefinePersona = document.getElementById('block_ai_chat_define_persona');
btnDefinePersona.addEventListener('click', () => {
personaForm.show();
});
// Views.
const btnChatwindow = document.getElementById(VIEW_CHATWINDOW);
btnChatwindow.addEventListener('click', () => {
Expand Down Expand Up @@ -657,7 +700,6 @@ const addTextareaListener = (textarea) => {
* @param {*} event
*/
const textareaOnKeydown = (event) => {
// TODO check for mobile devices.
if (event.key === 'Enter' && !aiAtWork && !event.shiftKey) {
aiAtWork = true;
enterQuestion(event.target.value);
Expand Down
7 changes: 6 additions & 1 deletion block_ai_chat.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ public function get_content(): stdClass {
$aioutput = $this->page->get_renderer('block_ai_chat');
$this->content->text = $aioutput->render_ai_chat_content($this);

if ($this->page->user_is_editing()) {
$this->content->text .= get_string('editsystemprompt', 'block_ai_chat');
return $this->content;
}

return $this->content;
}

Expand Down Expand Up @@ -133,7 +138,7 @@ public function applicable_formats(): array {
*/
#[\Override]
public function user_can_addto($page) {
return false;
return true;
}

/**
Expand Down
217 changes: 217 additions & 0 deletions classes/form/persona_form.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
<?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 block_ai_chat\form;

use core_form\dynamic_form;
use context;
use function DI\get;

/**
* Class base_form
*
* @package block_ai_chat
* @copyright 2025 Tobias Garske, ISB Bayern
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class persona_form extends dynamic_form {
/** @var array $personas */
protected array $personas;
/** @var bool|object $personaselected */
protected bool|object $personaselected;
/** @var int $blockcontextid */
protected int $blockcontextid;

/**
* Form definition.
*/
public function definition() {
global $USER, $DB;

// Load default and user personas here since select options need to be inserted.
$names = [];
$prompts = [];
$currentprompt = '';
$sql = "SELECT per.id, per.userid, per.name, per.prompt FROM {block_ai_chat_personas} per
LEFT JOIN {block_ai_chat_personas_selected} sel ON sel.personasid = per.id
WHERE per.userid = 0 OR per.userid = :userid";
$this->personas = $DB->get_records_sql($sql, ['userid' => $USER->id]);
foreach ($this->personas as $key => $persona) {
$names[$persona->id] = $persona->name;
$prompts[$persona->id] = $persona->prompt;
// Get current persona.
if ($persona->userid != 0) {
$currentprompt = $persona->prompt;
}
}
// Add option "none".
$names[0] = get_string('nopersona', 'block_ai_chat');
// Stringify.
$prompts = json_encode($prompts);

$mform =& $this->_form;

$mform->addElement('hidden', 'contextid');
$mform->setType('contextid', PARAM_INT);
$mform->setDefault('contextid', $this->optional_param('contextid', null, PARAM_INT));

$mform->addElement('hidden', 'prompts');
$mform->setType('prompts', PARAM_TEXT);
$mform->setDefault('prompts', $prompts);

$mform->addElement('select', 'name', get_string('name', 'block_ai_chat'), $names);
$mform->setType('name', PARAM_ALPHANUM);

$mform->addElement('textarea', 'prompt', get_string('prompt', 'block_ai_chat'));
$mform->setType('prompt', PARAM_TEXT);
$mform->setDefault('prompt', $currentprompt);
}

/**
* Returns the user context
*
* @return context
*/
protected function get_context_for_dynamic_submission(): context {
// When modal is built, contextid is passed as optional_param. For submission it is accessed via formdata.
if (!isset($this->blockcontextid)) {
$this->blockcontextid = $this->optional_param('contextid', null, PARAM_INT);
}
return \context::instance_by_id($this->blockcontextid);
}

/**
*
* Checks if current user has sufficient permissions, otherwise throws exception
*/
protected function check_access_for_dynamic_submission(): void {
require_capability('block/ai_chat:addinstance', \context_system::instance());
}

/**
* Form validation.
*
* @param array $data array of ("fieldname"=>value) of submitted data
* @param array $files array of uploaded files "element_name"=>tmp_file_path
* @return array of "element_name"=>"error_description" if there are errors,
* or an empty array if everything is OK (true allowed for backwards compatibility too).
*/
public function validation($data, $files) {
$errors = [];
// $data['name'] = "0" is the option to delete the current persona.
if (empty($data['name']) && $data['name'] !== "0") {
$errors['name'] = get_string('errorname', 'block_ai_chat');
}
if (empty($data['prompt']) && $data['name'] !== "0") {
$errors['prompt'] = get_string('errorprompt', 'block_ai_chat');
}
return $errors;
}

/**
* Process the form submission, used if form was submitted via AJAX
*
* @return array Returns whether a new source was created.
*/
public function process_dynamic_submission(): array {
global $USER, $DB;

$formdata = $this->get_data();

$formdata->timemodified = time();

$context = $this->get_context_for_dynamic_submission();

// Delete connected persona entries.
if ($formdata->name == 0) {
$params = ['id' => $this->personaselected->personasid];
$DB->delete_records('block_ai_chat_personas', $params);
$params = ['contextid' => $formdata->contextid, 'personasid' => $this->personaselected->personasid];
$DB->delete_records('block_ai_chat_personas_selected', $params);
return [
'update' => true,
];
}

// Check if persona is selected for this instance.
if ($this->personaselected) {
// Update personas_selected.
// This is one record per instance, where the current prompt is saved.
$record = new \stdClass();
$record->id = $this->personaselected->personasid;
$record->userid = $USER->id;
$record->prompt = $formdata->prompt;
$record->timemodified = time();
$result1 = $DB->update_record('block_ai_chat_personas', $record);

} else {
// Insert new records.
// This is to allow custom prompts.
$record = new \stdClass();
$record->userid = $USER->id;
$record->name = get_string('chosenpersona', 'block_ai_chat');
$record->prompt = $formdata->prompt;
$record->timecreated = time();
$record->timemodified = time();
$entry = $DB->insert_record('block_ai_chat_personas', $record);

// Insert to personas_selected.
$record = new \stdClass();
$record->personasid = $entry;
$record->contextid = $formdata->contextid;
$result2 = $DB->insert_record('block_ai_chat_personas_selected', $record);
}

// If no persona selected delete entry.
// TODO

return [
'update' => true,
];
}

/**
* Load in existing data as form defaults
*/
public function set_data_for_dynamic_submission(): void {
global $DB;

$this->get_context_for_dynamic_submission();

// Check if a persona is selected for this instance.
$param = [$this->blockcontextid];
$this->personaselected = $DB->get_record_select('block_ai_chat_personas_selected', 'contextid = ?', $param);
if ($this->personaselected) {
$data = [
'name' => $this->personaselected->personasid,
'prompt' => $this->personas[$this->personaselected->personasid]->prompt,
];
} else {
$data = [];
}

$this->set_data($data);
}

/**
* Returns url to set in $PAGE->set_url() when form is being rendered or submitted via AJAX
*
* @return moodle_url
*/
protected function get_page_url_for_dynamic_submission(): \moodle_url {
return new \moodle_url('/block_ai_chat_persona_dummy.php');
}
}
Loading

0 comments on commit 45e1929

Please sign in to comment.