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