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 images.
18
 *
19
 * @module      tiny_aiplacement/generateimage
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 ImageModal from 'tiny_aiplacement/imagemodal';
25
import Ajax from 'core/ajax';
26
import {getString} from 'core/str';
27
import Templates from 'core/templates';
28
import AiMediaImage from './mediaimage';
29
import {getContextId} from 'tiny_aiplacement/options';
30
import GenerateBase from 'tiny_aiplacement/generatebase';
31
 
32
export default class GenerateImage extends GenerateBase {
33
    SELECTORS = {
34
        GENERATEBUTTON: () => `[id="${this.editor.id}_tiny_aiplacement_generatebutton"]`,
35
        PROMPTAREA: () => `[id="${this.editor.id}_tiny_aiplacement_imageprompt"]`,
36
        IMAGECONTAINER: () => `[id="${this.editor.id}_tiny_aiplacement_generate_image"]`,
37
        GENERATEBTN: '[data-action="generate"]',
38
        INSERTBTN: '[data-action="inserter"]',
39
        BACKTBTN: '[data-action="back"]',
40
        GENERATEDIMAGE: () => `[id="${this.editor.id}_tiny_generated_image"]`,
41
    };
42
 
43
    imageURL = null;
44
 
45
    getModalClass() {
46
        return ImageModal;
47
    }
48
 
49
    /**
50
     * Handle click events within the image modal.
51
     *
52
     * @param {Event} e - The click event object.
53
     * @param {HTMLElement} root - The root element of the modal.
54
     */
55
    handleContentModalClick(e, root) {
56
        const actions = {
57
            generate: () => this.handleSubmit(root, e.target),
58
            inserter: () => this.handleInsert(),
59
            cancel: () => this.modalObject.destroy(),
60
            back: () => {
61
                this.modalObject.destroy();
62
                this.displayContentModal();
63
            },
64
        };
65
 
66
        const actionKey = Object.keys(actions).find(key => e.target.closest(`[data-action="${key}"]`));
67
        if (actionKey) {
68
            e.preventDefault();
69
            actions[actionKey]();
70
        }
71
    }
72
 
73
    /**
74
     * Set up the prompt area in the modal, adding necessary event listeners.
75
     *
76
     * @param {HTMLElement} root - The root element of the modal.
77
     */
78
    setupPromptArea(root) {
79
        const generateBtn = root.querySelector(this.SELECTORS.GENERATEBUTTON());
80
        const promptArea = root.querySelector(this.SELECTORS.PROMPTAREA());
81
 
82
        promptArea.addEventListener('input', () => {
83
            generateBtn.disabled = promptArea.value.trim() === '';
84
        });
85
    }
86
 
87
    /**
88
     * Handle the submit action.
89
     *
90
     * @param {Object} root The root element of the modal.
91
     * @param {Object} submitBtn The submit button element.
92
     */
93
    async handleSubmit(root, submitBtn) {
94
        await this.displayLoading(root, submitBtn);
95
 
96
        const displayArgs = this.getDisplayArgs(root);
97
        const request = {
98
            methodname: 'aiplacement_editor_generate_image',
99
            args: displayArgs
100
        };
101
 
102
        try {
103
            this.responseObj = await Ajax.call([request])[0];
104
            if (this.responseObj.error) {
105
                this.handleGenerationError(root, submitBtn, '');
106
            } else {
107
                await this.displayGeneratedImage(root);
108
                await this.hideLoading(root, submitBtn);
109
                // Focus the container for accessibility.
110
                const imageDisplayContainer = root.querySelector(this.SELECTORS.IMAGECONTAINER());
111
                imageDisplayContainer.focus();
112
            }
113
        } catch (error) {
114
            this.handleGenerationError(root, submitBtn, '');
115
        }
116
    }
117
 
118
    /**
119
     * Handle the insert action.
120
     *
121
     */
122
    async handleInsert() {
123
        // Use the revised prompt for the image alt text if it is available in the response.
124
        const revisedPrompt = this.responseObj.revisedprompt;
125
        const altTextToUse = revisedPrompt ? revisedPrompt : this.promptText;
126
        const mediaImage = new AiMediaImage(this.editor, this.imageURL, altTextToUse);
127
        await mediaImage.displayDialogue();
128
        this.modalObject.destroy();
129
    }
130
 
131
    /**
132
     * Handle a generation error.
133
     *
134
     * @param {Object} root The root element of the modal.
135
     * @param {Object} submitBtn The submit button element.
136
     * @param {String} errorMessage The error message to display.
137
     */
138
    async handleGenerationError(root, submitBtn, errorMessage = '') {
139
        if (!errorMessage) {
140
            // Get the default error message.
141
            errorMessage = await getString('errorgeneral', 'tiny_aiplacement');
142
        }
143
        this.modalObject.setBody(await Templates.render('tiny_aiplacement/modalbodyerror', {'errorMessage': errorMessage}));
144
        const backBtn = root.querySelector(this.SELECTORS.BACKTBTN);
145
        const generateBtn = root.querySelector(this.SELECTORS.GENERATEBUTTON());
146
        backBtn.classList.remove('hidden');
147
        generateBtn.classList.add('hidden');
148
        await this.hideLoading(root, submitBtn);
149
        // Focus the back button for accessibility.
150
        backBtn.focus();
151
    }
152
 
153
    /**
154
     * Display the generated image in the modal.
155
     *
156
     * @param {HTMLElement} root - The root element of the modal.
157
     */
158
    async displayGeneratedImage(root) {
159
        const imageDisplayContainer = root.querySelector(this.SELECTORS.IMAGECONTAINER());
160
        const insertBtn = root.querySelector(this.SELECTORS.INSERTBTN);
161
        // Set the draft URL as it's used elsewhere.
162
        this.imageURL = this.responseObj.drafturl;
163
 
164
        // Render the image template and insert it into the modal.
165
        imageDisplayContainer.innerHTML = await Templates.render('tiny_aiplacement/image', {
166
            url: this.responseObj.drafturl,
167
            elementid: this.editor.id,
168
            alt: this.promptText,
169
        });
170
        const imagElement = root.querySelector(this.SELECTORS.GENERATEDIMAGE());
171
 
172
        return new Promise((resolve, reject) => {
173
            imagElement.onload = () => {
174
                insertBtn.classList.remove('hidden');
175
                imagElement.focus();
176
                resolve(); // Resolve the promise when the image is loaded.
177
            };
178
            imagElement.onerror = (error) => {
179
                reject(error); // Reject the promise if there is an error loading the image.
180
            };
181
        });
182
    }
183
 
184
    /**
185
     * Get the display args for the image.
186
     *
187
     * @param {Object} root The root element of the modal.
188
     */
189
    getDisplayArgs(root) {
190
        const contextId = getContextId(this.editor);
191
        const promptText = root.querySelector(this.SELECTORS.PROMPTAREA()).value;
192
        this.promptText = promptText;
193
 
194
        const aspectRatio = this.getSelectedRadioValue('aspect-ratio', 'square');
195
        const imageQuality = this.getSelectedRadioValue('quality', 'standard');
196
 
197
        return {
198
            contextid: contextId,
199
            prompttext: promptText,
200
            aspectratio: aspectRatio,
201
            quality: imageQuality,
202
            numimages: 1
203
        };
204
    }
205
 
206
    /**
207
     * Get the value of the selected radio button.
208
     *
209
     * @param {String} radioName The name of the radio button group.
210
     * @param {String} defaultValue The default value of the radio button.
211
     */
212
    getSelectedRadioValue(radioName, defaultValue = null) {
213
        const radios = document.getElementsByName(radioName);
214
        for (const radio of radios) {
215
            if (radio.checked) {
216
                return radio.value;
217
            }
218
        }
219
        return defaultValue;
220
    }
221
}