@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.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import * as
|
|
2
|
-
import
|
|
1
|
+
import * as React28 from 'react';
|
|
2
|
+
import React28__default, { useRef, useState, useImperativeHandle, useCallback, useEffect, forwardRef, useMemo } from 'react';
|
|
3
3
|
import ReactDOM from 'react-dom/client';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { genkit } from 'genkit';
|
|
7
|
+
import { gemini20Flash, googleAI } from '@genkit-ai/googleai';
|
|
4
8
|
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
|
|
5
|
-
import { Circle, Check, ChevronDown, ChevronUp, CheckCircle, XCircle, RotateCcw, BarChart2, Clock, Percent, AlertTriangle, LogOut,
|
|
9
|
+
import { Circle, Check, ChevronDown, ChevronUp, Loader2, Play, CheckCircle, XCircle, RotateCcw, BarChart2, Clock, Percent, AlertTriangle, LogOut, Wand2, AlertCircle, ChevronLeft, ChevronRight } from 'lucide-react';
|
|
6
10
|
import { clsx } from 'clsx';
|
|
7
11
|
import { twMerge } from 'tailwind-merge';
|
|
8
12
|
import * as LabelPrimitive from '@radix-ui/react-label';
|
|
@@ -15,6 +19,11 @@ import rehypeKatex from 'rehype-katex';
|
|
|
15
19
|
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
|
16
20
|
import { Slot } from '@radix-ui/react-slot';
|
|
17
21
|
import * as SelectPrimitive from '@radix-ui/react-select';
|
|
22
|
+
import CodeMirror from '@uiw/react-codemirror';
|
|
23
|
+
import { cpp } from '@codemirror/lang-cpp';
|
|
24
|
+
import { javascript } from '@codemirror/lang-javascript';
|
|
25
|
+
import { python } from '@codemirror/lang-python';
|
|
26
|
+
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
|
18
27
|
import * as ProgressPrimitive from '@radix-ui/react-progress';
|
|
19
28
|
import * as AccordionPrimitive from '@radix-ui/react-accordion';
|
|
20
29
|
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
|
|
@@ -298,6 +307,847 @@ var SCORMService = class {
|
|
|
298
307
|
}
|
|
299
308
|
};
|
|
300
309
|
|
|
310
|
+
// src/services/evaluators/multiple-choice-evaluator.ts
|
|
311
|
+
var MultipleChoiceEvaluator = class {
|
|
312
|
+
async evaluate(question, answer) {
|
|
313
|
+
var _a;
|
|
314
|
+
const points = (_a = question.points) != null ? _a : 0;
|
|
315
|
+
const correctAnswerId = question.correctAnswerId;
|
|
316
|
+
const isCorrect = answer === correctAnswerId;
|
|
317
|
+
const correctOption = question.options.find((opt) => opt.id === correctAnswerId);
|
|
318
|
+
const correctAnswerDetail = {
|
|
319
|
+
id: correctAnswerId,
|
|
320
|
+
value: (correctOption == null ? void 0 : correctOption.text) || ""
|
|
321
|
+
};
|
|
322
|
+
return Promise.resolve({
|
|
323
|
+
isCorrect,
|
|
324
|
+
correctAnswer: correctAnswerDetail,
|
|
325
|
+
pointsEarned: isCorrect ? points : 0
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
// src/services/evaluators/multiple-response-evaluator.ts
|
|
331
|
+
var MultipleResponseEvaluator = class {
|
|
332
|
+
async evaluate(question, answer) {
|
|
333
|
+
var _a;
|
|
334
|
+
const points = (_a = question.points) != null ? _a : 0;
|
|
335
|
+
const correctAnswerIds = question.correctAnswerIds;
|
|
336
|
+
let isCorrect = false;
|
|
337
|
+
if (Array.isArray(answer)) {
|
|
338
|
+
const userAnswerSet = new Set(answer);
|
|
339
|
+
const correctAnswerSet = new Set(correctAnswerIds);
|
|
340
|
+
isCorrect = userAnswerSet.size === correctAnswerSet.size && [...userAnswerSet].every((id) => correctAnswerSet.has(id));
|
|
341
|
+
}
|
|
342
|
+
const correctValues = correctAnswerIds.map(
|
|
343
|
+
(id) => {
|
|
344
|
+
var _a2;
|
|
345
|
+
return ((_a2 = question.options.find((opt) => opt.id === id)) == null ? void 0 : _a2.text) || "";
|
|
346
|
+
}
|
|
347
|
+
);
|
|
348
|
+
const correctAnswerDetail = {
|
|
349
|
+
id: correctAnswerIds,
|
|
350
|
+
value: correctValues
|
|
351
|
+
};
|
|
352
|
+
return Promise.resolve({
|
|
353
|
+
isCorrect,
|
|
354
|
+
correctAnswer: correctAnswerDetail,
|
|
355
|
+
pointsEarned: isCorrect ? points : 0
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
// src/services/evaluators/true-false-evaluator.ts
|
|
361
|
+
var TrueFalseEvaluator = class {
|
|
362
|
+
async evaluate(question, answer) {
|
|
363
|
+
var _a;
|
|
364
|
+
const points = (_a = question.points) != null ? _a : 0;
|
|
365
|
+
const correctAnswer = question.correctAnswer;
|
|
366
|
+
let userAnswer = answer;
|
|
367
|
+
if (typeof answer === "string") {
|
|
368
|
+
userAnswer = answer.toLowerCase() === "true";
|
|
369
|
+
}
|
|
370
|
+
const isCorrect = typeof userAnswer === "boolean" && userAnswer === correctAnswer;
|
|
371
|
+
const correctAnswerDetail = {
|
|
372
|
+
id: null,
|
|
373
|
+
value: correctAnswer
|
|
374
|
+
};
|
|
375
|
+
return Promise.resolve({
|
|
376
|
+
isCorrect,
|
|
377
|
+
correctAnswer: correctAnswerDetail,
|
|
378
|
+
pointsEarned: isCorrect ? points : 0
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
// src/services/evaluators/short-answer-evaluator.ts
|
|
384
|
+
var ShortAnswerEvaluator = class {
|
|
385
|
+
async evaluate(question, answer) {
|
|
386
|
+
var _a, _b;
|
|
387
|
+
const points = (_a = question.points) != null ? _a : 0;
|
|
388
|
+
let isCorrect = false;
|
|
389
|
+
if (typeof answer === "string") {
|
|
390
|
+
const userAnswerTrimmed = answer.trim();
|
|
391
|
+
const caseSensitive = (_b = question.isCaseSensitive) != null ? _b : false;
|
|
392
|
+
isCorrect = question.acceptedAnswers.some(
|
|
393
|
+
(accAns) => caseSensitive ? accAns.trim() === userAnswerTrimmed : accAns.trim().toLowerCase() === userAnswerTrimmed.toLowerCase()
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
const correctAnswerDetail = {
|
|
397
|
+
id: null,
|
|
398
|
+
value: question.acceptedAnswers
|
|
399
|
+
};
|
|
400
|
+
return Promise.resolve({
|
|
401
|
+
isCorrect,
|
|
402
|
+
correctAnswer: correctAnswerDetail,
|
|
403
|
+
pointsEarned: isCorrect ? points : 0
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
// src/services/evaluators/numeric-evaluator.ts
|
|
409
|
+
var NumericEvaluator = class {
|
|
410
|
+
async evaluate(question, answer) {
|
|
411
|
+
var _a;
|
|
412
|
+
const points = (_a = question.points) != null ? _a : 0;
|
|
413
|
+
let isCorrect = false;
|
|
414
|
+
if (typeof answer === "string" || typeof answer === "number") {
|
|
415
|
+
const userAnswerNum = parseFloat(String(answer));
|
|
416
|
+
if (!isNaN(userAnswerNum)) {
|
|
417
|
+
isCorrect = question.tolerance != null ? Math.abs(userAnswerNum - question.answer) <= question.tolerance : userAnswerNum === question.answer;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const correctAnswerDetail = {
|
|
421
|
+
id: null,
|
|
422
|
+
value: question.answer
|
|
423
|
+
};
|
|
424
|
+
return Promise.resolve({
|
|
425
|
+
isCorrect,
|
|
426
|
+
correctAnswer: correctAnswerDetail,
|
|
427
|
+
pointsEarned: isCorrect ? points : 0
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
// src/services/evaluators/sequence-evaluator.ts
|
|
433
|
+
var SequenceEvaluator = class {
|
|
434
|
+
async evaluate(question, answer) {
|
|
435
|
+
var _a;
|
|
436
|
+
const points = (_a = question.points) != null ? _a : 0;
|
|
437
|
+
let isCorrect = false;
|
|
438
|
+
if (Array.isArray(answer) && answer.length === question.correctOrder.length) {
|
|
439
|
+
isCorrect = answer.every((itemId, index) => itemId === question.correctOrder[index]);
|
|
440
|
+
}
|
|
441
|
+
const correctValues = question.correctOrder.map(
|
|
442
|
+
(id) => {
|
|
443
|
+
var _a2;
|
|
444
|
+
return ((_a2 = question.items.find((item) => item.id === id)) == null ? void 0 : _a2.content) || "";
|
|
445
|
+
}
|
|
446
|
+
);
|
|
447
|
+
const correctAnswerDetail = {
|
|
448
|
+
id: question.correctOrder,
|
|
449
|
+
value: correctValues
|
|
450
|
+
};
|
|
451
|
+
return Promise.resolve({
|
|
452
|
+
isCorrect,
|
|
453
|
+
correctAnswer: correctAnswerDetail,
|
|
454
|
+
pointsEarned: isCorrect ? points : 0
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
// src/services/evaluators/matching-evaluator.ts
|
|
460
|
+
var MatchingEvaluator = class {
|
|
461
|
+
async evaluate(question, answer) {
|
|
462
|
+
var _a;
|
|
463
|
+
const points = (_a = question.points) != null ? _a : 0;
|
|
464
|
+
let isCorrect = false;
|
|
465
|
+
if (typeof answer === "object" && answer !== null && !Array.isArray(answer)) {
|
|
466
|
+
const userAnswerMap = answer;
|
|
467
|
+
isCorrect = question.correctAnswerMap.length === Object.keys(userAnswerMap).length && question.correctAnswerMap.every((map) => userAnswerMap[map.promptId] === map.optionId);
|
|
468
|
+
}
|
|
469
|
+
const correctMap = question.correctAnswerMap.reduce((acc, curr) => {
|
|
470
|
+
var _a2, _b;
|
|
471
|
+
const promptText = ((_a2 = question.prompts.find((p) => p.id === curr.promptId)) == null ? void 0 : _a2.content) || "";
|
|
472
|
+
const optionText = ((_b = question.options.find((o) => o.id === curr.optionId)) == null ? void 0 : _b.content) || "";
|
|
473
|
+
acc[promptText] = optionText;
|
|
474
|
+
return acc;
|
|
475
|
+
}, {});
|
|
476
|
+
const correctAnswerDetail = {
|
|
477
|
+
id: null,
|
|
478
|
+
value: correctMap
|
|
479
|
+
};
|
|
480
|
+
return Promise.resolve({
|
|
481
|
+
isCorrect,
|
|
482
|
+
correctAnswer: correctAnswerDetail,
|
|
483
|
+
pointsEarned: isCorrect ? points : 0
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
// src/services/evaluators/fill-in-the-blanks-evaluator.ts
|
|
489
|
+
var FillInTheBlanksEvaluator = class {
|
|
490
|
+
async evaluate(question, answer) {
|
|
491
|
+
var _a;
|
|
492
|
+
const points = (_a = question.points) != null ? _a : 0;
|
|
493
|
+
let isCorrect = false;
|
|
494
|
+
if (typeof answer === "object" && answer !== null && !Array.isArray(answer)) {
|
|
495
|
+
const userAnswerMap = answer;
|
|
496
|
+
isCorrect = question.answers.length > 0 && question.answers.every((correctAnsDef) => {
|
|
497
|
+
var _a2, _b;
|
|
498
|
+
const userValForBlank = (_a2 = userAnswerMap[correctAnsDef.blankId]) == null ? void 0 : _a2.trim();
|
|
499
|
+
if (userValForBlank === void 0) return false;
|
|
500
|
+
const caseSensitive = (_b = question.isCaseSensitive) != null ? _b : false;
|
|
501
|
+
return correctAnsDef.acceptedValues.some(
|
|
502
|
+
(accVal) => caseSensitive ? accVal.trim() === userValForBlank : accVal.trim().toLowerCase() === userValForBlank.toLowerCase()
|
|
503
|
+
);
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
const correctMap = question.answers.reduce((acc, curr) => {
|
|
507
|
+
acc[curr.blankId] = curr.acceptedValues.join(" | ");
|
|
508
|
+
return acc;
|
|
509
|
+
}, {});
|
|
510
|
+
const correctAnswerDetail = {
|
|
511
|
+
id: null,
|
|
512
|
+
value: correctMap
|
|
513
|
+
};
|
|
514
|
+
return Promise.resolve({
|
|
515
|
+
isCorrect,
|
|
516
|
+
correctAnswer: correctAnswerDetail,
|
|
517
|
+
pointsEarned: isCorrect ? points : 0
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
// src/services/evaluators/drag-and-drop-evaluator.ts
|
|
523
|
+
var DragAndDropEvaluator = class {
|
|
524
|
+
async evaluate(question, answer) {
|
|
525
|
+
var _a;
|
|
526
|
+
const points = (_a = question.points) != null ? _a : 0;
|
|
527
|
+
let isCorrect = false;
|
|
528
|
+
if (typeof answer === "object" && answer !== null && !Array.isArray(answer)) {
|
|
529
|
+
const userAnswerMap = answer;
|
|
530
|
+
isCorrect = question.answerMap.length === Object.keys(userAnswerMap).length && question.answerMap.every((map) => userAnswerMap[map.draggableId] === map.dropZoneId);
|
|
531
|
+
}
|
|
532
|
+
const correctMap = question.answerMap.reduce((acc, curr) => {
|
|
533
|
+
var _a2, _b;
|
|
534
|
+
const draggableText = ((_a2 = question.draggableItems.find((d) => d.id === curr.draggableId)) == null ? void 0 : _a2.content) || "";
|
|
535
|
+
const dropZoneText = ((_b = question.dropZones.find((z3) => z3.id === curr.dropZoneId)) == null ? void 0 : _b.label) || "";
|
|
536
|
+
acc[draggableText] = dropZoneText;
|
|
537
|
+
return acc;
|
|
538
|
+
}, {});
|
|
539
|
+
const correctAnswerDetail = {
|
|
540
|
+
id: null,
|
|
541
|
+
value: correctMap
|
|
542
|
+
};
|
|
543
|
+
return Promise.resolve({
|
|
544
|
+
isCorrect,
|
|
545
|
+
correctAnswer: correctAnswerDetail,
|
|
546
|
+
pointsEarned: isCorrect ? points : 0
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
// src/services/evaluators/hotspot-evaluator.ts
|
|
552
|
+
var HotspotEvaluator = class {
|
|
553
|
+
async evaluate(question, answer) {
|
|
554
|
+
var _a;
|
|
555
|
+
const points = (_a = question.points) != null ? _a : 0;
|
|
556
|
+
let isCorrect = false;
|
|
557
|
+
if (Array.isArray(answer)) {
|
|
558
|
+
const userAnswerSet = new Set(answer);
|
|
559
|
+
const correctAnswerSet = new Set(question.correctHotspotIds);
|
|
560
|
+
isCorrect = userAnswerSet.size === correctAnswerSet.size && [...userAnswerSet].every((id) => correctAnswerSet.has(id));
|
|
561
|
+
}
|
|
562
|
+
const correctValues = question.correctHotspotIds.map(
|
|
563
|
+
(id) => {
|
|
564
|
+
var _a2;
|
|
565
|
+
return ((_a2 = question.hotspots.find((h) => h.id === id)) == null ? void 0 : _a2.description) || id;
|
|
566
|
+
}
|
|
567
|
+
);
|
|
568
|
+
const correctAnswerDetail = {
|
|
569
|
+
id: question.correctHotspotIds,
|
|
570
|
+
value: correctValues
|
|
571
|
+
};
|
|
572
|
+
return Promise.resolve({
|
|
573
|
+
isCorrect,
|
|
574
|
+
correctAnswer: correctAnswerDetail,
|
|
575
|
+
pointsEarned: isCorrect ? points : 0
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
// src/services/evaluators/programming-evaluator.ts
|
|
581
|
+
var ProgrammingEvaluator = class {
|
|
582
|
+
async evaluate(question, answer) {
|
|
583
|
+
var _a, _b;
|
|
584
|
+
const points = (_a = question.points) != null ? _a : 0;
|
|
585
|
+
let isCorrect = false;
|
|
586
|
+
if (typeof answer === "string" && typeof question.solutionGeneratedCode === "string") {
|
|
587
|
+
if (typeof window !== "undefined" && ((_b = window.Blockly) == null ? void 0 : _b.JavaScript)) {
|
|
588
|
+
const LocalBlockly = window.Blockly;
|
|
589
|
+
let generatedUserCode = "";
|
|
590
|
+
try {
|
|
591
|
+
const tempWorkspace = new LocalBlockly.Workspace();
|
|
592
|
+
const dom = LocalBlockly.Xml.textToDom(answer);
|
|
593
|
+
LocalBlockly.Xml.domToWorkspace(dom, tempWorkspace);
|
|
594
|
+
generatedUserCode = LocalBlockly.JavaScript.workspaceToCode(tempWorkspace) || "";
|
|
595
|
+
const normalize = (code) => code.replace(/\s+/g, " ").trim();
|
|
596
|
+
isCorrect = normalize(generatedUserCode) === normalize(question.solutionGeneratedCode);
|
|
597
|
+
tempWorkspace.dispose();
|
|
598
|
+
} catch (e) {
|
|
599
|
+
console.error(`Error generating code from user's ${question.questionType} XML for evaluation:`, e);
|
|
600
|
+
isCorrect = false;
|
|
601
|
+
}
|
|
602
|
+
} else {
|
|
603
|
+
console.warn(`Blockly library not available for ${question.questionType} evaluation. Skipping code comparison.`);
|
|
604
|
+
isCorrect = false;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
const correctAnswerDetail = {
|
|
608
|
+
id: null,
|
|
609
|
+
value: question.solutionGeneratedCode || ""
|
|
610
|
+
};
|
|
611
|
+
return Promise.resolve({
|
|
612
|
+
isCorrect,
|
|
613
|
+
correctAnswer: correctAnswerDetail,
|
|
614
|
+
pointsEarned: isCorrect ? points : 0
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
// src/utils/jsonUtils.ts
|
|
620
|
+
var JsonRepairEngine = class {
|
|
621
|
+
/**
|
|
622
|
+
* Attempts to repair unterminated strings in JSON.
|
|
623
|
+
* NOTE: This is a heuristic approach and may not be perfect for all cases.
|
|
624
|
+
*/
|
|
625
|
+
static repairUnterminatedStrings(jsonStr) {
|
|
626
|
+
let repaired = jsonStr;
|
|
627
|
+
let inString = false;
|
|
628
|
+
let escaped = false;
|
|
629
|
+
let lastQuoteIndex = -1;
|
|
630
|
+
for (let i = 0; i < repaired.length; i++) {
|
|
631
|
+
const char = repaired[i];
|
|
632
|
+
if (escaped) {
|
|
633
|
+
escaped = false;
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
if (char === "\\") {
|
|
637
|
+
escaped = true;
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
if (char === '"') {
|
|
641
|
+
inString = !inString;
|
|
642
|
+
if (inString) {
|
|
643
|
+
lastQuoteIndex = i;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (inString && lastQuoteIndex !== -1) {
|
|
648
|
+
const beforeUnterminated = repaired.substring(0, lastQuoteIndex + 1);
|
|
649
|
+
const afterUnterminated = repaired.substring(lastQuoteIndex + 1);
|
|
650
|
+
const breakPoints = [",", "}", "]", "\n"];
|
|
651
|
+
let breakIndex = -1;
|
|
652
|
+
for (let i = 0; i < afterUnterminated.length; i++) {
|
|
653
|
+
if (breakPoints.includes(afterUnterminated[i])) {
|
|
654
|
+
breakIndex = i;
|
|
655
|
+
break;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (breakIndex !== -1) {
|
|
659
|
+
const stringContent = afterUnterminated.substring(0, breakIndex);
|
|
660
|
+
const remainder = afterUnterminated.substring(breakIndex);
|
|
661
|
+
const escapedContent = stringContent.replace(new RegExp('(?<!\\\\)"', "g"), '\\"');
|
|
662
|
+
repaired = beforeUnterminated + escapedContent + '"' + remainder;
|
|
663
|
+
} else {
|
|
664
|
+
const escapedContent = afterUnterminated.replace(new RegExp('(?<!\\\\)"', "g"), '\\"');
|
|
665
|
+
repaired = beforeUnterminated + escapedContent + '"';
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return repaired;
|
|
669
|
+
}
|
|
670
|
+
// FIX: Replaced unsafe single quote replacement with a stateful parser.
|
|
671
|
+
/**
|
|
672
|
+
* Safely replaces single quotes with double quotes only for keys and string values,
|
|
673
|
+
* ignoring apostrophes inside already double-quoted strings.
|
|
674
|
+
*/
|
|
675
|
+
static safelyFixQuotes(jsonStr) {
|
|
676
|
+
let result = "";
|
|
677
|
+
let inDoubleQuoteString = false;
|
|
678
|
+
let escaped = false;
|
|
679
|
+
for (let i = 0; i < jsonStr.length; i++) {
|
|
680
|
+
const char = jsonStr[i];
|
|
681
|
+
if (escaped) {
|
|
682
|
+
result += char;
|
|
683
|
+
escaped = false;
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
if (char === "\\") {
|
|
687
|
+
escaped = true;
|
|
688
|
+
result += char;
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
if (char === '"') {
|
|
692
|
+
inDoubleQuoteString = !inDoubleQuoteString;
|
|
693
|
+
}
|
|
694
|
+
if (char === "'" && !inDoubleQuoteString) {
|
|
695
|
+
result += '"';
|
|
696
|
+
} else {
|
|
697
|
+
result += char;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return result;
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Fixes common JSON formatting issues using more robust methods.
|
|
704
|
+
*/
|
|
705
|
+
static applyCommonFixes(jsonStr) {
|
|
706
|
+
let fixed = jsonStr;
|
|
707
|
+
fixed = this.safelyFixQuotes(fixed);
|
|
708
|
+
fixed = fixed.replace(/,\s*([}\]])/g, "$1");
|
|
709
|
+
fixed = fixed.replace(/("|}|\d|]|true|false|null)\s*\n\s*(")/g, "$1,\n$2");
|
|
710
|
+
fixed = fixed.replace(/"[\s\S]*?"/g, (match) => {
|
|
711
|
+
const content = match.substring(1, match.length - 1);
|
|
712
|
+
const fixedContent = content.replace(/\n/g, "\\n").replace(/\r/g, "\\r");
|
|
713
|
+
return `"${fixedContent}"`;
|
|
714
|
+
});
|
|
715
|
+
fixed = fixed.replace(/"(true|false|null)"/g, "$1");
|
|
716
|
+
return fixed;
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Validates JSON by attempting to parse and providing detailed error info.
|
|
720
|
+
*/
|
|
721
|
+
static validateAndGetError(jsonStr) {
|
|
722
|
+
try {
|
|
723
|
+
JSON.parse(jsonStr);
|
|
724
|
+
return { isValid: true };
|
|
725
|
+
} catch (error) {
|
|
726
|
+
const errorMessage = error.message || "";
|
|
727
|
+
const positionMatch = errorMessage.match(/position (\d+)/);
|
|
728
|
+
const position = positionMatch ? parseInt(positionMatch[1], 10) : void 0;
|
|
729
|
+
return {
|
|
730
|
+
isValid: false,
|
|
731
|
+
error: errorMessage,
|
|
732
|
+
position
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Main repair function that attempts multiple strategies.
|
|
738
|
+
*/
|
|
739
|
+
static repairJson(jsonStr) {
|
|
740
|
+
var _a;
|
|
741
|
+
let current = jsonStr.trim();
|
|
742
|
+
const maxAttempts = 5;
|
|
743
|
+
let lastError = "";
|
|
744
|
+
let lastPosition = -1;
|
|
745
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
746
|
+
const validation = this.validateAndGetError(current);
|
|
747
|
+
if (validation.isValid) {
|
|
748
|
+
return current;
|
|
749
|
+
}
|
|
750
|
+
console.warn(`JSON repair attempt ${attempt + 1}: ${validation.error}`);
|
|
751
|
+
if (validation.error === lastError && validation.position === lastPosition) {
|
|
752
|
+
console.error("Repair attempt stuck on the same error, aborting this strategy.");
|
|
753
|
+
if (validation.position) {
|
|
754
|
+
const truncated = current.substring(0, validation.position);
|
|
755
|
+
const openBraces = (truncated.match(/{/g) || []).length;
|
|
756
|
+
const closeBraces = (truncated.match(/}/g) || []).length;
|
|
757
|
+
const openBrackets = (truncated.match(/\[/g) || []).length;
|
|
758
|
+
const closeBrackets = (truncated.match(/\]/g) || []).length;
|
|
759
|
+
let repaired = truncated.replace(/,\s*$/, "");
|
|
760
|
+
for (let i = 0; i < openBrackets - closeBrackets; i++) repaired += "]";
|
|
761
|
+
for (let i = 0; i < openBraces - closeBraces; i++) repaired += "}";
|
|
762
|
+
current = repaired;
|
|
763
|
+
const finalValidation = this.validateAndGetError(current);
|
|
764
|
+
if (finalValidation.isValid) return current;
|
|
765
|
+
}
|
|
766
|
+
break;
|
|
767
|
+
}
|
|
768
|
+
lastError = validation.error || "";
|
|
769
|
+
lastPosition = validation.position;
|
|
770
|
+
if ((_a = validation.error) == null ? void 0 : _a.includes("Unterminated string")) {
|
|
771
|
+
current = this.repairUnterminatedStrings(current);
|
|
772
|
+
} else {
|
|
773
|
+
current = this.applyCommonFixes(current);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
try {
|
|
777
|
+
let finalAttempt = this.applyCommonFixes(jsonStr.trim());
|
|
778
|
+
finalAttempt = this.repairUnterminatedStrings(finalAttempt);
|
|
779
|
+
JSON.parse(finalAttempt);
|
|
780
|
+
return finalAttempt;
|
|
781
|
+
} catch (e) {
|
|
782
|
+
throw new Error(`Unable to repair JSON after ${maxAttempts} attempts. Last known error: ${lastError}`);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
function extractJsonFromMarkdown(text) {
|
|
787
|
+
if (!text) {
|
|
788
|
+
throw new Error("Input text is empty or null.");
|
|
789
|
+
}
|
|
790
|
+
const trimmedText = text.trim();
|
|
791
|
+
try {
|
|
792
|
+
JSON.parse(trimmedText);
|
|
793
|
+
return trimmedText;
|
|
794
|
+
} catch (e) {
|
|
795
|
+
}
|
|
796
|
+
const markdownPatterns = [
|
|
797
|
+
/```(?:json|JSON)\s*([\s\S]*?)\s*```/,
|
|
798
|
+
// ```json ... ```
|
|
799
|
+
/```\s*({[\s\S]*?}|\[[\s\S]*?\])\s*```/
|
|
800
|
+
// ``` { ... } ``` or ``` [ ... ] ```
|
|
801
|
+
];
|
|
802
|
+
for (const pattern of markdownPatterns) {
|
|
803
|
+
const match = trimmedText.match(pattern);
|
|
804
|
+
if (match && match[1]) {
|
|
805
|
+
const content = match[1].trim();
|
|
806
|
+
try {
|
|
807
|
+
JSON.parse(content);
|
|
808
|
+
return content;
|
|
809
|
+
} catch (e) {
|
|
810
|
+
console.warn("JSON inside markdown block is invalid, attempting repair...");
|
|
811
|
+
try {
|
|
812
|
+
return JsonRepairEngine.repairJson(content);
|
|
813
|
+
} catch (repairError) {
|
|
814
|
+
console.warn(`Markdown block repair failed: ${repairError.message}. Trying other strategies...`);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
const firstBrace = trimmedText.indexOf("{");
|
|
820
|
+
const firstBracket = trimmedText.indexOf("[");
|
|
821
|
+
let startIndex = -1;
|
|
822
|
+
if (firstBrace === -1 && firstBracket === -1) ; else if (firstBrace === -1) {
|
|
823
|
+
startIndex = firstBracket;
|
|
824
|
+
} else if (firstBracket === -1) {
|
|
825
|
+
startIndex = firstBrace;
|
|
826
|
+
} else {
|
|
827
|
+
startIndex = Math.min(firstBrace, firstBracket);
|
|
828
|
+
}
|
|
829
|
+
if (startIndex !== -1) {
|
|
830
|
+
const textToProcess = trimmedText.substring(startIndex);
|
|
831
|
+
let balance = 0;
|
|
832
|
+
let inString = false;
|
|
833
|
+
let escaped = false;
|
|
834
|
+
const startChar = textToProcess[0];
|
|
835
|
+
const endChar = startChar === "{" ? "}" : "]";
|
|
836
|
+
for (let i = 0; i < textToProcess.length; i++) {
|
|
837
|
+
const char = textToProcess[i];
|
|
838
|
+
if (escaped) {
|
|
839
|
+
escaped = false;
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
if (char === "\\") {
|
|
843
|
+
escaped = true;
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
if (char === '"') {
|
|
847
|
+
inString = !inString;
|
|
848
|
+
}
|
|
849
|
+
if (!inString) {
|
|
850
|
+
if (char === startChar) balance++;
|
|
851
|
+
if (char === endChar) balance--;
|
|
852
|
+
}
|
|
853
|
+
if (balance === 0 && i > 0) {
|
|
854
|
+
const potentialJson = textToProcess.substring(0, i + 1);
|
|
855
|
+
try {
|
|
856
|
+
JSON.parse(potentialJson);
|
|
857
|
+
return potentialJson;
|
|
858
|
+
} catch (e) {
|
|
859
|
+
console.warn(`Balanced JSON segment is invalid, attempting repair...`);
|
|
860
|
+
try {
|
|
861
|
+
return JsonRepairEngine.repairJson(potentialJson);
|
|
862
|
+
} catch (repairError) {
|
|
863
|
+
console.warn(`Repair failed for balanced segment: ${repairError.message}`);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
break;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
console.warn("All extraction strategies failed, attempting to repair the entire input text as a last resort.");
|
|
871
|
+
try {
|
|
872
|
+
return JsonRepairEngine.repairJson(trimmedText);
|
|
873
|
+
} catch (finalError) {
|
|
874
|
+
throw new Error(`Unable to extract or repair valid JSON from AI response. Preview: "${trimmedText.substring(0, 100)}...". Final error: ${finalError.message}`);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
z.object({
|
|
878
|
+
language: z.custom(),
|
|
879
|
+
problemPrompt: z.string(),
|
|
880
|
+
userCode: z.string(),
|
|
881
|
+
testCase: z.custom()
|
|
882
|
+
});
|
|
883
|
+
var AIEvaluationOutputSchema = z.object({
|
|
884
|
+
passed: z.boolean().describe("Did the user's code produce the expected output for the given input?"),
|
|
885
|
+
actualOutput: z.any().describe("The actual output produced by the user's code."),
|
|
886
|
+
reasoning: z.string().describe("A brief explanation of why the code passed or failed, or if there was a syntax error.")
|
|
887
|
+
});
|
|
888
|
+
var EvaluateUserCodeOutputSchema = AIEvaluationOutputSchema;
|
|
889
|
+
|
|
890
|
+
// src/ai/flows/evaluate-user-code.ts
|
|
891
|
+
async function evaluateUserCode(clientInput, apiKey) {
|
|
892
|
+
try {
|
|
893
|
+
const ai = genkit({
|
|
894
|
+
plugins: [googleAI({ apiKey })],
|
|
895
|
+
model: gemini20Flash
|
|
896
|
+
});
|
|
897
|
+
const { language, problemPrompt, userCode, testCase } = clientInput;
|
|
898
|
+
const promptText = `
|
|
899
|
+
You are an expert Code Judge and Teaching Assistant for a ${language} programming course.
|
|
900
|
+
Your task is to evaluate a student's code submission for a specific problem against a single test case.
|
|
901
|
+
|
|
902
|
+
## Problem Description
|
|
903
|
+
${problemPrompt}
|
|
904
|
+
|
|
905
|
+
## Student's Code Submission
|
|
906
|
+
\`\`\`${language}
|
|
907
|
+
${userCode}
|
|
908
|
+
\`\`\`
|
|
909
|
+
|
|
910
|
+
## Test Case to Evaluate
|
|
911
|
+
- Input(s): ${JSON.stringify(testCase.input)}
|
|
912
|
+
- Expected Output: ${JSON.stringify(testCase.expectedOutput)}
|
|
913
|
+
|
|
914
|
+
## Your Task
|
|
915
|
+
1. **Analyze Execution:** Mentally execute the student's code with the provided input(s).
|
|
916
|
+
2. **Determine Output:** Figure out what the actual output of the code would be.
|
|
917
|
+
3. **Compare:** Compare the actual output with the expected output.
|
|
918
|
+
4. **Handle Errors:** If the code has a syntax error or would crash, treat it as a failure.
|
|
919
|
+
5. **Provide Reasoning:** Briefly explain your conclusion. If it failed, explain why (e.g., "incorrect result", "infinite loop", "syntax error on line 5").
|
|
920
|
+
|
|
921
|
+
**CRITICAL JSON OUTPUT FORMAT:**
|
|
922
|
+
Return ONLY the JSON object with this EXACT structure.
|
|
923
|
+
|
|
924
|
+
\`\`\`json
|
|
925
|
+
{
|
|
926
|
+
"passed": false,
|
|
927
|
+
"actualOutput": 5,
|
|
928
|
+
"reasoning": "The function correctly summed the numbers but did not filter for only even numbers."
|
|
929
|
+
}
|
|
930
|
+
\`\`\`
|
|
931
|
+
|
|
932
|
+
Return only the JSON response.`;
|
|
933
|
+
const response = await ai.generate(promptText);
|
|
934
|
+
const rawText = response.text;
|
|
935
|
+
const jsonText = extractJsonFromMarkdown(rawText);
|
|
936
|
+
const aiGeneratedContent = JSON.parse(jsonText);
|
|
937
|
+
return EvaluateUserCodeOutputSchema.parse(aiGeneratedContent);
|
|
938
|
+
} catch (error) {
|
|
939
|
+
console.error("Error evaluating user code:", error);
|
|
940
|
+
if (error instanceof z.ZodError) {
|
|
941
|
+
throw new Error(`AI evaluation output validation failed: ${error.message}`);
|
|
942
|
+
}
|
|
943
|
+
return {
|
|
944
|
+
passed: false,
|
|
945
|
+
actualOutput: "Evaluation Error",
|
|
946
|
+
reasoning: `The AI judge failed to process the code. Error: ${error.message}`
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// src/services/APIKeyService.ts
|
|
952
|
+
var GEMINI_API_KEY_SERVICE_NAME = "gemini";
|
|
953
|
+
var LOCAL_STORAGE_PREFIX = "iqk_api_keys_";
|
|
954
|
+
function _encode(data) {
|
|
955
|
+
if (typeof window !== "undefined" && typeof window.btoa === "function") {
|
|
956
|
+
try {
|
|
957
|
+
return window.btoa(data);
|
|
958
|
+
} catch (e) {
|
|
959
|
+
console.error("Base64 encoding (btoa) failed:", e);
|
|
960
|
+
return data;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
return data;
|
|
964
|
+
}
|
|
965
|
+
function _decode(data) {
|
|
966
|
+
if (typeof window !== "undefined" && typeof window.atob === "function") {
|
|
967
|
+
try {
|
|
968
|
+
return window.atob(data);
|
|
969
|
+
} catch (e) {
|
|
970
|
+
console.error("Base64 decoding (atob) failed:", e);
|
|
971
|
+
return data;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
return data;
|
|
975
|
+
}
|
|
976
|
+
var APIKeyService = class {
|
|
977
|
+
static getStorageKey(serviceName) {
|
|
978
|
+
return `${LOCAL_STORAGE_PREFIX}${serviceName}`;
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Saves an API key to localStorage. The key is mildly obfuscated using Base64.
|
|
982
|
+
* @param serviceName - The name of the service (e.g., 'gemini').
|
|
983
|
+
* @param apiKey - The API key to save.
|
|
984
|
+
*/
|
|
985
|
+
static saveAPIKey(serviceName, apiKey) {
|
|
986
|
+
if (typeof window !== "undefined" && window.localStorage) {
|
|
987
|
+
try {
|
|
988
|
+
const encodedKey = _encode(apiKey);
|
|
989
|
+
localStorage.setItem(this.getStorageKey(serviceName), encodedKey);
|
|
990
|
+
} catch (e) {
|
|
991
|
+
console.error(`Error saving API key for ${serviceName} to localStorage:`, e);
|
|
992
|
+
}
|
|
993
|
+
} else {
|
|
994
|
+
console.warn("localStorage is not available. APIKeyService cannot save keys.");
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Retrieves an API key from localStorage.
|
|
999
|
+
* @param serviceName - The name of the service.
|
|
1000
|
+
* @returns The decoded API key, or null if not found or if localStorage is unavailable.
|
|
1001
|
+
*/
|
|
1002
|
+
static getAPIKey(serviceName) {
|
|
1003
|
+
if (typeof window !== "undefined" && window.localStorage) {
|
|
1004
|
+
try {
|
|
1005
|
+
const storedKey = localStorage.getItem(this.getStorageKey(serviceName));
|
|
1006
|
+
if (storedKey) {
|
|
1007
|
+
return _decode(storedKey);
|
|
1008
|
+
}
|
|
1009
|
+
} catch (e) {
|
|
1010
|
+
console.error(`Error retrieving API key for ${serviceName} from localStorage:`, e);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
return null;
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Removes an API key from localStorage.
|
|
1017
|
+
* @param serviceName - The name of the service.
|
|
1018
|
+
*/
|
|
1019
|
+
static removeAPIKey(serviceName) {
|
|
1020
|
+
if (typeof window !== "undefined" && window.localStorage) {
|
|
1021
|
+
try {
|
|
1022
|
+
localStorage.removeItem(this.getStorageKey(serviceName));
|
|
1023
|
+
} catch (e) {
|
|
1024
|
+
console.error(`Error removing API key for ${serviceName} from localStorage:`, e);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Checks if an API key exists in localStorage for the given service.
|
|
1030
|
+
* @param serviceName - The name of the service.
|
|
1031
|
+
* @returns True if a key exists, false otherwise.
|
|
1032
|
+
*/
|
|
1033
|
+
static hasAPIKey(serviceName) {
|
|
1034
|
+
return this.getAPIKey(serviceName) !== null;
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
|
|
1038
|
+
// src/services/CodeEvaluationService.ts
|
|
1039
|
+
var CodeEvaluationService = class {
|
|
1040
|
+
constructor() {
|
|
1041
|
+
this.apiKey = APIKeyService.getAPIKey(GEMINI_API_KEY_SERVICE_NAME);
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Evaluates a user's code against a single test case using an AI judge.
|
|
1045
|
+
* @param question The full CodingQuestion object.
|
|
1046
|
+
* @param userCode The user's submitted code string.
|
|
1047
|
+
* @param testCase The specific TestCase to evaluate against.
|
|
1048
|
+
* @returns A promise that resolves to an EvaluationResult object.
|
|
1049
|
+
*/
|
|
1050
|
+
async evaluateSingleTestCase(question, userCode, testCase) {
|
|
1051
|
+
if (!this.apiKey) {
|
|
1052
|
+
return {
|
|
1053
|
+
testCaseId: testCase.id,
|
|
1054
|
+
passed: false,
|
|
1055
|
+
actualOutput: "Configuration Error",
|
|
1056
|
+
reasoning: "API Key is not configured."
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
const aiResult = await evaluateUserCode({
|
|
1060
|
+
language: question.language,
|
|
1061
|
+
problemPrompt: question.prompt,
|
|
1062
|
+
userCode,
|
|
1063
|
+
testCase
|
|
1064
|
+
}, this.apiKey);
|
|
1065
|
+
return __spreadValues({
|
|
1066
|
+
testCaseId: testCase.id
|
|
1067
|
+
}, aiResult);
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Evaluates user's code against all test cases for a given question.
|
|
1071
|
+
* @param question The full CodingQuestion object.
|
|
1072
|
+
* @param userCode The user's submitted code string.
|
|
1073
|
+
* @returns A promise that resolves to an array of EvaluationResult objects.
|
|
1074
|
+
*/
|
|
1075
|
+
async evaluateAllTestCases(question, userCode) {
|
|
1076
|
+
const results = [];
|
|
1077
|
+
for (const testCase of question.testCases) {
|
|
1078
|
+
const result = await this.evaluateSingleTestCase(question, userCode, testCase);
|
|
1079
|
+
results.push(result);
|
|
1080
|
+
}
|
|
1081
|
+
return results;
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Evaluates user's code against only the public test cases for a given question.
|
|
1085
|
+
* Useful for a "Run Tests" button before final submission.
|
|
1086
|
+
* @param question The full CodingQuestion object.
|
|
1087
|
+
* @param userCode The user's submitted code string.
|
|
1088
|
+
* @returns A promise that resolves to an array of EvaluationResult objects.
|
|
1089
|
+
*/
|
|
1090
|
+
async evaluatePublicTestCases(question, userCode) {
|
|
1091
|
+
const publicTestCases = question.testCases.filter((tc) => tc.isPublic);
|
|
1092
|
+
const results = [];
|
|
1093
|
+
for (const testCase of publicTestCases) {
|
|
1094
|
+
const result = await this.evaluateSingleTestCase(question, userCode, testCase);
|
|
1095
|
+
results.push(result);
|
|
1096
|
+
}
|
|
1097
|
+
return results;
|
|
1098
|
+
}
|
|
1099
|
+
};
|
|
1100
|
+
|
|
1101
|
+
// src/services/evaluators/coding-evaluator.ts
|
|
1102
|
+
var CodingEvaluator = class {
|
|
1103
|
+
async evaluate(question, answer) {
|
|
1104
|
+
var _a;
|
|
1105
|
+
const points = (_a = question.points) != null ? _a : 0;
|
|
1106
|
+
if (typeof answer !== "string" || !answer.trim()) {
|
|
1107
|
+
return {
|
|
1108
|
+
isCorrect: false,
|
|
1109
|
+
correctAnswer: { id: null, value: question.solutionCode },
|
|
1110
|
+
pointsEarned: 0,
|
|
1111
|
+
evaluationDetails: question.testCases.map((tc) => ({
|
|
1112
|
+
testCaseId: tc.id,
|
|
1113
|
+
passed: false,
|
|
1114
|
+
actualOutput: "No submission",
|
|
1115
|
+
reasoning: "User did not submit any code."
|
|
1116
|
+
}))
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
try {
|
|
1120
|
+
const evaluationService = new CodeEvaluationService();
|
|
1121
|
+
const testCaseResults = await evaluationService.evaluateAllTestCases(question, answer);
|
|
1122
|
+
const isCorrect = testCaseResults.every((result) => result.passed);
|
|
1123
|
+
const correctAnswerDetail = {
|
|
1124
|
+
id: null,
|
|
1125
|
+
value: question.solutionCode
|
|
1126
|
+
};
|
|
1127
|
+
return {
|
|
1128
|
+
isCorrect,
|
|
1129
|
+
correctAnswer: correctAnswerDetail,
|
|
1130
|
+
pointsEarned: isCorrect ? points : 0,
|
|
1131
|
+
evaluationDetails: testCaseResults
|
|
1132
|
+
// Pass through the detailed results
|
|
1133
|
+
};
|
|
1134
|
+
} catch (error) {
|
|
1135
|
+
console.error("A critical error occurred during code evaluation:", error);
|
|
1136
|
+
return {
|
|
1137
|
+
isCorrect: false,
|
|
1138
|
+
correctAnswer: { id: null, value: question.solutionCode },
|
|
1139
|
+
pointsEarned: 0,
|
|
1140
|
+
evaluationDetails: question.testCases.map((tc) => ({
|
|
1141
|
+
testCaseId: tc.id,
|
|
1142
|
+
passed: false,
|
|
1143
|
+
actualOutput: "Evaluation Error",
|
|
1144
|
+
reasoning: error instanceof Error ? error.message : "An unknown error occurred."
|
|
1145
|
+
}))
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
|
|
301
1151
|
// src/services/QuizEngine.ts
|
|
302
1152
|
var QuizEngine = class {
|
|
303
1153
|
constructor(options) {
|
|
@@ -314,6 +1164,8 @@ var QuizEngine = class {
|
|
|
314
1164
|
this.callbacks = options.callbacks || {};
|
|
315
1165
|
this.questions = ((_a = this.config.settings) == null ? void 0 : _a.shuffleQuestions) ? [...this.config.questions].sort(() => Math.random() - 0.5) : this.config.questions;
|
|
316
1166
|
this.overallStartTime = Date.now();
|
|
1167
|
+
this.evaluators = /* @__PURE__ */ new Map();
|
|
1168
|
+
this.registerEvaluators();
|
|
317
1169
|
if (((_b = this.config.settings) == null ? void 0 : _b.timeLimitMinutes) && this.config.settings.timeLimitMinutes > 0) {
|
|
318
1170
|
this.timeLeftInSeconds = this.config.settings.timeLimitMinutes * 60;
|
|
319
1171
|
}
|
|
@@ -352,6 +1204,22 @@ var QuizEngine = class {
|
|
|
352
1204
|
}
|
|
353
1205
|
(_e = (_d = this.callbacks).onQuestionChange) == null ? void 0 : _e.call(_d, initialQ, this.getCurrentQuestionNumber(), this.getTotalQuestions());
|
|
354
1206
|
}
|
|
1207
|
+
registerEvaluators() {
|
|
1208
|
+
this.evaluators.set("multiple_choice", new MultipleChoiceEvaluator());
|
|
1209
|
+
this.evaluators.set("multiple_response", new MultipleResponseEvaluator());
|
|
1210
|
+
this.evaluators.set("true_false", new TrueFalseEvaluator());
|
|
1211
|
+
this.evaluators.set("short_answer", new ShortAnswerEvaluator());
|
|
1212
|
+
this.evaluators.set("numeric", new NumericEvaluator());
|
|
1213
|
+
this.evaluators.set("sequence", new SequenceEvaluator());
|
|
1214
|
+
this.evaluators.set("matching", new MatchingEvaluator());
|
|
1215
|
+
this.evaluators.set("fill_in_the_blanks", new FillInTheBlanksEvaluator());
|
|
1216
|
+
this.evaluators.set("drag_and_drop", new DragAndDropEvaluator());
|
|
1217
|
+
this.evaluators.set("hotspot", new HotspotEvaluator());
|
|
1218
|
+
const programmingEvaluator = new ProgrammingEvaluator();
|
|
1219
|
+
this.evaluators.set("blockly_programming", programmingEvaluator);
|
|
1220
|
+
this.evaluators.set("scratch_programming", programmingEvaluator);
|
|
1221
|
+
this.evaluators.set("coding", new CodingEvaluator());
|
|
1222
|
+
}
|
|
355
1223
|
_recordCurrentQuestionTime() {
|
|
356
1224
|
if (this.questionStartTime && this.currentQuestionIndex >= 0 && this.currentQuestionIndex < this.questions.length) {
|
|
357
1225
|
const currentQId = this.questions[this.currentQuestionIndex].id;
|
|
@@ -444,180 +1312,28 @@ var QuizEngine = class {
|
|
|
444
1312
|
}
|
|
445
1313
|
return this.getCurrentQuestion();
|
|
446
1314
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
correctAnswerDetail = { id: correctAnswerId, value: correctValue };
|
|
462
|
-
isCorrect = answer === correctAnswerId;
|
|
463
|
-
break;
|
|
464
|
-
}
|
|
465
|
-
case "multiple_response": {
|
|
466
|
-
const q = question;
|
|
467
|
-
const correctAnswerIds = q.correctAnswerIds;
|
|
468
|
-
const correctValues = correctAnswerIds.map((id) => findOptionText(q, id));
|
|
469
|
-
correctAnswerDetail = { id: correctAnswerIds, value: correctValues };
|
|
470
|
-
if (Array.isArray(answer)) {
|
|
471
|
-
const userAnswerSet = new Set(answer);
|
|
472
|
-
const correctAnswerSet = new Set(correctAnswerIds);
|
|
473
|
-
isCorrect = userAnswerSet.size === correctAnswerSet.size && [...userAnswerSet].every((id) => correctAnswerSet.has(id));
|
|
474
|
-
}
|
|
475
|
-
break;
|
|
476
|
-
}
|
|
477
|
-
case "true_false": {
|
|
478
|
-
const q = question;
|
|
479
|
-
correctAnswerDetail = { id: null, value: q.correctAnswer };
|
|
480
|
-
let tfAnswer = answer;
|
|
481
|
-
if (typeof answer === "string") tfAnswer = answer.toLowerCase() === "true";
|
|
482
|
-
isCorrect = typeof tfAnswer === "boolean" && tfAnswer === q.correctAnswer;
|
|
483
|
-
break;
|
|
484
|
-
}
|
|
485
|
-
case "short_answer": {
|
|
486
|
-
const q = question;
|
|
487
|
-
correctAnswerDetail = { id: null, value: q.acceptedAnswers };
|
|
488
|
-
if (typeof answer === "string") {
|
|
489
|
-
const userAnswerTrimmed = answer.trim();
|
|
490
|
-
const caseSensitive = (_b = q.isCaseSensitive) != null ? _b : false;
|
|
491
|
-
isCorrect = q.acceptedAnswers.some((accAns) => caseSensitive ? accAns.trim() === userAnswerTrimmed : accAns.trim().toLowerCase() === userAnswerTrimmed.toLowerCase());
|
|
492
|
-
}
|
|
493
|
-
break;
|
|
494
|
-
}
|
|
495
|
-
case "numeric": {
|
|
496
|
-
const q = question;
|
|
497
|
-
correctAnswerDetail = { id: null, value: q.answer };
|
|
498
|
-
if (typeof answer === "string" || typeof answer === "number") {
|
|
499
|
-
const userAnswerNum = parseFloat(String(answer));
|
|
500
|
-
if (!isNaN(userAnswerNum)) {
|
|
501
|
-
isCorrect = q.tolerance != null ? Math.abs(userAnswerNum - q.answer) <= q.tolerance : userAnswerNum === q.answer;
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
break;
|
|
505
|
-
}
|
|
506
|
-
case "sequence": {
|
|
507
|
-
const q = question;
|
|
508
|
-
const correctValues = q.correctOrder.map((id) => {
|
|
509
|
-
var _a2;
|
|
510
|
-
return ((_a2 = q.items.find((item) => item.id === id)) == null ? void 0 : _a2.content) || "";
|
|
511
|
-
});
|
|
512
|
-
correctAnswerDetail = { id: q.correctOrder, value: correctValues };
|
|
513
|
-
if (Array.isArray(answer) && answer.length === q.correctOrder.length) {
|
|
514
|
-
isCorrect = answer.every((itemId, index) => itemId === q.correctOrder[index]);
|
|
515
|
-
}
|
|
516
|
-
break;
|
|
517
|
-
}
|
|
518
|
-
case "matching": {
|
|
519
|
-
const q = question;
|
|
520
|
-
const correctMap = q.correctAnswerMap.reduce((acc, curr) => {
|
|
521
|
-
var _a2, _b2;
|
|
522
|
-
const promptText = ((_a2 = q.prompts.find((p) => p.id === curr.promptId)) == null ? void 0 : _a2.content) || "";
|
|
523
|
-
const optionText = ((_b2 = q.options.find((o) => o.id === curr.optionId)) == null ? void 0 : _b2.content) || "";
|
|
524
|
-
acc[promptText] = optionText;
|
|
525
|
-
return acc;
|
|
526
|
-
}, {});
|
|
527
|
-
correctAnswerDetail = { id: null, value: correctMap };
|
|
528
|
-
if (typeof answer === "object" && answer !== null && !Array.isArray(answer)) {
|
|
529
|
-
const userAnswerMap = answer;
|
|
530
|
-
isCorrect = q.correctAnswerMap.length === Object.keys(userAnswerMap).length && q.correctAnswerMap.every((map) => userAnswerMap[map.promptId] === map.optionId);
|
|
531
|
-
}
|
|
532
|
-
break;
|
|
533
|
-
}
|
|
534
|
-
case "fill_in_the_blanks": {
|
|
535
|
-
const q = question;
|
|
536
|
-
const correctMap = q.answers.reduce((acc, curr) => {
|
|
537
|
-
acc[curr.blankId] = curr.acceptedValues.join(" | ");
|
|
538
|
-
return acc;
|
|
539
|
-
}, {});
|
|
540
|
-
correctAnswerDetail = { id: null, value: correctMap };
|
|
541
|
-
if (typeof answer === "object" && answer !== null && !Array.isArray(answer)) {
|
|
542
|
-
const userAnswerMap = answer;
|
|
543
|
-
isCorrect = q.answers.every((correctAnsDef) => {
|
|
544
|
-
var _a2, _b2;
|
|
545
|
-
const userValForBlank = (_a2 = userAnswerMap[correctAnsDef.blankId]) == null ? void 0 : _a2.trim();
|
|
546
|
-
if (userValForBlank === void 0) return false;
|
|
547
|
-
const caseSensitive = (_b2 = q.isCaseSensitive) != null ? _b2 : false;
|
|
548
|
-
return correctAnsDef.acceptedValues.some((accVal) => caseSensitive ? accVal.trim() === userValForBlank : accVal.trim().toLowerCase() === userValForBlank.toLowerCase());
|
|
549
|
-
});
|
|
550
|
-
}
|
|
551
|
-
break;
|
|
552
|
-
}
|
|
553
|
-
case "drag_and_drop": {
|
|
554
|
-
const q = question;
|
|
555
|
-
const correctMap = q.answerMap.reduce((acc, curr) => {
|
|
556
|
-
var _a2, _b2;
|
|
557
|
-
const draggableText = ((_a2 = q.draggableItems.find((d) => d.id === curr.draggableId)) == null ? void 0 : _a2.content) || "";
|
|
558
|
-
const dropZoneText = ((_b2 = q.dropZones.find((z) => z.id === curr.dropZoneId)) == null ? void 0 : _b2.label) || "";
|
|
559
|
-
acc[draggableText] = dropZoneText;
|
|
560
|
-
return acc;
|
|
561
|
-
}, {});
|
|
562
|
-
correctAnswerDetail = { id: null, value: correctMap };
|
|
563
|
-
if (typeof answer === "object" && answer !== null && !Array.isArray(answer)) {
|
|
564
|
-
const userAnswerMap = answer;
|
|
565
|
-
isCorrect = q.answerMap.length === Object.keys(userAnswerMap).length && q.answerMap.every((map) => userAnswerMap[map.draggableId] === map.dropZoneId);
|
|
566
|
-
}
|
|
567
|
-
break;
|
|
568
|
-
}
|
|
569
|
-
case "hotspot": {
|
|
570
|
-
const q = question;
|
|
571
|
-
const correctValues = q.correctHotspotIds.map((id) => {
|
|
572
|
-
var _a2;
|
|
573
|
-
return ((_a2 = q.hotspots.find((h) => h.id === id)) == null ? void 0 : _a2.description) || id;
|
|
574
|
-
});
|
|
575
|
-
correctAnswerDetail = { id: q.correctHotspotIds, value: correctValues };
|
|
576
|
-
if (Array.isArray(answer)) {
|
|
577
|
-
const userAnswerSet = new Set(answer);
|
|
578
|
-
const correctAnswerSet = new Set(q.correctHotspotIds);
|
|
579
|
-
isCorrect = userAnswerSet.size === correctAnswerSet.size && [...userAnswerSet].every((id) => correctAnswerSet.has(id));
|
|
580
|
-
}
|
|
581
|
-
break;
|
|
582
|
-
}
|
|
583
|
-
case "blockly_programming":
|
|
584
|
-
case "scratch_programming": {
|
|
585
|
-
const q = question;
|
|
586
|
-
correctAnswerDetail = { id: null, value: q.solutionGeneratedCode || "" };
|
|
587
|
-
if (typeof answer === "string" && typeof q.solutionGeneratedCode === "string") {
|
|
588
|
-
if (typeof window !== "undefined" && ((_c = window.Blockly) == null ? void 0 : _c.JavaScript)) {
|
|
589
|
-
const LocalBlockly = window.Blockly;
|
|
590
|
-
let generatedUserCode = "";
|
|
591
|
-
try {
|
|
592
|
-
const tempWorkspace = new LocalBlockly.Workspace();
|
|
593
|
-
const dom = LocalBlockly.Xml.textToDom(answer);
|
|
594
|
-
LocalBlockly.Xml.domToWorkspace(dom, tempWorkspace);
|
|
595
|
-
generatedUserCode = LocalBlockly.JavaScript.workspaceToCode(tempWorkspace) || "";
|
|
596
|
-
const normalize = (code) => code.replace(/\s+/g, " ").trim();
|
|
597
|
-
isCorrect = normalize(generatedUserCode) === normalize(q.solutionGeneratedCode);
|
|
598
|
-
tempWorkspace.dispose();
|
|
599
|
-
} catch (e) {
|
|
600
|
-
console.error(`Error generating code from user's ${q.questionType} XML for evaluation:`, e);
|
|
601
|
-
isCorrect = false;
|
|
602
|
-
}
|
|
603
|
-
} else {
|
|
604
|
-
console.warn(`Blockly library not available in QuizEngine for ${q.questionType} code generation during evaluation. Skipping code comparison.`);
|
|
605
|
-
isCorrect = false;
|
|
606
|
-
}
|
|
1315
|
+
getElapsedTime() {
|
|
1316
|
+
return Date.now() - this.overallStartTime;
|
|
1317
|
+
}
|
|
1318
|
+
destroy() {
|
|
1319
|
+
this.stopTimer();
|
|
1320
|
+
this._recordCurrentQuestionTime();
|
|
1321
|
+
if (this.scormService && this.scormService.hasAPI()) {
|
|
1322
|
+
if (["initialized", "committed", "sending_data"].includes(this.quizResultState.scormStatus || "")) {
|
|
1323
|
+
const termResult = this.scormService.terminate();
|
|
1324
|
+
if (termResult.success) {
|
|
1325
|
+
this.quizResultState.scormStatus = "terminated";
|
|
1326
|
+
} else {
|
|
1327
|
+
this.quizResultState.scormStatus = "error";
|
|
1328
|
+
this.quizResultState.scormError = termResult.error || "SCORM termination failed on destroy.";
|
|
607
1329
|
}
|
|
608
|
-
break;
|
|
609
|
-
}
|
|
610
|
-
default: {
|
|
611
|
-
const _exhaustiveCheck = question;
|
|
612
|
-
console.warn("Unsupported question type in QuizEngine evaluation:", _exhaustiveCheck);
|
|
613
|
-
isCorrect = false;
|
|
614
|
-
correctAnswerDetail = { id: null, value: "Evaluation not implemented." };
|
|
615
1330
|
}
|
|
616
1331
|
}
|
|
617
|
-
|
|
1332
|
+
this.scormService = null;
|
|
618
1333
|
}
|
|
1334
|
+
// (Tiếp theo từ Phần 1)
|
|
619
1335
|
async calculateResults() {
|
|
620
|
-
var _a, _b, _c, _d, _e
|
|
1336
|
+
var _a, _b, _c, _d, _e;
|
|
621
1337
|
this.stopTimer();
|
|
622
1338
|
this._recordCurrentQuestionTime();
|
|
623
1339
|
let totalScore = 0;
|
|
@@ -627,92 +1343,31 @@ var QuizEngine = class {
|
|
|
627
1343
|
for (const question of this.questions) {
|
|
628
1344
|
const userAnswerRaw = this.userAnswers.get(question.id) || null;
|
|
629
1345
|
maxScore += (_a = question.points) != null ? _a : 0;
|
|
630
|
-
const
|
|
1346
|
+
const evaluator = this.evaluators.get(question.questionType);
|
|
1347
|
+
if (!evaluator) {
|
|
1348
|
+
console.warn(`No evaluator found for question type: ${question.questionType}`);
|
|
1349
|
+
questionResultsArray.push({
|
|
1350
|
+
questionId: question.id,
|
|
1351
|
+
questionType: question.questionType,
|
|
1352
|
+
prompt: question.prompt,
|
|
1353
|
+
isCorrect: false,
|
|
1354
|
+
pointsEarned: 0,
|
|
1355
|
+
userAnswer: { id: null, value: userAnswerRaw },
|
|
1356
|
+
correctAnswer: { id: null, value: "Evaluation not implemented." },
|
|
1357
|
+
timeSpentSeconds: parseFloat((this.questionTimings.get(question.id) || 0).toFixed(2))
|
|
1358
|
+
});
|
|
1359
|
+
continue;
|
|
1360
|
+
}
|
|
1361
|
+
const {
|
|
1362
|
+
isCorrect,
|
|
1363
|
+
correctAnswer: correctAnswerDetail,
|
|
1364
|
+
pointsEarned,
|
|
1365
|
+
evaluationDetails
|
|
1366
|
+
} = await evaluator.evaluate(question, userAnswerRaw);
|
|
631
1367
|
totalScore += pointsEarned;
|
|
632
1368
|
const timeSpentOnThisQuestion = parseFloat((this.questionTimings.get(question.id) || 0).toFixed(2));
|
|
633
|
-
accumulatedTotalTimeSpent += timeSpentOnThisQuestion;
|
|
634
|
-
|
|
635
|
-
let allOptions = void 0;
|
|
636
|
-
if (userAnswerRaw !== null) {
|
|
637
|
-
switch (question.questionType) {
|
|
638
|
-
case "multiple_choice": {
|
|
639
|
-
const q = question;
|
|
640
|
-
allOptions = q.options.map((opt) => ({ id: opt.id, value: opt.text }));
|
|
641
|
-
const id = userAnswerRaw;
|
|
642
|
-
userAnswerDetail = { id, value: ((_b = allOptions.find((opt) => opt.id === id)) == null ? void 0 : _b.value) || "" };
|
|
643
|
-
break;
|
|
644
|
-
}
|
|
645
|
-
case "multiple_response": {
|
|
646
|
-
const q = question;
|
|
647
|
-
allOptions = q.options.map((opt) => ({ id: opt.id, value: opt.text }));
|
|
648
|
-
const ids = userAnswerRaw;
|
|
649
|
-
const values = ids.map((id) => {
|
|
650
|
-
var _a2;
|
|
651
|
-
return ((_a2 = allOptions == null ? void 0 : allOptions.find((opt) => opt.id === id)) == null ? void 0 : _a2.value) || "";
|
|
652
|
-
});
|
|
653
|
-
userAnswerDetail = { id: ids, value: values };
|
|
654
|
-
break;
|
|
655
|
-
}
|
|
656
|
-
case "true_false":
|
|
657
|
-
case "short_answer":
|
|
658
|
-
case "numeric":
|
|
659
|
-
userAnswerDetail = { id: null, value: userAnswerRaw };
|
|
660
|
-
break;
|
|
661
|
-
case "sequence": {
|
|
662
|
-
const q = question;
|
|
663
|
-
allOptions = q.items.map((item) => ({ id: item.id, value: item.content }));
|
|
664
|
-
const ids = userAnswerRaw;
|
|
665
|
-
const values = ids.map((id) => {
|
|
666
|
-
var _a2;
|
|
667
|
-
return ((_a2 = allOptions == null ? void 0 : allOptions.find((opt) => opt.id === id)) == null ? void 0 : _a2.value) || "";
|
|
668
|
-
});
|
|
669
|
-
userAnswerDetail = { id: ids, value: values };
|
|
670
|
-
break;
|
|
671
|
-
}
|
|
672
|
-
case "matching": {
|
|
673
|
-
const q = question;
|
|
674
|
-
const userAnswerMap = userAnswerRaw;
|
|
675
|
-
const valueMap = {};
|
|
676
|
-
for (const promptId in userAnswerMap) {
|
|
677
|
-
const optionId = userAnswerMap[promptId];
|
|
678
|
-
const promptText = ((_c = q.prompts.find((p) => p.id === promptId)) == null ? void 0 : _c.content) || "";
|
|
679
|
-
const optionText = ((_d = q.options.find((o) => o.id === optionId)) == null ? void 0 : _d.content) || "";
|
|
680
|
-
valueMap[promptText] = optionText;
|
|
681
|
-
}
|
|
682
|
-
userAnswerDetail = { id: null, value: valueMap };
|
|
683
|
-
break;
|
|
684
|
-
}
|
|
685
|
-
// --- LOGIC MỚI ĐƯỢC THÊM VÀO ---
|
|
686
|
-
case "fill_in_the_blanks": {
|
|
687
|
-
if (typeof userAnswerRaw === "object" && userAnswerRaw !== null && !Array.isArray(userAnswerRaw)) {
|
|
688
|
-
userAnswerDetail = { id: null, value: userAnswerRaw };
|
|
689
|
-
}
|
|
690
|
-
break;
|
|
691
|
-
}
|
|
692
|
-
case "drag_and_drop": {
|
|
693
|
-
const q = question;
|
|
694
|
-
if (typeof userAnswerRaw === "object" && userAnswerRaw !== null && !Array.isArray(userAnswerRaw)) {
|
|
695
|
-
const userAnswerMapByIds = userAnswerRaw;
|
|
696
|
-
const enrichedUserAnswerMap = {};
|
|
697
|
-
for (const draggableId in userAnswerMapByIds) {
|
|
698
|
-
const dropZoneId = userAnswerMapByIds[draggableId];
|
|
699
|
-
const draggableText = ((_e = q.draggableItems.find((d) => d.id === draggableId)) == null ? void 0 : _e.content) || `(ID: ${draggableId})`;
|
|
700
|
-
const dropZoneText = ((_f = q.dropZones.find((z) => z.id === dropZoneId)) == null ? void 0 : _f.label) || `(ID: ${dropZoneId})`;
|
|
701
|
-
enrichedUserAnswerMap[draggableText] = dropZoneText;
|
|
702
|
-
}
|
|
703
|
-
userAnswerDetail = { id: null, value: enrichedUserAnswerMap };
|
|
704
|
-
}
|
|
705
|
-
break;
|
|
706
|
-
}
|
|
707
|
-
// ------------------------------------
|
|
708
|
-
// Các loại câu hỏi còn lại vẫn giữ fallback
|
|
709
|
-
case "hotspot":
|
|
710
|
-
case "blockly_programming":
|
|
711
|
-
case "scratch_programming":
|
|
712
|
-
userAnswerDetail = { id: null, value: userAnswerRaw };
|
|
713
|
-
break;
|
|
714
|
-
}
|
|
715
|
-
}
|
|
1369
|
+
accumulatedTotalTimeSpent += timeSpentOnThisQuestion;
|
|
1370
|
+
const userAnswerDetail = this.formatUserAnswerDetail(question, userAnswerRaw);
|
|
716
1371
|
questionResultsArray.push({
|
|
717
1372
|
questionId: question.id,
|
|
718
1373
|
questionType: question.questionType,
|
|
@@ -721,18 +1376,18 @@ var QuizEngine = class {
|
|
|
721
1376
|
pointsEarned,
|
|
722
1377
|
userAnswer: userAnswerDetail,
|
|
723
1378
|
correctAnswer: correctAnswerDetail,
|
|
724
|
-
|
|
725
|
-
|
|
1379
|
+
timeSpentSeconds: timeSpentOnThisQuestion,
|
|
1380
|
+
evaluationDetails
|
|
726
1381
|
});
|
|
727
1382
|
}
|
|
728
1383
|
const percentage = maxScore > 0 ? parseFloat((totalScore / maxScore * 100).toFixed(2)) : 0;
|
|
729
1384
|
let passed = void 0;
|
|
730
|
-
if (((
|
|
1385
|
+
if (((_b = this.config.settings) == null ? void 0 : _b.passingScorePercent) != null) {
|
|
731
1386
|
passed = percentage >= this.config.settings.passingScorePercent;
|
|
732
1387
|
}
|
|
733
1388
|
const totalQuizTimeSpentSeconds = parseFloat(accumulatedTotalTimeSpent.toFixed(2));
|
|
734
1389
|
const averageTimePerQuestionSeconds = this.questions.length > 0 ? parseFloat((totalQuizTimeSpentSeconds / this.questions.length).toFixed(2)) : 0;
|
|
735
|
-
const metadataPerformance = this._calculateMetadataPerformance();
|
|
1390
|
+
const metadataPerformance = await this._calculateMetadataPerformance();
|
|
736
1391
|
const finalResults = __spreadValues({
|
|
737
1392
|
score: totalScore,
|
|
738
1393
|
maxScore,
|
|
@@ -748,11 +1403,118 @@ var QuizEngine = class {
|
|
|
748
1403
|
averageTimePerQuestionSeconds
|
|
749
1404
|
}, metadataPerformance);
|
|
750
1405
|
this.quizResultState = __spreadValues(__spreadValues({}, this.quizResultState), finalResults);
|
|
751
|
-
if ((
|
|
1406
|
+
if ((_c = this.config.settings) == null ? void 0 : _c.scorm) this._sendResultsToSCORM(finalResults);
|
|
752
1407
|
await this._sendResultsToWebhook(finalResults);
|
|
753
|
-
(
|
|
1408
|
+
(_e = (_d = this.callbacks).onQuizFinish) == null ? void 0 : _e.call(_d, finalResults);
|
|
754
1409
|
return finalResults;
|
|
755
1410
|
}
|
|
1411
|
+
formatUserAnswerDetail(question, userAnswerRaw) {
|
|
1412
|
+
var _a, _b, _c, _d, _e;
|
|
1413
|
+
if (userAnswerRaw === null) return null;
|
|
1414
|
+
switch (question.questionType) {
|
|
1415
|
+
case "multiple_choice": {
|
|
1416
|
+
const q = question;
|
|
1417
|
+
const id = userAnswerRaw;
|
|
1418
|
+
return { id, value: ((_a = q.options.find((opt) => opt.id === id)) == null ? void 0 : _a.text) || "" };
|
|
1419
|
+
}
|
|
1420
|
+
case "multiple_response": {
|
|
1421
|
+
const q = question;
|
|
1422
|
+
const ids = userAnswerRaw;
|
|
1423
|
+
const values = ids.map((id) => {
|
|
1424
|
+
var _a2;
|
|
1425
|
+
return ((_a2 = q.options.find((opt) => opt.id === id)) == null ? void 0 : _a2.text) || "";
|
|
1426
|
+
});
|
|
1427
|
+
return { id: ids, value: values };
|
|
1428
|
+
}
|
|
1429
|
+
case "sequence": {
|
|
1430
|
+
const q = question;
|
|
1431
|
+
const ids = userAnswerRaw;
|
|
1432
|
+
const values = ids.map((id) => {
|
|
1433
|
+
var _a2;
|
|
1434
|
+
return ((_a2 = q.items.find((item) => item.id === id)) == null ? void 0 : _a2.content) || "";
|
|
1435
|
+
});
|
|
1436
|
+
return { id: ids, value: values };
|
|
1437
|
+
}
|
|
1438
|
+
case "matching": {
|
|
1439
|
+
const q = question;
|
|
1440
|
+
const userAnswerMap = userAnswerRaw;
|
|
1441
|
+
const valueMap = {};
|
|
1442
|
+
for (const promptId in userAnswerMap) {
|
|
1443
|
+
const optionId = userAnswerMap[promptId];
|
|
1444
|
+
const promptText = ((_b = q.prompts.find((p) => p.id === promptId)) == null ? void 0 : _b.content) || "";
|
|
1445
|
+
const optionText = ((_c = q.options.find((o) => o.id === optionId)) == null ? void 0 : _c.content) || "";
|
|
1446
|
+
valueMap[promptText] = optionText;
|
|
1447
|
+
}
|
|
1448
|
+
return { id: null, value: valueMap };
|
|
1449
|
+
}
|
|
1450
|
+
case "drag_and_drop": {
|
|
1451
|
+
const q = question;
|
|
1452
|
+
if (typeof userAnswerRaw === "object" && userAnswerRaw !== null && !Array.isArray(userAnswerRaw)) {
|
|
1453
|
+
const userAnswerMapByIds = userAnswerRaw;
|
|
1454
|
+
const enrichedUserAnswerMap = {};
|
|
1455
|
+
for (const draggableId in userAnswerMapByIds) {
|
|
1456
|
+
const dropZoneId = userAnswerMapByIds[draggableId];
|
|
1457
|
+
const draggableText = ((_d = q.draggableItems.find((d) => d.id === draggableId)) == null ? void 0 : _d.content) || `(ID: ${draggableId})`;
|
|
1458
|
+
const dropZoneText = ((_e = q.dropZones.find((z3) => z3.id === dropZoneId)) == null ? void 0 : _e.label) || `(ID: ${dropZoneId})`;
|
|
1459
|
+
enrichedUserAnswerMap[draggableText] = dropZoneText;
|
|
1460
|
+
}
|
|
1461
|
+
return { id: null, value: enrichedUserAnswerMap };
|
|
1462
|
+
}
|
|
1463
|
+
return { id: null, value: userAnswerRaw };
|
|
1464
|
+
}
|
|
1465
|
+
default:
|
|
1466
|
+
return { id: null, value: userAnswerRaw };
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
async _calculateMetadataPerformance() {
|
|
1470
|
+
var _a;
|
|
1471
|
+
const loPerformanceMap = /* @__PURE__ */ new Map();
|
|
1472
|
+
const categoryPerformanceMap = /* @__PURE__ */ new Map();
|
|
1473
|
+
const topicPerformanceMap = /* @__PURE__ */ new Map();
|
|
1474
|
+
const difficultyPerformanceMap = /* @__PURE__ */ new Map();
|
|
1475
|
+
const bloomLevelPerformanceMap = /* @__PURE__ */ new Map();
|
|
1476
|
+
const updateMap = (map, key, points, isCorrect) => {
|
|
1477
|
+
if (!key) return;
|
|
1478
|
+
const current = map.get(key) || { totalQuestions: 0, correctQuestions: 0, pointsEarned: 0, maxPoints: 0 };
|
|
1479
|
+
current.totalQuestions++;
|
|
1480
|
+
current.maxPoints += points;
|
|
1481
|
+
if (isCorrect) {
|
|
1482
|
+
current.correctQuestions++;
|
|
1483
|
+
current.pointsEarned += points;
|
|
1484
|
+
}
|
|
1485
|
+
map.set(key, current);
|
|
1486
|
+
};
|
|
1487
|
+
for (const q of this.questions) {
|
|
1488
|
+
const userAnswer = this.userAnswers.get(q.id) || null;
|
|
1489
|
+
const evaluator = this.evaluators.get(q.questionType);
|
|
1490
|
+
if (evaluator) {
|
|
1491
|
+
const { isCorrect } = await evaluator.evaluate(q, userAnswer);
|
|
1492
|
+
const pointsForThisQuestion = (_a = q.points) != null ? _a : 0;
|
|
1493
|
+
updateMap(loPerformanceMap, q.learningObjective, pointsForThisQuestion, isCorrect);
|
|
1494
|
+
updateMap(categoryPerformanceMap, q.category, pointsForThisQuestion, isCorrect);
|
|
1495
|
+
updateMap(topicPerformanceMap, q.topic, pointsForThisQuestion, isCorrect);
|
|
1496
|
+
updateMap(difficultyPerformanceMap, q.difficulty, pointsForThisQuestion, isCorrect);
|
|
1497
|
+
updateMap(bloomLevelPerformanceMap, q.bloomLevel, pointsForThisQuestion, isCorrect);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
const formatPerformanceArray = (map, keyName) => {
|
|
1501
|
+
return Array.from(map.entries()).map(([key, data]) => ({
|
|
1502
|
+
[keyName]: key,
|
|
1503
|
+
totalQuestions: data.totalQuestions,
|
|
1504
|
+
correctQuestions: data.correctQuestions,
|
|
1505
|
+
pointsEarned: data.pointsEarned,
|
|
1506
|
+
maxPoints: data.maxPoints,
|
|
1507
|
+
percentage: data.maxPoints > 0 ? parseFloat((data.pointsEarned / data.maxPoints * 100).toFixed(2)) : 0
|
|
1508
|
+
}));
|
|
1509
|
+
};
|
|
1510
|
+
return {
|
|
1511
|
+
performanceByLearningObjective: formatPerformanceArray(loPerformanceMap, "learningObjective"),
|
|
1512
|
+
performanceByCategory: formatPerformanceArray(categoryPerformanceMap, "category"),
|
|
1513
|
+
performanceByTopic: formatPerformanceArray(topicPerformanceMap, "topic"),
|
|
1514
|
+
performanceByDifficulty: formatPerformanceArray(difficultyPerformanceMap, "difficulty"),
|
|
1515
|
+
performanceByBloomLevel: formatPerformanceArray(bloomLevelPerformanceMap, "bloomLevel")
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
756
1518
|
async _sendResultsToWebhook(results) {
|
|
757
1519
|
var _a;
|
|
758
1520
|
if (!((_a = this.config.settings) == null ? void 0 : _a.webhookUrl)) {
|
|
@@ -820,80 +1582,15 @@ var QuizEngine = class {
|
|
|
820
1582
|
results.scormError = e instanceof Error ? e.message : "Unknown SCORM data sending error.";
|
|
821
1583
|
}
|
|
822
1584
|
}
|
|
823
|
-
_calculateMetadataPerformance() {
|
|
824
|
-
const loPerformanceMap = /* @__PURE__ */ new Map();
|
|
825
|
-
const categoryPerformanceMap = /* @__PURE__ */ new Map();
|
|
826
|
-
const topicPerformanceMap = /* @__PURE__ */ new Map();
|
|
827
|
-
const difficultyPerformanceMap = /* @__PURE__ */ new Map();
|
|
828
|
-
const bloomLevelPerformanceMap = /* @__PURE__ */ new Map();
|
|
829
|
-
const updateMap = (map, key, points, isCorrect) => {
|
|
830
|
-
if (!key) return;
|
|
831
|
-
const current = map.get(key) || { totalQuestions: 0, correctQuestions: 0, pointsEarned: 0, maxPoints: 0 };
|
|
832
|
-
current.totalQuestions++;
|
|
833
|
-
current.maxPoints += points;
|
|
834
|
-
if (isCorrect) {
|
|
835
|
-
current.correctQuestions++;
|
|
836
|
-
current.pointsEarned += points;
|
|
837
|
-
}
|
|
838
|
-
map.set(key, current);
|
|
839
|
-
};
|
|
840
|
-
this.questions.forEach((q) => {
|
|
841
|
-
var _a;
|
|
842
|
-
const qResult = this.userAnswers.get(q.id);
|
|
843
|
-
const { isCorrect } = this.evaluateQuestion(q, qResult || null);
|
|
844
|
-
const pointsForThisQuestion = (_a = q.points) != null ? _a : 0;
|
|
845
|
-
updateMap(loPerformanceMap, q.learningObjective, pointsForThisQuestion, isCorrect);
|
|
846
|
-
updateMap(categoryPerformanceMap, q.category, pointsForThisQuestion, isCorrect);
|
|
847
|
-
updateMap(topicPerformanceMap, q.topic, pointsForThisQuestion, isCorrect);
|
|
848
|
-
updateMap(difficultyPerformanceMap, q.difficulty, pointsForThisQuestion, isCorrect);
|
|
849
|
-
updateMap(bloomLevelPerformanceMap, q.bloomLevel, pointsForThisQuestion, isCorrect);
|
|
850
|
-
});
|
|
851
|
-
const formatPerformanceArray = (map, keyName) => {
|
|
852
|
-
return Array.from(map.entries()).map(([key, data]) => ({
|
|
853
|
-
[keyName]: key,
|
|
854
|
-
totalQuestions: data.totalQuestions,
|
|
855
|
-
correctQuestions: data.correctQuestions,
|
|
856
|
-
pointsEarned: data.pointsEarned,
|
|
857
|
-
maxPoints: data.maxPoints,
|
|
858
|
-
percentage: data.maxPoints > 0 ? parseFloat((data.pointsEarned / data.maxPoints * 100).toFixed(2)) : 0
|
|
859
|
-
}));
|
|
860
|
-
};
|
|
861
|
-
return {
|
|
862
|
-
performanceByLearningObjective: formatPerformanceArray(loPerformanceMap, "learningObjective"),
|
|
863
|
-
performanceByCategory: formatPerformanceArray(categoryPerformanceMap, "category"),
|
|
864
|
-
performanceByTopic: formatPerformanceArray(topicPerformanceMap, "topic"),
|
|
865
|
-
performanceByDifficulty: formatPerformanceArray(difficultyPerformanceMap, "difficulty"),
|
|
866
|
-
performanceByBloomLevel: formatPerformanceArray(bloomLevelPerformanceMap, "bloomLevel")
|
|
867
|
-
};
|
|
868
|
-
}
|
|
869
|
-
getElapsedTime() {
|
|
870
|
-
return Date.now() - this.overallStartTime;
|
|
871
|
-
}
|
|
872
|
-
destroy() {
|
|
873
|
-
this.stopTimer();
|
|
874
|
-
this._recordCurrentQuestionTime();
|
|
875
|
-
if (this.scormService && this.scormService.hasAPI()) {
|
|
876
|
-
if (["initialized", "committed", "sending_data"].includes(this.quizResultState.scormStatus || "")) {
|
|
877
|
-
const termResult = this.scormService.terminate();
|
|
878
|
-
if (termResult.success) {
|
|
879
|
-
this.quizResultState.scormStatus = "terminated";
|
|
880
|
-
} else {
|
|
881
|
-
this.quizResultState.scormStatus = "error";
|
|
882
|
-
this.quizResultState.scormError = termResult.error || "SCORM termination failed on destroy.";
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
this.scormService = null;
|
|
887
|
-
}
|
|
888
1585
|
};
|
|
889
1586
|
function cn(...inputs) {
|
|
890
1587
|
return twMerge(clsx(inputs));
|
|
891
1588
|
}
|
|
892
1589
|
|
|
893
1590
|
// src/react-ui/components/elements/radio-group.tsx
|
|
894
|
-
var RadioGroup =
|
|
1591
|
+
var RadioGroup = React28.forwardRef((_a, ref) => {
|
|
895
1592
|
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
896
|
-
return /* @__PURE__ */
|
|
1593
|
+
return /* @__PURE__ */ React28.createElement(
|
|
897
1594
|
RadioGroupPrimitive.Root,
|
|
898
1595
|
__spreadProps(__spreadValues({
|
|
899
1596
|
className: cn("grid gap-2", className)
|
|
@@ -903,9 +1600,9 @@ var RadioGroup = React25.forwardRef((_a, ref) => {
|
|
|
903
1600
|
);
|
|
904
1601
|
});
|
|
905
1602
|
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
|
|
906
|
-
var RadioGroupItem =
|
|
1603
|
+
var RadioGroupItem = React28.forwardRef((_a, ref) => {
|
|
907
1604
|
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
908
|
-
return /* @__PURE__ */
|
|
1605
|
+
return /* @__PURE__ */ React28.createElement(
|
|
909
1606
|
RadioGroupPrimitive.Item,
|
|
910
1607
|
__spreadValues({
|
|
911
1608
|
ref,
|
|
@@ -914,16 +1611,16 @@ var RadioGroupItem = React25.forwardRef((_a, ref) => {
|
|
|
914
1611
|
className
|
|
915
1612
|
)
|
|
916
1613
|
}, props),
|
|
917
|
-
/* @__PURE__ */
|
|
1614
|
+
/* @__PURE__ */ React28.createElement(RadioGroupPrimitive.Indicator, { className: "flex items-center justify-center" }, /* @__PURE__ */ React28.createElement(Circle, { className: "h-2.5 w-2.5 fill-current text-current" }))
|
|
918
1615
|
);
|
|
919
1616
|
});
|
|
920
1617
|
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
|
|
921
1618
|
var labelVariants = cva(
|
|
922
1619
|
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
923
1620
|
);
|
|
924
|
-
var Label =
|
|
1621
|
+
var Label = React28.forwardRef((_a, ref) => {
|
|
925
1622
|
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
926
|
-
return /* @__PURE__ */
|
|
1623
|
+
return /* @__PURE__ */ React28.createElement(
|
|
927
1624
|
LabelPrimitive.Root,
|
|
928
1625
|
__spreadValues({
|
|
929
1626
|
ref,
|
|
@@ -932,9 +1629,9 @@ var Label = React25.forwardRef((_a, ref) => {
|
|
|
932
1629
|
);
|
|
933
1630
|
});
|
|
934
1631
|
Label.displayName = LabelPrimitive.Root.displayName;
|
|
935
|
-
var Card =
|
|
1632
|
+
var Card = React28.forwardRef((_a, ref) => {
|
|
936
1633
|
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
937
|
-
return /* @__PURE__ */
|
|
1634
|
+
return /* @__PURE__ */ React28.createElement(
|
|
938
1635
|
"div",
|
|
939
1636
|
__spreadValues({
|
|
940
1637
|
ref,
|
|
@@ -946,9 +1643,9 @@ var Card = React25.forwardRef((_a, ref) => {
|
|
|
946
1643
|
);
|
|
947
1644
|
});
|
|
948
1645
|
Card.displayName = "Card";
|
|
949
|
-
var CardHeader =
|
|
1646
|
+
var CardHeader = React28.forwardRef((_a, ref) => {
|
|
950
1647
|
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
951
|
-
return /* @__PURE__ */
|
|
1648
|
+
return /* @__PURE__ */ React28.createElement(
|
|
952
1649
|
"div",
|
|
953
1650
|
__spreadValues({
|
|
954
1651
|
ref,
|
|
@@ -957,9 +1654,9 @@ var CardHeader = React25.forwardRef((_a, ref) => {
|
|
|
957
1654
|
);
|
|
958
1655
|
});
|
|
959
1656
|
CardHeader.displayName = "CardHeader";
|
|
960
|
-
var CardTitle =
|
|
1657
|
+
var CardTitle = React28.forwardRef((_a, ref) => {
|
|
961
1658
|
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
962
|
-
return /* @__PURE__ */
|
|
1659
|
+
return /* @__PURE__ */ React28.createElement(
|
|
963
1660
|
"div",
|
|
964
1661
|
__spreadValues({
|
|
965
1662
|
ref,
|
|
@@ -971,9 +1668,9 @@ var CardTitle = React25.forwardRef((_a, ref) => {
|
|
|
971
1668
|
);
|
|
972
1669
|
});
|
|
973
1670
|
CardTitle.displayName = "CardTitle";
|
|
974
|
-
var CardDescription =
|
|
1671
|
+
var CardDescription = React28.forwardRef((_a, ref) => {
|
|
975
1672
|
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
976
|
-
return /* @__PURE__ */
|
|
1673
|
+
return /* @__PURE__ */ React28.createElement(
|
|
977
1674
|
"div",
|
|
978
1675
|
__spreadValues({
|
|
979
1676
|
ref,
|
|
@@ -982,14 +1679,14 @@ var CardDescription = React25.forwardRef((_a, ref) => {
|
|
|
982
1679
|
);
|
|
983
1680
|
});
|
|
984
1681
|
CardDescription.displayName = "CardDescription";
|
|
985
|
-
var CardContent =
|
|
1682
|
+
var CardContent = React28.forwardRef((_a, ref) => {
|
|
986
1683
|
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
987
|
-
return /* @__PURE__ */
|
|
1684
|
+
return /* @__PURE__ */ React28.createElement("div", __spreadValues({ ref, className: cn("p-6 pt-0", className) }, props));
|
|
988
1685
|
});
|
|
989
1686
|
CardContent.displayName = "CardContent";
|
|
990
|
-
var CardFooter =
|
|
1687
|
+
var CardFooter = React28.forwardRef((_a, ref) => {
|
|
991
1688
|
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
992
|
-
return /* @__PURE__ */
|
|
1689
|
+
return /* @__PURE__ */ React28.createElement(
|
|
993
1690
|
"div",
|
|
994
1691
|
__spreadValues({
|
|
995
1692
|
ref,
|
|
@@ -1040,7 +1737,7 @@ var MarkdownRenderer = ({
|
|
|
1040
1737
|
const processedContent = processContentForVideos(content);
|
|
1041
1738
|
return (
|
|
1042
1739
|
// Using Tailwind Typography for beautiful default styling of markdown content
|
|
1043
|
-
/* @__PURE__ */
|
|
1740
|
+
/* @__PURE__ */ React28__default.createElement("div", { className: `prose dark:prose-invert max-w-none ${className}` }, /* @__PURE__ */ React28__default.createElement(
|
|
1044
1741
|
ReactMarkdown,
|
|
1045
1742
|
{
|
|
1046
1743
|
remarkPlugins: [remarkGfm, remarkMath],
|
|
@@ -1053,7 +1750,7 @@ var MarkdownRenderer = ({
|
|
|
1053
1750
|
const { platform, id } = getVideoId(src);
|
|
1054
1751
|
if (platform && id) {
|
|
1055
1752
|
const videoSrc = platform === "youtube" ? `https://www.youtube.com/embed/${id}` : `https://player.vimeo.com/video/${id}`;
|
|
1056
|
-
return /* @__PURE__ */
|
|
1753
|
+
return /* @__PURE__ */ React28__default.createElement("div", { className: "aspect-w-16 aspect-h-9 my-4" }, /* @__PURE__ */ React28__default.createElement(
|
|
1057
1754
|
"iframe",
|
|
1058
1755
|
{
|
|
1059
1756
|
src: videoSrc,
|
|
@@ -1066,7 +1763,7 @@ var MarkdownRenderer = ({
|
|
|
1066
1763
|
}
|
|
1067
1764
|
return (
|
|
1068
1765
|
// eslint-disable-next-line @next/next/no-img-element
|
|
1069
|
-
/* @__PURE__ */
|
|
1766
|
+
/* @__PURE__ */ React28__default.createElement(
|
|
1070
1767
|
"img",
|
|
1071
1768
|
__spreadProps(__spreadValues({}, props), {
|
|
1072
1769
|
style: {
|
|
@@ -1083,12 +1780,12 @@ var MarkdownRenderer = ({
|
|
|
1083
1780
|
// Override the default table to add responsive wrapper
|
|
1084
1781
|
table: (_c) => {
|
|
1085
1782
|
var _d = _c, { node } = _d, props = __objRest(_d, ["node"]);
|
|
1086
|
-
return /* @__PURE__ */
|
|
1783
|
+
return /* @__PURE__ */ React28__default.createElement("div", { className: "overflow-x-auto" }, /* @__PURE__ */ React28__default.createElement("table", __spreadProps(__spreadValues({}, props), { className: "my-4 w-full text-sm" })));
|
|
1087
1784
|
},
|
|
1088
1785
|
// Override default blockquote for better styling
|
|
1089
1786
|
blockquote: (_e) => {
|
|
1090
1787
|
var _f = _e, { node } = _f, props = __objRest(_f, ["node"]);
|
|
1091
|
-
return /* @__PURE__ */
|
|
1788
|
+
return /* @__PURE__ */ React28__default.createElement(
|
|
1092
1789
|
"blockquote",
|
|
1093
1790
|
__spreadProps(__spreadValues({}, props), {
|
|
1094
1791
|
className: "border-l-4 border-primary bg-muted/50 p-4 my-4 italic"
|
|
@@ -1113,7 +1810,7 @@ var MultipleChoiceQuestionUI = ({
|
|
|
1113
1810
|
const handleSelection = (value) => {
|
|
1114
1811
|
onAnswerChange(value);
|
|
1115
1812
|
};
|
|
1116
|
-
return /* @__PURE__ */
|
|
1813
|
+
return /* @__PURE__ */ React28__default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React28__default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React28__default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, /* @__PURE__ */ React28__default.createElement(MarkdownRenderer, { content: prompt })), points && /* @__PURE__ */ React28__default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", points)), /* @__PURE__ */ React28__default.createElement(CardContent, { className: "p-0" }, /* @__PURE__ */ React28__default.createElement(
|
|
1117
1814
|
RadioGroup,
|
|
1118
1815
|
{
|
|
1119
1816
|
value: userAnswer || void 0,
|
|
@@ -1136,17 +1833,17 @@ var MultipleChoiceQuestionUI = ({
|
|
|
1136
1833
|
} else {
|
|
1137
1834
|
itemClassName += isSelected ? " border-primary bg-primary/10" : " border-muted";
|
|
1138
1835
|
}
|
|
1139
|
-
return /* @__PURE__ */
|
|
1836
|
+
return /* @__PURE__ */ React28__default.createElement(
|
|
1140
1837
|
Label,
|
|
1141
1838
|
{
|
|
1142
1839
|
key: option.id,
|
|
1143
1840
|
htmlFor: option.id,
|
|
1144
1841
|
className: itemClassName
|
|
1145
1842
|
},
|
|
1146
|
-
/* @__PURE__ */
|
|
1843
|
+
/* @__PURE__ */ React28__default.createElement("div", { className: "flex items-center" }, /* @__PURE__ */ React28__default.createElement(RadioGroupItem, { value: option.id, id: option.id, className: "mr-3" }), /* @__PURE__ */ React28__default.createElement("div", { className: "text-base flex-1" }, /* @__PURE__ */ React28__default.createElement(MarkdownRenderer, { content: option.text })))
|
|
1147
1844
|
);
|
|
1148
1845
|
})
|
|
1149
|
-
), showCorrectAnswer && explanation && /* @__PURE__ */
|
|
1846
|
+
), showCorrectAnswer && explanation && /* @__PURE__ */ React28__default.createElement("div", { className: "mt-4 p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React28__default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Explanation:"), /* @__PURE__ */ React28__default.createElement(MarkdownRenderer, { content: explanation, className: "text-sm text-accent-foreground/80" }))));
|
|
1150
1847
|
};
|
|
1151
1848
|
var TrueFalseQuestionUI = ({
|
|
1152
1849
|
question,
|
|
@@ -1162,7 +1859,7 @@ var TrueFalseQuestionUI = ({
|
|
|
1162
1859
|
const handleSelection = (value) => {
|
|
1163
1860
|
onAnswerChange(value);
|
|
1164
1861
|
};
|
|
1165
|
-
return /* @__PURE__ */
|
|
1862
|
+
return /* @__PURE__ */ React28__default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React28__default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React28__default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, /* @__PURE__ */ React28__default.createElement(MarkdownRenderer, { content: prompt })), points && /* @__PURE__ */ React28__default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", points)), /* @__PURE__ */ React28__default.createElement(CardContent, { className: "p-0" }, /* @__PURE__ */ React28__default.createElement(
|
|
1166
1863
|
RadioGroup,
|
|
1167
1864
|
{
|
|
1168
1865
|
value: userAnswer || void 0,
|
|
@@ -1185,21 +1882,21 @@ var TrueFalseQuestionUI = ({
|
|
|
1185
1882
|
} else {
|
|
1186
1883
|
itemClassName += isSelected ? " border-primary bg-primary/10" : " border-muted";
|
|
1187
1884
|
}
|
|
1188
|
-
return /* @__PURE__ */
|
|
1885
|
+
return /* @__PURE__ */ React28__default.createElement(
|
|
1189
1886
|
Label,
|
|
1190
1887
|
{
|
|
1191
1888
|
key: option.id,
|
|
1192
1889
|
htmlFor: option.id,
|
|
1193
1890
|
className: itemClassName
|
|
1194
1891
|
},
|
|
1195
|
-
/* @__PURE__ */
|
|
1892
|
+
/* @__PURE__ */ React28__default.createElement("div", { className: "flex items-center" }, /* @__PURE__ */ React28__default.createElement(RadioGroupItem, { value: option.value, id: option.id, className: "mr-3" }), /* @__PURE__ */ React28__default.createElement("span", { className: "text-base" }, option.label))
|
|
1196
1893
|
);
|
|
1197
1894
|
})
|
|
1198
|
-
), showCorrectAnswer && explanation && /* @__PURE__ */
|
|
1895
|
+
), showCorrectAnswer && explanation && /* @__PURE__ */ React28__default.createElement("div", { className: "mt-4 p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React28__default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Explanation:"), /* @__PURE__ */ React28__default.createElement(MarkdownRenderer, { content: explanation, className: "text-sm text-accent-foreground/80" }))));
|
|
1199
1896
|
};
|
|
1200
|
-
var Checkbox =
|
|
1897
|
+
var Checkbox = React28.forwardRef((_a, ref) => {
|
|
1201
1898
|
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
1202
|
-
return /* @__PURE__ */
|
|
1899
|
+
return /* @__PURE__ */ React28.createElement(
|
|
1203
1900
|
CheckboxPrimitive.Root,
|
|
1204
1901
|
__spreadValues({
|
|
1205
1902
|
ref,
|
|
@@ -1208,12 +1905,12 @@ var Checkbox = React25.forwardRef((_a, ref) => {
|
|
|
1208
1905
|
className
|
|
1209
1906
|
)
|
|
1210
1907
|
}, props),
|
|
1211
|
-
/* @__PURE__ */
|
|
1908
|
+
/* @__PURE__ */ React28.createElement(
|
|
1212
1909
|
CheckboxPrimitive.Indicator,
|
|
1213
1910
|
{
|
|
1214
1911
|
className: cn("flex items-center justify-center text-current")
|
|
1215
1912
|
},
|
|
1216
|
-
/* @__PURE__ */
|
|
1913
|
+
/* @__PURE__ */ React28.createElement(Check, { className: "h-4 w-4" })
|
|
1217
1914
|
)
|
|
1218
1915
|
);
|
|
1219
1916
|
});
|
|
@@ -1241,7 +1938,7 @@ var MultipleResponseQuestionUI = ({
|
|
|
1241
1938
|
}
|
|
1242
1939
|
onAnswerChange(newAnswers.length > 0 ? newAnswers : null);
|
|
1243
1940
|
};
|
|
1244
|
-
return /* @__PURE__ */
|
|
1941
|
+
return /* @__PURE__ */ React28__default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React28__default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React28__default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, /* @__PURE__ */ React28__default.createElement(MarkdownRenderer, { content: prompt })), points && /* @__PURE__ */ React28__default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", points)), /* @__PURE__ */ React28__default.createElement(CardContent, { className: "p-0" }, /* @__PURE__ */ React28__default.createElement("div", { className: "space-y-3", role: "group", "aria-labelledby": `question-prompt-${questionId}` }, options.map((option) => {
|
|
1245
1942
|
const isSelected = Array.isArray(userAnswer) && userAnswer.includes(option.id);
|
|
1246
1943
|
const isCorrectOption = correctAnswerIds.includes(option.id);
|
|
1247
1944
|
let itemClassName = "p-4 rounded-lg border-2 transition-all cursor-pointer hover:border-primary flex items-center";
|
|
@@ -1254,14 +1951,14 @@ var MultipleResponseQuestionUI = ({
|
|
|
1254
1951
|
} else {
|
|
1255
1952
|
itemClassName += isSelected ? " border-primary bg-primary/10" : " border-muted";
|
|
1256
1953
|
}
|
|
1257
|
-
return /* @__PURE__ */
|
|
1954
|
+
return /* @__PURE__ */ React28__default.createElement(
|
|
1258
1955
|
Label,
|
|
1259
1956
|
{
|
|
1260
1957
|
key: option.id,
|
|
1261
1958
|
htmlFor: option.id,
|
|
1262
1959
|
className: itemClassName
|
|
1263
1960
|
},
|
|
1264
|
-
/* @__PURE__ */
|
|
1961
|
+
/* @__PURE__ */ React28__default.createElement(
|
|
1265
1962
|
Checkbox,
|
|
1266
1963
|
{
|
|
1267
1964
|
id: option.id,
|
|
@@ -1271,14 +1968,14 @@ var MultipleResponseQuestionUI = ({
|
|
|
1271
1968
|
"aria-label": option.text.replace(/<[^>]*>?/gm, "")
|
|
1272
1969
|
}
|
|
1273
1970
|
),
|
|
1274
|
-
/* @__PURE__ */
|
|
1971
|
+
/* @__PURE__ */ React28__default.createElement("div", { className: "text-base flex-1" }, /* @__PURE__ */ React28__default.createElement(MarkdownRenderer, { content: option.text }))
|
|
1275
1972
|
);
|
|
1276
|
-
})), showCorrectAnswer && explanation && /* @__PURE__ */
|
|
1973
|
+
})), showCorrectAnswer && explanation && /* @__PURE__ */ React28__default.createElement("div", { className: "mt-4 p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React28__default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Explanation:"), /* @__PURE__ */ React28__default.createElement(MarkdownRenderer, { content: explanation, className: "text-sm text-accent-foreground/80" }))));
|
|
1277
1974
|
};
|
|
1278
|
-
var Input =
|
|
1975
|
+
var Input = React28.forwardRef(
|
|
1279
1976
|
(_a, ref) => {
|
|
1280
1977
|
var _b = _a, { className, type } = _b, props = __objRest(_b, ["className", "type"]);
|
|
1281
|
-
return /* @__PURE__ */
|
|
1978
|
+
return /* @__PURE__ */ React28.createElement(
|
|
1282
1979
|
"input",
|
|
1283
1980
|
__spreadValues({
|
|
1284
1981
|
type,
|
|
@@ -1312,7 +2009,7 @@ var ShortAnswerQuestionUI = ({
|
|
|
1312
2009
|
(accAns) => isCaseSensitive ? accAns.trim() === userAnswerTrimmed : accAns.trim().toLowerCase() === userAnswerTrimmed.toLowerCase()
|
|
1313
2010
|
);
|
|
1314
2011
|
}
|
|
1315
|
-
return /* @__PURE__ */
|
|
2012
|
+
return /* @__PURE__ */ React28__default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React28__default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React28__default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, /* @__PURE__ */ React28__default.createElement(MarkdownRenderer, { content: prompt })), points && /* @__PURE__ */ React28__default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", points)), /* @__PURE__ */ React28__default.createElement(CardContent, { className: "p-0" }, /* @__PURE__ */ React28__default.createElement(Label, { htmlFor: `short-answer-input-${questionId}`, className: "sr-only" }, "Your Answer"), /* @__PURE__ */ React28__default.createElement(
|
|
1316
2013
|
Input,
|
|
1317
2014
|
{
|
|
1318
2015
|
id: `short-answer-input-${questionId}`,
|
|
@@ -1325,7 +2022,7 @@ var ShortAnswerQuestionUI = ({
|
|
|
1325
2022
|
${showCorrectAnswer && userAnswer ? isActuallyCorrect ? "border-green-500 focus-visible:ring-green-500" : "border-destructive focus-visible:ring-destructive" : "border-input"}
|
|
1326
2023
|
`
|
|
1327
2024
|
}
|
|
1328
|
-
), showCorrectAnswer && /* @__PURE__ */
|
|
2025
|
+
), showCorrectAnswer && /* @__PURE__ */ React28__default.createElement("div", { className: "mt-4 space-y-2" }, userAnswer && !isActuallyCorrect && /* @__PURE__ */ React28__default.createElement("p", { className: "text-sm text-destructive" }, "Your answer was marked incorrect."), /* @__PURE__ */ React28__default.createElement("div", { className: "p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React28__default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Accepted Answers:"), /* @__PURE__ */ React28__default.createElement("ul", { className: "list-disc list-inside text-sm text-accent-foreground/80" }, acceptedAnswers.map((ans, idx) => /* @__PURE__ */ React28__default.createElement("li", { key: idx }, ans))), isCaseSensitive && /* @__PURE__ */ React28__default.createElement("p", { className: "text-xs text-muted-foreground mt-1" }, "(Case-sensitive)")), explanation && /* @__PURE__ */ React28__default.createElement("div", { id: `explanation-${questionId}`, className: "mt-2 p-3 bg-muted/30 border border-muted rounded-md" }, /* @__PURE__ */ React28__default.createElement("p", { className: "text-sm font-semibold" }, "Explanation:"), /* @__PURE__ */ React28__default.createElement(MarkdownRenderer, { content: explanation, className: "text-sm text-muted-foreground" })))));
|
|
1329
2026
|
};
|
|
1330
2027
|
var NumericQuestionUI = ({
|
|
1331
2028
|
question,
|
|
@@ -1348,7 +2045,7 @@ var NumericQuestionUI = ({
|
|
|
1348
2045
|
isActuallyCorrect = tolerance !== void 0 && tolerance !== null ? Math.abs(userAnswerNum - correctAnswerValue) <= tolerance : userAnswerNum === correctAnswerValue;
|
|
1349
2046
|
}
|
|
1350
2047
|
}
|
|
1351
|
-
return /* @__PURE__ */
|
|
2048
|
+
return /* @__PURE__ */ React28__default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React28__default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React28__default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, /* @__PURE__ */ React28__default.createElement(MarkdownRenderer, { content: prompt })), points && /* @__PURE__ */ React28__default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", points)), /* @__PURE__ */ React28__default.createElement(CardContent, { className: "p-0" }, /* @__PURE__ */ React28__default.createElement(Label, { htmlFor: `numeric-input-${questionId}`, className: "sr-only" }, "Your Answer"), /* @__PURE__ */ React28__default.createElement(
|
|
1352
2049
|
Input,
|
|
1353
2050
|
{
|
|
1354
2051
|
id: `numeric-input-${questionId}`,
|
|
@@ -1363,7 +2060,7 @@ var NumericQuestionUI = ({
|
|
|
1363
2060
|
${showCorrectAnswer && userAnswer !== null && userAnswer !== "" ? isActuallyCorrect ? "border-green-500 focus-visible:ring-green-500" : "border-destructive focus-visible:ring-destructive" : "border-input"}
|
|
1364
2061
|
`
|
|
1365
2062
|
}
|
|
1366
|
-
), showCorrectAnswer && /* @__PURE__ */
|
|
2063
|
+
), showCorrectAnswer && /* @__PURE__ */ React28__default.createElement("div", { className: "mt-4 space-y-2" }, userAnswer !== null && userAnswer !== "" && !isActuallyCorrect && /* @__PURE__ */ React28__default.createElement("p", { className: "text-sm text-destructive" }, "Your answer was marked incorrect."), /* @__PURE__ */ React28__default.createElement("div", { className: "p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React28__default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Correct Answer:"), /* @__PURE__ */ React28__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__default.createElement("div", { id: `explanation-${questionId}`, className: "mt-2 p-3 bg-muted/30 border border-muted rounded-md" }, /* @__PURE__ */ React28__default.createElement("p", { className: "text-sm font-semibold" }, "Explanation:"), /* @__PURE__ */ React28__default.createElement(MarkdownRenderer, { content: explanation, className: "text-sm text-muted-foreground" })))));
|
|
1367
2064
|
};
|
|
1368
2065
|
var FillInTheBlanksQuestionUI = ({
|
|
1369
2066
|
question,
|
|
@@ -1403,10 +2100,10 @@ var FillInTheBlanksQuestionUI = ({
|
|
|
1403
2100
|
(accVal) => caseSensitive ? accVal.trim() === userAnswerForBlank : accVal.trim().toLowerCase() === userAnswerForBlank.toLowerCase()
|
|
1404
2101
|
);
|
|
1405
2102
|
};
|
|
1406
|
-
return /* @__PURE__ */
|
|
2103
|
+
return /* @__PURE__ */ React28__default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React28__default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React28__default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, /* @__PURE__ */ React28__default.createElement(MarkdownRenderer, { content: prompt })), points && /* @__PURE__ */ React28__default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", points)), /* @__PURE__ */ React28__default.createElement(CardContent, { className: "p-0" }, /* @__PURE__ */ React28__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) => {
|
|
1407
2104
|
var _a;
|
|
1408
2105
|
if (segment.type === "text") {
|
|
1409
|
-
return /* @__PURE__ */
|
|
2106
|
+
return /* @__PURE__ */ React28__default.createElement(
|
|
1410
2107
|
MarkdownRenderer,
|
|
1411
2108
|
{
|
|
1412
2109
|
key: `text-${index}`,
|
|
@@ -1424,7 +2121,7 @@ var FillInTheBlanksQuestionUI = ({
|
|
|
1424
2121
|
} else {
|
|
1425
2122
|
inputClassName += " border-input";
|
|
1426
2123
|
}
|
|
1427
|
-
return /* @__PURE__ */
|
|
2124
|
+
return /* @__PURE__ */ React28__default.createElement(
|
|
1428
2125
|
Input,
|
|
1429
2126
|
{
|
|
1430
2127
|
key: blankId,
|
|
@@ -1440,11 +2137,11 @@ var FillInTheBlanksQuestionUI = ({
|
|
|
1440
2137
|
);
|
|
1441
2138
|
}
|
|
1442
2139
|
return null;
|
|
1443
|
-
})), showCorrectAnswer && explanation && /* @__PURE__ */
|
|
2140
|
+
})), showCorrectAnswer && explanation && /* @__PURE__ */ React28__default.createElement("div", { className: "mt-6 p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React28__default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Gi\u1EA3i th\xEDch chung:"), /* @__PURE__ */ React28__default.createElement(MarkdownRenderer, { content: explanation, className: "text-sm text-accent-foreground/80" })), showCorrectAnswer && /* @__PURE__ */ React28__default.createElement("div", { className: "mt-4 space-y-3" }, correctAnswersMap.map((ansDef) => {
|
|
1444
2141
|
var _a;
|
|
1445
2142
|
const isBlankCorrect = getCorrectnessForBlank(ansDef.blankId);
|
|
1446
2143
|
const userAnswerDisplay = userInputs[ansDef.blankId] || "Ch\u01B0a tr\u1EA3 l\u1EDDi";
|
|
1447
|
-
return /* @__PURE__ */
|
|
2144
|
+
return /* @__PURE__ */ React28__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__default.createElement("p", { className: "text-sm" }, /* @__PURE__ */ React28__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__default.createElement("p", { className: "text-xs" }, "\u0110\xE1p \xE1n ch\u1EA5p nh\u1EADn: ", ansDef.acceptedValues.join(", ")));
|
|
1448
2145
|
}))));
|
|
1449
2146
|
};
|
|
1450
2147
|
var buttonVariants = cva(
|
|
@@ -1472,11 +2169,11 @@ var buttonVariants = cva(
|
|
|
1472
2169
|
}
|
|
1473
2170
|
}
|
|
1474
2171
|
);
|
|
1475
|
-
var Button =
|
|
2172
|
+
var Button = React28.forwardRef(
|
|
1476
2173
|
(_a, ref) => {
|
|
1477
2174
|
var _b = _a, { className, variant, size, asChild = false } = _b, props = __objRest(_b, ["className", "variant", "size", "asChild"]);
|
|
1478
2175
|
const Comp = asChild ? Slot : "button";
|
|
1479
|
-
return /* @__PURE__ */
|
|
2176
|
+
return /* @__PURE__ */ React28.createElement(
|
|
1480
2177
|
Comp,
|
|
1481
2178
|
__spreadValues({
|
|
1482
2179
|
className: cn(buttonVariants({ variant, size, className })),
|
|
@@ -1536,37 +2233,37 @@ var SequenceQuestionUI = ({
|
|
|
1536
2233
|
if (!showCorrectAnswer || !Array.isArray(userAnswer) || userAnswer.length <= index) return null;
|
|
1537
2234
|
const userItemId = userAnswer[index];
|
|
1538
2235
|
const correctItemId = correctOrder[index];
|
|
1539
|
-
return userItemId === correctItemId ? /* @__PURE__ */
|
|
2236
|
+
return userItemId === correctItemId ? /* @__PURE__ */ React28__default.createElement(CheckCircle, { className: "h-4 w-4 text-green-500 ml-2 flex-shrink-0" }) : /* @__PURE__ */ React28__default.createElement(XCircle, { className: "h-4 w-4 text-destructive ml-2 flex-shrink-0" });
|
|
1540
2237
|
};
|
|
1541
|
-
return /* @__PURE__ */
|
|
2238
|
+
return /* @__PURE__ */ React28__default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React28__default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React28__default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, /* @__PURE__ */ React28__default.createElement(MarkdownRenderer, { content: prompt })), points && /* @__PURE__ */ React28__default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", points)), /* @__PURE__ */ React28__default.createElement(CardContent, { className: "p-0 space-y-6" }, /* @__PURE__ */ React28__default.createElement("div", { className: "space-y-3" }, /* @__PURE__ */ React28__default.createElement(Label, { className: "font-semibold" }, "S\u1EAFp x\u1EBFp c\xE1c m\u1EE5c sau theo \u0111\xFAng th\u1EE9 t\u1EF1:"), /* @__PURE__ */ React28__default.createElement("div", { className: "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-2" }, availableItems.map((item) => /* @__PURE__ */ React28__default.createElement(
|
|
1542
2239
|
Button,
|
|
1543
2240
|
{
|
|
1544
2241
|
key: item.id,
|
|
1545
2242
|
variant: "outline",
|
|
1546
2243
|
onClick: () => handleSelectItem(item),
|
|
1547
|
-
className: "justify-start text-left h-auto py-2 px-3",
|
|
2244
|
+
className: "justify-start text-left h-auto py-2 px-3 whitespace-normal",
|
|
1548
2245
|
disabled: showCorrectAnswer
|
|
1549
2246
|
},
|
|
1550
|
-
/* @__PURE__ */
|
|
1551
|
-
))), availableItems.length === 0 && selectedSequence.length > 0 && /* @__PURE__ */
|
|
2247
|
+
/* @__PURE__ */ React28__default.createElement(MarkdownRenderer, { content: item.content })
|
|
2248
|
+
))), availableItems.length === 0 && selectedSequence.length > 0 && /* @__PURE__ */ React28__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__default.createElement("div", { className: "space-y-2" }, /* @__PURE__ */ React28__default.createElement("div", { className: "flex justify-between items-center" }, /* @__PURE__ */ React28__default.createElement(Label, { className: "font-semibold" }, "Th\u1EE9 t\u1EF1 b\u1EA1n \u0111\xE3 ch\u1ECDn:"), /* @__PURE__ */ React28__default.createElement(Button, { variant: "ghost", size: "sm", onClick: handleResetSequence, disabled: showCorrectAnswer || selectedSequence.length === 0 }, /* @__PURE__ */ React28__default.createElement(RotateCcw, { className: "mr-2 h-3.5 w-3.5" }), " \u0110\u1EB7t l\u1EA1i")), selectedSequence.length === 0 ? /* @__PURE__ */ React28__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__default.createElement("ul", { className: "space-y-2" }, selectedSequence.map((item, index) => /* @__PURE__ */ React28__default.createElement(
|
|
1552
2249
|
"li",
|
|
1553
2250
|
{
|
|
1554
2251
|
key: item.id,
|
|
1555
2252
|
onClick: () => handleRemoveFromSequence(item, index),
|
|
1556
|
-
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`
|
|
2253
|
+
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`
|
|
1557
2254
|
},
|
|
1558
|
-
/* @__PURE__ */
|
|
1559
|
-
showCorrectAnswer ? getFeedbackIcon(index) : /* @__PURE__ */
|
|
1560
|
-
)))), showCorrectAnswer && /* @__PURE__ */
|
|
2255
|
+
/* @__PURE__ */ React28__default.createElement("div", { className: "flex-grow flex items-center" }, /* @__PURE__ */ React28__default.createElement("span", { className: "font-semibold mr-2" }, index + 1, "."), /* @__PURE__ */ React28__default.createElement(MarkdownRenderer, { content: item.content })),
|
|
2256
|
+
showCorrectAnswer ? getFeedbackIcon(index) : /* @__PURE__ */ React28__default.createElement(XCircle, { className: "h-4 w-4 text-muted-foreground hover:text-destructive flex-shrink-0" })
|
|
2257
|
+
)))), showCorrectAnswer && /* @__PURE__ */ React28__default.createElement("div", { className: "mt-4 space-y-2" }, /* @__PURE__ */ React28__default.createElement("div", { className: "p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React28__default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Th\u1EE9 t\u1EF1 \u0111\xFAng:"), /* @__PURE__ */ React28__default.createElement("ol", { className: "list-decimal list-inside text-sm text-accent-foreground/80 space-y-1 mt-1" }, correctOrder.map((itemId) => {
|
|
1561
2258
|
const item = items.find((i) => i.id === itemId);
|
|
1562
|
-
return /* @__PURE__ */
|
|
1563
|
-
}))), explanation && /* @__PURE__ */
|
|
2259
|
+
return /* @__PURE__ */ React28__default.createElement("li", { key: itemId }, /* @__PURE__ */ React28__default.createElement(MarkdownRenderer, { content: item ? item.content : "Kh\xF4ng t\xECm th\u1EA5y m\u1EE5c" }));
|
|
2260
|
+
}))), explanation && /* @__PURE__ */ React28__default.createElement("div", { className: "mt-2 p-3 bg-muted/30 border border-muted rounded-md" }, /* @__PURE__ */ React28__default.createElement("p", { className: "text-sm font-semibold" }, "Gi\u1EA3i th\xEDch:"), /* @__PURE__ */ React28__default.createElement(MarkdownRenderer, { content: explanation, className: "text-sm text-muted-foreground" })))));
|
|
1564
2261
|
};
|
|
1565
2262
|
var Select = SelectPrimitive.Root;
|
|
1566
2263
|
var SelectValue = SelectPrimitive.Value;
|
|
1567
|
-
var SelectTrigger =
|
|
2264
|
+
var SelectTrigger = React28.forwardRef((_a, ref) => {
|
|
1568
2265
|
var _b = _a, { className, children } = _b, props = __objRest(_b, ["className", "children"]);
|
|
1569
|
-
return /* @__PURE__ */
|
|
2266
|
+
return /* @__PURE__ */ React28.createElement(
|
|
1570
2267
|
SelectPrimitive.Trigger,
|
|
1571
2268
|
__spreadValues({
|
|
1572
2269
|
ref,
|
|
@@ -1576,13 +2273,13 @@ var SelectTrigger = React25.forwardRef((_a, ref) => {
|
|
|
1576
2273
|
)
|
|
1577
2274
|
}, props),
|
|
1578
2275
|
children,
|
|
1579
|
-
/* @__PURE__ */
|
|
2276
|
+
/* @__PURE__ */ React28.createElement(SelectPrimitive.Icon, { asChild: true }, /* @__PURE__ */ React28.createElement(ChevronDown, { className: "h-4 w-4 opacity-50" }))
|
|
1580
2277
|
);
|
|
1581
2278
|
});
|
|
1582
2279
|
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
|
1583
|
-
var SelectScrollUpButton =
|
|
2280
|
+
var SelectScrollUpButton = React28.forwardRef((_a, ref) => {
|
|
1584
2281
|
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
1585
|
-
return /* @__PURE__ */
|
|
2282
|
+
return /* @__PURE__ */ React28.createElement(
|
|
1586
2283
|
SelectPrimitive.ScrollUpButton,
|
|
1587
2284
|
__spreadValues({
|
|
1588
2285
|
ref,
|
|
@@ -1591,13 +2288,13 @@ var SelectScrollUpButton = React25.forwardRef((_a, ref) => {
|
|
|
1591
2288
|
className
|
|
1592
2289
|
)
|
|
1593
2290
|
}, props),
|
|
1594
|
-
/* @__PURE__ */
|
|
2291
|
+
/* @__PURE__ */ React28.createElement(ChevronUp, { className: "h-4 w-4" })
|
|
1595
2292
|
);
|
|
1596
2293
|
});
|
|
1597
2294
|
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
|
1598
|
-
var SelectScrollDownButton =
|
|
2295
|
+
var SelectScrollDownButton = React28.forwardRef((_a, ref) => {
|
|
1599
2296
|
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
1600
|
-
return /* @__PURE__ */
|
|
2297
|
+
return /* @__PURE__ */ React28.createElement(
|
|
1601
2298
|
SelectPrimitive.ScrollDownButton,
|
|
1602
2299
|
__spreadValues({
|
|
1603
2300
|
ref,
|
|
@@ -1606,13 +2303,13 @@ var SelectScrollDownButton = React25.forwardRef((_a, ref) => {
|
|
|
1606
2303
|
className
|
|
1607
2304
|
)
|
|
1608
2305
|
}, props),
|
|
1609
|
-
/* @__PURE__ */
|
|
2306
|
+
/* @__PURE__ */ React28.createElement(ChevronDown, { className: "h-4 w-4" })
|
|
1610
2307
|
);
|
|
1611
2308
|
});
|
|
1612
2309
|
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
|
|
1613
|
-
var SelectContent =
|
|
2310
|
+
var SelectContent = React28.forwardRef((_a, ref) => {
|
|
1614
2311
|
var _b = _a, { className, children, position = "popper" } = _b, props = __objRest(_b, ["className", "children", "position"]);
|
|
1615
|
-
return /* @__PURE__ */
|
|
2312
|
+
return /* @__PURE__ */ React28.createElement(SelectPrimitive.Portal, null, /* @__PURE__ */ React28.createElement(
|
|
1616
2313
|
SelectPrimitive.Content,
|
|
1617
2314
|
__spreadValues({
|
|
1618
2315
|
ref,
|
|
@@ -1623,8 +2320,8 @@ var SelectContent = React25.forwardRef((_a, ref) => {
|
|
|
1623
2320
|
),
|
|
1624
2321
|
position
|
|
1625
2322
|
}, props),
|
|
1626
|
-
/* @__PURE__ */
|
|
1627
|
-
/* @__PURE__ */
|
|
2323
|
+
/* @__PURE__ */ React28.createElement(SelectScrollUpButton, null),
|
|
2324
|
+
/* @__PURE__ */ React28.createElement(
|
|
1628
2325
|
SelectPrimitive.Viewport,
|
|
1629
2326
|
{
|
|
1630
2327
|
className: cn(
|
|
@@ -1634,13 +2331,13 @@ var SelectContent = React25.forwardRef((_a, ref) => {
|
|
|
1634
2331
|
},
|
|
1635
2332
|
children
|
|
1636
2333
|
),
|
|
1637
|
-
/* @__PURE__ */
|
|
2334
|
+
/* @__PURE__ */ React28.createElement(SelectScrollDownButton, null)
|
|
1638
2335
|
));
|
|
1639
2336
|
});
|
|
1640
2337
|
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
|
1641
|
-
var SelectLabel =
|
|
2338
|
+
var SelectLabel = React28.forwardRef((_a, ref) => {
|
|
1642
2339
|
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
1643
|
-
return /* @__PURE__ */
|
|
2340
|
+
return /* @__PURE__ */ React28.createElement(
|
|
1644
2341
|
SelectPrimitive.Label,
|
|
1645
2342
|
__spreadValues({
|
|
1646
2343
|
ref,
|
|
@@ -1649,9 +2346,9 @@ var SelectLabel = React25.forwardRef((_a, ref) => {
|
|
|
1649
2346
|
);
|
|
1650
2347
|
});
|
|
1651
2348
|
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
|
1652
|
-
var SelectItem =
|
|
2349
|
+
var SelectItem = React28.forwardRef((_a, ref) => {
|
|
1653
2350
|
var _b = _a, { className, children } = _b, props = __objRest(_b, ["className", "children"]);
|
|
1654
|
-
return /* @__PURE__ */
|
|
2351
|
+
return /* @__PURE__ */ React28.createElement(
|
|
1655
2352
|
SelectPrimitive.Item,
|
|
1656
2353
|
__spreadValues({
|
|
1657
2354
|
ref,
|
|
@@ -1660,14 +2357,14 @@ var SelectItem = React25.forwardRef((_a, ref) => {
|
|
|
1660
2357
|
className
|
|
1661
2358
|
)
|
|
1662
2359
|
}, props),
|
|
1663
|
-
/* @__PURE__ */
|
|
1664
|
-
/* @__PURE__ */
|
|
2360
|
+
/* @__PURE__ */ React28.createElement("span", { className: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center" }, /* @__PURE__ */ React28.createElement(SelectPrimitive.ItemIndicator, null, /* @__PURE__ */ React28.createElement(Check, { className: "h-4 w-4" }))),
|
|
2361
|
+
/* @__PURE__ */ React28.createElement(SelectPrimitive.ItemText, null, children)
|
|
1665
2362
|
);
|
|
1666
2363
|
});
|
|
1667
2364
|
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
|
1668
|
-
var SelectSeparator =
|
|
2365
|
+
var SelectSeparator = React28.forwardRef((_a, ref) => {
|
|
1669
2366
|
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
1670
|
-
return /* @__PURE__ */
|
|
2367
|
+
return /* @__PURE__ */ React28.createElement(
|
|
1671
2368
|
SelectPrimitive.Separator,
|
|
1672
2369
|
__spreadValues({
|
|
1673
2370
|
ref,
|
|
@@ -1720,7 +2417,7 @@ var MatchingQuestionUI = ({
|
|
|
1720
2417
|
}
|
|
1721
2418
|
return htmlString.replace(/<[^>]*>?/gm, "");
|
|
1722
2419
|
};
|
|
1723
|
-
return /* @__PURE__ */
|
|
2420
|
+
return /* @__PURE__ */ React28__default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React28__default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React28__default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, /* @__PURE__ */ React28__default.createElement(MarkdownRenderer, { content: prompt })), points && /* @__PURE__ */ React28__default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", points)), /* @__PURE__ */ React28__default.createElement(CardContent, { className: "p-0 space-y-4" }, /* @__PURE__ */ React28__default.createElement("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-4" }, prompts.map((promptItem) => {
|
|
1724
2421
|
var _a;
|
|
1725
2422
|
const selectedOptionId = currentAnswers[promptItem.id] || "";
|
|
1726
2423
|
const correctOptionId = getCorrectOptionIdForPrompt(promptItem.id);
|
|
@@ -1729,21 +2426,21 @@ var MatchingQuestionUI = ({
|
|
|
1729
2426
|
if (showCorrectAnswer && selectedOptionId) {
|
|
1730
2427
|
borderColor = isSelectionCorrect ? "border-green-500" : "border-destructive";
|
|
1731
2428
|
}
|
|
1732
|
-
return /* @__PURE__ */
|
|
2429
|
+
return /* @__PURE__ */ React28__default.createElement("div", { key: promptItem.id, className: `p-3 border rounded-md ${borderColor} transition-colors bg-background` }, /* @__PURE__ */ React28__default.createElement(Label, { htmlFor: `select-prompt-${promptItem.id}`, className: "font-medium text-base block mb-2 whitespace-normal" }, /* @__PURE__ */ React28__default.createElement(MarkdownRenderer, { content: promptItem.content })), /* @__PURE__ */ React28__default.createElement(
|
|
1733
2430
|
Select,
|
|
1734
2431
|
{
|
|
1735
2432
|
value: selectedOptionId,
|
|
1736
2433
|
onValueChange: (value) => handleSelectChange(promptItem.id, value),
|
|
1737
2434
|
disabled: showCorrectAnswer
|
|
1738
2435
|
},
|
|
1739
|
-
/* @__PURE__ */
|
|
1740
|
-
/* @__PURE__ */
|
|
1741
|
-
), showCorrectAnswer && selectedOptionId && (isSelectionCorrect ? /* @__PURE__ */
|
|
1742
|
-
})), showCorrectAnswer && explanation && /* @__PURE__ */
|
|
2436
|
+
/* @__PURE__ */ React28__default.createElement(SelectTrigger, { id: `select-prompt-${promptItem.id}` }, /* @__PURE__ */ React28__default.createElement(SelectValue, { placeholder: "Ch\u1ECDn \u0111\xE1p \xE1n..." })),
|
|
2437
|
+
/* @__PURE__ */ React28__default.createElement(SelectContent, null, shuffledOptions.map((option) => /* @__PURE__ */ React28__default.createElement(SelectItem, { key: option.id, value: option.id, className: "whitespace-normal" }, getPlainText(option.content))))
|
|
2438
|
+
), showCorrectAnswer && selectedOptionId && (isSelectionCorrect ? /* @__PURE__ */ React28__default.createElement(CheckCircle, { className: "h-5 w-5 text-green-500 mt-2 inline-block" }) : /* @__PURE__ */ React28__default.createElement(XCircle, { className: "h-5 w-5 text-destructive mt-2 inline-block" })), showCorrectAnswer && !selectedOptionId && correctOptionId && /* @__PURE__ */ React28__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)));
|
|
2439
|
+
})), showCorrectAnswer && explanation && /* @__PURE__ */ React28__default.createElement("div", { className: "mt-6 p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React28__default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Gi\u1EA3i th\xEDch:"), /* @__PURE__ */ React28__default.createElement(MarkdownRenderer, { content: explanation, className: "text-sm text-accent-foreground/80" })), showCorrectAnswer && /* @__PURE__ */ React28__default.createElement("div", { className: "mt-4 space-y-2" }, /* @__PURE__ */ React28__default.createElement("p", { className: "font-semibold text-md" }, "\u0110\xE1p \xE1n \u0111\xFAng:"), /* @__PURE__ */ React28__default.createElement("ul", { className: "list-disc list-inside space-y-1 text-sm" }, correctAnswerMap.map((map) => {
|
|
1743
2440
|
var _a, _b;
|
|
1744
2441
|
const promptText = (_a = prompts.find((p) => p.id === map.promptId)) == null ? void 0 : _a.content;
|
|
1745
2442
|
const optionText = (_b = initialOptions.find((o) => o.id === map.optionId)) == null ? void 0 : _b.content;
|
|
1746
|
-
return /* @__PURE__ */
|
|
2443
|
+
return /* @__PURE__ */ React28__default.createElement("li", { key: map.promptId }, /* @__PURE__ */ React28__default.createElement(MarkdownRenderer, { content: `<strong>${getPlainText(promptText) || "N/A"}</strong> gh\xE9p v\u1EDBi <strong>${getPlainText(optionText) || "N/A"}</strong>` }));
|
|
1747
2444
|
})))));
|
|
1748
2445
|
};
|
|
1749
2446
|
var DragAndDropQuestionUI = ({
|
|
@@ -1773,7 +2470,7 @@ var DragAndDropQuestionUI = ({
|
|
|
1773
2470
|
var _a;
|
|
1774
2471
|
return (_a = answerMap.find((map) => map.draggableId === draggableItemId)) == null ? void 0 : _a.dropZoneId;
|
|
1775
2472
|
};
|
|
1776
|
-
return /* @__PURE__ */
|
|
2473
|
+
return /* @__PURE__ */ React28__default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React28__default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React28__default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, /* @__PURE__ */ React28__default.createElement(MarkdownRenderer, { content: prompt })), points && /* @__PURE__ */ React28__default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", points)), /* @__PURE__ */ React28__default.createElement(CardContent, { className: "p-0 space-y-4" }, backgroundImageUrl && /* @__PURE__ */ React28__default.createElement("div", { className: "mb-4 overflow-hidden rounded-md border" }, /* @__PURE__ */ React28__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__default.createElement("div", { className: "space-y-3" }, /* @__PURE__ */ React28__default.createElement(Label, { className: "font-semibold" }, "Gh\xE9p c\xE1c m\u1EE5c sau v\xE0o \u0111\xFAng v\u1ECB tr\xED:"), draggableItems.map((item) => {
|
|
1777
2474
|
const selectedDropZoneId = currentAnswers[item.id] || "";
|
|
1778
2475
|
const correctDropZoneId = getCorrectDropZoneIdForDraggable(item.id);
|
|
1779
2476
|
const isSelectionCorrect = showCorrectAnswer && selectedDropZoneId ? selectedDropZoneId === correctDropZoneId : null;
|
|
@@ -1783,21 +2480,21 @@ var DragAndDropQuestionUI = ({
|
|
|
1783
2480
|
} else {
|
|
1784
2481
|
itemStyle += " border-muted";
|
|
1785
2482
|
}
|
|
1786
|
-
return /* @__PURE__ */
|
|
2483
|
+
return /* @__PURE__ */ React28__default.createElement("div", { key: item.id, className: itemStyle }, /* @__PURE__ */ React28__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__default.createElement(MarkdownRenderer, { content: item.content })), /* @__PURE__ */ React28__default.createElement("div", { className: "flex items-center space-x-2 w-full sm:w-auto" }, /* @__PURE__ */ React28__default.createElement(
|
|
1787
2484
|
Select,
|
|
1788
2485
|
{
|
|
1789
2486
|
value: selectedDropZoneId,
|
|
1790
2487
|
onValueChange: (value) => handleSelectChange(item.id, value),
|
|
1791
2488
|
disabled: showCorrectAnswer
|
|
1792
2489
|
},
|
|
1793
|
-
/* @__PURE__ */
|
|
1794
|
-
/* @__PURE__ */
|
|
1795
|
-
), showCorrectAnswer && selectedDropZoneId && (isSelectionCorrect ? /* @__PURE__ */
|
|
1796
|
-
})), showCorrectAnswer && explanation && /* @__PURE__ */
|
|
2490
|
+
/* @__PURE__ */ React28__default.createElement(SelectTrigger, { id: `select-draggable-${item.id}`, className: "w-full sm:min-w-[200px]" }, /* @__PURE__ */ React28__default.createElement(SelectValue, { placeholder: "Ch\u1ECDn v\u1ECB tr\xED..." })),
|
|
2491
|
+
/* @__PURE__ */ React28__default.createElement(SelectContent, null, dropZones.map((zone) => /* @__PURE__ */ React28__default.createElement(SelectItem, { key: zone.id, value: zone.id }, zone.label.replace(/<[^>]*>?/gm, ""))))
|
|
2492
|
+
), showCorrectAnswer && selectedDropZoneId && (isSelectionCorrect ? /* @__PURE__ */ React28__default.createElement(CheckCircle, { className: "h-5 w-5 text-green-500 flex-shrink-0" }) : /* @__PURE__ */ React28__default.createElement(XCircle, { className: "h-5 w-5 text-destructive flex-shrink-0" }))));
|
|
2493
|
+
})), showCorrectAnswer && explanation && /* @__PURE__ */ React28__default.createElement("div", { className: "mt-6 p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React28__default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Gi\u1EA3i th\xEDch:"), /* @__PURE__ */ React28__default.createElement(MarkdownRenderer, { content: explanation, className: "text-sm text-accent-foreground/80" })), showCorrectAnswer && /* @__PURE__ */ React28__default.createElement("div", { className: "mt-4 space-y-2" }, /* @__PURE__ */ React28__default.createElement("p", { className: "font-semibold text-md" }, "\u0110\xE1p \xE1n \u0111\xFAng:"), /* @__PURE__ */ React28__default.createElement("ul", { className: "list-disc list-inside space-y-1 text-sm" }, answerMap.map((map) => {
|
|
1797
2494
|
var _a, _b;
|
|
1798
2495
|
const draggableText = (_a = draggableItems.find((d) => d.id === map.draggableId)) == null ? void 0 : _a.content;
|
|
1799
|
-
const dropZoneText = (_b = dropZones.find((
|
|
1800
|
-
return /* @__PURE__ */
|
|
2496
|
+
const dropZoneText = (_b = dropZones.find((z3) => z3.id === map.dropZoneId)) == null ? void 0 : _b.label;
|
|
2497
|
+
return /* @__PURE__ */ React28__default.createElement("li", { key: map.draggableId }, /* @__PURE__ */ React28__default.createElement(MarkdownRenderer, { content: `<strong>${draggableText || "N/A"}</strong> v\xE0o <strong>${dropZoneText || "N/A"}</strong>` }));
|
|
1801
2498
|
})))));
|
|
1802
2499
|
};
|
|
1803
2500
|
var HotspotQuestionUI = ({
|
|
@@ -1868,7 +2565,7 @@ var HotspotQuestionUI = ({
|
|
|
1868
2565
|
tempDiv.innerHTML = htmlString;
|
|
1869
2566
|
return tempDiv.textContent || tempDiv.innerText || "";
|
|
1870
2567
|
};
|
|
1871
|
-
return /* @__PURE__ */
|
|
2568
|
+
return /* @__PURE__ */ React28__default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React28__default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React28__default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, /* @__PURE__ */ React28__default.createElement(MarkdownRenderer, { content: prompt })), points && /* @__PURE__ */ React28__default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", points)), /* @__PURE__ */ React28__default.createElement(CardContent, { className: "p-0 space-y-4" }, /* @__PURE__ */ React28__default.createElement("div", { className: "relative w-full border border-muted rounded-md overflow-hidden", style: { maxWidth: "100%", maxHeight: "500px" } }, /* @__PURE__ */ React28__default.createElement(
|
|
1872
2569
|
"img",
|
|
1873
2570
|
{
|
|
1874
2571
|
src: imageUrl,
|
|
@@ -1876,7 +2573,7 @@ var HotspotQuestionUI = ({
|
|
|
1876
2573
|
className: "block max-w-full max-h-full object-contain",
|
|
1877
2574
|
"data-ai-hint": question.imageAltText ? question.imageAltText.split(" ").slice(0, 2).join(" ") : "diagram illustration"
|
|
1878
2575
|
}
|
|
1879
|
-
), hotspots.map((hotspot) => /* @__PURE__ */
|
|
2576
|
+
), hotspots.map((hotspot) => /* @__PURE__ */ React28__default.createElement(
|
|
1880
2577
|
"div",
|
|
1881
2578
|
{
|
|
1882
2579
|
key: hotspot.id,
|
|
@@ -1890,9 +2587,9 @@ var HotspotQuestionUI = ({
|
|
|
1890
2587
|
if (e.key === "Enter" || e.key === " ") handleHotspotClick(hotspot.id);
|
|
1891
2588
|
}
|
|
1892
2589
|
}
|
|
1893
|
-
))), showCorrectAnswer && explanation && /* @__PURE__ */
|
|
2590
|
+
))), showCorrectAnswer && explanation && /* @__PURE__ */ React28__default.createElement("div", { className: "mt-4 p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React28__default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Explanation:"), /* @__PURE__ */ React28__default.createElement(MarkdownRenderer, { content: explanation, className: "text-sm text-accent-foreground/80" })), showCorrectAnswer && /* @__PURE__ */ React28__default.createElement("div", { className: "mt-4 space-y-2" }, /* @__PURE__ */ React28__default.createElement("p", { className: "font-semibold text-md" }, "Correct Hotspots:"), /* @__PURE__ */ React28__default.createElement("ul", { className: "list-disc list-inside space-y-1 text-sm" }, (correctAnswerIds || []).map((id) => {
|
|
1894
2591
|
const hotspot = hotspots.find((h) => h.id === id);
|
|
1895
|
-
return /* @__PURE__ */
|
|
2592
|
+
return /* @__PURE__ */ React28__default.createElement("li", { key: id }, /* @__PURE__ */ React28__default.createElement(MarkdownRenderer, { content: (hotspot == null ? void 0 : hotspot.description) || (hotspot == null ? void 0 : hotspot.id) || "N/A", className: "inline" }));
|
|
1896
2593
|
})))));
|
|
1897
2594
|
};
|
|
1898
2595
|
var loadScript = (src, async = true) => {
|
|
@@ -2010,7 +2707,7 @@ var useBlocklyLoader = () => {
|
|
|
2010
2707
|
}, []);
|
|
2011
2708
|
return { isLoading, loadError, isReady, retry };
|
|
2012
2709
|
};
|
|
2013
|
-
var BlocklyProgrammingQuestionUI =
|
|
2710
|
+
var BlocklyProgrammingQuestionUI = React28__default.forwardRef(({
|
|
2014
2711
|
question,
|
|
2015
2712
|
userAnswer,
|
|
2016
2713
|
showCorrectAnswer = false
|
|
@@ -2185,7 +2882,7 @@ var BlocklyProgrammingQuestionUI = React25__default.forwardRef(({
|
|
|
2185
2882
|
}, [blocklyReady]);
|
|
2186
2883
|
const workspaceHeight = showCorrectAnswer ? "300px" : "450px";
|
|
2187
2884
|
const workspaceContainerId = `blockly-workspace-container-${question.id}`;
|
|
2188
|
-
return /* @__PURE__ */
|
|
2885
|
+
return /* @__PURE__ */ React28__default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React28__default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React28__default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, /* @__PURE__ */ React28__default.createElement(MarkdownRenderer, { content: question.prompt })), question.points && /* @__PURE__ */ React28__default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", question.points)), /* @__PURE__ */ React28__default.createElement(CardContent, { className: "p-0" }, blocklyLoading && /* @__PURE__ */ React28__default.createElement(
|
|
2189
2886
|
"div",
|
|
2190
2887
|
{
|
|
2191
2888
|
style: {
|
|
@@ -2197,8 +2894,8 @@ var BlocklyProgrammingQuestionUI = React25__default.forwardRef(({
|
|
|
2197
2894
|
},
|
|
2198
2895
|
className: "flex items-center justify-center"
|
|
2199
2896
|
},
|
|
2200
|
-
/* @__PURE__ */
|
|
2201
|
-
), blocklyLoadError && !blocklyLoading && /* @__PURE__ */
|
|
2897
|
+
/* @__PURE__ */ React28__default.createElement("div", { className: "text-center" }, /* @__PURE__ */ React28__default.createElement("p", { className: "text-muted-foreground animate-pulse mb-2" }, "Loading Blockly Environment..."), /* @__PURE__ */ React28__default.createElement("div", { className: "w-8 h-8 border-4 border-muted border-t-primary rounded-full animate-spin mx-auto" }))
|
|
2898
|
+
), blocklyLoadError && !blocklyLoading && /* @__PURE__ */ React28__default.createElement(
|
|
2202
2899
|
"div",
|
|
2203
2900
|
{
|
|
2204
2901
|
style: {
|
|
@@ -2210,14 +2907,14 @@ var BlocklyProgrammingQuestionUI = React25__default.forwardRef(({
|
|
|
2210
2907
|
},
|
|
2211
2908
|
className: "flex items-center justify-center p-4"
|
|
2212
2909
|
},
|
|
2213
|
-
/* @__PURE__ */
|
|
2910
|
+
/* @__PURE__ */ React28__default.createElement("div", { className: "text-destructive text-center" }, /* @__PURE__ */ React28__default.createElement("p", { className: "font-semibold text-lg" }, "Failed to load Blockly"), /* @__PURE__ */ React28__default.createElement("p", { className: "text-sm mt-2 mb-3" }, blocklyLoadError), /* @__PURE__ */ React28__default.createElement("div", { className: "space-x-2" }, /* @__PURE__ */ React28__default.createElement(
|
|
2214
2911
|
"button",
|
|
2215
2912
|
{
|
|
2216
2913
|
onClick: retry,
|
|
2217
2914
|
className: "px-4 py-2 bg-primary text-primary-foreground rounded hover:bg-primary/90 transition-colors text-sm"
|
|
2218
2915
|
},
|
|
2219
2916
|
"Try Again"
|
|
2220
|
-
), /* @__PURE__ */
|
|
2917
|
+
), /* @__PURE__ */ React28__default.createElement(
|
|
2221
2918
|
"button",
|
|
2222
2919
|
{
|
|
2223
2920
|
onClick: () => window.location.reload(),
|
|
@@ -2225,7 +2922,7 @@ var BlocklyProgrammingQuestionUI = React25__default.forwardRef(({
|
|
|
2225
2922
|
},
|
|
2226
2923
|
"Refresh Page"
|
|
2227
2924
|
)))
|
|
2228
|
-
), !blocklyLoading && !blocklyLoadError && /* @__PURE__ */
|
|
2925
|
+
), !blocklyLoading && !blocklyLoadError && /* @__PURE__ */ React28__default.createElement(
|
|
2229
2926
|
"div",
|
|
2230
2927
|
{
|
|
2231
2928
|
id: workspaceContainerId,
|
|
@@ -2242,7 +2939,7 @@ var BlocklyProgrammingQuestionUI = React25__default.forwardRef(({
|
|
|
2242
2939
|
},
|
|
2243
2940
|
"aria-label": `Blockly programming workspace for question: ${question.prompt}`
|
|
2244
2941
|
}
|
|
2245
|
-
), showCorrectAnswer && question.explanation && /* @__PURE__ */
|
|
2942
|
+
), showCorrectAnswer && question.explanation && /* @__PURE__ */ React28__default.createElement("div", { className: "mt-4 p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React28__default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Explanation:"), /* @__PURE__ */ React28__default.createElement(MarkdownRenderer, { content: question.explanation, className: "text-sm text-accent-foreground/80" }))));
|
|
2246
2943
|
});
|
|
2247
2944
|
BlocklyProgrammingQuestionUI.displayName = "BlocklyProgrammingQuestionUI";
|
|
2248
2945
|
var SCRATCH_JS_ENGINE_LAYOUT_PATH = "/static/scratch-blocks/js/blockly_compressed_vertical.js";
|
|
@@ -2523,15 +3220,15 @@ var ScratchProgrammingQuestionUI = forwardRef(({
|
|
|
2523
3220
|
}, [isBlocklyReady, initializeWorkspace]);
|
|
2524
3221
|
const workspaceHeight = showCorrectAnswer ? "300px" : "450px";
|
|
2525
3222
|
if (isLoadingScripts) {
|
|
2526
|
-
return /* @__PURE__ */
|
|
3223
|
+
return /* @__PURE__ */ React28__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__default.createElement("div", { className: "text-center" }, /* @__PURE__ */ React28__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__default.createElement("p", { className: "text-muted-foreground" }, "Loading Scratch Assets...")));
|
|
2527
3224
|
}
|
|
2528
3225
|
if (componentError) {
|
|
2529
|
-
return /* @__PURE__ */
|
|
3226
|
+
return /* @__PURE__ */ React28__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__default.createElement("p", { className: "font-semibold text-lg mb-2" }, "Failed to load Scratch Workspace."), /* @__PURE__ */ React28__default.createElement("p", { className: "text-sm text-muted-foreground mb-3 text-center" }, componentError), /* @__PURE__ */ React28__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__default.createElement("code", null, "public/static/scratch-blocks/js"), " directory. Check browser console for more details.", /* @__PURE__ */ React28__default.createElement("br", null), /* @__PURE__ */ React28__default.createElement("strong", null, "CRITICAL: Ensure you have copied the CSS files from ", /* @__PURE__ */ React28__default.createElement("code", null, "node_modules/scratch-blocks/css/"), " (e.g., ", /* @__PURE__ */ React28__default.createElement("code", null, "vertical.css"), ") to ", /* @__PURE__ */ React28__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__default.createElement(Button, { onClick: attemptLoadScripts, variant: "outline" }, "Try Reloading Scripts"));
|
|
2530
3227
|
}
|
|
2531
3228
|
if (!isBlocklyReady && !isLoadingScripts) {
|
|
2532
|
-
return /* @__PURE__ */
|
|
3229
|
+
return /* @__PURE__ */ React28__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__default.createElement("p", { className: "text-muted-foreground" }, "Scratch environment did not initialize (Blockly not ready). Check console for script loading errors."));
|
|
2533
3230
|
}
|
|
2534
|
-
return /* @__PURE__ */
|
|
3231
|
+
return /* @__PURE__ */ React28__default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React28__default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React28__default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, question.prompt), question.points && /* @__PURE__ */ React28__default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", question.points)), /* @__PURE__ */ React28__default.createElement(CardContent, { className: "p-0" }, /* @__PURE__ */ React28__default.createElement(
|
|
2535
3232
|
"div",
|
|
2536
3233
|
{
|
|
2537
3234
|
ref: blocklyDivRef,
|
|
@@ -2547,17 +3244,237 @@ var ScratchProgrammingQuestionUI = forwardRef(({
|
|
|
2547
3244
|
},
|
|
2548
3245
|
"aria-label": `Scratch programming workspace for question: ${question.prompt}`
|
|
2549
3246
|
}
|
|
2550
|
-
), showCorrectAnswer && question.explanation && /* @__PURE__ */
|
|
3247
|
+
), showCorrectAnswer && question.explanation && /* @__PURE__ */ React28__default.createElement("div", { className: "mt-4 p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React28__default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Explanation:"), /* @__PURE__ */ React28__default.createElement("p", { className: "text-sm text-accent-foreground/80" }, question.explanation))));
|
|
2551
3248
|
});
|
|
2552
3249
|
ScratchProgrammingQuestionUI.displayName = "ScratchProgrammingQuestionUI";
|
|
3250
|
+
var Tabs = TabsPrimitive.Root;
|
|
3251
|
+
var TabsList = React28.forwardRef((_a, ref) => {
|
|
3252
|
+
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
3253
|
+
return /* @__PURE__ */ React28.createElement(
|
|
3254
|
+
TabsPrimitive.List,
|
|
3255
|
+
__spreadValues({
|
|
3256
|
+
ref,
|
|
3257
|
+
className: cn(
|
|
3258
|
+
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
|
3259
|
+
className
|
|
3260
|
+
)
|
|
3261
|
+
}, props)
|
|
3262
|
+
);
|
|
3263
|
+
});
|
|
3264
|
+
TabsList.displayName = TabsPrimitive.List.displayName;
|
|
3265
|
+
var TabsTrigger = React28.forwardRef((_a, ref) => {
|
|
3266
|
+
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
3267
|
+
return /* @__PURE__ */ React28.createElement(
|
|
3268
|
+
TabsPrimitive.Trigger,
|
|
3269
|
+
__spreadValues({
|
|
3270
|
+
ref,
|
|
3271
|
+
className: cn(
|
|
3272
|
+
"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",
|
|
3273
|
+
className
|
|
3274
|
+
)
|
|
3275
|
+
}, props)
|
|
3276
|
+
);
|
|
3277
|
+
});
|
|
3278
|
+
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
|
3279
|
+
var TabsContent = React28.forwardRef((_a, ref) => {
|
|
3280
|
+
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
3281
|
+
return /* @__PURE__ */ React28.createElement(
|
|
3282
|
+
TabsPrimitive.Content,
|
|
3283
|
+
__spreadValues({
|
|
3284
|
+
ref,
|
|
3285
|
+
className: cn(
|
|
3286
|
+
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
3287
|
+
className
|
|
3288
|
+
)
|
|
3289
|
+
}, props)
|
|
3290
|
+
);
|
|
3291
|
+
});
|
|
3292
|
+
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
|
3293
|
+
var TOAST_LIMIT = 1;
|
|
3294
|
+
var TOAST_REMOVE_DELAY = 1e6;
|
|
3295
|
+
var count = 0;
|
|
3296
|
+
function genId() {
|
|
3297
|
+
count = (count + 1) % Number.MAX_SAFE_INTEGER;
|
|
3298
|
+
return count.toString();
|
|
3299
|
+
}
|
|
3300
|
+
var toastTimeouts = /* @__PURE__ */ new Map();
|
|
3301
|
+
var addToRemoveQueue = (toastId) => {
|
|
3302
|
+
if (toastTimeouts.has(toastId)) {
|
|
3303
|
+
return;
|
|
3304
|
+
}
|
|
3305
|
+
const timeout = setTimeout(() => {
|
|
3306
|
+
toastTimeouts.delete(toastId);
|
|
3307
|
+
dispatch({
|
|
3308
|
+
type: "REMOVE_TOAST",
|
|
3309
|
+
toastId
|
|
3310
|
+
});
|
|
3311
|
+
}, TOAST_REMOVE_DELAY);
|
|
3312
|
+
toastTimeouts.set(toastId, timeout);
|
|
3313
|
+
};
|
|
3314
|
+
var reducer = (state, action) => {
|
|
3315
|
+
switch (action.type) {
|
|
3316
|
+
case "ADD_TOAST":
|
|
3317
|
+
return __spreadProps(__spreadValues({}, state), {
|
|
3318
|
+
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT)
|
|
3319
|
+
});
|
|
3320
|
+
case "UPDATE_TOAST":
|
|
3321
|
+
return __spreadProps(__spreadValues({}, state), {
|
|
3322
|
+
toasts: state.toasts.map(
|
|
3323
|
+
(t) => t.id === action.toast.id ? __spreadValues(__spreadValues({}, t), action.toast) : t
|
|
3324
|
+
)
|
|
3325
|
+
});
|
|
3326
|
+
case "DISMISS_TOAST": {
|
|
3327
|
+
const { toastId } = action;
|
|
3328
|
+
if (toastId) {
|
|
3329
|
+
addToRemoveQueue(toastId);
|
|
3330
|
+
} else {
|
|
3331
|
+
state.toasts.forEach((toast2) => {
|
|
3332
|
+
addToRemoveQueue(toast2.id);
|
|
3333
|
+
});
|
|
3334
|
+
}
|
|
3335
|
+
return __spreadProps(__spreadValues({}, state), {
|
|
3336
|
+
toasts: state.toasts.map(
|
|
3337
|
+
(t) => t.id === toastId || toastId === void 0 ? __spreadProps(__spreadValues({}, t), {
|
|
3338
|
+
open: false
|
|
3339
|
+
}) : t
|
|
3340
|
+
)
|
|
3341
|
+
});
|
|
3342
|
+
}
|
|
3343
|
+
case "REMOVE_TOAST":
|
|
3344
|
+
if (action.toastId === void 0) {
|
|
3345
|
+
return __spreadProps(__spreadValues({}, state), {
|
|
3346
|
+
toasts: []
|
|
3347
|
+
});
|
|
3348
|
+
}
|
|
3349
|
+
return __spreadProps(__spreadValues({}, state), {
|
|
3350
|
+
toasts: state.toasts.filter((t) => t.id !== action.toastId)
|
|
3351
|
+
});
|
|
3352
|
+
}
|
|
3353
|
+
};
|
|
3354
|
+
var listeners = [];
|
|
3355
|
+
var memoryState = { toasts: [] };
|
|
3356
|
+
function dispatch(action) {
|
|
3357
|
+
memoryState = reducer(memoryState, action);
|
|
3358
|
+
listeners.forEach((listener) => {
|
|
3359
|
+
listener(memoryState);
|
|
3360
|
+
});
|
|
3361
|
+
}
|
|
3362
|
+
function toast(_a) {
|
|
3363
|
+
var props = __objRest(_a, []);
|
|
3364
|
+
const id = genId();
|
|
3365
|
+
const update = (props2) => dispatch({
|
|
3366
|
+
type: "UPDATE_TOAST",
|
|
3367
|
+
toast: __spreadProps(__spreadValues({}, props2), { id })
|
|
3368
|
+
});
|
|
3369
|
+
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
|
|
3370
|
+
dispatch({
|
|
3371
|
+
type: "ADD_TOAST",
|
|
3372
|
+
toast: __spreadProps(__spreadValues({}, props), {
|
|
3373
|
+
id,
|
|
3374
|
+
open: true,
|
|
3375
|
+
onOpenChange: (open) => {
|
|
3376
|
+
if (!open) dismiss();
|
|
3377
|
+
}
|
|
3378
|
+
})
|
|
3379
|
+
});
|
|
3380
|
+
return {
|
|
3381
|
+
id,
|
|
3382
|
+
dismiss,
|
|
3383
|
+
update
|
|
3384
|
+
};
|
|
3385
|
+
}
|
|
3386
|
+
function useToast() {
|
|
3387
|
+
const [state, setState] = React28.useState(memoryState);
|
|
3388
|
+
React28.useEffect(() => {
|
|
3389
|
+
listeners.push(setState);
|
|
3390
|
+
return () => {
|
|
3391
|
+
const index = listeners.indexOf(setState);
|
|
3392
|
+
if (index > -1) {
|
|
3393
|
+
listeners.splice(index, 1);
|
|
3394
|
+
}
|
|
3395
|
+
};
|
|
3396
|
+
}, [state]);
|
|
3397
|
+
return __spreadProps(__spreadValues({}, state), {
|
|
3398
|
+
toast,
|
|
3399
|
+
dismiss: (toastId) => dispatch({ type: "DISMISS_TOAST", toastId })
|
|
3400
|
+
});
|
|
3401
|
+
}
|
|
2553
3402
|
|
|
2554
|
-
// src/react-ui/components/ui/
|
|
2555
|
-
var
|
|
3403
|
+
// src/react-ui/components/ui/CodingQuestionUI.tsx
|
|
3404
|
+
var languageMap = {
|
|
3405
|
+
cpp: cpp(),
|
|
3406
|
+
javascript: javascript({ typescript: true }),
|
|
3407
|
+
// Use TS extension for better JS support
|
|
3408
|
+
python: python(),
|
|
3409
|
+
swift: javascript({ typescript: true }),
|
|
3410
|
+
// Fallback for Swift syntax highlighting
|
|
3411
|
+
csharp: cpp()
|
|
3412
|
+
// Fallback for C# syntax highlighting
|
|
3413
|
+
};
|
|
3414
|
+
var CodingQuestionUI = ({
|
|
3415
|
+
question,
|
|
3416
|
+
onAnswerChange,
|
|
3417
|
+
userAnswer,
|
|
3418
|
+
showCorrectAnswer = false
|
|
3419
|
+
}) => {
|
|
3420
|
+
const [code, setCode] = useState("");
|
|
3421
|
+
const [isRunningTests, setIsRunningTests] = useState(false);
|
|
3422
|
+
const [testResults, setTestResults] = useState([]);
|
|
3423
|
+
const { toast: toast2 } = useToast();
|
|
3424
|
+
useEffect(() => {
|
|
3425
|
+
const initialCode = typeof userAnswer === "string" ? userAnswer : question.functionSignature || "";
|
|
3426
|
+
setCode(initialCode);
|
|
3427
|
+
}, [question.id, userAnswer, question.functionSignature]);
|
|
3428
|
+
const handleCodeChange = useCallback((value) => {
|
|
3429
|
+
setCode(value);
|
|
3430
|
+
onAnswerChange(value);
|
|
3431
|
+
}, [onAnswerChange]);
|
|
3432
|
+
const handleRunPublicTests = async () => {
|
|
3433
|
+
setIsRunningTests(true);
|
|
3434
|
+
setTestResults([]);
|
|
3435
|
+
try {
|
|
3436
|
+
const evaluationService = new CodeEvaluationService();
|
|
3437
|
+
const results = await evaluationService.evaluatePublicTestCases(question, code);
|
|
3438
|
+
setTestResults(results);
|
|
3439
|
+
} catch (error) {
|
|
3440
|
+
toast2({
|
|
3441
|
+
title: "Evaluation Error",
|
|
3442
|
+
description: error instanceof Error ? error.message : "An unknown error occurred.",
|
|
3443
|
+
variant: "destructive"
|
|
3444
|
+
});
|
|
3445
|
+
} finally {
|
|
3446
|
+
setIsRunningTests(false);
|
|
3447
|
+
}
|
|
3448
|
+
};
|
|
3449
|
+
const langExtension = languageMap[question.codingLanguage];
|
|
3450
|
+
return /* @__PURE__ */ React28__default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React28__default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React28__default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, /* @__PURE__ */ React28__default.createElement(MarkdownRenderer, { content: question.prompt })), question.points && /* @__PURE__ */ React28__default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", question.points)), /* @__PURE__ */ React28__default.createElement(CardContent, { className: "p-0 space-y-4" }, /* @__PURE__ */ React28__default.createElement("div", { className: "font-mono text-sm border rounded-md overflow-hidden" }, /* @__PURE__ */ React28__default.createElement(
|
|
3451
|
+
CodeMirror,
|
|
3452
|
+
{
|
|
3453
|
+
value: code,
|
|
3454
|
+
height: "300px",
|
|
3455
|
+
extensions: [langExtension],
|
|
3456
|
+
onChange: handleCodeChange,
|
|
3457
|
+
readOnly: showCorrectAnswer,
|
|
3458
|
+
theme: "dark"
|
|
3459
|
+
}
|
|
3460
|
+
)), !showCorrectAnswer && /* @__PURE__ */ React28__default.createElement(Button, { onClick: handleRunPublicTests, disabled: isRunningTests }, isRunningTests ? /* @__PURE__ */ React28__default.createElement(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : /* @__PURE__ */ React28__default.createElement(Play, { className: "mr-2 h-4 w-4" }), isRunningTests ? "Running..." : "Run Public Tests"), /* @__PURE__ */ React28__default.createElement(Tabs, { defaultValue: "tests", className: "w-full" }, /* @__PURE__ */ React28__default.createElement(TabsList, null, /* @__PURE__ */ React28__default.createElement(TabsTrigger, { value: "tests" }, "Test Cases"), showCorrectAnswer && /* @__PURE__ */ React28__default.createElement(TabsTrigger, { value: "solution" }, "Solution")), /* @__PURE__ */ React28__default.createElement(TabsContent, { value: "tests" }, /* @__PURE__ */ React28__default.createElement("div", { className: "space-y-2 p-2 border rounded-md min-h-[100px]" }, testResults.length > 0 ? testResults.map((result, index) => /* @__PURE__ */ React28__default.createElement("div", { key: result.testCaseId, className: "flex items-center text-sm" }, result.passed ? /* @__PURE__ */ React28__default.createElement(CheckCircle, { className: "h-4 w-4 text-green-500 mr-2" }) : /* @__PURE__ */ React28__default.createElement(XCircle, { className: "h-4 w-4 text-destructive mr-2" }), /* @__PURE__ */ React28__default.createElement("span", null, "Test Case #", index + 1, ": ", result.passed ? "Passed" : "Failed"), !result.passed && /* @__PURE__ */ React28__default.createElement("p", { className: "text-xs text-muted-foreground ml-2" }, "- ", result.reasoning))) : /* @__PURE__ */ React28__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__default.createElement(TabsContent, { value: "solution" }, /* @__PURE__ */ React28__default.createElement("div", { className: "font-mono text-sm border rounded-md overflow-hidden" }, /* @__PURE__ */ React28__default.createElement(
|
|
3461
|
+
CodeMirror,
|
|
3462
|
+
{
|
|
3463
|
+
value: question.solutionCode,
|
|
3464
|
+
height: "300px",
|
|
3465
|
+
extensions: [langExtension],
|
|
3466
|
+
readOnly: true,
|
|
3467
|
+
theme: "dark"
|
|
3468
|
+
}
|
|
3469
|
+
)))), showCorrectAnswer && question.explanation && /* @__PURE__ */ React28__default.createElement("div", { className: "mt-4 p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React28__default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Explanation:"), /* @__PURE__ */ React28__default.createElement(MarkdownRenderer, { content: question.explanation, className: "text-sm text-accent-foreground/80" }))));
|
|
3470
|
+
};
|
|
3471
|
+
var QuestionRenderer = React28__default.forwardRef(({
|
|
2556
3472
|
question,
|
|
2557
3473
|
onAnswerChange,
|
|
2558
3474
|
userAnswer,
|
|
2559
3475
|
showCorrectAnswer = false
|
|
2560
3476
|
}, ref) => {
|
|
3477
|
+
const { t } = useTranslation();
|
|
2561
3478
|
const commonProps = {
|
|
2562
3479
|
question,
|
|
2563
3480
|
onAnswerChange,
|
|
@@ -2566,37 +3483,39 @@ var QuestionRenderer = React25__default.forwardRef(({
|
|
|
2566
3483
|
};
|
|
2567
3484
|
switch (question.questionType) {
|
|
2568
3485
|
case "multiple_choice":
|
|
2569
|
-
return /* @__PURE__ */
|
|
3486
|
+
return /* @__PURE__ */ React28__default.createElement(MultipleChoiceQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question }));
|
|
2570
3487
|
case "true_false":
|
|
2571
|
-
return /* @__PURE__ */
|
|
3488
|
+
return /* @__PURE__ */ React28__default.createElement(TrueFalseQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question }));
|
|
2572
3489
|
case "multiple_response":
|
|
2573
|
-
return /* @__PURE__ */
|
|
3490
|
+
return /* @__PURE__ */ React28__default.createElement(MultipleResponseQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question }));
|
|
2574
3491
|
case "short_answer":
|
|
2575
|
-
return /* @__PURE__ */
|
|
3492
|
+
return /* @__PURE__ */ React28__default.createElement(ShortAnswerQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question }));
|
|
2576
3493
|
case "numeric":
|
|
2577
|
-
return /* @__PURE__ */
|
|
3494
|
+
return /* @__PURE__ */ React28__default.createElement(NumericQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question }));
|
|
2578
3495
|
case "fill_in_the_blanks":
|
|
2579
|
-
return /* @__PURE__ */
|
|
3496
|
+
return /* @__PURE__ */ React28__default.createElement(FillInTheBlanksQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question }));
|
|
2580
3497
|
case "sequence":
|
|
2581
|
-
return /* @__PURE__ */
|
|
3498
|
+
return /* @__PURE__ */ React28__default.createElement(SequenceQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question }));
|
|
2582
3499
|
case "matching":
|
|
2583
|
-
return /* @__PURE__ */
|
|
3500
|
+
return /* @__PURE__ */ React28__default.createElement(MatchingQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question }));
|
|
2584
3501
|
case "drag_and_drop":
|
|
2585
|
-
return /* @__PURE__ */
|
|
3502
|
+
return /* @__PURE__ */ React28__default.createElement(DragAndDropQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question }));
|
|
2586
3503
|
case "hotspot":
|
|
2587
|
-
return /* @__PURE__ */
|
|
3504
|
+
return /* @__PURE__ */ React28__default.createElement(HotspotQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question }));
|
|
2588
3505
|
case "blockly_programming":
|
|
2589
|
-
return /* @__PURE__ */
|
|
3506
|
+
return /* @__PURE__ */ React28__default.createElement(BlocklyProgrammingQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question, ref }));
|
|
2590
3507
|
case "scratch_programming":
|
|
2591
|
-
return /* @__PURE__ */
|
|
3508
|
+
return /* @__PURE__ */ React28__default.createElement(ScratchProgrammingQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question, ref }));
|
|
3509
|
+
case "coding":
|
|
3510
|
+
return /* @__PURE__ */ React28__default.createElement(CodingQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question }));
|
|
2592
3511
|
default:
|
|
2593
|
-
return /* @__PURE__ */
|
|
3512
|
+
return /* @__PURE__ */ React28__default.createElement("div", { className: "p-4 border border-destructive bg-destructive/10 rounded-md" }, /* @__PURE__ */ React28__default.createElement("p", { className: "font-semibold text-destructive" }, t("unsupportedQuestionType")), /* @__PURE__ */ React28__default.createElement("p", { className: "text-sm text-muted-foreground" }, t("unsupportedQuestionTypeDescription")), /* @__PURE__ */ React28__default.createElement("pre", { className: "mt-2 p-2 bg-muted rounded text-xs font-code overflow-x-auto" }, JSON.stringify(question, null, 2)));
|
|
2594
3513
|
}
|
|
2595
3514
|
});
|
|
2596
3515
|
QuestionRenderer.displayName = "QuestionRenderer";
|
|
2597
|
-
var Progress =
|
|
3516
|
+
var Progress = React28.forwardRef((_a, ref) => {
|
|
2598
3517
|
var _b = _a, { className, value } = _b, props = __objRest(_b, ["className", "value"]);
|
|
2599
|
-
return /* @__PURE__ */
|
|
3518
|
+
return /* @__PURE__ */ React28.createElement(
|
|
2600
3519
|
ProgressPrimitive.Root,
|
|
2601
3520
|
__spreadValues({
|
|
2602
3521
|
ref,
|
|
@@ -2605,7 +3524,7 @@ var Progress = React25.forwardRef((_a, ref) => {
|
|
|
2605
3524
|
className
|
|
2606
3525
|
)
|
|
2607
3526
|
}, props),
|
|
2608
|
-
/* @__PURE__ */
|
|
3527
|
+
/* @__PURE__ */ React28.createElement(
|
|
2609
3528
|
ProgressPrimitive.Indicator,
|
|
2610
3529
|
{
|
|
2611
3530
|
className: "h-full w-full flex-1 bg-primary transition-all",
|
|
@@ -2616,9 +3535,9 @@ var Progress = React25.forwardRef((_a, ref) => {
|
|
|
2616
3535
|
});
|
|
2617
3536
|
Progress.displayName = ProgressPrimitive.Root.displayName;
|
|
2618
3537
|
var Accordion = AccordionPrimitive.Root;
|
|
2619
|
-
var AccordionItem =
|
|
3538
|
+
var AccordionItem = React28.forwardRef((_a, ref) => {
|
|
2620
3539
|
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
2621
|
-
return /* @__PURE__ */
|
|
3540
|
+
return /* @__PURE__ */ React28.createElement(
|
|
2622
3541
|
AccordionPrimitive.Item,
|
|
2623
3542
|
__spreadValues({
|
|
2624
3543
|
ref,
|
|
@@ -2627,9 +3546,9 @@ var AccordionItem = React25.forwardRef((_a, ref) => {
|
|
|
2627
3546
|
);
|
|
2628
3547
|
});
|
|
2629
3548
|
AccordionItem.displayName = "AccordionItem";
|
|
2630
|
-
var AccordionTrigger =
|
|
3549
|
+
var AccordionTrigger = React28.forwardRef((_a, ref) => {
|
|
2631
3550
|
var _b = _a, { className, children } = _b, props = __objRest(_b, ["className", "children"]);
|
|
2632
|
-
return /* @__PURE__ */
|
|
3551
|
+
return /* @__PURE__ */ React28.createElement(AccordionPrimitive.Header, { className: "flex" }, /* @__PURE__ */ React28.createElement(
|
|
2633
3552
|
AccordionPrimitive.Trigger,
|
|
2634
3553
|
__spreadValues({
|
|
2635
3554
|
ref,
|
|
@@ -2639,39 +3558,39 @@ var AccordionTrigger = React25.forwardRef((_a, ref) => {
|
|
|
2639
3558
|
)
|
|
2640
3559
|
}, props),
|
|
2641
3560
|
children,
|
|
2642
|
-
/* @__PURE__ */
|
|
3561
|
+
/* @__PURE__ */ React28.createElement(ChevronDown, { className: "h-4 w-4 shrink-0 transition-transform duration-200" })
|
|
2643
3562
|
));
|
|
2644
3563
|
});
|
|
2645
3564
|
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
|
|
2646
|
-
var AccordionContent =
|
|
3565
|
+
var AccordionContent = React28.forwardRef((_a, ref) => {
|
|
2647
3566
|
var _b = _a, { className, children } = _b, props = __objRest(_b, ["className", "children"]);
|
|
2648
|
-
return /* @__PURE__ */
|
|
3567
|
+
return /* @__PURE__ */ React28.createElement(
|
|
2649
3568
|
AccordionPrimitive.Content,
|
|
2650
3569
|
__spreadValues({
|
|
2651
3570
|
ref,
|
|
2652
3571
|
className: "overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
|
2653
3572
|
}, props),
|
|
2654
|
-
/* @__PURE__ */
|
|
3573
|
+
/* @__PURE__ */ React28.createElement("div", { className: cn("pb-4 pt-0", className) }, children)
|
|
2655
3574
|
);
|
|
2656
3575
|
});
|
|
2657
3576
|
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
|
|
2658
|
-
var ScrollArea =
|
|
3577
|
+
var ScrollArea = React28.forwardRef((_a, ref) => {
|
|
2659
3578
|
var _b = _a, { className, children } = _b, props = __objRest(_b, ["className", "children"]);
|
|
2660
|
-
return /* @__PURE__ */
|
|
3579
|
+
return /* @__PURE__ */ React28.createElement(
|
|
2661
3580
|
ScrollAreaPrimitive.Root,
|
|
2662
3581
|
__spreadValues({
|
|
2663
3582
|
ref,
|
|
2664
3583
|
className: cn("relative overflow-hidden", className)
|
|
2665
3584
|
}, props),
|
|
2666
|
-
/* @__PURE__ */
|
|
2667
|
-
/* @__PURE__ */
|
|
2668
|
-
/* @__PURE__ */
|
|
3585
|
+
/* @__PURE__ */ React28.createElement(ScrollAreaPrimitive.Viewport, { className: "h-full w-full rounded-[inherit]" }, children),
|
|
3586
|
+
/* @__PURE__ */ React28.createElement(ScrollBar, null),
|
|
3587
|
+
/* @__PURE__ */ React28.createElement(ScrollAreaPrimitive.Corner, null)
|
|
2669
3588
|
);
|
|
2670
3589
|
});
|
|
2671
3590
|
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
|
|
2672
|
-
var ScrollBar =
|
|
3591
|
+
var ScrollBar = React28.forwardRef((_a, ref) => {
|
|
2673
3592
|
var _b = _a, { className, orientation = "vertical" } = _b, props = __objRest(_b, ["className", "orientation"]);
|
|
2674
|
-
return /* @__PURE__ */
|
|
3593
|
+
return /* @__PURE__ */ React28.createElement(
|
|
2675
3594
|
ScrollAreaPrimitive.ScrollAreaScrollbar,
|
|
2676
3595
|
__spreadValues({
|
|
2677
3596
|
ref,
|
|
@@ -2683,25 +3602,52 @@ var ScrollBar = React25.forwardRef((_a, ref) => {
|
|
|
2683
3602
|
className
|
|
2684
3603
|
)
|
|
2685
3604
|
}, props),
|
|
2686
|
-
/* @__PURE__ */
|
|
3605
|
+
/* @__PURE__ */ React28.createElement(ScrollAreaPrimitive.ScrollAreaThumb, { className: "relative flex-1 rounded-full bg-border" })
|
|
2687
3606
|
);
|
|
2688
3607
|
});
|
|
2689
3608
|
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
|
|
2690
3609
|
|
|
2691
3610
|
// src/react-ui/components/ui/QuizResult.tsx
|
|
2692
|
-
var QuizResult = ({
|
|
3611
|
+
var QuizResult = ({
|
|
3612
|
+
result,
|
|
3613
|
+
quizTitle,
|
|
3614
|
+
onExitQuiz,
|
|
3615
|
+
onGenerateReview,
|
|
3616
|
+
showReviewButton = false,
|
|
3617
|
+
isReviewLoading = false
|
|
3618
|
+
}) => {
|
|
2693
3619
|
var _a, _b, _c, _d;
|
|
3620
|
+
const { t } = useTranslation();
|
|
2694
3621
|
const getAnswerDisplay = (answer) => {
|
|
2695
|
-
if (answer === null || answer === void 0) return "
|
|
3622
|
+
if (answer === null || answer === void 0) return t("practiceFlow.results.notAnswered");
|
|
2696
3623
|
if (typeof answer === "boolean") return answer ? "True" : "False";
|
|
2697
3624
|
if (Array.isArray(answer)) return answer.join(", ");
|
|
2698
|
-
if (typeof answer === "object")
|
|
3625
|
+
if (typeof answer === "object") {
|
|
3626
|
+
if (answer.hasOwnProperty("value")) {
|
|
3627
|
+
const value = answer.value;
|
|
3628
|
+
if (value === null || value === void 0) return t("practiceFlow.results.notAnswered");
|
|
3629
|
+
if (typeof value === "boolean") return value ? "True" : "False";
|
|
3630
|
+
if (Array.isArray(value)) return value.join(", ");
|
|
3631
|
+
if (typeof value === "object") return JSON.stringify(value, null, 2);
|
|
3632
|
+
return String(value);
|
|
3633
|
+
}
|
|
3634
|
+
return JSON.stringify(answer, null, 2);
|
|
3635
|
+
}
|
|
2699
3636
|
return String(answer);
|
|
2700
3637
|
};
|
|
2701
|
-
return /* @__PURE__ */
|
|
3638
|
+
return /* @__PURE__ */ React28__default.createElement(Card, { className: "w-full max-w-3xl mx-auto shadow-xl" }, /* @__PURE__ */ React28__default.createElement(CardHeader, null, /* @__PURE__ */ React28__default.createElement(CardTitle, { className: "text-3xl font-headline text-center" }, t("practiceFlow.results.title", { quizTitle })), /* @__PURE__ */ React28__default.createElement(CardDescription, { className: "text-center text-lg" }, t("practiceFlow.results.description"))), /* @__PURE__ */ React28__default.createElement(CardContent, { className: "space-y-6" }, /* @__PURE__ */ React28__default.createElement(Card, { className: "bg-secondary/50" }, /* @__PURE__ */ React28__default.createElement(CardHeader, null, /* @__PURE__ */ React28__default.createElement(CardTitle, { className: "text-xl flex items-center" }, /* @__PURE__ */ React28__default.createElement(BarChart2, { className: "mr-2 h-5 w-5 text-primary" }), t("practiceFlow.results.overallScore"))), /* @__PURE__ */ React28__default.createElement(CardContent, { className: "grid grid-cols-1 md:grid-cols-3 gap-4 text-center" }, /* @__PURE__ */ React28__default.createElement("div", null, /* @__PURE__ */ React28__default.createElement("p", { className: "text-3xl font-bold text-primary" }, result.score, " / ", result.maxScore), /* @__PURE__ */ React28__default.createElement("p", { className: "text-sm text-muted-foreground" }, t("practiceFlow.results.points"))), /* @__PURE__ */ React28__default.createElement("div", null, /* @__PURE__ */ React28__default.createElement("p", { className: "text-3xl font-bold text-primary" }, result.percentage.toFixed(2), "%"), /* @__PURE__ */ React28__default.createElement("p", { className: "text-sm text-muted-foreground" }, t("practiceFlow.results.percentage"))), /* @__PURE__ */ React28__default.createElement("div", null, result.passed !== void 0 && (result.passed ? /* @__PURE__ */ React28__default.createElement("div", { className: "flex flex-col items-center text-green-600" }, /* @__PURE__ */ React28__default.createElement(CheckCircle, { className: "h-10 w-10" }), /* @__PURE__ */ React28__default.createElement("p", { className: "text-xl font-semibold mt-1" }, t("practiceFlow.results.passed"))) : /* @__PURE__ */ React28__default.createElement("div", { className: "flex flex-col items-center text-destructive" }, /* @__PURE__ */ React28__default.createElement(XCircle, { className: "h-10 w-10" }), /* @__PURE__ */ React28__default.createElement("p", { className: "text-xl font-semibold mt-1" }, t("practiceFlow.results.failed"))))))), /* @__PURE__ */ React28__default.createElement("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4 text-sm" }, /* @__PURE__ */ React28__default.createElement("div", { className: "flex items-center space-x-2 p-3 bg-muted rounded-md" }, /* @__PURE__ */ React28__default.createElement(Clock, { className: "h-5 w-5 text-primary" }), /* @__PURE__ */ React28__default.createElement("span", null, t("practiceFlow.results.timeSpent")), /* @__PURE__ */ React28__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__default.createElement("div", { className: "flex items-center space-x-2 p-3 bg-muted rounded-md" }, /* @__PURE__ */ React28__default.createElement(Percent, { className: "h-5 w-5 text-primary" }), /* @__PURE__ */ React28__default.createElement("span", null, t("practiceFlow.results.avgTimePerQuestion")), /* @__PURE__ */ React28__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__default.createElement(Card, null, /* @__PURE__ */ React28__default.createElement(CardHeader, null, /* @__PURE__ */ React28__default.createElement(CardTitle, { className: "text-lg" }, "SCORM Sync Status")), /* @__PURE__ */ React28__default.createElement(CardContent, null, /* @__PURE__ */ React28__default.createElement("p", { className: `flex items-center ${result.scormStatus === "error" ? "text-destructive" : "text-muted-foreground"}` }, result.scormStatus === "error" && /* @__PURE__ */ React28__default.createElement(AlertTriangle, { className: "mr-2 h-4 w-4" }), "Status: ", /* @__PURE__ */ React28__default.createElement("span", { className: "font-semibold ml-1" }, result.scormStatus)), result.scormError && /* @__PURE__ */ React28__default.createElement("p", { className: "text-xs text-destructive mt-1" }, "Details: ", result.scormError))), result.webhookStatus && result.webhookStatus !== "idle" && /* @__PURE__ */ React28__default.createElement(Card, null, /* @__PURE__ */ React28__default.createElement(CardHeader, null, /* @__PURE__ */ React28__default.createElement(CardTitle, { className: "text-lg" }, "Webhook Sync Status")), /* @__PURE__ */ React28__default.createElement(CardContent, null, /* @__PURE__ */ React28__default.createElement("p", { className: `flex items-center ${result.webhookStatus === "error" ? "text-destructive" : "text-muted-foreground"}` }, result.webhookStatus === "error" && /* @__PURE__ */ React28__default.createElement(AlertTriangle, { className: "mr-2 h-4 w-4" }), "Status: ", /* @__PURE__ */ React28__default.createElement("span", { className: "font-semibold ml-1" }, result.webhookStatus)), result.webhookError && /* @__PURE__ */ React28__default.createElement("p", { className: "text-xs text-destructive mt-1" }, "Details: ", result.webhookError))), /* @__PURE__ */ React28__default.createElement(Accordion, { type: "single", collapsible: true, className: "w-full" }, /* @__PURE__ */ React28__default.createElement(AccordionItem, { value: "question-breakdown" }, /* @__PURE__ */ React28__default.createElement(AccordionTrigger, { className: "text-lg font-semibold" }, t("practiceFlow.results.questionBreakdown")), /* @__PURE__ */ React28__default.createElement(AccordionContent, null, /* @__PURE__ */ React28__default.createElement(ScrollArea, { className: "h-[300px] pr-4" }, /* @__PURE__ */ React28__default.createElement("ul", { className: "space-y-4" }, result.questionResults.map((qResult, index) => {
|
|
2702
3639
|
var _a2, _b2;
|
|
2703
|
-
return /* @__PURE__ */
|
|
2704
|
-
}))))))), /* @__PURE__ */
|
|
3640
|
+
return /* @__PURE__ */ React28__default.createElement("li", { key: qResult.questionId, className: "p-4 border rounded-md bg-background" }, /* @__PURE__ */ React28__default.createElement("div", { className: "flex justify-between items-center mb-2" }, /* @__PURE__ */ React28__default.createElement("h4", { className: "font-semibold" }, t("common.questions", { count: index + 1 })), qResult.isCorrect ? /* @__PURE__ */ React28__default.createElement("span", { className: "text-green-600 font-medium flex items-center" }, /* @__PURE__ */ React28__default.createElement(CheckCircle, { className: "mr-1 h-4 w-4" }), " ", t("practiceFlow.results.passed")) : /* @__PURE__ */ React28__default.createElement("span", { className: "text-destructive font-medium flex items-center" }, /* @__PURE__ */ React28__default.createElement(XCircle, { className: "mr-1 h-4 w-4" }), " ", t("practiceFlow.results.failed"))), /* @__PURE__ */ React28__default.createElement("p", { className: "text-sm" }, /* @__PURE__ */ React28__default.createElement("span", { className: "font-medium" }, t("practiceFlow.results.yourAnswer")), " ", getAnswerDisplay(qResult.userAnswer)), !qResult.isCorrect && /* @__PURE__ */ React28__default.createElement("p", { className: "text-sm" }, /* @__PURE__ */ React28__default.createElement("span", { className: "font-medium" }, t("practiceFlow.results.correctAnswer")), " ", getAnswerDisplay(qResult.correctAnswer)), /* @__PURE__ */ React28__default.createElement("p", { className: "text-xs text-muted-foreground" }, /* @__PURE__ */ React28__default.createElement("span", { className: "font-medium" }, t("practiceFlow.results.pointsEarned")), " ", qResult.pointsEarned), /* @__PURE__ */ React28__default.createElement("p", { className: "text-xs text-muted-foreground" }, /* @__PURE__ */ React28__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")));
|
|
3641
|
+
}))))))), /* @__PURE__ */ React28__default.createElement(CardFooter, { className: "flex flex-col sm:flex-row justify-between gap-2" }, onExitQuiz && /* @__PURE__ */ React28__default.createElement(Button, { variant: "outline", onClick: onExitQuiz, className: "w-full sm:w-auto" }, /* @__PURE__ */ React28__default.createElement(LogOut, { className: "mr-2 h-4 w-4" }), t("common.exit")), showReviewButton && onGenerateReview && /* @__PURE__ */ React28__default.createElement(
|
|
3642
|
+
Button,
|
|
3643
|
+
{
|
|
3644
|
+
onClick: onGenerateReview,
|
|
3645
|
+
disabled: isReviewLoading,
|
|
3646
|
+
className: "w-full sm:w-auto"
|
|
3647
|
+
},
|
|
3648
|
+
isReviewLoading ? /* @__PURE__ */ React28__default.createElement(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : /* @__PURE__ */ React28__default.createElement(Wand2, { className: "mr-2 h-4 w-4" }),
|
|
3649
|
+
isReviewLoading ? t("practiceFlow.results.generatingReview") : t("practiceFlow.results.generateReview")
|
|
3650
|
+
)));
|
|
2705
3651
|
};
|
|
2706
3652
|
|
|
2707
3653
|
// src/react-ui/components/ui/QuizPlayer.tsx
|
|
@@ -2717,49 +3663,65 @@ var QuizPlayer = ({ quizConfig, onQuizComplete, onExitQuiz }) => {
|
|
|
2717
3663
|
const [timeLeft, setTimeLeft] = useState(null);
|
|
2718
3664
|
const [isLoading, setIsLoading] = useState(true);
|
|
2719
3665
|
const [error, setError] = useState(null);
|
|
3666
|
+
const { t } = useTranslation();
|
|
2720
3667
|
const programmingQuestionRef = useRef(null);
|
|
3668
|
+
const engineRef = useRef(null);
|
|
3669
|
+
const isInitializedRef = useRef(false);
|
|
3670
|
+
const callbacks = useMemo(() => ({
|
|
3671
|
+
onQuizStart: (initialData) => {
|
|
3672
|
+
setCurrentQuestion(initialData.initialQuestion);
|
|
3673
|
+
setCurrentQuestionNumber(initialData.currentQuestionNumber);
|
|
3674
|
+
setTotalQuestions(initialData.totalQuestions);
|
|
3675
|
+
setTimeLeft(initialData.timeLimitInSeconds);
|
|
3676
|
+
setIsLoading(false);
|
|
3677
|
+
},
|
|
3678
|
+
onQuestionChange: (question, qNum, total) => {
|
|
3679
|
+
var _a2;
|
|
3680
|
+
setCurrentQuestion(question);
|
|
3681
|
+
setCurrentQuestionNumber(qNum);
|
|
3682
|
+
setTotalQuestions(total);
|
|
3683
|
+
const existingAnswer = (_a2 = engineRef.current) == null ? void 0 : _a2.getUserAnswer((question == null ? void 0 : question.id) || "");
|
|
3684
|
+
setUserAnswer(existingAnswer !== void 0 ? existingAnswer : null);
|
|
3685
|
+
},
|
|
3686
|
+
onQuizFinish: (results) => {
|
|
3687
|
+
setFinalResult(results);
|
|
3688
|
+
setQuizFinished(true);
|
|
3689
|
+
onQuizComplete(results);
|
|
3690
|
+
setIsLoading(false);
|
|
3691
|
+
},
|
|
3692
|
+
onTimeTick: (timeLeftInSeconds) => {
|
|
3693
|
+
setTimeLeft(timeLeftInSeconds);
|
|
3694
|
+
},
|
|
3695
|
+
onQuizTimeUp: () => {
|
|
3696
|
+
setError("Time's up! Your quiz has been submitted automatically.");
|
|
3697
|
+
}
|
|
3698
|
+
}), [onQuizComplete]);
|
|
2721
3699
|
const handleAnswerChange = useCallback((answer) => {
|
|
2722
3700
|
if ((currentQuestion == null ? void 0 : currentQuestion.questionType) !== "blockly_programming" && (currentQuestion == null ? void 0 : currentQuestion.questionType) !== "scratch_programming") {
|
|
2723
3701
|
setUserAnswer(answer);
|
|
2724
3702
|
}
|
|
2725
|
-
}, [currentQuestion]);
|
|
3703
|
+
}, [currentQuestion == null ? void 0 : currentQuestion.questionType]);
|
|
3704
|
+
const quizConfigKey = useMemo(() => {
|
|
3705
|
+
return JSON.stringify({
|
|
3706
|
+
id: quizConfig.id,
|
|
3707
|
+
version: quizConfig.version,
|
|
3708
|
+
title: quizConfig.title
|
|
3709
|
+
});
|
|
3710
|
+
}, [quizConfig.id, quizConfig.version, quizConfig.title]);
|
|
2726
3711
|
useEffect(() => {
|
|
3712
|
+
if (isInitializedRef.current) {
|
|
3713
|
+
return;
|
|
3714
|
+
}
|
|
2727
3715
|
setIsLoading(true);
|
|
2728
3716
|
setError(null);
|
|
2729
3717
|
setQuizFinished(false);
|
|
2730
3718
|
setFinalResult(null);
|
|
2731
3719
|
setUserAnswer(null);
|
|
3720
|
+
isInitializedRef.current = true;
|
|
2732
3721
|
let localQuizEngine = null;
|
|
2733
3722
|
try {
|
|
2734
|
-
const callbacks = {
|
|
2735
|
-
onQuizStart: (initialData) => {
|
|
2736
|
-
setCurrentQuestion(initialData.initialQuestion);
|
|
2737
|
-
setCurrentQuestionNumber(initialData.currentQuestionNumber);
|
|
2738
|
-
setTotalQuestions(initialData.totalQuestions);
|
|
2739
|
-
setTimeLeft(initialData.timeLimitInSeconds);
|
|
2740
|
-
setIsLoading(false);
|
|
2741
|
-
},
|
|
2742
|
-
onQuestionChange: (question, qNum, total) => {
|
|
2743
|
-
setCurrentQuestion(question);
|
|
2744
|
-
setCurrentQuestionNumber(qNum);
|
|
2745
|
-
setTotalQuestions(total);
|
|
2746
|
-
const existingAnswer = engine == null ? void 0 : engine.getUserAnswer((question == null ? void 0 : question.id) || "");
|
|
2747
|
-
setUserAnswer(existingAnswer !== void 0 ? existingAnswer : null);
|
|
2748
|
-
},
|
|
2749
|
-
onQuizFinish: (results) => {
|
|
2750
|
-
setFinalResult(results);
|
|
2751
|
-
setQuizFinished(true);
|
|
2752
|
-
onQuizComplete(results);
|
|
2753
|
-
setIsLoading(false);
|
|
2754
|
-
},
|
|
2755
|
-
onTimeTick: (timeLeftInSeconds) => {
|
|
2756
|
-
setTimeLeft(timeLeftInSeconds);
|
|
2757
|
-
},
|
|
2758
|
-
onQuizTimeUp: () => {
|
|
2759
|
-
setError("Time's up! Your quiz has been submitted automatically.");
|
|
2760
|
-
}
|
|
2761
|
-
};
|
|
2762
3723
|
localQuizEngine = new QuizEngine({ config: quizConfig, callbacks });
|
|
3724
|
+
engineRef.current = localQuizEngine;
|
|
2763
3725
|
setEngine(localQuizEngine);
|
|
2764
3726
|
const initialQ = localQuizEngine.getCurrentQuestion();
|
|
2765
3727
|
setCurrentQuestion(initialQ);
|
|
@@ -2771,84 +3733,88 @@ var QuizPlayer = ({ quizConfig, onQuizComplete, onExitQuiz }) => {
|
|
|
2771
3733
|
setUserAnswer(existingAnswer !== void 0 ? existingAnswer : null);
|
|
2772
3734
|
}
|
|
2773
3735
|
setIsLoading(false);
|
|
2774
|
-
return () => {
|
|
2775
|
-
if (localQuizEngine) {
|
|
2776
|
-
localQuizEngine.destroy();
|
|
2777
|
-
}
|
|
2778
|
-
};
|
|
2779
3736
|
} catch (e) {
|
|
2780
|
-
console.error("Error initializing QuizEngine:", e);
|
|
2781
3737
|
setError(e instanceof Error ? e.message : "Failed to load quiz.");
|
|
2782
3738
|
setIsLoading(false);
|
|
2783
|
-
|
|
2784
|
-
localQuizEngine.destroy();
|
|
2785
|
-
}
|
|
3739
|
+
isInitializedRef.current = false;
|
|
2786
3740
|
}
|
|
2787
|
-
|
|
2788
|
-
|
|
3741
|
+
return () => {
|
|
3742
|
+
if (localQuizEngine) localQuizEngine.destroy();
|
|
3743
|
+
if (engineRef.current) engineRef.current = null;
|
|
3744
|
+
isInitializedRef.current = false;
|
|
3745
|
+
};
|
|
3746
|
+
}, [quizConfigKey, callbacks, quizConfig]);
|
|
3747
|
+
const handleSubmitAnswer = useCallback(() => {
|
|
2789
3748
|
var _a2;
|
|
2790
|
-
|
|
3749
|
+
const currentEngine = engineRef.current;
|
|
3750
|
+
if (!currentEngine || !currentQuestion) return;
|
|
2791
3751
|
let answerToSubmit = null;
|
|
2792
3752
|
if (currentQuestion.questionType === "blockly_programming" || currentQuestion.questionType === "scratch_programming") {
|
|
2793
3753
|
if (programmingQuestionRef.current && typeof programmingQuestionRef.current.getWorkspaceXml === "function") {
|
|
2794
3754
|
answerToSubmit = programmingQuestionRef.current.getWorkspaceXml();
|
|
2795
3755
|
} else {
|
|
2796
|
-
|
|
2797
|
-
answerToSubmit = (_a2 = engine.getUserAnswer(currentQuestion.id)) != null ? _a2 : null;
|
|
3756
|
+
answerToSubmit = (_a2 = currentEngine.getUserAnswer(currentQuestion.id)) != null ? _a2 : null;
|
|
2798
3757
|
}
|
|
2799
3758
|
} else {
|
|
2800
3759
|
answerToSubmit = userAnswer;
|
|
2801
3760
|
}
|
|
2802
3761
|
if (answerToSubmit !== void 0) {
|
|
2803
|
-
|
|
2804
|
-
}
|
|
2805
|
-
};
|
|
2806
|
-
const handleNext = () => {
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
}
|
|
2815
|
-
};
|
|
2816
|
-
const handlePrevious = () => {
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
};
|
|
2822
|
-
const handleFinishQuiz = async () => {
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
};
|
|
3762
|
+
currentEngine.submitAnswer(currentQuestion.id, answerToSubmit);
|
|
3763
|
+
}
|
|
3764
|
+
}, [currentQuestion, userAnswer]);
|
|
3765
|
+
const handleNext = useCallback(() => {
|
|
3766
|
+
const currentEngine = engineRef.current;
|
|
3767
|
+
if (!currentEngine) return;
|
|
3768
|
+
handleSubmitAnswer();
|
|
3769
|
+
if (currentEngine.getCurrentQuestionNumber() < currentEngine.getTotalQuestions()) {
|
|
3770
|
+
currentEngine.nextQuestion();
|
|
3771
|
+
} else {
|
|
3772
|
+
handleFinishQuiz();
|
|
3773
|
+
}
|
|
3774
|
+
}, [handleSubmitAnswer]);
|
|
3775
|
+
const handlePrevious = useCallback(() => {
|
|
3776
|
+
const currentEngine = engineRef.current;
|
|
3777
|
+
if (!currentEngine || currentEngine.getCurrentQuestionNumber() <= 1) return;
|
|
3778
|
+
handleSubmitAnswer();
|
|
3779
|
+
currentEngine.previousQuestion();
|
|
3780
|
+
}, [handleSubmitAnswer]);
|
|
3781
|
+
const handleFinishQuiz = useCallback(async () => {
|
|
3782
|
+
const currentEngine = engineRef.current;
|
|
3783
|
+
if (!currentEngine) return;
|
|
3784
|
+
setIsLoading(true);
|
|
3785
|
+
handleSubmitAnswer();
|
|
3786
|
+
await currentEngine.calculateResults();
|
|
3787
|
+
}, [handleSubmitAnswer]);
|
|
2829
3788
|
const progressPercent = useMemo(() => {
|
|
2830
3789
|
if (totalQuestions === 0) return 0;
|
|
2831
3790
|
return quizFinished ? 100 : Math.max(0, Math.min(100, (currentQuestionNumber - 1) / totalQuestions * 100));
|
|
2832
3791
|
}, [currentQuestionNumber, totalQuestions, quizFinished]);
|
|
3792
|
+
const formatTime = useCallback((seconds) => {
|
|
3793
|
+
if (seconds === null) return "-:--";
|
|
3794
|
+
const mins = Math.floor(seconds / 60);
|
|
3795
|
+
const secs = seconds % 60;
|
|
3796
|
+
return `${mins}:${secs < 10 ? "0" : ""}${secs}`;
|
|
3797
|
+
}, []);
|
|
2833
3798
|
if (isLoading) {
|
|
2834
|
-
return /* @__PURE__ */
|
|
3799
|
+
return /* @__PURE__ */ React28__default.createElement("div", { className: "flex flex-col items-center justify-center h-64" }, /* @__PURE__ */ React28__default.createElement(Loader2, { className: "h-12 w-12 animate-spin text-primary" }), /* @__PURE__ */ React28__default.createElement("p", { className: "mt-4 text-muted-foreground" }, t("common.loading")));
|
|
2835
3800
|
}
|
|
2836
3801
|
if (error) {
|
|
2837
|
-
return /* @__PURE__ */
|
|
3802
|
+
return /* @__PURE__ */ React28__default.createElement(Card, { className: "w-full max-w-2xl mx-auto shadow-xl" }, /* @__PURE__ */ React28__default.createElement(CardHeader, null, /* @__PURE__ */ React28__default.createElement(CardTitle, { className: "text-destructive flex items-center" }, /* @__PURE__ */ React28__default.createElement(AlertCircle, { className: "mr-2 h-6 w-6" }), "Quiz Error")), /* @__PURE__ */ React28__default.createElement(CardContent, null, /* @__PURE__ */ React28__default.createElement("p", null, error)), /* @__PURE__ */ React28__default.createElement(CardFooter, null, onExitQuiz && /* @__PURE__ */ React28__default.createElement(Button, { variant: "outline", onClick: onExitQuiz }, /* @__PURE__ */ React28__default.createElement(LogOut, { className: "mr-2 h-4 w-4" }), " ", t("common.exit"))));
|
|
2838
3803
|
}
|
|
2839
3804
|
if (quizFinished && finalResult) {
|
|
2840
|
-
return /* @__PURE__ */
|
|
3805
|
+
return /* @__PURE__ */ React28__default.createElement(QuizResult, { result: finalResult, onExitQuiz, quizTitle: quizConfig.title });
|
|
2841
3806
|
}
|
|
2842
3807
|
if (!currentQuestion) {
|
|
2843
|
-
return /* @__PURE__ */
|
|
3808
|
+
return /* @__PURE__ */ React28__default.createElement(Card, { className: "w-full max-w-2xl mx-auto shadow-xl" }, /* @__PURE__ */ React28__default.createElement(CardHeader, null, /* @__PURE__ */ React28__default.createElement(CardTitle, null, "Quiz Ended"), /* @__PURE__ */ React28__default.createElement(CardDescription, null, "No more questions, or quiz not loaded correctly.")), /* @__PURE__ */ React28__default.createElement(CardFooter, null, onExitQuiz && /* @__PURE__ */ React28__default.createElement(Button, { variant: "outline", onClick: onExitQuiz }, /* @__PURE__ */ React28__default.createElement(LogOut, { className: "mr-2 h-4 w-4" }), " ", t("common.exit"))));
|
|
2844
3809
|
}
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
3810
|
+
return /* @__PURE__ */ React28__default.createElement(Card, { className: "w-full max-w-3xl mx-auto shadow-xl" }, /* @__PURE__ */ React28__default.createElement(CardHeader, null, /* @__PURE__ */ React28__default.createElement("div", { className: "flex justify-between items-center" }, /* @__PURE__ */ React28__default.createElement(CardTitle, { className: "text-2xl font-headline" }, quizConfig.title), timeLeft !== null && /* @__PURE__ */ React28__default.createElement("div", { className: "flex items-center text-sm text-muted-foreground" }, /* @__PURE__ */ React28__default.createElement(Clock, { className: "mr-1 h-4 w-4" }), t("practiceFlow.player.timeLeft", { time: formatTime(timeLeft) }))), quizConfig.description && /* @__PURE__ */ React28__default.createElement(CardDescription, null, quizConfig.description), /* @__PURE__ */ React28__default.createElement("div", { className: "mt-2" }, /* @__PURE__ */ React28__default.createElement(
|
|
3811
|
+
Progress,
|
|
3812
|
+
{
|
|
3813
|
+
value: progressPercent,
|
|
3814
|
+
"aria-label": `Quiz progress: ${currentQuestionNumber} of ${totalQuestions} questions`,
|
|
3815
|
+
className: "w-full"
|
|
3816
|
+
}
|
|
3817
|
+
), /* @__PURE__ */ React28__default.createElement("p", { className: "text-sm text-muted-foreground mt-1 text-right" }, t("practiceFlow.player.questionProgress", { current: currentQuestionNumber, total: totalQuestions })))), /* @__PURE__ */ React28__default.createElement(CardContent, { className: "min-h-[200px]" }, /* @__PURE__ */ React28__default.createElement(
|
|
2852
3818
|
QuestionRenderer,
|
|
2853
3819
|
{
|
|
2854
3820
|
question: currentQuestion,
|
|
@@ -2858,7 +3824,25 @@ var QuizPlayer = ({ quizConfig, onQuizComplete, onExitQuiz }) => {
|
|
|
2858
3824
|
key: currentQuestion.id,
|
|
2859
3825
|
ref: currentQuestion.questionType === "blockly_programming" || currentQuestion.questionType === "scratch_programming" ? programmingQuestionRef : null
|
|
2860
3826
|
}
|
|
2861
|
-
)), /* @__PURE__ */
|
|
3827
|
+
)), /* @__PURE__ */ React28__default.createElement(CardFooter, { className: "flex justify-between items-center" }, /* @__PURE__ */ React28__default.createElement(
|
|
3828
|
+
Button,
|
|
3829
|
+
{
|
|
3830
|
+
variant: "outline",
|
|
3831
|
+
onClick: handlePrevious,
|
|
3832
|
+
disabled: currentQuestionNumber <= 1
|
|
3833
|
+
},
|
|
3834
|
+
/* @__PURE__ */ React28__default.createElement(ChevronLeft, { className: "mr-2 h-4 w-4" }),
|
|
3835
|
+
" ",
|
|
3836
|
+
t("common.previous")
|
|
3837
|
+
), onExitQuiz && /* @__PURE__ */ React28__default.createElement(
|
|
3838
|
+
Button,
|
|
3839
|
+
{
|
|
3840
|
+
variant: "ghost",
|
|
3841
|
+
onClick: onExitQuiz,
|
|
3842
|
+
className: "text-muted-foreground hover:text-destructive"
|
|
3843
|
+
},
|
|
3844
|
+
t("common.exit")
|
|
3845
|
+
), /* @__PURE__ */ React28__default.createElement(Button, { onClick: handleNext }, currentQuestionNumber === totalQuestions ? t("practiceFlow.player.finishQuiz") : t("common.next"), currentQuestionNumber !== totalQuestions && /* @__PURE__ */ React28__default.createElement(ChevronRight, { className: "ml-2 h-4 w-4" }), currentQuestionNumber === totalQuestions && /* @__PURE__ */ React28__default.createElement(CheckCircle, { className: "ml-2 h-4 w-4" }))));
|
|
2862
3846
|
};
|
|
2863
3847
|
|
|
2864
3848
|
// src/player.ts
|
|
@@ -2887,20 +3871,20 @@ function mountQuizPlayer(targetElementId, quizConfig) {
|
|
|
2887
3871
|
}
|
|
2888
3872
|
};
|
|
2889
3873
|
if (quizResult) {
|
|
2890
|
-
return
|
|
3874
|
+
return React28__default.createElement(QuizResult, {
|
|
2891
3875
|
result: quizResult,
|
|
2892
3876
|
quizTitle: quizConfig.title,
|
|
2893
3877
|
onExitQuiz: handleExit
|
|
2894
3878
|
});
|
|
2895
3879
|
}
|
|
2896
|
-
return
|
|
3880
|
+
return React28__default.createElement(QuizPlayer, {
|
|
2897
3881
|
quizConfig,
|
|
2898
3882
|
onQuizComplete: handleQuizComplete,
|
|
2899
3883
|
onExitQuiz: handleExit
|
|
2900
3884
|
});
|
|
2901
3885
|
};
|
|
2902
3886
|
const root = ReactDOM.createRoot(targetElement);
|
|
2903
|
-
root.render(
|
|
3887
|
+
root.render(React28__default.createElement(React28__default.StrictMode, null, React28__default.createElement(AppContainer)));
|
|
2904
3888
|
}
|
|
2905
3889
|
|
|
2906
3890
|
export { mountQuizPlayer };
|