@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/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { genkit } from 'genkit';
|
|
3
|
+
import { gemini20Flash, googleAI } from '@genkit-ai/googleai';
|
|
2
4
|
import JSZip from 'jszip';
|
|
3
5
|
import { clsx } from 'clsx';
|
|
4
6
|
import { twMerge } from 'tailwind-merge';
|
|
@@ -270,6 +272,847 @@ var SCORMService = class {
|
|
|
270
272
|
}
|
|
271
273
|
};
|
|
272
274
|
|
|
275
|
+
// src/services/evaluators/multiple-choice-evaluator.ts
|
|
276
|
+
var MultipleChoiceEvaluator = class {
|
|
277
|
+
async evaluate(question, answer) {
|
|
278
|
+
var _a;
|
|
279
|
+
const points = (_a = question.points) != null ? _a : 0;
|
|
280
|
+
const correctAnswerId = question.correctAnswerId;
|
|
281
|
+
const isCorrect = answer === correctAnswerId;
|
|
282
|
+
const correctOption = question.options.find((opt) => opt.id === correctAnswerId);
|
|
283
|
+
const correctAnswerDetail = {
|
|
284
|
+
id: correctAnswerId,
|
|
285
|
+
value: (correctOption == null ? void 0 : correctOption.text) || ""
|
|
286
|
+
};
|
|
287
|
+
return Promise.resolve({
|
|
288
|
+
isCorrect,
|
|
289
|
+
correctAnswer: correctAnswerDetail,
|
|
290
|
+
pointsEarned: isCorrect ? points : 0
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// src/services/evaluators/multiple-response-evaluator.ts
|
|
296
|
+
var MultipleResponseEvaluator = class {
|
|
297
|
+
async evaluate(question, answer) {
|
|
298
|
+
var _a;
|
|
299
|
+
const points = (_a = question.points) != null ? _a : 0;
|
|
300
|
+
const correctAnswerIds = question.correctAnswerIds;
|
|
301
|
+
let isCorrect = false;
|
|
302
|
+
if (Array.isArray(answer)) {
|
|
303
|
+
const userAnswerSet = new Set(answer);
|
|
304
|
+
const correctAnswerSet = new Set(correctAnswerIds);
|
|
305
|
+
isCorrect = userAnswerSet.size === correctAnswerSet.size && [...userAnswerSet].every((id) => correctAnswerSet.has(id));
|
|
306
|
+
}
|
|
307
|
+
const correctValues = correctAnswerIds.map(
|
|
308
|
+
(id) => {
|
|
309
|
+
var _a2;
|
|
310
|
+
return ((_a2 = question.options.find((opt) => opt.id === id)) == null ? void 0 : _a2.text) || "";
|
|
311
|
+
}
|
|
312
|
+
);
|
|
313
|
+
const correctAnswerDetail = {
|
|
314
|
+
id: correctAnswerIds,
|
|
315
|
+
value: correctValues
|
|
316
|
+
};
|
|
317
|
+
return Promise.resolve({
|
|
318
|
+
isCorrect,
|
|
319
|
+
correctAnswer: correctAnswerDetail,
|
|
320
|
+
pointsEarned: isCorrect ? points : 0
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// src/services/evaluators/true-false-evaluator.ts
|
|
326
|
+
var TrueFalseEvaluator = class {
|
|
327
|
+
async evaluate(question, answer) {
|
|
328
|
+
var _a;
|
|
329
|
+
const points = (_a = question.points) != null ? _a : 0;
|
|
330
|
+
const correctAnswer = question.correctAnswer;
|
|
331
|
+
let userAnswer = answer;
|
|
332
|
+
if (typeof answer === "string") {
|
|
333
|
+
userAnswer = answer.toLowerCase() === "true";
|
|
334
|
+
}
|
|
335
|
+
const isCorrect = typeof userAnswer === "boolean" && userAnswer === correctAnswer;
|
|
336
|
+
const correctAnswerDetail = {
|
|
337
|
+
id: null,
|
|
338
|
+
value: correctAnswer
|
|
339
|
+
};
|
|
340
|
+
return Promise.resolve({
|
|
341
|
+
isCorrect,
|
|
342
|
+
correctAnswer: correctAnswerDetail,
|
|
343
|
+
pointsEarned: isCorrect ? points : 0
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
// src/services/evaluators/short-answer-evaluator.ts
|
|
349
|
+
var ShortAnswerEvaluator = class {
|
|
350
|
+
async evaluate(question, answer) {
|
|
351
|
+
var _a, _b;
|
|
352
|
+
const points = (_a = question.points) != null ? _a : 0;
|
|
353
|
+
let isCorrect = false;
|
|
354
|
+
if (typeof answer === "string") {
|
|
355
|
+
const userAnswerTrimmed = answer.trim();
|
|
356
|
+
const caseSensitive = (_b = question.isCaseSensitive) != null ? _b : false;
|
|
357
|
+
isCorrect = question.acceptedAnswers.some(
|
|
358
|
+
(accAns) => caseSensitive ? accAns.trim() === userAnswerTrimmed : accAns.trim().toLowerCase() === userAnswerTrimmed.toLowerCase()
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
const correctAnswerDetail = {
|
|
362
|
+
id: null,
|
|
363
|
+
value: question.acceptedAnswers
|
|
364
|
+
};
|
|
365
|
+
return Promise.resolve({
|
|
366
|
+
isCorrect,
|
|
367
|
+
correctAnswer: correctAnswerDetail,
|
|
368
|
+
pointsEarned: isCorrect ? points : 0
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
// src/services/evaluators/numeric-evaluator.ts
|
|
374
|
+
var NumericEvaluator = class {
|
|
375
|
+
async evaluate(question, answer) {
|
|
376
|
+
var _a;
|
|
377
|
+
const points = (_a = question.points) != null ? _a : 0;
|
|
378
|
+
let isCorrect = false;
|
|
379
|
+
if (typeof answer === "string" || typeof answer === "number") {
|
|
380
|
+
const userAnswerNum = parseFloat(String(answer));
|
|
381
|
+
if (!isNaN(userAnswerNum)) {
|
|
382
|
+
isCorrect = question.tolerance != null ? Math.abs(userAnswerNum - question.answer) <= question.tolerance : userAnswerNum === question.answer;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
const correctAnswerDetail = {
|
|
386
|
+
id: null,
|
|
387
|
+
value: question.answer
|
|
388
|
+
};
|
|
389
|
+
return Promise.resolve({
|
|
390
|
+
isCorrect,
|
|
391
|
+
correctAnswer: correctAnswerDetail,
|
|
392
|
+
pointsEarned: isCorrect ? points : 0
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
// src/services/evaluators/sequence-evaluator.ts
|
|
398
|
+
var SequenceEvaluator = class {
|
|
399
|
+
async evaluate(question, answer) {
|
|
400
|
+
var _a;
|
|
401
|
+
const points = (_a = question.points) != null ? _a : 0;
|
|
402
|
+
let isCorrect = false;
|
|
403
|
+
if (Array.isArray(answer) && answer.length === question.correctOrder.length) {
|
|
404
|
+
isCorrect = answer.every((itemId, index) => itemId === question.correctOrder[index]);
|
|
405
|
+
}
|
|
406
|
+
const correctValues = question.correctOrder.map(
|
|
407
|
+
(id) => {
|
|
408
|
+
var _a2;
|
|
409
|
+
return ((_a2 = question.items.find((item) => item.id === id)) == null ? void 0 : _a2.content) || "";
|
|
410
|
+
}
|
|
411
|
+
);
|
|
412
|
+
const correctAnswerDetail = {
|
|
413
|
+
id: question.correctOrder,
|
|
414
|
+
value: correctValues
|
|
415
|
+
};
|
|
416
|
+
return Promise.resolve({
|
|
417
|
+
isCorrect,
|
|
418
|
+
correctAnswer: correctAnswerDetail,
|
|
419
|
+
pointsEarned: isCorrect ? points : 0
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
// src/services/evaluators/matching-evaluator.ts
|
|
425
|
+
var MatchingEvaluator = class {
|
|
426
|
+
async evaluate(question, answer) {
|
|
427
|
+
var _a;
|
|
428
|
+
const points = (_a = question.points) != null ? _a : 0;
|
|
429
|
+
let isCorrect = false;
|
|
430
|
+
if (typeof answer === "object" && answer !== null && !Array.isArray(answer)) {
|
|
431
|
+
const userAnswerMap = answer;
|
|
432
|
+
isCorrect = question.correctAnswerMap.length === Object.keys(userAnswerMap).length && question.correctAnswerMap.every((map) => userAnswerMap[map.promptId] === map.optionId);
|
|
433
|
+
}
|
|
434
|
+
const correctMap = question.correctAnswerMap.reduce((acc, curr) => {
|
|
435
|
+
var _a2, _b;
|
|
436
|
+
const promptText = ((_a2 = question.prompts.find((p) => p.id === curr.promptId)) == null ? void 0 : _a2.content) || "";
|
|
437
|
+
const optionText = ((_b = question.options.find((o) => o.id === curr.optionId)) == null ? void 0 : _b.content) || "";
|
|
438
|
+
acc[promptText] = optionText;
|
|
439
|
+
return acc;
|
|
440
|
+
}, {});
|
|
441
|
+
const correctAnswerDetail = {
|
|
442
|
+
id: null,
|
|
443
|
+
value: correctMap
|
|
444
|
+
};
|
|
445
|
+
return Promise.resolve({
|
|
446
|
+
isCorrect,
|
|
447
|
+
correctAnswer: correctAnswerDetail,
|
|
448
|
+
pointsEarned: isCorrect ? points : 0
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// src/services/evaluators/fill-in-the-blanks-evaluator.ts
|
|
454
|
+
var FillInTheBlanksEvaluator = class {
|
|
455
|
+
async evaluate(question, answer) {
|
|
456
|
+
var _a;
|
|
457
|
+
const points = (_a = question.points) != null ? _a : 0;
|
|
458
|
+
let isCorrect = false;
|
|
459
|
+
if (typeof answer === "object" && answer !== null && !Array.isArray(answer)) {
|
|
460
|
+
const userAnswerMap = answer;
|
|
461
|
+
isCorrect = question.answers.length > 0 && question.answers.every((correctAnsDef) => {
|
|
462
|
+
var _a2, _b;
|
|
463
|
+
const userValForBlank = (_a2 = userAnswerMap[correctAnsDef.blankId]) == null ? void 0 : _a2.trim();
|
|
464
|
+
if (userValForBlank === void 0) return false;
|
|
465
|
+
const caseSensitive = (_b = question.isCaseSensitive) != null ? _b : false;
|
|
466
|
+
return correctAnsDef.acceptedValues.some(
|
|
467
|
+
(accVal) => caseSensitive ? accVal.trim() === userValForBlank : accVal.trim().toLowerCase() === userValForBlank.toLowerCase()
|
|
468
|
+
);
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
const correctMap = question.answers.reduce((acc, curr) => {
|
|
472
|
+
acc[curr.blankId] = curr.acceptedValues.join(" | ");
|
|
473
|
+
return acc;
|
|
474
|
+
}, {});
|
|
475
|
+
const correctAnswerDetail = {
|
|
476
|
+
id: null,
|
|
477
|
+
value: correctMap
|
|
478
|
+
};
|
|
479
|
+
return Promise.resolve({
|
|
480
|
+
isCorrect,
|
|
481
|
+
correctAnswer: correctAnswerDetail,
|
|
482
|
+
pointsEarned: isCorrect ? points : 0
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
// src/services/evaluators/drag-and-drop-evaluator.ts
|
|
488
|
+
var DragAndDropEvaluator = class {
|
|
489
|
+
async evaluate(question, answer) {
|
|
490
|
+
var _a;
|
|
491
|
+
const points = (_a = question.points) != null ? _a : 0;
|
|
492
|
+
let isCorrect = false;
|
|
493
|
+
if (typeof answer === "object" && answer !== null && !Array.isArray(answer)) {
|
|
494
|
+
const userAnswerMap = answer;
|
|
495
|
+
isCorrect = question.answerMap.length === Object.keys(userAnswerMap).length && question.answerMap.every((map) => userAnswerMap[map.draggableId] === map.dropZoneId);
|
|
496
|
+
}
|
|
497
|
+
const correctMap = question.answerMap.reduce((acc, curr) => {
|
|
498
|
+
var _a2, _b;
|
|
499
|
+
const draggableText = ((_a2 = question.draggableItems.find((d) => d.id === curr.draggableId)) == null ? void 0 : _a2.content) || "";
|
|
500
|
+
const dropZoneText = ((_b = question.dropZones.find((z4) => z4.id === curr.dropZoneId)) == null ? void 0 : _b.label) || "";
|
|
501
|
+
acc[draggableText] = dropZoneText;
|
|
502
|
+
return acc;
|
|
503
|
+
}, {});
|
|
504
|
+
const correctAnswerDetail = {
|
|
505
|
+
id: null,
|
|
506
|
+
value: correctMap
|
|
507
|
+
};
|
|
508
|
+
return Promise.resolve({
|
|
509
|
+
isCorrect,
|
|
510
|
+
correctAnswer: correctAnswerDetail,
|
|
511
|
+
pointsEarned: isCorrect ? points : 0
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
// src/services/evaluators/hotspot-evaluator.ts
|
|
517
|
+
var HotspotEvaluator = class {
|
|
518
|
+
async evaluate(question, answer) {
|
|
519
|
+
var _a;
|
|
520
|
+
const points = (_a = question.points) != null ? _a : 0;
|
|
521
|
+
let isCorrect = false;
|
|
522
|
+
if (Array.isArray(answer)) {
|
|
523
|
+
const userAnswerSet = new Set(answer);
|
|
524
|
+
const correctAnswerSet = new Set(question.correctHotspotIds);
|
|
525
|
+
isCorrect = userAnswerSet.size === correctAnswerSet.size && [...userAnswerSet].every((id) => correctAnswerSet.has(id));
|
|
526
|
+
}
|
|
527
|
+
const correctValues = question.correctHotspotIds.map(
|
|
528
|
+
(id) => {
|
|
529
|
+
var _a2;
|
|
530
|
+
return ((_a2 = question.hotspots.find((h) => h.id === id)) == null ? void 0 : _a2.description) || id;
|
|
531
|
+
}
|
|
532
|
+
);
|
|
533
|
+
const correctAnswerDetail = {
|
|
534
|
+
id: question.correctHotspotIds,
|
|
535
|
+
value: correctValues
|
|
536
|
+
};
|
|
537
|
+
return Promise.resolve({
|
|
538
|
+
isCorrect,
|
|
539
|
+
correctAnswer: correctAnswerDetail,
|
|
540
|
+
pointsEarned: isCorrect ? points : 0
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
// src/services/evaluators/programming-evaluator.ts
|
|
546
|
+
var ProgrammingEvaluator = class {
|
|
547
|
+
async evaluate(question, answer) {
|
|
548
|
+
var _a, _b;
|
|
549
|
+
const points = (_a = question.points) != null ? _a : 0;
|
|
550
|
+
let isCorrect = false;
|
|
551
|
+
if (typeof answer === "string" && typeof question.solutionGeneratedCode === "string") {
|
|
552
|
+
if (typeof window !== "undefined" && ((_b = window.Blockly) == null ? void 0 : _b.JavaScript)) {
|
|
553
|
+
const LocalBlockly = window.Blockly;
|
|
554
|
+
let generatedUserCode = "";
|
|
555
|
+
try {
|
|
556
|
+
const tempWorkspace = new LocalBlockly.Workspace();
|
|
557
|
+
const dom = LocalBlockly.Xml.textToDom(answer);
|
|
558
|
+
LocalBlockly.Xml.domToWorkspace(dom, tempWorkspace);
|
|
559
|
+
generatedUserCode = LocalBlockly.JavaScript.workspaceToCode(tempWorkspace) || "";
|
|
560
|
+
const normalize = (code) => code.replace(/\s+/g, " ").trim();
|
|
561
|
+
isCorrect = normalize(generatedUserCode) === normalize(question.solutionGeneratedCode);
|
|
562
|
+
tempWorkspace.dispose();
|
|
563
|
+
} catch (e) {
|
|
564
|
+
console.error(`Error generating code from user's ${question.questionType} XML for evaluation:`, e);
|
|
565
|
+
isCorrect = false;
|
|
566
|
+
}
|
|
567
|
+
} else {
|
|
568
|
+
console.warn(`Blockly library not available for ${question.questionType} evaluation. Skipping code comparison.`);
|
|
569
|
+
isCorrect = false;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
const correctAnswerDetail = {
|
|
573
|
+
id: null,
|
|
574
|
+
value: question.solutionGeneratedCode || ""
|
|
575
|
+
};
|
|
576
|
+
return Promise.resolve({
|
|
577
|
+
isCorrect,
|
|
578
|
+
correctAnswer: correctAnswerDetail,
|
|
579
|
+
pointsEarned: isCorrect ? points : 0
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
// src/utils/jsonUtils.ts
|
|
585
|
+
var JsonRepairEngine = class {
|
|
586
|
+
/**
|
|
587
|
+
* Attempts to repair unterminated strings in JSON.
|
|
588
|
+
* NOTE: This is a heuristic approach and may not be perfect for all cases.
|
|
589
|
+
*/
|
|
590
|
+
static repairUnterminatedStrings(jsonStr) {
|
|
591
|
+
let repaired = jsonStr;
|
|
592
|
+
let inString = false;
|
|
593
|
+
let escaped = false;
|
|
594
|
+
let lastQuoteIndex = -1;
|
|
595
|
+
for (let i = 0; i < repaired.length; i++) {
|
|
596
|
+
const char = repaired[i];
|
|
597
|
+
if (escaped) {
|
|
598
|
+
escaped = false;
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
if (char === "\\") {
|
|
602
|
+
escaped = true;
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
if (char === '"') {
|
|
606
|
+
inString = !inString;
|
|
607
|
+
if (inString) {
|
|
608
|
+
lastQuoteIndex = i;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
if (inString && lastQuoteIndex !== -1) {
|
|
613
|
+
const beforeUnterminated = repaired.substring(0, lastQuoteIndex + 1);
|
|
614
|
+
const afterUnterminated = repaired.substring(lastQuoteIndex + 1);
|
|
615
|
+
const breakPoints = [",", "}", "]", "\n"];
|
|
616
|
+
let breakIndex = -1;
|
|
617
|
+
for (let i = 0; i < afterUnterminated.length; i++) {
|
|
618
|
+
if (breakPoints.includes(afterUnterminated[i])) {
|
|
619
|
+
breakIndex = i;
|
|
620
|
+
break;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if (breakIndex !== -1) {
|
|
624
|
+
const stringContent = afterUnterminated.substring(0, breakIndex);
|
|
625
|
+
const remainder = afterUnterminated.substring(breakIndex);
|
|
626
|
+
const escapedContent = stringContent.replace(new RegExp('(?<!\\\\)"', "g"), '\\"');
|
|
627
|
+
repaired = beforeUnterminated + escapedContent + '"' + remainder;
|
|
628
|
+
} else {
|
|
629
|
+
const escapedContent = afterUnterminated.replace(new RegExp('(?<!\\\\)"', "g"), '\\"');
|
|
630
|
+
repaired = beforeUnterminated + escapedContent + '"';
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return repaired;
|
|
634
|
+
}
|
|
635
|
+
// FIX: Replaced unsafe single quote replacement with a stateful parser.
|
|
636
|
+
/**
|
|
637
|
+
* Safely replaces single quotes with double quotes only for keys and string values,
|
|
638
|
+
* ignoring apostrophes inside already double-quoted strings.
|
|
639
|
+
*/
|
|
640
|
+
static safelyFixQuotes(jsonStr) {
|
|
641
|
+
let result = "";
|
|
642
|
+
let inDoubleQuoteString = false;
|
|
643
|
+
let escaped = false;
|
|
644
|
+
for (let i = 0; i < jsonStr.length; i++) {
|
|
645
|
+
const char = jsonStr[i];
|
|
646
|
+
if (escaped) {
|
|
647
|
+
result += char;
|
|
648
|
+
escaped = false;
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
if (char === "\\") {
|
|
652
|
+
escaped = true;
|
|
653
|
+
result += char;
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
if (char === '"') {
|
|
657
|
+
inDoubleQuoteString = !inDoubleQuoteString;
|
|
658
|
+
}
|
|
659
|
+
if (char === "'" && !inDoubleQuoteString) {
|
|
660
|
+
result += '"';
|
|
661
|
+
} else {
|
|
662
|
+
result += char;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return result;
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Fixes common JSON formatting issues using more robust methods.
|
|
669
|
+
*/
|
|
670
|
+
static applyCommonFixes(jsonStr) {
|
|
671
|
+
let fixed = jsonStr;
|
|
672
|
+
fixed = this.safelyFixQuotes(fixed);
|
|
673
|
+
fixed = fixed.replace(/,\s*([}\]])/g, "$1");
|
|
674
|
+
fixed = fixed.replace(/("|}|\d|]|true|false|null)\s*\n\s*(")/g, "$1,\n$2");
|
|
675
|
+
fixed = fixed.replace(/"[\s\S]*?"/g, (match) => {
|
|
676
|
+
const content = match.substring(1, match.length - 1);
|
|
677
|
+
const fixedContent = content.replace(/\n/g, "\\n").replace(/\r/g, "\\r");
|
|
678
|
+
return `"${fixedContent}"`;
|
|
679
|
+
});
|
|
680
|
+
fixed = fixed.replace(/"(true|false|null)"/g, "$1");
|
|
681
|
+
return fixed;
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Validates JSON by attempting to parse and providing detailed error info.
|
|
685
|
+
*/
|
|
686
|
+
static validateAndGetError(jsonStr) {
|
|
687
|
+
try {
|
|
688
|
+
JSON.parse(jsonStr);
|
|
689
|
+
return { isValid: true };
|
|
690
|
+
} catch (error) {
|
|
691
|
+
const errorMessage = error.message || "";
|
|
692
|
+
const positionMatch = errorMessage.match(/position (\d+)/);
|
|
693
|
+
const position = positionMatch ? parseInt(positionMatch[1], 10) : void 0;
|
|
694
|
+
return {
|
|
695
|
+
isValid: false,
|
|
696
|
+
error: errorMessage,
|
|
697
|
+
position
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Main repair function that attempts multiple strategies.
|
|
703
|
+
*/
|
|
704
|
+
static repairJson(jsonStr) {
|
|
705
|
+
var _a;
|
|
706
|
+
let current = jsonStr.trim();
|
|
707
|
+
const maxAttempts = 5;
|
|
708
|
+
let lastError = "";
|
|
709
|
+
let lastPosition = -1;
|
|
710
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
711
|
+
const validation = this.validateAndGetError(current);
|
|
712
|
+
if (validation.isValid) {
|
|
713
|
+
return current;
|
|
714
|
+
}
|
|
715
|
+
console.warn(`JSON repair attempt ${attempt + 1}: ${validation.error}`);
|
|
716
|
+
if (validation.error === lastError && validation.position === lastPosition) {
|
|
717
|
+
console.error("Repair attempt stuck on the same error, aborting this strategy.");
|
|
718
|
+
if (validation.position) {
|
|
719
|
+
const truncated = current.substring(0, validation.position);
|
|
720
|
+
const openBraces = (truncated.match(/{/g) || []).length;
|
|
721
|
+
const closeBraces = (truncated.match(/}/g) || []).length;
|
|
722
|
+
const openBrackets = (truncated.match(/\[/g) || []).length;
|
|
723
|
+
const closeBrackets = (truncated.match(/\]/g) || []).length;
|
|
724
|
+
let repaired = truncated.replace(/,\s*$/, "");
|
|
725
|
+
for (let i = 0; i < openBrackets - closeBrackets; i++) repaired += "]";
|
|
726
|
+
for (let i = 0; i < openBraces - closeBraces; i++) repaired += "}";
|
|
727
|
+
current = repaired;
|
|
728
|
+
const finalValidation = this.validateAndGetError(current);
|
|
729
|
+
if (finalValidation.isValid) return current;
|
|
730
|
+
}
|
|
731
|
+
break;
|
|
732
|
+
}
|
|
733
|
+
lastError = validation.error || "";
|
|
734
|
+
lastPosition = validation.position;
|
|
735
|
+
if ((_a = validation.error) == null ? void 0 : _a.includes("Unterminated string")) {
|
|
736
|
+
current = this.repairUnterminatedStrings(current);
|
|
737
|
+
} else {
|
|
738
|
+
current = this.applyCommonFixes(current);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
try {
|
|
742
|
+
let finalAttempt = this.applyCommonFixes(jsonStr.trim());
|
|
743
|
+
finalAttempt = this.repairUnterminatedStrings(finalAttempt);
|
|
744
|
+
JSON.parse(finalAttempt);
|
|
745
|
+
return finalAttempt;
|
|
746
|
+
} catch (e) {
|
|
747
|
+
throw new Error(`Unable to repair JSON after ${maxAttempts} attempts. Last known error: ${lastError}`);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
function extractJsonFromMarkdown(text) {
|
|
752
|
+
if (!text) {
|
|
753
|
+
throw new Error("Input text is empty or null.");
|
|
754
|
+
}
|
|
755
|
+
const trimmedText = text.trim();
|
|
756
|
+
try {
|
|
757
|
+
JSON.parse(trimmedText);
|
|
758
|
+
return trimmedText;
|
|
759
|
+
} catch (e) {
|
|
760
|
+
}
|
|
761
|
+
const markdownPatterns = [
|
|
762
|
+
/```(?:json|JSON)\s*([\s\S]*?)\s*```/,
|
|
763
|
+
// ```json ... ```
|
|
764
|
+
/```\s*({[\s\S]*?}|\[[\s\S]*?\])\s*```/
|
|
765
|
+
// ``` { ... } ``` or ``` [ ... ] ```
|
|
766
|
+
];
|
|
767
|
+
for (const pattern of markdownPatterns) {
|
|
768
|
+
const match = trimmedText.match(pattern);
|
|
769
|
+
if (match && match[1]) {
|
|
770
|
+
const content = match[1].trim();
|
|
771
|
+
try {
|
|
772
|
+
JSON.parse(content);
|
|
773
|
+
return content;
|
|
774
|
+
} catch (e) {
|
|
775
|
+
console.warn("JSON inside markdown block is invalid, attempting repair...");
|
|
776
|
+
try {
|
|
777
|
+
return JsonRepairEngine.repairJson(content);
|
|
778
|
+
} catch (repairError) {
|
|
779
|
+
console.warn(`Markdown block repair failed: ${repairError.message}. Trying other strategies...`);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
const firstBrace = trimmedText.indexOf("{");
|
|
785
|
+
const firstBracket = trimmedText.indexOf("[");
|
|
786
|
+
let startIndex = -1;
|
|
787
|
+
if (firstBrace === -1 && firstBracket === -1) ; else if (firstBrace === -1) {
|
|
788
|
+
startIndex = firstBracket;
|
|
789
|
+
} else if (firstBracket === -1) {
|
|
790
|
+
startIndex = firstBrace;
|
|
791
|
+
} else {
|
|
792
|
+
startIndex = Math.min(firstBrace, firstBracket);
|
|
793
|
+
}
|
|
794
|
+
if (startIndex !== -1) {
|
|
795
|
+
const textToProcess = trimmedText.substring(startIndex);
|
|
796
|
+
let balance = 0;
|
|
797
|
+
let inString = false;
|
|
798
|
+
let escaped = false;
|
|
799
|
+
const startChar = textToProcess[0];
|
|
800
|
+
const endChar = startChar === "{" ? "}" : "]";
|
|
801
|
+
for (let i = 0; i < textToProcess.length; i++) {
|
|
802
|
+
const char = textToProcess[i];
|
|
803
|
+
if (escaped) {
|
|
804
|
+
escaped = false;
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
if (char === "\\") {
|
|
808
|
+
escaped = true;
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
if (char === '"') {
|
|
812
|
+
inString = !inString;
|
|
813
|
+
}
|
|
814
|
+
if (!inString) {
|
|
815
|
+
if (char === startChar) balance++;
|
|
816
|
+
if (char === endChar) balance--;
|
|
817
|
+
}
|
|
818
|
+
if (balance === 0 && i > 0) {
|
|
819
|
+
const potentialJson = textToProcess.substring(0, i + 1);
|
|
820
|
+
try {
|
|
821
|
+
JSON.parse(potentialJson);
|
|
822
|
+
return potentialJson;
|
|
823
|
+
} catch (e) {
|
|
824
|
+
console.warn(`Balanced JSON segment is invalid, attempting repair...`);
|
|
825
|
+
try {
|
|
826
|
+
return JsonRepairEngine.repairJson(potentialJson);
|
|
827
|
+
} catch (repairError) {
|
|
828
|
+
console.warn(`Repair failed for balanced segment: ${repairError.message}`);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
break;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
console.warn("All extraction strategies failed, attempting to repair the entire input text as a last resort.");
|
|
836
|
+
try {
|
|
837
|
+
return JsonRepairEngine.repairJson(trimmedText);
|
|
838
|
+
} catch (finalError) {
|
|
839
|
+
throw new Error(`Unable to extract or repair valid JSON from AI response. Preview: "${trimmedText.substring(0, 100)}...". Final error: ${finalError.message}`);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
z.object({
|
|
843
|
+
language: z.custom(),
|
|
844
|
+
problemPrompt: z.string(),
|
|
845
|
+
userCode: z.string(),
|
|
846
|
+
testCase: z.custom()
|
|
847
|
+
});
|
|
848
|
+
var AIEvaluationOutputSchema = z.object({
|
|
849
|
+
passed: z.boolean().describe("Did the user's code produce the expected output for the given input?"),
|
|
850
|
+
actualOutput: z.any().describe("The actual output produced by the user's code."),
|
|
851
|
+
reasoning: z.string().describe("A brief explanation of why the code passed or failed, or if there was a syntax error.")
|
|
852
|
+
});
|
|
853
|
+
var EvaluateUserCodeOutputSchema = AIEvaluationOutputSchema;
|
|
854
|
+
|
|
855
|
+
// src/ai/flows/evaluate-user-code.ts
|
|
856
|
+
async function evaluateUserCode(clientInput, apiKey) {
|
|
857
|
+
try {
|
|
858
|
+
const ai = genkit({
|
|
859
|
+
plugins: [googleAI({ apiKey })],
|
|
860
|
+
model: gemini20Flash
|
|
861
|
+
});
|
|
862
|
+
const { language, problemPrompt, userCode, testCase } = clientInput;
|
|
863
|
+
const promptText = `
|
|
864
|
+
You are an expert Code Judge and Teaching Assistant for a ${language} programming course.
|
|
865
|
+
Your task is to evaluate a student's code submission for a specific problem against a single test case.
|
|
866
|
+
|
|
867
|
+
## Problem Description
|
|
868
|
+
${problemPrompt}
|
|
869
|
+
|
|
870
|
+
## Student's Code Submission
|
|
871
|
+
\`\`\`${language}
|
|
872
|
+
${userCode}
|
|
873
|
+
\`\`\`
|
|
874
|
+
|
|
875
|
+
## Test Case to Evaluate
|
|
876
|
+
- Input(s): ${JSON.stringify(testCase.input)}
|
|
877
|
+
- Expected Output: ${JSON.stringify(testCase.expectedOutput)}
|
|
878
|
+
|
|
879
|
+
## Your Task
|
|
880
|
+
1. **Analyze Execution:** Mentally execute the student's code with the provided input(s).
|
|
881
|
+
2. **Determine Output:** Figure out what the actual output of the code would be.
|
|
882
|
+
3. **Compare:** Compare the actual output with the expected output.
|
|
883
|
+
4. **Handle Errors:** If the code has a syntax error or would crash, treat it as a failure.
|
|
884
|
+
5. **Provide Reasoning:** Briefly explain your conclusion. If it failed, explain why (e.g., "incorrect result", "infinite loop", "syntax error on line 5").
|
|
885
|
+
|
|
886
|
+
**CRITICAL JSON OUTPUT FORMAT:**
|
|
887
|
+
Return ONLY the JSON object with this EXACT structure.
|
|
888
|
+
|
|
889
|
+
\`\`\`json
|
|
890
|
+
{
|
|
891
|
+
"passed": false,
|
|
892
|
+
"actualOutput": 5,
|
|
893
|
+
"reasoning": "The function correctly summed the numbers but did not filter for only even numbers."
|
|
894
|
+
}
|
|
895
|
+
\`\`\`
|
|
896
|
+
|
|
897
|
+
Return only the JSON response.`;
|
|
898
|
+
const response = await ai.generate(promptText);
|
|
899
|
+
const rawText = response.text;
|
|
900
|
+
const jsonText = extractJsonFromMarkdown(rawText);
|
|
901
|
+
const aiGeneratedContent = JSON.parse(jsonText);
|
|
902
|
+
return EvaluateUserCodeOutputSchema.parse(aiGeneratedContent);
|
|
903
|
+
} catch (error) {
|
|
904
|
+
console.error("Error evaluating user code:", error);
|
|
905
|
+
if (error instanceof z.ZodError) {
|
|
906
|
+
throw new Error(`AI evaluation output validation failed: ${error.message}`);
|
|
907
|
+
}
|
|
908
|
+
return {
|
|
909
|
+
passed: false,
|
|
910
|
+
actualOutput: "Evaluation Error",
|
|
911
|
+
reasoning: `The AI judge failed to process the code. Error: ${error.message}`
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// src/services/APIKeyService.ts
|
|
917
|
+
var GEMINI_API_KEY_SERVICE_NAME = "gemini";
|
|
918
|
+
var LOCAL_STORAGE_PREFIX = "iqk_api_keys_";
|
|
919
|
+
function _encode(data) {
|
|
920
|
+
if (typeof window !== "undefined" && typeof window.btoa === "function") {
|
|
921
|
+
try {
|
|
922
|
+
return window.btoa(data);
|
|
923
|
+
} catch (e) {
|
|
924
|
+
console.error("Base64 encoding (btoa) failed:", e);
|
|
925
|
+
return data;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
return data;
|
|
929
|
+
}
|
|
930
|
+
function _decode(data) {
|
|
931
|
+
if (typeof window !== "undefined" && typeof window.atob === "function") {
|
|
932
|
+
try {
|
|
933
|
+
return window.atob(data);
|
|
934
|
+
} catch (e) {
|
|
935
|
+
console.error("Base64 decoding (atob) failed:", e);
|
|
936
|
+
return data;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
return data;
|
|
940
|
+
}
|
|
941
|
+
var APIKeyService = class {
|
|
942
|
+
static getStorageKey(serviceName) {
|
|
943
|
+
return `${LOCAL_STORAGE_PREFIX}${serviceName}`;
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Saves an API key to localStorage. The key is mildly obfuscated using Base64.
|
|
947
|
+
* @param serviceName - The name of the service (e.g., 'gemini').
|
|
948
|
+
* @param apiKey - The API key to save.
|
|
949
|
+
*/
|
|
950
|
+
static saveAPIKey(serviceName, apiKey) {
|
|
951
|
+
if (typeof window !== "undefined" && window.localStorage) {
|
|
952
|
+
try {
|
|
953
|
+
const encodedKey = _encode(apiKey);
|
|
954
|
+
localStorage.setItem(this.getStorageKey(serviceName), encodedKey);
|
|
955
|
+
} catch (e) {
|
|
956
|
+
console.error(`Error saving API key for ${serviceName} to localStorage:`, e);
|
|
957
|
+
}
|
|
958
|
+
} else {
|
|
959
|
+
console.warn("localStorage is not available. APIKeyService cannot save keys.");
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Retrieves an API key from localStorage.
|
|
964
|
+
* @param serviceName - The name of the service.
|
|
965
|
+
* @returns The decoded API key, or null if not found or if localStorage is unavailable.
|
|
966
|
+
*/
|
|
967
|
+
static getAPIKey(serviceName) {
|
|
968
|
+
if (typeof window !== "undefined" && window.localStorage) {
|
|
969
|
+
try {
|
|
970
|
+
const storedKey = localStorage.getItem(this.getStorageKey(serviceName));
|
|
971
|
+
if (storedKey) {
|
|
972
|
+
return _decode(storedKey);
|
|
973
|
+
}
|
|
974
|
+
} catch (e) {
|
|
975
|
+
console.error(`Error retrieving API key for ${serviceName} from localStorage:`, e);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
return null;
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Removes an API key from localStorage.
|
|
982
|
+
* @param serviceName - The name of the service.
|
|
983
|
+
*/
|
|
984
|
+
static removeAPIKey(serviceName) {
|
|
985
|
+
if (typeof window !== "undefined" && window.localStorage) {
|
|
986
|
+
try {
|
|
987
|
+
localStorage.removeItem(this.getStorageKey(serviceName));
|
|
988
|
+
} catch (e) {
|
|
989
|
+
console.error(`Error removing API key for ${serviceName} from localStorage:`, e);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Checks if an API key exists in localStorage for the given service.
|
|
995
|
+
* @param serviceName - The name of the service.
|
|
996
|
+
* @returns True if a key exists, false otherwise.
|
|
997
|
+
*/
|
|
998
|
+
static hasAPIKey(serviceName) {
|
|
999
|
+
return this.getAPIKey(serviceName) !== null;
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
|
|
1003
|
+
// src/services/CodeEvaluationService.ts
|
|
1004
|
+
var CodeEvaluationService = class {
|
|
1005
|
+
constructor() {
|
|
1006
|
+
this.apiKey = APIKeyService.getAPIKey(GEMINI_API_KEY_SERVICE_NAME);
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Evaluates a user's code against a single test case using an AI judge.
|
|
1010
|
+
* @param question The full CodingQuestion object.
|
|
1011
|
+
* @param userCode The user's submitted code string.
|
|
1012
|
+
* @param testCase The specific TestCase to evaluate against.
|
|
1013
|
+
* @returns A promise that resolves to an EvaluationResult object.
|
|
1014
|
+
*/
|
|
1015
|
+
async evaluateSingleTestCase(question, userCode, testCase) {
|
|
1016
|
+
if (!this.apiKey) {
|
|
1017
|
+
return {
|
|
1018
|
+
testCaseId: testCase.id,
|
|
1019
|
+
passed: false,
|
|
1020
|
+
actualOutput: "Configuration Error",
|
|
1021
|
+
reasoning: "API Key is not configured."
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
const aiResult = await evaluateUserCode({
|
|
1025
|
+
language: question.language,
|
|
1026
|
+
problemPrompt: question.prompt,
|
|
1027
|
+
userCode,
|
|
1028
|
+
testCase
|
|
1029
|
+
}, this.apiKey);
|
|
1030
|
+
return __spreadValues({
|
|
1031
|
+
testCaseId: testCase.id
|
|
1032
|
+
}, aiResult);
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Evaluates user's code against all test cases for a given question.
|
|
1036
|
+
* @param question The full CodingQuestion object.
|
|
1037
|
+
* @param userCode The user's submitted code string.
|
|
1038
|
+
* @returns A promise that resolves to an array of EvaluationResult objects.
|
|
1039
|
+
*/
|
|
1040
|
+
async evaluateAllTestCases(question, userCode) {
|
|
1041
|
+
const results = [];
|
|
1042
|
+
for (const testCase of question.testCases) {
|
|
1043
|
+
const result = await this.evaluateSingleTestCase(question, userCode, testCase);
|
|
1044
|
+
results.push(result);
|
|
1045
|
+
}
|
|
1046
|
+
return results;
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Evaluates user's code against only the public test cases for a given question.
|
|
1050
|
+
* Useful for a "Run Tests" button before final submission.
|
|
1051
|
+
* @param question The full CodingQuestion object.
|
|
1052
|
+
* @param userCode The user's submitted code string.
|
|
1053
|
+
* @returns A promise that resolves to an array of EvaluationResult objects.
|
|
1054
|
+
*/
|
|
1055
|
+
async evaluatePublicTestCases(question, userCode) {
|
|
1056
|
+
const publicTestCases = question.testCases.filter((tc) => tc.isPublic);
|
|
1057
|
+
const results = [];
|
|
1058
|
+
for (const testCase of publicTestCases) {
|
|
1059
|
+
const result = await this.evaluateSingleTestCase(question, userCode, testCase);
|
|
1060
|
+
results.push(result);
|
|
1061
|
+
}
|
|
1062
|
+
return results;
|
|
1063
|
+
}
|
|
1064
|
+
};
|
|
1065
|
+
|
|
1066
|
+
// src/services/evaluators/coding-evaluator.ts
|
|
1067
|
+
var CodingEvaluator = class {
|
|
1068
|
+
async evaluate(question, answer) {
|
|
1069
|
+
var _a;
|
|
1070
|
+
const points = (_a = question.points) != null ? _a : 0;
|
|
1071
|
+
if (typeof answer !== "string" || !answer.trim()) {
|
|
1072
|
+
return {
|
|
1073
|
+
isCorrect: false,
|
|
1074
|
+
correctAnswer: { id: null, value: question.solutionCode },
|
|
1075
|
+
pointsEarned: 0,
|
|
1076
|
+
evaluationDetails: question.testCases.map((tc) => ({
|
|
1077
|
+
testCaseId: tc.id,
|
|
1078
|
+
passed: false,
|
|
1079
|
+
actualOutput: "No submission",
|
|
1080
|
+
reasoning: "User did not submit any code."
|
|
1081
|
+
}))
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
try {
|
|
1085
|
+
const evaluationService = new CodeEvaluationService();
|
|
1086
|
+
const testCaseResults = await evaluationService.evaluateAllTestCases(question, answer);
|
|
1087
|
+
const isCorrect = testCaseResults.every((result) => result.passed);
|
|
1088
|
+
const correctAnswerDetail = {
|
|
1089
|
+
id: null,
|
|
1090
|
+
value: question.solutionCode
|
|
1091
|
+
};
|
|
1092
|
+
return {
|
|
1093
|
+
isCorrect,
|
|
1094
|
+
correctAnswer: correctAnswerDetail,
|
|
1095
|
+
pointsEarned: isCorrect ? points : 0,
|
|
1096
|
+
evaluationDetails: testCaseResults
|
|
1097
|
+
// Pass through the detailed results
|
|
1098
|
+
};
|
|
1099
|
+
} catch (error) {
|
|
1100
|
+
console.error("A critical error occurred during code evaluation:", error);
|
|
1101
|
+
return {
|
|
1102
|
+
isCorrect: false,
|
|
1103
|
+
correctAnswer: { id: null, value: question.solutionCode },
|
|
1104
|
+
pointsEarned: 0,
|
|
1105
|
+
evaluationDetails: question.testCases.map((tc) => ({
|
|
1106
|
+
testCaseId: tc.id,
|
|
1107
|
+
passed: false,
|
|
1108
|
+
actualOutput: "Evaluation Error",
|
|
1109
|
+
reasoning: error instanceof Error ? error.message : "An unknown error occurred."
|
|
1110
|
+
}))
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
|
|
273
1116
|
// src/services/QuizEngine.ts
|
|
274
1117
|
var QuizEngine = class {
|
|
275
1118
|
constructor(options) {
|
|
@@ -286,6 +1129,8 @@ var QuizEngine = class {
|
|
|
286
1129
|
this.callbacks = options.callbacks || {};
|
|
287
1130
|
this.questions = ((_a = this.config.settings) == null ? void 0 : _a.shuffleQuestions) ? [...this.config.questions].sort(() => Math.random() - 0.5) : this.config.questions;
|
|
288
1131
|
this.overallStartTime = Date.now();
|
|
1132
|
+
this.evaluators = /* @__PURE__ */ new Map();
|
|
1133
|
+
this.registerEvaluators();
|
|
289
1134
|
if (((_b = this.config.settings) == null ? void 0 : _b.timeLimitMinutes) && this.config.settings.timeLimitMinutes > 0) {
|
|
290
1135
|
this.timeLeftInSeconds = this.config.settings.timeLimitMinutes * 60;
|
|
291
1136
|
}
|
|
@@ -324,6 +1169,22 @@ var QuizEngine = class {
|
|
|
324
1169
|
}
|
|
325
1170
|
(_e = (_d = this.callbacks).onQuestionChange) == null ? void 0 : _e.call(_d, initialQ, this.getCurrentQuestionNumber(), this.getTotalQuestions());
|
|
326
1171
|
}
|
|
1172
|
+
registerEvaluators() {
|
|
1173
|
+
this.evaluators.set("multiple_choice", new MultipleChoiceEvaluator());
|
|
1174
|
+
this.evaluators.set("multiple_response", new MultipleResponseEvaluator());
|
|
1175
|
+
this.evaluators.set("true_false", new TrueFalseEvaluator());
|
|
1176
|
+
this.evaluators.set("short_answer", new ShortAnswerEvaluator());
|
|
1177
|
+
this.evaluators.set("numeric", new NumericEvaluator());
|
|
1178
|
+
this.evaluators.set("sequence", new SequenceEvaluator());
|
|
1179
|
+
this.evaluators.set("matching", new MatchingEvaluator());
|
|
1180
|
+
this.evaluators.set("fill_in_the_blanks", new FillInTheBlanksEvaluator());
|
|
1181
|
+
this.evaluators.set("drag_and_drop", new DragAndDropEvaluator());
|
|
1182
|
+
this.evaluators.set("hotspot", new HotspotEvaluator());
|
|
1183
|
+
const programmingEvaluator = new ProgrammingEvaluator();
|
|
1184
|
+
this.evaluators.set("blockly_programming", programmingEvaluator);
|
|
1185
|
+
this.evaluators.set("scratch_programming", programmingEvaluator);
|
|
1186
|
+
this.evaluators.set("coding", new CodingEvaluator());
|
|
1187
|
+
}
|
|
327
1188
|
_recordCurrentQuestionTime() {
|
|
328
1189
|
if (this.questionStartTime && this.currentQuestionIndex >= 0 && this.currentQuestionIndex < this.questions.length) {
|
|
329
1190
|
const currentQId = this.questions[this.currentQuestionIndex].id;
|
|
@@ -416,180 +1277,28 @@ var QuizEngine = class {
|
|
|
416
1277
|
}
|
|
417
1278
|
return this.getCurrentQuestion();
|
|
418
1279
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
isCorrect = answer === correctAnswerId;
|
|
435
|
-
break;
|
|
436
|
-
}
|
|
437
|
-
case "multiple_response": {
|
|
438
|
-
const q = question;
|
|
439
|
-
const correctAnswerIds = q.correctAnswerIds;
|
|
440
|
-
const correctValues = correctAnswerIds.map((id) => findOptionText(q, id));
|
|
441
|
-
correctAnswerDetail = { id: correctAnswerIds, value: correctValues };
|
|
442
|
-
if (Array.isArray(answer)) {
|
|
443
|
-
const userAnswerSet = new Set(answer);
|
|
444
|
-
const correctAnswerSet = new Set(correctAnswerIds);
|
|
445
|
-
isCorrect = userAnswerSet.size === correctAnswerSet.size && [...userAnswerSet].every((id) => correctAnswerSet.has(id));
|
|
446
|
-
}
|
|
447
|
-
break;
|
|
448
|
-
}
|
|
449
|
-
case "true_false": {
|
|
450
|
-
const q = question;
|
|
451
|
-
correctAnswerDetail = { id: null, value: q.correctAnswer };
|
|
452
|
-
let tfAnswer = answer;
|
|
453
|
-
if (typeof answer === "string") tfAnswer = answer.toLowerCase() === "true";
|
|
454
|
-
isCorrect = typeof tfAnswer === "boolean" && tfAnswer === q.correctAnswer;
|
|
455
|
-
break;
|
|
456
|
-
}
|
|
457
|
-
case "short_answer": {
|
|
458
|
-
const q = question;
|
|
459
|
-
correctAnswerDetail = { id: null, value: q.acceptedAnswers };
|
|
460
|
-
if (typeof answer === "string") {
|
|
461
|
-
const userAnswerTrimmed = answer.trim();
|
|
462
|
-
const caseSensitive = (_b = q.isCaseSensitive) != null ? _b : false;
|
|
463
|
-
isCorrect = q.acceptedAnswers.some((accAns) => caseSensitive ? accAns.trim() === userAnswerTrimmed : accAns.trim().toLowerCase() === userAnswerTrimmed.toLowerCase());
|
|
464
|
-
}
|
|
465
|
-
break;
|
|
466
|
-
}
|
|
467
|
-
case "numeric": {
|
|
468
|
-
const q = question;
|
|
469
|
-
correctAnswerDetail = { id: null, value: q.answer };
|
|
470
|
-
if (typeof answer === "string" || typeof answer === "number") {
|
|
471
|
-
const userAnswerNum = parseFloat(String(answer));
|
|
472
|
-
if (!isNaN(userAnswerNum)) {
|
|
473
|
-
isCorrect = q.tolerance != null ? Math.abs(userAnswerNum - q.answer) <= q.tolerance : userAnswerNum === q.answer;
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
break;
|
|
477
|
-
}
|
|
478
|
-
case "sequence": {
|
|
479
|
-
const q = question;
|
|
480
|
-
const correctValues = q.correctOrder.map((id) => {
|
|
481
|
-
var _a2;
|
|
482
|
-
return ((_a2 = q.items.find((item) => item.id === id)) == null ? void 0 : _a2.content) || "";
|
|
483
|
-
});
|
|
484
|
-
correctAnswerDetail = { id: q.correctOrder, value: correctValues };
|
|
485
|
-
if (Array.isArray(answer) && answer.length === q.correctOrder.length) {
|
|
486
|
-
isCorrect = answer.every((itemId, index) => itemId === q.correctOrder[index]);
|
|
487
|
-
}
|
|
488
|
-
break;
|
|
489
|
-
}
|
|
490
|
-
case "matching": {
|
|
491
|
-
const q = question;
|
|
492
|
-
const correctMap = q.correctAnswerMap.reduce((acc, curr) => {
|
|
493
|
-
var _a2, _b2;
|
|
494
|
-
const promptText = ((_a2 = q.prompts.find((p) => p.id === curr.promptId)) == null ? void 0 : _a2.content) || "";
|
|
495
|
-
const optionText = ((_b2 = q.options.find((o) => o.id === curr.optionId)) == null ? void 0 : _b2.content) || "";
|
|
496
|
-
acc[promptText] = optionText;
|
|
497
|
-
return acc;
|
|
498
|
-
}, {});
|
|
499
|
-
correctAnswerDetail = { id: null, value: correctMap };
|
|
500
|
-
if (typeof answer === "object" && answer !== null && !Array.isArray(answer)) {
|
|
501
|
-
const userAnswerMap = answer;
|
|
502
|
-
isCorrect = q.correctAnswerMap.length === Object.keys(userAnswerMap).length && q.correctAnswerMap.every((map) => userAnswerMap[map.promptId] === map.optionId);
|
|
503
|
-
}
|
|
504
|
-
break;
|
|
505
|
-
}
|
|
506
|
-
case "fill_in_the_blanks": {
|
|
507
|
-
const q = question;
|
|
508
|
-
const correctMap = q.answers.reduce((acc, curr) => {
|
|
509
|
-
acc[curr.blankId] = curr.acceptedValues.join(" | ");
|
|
510
|
-
return acc;
|
|
511
|
-
}, {});
|
|
512
|
-
correctAnswerDetail = { id: null, value: correctMap };
|
|
513
|
-
if (typeof answer === "object" && answer !== null && !Array.isArray(answer)) {
|
|
514
|
-
const userAnswerMap = answer;
|
|
515
|
-
isCorrect = q.answers.every((correctAnsDef) => {
|
|
516
|
-
var _a2, _b2;
|
|
517
|
-
const userValForBlank = (_a2 = userAnswerMap[correctAnsDef.blankId]) == null ? void 0 : _a2.trim();
|
|
518
|
-
if (userValForBlank === void 0) return false;
|
|
519
|
-
const caseSensitive = (_b2 = q.isCaseSensitive) != null ? _b2 : false;
|
|
520
|
-
return correctAnsDef.acceptedValues.some((accVal) => caseSensitive ? accVal.trim() === userValForBlank : accVal.trim().toLowerCase() === userValForBlank.toLowerCase());
|
|
521
|
-
});
|
|
522
|
-
}
|
|
523
|
-
break;
|
|
524
|
-
}
|
|
525
|
-
case "drag_and_drop": {
|
|
526
|
-
const q = question;
|
|
527
|
-
const correctMap = q.answerMap.reduce((acc, curr) => {
|
|
528
|
-
var _a2, _b2;
|
|
529
|
-
const draggableText = ((_a2 = q.draggableItems.find((d) => d.id === curr.draggableId)) == null ? void 0 : _a2.content) || "";
|
|
530
|
-
const dropZoneText = ((_b2 = q.dropZones.find((z13) => z13.id === curr.dropZoneId)) == null ? void 0 : _b2.label) || "";
|
|
531
|
-
acc[draggableText] = dropZoneText;
|
|
532
|
-
return acc;
|
|
533
|
-
}, {});
|
|
534
|
-
correctAnswerDetail = { id: null, value: correctMap };
|
|
535
|
-
if (typeof answer === "object" && answer !== null && !Array.isArray(answer)) {
|
|
536
|
-
const userAnswerMap = answer;
|
|
537
|
-
isCorrect = q.answerMap.length === Object.keys(userAnswerMap).length && q.answerMap.every((map) => userAnswerMap[map.draggableId] === map.dropZoneId);
|
|
538
|
-
}
|
|
539
|
-
break;
|
|
540
|
-
}
|
|
541
|
-
case "hotspot": {
|
|
542
|
-
const q = question;
|
|
543
|
-
const correctValues = q.correctHotspotIds.map((id) => {
|
|
544
|
-
var _a2;
|
|
545
|
-
return ((_a2 = q.hotspots.find((h) => h.id === id)) == null ? void 0 : _a2.description) || id;
|
|
546
|
-
});
|
|
547
|
-
correctAnswerDetail = { id: q.correctHotspotIds, value: correctValues };
|
|
548
|
-
if (Array.isArray(answer)) {
|
|
549
|
-
const userAnswerSet = new Set(answer);
|
|
550
|
-
const correctAnswerSet = new Set(q.correctHotspotIds);
|
|
551
|
-
isCorrect = userAnswerSet.size === correctAnswerSet.size && [...userAnswerSet].every((id) => correctAnswerSet.has(id));
|
|
552
|
-
}
|
|
553
|
-
break;
|
|
554
|
-
}
|
|
555
|
-
case "blockly_programming":
|
|
556
|
-
case "scratch_programming": {
|
|
557
|
-
const q = question;
|
|
558
|
-
correctAnswerDetail = { id: null, value: q.solutionGeneratedCode || "" };
|
|
559
|
-
if (typeof answer === "string" && typeof q.solutionGeneratedCode === "string") {
|
|
560
|
-
if (typeof window !== "undefined" && ((_c = window.Blockly) == null ? void 0 : _c.JavaScript)) {
|
|
561
|
-
const LocalBlockly = window.Blockly;
|
|
562
|
-
let generatedUserCode = "";
|
|
563
|
-
try {
|
|
564
|
-
const tempWorkspace = new LocalBlockly.Workspace();
|
|
565
|
-
const dom = LocalBlockly.Xml.textToDom(answer);
|
|
566
|
-
LocalBlockly.Xml.domToWorkspace(dom, tempWorkspace);
|
|
567
|
-
generatedUserCode = LocalBlockly.JavaScript.workspaceToCode(tempWorkspace) || "";
|
|
568
|
-
const normalize = (code) => code.replace(/\s+/g, " ").trim();
|
|
569
|
-
isCorrect = normalize(generatedUserCode) === normalize(q.solutionGeneratedCode);
|
|
570
|
-
tempWorkspace.dispose();
|
|
571
|
-
} catch (e) {
|
|
572
|
-
console.error(`Error generating code from user's ${q.questionType} XML for evaluation:`, e);
|
|
573
|
-
isCorrect = false;
|
|
574
|
-
}
|
|
575
|
-
} else {
|
|
576
|
-
console.warn(`Blockly library not available in QuizEngine for ${q.questionType} code generation during evaluation. Skipping code comparison.`);
|
|
577
|
-
isCorrect = false;
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
break;
|
|
581
|
-
}
|
|
582
|
-
default: {
|
|
583
|
-
const _exhaustiveCheck = question;
|
|
584
|
-
console.warn("Unsupported question type in QuizEngine evaluation:", _exhaustiveCheck);
|
|
585
|
-
isCorrect = false;
|
|
586
|
-
correctAnswerDetail = { id: null, value: "Evaluation not implemented." };
|
|
1280
|
+
getElapsedTime() {
|
|
1281
|
+
return Date.now() - this.overallStartTime;
|
|
1282
|
+
}
|
|
1283
|
+
destroy() {
|
|
1284
|
+
this.stopTimer();
|
|
1285
|
+
this._recordCurrentQuestionTime();
|
|
1286
|
+
if (this.scormService && this.scormService.hasAPI()) {
|
|
1287
|
+
if (["initialized", "committed", "sending_data"].includes(this.quizResultState.scormStatus || "")) {
|
|
1288
|
+
const termResult = this.scormService.terminate();
|
|
1289
|
+
if (termResult.success) {
|
|
1290
|
+
this.quizResultState.scormStatus = "terminated";
|
|
1291
|
+
} else {
|
|
1292
|
+
this.quizResultState.scormStatus = "error";
|
|
1293
|
+
this.quizResultState.scormError = termResult.error || "SCORM termination failed on destroy.";
|
|
1294
|
+
}
|
|
587
1295
|
}
|
|
588
1296
|
}
|
|
589
|
-
|
|
1297
|
+
this.scormService = null;
|
|
590
1298
|
}
|
|
1299
|
+
// (Tiếp theo từ Phần 1)
|
|
591
1300
|
async calculateResults() {
|
|
592
|
-
var _a, _b, _c, _d, _e
|
|
1301
|
+
var _a, _b, _c, _d, _e;
|
|
593
1302
|
this.stopTimer();
|
|
594
1303
|
this._recordCurrentQuestionTime();
|
|
595
1304
|
let totalScore = 0;
|
|
@@ -599,92 +1308,31 @@ var QuizEngine = class {
|
|
|
599
1308
|
for (const question of this.questions) {
|
|
600
1309
|
const userAnswerRaw = this.userAnswers.get(question.id) || null;
|
|
601
1310
|
maxScore += (_a = question.points) != null ? _a : 0;
|
|
602
|
-
const
|
|
1311
|
+
const evaluator = this.evaluators.get(question.questionType);
|
|
1312
|
+
if (!evaluator) {
|
|
1313
|
+
console.warn(`No evaluator found for question type: ${question.questionType}`);
|
|
1314
|
+
questionResultsArray.push({
|
|
1315
|
+
questionId: question.id,
|
|
1316
|
+
questionType: question.questionType,
|
|
1317
|
+
prompt: question.prompt,
|
|
1318
|
+
isCorrect: false,
|
|
1319
|
+
pointsEarned: 0,
|
|
1320
|
+
userAnswer: { id: null, value: userAnswerRaw },
|
|
1321
|
+
correctAnswer: { id: null, value: "Evaluation not implemented." },
|
|
1322
|
+
timeSpentSeconds: parseFloat((this.questionTimings.get(question.id) || 0).toFixed(2))
|
|
1323
|
+
});
|
|
1324
|
+
continue;
|
|
1325
|
+
}
|
|
1326
|
+
const {
|
|
1327
|
+
isCorrect,
|
|
1328
|
+
correctAnswer: correctAnswerDetail,
|
|
1329
|
+
pointsEarned,
|
|
1330
|
+
evaluationDetails
|
|
1331
|
+
} = await evaluator.evaluate(question, userAnswerRaw);
|
|
603
1332
|
totalScore += pointsEarned;
|
|
604
1333
|
const timeSpentOnThisQuestion = parseFloat((this.questionTimings.get(question.id) || 0).toFixed(2));
|
|
605
1334
|
accumulatedTotalTimeSpent += timeSpentOnThisQuestion;
|
|
606
|
-
|
|
607
|
-
let allOptions = void 0;
|
|
608
|
-
if (userAnswerRaw !== null) {
|
|
609
|
-
switch (question.questionType) {
|
|
610
|
-
case "multiple_choice": {
|
|
611
|
-
const q = question;
|
|
612
|
-
allOptions = q.options.map((opt) => ({ id: opt.id, value: opt.text }));
|
|
613
|
-
const id = userAnswerRaw;
|
|
614
|
-
userAnswerDetail = { id, value: ((_b = allOptions.find((opt) => opt.id === id)) == null ? void 0 : _b.value) || "" };
|
|
615
|
-
break;
|
|
616
|
-
}
|
|
617
|
-
case "multiple_response": {
|
|
618
|
-
const q = question;
|
|
619
|
-
allOptions = q.options.map((opt) => ({ id: opt.id, value: opt.text }));
|
|
620
|
-
const ids = userAnswerRaw;
|
|
621
|
-
const values = ids.map((id) => {
|
|
622
|
-
var _a2;
|
|
623
|
-
return ((_a2 = allOptions == null ? void 0 : allOptions.find((opt) => opt.id === id)) == null ? void 0 : _a2.value) || "";
|
|
624
|
-
});
|
|
625
|
-
userAnswerDetail = { id: ids, value: values };
|
|
626
|
-
break;
|
|
627
|
-
}
|
|
628
|
-
case "true_false":
|
|
629
|
-
case "short_answer":
|
|
630
|
-
case "numeric":
|
|
631
|
-
userAnswerDetail = { id: null, value: userAnswerRaw };
|
|
632
|
-
break;
|
|
633
|
-
case "sequence": {
|
|
634
|
-
const q = question;
|
|
635
|
-
allOptions = q.items.map((item) => ({ id: item.id, value: item.content }));
|
|
636
|
-
const ids = userAnswerRaw;
|
|
637
|
-
const values = ids.map((id) => {
|
|
638
|
-
var _a2;
|
|
639
|
-
return ((_a2 = allOptions == null ? void 0 : allOptions.find((opt) => opt.id === id)) == null ? void 0 : _a2.value) || "";
|
|
640
|
-
});
|
|
641
|
-
userAnswerDetail = { id: ids, value: values };
|
|
642
|
-
break;
|
|
643
|
-
}
|
|
644
|
-
case "matching": {
|
|
645
|
-
const q = question;
|
|
646
|
-
const userAnswerMap = userAnswerRaw;
|
|
647
|
-
const valueMap = {};
|
|
648
|
-
for (const promptId in userAnswerMap) {
|
|
649
|
-
const optionId = userAnswerMap[promptId];
|
|
650
|
-
const promptText = ((_c = q.prompts.find((p) => p.id === promptId)) == null ? void 0 : _c.content) || "";
|
|
651
|
-
const optionText = ((_d = q.options.find((o) => o.id === optionId)) == null ? void 0 : _d.content) || "";
|
|
652
|
-
valueMap[promptText] = optionText;
|
|
653
|
-
}
|
|
654
|
-
userAnswerDetail = { id: null, value: valueMap };
|
|
655
|
-
break;
|
|
656
|
-
}
|
|
657
|
-
// --- LOGIC MỚI ĐƯỢC THÊM VÀO ---
|
|
658
|
-
case "fill_in_the_blanks": {
|
|
659
|
-
if (typeof userAnswerRaw === "object" && userAnswerRaw !== null && !Array.isArray(userAnswerRaw)) {
|
|
660
|
-
userAnswerDetail = { id: null, value: userAnswerRaw };
|
|
661
|
-
}
|
|
662
|
-
break;
|
|
663
|
-
}
|
|
664
|
-
case "drag_and_drop": {
|
|
665
|
-
const q = question;
|
|
666
|
-
if (typeof userAnswerRaw === "object" && userAnswerRaw !== null && !Array.isArray(userAnswerRaw)) {
|
|
667
|
-
const userAnswerMapByIds = userAnswerRaw;
|
|
668
|
-
const enrichedUserAnswerMap = {};
|
|
669
|
-
for (const draggableId in userAnswerMapByIds) {
|
|
670
|
-
const dropZoneId = userAnswerMapByIds[draggableId];
|
|
671
|
-
const draggableText = ((_e = q.draggableItems.find((d) => d.id === draggableId)) == null ? void 0 : _e.content) || `(ID: ${draggableId})`;
|
|
672
|
-
const dropZoneText = ((_f = q.dropZones.find((z13) => z13.id === dropZoneId)) == null ? void 0 : _f.label) || `(ID: ${dropZoneId})`;
|
|
673
|
-
enrichedUserAnswerMap[draggableText] = dropZoneText;
|
|
674
|
-
}
|
|
675
|
-
userAnswerDetail = { id: null, value: enrichedUserAnswerMap };
|
|
676
|
-
}
|
|
677
|
-
break;
|
|
678
|
-
}
|
|
679
|
-
// ------------------------------------
|
|
680
|
-
// Các loại câu hỏi còn lại vẫn giữ fallback
|
|
681
|
-
case "hotspot":
|
|
682
|
-
case "blockly_programming":
|
|
683
|
-
case "scratch_programming":
|
|
684
|
-
userAnswerDetail = { id: null, value: userAnswerRaw };
|
|
685
|
-
break;
|
|
686
|
-
}
|
|
687
|
-
}
|
|
1335
|
+
const userAnswerDetail = this.formatUserAnswerDetail(question, userAnswerRaw);
|
|
688
1336
|
questionResultsArray.push({
|
|
689
1337
|
questionId: question.id,
|
|
690
1338
|
questionType: question.questionType,
|
|
@@ -693,18 +1341,18 @@ var QuizEngine = class {
|
|
|
693
1341
|
pointsEarned,
|
|
694
1342
|
userAnswer: userAnswerDetail,
|
|
695
1343
|
correctAnswer: correctAnswerDetail,
|
|
696
|
-
|
|
697
|
-
|
|
1344
|
+
timeSpentSeconds: timeSpentOnThisQuestion,
|
|
1345
|
+
evaluationDetails
|
|
698
1346
|
});
|
|
699
1347
|
}
|
|
700
1348
|
const percentage = maxScore > 0 ? parseFloat((totalScore / maxScore * 100).toFixed(2)) : 0;
|
|
701
1349
|
let passed = void 0;
|
|
702
|
-
if (((
|
|
1350
|
+
if (((_b = this.config.settings) == null ? void 0 : _b.passingScorePercent) != null) {
|
|
703
1351
|
passed = percentage >= this.config.settings.passingScorePercent;
|
|
704
1352
|
}
|
|
705
1353
|
const totalQuizTimeSpentSeconds = parseFloat(accumulatedTotalTimeSpent.toFixed(2));
|
|
706
1354
|
const averageTimePerQuestionSeconds = this.questions.length > 0 ? parseFloat((totalQuizTimeSpentSeconds / this.questions.length).toFixed(2)) : 0;
|
|
707
|
-
const metadataPerformance = this._calculateMetadataPerformance();
|
|
1355
|
+
const metadataPerformance = await this._calculateMetadataPerformance();
|
|
708
1356
|
const finalResults = __spreadValues({
|
|
709
1357
|
score: totalScore,
|
|
710
1358
|
maxScore,
|
|
@@ -720,11 +1368,118 @@ var QuizEngine = class {
|
|
|
720
1368
|
averageTimePerQuestionSeconds
|
|
721
1369
|
}, metadataPerformance);
|
|
722
1370
|
this.quizResultState = __spreadValues(__spreadValues({}, this.quizResultState), finalResults);
|
|
723
|
-
if ((
|
|
1371
|
+
if ((_c = this.config.settings) == null ? void 0 : _c.scorm) this._sendResultsToSCORM(finalResults);
|
|
724
1372
|
await this._sendResultsToWebhook(finalResults);
|
|
725
|
-
(
|
|
1373
|
+
(_e = (_d = this.callbacks).onQuizFinish) == null ? void 0 : _e.call(_d, finalResults);
|
|
726
1374
|
return finalResults;
|
|
727
1375
|
}
|
|
1376
|
+
formatUserAnswerDetail(question, userAnswerRaw) {
|
|
1377
|
+
var _a, _b, _c, _d, _e;
|
|
1378
|
+
if (userAnswerRaw === null) return null;
|
|
1379
|
+
switch (question.questionType) {
|
|
1380
|
+
case "multiple_choice": {
|
|
1381
|
+
const q = question;
|
|
1382
|
+
const id = userAnswerRaw;
|
|
1383
|
+
return { id, value: ((_a = q.options.find((opt) => opt.id === id)) == null ? void 0 : _a.text) || "" };
|
|
1384
|
+
}
|
|
1385
|
+
case "multiple_response": {
|
|
1386
|
+
const q = question;
|
|
1387
|
+
const ids = userAnswerRaw;
|
|
1388
|
+
const values = ids.map((id) => {
|
|
1389
|
+
var _a2;
|
|
1390
|
+
return ((_a2 = q.options.find((opt) => opt.id === id)) == null ? void 0 : _a2.text) || "";
|
|
1391
|
+
});
|
|
1392
|
+
return { id: ids, value: values };
|
|
1393
|
+
}
|
|
1394
|
+
case "sequence": {
|
|
1395
|
+
const q = question;
|
|
1396
|
+
const ids = userAnswerRaw;
|
|
1397
|
+
const values = ids.map((id) => {
|
|
1398
|
+
var _a2;
|
|
1399
|
+
return ((_a2 = q.items.find((item) => item.id === id)) == null ? void 0 : _a2.content) || "";
|
|
1400
|
+
});
|
|
1401
|
+
return { id: ids, value: values };
|
|
1402
|
+
}
|
|
1403
|
+
case "matching": {
|
|
1404
|
+
const q = question;
|
|
1405
|
+
const userAnswerMap = userAnswerRaw;
|
|
1406
|
+
const valueMap = {};
|
|
1407
|
+
for (const promptId in userAnswerMap) {
|
|
1408
|
+
const optionId = userAnswerMap[promptId];
|
|
1409
|
+
const promptText = ((_b = q.prompts.find((p) => p.id === promptId)) == null ? void 0 : _b.content) || "";
|
|
1410
|
+
const optionText = ((_c = q.options.find((o) => o.id === optionId)) == null ? void 0 : _c.content) || "";
|
|
1411
|
+
valueMap[promptText] = optionText;
|
|
1412
|
+
}
|
|
1413
|
+
return { id: null, value: valueMap };
|
|
1414
|
+
}
|
|
1415
|
+
case "drag_and_drop": {
|
|
1416
|
+
const q = question;
|
|
1417
|
+
if (typeof userAnswerRaw === "object" && userAnswerRaw !== null && !Array.isArray(userAnswerRaw)) {
|
|
1418
|
+
const userAnswerMapByIds = userAnswerRaw;
|
|
1419
|
+
const enrichedUserAnswerMap = {};
|
|
1420
|
+
for (const draggableId in userAnswerMapByIds) {
|
|
1421
|
+
const dropZoneId = userAnswerMapByIds[draggableId];
|
|
1422
|
+
const draggableText = ((_d = q.draggableItems.find((d) => d.id === draggableId)) == null ? void 0 : _d.content) || `(ID: ${draggableId})`;
|
|
1423
|
+
const dropZoneText = ((_e = q.dropZones.find((z4) => z4.id === dropZoneId)) == null ? void 0 : _e.label) || `(ID: ${dropZoneId})`;
|
|
1424
|
+
enrichedUserAnswerMap[draggableText] = dropZoneText;
|
|
1425
|
+
}
|
|
1426
|
+
return { id: null, value: enrichedUserAnswerMap };
|
|
1427
|
+
}
|
|
1428
|
+
return { id: null, value: userAnswerRaw };
|
|
1429
|
+
}
|
|
1430
|
+
default:
|
|
1431
|
+
return { id: null, value: userAnswerRaw };
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
async _calculateMetadataPerformance() {
|
|
1435
|
+
var _a;
|
|
1436
|
+
const loPerformanceMap = /* @__PURE__ */ new Map();
|
|
1437
|
+
const categoryPerformanceMap = /* @__PURE__ */ new Map();
|
|
1438
|
+
const topicPerformanceMap = /* @__PURE__ */ new Map();
|
|
1439
|
+
const difficultyPerformanceMap = /* @__PURE__ */ new Map();
|
|
1440
|
+
const bloomLevelPerformanceMap = /* @__PURE__ */ new Map();
|
|
1441
|
+
const updateMap = (map, key, points, isCorrect) => {
|
|
1442
|
+
if (!key) return;
|
|
1443
|
+
const current = map.get(key) || { totalQuestions: 0, correctQuestions: 0, pointsEarned: 0, maxPoints: 0 };
|
|
1444
|
+
current.totalQuestions++;
|
|
1445
|
+
current.maxPoints += points;
|
|
1446
|
+
if (isCorrect) {
|
|
1447
|
+
current.correctQuestions++;
|
|
1448
|
+
current.pointsEarned += points;
|
|
1449
|
+
}
|
|
1450
|
+
map.set(key, current);
|
|
1451
|
+
};
|
|
1452
|
+
for (const q of this.questions) {
|
|
1453
|
+
const userAnswer = this.userAnswers.get(q.id) || null;
|
|
1454
|
+
const evaluator = this.evaluators.get(q.questionType);
|
|
1455
|
+
if (evaluator) {
|
|
1456
|
+
const { isCorrect } = await evaluator.evaluate(q, userAnswer);
|
|
1457
|
+
const pointsForThisQuestion = (_a = q.points) != null ? _a : 0;
|
|
1458
|
+
updateMap(loPerformanceMap, q.learningObjective, pointsForThisQuestion, isCorrect);
|
|
1459
|
+
updateMap(categoryPerformanceMap, q.category, pointsForThisQuestion, isCorrect);
|
|
1460
|
+
updateMap(topicPerformanceMap, q.topic, pointsForThisQuestion, isCorrect);
|
|
1461
|
+
updateMap(difficultyPerformanceMap, q.difficulty, pointsForThisQuestion, isCorrect);
|
|
1462
|
+
updateMap(bloomLevelPerformanceMap, q.bloomLevel, pointsForThisQuestion, isCorrect);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
const formatPerformanceArray = (map, keyName) => {
|
|
1466
|
+
return Array.from(map.entries()).map(([key, data]) => ({
|
|
1467
|
+
[keyName]: key,
|
|
1468
|
+
totalQuestions: data.totalQuestions,
|
|
1469
|
+
correctQuestions: data.correctQuestions,
|
|
1470
|
+
pointsEarned: data.pointsEarned,
|
|
1471
|
+
maxPoints: data.maxPoints,
|
|
1472
|
+
percentage: data.maxPoints > 0 ? parseFloat((data.pointsEarned / data.maxPoints * 100).toFixed(2)) : 0
|
|
1473
|
+
}));
|
|
1474
|
+
};
|
|
1475
|
+
return {
|
|
1476
|
+
performanceByLearningObjective: formatPerformanceArray(loPerformanceMap, "learningObjective"),
|
|
1477
|
+
performanceByCategory: formatPerformanceArray(categoryPerformanceMap, "category"),
|
|
1478
|
+
performanceByTopic: formatPerformanceArray(topicPerformanceMap, "topic"),
|
|
1479
|
+
performanceByDifficulty: formatPerformanceArray(difficultyPerformanceMap, "difficulty"),
|
|
1480
|
+
performanceByBloomLevel: formatPerformanceArray(bloomLevelPerformanceMap, "bloomLevel")
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
728
1483
|
async _sendResultsToWebhook(results) {
|
|
729
1484
|
var _a;
|
|
730
1485
|
if (!((_a = this.config.settings) == null ? void 0 : _a.webhookUrl)) {
|
|
@@ -792,71 +1547,6 @@ var QuizEngine = class {
|
|
|
792
1547
|
results.scormError = e instanceof Error ? e.message : "Unknown SCORM data sending error.";
|
|
793
1548
|
}
|
|
794
1549
|
}
|
|
795
|
-
_calculateMetadataPerformance() {
|
|
796
|
-
const loPerformanceMap = /* @__PURE__ */ new Map();
|
|
797
|
-
const categoryPerformanceMap = /* @__PURE__ */ new Map();
|
|
798
|
-
const topicPerformanceMap = /* @__PURE__ */ new Map();
|
|
799
|
-
const difficultyPerformanceMap = /* @__PURE__ */ new Map();
|
|
800
|
-
const bloomLevelPerformanceMap = /* @__PURE__ */ new Map();
|
|
801
|
-
const updateMap = (map, key, points, isCorrect) => {
|
|
802
|
-
if (!key) return;
|
|
803
|
-
const current = map.get(key) || { totalQuestions: 0, correctQuestions: 0, pointsEarned: 0, maxPoints: 0 };
|
|
804
|
-
current.totalQuestions++;
|
|
805
|
-
current.maxPoints += points;
|
|
806
|
-
if (isCorrect) {
|
|
807
|
-
current.correctQuestions++;
|
|
808
|
-
current.pointsEarned += points;
|
|
809
|
-
}
|
|
810
|
-
map.set(key, current);
|
|
811
|
-
};
|
|
812
|
-
this.questions.forEach((q) => {
|
|
813
|
-
var _a;
|
|
814
|
-
const qResult = this.userAnswers.get(q.id);
|
|
815
|
-
const { isCorrect } = this.evaluateQuestion(q, qResult || null);
|
|
816
|
-
const pointsForThisQuestion = (_a = q.points) != null ? _a : 0;
|
|
817
|
-
updateMap(loPerformanceMap, q.learningObjective, pointsForThisQuestion, isCorrect);
|
|
818
|
-
updateMap(categoryPerformanceMap, q.category, pointsForThisQuestion, isCorrect);
|
|
819
|
-
updateMap(topicPerformanceMap, q.topic, pointsForThisQuestion, isCorrect);
|
|
820
|
-
updateMap(difficultyPerformanceMap, q.difficulty, pointsForThisQuestion, isCorrect);
|
|
821
|
-
updateMap(bloomLevelPerformanceMap, q.bloomLevel, pointsForThisQuestion, isCorrect);
|
|
822
|
-
});
|
|
823
|
-
const formatPerformanceArray = (map, keyName) => {
|
|
824
|
-
return Array.from(map.entries()).map(([key, data]) => ({
|
|
825
|
-
[keyName]: key,
|
|
826
|
-
totalQuestions: data.totalQuestions,
|
|
827
|
-
correctQuestions: data.correctQuestions,
|
|
828
|
-
pointsEarned: data.pointsEarned,
|
|
829
|
-
maxPoints: data.maxPoints,
|
|
830
|
-
percentage: data.maxPoints > 0 ? parseFloat((data.pointsEarned / data.maxPoints * 100).toFixed(2)) : 0
|
|
831
|
-
}));
|
|
832
|
-
};
|
|
833
|
-
return {
|
|
834
|
-
performanceByLearningObjective: formatPerformanceArray(loPerformanceMap, "learningObjective"),
|
|
835
|
-
performanceByCategory: formatPerformanceArray(categoryPerformanceMap, "category"),
|
|
836
|
-
performanceByTopic: formatPerformanceArray(topicPerformanceMap, "topic"),
|
|
837
|
-
performanceByDifficulty: formatPerformanceArray(difficultyPerformanceMap, "difficulty"),
|
|
838
|
-
performanceByBloomLevel: formatPerformanceArray(bloomLevelPerformanceMap, "bloomLevel")
|
|
839
|
-
};
|
|
840
|
-
}
|
|
841
|
-
getElapsedTime() {
|
|
842
|
-
return Date.now() - this.overallStartTime;
|
|
843
|
-
}
|
|
844
|
-
destroy() {
|
|
845
|
-
this.stopTimer();
|
|
846
|
-
this._recordCurrentQuestionTime();
|
|
847
|
-
if (this.scormService && this.scormService.hasAPI()) {
|
|
848
|
-
if (["initialized", "committed", "sending_data"].includes(this.quizResultState.scormStatus || "")) {
|
|
849
|
-
const termResult = this.scormService.terminate();
|
|
850
|
-
if (termResult.success) {
|
|
851
|
-
this.quizResultState.scormStatus = "terminated";
|
|
852
|
-
} else {
|
|
853
|
-
this.quizResultState.scormStatus = "error";
|
|
854
|
-
this.quizResultState.scormError = termResult.error || "SCORM termination failed on destroy.";
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
this.scormService = null;
|
|
859
|
-
}
|
|
860
1550
|
};
|
|
861
1551
|
|
|
862
1552
|
// src/utils/idGenerators.ts
|
|
@@ -869,22 +1559,12 @@ var QuizEditorService = class {
|
|
|
869
1559
|
constructor(initialQuiz) {
|
|
870
1560
|
this.quiz = JSON.parse(JSON.stringify(initialQuiz));
|
|
871
1561
|
}
|
|
872
|
-
/**
|
|
873
|
-
* Returns the current state of the quiz configuration.
|
|
874
|
-
* @returns The current QuizConfig object.
|
|
875
|
-
*/
|
|
876
1562
|
getQuiz() {
|
|
877
1563
|
return this.quiz;
|
|
878
1564
|
}
|
|
879
|
-
/**
|
|
880
|
-
* Creates a new, "empty" question object based on the specified question type.
|
|
881
|
-
* @param type The type of question to create.
|
|
882
|
-
* @returns A new QuizQuestion object with default values.
|
|
883
|
-
*/
|
|
884
1565
|
static createNewQuestionTemplate(type) {
|
|
885
1566
|
const baseNewQuestion = {
|
|
886
1567
|
id: generateUniqueId(`new_${type}_`),
|
|
887
|
-
// 'new_' prefix indicates it's a new, unsaved question
|
|
888
1568
|
questionType: type,
|
|
889
1569
|
prompt: "",
|
|
890
1570
|
points: 10,
|
|
@@ -938,16 +1618,21 @@ var QuizEditorService = class {
|
|
|
938
1618
|
solutionWorkspaceXML: "",
|
|
939
1619
|
solutionGeneratedCode: ""
|
|
940
1620
|
});
|
|
1621
|
+
case "coding":
|
|
1622
|
+
return __spreadProps(__spreadValues({}, baseNewQuestion), {
|
|
1623
|
+
questionType: "coding",
|
|
1624
|
+
language: "javascript",
|
|
1625
|
+
solutionCode: "",
|
|
1626
|
+
testCases: [],
|
|
1627
|
+
functionSignature: "",
|
|
1628
|
+
points: 25
|
|
1629
|
+
// Coding questions are worth more by default
|
|
1630
|
+
});
|
|
941
1631
|
default:
|
|
942
1632
|
const _exhaustiveCheck = type;
|
|
943
1633
|
throw new Error(`Question type "${_exhaustiveCheck}" is not supported for creation.`);
|
|
944
1634
|
}
|
|
945
1635
|
}
|
|
946
|
-
/**
|
|
947
|
-
* Adds a new question to the quiz. If the question ID is temporary, a new permanent ID is generated.
|
|
948
|
-
* @param question The question object to add.
|
|
949
|
-
* @returns The updated QuizConfig.
|
|
950
|
-
*/
|
|
951
1636
|
addQuestion(question) {
|
|
952
1637
|
const newQuestion = __spreadValues({}, question);
|
|
953
1638
|
if (newQuestion.id.startsWith("new_")) {
|
|
@@ -956,11 +1641,6 @@ var QuizEditorService = class {
|
|
|
956
1641
|
this.quiz.questions.push(newQuestion);
|
|
957
1642
|
return this.quiz;
|
|
958
1643
|
}
|
|
959
|
-
/**
|
|
960
|
-
* Updates an existing question in the quiz.
|
|
961
|
-
* @param updatedQuestion The full question object with changes.
|
|
962
|
-
* @returns The updated QuizConfig.
|
|
963
|
-
*/
|
|
964
1644
|
updateQuestion(updatedQuestion) {
|
|
965
1645
|
const questionIndex = this.quiz.questions.findIndex((q) => q.id === updatedQuestion.id);
|
|
966
1646
|
if (questionIndex === -1) {
|
|
@@ -969,11 +1649,6 @@ var QuizEditorService = class {
|
|
|
969
1649
|
this.quiz.questions[questionIndex] = updatedQuestion;
|
|
970
1650
|
return this.quiz;
|
|
971
1651
|
}
|
|
972
|
-
/**
|
|
973
|
-
* Deletes a question from the quiz by its index.
|
|
974
|
-
* @param index The index of the question to delete.
|
|
975
|
-
* @returns The updated QuizConfig.
|
|
976
|
-
*/
|
|
977
1652
|
deleteQuestionByIndex(index) {
|
|
978
1653
|
if (index < 0 || index >= this.quiz.questions.length) {
|
|
979
1654
|
throw new Error(`Invalid index ${index} for question deletion.`);
|
|
@@ -981,12 +1656,6 @@ var QuizEditorService = class {
|
|
|
981
1656
|
this.quiz.questions.splice(index, 1);
|
|
982
1657
|
return this.quiz;
|
|
983
1658
|
}
|
|
984
|
-
/**
|
|
985
|
-
* Moves a question from one position to another.
|
|
986
|
-
* @param fromIndex The current index of the question.
|
|
987
|
-
* @param toIndex The target index for the question.
|
|
988
|
-
* @returns The updated QuizConfig.
|
|
989
|
-
*/
|
|
990
1659
|
moveQuestion(fromIndex, toIndex) {
|
|
991
1660
|
if (fromIndex < 0 || fromIndex >= this.quiz.questions.length || toIndex < 0 || toIndex >= this.quiz.questions.length) {
|
|
992
1661
|
throw new Error("Invalid index for moving question.");
|
|
@@ -996,823 +1665,960 @@ var QuizEditorService = class {
|
|
|
996
1665
|
return this.quiz;
|
|
997
1666
|
}
|
|
998
1667
|
};
|
|
999
|
-
var
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
});
|
|
1003
|
-
var MultipleChoiceQuestionSchema = z.object({
|
|
1004
|
-
// Các trường bắt buộc (required)
|
|
1005
|
-
id: z.string().describe("Unique identifier."),
|
|
1006
|
-
questionType: z.literal("multiple_choice"),
|
|
1007
|
-
// Tương đương với "const" trong JSON Schema
|
|
1008
|
-
prompt: z.string().describe("Question statement."),
|
|
1009
|
-
options: z.array(QuestionOptionSchema).min(1).describe("Array of answer choices."),
|
|
1010
|
-
correctAnswerId: z.string().describe("ID of the correct option."),
|
|
1011
|
-
// Các trường tùy chọn (optional)
|
|
1012
|
-
points: z.number().optional().describe("Points for correct answer."),
|
|
1013
|
-
explanation: z.string().optional().describe("Explanation for the answer."),
|
|
1014
|
-
learningObjective: z.string().optional(),
|
|
1015
|
-
glossary: z.array(z.string()).optional(),
|
|
1016
|
-
bloomLevel: z.string().optional(),
|
|
1017
|
-
difficulty: z.enum(["easy", "medium", "hard"]).optional(),
|
|
1018
|
-
// Có thể làm chặt chẽ hơn với z.enum
|
|
1019
|
-
contextCode: z.string().optional(),
|
|
1020
|
-
gradeBand: z.string().optional(),
|
|
1021
|
-
course: z.string().optional(),
|
|
1022
|
-
category: z.string().optional(),
|
|
1023
|
-
topic: z.string().optional()
|
|
1024
|
-
});
|
|
1025
|
-
var exampleData = {
|
|
1026
|
-
id: "mcq-123",
|
|
1027
|
-
questionType: "multiple_choice",
|
|
1028
|
-
prompt: "What is 2 + 2?",
|
|
1029
|
-
options: [
|
|
1030
|
-
{ id: "opt-1", text: "3" },
|
|
1031
|
-
{ id: "opt-2", text: "4" }
|
|
1032
|
-
],
|
|
1033
|
-
correctAnswerId: "opt-2",
|
|
1034
|
-
points: 10
|
|
1035
|
-
};
|
|
1036
|
-
try {
|
|
1037
|
-
const validatedQuestion = MultipleChoiceQuestionSchema.parse(exampleData);
|
|
1038
|
-
console.log("Validation successful:", validatedQuestion);
|
|
1039
|
-
} catch (error) {
|
|
1040
|
-
console.error("Validation failed:", error);
|
|
1041
|
-
}
|
|
1042
|
-
var QuestionOptionSchema2 = z.object({
|
|
1043
|
-
id: z.string(),
|
|
1044
|
-
text: z.string()
|
|
1045
|
-
});
|
|
1046
|
-
var MultipleResponseQuestionSchema = z.object({
|
|
1047
|
-
// Các trường bắt buộc
|
|
1048
|
-
id: z.string(),
|
|
1049
|
-
questionType: z.literal("multiple_response"),
|
|
1050
|
-
prompt: z.string(),
|
|
1051
|
-
options: z.array(QuestionOptionSchema2).min(1),
|
|
1052
|
-
correctAnswerIds: z.array(z.string()).min(1).describe("Array of IDs of the correct options."),
|
|
1053
|
-
// Các trường tùy chọn
|
|
1054
|
-
points: z.number().optional(),
|
|
1055
|
-
explanation: z.string().optional(),
|
|
1056
|
-
learningObjective: z.string().optional(),
|
|
1057
|
-
glossary: z.array(z.string()).optional(),
|
|
1058
|
-
bloomLevel: z.string().optional(),
|
|
1059
|
-
difficulty: z.enum(["easy", "medium", "hard"]).optional(),
|
|
1060
|
-
contextCode: z.string().optional(),
|
|
1061
|
-
gradeBand: z.string().optional(),
|
|
1062
|
-
course: z.string().optional(),
|
|
1063
|
-
category: z.string().optional(),
|
|
1064
|
-
topic: z.string().optional()
|
|
1065
|
-
});
|
|
1066
|
-
var TextSegmentSchema = z.object({
|
|
1067
|
-
type: z.literal("text"),
|
|
1068
|
-
content: z.string().describe("Text content for 'text' type segments.")
|
|
1069
|
-
});
|
|
1070
|
-
var BlankSegmentSchema = z.object({
|
|
1071
|
-
type: z.literal("blank"),
|
|
1072
|
-
id: z.string().describe("Unique ID for 'blank' type segments, used to map to answers.")
|
|
1073
|
-
});
|
|
1074
|
-
var SegmentSchema = z.discriminatedUnion("type", [
|
|
1075
|
-
TextSegmentSchema,
|
|
1076
|
-
BlankSegmentSchema
|
|
1077
|
-
]);
|
|
1078
|
-
var AnswerSchema = z.object({
|
|
1079
|
-
blankId: z.string().describe("ID of the blank this answer corresponds to."),
|
|
1080
|
-
acceptedValues: z.array(z.string()).min(1).describe("Array of acceptable string values for this blank.")
|
|
1081
|
-
});
|
|
1082
|
-
var FillInTheBlanksQuestionSchema = z.object({
|
|
1083
|
-
// Các trường bắt buộc
|
|
1084
|
-
id: z.string(),
|
|
1085
|
-
questionType: z.literal("fill_in_the_blanks"),
|
|
1086
|
-
prompt: z.string().describe("Overall instruction or context for the fill-in-the-blanks sentence(s)."),
|
|
1087
|
-
segments: z.array(SegmentSchema).min(1).describe("Array of text and blank segments constructing the question."),
|
|
1088
|
-
answers: z.array(AnswerSchema).min(1).describe("Definitions of correct answers for each blank."),
|
|
1089
|
-
// Các trường tùy chọn
|
|
1090
|
-
isCaseSensitive: z.boolean().optional().describe("Whether answer evaluation should be case sensitive."),
|
|
1668
|
+
var BaseRawQuestionSchema = z.object({
|
|
1669
|
+
questionType: z.string(),
|
|
1670
|
+
prompt: z.string().min(1, { message: "Prompt cannot be empty." }),
|
|
1091
1671
|
points: z.number().optional(),
|
|
1092
1672
|
explanation: z.string().optional(),
|
|
1093
|
-
|
|
1094
|
-
glossary: z.array(z.string()).optional(),
|
|
1095
|
-
bloomLevel: z.string().optional(),
|
|
1673
|
+
topic: z.string().optional(),
|
|
1096
1674
|
difficulty: z.enum(["easy", "medium", "hard"]).optional(),
|
|
1097
|
-
|
|
1098
|
-
gradeBand: z.string().optional(),
|
|
1099
|
-
course: z.string().optional(),
|
|
1100
|
-
category: z.string().optional(),
|
|
1101
|
-
topic: z.string().optional()
|
|
1102
|
-
});
|
|
1103
|
-
var DraggableItemSchema = z.object({
|
|
1104
|
-
id: z.string(),
|
|
1105
|
-
content: z.string()
|
|
1675
|
+
bloomLevel: z.string().optional()
|
|
1106
1676
|
});
|
|
1107
|
-
var
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
})
|
|
1111
|
-
var AnswerMapSchema = z.object({
|
|
1112
|
-
draggableId: z.string(),
|
|
1113
|
-
dropZoneId: z.string()
|
|
1677
|
+
var RawMCQSchema = BaseRawQuestionSchema.extend({
|
|
1678
|
+
questionType: z.literal("multiple_choice"),
|
|
1679
|
+
options: z.array(z.string()).min(2, { message: "Multiple Choice questions need at least 2 options." }),
|
|
1680
|
+
correctAnswer: z.string({ required_error: "A correct answer is required." })
|
|
1114
1681
|
});
|
|
1115
|
-
var
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
prompt: z.string(),
|
|
1120
|
-
draggableItems: z.array(DraggableItemSchema).min(1),
|
|
1121
|
-
dropZones: z.array(DropZoneSchema).min(1),
|
|
1122
|
-
answerMap: z.array(AnswerMapSchema).min(1),
|
|
1123
|
-
// Các trường tùy chọn
|
|
1124
|
-
backgroundImageUrl: z.string().url().optional().describe("Must be a valid URL format."),
|
|
1125
|
-
// .url() tương đương "format": "uri-reference"
|
|
1126
|
-
imageAltText: z.string().optional(),
|
|
1127
|
-
points: z.number().optional(),
|
|
1128
|
-
explanation: z.string().optional(),
|
|
1129
|
-
learningObjective: z.string().optional(),
|
|
1130
|
-
glossary: z.array(z.string()).optional(),
|
|
1131
|
-
bloomLevel: z.string().optional(),
|
|
1132
|
-
difficulty: z.enum(["easy", "medium", "hard"]).optional(),
|
|
1133
|
-
contextCode: z.string().optional(),
|
|
1134
|
-
gradeBand: z.string().optional(),
|
|
1135
|
-
course: z.string().optional(),
|
|
1136
|
-
category: z.string().optional(),
|
|
1137
|
-
topic: z.string().optional()
|
|
1682
|
+
var RawMRQSchema = BaseRawQuestionSchema.extend({
|
|
1683
|
+
questionType: z.literal("multiple_response"),
|
|
1684
|
+
options: z.array(z.string()).min(2, { message: "Multiple Response questions need at least 2 options." }),
|
|
1685
|
+
correctAnswers: z.array(z.string()).min(1, { message: "At least one correct answer is required." })
|
|
1138
1686
|
});
|
|
1139
|
-
var
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
questionType: z.literal("true_false").describe("The type of the question."),
|
|
1143
|
-
prompt: z.string().describe("The main text or statement for the question."),
|
|
1144
|
-
correctAnswer: z.boolean().describe("The correct answer for the statement (true if the statement is true, false if it is false)."),
|
|
1145
|
-
// Các trường tùy chọn
|
|
1146
|
-
points: z.number().optional().describe("Points awarded for a correct answer."),
|
|
1147
|
-
explanation: z.string().optional().describe("Explanation for why the answer is correct or incorrect."),
|
|
1148
|
-
learningObjective: z.string().optional().describe("The learning objective this question addresses."),
|
|
1149
|
-
glossary: z.array(z.string()).optional().describe("List of related glossary terms."),
|
|
1150
|
-
bloomLevel: z.string().optional().describe("Cognitive level based on Bloom's Taxonomy (e.g., 'Remembering', 'Applying')."),
|
|
1151
|
-
difficulty: z.enum(["easy", "medium", "hard"]).optional().describe("Difficulty level of the question."),
|
|
1152
|
-
contextCode: z.string().optional().describe("Identifier for the context of the question."),
|
|
1153
|
-
gradeBand: z.string().optional().describe("Target grade band for the question (e.g., 'K-2', 'Middle School')."),
|
|
1154
|
-
course: z.string().optional().describe("Associated course name."),
|
|
1155
|
-
category: z.string().optional().describe("General category of the question content (e.g., 'Mathematics', 'History')."),
|
|
1156
|
-
topic: z.string().optional().describe("Specific topic within the category.")
|
|
1687
|
+
var RawTFSchema = BaseRawQuestionSchema.extend({
|
|
1688
|
+
questionType: z.literal("true_false"),
|
|
1689
|
+
correctAnswer: z.boolean({ required_error: "A correct answer (true/false) is required." })
|
|
1157
1690
|
});
|
|
1158
|
-
var
|
|
1159
|
-
// Các trường bắt buộc
|
|
1160
|
-
id: z.string(),
|
|
1691
|
+
var RawSASchema = BaseRawQuestionSchema.extend({
|
|
1161
1692
|
questionType: z.literal("short_answer"),
|
|
1162
|
-
|
|
1163
|
-
acceptedAnswers: z.array(z.string()).min(1).describe("An array of acceptable short answers."),
|
|
1164
|
-
// Các trường tùy chọn
|
|
1165
|
-
isCaseSensitive: z.boolean().optional().describe("Whether the answer evaluation should be case sensitive."),
|
|
1166
|
-
points: z.number().optional(),
|
|
1167
|
-
explanation: z.string().optional(),
|
|
1168
|
-
learningObjective: z.string().optional(),
|
|
1169
|
-
glossary: z.array(z.string()).optional(),
|
|
1170
|
-
bloomLevel: z.string().optional(),
|
|
1171
|
-
difficulty: z.enum(["easy", "medium", "hard"]).optional(),
|
|
1172
|
-
contextCode: z.string().optional(),
|
|
1173
|
-
gradeBand: z.string().optional(),
|
|
1174
|
-
course: z.string().optional(),
|
|
1175
|
-
category: z.string().optional(),
|
|
1176
|
-
topic: z.string().optional()
|
|
1693
|
+
acceptedAnswers: z.array(z.string()).min(1, { message: "At least one accepted answer is required." })
|
|
1177
1694
|
});
|
|
1178
|
-
var
|
|
1179
|
-
// Các trường bắt buộc
|
|
1180
|
-
id: z.string(),
|
|
1695
|
+
var RawNumericSchema = BaseRawQuestionSchema.extend({
|
|
1181
1696
|
questionType: z.literal("numeric"),
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
// Các trường tùy chọn
|
|
1185
|
-
tolerance: z.number().optional().describe("The acceptable range of error (plus or minus)."),
|
|
1186
|
-
points: z.number().optional(),
|
|
1187
|
-
explanation: z.string().optional(),
|
|
1188
|
-
learningObjective: z.string().optional(),
|
|
1189
|
-
glossary: z.array(z.string()).optional(),
|
|
1190
|
-
bloomLevel: z.string().optional(),
|
|
1191
|
-
difficulty: z.enum(["easy", "medium", "hard"]).optional(),
|
|
1192
|
-
contextCode: z.string().optional(),
|
|
1193
|
-
gradeBand: z.string().optional(),
|
|
1194
|
-
course: z.string().optional(),
|
|
1195
|
-
category: z.string().optional(),
|
|
1196
|
-
topic: z.string().optional()
|
|
1697
|
+
answer: z.number({ required_error: "A numeric answer is required." }),
|
|
1698
|
+
tolerance: z.number().optional()
|
|
1197
1699
|
});
|
|
1198
|
-
var
|
|
1199
|
-
id: z.string().describe("Unique ID for the item."),
|
|
1200
|
-
content: z.string().describe("Text content of the item.")
|
|
1201
|
-
});
|
|
1202
|
-
var SequenceQuestionSchema = z.object({
|
|
1203
|
-
// Các trường bắt buộc
|
|
1204
|
-
id: z.string(),
|
|
1700
|
+
var RawSequenceSchema = BaseRawQuestionSchema.extend({
|
|
1205
1701
|
questionType: z.literal("sequence"),
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
correctOrder: z.array(z.string()).min(2).describe("Array of item IDs in the correct sequence."),
|
|
1209
|
-
// Các trường tùy chọn
|
|
1210
|
-
points: z.number().optional(),
|
|
1211
|
-
explanation: z.string().optional(),
|
|
1212
|
-
learningObjective: z.string().optional(),
|
|
1213
|
-
glossary: z.array(z.string()).optional(),
|
|
1214
|
-
bloomLevel: z.string().optional(),
|
|
1215
|
-
difficulty: z.enum(["easy", "medium", "hard"]).optional(),
|
|
1216
|
-
contextCode: z.string().optional(),
|
|
1217
|
-
gradeBand: z.string().optional(),
|
|
1218
|
-
course: z.string().optional(),
|
|
1219
|
-
category: z.string().optional(),
|
|
1220
|
-
topic: z.string().optional()
|
|
1221
|
-
});
|
|
1222
|
-
var MatchPromptItemSchema = z.object({
|
|
1223
|
-
id: z.string(),
|
|
1224
|
-
content: z.string()
|
|
1225
|
-
});
|
|
1226
|
-
var MatchOptionItemSchema = z.object({
|
|
1227
|
-
id: z.string(),
|
|
1228
|
-
content: z.string()
|
|
1702
|
+
items: z.array(z.string()).min(2, { message: "Sequence questions need at least 2 items." }),
|
|
1703
|
+
correctOrder: z.array(z.string()).min(2)
|
|
1229
1704
|
});
|
|
1230
|
-
var
|
|
1231
|
-
promptId: z.string(),
|
|
1232
|
-
optionId: z.string()
|
|
1233
|
-
});
|
|
1234
|
-
var MatchingQuestionSchema = z.object({
|
|
1235
|
-
// Các trường bắt buộc
|
|
1236
|
-
id: z.string(),
|
|
1705
|
+
var RawMatchingSchema = BaseRawQuestionSchema.extend({
|
|
1237
1706
|
questionType: z.literal("matching"),
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
correctAnswerMap: z.array(CorrectAnswerMapSchema).min(1).describe("Array defining the correct pairings between prompt IDs and option IDs."),
|
|
1242
|
-
// Các trường tùy chọn
|
|
1243
|
-
shuffleOptions: z.boolean().optional().describe("Whether the display order of options should be shuffled for the user."),
|
|
1244
|
-
points: z.number().optional(),
|
|
1245
|
-
explanation: z.string().optional(),
|
|
1246
|
-
learningObjective: z.string().optional(),
|
|
1247
|
-
glossary: z.array(z.string()).optional(),
|
|
1248
|
-
bloomLevel: z.string().optional(),
|
|
1249
|
-
difficulty: z.enum(["easy", "medium", "hard"]).optional(),
|
|
1250
|
-
contextCode: z.string().optional(),
|
|
1251
|
-
gradeBand: z.string().optional(),
|
|
1252
|
-
course: z.string().optional(),
|
|
1253
|
-
category: z.string().optional(),
|
|
1254
|
-
topic: z.string().optional()
|
|
1707
|
+
prompts: z.array(z.string()).min(1),
|
|
1708
|
+
options: z.array(z.string()).min(1),
|
|
1709
|
+
correctAnswerMap: z.record(z.string(), { required_error: "A map of correct answers is required." })
|
|
1255
1710
|
});
|
|
1256
|
-
var
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
description: z.string().optional().describe("Optional description for the hotspot (e.g., for tooltips).")
|
|
1261
|
-
});
|
|
1262
|
-
var HotspotCircleSchema = z.object({
|
|
1263
|
-
id: z.string().describe("Unique ID for the hotspot."),
|
|
1264
|
-
shape: z.literal("circle"),
|
|
1265
|
-
coords: z.number().array().length(3).describe("Coordinates for the circle: [centerX, centerY, radius]."),
|
|
1266
|
-
description: z.string().optional().describe("Optional description for the hotspot (e.g., for tooltips).")
|
|
1711
|
+
var RawFITBSchema = BaseRawQuestionSchema.extend({
|
|
1712
|
+
questionType: z.literal("fill_in_the_blanks"),
|
|
1713
|
+
sentenceWithPlaceholders: z.string().includes("{{", { message: "Sentence must contain at least one placeholder like {{placeholder}}." }),
|
|
1714
|
+
blanks: z.record(z.array(z.string()).min(1), { required_error: "Blanks definitions are required." })
|
|
1267
1715
|
});
|
|
1268
|
-
var
|
|
1269
|
-
|
|
1270
|
-
|
|
1716
|
+
var AnyRawQuestionSchema = z.discriminatedUnion("questionType", [
|
|
1717
|
+
RawMCQSchema,
|
|
1718
|
+
RawMRQSchema,
|
|
1719
|
+
RawTFSchema,
|
|
1720
|
+
RawSASchema,
|
|
1721
|
+
RawNumericSchema,
|
|
1722
|
+
RawSequenceSchema,
|
|
1723
|
+
RawMatchingSchema,
|
|
1724
|
+
RawFITBSchema
|
|
1271
1725
|
]);
|
|
1272
|
-
var HotspotQuestionSchema = z.object({
|
|
1273
|
-
// Các trường bắt buộc
|
|
1274
|
-
id: z.string(),
|
|
1275
|
-
questionType: z.literal("hotspot"),
|
|
1276
|
-
prompt: z.string(),
|
|
1277
|
-
imageUrl: z.string().url().describe("URL of the image to be used."),
|
|
1278
|
-
hotspots: z.array(HotspotAreaSchema).min(1).describe("Array of clickable hotspot areas on the image."),
|
|
1279
|
-
correctHotspotIds: z.array(z.string()).min(1).describe("Array of IDs of the correct hotspots."),
|
|
1280
|
-
// Các trường tùy chọn
|
|
1281
|
-
imageAltText: z.string().optional().describe("Alternative text for the image, for accessibility."),
|
|
1282
|
-
points: z.number().optional(),
|
|
1283
|
-
explanation: z.string().optional(),
|
|
1284
|
-
learningObjective: z.string().optional(),
|
|
1285
|
-
glossary: z.array(z.string()).optional(),
|
|
1286
|
-
bloomLevel: z.string().optional(),
|
|
1287
|
-
difficulty: z.enum(["easy", "medium", "hard"]).optional(),
|
|
1288
|
-
contextCode: z.string().optional(),
|
|
1289
|
-
gradeBand: z.string().optional(),
|
|
1290
|
-
course: z.string().optional(),
|
|
1291
|
-
category: z.string().optional(),
|
|
1292
|
-
topic: z.string().optional()
|
|
1293
|
-
});
|
|
1294
|
-
var BlocklyProgrammingQuestionSchema = z.object({
|
|
1295
|
-
// Các trường bắt buộc (required)
|
|
1296
|
-
id: z.string(),
|
|
1297
|
-
questionType: z.literal("blockly_programming"),
|
|
1298
|
-
prompt: z.string(),
|
|
1299
|
-
toolboxDefinition: z.string().describe("XML string defining the Blockly toolbox."),
|
|
1300
|
-
// Các trường tùy chọn (optional)
|
|
1301
|
-
initialWorkspace: z.string().optional().describe("Optional XML string for the initial state of the Blockly workspace."),
|
|
1302
|
-
solutionWorkspaceXML: z.string().optional().describe("Optional XML string representing the solution, for visual display."),
|
|
1303
|
-
solutionGeneratedCode: z.string().optional().describe("The JavaScript code that a correct Blockly program should generate, used for grading."),
|
|
1304
|
-
points: z.number().optional(),
|
|
1305
|
-
explanation: z.string().optional(),
|
|
1306
|
-
learningObjective: z.string().optional(),
|
|
1307
|
-
glossary: z.array(z.string()).optional(),
|
|
1308
|
-
bloomLevel: z.string().optional(),
|
|
1309
|
-
difficulty: z.enum(["easy", "medium", "hard"]).optional(),
|
|
1310
|
-
contextCode: z.string().optional(),
|
|
1311
|
-
gradeBand: z.string().optional(),
|
|
1312
|
-
course: z.string().optional(),
|
|
1313
|
-
category: z.string().optional(),
|
|
1314
|
-
topic: z.string().optional()
|
|
1315
|
-
});
|
|
1316
|
-
var exampleData2 = {
|
|
1317
|
-
id: "blkq-001",
|
|
1318
|
-
questionType: "blockly_programming",
|
|
1319
|
-
prompt: "Create a program to say 'Hello!'",
|
|
1320
|
-
toolboxDefinition: "<xml>...</xml>",
|
|
1321
|
-
solutionGeneratedCode: "window.alert('Hello!');"
|
|
1322
|
-
};
|
|
1323
|
-
try {
|
|
1324
|
-
const validatedQuestion = BlocklyProgrammingQuestionSchema.parse(exampleData2);
|
|
1325
|
-
console.log("Validation successful:", validatedQuestion);
|
|
1326
|
-
} catch (error) {
|
|
1327
|
-
console.error("Validation failed:", error);
|
|
1328
|
-
}
|
|
1329
|
-
var ScratchProgrammingQuestionSchema = z.object({
|
|
1330
|
-
// Các trường bắt buộc (required)
|
|
1331
|
-
id: z.string(),
|
|
1332
|
-
questionType: z.literal("scratch_programming"),
|
|
1333
|
-
prompt: z.string(),
|
|
1334
|
-
toolboxDefinition: z.string().describe("XML string defining the Blockly/Scratch toolbox."),
|
|
1335
|
-
// Các trường tùy chọn (optional)
|
|
1336
|
-
initialWorkspace: z.string().optional().describe("Optional XML string for the initial state of the workspace."),
|
|
1337
|
-
solutionWorkspaceXML: z.string().optional().describe("Optional XML string representing the solution blocks."),
|
|
1338
|
-
solutionGeneratedCode: z.string().optional().describe("The code or logic representation that a correct program should achieve, used for grading."),
|
|
1339
|
-
points: z.number().optional(),
|
|
1340
|
-
explanation: z.string().optional(),
|
|
1341
|
-
learningObjective: z.string().optional(),
|
|
1342
|
-
glossary: z.array(z.string()).optional(),
|
|
1343
|
-
bloomLevel: z.string().optional(),
|
|
1344
|
-
difficulty: z.enum(["easy", "medium", "hard"]).optional(),
|
|
1345
|
-
contextCode: z.string().optional(),
|
|
1346
|
-
gradeBand: z.string().optional(),
|
|
1347
|
-
course: z.string().optional(),
|
|
1348
|
-
category: z.string().optional(),
|
|
1349
|
-
topic: z.string().optional()
|
|
1350
|
-
});
|
|
1351
|
-
var exampleData3 = {
|
|
1352
|
-
id: "scrq-001",
|
|
1353
|
-
questionType: "scratch_programming",
|
|
1354
|
-
prompt: "Make the cat move 10 steps.",
|
|
1355
|
-
toolboxDefinition: "<xml>...</xml>",
|
|
1356
|
-
solutionGeneratedCode: "move(10)"
|
|
1357
|
-
};
|
|
1358
|
-
try {
|
|
1359
|
-
const validatedQuestion = ScratchProgrammingQuestionSchema.parse(exampleData3);
|
|
1360
|
-
console.log("Validation successful:", validatedQuestion);
|
|
1361
|
-
} catch (error) {
|
|
1362
|
-
console.error("Validation failed:", error);
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
// src/services/QuestionImportService.ts
|
|
1366
|
-
var tsvQuestionTypeMap = {
|
|
1367
|
-
TF: "true_false",
|
|
1368
|
-
MC: "multiple_choice",
|
|
1369
|
-
MR: "multiple_response",
|
|
1370
|
-
TI: "short_answer",
|
|
1371
|
-
MG: "matching",
|
|
1372
|
-
SEQ: "sequence",
|
|
1373
|
-
NUMG: "numeric"
|
|
1374
|
-
// Add other mappings if needed
|
|
1375
|
-
};
|
|
1376
|
-
var tsvDifficultyMap = {
|
|
1377
|
-
"d\u1EC5": "easy",
|
|
1378
|
-
"easy": "easy",
|
|
1379
|
-
"trung b\xECnh": "medium",
|
|
1380
|
-
"medium": "medium",
|
|
1381
|
-
"kh\xF3": "hard",
|
|
1382
|
-
"hard": "hard"
|
|
1383
|
-
};
|
|
1384
|
-
var tsvBloomLevelMap = {
|
|
1385
|
-
"nh\u1EDB": "remembering",
|
|
1386
|
-
"remembering": "remembering",
|
|
1387
|
-
"hi\u1EC3u": "understanding",
|
|
1388
|
-
"understanding": "understanding",
|
|
1389
|
-
"\xE1p d\u1EE5ng": "applying",
|
|
1390
|
-
"applying": "applying",
|
|
1391
|
-
"v\u1EADn d\u1EE5ng": "applying"
|
|
1392
|
-
// Add other bloom levels if needed
|
|
1393
|
-
};
|
|
1394
|
-
var schemaMap = {
|
|
1395
|
-
multiple_choice: MultipleChoiceQuestionSchema,
|
|
1396
|
-
multiple_response: MultipleResponseQuestionSchema,
|
|
1397
|
-
fill_in_the_blanks: FillInTheBlanksQuestionSchema,
|
|
1398
|
-
drag_and_drop: DragAndDropQuestionSchema,
|
|
1399
|
-
true_false: TrueFalseQuestionSchema,
|
|
1400
|
-
short_answer: ShortAnswerQuestionSchema,
|
|
1401
|
-
numeric: NumericQuestionSchema,
|
|
1402
|
-
sequence: SequenceQuestionSchema,
|
|
1403
|
-
matching: MatchingQuestionSchema,
|
|
1404
|
-
hotspot: HotspotQuestionSchema,
|
|
1405
|
-
blockly_programming: BlocklyProgrammingQuestionSchema,
|
|
1406
|
-
scratch_programming: ScratchProgrammingQuestionSchema
|
|
1407
|
-
};
|
|
1408
1726
|
var QuestionImportService = class {
|
|
1409
1727
|
static processJSON(jsonString) {
|
|
1410
|
-
const validQuestions = [];
|
|
1411
|
-
const errors = [];
|
|
1412
|
-
let parsedData;
|
|
1413
1728
|
try {
|
|
1414
|
-
|
|
1729
|
+
const rawData = JSON.parse(jsonString);
|
|
1730
|
+
if (!Array.isArray(rawData)) {
|
|
1731
|
+
return { validQuestions: [], errors: [{ index: 0, message: "JSON content must be an array of question objects.", data: rawData }] };
|
|
1732
|
+
}
|
|
1733
|
+
return this.processRawObjects(rawData);
|
|
1415
1734
|
} catch (e) {
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
message: `Invalid JSON format: ${e.message || "Could not parse the file/text."}`,
|
|
1419
|
-
data: jsonString.substring(0, 500)
|
|
1420
|
-
});
|
|
1421
|
-
return { validQuestions, errors };
|
|
1735
|
+
const message = e instanceof Error ? e.message : "Invalid JSON format.";
|
|
1736
|
+
return { validQuestions: [], errors: [{ index: 0, message, data: jsonString.substring(0, 500) }] };
|
|
1422
1737
|
}
|
|
1423
|
-
if (!Array.isArray(parsedData)) {
|
|
1424
|
-
errors.push({
|
|
1425
|
-
index: 0,
|
|
1426
|
-
message: "Invalid format. The root of the JSON must be an array of question objects.",
|
|
1427
|
-
data: parsedData
|
|
1428
|
-
});
|
|
1429
|
-
return { validQuestions, errors };
|
|
1430
|
-
}
|
|
1431
|
-
parsedData.forEach((rawQuestion, index) => {
|
|
1432
|
-
if (typeof rawQuestion !== "object" || rawQuestion === null || !rawQuestion.questionType) {
|
|
1433
|
-
errors.push({
|
|
1434
|
-
index: index + 1,
|
|
1435
|
-
message: 'Invalid question object. Each item must be an object with a "questionType" property.',
|
|
1436
|
-
data: rawQuestion
|
|
1437
|
-
});
|
|
1438
|
-
return;
|
|
1439
|
-
}
|
|
1440
|
-
const schema = schemaMap[rawQuestion.questionType];
|
|
1441
|
-
if (!schema) {
|
|
1442
|
-
errors.push({
|
|
1443
|
-
index: index + 1,
|
|
1444
|
-
message: `Unsupported question type: "${rawQuestion.questionType}".`,
|
|
1445
|
-
data: rawQuestion
|
|
1446
|
-
});
|
|
1447
|
-
return;
|
|
1448
|
-
}
|
|
1449
|
-
const validationResult = schema.safeParse(rawQuestion);
|
|
1450
|
-
if (validationResult.success) {
|
|
1451
|
-
const sanitizedQuestion = this._sanitizeAndRegenerateIds(validationResult.data);
|
|
1452
|
-
validQuestions.push(sanitizedQuestion);
|
|
1453
|
-
} else {
|
|
1454
|
-
const errorMessages = validationResult.error.errors.map((e) => `${e.path.join(".")} - ${e.message}`).join("; ");
|
|
1455
|
-
errors.push({
|
|
1456
|
-
index: index + 1,
|
|
1457
|
-
message: `Validation failed: ${errorMessages}`,
|
|
1458
|
-
data: rawQuestion
|
|
1459
|
-
});
|
|
1460
|
-
}
|
|
1461
|
-
});
|
|
1462
|
-
return { validQuestions, errors };
|
|
1463
1738
|
}
|
|
1464
|
-
/**
|
|
1465
|
-
* Processes a TSV string to import questions.
|
|
1466
|
-
* @param tsvString The raw TSV string to process.
|
|
1467
|
-
* @returns An object containing arrays of valid questions and any errors encountered.
|
|
1468
|
-
*/
|
|
1469
1739
|
static processTSV(tsvString) {
|
|
1470
|
-
const validQuestions = [];
|
|
1471
|
-
const errors = [];
|
|
1472
1740
|
const lines = tsvString.split(/\r?\n/).filter((line) => line.trim() !== "");
|
|
1473
1741
|
if (lines.length < 2) {
|
|
1474
|
-
errors
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
const
|
|
1478
|
-
const
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
const dataRows = lines.slice(1);
|
|
1487
|
-
dataRows.forEach((row, index) => {
|
|
1488
|
-
const rowNumber = index + 2;
|
|
1489
|
-
const columns = row.split(" ");
|
|
1742
|
+
return { validQuestions: [], errors: [{ index: 0, message: "TSV file must have a header and at least one data row.", data: tsvString }] };
|
|
1743
|
+
}
|
|
1744
|
+
const header = lines.shift().split(" ").map((h) => h.trim());
|
|
1745
|
+
const rawObjects = [];
|
|
1746
|
+
const errors = [];
|
|
1747
|
+
lines.forEach((line, index) => {
|
|
1748
|
+
const values = line.split(" ");
|
|
1749
|
+
const rowObject = {};
|
|
1750
|
+
header.forEach((h, i) => {
|
|
1751
|
+
var _a;
|
|
1752
|
+
rowObject[h] = ((_a = values[i]) == null ? void 0 : _a.trim()) || "";
|
|
1753
|
+
});
|
|
1490
1754
|
try {
|
|
1491
|
-
const
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1755
|
+
const transformedObject = this.transformTsvRowToRawObject(rowObject);
|
|
1756
|
+
rawObjects.push(transformedObject);
|
|
1757
|
+
} catch (e) {
|
|
1758
|
+
const message = e instanceof Error ? e.message : "Error transforming TSV row.";
|
|
1759
|
+
errors.push({ index: index + 2, message, data: line });
|
|
1760
|
+
}
|
|
1761
|
+
});
|
|
1762
|
+
const processedResult = this.processRawObjects(rawObjects);
|
|
1763
|
+
return { validQuestions: processedResult.validQuestions, errors: [...errors, ...processedResult.errors] };
|
|
1764
|
+
}
|
|
1765
|
+
static processRawObjects(rawObjects) {
|
|
1766
|
+
const validQuestions = [];
|
|
1767
|
+
const errors = [];
|
|
1768
|
+
rawObjects.forEach((rawQ, index) => {
|
|
1769
|
+
try {
|
|
1770
|
+
const validatedRawQ = AnyRawQuestionSchema.parse(rawQ);
|
|
1771
|
+
const question = this.createQuestionFromRawObject(validatedRawQ);
|
|
1772
|
+
validQuestions.push(question);
|
|
1506
1773
|
} catch (e) {
|
|
1507
|
-
errors.
|
|
1508
|
-
|
|
1509
|
-
message: e.message,
|
|
1510
|
-
data: row
|
|
1511
|
-
});
|
|
1774
|
+
const message = e instanceof z.ZodError ? e.errors.map((err) => `${err.path.join(".")} - ${err.message}`).join("; ") : e instanceof Error ? e.message : "Unknown validation error.";
|
|
1775
|
+
errors.push({ index: index + 1, message, data: rawQ });
|
|
1512
1776
|
}
|
|
1513
1777
|
});
|
|
1514
1778
|
return { validQuestions, errors };
|
|
1515
1779
|
}
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
explanation: getColumn("Explanation"),
|
|
1528
|
-
points: parseInt(getColumn("Points"), 10) || 10,
|
|
1529
|
-
difficulty: tsvDifficultyMap[getColumn("Difficulty").toLowerCase()] || "medium",
|
|
1530
|
-
bloomLevel: tsvBloomLevelMap[getColumn("Bloom Taxonomy").toLowerCase()],
|
|
1531
|
-
topic: getColumn("Concepts")
|
|
1532
|
-
// Add other base fields here if needed
|
|
1780
|
+
static transformTsvRowToRawObject(row) {
|
|
1781
|
+
const { questionType, prompt, options, correctAnswer, points, explanation, topic, difficulty, bloomLevel, tolerance } = row;
|
|
1782
|
+
if (!questionType) throw new Error("`questionType` column is missing or empty.");
|
|
1783
|
+
const base = {
|
|
1784
|
+
questionType,
|
|
1785
|
+
prompt,
|
|
1786
|
+
points: points ? parseInt(points, 10) : void 0,
|
|
1787
|
+
explanation,
|
|
1788
|
+
topic,
|
|
1789
|
+
difficulty,
|
|
1790
|
+
bloomLevel
|
|
1533
1791
|
};
|
|
1534
|
-
|
|
1535
|
-
switch (type) {
|
|
1792
|
+
switch (questionType) {
|
|
1536
1793
|
case "multiple_choice":
|
|
1537
|
-
return
|
|
1794
|
+
return __spreadProps(__spreadValues({}, base), { options: options.split("|"), correctAnswer });
|
|
1538
1795
|
case "multiple_response":
|
|
1539
|
-
return
|
|
1796
|
+
return __spreadProps(__spreadValues({}, base), { options: options.split("|"), correctAnswers: correctAnswer.split("|") });
|
|
1540
1797
|
case "true_false":
|
|
1541
|
-
return
|
|
1798
|
+
return __spreadProps(__spreadValues({}, base), { correctAnswer: correctAnswer.toLowerCase() === "true" });
|
|
1542
1799
|
case "short_answer":
|
|
1543
|
-
return __spreadProps(__spreadValues({},
|
|
1800
|
+
return __spreadProps(__spreadValues({}, base), { acceptedAnswers: correctAnswer.split("|") });
|
|
1544
1801
|
case "numeric":
|
|
1545
|
-
|
|
1546
|
-
if (isNaN(answer)) throw new Error("Numeric answer is not a valid number.");
|
|
1547
|
-
return __spreadProps(__spreadValues({}, baseQuestion), { questionType: "numeric", answer, tolerance: void 0 });
|
|
1548
|
-
case "matching":
|
|
1549
|
-
return this._transformMG(baseQuestion, answerCols);
|
|
1802
|
+
return __spreadProps(__spreadValues({}, base), { answer: parseFloat(correctAnswer), tolerance: tolerance ? parseFloat(tolerance) : void 0 });
|
|
1550
1803
|
case "sequence":
|
|
1551
|
-
return
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
const id = generateUniqueId("opt_");
|
|
1563
|
-
if (isCorrect) {
|
|
1564
|
-
correctAnswerId = id;
|
|
1804
|
+
return __spreadProps(__spreadValues({}, base), { items: options.split("|"), correctOrder: correctAnswer.split("|") });
|
|
1805
|
+
case "matching": {
|
|
1806
|
+
const [promptsStr, optionsStr] = options.split("#");
|
|
1807
|
+
const prompts = promptsStr.replace("prompts:", "").split("|");
|
|
1808
|
+
const opts = optionsStr.replace("options:", "").split("|");
|
|
1809
|
+
const correctAnswerMap = correctAnswer.split("|").reduce((acc, pair) => {
|
|
1810
|
+
const [key, ...valParts] = pair.split(":");
|
|
1811
|
+
acc[key] = valParts.join(":");
|
|
1812
|
+
return acc;
|
|
1813
|
+
}, {});
|
|
1814
|
+
return __spreadProps(__spreadValues({}, base), { prompts, options: opts, correctAnswerMap });
|
|
1565
1815
|
}
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
const options = answers.map((ans) => {
|
|
1574
|
-
const isCorrect = ans.startsWith("*");
|
|
1575
|
-
const text = isCorrect ? ans.substring(1).trim() : ans.trim();
|
|
1576
|
-
const id = generateUniqueId("opt_mr_");
|
|
1577
|
-
if (isCorrect) {
|
|
1578
|
-
correctAnswerIds.push(id);
|
|
1816
|
+
case "fill_in_the_blanks": {
|
|
1817
|
+
const blanks = correctAnswer.split("#").reduce((acc, part) => {
|
|
1818
|
+
const [key, valuesStr] = part.split(":");
|
|
1819
|
+
acc[key] = valuesStr.split("|");
|
|
1820
|
+
return acc;
|
|
1821
|
+
}, {});
|
|
1822
|
+
return __spreadProps(__spreadValues({}, base), { sentenceWithPlaceholders: options, blanks });
|
|
1579
1823
|
}
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
return __spreadProps(__spreadValues({}, base), { questionType: "multiple_response", options, correctAnswerIds });
|
|
1584
|
-
}
|
|
1585
|
-
static _transformTF(base, answers) {
|
|
1586
|
-
if (answers.length < 2) throw new Error("True/False question requires at least two answer columns.");
|
|
1587
|
-
const isTrueCorrect = answers[0].startsWith("*");
|
|
1588
|
-
const isFalseCorrect = answers[1].startsWith("*");
|
|
1589
|
-
if (isTrueCorrect === isFalseCorrect) throw new Error("True/False question must have exactly one correct answer marked with *.");
|
|
1590
|
-
return __spreadProps(__spreadValues({}, base), { questionType: "true_false", correctAnswer: isTrueCorrect });
|
|
1591
|
-
}
|
|
1592
|
-
static _transformMG(base, answers) {
|
|
1593
|
-
const prompts = [];
|
|
1594
|
-
const options = [];
|
|
1595
|
-
const correctAnswerMap = [];
|
|
1596
|
-
answers.forEach((pair) => {
|
|
1597
|
-
const parts = pair.split("|");
|
|
1598
|
-
if (parts.length !== 2) throw new Error(`Invalid matching pair format: "${pair}". Must be "prompt|option".`);
|
|
1599
|
-
const promptItem = { id: generateUniqueId("m_p_"), content: parts[0].trim() };
|
|
1600
|
-
const optionItem = { id: generateUniqueId("m_o_"), content: parts[1].trim() };
|
|
1601
|
-
prompts.push(promptItem);
|
|
1602
|
-
options.push(optionItem);
|
|
1603
|
-
correctAnswerMap.push({ promptId: promptItem.id, optionId: optionItem.id });
|
|
1604
|
-
});
|
|
1605
|
-
return __spreadProps(__spreadValues({}, base), { questionType: "matching", prompts, options, correctAnswerMap, shuffleOptions: true });
|
|
1606
|
-
}
|
|
1607
|
-
static _transformSEQ(base, answers) {
|
|
1608
|
-
const items = [];
|
|
1609
|
-
const correctOrder = [];
|
|
1610
|
-
answers.forEach((content) => {
|
|
1611
|
-
const item = { id: generateUniqueId("seqi_"), content: content.trim() };
|
|
1612
|
-
items.push(item);
|
|
1613
|
-
correctOrder.push(item.id);
|
|
1614
|
-
});
|
|
1615
|
-
return __spreadProps(__spreadValues({}, base), { questionType: "sequence", items, correctOrder });
|
|
1824
|
+
default:
|
|
1825
|
+
throw new Error(`Unsupported questionType "${questionType}" in TSV.`);
|
|
1826
|
+
}
|
|
1616
1827
|
}
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1828
|
+
static createQuestionFromRawObject(validatedRawQ) {
|
|
1829
|
+
const baseQuestionData = {
|
|
1830
|
+
id: generateUniqueId(validatedRawQ.questionType),
|
|
1831
|
+
prompt: validatedRawQ.prompt,
|
|
1832
|
+
points: validatedRawQ.points,
|
|
1833
|
+
explanation: validatedRawQ.explanation,
|
|
1834
|
+
topic: validatedRawQ.topic,
|
|
1835
|
+
difficulty: validatedRawQ.difficulty,
|
|
1836
|
+
bloomLevel: validatedRawQ.bloomLevel
|
|
1837
|
+
};
|
|
1838
|
+
switch (validatedRawQ.questionType) {
|
|
1623
1839
|
case "multiple_choice": {
|
|
1624
|
-
const
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
return __spreadProps(__spreadValues({}, opt), { id: newId });
|
|
1629
|
-
});
|
|
1630
|
-
newQuestion.correctAnswerId = oldIdToNewIdMap.get(newQuestion.correctAnswerId) || "";
|
|
1631
|
-
break;
|
|
1840
|
+
const options = validatedRawQ.options.map((text) => ({ id: generateUniqueId("opt_"), text }));
|
|
1841
|
+
const correctOption = options.find((opt) => opt.text === validatedRawQ.correctAnswer);
|
|
1842
|
+
if (!correctOption) throw new Error(`Correct answer "${validatedRawQ.correctAnswer}" not found in options.`);
|
|
1843
|
+
return __spreadProps(__spreadValues({}, baseQuestionData), { questionType: "multiple_choice", options, correctAnswerId: correctOption.id });
|
|
1632
1844
|
}
|
|
1633
1845
|
case "multiple_response": {
|
|
1634
|
-
const
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
return __spreadProps(__spreadValues({}, opt), { id: newId });
|
|
1639
|
-
});
|
|
1640
|
-
newQuestion.correctAnswerIds = newQuestion.correctAnswerIds.map((oldId) => oldIdToNewIdMap.get(oldId)).filter((newId) => !!newId);
|
|
1641
|
-
break;
|
|
1642
|
-
}
|
|
1643
|
-
case "fill_in_the_blanks": {
|
|
1644
|
-
const oldBlankIdToNewIdMap = /* @__PURE__ */ new Map();
|
|
1645
|
-
newQuestion.segments = newQuestion.segments.map((seg) => {
|
|
1646
|
-
if (seg.type === "blank" && seg.id) {
|
|
1647
|
-
const newId = generateUniqueId("blank_");
|
|
1648
|
-
oldBlankIdToNewIdMap.set(seg.id, newId);
|
|
1649
|
-
return __spreadProps(__spreadValues({}, seg), { id: newId });
|
|
1650
|
-
}
|
|
1651
|
-
return seg;
|
|
1652
|
-
});
|
|
1653
|
-
newQuestion.answers = newQuestion.answers.map((ans) => {
|
|
1654
|
-
const newBlankId = oldBlankIdToNewIdMap.get(ans.blankId);
|
|
1655
|
-
return newBlankId ? __spreadProps(__spreadValues({}, ans), { blankId: newBlankId }) : ans;
|
|
1656
|
-
});
|
|
1657
|
-
break;
|
|
1846
|
+
const options = validatedRawQ.options.map((text) => ({ id: generateUniqueId("opt_mr_"), text }));
|
|
1847
|
+
const correctIds = options.filter((opt) => validatedRawQ.correctAnswers.includes(opt.text)).map((opt) => opt.id);
|
|
1848
|
+
if (correctIds.length !== validatedRawQ.correctAnswers.length) throw new Error("Some correct answers were not found in options.");
|
|
1849
|
+
return __spreadProps(__spreadValues({}, baseQuestionData), { questionType: "multiple_response", options, correctAnswerIds: correctIds });
|
|
1658
1850
|
}
|
|
1851
|
+
case "true_false":
|
|
1852
|
+
return __spreadProps(__spreadValues({}, baseQuestionData), { questionType: "true_false", correctAnswer: validatedRawQ.correctAnswer });
|
|
1853
|
+
case "short_answer":
|
|
1854
|
+
return __spreadProps(__spreadValues({}, baseQuestionData), { questionType: "short_answer", acceptedAnswers: validatedRawQ.acceptedAnswers, isCaseSensitive: false });
|
|
1855
|
+
case "numeric":
|
|
1856
|
+
return __spreadProps(__spreadValues({}, baseQuestionData), { questionType: "numeric", answer: validatedRawQ.answer, tolerance: validatedRawQ.tolerance });
|
|
1659
1857
|
case "sequence": {
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1858
|
+
if (validatedRawQ.items.length !== validatedRawQ.correctOrder.length) {
|
|
1859
|
+
throw new Error("The number of items must match the number of items in the correct order for a sequence question.");
|
|
1860
|
+
}
|
|
1861
|
+
const items = validatedRawQ.items.map((content) => ({ id: generateUniqueId("seqi_"), content }));
|
|
1862
|
+
const correctOrder = validatedRawQ.correctOrder.map((orderText) => {
|
|
1863
|
+
const foundItem = items.find((item) => item.content === orderText);
|
|
1864
|
+
if (!foundItem) throw new Error(`Sequence item "${orderText}" in correctOrder not found in items list.`);
|
|
1865
|
+
return foundItem.id;
|
|
1665
1866
|
});
|
|
1666
|
-
|
|
1667
|
-
break;
|
|
1867
|
+
return __spreadProps(__spreadValues({}, baseQuestionData), { questionType: "sequence", items, correctOrder });
|
|
1668
1868
|
}
|
|
1669
1869
|
case "matching": {
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
return __spreadProps(__spreadValues({}, o), { id: newId });
|
|
1681
|
-
});
|
|
1682
|
-
newQuestion.correctAnswerMap = newQuestion.correctAnswerMap.map((map) => ({
|
|
1683
|
-
promptId: oldPromptIdMap.get(map.promptId) || "",
|
|
1684
|
-
optionId: oldOptionIdMap.get(map.optionId) || ""
|
|
1685
|
-
})).filter((map) => map.promptId && map.optionId);
|
|
1686
|
-
break;
|
|
1687
|
-
}
|
|
1688
|
-
case "drag_and_drop": {
|
|
1689
|
-
const oldDraggableIdMap = /* @__PURE__ */ new Map();
|
|
1690
|
-
const oldDropZoneIdMap = /* @__PURE__ */ new Map();
|
|
1691
|
-
newQuestion.draggableItems = newQuestion.draggableItems.map((item) => {
|
|
1692
|
-
const newId = generateUniqueId("drag_");
|
|
1693
|
-
oldDraggableIdMap.set(item.id, newId);
|
|
1694
|
-
return __spreadProps(__spreadValues({}, item), { id: newId });
|
|
1695
|
-
});
|
|
1696
|
-
newQuestion.dropZones = newQuestion.dropZones.map((zone) => {
|
|
1697
|
-
const newId = generateUniqueId("zone_");
|
|
1698
|
-
oldDropZoneIdMap.set(zone.id, newId);
|
|
1699
|
-
return __spreadProps(__spreadValues({}, zone), { id: newId });
|
|
1870
|
+
if (validatedRawQ.prompts.length !== Object.keys(validatedRawQ.correctAnswerMap).length) {
|
|
1871
|
+
throw new Error("Each prompt must have a corresponding correct answer in the map for a matching question.");
|
|
1872
|
+
}
|
|
1873
|
+
const prompts = validatedRawQ.prompts.map((p) => ({ id: generateUniqueId("matp_"), content: p }));
|
|
1874
|
+
const options = validatedRawQ.options.map((o) => ({ id: generateUniqueId("mato_"), content: o }));
|
|
1875
|
+
const correctAnswerMap = Object.entries(validatedRawQ.correctAnswerMap).map(([promptText, optionText]) => {
|
|
1876
|
+
const prompt = prompts.find((p) => p.content === promptText);
|
|
1877
|
+
const option = options.find((o) => o.content === optionText);
|
|
1878
|
+
if (!prompt || !option) throw new Error(`Matching pair "${promptText}":"${optionText}" not found in prompts/options.`);
|
|
1879
|
+
return { promptId: prompt.id, optionId: option.id };
|
|
1700
1880
|
});
|
|
1701
|
-
|
|
1702
|
-
draggableId: oldDraggableIdMap.get(map.draggableId) || "",
|
|
1703
|
-
dropZoneId: oldDropZoneIdMap.get(map.dropZoneId) || ""
|
|
1704
|
-
})).filter((map) => map.draggableId && map.dropZoneId);
|
|
1705
|
-
break;
|
|
1881
|
+
return __spreadProps(__spreadValues({}, baseQuestionData), { questionType: "matching", prompts, options, correctAnswerMap, shuffleOptions: true });
|
|
1706
1882
|
}
|
|
1707
|
-
case "
|
|
1708
|
-
const
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1883
|
+
case "fill_in_the_blanks": {
|
|
1884
|
+
const { sentenceWithPlaceholders, blanks } = validatedRawQ;
|
|
1885
|
+
const segments = [];
|
|
1886
|
+
const answers = [];
|
|
1887
|
+
const placeholderMap = {};
|
|
1888
|
+
Object.keys(blanks).forEach((placeholder) => {
|
|
1889
|
+
const blankId = generateUniqueId("blank_");
|
|
1890
|
+
placeholderMap[placeholder] = blankId;
|
|
1891
|
+
answers.push({ blankId, acceptedValues: blanks[placeholder] });
|
|
1713
1892
|
});
|
|
1714
|
-
|
|
1715
|
-
|
|
1893
|
+
const regex = /\{\{([^}]+)\}\}/g;
|
|
1894
|
+
let lastIndex = 0;
|
|
1895
|
+
let match;
|
|
1896
|
+
while ((match = regex.exec(sentenceWithPlaceholders)) !== null) {
|
|
1897
|
+
if (match.index > lastIndex) {
|
|
1898
|
+
segments.push({ type: "text", content: sentenceWithPlaceholders.substring(lastIndex, match.index) });
|
|
1899
|
+
}
|
|
1900
|
+
const placeholder = match[1];
|
|
1901
|
+
const blankId = placeholderMap[placeholder];
|
|
1902
|
+
if (!blankId) throw new Error(`Placeholder "{{${placeholder}}}" found in sentence but not defined in blanks object.`);
|
|
1903
|
+
segments.push({ type: "blank", id: blankId });
|
|
1904
|
+
lastIndex = regex.lastIndex;
|
|
1905
|
+
}
|
|
1906
|
+
if (lastIndex < sentenceWithPlaceholders.length) {
|
|
1907
|
+
segments.push({ type: "text", content: sentenceWithPlaceholders.substring(lastIndex) });
|
|
1908
|
+
}
|
|
1909
|
+
return __spreadProps(__spreadValues({}, baseQuestionData), { questionType: "fill_in_the_blanks", segments, answers, isCaseSensitive: false });
|
|
1716
1910
|
}
|
|
1717
|
-
case "true_false":
|
|
1718
|
-
case "short_answer":
|
|
1719
|
-
case "numeric":
|
|
1720
|
-
case "blockly_programming":
|
|
1721
|
-
case "scratch_programming":
|
|
1722
|
-
break;
|
|
1723
|
-
default:
|
|
1724
|
-
const _exhaustiveCheck = newQuestion;
|
|
1725
|
-
console.warn("Unhandled question type in _sanitizeAndRegenerateIds:", _exhaustiveCheck);
|
|
1726
|
-
break;
|
|
1727
1911
|
}
|
|
1728
|
-
|
|
1912
|
+
throw new Error(`Unhandled question type in createQuestionFromRawObject: ${validatedRawQ.questionType}`);
|
|
1729
1913
|
}
|
|
1730
1914
|
};
|
|
1731
1915
|
|
|
1732
|
-
// src/services/
|
|
1733
|
-
var
|
|
1734
|
-
var
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
try {
|
|
1738
|
-
return window.btoa(data);
|
|
1739
|
-
} catch (e) {
|
|
1740
|
-
console.error("Base64 encoding (btoa) failed:", e);
|
|
1741
|
-
return data;
|
|
1742
|
-
}
|
|
1916
|
+
// src/services/UserConfigService.ts
|
|
1917
|
+
var LOCAL_STORAGE_PREFIX2 = "iqk_user_config_";
|
|
1918
|
+
var UserConfigService = class {
|
|
1919
|
+
static getStorageKey(key) {
|
|
1920
|
+
return `${LOCAL_STORAGE_PREFIX2}${key}`;
|
|
1743
1921
|
}
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
return data;
|
|
1922
|
+
static setConfig(key, value) {
|
|
1923
|
+
if (typeof window !== "undefined" && window.localStorage) {
|
|
1924
|
+
try {
|
|
1925
|
+
const serializedValue = JSON.stringify(value);
|
|
1926
|
+
localStorage.setItem(this.getStorageKey(key), serializedValue);
|
|
1927
|
+
} catch (e) {
|
|
1928
|
+
console.error(`Error saving config key "${key}" to localStorage:`, e);
|
|
1929
|
+
}
|
|
1753
1930
|
}
|
|
1754
1931
|
}
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1932
|
+
static getConfig(key, defaultValue = null) {
|
|
1933
|
+
if (typeof window !== "undefined" && window.localStorage) {
|
|
1934
|
+
try {
|
|
1935
|
+
const storedValue = localStorage.getItem(this.getStorageKey(key));
|
|
1936
|
+
if (storedValue !== null) {
|
|
1937
|
+
return JSON.parse(storedValue);
|
|
1938
|
+
}
|
|
1939
|
+
} catch (e) {
|
|
1940
|
+
console.error(`Error retrieving or parsing config key "${key}" from localStorage:`, e);
|
|
1941
|
+
return defaultValue;
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
return defaultValue;
|
|
1760
1945
|
}
|
|
1761
|
-
|
|
1762
|
-
* Saves an API key to localStorage. The key is mildly obfuscated using Base64.
|
|
1763
|
-
* @param serviceName - The name of the service (e.g., 'gemini').
|
|
1764
|
-
* @param apiKey - The API key to save.
|
|
1765
|
-
*/
|
|
1766
|
-
static saveAPIKey(serviceName, apiKey) {
|
|
1946
|
+
static removeConfig(key) {
|
|
1767
1947
|
if (typeof window !== "undefined" && window.localStorage) {
|
|
1768
1948
|
try {
|
|
1769
|
-
|
|
1770
|
-
localStorage.setItem(this.getStorageKey(serviceName), encodedKey);
|
|
1949
|
+
localStorage.removeItem(this.getStorageKey(key));
|
|
1771
1950
|
} catch (e) {
|
|
1772
|
-
console.error(`Error
|
|
1951
|
+
console.error(`Error removing config key "${key}" from localStorage:`, e);
|
|
1773
1952
|
}
|
|
1774
|
-
} else {
|
|
1775
|
-
console.warn("localStorage is not available. APIKeyService cannot save keys.");
|
|
1776
1953
|
}
|
|
1777
1954
|
}
|
|
1955
|
+
// --- Convenience Methods for Simple Configs ---
|
|
1956
|
+
static getFullName() {
|
|
1957
|
+
return this.getConfig("fullName", null);
|
|
1958
|
+
}
|
|
1959
|
+
static setFullName(name) {
|
|
1960
|
+
this.setConfig("fullName", name);
|
|
1961
|
+
}
|
|
1962
|
+
static getWeeklyGoal() {
|
|
1963
|
+
const goal = this.getConfig("weeklyGoal", 5);
|
|
1964
|
+
return typeof goal === "number" ? goal : 5;
|
|
1965
|
+
}
|
|
1966
|
+
static setWeeklyGoal(goal) {
|
|
1967
|
+
this.setConfig("weeklyGoal", goal);
|
|
1968
|
+
}
|
|
1969
|
+
static getLanguage() {
|
|
1970
|
+
var _a;
|
|
1971
|
+
return (_a = this.getConfig("language", "en")) != null ? _a : "en";
|
|
1972
|
+
}
|
|
1973
|
+
static setLanguage(language) {
|
|
1974
|
+
this.setConfig("language", language);
|
|
1975
|
+
}
|
|
1976
|
+
// --- Methods for Advanced Goal Management ---
|
|
1977
|
+
static getGoals() {
|
|
1978
|
+
return this.getConfig("advanced_goals", []) || [];
|
|
1979
|
+
}
|
|
1980
|
+
static saveGoals(goals) {
|
|
1981
|
+
this.setConfig("advanced_goals", goals);
|
|
1982
|
+
}
|
|
1778
1983
|
/**
|
|
1779
|
-
*
|
|
1780
|
-
* @param
|
|
1781
|
-
* @returns The decoded API key, or null if not found or if localStorage is unavailable.
|
|
1984
|
+
* Adds a new goal to the user's list. The goal object should not contain a description.
|
|
1985
|
+
* @param newGoal A partial Goal object without id, description, isAchieved, or achievedAt.
|
|
1782
1986
|
*/
|
|
1783
|
-
static
|
|
1784
|
-
|
|
1987
|
+
static addGoal(newGoal) {
|
|
1988
|
+
const goals = this.getGoals();
|
|
1989
|
+
const goalToAdd = __spreadProps(__spreadValues({}, newGoal), {
|
|
1990
|
+
id: generateUniqueId("goal_"),
|
|
1991
|
+
isAchieved: false
|
|
1992
|
+
});
|
|
1993
|
+
this.saveGoals([...goals, goalToAdd]);
|
|
1994
|
+
}
|
|
1995
|
+
static updateGoal(updatedGoal) {
|
|
1996
|
+
const goals = this.getGoals();
|
|
1997
|
+
const index = goals.findIndex((g) => g.id === updatedGoal.id);
|
|
1998
|
+
if (index > -1) {
|
|
1999
|
+
goals[index] = updatedGoal;
|
|
2000
|
+
this.saveGoals(goals);
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
static deleteGoal(goalId) {
|
|
2004
|
+
const goals = this.getGoals();
|
|
2005
|
+
const filteredGoals = goals.filter((g) => g.id !== goalId);
|
|
2006
|
+
this.saveGoals(filteredGoals);
|
|
2007
|
+
}
|
|
2008
|
+
};
|
|
2009
|
+
|
|
2010
|
+
// src/services/PracticeHistoryService.ts
|
|
2011
|
+
var LOCAL_STORAGE_KEY = "iqk_practice_history_v2";
|
|
2012
|
+
var PracticeHistoryService = class {
|
|
2013
|
+
static getPracticeHistory() {
|
|
2014
|
+
if (typeof window === "undefined") return [];
|
|
2015
|
+
try {
|
|
2016
|
+
const storedData = localStorage.getItem(LOCAL_STORAGE_KEY);
|
|
2017
|
+
return storedData ? JSON.parse(storedData) : [];
|
|
2018
|
+
} catch (e) {
|
|
2019
|
+
console.error("Error retrieving practice history:", e);
|
|
2020
|
+
return [];
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
static saveHistory(history) {
|
|
2024
|
+
if (typeof window !== "undefined") {
|
|
1785
2025
|
try {
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
2026
|
+
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(history));
|
|
2027
|
+
} catch (e) {
|
|
2028
|
+
console.error("Error saving practice history to localStorage:", e);
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
static saveCompletedPracticeSession(quizConfig, result, review = null) {
|
|
2033
|
+
const history = this.getPracticeHistory();
|
|
2034
|
+
const topicsCovered = Array.from(
|
|
2035
|
+
new Set(quizConfig.questions.map((q) => JSON.stringify({
|
|
2036
|
+
subject: q.subject || "Uncategorized",
|
|
2037
|
+
category: q.category || "General",
|
|
2038
|
+
topic: q.topic || "General Topic"
|
|
2039
|
+
})))
|
|
2040
|
+
).map((s) => JSON.parse(s));
|
|
2041
|
+
const newSession = {
|
|
2042
|
+
id: `session_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`,
|
|
2043
|
+
timestamp: Date.now(),
|
|
2044
|
+
quizConfig,
|
|
2045
|
+
quizResult: result,
|
|
2046
|
+
quizReview: review,
|
|
2047
|
+
summary: {
|
|
2048
|
+
topics: topicsCovered,
|
|
2049
|
+
score: result.score,
|
|
2050
|
+
maxScore: result.maxScore,
|
|
2051
|
+
percentage: result.percentage
|
|
2052
|
+
}
|
|
2053
|
+
};
|
|
2054
|
+
history.unshift(newSession);
|
|
2055
|
+
this.saveHistory(history);
|
|
2056
|
+
return newSession.id;
|
|
2057
|
+
}
|
|
2058
|
+
static updatePracticeReview(sessionId, review) {
|
|
2059
|
+
const history = this.getPracticeHistory();
|
|
2060
|
+
const sessionIndex = history.findIndex((s) => s.id === sessionId);
|
|
2061
|
+
if (sessionIndex > -1) {
|
|
2062
|
+
history[sessionIndex].quizReview = review;
|
|
2063
|
+
this.saveHistory(history);
|
|
2064
|
+
} else {
|
|
2065
|
+
console.warn(`Could not find session with ID "${sessionId}" to update review.`);
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
static getPracticeSessionById(sessionId) {
|
|
2069
|
+
return this.getPracticeHistory().find((s) => s.id === sessionId) || null;
|
|
2070
|
+
}
|
|
2071
|
+
static getPracticeHistorySummary() {
|
|
2072
|
+
const history = this.getPracticeHistory();
|
|
2073
|
+
return history.map((session) => ({
|
|
2074
|
+
id: session.id,
|
|
2075
|
+
timestamp: session.timestamp,
|
|
2076
|
+
quizTitle: session.quizConfig.title,
|
|
2077
|
+
topics: session.summary.topics,
|
|
2078
|
+
score: session.summary.score,
|
|
2079
|
+
maxScore: session.summary.maxScore,
|
|
2080
|
+
percentage: session.summary.percentage
|
|
2081
|
+
}));
|
|
2082
|
+
}
|
|
2083
|
+
static getPracticeStats() {
|
|
2084
|
+
const history = this.getPracticeHistory();
|
|
2085
|
+
if (history.length === 0) {
|
|
2086
|
+
return {
|
|
2087
|
+
totalSessions: 0,
|
|
2088
|
+
currentStreak: 0,
|
|
2089
|
+
longestStreak: 0,
|
|
2090
|
+
activityCalendar: {},
|
|
2091
|
+
performanceBySubject: [],
|
|
2092
|
+
performanceByTopic: []
|
|
2093
|
+
};
|
|
2094
|
+
}
|
|
2095
|
+
const toLocalDateString = (timestamp) => {
|
|
2096
|
+
const date = new Date(timestamp);
|
|
2097
|
+
const offset = date.getTimezoneOffset();
|
|
2098
|
+
const adjustedDate = new Date(date.getTime() - offset * 60 * 1e3);
|
|
2099
|
+
return adjustedDate.toISOString().split("T")[0];
|
|
2100
|
+
};
|
|
2101
|
+
const activityCalendar = {};
|
|
2102
|
+
const uniquePracticeDays = /* @__PURE__ */ new Set();
|
|
2103
|
+
history.forEach((session) => {
|
|
2104
|
+
const dateStr = toLocalDateString(session.timestamp);
|
|
2105
|
+
activityCalendar[dateStr] = (activityCalendar[dateStr] || 0) + 1;
|
|
2106
|
+
uniquePracticeDays.add(dateStr);
|
|
2107
|
+
});
|
|
2108
|
+
const sortedDays = Array.from(uniquePracticeDays).sort().reverse();
|
|
2109
|
+
let currentStreak = 0;
|
|
2110
|
+
let longestStreak = 0;
|
|
2111
|
+
if (sortedDays.length > 0) {
|
|
2112
|
+
const today = /* @__PURE__ */ new Date();
|
|
2113
|
+
const todayStr = toLocalDateString(today.getTime());
|
|
2114
|
+
const yesterday = /* @__PURE__ */ new Date();
|
|
2115
|
+
yesterday.setDate(today.getDate() - 1);
|
|
2116
|
+
const yesterdayStr = toLocalDateString(yesterday.getTime());
|
|
2117
|
+
if (sortedDays[0] === todayStr || sortedDays[0] === yesterdayStr) {
|
|
2118
|
+
currentStreak = 1;
|
|
2119
|
+
for (let i = 0; i < sortedDays.length - 1; i++) {
|
|
2120
|
+
const currentDay = new Date(sortedDays[i]);
|
|
2121
|
+
const nextDay = new Date(sortedDays[i + 1]);
|
|
2122
|
+
const diffTime = currentDay.getTime() - nextDay.getTime();
|
|
2123
|
+
const diffDays = Math.round(diffTime / (1e3 * 60 * 60 * 24));
|
|
2124
|
+
if (diffDays === 1) {
|
|
2125
|
+
currentStreak++;
|
|
2126
|
+
} else {
|
|
2127
|
+
break;
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
if (sortedDays.length > 0) {
|
|
2133
|
+
let tempStreak = 1;
|
|
2134
|
+
longestStreak = 1;
|
|
2135
|
+
for (let i = 0; i < sortedDays.length - 1; i++) {
|
|
2136
|
+
const currentDay = new Date(sortedDays[i]);
|
|
2137
|
+
const nextDay = new Date(sortedDays[i + 1]);
|
|
2138
|
+
const diffTime = currentDay.getTime() - nextDay.getTime();
|
|
2139
|
+
const diffDays = Math.round(diffTime / (1e3 * 60 * 60 * 24));
|
|
2140
|
+
if (diffDays === 1) {
|
|
2141
|
+
tempStreak++;
|
|
2142
|
+
} else {
|
|
2143
|
+
tempStreak = 1;
|
|
1789
2144
|
}
|
|
2145
|
+
if (tempStreak > longestStreak) {
|
|
2146
|
+
longestStreak = tempStreak;
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
} else {
|
|
2150
|
+
longestStreak = 0;
|
|
2151
|
+
}
|
|
2152
|
+
const subjectPerf = {};
|
|
2153
|
+
const topicPerf = {};
|
|
2154
|
+
history.forEach((session) => {
|
|
2155
|
+
session.summary.topics.forEach((topicInfo) => {
|
|
2156
|
+
if (session.summary.percentage !== null) {
|
|
2157
|
+
if (!subjectPerf[topicInfo.subject]) subjectPerf[topicInfo.subject] = { total: 0, count: 0 };
|
|
2158
|
+
subjectPerf[topicInfo.subject].total += session.summary.percentage;
|
|
2159
|
+
subjectPerf[topicInfo.subject].count++;
|
|
2160
|
+
if (!topicPerf[topicInfo.topic]) topicPerf[topicInfo.topic] = { total: 0, count: 0 };
|
|
2161
|
+
topicPerf[topicInfo.topic].total += session.summary.percentage;
|
|
2162
|
+
topicPerf[topicInfo.topic].count++;
|
|
2163
|
+
}
|
|
2164
|
+
});
|
|
2165
|
+
});
|
|
2166
|
+
const formatPerf = (perfData) => {
|
|
2167
|
+
return Object.entries(perfData).map(([name, data]) => ({
|
|
2168
|
+
name,
|
|
2169
|
+
totalSessions: data.count,
|
|
2170
|
+
averageScore: parseFloat((data.total / data.count).toFixed(2))
|
|
2171
|
+
})).sort((a, b) => b.totalSessions - a.totalSessions);
|
|
2172
|
+
};
|
|
2173
|
+
return {
|
|
2174
|
+
totalSessions: history.length,
|
|
2175
|
+
currentStreak,
|
|
2176
|
+
longestStreak,
|
|
2177
|
+
activityCalendar,
|
|
2178
|
+
performanceBySubject: formatPerf(subjectPerf),
|
|
2179
|
+
performanceByTopic: formatPerf(topicPerf)
|
|
2180
|
+
};
|
|
2181
|
+
}
|
|
2182
|
+
static clearHistory() {
|
|
2183
|
+
if (typeof window !== "undefined") {
|
|
2184
|
+
localStorage.removeItem(LOCAL_STORAGE_KEY);
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
};
|
|
2188
|
+
|
|
2189
|
+
// src/data/achievements.json
|
|
2190
|
+
var achievements_default = [
|
|
2191
|
+
{
|
|
2192
|
+
id: "first_session",
|
|
2193
|
+
icon: "Rocket",
|
|
2194
|
+
nameKey: "achievements.first_session.name",
|
|
2195
|
+
descriptionKey: "achievements.first_session.description",
|
|
2196
|
+
condition: {
|
|
2197
|
+
type: "SESSION_COUNT",
|
|
2198
|
+
params: { count: 1 }
|
|
2199
|
+
}
|
|
2200
|
+
},
|
|
2201
|
+
{
|
|
2202
|
+
id: "five_sessions",
|
|
2203
|
+
icon: "BookOpen",
|
|
2204
|
+
nameKey: "achievements.five_sessions.name",
|
|
2205
|
+
descriptionKey: "achievements.five_sessions.description",
|
|
2206
|
+
condition: {
|
|
2207
|
+
type: "SESSION_COUNT",
|
|
2208
|
+
params: { count: 5 }
|
|
2209
|
+
}
|
|
2210
|
+
},
|
|
2211
|
+
{
|
|
2212
|
+
id: "perfect_score",
|
|
2213
|
+
icon: "Star",
|
|
2214
|
+
nameKey: "achievements.perfect_score.name",
|
|
2215
|
+
descriptionKey: "achievements.perfect_score.description",
|
|
2216
|
+
condition: {
|
|
2217
|
+
type: "PERFECT_SCORE",
|
|
2218
|
+
params: {}
|
|
2219
|
+
}
|
|
2220
|
+
},
|
|
2221
|
+
{
|
|
2222
|
+
id: "streak_3_days",
|
|
2223
|
+
icon: "Flame",
|
|
2224
|
+
nameKey: "achievements.streak_3_days.name",
|
|
2225
|
+
descriptionKey: "achievements.streak_3_days.description",
|
|
2226
|
+
condition: {
|
|
2227
|
+
type: "STREAK",
|
|
2228
|
+
params: { days: 3 }
|
|
2229
|
+
}
|
|
2230
|
+
},
|
|
2231
|
+
{
|
|
2232
|
+
id: "ten_sessions",
|
|
2233
|
+
icon: "Award",
|
|
2234
|
+
nameKey: "achievements.ten_sessions.name",
|
|
2235
|
+
descriptionKey: "achievements.ten_sessions.description",
|
|
2236
|
+
condition: {
|
|
2237
|
+
type: "SESSION_COUNT",
|
|
2238
|
+
params: { count: 10 }
|
|
2239
|
+
}
|
|
2240
|
+
},
|
|
2241
|
+
{
|
|
2242
|
+
id: "streak_7_days",
|
|
2243
|
+
icon: "Sparkles",
|
|
2244
|
+
nameKey: "achievements.streak_7_days.name",
|
|
2245
|
+
descriptionKey: "achievements.streak_7_days.description",
|
|
2246
|
+
condition: {
|
|
2247
|
+
type: "STREAK",
|
|
2248
|
+
params: { days: 7 }
|
|
2249
|
+
}
|
|
2250
|
+
},
|
|
2251
|
+
{
|
|
2252
|
+
id: "weekend_warrior",
|
|
2253
|
+
icon: "CalendarDays",
|
|
2254
|
+
nameKey: "achievements.weekend_warrior.name",
|
|
2255
|
+
descriptionKey: "achievements.weekend_warrior.description",
|
|
2256
|
+
condition: {
|
|
2257
|
+
type: "SESSION_ON_WEEKEND",
|
|
2258
|
+
params: {}
|
|
2259
|
+
}
|
|
2260
|
+
},
|
|
2261
|
+
{
|
|
2262
|
+
id: "night_owl",
|
|
2263
|
+
icon: "Moon",
|
|
2264
|
+
nameKey: "achievements.night_owl.name",
|
|
2265
|
+
descriptionKey: "achievements.night_owl.description",
|
|
2266
|
+
condition: {
|
|
2267
|
+
type: "SESSION_AFTER_HOUR",
|
|
2268
|
+
params: { hour: 22 }
|
|
2269
|
+
}
|
|
2270
|
+
},
|
|
2271
|
+
{
|
|
2272
|
+
id: "polymath_prospect",
|
|
2273
|
+
icon: "Library",
|
|
2274
|
+
nameKey: "achievements.polymath_prospect.name",
|
|
2275
|
+
descriptionKey: "achievements.polymath_prospect.description",
|
|
2276
|
+
condition: {
|
|
2277
|
+
type: "DISTINCT_SUBJECTS",
|
|
2278
|
+
params: { count: 3 }
|
|
2279
|
+
}
|
|
2280
|
+
},
|
|
2281
|
+
{
|
|
2282
|
+
id: "subject_specialist",
|
|
2283
|
+
icon: "Target",
|
|
2284
|
+
nameKey: "achievements.subject_specialist.name",
|
|
2285
|
+
descriptionKey: "achievements.subject_specialist.description",
|
|
2286
|
+
condition: {
|
|
2287
|
+
type: "SESSIONS_IN_SUBJECT",
|
|
2288
|
+
params: { count: 5 }
|
|
2289
|
+
}
|
|
2290
|
+
},
|
|
2291
|
+
{
|
|
2292
|
+
id: "high_achiever",
|
|
2293
|
+
icon: "TrendingUp",
|
|
2294
|
+
nameKey: "achievements.high_achiever.name",
|
|
2295
|
+
descriptionKey: "achievements.high_achiever.description",
|
|
2296
|
+
condition: {
|
|
2297
|
+
type: "CONSECUTIVE_HIGH_SCORE",
|
|
2298
|
+
params: { count: 3, score: 90 }
|
|
2299
|
+
}
|
|
2300
|
+
},
|
|
2301
|
+
{
|
|
2302
|
+
id: "comeback_kid",
|
|
2303
|
+
icon: "ChevronsUp",
|
|
2304
|
+
nameKey: "achievements.comeback_kid.name",
|
|
2305
|
+
descriptionKey: "achievements.comeback_kid.description",
|
|
2306
|
+
condition: {
|
|
2307
|
+
type: "SCORE_IMPROVEMENT",
|
|
2308
|
+
params: { improvement: 20 }
|
|
2309
|
+
}
|
|
2310
|
+
},
|
|
2311
|
+
{
|
|
2312
|
+
id: "twenty_five_sessions",
|
|
2313
|
+
icon: "Medal",
|
|
2314
|
+
nameKey: "achievements.twenty_five_sessions.name",
|
|
2315
|
+
descriptionKey: "achievements.twenty_five_sessions.description",
|
|
2316
|
+
condition: {
|
|
2317
|
+
type: "SESSION_COUNT",
|
|
2318
|
+
params: { count: 25 }
|
|
2319
|
+
}
|
|
2320
|
+
},
|
|
2321
|
+
{
|
|
2322
|
+
id: "streak_14_days",
|
|
2323
|
+
icon: "Gem",
|
|
2324
|
+
nameKey: "achievements.streak_14_days.name",
|
|
2325
|
+
descriptionKey: "achievements.streak_14_days.description",
|
|
2326
|
+
condition: {
|
|
2327
|
+
type: "STREAK",
|
|
2328
|
+
params: { days: 14 }
|
|
2329
|
+
}
|
|
2330
|
+
},
|
|
2331
|
+
{
|
|
2332
|
+
id: "subject_master",
|
|
2333
|
+
icon: "Crown",
|
|
2334
|
+
nameKey: "achievements.subject_master.name",
|
|
2335
|
+
descriptionKey: "achievements.subject_master.description",
|
|
2336
|
+
condition: {
|
|
2337
|
+
type: "SUBJECT_MASTERY",
|
|
2338
|
+
params: { score: 90, sessions: 5 }
|
|
2339
|
+
}
|
|
2340
|
+
},
|
|
2341
|
+
{
|
|
2342
|
+
id: "centurion",
|
|
2343
|
+
icon: "ShieldCheck",
|
|
2344
|
+
nameKey: "achievements.centurion.name",
|
|
2345
|
+
descriptionKey: "achievements.centurion.description",
|
|
2346
|
+
condition: {
|
|
2347
|
+
type: "SESSION_COUNT",
|
|
2348
|
+
params: { count: 100 }
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
];
|
|
2352
|
+
|
|
2353
|
+
// src/services/AchievementService.ts
|
|
2354
|
+
var LOCAL_STORAGE_KEY2 = "iqk_unlocked_achievements_v2";
|
|
2355
|
+
var ALL_ACHIEVEMENT_DEFS = achievements_default;
|
|
2356
|
+
var conditionEvaluators = {
|
|
2357
|
+
SESSION_COUNT: (params, history) => history.length >= params.count,
|
|
2358
|
+
PERFECT_SCORE: (params, history) => history.some((s) => s.summary.percentage === 100),
|
|
2359
|
+
STREAK: (params, history, stats) => stats.longestStreak >= params.days,
|
|
2360
|
+
SESSION_ON_WEEKEND: (params, history) => history.some((s) => [0, 6].includes(new Date(s.timestamp).getDay())),
|
|
2361
|
+
SESSION_AFTER_HOUR: (params, history) => history.some((s) => new Date(s.timestamp).getHours() >= params.hour),
|
|
2362
|
+
DISTINCT_SUBJECTS: (params, history) => {
|
|
2363
|
+
const subjects = new Set(history.flatMap((s) => s.summary.topics.map((t) => t.subject)));
|
|
2364
|
+
return subjects.size >= params.count;
|
|
2365
|
+
},
|
|
2366
|
+
SESSIONS_IN_SUBJECT: (params, history) => {
|
|
2367
|
+
const subjectCounts = history.flatMap((s) => s.summary.topics.map((t) => t.subject)).reduce((acc, subject) => {
|
|
2368
|
+
acc[subject] = (acc[subject] || 0) + 1;
|
|
2369
|
+
return acc;
|
|
2370
|
+
}, {});
|
|
2371
|
+
return Object.values(subjectCounts).some((count) => count >= params.count);
|
|
2372
|
+
},
|
|
2373
|
+
CONSECUTIVE_HIGH_SCORE: (params, history) => {
|
|
2374
|
+
if (history.length < params.count) return false;
|
|
2375
|
+
for (let i = 0; i <= history.length - params.count; i++) {
|
|
2376
|
+
const recentSessions = history.slice(i, i + params.count);
|
|
2377
|
+
if (recentSessions.every((s) => s.summary.percentage !== null && s.summary.percentage >= params.score)) {
|
|
2378
|
+
return true;
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
return false;
|
|
2382
|
+
},
|
|
2383
|
+
SCORE_IMPROVEMENT: (params, history) => {
|
|
2384
|
+
if (history.length < 2) return false;
|
|
2385
|
+
for (let i = 0; i < history.length - 1; i++) {
|
|
2386
|
+
const currentPercent = history[i].summary.percentage;
|
|
2387
|
+
const previousPercent = history[i + 1].summary.percentage;
|
|
2388
|
+
if (currentPercent !== null && previousPercent !== null && currentPercent - previousPercent >= params.improvement) {
|
|
2389
|
+
return true;
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
return false;
|
|
2393
|
+
},
|
|
2394
|
+
SUBJECT_MASTERY: (params, history, stats) => {
|
|
2395
|
+
return stats.performanceBySubject.some((s) => s.averageScore >= params.score && s.totalSessions >= params.sessions);
|
|
2396
|
+
}
|
|
2397
|
+
};
|
|
2398
|
+
var AchievementService = class {
|
|
2399
|
+
static getUnlockedAchievementsMap() {
|
|
2400
|
+
if (typeof window === "undefined") return {};
|
|
2401
|
+
try {
|
|
2402
|
+
const storedData = localStorage.getItem(LOCAL_STORAGE_KEY2);
|
|
2403
|
+
return storedData ? JSON.parse(storedData) : {};
|
|
2404
|
+
} catch (e) {
|
|
2405
|
+
console.error("Error retrieving unlocked achievements map:", e);
|
|
2406
|
+
return {};
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
static saveUnlockedAchievementsMap(unlockedMap) {
|
|
2410
|
+
if (typeof window !== "undefined") {
|
|
2411
|
+
try {
|
|
2412
|
+
localStorage.setItem(LOCAL_STORAGE_KEY2, JSON.stringify(unlockedMap));
|
|
1790
2413
|
} catch (e) {
|
|
1791
|
-
console.error(
|
|
2414
|
+
console.error("Error saving unlocked achievements map:", e);
|
|
1792
2415
|
}
|
|
1793
2416
|
}
|
|
1794
|
-
return null;
|
|
1795
2417
|
}
|
|
2418
|
+
static checkAndUnlockAchievements(history, stats) {
|
|
2419
|
+
const unlockedMap = this.getUnlockedAchievementsMap();
|
|
2420
|
+
const newlyUnlocked = [];
|
|
2421
|
+
let hasChanges = false;
|
|
2422
|
+
ALL_ACHIEVEMENT_DEFS.forEach((achievementDef) => {
|
|
2423
|
+
if (unlockedMap[achievementDef.id]) {
|
|
2424
|
+
return;
|
|
2425
|
+
}
|
|
2426
|
+
const evaluator = conditionEvaluators[achievementDef.condition.type];
|
|
2427
|
+
if (evaluator) {
|
|
2428
|
+
const isUnlocked = evaluator(achievementDef.condition.params, history, stats);
|
|
2429
|
+
if (isUnlocked) {
|
|
2430
|
+
const unlockTime = Date.now();
|
|
2431
|
+
unlockedMap[achievementDef.id] = unlockTime;
|
|
2432
|
+
newlyUnlocked.push(achievementDef);
|
|
2433
|
+
hasChanges = true;
|
|
2434
|
+
}
|
|
2435
|
+
} else {
|
|
2436
|
+
console.warn(`No evaluator found for achievement condition type: "${achievementDef.condition.type}"`);
|
|
2437
|
+
}
|
|
2438
|
+
});
|
|
2439
|
+
if (hasChanges) {
|
|
2440
|
+
this.saveUnlockedAchievementsMap(unlockedMap);
|
|
2441
|
+
}
|
|
2442
|
+
return newlyUnlocked;
|
|
2443
|
+
}
|
|
2444
|
+
static getAllAchievementDefinitions() {
|
|
2445
|
+
return ALL_ACHIEVEMENT_DEFS;
|
|
2446
|
+
}
|
|
2447
|
+
static getAllAchievementsWithStatus() {
|
|
2448
|
+
const unlockedMap = this.getUnlockedAchievementsMap();
|
|
2449
|
+
return ALL_ACHIEVEMENT_DEFS.map((def) => {
|
|
2450
|
+
const unlockedAt = unlockedMap[def.id];
|
|
2451
|
+
return {
|
|
2452
|
+
id: def.id,
|
|
2453
|
+
icon: def.icon,
|
|
2454
|
+
name: def.nameKey,
|
|
2455
|
+
description: def.descriptionKey,
|
|
2456
|
+
unlockedAt: unlockedAt || void 0
|
|
2457
|
+
};
|
|
2458
|
+
});
|
|
2459
|
+
}
|
|
2460
|
+
};
|
|
2461
|
+
|
|
2462
|
+
// src/services/MotivationalQuotes.ts
|
|
2463
|
+
var QUOTE_BANK = [
|
|
2464
|
+
{ text: "H\u1ECDc, h\u1ECDc n\u1EEFa, h\u1ECDc m\xE3i.", author: "V.I. Lenin" },
|
|
2465
|
+
{ text: "H\xE0nh tr\xECnh v\u1EA1n d\u1EB7m b\u1EAFt \u0111\u1EA7u b\u1EB1ng m\u1ED9t b\u01B0\u1EDBc ch\xE2n.", author: "L\xE3o T\u1EED" },
|
|
2466
|
+
{ text: "C\xE1ch t\u1ED1t nh\u1EA5t \u0111\u1EC3 d\u1EF1 \u0111o\xE1n t\u01B0\u01A1ng lai l\xE0 t\u1EA1o ra n\xF3.", author: "Peter Drucker" },
|
|
2467
|
+
{ text: "\u0110\u1EEBng lo l\u1EAFng v\u1EC1 th\u1EA5t b\u1EA1i; h\xE3y lo l\u1EAFng v\u1EC1 nh\u1EEFng c\u01A1 h\u1ED9i b\u1EA1n b\u1ECF l\u1EE1 khi kh\xF4ng d\xE1m th\u1EED.", author: "Jack Canfield" },
|
|
2468
|
+
{ text: "Th\xE0nh c\xF4ng kh\xF4ng ph\u1EA3i l\xE0 cu\u1ED1i c\xF9ng, th\u1EA5t b\u1EA1i kh\xF4ng ph\u1EA3i l\xE0 ch\u1EBFt ng\u01B0\u1EDDi: l\xF2ng can \u0111\u1EA3m \u0111i ti\u1EBFp m\u1EDBi quan tr\u1ECDng.", author: "Winston Churchill" },
|
|
2469
|
+
{ text: "Ch\u1EC9 nh\u1EEFng ng\u01B0\u1EDDi d\xE1m th\u1EA5t b\u1EA1i l\u1EDBn m\u1EDBi c\xF3 th\u1EC3 \u0111\u1EA1t \u0111\u01B0\u1EE3c th\xE0nh c\xF4ng l\u1EDBn.", author: "Robert F. Kennedy" },
|
|
2470
|
+
{ text: "Ki\u1EBFn th\u1EE9c l\xE0 s\u1EE9c m\u1EA1nh.", author: "Francis Bacon" },
|
|
2471
|
+
{ text: "S\u1EF1 kh\xE1c bi\u1EC7t gi\u1EEFa b\xECnh th\u01B0\u1EDDng v\xE0 phi th\u01B0\u1EDDng ch\u1EC9 l\xE0 m\u1ED9t ch\xFAt n\u1ED7 l\u1EF1c.", author: "Jimmy Johnson" },
|
|
2472
|
+
{ text: "H\xE3y l\xE0 s\u1EF1 thay \u0111\u1ED5i m\xE0 b\u1EA1n mu\u1ED1n th\u1EA5y tr\xEAn th\u1EBF gi\u1EDBi.", author: "Mahatma Gandhi" },
|
|
2473
|
+
{ text: "H\xF4m nay b\u1EA1n \u0111\u1ECDc, ng\xE0y mai b\u1EA1n s\u1EBD l\xE3nh \u0111\u1EA1o.", author: "Margaret Fuller" },
|
|
2474
|
+
{ text: "R\u1EC5 c\u1EE7a gi\xE1o d\u1EE5c th\xEC \u0111\u1EAFng, nh\u01B0ng qu\u1EA3 c\u1EE7a n\xF3 th\xEC ng\u1ECDt.", author: "Aristotle" },
|
|
2475
|
+
{ text: "C\xE1ch \u0111\u1EC3 b\u1EAFt \u0111\u1EA7u l\xE0 ng\u1EEBng n\xF3i v\xE0 b\u1EAFt \u0111\u1EA7u l\xE0m.", author: "Walt Disney" },
|
|
2476
|
+
{ text: "K\u1EF7 lu\u1EADt l\xE0 c\u1EA7u n\u1ED1i gi\u1EEFa m\u1EE5c ti\xEAu v\xE0 th\xE0nh t\u1EF1u.", author: "Jim Rohn" },
|
|
2477
|
+
{ text: "B\u1EA1n kh\xF4ng c\u1EA7n ph\u1EA3i v\u0129 \u0111\u1EA1i \u0111\u1EC3 b\u1EAFt \u0111\u1EA7u, nh\u01B0ng b\u1EA1n ph\u1EA3i b\u1EAFt \u0111\u1EA7u \u0111\u1EC3 tr\u1EDF n\xEAn v\u0129 \u0111\u1EA1i.", author: "Zig Ziglar" },
|
|
2478
|
+
{ text: "Tin r\u1EB1ng b\u1EA1n c\xF3 th\u1EC3 v\xE0 b\u1EA1n \u0111\xE3 \u0111i \u0111\u01B0\u1EE3c n\u1EEDa \u0111\u01B0\u1EDDng.", author: "Theodore Roosevelt" },
|
|
2479
|
+
{ text: "M\u1ED9t ng\u01B0\u1EDDi kh\xF4ng bao gi\u1EDD m\u1EAFc sai l\u1EA7m l\xE0 ng\u01B0\u1EDDi kh\xF4ng bao gi\u1EDD th\u1EED b\u1EA5t c\u1EE9 \u0111i\u1EC1u g\xEC m\u1EDBi.", author: "Albert Einstein" },
|
|
2480
|
+
{ text: "Th\u1EED th\xE1ch l\xE0 th\u1EE9 l\xE0m cho cu\u1ED9c s\u1ED1ng th\xFA v\u1ECB v\xE0 v\u01B0\u1EE3t qua ch\xFAng l\xE0 \u0111i\u1EC1u l\xE0m cho cu\u1ED9c s\u1ED1ng c\xF3 \xFD ngh\u0129a.", author: "Joshua J. Marine" },
|
|
2481
|
+
{ text: "Thi\xEAn t\xE0i l\xE0 m\u1ED9t ph\u1EA7n tr\u0103m c\u1EA3m h\u1EE9ng v\xE0 ch\xEDn m\u01B0\u01A1i ch\xEDn ph\u1EA7n tr\u0103m m\u1ED3 h\xF4i.", author: "Thomas Edison" },
|
|
2482
|
+
{ text: "\u0110\u1EEBng \u0111\u1EC3 ng\xE0y h\xF4m qua chi\u1EBFm qu\xE1 nhi\u1EC1u th\u1EDDi gian c\u1EE7a ng\xE0y h\xF4m nay.", author: "Will Rogers" },
|
|
2483
|
+
{ text: "N\u1EBFu c\u01A1 h\u1ED9i kh\xF4ng g\xF5 c\u1EEDa, h\xE3y x\xE2y m\u1ED9t c\xE1nh c\u1EEDa.", author: "Milton Berle" }
|
|
2484
|
+
];
|
|
2485
|
+
var QuoteService = class {
|
|
1796
2486
|
/**
|
|
1797
|
-
*
|
|
1798
|
-
* @
|
|
2487
|
+
* Retrieves a random quote from the internal quote bank.
|
|
2488
|
+
* @returns A quote object containing text and author.
|
|
1799
2489
|
*/
|
|
1800
|
-
static
|
|
1801
|
-
if (
|
|
2490
|
+
static getRandomQuote() {
|
|
2491
|
+
if (QUOTE_BANK.length === 0) {
|
|
2492
|
+
return { text: "H\xE3y b\u1EAFt \u0111\u1EA7u h\xE0nh tr\xECnh h\u1ECDc t\u1EADp c\u1EE7a b\u1EA1n.", author: "QuizKit" };
|
|
2493
|
+
}
|
|
2494
|
+
const randomIndex = Math.floor(Math.random() * QUOTE_BANK.length);
|
|
2495
|
+
return QUOTE_BANK[randomIndex];
|
|
2496
|
+
}
|
|
2497
|
+
};
|
|
2498
|
+
|
|
2499
|
+
// src/services/KnowledgeCardService.ts
|
|
2500
|
+
var LOCAL_STORAGE_KEY3 = "iqk_knowledge_cards_store";
|
|
2501
|
+
var GENERATION_FLAG_KEY = "iqk_card_generation_in_progress";
|
|
2502
|
+
var KnowledgeCardService = class {
|
|
2503
|
+
static generateHash(str) {
|
|
2504
|
+
let hash = 0;
|
|
2505
|
+
if (str.length === 0) return hash.toString();
|
|
2506
|
+
for (let i = 0; i < str.length; i++) {
|
|
2507
|
+
const char = str.charCodeAt(i);
|
|
2508
|
+
hash = (hash << 5) - hash + char;
|
|
2509
|
+
hash |= 0;
|
|
2510
|
+
}
|
|
2511
|
+
return hash.toString();
|
|
2512
|
+
}
|
|
2513
|
+
static getStore() {
|
|
2514
|
+
if (typeof window === "undefined") {
|
|
2515
|
+
return { sourceHash: null, conceptsPlan: [], generatedConcepts: {}, cards: [] };
|
|
2516
|
+
}
|
|
2517
|
+
try {
|
|
2518
|
+
const storedData = localStorage.getItem(LOCAL_STORAGE_KEY3);
|
|
2519
|
+
if (storedData) {
|
|
2520
|
+
return JSON.parse(storedData);
|
|
2521
|
+
}
|
|
2522
|
+
} catch (e) {
|
|
2523
|
+
console.error("Error reading Knowledge Card store from localStorage:", e);
|
|
2524
|
+
}
|
|
2525
|
+
return { sourceHash: null, conceptsPlan: [], generatedConcepts: {}, cards: [] };
|
|
2526
|
+
}
|
|
2527
|
+
static saveStore(store) {
|
|
2528
|
+
if (typeof window !== "undefined") {
|
|
1802
2529
|
try {
|
|
1803
|
-
localStorage.
|
|
2530
|
+
localStorage.setItem(LOCAL_STORAGE_KEY3, JSON.stringify(store));
|
|
1804
2531
|
} catch (e) {
|
|
1805
|
-
console.error(
|
|
2532
|
+
console.error("Error saving Knowledge Card store to localStorage:", e);
|
|
1806
2533
|
}
|
|
1807
2534
|
}
|
|
1808
2535
|
}
|
|
2536
|
+
static getSourceHash() {
|
|
2537
|
+
return this.getStore().sourceHash;
|
|
2538
|
+
}
|
|
2539
|
+
/**
|
|
2540
|
+
* Saves a new card generation plan. This clears old cards and resets the generation progress.
|
|
2541
|
+
* @param sourceHash The hash of the new source content.
|
|
2542
|
+
* @param concepts An array of concept strings to be generated.
|
|
2543
|
+
*/
|
|
2544
|
+
static saveCardPlan(sourceHash, concepts) {
|
|
2545
|
+
const newStore = {
|
|
2546
|
+
sourceHash,
|
|
2547
|
+
conceptsPlan: concepts,
|
|
2548
|
+
generatedConcepts: {},
|
|
2549
|
+
cards: []
|
|
2550
|
+
};
|
|
2551
|
+
this.saveStore(newStore);
|
|
2552
|
+
}
|
|
2553
|
+
/**
|
|
2554
|
+
* Saves a newly generated card to the store and marks its concept as completed.
|
|
2555
|
+
* @param card The KnowledgeCard object to save.
|
|
2556
|
+
*/
|
|
2557
|
+
static saveGeneratedCard(card) {
|
|
2558
|
+
const store = this.getStore();
|
|
2559
|
+
if (!store.cards.some((c) => c.id === card.id)) {
|
|
2560
|
+
store.cards.push(card);
|
|
2561
|
+
store.generatedConcepts[card.concept] = true;
|
|
2562
|
+
this.saveStore(store);
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
1809
2565
|
/**
|
|
1810
|
-
*
|
|
1811
|
-
* @
|
|
1812
|
-
* @returns True if a key exists, false otherwise.
|
|
2566
|
+
* Gets the list of concepts that are in the plan but have not been generated yet.
|
|
2567
|
+
* @returns An array of concept strings that are pending generation.
|
|
1813
2568
|
*/
|
|
1814
|
-
static
|
|
1815
|
-
|
|
2569
|
+
static getPendingConcepts() {
|
|
2570
|
+
const store = this.getStore();
|
|
2571
|
+
return store.conceptsPlan.filter((concept) => !store.generatedConcepts[concept]);
|
|
2572
|
+
}
|
|
2573
|
+
/**
|
|
2574
|
+
* Gets the current status of the card generation process.
|
|
2575
|
+
* @returns An object with the total number of cards planned and the number completed.
|
|
2576
|
+
*/
|
|
2577
|
+
static getGenerationStatus() {
|
|
2578
|
+
const store = this.getStore();
|
|
2579
|
+
return {
|
|
2580
|
+
total: store.conceptsPlan.length,
|
|
2581
|
+
completed: Object.keys(store.generatedConcepts).length
|
|
2582
|
+
};
|
|
2583
|
+
}
|
|
2584
|
+
/**
|
|
2585
|
+
* Checks if a generation process is currently marked as running in this browser tab/session.
|
|
2586
|
+
* Uses sessionStorage to prevent multiple tabs from running the same process.
|
|
2587
|
+
* @returns True if a process is marked as running, false otherwise.
|
|
2588
|
+
*/
|
|
2589
|
+
static isGenerationInProgress() {
|
|
2590
|
+
if (typeof window === "undefined") return false;
|
|
2591
|
+
return sessionStorage.getItem(GENERATION_FLAG_KEY) === "true";
|
|
2592
|
+
}
|
|
2593
|
+
/**
|
|
2594
|
+
* Sets or clears the flag indicating that a generation process is active.
|
|
2595
|
+
* @param status The status to set (true for running, false for stopped).
|
|
2596
|
+
*/
|
|
2597
|
+
static setGenerationStatus(status) {
|
|
2598
|
+
if (typeof window !== "undefined") {
|
|
2599
|
+
if (status) {
|
|
2600
|
+
sessionStorage.setItem(GENERATION_FLAG_KEY, "true");
|
|
2601
|
+
} else {
|
|
2602
|
+
sessionStorage.removeItem(GENERATION_FLAG_KEY);
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
static getCards() {
|
|
2607
|
+
return this.getStore().cards;
|
|
2608
|
+
}
|
|
2609
|
+
static getRandomCard() {
|
|
2610
|
+
const cards = this.getCards();
|
|
2611
|
+
if (cards.length === 0) return null;
|
|
2612
|
+
const randomIndex = Math.floor(Math.random() * cards.length);
|
|
2613
|
+
return cards[randomIndex];
|
|
2614
|
+
}
|
|
2615
|
+
static searchCards(query) {
|
|
2616
|
+
const cards = this.getCards();
|
|
2617
|
+
if (!query.trim()) return [];
|
|
2618
|
+
const lowerCaseQuery = query.toLowerCase();
|
|
2619
|
+
return cards.filter(
|
|
2620
|
+
(card) => card.concept.toLowerCase().includes(lowerCaseQuery) || card.definition.toLowerCase().includes(lowerCaseQuery)
|
|
2621
|
+
);
|
|
1816
2622
|
}
|
|
1817
2623
|
};
|
|
1818
2624
|
|
|
@@ -2437,4 +3243,4 @@ function cn(...inputs) {
|
|
|
2437
3243
|
return twMerge(clsx(inputs));
|
|
2438
3244
|
}
|
|
2439
3245
|
|
|
2440
|
-
export { APIKeyService, GEMINI_API_KEY_SERVICE_NAME, QuestionImportService, QuizEditorService, QuizEngine, SCORMService, cn, emptyQuiz, exportQuizAsSCORMZip, generateLauncherHTML, generateSCORMManifest, generateUniqueId, sampleQuiz };
|
|
3246
|
+
export { APIKeyService, AchievementService, GEMINI_API_KEY_SERVICE_NAME, KnowledgeCardService, PracticeHistoryService, QuestionImportService, QuizEditorService, QuizEngine, QuoteService, SCORMService, UserConfigService, cn, emptyQuiz, exportQuizAsSCORMZip, generateLauncherHTML, generateSCORMManifest, generateUniqueId, sampleQuiz };
|