@tinyweb_dev/oe-exam-sdk 0.2.4 → 0.2.6

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 (153) hide show
  1. package/dist/components/exams/take/ExamTakingPageContainer.js +6 -0
  2. package/dist/components/exams/take/components/QuestionRenderer.js +19 -3
  3. package/dist/components/exams/take/components/question-renderers/ChooseBestAnswerImages.d.ts +10 -0
  4. package/dist/components/exams/take/components/question-renderers/ChooseBestAnswerImages.js +38 -0
  5. package/dist/components/exams/take/components/question-renderers/MoversChooseBestAnswerRenderer.js +5 -11
  6. package/dist/components/exams/take/components/question-renderers/MoversFillInBlankGroupRenderer.d.ts +22 -0
  7. package/dist/components/exams/take/components/question-renderers/MoversFillInBlankGroupRenderer.js +60 -0
  8. package/dist/components/exams/take/components/question-renderers/MoversMatchWordToPictureRenderer.d.ts +16 -0
  9. package/dist/components/exams/take/components/question-renderers/MoversMatchWordToPictureRenderer.js +75 -0
  10. package/dist/components/exams/take/components/question-renderers/MoversMatchingWithLinesGroupRenderer.d.ts +13 -0
  11. package/dist/components/exams/take/components/question-renderers/MoversMatchingWithLinesGroupRenderer.js +31 -0
  12. package/dist/components/exams/take/components/question-renderers/MoversTrueFalseCorrectGroupRenderer.d.ts +18 -0
  13. package/dist/components/exams/take/components/question-renderers/MoversTrueFalseCorrectGroupRenderer.js +92 -0
  14. package/dist/components/exams/take/components/question-renderers/MoversWritingRenderer.d.ts +2 -1
  15. package/dist/components/exams/take/components/question-renderers/MoversWritingRenderer.js +2 -2
  16. package/dist/components/exams/take/components/question-renderers/index.d.ts +4 -0
  17. package/dist/components/exams/take/components/question-renderers/index.js +4 -0
  18. package/dist/components/exams/take/exam-taking.utils.d.ts +1 -0
  19. package/dist/components/exams/take/exam-taking.utils.js +9 -1
  20. package/dist/components/exams/take/types.d.ts +3 -2
  21. package/dist/components/exams/take/utils/answer-transformers.js +28 -0
  22. package/dist/components/exams/take/utils/question-transformers.d.ts +73 -1
  23. package/dist/components/exams/take/utils/question-transformers.js +188 -0
  24. package/dist/components/questions/_shared/config/question-types.config.js +24 -0
  25. package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.d.ts +9 -0
  26. package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.js +49 -1
  27. package/dist/components/questions/_shared/types/fill-in-blank-group.type.d.ts +50 -0
  28. package/dist/components/questions/_shared/types/fill-in-blank-group.type.js +2 -0
  29. package/dist/components/questions/_shared/types/index.d.ts +4 -0
  30. package/dist/components/questions/_shared/types/index.js +4 -0
  31. package/dist/components/questions/_shared/types/match-word-to-picture.type.d.ts +62 -0
  32. package/dist/components/questions/_shared/types/match-word-to-picture.type.js +1 -0
  33. package/dist/components/questions/_shared/types/matching-with-lines-group.type.d.ts +40 -0
  34. package/dist/components/questions/_shared/types/matching-with-lines-group.type.js +1 -0
  35. package/dist/components/questions/_shared/types/true-false-correct-group.type.d.ts +42 -0
  36. package/dist/components/questions/_shared/types/true-false-correct-group.type.js +2 -0
  37. package/dist/components/questions/_shared/types/write-a-short-letter.type.d.ts +4 -0
  38. package/dist/components/questions/components/QuestionSearchDropdown.js +6 -0
  39. package/dist/components/questions/components/QuestionTypeSelector.js +3 -0
  40. package/dist/components/questions/creator/question-type-registry.js +8 -0
  41. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseAnswerGroupImageUpload.d.ts +13 -0
  42. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseAnswerGroupImageUpload.js +32 -0
  43. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.d.ts +1 -1
  44. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +42 -6
  45. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupCreator.js +40 -20
  46. package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.d.ts +1 -1
  47. package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.js +5 -0
  48. package/dist/components/questions/types/choose-the-correct-answer-group/transform.js +4 -0
  49. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupClient.d.ts +2 -0
  50. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupClient.js +64 -0
  51. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupCreator.d.ts +2 -0
  52. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupCreator.js +158 -0
  53. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupMediaField.d.ts +7 -0
  54. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupMediaField.js +30 -0
  55. package/dist/components/questions/types/fill-in-blank-group/blank-utils.d.ts +20 -0
  56. package/dist/components/questions/types/fill-in-blank-group/blank-utils.js +99 -0
  57. package/dist/components/questions/types/fill-in-blank-group/index.d.ts +5 -0
  58. package/dist/components/questions/types/fill-in-blank-group/index.js +5 -0
  59. package/dist/components/questions/types/fill-in-blank-group/map-fill-in-blank-group-data.d.ts +4 -0
  60. package/dist/components/questions/types/fill-in-blank-group/map-fill-in-blank-group-data.js +102 -0
  61. package/dist/components/questions/types/fill-in-blank-group/register.d.ts +3 -0
  62. package/dist/components/questions/types/fill-in-blank-group/register.js +37 -0
  63. package/dist/components/questions/types/fill-in-blank-group/transform.d.ts +2 -0
  64. package/dist/components/questions/types/fill-in-blank-group/transform.js +68 -0
  65. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureClient.d.ts +2 -0
  66. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureClient.js +31 -0
  67. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureCreator.d.ts +2 -0
  68. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureCreator.js +267 -0
  69. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.d.ts +2 -0
  70. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.js +65 -0
  71. package/dist/components/questions/types/match-word-to-picture/WordBankEditor.d.ts +8 -0
  72. package/dist/components/questions/types/match-word-to-picture/WordBankEditor.js +15 -0
  73. package/dist/components/questions/types/match-word-to-picture/index.d.ts +6 -0
  74. package/dist/components/questions/types/match-word-to-picture/index.js +6 -0
  75. package/dist/components/questions/types/match-word-to-picture/map-match-word-to-picture-data.d.ts +2 -0
  76. package/dist/components/questions/types/match-word-to-picture/map-match-word-to-picture-data.js +53 -0
  77. package/dist/components/questions/types/match-word-to-picture/map-match-word-to-picture-group-data.d.ts +4 -0
  78. package/dist/components/questions/types/match-word-to-picture/map-match-word-to-picture-group-data.js +75 -0
  79. package/dist/components/questions/types/match-word-to-picture/register.d.ts +2 -0
  80. package/dist/components/questions/types/match-word-to-picture/register.js +55 -0
  81. package/dist/components/questions/types/match-word-to-picture/transform.d.ts +11 -0
  82. package/dist/components/questions/types/match-word-to-picture/transform.js +43 -0
  83. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesBoard.d.ts +20 -0
  84. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesBoard.js +109 -0
  85. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.d.ts +2 -0
  86. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.js +30 -0
  87. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupCreator.d.ts +2 -0
  88. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupCreator.js +180 -0
  89. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesImageField.d.ts +6 -0
  90. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesImageField.js +26 -0
  91. package/dist/components/questions/types/matching-with-lines-group/index.d.ts +4 -0
  92. package/dist/components/questions/types/matching-with-lines-group/index.js +4 -0
  93. package/dist/components/questions/types/matching-with-lines-group/map-matching-with-lines-group-data.d.ts +8 -0
  94. package/dist/components/questions/types/matching-with-lines-group/map-matching-with-lines-group-data.js +104 -0
  95. package/dist/components/questions/types/matching-with-lines-group/register.d.ts +3 -0
  96. package/dist/components/questions/types/matching-with-lines-group/register.js +37 -0
  97. package/dist/components/questions/types/matching-with-lines-group/transform.d.ts +2 -0
  98. package/dist/components/questions/types/matching-with-lines-group/transform.js +44 -0
  99. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.d.ts +2 -0
  100. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js +25 -0
  101. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupCreator.d.ts +2 -0
  102. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupCreator.js +144 -0
  103. package/dist/components/questions/types/true-false-correct-group/index.d.ts +5 -0
  104. package/dist/components/questions/types/true-false-correct-group/index.js +5 -0
  105. package/dist/components/questions/types/true-false-correct-group/map-true-false-correct-group-data.d.ts +4 -0
  106. package/dist/components/questions/types/true-false-correct-group/map-true-false-correct-group-data.js +79 -0
  107. package/dist/components/questions/types/true-false-correct-group/register.d.ts +2 -0
  108. package/dist/components/questions/types/true-false-correct-group/register.js +36 -0
  109. package/dist/components/questions/types/true-false-correct-group/transform.d.ts +2 -0
  110. package/dist/components/questions/types/true-false-correct-group/transform.js +49 -0
  111. package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterClient.js +11 -1
  112. package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterCreator.js +8 -2
  113. package/dist/components/questions/types/write-a-short-letter/register.js +4 -0
  114. package/dist/components/questions/types/write-a-short-letter/transform.js +4 -0
  115. package/dist/components/questions/viewer/ChooseTheCorrectAnswerGroupViewer.d.ts +2 -1
  116. package/dist/components/questions/viewer/ChooseTheCorrectAnswerGroupViewer.js +2 -2
  117. package/dist/components/questions/viewer/FillInBlankGroupViewer.d.ts +10 -0
  118. package/dist/components/questions/viewer/FillInBlankGroupViewer.js +14 -0
  119. package/dist/components/questions/viewer/MatchWordToPictureGroupViewer.d.ts +9 -0
  120. package/dist/components/questions/viewer/MatchWordToPictureGroupViewer.js +11 -0
  121. package/dist/components/questions/viewer/MatchWordToPictureViewer.d.ts +9 -0
  122. package/dist/components/questions/viewer/MatchWordToPictureViewer.js +8 -0
  123. package/dist/components/questions/viewer/MatchingWithLinesGroupViewer.d.ts +7 -0
  124. package/dist/components/questions/viewer/MatchingWithLinesGroupViewer.js +48 -0
  125. package/dist/components/questions/viewer/QuestionViewer.js +24 -3
  126. package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.d.ts +9 -0
  127. package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.js +14 -0
  128. package/dist/components/questions/viewer/index.d.ts +5 -0
  129. package/dist/components/questions/viewer/index.js +5 -0
  130. package/dist/components/results/renderers/ReviewChooseBestAnswerRenderer.js +4 -9
  131. package/dist/components/results/renderers/review-question-body-movers.js +74 -3
  132. package/dist/components/results/review-data.js +59 -0
  133. package/dist/components/themes/english-certification/EcWritingTask.js +2 -2
  134. package/dist/index.d.ts +1 -1
  135. package/dist/index.js +1 -1
  136. package/dist/shared/constants/question-skills.js +8 -0
  137. package/dist/shared/lib/hooks/usePresignedFileUrl.d.ts +3 -1
  138. package/dist/shared/lib/hooks/usePresignedFileUrl.js +30 -12
  139. package/dist/shared/lib/utils/question-reverse-transform.js +142 -0
  140. package/dist/shared/lib/utils/question-transform.js +8 -0
  141. package/dist/shared/types/common.types.d.ts +1 -1
  142. package/dist/shared/types/questions/choose-the-correct-answer-group.d.ts +1 -0
  143. package/dist/shared/types/questions/fill-in-blank-group.d.ts +45 -0
  144. package/dist/shared/types/questions/fill-in-blank-group.js +1 -0
  145. package/dist/shared/types/questions/index.d.ts +4 -0
  146. package/dist/shared/types/questions/index.js +8 -0
  147. package/dist/shared/types/questions/match-word-to-picture.d.ts +35 -0
  148. package/dist/shared/types/questions/match-word-to-picture.js +5 -0
  149. package/dist/shared/types/questions/matching-with-lines-group.d.ts +39 -0
  150. package/dist/shared/types/questions/matching-with-lines-group.js +1 -0
  151. package/dist/shared/types/questions/true-false-correct-group.d.ts +39 -0
  152. package/dist/shared/types/questions/true-false-correct-group.js +1 -0
  153. package/package.json +7 -2
@@ -36,6 +36,7 @@ function WriteAShortLetterCreatorContent({ initialData, onChange, externalErrors
36
36
  const [maxWords, setMaxWords] = useState(initialData?.maxWords);
37
37
  const [instruction, setInstruction] = useState(initialData?.instructions?.mainInstruction || '');
38
38
  const [guideImages, setGuideImages] = useState(initialData?.guideImages || []);
39
+ const [passage, setPassage] = useState(initialData?.passage || '');
39
40
  const [points, setPoints] = useState(initialData?.points || 10);
40
41
  // Store presigned URLs keyed by imageKey (NOT index) to avoid index races
41
42
  // when the user uploads several images quickly.
@@ -80,7 +81,7 @@ function WriteAShortLetterCreatorContent({ initialData, onChange, externalErrors
80
81
  },
81
82
  };
82
83
  }
83
- }, [validationRef, informations, minWords, maxWords, instruction, guideImages, points]);
84
+ }, [validationRef, informations, minWords, maxWords, instruction, guideImages, passage, points]);
84
85
  // Sync state from initialData when it changes
85
86
  useEffect(() => {
86
87
  const incomingStr = JSON.stringify(initialData);
@@ -106,6 +107,7 @@ function WriteAShortLetterCreatorContent({ initialData, onChange, externalErrors
106
107
  setMaxWords(initialData?.maxWords);
107
108
  setInstruction(initialData?.instructions?.mainInstruction ?? '');
108
109
  setGuideImages(initialData?.guideImages ?? []);
110
+ setPassage(initialData?.passage ?? '');
109
111
  setPoints(initialData?.points ?? 10);
110
112
  // Clear stale preview URLs from the previous question's uploads
111
113
  setGuideImagePreviewUrls({});
@@ -133,6 +135,7 @@ function WriteAShortLetterCreatorContent({ initialData, onChange, externalErrors
133
135
  maxWords,
134
136
  instruction,
135
137
  guideImages,
138
+ passage,
136
139
  points,
137
140
  });
138
141
  previousPayloadRef.current = currentPayload;
@@ -151,6 +154,7 @@ function WriteAShortLetterCreatorContent({ initialData, onChange, externalErrors
151
154
  mainInstruction: instruction || '',
152
155
  },
153
156
  guideImages: guideImages.length > 0 ? guideImages : undefined,
157
+ ...(passage ? { passage } : {}),
154
158
  points,
155
159
  };
156
160
  // Serialize payload for comparison
@@ -169,6 +173,7 @@ function WriteAShortLetterCreatorContent({ initialData, onChange, externalErrors
169
173
  maxWords,
170
174
  instructions: { mainInstruction: instruction || '' },
171
175
  guideImages,
176
+ ...(passage ? { passage } : {}),
172
177
  points,
173
178
  });
174
179
  recentEmittedInitialDataRef.current = [
@@ -177,7 +182,7 @@ function WriteAShortLetterCreatorContent({ initialData, onChange, externalErrors
177
182
  ].slice(-10);
178
183
  onChangeRef.current?.(payload);
179
184
  }
180
- }, [informations, minWords, maxWords, instruction, guideImages, points]);
185
+ }, [informations, minWords, maxWords, instruction, guideImages, passage, points]);
181
186
  // Get validation errors
182
187
  const getErrors = () => {
183
188
  const errors = {};
@@ -287,6 +292,7 @@ function WriteAShortLetterCreatorContent({ initialData, onChange, externalErrors
287
292
  mainInstruction: instruction || '',
288
293
  },
289
294
  guideImages: guideImages.length > 0 ? guideImages : undefined,
295
+ ...(passage ? { passage } : {}),
290
296
  };
291
297
  return (_jsxs("div", { className: "space-y-6", children: [_jsx(Card, { children: _jsx(CardHeader, { children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx(CardTitle, { children: "Write a Short Letter (Preview)" }), _jsx("p", { className: "mt-1 text-sm text-gray-500", children: "Ch\u1EBF \u0111\u1ED9 xem tr\u01B0\u1EDBc - Hi\u1EC3n th\u1ECB \u0111\u00E1p \u00E1n \u0111\u00FAng" })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setIsClientMode(false), children: [_jsx(Pencil, { className: "mr-2 h-4 w-4" }), "Quay l\u1EA1i Edit Mode"] })] }) }) }), _jsx(WriteAShortLetterClient, { questionData: questionData, onSubmit: () => { }, isReviewMode: true })] }));
292
298
  }
@@ -8,6 +8,7 @@ export const writeAShortLetterRegistration = {
8
8
  ? parseFloat(initialData.total_points)
9
9
  : (initialData?.points || 10); // WRITE_A_SHORT_LETTER defaults to 10 points
10
10
  let creatorData = { points: apiPoints };
11
+ const passage = answer?.passage || content?.passage;
11
12
  // Priority 1: Data from store (answer has informations directly)
12
13
  if (answer?.informations && Array.isArray(answer.informations)) {
13
14
  creatorData = {
@@ -16,6 +17,7 @@ export const writeAShortLetterRegistration = {
16
17
  maxWords: answer.maxWords,
17
18
  instructions: answer.instructions || { mainInstruction: '' },
18
19
  guideImages: answer.guideImages || [],
20
+ ...(passage ? { passage } : {}),
19
21
  points: apiPoints,
20
22
  };
21
23
  }
@@ -27,6 +29,7 @@ export const writeAShortLetterRegistration = {
27
29
  maxWords: content.maxWords,
28
30
  instructions: content.instructions || { mainInstruction: '' },
29
31
  guideImages: content.guideImages || [],
32
+ ...(passage ? { passage } : {}),
30
33
  points: apiPoints,
31
34
  };
32
35
  }
@@ -42,6 +45,7 @@ export const writeAShortLetterRegistration = {
42
45
  maxWords: content?.maxWords ?? answer?.maxWords,
43
46
  instructions: content?.instructions || answer?.instructions || { mainInstruction: '' },
44
47
  guideImages: content?.guideImages || answer?.guideImages || [],
48
+ ...(passage ? { passage } : {}),
45
49
  points: apiPoints,
46
50
  };
47
51
  }
@@ -9,6 +9,7 @@ export const transformWriteAShortLetter = (question) => {
9
9
  const guideImages = Array.isArray(answerData?.guideImages)
10
10
  ? answerData.guideImages.filter((url) => typeof url === 'string' && url.trim() !== '')
11
11
  : [];
12
+ const passage = typeof answerData?.passage === 'string' ? answerData.passage.trim() : '';
12
13
  const apiContent = {
13
14
  informations,
14
15
  minWords: answerData?.minWords || 20,
@@ -18,6 +19,9 @@ export const transformWriteAShortLetter = (question) => {
18
19
  if (guideImages.length > 0) {
19
20
  apiContent.guideImages = guideImages;
20
21
  }
22
+ if (passage) {
23
+ apiContent.passage = passage;
24
+ }
21
25
  return {
22
26
  apiContent,
23
27
  apiCorrectAnswer: {},
@@ -3,6 +3,7 @@ type ChooseTheCorrectAnswerGroupViewerProps = {
3
3
  question: Question;
4
4
  isReviewMode?: boolean;
5
5
  userAnswer?: string[];
6
+ onSubmitAnswer?: (answer: string[]) => void;
6
7
  };
7
- export declare function ChooseTheCorrectAnswerGroupViewer({ question, isReviewMode, userAnswer, }: ChooseTheCorrectAnswerGroupViewerProps): import("react").JSX.Element;
8
+ export declare function ChooseTheCorrectAnswerGroupViewer({ question, isReviewMode, userAnswer, onSubmitAnswer, }: ChooseTheCorrectAnswerGroupViewerProps): import("react").JSX.Element;
8
9
  export {};
@@ -2,10 +2,10 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { ChooseTheCorrectAnswerGroupClient } from '../types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient';
4
4
  import { mapQuestionToChooseAnswerGroupData } from '../types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data';
5
- export function ChooseTheCorrectAnswerGroupViewer({ question, isReviewMode = false, userAnswer, }) {
5
+ export function ChooseTheCorrectAnswerGroupViewer({ question, isReviewMode = false, userAnswer, onSubmitAnswer, }) {
6
6
  const questionData = mapQuestionToChooseAnswerGroupData(question);
7
7
  const reviewAnswers = isReviewMode && !userAnswer
8
8
  ? questionData.items.map((item) => item.correctAnswer)
9
9
  : userAnswer;
10
- return (_jsx(ChooseTheCorrectAnswerGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: reviewAnswers }));
10
+ return (_jsx(ChooseTheCorrectAnswerGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: reviewAnswers, onAnswerChange: isReviewMode ? undefined : onSubmitAnswer }));
11
11
  }
@@ -0,0 +1,10 @@
1
+ import type { Question } from '../../../shared/types/entities/question.types';
2
+ import type { FillInBlankGroupStudentItemAnswer } from '../_shared/types/fill-in-blank-group.type';
3
+ type FillInBlankGroupViewerProps = {
4
+ question: Question;
5
+ isReviewMode?: boolean;
6
+ userAnswer?: FillInBlankGroupStudentItemAnswer[];
7
+ onSubmitAnswer?: (answer: FillInBlankGroupStudentItemAnswer[]) => void;
8
+ };
9
+ export declare function FillInBlankGroupViewer({ question, isReviewMode, userAnswer, onSubmitAnswer, }: FillInBlankGroupViewerProps): import("react").JSX.Element;
10
+ export {};
@@ -0,0 +1,14 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { FillInBlankGroupClient } from '../types/fill-in-blank-group/FillInBlankGroupClient';
4
+ import { mapQuestionToFillInBlankGroupData } from '../types/fill-in-blank-group/map-fill-in-blank-group-data';
5
+ export function FillInBlankGroupViewer({ question, isReviewMode = false, userAnswer, onSubmitAnswer, }) {
6
+ const questionData = mapQuestionToFillInBlankGroupData(question);
7
+ const studentAnswers = Array.isArray(userAnswer) ? userAnswer : undefined;
8
+ const reviewAnswers = isReviewMode && !studentAnswers
9
+ ? questionData.items.map((item) => ({
10
+ answers: item.answers.map((alts) => alts[0] || ''),
11
+ }))
12
+ : studentAnswers;
13
+ return (_jsx(FillInBlankGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: reviewAnswers, onAnswerChange: isReviewMode ? undefined : onSubmitAnswer }));
14
+ }
@@ -0,0 +1,9 @@
1
+ import type { Question } from '../../../shared/types/entities/question.types';
2
+ type MatchWordToPictureGroupViewerProps = {
3
+ question: Question;
4
+ isReviewMode?: boolean;
5
+ userAnswer?: string[];
6
+ onSubmitAnswer?: (answer: string[]) => void;
7
+ };
8
+ export declare function MatchWordToPictureGroupViewer({ question, isReviewMode, userAnswer, onSubmitAnswer, }: MatchWordToPictureGroupViewerProps): import("react").JSX.Element;
9
+ export {};
@@ -0,0 +1,11 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { MatchWordToPictureGroupClient } from '../types/match-word-to-picture/MatchWordToPictureGroupClient';
4
+ import { mapQuestionToMatchWordToPictureGroupData } from '../types/match-word-to-picture/map-match-word-to-picture-group-data';
5
+ export function MatchWordToPictureGroupViewer({ question, isReviewMode = false, userAnswer, onSubmitAnswer, }) {
6
+ const questionData = mapQuestionToMatchWordToPictureGroupData(question);
7
+ const reviewAnswers = isReviewMode && !userAnswer
8
+ ? questionData.items.map((item) => item.correctAnswer)
9
+ : userAnswer;
10
+ return (_jsx(MatchWordToPictureGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: reviewAnswers, onAnswerChange: isReviewMode ? undefined : onSubmitAnswer }));
11
+ }
@@ -0,0 +1,9 @@
1
+ import type { Question } from '../../../shared/types/entities/question.types';
2
+ type MatchWordToPictureViewerProps = {
3
+ question: Question;
4
+ isReviewMode?: boolean;
5
+ userAnswer?: string;
6
+ onSubmitAnswer?: (answer: string) => void;
7
+ };
8
+ export declare function MatchWordToPictureViewer({ question, isReviewMode, userAnswer, onSubmitAnswer, }: MatchWordToPictureViewerProps): import("react").JSX.Element;
9
+ export {};
@@ -0,0 +1,8 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { MatchWordToPictureClient } from '../types/match-word-to-picture/MatchWordToPictureClient';
4
+ import { mapQuestionToMatchWordToPictureData } from '../types/match-word-to-picture/map-match-word-to-picture-data';
5
+ export function MatchWordToPictureViewer({ question, isReviewMode = false, userAnswer, onSubmitAnswer, }) {
6
+ const questionData = mapQuestionToMatchWordToPictureData(question);
7
+ return (_jsx(MatchWordToPictureClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswer: userAnswer, autoFillCorrectAnswers: isReviewMode && !userAnswer, explanation: question.explanation, onSubmit: isReviewMode ? undefined : onSubmitAnswer }));
8
+ }
@@ -0,0 +1,7 @@
1
+ import type { Question } from '../../../shared/types/entities/question.types';
2
+ export declare function MatchingWithLinesGroupViewer({ question, isReviewMode, userAnswer, onSubmitAnswer, }: {
3
+ question: Question;
4
+ isReviewMode?: boolean;
5
+ userAnswer?: unknown;
6
+ onSubmitAnswer?: (answer: string[]) => void;
7
+ }): import("react").JSX.Element;
@@ -0,0 +1,48 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useState } from 'react';
4
+ import { MatchingWithLinesGroupClient } from '../types/matching-with-lines-group/MatchingWithLinesGroupClient';
5
+ import { mapQuestionToMatchingWithLinesGroupData } from '../types/matching-with-lines-group/map-matching-with-lines-group-data';
6
+ function isRecord(value) {
7
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
8
+ }
9
+ function emptyAnswers(length) {
10
+ return Array.from({ length }, () => '');
11
+ }
12
+ function parseMatchingAnswers(value, length) {
13
+ const source = Array.isArray(value)
14
+ ? value
15
+ : isRecord(value) && Array.isArray(value.answer)
16
+ ? value.answer
17
+ : null;
18
+ if (!source)
19
+ return undefined;
20
+ return Array.from({ length }, (_, index) => typeof source[index] === 'string' ? source[index] : '');
21
+ }
22
+ export function MatchingWithLinesGroupViewer({ question, isReviewMode = false, userAnswer, onSubmitAnswer, }) {
23
+ const questionData = mapQuestionToMatchingWithLinesGroupData(question);
24
+ const itemCount = questionData.items.length;
25
+ const parsedUser = parseMatchingAnswers(userAnswer, itemCount);
26
+ const questionKey = `${question.id}:${question.type}:${itemCount}:${questionData.rightItems
27
+ .map((item) => `${item.id}:${item.text}`)
28
+ .join(',')}`;
29
+ const [cachedKey, setCachedKey] = useState(questionKey);
30
+ const [localAnswers, setLocalAnswers] = useState(() => parsedUser ?? emptyAnswers(itemCount));
31
+ if (cachedKey !== questionKey) {
32
+ setCachedKey(questionKey);
33
+ setLocalAnswers(parsedUser ?? emptyAnswers(itemCount));
34
+ }
35
+ const userAnswers = isReviewMode && !parsedUser
36
+ ? questionData.items.map((item) => item.correctAnswer)
37
+ : parsedUser ?? localAnswers;
38
+ const handleAnswersChange = (next) => {
39
+ const padded = questionData.items.map((item, index) => {
40
+ if (item.isExample)
41
+ return item.correctAnswer || '';
42
+ return typeof next[index] === 'string' ? next[index] : '';
43
+ });
44
+ setLocalAnswers(padded);
45
+ onSubmitAnswer?.(padded);
46
+ };
47
+ return (_jsx(MatchingWithLinesGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: userAnswers, onAnswersChange: isReviewMode ? undefined : handleAnswersChange }));
48
+ }
@@ -25,10 +25,15 @@ import { parseFillBlankStemLine } from '../_shared/utils/parseFillBlankStem';
25
25
  import { WordFillStructuredFormViewer } from './WordFillStructuredFormViewer';
26
26
  import { ChooseTheCorrectAnswerGroupViewer } from './ChooseTheCorrectAnswerGroupViewer';
27
27
  import { TrueFalseGroupViewer } from './TrueFalseGroupViewer';
28
+ import { TrueFalseCorrectGroupViewer } from './TrueFalseCorrectGroupViewer';
29
+ import { FillInBlankGroupViewer } from './FillInBlankGroupViewer';
30
+ import { MatchingWithLinesGroupViewer } from './MatchingWithLinesGroupViewer';
28
31
  import { CrossOutWordGroupViewer } from './CrossOutWordGroupViewer';
29
32
  import { SpontaneousQaGroupViewer } from './SpontaneousQaGroupViewer';
30
33
  import { WordOrderAndMatchGroupViewer } from './WordOrderAndMatchGroupViewer';
31
34
  import { AnswerTheQuestionGroupViewer } from './AnswerTheQuestionGroupViewer';
35
+ import { MatchWordToPictureViewer } from './MatchWordToPictureViewer';
36
+ import { MatchWordToPictureGroupViewer } from './MatchWordToPictureGroupViewer';
32
37
  export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false, userAnswer, }) {
33
38
  const [selectedAnswer, setSelectedAnswer] = useState(userAnswer || null);
34
39
  const [playCount, setPlayCount] = useState(0);
@@ -338,6 +343,7 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
338
343
  : Array.isArray(answer?.guideImages)
339
344
  ? answer.guideImages
340
345
  : [];
346
+ const passage = content?.passage ?? answer?.passage;
341
347
  const contexts = content?.contexts || answer?.contexts || [];
342
348
  const minWords = content?.minWords ?? answer?.minWords ?? 20;
343
349
  const maxWords = content?.maxWords ?? answer?.maxWords ?? null;
@@ -345,6 +351,7 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
345
351
  contexts,
346
352
  informations,
347
353
  guideImages,
354
+ passage,
348
355
  minWords,
349
356
  maxWords,
350
357
  instructions: {
@@ -625,9 +632,15 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
625
632
  case 'CHOOSE_THE_CORRECT_ANSWER':
626
633
  return renderChooseTheCorrectAnswer();
627
634
  case 'CHOOSE_THE_CORRECT_ANSWER_GROUP':
628
- return (_jsx(ChooseTheCorrectAnswerGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer }));
635
+ return (_jsx(ChooseTheCorrectAnswerGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
629
636
  case 'TRUE_FALSE_GROUP':
630
637
  return (_jsx(TrueFalseGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer }));
638
+ case 'TRUE_FALSE_CORRECT_GROUP':
639
+ return (_jsx(TrueFalseCorrectGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer }));
640
+ case 'FILL_IN_BLANK_GROUP':
641
+ return (_jsx(FillInBlankGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
642
+ case 'MATCHING_WITH_LINES_GROUP':
643
+ return (_jsx(MatchingWithLinesGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
631
644
  case 'CROSS_OUT_WORD_GROUP':
632
645
  return (_jsx(CrossOutWordGroupViewer, { question: question, isReviewMode: isReviewMode }));
633
646
  case 'ANSWER_THE_QUESTION_GROUP':
@@ -637,6 +650,10 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
637
650
  case 'WORD_ORDER_AND_MATCH_GROUP':
638
651
  case 'WORD_ORDER_AND_MATCH':
639
652
  return (_jsx(WordOrderAndMatchGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer }));
653
+ case 'MATCH_WORD_TO_PICTURE_GROUP':
654
+ return (_jsx(MatchWordToPictureGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
655
+ case 'MATCH_WORD_TO_PICTURE':
656
+ return (_jsx(MatchWordToPictureViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
640
657
  case 'FILL_IN_BLANK':
641
658
  return renderFillInBlank();
642
659
  case 'TRUE_FALSE':
@@ -691,7 +708,9 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
691
708
  */
692
709
  const resolveCardTitle = () => {
693
710
  // WFSF and Crossword show title/instruction inside the client — no outer card label.
694
- if (question.type === 'WORD_FILL_STRUCTURED_FORM' || question.type === 'CROSSWORD_PUZZLE') {
711
+ if (question.type === 'WORD_FILL_STRUCTURED_FORM' ||
712
+ question.type === 'CROSSWORD_PUZZLE' ||
713
+ question.type === 'MATCH_WORD_TO_PICTURE') {
695
714
  return null;
696
715
  }
697
716
  // Modern fill stems live entirely in the body; title is a short label.
@@ -714,7 +733,9 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
714
733
  };
715
734
  const cardTitle = resolveCardTitle();
716
735
  // WFSF and Crossword already have their own bordered shell — avoid double Card border.
717
- if (question.type === 'WORD_FILL_STRUCTURED_FORM' || question.type === 'CROSSWORD_PUZZLE') {
736
+ if (question.type === 'WORD_FILL_STRUCTURED_FORM' ||
737
+ question.type === 'CROSSWORD_PUZZLE' ||
738
+ question.type === 'MATCH_WORD_TO_PICTURE') {
718
739
  return (_jsx("div", { className: "space-y-4", children: renderQuestion() }));
719
740
  }
720
741
  return (_jsxs(Card, { children: [cardTitle ? (_jsx(CardHeader, { children: _jsxs(CardTitle, { className: "flex items-center justify-between", children: [_jsx("span", { className: "min-w-0 pr-2 text-base font-semibold leading-snug", children: cardTitle }), _jsxs("span", { className: "shrink-0 text-sm font-normal text-gray-500", children: [question.points, " \u0111i\u1EC3m"] })] }) })) : null, _jsx(CardContent, { className: cardTitle ? 'space-y-4' : 'space-y-4 pt-4', children: renderQuestion() })] }));
@@ -0,0 +1,9 @@
1
+ import type { Question } from '../../../shared/types/entities/question.types';
2
+ import type { TrueFalseCorrectStudentItemAnswer } from '../_shared/types/true-false-correct-group.type';
3
+ type TrueFalseCorrectGroupViewerProps = {
4
+ question: Question;
5
+ isReviewMode?: boolean;
6
+ userAnswer?: TrueFalseCorrectStudentItemAnswer[];
7
+ };
8
+ export declare function TrueFalseCorrectGroupViewer({ question, isReviewMode, userAnswer, }: TrueFalseCorrectGroupViewerProps): import("react").JSX.Element;
9
+ export {};
@@ -0,0 +1,14 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { TrueFalseCorrectGroupClient } from '../types/true-false-correct-group/TrueFalseCorrectGroupClient';
4
+ import { mapQuestionToTrueFalseCorrectGroupData } from '../types/true-false-correct-group/map-true-false-correct-group-data';
5
+ export function TrueFalseCorrectGroupViewer({ question, isReviewMode = false, userAnswer, }) {
6
+ const questionData = mapQuestionToTrueFalseCorrectGroupData(question);
7
+ const reviewAnswers = isReviewMode && !userAnswer
8
+ ? questionData.items.map((item) => ({
9
+ isTrue: item.isTrue,
10
+ correction: item.correction,
11
+ }))
12
+ : userAnswer;
13
+ return (_jsx(TrueFalseCorrectGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: reviewAnswers }));
14
+ }
@@ -3,7 +3,12 @@ export { QuestionGroupViewer } from './QuestionGroupViewer';
3
3
  export { WordFillStructuredFormViewer } from './WordFillStructuredFormViewer';
4
4
  export { ChooseTheCorrectAnswerGroupViewer } from './ChooseTheCorrectAnswerGroupViewer';
5
5
  export { TrueFalseGroupViewer } from './TrueFalseGroupViewer';
6
+ export { TrueFalseCorrectGroupViewer } from './TrueFalseCorrectGroupViewer';
7
+ export { FillInBlankGroupViewer } from './FillInBlankGroupViewer';
8
+ export { MatchingWithLinesGroupViewer } from './MatchingWithLinesGroupViewer';
6
9
  export { CrossOutWordGroupViewer } from './CrossOutWordGroupViewer';
7
10
  export { SpontaneousQaGroupViewer } from './SpontaneousQaGroupViewer';
8
11
  export { WordOrderAndMatchGroupViewer } from './WordOrderAndMatchGroupViewer';
9
12
  export { AnswerTheQuestionGroupViewer } from './AnswerTheQuestionGroupViewer';
13
+ export { MatchWordToPictureViewer } from './MatchWordToPictureViewer';
14
+ export { MatchWordToPictureGroupViewer } from './MatchWordToPictureGroupViewer';
@@ -3,7 +3,12 @@ export { QuestionGroupViewer } from './QuestionGroupViewer';
3
3
  export { WordFillStructuredFormViewer } from './WordFillStructuredFormViewer';
4
4
  export { ChooseTheCorrectAnswerGroupViewer } from './ChooseTheCorrectAnswerGroupViewer';
5
5
  export { TrueFalseGroupViewer } from './TrueFalseGroupViewer';
6
+ export { TrueFalseCorrectGroupViewer } from './TrueFalseCorrectGroupViewer';
7
+ export { FillInBlankGroupViewer } from './FillInBlankGroupViewer';
8
+ export { MatchingWithLinesGroupViewer } from './MatchingWithLinesGroupViewer';
6
9
  export { CrossOutWordGroupViewer } from './CrossOutWordGroupViewer';
7
10
  export { SpontaneousQaGroupViewer } from './SpontaneousQaGroupViewer';
8
11
  export { WordOrderAndMatchGroupViewer } from './WordOrderAndMatchGroupViewer';
9
12
  export { AnswerTheQuestionGroupViewer } from './AnswerTheQuestionGroupViewer';
13
+ export { MatchWordToPictureViewer } from './MatchWordToPictureViewer';
14
+ export { MatchWordToPictureGroupViewer } from './MatchWordToPictureGroupViewer';
@@ -1,16 +1,11 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { Check, X, ZoomIn } from 'lucide-react';
4
- import { useState } from 'react';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Check, X } from 'lucide-react';
5
4
  import { ResolvedImage } from '../../../components/common/ResolvedImage';
6
5
  import CambridgeYlePartBanner from '../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
7
6
  import CambridgeYleInstructionBanner from '../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
8
7
  import CambridgeYleAudioPlayer from '../../../components/themes/cambridge-yle/CambridgeYleAudioPlayer';
9
- // Image with zoom functionality
10
- function QuestionImage({ imageUrl }) {
11
- const [isZoomed, setIsZoomed] = useState(false);
12
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "relative flex items-center justify-center", children: [_jsx(ResolvedImage, { src: imageUrl, alt: "Question illustration", className: "max-h-[250px] cursor-pointer rounded-lg object-contain shadow-md", onClick: () => setIsZoomed(true) }), _jsx("button", { type: "button", onClick: () => setIsZoomed(true), className: "absolute right-2 top-2 rounded-full bg-white/80 p-1.5 shadow-md hover:bg-white", "aria-label": "Zoom image", children: _jsx(ZoomIn, { className: "h-4 w-4 text-gray-600" }) })] }), isZoomed && (_jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/70", onClick: () => setIsZoomed(false), children: _jsx(ResolvedImage, { src: imageUrl, alt: "Question illustration (zoomed)", className: "max-h-[90vh] max-w-[90vw] rounded-lg object-contain", showLoadingState: false }) }))] }));
13
- }
8
+ import { ChooseBestAnswerImages, getChooseBestAnswerOptionImageSrc } from '../../../components/exams/take/components/question-renderers/ChooseBestAnswerImages';
14
9
  // Single Question Component for Review Mode
15
10
  function ReviewQuestionItem({ question, selectedOptionIds, isCorrect, correctOptionIds, }) {
16
11
  const hasStudentAnswered = selectedOptionIds.length > 0;
@@ -48,7 +43,7 @@ function ReviewQuestionItem({ question, selectedOptionIds, isCorrect, correctOpt
48
43
  }
49
44
  return _jsx("div", { className: `flex h-5 w-5 items-center justify-center border-2 border-gray-300 bg-white ${shapeClass}` });
50
45
  };
51
- return (_jsxs("div", { className: "mb-4", children: [_jsxs("p", { className: "mb-2 text-lg font-bold text-gray-900", children: [question.questionNumber, ". ", question.questionText] }), isMultipleAnswers && (_jsx("p", { className: "mb-3 pl-2 text-sm font-medium text-blue-600", children: "Ch\u1ECDn nhi\u1EC1u \u0111\u00E1p \u00E1n" })), question.imageUrl && (_jsx("div", { className: "mb-3", children: _jsx(QuestionImage, { imageUrl: question.imageUrl }) })), isImageOptions ? (_jsx("div", { className: "grid gap-3 px-2", style: { gridTemplateColumns: `repeat(${question.options.length}, minmax(0, 1fr))` }, children: question.options.map((option) => (_jsxs("div", { className: `relative flex flex-col items-center gap-1 rounded-lg border-2 p-2 cursor-default ${getOptionStyle(option.id)}`, children: [_jsx("span", { className: "text-xs font-medium text-gray-600", children: option.label }), _jsx(ResolvedImage, { src: option.text, alt: `Option ${option.label}`, className: "h-28 w-28 rounded object-contain" }), renderIndicator(option.id)] }, option.id))) })) : (
46
+ return (_jsxs("div", { className: "mb-4", children: [_jsxs("p", { className: "mb-2 text-lg font-bold text-gray-900", children: [question.questionNumber, ". ", question.questionText] }), isMultipleAnswers && (_jsx("p", { className: "mb-3 pl-2 text-sm font-medium text-blue-600", children: "Ch\u1ECDn nhi\u1EC1u \u0111\u00E1p \u00E1n" })), _jsx(ChooseBestAnswerImages, { imageUrl: question.imageUrl, className: "mb-3" }), isImageOptions ? (_jsx("div", { className: "grid gap-3 px-2", style: { gridTemplateColumns: `repeat(${question.options.length}, minmax(0, 1fr))` }, children: question.options.map((option) => (_jsxs("div", { className: `relative flex flex-col items-center gap-1 rounded-lg border-2 p-2 cursor-default ${getOptionStyle(option.id)}`, children: [_jsx("span", { className: "text-xs font-medium text-gray-600", children: option.label }), _jsx(ResolvedImage, { src: getChooseBestAnswerOptionImageSrc(option), alt: `Option ${option.label}`, className: "h-28 w-28 rounded object-contain" }), renderIndicator(option.id)] }, option.id))) })) : (
52
47
  /* Options — text variant */
53
48
  _jsx("div", { className: "flex flex-col gap-2 pl-2", children: question.options.map((option) => (_jsxs("div", { className: `flex items-center gap-3 rounded-lg border-2 px-3 py-2 cursor-default ${getOptionStyle(option.id)}`, children: [_jsxs("span", { className: "text-sm font-medium text-gray-600", children: [option.label, "."] }), _jsx("span", { className: "flex-1 text-sm text-gray-700", children: option.text }), renderIndicator(option.id)] }, option.id))) }))] }));
54
49
  }
@@ -4,8 +4,8 @@ import { jsx as _jsx } from "react/jsx-runtime";
4
4
  * Movers fallback review cases (isReviewMode=true).
5
5
  * Source: oe-exam-fe ReviewQuestionRenderer.tsx
6
6
  */
7
- import { MoversAnswerTheQuestionRenderer, MoversFillWordHintRenderer, MoversGridFillRenderer, MoversLabelThePictureRenderer, MoversMatchByWritingAnswerRenderer, MoversPictureChooseRenderer, MoversPictureFillBlankChooseRenderer, MoversReadingPassageRenderer, MoversTrueFalseRenderer, MoversCrossOutWordGroupRenderer, MoversWordFillParagraphRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderingRenderer, MoversWriteSentencesRenderer, MoversWritingRenderer, MoversCrosswordPuzzleRenderer, } from '../../../components/exams/take/components/question-renderers';
8
- import { transformAnswerTheQuestion, transformFillWordHint, transformGridFill, transformLabelPicture, transformMatchByWritingAnswer, transformPictureChoose, transformPictureFillBlankChoose, transformReadingPassage, transformTrueFalse, transformWordFillParagraph, transformWordFillStructuredForm, transformWordOrdering, transformWriteSentences, transformWriteShortLetter, transformCrosswordPuzzle, } from '../../../components/exams/take/utils/question-transformers';
7
+ import { MoversAnswerTheQuestionRenderer, MoversFillWordHintRenderer, MoversGridFillRenderer, MoversLabelThePictureRenderer, MoversMatchByWritingAnswerRenderer, MoversMatchWordToPictureRenderer, MoversPictureChooseRenderer, MoversPictureFillBlankChooseRenderer, MoversReadingPassageRenderer, MoversTrueFalseRenderer, MoversTrueFalseCorrectGroupRenderer, MoversFillInBlankGroupRenderer, MoversMatchingWithLinesGroupRenderer, MoversCrossOutWordGroupRenderer, MoversWordFillParagraphRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderingRenderer, MoversWriteSentencesRenderer, MoversWritingRenderer, MoversCrosswordPuzzleRenderer, } from '../../../components/exams/take/components/question-renderers';
8
+ import { transformAnswerTheQuestion, transformFillWordHint, transformGridFill, transformLabelPicture, transformMatchByWritingAnswer, transformMatchWordToPicture, transformPictureChoose, transformPictureFillBlankChoose, transformReadingPassage, transformTrueFalse, transformTrueFalseCorrectGroup, transformFillInBlankGroup, transformMatchingWithLinesGroup, transformWordFillParagraph, transformWordFillStructuredForm, transformWordOrdering, transformWriteSentences, transformWriteShortLetter, transformCrosswordPuzzle, } from '../../../components/exams/take/utils/question-transformers';
9
9
  import { normalizeCrossOutWordGroupAnswer, transformCrossOutWordGroup, } from '../../../components/exams/take/utils/cross-out-word-group.transformer';
10
10
  function asApiQuestions(questions) {
11
11
  return questions;
@@ -26,7 +26,7 @@ export function renderMoversReviewBody(ctx) {
26
26
  const data = transformWriteShortLetter(firstQuestion);
27
27
  const writingAnswer = answers[firstQuestion.id] || '';
28
28
  const gradingInfo = essayGradingMap[firstQuestion.id];
29
- return (_jsx(MoversWritingRenderer, { partNumber: partNo, partName: part.name, prompt: data.prompt, requirements: data.requirements, minWords: data.minWords, maxWords: data.maxWords, answer: writingAnswer, onAnswerChange: handleAnswerChange, isReviewMode: true, teacherScore: gradingInfo?.score, maxScore: gradingInfo?.maxScore, teacherFeedback: gradingInfo?.teacherFeedback, writingAssessment: gradingInfo?.writingAssessment }));
29
+ return (_jsx(MoversWritingRenderer, { partNumber: partNo, partName: part.name, prompt: data.prompt, requirements: data.requirements, minWords: data.minWords, maxWords: data.maxWords, answer: writingAnswer, onAnswerChange: handleAnswerChange, isReviewMode: true, guideImages: data.guideImages, passage: data.passage, teacherScore: gradingInfo?.score, maxScore: gradingInfo?.maxScore, teacherFeedback: gradingInfo?.teacherFeedback, writingAssessment: gradingInfo?.writingAssessment }));
30
30
  }
31
31
  case 'LOOK_PICTURE_CHOOSE_CORRECT_ANSWER': {
32
32
  const data = transformPictureChoose(apiQuestions);
@@ -110,6 +110,73 @@ export function renderMoversReviewBody(ctx) {
110
110
  });
111
111
  return (_jsx(MoversCrossOutWordGroupRenderer, { partNumber: partNo, partName: part.name, instruction: part.instructions || data.instruction, questionCount: questionCount, questions: data.questions, answers: answers, onAnswerChange: handleAnswerChange, isReviewMode: true, correctAnswerMap: correctAnswerMap }));
112
112
  }
113
+ case 'TRUE_FALSE_CORRECT_GROUP': {
114
+ const data = transformTrueFalseCorrectGroup(apiQuestions);
115
+ const correctAnswerMap = {};
116
+ data.questions.forEach((question) => {
117
+ if (question.exampleCorrectAnswer) {
118
+ correctAnswerMap[question.id] = question.exampleCorrectAnswer;
119
+ }
120
+ });
121
+ const parent = apiQuestions[0];
122
+ const rawCorrect = parent?.id ? rawCorrectAnswerMap[parent.id] : undefined;
123
+ const rawCorrectRecord = rawCorrect && typeof rawCorrect === 'object' && !Array.isArray(rawCorrect)
124
+ ? rawCorrect
125
+ : undefined;
126
+ const correctArr = Array.isArray(rawCorrectRecord?.answer)
127
+ ? rawCorrectRecord.answer
128
+ : Array.isArray(parent?.correct_answer?.answer)
129
+ ? parent.correct_answer.answer
130
+ : [];
131
+ data.questions.forEach((question, index) => {
132
+ if (!question.isExample && correctArr[index] && typeof correctArr[index] === 'object') {
133
+ const obj = correctArr[index];
134
+ const isTrue = obj.isTrue === true ? true : obj.isTrue === false ? false : null;
135
+ if (isTrue !== null) {
136
+ correctAnswerMap[question.id] = {
137
+ isTrue,
138
+ correction: typeof obj.correction === 'string' ? obj.correction : '',
139
+ };
140
+ }
141
+ }
142
+ });
143
+ return (_jsx(MoversTrueFalseCorrectGroupRenderer, { partNumber: partNo, partName: part.name, instruction: part.instructions || data.instruction, questionCount: questionCount, questions: data.questions, answers: answers, onAnswerChange: handleAnswerChange, isReviewMode: true, correctAnswerMap: correctAnswerMap }));
144
+ }
145
+ case 'FILL_IN_BLANK_GROUP': {
146
+ const data = transformFillInBlankGroup(apiQuestions);
147
+ const correctAnswerMap = {};
148
+ const parent = apiQuestions[0];
149
+ const rawCorrect = parent?.id ? rawCorrectAnswerMap[parent.id] : undefined;
150
+ const rawCorrectRecord = rawCorrect && typeof rawCorrect === 'object' && !Array.isArray(rawCorrect)
151
+ ? rawCorrect
152
+ : undefined;
153
+ const correctArr = Array.isArray(rawCorrectRecord?.answer)
154
+ ? rawCorrectRecord.answer
155
+ : Array.isArray(parent?.correct_answer?.answer)
156
+ ? parent.correct_answer.answer
157
+ : [];
158
+ data.questions.forEach((question, index) => {
159
+ const rawItem = correctArr[index];
160
+ if (!rawItem || typeof rawItem !== 'object' || Array.isArray(rawItem)) {
161
+ return;
162
+ }
163
+ const obj = rawItem;
164
+ if (!Array.isArray(obj.answers)) {
165
+ return;
166
+ }
167
+ correctAnswerMap[question.id] = {
168
+ answers: obj.answers.map((blank) => (Array.isArray(blank)
169
+ ? blank.map((alt) => String(alt ?? '').trim()).filter(Boolean)
170
+ : [String(blank ?? '').trim()].filter(Boolean))),
171
+ caseSensitive: obj.caseSensitive === true,
172
+ };
173
+ });
174
+ return (_jsx(MoversFillInBlankGroupRenderer, { partNumber: partNo, partName: part.name, instruction: part.instructions || data.instruction, questionCount: data.questions.filter((question) => !question.isExample).length, audioUrl: data.audioUrl, imageUrl: data.imageUrl, showHints: data.showHints, hints: data.hints, questions: data.questions, answers: answers, onAnswerChange: handleAnswerChange, isReviewMode: true, correctAnswerMap: correctAnswerMap }));
175
+ }
176
+ case 'MATCHING_WITH_LINES_GROUP': {
177
+ const data = transformMatchingWithLinesGroup(apiQuestions);
178
+ return (_jsx(MoversMatchingWithLinesGroupRenderer, { partNumber: partNo, partName: part.name, instruction: part.instructions || data.instruction, questionCount: questionCount, data: data, answers: answers, onAnswerChange: handleAnswerChange, isReviewMode: true }));
179
+ }
113
180
  case 'TRUE_FALSE': {
114
181
  const data = transformTrueFalse(apiQuestions);
115
182
  return (_jsx(MoversTrueFalseRenderer, { partNumber: partNo, partName: part.name, instruction: part.instructions || data.instruction, questionCount: questionCount, imageUrl: data.imageUrl, questions: data.questions, answers: answers, onAnswerChange: handleAnswerChange, isReviewMode: true, correctAnswerTextMap: correctAnswerTextMap }));
@@ -118,6 +185,10 @@ export function renderMoversReviewBody(ctx) {
118
185
  const data = transformMatchByWritingAnswer(apiQuestions);
119
186
  return (_jsx(MoversMatchByWritingAnswerRenderer, { partNumber: partNo, partName: part.name, instruction: part.instructions || data.instruction, questionCount: questionCount, audioUrl: part.audioUrl, options: data.options, questions: data.questions, answers: answers, onAnswerChange: handleAnswerChange, isReviewMode: true, viewMode: data.viewMode, groupImageUrl: data.groupImageUrl, correctAnswerTextMap: correctAnswerTextMap, isCorrectMap: isCorrectMap }));
120
187
  }
188
+ case 'MATCH_WORD_TO_PICTURE': {
189
+ const data = transformMatchWordToPicture(apiQuestions);
190
+ return (_jsx(MoversMatchWordToPictureRenderer, { partNumber: partNo, partName: part.name, instruction: part.instructions || data.instruction, questionCount: questionCount, wordBank: data.wordBank, questions: data.questions, answers: answers, onAnswerChange: handleAnswerChange, isReviewMode: true, correctAnswerTextMap: correctAnswerTextMap, isCorrectMap: isCorrectMap }));
191
+ }
121
192
  case 'WORD_ORDERING': {
122
193
  const data = transformWordOrdering(apiQuestions);
123
194
  return (_jsx(MoversWordOrderingRenderer, { partNumber: partNo, partName: part.name, instruction: part.instructions || data.instruction, questionCount: questionCount, questions: data.questions, answers: answers, onAnswerChange: handleAnswerChange, isReviewMode: true, rawCorrectAnswerMap: rawCorrectAnswerMap, isCorrectMap: isCorrectMap }));
@@ -8,6 +8,9 @@ const FLATTENED_ANSWER_TYPES = new Set([
8
8
  'CHOOSE_THE_CORRECT_ANSWER_GROUP',
9
9
  'ANSWER_THE_QUESTION_GROUP',
10
10
  'CROSS_OUT_WORD_GROUP',
11
+ 'TRUE_FALSE_CORRECT_GROUP',
12
+ 'FILL_IN_BLANK_GROUP',
13
+ 'MATCHING_WITH_LINES_GROUP',
11
14
  ]);
12
15
  export function transformResultAnswersForReview(answers, templateParts = []) {
13
16
  const groups = groupAnswers(answers);
@@ -183,6 +186,62 @@ function transformStudentAnswer(questionId, questionType, value) {
183
186
  normalizeCrossOutWordGroupAnswer(item),
184
187
  ]));
185
188
  }
189
+ if (questionType === 'TRUE_FALSE_CORRECT_GROUP') {
190
+ const answer = Array.isArray(record.answer)
191
+ ? record.answer
192
+ : Array.isArray(value)
193
+ ? value
194
+ : [];
195
+ return Object.fromEntries(answer.map((item, index) => {
196
+ if (item && typeof item === 'object' && !Array.isArray(item)) {
197
+ const obj = item;
198
+ const isTrue = obj.isTrue === true ? true : obj.isTrue === false ? false : null;
199
+ return [
200
+ `${questionId}-item-${index}`,
201
+ {
202
+ isTrue,
203
+ correction: typeof obj.correction === 'string' ? obj.correction : '',
204
+ },
205
+ ];
206
+ }
207
+ return [`${questionId}-item-${index}`, { isTrue: null, correction: '' }];
208
+ }));
209
+ }
210
+ if (questionType === 'FILL_IN_BLANK_GROUP') {
211
+ const answer = Array.isArray(record.answer)
212
+ ? record.answer
213
+ : Array.isArray(value)
214
+ ? value
215
+ : [];
216
+ return Object.fromEntries(answer.map((item, index) => {
217
+ if (item && typeof item === 'object' && !Array.isArray(item) && Array.isArray(item.answers)) {
218
+ return [
219
+ `${questionId}-item-${index}`,
220
+ {
221
+ answers: item.answers.map((value) => String(value ?? '')),
222
+ },
223
+ ];
224
+ }
225
+ if (Array.isArray(item)) {
226
+ return [`${questionId}-item-${index}`, { answers: item.map((value) => String(value ?? '')) }];
227
+ }
228
+ if (typeof item === 'string') {
229
+ return [`${questionId}-item-${index}`, { answers: [item] }];
230
+ }
231
+ return [`${questionId}-item-${index}`, { answers: [] }];
232
+ }));
233
+ }
234
+ if (questionType === 'MATCHING_WITH_LINES_GROUP') {
235
+ const answer = Array.isArray(record.answer)
236
+ ? record.answer
237
+ : Array.isArray(value)
238
+ ? value
239
+ : [];
240
+ return Object.fromEntries(answer.map((item, index) => [
241
+ `${questionId}-item-${index}`,
242
+ typeof item === 'string' ? item : '',
243
+ ]));
244
+ }
186
245
  if (questionType === 'CHOOSE_THE_CORRECT_ANSWER' || questionType === 'CHOOSE_CORRECT_ADJECTIVE') {
187
246
  const rawAns = record.answer ?? record.optionId ?? record.optionIds;
188
247
  const ids = getOptionIds(questionId, rawAns);