Passed
Pull Request — master (#6087)
by
unknown
08:38
created

LpAiHelper   A

Complexity

Total Complexity 21

Size/Duplication

Total Lines 295
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 126
c 1
b 0
f 1
dl 0
loc 295
rs 10
wmc 21

3 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 1 1
C createLearningPathFromAI() 0 95 13
B aiHelperForm() 0 187 7
1
<?php
2
/* For licensing terms, see /license.txt */
3
4
declare(strict_types=1);
5
6
use Chamilo\CoreBundle\Framework\Container;
7
8
class LpAiHelper
9
{
10
    /**
11
     * AiHelper constructor.
12
     * Requires AI helpers to be enabled in the settings.
13
     */
14
    public function __construct() {}
15
16
    /**
17
     * Get the form to generate Learning Path (LP) items using AI.
18
     */
19
    public function aiHelperForm()
20
    {
21
        if ('true' !== api_get_setting('ai_helpers.enable_ai_helpers') ||
22
            'true' !== api_get_course_setting('learning_path_generator')) {
23
24
            return false;
25
        }
26
27
        // Get AI providers from settings
28
        $aiProvidersJson = api_get_setting('ai_helpers.ai_providers');
29
        $configuredApi = api_get_setting('ai_helpers.default_ai_provider');
30
        $availableApis = json_decode($aiProvidersJson, true) ?? [];
31
        $hasSingleApi = count($availableApis) === 1 || isset($availableApis[$configuredApi]);
32
33
        $form = new FormValidator(
34
            'lp_ai_generate',
35
            'post',
36
            api_get_self()."?".api_get_cidreq(),
37
            null
38
        );
39
        $form->addElement('header', get_lang('Lp Ai generator'));
40
41
        // Show the AI provider being used
42
        if ($hasSingleApi) {
43
            $apiName = $availableApis[$configuredApi]['model'] ?? $configuredApi;
44
            $form->addHtml('<div style="margin-bottom: 10px; font-size: 14px; color: #555;">'
45
                .sprintf(get_lang('Using AI Provider: %s'), '<strong>'.htmlspecialchars($apiName).'</strong>').'</div>');
46
        }
47
48
        // Input fields for LP generation
49
        $form->addElement('text', 'lp_name', get_lang('Lp Ai Topic'));
50
        $form->addRule('lp_name', get_lang('This field is required'), 'required');
51
        $form->addElement('number', 'nro_items', get_lang('Lp Ai number of items'));
52
        $form->addRule('nro_items', get_lang('This field is required'), 'required');
53
        $form->addElement('number', 'words_count', get_lang('Lp Ai words count'));
54
        $form->addRule('words_count', get_lang('This field is required'), 'required');
55
56
        // Checkbox for adding quizzes
57
        $form->addElement('checkbox', 'add_lp_quiz', null, get_lang('Add test after each page'), ['id' => 'add-lp-quiz']);
58
        $form->addHtml('<div id="lp-quiz-area">');
59
        $form->addElement('number', 'nro_questions', get_lang('Number of questions'));
60
        $form->addRule('nro_questions', get_lang('This field is required'), 'required');
61
        $form->addHtml('</div>');
62
        $form->setDefaults(['nro_questions' => 2]);
63
64
        // Allow provider selection if multiple are available
65
        if (!$hasSingleApi) {
66
            $form->addSelect(
67
                'ai_provider',
68
                get_lang('AI Provider'),
69
                array_combine(array_keys($availableApis), array_keys($availableApis))
70
            );
71
        }
72
73
        // API URLs
74
        $generateUrl = api_get_path(WEB_PATH).'ai/generate_learnpath';
75
        $courseInfo = api_get_course_info();
76
        $language = $courseInfo['language'];
77
        $courseCode = api_get_course_id();
78
        $sessionId = api_get_session_id();
79
        $redirectSuccess = api_get_path(WEB_CODE_PATH).'lp/lp_controller.php?'.api_get_cidreq().'&action=add_item&type=step&isStudentView=false&lp_id=';
80
81
        // JavaScript to handle form submission
82
        $form->addHtml('<script>
83
        $(function () {
84
            $("#lp-quiz-area").hide();
85
            $("#add-lp-quiz").change(function() {
86
                $("#lp-quiz-area").toggle(this.checked);
87
            });
88
89
            $("#create-lp-ai").on("click", function (e) {
90
                e.preventDefault();
91
                e.stopPropagation();
92
93
                var btnGenerate = $(this);
94
                var lpName = $("[name=\'lp_name\']").val().trim();
95
                var nroItems = parseInt($("[name=\'nro_items\']").val());
96
                var wordsCount = parseInt($("[name=\'words_count\']").val());
97
                var addTests = $("#add-lp-quiz").is(":checked");
98
                var nroQuestions = parseInt($("[name=\'nro_questions\']").val());
99
                var provider = '.(!$hasSingleApi ? '$("[name=\'ai_provider\']").val()' : '"'.$configuredApi.'"').';
100
101
                var isValid = true;
102
103
                $(".error-message").remove();
104
105
                if (lpName === "") {
106
                    $("[name=\'lp_name\']").after("<div class=\'error-message\' style=\'color: red;\'>'.get_lang('This field is required').'</div>");
107
                    isValid = false;
108
                }
109
110
                if (isNaN(nroItems) || nroItems <= 0) {
111
                    $("[name=\'nro_items\']").after("<div class=\'error-message\' style=\'color: red;\'>'.get_lang('Please enter a valid number').'</div>");
112
                    isValid = false;
113
                }
114
115
                if (isNaN(wordsCount) || wordsCount <= 0) {
116
                    $("[name=\'words_count\']").after("<div class=\'error-message\' style=\'color: red;\'>'.get_lang('Please enter a valid word count').'</div>");
117
                    isValid = false;
118
                }
119
120
                if (addTests && (isNaN(nroQuestions) || nroQuestions <= 0 || nroQuestions > 5)) {
121
                    $("[name=\'nro_questions\']").after("<div class=\'error-message\' style=\'color: red;\'>'.sprintf(get_lang('Number of questions limited from %d to %d'), 1, 5).'</div>");
122
                    isValid = false;
123
                }
124
125
                if (!isValid) {
126
                    return;
127
                }
128
129
                btnGenerate.attr("disabled", true).text("'.get_lang('Please wait this could take a while').'");
130
131
                var requestData = JSON.stringify({
132
                    "lp_name": lpName,
133
                    "nro_items": nroItems,
134
                    "words_count": wordsCount,
135
                    "language": "'.$language.'",
136
                    "add_tests": addTests,
137
                    "nro_questions": nroQuestions,
138
                    "ai_provider": provider,
139
                    "course_code": "'.$courseCode.'",
140
                });
141
142
                $.ajax({
143
                    url: "'.$generateUrl.'",
144
                    type: "POST",
145
                    contentType: "application/json",
146
                    data: requestData,
147
                    dataType: "json",
148
                    success: function (data) {
149
                        btnGenerate.attr("disabled", false).text("'.get_lang('Generate').'");
150
151
                        if (data.success) {
152
                            $.ajax({
153
                                url: "'.api_get_path(WEB_AJAX_PATH).'lp.ajax.php?a=add_lp_ai&'.api_get_cidreq().'",
154
                                type: "POST",
155
                                contentType: "application/json",
156
                                data: JSON.stringify({
157
                                    "lp_data": data.data,
158
                                    "course_code": "'.$courseCode.'"
159
                                }),
160
                                success: function (result) {
161
                                    console.log("🔥 Response from add_lp_ai:", result);
162
163
                                    try {
164
                                        let parsedResult = (typeof result === "string") ? JSON.parse(result) : result;
165
                                        let isSuccess = Boolean(parsedResult.success);
166
167
                                        if (isSuccess) {
168
                                            location.href = "'.$redirectSuccess.'" + parsedResult.lp_id;
169
                                        } else {
170
                                            alert("Error: " + (parsedResult.text || "'.get_lang('Error creating Learning Path').'"));
171
                                        }
172
                                    } catch (e) {
173
                                        console.error("❌ JSON Parse Error: ", e);
174
                                        alert("'.get_lang('Invalid server response').'");
175
                                    }
176
                                }
177
                            });
178
                        } else {
179
                            alert(data.text || "'.get_lang('No results found').'. '.get_lang('Please try again').'");
180
                        }
181
                    },
182
                    error: function (jqXHR) {
183
                        btnGenerate.attr("disabled", false).text("'.get_lang('Generate').'");
184
185
                        try {
186
                            var response = JSON.parse(jqXHR.responseText);
187
                            var errorMessage = "'.get_lang('An unexpected error occurred. Please try again later.').'";
188
189
                            if (response && response.text) {
190
                                errorMessage = response.text;
191
                            }
192
193
                            alert("'.get_lang('Request failed').': " + errorMessage);
194
                        } catch (e) {
195
                            alert("'.get_lang('Request failed').': " + "'.get_lang('An unexpected error occurred. Please contact support.').'");
196
                        }
197
                    }
198
                });
199
200
            });
201
        });
202
        </script>');
203
204
        $form->addButton('create_lp_button', get_lang('Add Learnpath'), 'check', 'primary', '', null, ['id' => 'create-lp-ai']);
205
        echo $form->returnForm();
206
    }
207
208
    public function createLearningPathFromAI(array $lpData, string $courseCode): array
209
    {
210
        if (!isset($lpData['topic'])) {
211
            return ['success' => false, 'text' => 'Error: Topic not set in AI response.'];
212
        }
213
214
        $lp = learnpath::add_lp(
215
            $courseCode,
216
            $lpData['topic'],
217
            '',
218
            'chamilo',
219
            'manual'
220
        );
221
222
        if (null === $lp) {
223
            return ['success' => false, 'text' => 'Failed to create Learning Path.'];
224
        }
225
226
        $lpId = $lp->getIid();
227
        if (empty($lpId)) {
228
            return ['success' => false, 'text' => 'Failed to retrieve Learning Path ID.'];
229
        }
230
231
        $courseInfo = api_get_course_info($courseCode);
232
        $learningPath = new learnpath($lp, $courseInfo, api_get_user_id());
233
234
        $lpItemRepo = Container::getLpItemRepository();
235
236
        $parent = $lpItemRepo->getRootItem($lpId);
237
        $order = 1;
238
        $lpItemsIds = [];
239
240
        foreach ($lpData['lp_items'] as $item) {
241
            $documentId = $learningPath->create_document(
242
                $courseInfo,
243
                $item['content'],
244
                $item['title']
245
            );
246
247
            if (!empty($documentId)) {
248
                $previousId = isset($lpItemsIds[$order - 1]) ? (int) $lpItemsIds[$order - 1]['item_id'] : 0;
249
                $lpItemId = $learningPath->add_item(
250
                    $parent,
251
                    $previousId,
252
                    TOOL_DOCUMENT,
253
                    $documentId,
254
                    $item['title']
255
                );
256
                $lpItemsIds[$order] = ['item_id' => $lpItemId, 'item_type' => TOOL_DOCUMENT];
257
            }
258
            $order++;
259
        }
260
261
        if (!empty($lpData['quiz_items'])) {
262
            require_once api_get_path(SYS_CODE_PATH).'exercise/export/aiken/aiken_import.inc.php';
263
            require_once api_get_path(SYS_CODE_PATH).'exercise/export/aiken/aiken_classes.php';
264
265
            foreach ($lpData['quiz_items'] as $quiz) {
266
                if (empty(trim($quiz['content']))) {
267
                    continue;
268
                }
269
270
                $request = [
271
                    'quiz_name' => get_lang('Exercise') . ': ' . $quiz['title'],
272
                    'nro_questions' => count(explode("\n", trim($quiz['content']))),
273
                    'course_id' => api_get_course_int_id($courseCode),
274
                    'aiken_format' => trim($quiz['content']),
275
                ];
276
277
                $exerciseId = aiken_import_exercise(null, $request);
278
279
                if (!empty($exerciseId)) {
280
                    $previousId = isset($lpItemsIds[$order - 1]) ? (int) $lpItemsIds[$order - 1]['item_id'] : 0;
281
                    $lpQuizItemId = $learningPath->add_item(
282
                        $parent,
283
                        $previousId,
284
                        TOOL_QUIZ,
285
                        $exerciseId,
286
                        $request['quiz_name']
287
                    );
288
289
                    if (!empty($lpQuizItemId)) {
290
                        $lpItemsIds[$order] = [
291
                            'item_id' => $lpQuizItemId,
292
                            'item_type' => TOOL_QUIZ,
293
                            'min_score' => round($request['nro_questions'] / 2, 2),
294
                            'max_score' => (float) $request['nro_questions'],
295
                        ];
296
                    }
297
                    $order++;
298
                }
299
            }
300
        }
301
302
        return ['success' => true, 'lp_id' => $lpId];
303
    }
304
}
305