@tinyweb_dev/oe-exam-sdk 1.0.7 → 1.0.9

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.
Files changed (65) hide show
  1. package/dist/components/exams/take/components/question-renderers/ChooseThenAnswerGroupRenderer.js +1 -1
  2. package/dist/components/questions/_shared/types/answer-the-question-group.type.d.ts +3 -0
  3. package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.d.ts +2 -2
  4. package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.js +5 -1
  5. package/dist/components/questions/_shared/types/choose-then-answer-group.type.d.ts +17 -0
  6. package/dist/components/questions/_shared/types/cross-out-word-group.type.d.ts +1 -0
  7. package/dist/components/questions/_shared/types/true-false-correct-group.type.d.ts +1 -0
  8. package/dist/components/questions/_shared/types/true-false-group.type.d.ts +1 -0
  9. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.d.ts +1 -1
  10. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +13 -15
  11. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js +2 -0
  12. package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.js +2 -0
  13. package/dist/components/questions/types/answer-the-question-group/transform.js +12 -5
  14. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseAnswerGroupImageUpload.d.ts +7 -0
  15. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseAnswerGroupImageUpload.js +4 -1
  16. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +24 -22
  17. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupCreator.js +45 -58
  18. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupItemEditor.d.ts +6 -0
  19. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupItemEditor.js +145 -0
  20. package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.js +6 -15
  21. package/dist/components/questions/types/choose-the-correct-answer-group/transform.js +5 -4
  22. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.d.ts +1 -1
  23. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js +75 -11
  24. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupCreator.js +54 -6
  25. package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.d.ts +4 -1
  26. package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.js +39 -2
  27. package/dist/components/questions/types/choose-then-answer-group/transform.js +27 -14
  28. package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.d.ts +1 -1
  29. package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.js +54 -3
  30. package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.d.ts +1 -1
  31. package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.js +15 -5
  32. package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.d.ts +12 -2
  33. package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.js +144 -45
  34. package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.d.ts +1 -1
  35. package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.js +79 -3
  36. package/dist/components/questions/types/spontaneous-qa-group/index.d.ts +1 -0
  37. package/dist/components/questions/types/spontaneous-qa-group/index.js +1 -0
  38. package/dist/components/questions/types/spontaneous-qa-group/map-spontaneous-qa-group-data.d.ts +2 -0
  39. package/dist/components/questions/types/spontaneous-qa-group/map-spontaneous-qa-group-data.js +74 -0
  40. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.d.ts +1 -1
  41. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js +26 -4
  42. package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.d.ts +1 -1
  43. package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.js +13 -3
  44. package/dist/components/questions/viewer/AnswerTheQuestionGroupViewer.d.ts +3 -2
  45. package/dist/components/questions/viewer/AnswerTheQuestionGroupViewer.js +12 -5
  46. package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.d.ts +3 -2
  47. package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.js +4 -3
  48. package/dist/components/questions/viewer/CrossOutWordGroupViewer.d.ts +3 -1
  49. package/dist/components/questions/viewer/CrossOutWordGroupViewer.js +15 -2
  50. package/dist/components/questions/viewer/QuestionViewer.d.ts +9 -1
  51. package/dist/components/questions/viewer/QuestionViewer.js +22 -9
  52. package/dist/components/questions/viewer/SpontaneousQaGroupViewer.js +4 -79
  53. package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.d.ts +3 -2
  54. package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.js +14 -4
  55. package/dist/components/questions/viewer/TrueFalseGroupViewer.d.ts +5 -2
  56. package/dist/components/questions/viewer/TrueFalseGroupViewer.js +14 -4
  57. package/dist/components/questions/viewer/WordOrderAndMatchGroupViewer.d.ts +6 -2
  58. package/dist/components/questions/viewer/WordOrderAndMatchGroupViewer.js +26 -6
  59. package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.js +1 -1
  60. package/dist/shared/lib/utils/question-reverse-transform.js +12 -4
  61. package/dist/shared/types/questions/answer-the-question-group.d.ts +8 -3
  62. package/dist/shared/types/questions/answer-the-question-group.js +5 -0
  63. package/dist/shared/types/questions/choose-the-correct-answer-group.d.ts +1 -1
  64. package/dist/shared/types/questions/choose-then-answer-group.d.ts +3 -0
  65. package/package.json +1 -1
@@ -2,7 +2,20 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { CrossOutWordGroupClient } from '../types/cross-out-word-group/CrossOutWordGroupClient';
4
4
  import { mapQuestionToCrossOutWordGroupData } from '../types/cross-out-word-group/map-cross-out-word-group-data';
5
- export function CrossOutWordGroupViewer({ question, isReviewMode = false, }) {
5
+ function readCrossOutAnswers(value) {
6
+ if (Array.isArray(value)) {
7
+ return value;
8
+ }
9
+ if (value && typeof value === 'object' && Array.isArray(value.answer)) {
10
+ return value.answer;
11
+ }
12
+ return undefined;
13
+ }
14
+ export function CrossOutWordGroupViewer({ question, isReviewMode = false, userAnswer, onSubmitAnswer, }) {
6
15
  const questionData = mapQuestionToCrossOutWordGroupData(question);
7
- return (_jsx(CrossOutWordGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: isReviewMode ? questionData.items.map((item) => item.correctAnswer) : [] }));
16
+ const storedAnswers = readCrossOutAnswers(userAnswer);
17
+ const userAnswers = isReviewMode && !storedAnswers
18
+ ? questionData.items.map((item) => item.correctAnswer)
19
+ : storedAnswers;
20
+ return (_jsx(CrossOutWordGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: userAnswers, onAnswerChange: isReviewMode ? undefined : onSubmitAnswer }));
8
21
  }
@@ -4,6 +4,14 @@ interface QuestionViewerProps {
4
4
  onSubmitAnswer?: (answer: any) => void;
5
5
  isReviewMode?: boolean;
6
6
  userAnswer?: any;
7
+ gradeDetails?: Array<{
8
+ itemId: string;
9
+ isCorrect?: boolean;
10
+ userAnswer?: unknown;
11
+ correctAnswer?: unknown;
12
+ partialScore?: number;
13
+ feedback?: string;
14
+ }>;
7
15
  }
8
- export declare function QuestionViewer({ question, onSubmitAnswer, isReviewMode, userAnswer, }: QuestionViewerProps): import("react").JSX.Element;
16
+ export declare function QuestionViewer({ question, onSubmitAnswer, isReviewMode, userAnswer, gradeDetails, }: QuestionViewerProps): import("react").JSX.Element;
9
17
  export {};
@@ -38,7 +38,7 @@ import { MatchWordToPictureViewer } from './MatchWordToPictureViewer';
38
38
  import { MatchWordToPictureGroupViewer } from './MatchWordToPictureGroupViewer';
39
39
  import { MatchColumnsToMakeSentencesViewer } from './MatchColumnsToMakeSentencesViewer';
40
40
  import { WordOrderingGroupViewer } from './WordOrderingGroupViewer';
41
- export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false, userAnswer, }) {
41
+ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false, userAnswer, gradeDetails, }) {
42
42
  const [selectedAnswer, setSelectedAnswer] = useState(userAnswer || null);
43
43
  const [playCount, setPlayCount] = useState(0);
44
44
  const audioRef = useRef(null);
@@ -477,6 +477,19 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
477
477
  }
478
478
  return (_jsxs("div", { className: "space-y-3", children: [content.title ? _jsx("h3", { className: "text-center text-lg font-bold uppercase text-slate-800", children: content.title }) : null, content.imageUrl ? (_jsx("div", { className: "flex justify-center", children: _jsx("img", { src: String(content.imageUrl), alt: "", className: "max-h-[180px] rounded-lg object-contain" }) })) : null, _jsx("div", { className: "space-y-2 text-base leading-relaxed text-slate-800", children: paragraph.split('\n').map((line, li) => (_jsx("div", { className: "flex flex-wrap items-baseline", children: renderLine(line, `wfp-${li}`) }, `wfp-l${li}`))) }), isReviewMode && correctAnswers.length > 0 ? (_jsxs("div", { className: "rounded-lg border border-emerald-200 bg-emerald-50/80 p-2.5 text-sm text-emerald-800", children: [_jsx("span", { className: "font-semibold", children: "\u0110\u00E1p \u00E1n: " }), correctAnswers.map((a, i) => (_jsxs("span", { className: "mr-2", children: ["(", i + 1, ") ", _jsx("span", { className: "font-bold underline", children: a || '—' })] }, `wfp-a${i}`)))] })) : null] }));
479
479
  };
480
+ const renderCrosswordPuzzle = () => {
481
+ const raw = userAnswer && typeof userAnswer === 'object' && !Array.isArray(userAnswer)
482
+ ? userAnswer
483
+ : {};
484
+ const answers = raw.answers && typeof raw.answers === 'object' && !Array.isArray(raw.answers)
485
+ ? raw.answers
486
+ : raw;
487
+ return (_jsx(CrosswordPuzzleClient, { content: question.content, correctAnswer: question.correctAnswer ?? question.answer, isReviewMode: isReviewMode, userAnswers: answers, onAnswerChange: isReviewMode
488
+ ? undefined
489
+ : (key, value) => {
490
+ onSubmitAnswer?.({ answers: { ...answers, [key]: value } });
491
+ } }));
492
+ };
480
493
  const renderQuestion = () => {
481
494
  const resolvedType = String(question.type ||
482
495
  question.questionType ||
@@ -488,9 +501,9 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
488
501
  case 'CHOOSE_THE_CORRECT_ANSWER_GROUP':
489
502
  return (_jsx(ChooseTheCorrectAnswerGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
490
503
  case 'TRUE_FALSE_GROUP':
491
- return (_jsx(TrueFalseGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer }));
504
+ return (_jsx(TrueFalseGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
492
505
  case 'TRUE_FALSE_CORRECT_GROUP':
493
- return (_jsx(TrueFalseCorrectGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer }));
506
+ return (_jsx(TrueFalseCorrectGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
494
507
  case 'FILL_IN_BLANK_GROUP':
495
508
  return (_jsx(FillInBlankGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
496
509
  case 'MATCHING_WITH_LINES_GROUP':
@@ -498,11 +511,11 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
498
511
  case 'MATCH_COLUMNS_TO_MAKE_SENTENCES':
499
512
  return (_jsx(MatchColumnsToMakeSentencesViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
500
513
  case 'CROSS_OUT_WORD_GROUP':
501
- return (_jsx(CrossOutWordGroupViewer, { question: question, isReviewMode: isReviewMode }));
514
+ return (_jsx(CrossOutWordGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
502
515
  case 'ANSWER_THE_QUESTION':
503
516
  return (_jsx(AnswerTheQuestionViewer, { question: question, isReviewMode: isReviewMode, userAnswer: typeof userAnswer === 'string' ? userAnswer : undefined, onSubmitAnswer: onSubmitAnswer }));
504
517
  case 'ANSWER_THE_QUESTION_GROUP':
505
- return (_jsx(AnswerTheQuestionGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer }));
518
+ return (_jsx(AnswerTheQuestionGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
506
519
  case 'SPEAKING_CUE_CARD':
507
520
  return (_jsx(SpeakingCueCardClient, { questionData: mapQuestionToSpeakingCueCardData(question), isReviewMode: isReviewMode }));
508
521
  case 'SPONTANEOUS_QA':
@@ -511,7 +524,7 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
511
524
  return (_jsx(SpontaneousQaGroupViewer, { question: question, isReviewMode: isReviewMode }));
512
525
  case 'WORD_ORDER_AND_MATCH_GROUP':
513
526
  case 'WORD_ORDER_AND_MATCH':
514
- return (_jsx(WordOrderAndMatchGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer }));
527
+ return (_jsx(WordOrderAndMatchGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
515
528
  case 'WORD_ORDERING_GROUP':
516
529
  return (_jsx(WordOrderingGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
517
530
  case 'MATCH_WORD_TO_PICTURE_GROUP':
@@ -551,13 +564,13 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
551
564
  case 'WORD_FILL_STRUCTURED_FORM':
552
565
  return (_jsx(WordFillStructuredFormViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
553
566
  case 'CROSSWORD_PUZZLE':
554
- return (_jsx(CrosswordPuzzleClient, { content: question.content, correctAnswer: question.answer, isReviewMode: isReviewMode, userAnswers: userAnswer }));
567
+ return renderCrosswordPuzzle();
555
568
  case 'FIND_WORDS_IN_MATRIX':
556
- return (_jsx(FindWordsInMatrixClient, { content: question.content, correctAnswer: question.answer, showSolution: isReviewMode }));
569
+ return (_jsx(FindWordsInMatrixClient, { content: question.content, correctAnswer: question.correctAnswer ?? question.answer, showSolution: isReviewMode, userAnswer: userAnswer, onAnswerChange: isReviewMode ? undefined : onSubmitAnswer }));
557
570
  case 'SORT_WORDS_INTO_CATEGORIES':
558
571
  return (_jsx(SortWordsIntoCategoriesViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
559
572
  case 'CHOOSE_THEN_ANSWER_GROUP':
560
- return (_jsx(ChooseThenAnswerGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
573
+ return (_jsx(ChooseThenAnswerGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, gradeDetails: gradeDetails, onSubmitAnswer: onSubmitAnswer }));
561
574
  default:
562
575
  return _jsxs("div", { children: ["Lo\u1EA1i c\u00E2u h\u1ECFi kh\u00F4ng \u0111\u01B0\u1EE3c h\u1ED7 tr\u1EE3: ", question.type] });
563
576
  }
@@ -1,82 +1,7 @@
1
1
  'use client';
2
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
3
- function isRecord(value) {
4
- return typeof value === 'object' && value !== null && !Array.isArray(value);
5
- }
6
- function asRecord(value) {
7
- return isRecord(value) ? value : {};
8
- }
9
- function asString(value, fallback = '') {
10
- return typeof value === 'string' ? value : fallback;
11
- }
12
- function mapItem(item, index) {
13
- const record = asRecord(item);
14
- const content = asRecord(record.content);
15
- const correctAnswer = asRecord(record.correctAnswer);
16
- const expected = Array.isArray(correctAnswer.expectedAnswers)
17
- ? correctAnswer.expectedAnswers.map((value) => String(value ?? ''))
18
- : [];
19
- return {
20
- questionText: asString(content.questionText) ||
21
- asString(content.question) ||
22
- asString(record.questionText),
23
- audioUrl: asString(content.audioUrl) || undefined,
24
- expectedAnswers: expected,
25
- isExample: record.isExample === true,
26
- points: typeof record.points === 'number' ? record.points : 1,
27
- questionNumber: typeof record.questionNumber === 'number' ? record.questionNumber : index + 1,
28
- };
29
- }
30
- function isNestedPart(item) {
31
- const record = asRecord(item);
32
- return Array.isArray(record.questions);
33
- }
34
- function resolveGroup(question) {
35
- const content = asRecord(question.content);
36
- const list = asRecord(question.spontaneousQAList ?? content.spontaneousQAList ?? content);
37
- const itemsRaw = Array.isArray(list.items)
38
- ? list.items
39
- : Array.isArray(content.items)
40
- ? content.items
41
- : [];
42
- const instruction = asString(asRecord(list.meta).instruction) ||
43
- asString(asRecord(content.meta).instruction) ||
44
- asString(question.explanation) ||
45
- 'Talk about the topic.';
46
- if (itemsRaw.some(isNestedPart)) {
47
- return {
48
- instruction,
49
- parts: itemsRaw.map((item, index) => {
50
- const record = asRecord(item);
51
- const questions = Array.isArray(record.questions)
52
- ? record.questions.map(mapItem)
53
- : [];
54
- return {
55
- partNumber: typeof record.partNumber === 'number' ? record.partNumber : index + 1,
56
- partTitle: asString(record.partTitle) || `PART ${index + 1}`,
57
- topic: asString(record.topic),
58
- questions,
59
- };
60
- }),
61
- };
62
- }
63
- return {
64
- instruction,
65
- parts: [
66
- {
67
- partNumber: 1,
68
- partTitle: '',
69
- topic: '',
70
- questions: itemsRaw.map(mapItem),
71
- },
72
- ],
73
- };
74
- }
75
- function QaPromptCard({ item, index, isReviewMode = false, }) {
76
- return (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gray-50 px-4 py-2.5", children: [_jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u00E2u ", item.questionNumber || index + 1] }), _jsxs("span", { className: "text-xs text-gray-500", children: [item.points || 0, " \u0111i\u1EC3m"] })] }), _jsxs("div", { className: "space-y-2 p-4", children: [_jsx("p", { className: "text-sm font-semibold text-gray-800", children: item.questionText || 'Câu hỏi chưa được nhập' }), item.audioUrl ? (_jsxs("p", { className: "text-xs text-gray-500", children: ["Audio: ", item.audioUrl] })) : null, isReviewMode && item.expectedAnswers.length > 0 ? (_jsxs("div", { className: "rounded-lg border border-emerald-100 bg-emerald-50 px-3 py-2 text-sm text-emerald-800", children: [_jsx("p", { className: "mb-1 font-medium", children: "G\u1EE3i \u00FD \u0111\u00E1p \u00E1n" }), _jsx("ul", { className: "list-disc space-y-1 pl-4", children: item.expectedAnswers.map((answer) => (_jsx("li", { children: answer }, answer))) })] })) : null] })] }));
77
- }
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { SpontaneousQaGroupClient } from '../types/spontaneous-qa-group/SpontaneousQaGroupClient';
4
+ import { mapQuestionToSpontaneousQaGroupData } from '../types/spontaneous-qa-group/map-spontaneous-qa-group-data';
78
5
  export function SpontaneousQaGroupViewer({ question, isReviewMode = false, }) {
79
- const { instruction, parts } = resolveGroup(question);
80
- const showPartChrome = parts.length > 1 || parts.some((part) => part.partTitle || part.topic);
81
- return (_jsxs("div", { className: "space-y-5", children: [instruction ? (_jsx("div", { className: "rounded-xl border border-indigo-100 bg-indigo-50 px-4 py-3 text-sm font-medium text-indigo-800", children: instruction })) : null, parts.map((part) => (_jsxs("section", { className: "space-y-3", children: [showPartChrome ? (_jsxs("div", { children: [part.partTitle ? (_jsx("h3", { className: "text-base font-bold tracking-wide text-sky-600", children: part.partTitle })) : null, part.topic ? (_jsx("p", { className: "text-sm font-semibold text-slate-600", children: part.topic })) : null] })) : null, _jsx("div", { className: "space-y-3", children: part.questions.map((item, index) => (_jsx(QaPromptCard, { item: item, index: index, isReviewMode: isReviewMode }, `${part.partNumber}-${item.questionNumber}-${index}`))) })] }, `part-${part.partNumber}`)))] }));
6
+ return (_jsx(SpontaneousQaGroupClient, { questionData: mapQuestionToSpontaneousQaGroupData(question), isReviewMode: isReviewMode }));
82
7
  }
@@ -3,7 +3,8 @@ import type { TrueFalseCorrectStudentItemAnswer } from '../_shared/types/true-fa
3
3
  type TrueFalseCorrectGroupViewerProps = {
4
4
  question: Question;
5
5
  isReviewMode?: boolean;
6
- userAnswer?: TrueFalseCorrectStudentItemAnswer[];
6
+ userAnswer?: unknown;
7
+ onSubmitAnswer?: (answer: TrueFalseCorrectStudentItemAnswer[]) => void;
7
8
  };
8
- export declare function TrueFalseCorrectGroupViewer({ question, isReviewMode, userAnswer, }: TrueFalseCorrectGroupViewerProps): import("react").JSX.Element;
9
+ export declare function TrueFalseCorrectGroupViewer({ question, isReviewMode, userAnswer, onSubmitAnswer, }: TrueFalseCorrectGroupViewerProps): import("react").JSX.Element;
9
10
  export {};
@@ -2,13 +2,23 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { TrueFalseCorrectGroupClient } from '../types/true-false-correct-group/TrueFalseCorrectGroupClient';
4
4
  import { mapQuestionToTrueFalseCorrectGroupData } from '../types/true-false-correct-group/map-true-false-correct-group-data';
5
- export function TrueFalseCorrectGroupViewer({ question, isReviewMode = false, userAnswer, }) {
5
+ function readStudentAnswers(value) {
6
+ if (Array.isArray(value)) {
7
+ return value;
8
+ }
9
+ if (value && typeof value === 'object' && Array.isArray(value.answer)) {
10
+ return value.answer;
11
+ }
12
+ return undefined;
13
+ }
14
+ export function TrueFalseCorrectGroupViewer({ question, isReviewMode = false, userAnswer, onSubmitAnswer, }) {
6
15
  const questionData = mapQuestionToTrueFalseCorrectGroupData(question);
7
- const reviewAnswers = isReviewMode && !userAnswer
16
+ const storedAnswers = readStudentAnswers(userAnswer);
17
+ const reviewAnswers = isReviewMode && !storedAnswers
8
18
  ? questionData.items.map((item) => ({
9
19
  isTrue: item.isTrue,
10
20
  correction: item.correction,
11
21
  }))
12
- : userAnswer;
13
- return (_jsx(TrueFalseCorrectGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: reviewAnswers }));
22
+ : storedAnswers;
23
+ return (_jsx(TrueFalseCorrectGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: reviewAnswers, onAnswerChange: isReviewMode ? undefined : onSubmitAnswer }));
14
24
  }
@@ -2,7 +2,10 @@ import type { Question } from '../../../shared/types/entities/question.types';
2
2
  type TrueFalseGroupViewerProps = {
3
3
  question: Question;
4
4
  isReviewMode?: boolean;
5
- userAnswer?: boolean[];
5
+ userAnswer?: boolean[] | {
6
+ answer?: boolean[];
7
+ };
8
+ onSubmitAnswer?: (answer: boolean[]) => void;
6
9
  };
7
- export declare function TrueFalseGroupViewer({ question, isReviewMode, userAnswer, }: TrueFalseGroupViewerProps): import("react").JSX.Element;
10
+ export declare function TrueFalseGroupViewer({ question, isReviewMode, userAnswer, onSubmitAnswer, }: TrueFalseGroupViewerProps): import("react").JSX.Element;
8
11
  export {};
@@ -2,10 +2,20 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { TrueFalseGroupClient } from '../types/true-false-group/TrueFalseGroupClient';
4
4
  import { mapQuestionToTrueFalseGroupData } from '../types/true-false-group/map-true-false-group-data';
5
- export function TrueFalseGroupViewer({ question, isReviewMode = false, userAnswer, }) {
5
+ function readBooleanAnswers(value) {
6
+ if (Array.isArray(value)) {
7
+ return value;
8
+ }
9
+ if (value && typeof value === 'object' && Array.isArray(value.answer)) {
10
+ return value.answer;
11
+ }
12
+ return undefined;
13
+ }
14
+ export function TrueFalseGroupViewer({ question, isReviewMode = false, userAnswer, onSubmitAnswer, }) {
6
15
  const questionData = mapQuestionToTrueFalseGroupData(question);
7
- const reviewAnswers = isReviewMode && !userAnswer
16
+ const storedAnswers = readBooleanAnswers(userAnswer);
17
+ const reviewAnswers = isReviewMode && !storedAnswers
8
18
  ? questionData.items.map((item) => item.correctAnswer)
9
- : userAnswer;
10
- return (_jsx(TrueFalseGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: reviewAnswers }));
19
+ : storedAnswers;
20
+ return (_jsx(TrueFalseGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: reviewAnswers, onAnswerChange: isReviewMode ? undefined : onSubmitAnswer }));
11
21
  }
@@ -1,6 +1,10 @@
1
1
  import type { Question } from '../../../shared/types/entities/question.types';
2
- export declare function WordOrderAndMatchGroupViewer({ question, isReviewMode, userAnswer, }: {
2
+ export declare function WordOrderAndMatchGroupViewer({ question, isReviewMode, userAnswer, onSubmitAnswer, }: {
3
3
  question: Question;
4
4
  isReviewMode?: boolean;
5
- userAnswer?: any;
5
+ userAnswer?: unknown;
6
+ onSubmitAnswer?: (answer: Array<{
7
+ orderedPhrase: string;
8
+ matchLabel: string;
9
+ }>) => void;
6
10
  }): import("react").JSX.Element;
@@ -76,20 +76,40 @@ function resolveGroup(question) {
76
76
  items: itemsRaw.map((item, idx) => mapItem(item, idx, answersRaw[idx])),
77
77
  };
78
78
  }
79
- export function WordOrderAndMatchGroupViewer({ question, isReviewMode = false, userAnswer, }) {
79
+ function readMatchAnswers(value) {
80
+ if (Array.isArray(value)) {
81
+ return value;
82
+ }
83
+ if (value && typeof value === 'object' && Array.isArray(value.answer)) {
84
+ return value.answer;
85
+ }
86
+ return undefined;
87
+ }
88
+ export function WordOrderAndMatchGroupViewer({ question, isReviewMode = false, userAnswer, onSubmitAnswer, }) {
80
89
  const { instruction, matchOptions, items } = resolveGroup(question);
81
90
  const questionData = {
82
91
  instruction,
83
92
  matchOptions,
84
93
  items,
85
94
  };
86
- const autoUserAnswers = isReviewMode
95
+ const storedAnswers = readMatchAnswers(userAnswer);
96
+ const autoUserAnswers = isReviewMode && !storedAnswers
87
97
  ? items.map((item) => ({
88
98
  orderedPhrase: item.orderedPhrase,
89
99
  matchLabel: item.matchLabel,
90
100
  }))
91
- : Array.isArray(userAnswer)
92
- ? userAnswer
93
- : undefined;
94
- return (_jsx(WordOrderAndMatchGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: autoUserAnswers }));
101
+ : storedAnswers;
102
+ return (_jsx(WordOrderAndMatchGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: autoUserAnswers, onAnswerChange: isReviewMode
103
+ ? undefined
104
+ : (itemIndex, next) => {
105
+ const current = items.map((_, index) => {
106
+ const existing = storedAnswers?.[index];
107
+ return {
108
+ orderedPhrase: existing?.orderedPhrase || '',
109
+ matchLabel: existing?.matchLabel || '',
110
+ };
111
+ });
112
+ current[itemIndex] = next;
113
+ onSubmitAnswer?.(current);
114
+ } }));
95
115
  }
@@ -31,6 +31,6 @@ export function ReviewAnswerTheQuestionGroupRenderer({ partNumber, partName, que
31
31
  ? 'border-red-200 bg-red-50 text-red-800'
32
32
  : 'border-gray-200 bg-gray-50 text-gray-700'}`, children: question.isExample
33
33
  ? (question.exampleAnswer || '(Câu mẫu)')
34
- : (studentAnswer || '(Chưa trả lời)') }), expectedAnswers.length > 0 && (_jsxs("div", { className: "space-y-1", children: [_jsx("p", { className: "text-xs font-medium text-gray-500", children: "\u0110\u00E1p \u00E1n m\u1EABu (AI)" }), _jsx("ul", { className: "list-disc space-y-1 pl-5 text-sm text-gray-700", children: expectedAnswers.map((answer) => (_jsx("li", { children: answer }, answer))) })] })), feedback && (_jsx("p", { className: "text-xs text-violet-700", children: feedback }))] })] }, question.id));
34
+ : (studentAnswer || '(Chưa trả lời)') }), expectedAnswers.length > 0 && (_jsxs("div", { className: "space-y-1", children: [_jsx("p", { className: "text-xs font-medium text-gray-500", children: "\u0110\u00E1p \u00E1n" }), _jsx("ul", { className: "list-disc space-y-1 pl-5 text-sm text-gray-700", children: expectedAnswers.map((answer) => (_jsx("li", { children: answer }, answer))) })] })), feedback && (_jsx("p", { className: "text-xs text-violet-700", children: feedback }))] })] }, question.id));
35
35
  })] }));
36
36
  }
@@ -231,7 +231,7 @@ const reverseChooseTheCorrectAnswerGroup = (apiQuestion) => {
231
231
  });
232
232
  const isExample = item.isExample === true;
233
233
  const imageUrl = normalizeChooseAnswerGroupImageUrl(itemContent.imageUrl);
234
- const audioUrl = asString(itemContent.audioUrl).trim();
234
+ const audioUrl = normalizeChooseAnswerGroupImageUrl(itemContent.audioUrl);
235
235
  return {
236
236
  question: asString(itemContent.question),
237
237
  optionType: asString(itemContent.optionType, 'text') === 'image' ? 'image' : 'text',
@@ -251,17 +251,17 @@ const reverseChooseTheCorrectAnswerGroup = (apiQuestion) => {
251
251
  points: isExample ? 0 : asNumber(item.points, 1),
252
252
  questionNumber: asNumber(item.questionNumber, index + 1),
253
253
  ...(imageUrl !== undefined ? { imageUrl } : {}),
254
- ...(audioUrl ? { audioUrl } : {}),
254
+ ...(audioUrl !== undefined ? { audioUrl } : {}),
255
255
  };
256
256
  });
257
- const audioUrl = asString(asRecord(content.meta).audioUrl).trim();
257
+ const audioUrl = normalizeChooseAnswerGroupImageUrl(asRecord(content.meta).audioUrl);
258
258
  const imageUrl = normalizeChooseAnswerGroupImageUrl(asRecord(content.meta).imageUrl);
259
259
  return {
260
260
  content: '',
261
261
  answer: {
262
262
  instruction: asString(asRecord(content.meta).instruction, 'Choose the correct answer.'),
263
263
  passage: asString(asRecord(content.meta).passage),
264
- ...(audioUrl ? { audioUrl } : {}),
264
+ ...(audioUrl !== undefined ? { audioUrl } : {}),
265
265
  ...(imageUrl !== undefined ? { imageUrl } : {}),
266
266
  items,
267
267
  explanation: asString(apiQuestion.explanation),
@@ -315,6 +315,7 @@ const reverseAnswerTheQuestionGroup = (apiQuestion) => {
315
315
  items,
316
316
  explanation: asString(apiQuestion.explanation),
317
317
  points: items.reduce((total, item) => (item.isExample ? total : total + item.points), 0),
318
+ ...(meta.gradingType === 'MAPPING' ? { gradingType: 'MAPPING' } : {}),
318
319
  },
319
320
  };
320
321
  };
@@ -404,6 +405,13 @@ const reverseChooseThenAnswerGroup = (apiQuestion) => {
404
405
  question: asString(itemContent.question),
405
406
  options,
406
407
  optionId: asString(rawAnswer.optionId) || options[0]?.id || '',
408
+ expectedAnswers: (() => {
409
+ const raw = asArray(rawAnswer.expectedAnswers)
410
+ .filter((value) => typeof value === 'string')
411
+ .map((value) => value.trim())
412
+ .filter((value) => value !== '');
413
+ return raw.length > 0 ? raw : [''];
414
+ })(),
407
415
  isExample,
408
416
  points: isExample ? 0 : asNumber(item.points, 1),
409
417
  questionNumber: asNumber(item.questionNumber, index + 1),
@@ -1,4 +1,8 @@
1
- export declare const ANSWER_THE_QUESTION_GROUP_GRADING_TYPE: "AI";
1
+ export declare const ANSWER_THE_QUESTION_GROUP_GRADING_TYPES: readonly ["AI", "MAPPING"];
2
+ export type AnswerTheQuestionGroupGradingType = (typeof ANSWER_THE_QUESTION_GROUP_GRADING_TYPES)[number];
3
+ /** Default for newly created groups. */
4
+ export declare const ANSWER_THE_QUESTION_GROUP_GRADING_TYPE: AnswerTheQuestionGroupGradingType;
5
+ export declare function resolveAnswerTheQuestionGroupGradingType(value: unknown): AnswerTheQuestionGroupGradingType;
2
6
  export interface AnswerTheQuestionGroupItemContent {
3
7
  question: string;
4
8
  imageUrl?: string | string[];
@@ -20,8 +24,9 @@ export interface AnswerTheQuestionGroupContent {
20
24
  passage?: string;
21
25
  audioUrl?: string;
22
26
  imageUrl?: string | string[];
23
- gradingType: typeof ANSWER_THE_QUESTION_GROUP_GRADING_TYPE;
24
- isAiGraded: true;
27
+ /** AI = LLM short-answer; MAPPING = exact-match expectedAnswers. */
28
+ gradingType: AnswerTheQuestionGroupGradingType;
29
+ isAiGraded: boolean;
25
30
  };
26
31
  items: AnswerTheQuestionGroupItem[];
27
32
  }
@@ -1 +1,6 @@
1
+ export const ANSWER_THE_QUESTION_GROUP_GRADING_TYPES = ['AI', 'MAPPING'];
2
+ /** Default for newly created groups. */
1
3
  export const ANSWER_THE_QUESTION_GROUP_GRADING_TYPE = 'AI';
4
+ export function resolveAnswerTheQuestionGroupGradingType(value) {
5
+ return value === 'MAPPING' ? 'MAPPING' : 'AI';
6
+ }
@@ -24,7 +24,7 @@ export interface ChooseTheCorrectAnswerGroupContent {
24
24
  meta?: {
25
25
  instruction?: string;
26
26
  passage?: string;
27
- audioUrl?: string;
27
+ audioUrl?: string | string[];
28
28
  imageUrl?: string | string[];
29
29
  };
30
30
  items: ChooseTheCorrectAnswerGroupItem[];
@@ -4,6 +4,7 @@ export interface ChooseThenAnswerOption {
4
4
  }
5
5
  export interface ChooseThenAnswerItemAnswer {
6
6
  optionId: string;
7
+ expectedAnswers?: string[];
7
8
  }
8
9
  export interface ChooseThenAnswerStudentItemAnswer {
9
10
  optionId: string;
@@ -27,6 +28,8 @@ export interface ChooseThenAnswerGroupContent {
27
28
  meta?: {
28
29
  instruction?: string;
29
30
  title?: string;
31
+ gradingType?: 'AI';
32
+ isAiGraded?: boolean;
30
33
  };
31
34
  items: ChooseThenAnswerGroupItem[];
32
35
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tinyweb_dev/oe-exam-sdk",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "Reusable OceanEdu question components.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",