Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1441 ariadna 1
// This file is part of Moodle - http://moodle.org/
2
//
3
// Moodle is free software: you can redistribute it and/or modify
4
// it under the terms of the GNU General Public License as published by
5
// the Free Software Foundation, either version 3 of the License, or
6
// (at your option) any later version.
7
//
8
// Moodle is distributed in the hope that it will be useful,
9
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
// GNU General Public License for more details.
12
//
13
// You should have received a copy of the GNU General Public License
14
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
15
 
16
/**
17
 * Tiny AI generate text.
18
 *
19
 * @module      tiny_aiplacement/generatetext
20
 * @copyright   2024 Matt Porritt <matt.porritt@moodle.com>
21
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22
 */
23
 
24
import TextModal from './textmodal';
25
import Ajax from 'core/ajax';
26
import {getString} from 'core/str';
27
import Templates from 'core/templates';
28
import AIHelper from 'core_ai/helper';
29
import {getContextId} from './options';
30
import TinyAiTextMarker from './textmark';
31
import GenerateBase from './generatebase';
32
 
33
export default class GenerateText extends GenerateBase {
34
    SELECTORS = {
35
        GENERATEBUTTON: () => `[id="${this.editor.id}_tiny_aiplacement_generatebutton"]`,
36
        PROMPTAREA: () => `[id="${this.editor.id}_tiny_aiplacement_textprompt"]`,
37
        TEXTCONTAINER: () => `[id="${this.editor.id}_tiny_aiplacement_generate_text"]`,
38
        GENERATEDRESPONSE: () => `[id="${this.editor.id}_tiny_aiplacement_textresponse"]`,
39
        INSERTBTN: '[data-action="inserter"]',
40
        BACKTBTN: '[data-action="back"]',
41
    };
42
 
43
    getModalClass() {
44
        return TextModal;
45
    }
46
 
47
    /**
48
     * Handle click events within the text modal.
49
     *
50
     * @param {Event} e - The click event object.
51
     * @param {HTMLElement} root - The root element of the modal.
52
     */
53
    handleContentModalClick(e, root) {
54
        const actions = {
55
            generate: () => this.handleSubmit(root, e.target),
56
            inserter: () => this.handleInsert(root, e.target),
57
            cancel: () => this.modalObject.destroy(),
58
            back: () => {
59
                this.modalObject.destroy();
60
                this.displayContentModal();
61
            },
62
        };
63
 
64
        const actionKey = Object.keys(actions).find(key => e.target.closest(`[data-action="${key}"]`));
65
        if (actionKey) {
66
            e.preventDefault();
67
            actions[actionKey]();
68
        }
69
    }
70
 
71
    /**
72
     * Set up the prompt area in the modal, adding necessary event listeners.
73
     *
74
     * @param {HTMLElement} root - The root element of the modal.
75
     */
76
    setupPromptArea(root) {
77
        const generateBtn = root.querySelector(this.SELECTORS.GENERATEBUTTON());
78
        const promptArea = root.querySelector(this.SELECTORS.PROMPTAREA());
79
 
80
        promptArea.addEventListener('input', () => {
81
            generateBtn.disabled = promptArea.value.trim() === '';
82
        });
83
    }
84
 
85
    /**
86
     * Handle the submit action.
87
     *
88
     * @param {Object} root The root element of the modal.
89
     * @param {Object} submitBtn The submit button element.
90
     */
91
    async handleSubmit(root, submitBtn) {
92
        await this.displayLoading(root, submitBtn);
93
 
94
        const requestArgs = this.getRequestArgs(root);
95
        const request = {
96
            methodname: 'aiplacement_editor_generate_text',
97
            args: requestArgs
98
        };
99
 
100
        try {
101
            this.responseObj = await Ajax.call([request])[0];
102
            if (this.responseObj.error) {
103
                this.handleGenerationError(root, submitBtn, '');
104
            } else {
105
                await this.displayGeneratedText(root);
106
                await this.hideLoading(root, submitBtn);
107
                // Focus the container for accessibility.
108
                const textDisplayContainer = root.querySelector(this.SELECTORS.TEXTCONTAINER());
109
                textDisplayContainer.focus();
110
            }
111
        } catch (error) {
112
            this.handleGenerationError(root, submitBtn, '');
113
        }
114
    }
115
 
116
    /**
117
     * Handle the insert action.
118
     *
119
     * @param {Object} root The root element of the modal.
120
     * @param {HTMLElement} submitBtn - The submit button element.
121
     */
122
    async handleInsert(root, submitBtn) {
123
        await this.displayLoading(root, submitBtn);
124
 
125
        // Update the generated response with the content from the form.
126
        // In case the user has edited the response.
127
        const generatedResponseDiv = root.querySelector(this.SELECTORS.GENERATEDRESPONSE());
128
 
129
        // Wrap the edited sections in the response with tags.
130
        // This is so we can differentiate between the edited sections and the generated content.
131
        const wrappedEditedResponse = await TinyAiTextMarker.wrapEditedSections(
132
            this.responseObj.generatedcontent,
133
            generatedResponseDiv.value)
134
        ;
135
 
136
        // Replace double line breaks with <br> and with </p><p> for paragraphs and removed any markdown.
137
        this.responseObj.editedtext = AIHelper.formatResponse(wrappedEditedResponse);
138
 
139
        // Generate the HTML for the response.
140
        const formattedResponse = await Templates.render('tiny_aiplacement/textinsert', this.responseObj);
141
 
142
        // Insert the response into the editor.
143
        this.editor.insertContent(formattedResponse);
144
        this.editor.execCommand('mceRepaint');
145
        this.editor.windowManager.close();
146
 
147
        // Close the modal and return to the editor.
148
        this.modalObject.hide();
149
    }
150
 
151
    /**
152
     * Handle a generation error.
153
     *
154
     * @param {Object} root The root element of the modal.
155
     * @param {Object} submitBtn The submit button element.
156
     * @param {String} errorMessage The error message to display.
157
     */
158
    async handleGenerationError(root, submitBtn, errorMessage = '') {
159
        if (!errorMessage) {
160
            // Get the default error message.
161
            errorMessage = await getString('errorgeneral', 'tiny_aiplacement');
162
        }
163
        this.modalObject.setBody(await Templates.render('tiny_aiplacement/modalbodyerror', {'errorMessage': errorMessage}));
164
        const backBtn = root.querySelector(this.SELECTORS.BACKTBTN);
165
        const generateBtn = root.querySelector(this.SELECTORS.GENERATEBUTTON());
166
        backBtn.classList.remove('hidden');
167
        generateBtn.classList.add('hidden');
168
        await this.hideLoading(root, submitBtn);
169
        // Focus the back button for accessibility.
170
        backBtn.focus();
171
    }
172
 
173
    /**
174
     * Display the generated text in the modal.
175
     *
176
     * @param {HTMLElement} root - The root element of the modal.
177
     */
178
    async displayGeneratedText(root) {
179
        const textDisplayContainer = root.querySelector(this.SELECTORS.TEXTCONTAINER());
180
        const insertBtn = root.querySelector(this.SELECTORS.INSERTBTN);
181
 
182
        // Render the textarea template and insert it into the modal.
183
        textDisplayContainer.innerHTML = await Templates.render('tiny_aiplacement/textarea', {
184
            elementid: this.editor.id,
185
            text: this.responseObj.generatedcontent,
186
        });
187
        const textareaElement = root.querySelector(this.SELECTORS.GENERATEDRESPONSE());
188
 
189
        return new Promise((resolve, reject) => {
190
            if (textareaElement.textLength > 0) {
191
                insertBtn.classList.remove('hidden');
192
                textareaElement.focus();
193
                resolve();
194
            } else {
195
                reject();
196
            }
197
        });
198
    }
199
 
200
    /**
201
     * Get the request args for the generated text.
202
     *
203
     * @param {Object} root The root element of the modal.
204
     */
205
    getRequestArgs(root) {
206
        const contextId = getContextId(this.editor);
207
        const promptText = root.querySelector(this.SELECTORS.PROMPTAREA()).value;
208
 
209
        return {
210
            contextid: contextId,
211
            prompttext: promptText
212
        };
213
    }
214
}