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

aiken_import_exercise()   B

Complexity

Conditions 8
Paths 9

Size

Total Lines 35
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 22
nc 9
nop 2
dl 0
loc 35
rs 8.4444
c 0
b 0
f 0
1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
/**
6
 * Library for the import of Aiken format.
7
 *
8
 * @author claro team <[email protected]>
9
 * @author Guillaume Lederer <[email protected]>
10
 * @author César Perales <[email protected]> Parse function for Aiken format
11
 */
12
13
use Chamilo\CoreBundle\Component\Utils\ActionIcon;
14
15
/**
16
 * This function displays the form for import of the zip file with qti2.
17
 *
18
 * @param   string  Report message to show in case of error
19
 */
20
function aiken_display_form()
21
{
22
    $name_tools = get_lang('Import Aiken quiz');
23
    $form = '<div class="actions">';
24
    $form .= '<a href="exercise.php?show=test&'.api_get_cidreq().'">'.
25
        Display::getMdiIcon(ActionIcon::BACK, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Back to Tests tool')).'</a>';
26
    $form .= '</div>';
27
    $form_validator = new FormValidator(
28
        'aiken_upload',
29
        'post',
30
        api_get_self().'?'.api_get_cidreq(),
31
        null,
32
        ['enctype' => 'multipart/form-data']
33
    );
34
    $form_validator->addElement('header', $name_tools);
35
    $form_validator->addElement('text', 'total_weight', get_lang('Total weight'));
36
    $form_validator->addElement('file', 'userFile', get_lang('File'));
37
    $form_validator->addButtonUpload(get_lang('Upload'), 'submit');
38
    $form .= $form_validator->returnForm();
39
    $form .= '<blockquote>'.get_lang('Import Aiken quizExplanation').'<br /><pre>'.get_lang('Import Aiken quizExplanationExample').'</pre></blockquote>';
40
    echo $form;
41
}
42
43
/**
44
 * Set the exercise information from an aiken text formatted.
45
 */
46
function setExerciseInfoFromAikenText($aikenText, &$exerciseInfo): void
47
{
48
    $detect = mb_detect_encoding($aikenText, 'ASCII', true);
49
    if ('ASCII' === $detect) {
50
        $data = explode("\n", $aikenText);
51
    } else {
52
        if (false !== stripos($aikenText, "\x0D") || false !== stripos($aikenText, "\r\n")) {
53
            $text = str_ireplace(["\x0D", "\r\n"], "\n", $aikenText);
54
            $data = explode("\n", $text);
55
        } else {
56
            $data = explode("\n", $aikenText);
57
        }
58
    }
59
60
    $questionIndex = -1;
61
    $answersArray = [];
62
    $currentQuestion = null;
63
64
    foreach ($data as $line) {
65
        $line = trim($line);
66
        if (empty($line)) {
67
            continue;
68
        }
69
70
        if (!preg_match('/^[A-Z]\.\s/', $line) && !preg_match('/^ANSWER:\s?[A-Z]/', $line) && !preg_match('/^ANSWER_EXPLANATION:\s?(.*)/', $line)) {
71
            $questionIndex++;
72
            $exerciseInfo['question'][$questionIndex] = [
73
                'type' => 'MCUA',
74
                'title' => $line,
75
                'answer' => [],
76
                'correct_answers' => [],
77
                'weighting' => [],
78
                'feedback' => '',
79
                'description' => '',
80
                'answer_tags' => []
81
            ];
82
            $answersArray = [];
83
            $currentQuestion = &$exerciseInfo['question'][$questionIndex];
84
            continue;
85
        }
86
87
        if (preg_match('/^([A-Z])\.\s(.*)/', $line, $matches)) {
88
            $answerIndex = count($currentQuestion['answer']);
89
            $currentQuestion['answer'][] = ['value' => $matches[2]];
90
            $answersArray[$matches[1]] = $answerIndex + 1;
91
            continue;
92
        }
93
94
        if (preg_match('/^ANSWER:\s?([A-Z])/', $line, $matches)) {
95
            if (isset($answersArray[$matches[1]])) {
96
                $currentQuestion['correct_answers'][] = $answersArray[$matches[1]];
97
            }
98
            continue;
99
        }
100
101
        if (preg_match('/^ANSWER_EXPLANATION:\s?(.*)/', $line, $matches)) {
102
            if ($questionIndex >= 0) {
103
                $exerciseInfo['question'][$questionIndex]['feedback'] = $matches[1];
104
            }
105
            continue;
106
        }
107
    }
108
109
    $totalQuestions = count($exerciseInfo['question']);
110
    $totalWeight = (int) ($exerciseInfo['total_weight'] ?? 20);
111
    foreach ($exerciseInfo['question'] as $key => $question) {
112
        $exerciseInfo['question'][$key]['weighting'][0] = $totalWeight / $totalQuestions;
113
    }
114
}
115
116
/**
117
 * Imports an Aiken file or AI-generated text and creates an exercise in Chamilo 2.
118
 *
119
 * @param string|null $file Path to the Aiken file (optional)
120
 * @param array|null $request AI form data (optional)
121
 *
122
 * @return mixed Exercise ID on success, error message on failure
123
 */
124
function aiken_import_exercise(string $file = null, ?array $request = [])
125
{
126
    $exerciseInfo = [];
127
    $fileIsSet = false;
128
    $baseWorkDir = api_get_path(SYS_ARCHIVE_PATH) . 'aiken/';
129
    $uploadPath = 'aiken_' . api_get_unique_id();
130
131
    if ($file) {
132
        $fileIsSet = true;
133
134
        if (!is_dir($baseWorkDir . $uploadPath)) {
135
            mkdir($baseWorkDir . $uploadPath, api_get_permissions_for_new_directories(), true);
136
        }
137
138
        $exerciseInfo['name'] = preg_replace('/\.(zip|txt)$/i', '', basename($file));
139
        $exerciseInfo['question'] = [];
140
141
        if (!preg_match('/\.(zip|txt)$/i', $file)) {
142
            return get_lang('You must upload a .zip or .txt file');
143
        }
144
145
        $result = aiken_parse_file($exerciseInfo, $file);
146
147
        if ($result !== true) {
148
            return $result;
149
        }
150
    } elseif (!empty($request)) {
151
        $exerciseInfo['name'] = $request['quiz_name'];
152
        $exerciseInfo['total_weight'] = !empty($_POST['ai_total_weight']) ? (int) ($_POST['ai_total_weight']) : (int) $request['nro_questions'];
153
        $exerciseInfo['question'] = [];
154
        $exerciseInfo['course_id'] = api_get_course_int_id();
155
        setExerciseInfoFromAikenText($request['aiken_format'], $exerciseInfo);
156
    }
157
158
    return create_exercise_from_aiken($exerciseInfo, $fileIsSet ? $baseWorkDir . $uploadPath : null);
159
}
160
161
/**
162
 * Creates an exercise from Aiken format data.
163
 */
164
function create_exercise_from_aiken(array $exerciseInfo, ?string $workDir): int|false
165
{
166
    if (empty($exerciseInfo)) {
167
        return false;
168
    }
169
170
    // 1. Create a new exercise
171
    $exercise = new Exercise();
172
    $exercise->exercise = $exerciseInfo['name'];
173
    $exercise->save();
174
    $lastExerciseId = $exercise->getId();
175
176
    if (!$lastExerciseId) {
177
        return false;
178
    }
179
180
    // Database table references
181
    $tableQuestion = Database::get_course_table(TABLE_QUIZ_QUESTION);
182
    $tableAnswer = Database::get_course_table(TABLE_QUIZ_ANSWER);
183
    $courseId = api_get_course_int_id();
184
185
    // 2. Iterate over each question in the parsed Aiken data
186
    foreach ($exerciseInfo['question'] as $index => $questionData) {
187
        if (!isset($questionData['title'])) {
188
            continue;
189
        }
190
191
192
        // 3. Create a new question
193
        $question = new Aiken2Question();
194
        $question->type = $questionData['type'];
195
        $question->setAnswer();
196
        $question->updateTitle($questionData['title']);
197
198
        if (isset($questionData['description'])) {
199
            $question->updateDescription($questionData['description']);
200
        }
201
202
        // Determine question type
203
        $type = $question->selectType();
204
        $question->type = constant($type);
205
206
        // Try to save the question
207
        try {
208
            $question->save($exercise);
209
            $lastQuestionId = $question->id;
210
211
            if (!$lastQuestionId) {
212
                throw new Exception("Question ID is NULL after saving.");
213
            }
214
215
        } catch (Exception $e) {
216
            error_log("[ERROR] create_exercise_from_aiken: Error saving question '{$questionData['title']}' - " . $e->getMessage());
217
            continue;
218
        }
219
220
        // 4. Create answers for the question
221
        $answer = new Answer($lastQuestionId, $courseId, $exercise, false);
222
        $answer->new_nbrAnswers = count($questionData['answer']);
223
        $maxScore = 0;
224
        $scoreFromFile = $questionData['score'] ?? 0;
225
226
        foreach ($questionData['answer'] as $key => $answerData) {
227
            $answerIndex = $key + 1;
228
            $answer->new_answer[$answerIndex] = $answerData['value'];
229
            $answer->new_position[$answerIndex] = $answerIndex;
230
            $answer->new_comment[$answerIndex] = '';
231
232
            // Check if the answer is correct
233
            if (isset($questionData['correct_answers']) && in_array($answerIndex, $questionData['correct_answers'])) {
234
                $answer->new_correct[$answerIndex] = 1;
235
                if (isset($questionData['feedback'])) {
236
                    $answer->new_comment[$answerIndex] = $questionData['feedback'];
237
                }
238
239
                // Set answer weight (score)
240
                if (isset($questionData['weighting'])) {
241
                    $answer->new_weighting[$answerIndex] = $questionData['weighting'][0];
242
                    $maxScore += $questionData['weighting'][0];
243
                }
244
245
            } else {
246
                $answer->new_correct[$answerIndex] = 0;
247
            }
248
249
            if (!empty($scoreFromFile) && $answer->new_correct[$answerIndex]) {
250
                $answer->new_weighting[$answerIndex] = $scoreFromFile;
251
            }
252
253
            // Insert answer into database
254
            $params = [
255
                'c_id' => $courseId,
256
                'question_id' => $lastQuestionId,
257
                'answer' => $answer->new_answer[$answerIndex],
258
                'correct' => $answer->new_correct[$answerIndex],
259
                'comment' => $answer->new_comment[$answerIndex],
260
                'ponderation' => $answer->new_weighting[$answerIndex] ?? 0,
261
                'position' => $answer->new_position[$answerIndex],
262
                'hotspot_coordinates' => '',
263
                'hotspot_type' => '',
264
            ];
265
266
            $answerId = Database::insert($tableAnswer, $params);
267
268
            if (!$answerId) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $answerId of type false|integer is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
269
                error_log("[ERROR] create_exercise_from_aiken: Failed to insert answer for question ID: $lastQuestionId");
270
                continue;
271
            }
272
273
            Database::update($tableAnswer, ['iid' => $answerId], ['iid = ?' => [$answerId]]);
274
        }
275
276
        // Update question score
277
        if (!empty($scoreFromFile)) {
278
            $maxScore = $scoreFromFile;
279
        }
280
281
        Database::update($tableQuestion, ['ponderation' => $maxScore], ['iid = ?' => [$lastQuestionId]]);
282
    }
283
284
    // 5. Clean up temporary files if needed
285
    if ($workDir) {
286
        my_delete($workDir);
0 ignored issues
show
Bug introduced by
The function my_delete was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

286
        /** @scrutinizer ignore-call */ 
287
        my_delete($workDir);
Loading history...
287
    }
288
289
    return $lastExerciseId;
290
}
291
292
/**
293
 * Parses an Aiken file and builds an array of exercise + questions to be
294
 * imported by the import_exercise() function.
295
 *
296
 * @param array $exercise_info The reference to the array in which to store the questions
297
 * @param string $file
298
 *
299
 * @return string|bool True on success, error message on error
300
 * @assert ('','','') === false
301
 */
302
function aiken_parse_file(&$exercise_info, $file)
303
{
304
    if (!is_file($file)) {
305
        return 'FileNotFound';
306
    }
307
308
    $text = file_get_contents($file);
309
    $detect = mb_detect_encoding($text, 'ASCII', true);
310
    if ('ASCII' === $detect) {
311
        $data = explode("\n", $text);
312
    } else {
313
        $text = str_ireplace(["\x0D", "\r\n"], "\n", $text); // Removes ^M char from win files.
314
        $data = explode("\n\n", $text);
315
    }
316
317
    $question_index = 0;
318
    $answers_array = [];
319
    foreach ($data as $line => $info) {
320
        $info = trim($info);
321
        if (empty($info)) {
322
            continue;
323
        }
324
        //make sure it is transformed from iso-8859-1 to utf-8 if in that form
325
        if (!mb_check_encoding($info, 'utf-8') && mb_check_encoding($info, 'iso-8859-1')) {
326
            $info = utf8_encode($info);
327
        }
328
        $exercise_info['question'][$question_index]['type'] = 'MCUA';
329
        if (preg_match('/^([A-Za-z])(\)|\.)\s(.*)/', $info, $matches)) {
330
            //adding one of the possible answers
331
            $exercise_info['question'][$question_index]['answer'][]['value'] = $matches[3];
332
            $answers_array[] = $matches[1];
333
        } elseif (preg_match('/^ANSWER:\s?([A-Z])\s?/', $info, $matches)) {
334
            //the correct answers
335
            $correct_answer_index = array_search($matches[1], $answers_array);
336
            $exercise_info['question'][$question_index]['correct_answers'][] = $correct_answer_index + 1;
337
            //weight for correct answer
338
            $exercise_info['question'][$question_index]['weighting'][$correct_answer_index] = 1;
339
            $next = $line + 1;
340
341
            if (false !== strpos($data[$next], 'ANSWER_EXPLANATION:')) {
342
                continue;
343
            }
344
345
            if (false !== strpos($data[$next], 'DESCRIPTION:')) {
346
                continue;
347
            }
348
            // Check if next has score, otherwise loop too next question.
349
            if (false === strpos($data[$next], 'SCORE:')) {
350
                $answers_array = [];
351
                $question_index++;
352
                continue;
353
            }
354
        } elseif (preg_match('/^SCORE:\s?(.*)/', $info, $matches)) {
355
            $exercise_info['question'][$question_index]['score'] = (float) $matches[1];
356
            $answers_array = [];
357
            $question_index++;
358
            continue;
359
        } elseif (preg_match('/^DESCRIPTION:\s?(.*)/', $info, $matches)) {
360
            $exercise_info['question'][$question_index]['description'] = $matches[1];
361
            $next = $line + 1;
362
363
            if (false !== strpos($data[$next], 'ANSWER_EXPLANATION:')) {
364
                continue;
365
            }
366
            // Check if next has score, otherwise loop too next question.
367
            if (false === strpos($data[$next], 'SCORE:')) {
368
                $answers_array = [];
369
                $question_index++;
370
                continue;
371
            }
372
        } elseif (preg_match('/^ANSWER_EXPLANATION:\s?(.*)/', $info, $matches)) {
373
            //Comment of correct answer
374
            $correct_answer_index = array_search($matches[1], $answers_array);
375
            $exercise_info['question'][$question_index]['feedback'] = $matches[1];
376
            $next = $line + 1;
377
            // Check if next has score, otherwise loop too next question.
378
            if (false === strpos($data[$next], 'SCORE:')) {
379
                $answers_array = [];
380
                $question_index++;
381
                continue;
382
            }
383
        } elseif (preg_match('/^TEXTO_CORRECTA:\s?(.*)/', $info, $matches)) {
384
            //Comment of correct answer (Spanish e-ducativa format)
385
            $correct_answer_index = array_search($matches[1], $answers_array);
386
            $exercise_info['question'][$question_index]['feedback'] = $matches[1];
387
        } elseif (preg_match('/^T:\s?(.*)/', $info, $matches)) {
388
            //Question Title
389
            $correct_answer_index = array_search($matches[1], $answers_array);
390
            $exercise_info['question'][$question_index]['title'] = $matches[1];
391
        } elseif (preg_match('/^TAGS:\s?([A-Z])\s?/', $info, $matches)) {
392
            //TAGS for chamilo >= 1.10
393
            $exercise_info['question'][$question_index]['answer_tags'] = explode(',', $matches[1]);
394
        } elseif (preg_match('/^ETIQUETAS:\s?([A-Z])\s?/', $info, $matches)) {
395
            //TAGS for chamilo >= 1.10 (Spanish e-ducativa format)
396
            $exercise_info['question'][$question_index]['answer_tags'] = explode(',', $matches[1]);
397
        } elseif (empty($info)) {
398
            /*if (empty($exercise_info['question'][$question_index]['title'])) {
399
                $exercise_info['question'][$question_index]['title'] = $info;
400
            }
401
            //moving to next question (tolerate \r\n or just \n)
402
            if (empty($exercise_info['question'][$question_index]['correct_answers'])) {
403
                error_log('Aiken: Error in question index '.$question_index.': no correct answer defined');
404
405
                return 'ExerciseAikenErrorNoCorrectAnswerDefined';
406
            }
407
            if (empty($exercise_info['question'][$question_index]['answer'])) {
408
                error_log('Aiken: Error in question index '.$question_index.': no answer option given');
409
410
                return 'ExerciseAikenErrorNoAnswerOptionGiven';
411
            }
412
            $question_index++;
413
            //emptying answers array when moving to next question
414
            $answers_array = [];
415
        } else {
416
            if (empty($exercise_info['question'][$question_index]['title'])) {
417
                $exercise_info['question'][$question_index]['title'] = $info;
418
            }
419
            /*$question_index++;
420
            //emptying answers array when moving to next question
421
            $answers_array = [];
422
            $new_question = true;*/
423
        }
424
    }
425
    $total_questions = count($exercise_info['question']);
426
    $total_weight = !empty($_POST['total_weight']) ? (int) ($_POST['total_weight']) : 20;
427
    foreach ($exercise_info['question'] as $key => $question) {
428
        if (!isset($exercise_info['question'][$key]['weighting'])) {
429
            continue;
430
        }
431
        $exercise_info['question'][$key]['weighting'][current(array_keys($exercise_info['question'][$key]['weighting']))] = $total_weight / $total_questions;
432
    }
433
434
    return true;
435
}
436
437
/**
438
 * Imports the zip file.
439
 *
440
 * @param array $array_file ($_FILES)
441
 */
442
function aiken_import_file(array $array_file)
443
{
444
    $unzip = 0;
445
    $process = process_uploaded_file($array_file, false);
446
    if (preg_match('/\.(zip|txt)$/i', $array_file['name'])) {
447
        // if it's a zip, allow zip upload
448
        $unzip = 1;
449
    }
450
451
    if ($process && 1 == $unzip) {
452
        $imported = aiken_import_exercise($array_file['name']);
453
        if (is_numeric($imported) && !empty($imported)) {
454
            Display::addFlash(Display::return_message(get_lang('Uploaded.')));
455
456
            return $imported;
457
        } else {
458
            Display::addFlash(Display::return_message(get_lang($imported), 'error'));
459
460
            return false;
461
        }
462
    }
463
464
    return false;
465
}
466
467
/**
468
 * Generates the Aiken question form with AI integration.
469
 */
470
function generateAikenForm()
471
{
472
    if ('true' !== api_get_setting('ai_helpers.enable_ai_helpers')) {
473
        return false;
474
    }
475
476
    // Get AI providers configuration from settings
477
    $aiProvidersJson = api_get_setting('ai_helpers.ai_providers');
478
479
    $configuredApi = api_get_setting('ai_helpers.default_ai_provider');
480
481
    $availableApis = json_decode($aiProvidersJson, true) ?? [];
482
    $hasSingleApi = count($availableApis) === 1 || isset($availableApis[$configuredApi]);
483
484
    $form = new FormValidator(
485
        'aiken_generate',
486
        'post',
487
        api_get_self()."?".api_get_cidreq(),
488
        null
489
    );
490
    $form->addElement('header', get_lang('AI Questions Generator'));
491
492
    if ($hasSingleApi) {
493
        $apiName = $availableApis[$configuredApi]['model'] ?? $configuredApi;
494
        $form->addHtml('<div style="margin-bottom: 10px; font-size: 14px; color: #555;">'
495
            .sprintf(get_lang('Using AI provider %s'), '<strong>'.htmlspecialchars($apiName).'</strong>').'</div>');
496
    }
497
498
    $form->addHtml('<div class="alert alert-info">
499
        <strong>'.get_lang('Aiken Format Example').'</strong><br>
500
        What is the capital of France?<br>
501
        A. Berlin<br>
502
        B. Madrid<br>
503
        C. Paris<br>
504
        D. Rome<br>
505
        ANSWER: C
506
    </div>');
507
508
    $form->addElement('text', 'quiz_name', get_lang('Questions topic'));
509
    $form->addRule('quiz_name', get_lang('This field is required'), 'required');
510
    $form->addElement('number', 'nro_questions', get_lang('Number of questions'));
511
    $form->addRule('nro_questions', get_lang('This field is required'), 'required');
512
513
    $options = [
514
        'multiple_choice' => get_lang('Multiple answer'),
515
    ];
516
517
    $form->addSelect(
518
        'question_type',
519
        get_lang('Question yype'),
520
        $options
521
    );
522
523
    if (!$hasSingleApi) {
524
        $form->addSelect(
525
            'ai_provider',
526
            get_lang('Ai provider'),
527
            array_combine(array_keys($availableApis), array_keys($availableApis))
528
        );
529
    }
530
531
    $generateUrl = api_get_path(WEB_PATH).'ai/generate_aiken';
532
533
    $courseInfo = api_get_course_info();
534
    $language = $courseInfo['language'];
535
    $form->addHtml('<script>
536
    $(function () {
537
        $("#aiken-area").hide();
538
539
        $("#generate-aiken").on("click", function (e) {
540
            e.preventDefault();
541
            e.stopPropagation();
542
543
            var btnGenerate = $(this);
544
            var quizName = $("[name=\'quiz_name\']").val().trim();
545
            var nroQ = parseInt($("[name=\'nro_questions\']").val());
546
            var qType = $("[name=\'question_type\']").val();'
547
        . (!$hasSingleApi ? 'var provider = $("[name=\'ai_provider\']").val();' : 'var provider = "'.$configuredApi.'";') .
0 ignored issues
show
Bug introduced by
Are you sure $configuredApi of type array|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

547
        . (!$hasSingleApi ? 'var provider = $("[name=\'ai_provider\']").val();' : 'var provider = "'./** @scrutinizer ignore-type */ $configuredApi.'";') .
Loading history...
548
        'var isValid = true;
549
550
            // Remove previous error messages
551
            $(".error-message").remove();
552
553
            // Validate quiz name
554
            if (quizName === "") {
555
                $("[name=\'quiz_name\']").after("<div class=\'error-message\' style=\'color: red;\'>'.get_lang('This field is required').'</div>");
556
                isValid = false;
557
            }
558
559
            // Validate number of questions
560
            if (isNaN(nroQ) || nroQ <= 0) {
561
                $("[name=\'nro_questions\']").after("<div class=\'error-message\' style=\'color: red;\'>'.get_lang('Please enter a valid number of questions').'</div>");
562
                isValid = false;
563
            }
564
565
            if (!isValid) {
566
                return; // Stop execution if validation fails
567
            }
568
569
            btnGenerate.attr("disabled", true);
570
            btnGenerate.text("'.get_lang('Please wait this could take a while').'");
571
572
            $("#textarea-aiken").text("");
573
            $("#aiken-area").hide();
574
575
            var requestData = JSON.stringify({
576
                "quiz_name": quizName,
577
                "nro_questions": nroQ,
578
                "question_type": qType,
579
                "language": "'.$language.'",
580
                "ai_provider": provider
581
            });
582
583
            $.ajax({
584
                url: "'.$generateUrl.'",
585
                type: "POST",
586
                contentType: "application/json",
587
                data: requestData,
588
                dataType: "json",
589
                success: function (data) {
590
                    btnGenerate.attr("disabled", false);
591
                    btnGenerate.text("'.get_lang('Generate').'");
592
593
                    if (data.success) {
594
                        $("#aiken-area").show();
595
                        $("#textarea-aiken").text(data.text);
596
                        $("#textarea-aiken").focus();
597
                    } else {
598
                        alert("'.get_lang('Error occurred').': " + data.text);
599
                    }
600
                },
601
                 error: function (jqXHR) {
602
                    btnGenerate.attr("disabled", false);
603
                    btnGenerate.text("'.get_lang('Generate').'");
604
605
                    try {
606
                        var response = JSON.parse(jqXHR.responseText);
607
                        var errorMessage = "'.get_lang('An unexpected error occurred. Please try again later.').'";
608
609
                        if (response && response.text) {
610
                            errorMessage = response.text;
611
                        }
612
613
                        alert("'.get_lang('Request failed').': " + errorMessage);
614
                    } catch (e) {
615
                        alert("'.get_lang('Request failed').': " + "'.get_lang('An unexpected error occurred. Please contact support.').'");
616
                    }
617
                }
618
            });
619
        });
620
    });
621
</script>');
622
623
    $form->addButtonSend(get_lang('Generate Aiken'), 'submit', false, ['id' => 'generate-aiken']);
624
    $form->addHtml('<div id="aiken-area">');
625
    $form->addElement(
626
        'textarea',
627
        'aiken_format',
628
        get_lang('Answers'),
629
        [
630
            'id' => 'textarea-aiken',
631
            'style' => 'width: 100%; height: 250px;',
632
        ]
633
    );
634
    $form->addElement('number', 'ai_total_weight', get_lang('Total weight'));
635
    $form->addButtonImport(get_lang('Import'), 'submit_aiken_generated');
636
    $form->addHtml('</div>');
637
638
    echo $form->returnForm();
639
}
640