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