@thanh01.pmt/interactive-quiz-kit 1.0.21 → 1.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai.cjs +3263 -1117
- package/dist/ai.js +3256 -1118
- package/dist/authoring.cjs +6717 -4885
- package/dist/authoring.js +6606 -4789
- package/dist/index.cjs +1898 -1087
- package/dist/index.js +1894 -1088
- package/dist/player.cjs +1609 -623
- package/dist/player.js +1554 -570
- package/dist/react-ui.cjs +13310 -7005
- package/dist/react-ui.js +12762 -6511
- package/package.json +53 -25
package/dist/player.cjs
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var React28 = require('react');
|
|
4
4
|
var ReactDOM = require('react-dom/client');
|
|
5
|
+
var reactI18next = require('react-i18next');
|
|
6
|
+
var zod = require('zod');
|
|
7
|
+
var genkit = require('genkit');
|
|
8
|
+
var googleai = require('@genkit-ai/googleai');
|
|
5
9
|
var RadioGroupPrimitive = require('@radix-ui/react-radio-group');
|
|
6
10
|
var lucideReact = require('lucide-react');
|
|
7
11
|
var clsx = require('clsx');
|
|
@@ -16,6 +20,11 @@ var rehypeKatex = require('rehype-katex');
|
|
|
16
20
|
var CheckboxPrimitive = require('@radix-ui/react-checkbox');
|
|
17
21
|
var reactSlot = require('@radix-ui/react-slot');
|
|
18
22
|
var SelectPrimitive = require('@radix-ui/react-select');
|
|
23
|
+
var CodeMirror = require('@uiw/react-codemirror');
|
|
24
|
+
var langCpp = require('@codemirror/lang-cpp');
|
|
25
|
+
var langJavascript = require('@codemirror/lang-javascript');
|
|
26
|
+
var langPython = require('@codemirror/lang-python');
|
|
27
|
+
var TabsPrimitive = require('@radix-ui/react-tabs');
|
|
19
28
|
var ProgressPrimitive = require('@radix-ui/react-progress');
|
|
20
29
|
var AccordionPrimitive = require('@radix-ui/react-accordion');
|
|
21
30
|
var ScrollAreaPrimitive = require('@radix-ui/react-scroll-area');
|
|
@@ -40,7 +49,7 @@ function _interopNamespace(e) {
|
|
|
40
49
|
return Object.freeze(n);
|
|
41
50
|
}
|
|
42
51
|
|
|
43
|
-
var
|
|
52
|
+
var React28__namespace = /*#__PURE__*/_interopNamespace(React28);
|
|
44
53
|
var ReactDOM__default = /*#__PURE__*/_interopDefault(ReactDOM);
|
|
45
54
|
var RadioGroupPrimitive__namespace = /*#__PURE__*/_interopNamespace(RadioGroupPrimitive);
|
|
46
55
|
var LabelPrimitive__namespace = /*#__PURE__*/_interopNamespace(LabelPrimitive);
|
|
@@ -51,6 +60,8 @@ var remarkMath__default = /*#__PURE__*/_interopDefault(remarkMath);
|
|
|
51
60
|
var rehypeKatex__default = /*#__PURE__*/_interopDefault(rehypeKatex);
|
|
52
61
|
var CheckboxPrimitive__namespace = /*#__PURE__*/_interopNamespace(CheckboxPrimitive);
|
|
53
62
|
var SelectPrimitive__namespace = /*#__PURE__*/_interopNamespace(SelectPrimitive);
|
|
63
|
+
var CodeMirror__default = /*#__PURE__*/_interopDefault(CodeMirror);
|
|
64
|
+
var TabsPrimitive__namespace = /*#__PURE__*/_interopNamespace(TabsPrimitive);
|
|
54
65
|
var ProgressPrimitive__namespace = /*#__PURE__*/_interopNamespace(ProgressPrimitive);
|
|
55
66
|
var AccordionPrimitive__namespace = /*#__PURE__*/_interopNamespace(AccordionPrimitive);
|
|
56
67
|
var ScrollAreaPrimitive__namespace = /*#__PURE__*/_interopNamespace(ScrollAreaPrimitive);
|
|
@@ -334,6 +345,847 @@ var SCORMService = class {
|
|
|
334
345
|
}
|
|
335
346
|
};
|
|
336
347
|
|
|
348
|
+
// src/services/evaluators/multiple-choice-evaluator.ts
|
|
349
|
+
var MultipleChoiceEvaluator = class {
|
|
350
|
+
async evaluate(question, answer) {
|
|
351
|
+
var _a;
|
|
352
|
+
const points = (_a = question.points) != null ? _a : 0;
|
|
353
|
+
const correctAnswerId = question.correctAnswerId;
|
|
354
|
+
const isCorrect = answer === correctAnswerId;
|
|
355
|
+
const correctOption = question.options.find((opt) => opt.id === correctAnswerId);
|
|
356
|
+
const correctAnswerDetail = {
|
|
357
|
+
id: correctAnswerId,
|
|
358
|
+
value: (correctOption == null ? void 0 : correctOption.text) || ""
|
|
359
|
+
};
|
|
360
|
+
return Promise.resolve({
|
|
361
|
+
isCorrect,
|
|
362
|
+
correctAnswer: correctAnswerDetail,
|
|
363
|
+
pointsEarned: isCorrect ? points : 0
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
// src/services/evaluators/multiple-response-evaluator.ts
|
|
369
|
+
var MultipleResponseEvaluator = class {
|
|
370
|
+
async evaluate(question, answer) {
|
|
371
|
+
var _a;
|
|
372
|
+
const points = (_a = question.points) != null ? _a : 0;
|
|
373
|
+
const correctAnswerIds = question.correctAnswerIds;
|
|
374
|
+
let isCorrect = false;
|
|
375
|
+
if (Array.isArray(answer)) {
|
|
376
|
+
const userAnswerSet = new Set(answer);
|
|
377
|
+
const correctAnswerSet = new Set(correctAnswerIds);
|
|
378
|
+
isCorrect = userAnswerSet.size === correctAnswerSet.size && [...userAnswerSet].every((id) => correctAnswerSet.has(id));
|
|
379
|
+
}
|
|
380
|
+
const correctValues = correctAnswerIds.map(
|
|
381
|
+
(id) => {
|
|
382
|
+
var _a2;
|
|
383
|
+
return ((_a2 = question.options.find((opt) => opt.id === id)) == null ? void 0 : _a2.text) || "";
|
|
384
|
+
}
|
|
385
|
+
);
|
|
386
|
+
const correctAnswerDetail = {
|
|
387
|
+
id: correctAnswerIds,
|
|
388
|
+
value: correctValues
|
|
389
|
+
};
|
|
390
|
+
return Promise.resolve({
|
|
391
|
+
isCorrect,
|
|
392
|
+
correctAnswer: correctAnswerDetail,
|
|
393
|
+
pointsEarned: isCorrect ? points : 0
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
// src/services/evaluators/true-false-evaluator.ts
|
|
399
|
+
var TrueFalseEvaluator = class {
|
|
400
|
+
async evaluate(question, answer) {
|
|
401
|
+
var _a;
|
|
402
|
+
const points = (_a = question.points) != null ? _a : 0;
|
|
403
|
+
const correctAnswer = question.correctAnswer;
|
|
404
|
+
let userAnswer = answer;
|
|
405
|
+
if (typeof answer === "string") {
|
|
406
|
+
userAnswer = answer.toLowerCase() === "true";
|
|
407
|
+
}
|
|
408
|
+
const isCorrect = typeof userAnswer === "boolean" && userAnswer === correctAnswer;
|
|
409
|
+
const correctAnswerDetail = {
|
|
410
|
+
id: null,
|
|
411
|
+
value: correctAnswer
|
|
412
|
+
};
|
|
413
|
+
return Promise.resolve({
|
|
414
|
+
isCorrect,
|
|
415
|
+
correctAnswer: correctAnswerDetail,
|
|
416
|
+
pointsEarned: isCorrect ? points : 0
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
// src/services/evaluators/short-answer-evaluator.ts
|
|
422
|
+
var ShortAnswerEvaluator = class {
|
|
423
|
+
async evaluate(question, answer) {
|
|
424
|
+
var _a, _b;
|
|
425
|
+
const points = (_a = question.points) != null ? _a : 0;
|
|
426
|
+
let isCorrect = false;
|
|
427
|
+
if (typeof answer === "string") {
|
|
428
|
+
const userAnswerTrimmed = answer.trim();
|
|
429
|
+
const caseSensitive = (_b = question.isCaseSensitive) != null ? _b : false;
|
|
430
|
+
isCorrect = question.acceptedAnswers.some(
|
|
431
|
+
(accAns) => caseSensitive ? accAns.trim() === userAnswerTrimmed : accAns.trim().toLowerCase() === userAnswerTrimmed.toLowerCase()
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
const correctAnswerDetail = {
|
|
435
|
+
id: null,
|
|
436
|
+
value: question.acceptedAnswers
|
|
437
|
+
};
|
|
438
|
+
return Promise.resolve({
|
|
439
|
+
isCorrect,
|
|
440
|
+
correctAnswer: correctAnswerDetail,
|
|
441
|
+
pointsEarned: isCorrect ? points : 0
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
// src/services/evaluators/numeric-evaluator.ts
|
|
447
|
+
var NumericEvaluator = class {
|
|
448
|
+
async evaluate(question, answer) {
|
|
449
|
+
var _a;
|
|
450
|
+
const points = (_a = question.points) != null ? _a : 0;
|
|
451
|
+
let isCorrect = false;
|
|
452
|
+
if (typeof answer === "string" || typeof answer === "number") {
|
|
453
|
+
const userAnswerNum = parseFloat(String(answer));
|
|
454
|
+
if (!isNaN(userAnswerNum)) {
|
|
455
|
+
isCorrect = question.tolerance != null ? Math.abs(userAnswerNum - question.answer) <= question.tolerance : userAnswerNum === question.answer;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const correctAnswerDetail = {
|
|
459
|
+
id: null,
|
|
460
|
+
value: question.answer
|
|
461
|
+
};
|
|
462
|
+
return Promise.resolve({
|
|
463
|
+
isCorrect,
|
|
464
|
+
correctAnswer: correctAnswerDetail,
|
|
465
|
+
pointsEarned: isCorrect ? points : 0
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
// src/services/evaluators/sequence-evaluator.ts
|
|
471
|
+
var SequenceEvaluator = class {
|
|
472
|
+
async evaluate(question, answer) {
|
|
473
|
+
var _a;
|
|
474
|
+
const points = (_a = question.points) != null ? _a : 0;
|
|
475
|
+
let isCorrect = false;
|
|
476
|
+
if (Array.isArray(answer) && answer.length === question.correctOrder.length) {
|
|
477
|
+
isCorrect = answer.every((itemId, index) => itemId === question.correctOrder[index]);
|
|
478
|
+
}
|
|
479
|
+
const correctValues = question.correctOrder.map(
|
|
480
|
+
(id) => {
|
|
481
|
+
var _a2;
|
|
482
|
+
return ((_a2 = question.items.find((item) => item.id === id)) == null ? void 0 : _a2.content) || "";
|
|
483
|
+
}
|
|
484
|
+
);
|
|
485
|
+
const correctAnswerDetail = {
|
|
486
|
+
id: question.correctOrder,
|
|
487
|
+
value: correctValues
|
|
488
|
+
};
|
|
489
|
+
return Promise.resolve({
|
|
490
|
+
isCorrect,
|
|
491
|
+
correctAnswer: correctAnswerDetail,
|
|
492
|
+
pointsEarned: isCorrect ? points : 0
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
// src/services/evaluators/matching-evaluator.ts
|
|
498
|
+
var MatchingEvaluator = class {
|
|
499
|
+
async evaluate(question, answer) {
|
|
500
|
+
var _a;
|
|
501
|
+
const points = (_a = question.points) != null ? _a : 0;
|
|
502
|
+
let isCorrect = false;
|
|
503
|
+
if (typeof answer === "object" && answer !== null && !Array.isArray(answer)) {
|
|
504
|
+
const userAnswerMap = answer;
|
|
505
|
+
isCorrect = question.correctAnswerMap.length === Object.keys(userAnswerMap).length && question.correctAnswerMap.every((map) => userAnswerMap[map.promptId] === map.optionId);
|
|
506
|
+
}
|
|
507
|
+
const correctMap = question.correctAnswerMap.reduce((acc, curr) => {
|
|
508
|
+
var _a2, _b;
|
|
509
|
+
const promptText = ((_a2 = question.prompts.find((p) => p.id === curr.promptId)) == null ? void 0 : _a2.content) || "";
|
|
510
|
+
const optionText = ((_b = question.options.find((o) => o.id === curr.optionId)) == null ? void 0 : _b.content) || "";
|
|
511
|
+
acc[promptText] = optionText;
|
|
512
|
+
return acc;
|
|
513
|
+
}, {});
|
|
514
|
+
const correctAnswerDetail = {
|
|
515
|
+
id: null,
|
|
516
|
+
value: correctMap
|
|
517
|
+
};
|
|
518
|
+
return Promise.resolve({
|
|
519
|
+
isCorrect,
|
|
520
|
+
correctAnswer: correctAnswerDetail,
|
|
521
|
+
pointsEarned: isCorrect ? points : 0
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
// src/services/evaluators/fill-in-the-blanks-evaluator.ts
|
|
527
|
+
var FillInTheBlanksEvaluator = class {
|
|
528
|
+
async evaluate(question, answer) {
|
|
529
|
+
var _a;
|
|
530
|
+
const points = (_a = question.points) != null ? _a : 0;
|
|
531
|
+
let isCorrect = false;
|
|
532
|
+
if (typeof answer === "object" && answer !== null && !Array.isArray(answer)) {
|
|
533
|
+
const userAnswerMap = answer;
|
|
534
|
+
isCorrect = question.answers.length > 0 && question.answers.every((correctAnsDef) => {
|
|
535
|
+
var _a2, _b;
|
|
536
|
+
const userValForBlank = (_a2 = userAnswerMap[correctAnsDef.blankId]) == null ? void 0 : _a2.trim();
|
|
537
|
+
if (userValForBlank === void 0) return false;
|
|
538
|
+
const caseSensitive = (_b = question.isCaseSensitive) != null ? _b : false;
|
|
539
|
+
return correctAnsDef.acceptedValues.some(
|
|
540
|
+
(accVal) => caseSensitive ? accVal.trim() === userValForBlank : accVal.trim().toLowerCase() === userValForBlank.toLowerCase()
|
|
541
|
+
);
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
const correctMap = question.answers.reduce((acc, curr) => {
|
|
545
|
+
acc[curr.blankId] = curr.acceptedValues.join(" | ");
|
|
546
|
+
return acc;
|
|
547
|
+
}, {});
|
|
548
|
+
const correctAnswerDetail = {
|
|
549
|
+
id: null,
|
|
550
|
+
value: correctMap
|
|
551
|
+
};
|
|
552
|
+
return Promise.resolve({
|
|
553
|
+
isCorrect,
|
|
554
|
+
correctAnswer: correctAnswerDetail,
|
|
555
|
+
pointsEarned: isCorrect ? points : 0
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
// src/services/evaluators/drag-and-drop-evaluator.ts
|
|
561
|
+
var DragAndDropEvaluator = class {
|
|
562
|
+
async evaluate(question, answer) {
|
|
563
|
+
var _a;
|
|
564
|
+
const points = (_a = question.points) != null ? _a : 0;
|
|
565
|
+
let isCorrect = false;
|
|
566
|
+
if (typeof answer === "object" && answer !== null && !Array.isArray(answer)) {
|
|
567
|
+
const userAnswerMap = answer;
|
|
568
|
+
isCorrect = question.answerMap.length === Object.keys(userAnswerMap).length && question.answerMap.every((map) => userAnswerMap[map.draggableId] === map.dropZoneId);
|
|
569
|
+
}
|
|
570
|
+
const correctMap = question.answerMap.reduce((acc, curr) => {
|
|
571
|
+
var _a2, _b;
|
|
572
|
+
const draggableText = ((_a2 = question.draggableItems.find((d) => d.id === curr.draggableId)) == null ? void 0 : _a2.content) || "";
|
|
573
|
+
const dropZoneText = ((_b = question.dropZones.find((z3) => z3.id === curr.dropZoneId)) == null ? void 0 : _b.label) || "";
|
|
574
|
+
acc[draggableText] = dropZoneText;
|
|
575
|
+
return acc;
|
|
576
|
+
}, {});
|
|
577
|
+
const correctAnswerDetail = {
|
|
578
|
+
id: null,
|
|
579
|
+
value: correctMap
|
|
580
|
+
};
|
|
581
|
+
return Promise.resolve({
|
|
582
|
+
isCorrect,
|
|
583
|
+
correctAnswer: correctAnswerDetail,
|
|
584
|
+
pointsEarned: isCorrect ? points : 0
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
// src/services/evaluators/hotspot-evaluator.ts
|
|
590
|
+
var HotspotEvaluator = class {
|
|
591
|
+
async evaluate(question, answer) {
|
|
592
|
+
var _a;
|
|
593
|
+
const points = (_a = question.points) != null ? _a : 0;
|
|
594
|
+
let isCorrect = false;
|
|
595
|
+
if (Array.isArray(answer)) {
|
|
596
|
+
const userAnswerSet = new Set(answer);
|
|
597
|
+
const correctAnswerSet = new Set(question.correctHotspotIds);
|
|
598
|
+
isCorrect = userAnswerSet.size === correctAnswerSet.size && [...userAnswerSet].every((id) => correctAnswerSet.has(id));
|
|
599
|
+
}
|
|
600
|
+
const correctValues = question.correctHotspotIds.map(
|
|
601
|
+
(id) => {
|
|
602
|
+
var _a2;
|
|
603
|
+
return ((_a2 = question.hotspots.find((h) => h.id === id)) == null ? void 0 : _a2.description) || id;
|
|
604
|
+
}
|
|
605
|
+
);
|
|
606
|
+
const correctAnswerDetail = {
|
|
607
|
+
id: question.correctHotspotIds,
|
|
608
|
+
value: correctValues
|
|
609
|
+
};
|
|
610
|
+
return Promise.resolve({
|
|
611
|
+
isCorrect,
|
|
612
|
+
correctAnswer: correctAnswerDetail,
|
|
613
|
+
pointsEarned: isCorrect ? points : 0
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
// src/services/evaluators/programming-evaluator.ts
|
|
619
|
+
var ProgrammingEvaluator = class {
|
|
620
|
+
async evaluate(question, answer) {
|
|
621
|
+
var _a, _b;
|
|
622
|
+
const points = (_a = question.points) != null ? _a : 0;
|
|
623
|
+
let isCorrect = false;
|
|
624
|
+
if (typeof answer === "string" && typeof question.solutionGeneratedCode === "string") {
|
|
625
|
+
if (typeof window !== "undefined" && ((_b = window.Blockly) == null ? void 0 : _b.JavaScript)) {
|
|
626
|
+
const LocalBlockly = window.Blockly;
|
|
627
|
+
let generatedUserCode = "";
|
|
628
|
+
try {
|
|
629
|
+
const tempWorkspace = new LocalBlockly.Workspace();
|
|
630
|
+
const dom = LocalBlockly.Xml.textToDom(answer);
|
|
631
|
+
LocalBlockly.Xml.domToWorkspace(dom, tempWorkspace);
|
|
632
|
+
generatedUserCode = LocalBlockly.JavaScript.workspaceToCode(tempWorkspace) || "";
|
|
633
|
+
const normalize = (code) => code.replace(/\s+/g, " ").trim();
|
|
634
|
+
isCorrect = normalize(generatedUserCode) === normalize(question.solutionGeneratedCode);
|
|
635
|
+
tempWorkspace.dispose();
|
|
636
|
+
} catch (e) {
|
|
637
|
+
console.error(`Error generating code from user's ${question.questionType} XML for evaluation:`, e);
|
|
638
|
+
isCorrect = false;
|
|
639
|
+
}
|
|
640
|
+
} else {
|
|
641
|
+
console.warn(`Blockly library not available for ${question.questionType} evaluation. Skipping code comparison.`);
|
|
642
|
+
isCorrect = false;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
const correctAnswerDetail = {
|
|
646
|
+
id: null,
|
|
647
|
+
value: question.solutionGeneratedCode || ""
|
|
648
|
+
};
|
|
649
|
+
return Promise.resolve({
|
|
650
|
+
isCorrect,
|
|
651
|
+
correctAnswer: correctAnswerDetail,
|
|
652
|
+
pointsEarned: isCorrect ? points : 0
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
// src/utils/jsonUtils.ts
|
|
658
|
+
var JsonRepairEngine = class {
|
|
659
|
+
/**
|
|
660
|
+
* Attempts to repair unterminated strings in JSON.
|
|
661
|
+
* NOTE: This is a heuristic approach and may not be perfect for all cases.
|
|
662
|
+
*/
|
|
663
|
+
static repairUnterminatedStrings(jsonStr) {
|
|
664
|
+
let repaired = jsonStr;
|
|
665
|
+
let inString = false;
|
|
666
|
+
let escaped = false;
|
|
667
|
+
let lastQuoteIndex = -1;
|
|
668
|
+
for (let i = 0; i < repaired.length; i++) {
|
|
669
|
+
const char = repaired[i];
|
|
670
|
+
if (escaped) {
|
|
671
|
+
escaped = false;
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
if (char === "\\") {
|
|
675
|
+
escaped = true;
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
if (char === '"') {
|
|
679
|
+
inString = !inString;
|
|
680
|
+
if (inString) {
|
|
681
|
+
lastQuoteIndex = i;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
if (inString && lastQuoteIndex !== -1) {
|
|
686
|
+
const beforeUnterminated = repaired.substring(0, lastQuoteIndex + 1);
|
|
687
|
+
const afterUnterminated = repaired.substring(lastQuoteIndex + 1);
|
|
688
|
+
const breakPoints = [",", "}", "]", "\n"];
|
|
689
|
+
let breakIndex = -1;
|
|
690
|
+
for (let i = 0; i < afterUnterminated.length; i++) {
|
|
691
|
+
if (breakPoints.includes(afterUnterminated[i])) {
|
|
692
|
+
breakIndex = i;
|
|
693
|
+
break;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
if (breakIndex !== -1) {
|
|
697
|
+
const stringContent = afterUnterminated.substring(0, breakIndex);
|
|
698
|
+
const remainder = afterUnterminated.substring(breakIndex);
|
|
699
|
+
const escapedContent = stringContent.replace(new RegExp('(?<!\\\\)"', "g"), '\\"');
|
|
700
|
+
repaired = beforeUnterminated + escapedContent + '"' + remainder;
|
|
701
|
+
} else {
|
|
702
|
+
const escapedContent = afterUnterminated.replace(new RegExp('(?<!\\\\)"', "g"), '\\"');
|
|
703
|
+
repaired = beforeUnterminated + escapedContent + '"';
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return repaired;
|
|
707
|
+
}
|
|
708
|
+
// FIX: Replaced unsafe single quote replacement with a stateful parser.
|
|
709
|
+
/**
|
|
710
|
+
* Safely replaces single quotes with double quotes only for keys and string values,
|
|
711
|
+
* ignoring apostrophes inside already double-quoted strings.
|
|
712
|
+
*/
|
|
713
|
+
static safelyFixQuotes(jsonStr) {
|
|
714
|
+
let result = "";
|
|
715
|
+
let inDoubleQuoteString = false;
|
|
716
|
+
let escaped = false;
|
|
717
|
+
for (let i = 0; i < jsonStr.length; i++) {
|
|
718
|
+
const char = jsonStr[i];
|
|
719
|
+
if (escaped) {
|
|
720
|
+
result += char;
|
|
721
|
+
escaped = false;
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
if (char === "\\") {
|
|
725
|
+
escaped = true;
|
|
726
|
+
result += char;
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
if (char === '"') {
|
|
730
|
+
inDoubleQuoteString = !inDoubleQuoteString;
|
|
731
|
+
}
|
|
732
|
+
if (char === "'" && !inDoubleQuoteString) {
|
|
733
|
+
result += '"';
|
|
734
|
+
} else {
|
|
735
|
+
result += char;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return result;
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Fixes common JSON formatting issues using more robust methods.
|
|
742
|
+
*/
|
|
743
|
+
static applyCommonFixes(jsonStr) {
|
|
744
|
+
let fixed = jsonStr;
|
|
745
|
+
fixed = this.safelyFixQuotes(fixed);
|
|
746
|
+
fixed = fixed.replace(/,\s*([}\]])/g, "$1");
|
|
747
|
+
fixed = fixed.replace(/("|}|\d|]|true|false|null)\s*\n\s*(")/g, "$1,\n$2");
|
|
748
|
+
fixed = fixed.replace(/"[\s\S]*?"/g, (match) => {
|
|
749
|
+
const content = match.substring(1, match.length - 1);
|
|
750
|
+
const fixedContent = content.replace(/\n/g, "\\n").replace(/\r/g, "\\r");
|
|
751
|
+
return `"${fixedContent}"`;
|
|
752
|
+
});
|
|
753
|
+
fixed = fixed.replace(/"(true|false|null)"/g, "$1");
|
|
754
|
+
return fixed;
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Validates JSON by attempting to parse and providing detailed error info.
|
|
758
|
+
*/
|
|
759
|
+
static validateAndGetError(jsonStr) {
|
|
760
|
+
try {
|
|
761
|
+
JSON.parse(jsonStr);
|
|
762
|
+
return { isValid: true };
|
|
763
|
+
} catch (error) {
|
|
764
|
+
const errorMessage = error.message || "";
|
|
765
|
+
const positionMatch = errorMessage.match(/position (\d+)/);
|
|
766
|
+
const position = positionMatch ? parseInt(positionMatch[1], 10) : void 0;
|
|
767
|
+
return {
|
|
768
|
+
isValid: false,
|
|
769
|
+
error: errorMessage,
|
|
770
|
+
position
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Main repair function that attempts multiple strategies.
|
|
776
|
+
*/
|
|
777
|
+
static repairJson(jsonStr) {
|
|
778
|
+
var _a;
|
|
779
|
+
let current = jsonStr.trim();
|
|
780
|
+
const maxAttempts = 5;
|
|
781
|
+
let lastError = "";
|
|
782
|
+
let lastPosition = -1;
|
|
783
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
784
|
+
const validation = this.validateAndGetError(current);
|
|
785
|
+
if (validation.isValid) {
|
|
786
|
+
return current;
|
|
787
|
+
}
|
|
788
|
+
console.warn(`JSON repair attempt ${attempt + 1}: ${validation.error}`);
|
|
789
|
+
if (validation.error === lastError && validation.position === lastPosition) {
|
|
790
|
+
console.error("Repair attempt stuck on the same error, aborting this strategy.");
|
|
791
|
+
if (validation.position) {
|
|
792
|
+
const truncated = current.substring(0, validation.position);
|
|
793
|
+
const openBraces = (truncated.match(/{/g) || []).length;
|
|
794
|
+
const closeBraces = (truncated.match(/}/g) || []).length;
|
|
795
|
+
const openBrackets = (truncated.match(/\[/g) || []).length;
|
|
796
|
+
const closeBrackets = (truncated.match(/\]/g) || []).length;
|
|
797
|
+
let repaired = truncated.replace(/,\s*$/, "");
|
|
798
|
+
for (let i = 0; i < openBrackets - closeBrackets; i++) repaired += "]";
|
|
799
|
+
for (let i = 0; i < openBraces - closeBraces; i++) repaired += "}";
|
|
800
|
+
current = repaired;
|
|
801
|
+
const finalValidation = this.validateAndGetError(current);
|
|
802
|
+
if (finalValidation.isValid) return current;
|
|
803
|
+
}
|
|
804
|
+
break;
|
|
805
|
+
}
|
|
806
|
+
lastError = validation.error || "";
|
|
807
|
+
lastPosition = validation.position;
|
|
808
|
+
if ((_a = validation.error) == null ? void 0 : _a.includes("Unterminated string")) {
|
|
809
|
+
current = this.repairUnterminatedStrings(current);
|
|
810
|
+
} else {
|
|
811
|
+
current = this.applyCommonFixes(current);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
try {
|
|
815
|
+
let finalAttempt = this.applyCommonFixes(jsonStr.trim());
|
|
816
|
+
finalAttempt = this.repairUnterminatedStrings(finalAttempt);
|
|
817
|
+
JSON.parse(finalAttempt);
|
|
818
|
+
return finalAttempt;
|
|
819
|
+
} catch (e) {
|
|
820
|
+
throw new Error(`Unable to repair JSON after ${maxAttempts} attempts. Last known error: ${lastError}`);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
function extractJsonFromMarkdown(text) {
|
|
825
|
+
if (!text) {
|
|
826
|
+
throw new Error("Input text is empty or null.");
|
|
827
|
+
}
|
|
828
|
+
const trimmedText = text.trim();
|
|
829
|
+
try {
|
|
830
|
+
JSON.parse(trimmedText);
|
|
831
|
+
return trimmedText;
|
|
832
|
+
} catch (e) {
|
|
833
|
+
}
|
|
834
|
+
const markdownPatterns = [
|
|
835
|
+
/```(?:json|JSON)\s*([\s\S]*?)\s*```/,
|
|
836
|
+
// ```json ... ```
|
|
837
|
+
/```\s*({[\s\S]*?}|\[[\s\S]*?\])\s*```/
|
|
838
|
+
// ``` { ... } ``` or ``` [ ... ] ```
|
|
839
|
+
];
|
|
840
|
+
for (const pattern of markdownPatterns) {
|
|
841
|
+
const match = trimmedText.match(pattern);
|
|
842
|
+
if (match && match[1]) {
|
|
843
|
+
const content = match[1].trim();
|
|
844
|
+
try {
|
|
845
|
+
JSON.parse(content);
|
|
846
|
+
return content;
|
|
847
|
+
} catch (e) {
|
|
848
|
+
console.warn("JSON inside markdown block is invalid, attempting repair...");
|
|
849
|
+
try {
|
|
850
|
+
return JsonRepairEngine.repairJson(content);
|
|
851
|
+
} catch (repairError) {
|
|
852
|
+
console.warn(`Markdown block repair failed: ${repairError.message}. Trying other strategies...`);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
const firstBrace = trimmedText.indexOf("{");
|
|
858
|
+
const firstBracket = trimmedText.indexOf("[");
|
|
859
|
+
let startIndex = -1;
|
|
860
|
+
if (firstBrace === -1 && firstBracket === -1) ; else if (firstBrace === -1) {
|
|
861
|
+
startIndex = firstBracket;
|
|
862
|
+
} else if (firstBracket === -1) {
|
|
863
|
+
startIndex = firstBrace;
|
|
864
|
+
} else {
|
|
865
|
+
startIndex = Math.min(firstBrace, firstBracket);
|
|
866
|
+
}
|
|
867
|
+
if (startIndex !== -1) {
|
|
868
|
+
const textToProcess = trimmedText.substring(startIndex);
|
|
869
|
+
let balance = 0;
|
|
870
|
+
let inString = false;
|
|
871
|
+
let escaped = false;
|
|
872
|
+
const startChar = textToProcess[0];
|
|
873
|
+
const endChar = startChar === "{" ? "}" : "]";
|
|
874
|
+
for (let i = 0; i < textToProcess.length; i++) {
|
|
875
|
+
const char = textToProcess[i];
|
|
876
|
+
if (escaped) {
|
|
877
|
+
escaped = false;
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
if (char === "\\") {
|
|
881
|
+
escaped = true;
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
if (char === '"') {
|
|
885
|
+
inString = !inString;
|
|
886
|
+
}
|
|
887
|
+
if (!inString) {
|
|
888
|
+
if (char === startChar) balance++;
|
|
889
|
+
if (char === endChar) balance--;
|
|
890
|
+
}
|
|
891
|
+
if (balance === 0 && i > 0) {
|
|
892
|
+
const potentialJson = textToProcess.substring(0, i + 1);
|
|
893
|
+
try {
|
|
894
|
+
JSON.parse(potentialJson);
|
|
895
|
+
return potentialJson;
|
|
896
|
+
} catch (e) {
|
|
897
|
+
console.warn(`Balanced JSON segment is invalid, attempting repair...`);
|
|
898
|
+
try {
|
|
899
|
+
return JsonRepairEngine.repairJson(potentialJson);
|
|
900
|
+
} catch (repairError) {
|
|
901
|
+
console.warn(`Repair failed for balanced segment: ${repairError.message}`);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
break;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
console.warn("All extraction strategies failed, attempting to repair the entire input text as a last resort.");
|
|
909
|
+
try {
|
|
910
|
+
return JsonRepairEngine.repairJson(trimmedText);
|
|
911
|
+
} catch (finalError) {
|
|
912
|
+
throw new Error(`Unable to extract or repair valid JSON from AI response. Preview: "${trimmedText.substring(0, 100)}...". Final error: ${finalError.message}`);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
zod.z.object({
|
|
916
|
+
language: zod.z.custom(),
|
|
917
|
+
problemPrompt: zod.z.string(),
|
|
918
|
+
userCode: zod.z.string(),
|
|
919
|
+
testCase: zod.z.custom()
|
|
920
|
+
});
|
|
921
|
+
var AIEvaluationOutputSchema = zod.z.object({
|
|
922
|
+
passed: zod.z.boolean().describe("Did the user's code produce the expected output for the given input?"),
|
|
923
|
+
actualOutput: zod.z.any().describe("The actual output produced by the user's code."),
|
|
924
|
+
reasoning: zod.z.string().describe("A brief explanation of why the code passed or failed, or if there was a syntax error.")
|
|
925
|
+
});
|
|
926
|
+
var EvaluateUserCodeOutputSchema = AIEvaluationOutputSchema;
|
|
927
|
+
|
|
928
|
+
// src/ai/flows/evaluate-user-code.ts
|
|
929
|
+
async function evaluateUserCode(clientInput, apiKey) {
|
|
930
|
+
try {
|
|
931
|
+
const ai = genkit.genkit({
|
|
932
|
+
plugins: [googleai.googleAI({ apiKey })],
|
|
933
|
+
model: googleai.gemini20Flash
|
|
934
|
+
});
|
|
935
|
+
const { language, problemPrompt, userCode, testCase } = clientInput;
|
|
936
|
+
const promptText = `
|
|
937
|
+
You are an expert Code Judge and Teaching Assistant for a ${language} programming course.
|
|
938
|
+
Your task is to evaluate a student's code submission for a specific problem against a single test case.
|
|
939
|
+
|
|
940
|
+
## Problem Description
|
|
941
|
+
${problemPrompt}
|
|
942
|
+
|
|
943
|
+
## Student's Code Submission
|
|
944
|
+
\`\`\`${language}
|
|
945
|
+
${userCode}
|
|
946
|
+
\`\`\`
|
|
947
|
+
|
|
948
|
+
## Test Case to Evaluate
|
|
949
|
+
- Input(s): ${JSON.stringify(testCase.input)}
|
|
950
|
+
- Expected Output: ${JSON.stringify(testCase.expectedOutput)}
|
|
951
|
+
|
|
952
|
+
## Your Task
|
|
953
|
+
1. **Analyze Execution:** Mentally execute the student's code with the provided input(s).
|
|
954
|
+
2. **Determine Output:** Figure out what the actual output of the code would be.
|
|
955
|
+
3. **Compare:** Compare the actual output with the expected output.
|
|
956
|
+
4. **Handle Errors:** If the code has a syntax error or would crash, treat it as a failure.
|
|
957
|
+
5. **Provide Reasoning:** Briefly explain your conclusion. If it failed, explain why (e.g., "incorrect result", "infinite loop", "syntax error on line 5").
|
|
958
|
+
|
|
959
|
+
**CRITICAL JSON OUTPUT FORMAT:**
|
|
960
|
+
Return ONLY the JSON object with this EXACT structure.
|
|
961
|
+
|
|
962
|
+
\`\`\`json
|
|
963
|
+
{
|
|
964
|
+
"passed": false,
|
|
965
|
+
"actualOutput": 5,
|
|
966
|
+
"reasoning": "The function correctly summed the numbers but did not filter for only even numbers."
|
|
967
|
+
}
|
|
968
|
+
\`\`\`
|
|
969
|
+
|
|
970
|
+
Return only the JSON response.`;
|
|
971
|
+
const response = await ai.generate(promptText);
|
|
972
|
+
const rawText = response.text;
|
|
973
|
+
const jsonText = extractJsonFromMarkdown(rawText);
|
|
974
|
+
const aiGeneratedContent = JSON.parse(jsonText);
|
|
975
|
+
return EvaluateUserCodeOutputSchema.parse(aiGeneratedContent);
|
|
976
|
+
} catch (error) {
|
|
977
|
+
console.error("Error evaluating user code:", error);
|
|
978
|
+
if (error instanceof zod.z.ZodError) {
|
|
979
|
+
throw new Error(`AI evaluation output validation failed: ${error.message}`);
|
|
980
|
+
}
|
|
981
|
+
return {
|
|
982
|
+
passed: false,
|
|
983
|
+
actualOutput: "Evaluation Error",
|
|
984
|
+
reasoning: `The AI judge failed to process the code. Error: ${error.message}`
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// src/services/APIKeyService.ts
|
|
990
|
+
var GEMINI_API_KEY_SERVICE_NAME = "gemini";
|
|
991
|
+
var LOCAL_STORAGE_PREFIX = "iqk_api_keys_";
|
|
992
|
+
function _encode(data) {
|
|
993
|
+
if (typeof window !== "undefined" && typeof window.btoa === "function") {
|
|
994
|
+
try {
|
|
995
|
+
return window.btoa(data);
|
|
996
|
+
} catch (e) {
|
|
997
|
+
console.error("Base64 encoding (btoa) failed:", e);
|
|
998
|
+
return data;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
return data;
|
|
1002
|
+
}
|
|
1003
|
+
function _decode(data) {
|
|
1004
|
+
if (typeof window !== "undefined" && typeof window.atob === "function") {
|
|
1005
|
+
try {
|
|
1006
|
+
return window.atob(data);
|
|
1007
|
+
} catch (e) {
|
|
1008
|
+
console.error("Base64 decoding (atob) failed:", e);
|
|
1009
|
+
return data;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
return data;
|
|
1013
|
+
}
|
|
1014
|
+
var APIKeyService = class {
|
|
1015
|
+
static getStorageKey(serviceName) {
|
|
1016
|
+
return `${LOCAL_STORAGE_PREFIX}${serviceName}`;
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Saves an API key to localStorage. The key is mildly obfuscated using Base64.
|
|
1020
|
+
* @param serviceName - The name of the service (e.g., 'gemini').
|
|
1021
|
+
* @param apiKey - The API key to save.
|
|
1022
|
+
*/
|
|
1023
|
+
static saveAPIKey(serviceName, apiKey) {
|
|
1024
|
+
if (typeof window !== "undefined" && window.localStorage) {
|
|
1025
|
+
try {
|
|
1026
|
+
const encodedKey = _encode(apiKey);
|
|
1027
|
+
localStorage.setItem(this.getStorageKey(serviceName), encodedKey);
|
|
1028
|
+
} catch (e) {
|
|
1029
|
+
console.error(`Error saving API key for ${serviceName} to localStorage:`, e);
|
|
1030
|
+
}
|
|
1031
|
+
} else {
|
|
1032
|
+
console.warn("localStorage is not available. APIKeyService cannot save keys.");
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Retrieves an API key from localStorage.
|
|
1037
|
+
* @param serviceName - The name of the service.
|
|
1038
|
+
* @returns The decoded API key, or null if not found or if localStorage is unavailable.
|
|
1039
|
+
*/
|
|
1040
|
+
static getAPIKey(serviceName) {
|
|
1041
|
+
if (typeof window !== "undefined" && window.localStorage) {
|
|
1042
|
+
try {
|
|
1043
|
+
const storedKey = localStorage.getItem(this.getStorageKey(serviceName));
|
|
1044
|
+
if (storedKey) {
|
|
1045
|
+
return _decode(storedKey);
|
|
1046
|
+
}
|
|
1047
|
+
} catch (e) {
|
|
1048
|
+
console.error(`Error retrieving API key for ${serviceName} from localStorage:`, e);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
return null;
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Removes an API key from localStorage.
|
|
1055
|
+
* @param serviceName - The name of the service.
|
|
1056
|
+
*/
|
|
1057
|
+
static removeAPIKey(serviceName) {
|
|
1058
|
+
if (typeof window !== "undefined" && window.localStorage) {
|
|
1059
|
+
try {
|
|
1060
|
+
localStorage.removeItem(this.getStorageKey(serviceName));
|
|
1061
|
+
} catch (e) {
|
|
1062
|
+
console.error(`Error removing API key for ${serviceName} from localStorage:`, e);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* Checks if an API key exists in localStorage for the given service.
|
|
1068
|
+
* @param serviceName - The name of the service.
|
|
1069
|
+
* @returns True if a key exists, false otherwise.
|
|
1070
|
+
*/
|
|
1071
|
+
static hasAPIKey(serviceName) {
|
|
1072
|
+
return this.getAPIKey(serviceName) !== null;
|
|
1073
|
+
}
|
|
1074
|
+
};
|
|
1075
|
+
|
|
1076
|
+
// src/services/CodeEvaluationService.ts
|
|
1077
|
+
var CodeEvaluationService = class {
|
|
1078
|
+
constructor() {
|
|
1079
|
+
this.apiKey = APIKeyService.getAPIKey(GEMINI_API_KEY_SERVICE_NAME);
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Evaluates a user's code against a single test case using an AI judge.
|
|
1083
|
+
* @param question The full CodingQuestion object.
|
|
1084
|
+
* @param userCode The user's submitted code string.
|
|
1085
|
+
* @param testCase The specific TestCase to evaluate against.
|
|
1086
|
+
* @returns A promise that resolves to an EvaluationResult object.
|
|
1087
|
+
*/
|
|
1088
|
+
async evaluateSingleTestCase(question, userCode, testCase) {
|
|
1089
|
+
if (!this.apiKey) {
|
|
1090
|
+
return {
|
|
1091
|
+
testCaseId: testCase.id,
|
|
1092
|
+
passed: false,
|
|
1093
|
+
actualOutput: "Configuration Error",
|
|
1094
|
+
reasoning: "API Key is not configured."
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
const aiResult = await evaluateUserCode({
|
|
1098
|
+
language: question.language,
|
|
1099
|
+
problemPrompt: question.prompt,
|
|
1100
|
+
userCode,
|
|
1101
|
+
testCase
|
|
1102
|
+
}, this.apiKey);
|
|
1103
|
+
return __spreadValues({
|
|
1104
|
+
testCaseId: testCase.id
|
|
1105
|
+
}, aiResult);
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Evaluates user's code against all test cases for a given question.
|
|
1109
|
+
* @param question The full CodingQuestion object.
|
|
1110
|
+
* @param userCode The user's submitted code string.
|
|
1111
|
+
* @returns A promise that resolves to an array of EvaluationResult objects.
|
|
1112
|
+
*/
|
|
1113
|
+
async evaluateAllTestCases(question, userCode) {
|
|
1114
|
+
const results = [];
|
|
1115
|
+
for (const testCase of question.testCases) {
|
|
1116
|
+
const result = await this.evaluateSingleTestCase(question, userCode, testCase);
|
|
1117
|
+
results.push(result);
|
|
1118
|
+
}
|
|
1119
|
+
return results;
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* Evaluates user's code against only the public test cases for a given question.
|
|
1123
|
+
* Useful for a "Run Tests" button before final submission.
|
|
1124
|
+
* @param question The full CodingQuestion object.
|
|
1125
|
+
* @param userCode The user's submitted code string.
|
|
1126
|
+
* @returns A promise that resolves to an array of EvaluationResult objects.
|
|
1127
|
+
*/
|
|
1128
|
+
async evaluatePublicTestCases(question, userCode) {
|
|
1129
|
+
const publicTestCases = question.testCases.filter((tc) => tc.isPublic);
|
|
1130
|
+
const results = [];
|
|
1131
|
+
for (const testCase of publicTestCases) {
|
|
1132
|
+
const result = await this.evaluateSingleTestCase(question, userCode, testCase);
|
|
1133
|
+
results.push(result);
|
|
1134
|
+
}
|
|
1135
|
+
return results;
|
|
1136
|
+
}
|
|
1137
|
+
};
|
|
1138
|
+
|
|
1139
|
+
// src/services/evaluators/coding-evaluator.ts
|
|
1140
|
+
var CodingEvaluator = class {
|
|
1141
|
+
async evaluate(question, answer) {
|
|
1142
|
+
var _a;
|
|
1143
|
+
const points = (_a = question.points) != null ? _a : 0;
|
|
1144
|
+
if (typeof answer !== "string" || !answer.trim()) {
|
|
1145
|
+
return {
|
|
1146
|
+
isCorrect: false,
|
|
1147
|
+
correctAnswer: { id: null, value: question.solutionCode },
|
|
1148
|
+
pointsEarned: 0,
|
|
1149
|
+
evaluationDetails: question.testCases.map((tc) => ({
|
|
1150
|
+
testCaseId: tc.id,
|
|
1151
|
+
passed: false,
|
|
1152
|
+
actualOutput: "No submission",
|
|
1153
|
+
reasoning: "User did not submit any code."
|
|
1154
|
+
}))
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
try {
|
|
1158
|
+
const evaluationService = new CodeEvaluationService();
|
|
1159
|
+
const testCaseResults = await evaluationService.evaluateAllTestCases(question, answer);
|
|
1160
|
+
const isCorrect = testCaseResults.every((result) => result.passed);
|
|
1161
|
+
const correctAnswerDetail = {
|
|
1162
|
+
id: null,
|
|
1163
|
+
value: question.solutionCode
|
|
1164
|
+
};
|
|
1165
|
+
return {
|
|
1166
|
+
isCorrect,
|
|
1167
|
+
correctAnswer: correctAnswerDetail,
|
|
1168
|
+
pointsEarned: isCorrect ? points : 0,
|
|
1169
|
+
evaluationDetails: testCaseResults
|
|
1170
|
+
// Pass through the detailed results
|
|
1171
|
+
};
|
|
1172
|
+
} catch (error) {
|
|
1173
|
+
console.error("A critical error occurred during code evaluation:", error);
|
|
1174
|
+
return {
|
|
1175
|
+
isCorrect: false,
|
|
1176
|
+
correctAnswer: { id: null, value: question.solutionCode },
|
|
1177
|
+
pointsEarned: 0,
|
|
1178
|
+
evaluationDetails: question.testCases.map((tc) => ({
|
|
1179
|
+
testCaseId: tc.id,
|
|
1180
|
+
passed: false,
|
|
1181
|
+
actualOutput: "Evaluation Error",
|
|
1182
|
+
reasoning: error instanceof Error ? error.message : "An unknown error occurred."
|
|
1183
|
+
}))
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
};
|
|
1188
|
+
|
|
337
1189
|
// src/services/QuizEngine.ts
|
|
338
1190
|
var QuizEngine = class {
|
|
339
1191
|
constructor(options) {
|
|
@@ -350,6 +1202,8 @@ var QuizEngine = class {
|
|
|
350
1202
|
this.callbacks = options.callbacks || {};
|
|
351
1203
|
this.questions = ((_a = this.config.settings) == null ? void 0 : _a.shuffleQuestions) ? [...this.config.questions].sort(() => Math.random() - 0.5) : this.config.questions;
|
|
352
1204
|
this.overallStartTime = Date.now();
|
|
1205
|
+
this.evaluators = /* @__PURE__ */ new Map();
|
|
1206
|
+
this.registerEvaluators();
|
|
353
1207
|
if (((_b = this.config.settings) == null ? void 0 : _b.timeLimitMinutes) && this.config.settings.timeLimitMinutes > 0) {
|
|
354
1208
|
this.timeLeftInSeconds = this.config.settings.timeLimitMinutes * 60;
|
|
355
1209
|
}
|
|
@@ -388,6 +1242,22 @@ var QuizEngine = class {
|
|
|
388
1242
|
}
|
|
389
1243
|
(_e = (_d = this.callbacks).onQuestionChange) == null ? void 0 : _e.call(_d, initialQ, this.getCurrentQuestionNumber(), this.getTotalQuestions());
|
|
390
1244
|
}
|
|
1245
|
+
registerEvaluators() {
|
|
1246
|
+
this.evaluators.set("multiple_choice", new MultipleChoiceEvaluator());
|
|
1247
|
+
this.evaluators.set("multiple_response", new MultipleResponseEvaluator());
|
|
1248
|
+
this.evaluators.set("true_false", new TrueFalseEvaluator());
|
|
1249
|
+
this.evaluators.set("short_answer", new ShortAnswerEvaluator());
|
|
1250
|
+
this.evaluators.set("numeric", new NumericEvaluator());
|
|
1251
|
+
this.evaluators.set("sequence", new SequenceEvaluator());
|
|
1252
|
+
this.evaluators.set("matching", new MatchingEvaluator());
|
|
1253
|
+
this.evaluators.set("fill_in_the_blanks", new FillInTheBlanksEvaluator());
|
|
1254
|
+
this.evaluators.set("drag_and_drop", new DragAndDropEvaluator());
|
|
1255
|
+
this.evaluators.set("hotspot", new HotspotEvaluator());
|
|
1256
|
+
const programmingEvaluator = new ProgrammingEvaluator();
|
|
1257
|
+
this.evaluators.set("blockly_programming", programmingEvaluator);
|
|
1258
|
+
this.evaluators.set("scratch_programming", programmingEvaluator);
|
|
1259
|
+
this.evaluators.set("coding", new CodingEvaluator());
|
|
1260
|
+
}
|
|
391
1261
|
_recordCurrentQuestionTime() {
|
|
392
1262
|
if (this.questionStartTime && this.currentQuestionIndex >= 0 && this.currentQuestionIndex < this.questions.length) {
|
|
393
1263
|
const currentQId = this.questions[this.currentQuestionIndex].id;
|
|
@@ -480,180 +1350,28 @@ var QuizEngine = class {
|
|
|
480
1350
|
}
|
|
481
1351
|
return this.getCurrentQuestion();
|
|
482
1352
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
correctAnswerDetail = { id: correctAnswerId, value: correctValue };
|
|
498
|
-
isCorrect = answer === correctAnswerId;
|
|
499
|
-
break;
|
|
500
|
-
}
|
|
501
|
-
case "multiple_response": {
|
|
502
|
-
const q = question;
|
|
503
|
-
const correctAnswerIds = q.correctAnswerIds;
|
|
504
|
-
const correctValues = correctAnswerIds.map((id) => findOptionText(q, id));
|
|
505
|
-
correctAnswerDetail = { id: correctAnswerIds, value: correctValues };
|
|
506
|
-
if (Array.isArray(answer)) {
|
|
507
|
-
const userAnswerSet = new Set(answer);
|
|
508
|
-
const correctAnswerSet = new Set(correctAnswerIds);
|
|
509
|
-
isCorrect = userAnswerSet.size === correctAnswerSet.size && [...userAnswerSet].every((id) => correctAnswerSet.has(id));
|
|
510
|
-
}
|
|
511
|
-
break;
|
|
512
|
-
}
|
|
513
|
-
case "true_false": {
|
|
514
|
-
const q = question;
|
|
515
|
-
correctAnswerDetail = { id: null, value: q.correctAnswer };
|
|
516
|
-
let tfAnswer = answer;
|
|
517
|
-
if (typeof answer === "string") tfAnswer = answer.toLowerCase() === "true";
|
|
518
|
-
isCorrect = typeof tfAnswer === "boolean" && tfAnswer === q.correctAnswer;
|
|
519
|
-
break;
|
|
520
|
-
}
|
|
521
|
-
case "short_answer": {
|
|
522
|
-
const q = question;
|
|
523
|
-
correctAnswerDetail = { id: null, value: q.acceptedAnswers };
|
|
524
|
-
if (typeof answer === "string") {
|
|
525
|
-
const userAnswerTrimmed = answer.trim();
|
|
526
|
-
const caseSensitive = (_b = q.isCaseSensitive) != null ? _b : false;
|
|
527
|
-
isCorrect = q.acceptedAnswers.some((accAns) => caseSensitive ? accAns.trim() === userAnswerTrimmed : accAns.trim().toLowerCase() === userAnswerTrimmed.toLowerCase());
|
|
528
|
-
}
|
|
529
|
-
break;
|
|
530
|
-
}
|
|
531
|
-
case "numeric": {
|
|
532
|
-
const q = question;
|
|
533
|
-
correctAnswerDetail = { id: null, value: q.answer };
|
|
534
|
-
if (typeof answer === "string" || typeof answer === "number") {
|
|
535
|
-
const userAnswerNum = parseFloat(String(answer));
|
|
536
|
-
if (!isNaN(userAnswerNum)) {
|
|
537
|
-
isCorrect = q.tolerance != null ? Math.abs(userAnswerNum - q.answer) <= q.tolerance : userAnswerNum === q.answer;
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
break;
|
|
541
|
-
}
|
|
542
|
-
case "sequence": {
|
|
543
|
-
const q = question;
|
|
544
|
-
const correctValues = q.correctOrder.map((id) => {
|
|
545
|
-
var _a2;
|
|
546
|
-
return ((_a2 = q.items.find((item) => item.id === id)) == null ? void 0 : _a2.content) || "";
|
|
547
|
-
});
|
|
548
|
-
correctAnswerDetail = { id: q.correctOrder, value: correctValues };
|
|
549
|
-
if (Array.isArray(answer) && answer.length === q.correctOrder.length) {
|
|
550
|
-
isCorrect = answer.every((itemId, index) => itemId === q.correctOrder[index]);
|
|
551
|
-
}
|
|
552
|
-
break;
|
|
553
|
-
}
|
|
554
|
-
case "matching": {
|
|
555
|
-
const q = question;
|
|
556
|
-
const correctMap = q.correctAnswerMap.reduce((acc, curr) => {
|
|
557
|
-
var _a2, _b2;
|
|
558
|
-
const promptText = ((_a2 = q.prompts.find((p) => p.id === curr.promptId)) == null ? void 0 : _a2.content) || "";
|
|
559
|
-
const optionText = ((_b2 = q.options.find((o) => o.id === curr.optionId)) == null ? void 0 : _b2.content) || "";
|
|
560
|
-
acc[promptText] = optionText;
|
|
561
|
-
return acc;
|
|
562
|
-
}, {});
|
|
563
|
-
correctAnswerDetail = { id: null, value: correctMap };
|
|
564
|
-
if (typeof answer === "object" && answer !== null && !Array.isArray(answer)) {
|
|
565
|
-
const userAnswerMap = answer;
|
|
566
|
-
isCorrect = q.correctAnswerMap.length === Object.keys(userAnswerMap).length && q.correctAnswerMap.every((map) => userAnswerMap[map.promptId] === map.optionId);
|
|
567
|
-
}
|
|
568
|
-
break;
|
|
569
|
-
}
|
|
570
|
-
case "fill_in_the_blanks": {
|
|
571
|
-
const q = question;
|
|
572
|
-
const correctMap = q.answers.reduce((acc, curr) => {
|
|
573
|
-
acc[curr.blankId] = curr.acceptedValues.join(" | ");
|
|
574
|
-
return acc;
|
|
575
|
-
}, {});
|
|
576
|
-
correctAnswerDetail = { id: null, value: correctMap };
|
|
577
|
-
if (typeof answer === "object" && answer !== null && !Array.isArray(answer)) {
|
|
578
|
-
const userAnswerMap = answer;
|
|
579
|
-
isCorrect = q.answers.every((correctAnsDef) => {
|
|
580
|
-
var _a2, _b2;
|
|
581
|
-
const userValForBlank = (_a2 = userAnswerMap[correctAnsDef.blankId]) == null ? void 0 : _a2.trim();
|
|
582
|
-
if (userValForBlank === void 0) return false;
|
|
583
|
-
const caseSensitive = (_b2 = q.isCaseSensitive) != null ? _b2 : false;
|
|
584
|
-
return correctAnsDef.acceptedValues.some((accVal) => caseSensitive ? accVal.trim() === userValForBlank : accVal.trim().toLowerCase() === userValForBlank.toLowerCase());
|
|
585
|
-
});
|
|
586
|
-
}
|
|
587
|
-
break;
|
|
588
|
-
}
|
|
589
|
-
case "drag_and_drop": {
|
|
590
|
-
const q = question;
|
|
591
|
-
const correctMap = q.answerMap.reduce((acc, curr) => {
|
|
592
|
-
var _a2, _b2;
|
|
593
|
-
const draggableText = ((_a2 = q.draggableItems.find((d) => d.id === curr.draggableId)) == null ? void 0 : _a2.content) || "";
|
|
594
|
-
const dropZoneText = ((_b2 = q.dropZones.find((z) => z.id === curr.dropZoneId)) == null ? void 0 : _b2.label) || "";
|
|
595
|
-
acc[draggableText] = dropZoneText;
|
|
596
|
-
return acc;
|
|
597
|
-
}, {});
|
|
598
|
-
correctAnswerDetail = { id: null, value: correctMap };
|
|
599
|
-
if (typeof answer === "object" && answer !== null && !Array.isArray(answer)) {
|
|
600
|
-
const userAnswerMap = answer;
|
|
601
|
-
isCorrect = q.answerMap.length === Object.keys(userAnswerMap).length && q.answerMap.every((map) => userAnswerMap[map.draggableId] === map.dropZoneId);
|
|
602
|
-
}
|
|
603
|
-
break;
|
|
604
|
-
}
|
|
605
|
-
case "hotspot": {
|
|
606
|
-
const q = question;
|
|
607
|
-
const correctValues = q.correctHotspotIds.map((id) => {
|
|
608
|
-
var _a2;
|
|
609
|
-
return ((_a2 = q.hotspots.find((h) => h.id === id)) == null ? void 0 : _a2.description) || id;
|
|
610
|
-
});
|
|
611
|
-
correctAnswerDetail = { id: q.correctHotspotIds, value: correctValues };
|
|
612
|
-
if (Array.isArray(answer)) {
|
|
613
|
-
const userAnswerSet = new Set(answer);
|
|
614
|
-
const correctAnswerSet = new Set(q.correctHotspotIds);
|
|
615
|
-
isCorrect = userAnswerSet.size === correctAnswerSet.size && [...userAnswerSet].every((id) => correctAnswerSet.has(id));
|
|
616
|
-
}
|
|
617
|
-
break;
|
|
618
|
-
}
|
|
619
|
-
case "blockly_programming":
|
|
620
|
-
case "scratch_programming": {
|
|
621
|
-
const q = question;
|
|
622
|
-
correctAnswerDetail = { id: null, value: q.solutionGeneratedCode || "" };
|
|
623
|
-
if (typeof answer === "string" && typeof q.solutionGeneratedCode === "string") {
|
|
624
|
-
if (typeof window !== "undefined" && ((_c = window.Blockly) == null ? void 0 : _c.JavaScript)) {
|
|
625
|
-
const LocalBlockly = window.Blockly;
|
|
626
|
-
let generatedUserCode = "";
|
|
627
|
-
try {
|
|
628
|
-
const tempWorkspace = new LocalBlockly.Workspace();
|
|
629
|
-
const dom = LocalBlockly.Xml.textToDom(answer);
|
|
630
|
-
LocalBlockly.Xml.domToWorkspace(dom, tempWorkspace);
|
|
631
|
-
generatedUserCode = LocalBlockly.JavaScript.workspaceToCode(tempWorkspace) || "";
|
|
632
|
-
const normalize = (code) => code.replace(/\s+/g, " ").trim();
|
|
633
|
-
isCorrect = normalize(generatedUserCode) === normalize(q.solutionGeneratedCode);
|
|
634
|
-
tempWorkspace.dispose();
|
|
635
|
-
} catch (e) {
|
|
636
|
-
console.error(`Error generating code from user's ${q.questionType} XML for evaluation:`, e);
|
|
637
|
-
isCorrect = false;
|
|
638
|
-
}
|
|
639
|
-
} else {
|
|
640
|
-
console.warn(`Blockly library not available in QuizEngine for ${q.questionType} code generation during evaluation. Skipping code comparison.`);
|
|
641
|
-
isCorrect = false;
|
|
642
|
-
}
|
|
1353
|
+
getElapsedTime() {
|
|
1354
|
+
return Date.now() - this.overallStartTime;
|
|
1355
|
+
}
|
|
1356
|
+
destroy() {
|
|
1357
|
+
this.stopTimer();
|
|
1358
|
+
this._recordCurrentQuestionTime();
|
|
1359
|
+
if (this.scormService && this.scormService.hasAPI()) {
|
|
1360
|
+
if (["initialized", "committed", "sending_data"].includes(this.quizResultState.scormStatus || "")) {
|
|
1361
|
+
const termResult = this.scormService.terminate();
|
|
1362
|
+
if (termResult.success) {
|
|
1363
|
+
this.quizResultState.scormStatus = "terminated";
|
|
1364
|
+
} else {
|
|
1365
|
+
this.quizResultState.scormStatus = "error";
|
|
1366
|
+
this.quizResultState.scormError = termResult.error || "SCORM termination failed on destroy.";
|
|
643
1367
|
}
|
|
644
|
-
break;
|
|
645
|
-
}
|
|
646
|
-
default: {
|
|
647
|
-
const _exhaustiveCheck = question;
|
|
648
|
-
console.warn("Unsupported question type in QuizEngine evaluation:", _exhaustiveCheck);
|
|
649
|
-
isCorrect = false;
|
|
650
|
-
correctAnswerDetail = { id: null, value: "Evaluation not implemented." };
|
|
651
1368
|
}
|
|
652
1369
|
}
|
|
653
|
-
|
|
1370
|
+
this.scormService = null;
|
|
654
1371
|
}
|
|
1372
|
+
// (Tiếp theo từ Phần 1)
|
|
655
1373
|
async calculateResults() {
|
|
656
|
-
var _a, _b, _c, _d, _e
|
|
1374
|
+
var _a, _b, _c, _d, _e;
|
|
657
1375
|
this.stopTimer();
|
|
658
1376
|
this._recordCurrentQuestionTime();
|
|
659
1377
|
let totalScore = 0;
|
|
@@ -663,92 +1381,31 @@ var QuizEngine = class {
|
|
|
663
1381
|
for (const question of this.questions) {
|
|
664
1382
|
const userAnswerRaw = this.userAnswers.get(question.id) || null;
|
|
665
1383
|
maxScore += (_a = question.points) != null ? _a : 0;
|
|
666
|
-
const
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
}
|
|
681
|
-
case "multiple_response": {
|
|
682
|
-
const q = question;
|
|
683
|
-
allOptions = q.options.map((opt) => ({ id: opt.id, value: opt.text }));
|
|
684
|
-
const ids = userAnswerRaw;
|
|
685
|
-
const values = ids.map((id) => {
|
|
686
|
-
var _a2;
|
|
687
|
-
return ((_a2 = allOptions == null ? void 0 : allOptions.find((opt) => opt.id === id)) == null ? void 0 : _a2.value) || "";
|
|
688
|
-
});
|
|
689
|
-
userAnswerDetail = { id: ids, value: values };
|
|
690
|
-
break;
|
|
691
|
-
}
|
|
692
|
-
case "true_false":
|
|
693
|
-
case "short_answer":
|
|
694
|
-
case "numeric":
|
|
695
|
-
userAnswerDetail = { id: null, value: userAnswerRaw };
|
|
696
|
-
break;
|
|
697
|
-
case "sequence": {
|
|
698
|
-
const q = question;
|
|
699
|
-
allOptions = q.items.map((item) => ({ id: item.id, value: item.content }));
|
|
700
|
-
const ids = userAnswerRaw;
|
|
701
|
-
const values = ids.map((id) => {
|
|
702
|
-
var _a2;
|
|
703
|
-
return ((_a2 = allOptions == null ? void 0 : allOptions.find((opt) => opt.id === id)) == null ? void 0 : _a2.value) || "";
|
|
704
|
-
});
|
|
705
|
-
userAnswerDetail = { id: ids, value: values };
|
|
706
|
-
break;
|
|
707
|
-
}
|
|
708
|
-
case "matching": {
|
|
709
|
-
const q = question;
|
|
710
|
-
const userAnswerMap = userAnswerRaw;
|
|
711
|
-
const valueMap = {};
|
|
712
|
-
for (const promptId in userAnswerMap) {
|
|
713
|
-
const optionId = userAnswerMap[promptId];
|
|
714
|
-
const promptText = ((_c = q.prompts.find((p) => p.id === promptId)) == null ? void 0 : _c.content) || "";
|
|
715
|
-
const optionText = ((_d = q.options.find((o) => o.id === optionId)) == null ? void 0 : _d.content) || "";
|
|
716
|
-
valueMap[promptText] = optionText;
|
|
717
|
-
}
|
|
718
|
-
userAnswerDetail = { id: null, value: valueMap };
|
|
719
|
-
break;
|
|
720
|
-
}
|
|
721
|
-
// --- LOGIC MỚI ĐƯỢC THÊM VÀO ---
|
|
722
|
-
case "fill_in_the_blanks": {
|
|
723
|
-
if (typeof userAnswerRaw === "object" && userAnswerRaw !== null && !Array.isArray(userAnswerRaw)) {
|
|
724
|
-
userAnswerDetail = { id: null, value: userAnswerRaw };
|
|
725
|
-
}
|
|
726
|
-
break;
|
|
727
|
-
}
|
|
728
|
-
case "drag_and_drop": {
|
|
729
|
-
const q = question;
|
|
730
|
-
if (typeof userAnswerRaw === "object" && userAnswerRaw !== null && !Array.isArray(userAnswerRaw)) {
|
|
731
|
-
const userAnswerMapByIds = userAnswerRaw;
|
|
732
|
-
const enrichedUserAnswerMap = {};
|
|
733
|
-
for (const draggableId in userAnswerMapByIds) {
|
|
734
|
-
const dropZoneId = userAnswerMapByIds[draggableId];
|
|
735
|
-
const draggableText = ((_e = q.draggableItems.find((d) => d.id === draggableId)) == null ? void 0 : _e.content) || `(ID: ${draggableId})`;
|
|
736
|
-
const dropZoneText = ((_f = q.dropZones.find((z) => z.id === dropZoneId)) == null ? void 0 : _f.label) || `(ID: ${dropZoneId})`;
|
|
737
|
-
enrichedUserAnswerMap[draggableText] = dropZoneText;
|
|
738
|
-
}
|
|
739
|
-
userAnswerDetail = { id: null, value: enrichedUserAnswerMap };
|
|
740
|
-
}
|
|
741
|
-
break;
|
|
742
|
-
}
|
|
743
|
-
// ------------------------------------
|
|
744
|
-
// Các loại câu hỏi còn lại vẫn giữ fallback
|
|
745
|
-
case "hotspot":
|
|
746
|
-
case "blockly_programming":
|
|
747
|
-
case "scratch_programming":
|
|
748
|
-
userAnswerDetail = { id: null, value: userAnswerRaw };
|
|
749
|
-
break;
|
|
750
|
-
}
|
|
1384
|
+
const evaluator = this.evaluators.get(question.questionType);
|
|
1385
|
+
if (!evaluator) {
|
|
1386
|
+
console.warn(`No evaluator found for question type: ${question.questionType}`);
|
|
1387
|
+
questionResultsArray.push({
|
|
1388
|
+
questionId: question.id,
|
|
1389
|
+
questionType: question.questionType,
|
|
1390
|
+
prompt: question.prompt,
|
|
1391
|
+
isCorrect: false,
|
|
1392
|
+
pointsEarned: 0,
|
|
1393
|
+
userAnswer: { id: null, value: userAnswerRaw },
|
|
1394
|
+
correctAnswer: { id: null, value: "Evaluation not implemented." },
|
|
1395
|
+
timeSpentSeconds: parseFloat((this.questionTimings.get(question.id) || 0).toFixed(2))
|
|
1396
|
+
});
|
|
1397
|
+
continue;
|
|
751
1398
|
}
|
|
1399
|
+
const {
|
|
1400
|
+
isCorrect,
|
|
1401
|
+
correctAnswer: correctAnswerDetail,
|
|
1402
|
+
pointsEarned,
|
|
1403
|
+
evaluationDetails
|
|
1404
|
+
} = await evaluator.evaluate(question, userAnswerRaw);
|
|
1405
|
+
totalScore += pointsEarned;
|
|
1406
|
+
const timeSpentOnThisQuestion = parseFloat((this.questionTimings.get(question.id) || 0).toFixed(2));
|
|
1407
|
+
accumulatedTotalTimeSpent += timeSpentOnThisQuestion;
|
|
1408
|
+
const userAnswerDetail = this.formatUserAnswerDetail(question, userAnswerRaw);
|
|
752
1409
|
questionResultsArray.push({
|
|
753
1410
|
questionId: question.id,
|
|
754
1411
|
questionType: question.questionType,
|
|
@@ -757,18 +1414,18 @@ var QuizEngine = class {
|
|
|
757
1414
|
pointsEarned,
|
|
758
1415
|
userAnswer: userAnswerDetail,
|
|
759
1416
|
correctAnswer: correctAnswerDetail,
|
|
760
|
-
|
|
761
|
-
|
|
1417
|
+
timeSpentSeconds: timeSpentOnThisQuestion,
|
|
1418
|
+
evaluationDetails
|
|
762
1419
|
});
|
|
763
1420
|
}
|
|
764
1421
|
const percentage = maxScore > 0 ? parseFloat((totalScore / maxScore * 100).toFixed(2)) : 0;
|
|
765
1422
|
let passed = void 0;
|
|
766
|
-
if (((
|
|
1423
|
+
if (((_b = this.config.settings) == null ? void 0 : _b.passingScorePercent) != null) {
|
|
767
1424
|
passed = percentage >= this.config.settings.passingScorePercent;
|
|
768
1425
|
}
|
|
769
1426
|
const totalQuizTimeSpentSeconds = parseFloat(accumulatedTotalTimeSpent.toFixed(2));
|
|
770
1427
|
const averageTimePerQuestionSeconds = this.questions.length > 0 ? parseFloat((totalQuizTimeSpentSeconds / this.questions.length).toFixed(2)) : 0;
|
|
771
|
-
const metadataPerformance = this._calculateMetadataPerformance();
|
|
1428
|
+
const metadataPerformance = await this._calculateMetadataPerformance();
|
|
772
1429
|
const finalResults = __spreadValues({
|
|
773
1430
|
score: totalScore,
|
|
774
1431
|
maxScore,
|
|
@@ -784,11 +1441,118 @@ var QuizEngine = class {
|
|
|
784
1441
|
averageTimePerQuestionSeconds
|
|
785
1442
|
}, metadataPerformance);
|
|
786
1443
|
this.quizResultState = __spreadValues(__spreadValues({}, this.quizResultState), finalResults);
|
|
787
|
-
if ((
|
|
1444
|
+
if ((_c = this.config.settings) == null ? void 0 : _c.scorm) this._sendResultsToSCORM(finalResults);
|
|
788
1445
|
await this._sendResultsToWebhook(finalResults);
|
|
789
|
-
(
|
|
1446
|
+
(_e = (_d = this.callbacks).onQuizFinish) == null ? void 0 : _e.call(_d, finalResults);
|
|
790
1447
|
return finalResults;
|
|
791
1448
|
}
|
|
1449
|
+
formatUserAnswerDetail(question, userAnswerRaw) {
|
|
1450
|
+
var _a, _b, _c, _d, _e;
|
|
1451
|
+
if (userAnswerRaw === null) return null;
|
|
1452
|
+
switch (question.questionType) {
|
|
1453
|
+
case "multiple_choice": {
|
|
1454
|
+
const q = question;
|
|
1455
|
+
const id = userAnswerRaw;
|
|
1456
|
+
return { id, value: ((_a = q.options.find((opt) => opt.id === id)) == null ? void 0 : _a.text) || "" };
|
|
1457
|
+
}
|
|
1458
|
+
case "multiple_response": {
|
|
1459
|
+
const q = question;
|
|
1460
|
+
const ids = userAnswerRaw;
|
|
1461
|
+
const values = ids.map((id) => {
|
|
1462
|
+
var _a2;
|
|
1463
|
+
return ((_a2 = q.options.find((opt) => opt.id === id)) == null ? void 0 : _a2.text) || "";
|
|
1464
|
+
});
|
|
1465
|
+
return { id: ids, value: values };
|
|
1466
|
+
}
|
|
1467
|
+
case "sequence": {
|
|
1468
|
+
const q = question;
|
|
1469
|
+
const ids = userAnswerRaw;
|
|
1470
|
+
const values = ids.map((id) => {
|
|
1471
|
+
var _a2;
|
|
1472
|
+
return ((_a2 = q.items.find((item) => item.id === id)) == null ? void 0 : _a2.content) || "";
|
|
1473
|
+
});
|
|
1474
|
+
return { id: ids, value: values };
|
|
1475
|
+
}
|
|
1476
|
+
case "matching": {
|
|
1477
|
+
const q = question;
|
|
1478
|
+
const userAnswerMap = userAnswerRaw;
|
|
1479
|
+
const valueMap = {};
|
|
1480
|
+
for (const promptId in userAnswerMap) {
|
|
1481
|
+
const optionId = userAnswerMap[promptId];
|
|
1482
|
+
const promptText = ((_b = q.prompts.find((p) => p.id === promptId)) == null ? void 0 : _b.content) || "";
|
|
1483
|
+
const optionText = ((_c = q.options.find((o) => o.id === optionId)) == null ? void 0 : _c.content) || "";
|
|
1484
|
+
valueMap[promptText] = optionText;
|
|
1485
|
+
}
|
|
1486
|
+
return { id: null, value: valueMap };
|
|
1487
|
+
}
|
|
1488
|
+
case "drag_and_drop": {
|
|
1489
|
+
const q = question;
|
|
1490
|
+
if (typeof userAnswerRaw === "object" && userAnswerRaw !== null && !Array.isArray(userAnswerRaw)) {
|
|
1491
|
+
const userAnswerMapByIds = userAnswerRaw;
|
|
1492
|
+
const enrichedUserAnswerMap = {};
|
|
1493
|
+
for (const draggableId in userAnswerMapByIds) {
|
|
1494
|
+
const dropZoneId = userAnswerMapByIds[draggableId];
|
|
1495
|
+
const draggableText = ((_d = q.draggableItems.find((d) => d.id === draggableId)) == null ? void 0 : _d.content) || `(ID: ${draggableId})`;
|
|
1496
|
+
const dropZoneText = ((_e = q.dropZones.find((z3) => z3.id === dropZoneId)) == null ? void 0 : _e.label) || `(ID: ${dropZoneId})`;
|
|
1497
|
+
enrichedUserAnswerMap[draggableText] = dropZoneText;
|
|
1498
|
+
}
|
|
1499
|
+
return { id: null, value: enrichedUserAnswerMap };
|
|
1500
|
+
}
|
|
1501
|
+
return { id: null, value: userAnswerRaw };
|
|
1502
|
+
}
|
|
1503
|
+
default:
|
|
1504
|
+
return { id: null, value: userAnswerRaw };
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
async _calculateMetadataPerformance() {
|
|
1508
|
+
var _a;
|
|
1509
|
+
const loPerformanceMap = /* @__PURE__ */ new Map();
|
|
1510
|
+
const categoryPerformanceMap = /* @__PURE__ */ new Map();
|
|
1511
|
+
const topicPerformanceMap = /* @__PURE__ */ new Map();
|
|
1512
|
+
const difficultyPerformanceMap = /* @__PURE__ */ new Map();
|
|
1513
|
+
const bloomLevelPerformanceMap = /* @__PURE__ */ new Map();
|
|
1514
|
+
const updateMap = (map, key, points, isCorrect) => {
|
|
1515
|
+
if (!key) return;
|
|
1516
|
+
const current = map.get(key) || { totalQuestions: 0, correctQuestions: 0, pointsEarned: 0, maxPoints: 0 };
|
|
1517
|
+
current.totalQuestions++;
|
|
1518
|
+
current.maxPoints += points;
|
|
1519
|
+
if (isCorrect) {
|
|
1520
|
+
current.correctQuestions++;
|
|
1521
|
+
current.pointsEarned += points;
|
|
1522
|
+
}
|
|
1523
|
+
map.set(key, current);
|
|
1524
|
+
};
|
|
1525
|
+
for (const q of this.questions) {
|
|
1526
|
+
const userAnswer = this.userAnswers.get(q.id) || null;
|
|
1527
|
+
const evaluator = this.evaluators.get(q.questionType);
|
|
1528
|
+
if (evaluator) {
|
|
1529
|
+
const { isCorrect } = await evaluator.evaluate(q, userAnswer);
|
|
1530
|
+
const pointsForThisQuestion = (_a = q.points) != null ? _a : 0;
|
|
1531
|
+
updateMap(loPerformanceMap, q.learningObjective, pointsForThisQuestion, isCorrect);
|
|
1532
|
+
updateMap(categoryPerformanceMap, q.category, pointsForThisQuestion, isCorrect);
|
|
1533
|
+
updateMap(topicPerformanceMap, q.topic, pointsForThisQuestion, isCorrect);
|
|
1534
|
+
updateMap(difficultyPerformanceMap, q.difficulty, pointsForThisQuestion, isCorrect);
|
|
1535
|
+
updateMap(bloomLevelPerformanceMap, q.bloomLevel, pointsForThisQuestion, isCorrect);
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
const formatPerformanceArray = (map, keyName) => {
|
|
1539
|
+
return Array.from(map.entries()).map(([key, data]) => ({
|
|
1540
|
+
[keyName]: key,
|
|
1541
|
+
totalQuestions: data.totalQuestions,
|
|
1542
|
+
correctQuestions: data.correctQuestions,
|
|
1543
|
+
pointsEarned: data.pointsEarned,
|
|
1544
|
+
maxPoints: data.maxPoints,
|
|
1545
|
+
percentage: data.maxPoints > 0 ? parseFloat((data.pointsEarned / data.maxPoints * 100).toFixed(2)) : 0
|
|
1546
|
+
}));
|
|
1547
|
+
};
|
|
1548
|
+
return {
|
|
1549
|
+
performanceByLearningObjective: formatPerformanceArray(loPerformanceMap, "learningObjective"),
|
|
1550
|
+
performanceByCategory: formatPerformanceArray(categoryPerformanceMap, "category"),
|
|
1551
|
+
performanceByTopic: formatPerformanceArray(topicPerformanceMap, "topic"),
|
|
1552
|
+
performanceByDifficulty: formatPerformanceArray(difficultyPerformanceMap, "difficulty"),
|
|
1553
|
+
performanceByBloomLevel: formatPerformanceArray(bloomLevelPerformanceMap, "bloomLevel")
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
792
1556
|
async _sendResultsToWebhook(results) {
|
|
793
1557
|
var _a;
|
|
794
1558
|
if (!((_a = this.config.settings) == null ? void 0 : _a.webhookUrl)) {
|
|
@@ -856,80 +1620,15 @@ var QuizEngine = class {
|
|
|
856
1620
|
results.scormError = e instanceof Error ? e.message : "Unknown SCORM data sending error.";
|
|
857
1621
|
}
|
|
858
1622
|
}
|
|
859
|
-
_calculateMetadataPerformance() {
|
|
860
|
-
const loPerformanceMap = /* @__PURE__ */ new Map();
|
|
861
|
-
const categoryPerformanceMap = /* @__PURE__ */ new Map();
|
|
862
|
-
const topicPerformanceMap = /* @__PURE__ */ new Map();
|
|
863
|
-
const difficultyPerformanceMap = /* @__PURE__ */ new Map();
|
|
864
|
-
const bloomLevelPerformanceMap = /* @__PURE__ */ new Map();
|
|
865
|
-
const updateMap = (map, key, points, isCorrect) => {
|
|
866
|
-
if (!key) return;
|
|
867
|
-
const current = map.get(key) || { totalQuestions: 0, correctQuestions: 0, pointsEarned: 0, maxPoints: 0 };
|
|
868
|
-
current.totalQuestions++;
|
|
869
|
-
current.maxPoints += points;
|
|
870
|
-
if (isCorrect) {
|
|
871
|
-
current.correctQuestions++;
|
|
872
|
-
current.pointsEarned += points;
|
|
873
|
-
}
|
|
874
|
-
map.set(key, current);
|
|
875
|
-
};
|
|
876
|
-
this.questions.forEach((q) => {
|
|
877
|
-
var _a;
|
|
878
|
-
const qResult = this.userAnswers.get(q.id);
|
|
879
|
-
const { isCorrect } = this.evaluateQuestion(q, qResult || null);
|
|
880
|
-
const pointsForThisQuestion = (_a = q.points) != null ? _a : 0;
|
|
881
|
-
updateMap(loPerformanceMap, q.learningObjective, pointsForThisQuestion, isCorrect);
|
|
882
|
-
updateMap(categoryPerformanceMap, q.category, pointsForThisQuestion, isCorrect);
|
|
883
|
-
updateMap(topicPerformanceMap, q.topic, pointsForThisQuestion, isCorrect);
|
|
884
|
-
updateMap(difficultyPerformanceMap, q.difficulty, pointsForThisQuestion, isCorrect);
|
|
885
|
-
updateMap(bloomLevelPerformanceMap, q.bloomLevel, pointsForThisQuestion, isCorrect);
|
|
886
|
-
});
|
|
887
|
-
const formatPerformanceArray = (map, keyName) => {
|
|
888
|
-
return Array.from(map.entries()).map(([key, data]) => ({
|
|
889
|
-
[keyName]: key,
|
|
890
|
-
totalQuestions: data.totalQuestions,
|
|
891
|
-
correctQuestions: data.correctQuestions,
|
|
892
|
-
pointsEarned: data.pointsEarned,
|
|
893
|
-
maxPoints: data.maxPoints,
|
|
894
|
-
percentage: data.maxPoints > 0 ? parseFloat((data.pointsEarned / data.maxPoints * 100).toFixed(2)) : 0
|
|
895
|
-
}));
|
|
896
|
-
};
|
|
897
|
-
return {
|
|
898
|
-
performanceByLearningObjective: formatPerformanceArray(loPerformanceMap, "learningObjective"),
|
|
899
|
-
performanceByCategory: formatPerformanceArray(categoryPerformanceMap, "category"),
|
|
900
|
-
performanceByTopic: formatPerformanceArray(topicPerformanceMap, "topic"),
|
|
901
|
-
performanceByDifficulty: formatPerformanceArray(difficultyPerformanceMap, "difficulty"),
|
|
902
|
-
performanceByBloomLevel: formatPerformanceArray(bloomLevelPerformanceMap, "bloomLevel")
|
|
903
|
-
};
|
|
904
|
-
}
|
|
905
|
-
getElapsedTime() {
|
|
906
|
-
return Date.now() - this.overallStartTime;
|
|
907
|
-
}
|
|
908
|
-
destroy() {
|
|
909
|
-
this.stopTimer();
|
|
910
|
-
this._recordCurrentQuestionTime();
|
|
911
|
-
if (this.scormService && this.scormService.hasAPI()) {
|
|
912
|
-
if (["initialized", "committed", "sending_data"].includes(this.quizResultState.scormStatus || "")) {
|
|
913
|
-
const termResult = this.scormService.terminate();
|
|
914
|
-
if (termResult.success) {
|
|
915
|
-
this.quizResultState.scormStatus = "terminated";
|
|
916
|
-
} else {
|
|
917
|
-
this.quizResultState.scormStatus = "error";
|
|
918
|
-
this.quizResultState.scormError = termResult.error || "SCORM termination failed on destroy.";
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
this.scormService = null;
|
|
923
|
-
}
|
|
924
1623
|
};
|
|
925
1624
|
function cn(...inputs) {
|
|
926
1625
|
return tailwindMerge.twMerge(clsx.clsx(inputs));
|
|
927
1626
|
}
|
|
928
1627
|
|
|
929
1628
|
// src/react-ui/components/elements/radio-group.tsx
|
|
930
|
-
var RadioGroup =
|
|
1629
|
+
var RadioGroup = React28__namespace.forwardRef((_a, ref) => {
|
|
931
1630
|
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
932
|
-
return /* @__PURE__ */
|
|
1631
|
+
return /* @__PURE__ */ React28__namespace.createElement(
|
|
933
1632
|
RadioGroupPrimitive__namespace.Root,
|
|
934
1633
|
__spreadProps(__spreadValues({
|
|
935
1634
|
className: cn("grid gap-2", className)
|
|
@@ -939,9 +1638,9 @@ var RadioGroup = React25__namespace.forwardRef((_a, ref) => {
|
|
|
939
1638
|
);
|
|
940
1639
|
});
|
|
941
1640
|
RadioGroup.displayName = RadioGroupPrimitive__namespace.Root.displayName;
|
|
942
|
-
var RadioGroupItem =
|
|
1641
|
+
var RadioGroupItem = React28__namespace.forwardRef((_a, ref) => {
|
|
943
1642
|
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
944
|
-
return /* @__PURE__ */
|
|
1643
|
+
return /* @__PURE__ */ React28__namespace.createElement(
|
|
945
1644
|
RadioGroupPrimitive__namespace.Item,
|
|
946
1645
|
__spreadValues({
|
|
947
1646
|
ref,
|
|
@@ -950,16 +1649,16 @@ var RadioGroupItem = React25__namespace.forwardRef((_a, ref) => {
|
|
|
950
1649
|
className
|
|
951
1650
|
)
|
|
952
1651
|
}, props),
|
|
953
|
-
/* @__PURE__ */
|
|
1652
|
+
/* @__PURE__ */ React28__namespace.createElement(RadioGroupPrimitive__namespace.Indicator, { className: "flex items-center justify-center" }, /* @__PURE__ */ React28__namespace.createElement(lucideReact.Circle, { className: "h-2.5 w-2.5 fill-current text-current" }))
|
|
954
1653
|
);
|
|
955
1654
|
});
|
|
956
1655
|
RadioGroupItem.displayName = RadioGroupPrimitive__namespace.Item.displayName;
|
|
957
1656
|
var labelVariants = classVarianceAuthority.cva(
|
|
958
1657
|
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
959
1658
|
);
|
|
960
|
-
var Label =
|
|
1659
|
+
var Label = React28__namespace.forwardRef((_a, ref) => {
|
|
961
1660
|
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
962
|
-
return /* @__PURE__ */
|
|
1661
|
+
return /* @__PURE__ */ React28__namespace.createElement(
|
|
963
1662
|
LabelPrimitive__namespace.Root,
|
|
964
1663
|
__spreadValues({
|
|
965
1664
|
ref,
|
|
@@ -968,9 +1667,9 @@ var Label = React25__namespace.forwardRef((_a, ref) => {
|
|
|
968
1667
|
);
|
|
969
1668
|
});
|
|
970
1669
|
Label.displayName = LabelPrimitive__namespace.Root.displayName;
|
|
971
|
-
var Card =
|
|
1670
|
+
var Card = React28__namespace.forwardRef((_a, ref) => {
|
|
972
1671
|
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
973
|
-
return /* @__PURE__ */
|
|
1672
|
+
return /* @__PURE__ */ React28__namespace.createElement(
|
|
974
1673
|
"div",
|
|
975
1674
|
__spreadValues({
|
|
976
1675
|
ref,
|
|
@@ -982,9 +1681,9 @@ var Card = React25__namespace.forwardRef((_a, ref) => {
|
|
|
982
1681
|
);
|
|
983
1682
|
});
|
|
984
1683
|
Card.displayName = "Card";
|
|
985
|
-
var CardHeader =
|
|
1684
|
+
var CardHeader = React28__namespace.forwardRef((_a, ref) => {
|
|
986
1685
|
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
987
|
-
return /* @__PURE__ */
|
|
1686
|
+
return /* @__PURE__ */ React28__namespace.createElement(
|
|
988
1687
|
"div",
|
|
989
1688
|
__spreadValues({
|
|
990
1689
|
ref,
|
|
@@ -993,9 +1692,9 @@ var CardHeader = React25__namespace.forwardRef((_a, ref) => {
|
|
|
993
1692
|
);
|
|
994
1693
|
});
|
|
995
1694
|
CardHeader.displayName = "CardHeader";
|
|
996
|
-
var CardTitle =
|
|
1695
|
+
var CardTitle = React28__namespace.forwardRef((_a, ref) => {
|
|
997
1696
|
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
998
|
-
return /* @__PURE__ */
|
|
1697
|
+
return /* @__PURE__ */ React28__namespace.createElement(
|
|
999
1698
|
"div",
|
|
1000
1699
|
__spreadValues({
|
|
1001
1700
|
ref,
|
|
@@ -1007,9 +1706,9 @@ var CardTitle = React25__namespace.forwardRef((_a, ref) => {
|
|
|
1007
1706
|
);
|
|
1008
1707
|
});
|
|
1009
1708
|
CardTitle.displayName = "CardTitle";
|
|
1010
|
-
var CardDescription =
|
|
1709
|
+
var CardDescription = React28__namespace.forwardRef((_a, ref) => {
|
|
1011
1710
|
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
1012
|
-
return /* @__PURE__ */
|
|
1711
|
+
return /* @__PURE__ */ React28__namespace.createElement(
|
|
1013
1712
|
"div",
|
|
1014
1713
|
__spreadValues({
|
|
1015
1714
|
ref,
|
|
@@ -1018,14 +1717,14 @@ var CardDescription = React25__namespace.forwardRef((_a, ref) => {
|
|
|
1018
1717
|
);
|
|
1019
1718
|
});
|
|
1020
1719
|
CardDescription.displayName = "CardDescription";
|
|
1021
|
-
var CardContent =
|
|
1720
|
+
var CardContent = React28__namespace.forwardRef((_a, ref) => {
|
|
1022
1721
|
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
1023
|
-
return /* @__PURE__ */
|
|
1722
|
+
return /* @__PURE__ */ React28__namespace.createElement("div", __spreadValues({ ref, className: cn("p-6 pt-0", className) }, props));
|
|
1024
1723
|
});
|
|
1025
1724
|
CardContent.displayName = "CardContent";
|
|
1026
|
-
var CardFooter =
|
|
1725
|
+
var CardFooter = React28__namespace.forwardRef((_a, ref) => {
|
|
1027
1726
|
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
1028
|
-
return /* @__PURE__ */
|
|
1727
|
+
return /* @__PURE__ */ React28__namespace.createElement(
|
|
1029
1728
|
"div",
|
|
1030
1729
|
__spreadValues({
|
|
1031
1730
|
ref,
|
|
@@ -1076,7 +1775,7 @@ var MarkdownRenderer = ({
|
|
|
1076
1775
|
const processedContent = processContentForVideos(content);
|
|
1077
1776
|
return (
|
|
1078
1777
|
// Using Tailwind Typography for beautiful default styling of markdown content
|
|
1079
|
-
/* @__PURE__ */
|
|
1778
|
+
/* @__PURE__ */ React28__namespace.default.createElement("div", { className: `prose dark:prose-invert max-w-none ${className}` }, /* @__PURE__ */ React28__namespace.default.createElement(
|
|
1080
1779
|
ReactMarkdown__default.default,
|
|
1081
1780
|
{
|
|
1082
1781
|
remarkPlugins: [remarkGfm__default.default, remarkMath__default.default],
|
|
@@ -1089,7 +1788,7 @@ var MarkdownRenderer = ({
|
|
|
1089
1788
|
const { platform, id } = getVideoId(src);
|
|
1090
1789
|
if (platform && id) {
|
|
1091
1790
|
const videoSrc = platform === "youtube" ? `https://www.youtube.com/embed/${id}` : `https://player.vimeo.com/video/${id}`;
|
|
1092
|
-
return /* @__PURE__ */
|
|
1791
|
+
return /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "aspect-w-16 aspect-h-9 my-4" }, /* @__PURE__ */ React28__namespace.default.createElement(
|
|
1093
1792
|
"iframe",
|
|
1094
1793
|
{
|
|
1095
1794
|
src: videoSrc,
|
|
@@ -1102,7 +1801,7 @@ var MarkdownRenderer = ({
|
|
|
1102
1801
|
}
|
|
1103
1802
|
return (
|
|
1104
1803
|
// eslint-disable-next-line @next/next/no-img-element
|
|
1105
|
-
/* @__PURE__ */
|
|
1804
|
+
/* @__PURE__ */ React28__namespace.default.createElement(
|
|
1106
1805
|
"img",
|
|
1107
1806
|
__spreadProps(__spreadValues({}, props), {
|
|
1108
1807
|
style: {
|
|
@@ -1119,12 +1818,12 @@ var MarkdownRenderer = ({
|
|
|
1119
1818
|
// Override the default table to add responsive wrapper
|
|
1120
1819
|
table: (_c) => {
|
|
1121
1820
|
var _d = _c, { node } = _d, props = __objRest(_d, ["node"]);
|
|
1122
|
-
return /* @__PURE__ */
|
|
1821
|
+
return /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "overflow-x-auto" }, /* @__PURE__ */ React28__namespace.default.createElement("table", __spreadProps(__spreadValues({}, props), { className: "my-4 w-full text-sm" })));
|
|
1123
1822
|
},
|
|
1124
1823
|
// Override default blockquote for better styling
|
|
1125
1824
|
blockquote: (_e) => {
|
|
1126
1825
|
var _f = _e, { node } = _f, props = __objRest(_f, ["node"]);
|
|
1127
|
-
return /* @__PURE__ */
|
|
1826
|
+
return /* @__PURE__ */ React28__namespace.default.createElement(
|
|
1128
1827
|
"blockquote",
|
|
1129
1828
|
__spreadProps(__spreadValues({}, props), {
|
|
1130
1829
|
className: "border-l-4 border-primary bg-muted/50 p-4 my-4 italic"
|
|
@@ -1149,7 +1848,7 @@ var MultipleChoiceQuestionUI = ({
|
|
|
1149
1848
|
const handleSelection = (value) => {
|
|
1150
1849
|
onAnswerChange(value);
|
|
1151
1850
|
};
|
|
1152
|
-
return /* @__PURE__ */
|
|
1851
|
+
return /* @__PURE__ */ React28__namespace.default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React28__namespace.default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React28__namespace.default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, /* @__PURE__ */ React28__namespace.default.createElement(MarkdownRenderer, { content: prompt })), points && /* @__PURE__ */ React28__namespace.default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", points)), /* @__PURE__ */ React28__namespace.default.createElement(CardContent, { className: "p-0" }, /* @__PURE__ */ React28__namespace.default.createElement(
|
|
1153
1852
|
RadioGroup,
|
|
1154
1853
|
{
|
|
1155
1854
|
value: userAnswer || void 0,
|
|
@@ -1172,17 +1871,17 @@ var MultipleChoiceQuestionUI = ({
|
|
|
1172
1871
|
} else {
|
|
1173
1872
|
itemClassName += isSelected ? " border-primary bg-primary/10" : " border-muted";
|
|
1174
1873
|
}
|
|
1175
|
-
return /* @__PURE__ */
|
|
1874
|
+
return /* @__PURE__ */ React28__namespace.default.createElement(
|
|
1176
1875
|
Label,
|
|
1177
1876
|
{
|
|
1178
1877
|
key: option.id,
|
|
1179
1878
|
htmlFor: option.id,
|
|
1180
1879
|
className: itemClassName
|
|
1181
1880
|
},
|
|
1182
|
-
/* @__PURE__ */
|
|
1881
|
+
/* @__PURE__ */ React28__namespace.default.createElement("div", { className: "flex items-center" }, /* @__PURE__ */ React28__namespace.default.createElement(RadioGroupItem, { value: option.id, id: option.id, className: "mr-3" }), /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "text-base flex-1" }, /* @__PURE__ */ React28__namespace.default.createElement(MarkdownRenderer, { content: option.text })))
|
|
1183
1882
|
);
|
|
1184
1883
|
})
|
|
1185
|
-
), showCorrectAnswer && explanation && /* @__PURE__ */
|
|
1884
|
+
), showCorrectAnswer && explanation && /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "mt-4 p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Explanation:"), /* @__PURE__ */ React28__namespace.default.createElement(MarkdownRenderer, { content: explanation, className: "text-sm text-accent-foreground/80" }))));
|
|
1186
1885
|
};
|
|
1187
1886
|
var TrueFalseQuestionUI = ({
|
|
1188
1887
|
question,
|
|
@@ -1198,7 +1897,7 @@ var TrueFalseQuestionUI = ({
|
|
|
1198
1897
|
const handleSelection = (value) => {
|
|
1199
1898
|
onAnswerChange(value);
|
|
1200
1899
|
};
|
|
1201
|
-
return /* @__PURE__ */
|
|
1900
|
+
return /* @__PURE__ */ React28__namespace.default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React28__namespace.default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React28__namespace.default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, /* @__PURE__ */ React28__namespace.default.createElement(MarkdownRenderer, { content: prompt })), points && /* @__PURE__ */ React28__namespace.default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", points)), /* @__PURE__ */ React28__namespace.default.createElement(CardContent, { className: "p-0" }, /* @__PURE__ */ React28__namespace.default.createElement(
|
|
1202
1901
|
RadioGroup,
|
|
1203
1902
|
{
|
|
1204
1903
|
value: userAnswer || void 0,
|
|
@@ -1221,21 +1920,21 @@ var TrueFalseQuestionUI = ({
|
|
|
1221
1920
|
} else {
|
|
1222
1921
|
itemClassName += isSelected ? " border-primary bg-primary/10" : " border-muted";
|
|
1223
1922
|
}
|
|
1224
|
-
return /* @__PURE__ */
|
|
1923
|
+
return /* @__PURE__ */ React28__namespace.default.createElement(
|
|
1225
1924
|
Label,
|
|
1226
1925
|
{
|
|
1227
1926
|
key: option.id,
|
|
1228
1927
|
htmlFor: option.id,
|
|
1229
1928
|
className: itemClassName
|
|
1230
1929
|
},
|
|
1231
|
-
/* @__PURE__ */
|
|
1930
|
+
/* @__PURE__ */ React28__namespace.default.createElement("div", { className: "flex items-center" }, /* @__PURE__ */ React28__namespace.default.createElement(RadioGroupItem, { value: option.value, id: option.id, className: "mr-3" }), /* @__PURE__ */ React28__namespace.default.createElement("span", { className: "text-base" }, option.label))
|
|
1232
1931
|
);
|
|
1233
1932
|
})
|
|
1234
|
-
), showCorrectAnswer && explanation && /* @__PURE__ */
|
|
1933
|
+
), showCorrectAnswer && explanation && /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "mt-4 p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Explanation:"), /* @__PURE__ */ React28__namespace.default.createElement(MarkdownRenderer, { content: explanation, className: "text-sm text-accent-foreground/80" }))));
|
|
1235
1934
|
};
|
|
1236
|
-
var Checkbox =
|
|
1935
|
+
var Checkbox = React28__namespace.forwardRef((_a, ref) => {
|
|
1237
1936
|
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
1238
|
-
return /* @__PURE__ */
|
|
1937
|
+
return /* @__PURE__ */ React28__namespace.createElement(
|
|
1239
1938
|
CheckboxPrimitive__namespace.Root,
|
|
1240
1939
|
__spreadValues({
|
|
1241
1940
|
ref,
|
|
@@ -1244,12 +1943,12 @@ var Checkbox = React25__namespace.forwardRef((_a, ref) => {
|
|
|
1244
1943
|
className
|
|
1245
1944
|
)
|
|
1246
1945
|
}, props),
|
|
1247
|
-
/* @__PURE__ */
|
|
1946
|
+
/* @__PURE__ */ React28__namespace.createElement(
|
|
1248
1947
|
CheckboxPrimitive__namespace.Indicator,
|
|
1249
1948
|
{
|
|
1250
1949
|
className: cn("flex items-center justify-center text-current")
|
|
1251
1950
|
},
|
|
1252
|
-
/* @__PURE__ */
|
|
1951
|
+
/* @__PURE__ */ React28__namespace.createElement(lucideReact.Check, { className: "h-4 w-4" })
|
|
1253
1952
|
)
|
|
1254
1953
|
);
|
|
1255
1954
|
});
|
|
@@ -1277,7 +1976,7 @@ var MultipleResponseQuestionUI = ({
|
|
|
1277
1976
|
}
|
|
1278
1977
|
onAnswerChange(newAnswers.length > 0 ? newAnswers : null);
|
|
1279
1978
|
};
|
|
1280
|
-
return /* @__PURE__ */
|
|
1979
|
+
return /* @__PURE__ */ React28__namespace.default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React28__namespace.default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React28__namespace.default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, /* @__PURE__ */ React28__namespace.default.createElement(MarkdownRenderer, { content: prompt })), points && /* @__PURE__ */ React28__namespace.default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", points)), /* @__PURE__ */ React28__namespace.default.createElement(CardContent, { className: "p-0" }, /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "space-y-3", role: "group", "aria-labelledby": `question-prompt-${questionId}` }, options.map((option) => {
|
|
1281
1980
|
const isSelected = Array.isArray(userAnswer) && userAnswer.includes(option.id);
|
|
1282
1981
|
const isCorrectOption = correctAnswerIds.includes(option.id);
|
|
1283
1982
|
let itemClassName = "p-4 rounded-lg border-2 transition-all cursor-pointer hover:border-primary flex items-center";
|
|
@@ -1290,14 +1989,14 @@ var MultipleResponseQuestionUI = ({
|
|
|
1290
1989
|
} else {
|
|
1291
1990
|
itemClassName += isSelected ? " border-primary bg-primary/10" : " border-muted";
|
|
1292
1991
|
}
|
|
1293
|
-
return /* @__PURE__ */
|
|
1992
|
+
return /* @__PURE__ */ React28__namespace.default.createElement(
|
|
1294
1993
|
Label,
|
|
1295
1994
|
{
|
|
1296
1995
|
key: option.id,
|
|
1297
1996
|
htmlFor: option.id,
|
|
1298
1997
|
className: itemClassName
|
|
1299
1998
|
},
|
|
1300
|
-
/* @__PURE__ */
|
|
1999
|
+
/* @__PURE__ */ React28__namespace.default.createElement(
|
|
1301
2000
|
Checkbox,
|
|
1302
2001
|
{
|
|
1303
2002
|
id: option.id,
|
|
@@ -1307,14 +2006,14 @@ var MultipleResponseQuestionUI = ({
|
|
|
1307
2006
|
"aria-label": option.text.replace(/<[^>]*>?/gm, "")
|
|
1308
2007
|
}
|
|
1309
2008
|
),
|
|
1310
|
-
/* @__PURE__ */
|
|
2009
|
+
/* @__PURE__ */ React28__namespace.default.createElement("div", { className: "text-base flex-1" }, /* @__PURE__ */ React28__namespace.default.createElement(MarkdownRenderer, { content: option.text }))
|
|
1311
2010
|
);
|
|
1312
|
-
})), showCorrectAnswer && explanation && /* @__PURE__ */
|
|
2011
|
+
})), showCorrectAnswer && explanation && /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "mt-4 p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Explanation:"), /* @__PURE__ */ React28__namespace.default.createElement(MarkdownRenderer, { content: explanation, className: "text-sm text-accent-foreground/80" }))));
|
|
1313
2012
|
};
|
|
1314
|
-
var Input =
|
|
2013
|
+
var Input = React28__namespace.forwardRef(
|
|
1315
2014
|
(_a, ref) => {
|
|
1316
2015
|
var _b = _a, { className, type } = _b, props = __objRest(_b, ["className", "type"]);
|
|
1317
|
-
return /* @__PURE__ */
|
|
2016
|
+
return /* @__PURE__ */ React28__namespace.createElement(
|
|
1318
2017
|
"input",
|
|
1319
2018
|
__spreadValues({
|
|
1320
2019
|
type,
|
|
@@ -1348,7 +2047,7 @@ var ShortAnswerQuestionUI = ({
|
|
|
1348
2047
|
(accAns) => isCaseSensitive ? accAns.trim() === userAnswerTrimmed : accAns.trim().toLowerCase() === userAnswerTrimmed.toLowerCase()
|
|
1349
2048
|
);
|
|
1350
2049
|
}
|
|
1351
|
-
return /* @__PURE__ */
|
|
2050
|
+
return /* @__PURE__ */ React28__namespace.default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React28__namespace.default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React28__namespace.default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, /* @__PURE__ */ React28__namespace.default.createElement(MarkdownRenderer, { content: prompt })), points && /* @__PURE__ */ React28__namespace.default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", points)), /* @__PURE__ */ React28__namespace.default.createElement(CardContent, { className: "p-0" }, /* @__PURE__ */ React28__namespace.default.createElement(Label, { htmlFor: `short-answer-input-${questionId}`, className: "sr-only" }, "Your Answer"), /* @__PURE__ */ React28__namespace.default.createElement(
|
|
1352
2051
|
Input,
|
|
1353
2052
|
{
|
|
1354
2053
|
id: `short-answer-input-${questionId}`,
|
|
@@ -1361,7 +2060,7 @@ var ShortAnswerQuestionUI = ({
|
|
|
1361
2060
|
${showCorrectAnswer && userAnswer ? isActuallyCorrect ? "border-green-500 focus-visible:ring-green-500" : "border-destructive focus-visible:ring-destructive" : "border-input"}
|
|
1362
2061
|
`
|
|
1363
2062
|
}
|
|
1364
|
-
), showCorrectAnswer && /* @__PURE__ */
|
|
2063
|
+
), showCorrectAnswer && /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "mt-4 space-y-2" }, userAnswer && !isActuallyCorrect && /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-sm text-destructive" }, "Your answer was marked incorrect."), /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Accepted Answers:"), /* @__PURE__ */ React28__namespace.default.createElement("ul", { className: "list-disc list-inside text-sm text-accent-foreground/80" }, acceptedAnswers.map((ans, idx) => /* @__PURE__ */ React28__namespace.default.createElement("li", { key: idx }, ans))), isCaseSensitive && /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-xs text-muted-foreground mt-1" }, "(Case-sensitive)")), explanation && /* @__PURE__ */ React28__namespace.default.createElement("div", { id: `explanation-${questionId}`, className: "mt-2 p-3 bg-muted/30 border border-muted rounded-md" }, /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-sm font-semibold" }, "Explanation:"), /* @__PURE__ */ React28__namespace.default.createElement(MarkdownRenderer, { content: explanation, className: "text-sm text-muted-foreground" })))));
|
|
1365
2064
|
};
|
|
1366
2065
|
var NumericQuestionUI = ({
|
|
1367
2066
|
question,
|
|
@@ -1384,7 +2083,7 @@ var NumericQuestionUI = ({
|
|
|
1384
2083
|
isActuallyCorrect = tolerance !== void 0 && tolerance !== null ? Math.abs(userAnswerNum - correctAnswerValue) <= tolerance : userAnswerNum === correctAnswerValue;
|
|
1385
2084
|
}
|
|
1386
2085
|
}
|
|
1387
|
-
return /* @__PURE__ */
|
|
2086
|
+
return /* @__PURE__ */ React28__namespace.default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React28__namespace.default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React28__namespace.default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, /* @__PURE__ */ React28__namespace.default.createElement(MarkdownRenderer, { content: prompt })), points && /* @__PURE__ */ React28__namespace.default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", points)), /* @__PURE__ */ React28__namespace.default.createElement(CardContent, { className: "p-0" }, /* @__PURE__ */ React28__namespace.default.createElement(Label, { htmlFor: `numeric-input-${questionId}`, className: "sr-only" }, "Your Answer"), /* @__PURE__ */ React28__namespace.default.createElement(
|
|
1388
2087
|
Input,
|
|
1389
2088
|
{
|
|
1390
2089
|
id: `numeric-input-${questionId}`,
|
|
@@ -1399,7 +2098,7 @@ var NumericQuestionUI = ({
|
|
|
1399
2098
|
${showCorrectAnswer && userAnswer !== null && userAnswer !== "" ? isActuallyCorrect ? "border-green-500 focus-visible:ring-green-500" : "border-destructive focus-visible:ring-destructive" : "border-input"}
|
|
1400
2099
|
`
|
|
1401
2100
|
}
|
|
1402
|
-
), showCorrectAnswer && /* @__PURE__ */
|
|
2101
|
+
), showCorrectAnswer && /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "mt-4 space-y-2" }, userAnswer !== null && userAnswer !== "" && !isActuallyCorrect && /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-sm text-destructive" }, "Your answer was marked incorrect."), /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Correct Answer:"), /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-sm text-accent-foreground/80" }, correctAnswerValue, tolerance !== void 0 && tolerance !== null && tolerance > 0 && ` (Tolerance: \xB1${tolerance}, Accepted range: ${correctAnswerValue - tolerance} to ${correctAnswerValue + tolerance})`)), explanation && /* @__PURE__ */ React28__namespace.default.createElement("div", { id: `explanation-${questionId}`, className: "mt-2 p-3 bg-muted/30 border border-muted rounded-md" }, /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-sm font-semibold" }, "Explanation:"), /* @__PURE__ */ React28__namespace.default.createElement(MarkdownRenderer, { content: explanation, className: "text-sm text-muted-foreground" })))));
|
|
1403
2102
|
};
|
|
1404
2103
|
var FillInTheBlanksQuestionUI = ({
|
|
1405
2104
|
question,
|
|
@@ -1408,8 +2107,8 @@ var FillInTheBlanksQuestionUI = ({
|
|
|
1408
2107
|
showCorrectAnswer = false
|
|
1409
2108
|
}) => {
|
|
1410
2109
|
const { prompt, segments, answers: correctAnswersMap, points, explanation, id: questionId, isCaseSensitive } = question;
|
|
1411
|
-
const [userInputs, setUserInputs] =
|
|
1412
|
-
|
|
2110
|
+
const [userInputs, setUserInputs] = React28.useState({});
|
|
2111
|
+
React28.useEffect(() => {
|
|
1413
2112
|
if (userAnswer && typeof userAnswer === "object" && !Array.isArray(userAnswer)) {
|
|
1414
2113
|
setUserInputs(userAnswer);
|
|
1415
2114
|
} else {
|
|
@@ -1439,10 +2138,10 @@ var FillInTheBlanksQuestionUI = ({
|
|
|
1439
2138
|
(accVal) => caseSensitive ? accVal.trim() === userAnswerForBlank : accVal.trim().toLowerCase() === userAnswerForBlank.toLowerCase()
|
|
1440
2139
|
);
|
|
1441
2140
|
};
|
|
1442
|
-
return /* @__PURE__ */
|
|
2141
|
+
return /* @__PURE__ */ React28__namespace.default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React28__namespace.default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React28__namespace.default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, /* @__PURE__ */ React28__namespace.default.createElement(MarkdownRenderer, { content: prompt })), points && /* @__PURE__ */ React28__namespace.default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", points)), /* @__PURE__ */ React28__namespace.default.createElement(CardContent, { className: "p-0" }, /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "text-base leading-relaxed flex flex-wrap items-center gap-x-1", "aria-labelledby": `question-prompt-${questionId}` }, segments.map((segment, index) => {
|
|
1443
2142
|
var _a;
|
|
1444
2143
|
if (segment.type === "text") {
|
|
1445
|
-
return /* @__PURE__ */
|
|
2144
|
+
return /* @__PURE__ */ React28__namespace.default.createElement(
|
|
1446
2145
|
MarkdownRenderer,
|
|
1447
2146
|
{
|
|
1448
2147
|
key: `text-${index}`,
|
|
@@ -1460,7 +2159,7 @@ var FillInTheBlanksQuestionUI = ({
|
|
|
1460
2159
|
} else {
|
|
1461
2160
|
inputClassName += " border-input";
|
|
1462
2161
|
}
|
|
1463
|
-
return /* @__PURE__ */
|
|
2162
|
+
return /* @__PURE__ */ React28__namespace.default.createElement(
|
|
1464
2163
|
Input,
|
|
1465
2164
|
{
|
|
1466
2165
|
key: blankId,
|
|
@@ -1476,11 +2175,11 @@ var FillInTheBlanksQuestionUI = ({
|
|
|
1476
2175
|
);
|
|
1477
2176
|
}
|
|
1478
2177
|
return null;
|
|
1479
|
-
})), showCorrectAnswer && explanation && /* @__PURE__ */
|
|
2178
|
+
})), showCorrectAnswer && explanation && /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "mt-6 p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Gi\u1EA3i th\xEDch chung:"), /* @__PURE__ */ React28__namespace.default.createElement(MarkdownRenderer, { content: explanation, className: "text-sm text-accent-foreground/80" })), showCorrectAnswer && /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "mt-4 space-y-3" }, correctAnswersMap.map((ansDef) => {
|
|
1480
2179
|
var _a;
|
|
1481
2180
|
const isBlankCorrect = getCorrectnessForBlank(ansDef.blankId);
|
|
1482
2181
|
const userAnswerDisplay = userInputs[ansDef.blankId] || "Ch\u01B0a tr\u1EA3 l\u1EDDi";
|
|
1483
|
-
return /* @__PURE__ */
|
|
2182
|
+
return /* @__PURE__ */ React28__namespace.default.createElement("div", { key: `feedback-${ansDef.blankId}`, className: `p-2 border rounded-md ${isBlankCorrect ? "border-green-500/50 bg-green-500/10" : "border-destructive/50 bg-destructive/10"}` }, /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-sm" }, /* @__PURE__ */ React28__namespace.default.createElement("span", { className: "font-semibold" }, "\xD4 tr\u1ED1ng '", ((_a = segments.find((s) => s.id === ansDef.blankId && s.type === "blank")) == null ? void 0 : _a.id) || ansDef.blankId, "':"), ' B\u1EA1n \u0111\xE3 \u0111i\u1EC1n: "', userAnswerDisplay, '".'), /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-xs" }, "\u0110\xE1p \xE1n ch\u1EA5p nh\u1EADn: ", ansDef.acceptedValues.join(", ")));
|
|
1484
2183
|
}))));
|
|
1485
2184
|
};
|
|
1486
2185
|
var buttonVariants = classVarianceAuthority.cva(
|
|
@@ -1508,11 +2207,11 @@ var buttonVariants = classVarianceAuthority.cva(
|
|
|
1508
2207
|
}
|
|
1509
2208
|
}
|
|
1510
2209
|
);
|
|
1511
|
-
var Button =
|
|
2210
|
+
var Button = React28__namespace.forwardRef(
|
|
1512
2211
|
(_a, ref) => {
|
|
1513
2212
|
var _b = _a, { className, variant, size, asChild = false } = _b, props = __objRest(_b, ["className", "variant", "size", "asChild"]);
|
|
1514
2213
|
const Comp = asChild ? reactSlot.Slot : "button";
|
|
1515
|
-
return /* @__PURE__ */
|
|
2214
|
+
return /* @__PURE__ */ React28__namespace.createElement(
|
|
1516
2215
|
Comp,
|
|
1517
2216
|
__spreadValues({
|
|
1518
2217
|
className: cn(buttonVariants({ variant, size, className })),
|
|
@@ -1529,9 +2228,9 @@ var SequenceQuestionUI = ({
|
|
|
1529
2228
|
showCorrectAnswer = false
|
|
1530
2229
|
}) => {
|
|
1531
2230
|
const { prompt, items, points, explanation, id: questionId, correctOrder } = question;
|
|
1532
|
-
const [selectedSequence, setSelectedSequence] =
|
|
1533
|
-
const [availableItems, setAvailableItems] =
|
|
1534
|
-
|
|
2231
|
+
const [selectedSequence, setSelectedSequence] = React28.useState([]);
|
|
2232
|
+
const [availableItems, setAvailableItems] = React28.useState([]);
|
|
2233
|
+
React28.useEffect(() => {
|
|
1535
2234
|
const initialUserOrder = Array.isArray(userAnswer) ? userAnswer : [];
|
|
1536
2235
|
const initialSelected = [];
|
|
1537
2236
|
const initialAvailable = [...items];
|
|
@@ -1572,37 +2271,37 @@ var SequenceQuestionUI = ({
|
|
|
1572
2271
|
if (!showCorrectAnswer || !Array.isArray(userAnswer) || userAnswer.length <= index) return null;
|
|
1573
2272
|
const userItemId = userAnswer[index];
|
|
1574
2273
|
const correctItemId = correctOrder[index];
|
|
1575
|
-
return userItemId === correctItemId ? /* @__PURE__ */
|
|
2274
|
+
return userItemId === correctItemId ? /* @__PURE__ */ React28__namespace.default.createElement(lucideReact.CheckCircle, { className: "h-4 w-4 text-green-500 ml-2 flex-shrink-0" }) : /* @__PURE__ */ React28__namespace.default.createElement(lucideReact.XCircle, { className: "h-4 w-4 text-destructive ml-2 flex-shrink-0" });
|
|
1576
2275
|
};
|
|
1577
|
-
return /* @__PURE__ */
|
|
2276
|
+
return /* @__PURE__ */ React28__namespace.default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React28__namespace.default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React28__namespace.default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, /* @__PURE__ */ React28__namespace.default.createElement(MarkdownRenderer, { content: prompt })), points && /* @__PURE__ */ React28__namespace.default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", points)), /* @__PURE__ */ React28__namespace.default.createElement(CardContent, { className: "p-0 space-y-6" }, /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "space-y-3" }, /* @__PURE__ */ React28__namespace.default.createElement(Label, { className: "font-semibold" }, "S\u1EAFp x\u1EBFp c\xE1c m\u1EE5c sau theo \u0111\xFAng th\u1EE9 t\u1EF1:"), /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-2" }, availableItems.map((item) => /* @__PURE__ */ React28__namespace.default.createElement(
|
|
1578
2277
|
Button,
|
|
1579
2278
|
{
|
|
1580
2279
|
key: item.id,
|
|
1581
2280
|
variant: "outline",
|
|
1582
2281
|
onClick: () => handleSelectItem(item),
|
|
1583
|
-
className: "justify-start text-left h-auto py-2 px-3",
|
|
2282
|
+
className: "justify-start text-left h-auto py-2 px-3 whitespace-normal",
|
|
1584
2283
|
disabled: showCorrectAnswer
|
|
1585
2284
|
},
|
|
1586
|
-
/* @__PURE__ */
|
|
1587
|
-
))), availableItems.length === 0 && selectedSequence.length > 0 && /* @__PURE__ */
|
|
2285
|
+
/* @__PURE__ */ React28__namespace.default.createElement(MarkdownRenderer, { content: item.content })
|
|
2286
|
+
))), availableItems.length === 0 && selectedSequence.length > 0 && /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-sm text-muted-foreground" }, 'T\u1EA5t c\u1EA3 c\xE1c m\u1EE5c \u0111\xE3 \u0111\u01B0\u1EE3c ch\u1ECDn. Nh\u1EA5p v\xE0o m\u1EE5c trong "Th\u1EE9 t\u1EF1 b\u1EA1n \u0111\xE3 ch\u1ECDn" \u0111\u1EC3 b\u1ECF ch\u1ECDn.')), /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "space-y-2" }, /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "flex justify-between items-center" }, /* @__PURE__ */ React28__namespace.default.createElement(Label, { className: "font-semibold" }, "Th\u1EE9 t\u1EF1 b\u1EA1n \u0111\xE3 ch\u1ECDn:"), /* @__PURE__ */ React28__namespace.default.createElement(Button, { variant: "ghost", size: "sm", onClick: handleResetSequence, disabled: showCorrectAnswer || selectedSequence.length === 0 }, /* @__PURE__ */ React28__namespace.default.createElement(lucideReact.RotateCcw, { className: "mr-2 h-3.5 w-3.5" }), " \u0110\u1EB7t l\u1EA1i")), selectedSequence.length === 0 ? /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-sm text-muted-foreground p-3 border border-dashed rounded-md" }, "Ch\u01B0a ch\u1ECDn m\u1EE5c n\xE0o. Nh\u1EA5p v\xE0o c\xE1c m\u1EE5c \u1EDF tr\xEAn \u0111\u1EC3 b\u1EAFt \u0111\u1EA7u.") : /* @__PURE__ */ React28__namespace.default.createElement("ul", { className: "space-y-2" }, selectedSequence.map((item, index) => /* @__PURE__ */ React28__namespace.default.createElement(
|
|
1588
2287
|
"li",
|
|
1589
2288
|
{
|
|
1590
2289
|
key: item.id,
|
|
1591
2290
|
onClick: () => handleRemoveFromSequence(item, index),
|
|
1592
|
-
className: `flex items-center justify-between p-3 border rounded-md ${showCorrectAnswer ? (userAnswer == null ? void 0 : userAnswer[index]) === correctOrder[index] ? "border-green-500 bg-green-500/10" : "border-destructive bg-destructive/10" : "bg-muted/30 cursor-pointer hover:border-destructive/50"} transition-colors`
|
|
2291
|
+
className: `flex items-center justify-between p-3 border rounded-md whitespace-normal ${showCorrectAnswer ? (userAnswer == null ? void 0 : userAnswer[index]) === correctOrder[index] ? "border-green-500 bg-green-500/10" : "border-destructive bg-destructive/10" : "bg-muted/30 cursor-pointer hover:border-destructive/50"} transition-colors`
|
|
1593
2292
|
},
|
|
1594
|
-
/* @__PURE__ */
|
|
1595
|
-
showCorrectAnswer ? getFeedbackIcon(index) : /* @__PURE__ */
|
|
1596
|
-
)))), showCorrectAnswer && /* @__PURE__ */
|
|
2293
|
+
/* @__PURE__ */ React28__namespace.default.createElement("div", { className: "flex-grow flex items-center" }, /* @__PURE__ */ React28__namespace.default.createElement("span", { className: "font-semibold mr-2" }, index + 1, "."), /* @__PURE__ */ React28__namespace.default.createElement(MarkdownRenderer, { content: item.content })),
|
|
2294
|
+
showCorrectAnswer ? getFeedbackIcon(index) : /* @__PURE__ */ React28__namespace.default.createElement(lucideReact.XCircle, { className: "h-4 w-4 text-muted-foreground hover:text-destructive flex-shrink-0" })
|
|
2295
|
+
)))), showCorrectAnswer && /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "mt-4 space-y-2" }, /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Th\u1EE9 t\u1EF1 \u0111\xFAng:"), /* @__PURE__ */ React28__namespace.default.createElement("ol", { className: "list-decimal list-inside text-sm text-accent-foreground/80 space-y-1 mt-1" }, correctOrder.map((itemId) => {
|
|
1597
2296
|
const item = items.find((i) => i.id === itemId);
|
|
1598
|
-
return /* @__PURE__ */
|
|
1599
|
-
}))), explanation && /* @__PURE__ */
|
|
2297
|
+
return /* @__PURE__ */ React28__namespace.default.createElement("li", { key: itemId }, /* @__PURE__ */ React28__namespace.default.createElement(MarkdownRenderer, { content: item ? item.content : "Kh\xF4ng t\xECm th\u1EA5y m\u1EE5c" }));
|
|
2298
|
+
}))), explanation && /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "mt-2 p-3 bg-muted/30 border border-muted rounded-md" }, /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-sm font-semibold" }, "Gi\u1EA3i th\xEDch:"), /* @__PURE__ */ React28__namespace.default.createElement(MarkdownRenderer, { content: explanation, className: "text-sm text-muted-foreground" })))));
|
|
1600
2299
|
};
|
|
1601
2300
|
var Select = SelectPrimitive__namespace.Root;
|
|
1602
2301
|
var SelectValue = SelectPrimitive__namespace.Value;
|
|
1603
|
-
var SelectTrigger =
|
|
2302
|
+
var SelectTrigger = React28__namespace.forwardRef((_a, ref) => {
|
|
1604
2303
|
var _b = _a, { className, children } = _b, props = __objRest(_b, ["className", "children"]);
|
|
1605
|
-
return /* @__PURE__ */
|
|
2304
|
+
return /* @__PURE__ */ React28__namespace.createElement(
|
|
1606
2305
|
SelectPrimitive__namespace.Trigger,
|
|
1607
2306
|
__spreadValues({
|
|
1608
2307
|
ref,
|
|
@@ -1612,13 +2311,13 @@ var SelectTrigger = React25__namespace.forwardRef((_a, ref) => {
|
|
|
1612
2311
|
)
|
|
1613
2312
|
}, props),
|
|
1614
2313
|
children,
|
|
1615
|
-
/* @__PURE__ */
|
|
2314
|
+
/* @__PURE__ */ React28__namespace.createElement(SelectPrimitive__namespace.Icon, { asChild: true }, /* @__PURE__ */ React28__namespace.createElement(lucideReact.ChevronDown, { className: "h-4 w-4 opacity-50" }))
|
|
1616
2315
|
);
|
|
1617
2316
|
});
|
|
1618
2317
|
SelectTrigger.displayName = SelectPrimitive__namespace.Trigger.displayName;
|
|
1619
|
-
var SelectScrollUpButton =
|
|
2318
|
+
var SelectScrollUpButton = React28__namespace.forwardRef((_a, ref) => {
|
|
1620
2319
|
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
1621
|
-
return /* @__PURE__ */
|
|
2320
|
+
return /* @__PURE__ */ React28__namespace.createElement(
|
|
1622
2321
|
SelectPrimitive__namespace.ScrollUpButton,
|
|
1623
2322
|
__spreadValues({
|
|
1624
2323
|
ref,
|
|
@@ -1627,13 +2326,13 @@ var SelectScrollUpButton = React25__namespace.forwardRef((_a, ref) => {
|
|
|
1627
2326
|
className
|
|
1628
2327
|
)
|
|
1629
2328
|
}, props),
|
|
1630
|
-
/* @__PURE__ */
|
|
2329
|
+
/* @__PURE__ */ React28__namespace.createElement(lucideReact.ChevronUp, { className: "h-4 w-4" })
|
|
1631
2330
|
);
|
|
1632
2331
|
});
|
|
1633
2332
|
SelectScrollUpButton.displayName = SelectPrimitive__namespace.ScrollUpButton.displayName;
|
|
1634
|
-
var SelectScrollDownButton =
|
|
2333
|
+
var SelectScrollDownButton = React28__namespace.forwardRef((_a, ref) => {
|
|
1635
2334
|
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
1636
|
-
return /* @__PURE__ */
|
|
2335
|
+
return /* @__PURE__ */ React28__namespace.createElement(
|
|
1637
2336
|
SelectPrimitive__namespace.ScrollDownButton,
|
|
1638
2337
|
__spreadValues({
|
|
1639
2338
|
ref,
|
|
@@ -1642,13 +2341,13 @@ var SelectScrollDownButton = React25__namespace.forwardRef((_a, ref) => {
|
|
|
1642
2341
|
className
|
|
1643
2342
|
)
|
|
1644
2343
|
}, props),
|
|
1645
|
-
/* @__PURE__ */
|
|
2344
|
+
/* @__PURE__ */ React28__namespace.createElement(lucideReact.ChevronDown, { className: "h-4 w-4" })
|
|
1646
2345
|
);
|
|
1647
2346
|
});
|
|
1648
2347
|
SelectScrollDownButton.displayName = SelectPrimitive__namespace.ScrollDownButton.displayName;
|
|
1649
|
-
var SelectContent =
|
|
2348
|
+
var SelectContent = React28__namespace.forwardRef((_a, ref) => {
|
|
1650
2349
|
var _b = _a, { className, children, position = "popper" } = _b, props = __objRest(_b, ["className", "children", "position"]);
|
|
1651
|
-
return /* @__PURE__ */
|
|
2350
|
+
return /* @__PURE__ */ React28__namespace.createElement(SelectPrimitive__namespace.Portal, null, /* @__PURE__ */ React28__namespace.createElement(
|
|
1652
2351
|
SelectPrimitive__namespace.Content,
|
|
1653
2352
|
__spreadValues({
|
|
1654
2353
|
ref,
|
|
@@ -1659,8 +2358,8 @@ var SelectContent = React25__namespace.forwardRef((_a, ref) => {
|
|
|
1659
2358
|
),
|
|
1660
2359
|
position
|
|
1661
2360
|
}, props),
|
|
1662
|
-
/* @__PURE__ */
|
|
1663
|
-
/* @__PURE__ */
|
|
2361
|
+
/* @__PURE__ */ React28__namespace.createElement(SelectScrollUpButton, null),
|
|
2362
|
+
/* @__PURE__ */ React28__namespace.createElement(
|
|
1664
2363
|
SelectPrimitive__namespace.Viewport,
|
|
1665
2364
|
{
|
|
1666
2365
|
className: cn(
|
|
@@ -1670,13 +2369,13 @@ var SelectContent = React25__namespace.forwardRef((_a, ref) => {
|
|
|
1670
2369
|
},
|
|
1671
2370
|
children
|
|
1672
2371
|
),
|
|
1673
|
-
/* @__PURE__ */
|
|
2372
|
+
/* @__PURE__ */ React28__namespace.createElement(SelectScrollDownButton, null)
|
|
1674
2373
|
));
|
|
1675
2374
|
});
|
|
1676
2375
|
SelectContent.displayName = SelectPrimitive__namespace.Content.displayName;
|
|
1677
|
-
var SelectLabel =
|
|
2376
|
+
var SelectLabel = React28__namespace.forwardRef((_a, ref) => {
|
|
1678
2377
|
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
1679
|
-
return /* @__PURE__ */
|
|
2378
|
+
return /* @__PURE__ */ React28__namespace.createElement(
|
|
1680
2379
|
SelectPrimitive__namespace.Label,
|
|
1681
2380
|
__spreadValues({
|
|
1682
2381
|
ref,
|
|
@@ -1685,9 +2384,9 @@ var SelectLabel = React25__namespace.forwardRef((_a, ref) => {
|
|
|
1685
2384
|
);
|
|
1686
2385
|
});
|
|
1687
2386
|
SelectLabel.displayName = SelectPrimitive__namespace.Label.displayName;
|
|
1688
|
-
var SelectItem =
|
|
2387
|
+
var SelectItem = React28__namespace.forwardRef((_a, ref) => {
|
|
1689
2388
|
var _b = _a, { className, children } = _b, props = __objRest(_b, ["className", "children"]);
|
|
1690
|
-
return /* @__PURE__ */
|
|
2389
|
+
return /* @__PURE__ */ React28__namespace.createElement(
|
|
1691
2390
|
SelectPrimitive__namespace.Item,
|
|
1692
2391
|
__spreadValues({
|
|
1693
2392
|
ref,
|
|
@@ -1696,14 +2395,14 @@ var SelectItem = React25__namespace.forwardRef((_a, ref) => {
|
|
|
1696
2395
|
className
|
|
1697
2396
|
)
|
|
1698
2397
|
}, props),
|
|
1699
|
-
/* @__PURE__ */
|
|
1700
|
-
/* @__PURE__ */
|
|
2398
|
+
/* @__PURE__ */ React28__namespace.createElement("span", { className: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center" }, /* @__PURE__ */ React28__namespace.createElement(SelectPrimitive__namespace.ItemIndicator, null, /* @__PURE__ */ React28__namespace.createElement(lucideReact.Check, { className: "h-4 w-4" }))),
|
|
2399
|
+
/* @__PURE__ */ React28__namespace.createElement(SelectPrimitive__namespace.ItemText, null, children)
|
|
1701
2400
|
);
|
|
1702
2401
|
});
|
|
1703
2402
|
SelectItem.displayName = SelectPrimitive__namespace.Item.displayName;
|
|
1704
|
-
var SelectSeparator =
|
|
2403
|
+
var SelectSeparator = React28__namespace.forwardRef((_a, ref) => {
|
|
1705
2404
|
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
1706
|
-
return /* @__PURE__ */
|
|
2405
|
+
return /* @__PURE__ */ React28__namespace.createElement(
|
|
1707
2406
|
SelectPrimitive__namespace.Separator,
|
|
1708
2407
|
__spreadValues({
|
|
1709
2408
|
ref,
|
|
@@ -1719,16 +2418,16 @@ var MatchingQuestionUI = ({
|
|
|
1719
2418
|
showCorrectAnswer = false
|
|
1720
2419
|
}) => {
|
|
1721
2420
|
const { prompt, prompts, options: initialOptions, points, explanation, correctAnswerMap, id: questionId } = question;
|
|
1722
|
-
const [currentAnswers, setCurrentAnswers] =
|
|
1723
|
-
const [shuffledOptions, setShuffledOptions] =
|
|
1724
|
-
|
|
2421
|
+
const [currentAnswers, setCurrentAnswers] = React28.useState({});
|
|
2422
|
+
const [shuffledOptions, setShuffledOptions] = React28.useState(initialOptions);
|
|
2423
|
+
React28.useEffect(() => {
|
|
1725
2424
|
if (question.shuffleOptions) {
|
|
1726
2425
|
setShuffledOptions([...initialOptions].sort(() => Math.random() - 0.5));
|
|
1727
2426
|
} else {
|
|
1728
2427
|
setShuffledOptions(initialOptions);
|
|
1729
2428
|
}
|
|
1730
2429
|
}, [initialOptions, question.shuffleOptions]);
|
|
1731
|
-
|
|
2430
|
+
React28.useEffect(() => {
|
|
1732
2431
|
if (userAnswer && typeof userAnswer === "object" && !Array.isArray(userAnswer)) {
|
|
1733
2432
|
setCurrentAnswers(userAnswer);
|
|
1734
2433
|
} else {
|
|
@@ -1756,7 +2455,7 @@ var MatchingQuestionUI = ({
|
|
|
1756
2455
|
}
|
|
1757
2456
|
return htmlString.replace(/<[^>]*>?/gm, "");
|
|
1758
2457
|
};
|
|
1759
|
-
return /* @__PURE__ */
|
|
2458
|
+
return /* @__PURE__ */ React28__namespace.default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React28__namespace.default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React28__namespace.default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, /* @__PURE__ */ React28__namespace.default.createElement(MarkdownRenderer, { content: prompt })), points && /* @__PURE__ */ React28__namespace.default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", points)), /* @__PURE__ */ React28__namespace.default.createElement(CardContent, { className: "p-0 space-y-4" }, /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-4" }, prompts.map((promptItem) => {
|
|
1760
2459
|
var _a;
|
|
1761
2460
|
const selectedOptionId = currentAnswers[promptItem.id] || "";
|
|
1762
2461
|
const correctOptionId = getCorrectOptionIdForPrompt(promptItem.id);
|
|
@@ -1765,21 +2464,21 @@ var MatchingQuestionUI = ({
|
|
|
1765
2464
|
if (showCorrectAnswer && selectedOptionId) {
|
|
1766
2465
|
borderColor = isSelectionCorrect ? "border-green-500" : "border-destructive";
|
|
1767
2466
|
}
|
|
1768
|
-
return /* @__PURE__ */
|
|
2467
|
+
return /* @__PURE__ */ React28__namespace.default.createElement("div", { key: promptItem.id, className: `p-3 border rounded-md ${borderColor} transition-colors bg-background` }, /* @__PURE__ */ React28__namespace.default.createElement(Label, { htmlFor: `select-prompt-${promptItem.id}`, className: "font-medium text-base block mb-2 whitespace-normal" }, /* @__PURE__ */ React28__namespace.default.createElement(MarkdownRenderer, { content: promptItem.content })), /* @__PURE__ */ React28__namespace.default.createElement(
|
|
1769
2468
|
Select,
|
|
1770
2469
|
{
|
|
1771
2470
|
value: selectedOptionId,
|
|
1772
2471
|
onValueChange: (value) => handleSelectChange(promptItem.id, value),
|
|
1773
2472
|
disabled: showCorrectAnswer
|
|
1774
2473
|
},
|
|
1775
|
-
/* @__PURE__ */
|
|
1776
|
-
/* @__PURE__ */
|
|
1777
|
-
), showCorrectAnswer && selectedOptionId && (isSelectionCorrect ? /* @__PURE__ */
|
|
1778
|
-
})), showCorrectAnswer && explanation && /* @__PURE__ */
|
|
2474
|
+
/* @__PURE__ */ React28__namespace.default.createElement(SelectTrigger, { id: `select-prompt-${promptItem.id}` }, /* @__PURE__ */ React28__namespace.default.createElement(SelectValue, { placeholder: "Ch\u1ECDn \u0111\xE1p \xE1n..." })),
|
|
2475
|
+
/* @__PURE__ */ React28__namespace.default.createElement(SelectContent, null, shuffledOptions.map((option) => /* @__PURE__ */ React28__namespace.default.createElement(SelectItem, { key: option.id, value: option.id, className: "whitespace-normal" }, getPlainText(option.content))))
|
|
2476
|
+
), showCorrectAnswer && selectedOptionId && (isSelectionCorrect ? /* @__PURE__ */ React28__namespace.default.createElement(lucideReact.CheckCircle, { className: "h-5 w-5 text-green-500 mt-2 inline-block" }) : /* @__PURE__ */ React28__namespace.default.createElement(lucideReact.XCircle, { className: "h-5 w-5 text-destructive mt-2 inline-block" })), showCorrectAnswer && !selectedOptionId && correctOptionId && /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-xs text-muted-foreground mt-1" }, "Ch\u01B0a ch\u1ECDn. \u0110\xE1p \xE1n \u0111\xFAng: ", getPlainText((_a = shuffledOptions.find((o) => o.id === correctOptionId)) == null ? void 0 : _a.content)));
|
|
2477
|
+
})), showCorrectAnswer && explanation && /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "mt-6 p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Gi\u1EA3i th\xEDch:"), /* @__PURE__ */ React28__namespace.default.createElement(MarkdownRenderer, { content: explanation, className: "text-sm text-accent-foreground/80" })), showCorrectAnswer && /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "mt-4 space-y-2" }, /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "font-semibold text-md" }, "\u0110\xE1p \xE1n \u0111\xFAng:"), /* @__PURE__ */ React28__namespace.default.createElement("ul", { className: "list-disc list-inside space-y-1 text-sm" }, correctAnswerMap.map((map) => {
|
|
1779
2478
|
var _a, _b;
|
|
1780
2479
|
const promptText = (_a = prompts.find((p) => p.id === map.promptId)) == null ? void 0 : _a.content;
|
|
1781
2480
|
const optionText = (_b = initialOptions.find((o) => o.id === map.optionId)) == null ? void 0 : _b.content;
|
|
1782
|
-
return /* @__PURE__ */
|
|
2481
|
+
return /* @__PURE__ */ React28__namespace.default.createElement("li", { key: map.promptId }, /* @__PURE__ */ React28__namespace.default.createElement(MarkdownRenderer, { content: `<strong>${getPlainText(promptText) || "N/A"}</strong> gh\xE9p v\u1EDBi <strong>${getPlainText(optionText) || "N/A"}</strong>` }));
|
|
1783
2482
|
})))));
|
|
1784
2483
|
};
|
|
1785
2484
|
var DragAndDropQuestionUI = ({
|
|
@@ -1789,8 +2488,8 @@ var DragAndDropQuestionUI = ({
|
|
|
1789
2488
|
showCorrectAnswer = false
|
|
1790
2489
|
}) => {
|
|
1791
2490
|
const { prompt, draggableItems, dropZones, points, explanation, answerMap, id: questionId, backgroundImageUrl } = question;
|
|
1792
|
-
const [currentAnswers, setCurrentAnswers] =
|
|
1793
|
-
|
|
2491
|
+
const [currentAnswers, setCurrentAnswers] = React28.useState({});
|
|
2492
|
+
React28.useEffect(() => {
|
|
1794
2493
|
if (userAnswer && typeof userAnswer === "object" && !Array.isArray(userAnswer)) {
|
|
1795
2494
|
setCurrentAnswers(userAnswer);
|
|
1796
2495
|
} else {
|
|
@@ -1809,7 +2508,7 @@ var DragAndDropQuestionUI = ({
|
|
|
1809
2508
|
var _a;
|
|
1810
2509
|
return (_a = answerMap.find((map) => map.draggableId === draggableItemId)) == null ? void 0 : _a.dropZoneId;
|
|
1811
2510
|
};
|
|
1812
|
-
return /* @__PURE__ */
|
|
2511
|
+
return /* @__PURE__ */ React28__namespace.default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React28__namespace.default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React28__namespace.default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, /* @__PURE__ */ React28__namespace.default.createElement(MarkdownRenderer, { content: prompt })), points && /* @__PURE__ */ React28__namespace.default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", points)), /* @__PURE__ */ React28__namespace.default.createElement(CardContent, { className: "p-0 space-y-4" }, backgroundImageUrl && /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "mb-4 overflow-hidden rounded-md border" }, /* @__PURE__ */ React28__namespace.default.createElement("img", { src: backgroundImageUrl, alt: question.imageAltText || "Drag and drop background", className: "w-full h-auto object-contain max-h-[300px]", "data-ai-hint": "abstract pattern" })), /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "space-y-3" }, /* @__PURE__ */ React28__namespace.default.createElement(Label, { className: "font-semibold" }, "Gh\xE9p c\xE1c m\u1EE5c sau v\xE0o \u0111\xFAng v\u1ECB tr\xED:"), draggableItems.map((item) => {
|
|
1813
2512
|
const selectedDropZoneId = currentAnswers[item.id] || "";
|
|
1814
2513
|
const correctDropZoneId = getCorrectDropZoneIdForDraggable(item.id);
|
|
1815
2514
|
const isSelectionCorrect = showCorrectAnswer && selectedDropZoneId ? selectedDropZoneId === correctDropZoneId : null;
|
|
@@ -1819,21 +2518,21 @@ var DragAndDropQuestionUI = ({
|
|
|
1819
2518
|
} else {
|
|
1820
2519
|
itemStyle += " border-muted";
|
|
1821
2520
|
}
|
|
1822
|
-
return /* @__PURE__ */
|
|
2521
|
+
return /* @__PURE__ */ React28__namespace.default.createElement("div", { key: item.id, className: itemStyle }, /* @__PURE__ */ React28__namespace.default.createElement(Label, { htmlFor: `select-draggable-${item.id}`, className: "font-medium text-base mb-2 sm:mb-0 sm:mr-4 flex-1" }, /* @__PURE__ */ React28__namespace.default.createElement(MarkdownRenderer, { content: item.content })), /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "flex items-center space-x-2 w-full sm:w-auto" }, /* @__PURE__ */ React28__namespace.default.createElement(
|
|
1823
2522
|
Select,
|
|
1824
2523
|
{
|
|
1825
2524
|
value: selectedDropZoneId,
|
|
1826
2525
|
onValueChange: (value) => handleSelectChange(item.id, value),
|
|
1827
2526
|
disabled: showCorrectAnswer
|
|
1828
2527
|
},
|
|
1829
|
-
/* @__PURE__ */
|
|
1830
|
-
/* @__PURE__ */
|
|
1831
|
-
), showCorrectAnswer && selectedDropZoneId && (isSelectionCorrect ? /* @__PURE__ */
|
|
1832
|
-
})), showCorrectAnswer && explanation && /* @__PURE__ */
|
|
2528
|
+
/* @__PURE__ */ React28__namespace.default.createElement(SelectTrigger, { id: `select-draggable-${item.id}`, className: "w-full sm:min-w-[200px]" }, /* @__PURE__ */ React28__namespace.default.createElement(SelectValue, { placeholder: "Ch\u1ECDn v\u1ECB tr\xED..." })),
|
|
2529
|
+
/* @__PURE__ */ React28__namespace.default.createElement(SelectContent, null, dropZones.map((zone) => /* @__PURE__ */ React28__namespace.default.createElement(SelectItem, { key: zone.id, value: zone.id }, zone.label.replace(/<[^>]*>?/gm, ""))))
|
|
2530
|
+
), showCorrectAnswer && selectedDropZoneId && (isSelectionCorrect ? /* @__PURE__ */ React28__namespace.default.createElement(lucideReact.CheckCircle, { className: "h-5 w-5 text-green-500 flex-shrink-0" }) : /* @__PURE__ */ React28__namespace.default.createElement(lucideReact.XCircle, { className: "h-5 w-5 text-destructive flex-shrink-0" }))));
|
|
2531
|
+
})), showCorrectAnswer && explanation && /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "mt-6 p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Gi\u1EA3i th\xEDch:"), /* @__PURE__ */ React28__namespace.default.createElement(MarkdownRenderer, { content: explanation, className: "text-sm text-accent-foreground/80" })), showCorrectAnswer && /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "mt-4 space-y-2" }, /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "font-semibold text-md" }, "\u0110\xE1p \xE1n \u0111\xFAng:"), /* @__PURE__ */ React28__namespace.default.createElement("ul", { className: "list-disc list-inside space-y-1 text-sm" }, answerMap.map((map) => {
|
|
1833
2532
|
var _a, _b;
|
|
1834
2533
|
const draggableText = (_a = draggableItems.find((d) => d.id === map.draggableId)) == null ? void 0 : _a.content;
|
|
1835
|
-
const dropZoneText = (_b = dropZones.find((
|
|
1836
|
-
return /* @__PURE__ */
|
|
2534
|
+
const dropZoneText = (_b = dropZones.find((z3) => z3.id === map.dropZoneId)) == null ? void 0 : _b.label;
|
|
2535
|
+
return /* @__PURE__ */ React28__namespace.default.createElement("li", { key: map.draggableId }, /* @__PURE__ */ React28__namespace.default.createElement(MarkdownRenderer, { content: `<strong>${draggableText || "N/A"}</strong> v\xE0o <strong>${dropZoneText || "N/A"}</strong>` }));
|
|
1837
2536
|
})))));
|
|
1838
2537
|
};
|
|
1839
2538
|
var HotspotQuestionUI = ({
|
|
@@ -1843,8 +2542,8 @@ var HotspotQuestionUI = ({
|
|
|
1843
2542
|
showCorrectAnswer = false
|
|
1844
2543
|
}) => {
|
|
1845
2544
|
const { prompt, imageUrl, imageAltText, hotspots, points, explanation, correctAnswerIds, id: questionId } = question;
|
|
1846
|
-
const [selectedIds, setSelectedIds] =
|
|
1847
|
-
|
|
2545
|
+
const [selectedIds, setSelectedIds] = React28.useState([]);
|
|
2546
|
+
React28.useEffect(() => {
|
|
1848
2547
|
if (Array.isArray(userAnswer)) {
|
|
1849
2548
|
setSelectedIds(userAnswer);
|
|
1850
2549
|
} else {
|
|
@@ -1904,7 +2603,7 @@ var HotspotQuestionUI = ({
|
|
|
1904
2603
|
tempDiv.innerHTML = htmlString;
|
|
1905
2604
|
return tempDiv.textContent || tempDiv.innerText || "";
|
|
1906
2605
|
};
|
|
1907
|
-
return /* @__PURE__ */
|
|
2606
|
+
return /* @__PURE__ */ React28__namespace.default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React28__namespace.default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React28__namespace.default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, /* @__PURE__ */ React28__namespace.default.createElement(MarkdownRenderer, { content: prompt })), points && /* @__PURE__ */ React28__namespace.default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", points)), /* @__PURE__ */ React28__namespace.default.createElement(CardContent, { className: "p-0 space-y-4" }, /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "relative w-full border border-muted rounded-md overflow-hidden", style: { maxWidth: "100%", maxHeight: "500px" } }, /* @__PURE__ */ React28__namespace.default.createElement(
|
|
1908
2607
|
"img",
|
|
1909
2608
|
{
|
|
1910
2609
|
src: imageUrl,
|
|
@@ -1912,7 +2611,7 @@ var HotspotQuestionUI = ({
|
|
|
1912
2611
|
className: "block max-w-full max-h-full object-contain",
|
|
1913
2612
|
"data-ai-hint": question.imageAltText ? question.imageAltText.split(" ").slice(0, 2).join(" ") : "diagram illustration"
|
|
1914
2613
|
}
|
|
1915
|
-
), hotspots.map((hotspot) => /* @__PURE__ */
|
|
2614
|
+
), hotspots.map((hotspot) => /* @__PURE__ */ React28__namespace.default.createElement(
|
|
1916
2615
|
"div",
|
|
1917
2616
|
{
|
|
1918
2617
|
key: hotspot.id,
|
|
@@ -1926,9 +2625,9 @@ var HotspotQuestionUI = ({
|
|
|
1926
2625
|
if (e.key === "Enter" || e.key === " ") handleHotspotClick(hotspot.id);
|
|
1927
2626
|
}
|
|
1928
2627
|
}
|
|
1929
|
-
))), showCorrectAnswer && explanation && /* @__PURE__ */
|
|
2628
|
+
))), showCorrectAnswer && explanation && /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "mt-4 p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Explanation:"), /* @__PURE__ */ React28__namespace.default.createElement(MarkdownRenderer, { content: explanation, className: "text-sm text-accent-foreground/80" })), showCorrectAnswer && /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "mt-4 space-y-2" }, /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "font-semibold text-md" }, "Correct Hotspots:"), /* @__PURE__ */ React28__namespace.default.createElement("ul", { className: "list-disc list-inside space-y-1 text-sm" }, (correctAnswerIds || []).map((id) => {
|
|
1930
2629
|
const hotspot = hotspots.find((h) => h.id === id);
|
|
1931
|
-
return /* @__PURE__ */
|
|
2630
|
+
return /* @__PURE__ */ React28__namespace.default.createElement("li", { key: id }, /* @__PURE__ */ React28__namespace.default.createElement(MarkdownRenderer, { content: (hotspot == null ? void 0 : hotspot.description) || (hotspot == null ? void 0 : hotspot.id) || "N/A", className: "inline" }));
|
|
1932
2631
|
})))));
|
|
1933
2632
|
};
|
|
1934
2633
|
var loadScript = (src, async = true) => {
|
|
@@ -2020,10 +2719,10 @@ var loadBlocklyScript = () => {
|
|
|
2020
2719
|
});
|
|
2021
2720
|
};
|
|
2022
2721
|
var useBlocklyLoader = () => {
|
|
2023
|
-
const [isLoading, setIsLoading] =
|
|
2024
|
-
const [loadError, setLoadError] =
|
|
2025
|
-
const [isReady, setIsReady] =
|
|
2026
|
-
const attemptLoad =
|
|
2722
|
+
const [isLoading, setIsLoading] = React28.useState(true);
|
|
2723
|
+
const [loadError, setLoadError] = React28.useState(null);
|
|
2724
|
+
const [isReady, setIsReady] = React28.useState(false);
|
|
2725
|
+
const attemptLoad = React28.useCallback(() => {
|
|
2027
2726
|
setLoadError(null);
|
|
2028
2727
|
setIsReady(false);
|
|
2029
2728
|
loadBlocklyScript().then(() => {
|
|
@@ -2036,27 +2735,27 @@ var useBlocklyLoader = () => {
|
|
|
2036
2735
|
setIsReady(false);
|
|
2037
2736
|
});
|
|
2038
2737
|
}, []);
|
|
2039
|
-
|
|
2738
|
+
React28.useEffect(() => {
|
|
2040
2739
|
if (isLoading && !isReady && !loadError) attemptLoad();
|
|
2041
2740
|
}, [isLoading, isReady, loadError, attemptLoad]);
|
|
2042
|
-
const retry =
|
|
2741
|
+
const retry = React28.useCallback(() => {
|
|
2043
2742
|
setLoadError(null);
|
|
2044
2743
|
setIsReady(false);
|
|
2045
2744
|
setIsLoading(true);
|
|
2046
2745
|
}, []);
|
|
2047
2746
|
return { isLoading, loadError, isReady, retry };
|
|
2048
2747
|
};
|
|
2049
|
-
var BlocklyProgrammingQuestionUI =
|
|
2748
|
+
var BlocklyProgrammingQuestionUI = React28__namespace.default.forwardRef(({
|
|
2050
2749
|
question,
|
|
2051
2750
|
userAnswer,
|
|
2052
2751
|
showCorrectAnswer = false
|
|
2053
2752
|
}, ref) => {
|
|
2054
|
-
const blocklyDivRef =
|
|
2055
|
-
const workspaceRef =
|
|
2056
|
-
const [isInitializingComponent, setIsInitializingComponent] =
|
|
2057
|
-
const [componentError, setComponentError] =
|
|
2753
|
+
const blocklyDivRef = React28.useRef(null);
|
|
2754
|
+
const workspaceRef = React28.useRef(null);
|
|
2755
|
+
const [isInitializingComponent, setIsInitializingComponent] = React28.useState(false);
|
|
2756
|
+
const [componentError, setComponentError] = React28.useState(null);
|
|
2058
2757
|
const { isLoading: blocklyLoading, loadError: blocklyLoadError, isReady: blocklyReady, retry } = useBlocklyLoader();
|
|
2059
|
-
|
|
2758
|
+
React28.useImperativeHandle(ref, () => ({
|
|
2060
2759
|
getWorkspaceXml: () => {
|
|
2061
2760
|
var _a, _b;
|
|
2062
2761
|
if (workspaceRef.current && blocklyReady) {
|
|
@@ -2076,7 +2775,7 @@ var BlocklyProgrammingQuestionUI = React25__namespace.default.forwardRef(({
|
|
|
2076
2775
|
return null;
|
|
2077
2776
|
}
|
|
2078
2777
|
}));
|
|
2079
|
-
const initializeBlocklyWorkspace =
|
|
2778
|
+
const initializeBlocklyWorkspace = React28.useCallback(() => {
|
|
2080
2779
|
var _a;
|
|
2081
2780
|
if (!blocklyReady || !blocklyDivRef.current) return;
|
|
2082
2781
|
const LocalBlockly = window.Blockly;
|
|
@@ -2184,7 +2883,7 @@ var BlocklyProgrammingQuestionUI = React25__namespace.default.forwardRef(({
|
|
|
2184
2883
|
showCorrectAnswer,
|
|
2185
2884
|
userAnswer
|
|
2186
2885
|
]);
|
|
2187
|
-
|
|
2886
|
+
React28.useEffect(() => {
|
|
2188
2887
|
if (blocklyReady && blocklyDivRef.current) {
|
|
2189
2888
|
initializeBlocklyWorkspace();
|
|
2190
2889
|
}
|
|
@@ -2200,7 +2899,7 @@ var BlocklyProgrammingQuestionUI = React25__namespace.default.forwardRef(({
|
|
|
2200
2899
|
}
|
|
2201
2900
|
};
|
|
2202
2901
|
}, [blocklyReady, question.id, initializeBlocklyWorkspace]);
|
|
2203
|
-
|
|
2902
|
+
React28.useEffect(() => {
|
|
2204
2903
|
let resizeTimeout;
|
|
2205
2904
|
const handleResize = () => {
|
|
2206
2905
|
clearTimeout(resizeTimeout);
|
|
@@ -2221,7 +2920,7 @@ var BlocklyProgrammingQuestionUI = React25__namespace.default.forwardRef(({
|
|
|
2221
2920
|
}, [blocklyReady]);
|
|
2222
2921
|
const workspaceHeight = showCorrectAnswer ? "300px" : "450px";
|
|
2223
2922
|
const workspaceContainerId = `blockly-workspace-container-${question.id}`;
|
|
2224
|
-
return /* @__PURE__ */
|
|
2923
|
+
return /* @__PURE__ */ React28__namespace.default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React28__namespace.default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React28__namespace.default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, /* @__PURE__ */ React28__namespace.default.createElement(MarkdownRenderer, { content: question.prompt })), question.points && /* @__PURE__ */ React28__namespace.default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", question.points)), /* @__PURE__ */ React28__namespace.default.createElement(CardContent, { className: "p-0" }, blocklyLoading && /* @__PURE__ */ React28__namespace.default.createElement(
|
|
2225
2924
|
"div",
|
|
2226
2925
|
{
|
|
2227
2926
|
style: {
|
|
@@ -2233,8 +2932,8 @@ var BlocklyProgrammingQuestionUI = React25__namespace.default.forwardRef(({
|
|
|
2233
2932
|
},
|
|
2234
2933
|
className: "flex items-center justify-center"
|
|
2235
2934
|
},
|
|
2236
|
-
/* @__PURE__ */
|
|
2237
|
-
), blocklyLoadError && !blocklyLoading && /* @__PURE__ */
|
|
2935
|
+
/* @__PURE__ */ React28__namespace.default.createElement("div", { className: "text-center" }, /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-muted-foreground animate-pulse mb-2" }, "Loading Blockly Environment..."), /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "w-8 h-8 border-4 border-muted border-t-primary rounded-full animate-spin mx-auto" }))
|
|
2936
|
+
), blocklyLoadError && !blocklyLoading && /* @__PURE__ */ React28__namespace.default.createElement(
|
|
2238
2937
|
"div",
|
|
2239
2938
|
{
|
|
2240
2939
|
style: {
|
|
@@ -2246,14 +2945,14 @@ var BlocklyProgrammingQuestionUI = React25__namespace.default.forwardRef(({
|
|
|
2246
2945
|
},
|
|
2247
2946
|
className: "flex items-center justify-center p-4"
|
|
2248
2947
|
},
|
|
2249
|
-
/* @__PURE__ */
|
|
2948
|
+
/* @__PURE__ */ React28__namespace.default.createElement("div", { className: "text-destructive text-center" }, /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "font-semibold text-lg" }, "Failed to load Blockly"), /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-sm mt-2 mb-3" }, blocklyLoadError), /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "space-x-2" }, /* @__PURE__ */ React28__namespace.default.createElement(
|
|
2250
2949
|
"button",
|
|
2251
2950
|
{
|
|
2252
2951
|
onClick: retry,
|
|
2253
2952
|
className: "px-4 py-2 bg-primary text-primary-foreground rounded hover:bg-primary/90 transition-colors text-sm"
|
|
2254
2953
|
},
|
|
2255
2954
|
"Try Again"
|
|
2256
|
-
), /* @__PURE__ */
|
|
2955
|
+
), /* @__PURE__ */ React28__namespace.default.createElement(
|
|
2257
2956
|
"button",
|
|
2258
2957
|
{
|
|
2259
2958
|
onClick: () => window.location.reload(),
|
|
@@ -2261,7 +2960,7 @@ var BlocklyProgrammingQuestionUI = React25__namespace.default.forwardRef(({
|
|
|
2261
2960
|
},
|
|
2262
2961
|
"Refresh Page"
|
|
2263
2962
|
)))
|
|
2264
|
-
), !blocklyLoading && !blocklyLoadError && /* @__PURE__ */
|
|
2963
|
+
), !blocklyLoading && !blocklyLoadError && /* @__PURE__ */ React28__namespace.default.createElement(
|
|
2265
2964
|
"div",
|
|
2266
2965
|
{
|
|
2267
2966
|
id: workspaceContainerId,
|
|
@@ -2278,7 +2977,7 @@ var BlocklyProgrammingQuestionUI = React25__namespace.default.forwardRef(({
|
|
|
2278
2977
|
},
|
|
2279
2978
|
"aria-label": `Blockly programming workspace for question: ${question.prompt}`
|
|
2280
2979
|
}
|
|
2281
|
-
), showCorrectAnswer && question.explanation && /* @__PURE__ */
|
|
2980
|
+
), showCorrectAnswer && question.explanation && /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "mt-4 p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Explanation:"), /* @__PURE__ */ React28__namespace.default.createElement(MarkdownRenderer, { content: question.explanation, className: "text-sm text-accent-foreground/80" }))));
|
|
2282
2981
|
});
|
|
2283
2982
|
BlocklyProgrammingQuestionUI.displayName = "BlocklyProgrammingQuestionUI";
|
|
2284
2983
|
var SCRATCH_JS_ENGINE_LAYOUT_PATH = "/static/scratch-blocks/js/blockly_compressed_vertical.js";
|
|
@@ -2348,17 +3047,17 @@ var loadScript2 = (src) => {
|
|
|
2348
3047
|
loadedScriptPromises.set(fullSrc, promise);
|
|
2349
3048
|
return promise;
|
|
2350
3049
|
};
|
|
2351
|
-
var ScratchProgrammingQuestionUI =
|
|
3050
|
+
var ScratchProgrammingQuestionUI = React28.forwardRef(({
|
|
2352
3051
|
question,
|
|
2353
3052
|
userAnswer,
|
|
2354
3053
|
showCorrectAnswer = false
|
|
2355
3054
|
}, ref) => {
|
|
2356
|
-
const blocklyDivRef =
|
|
2357
|
-
const workspaceRef =
|
|
2358
|
-
const [isBlocklyReady, setIsBlocklyReady] =
|
|
2359
|
-
const [componentError, setComponentError] =
|
|
2360
|
-
const [isLoadingScripts, setIsLoadingScripts] =
|
|
2361
|
-
const attemptLoadScripts =
|
|
3055
|
+
const blocklyDivRef = React28.useRef(null);
|
|
3056
|
+
const workspaceRef = React28.useRef(null);
|
|
3057
|
+
const [isBlocklyReady, setIsBlocklyReady] = React28.useState(false);
|
|
3058
|
+
const [componentError, setComponentError] = React28.useState(null);
|
|
3059
|
+
const [isLoadingScripts, setIsLoadingScripts] = React28.useState(true);
|
|
3060
|
+
const attemptLoadScripts = React28.useCallback(async () => {
|
|
2362
3061
|
var _a, _b, _c, _d;
|
|
2363
3062
|
setIsLoadingScripts(true);
|
|
2364
3063
|
setComponentError(null);
|
|
@@ -2410,10 +3109,10 @@ var ScratchProgrammingQuestionUI = React25.forwardRef(({
|
|
|
2410
3109
|
setIsLoadingScripts(false);
|
|
2411
3110
|
}
|
|
2412
3111
|
}, []);
|
|
2413
|
-
|
|
3112
|
+
React28.useEffect(() => {
|
|
2414
3113
|
attemptLoadScripts();
|
|
2415
3114
|
}, [attemptLoadScripts]);
|
|
2416
|
-
|
|
3115
|
+
React28.useImperativeHandle(ref, () => ({
|
|
2417
3116
|
getWorkspaceXml: () => {
|
|
2418
3117
|
const LocalBlockly = window.Blockly;
|
|
2419
3118
|
if (workspaceRef.current && (LocalBlockly == null ? void 0 : LocalBlockly.Xml)) {
|
|
@@ -2428,7 +3127,7 @@ var ScratchProgrammingQuestionUI = React25.forwardRef(({
|
|
|
2428
3127
|
return null;
|
|
2429
3128
|
}
|
|
2430
3129
|
}));
|
|
2431
|
-
const initializeWorkspace =
|
|
3130
|
+
const initializeWorkspace = React28.useCallback(() => {
|
|
2432
3131
|
var _a, _b;
|
|
2433
3132
|
const LocalBlockly = window.Blockly;
|
|
2434
3133
|
if (!isBlocklyReady || !blocklyDivRef.current || !LocalBlockly) {
|
|
@@ -2532,7 +3231,7 @@ var ScratchProgrammingQuestionUI = React25.forwardRef(({
|
|
|
2532
3231
|
setComponentError(`ScratchUI: Workspace initialization failed: ${e.message || String(e)}. Check console for details. Toolbox used: ${question.toolboxDefinition ? "Custom" : "Default Simplified"}.`);
|
|
2533
3232
|
}
|
|
2534
3233
|
}, [question, showCorrectAnswer, userAnswer, isBlocklyReady]);
|
|
2535
|
-
|
|
3234
|
+
React28.useEffect(() => {
|
|
2536
3235
|
if (isBlocklyReady) {
|
|
2537
3236
|
initializeWorkspace();
|
|
2538
3237
|
}
|
|
@@ -2559,15 +3258,15 @@ var ScratchProgrammingQuestionUI = React25.forwardRef(({
|
|
|
2559
3258
|
}, [isBlocklyReady, initializeWorkspace]);
|
|
2560
3259
|
const workspaceHeight = showCorrectAnswer ? "300px" : "450px";
|
|
2561
3260
|
if (isLoadingScripts) {
|
|
2562
|
-
return /* @__PURE__ */
|
|
3261
|
+
return /* @__PURE__ */ React28__namespace.default.createElement("div", { style: { height: workspaceHeight, width: "100%", display: "flex", alignItems: "center", justifyContent: "center", border: "1px solid hsl(var(--border))", borderRadius: "0.375rem", backgroundColor: "hsl(var(--background))" } }, /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "text-center" }, /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "w-8 h-8 border-4 border-muted border-t-primary rounded-full animate-spin mx-auto mb-2" }), /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-muted-foreground" }, "Loading Scratch Assets...")));
|
|
2563
3262
|
}
|
|
2564
3263
|
if (componentError) {
|
|
2565
|
-
return /* @__PURE__ */
|
|
3264
|
+
return /* @__PURE__ */ React28__namespace.default.createElement("div", { style: { height: workspaceHeight, width: "100%", color: "hsl(var(--destructive))", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", border: "1px solid hsl(var(--destructive))", borderRadius: "0.375rem", padding: "1rem", backgroundColor: "hsl(var(--card))" } }, /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "font-semibold text-lg mb-2" }, "Failed to load Scratch Workspace."), /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-sm text-muted-foreground mb-3 text-center" }, componentError), /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-xs text-muted-foreground mb-4 text-center" }, "Please ensure all Scratch/Blockly JavaScript files are correctly copied to your", /* @__PURE__ */ React28__namespace.default.createElement("code", null, "public/static/scratch-blocks/js"), " directory. Check browser console for more details.", /* @__PURE__ */ React28__namespace.default.createElement("br", null), /* @__PURE__ */ React28__namespace.default.createElement("strong", null, "CRITICAL: Ensure you have copied the CSS files from ", /* @__PURE__ */ React28__namespace.default.createElement("code", null, "node_modules/scratch-blocks/css/"), " (e.g., ", /* @__PURE__ */ React28__namespace.default.createElement("code", null, "vertical.css"), ") to ", /* @__PURE__ */ React28__namespace.default.createElement("code", null, "public/static/scratch-blocks/css/"), " and linked it in your main layout. Without CSS, blocks will not render correctly.")), /* @__PURE__ */ React28__namespace.default.createElement(Button, { onClick: attemptLoadScripts, variant: "outline" }, "Try Reloading Scripts"));
|
|
2566
3265
|
}
|
|
2567
3266
|
if (!isBlocklyReady && !isLoadingScripts) {
|
|
2568
|
-
return /* @__PURE__ */
|
|
3267
|
+
return /* @__PURE__ */ React28__namespace.default.createElement("div", { style: { height: workspaceHeight, width: "100%", display: "flex", alignItems: "center", justifyContent: "center", border: "1px solid hsl(var(--border))", borderRadius: "0.375rem", backgroundColor: "hsl(var(--background))" } }, /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-muted-foreground" }, "Scratch environment did not initialize (Blockly not ready). Check console for script loading errors."));
|
|
2569
3268
|
}
|
|
2570
|
-
return /* @__PURE__ */
|
|
3269
|
+
return /* @__PURE__ */ React28__namespace.default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React28__namespace.default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React28__namespace.default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, question.prompt), question.points && /* @__PURE__ */ React28__namespace.default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", question.points)), /* @__PURE__ */ React28__namespace.default.createElement(CardContent, { className: "p-0" }, /* @__PURE__ */ React28__namespace.default.createElement(
|
|
2571
3270
|
"div",
|
|
2572
3271
|
{
|
|
2573
3272
|
ref: blocklyDivRef,
|
|
@@ -2583,17 +3282,237 @@ var ScratchProgrammingQuestionUI = React25.forwardRef(({
|
|
|
2583
3282
|
},
|
|
2584
3283
|
"aria-label": `Scratch programming workspace for question: ${question.prompt}`
|
|
2585
3284
|
}
|
|
2586
|
-
), showCorrectAnswer && question.explanation && /* @__PURE__ */
|
|
3285
|
+
), showCorrectAnswer && question.explanation && /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "mt-4 p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Explanation:"), /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-sm text-accent-foreground/80" }, question.explanation))));
|
|
2587
3286
|
});
|
|
2588
3287
|
ScratchProgrammingQuestionUI.displayName = "ScratchProgrammingQuestionUI";
|
|
3288
|
+
var Tabs = TabsPrimitive__namespace.Root;
|
|
3289
|
+
var TabsList = React28__namespace.forwardRef((_a, ref) => {
|
|
3290
|
+
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
3291
|
+
return /* @__PURE__ */ React28__namespace.createElement(
|
|
3292
|
+
TabsPrimitive__namespace.List,
|
|
3293
|
+
__spreadValues({
|
|
3294
|
+
ref,
|
|
3295
|
+
className: cn(
|
|
3296
|
+
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
|
3297
|
+
className
|
|
3298
|
+
)
|
|
3299
|
+
}, props)
|
|
3300
|
+
);
|
|
3301
|
+
});
|
|
3302
|
+
TabsList.displayName = TabsPrimitive__namespace.List.displayName;
|
|
3303
|
+
var TabsTrigger = React28__namespace.forwardRef((_a, ref) => {
|
|
3304
|
+
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
3305
|
+
return /* @__PURE__ */ React28__namespace.createElement(
|
|
3306
|
+
TabsPrimitive__namespace.Trigger,
|
|
3307
|
+
__spreadValues({
|
|
3308
|
+
ref,
|
|
3309
|
+
className: cn(
|
|
3310
|
+
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
|
3311
|
+
className
|
|
3312
|
+
)
|
|
3313
|
+
}, props)
|
|
3314
|
+
);
|
|
3315
|
+
});
|
|
3316
|
+
TabsTrigger.displayName = TabsPrimitive__namespace.Trigger.displayName;
|
|
3317
|
+
var TabsContent = React28__namespace.forwardRef((_a, ref) => {
|
|
3318
|
+
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
3319
|
+
return /* @__PURE__ */ React28__namespace.createElement(
|
|
3320
|
+
TabsPrimitive__namespace.Content,
|
|
3321
|
+
__spreadValues({
|
|
3322
|
+
ref,
|
|
3323
|
+
className: cn(
|
|
3324
|
+
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
3325
|
+
className
|
|
3326
|
+
)
|
|
3327
|
+
}, props)
|
|
3328
|
+
);
|
|
3329
|
+
});
|
|
3330
|
+
TabsContent.displayName = TabsPrimitive__namespace.Content.displayName;
|
|
3331
|
+
var TOAST_LIMIT = 1;
|
|
3332
|
+
var TOAST_REMOVE_DELAY = 1e6;
|
|
3333
|
+
var count = 0;
|
|
3334
|
+
function genId() {
|
|
3335
|
+
count = (count + 1) % Number.MAX_SAFE_INTEGER;
|
|
3336
|
+
return count.toString();
|
|
3337
|
+
}
|
|
3338
|
+
var toastTimeouts = /* @__PURE__ */ new Map();
|
|
3339
|
+
var addToRemoveQueue = (toastId) => {
|
|
3340
|
+
if (toastTimeouts.has(toastId)) {
|
|
3341
|
+
return;
|
|
3342
|
+
}
|
|
3343
|
+
const timeout = setTimeout(() => {
|
|
3344
|
+
toastTimeouts.delete(toastId);
|
|
3345
|
+
dispatch({
|
|
3346
|
+
type: "REMOVE_TOAST",
|
|
3347
|
+
toastId
|
|
3348
|
+
});
|
|
3349
|
+
}, TOAST_REMOVE_DELAY);
|
|
3350
|
+
toastTimeouts.set(toastId, timeout);
|
|
3351
|
+
};
|
|
3352
|
+
var reducer = (state, action) => {
|
|
3353
|
+
switch (action.type) {
|
|
3354
|
+
case "ADD_TOAST":
|
|
3355
|
+
return __spreadProps(__spreadValues({}, state), {
|
|
3356
|
+
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT)
|
|
3357
|
+
});
|
|
3358
|
+
case "UPDATE_TOAST":
|
|
3359
|
+
return __spreadProps(__spreadValues({}, state), {
|
|
3360
|
+
toasts: state.toasts.map(
|
|
3361
|
+
(t) => t.id === action.toast.id ? __spreadValues(__spreadValues({}, t), action.toast) : t
|
|
3362
|
+
)
|
|
3363
|
+
});
|
|
3364
|
+
case "DISMISS_TOAST": {
|
|
3365
|
+
const { toastId } = action;
|
|
3366
|
+
if (toastId) {
|
|
3367
|
+
addToRemoveQueue(toastId);
|
|
3368
|
+
} else {
|
|
3369
|
+
state.toasts.forEach((toast2) => {
|
|
3370
|
+
addToRemoveQueue(toast2.id);
|
|
3371
|
+
});
|
|
3372
|
+
}
|
|
3373
|
+
return __spreadProps(__spreadValues({}, state), {
|
|
3374
|
+
toasts: state.toasts.map(
|
|
3375
|
+
(t) => t.id === toastId || toastId === void 0 ? __spreadProps(__spreadValues({}, t), {
|
|
3376
|
+
open: false
|
|
3377
|
+
}) : t
|
|
3378
|
+
)
|
|
3379
|
+
});
|
|
3380
|
+
}
|
|
3381
|
+
case "REMOVE_TOAST":
|
|
3382
|
+
if (action.toastId === void 0) {
|
|
3383
|
+
return __spreadProps(__spreadValues({}, state), {
|
|
3384
|
+
toasts: []
|
|
3385
|
+
});
|
|
3386
|
+
}
|
|
3387
|
+
return __spreadProps(__spreadValues({}, state), {
|
|
3388
|
+
toasts: state.toasts.filter((t) => t.id !== action.toastId)
|
|
3389
|
+
});
|
|
3390
|
+
}
|
|
3391
|
+
};
|
|
3392
|
+
var listeners = [];
|
|
3393
|
+
var memoryState = { toasts: [] };
|
|
3394
|
+
function dispatch(action) {
|
|
3395
|
+
memoryState = reducer(memoryState, action);
|
|
3396
|
+
listeners.forEach((listener) => {
|
|
3397
|
+
listener(memoryState);
|
|
3398
|
+
});
|
|
3399
|
+
}
|
|
3400
|
+
function toast(_a) {
|
|
3401
|
+
var props = __objRest(_a, []);
|
|
3402
|
+
const id = genId();
|
|
3403
|
+
const update = (props2) => dispatch({
|
|
3404
|
+
type: "UPDATE_TOAST",
|
|
3405
|
+
toast: __spreadProps(__spreadValues({}, props2), { id })
|
|
3406
|
+
});
|
|
3407
|
+
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
|
|
3408
|
+
dispatch({
|
|
3409
|
+
type: "ADD_TOAST",
|
|
3410
|
+
toast: __spreadProps(__spreadValues({}, props), {
|
|
3411
|
+
id,
|
|
3412
|
+
open: true,
|
|
3413
|
+
onOpenChange: (open) => {
|
|
3414
|
+
if (!open) dismiss();
|
|
3415
|
+
}
|
|
3416
|
+
})
|
|
3417
|
+
});
|
|
3418
|
+
return {
|
|
3419
|
+
id,
|
|
3420
|
+
dismiss,
|
|
3421
|
+
update
|
|
3422
|
+
};
|
|
3423
|
+
}
|
|
3424
|
+
function useToast() {
|
|
3425
|
+
const [state, setState] = React28__namespace.useState(memoryState);
|
|
3426
|
+
React28__namespace.useEffect(() => {
|
|
3427
|
+
listeners.push(setState);
|
|
3428
|
+
return () => {
|
|
3429
|
+
const index = listeners.indexOf(setState);
|
|
3430
|
+
if (index > -1) {
|
|
3431
|
+
listeners.splice(index, 1);
|
|
3432
|
+
}
|
|
3433
|
+
};
|
|
3434
|
+
}, [state]);
|
|
3435
|
+
return __spreadProps(__spreadValues({}, state), {
|
|
3436
|
+
toast,
|
|
3437
|
+
dismiss: (toastId) => dispatch({ type: "DISMISS_TOAST", toastId })
|
|
3438
|
+
});
|
|
3439
|
+
}
|
|
2589
3440
|
|
|
2590
|
-
// src/react-ui/components/ui/
|
|
2591
|
-
var
|
|
3441
|
+
// src/react-ui/components/ui/CodingQuestionUI.tsx
|
|
3442
|
+
var languageMap = {
|
|
3443
|
+
cpp: langCpp.cpp(),
|
|
3444
|
+
javascript: langJavascript.javascript({ typescript: true }),
|
|
3445
|
+
// Use TS extension for better JS support
|
|
3446
|
+
python: langPython.python(),
|
|
3447
|
+
swift: langJavascript.javascript({ typescript: true }),
|
|
3448
|
+
// Fallback for Swift syntax highlighting
|
|
3449
|
+
csharp: langCpp.cpp()
|
|
3450
|
+
// Fallback for C# syntax highlighting
|
|
3451
|
+
};
|
|
3452
|
+
var CodingQuestionUI = ({
|
|
3453
|
+
question,
|
|
3454
|
+
onAnswerChange,
|
|
3455
|
+
userAnswer,
|
|
3456
|
+
showCorrectAnswer = false
|
|
3457
|
+
}) => {
|
|
3458
|
+
const [code, setCode] = React28.useState("");
|
|
3459
|
+
const [isRunningTests, setIsRunningTests] = React28.useState(false);
|
|
3460
|
+
const [testResults, setTestResults] = React28.useState([]);
|
|
3461
|
+
const { toast: toast2 } = useToast();
|
|
3462
|
+
React28.useEffect(() => {
|
|
3463
|
+
const initialCode = typeof userAnswer === "string" ? userAnswer : question.functionSignature || "";
|
|
3464
|
+
setCode(initialCode);
|
|
3465
|
+
}, [question.id, userAnswer, question.functionSignature]);
|
|
3466
|
+
const handleCodeChange = React28.useCallback((value) => {
|
|
3467
|
+
setCode(value);
|
|
3468
|
+
onAnswerChange(value);
|
|
3469
|
+
}, [onAnswerChange]);
|
|
3470
|
+
const handleRunPublicTests = async () => {
|
|
3471
|
+
setIsRunningTests(true);
|
|
3472
|
+
setTestResults([]);
|
|
3473
|
+
try {
|
|
3474
|
+
const evaluationService = new CodeEvaluationService();
|
|
3475
|
+
const results = await evaluationService.evaluatePublicTestCases(question, code);
|
|
3476
|
+
setTestResults(results);
|
|
3477
|
+
} catch (error) {
|
|
3478
|
+
toast2({
|
|
3479
|
+
title: "Evaluation Error",
|
|
3480
|
+
description: error instanceof Error ? error.message : "An unknown error occurred.",
|
|
3481
|
+
variant: "destructive"
|
|
3482
|
+
});
|
|
3483
|
+
} finally {
|
|
3484
|
+
setIsRunningTests(false);
|
|
3485
|
+
}
|
|
3486
|
+
};
|
|
3487
|
+
const langExtension = languageMap[question.codingLanguage];
|
|
3488
|
+
return /* @__PURE__ */ React28__namespace.default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React28__namespace.default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React28__namespace.default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, /* @__PURE__ */ React28__namespace.default.createElement(MarkdownRenderer, { content: question.prompt })), question.points && /* @__PURE__ */ React28__namespace.default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", question.points)), /* @__PURE__ */ React28__namespace.default.createElement(CardContent, { className: "p-0 space-y-4" }, /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "font-mono text-sm border rounded-md overflow-hidden" }, /* @__PURE__ */ React28__namespace.default.createElement(
|
|
3489
|
+
CodeMirror__default.default,
|
|
3490
|
+
{
|
|
3491
|
+
value: code,
|
|
3492
|
+
height: "300px",
|
|
3493
|
+
extensions: [langExtension],
|
|
3494
|
+
onChange: handleCodeChange,
|
|
3495
|
+
readOnly: showCorrectAnswer,
|
|
3496
|
+
theme: "dark"
|
|
3497
|
+
}
|
|
3498
|
+
)), !showCorrectAnswer && /* @__PURE__ */ React28__namespace.default.createElement(Button, { onClick: handleRunPublicTests, disabled: isRunningTests }, isRunningTests ? /* @__PURE__ */ React28__namespace.default.createElement(lucideReact.Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : /* @__PURE__ */ React28__namespace.default.createElement(lucideReact.Play, { className: "mr-2 h-4 w-4" }), isRunningTests ? "Running..." : "Run Public Tests"), /* @__PURE__ */ React28__namespace.default.createElement(Tabs, { defaultValue: "tests", className: "w-full" }, /* @__PURE__ */ React28__namespace.default.createElement(TabsList, null, /* @__PURE__ */ React28__namespace.default.createElement(TabsTrigger, { value: "tests" }, "Test Cases"), showCorrectAnswer && /* @__PURE__ */ React28__namespace.default.createElement(TabsTrigger, { value: "solution" }, "Solution")), /* @__PURE__ */ React28__namespace.default.createElement(TabsContent, { value: "tests" }, /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "space-y-2 p-2 border rounded-md min-h-[100px]" }, testResults.length > 0 ? testResults.map((result, index) => /* @__PURE__ */ React28__namespace.default.createElement("div", { key: result.testCaseId, className: "flex items-center text-sm" }, result.passed ? /* @__PURE__ */ React28__namespace.default.createElement(lucideReact.CheckCircle, { className: "h-4 w-4 text-green-500 mr-2" }) : /* @__PURE__ */ React28__namespace.default.createElement(lucideReact.XCircle, { className: "h-4 w-4 text-destructive mr-2" }), /* @__PURE__ */ React28__namespace.default.createElement("span", null, "Test Case #", index + 1, ": ", result.passed ? "Passed" : "Failed"), !result.passed && /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-xs text-muted-foreground ml-2" }, "- ", result.reasoning))) : /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-sm text-muted-foreground" }, showCorrectAnswer ? "Final results are shown in the quiz summary." : "Click 'Run Public Tests' to see results."))), showCorrectAnswer && /* @__PURE__ */ React28__namespace.default.createElement(TabsContent, { value: "solution" }, /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "font-mono text-sm border rounded-md overflow-hidden" }, /* @__PURE__ */ React28__namespace.default.createElement(
|
|
3499
|
+
CodeMirror__default.default,
|
|
3500
|
+
{
|
|
3501
|
+
value: question.solutionCode,
|
|
3502
|
+
height: "300px",
|
|
3503
|
+
extensions: [langExtension],
|
|
3504
|
+
readOnly: true,
|
|
3505
|
+
theme: "dark"
|
|
3506
|
+
}
|
|
3507
|
+
)))), showCorrectAnswer && question.explanation && /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "mt-4 p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Explanation:"), /* @__PURE__ */ React28__namespace.default.createElement(MarkdownRenderer, { content: question.explanation, className: "text-sm text-accent-foreground/80" }))));
|
|
3508
|
+
};
|
|
3509
|
+
var QuestionRenderer = React28__namespace.default.forwardRef(({
|
|
2592
3510
|
question,
|
|
2593
3511
|
onAnswerChange,
|
|
2594
3512
|
userAnswer,
|
|
2595
3513
|
showCorrectAnswer = false
|
|
2596
3514
|
}, ref) => {
|
|
3515
|
+
const { t } = reactI18next.useTranslation();
|
|
2597
3516
|
const commonProps = {
|
|
2598
3517
|
question,
|
|
2599
3518
|
onAnswerChange,
|
|
@@ -2602,37 +3521,39 @@ var QuestionRenderer = React25__namespace.default.forwardRef(({
|
|
|
2602
3521
|
};
|
|
2603
3522
|
switch (question.questionType) {
|
|
2604
3523
|
case "multiple_choice":
|
|
2605
|
-
return /* @__PURE__ */
|
|
3524
|
+
return /* @__PURE__ */ React28__namespace.default.createElement(MultipleChoiceQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question }));
|
|
2606
3525
|
case "true_false":
|
|
2607
|
-
return /* @__PURE__ */
|
|
3526
|
+
return /* @__PURE__ */ React28__namespace.default.createElement(TrueFalseQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question }));
|
|
2608
3527
|
case "multiple_response":
|
|
2609
|
-
return /* @__PURE__ */
|
|
3528
|
+
return /* @__PURE__ */ React28__namespace.default.createElement(MultipleResponseQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question }));
|
|
2610
3529
|
case "short_answer":
|
|
2611
|
-
return /* @__PURE__ */
|
|
3530
|
+
return /* @__PURE__ */ React28__namespace.default.createElement(ShortAnswerQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question }));
|
|
2612
3531
|
case "numeric":
|
|
2613
|
-
return /* @__PURE__ */
|
|
3532
|
+
return /* @__PURE__ */ React28__namespace.default.createElement(NumericQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question }));
|
|
2614
3533
|
case "fill_in_the_blanks":
|
|
2615
|
-
return /* @__PURE__ */
|
|
3534
|
+
return /* @__PURE__ */ React28__namespace.default.createElement(FillInTheBlanksQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question }));
|
|
2616
3535
|
case "sequence":
|
|
2617
|
-
return /* @__PURE__ */
|
|
3536
|
+
return /* @__PURE__ */ React28__namespace.default.createElement(SequenceQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question }));
|
|
2618
3537
|
case "matching":
|
|
2619
|
-
return /* @__PURE__ */
|
|
3538
|
+
return /* @__PURE__ */ React28__namespace.default.createElement(MatchingQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question }));
|
|
2620
3539
|
case "drag_and_drop":
|
|
2621
|
-
return /* @__PURE__ */
|
|
3540
|
+
return /* @__PURE__ */ React28__namespace.default.createElement(DragAndDropQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question }));
|
|
2622
3541
|
case "hotspot":
|
|
2623
|
-
return /* @__PURE__ */
|
|
3542
|
+
return /* @__PURE__ */ React28__namespace.default.createElement(HotspotQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question }));
|
|
2624
3543
|
case "blockly_programming":
|
|
2625
|
-
return /* @__PURE__ */
|
|
3544
|
+
return /* @__PURE__ */ React28__namespace.default.createElement(BlocklyProgrammingQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question, ref }));
|
|
2626
3545
|
case "scratch_programming":
|
|
2627
|
-
return /* @__PURE__ */
|
|
3546
|
+
return /* @__PURE__ */ React28__namespace.default.createElement(ScratchProgrammingQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question, ref }));
|
|
3547
|
+
case "coding":
|
|
3548
|
+
return /* @__PURE__ */ React28__namespace.default.createElement(CodingQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question }));
|
|
2628
3549
|
default:
|
|
2629
|
-
return /* @__PURE__ */
|
|
3550
|
+
return /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "p-4 border border-destructive bg-destructive/10 rounded-md" }, /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "font-semibold text-destructive" }, t("unsupportedQuestionType")), /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-sm text-muted-foreground" }, t("unsupportedQuestionTypeDescription")), /* @__PURE__ */ React28__namespace.default.createElement("pre", { className: "mt-2 p-2 bg-muted rounded text-xs font-code overflow-x-auto" }, JSON.stringify(question, null, 2)));
|
|
2630
3551
|
}
|
|
2631
3552
|
});
|
|
2632
3553
|
QuestionRenderer.displayName = "QuestionRenderer";
|
|
2633
|
-
var Progress =
|
|
3554
|
+
var Progress = React28__namespace.forwardRef((_a, ref) => {
|
|
2634
3555
|
var _b = _a, { className, value } = _b, props = __objRest(_b, ["className", "value"]);
|
|
2635
|
-
return /* @__PURE__ */
|
|
3556
|
+
return /* @__PURE__ */ React28__namespace.createElement(
|
|
2636
3557
|
ProgressPrimitive__namespace.Root,
|
|
2637
3558
|
__spreadValues({
|
|
2638
3559
|
ref,
|
|
@@ -2641,7 +3562,7 @@ var Progress = React25__namespace.forwardRef((_a, ref) => {
|
|
|
2641
3562
|
className
|
|
2642
3563
|
)
|
|
2643
3564
|
}, props),
|
|
2644
|
-
/* @__PURE__ */
|
|
3565
|
+
/* @__PURE__ */ React28__namespace.createElement(
|
|
2645
3566
|
ProgressPrimitive__namespace.Indicator,
|
|
2646
3567
|
{
|
|
2647
3568
|
className: "h-full w-full flex-1 bg-primary transition-all",
|
|
@@ -2652,9 +3573,9 @@ var Progress = React25__namespace.forwardRef((_a, ref) => {
|
|
|
2652
3573
|
});
|
|
2653
3574
|
Progress.displayName = ProgressPrimitive__namespace.Root.displayName;
|
|
2654
3575
|
var Accordion = AccordionPrimitive__namespace.Root;
|
|
2655
|
-
var AccordionItem =
|
|
3576
|
+
var AccordionItem = React28__namespace.forwardRef((_a, ref) => {
|
|
2656
3577
|
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
2657
|
-
return /* @__PURE__ */
|
|
3578
|
+
return /* @__PURE__ */ React28__namespace.createElement(
|
|
2658
3579
|
AccordionPrimitive__namespace.Item,
|
|
2659
3580
|
__spreadValues({
|
|
2660
3581
|
ref,
|
|
@@ -2663,9 +3584,9 @@ var AccordionItem = React25__namespace.forwardRef((_a, ref) => {
|
|
|
2663
3584
|
);
|
|
2664
3585
|
});
|
|
2665
3586
|
AccordionItem.displayName = "AccordionItem";
|
|
2666
|
-
var AccordionTrigger =
|
|
3587
|
+
var AccordionTrigger = React28__namespace.forwardRef((_a, ref) => {
|
|
2667
3588
|
var _b = _a, { className, children } = _b, props = __objRest(_b, ["className", "children"]);
|
|
2668
|
-
return /* @__PURE__ */
|
|
3589
|
+
return /* @__PURE__ */ React28__namespace.createElement(AccordionPrimitive__namespace.Header, { className: "flex" }, /* @__PURE__ */ React28__namespace.createElement(
|
|
2669
3590
|
AccordionPrimitive__namespace.Trigger,
|
|
2670
3591
|
__spreadValues({
|
|
2671
3592
|
ref,
|
|
@@ -2675,39 +3596,39 @@ var AccordionTrigger = React25__namespace.forwardRef((_a, ref) => {
|
|
|
2675
3596
|
)
|
|
2676
3597
|
}, props),
|
|
2677
3598
|
children,
|
|
2678
|
-
/* @__PURE__ */
|
|
3599
|
+
/* @__PURE__ */ React28__namespace.createElement(lucideReact.ChevronDown, { className: "h-4 w-4 shrink-0 transition-transform duration-200" })
|
|
2679
3600
|
));
|
|
2680
3601
|
});
|
|
2681
3602
|
AccordionTrigger.displayName = AccordionPrimitive__namespace.Trigger.displayName;
|
|
2682
|
-
var AccordionContent =
|
|
3603
|
+
var AccordionContent = React28__namespace.forwardRef((_a, ref) => {
|
|
2683
3604
|
var _b = _a, { className, children } = _b, props = __objRest(_b, ["className", "children"]);
|
|
2684
|
-
return /* @__PURE__ */
|
|
3605
|
+
return /* @__PURE__ */ React28__namespace.createElement(
|
|
2685
3606
|
AccordionPrimitive__namespace.Content,
|
|
2686
3607
|
__spreadValues({
|
|
2687
3608
|
ref,
|
|
2688
3609
|
className: "overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
|
2689
3610
|
}, props),
|
|
2690
|
-
/* @__PURE__ */
|
|
3611
|
+
/* @__PURE__ */ React28__namespace.createElement("div", { className: cn("pb-4 pt-0", className) }, children)
|
|
2691
3612
|
);
|
|
2692
3613
|
});
|
|
2693
3614
|
AccordionContent.displayName = AccordionPrimitive__namespace.Content.displayName;
|
|
2694
|
-
var ScrollArea =
|
|
3615
|
+
var ScrollArea = React28__namespace.forwardRef((_a, ref) => {
|
|
2695
3616
|
var _b = _a, { className, children } = _b, props = __objRest(_b, ["className", "children"]);
|
|
2696
|
-
return /* @__PURE__ */
|
|
3617
|
+
return /* @__PURE__ */ React28__namespace.createElement(
|
|
2697
3618
|
ScrollAreaPrimitive__namespace.Root,
|
|
2698
3619
|
__spreadValues({
|
|
2699
3620
|
ref,
|
|
2700
3621
|
className: cn("relative overflow-hidden", className)
|
|
2701
3622
|
}, props),
|
|
2702
|
-
/* @__PURE__ */
|
|
2703
|
-
/* @__PURE__ */
|
|
2704
|
-
/* @__PURE__ */
|
|
3623
|
+
/* @__PURE__ */ React28__namespace.createElement(ScrollAreaPrimitive__namespace.Viewport, { className: "h-full w-full rounded-[inherit]" }, children),
|
|
3624
|
+
/* @__PURE__ */ React28__namespace.createElement(ScrollBar, null),
|
|
3625
|
+
/* @__PURE__ */ React28__namespace.createElement(ScrollAreaPrimitive__namespace.Corner, null)
|
|
2705
3626
|
);
|
|
2706
3627
|
});
|
|
2707
3628
|
ScrollArea.displayName = ScrollAreaPrimitive__namespace.Root.displayName;
|
|
2708
|
-
var ScrollBar =
|
|
3629
|
+
var ScrollBar = React28__namespace.forwardRef((_a, ref) => {
|
|
2709
3630
|
var _b = _a, { className, orientation = "vertical" } = _b, props = __objRest(_b, ["className", "orientation"]);
|
|
2710
|
-
return /* @__PURE__ */
|
|
3631
|
+
return /* @__PURE__ */ React28__namespace.createElement(
|
|
2711
3632
|
ScrollAreaPrimitive__namespace.ScrollAreaScrollbar,
|
|
2712
3633
|
__spreadValues({
|
|
2713
3634
|
ref,
|
|
@@ -2719,83 +3640,126 @@ var ScrollBar = React25__namespace.forwardRef((_a, ref) => {
|
|
|
2719
3640
|
className
|
|
2720
3641
|
)
|
|
2721
3642
|
}, props),
|
|
2722
|
-
/* @__PURE__ */
|
|
3643
|
+
/* @__PURE__ */ React28__namespace.createElement(ScrollAreaPrimitive__namespace.ScrollAreaThumb, { className: "relative flex-1 rounded-full bg-border" })
|
|
2723
3644
|
);
|
|
2724
3645
|
});
|
|
2725
3646
|
ScrollBar.displayName = ScrollAreaPrimitive__namespace.ScrollAreaScrollbar.displayName;
|
|
2726
3647
|
|
|
2727
3648
|
// src/react-ui/components/ui/QuizResult.tsx
|
|
2728
|
-
var QuizResult = ({
|
|
3649
|
+
var QuizResult = ({
|
|
3650
|
+
result,
|
|
3651
|
+
quizTitle,
|
|
3652
|
+
onExitQuiz,
|
|
3653
|
+
onGenerateReview,
|
|
3654
|
+
showReviewButton = false,
|
|
3655
|
+
isReviewLoading = false
|
|
3656
|
+
}) => {
|
|
2729
3657
|
var _a, _b, _c, _d;
|
|
3658
|
+
const { t } = reactI18next.useTranslation();
|
|
2730
3659
|
const getAnswerDisplay = (answer) => {
|
|
2731
|
-
if (answer === null || answer === void 0) return "
|
|
3660
|
+
if (answer === null || answer === void 0) return t("practiceFlow.results.notAnswered");
|
|
2732
3661
|
if (typeof answer === "boolean") return answer ? "True" : "False";
|
|
2733
3662
|
if (Array.isArray(answer)) return answer.join(", ");
|
|
2734
|
-
if (typeof answer === "object")
|
|
3663
|
+
if (typeof answer === "object") {
|
|
3664
|
+
if (answer.hasOwnProperty("value")) {
|
|
3665
|
+
const value = answer.value;
|
|
3666
|
+
if (value === null || value === void 0) return t("practiceFlow.results.notAnswered");
|
|
3667
|
+
if (typeof value === "boolean") return value ? "True" : "False";
|
|
3668
|
+
if (Array.isArray(value)) return value.join(", ");
|
|
3669
|
+
if (typeof value === "object") return JSON.stringify(value, null, 2);
|
|
3670
|
+
return String(value);
|
|
3671
|
+
}
|
|
3672
|
+
return JSON.stringify(answer, null, 2);
|
|
3673
|
+
}
|
|
2735
3674
|
return String(answer);
|
|
2736
3675
|
};
|
|
2737
|
-
return /* @__PURE__ */
|
|
3676
|
+
return /* @__PURE__ */ React28__namespace.default.createElement(Card, { className: "w-full max-w-3xl mx-auto shadow-xl" }, /* @__PURE__ */ React28__namespace.default.createElement(CardHeader, null, /* @__PURE__ */ React28__namespace.default.createElement(CardTitle, { className: "text-3xl font-headline text-center" }, t("practiceFlow.results.title", { quizTitle })), /* @__PURE__ */ React28__namespace.default.createElement(CardDescription, { className: "text-center text-lg" }, t("practiceFlow.results.description"))), /* @__PURE__ */ React28__namespace.default.createElement(CardContent, { className: "space-y-6" }, /* @__PURE__ */ React28__namespace.default.createElement(Card, { className: "bg-secondary/50" }, /* @__PURE__ */ React28__namespace.default.createElement(CardHeader, null, /* @__PURE__ */ React28__namespace.default.createElement(CardTitle, { className: "text-xl flex items-center" }, /* @__PURE__ */ React28__namespace.default.createElement(lucideReact.BarChart2, { className: "mr-2 h-5 w-5 text-primary" }), t("practiceFlow.results.overallScore"))), /* @__PURE__ */ React28__namespace.default.createElement(CardContent, { className: "grid grid-cols-1 md:grid-cols-3 gap-4 text-center" }, /* @__PURE__ */ React28__namespace.default.createElement("div", null, /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-3xl font-bold text-primary" }, result.score, " / ", result.maxScore), /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-sm text-muted-foreground" }, t("practiceFlow.results.points"))), /* @__PURE__ */ React28__namespace.default.createElement("div", null, /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-3xl font-bold text-primary" }, result.percentage.toFixed(2), "%"), /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-sm text-muted-foreground" }, t("practiceFlow.results.percentage"))), /* @__PURE__ */ React28__namespace.default.createElement("div", null, result.passed !== void 0 && (result.passed ? /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "flex flex-col items-center text-green-600" }, /* @__PURE__ */ React28__namespace.default.createElement(lucideReact.CheckCircle, { className: "h-10 w-10" }), /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-xl font-semibold mt-1" }, t("practiceFlow.results.passed"))) : /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "flex flex-col items-center text-destructive" }, /* @__PURE__ */ React28__namespace.default.createElement(lucideReact.XCircle, { className: "h-10 w-10" }), /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-xl font-semibold mt-1" }, t("practiceFlow.results.failed"))))))), /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4 text-sm" }, /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "flex items-center space-x-2 p-3 bg-muted rounded-md" }, /* @__PURE__ */ React28__namespace.default.createElement(lucideReact.Clock, { className: "h-5 w-5 text-primary" }), /* @__PURE__ */ React28__namespace.default.createElement("span", null, t("practiceFlow.results.timeSpent")), /* @__PURE__ */ React28__namespace.default.createElement("span", { className: "font-semibold" }, (_b = (_a = result.totalTimeSpentSeconds) == null ? void 0 : _a.toFixed(0)) != null ? _b : "N/A", " ", t("practiceFlow.results.timeUnit"))), /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "flex items-center space-x-2 p-3 bg-muted rounded-md" }, /* @__PURE__ */ React28__namespace.default.createElement(lucideReact.Percent, { className: "h-5 w-5 text-primary" }), /* @__PURE__ */ React28__namespace.default.createElement("span", null, t("practiceFlow.results.avgTimePerQuestion")), /* @__PURE__ */ React28__namespace.default.createElement("span", { className: "font-semibold" }, (_d = (_c = result.averageTimePerQuestionSeconds) == null ? void 0 : _c.toFixed(1)) != null ? _d : "N/A", " ", t("practiceFlow.results.timeUnit")))), result.scormStatus && result.scormStatus !== "idle" && result.scormStatus !== "no_api" && /* @__PURE__ */ React28__namespace.default.createElement(Card, null, /* @__PURE__ */ React28__namespace.default.createElement(CardHeader, null, /* @__PURE__ */ React28__namespace.default.createElement(CardTitle, { className: "text-lg" }, "SCORM Sync Status")), /* @__PURE__ */ React28__namespace.default.createElement(CardContent, null, /* @__PURE__ */ React28__namespace.default.createElement("p", { className: `flex items-center ${result.scormStatus === "error" ? "text-destructive" : "text-muted-foreground"}` }, result.scormStatus === "error" && /* @__PURE__ */ React28__namespace.default.createElement(lucideReact.AlertTriangle, { className: "mr-2 h-4 w-4" }), "Status: ", /* @__PURE__ */ React28__namespace.default.createElement("span", { className: "font-semibold ml-1" }, result.scormStatus)), result.scormError && /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-xs text-destructive mt-1" }, "Details: ", result.scormError))), result.webhookStatus && result.webhookStatus !== "idle" && /* @__PURE__ */ React28__namespace.default.createElement(Card, null, /* @__PURE__ */ React28__namespace.default.createElement(CardHeader, null, /* @__PURE__ */ React28__namespace.default.createElement(CardTitle, { className: "text-lg" }, "Webhook Sync Status")), /* @__PURE__ */ React28__namespace.default.createElement(CardContent, null, /* @__PURE__ */ React28__namespace.default.createElement("p", { className: `flex items-center ${result.webhookStatus === "error" ? "text-destructive" : "text-muted-foreground"}` }, result.webhookStatus === "error" && /* @__PURE__ */ React28__namespace.default.createElement(lucideReact.AlertTriangle, { className: "mr-2 h-4 w-4" }), "Status: ", /* @__PURE__ */ React28__namespace.default.createElement("span", { className: "font-semibold ml-1" }, result.webhookStatus)), result.webhookError && /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-xs text-destructive mt-1" }, "Details: ", result.webhookError))), /* @__PURE__ */ React28__namespace.default.createElement(Accordion, { type: "single", collapsible: true, className: "w-full" }, /* @__PURE__ */ React28__namespace.default.createElement(AccordionItem, { value: "question-breakdown" }, /* @__PURE__ */ React28__namespace.default.createElement(AccordionTrigger, { className: "text-lg font-semibold" }, t("practiceFlow.results.questionBreakdown")), /* @__PURE__ */ React28__namespace.default.createElement(AccordionContent, null, /* @__PURE__ */ React28__namespace.default.createElement(ScrollArea, { className: "h-[300px] pr-4" }, /* @__PURE__ */ React28__namespace.default.createElement("ul", { className: "space-y-4" }, result.questionResults.map((qResult, index) => {
|
|
2738
3677
|
var _a2, _b2;
|
|
2739
|
-
return /* @__PURE__ */
|
|
2740
|
-
}))))))), /* @__PURE__ */
|
|
3678
|
+
return /* @__PURE__ */ React28__namespace.default.createElement("li", { key: qResult.questionId, className: "p-4 border rounded-md bg-background" }, /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "flex justify-between items-center mb-2" }, /* @__PURE__ */ React28__namespace.default.createElement("h4", { className: "font-semibold" }, t("common.questions", { count: index + 1 })), qResult.isCorrect ? /* @__PURE__ */ React28__namespace.default.createElement("span", { className: "text-green-600 font-medium flex items-center" }, /* @__PURE__ */ React28__namespace.default.createElement(lucideReact.CheckCircle, { className: "mr-1 h-4 w-4" }), " ", t("practiceFlow.results.passed")) : /* @__PURE__ */ React28__namespace.default.createElement("span", { className: "text-destructive font-medium flex items-center" }, /* @__PURE__ */ React28__namespace.default.createElement(lucideReact.XCircle, { className: "mr-1 h-4 w-4" }), " ", t("practiceFlow.results.failed"))), /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-sm" }, /* @__PURE__ */ React28__namespace.default.createElement("span", { className: "font-medium" }, t("practiceFlow.results.yourAnswer")), " ", getAnswerDisplay(qResult.userAnswer)), !qResult.isCorrect && /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-sm" }, /* @__PURE__ */ React28__namespace.default.createElement("span", { className: "font-medium" }, t("practiceFlow.results.correctAnswer")), " ", getAnswerDisplay(qResult.correctAnswer)), /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-xs text-muted-foreground" }, /* @__PURE__ */ React28__namespace.default.createElement("span", { className: "font-medium" }, t("practiceFlow.results.pointsEarned")), " ", qResult.pointsEarned), /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-xs text-muted-foreground" }, /* @__PURE__ */ React28__namespace.default.createElement("span", { className: "font-medium" }, t("practiceFlow.results.timeSpent")), " ", (_b2 = (_a2 = qResult.timeSpentSeconds) == null ? void 0 : _a2.toFixed(0)) != null ? _b2 : "N/A", t("practiceFlow.results.timeUnit")));
|
|
3679
|
+
}))))))), /* @__PURE__ */ React28__namespace.default.createElement(CardFooter, { className: "flex flex-col sm:flex-row justify-between gap-2" }, onExitQuiz && /* @__PURE__ */ React28__namespace.default.createElement(Button, { variant: "outline", onClick: onExitQuiz, className: "w-full sm:w-auto" }, /* @__PURE__ */ React28__namespace.default.createElement(lucideReact.LogOut, { className: "mr-2 h-4 w-4" }), t("common.exit")), showReviewButton && onGenerateReview && /* @__PURE__ */ React28__namespace.default.createElement(
|
|
3680
|
+
Button,
|
|
3681
|
+
{
|
|
3682
|
+
onClick: onGenerateReview,
|
|
3683
|
+
disabled: isReviewLoading,
|
|
3684
|
+
className: "w-full sm:w-auto"
|
|
3685
|
+
},
|
|
3686
|
+
isReviewLoading ? /* @__PURE__ */ React28__namespace.default.createElement(lucideReact.Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : /* @__PURE__ */ React28__namespace.default.createElement(lucideReact.Wand2, { className: "mr-2 h-4 w-4" }),
|
|
3687
|
+
isReviewLoading ? t("practiceFlow.results.generatingReview") : t("practiceFlow.results.generateReview")
|
|
3688
|
+
)));
|
|
2741
3689
|
};
|
|
2742
3690
|
|
|
2743
3691
|
// src/react-ui/components/ui/QuizPlayer.tsx
|
|
2744
3692
|
var QuizPlayer = ({ quizConfig, onQuizComplete, onExitQuiz }) => {
|
|
2745
3693
|
var _a;
|
|
2746
|
-
const [engine, setEngine] =
|
|
2747
|
-
const [currentQuestion, setCurrentQuestion] =
|
|
2748
|
-
const [currentQuestionNumber, setCurrentQuestionNumber] =
|
|
2749
|
-
const [totalQuestions, setTotalQuestions] =
|
|
2750
|
-
const [userAnswer, setUserAnswer] =
|
|
2751
|
-
const [quizFinished, setQuizFinished] =
|
|
2752
|
-
const [finalResult, setFinalResult] =
|
|
2753
|
-
const [timeLeft, setTimeLeft] =
|
|
2754
|
-
const [isLoading, setIsLoading] =
|
|
2755
|
-
const [error, setError] =
|
|
2756
|
-
const
|
|
2757
|
-
const
|
|
3694
|
+
const [engine, setEngine] = React28.useState(null);
|
|
3695
|
+
const [currentQuestion, setCurrentQuestion] = React28.useState(null);
|
|
3696
|
+
const [currentQuestionNumber, setCurrentQuestionNumber] = React28.useState(0);
|
|
3697
|
+
const [totalQuestions, setTotalQuestions] = React28.useState(0);
|
|
3698
|
+
const [userAnswer, setUserAnswer] = React28.useState(null);
|
|
3699
|
+
const [quizFinished, setQuizFinished] = React28.useState(false);
|
|
3700
|
+
const [finalResult, setFinalResult] = React28.useState(null);
|
|
3701
|
+
const [timeLeft, setTimeLeft] = React28.useState(null);
|
|
3702
|
+
const [isLoading, setIsLoading] = React28.useState(true);
|
|
3703
|
+
const [error, setError] = React28.useState(null);
|
|
3704
|
+
const { t } = reactI18next.useTranslation();
|
|
3705
|
+
const programmingQuestionRef = React28.useRef(null);
|
|
3706
|
+
const engineRef = React28.useRef(null);
|
|
3707
|
+
const isInitializedRef = React28.useRef(false);
|
|
3708
|
+
const callbacks = React28.useMemo(() => ({
|
|
3709
|
+
onQuizStart: (initialData) => {
|
|
3710
|
+
setCurrentQuestion(initialData.initialQuestion);
|
|
3711
|
+
setCurrentQuestionNumber(initialData.currentQuestionNumber);
|
|
3712
|
+
setTotalQuestions(initialData.totalQuestions);
|
|
3713
|
+
setTimeLeft(initialData.timeLimitInSeconds);
|
|
3714
|
+
setIsLoading(false);
|
|
3715
|
+
},
|
|
3716
|
+
onQuestionChange: (question, qNum, total) => {
|
|
3717
|
+
var _a2;
|
|
3718
|
+
setCurrentQuestion(question);
|
|
3719
|
+
setCurrentQuestionNumber(qNum);
|
|
3720
|
+
setTotalQuestions(total);
|
|
3721
|
+
const existingAnswer = (_a2 = engineRef.current) == null ? void 0 : _a2.getUserAnswer((question == null ? void 0 : question.id) || "");
|
|
3722
|
+
setUserAnswer(existingAnswer !== void 0 ? existingAnswer : null);
|
|
3723
|
+
},
|
|
3724
|
+
onQuizFinish: (results) => {
|
|
3725
|
+
setFinalResult(results);
|
|
3726
|
+
setQuizFinished(true);
|
|
3727
|
+
onQuizComplete(results);
|
|
3728
|
+
setIsLoading(false);
|
|
3729
|
+
},
|
|
3730
|
+
onTimeTick: (timeLeftInSeconds) => {
|
|
3731
|
+
setTimeLeft(timeLeftInSeconds);
|
|
3732
|
+
},
|
|
3733
|
+
onQuizTimeUp: () => {
|
|
3734
|
+
setError("Time's up! Your quiz has been submitted automatically.");
|
|
3735
|
+
}
|
|
3736
|
+
}), [onQuizComplete]);
|
|
3737
|
+
const handleAnswerChange = React28.useCallback((answer) => {
|
|
2758
3738
|
if ((currentQuestion == null ? void 0 : currentQuestion.questionType) !== "blockly_programming" && (currentQuestion == null ? void 0 : currentQuestion.questionType) !== "scratch_programming") {
|
|
2759
3739
|
setUserAnswer(answer);
|
|
2760
3740
|
}
|
|
2761
|
-
}, [currentQuestion]);
|
|
2762
|
-
|
|
3741
|
+
}, [currentQuestion == null ? void 0 : currentQuestion.questionType]);
|
|
3742
|
+
const quizConfigKey = React28.useMemo(() => {
|
|
3743
|
+
return JSON.stringify({
|
|
3744
|
+
id: quizConfig.id,
|
|
3745
|
+
version: quizConfig.version,
|
|
3746
|
+
title: quizConfig.title
|
|
3747
|
+
});
|
|
3748
|
+
}, [quizConfig.id, quizConfig.version, quizConfig.title]);
|
|
3749
|
+
React28.useEffect(() => {
|
|
3750
|
+
if (isInitializedRef.current) {
|
|
3751
|
+
return;
|
|
3752
|
+
}
|
|
2763
3753
|
setIsLoading(true);
|
|
2764
3754
|
setError(null);
|
|
2765
3755
|
setQuizFinished(false);
|
|
2766
3756
|
setFinalResult(null);
|
|
2767
3757
|
setUserAnswer(null);
|
|
3758
|
+
isInitializedRef.current = true;
|
|
2768
3759
|
let localQuizEngine = null;
|
|
2769
3760
|
try {
|
|
2770
|
-
const callbacks = {
|
|
2771
|
-
onQuizStart: (initialData) => {
|
|
2772
|
-
setCurrentQuestion(initialData.initialQuestion);
|
|
2773
|
-
setCurrentQuestionNumber(initialData.currentQuestionNumber);
|
|
2774
|
-
setTotalQuestions(initialData.totalQuestions);
|
|
2775
|
-
setTimeLeft(initialData.timeLimitInSeconds);
|
|
2776
|
-
setIsLoading(false);
|
|
2777
|
-
},
|
|
2778
|
-
onQuestionChange: (question, qNum, total) => {
|
|
2779
|
-
setCurrentQuestion(question);
|
|
2780
|
-
setCurrentQuestionNumber(qNum);
|
|
2781
|
-
setTotalQuestions(total);
|
|
2782
|
-
const existingAnswer = engine == null ? void 0 : engine.getUserAnswer((question == null ? void 0 : question.id) || "");
|
|
2783
|
-
setUserAnswer(existingAnswer !== void 0 ? existingAnswer : null);
|
|
2784
|
-
},
|
|
2785
|
-
onQuizFinish: (results) => {
|
|
2786
|
-
setFinalResult(results);
|
|
2787
|
-
setQuizFinished(true);
|
|
2788
|
-
onQuizComplete(results);
|
|
2789
|
-
setIsLoading(false);
|
|
2790
|
-
},
|
|
2791
|
-
onTimeTick: (timeLeftInSeconds) => {
|
|
2792
|
-
setTimeLeft(timeLeftInSeconds);
|
|
2793
|
-
},
|
|
2794
|
-
onQuizTimeUp: () => {
|
|
2795
|
-
setError("Time's up! Your quiz has been submitted automatically.");
|
|
2796
|
-
}
|
|
2797
|
-
};
|
|
2798
3761
|
localQuizEngine = new QuizEngine({ config: quizConfig, callbacks });
|
|
3762
|
+
engineRef.current = localQuizEngine;
|
|
2799
3763
|
setEngine(localQuizEngine);
|
|
2800
3764
|
const initialQ = localQuizEngine.getCurrentQuestion();
|
|
2801
3765
|
setCurrentQuestion(initialQ);
|
|
@@ -2807,84 +3771,88 @@ var QuizPlayer = ({ quizConfig, onQuizComplete, onExitQuiz }) => {
|
|
|
2807
3771
|
setUserAnswer(existingAnswer !== void 0 ? existingAnswer : null);
|
|
2808
3772
|
}
|
|
2809
3773
|
setIsLoading(false);
|
|
2810
|
-
return () => {
|
|
2811
|
-
if (localQuizEngine) {
|
|
2812
|
-
localQuizEngine.destroy();
|
|
2813
|
-
}
|
|
2814
|
-
};
|
|
2815
3774
|
} catch (e) {
|
|
2816
|
-
console.error("Error initializing QuizEngine:", e);
|
|
2817
3775
|
setError(e instanceof Error ? e.message : "Failed to load quiz.");
|
|
2818
3776
|
setIsLoading(false);
|
|
2819
|
-
|
|
2820
|
-
localQuizEngine.destroy();
|
|
2821
|
-
}
|
|
3777
|
+
isInitializedRef.current = false;
|
|
2822
3778
|
}
|
|
2823
|
-
|
|
2824
|
-
|
|
3779
|
+
return () => {
|
|
3780
|
+
if (localQuizEngine) localQuizEngine.destroy();
|
|
3781
|
+
if (engineRef.current) engineRef.current = null;
|
|
3782
|
+
isInitializedRef.current = false;
|
|
3783
|
+
};
|
|
3784
|
+
}, [quizConfigKey, callbacks, quizConfig]);
|
|
3785
|
+
const handleSubmitAnswer = React28.useCallback(() => {
|
|
2825
3786
|
var _a2;
|
|
2826
|
-
|
|
3787
|
+
const currentEngine = engineRef.current;
|
|
3788
|
+
if (!currentEngine || !currentQuestion) return;
|
|
2827
3789
|
let answerToSubmit = null;
|
|
2828
3790
|
if (currentQuestion.questionType === "blockly_programming" || currentQuestion.questionType === "scratch_programming") {
|
|
2829
3791
|
if (programmingQuestionRef.current && typeof programmingQuestionRef.current.getWorkspaceXml === "function") {
|
|
2830
3792
|
answerToSubmit = programmingQuestionRef.current.getWorkspaceXml();
|
|
2831
3793
|
} else {
|
|
2832
|
-
|
|
2833
|
-
answerToSubmit = (_a2 = engine.getUserAnswer(currentQuestion.id)) != null ? _a2 : null;
|
|
3794
|
+
answerToSubmit = (_a2 = currentEngine.getUserAnswer(currentQuestion.id)) != null ? _a2 : null;
|
|
2834
3795
|
}
|
|
2835
3796
|
} else {
|
|
2836
3797
|
answerToSubmit = userAnswer;
|
|
2837
3798
|
}
|
|
2838
3799
|
if (answerToSubmit !== void 0) {
|
|
2839
|
-
|
|
2840
|
-
}
|
|
2841
|
-
};
|
|
2842
|
-
const handleNext = () => {
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
}
|
|
2851
|
-
};
|
|
2852
|
-
const handlePrevious = () => {
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
};
|
|
2858
|
-
const handleFinishQuiz = async () => {
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
};
|
|
2865
|
-
const progressPercent =
|
|
3800
|
+
currentEngine.submitAnswer(currentQuestion.id, answerToSubmit);
|
|
3801
|
+
}
|
|
3802
|
+
}, [currentQuestion, userAnswer]);
|
|
3803
|
+
const handleNext = React28.useCallback(() => {
|
|
3804
|
+
const currentEngine = engineRef.current;
|
|
3805
|
+
if (!currentEngine) return;
|
|
3806
|
+
handleSubmitAnswer();
|
|
3807
|
+
if (currentEngine.getCurrentQuestionNumber() < currentEngine.getTotalQuestions()) {
|
|
3808
|
+
currentEngine.nextQuestion();
|
|
3809
|
+
} else {
|
|
3810
|
+
handleFinishQuiz();
|
|
3811
|
+
}
|
|
3812
|
+
}, [handleSubmitAnswer]);
|
|
3813
|
+
const handlePrevious = React28.useCallback(() => {
|
|
3814
|
+
const currentEngine = engineRef.current;
|
|
3815
|
+
if (!currentEngine || currentEngine.getCurrentQuestionNumber() <= 1) return;
|
|
3816
|
+
handleSubmitAnswer();
|
|
3817
|
+
currentEngine.previousQuestion();
|
|
3818
|
+
}, [handleSubmitAnswer]);
|
|
3819
|
+
const handleFinishQuiz = React28.useCallback(async () => {
|
|
3820
|
+
const currentEngine = engineRef.current;
|
|
3821
|
+
if (!currentEngine) return;
|
|
3822
|
+
setIsLoading(true);
|
|
3823
|
+
handleSubmitAnswer();
|
|
3824
|
+
await currentEngine.calculateResults();
|
|
3825
|
+
}, [handleSubmitAnswer]);
|
|
3826
|
+
const progressPercent = React28.useMemo(() => {
|
|
2866
3827
|
if (totalQuestions === 0) return 0;
|
|
2867
3828
|
return quizFinished ? 100 : Math.max(0, Math.min(100, (currentQuestionNumber - 1) / totalQuestions * 100));
|
|
2868
3829
|
}, [currentQuestionNumber, totalQuestions, quizFinished]);
|
|
3830
|
+
const formatTime = React28.useCallback((seconds) => {
|
|
3831
|
+
if (seconds === null) return "-:--";
|
|
3832
|
+
const mins = Math.floor(seconds / 60);
|
|
3833
|
+
const secs = seconds % 60;
|
|
3834
|
+
return `${mins}:${secs < 10 ? "0" : ""}${secs}`;
|
|
3835
|
+
}, []);
|
|
2869
3836
|
if (isLoading) {
|
|
2870
|
-
return /* @__PURE__ */
|
|
3837
|
+
return /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "flex flex-col items-center justify-center h-64" }, /* @__PURE__ */ React28__namespace.default.createElement(lucideReact.Loader2, { className: "h-12 w-12 animate-spin text-primary" }), /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "mt-4 text-muted-foreground" }, t("common.loading")));
|
|
2871
3838
|
}
|
|
2872
3839
|
if (error) {
|
|
2873
|
-
return /* @__PURE__ */
|
|
3840
|
+
return /* @__PURE__ */ React28__namespace.default.createElement(Card, { className: "w-full max-w-2xl mx-auto shadow-xl" }, /* @__PURE__ */ React28__namespace.default.createElement(CardHeader, null, /* @__PURE__ */ React28__namespace.default.createElement(CardTitle, { className: "text-destructive flex items-center" }, /* @__PURE__ */ React28__namespace.default.createElement(lucideReact.AlertCircle, { className: "mr-2 h-6 w-6" }), "Quiz Error")), /* @__PURE__ */ React28__namespace.default.createElement(CardContent, null, /* @__PURE__ */ React28__namespace.default.createElement("p", null, error)), /* @__PURE__ */ React28__namespace.default.createElement(CardFooter, null, onExitQuiz && /* @__PURE__ */ React28__namespace.default.createElement(Button, { variant: "outline", onClick: onExitQuiz }, /* @__PURE__ */ React28__namespace.default.createElement(lucideReact.LogOut, { className: "mr-2 h-4 w-4" }), " ", t("common.exit"))));
|
|
2874
3841
|
}
|
|
2875
3842
|
if (quizFinished && finalResult) {
|
|
2876
|
-
return /* @__PURE__ */
|
|
3843
|
+
return /* @__PURE__ */ React28__namespace.default.createElement(QuizResult, { result: finalResult, onExitQuiz, quizTitle: quizConfig.title });
|
|
2877
3844
|
}
|
|
2878
3845
|
if (!currentQuestion) {
|
|
2879
|
-
return /* @__PURE__ */
|
|
3846
|
+
return /* @__PURE__ */ React28__namespace.default.createElement(Card, { className: "w-full max-w-2xl mx-auto shadow-xl" }, /* @__PURE__ */ React28__namespace.default.createElement(CardHeader, null, /* @__PURE__ */ React28__namespace.default.createElement(CardTitle, null, "Quiz Ended"), /* @__PURE__ */ React28__namespace.default.createElement(CardDescription, null, "No more questions, or quiz not loaded correctly.")), /* @__PURE__ */ React28__namespace.default.createElement(CardFooter, null, onExitQuiz && /* @__PURE__ */ React28__namespace.default.createElement(Button, { variant: "outline", onClick: onExitQuiz }, /* @__PURE__ */ React28__namespace.default.createElement(lucideReact.LogOut, { className: "mr-2 h-4 w-4" }), " ", t("common.exit"))));
|
|
2880
3847
|
}
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
3848
|
+
return /* @__PURE__ */ React28__namespace.default.createElement(Card, { className: "w-full max-w-3xl mx-auto shadow-xl" }, /* @__PURE__ */ React28__namespace.default.createElement(CardHeader, null, /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "flex justify-between items-center" }, /* @__PURE__ */ React28__namespace.default.createElement(CardTitle, { className: "text-2xl font-headline" }, quizConfig.title), timeLeft !== null && /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "flex items-center text-sm text-muted-foreground" }, /* @__PURE__ */ React28__namespace.default.createElement(lucideReact.Clock, { className: "mr-1 h-4 w-4" }), t("practiceFlow.player.timeLeft", { time: formatTime(timeLeft) }))), quizConfig.description && /* @__PURE__ */ React28__namespace.default.createElement(CardDescription, null, quizConfig.description), /* @__PURE__ */ React28__namespace.default.createElement("div", { className: "mt-2" }, /* @__PURE__ */ React28__namespace.default.createElement(
|
|
3849
|
+
Progress,
|
|
3850
|
+
{
|
|
3851
|
+
value: progressPercent,
|
|
3852
|
+
"aria-label": `Quiz progress: ${currentQuestionNumber} of ${totalQuestions} questions`,
|
|
3853
|
+
className: "w-full"
|
|
3854
|
+
}
|
|
3855
|
+
), /* @__PURE__ */ React28__namespace.default.createElement("p", { className: "text-sm text-muted-foreground mt-1 text-right" }, t("practiceFlow.player.questionProgress", { current: currentQuestionNumber, total: totalQuestions })))), /* @__PURE__ */ React28__namespace.default.createElement(CardContent, { className: "min-h-[200px]" }, /* @__PURE__ */ React28__namespace.default.createElement(
|
|
2888
3856
|
QuestionRenderer,
|
|
2889
3857
|
{
|
|
2890
3858
|
question: currentQuestion,
|
|
@@ -2894,7 +3862,25 @@ var QuizPlayer = ({ quizConfig, onQuizComplete, onExitQuiz }) => {
|
|
|
2894
3862
|
key: currentQuestion.id,
|
|
2895
3863
|
ref: currentQuestion.questionType === "blockly_programming" || currentQuestion.questionType === "scratch_programming" ? programmingQuestionRef : null
|
|
2896
3864
|
}
|
|
2897
|
-
)), /* @__PURE__ */
|
|
3865
|
+
)), /* @__PURE__ */ React28__namespace.default.createElement(CardFooter, { className: "flex justify-between items-center" }, /* @__PURE__ */ React28__namespace.default.createElement(
|
|
3866
|
+
Button,
|
|
3867
|
+
{
|
|
3868
|
+
variant: "outline",
|
|
3869
|
+
onClick: handlePrevious,
|
|
3870
|
+
disabled: currentQuestionNumber <= 1
|
|
3871
|
+
},
|
|
3872
|
+
/* @__PURE__ */ React28__namespace.default.createElement(lucideReact.ChevronLeft, { className: "mr-2 h-4 w-4" }),
|
|
3873
|
+
" ",
|
|
3874
|
+
t("common.previous")
|
|
3875
|
+
), onExitQuiz && /* @__PURE__ */ React28__namespace.default.createElement(
|
|
3876
|
+
Button,
|
|
3877
|
+
{
|
|
3878
|
+
variant: "ghost",
|
|
3879
|
+
onClick: onExitQuiz,
|
|
3880
|
+
className: "text-muted-foreground hover:text-destructive"
|
|
3881
|
+
},
|
|
3882
|
+
t("common.exit")
|
|
3883
|
+
), /* @__PURE__ */ React28__namespace.default.createElement(Button, { onClick: handleNext }, currentQuestionNumber === totalQuestions ? t("practiceFlow.player.finishQuiz") : t("common.next"), currentQuestionNumber !== totalQuestions && /* @__PURE__ */ React28__namespace.default.createElement(lucideReact.ChevronRight, { className: "ml-2 h-4 w-4" }), currentQuestionNumber === totalQuestions && /* @__PURE__ */ React28__namespace.default.createElement(lucideReact.CheckCircle, { className: "ml-2 h-4 w-4" }))));
|
|
2898
3884
|
};
|
|
2899
3885
|
|
|
2900
3886
|
// src/player.ts
|
|
@@ -2906,7 +3892,7 @@ function mountQuizPlayer(targetElementId, quizConfig) {
|
|
|
2906
3892
|
return;
|
|
2907
3893
|
}
|
|
2908
3894
|
const AppContainer = () => {
|
|
2909
|
-
const [quizResult, setQuizResult] =
|
|
3895
|
+
const [quizResult, setQuizResult] = React28.useState(null);
|
|
2910
3896
|
const handleQuizComplete = (result) => {
|
|
2911
3897
|
console.log("Quiz Complete (captured inside React AppContainer):", result);
|
|
2912
3898
|
setQuizResult(result);
|
|
@@ -2923,20 +3909,20 @@ function mountQuizPlayer(targetElementId, quizConfig) {
|
|
|
2923
3909
|
}
|
|
2924
3910
|
};
|
|
2925
3911
|
if (quizResult) {
|
|
2926
|
-
return
|
|
3912
|
+
return React28__namespace.default.createElement(QuizResult, {
|
|
2927
3913
|
result: quizResult,
|
|
2928
3914
|
quizTitle: quizConfig.title,
|
|
2929
3915
|
onExitQuiz: handleExit
|
|
2930
3916
|
});
|
|
2931
3917
|
}
|
|
2932
|
-
return
|
|
3918
|
+
return React28__namespace.default.createElement(QuizPlayer, {
|
|
2933
3919
|
quizConfig,
|
|
2934
3920
|
onQuizComplete: handleQuizComplete,
|
|
2935
3921
|
onExitQuiz: handleExit
|
|
2936
3922
|
});
|
|
2937
3923
|
};
|
|
2938
3924
|
const root = ReactDOM__default.default.createRoot(targetElement);
|
|
2939
|
-
root.render(
|
|
3925
|
+
root.render(React28__namespace.default.createElement(React28__namespace.default.StrictMode, null, React28__namespace.default.createElement(AppContainer)));
|
|
2940
3926
|
}
|
|
2941
3927
|
|
|
2942
3928
|
exports.mountQuizPlayer = mountQuizPlayer;
|