@tinyweb_dev/oe-exam-sdk 0.2.3 → 0.2.5

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 (106) hide show
  1. package/dist/components/exams/take/components/QuestionRenderer.js +11 -3
  2. package/dist/components/exams/take/components/question-renderers/ChooseBestAnswerImages.d.ts +10 -0
  3. package/dist/components/exams/take/components/question-renderers/ChooseBestAnswerImages.js +38 -0
  4. package/dist/components/exams/take/components/question-renderers/MoversChooseBestAnswerRenderer.js +5 -11
  5. package/dist/components/exams/take/components/question-renderers/MoversMatchWordToPictureRenderer.d.ts +16 -0
  6. package/dist/components/exams/take/components/question-renderers/MoversMatchWordToPictureRenderer.js +75 -0
  7. package/dist/components/exams/take/components/question-renderers/MoversTrueFalseCorrectGroupRenderer.d.ts +18 -0
  8. package/dist/components/exams/take/components/question-renderers/MoversTrueFalseCorrectGroupRenderer.js +92 -0
  9. package/dist/components/exams/take/components/question-renderers/MoversWritingRenderer.d.ts +2 -1
  10. package/dist/components/exams/take/components/question-renderers/MoversWritingRenderer.js +2 -2
  11. package/dist/components/exams/take/components/question-renderers/index.d.ts +2 -0
  12. package/dist/components/exams/take/components/question-renderers/index.js +2 -0
  13. package/dist/components/exams/take/exam-taking.utils.d.ts +1 -0
  14. package/dist/components/exams/take/exam-taking.utils.js +9 -1
  15. package/dist/components/exams/take/types.d.ts +3 -2
  16. package/dist/components/exams/take/utils/answer-transformers.js +12 -0
  17. package/dist/components/exams/take/utils/question-transformers.d.ts +29 -1
  18. package/dist/components/exams/take/utils/question-transformers.js +98 -0
  19. package/dist/components/questions/_shared/config/question-types.config.js +12 -0
  20. package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.d.ts +9 -0
  21. package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.js +49 -1
  22. package/dist/components/questions/_shared/types/index.d.ts +2 -0
  23. package/dist/components/questions/_shared/types/index.js +2 -0
  24. package/dist/components/questions/_shared/types/match-word-to-picture.type.d.ts +62 -0
  25. package/dist/components/questions/_shared/types/match-word-to-picture.type.js +1 -0
  26. package/dist/components/questions/_shared/types/true-false-correct-group.type.d.ts +42 -0
  27. package/dist/components/questions/_shared/types/true-false-correct-group.type.js +2 -0
  28. package/dist/components/questions/_shared/types/write-a-short-letter.type.d.ts +4 -0
  29. package/dist/components/questions/components/QuestionSearchDropdown.js +2 -0
  30. package/dist/components/questions/components/QuestionTypeSelector.js +1 -0
  31. package/dist/components/questions/creator/question-type-registry.js +4 -0
  32. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseAnswerGroupImageUpload.d.ts +13 -0
  33. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseAnswerGroupImageUpload.js +32 -0
  34. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.d.ts +1 -1
  35. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +42 -6
  36. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupCreator.js +40 -20
  37. package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.d.ts +1 -1
  38. package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.js +5 -0
  39. package/dist/components/questions/types/choose-the-correct-answer-group/transform.js +4 -0
  40. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureClient.d.ts +2 -0
  41. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureClient.js +31 -0
  42. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureCreator.d.ts +2 -0
  43. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureCreator.js +267 -0
  44. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.d.ts +2 -0
  45. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.js +65 -0
  46. package/dist/components/questions/types/match-word-to-picture/WordBankEditor.d.ts +8 -0
  47. package/dist/components/questions/types/match-word-to-picture/WordBankEditor.js +15 -0
  48. package/dist/components/questions/types/match-word-to-picture/index.d.ts +6 -0
  49. package/dist/components/questions/types/match-word-to-picture/index.js +6 -0
  50. package/dist/components/questions/types/match-word-to-picture/map-match-word-to-picture-data.d.ts +2 -0
  51. package/dist/components/questions/types/match-word-to-picture/map-match-word-to-picture-data.js +53 -0
  52. package/dist/components/questions/types/match-word-to-picture/map-match-word-to-picture-group-data.d.ts +4 -0
  53. package/dist/components/questions/types/match-word-to-picture/map-match-word-to-picture-group-data.js +75 -0
  54. package/dist/components/questions/types/match-word-to-picture/register.d.ts +2 -0
  55. package/dist/components/questions/types/match-word-to-picture/register.js +55 -0
  56. package/dist/components/questions/types/match-word-to-picture/transform.d.ts +11 -0
  57. package/dist/components/questions/types/match-word-to-picture/transform.js +43 -0
  58. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.d.ts +2 -0
  59. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js +25 -0
  60. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupCreator.d.ts +2 -0
  61. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupCreator.js +144 -0
  62. package/dist/components/questions/types/true-false-correct-group/index.d.ts +5 -0
  63. package/dist/components/questions/types/true-false-correct-group/index.js +5 -0
  64. package/dist/components/questions/types/true-false-correct-group/map-true-false-correct-group-data.d.ts +4 -0
  65. package/dist/components/questions/types/true-false-correct-group/map-true-false-correct-group-data.js +79 -0
  66. package/dist/components/questions/types/true-false-correct-group/register.d.ts +2 -0
  67. package/dist/components/questions/types/true-false-correct-group/register.js +36 -0
  68. package/dist/components/questions/types/true-false-correct-group/transform.d.ts +2 -0
  69. package/dist/components/questions/types/true-false-correct-group/transform.js +49 -0
  70. package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterClient.js +27 -3
  71. package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterCreator.js +8 -2
  72. package/dist/components/questions/types/write-a-short-letter/register.js +4 -0
  73. package/dist/components/questions/types/write-a-short-letter/transform.js +4 -0
  74. package/dist/components/questions/viewer/AnswerTheQuestionGroupViewer.d.ts +8 -0
  75. package/dist/components/questions/viewer/AnswerTheQuestionGroupViewer.js +11 -0
  76. package/dist/components/questions/viewer/ChooseTheCorrectAnswerGroupViewer.d.ts +2 -1
  77. package/dist/components/questions/viewer/ChooseTheCorrectAnswerGroupViewer.js +2 -2
  78. package/dist/components/questions/viewer/MatchWordToPictureGroupViewer.d.ts +9 -0
  79. package/dist/components/questions/viewer/MatchWordToPictureGroupViewer.js +11 -0
  80. package/dist/components/questions/viewer/MatchWordToPictureViewer.d.ts +9 -0
  81. package/dist/components/questions/viewer/MatchWordToPictureViewer.js +8 -0
  82. package/dist/components/questions/viewer/QuestionViewer.js +58 -9
  83. package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.d.ts +9 -0
  84. package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.js +14 -0
  85. package/dist/components/questions/viewer/index.d.ts +4 -0
  86. package/dist/components/questions/viewer/index.js +4 -0
  87. package/dist/components/results/renderers/ReviewChooseBestAnswerRenderer.js +4 -9
  88. package/dist/components/results/renderers/review-question-body-movers.js +39 -3
  89. package/dist/components/results/review-data.js +22 -0
  90. package/dist/components/themes/english-certification/EcWritingTask.js +2 -2
  91. package/dist/index.d.ts +1 -1
  92. package/dist/index.js +1 -1
  93. package/dist/shared/constants/question-skills.js +4 -0
  94. package/dist/shared/lib/hooks/usePresignedFileUrl.d.ts +3 -1
  95. package/dist/shared/lib/hooks/usePresignedFileUrl.js +30 -12
  96. package/dist/shared/lib/utils/question-reverse-transform.js +111 -2
  97. package/dist/shared/lib/utils/question-transform.js +4 -0
  98. package/dist/shared/types/common.types.d.ts +1 -1
  99. package/dist/shared/types/questions/choose-the-correct-answer-group.d.ts +1 -0
  100. package/dist/shared/types/questions/index.d.ts +2 -0
  101. package/dist/shared/types/questions/index.js +4 -0
  102. package/dist/shared/types/questions/match-word-to-picture.d.ts +35 -0
  103. package/dist/shared/types/questions/match-word-to-picture.js +5 -0
  104. package/dist/shared/types/questions/true-false-correct-group.d.ts +39 -0
  105. package/dist/shared/types/questions/true-false-correct-group.js +1 -0
  106. package/package.json +7 -2
@@ -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, 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, 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,38 @@ 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
+ }
113
145
  case 'TRUE_FALSE': {
114
146
  const data = transformTrueFalse(apiQuestions);
115
147
  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 +150,10 @@ export function renderMoversReviewBody(ctx) {
118
150
  const data = transformMatchByWritingAnswer(apiQuestions);
119
151
  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
152
  }
153
+ case 'MATCH_WORD_TO_PICTURE': {
154
+ const data = transformMatchWordToPicture(apiQuestions);
155
+ 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 }));
156
+ }
121
157
  case 'WORD_ORDERING': {
122
158
  const data = transformWordOrdering(apiQuestions);
123
159
  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,7 @@ 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',
11
12
  ]);
12
13
  export function transformResultAnswersForReview(answers, templateParts = []) {
13
14
  const groups = groupAnswers(answers);
@@ -183,6 +184,27 @@ function transformStudentAnswer(questionId, questionType, value) {
183
184
  normalizeCrossOutWordGroupAnswer(item),
184
185
  ]));
185
186
  }
187
+ if (questionType === 'TRUE_FALSE_CORRECT_GROUP') {
188
+ const answer = Array.isArray(record.answer)
189
+ ? record.answer
190
+ : Array.isArray(value)
191
+ ? value
192
+ : [];
193
+ return Object.fromEntries(answer.map((item, index) => {
194
+ if (item && typeof item === 'object' && !Array.isArray(item)) {
195
+ const obj = item;
196
+ const isTrue = obj.isTrue === true ? true : obj.isTrue === false ? false : null;
197
+ return [
198
+ `${questionId}-item-${index}`,
199
+ {
200
+ isTrue,
201
+ correction: typeof obj.correction === 'string' ? obj.correction : '',
202
+ },
203
+ ];
204
+ }
205
+ return [`${questionId}-item-${index}`, { isTrue: null, correction: '' }];
206
+ }));
207
+ }
186
208
  if (questionType === 'CHOOSE_THE_CORRECT_ANSWER' || questionType === 'CHOOSE_CORRECT_ADJECTIVE') {
187
209
  const rawAns = record.answer ?? record.optionId ?? record.optionIds;
188
210
  const ids = getOptionIds(questionId, rawAns);
@@ -46,7 +46,7 @@ export function EcWritingTask({ part, answers, onAnswerChange, isReviewMode = fa
46
46
  if (!firstQuestion || !data) {
47
47
  return null;
48
48
  }
49
- const { prompt, requirements, guideImages } = data;
49
+ const { prompt, requirements, guideImages, passage } = data;
50
50
  const meetsMin = minWords ? wordCount >= minWords : true;
51
51
  const withinMax = maxWords ? wordCount <= maxWords : true;
52
52
  const isValid = meetsMin && withinMax;
@@ -54,7 +54,7 @@ export function EcWritingTask({ part, answers, onAnswerChange, isReviewMode = fa
54
54
  const progressPct = progressTarget
55
55
  ? Math.min(100, Math.round((wordCount / progressTarget) * 100))
56
56
  : 0;
57
- return (_jsxs("div", { id: englishCertQuestionDomId(firstQuestion.id), className: "scroll-mt-24 flex flex-col gap-5", children: [_jsxs("div", { className: "flex flex-col gap-5", children: [_jsxs("div", { className: "overflow-hidden rounded-2xl border border-[#cfe0f7] bg-white shadow-[0_18px_40px_rgba(13,71,161,0.10)]", children: [prompt && (_jsx(HtmlOrText, { value: prompt, as: "div", className: "whitespace-pre-line px-5 pb-4 pt-5 text-base leading-relaxed text-[#0f1730]" })), requirements && requirements.length > 0 && (_jsx("div", { className: cn('px-5 pb-4', !prompt && 'pt-5'), children: _jsxs("div", { className: "rounded-xl border border-[#e3edfb] bg-[#f6faff] p-4", children: [_jsxs("div", { className: "flex items-center gap-1.5 text-[11px] font-bold uppercase tracking-wider text-[#0b57d0]", children: [_jsx(ListChecks, { className: "h-3.5 w-3.5" }), "Requirements"] }), _jsx("ul", { className: "mt-2 list-inside list-disc space-y-1.5 text-sm text-[#0f1730]", children: requirements.map((req, idx) => (_jsx("li", { children: _jsx(HtmlOrText, { value: req, as: "span" }) }, idx))) })] }) })), minWords && (_jsxs("div", { className: "border-t border-[#e3edfb] bg-[#f9fbfe] px-5 py-3 text-sm font-semibold text-[#0f1730]", children: ["Write at least ", minWords, " words."] }))] }), guideImages && guideImages.length > 0 && (_jsx("div", { className: "flex flex-col items-center justify-center gap-4 rounded-2xl border border-[#d9e4f2] bg-white/60 p-4", children: guideImages.map((img, idx) => (_jsx(ResolvedImage, { src: img, alt: `Guide ${idx + 1}`, className: "w-full max-w-3xl rounded-xl bg-white object-contain shadow-sm" }, idx))) }))] }), _jsxs("div", { className: "overflow-hidden rounded-2xl border border-[#cfe0f7] bg-white shadow-[0_18px_40px_rgba(13,71,161,0.10)]", children: [_jsxs("div", { className: "flex items-center gap-1.5 border-b border-[#e3edfb] bg-[#f6faff] px-4 py-2.5 text-xs font-bold uppercase tracking-wider text-[#0b57d0]", children: [_jsx(PenLine, { className: "h-3.5 w-3.5" }), "Your answer"] }), _jsx("textarea", { value: answer, disabled: isReviewMode, placeholder: "Write your essay here\u2026", onChange: (e) => onAnswerChange(firstQuestion.id, e.target.value), rows: 14, className: cn('w-full resize-y border-0 bg-white px-5 py-4 text-sm leading-relaxed text-[#0f1730] outline-none transition-all placeholder:text-[#9aa9c2] focus:bg-[#fbfdff]', isReviewMode && 'cursor-not-allowed opacity-80') }), _jsxs("div", { className: "flex flex-col gap-2 border-t border-[#e3edfb] bg-[#f9fbfe] px-4 py-3", children: [_jsxs("div", { className: "flex items-center justify-between text-xs", children: [_jsxs("span", { className: cn('inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 font-semibold', isValid
57
+ return (_jsxs("div", { id: englishCertQuestionDomId(firstQuestion.id), className: "scroll-mt-24 flex flex-col gap-5", children: [_jsxs("div", { className: "flex flex-col gap-5", children: [_jsxs("div", { className: "overflow-hidden rounded-2xl border border-[#cfe0f7] bg-white shadow-[0_18px_40px_rgba(13,71,161,0.10)]", children: [prompt && (_jsx(HtmlOrText, { value: prompt, as: "div", className: "whitespace-pre-line px-5 pb-4 pt-5 text-base leading-relaxed text-[#0f1730]" })), passage?.trim() && (_jsx("div", { className: "px-5 pb-4", children: _jsxs("div", { className: "rounded-xl border border-[#e3edfb] bg-[#f6faff] p-4", children: [_jsx("p", { className: "text-[11px] font-bold uppercase tracking-wider text-[#0b57d0]", children: "Read" }), _jsx(HtmlOrText, { value: passage, as: "div", className: "mt-2 text-sm leading-relaxed text-[#0f1730]" })] }) })), requirements && requirements.length > 0 && (_jsx("div", { className: cn('px-5 pb-4', !prompt && 'pt-5'), children: _jsxs("div", { className: "rounded-xl border border-[#e3edfb] bg-[#f6faff] p-4", children: [_jsxs("div", { className: "flex items-center gap-1.5 text-[11px] font-bold uppercase tracking-wider text-[#0b57d0]", children: [_jsx(ListChecks, { className: "h-3.5 w-3.5" }), "Requirements"] }), _jsx("ul", { className: "mt-2 list-inside list-disc space-y-1.5 text-sm text-[#0f1730]", children: requirements.map((req, idx) => (_jsx("li", { children: _jsx(HtmlOrText, { value: req, as: "span" }) }, idx))) })] }) })), minWords && (_jsxs("div", { className: "border-t border-[#e3edfb] bg-[#f9fbfe] px-5 py-3 text-sm font-semibold text-[#0f1730]", children: ["Write at least ", minWords, " words."] }))] }), guideImages && guideImages.length > 0 && (_jsx("div", { className: "flex flex-col items-center justify-center gap-4 rounded-2xl border border-[#d9e4f2] bg-white/60 p-4", children: guideImages.map((img, idx) => (_jsx(ResolvedImage, { src: img, alt: `Guide ${idx + 1}`, className: "w-full max-w-3xl rounded-xl bg-white object-contain shadow-sm" }, idx))) }))] }), _jsxs("div", { className: "overflow-hidden rounded-2xl border border-[#cfe0f7] bg-white shadow-[0_18px_40px_rgba(13,71,161,0.10)]", children: [_jsxs("div", { className: "flex items-center gap-1.5 border-b border-[#e3edfb] bg-[#f6faff] px-4 py-2.5 text-xs font-bold uppercase tracking-wider text-[#0b57d0]", children: [_jsx(PenLine, { className: "h-3.5 w-3.5" }), "Your answer"] }), _jsx("textarea", { value: answer, disabled: isReviewMode, placeholder: "Write your essay here\u2026", onChange: (e) => onAnswerChange(firstQuestion.id, e.target.value), rows: 14, className: cn('w-full resize-y border-0 bg-white px-5 py-4 text-sm leading-relaxed text-[#0f1730] outline-none transition-all placeholder:text-[#9aa9c2] focus:bg-[#fbfdff]', isReviewMode && 'cursor-not-allowed opacity-80') }), _jsxs("div", { className: "flex flex-col gap-2 border-t border-[#e3edfb] bg-[#f9fbfe] px-4 py-3", children: [_jsxs("div", { className: "flex items-center justify-between text-xs", children: [_jsxs("span", { className: cn('inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 font-semibold', isValid
58
58
  ? 'bg-[#ddf8e7] text-[#15803d]'
59
59
  : 'bg-[#fde8e8] text-[#ef4444]'), children: [wordCount, " words"] }), (minWords || maxWords) && (_jsxs("span", { className: "font-medium text-[#64748b]", children: [minWords ? `Min ${minWords}` : '', minWords && maxWords ? ' · ' : '', maxWords ? `Max ${maxWords}` : ''] }))] }), progressTarget > 0 && (_jsx("div", { className: "h-1.5 w-full overflow-hidden rounded-full bg-[#e3edfb]", children: _jsx("div", { className: cn('h-full rounded-full transition-all', isValid ? 'bg-[#15803d]' : 'bg-[#1167e8]'), style: { width: `${progressPct}%` } }) })), statusMessage && (_jsxs("div", { className: cn('flex items-center gap-1.5 text-xs font-medium', statusMessage.ok ? 'text-[#15803d]' : 'text-[#64748b]'), children: [statusMessage.ok ? (_jsx(CheckCircle2, { className: "h-3.5 w-3.5 shrink-0" })) : (_jsx(AlertCircle, { className: "h-3.5 w-3.5 shrink-0" })), statusMessage.text] }))] })] })] }));
60
60
  }
package/dist/index.d.ts CHANGED
@@ -26,4 +26,4 @@ export { validateQuestion } from './components/questions/_shared/utils/question-
26
26
  export { buildAnswerPayload } from './components/questions/_shared/utils/answer-builder';
27
27
  export type { QuestionFormState } from './components/questions/_shared/hooks/useQuestionFormState';
28
28
  export * from './components/questions/_shared/types';
29
- export { QuestionViewer, QuestionGroupViewer, WordFillStructuredFormViewer, ChooseTheCorrectAnswerGroupViewer, TrueFalseGroupViewer, CrossOutWordGroupViewer, SpontaneousQaGroupViewer, WordOrderAndMatchGroupViewer, } from './components/questions/viewer';
29
+ export { QuestionViewer, QuestionGroupViewer, WordFillStructuredFormViewer, ChooseTheCorrectAnswerGroupViewer, TrueFalseGroupViewer, TrueFalseCorrectGroupViewer, CrossOutWordGroupViewer, SpontaneousQaGroupViewer, WordOrderAndMatchGroupViewer, AnswerTheQuestionGroupViewer, MatchWordToPictureViewer, MatchWordToPictureGroupViewer, } from './components/questions/viewer';
package/dist/index.js CHANGED
@@ -21,4 +21,4 @@ export { QUESTION_TYPE_CONFIG, QUESTION_TYPES_WITH_DEDICATED_COMPONENTS } from '
21
21
  export { validateQuestion } from './components/questions/_shared/utils/question-validation';
22
22
  export { buildAnswerPayload } from './components/questions/_shared/utils/answer-builder';
23
23
  export * from './components/questions/_shared/types';
24
- export { QuestionViewer, QuestionGroupViewer, WordFillStructuredFormViewer, ChooseTheCorrectAnswerGroupViewer, TrueFalseGroupViewer, CrossOutWordGroupViewer, SpontaneousQaGroupViewer, WordOrderAndMatchGroupViewer, } from './components/questions/viewer';
24
+ export { QuestionViewer, QuestionGroupViewer, WordFillStructuredFormViewer, ChooseTheCorrectAnswerGroupViewer, TrueFalseGroupViewer, TrueFalseCorrectGroupViewer, CrossOutWordGroupViewer, SpontaneousQaGroupViewer, WordOrderAndMatchGroupViewer, AnswerTheQuestionGroupViewer, MatchWordToPictureViewer, MatchWordToPictureGroupViewer, } from './components/questions/viewer';
@@ -52,6 +52,7 @@ export const QUESTION_TYPE_SKILLS = {
52
52
  'WORD_ORDERING': SkillEnum.READING,
53
53
  'WORD_FILL_PARAGRAPH': SkillEnum.READING,
54
54
  'MATCH_BY_WRITING_ANSWER': SkillEnum.READING,
55
+ 'MATCH_WORD_TO_PICTURE': SkillEnum.READING,
55
56
  // Visual-based reading questions
56
57
  'LABEL_THE_PICTURE': SkillEnum.READING,
57
58
  'LOOK_PICTURE_CHOOSE_CORRECT_ANSWER': SkillEnum.READING,
@@ -62,6 +63,7 @@ export const QUESTION_TYPE_SKILLS = {
62
63
  // Other reading types
63
64
  'TRUE_FALSE': SkillEnum.READING,
64
65
  'TRUE_FALSE_GROUP': SkillEnum.READING,
66
+ 'TRUE_FALSE_CORRECT_GROUP': SkillEnum.READING,
65
67
  'CROSS_OUT_WORD_GROUP': SkillEnum.READING,
66
68
  'SPONTANEOUS_QA_GROUP': SkillEnum.SPEAKING,
67
69
  'WORD_ORDER_AND_MATCH_GROUP': SkillEnum.READING,
@@ -185,6 +187,7 @@ export const QUESTION_TYPE_LABELS = {
185
187
  'IMAGE_OBJECT_MATCHING': 'Ghép hình ảnh - Đối tượng',
186
188
  'TRUE_FALSE': 'Đúng/Sai',
187
189
  'TRUE_FALSE_GROUP': 'Đúng/Sai (nhóm)',
190
+ 'TRUE_FALSE_CORRECT_GROUP': 'Đúng/Sai + sửa câu (nhóm)',
188
191
  'CROSS_OUT_WORD_GROUP': 'Gạch từ / sửa từ (nhóm)',
189
192
  'SPONTANEOUS_QA_GROUP': 'Hỏi đáp tự phát (nhóm)',
190
193
  'WORD_ORDER_AND_MATCH_GROUP': 'Sắp xếp từ và nối nhãn (nhóm)',
@@ -205,6 +208,7 @@ export const QUESTION_TYPE_LABELS = {
205
208
  'WORD_ORDERING': 'Sắp xếp từ',
206
209
  'WORD_FILL_PARAGRAPH': 'Điền từ vào đoạn văn',
207
210
  'MATCH_BY_WRITING_ANSWER': 'Ghép đáp án bằng cách viết',
211
+ 'MATCH_WORD_TO_PICTURE': 'Nối từ với tranh',
208
212
  'WORD_FILL_STRUCTURED_FORM': 'Điền từ vào biểu mẫu có cấu trúc',
209
213
  'CROSSWORD_PUZZLE': 'Giải ô chữ (Crossword)',
210
214
  'FIND_WORDS_IN_MATRIX': 'Tìm từ trong ma trận (Word Search)',
@@ -9,7 +9,9 @@ export interface PresignedUrlResponse {
9
9
  * - Empty/undefined → empty string.
10
10
  * - Already a URL (http/https/blob/data) → returned unchanged.
11
11
  * - An S3 storage key (e.g. `exams/:id/audio/x.wav`) → resolved to a presigned
12
- * URL via the host API (`/api/v1/exam-storage/:key`).
12
+ * URL via the host API. Tries `/api/v1/exam-storage/:key` (oe-class proxy)
13
+ * then `/api/v1/storage/:key` (oe-exam-api). Override with
14
+ * `NEXT_PUBLIC_EXAM_STORAGE_PATH`.
13
15
  */
14
16
  export declare function usePresignedFileUrl(fileKey: string | undefined): {
15
17
  previewUrl: string;
@@ -6,7 +6,14 @@ import { useEffect, useMemo, useState } from 'react';
6
6
  * owns them. Reads NEXT_PUBLIC_API_URL — inlined at build time in the host.
7
7
  */
8
8
  const HOST_API_BASE_URL = (process.env.NEXT_PUBLIC_API_URL ?? '').replace(/\/+$/, '');
9
- const EXAM_STORAGE_PATH = '/api/v1/exam-storage';
9
+ const DEFAULT_STORAGE_PATHS = ['/api/v1/exam-storage', '/api/v1/storage'];
10
+ function getStoragePaths() {
11
+ const configured = process.env.NEXT_PUBLIC_EXAM_STORAGE_PATH;
12
+ const paths = configured
13
+ ? [configured, ...DEFAULT_STORAGE_PATHS]
14
+ : DEFAULT_STORAGE_PATHS;
15
+ return [...new Set(paths.map((path) => path.replace(/\/+$/, '')).filter(Boolean))];
16
+ }
10
17
  /** A resolved key is displayable as-is when it's already a URL or inline data. */
11
18
  function isDisplayableUrl(value) {
12
19
  return (value.startsWith('http://') ||
@@ -33,17 +40,26 @@ async function resolvePresignedUrl(key) {
33
40
  // caller at least doesn't crash. It won't load, but nothing throws.
34
41
  return key;
35
42
  }
36
- const response = await fetch(`${HOST_API_BASE_URL}${EXAM_STORAGE_PATH}/${encodeURIComponent(key)}`, { credentials: 'include' });
37
- if (!response.ok) {
38
- throw new Error(`Failed to resolve file URL (${response.status})`);
39
- }
40
- const body = (await response.json());
41
- const resolved = 'data' in body ? body.data?.url : body.url;
42
- if (!resolved) {
43
- throw new Error('Presigned URL missing in response');
43
+ let lastError = null;
44
+ for (const storagePath of getStoragePaths()) {
45
+ try {
46
+ const response = await fetch(`${HOST_API_BASE_URL}${storagePath}/${encodeURIComponent(key)}`, { credentials: 'include' });
47
+ if (!response.ok) {
48
+ throw new Error(`Failed to resolve file URL (${response.status})`);
49
+ }
50
+ const body = (await response.json());
51
+ const resolved = 'data' in body ? body.data?.url : body.url;
52
+ if (!resolved) {
53
+ throw new Error('Presigned URL missing in response');
54
+ }
55
+ urlCache.set(key, resolved);
56
+ return resolved;
57
+ }
58
+ catch (error) {
59
+ lastError = error instanceof Error ? error : new Error(String(error));
60
+ }
44
61
  }
45
- urlCache.set(key, resolved);
46
- return resolved;
62
+ throw lastError ?? new Error('Failed to resolve file URL');
47
63
  })().finally(() => {
48
64
  inFlight.delete(key);
49
65
  });
@@ -56,7 +72,9 @@ async function resolvePresignedUrl(key) {
56
72
  * - Empty/undefined → empty string.
57
73
  * - Already a URL (http/https/blob/data) → returned unchanged.
58
74
  * - An S3 storage key (e.g. `exams/:id/audio/x.wav`) → resolved to a presigned
59
- * URL via the host API (`/api/v1/exam-storage/:key`).
75
+ * URL via the host API. Tries `/api/v1/exam-storage/:key` (oe-class proxy)
76
+ * then `/api/v1/storage/:key` (oe-exam-api). Override with
77
+ * `NEXT_PUBLIC_EXAM_STORAGE_PATH`.
60
78
  */
61
79
  export function usePresignedFileUrl(fileKey) {
62
80
  const safeKey = typeof fileKey === 'string' ? fileKey : '';
@@ -145,6 +145,23 @@ const reverseGroupPayload = (apiQuestion) => {
145
145
  },
146
146
  };
147
147
  };
148
+ function normalizeChooseAnswerGroupImageUrl(value) {
149
+ if (typeof value === 'string') {
150
+ const trimmed = value.trim();
151
+ return trimmed !== '' ? trimmed : undefined;
152
+ }
153
+ if (!Array.isArray(value)) {
154
+ return undefined;
155
+ }
156
+ const urls = value
157
+ .filter((url) => typeof url === 'string')
158
+ .map((url) => url.trim())
159
+ .filter((url) => url !== '');
160
+ if (urls.length === 0) {
161
+ return undefined;
162
+ }
163
+ return urls.length === 1 ? urls[0] : urls;
164
+ }
148
165
  const reverseChooseTheCorrectAnswerGroup = (apiQuestion) => {
149
166
  const content = asRecord(apiQuestion.content);
150
167
  const answer = readAnswer(apiQuestion);
@@ -155,12 +172,15 @@ const reverseChooseTheCorrectAnswerGroup = (apiQuestion) => {
155
172
  const itemAnswer = asRecord(item.correctAnswer);
156
173
  const options = asArray(itemContent.options).map((rawOption, optionIndex) => {
157
174
  const option = asRecord(rawOption);
175
+ const imageUrl = asString(option.imageUrl);
158
176
  return {
159
177
  id: asString(option.id, `opt-${optionIndex + 1}`),
160
178
  text: asString(option.text),
179
+ ...(imageUrl ? { imageUrl } : {}),
161
180
  };
162
181
  });
163
182
  const isExample = item.isExample === true;
183
+ const imageUrl = normalizeChooseAnswerGroupImageUrl(itemContent.imageUrl);
164
184
  return {
165
185
  question: asString(itemContent.question),
166
186
  optionType: asString(itemContent.optionType, 'text') === 'image' ? 'image' : 'text',
@@ -173,6 +193,7 @@ const reverseChooseTheCorrectAnswerGroup = (apiQuestion) => {
173
193
  isExample,
174
194
  points: isExample ? 0 : asNumber(item.points, 1),
175
195
  questionNumber: asNumber(item.questionNumber, index + 1),
196
+ ...(imageUrl !== undefined ? { imageUrl } : {}),
176
197
  };
177
198
  });
178
199
  return {
@@ -256,6 +277,37 @@ const reverseTrueFalseGroup = (apiQuestion) => {
256
277
  },
257
278
  };
258
279
  };
280
+ const reverseTrueFalseCorrectGroup = (apiQuestion) => {
281
+ const content = asRecord(apiQuestion.content);
282
+ const answer = readAnswer(apiQuestion);
283
+ const fallbackAnswers = asArray(answer.answer);
284
+ const items = asArray(content.items).map((rawItem, index) => {
285
+ const item = asRecord(rawItem);
286
+ const itemContent = asRecord(item.content);
287
+ const itemAnswer = asRecord(asRecord(item.correctAnswer).answer);
288
+ const fallback = asRecord(fallbackAnswers[index]);
289
+ const rawAnswer = Object.keys(itemAnswer).length > 0 ? itemAnswer : fallback;
290
+ const isExample = item.isExample === true;
291
+ const isTrue = rawAnswer.isTrue === true;
292
+ return {
293
+ question: asString(itemContent.question),
294
+ isTrue,
295
+ correction: isTrue ? '' : asString(rawAnswer.correction),
296
+ isExample,
297
+ points: isExample ? 0 : asNumber(item.points, 1),
298
+ questionNumber: asNumber(item.questionNumber, index + 1),
299
+ };
300
+ });
301
+ return {
302
+ content: '',
303
+ answer: {
304
+ instruction: asString(asRecord(content.meta).instruction, 'Tick (✓) the true and cross (✗) the false sentences. Correct the false sentences.'),
305
+ items,
306
+ explanation: asString(apiQuestion.explanation),
307
+ points: items.reduce((total, item) => (item.isExample ? total : total + item.points), 0),
308
+ },
309
+ };
310
+ };
259
311
  const reverseCrossOutWordGroup = (apiQuestion) => {
260
312
  const content = asRecord(apiQuestion.content);
261
313
  const answer = readAnswer(apiQuestion);
@@ -407,6 +459,7 @@ const handlers = {
407
459
  CHOOSE_THE_CORRECT_ANSWER_GROUP: reverseChooseTheCorrectAnswerGroup,
408
460
  ANSWER_THE_QUESTION_GROUP: reverseAnswerTheQuestionGroup,
409
461
  TRUE_FALSE_GROUP: reverseTrueFalseGroup,
462
+ TRUE_FALSE_CORRECT_GROUP: reverseTrueFalseCorrectGroup,
410
463
  CROSS_OUT_WORD_GROUP: reverseCrossOutWordGroup,
411
464
  WORD_ORDER_AND_MATCH_GROUP: reverseWordOrderAndMatchGroup,
412
465
  WORD_ORDER_AND_MATCH: reverseWordOrderAndMatchGroup,
@@ -424,8 +477,9 @@ const handlers = {
424
477
  WORD_ORDERING: reverseSimpleAnswer,
425
478
  WORD_FILL_PARAGRAPH: reverseSimpleAnswer,
426
479
  MATCH_BY_WRITING_ANSWER: reverseGroupPayload,
427
- WRITE_SENTENCES: reverseGroupPayload,
428
- WRITE_A_SHORT_LETTER: reverseSimpleAnswer,
480
+ MATCH_WORD_TO_PICTURE: reverseGroupPayload,
481
+ WRITE_SHORT_PARAGRAPH: reverseWriteShortParagraph,
482
+ WRITE_A_SHORT_LETTER: reverseWriteAShortLetter,
429
483
  WRITE_CORRECT_VERB_FORM: reverseSimpleAnswer,
430
484
  CHOOSE_CORRECT_ADJECTIVE: reverseSimpleAnswer,
431
485
  WORD_FILL_STRUCTURED_FORM: reverseGroupPayload,
@@ -449,6 +503,61 @@ const handlers = {
449
503
  CROSSWORD_PUZZLE: reverseCrosswordPuzzle,
450
504
  FIND_WORDS_IN_MATRIX: reverseFindWordsInMatrix,
451
505
  };
506
+ function reverseWriteAShortLetter(apiQuestion) {
507
+ const content = asRecord(apiQuestion.content);
508
+ const answer = readAnswer(apiQuestion);
509
+ const informations = asArray(content.informations ?? answer.informations).map((item) => asString(item)).filter(Boolean);
510
+ const guideImages = asArray(content.guideImages ?? answer.guideImages).map((item) => asString(item)).filter(Boolean);
511
+ const passage = asString(content.passage ?? answer.passage);
512
+ const contexts = asArray(content.contexts ?? answer.contexts);
513
+ const instructions = isRecord(content.instructions)
514
+ ? content.instructions
515
+ : isRecord(answer.instructions)
516
+ ? answer.instructions
517
+ : { mainInstruction: asString(content.instruction ?? content.prompt ?? answer.instruction ?? answer.prompt) };
518
+ const minWords = asNumber(content.minWords ?? answer.minWords, 20);
519
+ const maxWords = content.maxWords !== undefined && content.maxWords !== null
520
+ ? asNumber(content.maxWords, 0) || null
521
+ : answer.maxWords !== undefined && answer.maxWords !== null
522
+ ? asNumber(answer.maxWords, 0) || null
523
+ : null;
524
+ const resolved = {
525
+ instructions,
526
+ instruction: asString(instructions.mainInstruction || instructions.instruction),
527
+ informations,
528
+ guideImages,
529
+ ...(passage ? { passage } : {}),
530
+ contexts,
531
+ minWords,
532
+ maxWords,
533
+ };
534
+ return {
535
+ content: resolved,
536
+ answer: resolved,
537
+ };
538
+ }
539
+ function reverseWriteShortParagraph(apiQuestion) {
540
+ const content = asRecord(apiQuestion.content);
541
+ const answer = readAnswer(apiQuestion);
542
+ const contexts = asArray(content.contexts ?? answer.contexts);
543
+ const instruction = asString(content.instruction ?? content.instructions?.mainInstruction ?? answer.instruction);
544
+ const minWords = asNumber(content.minWords ?? answer.minWords, 20);
545
+ const maxWords = content.maxWords !== undefined && content.maxWords !== null
546
+ ? asNumber(content.maxWords, 0) || null
547
+ : answer.maxWords !== undefined && answer.maxWords !== null
548
+ ? asNumber(answer.maxWords, 0) || null
549
+ : null;
550
+ const resolved = {
551
+ instruction,
552
+ contexts,
553
+ minWords,
554
+ maxWords,
555
+ };
556
+ return {
557
+ content: resolved,
558
+ answer: resolved,
559
+ };
560
+ }
452
561
  function reverseCrosswordPuzzle(apiQuestion) {
453
562
  const apiContent = (apiQuestion.content || {});
454
563
  const apiAnswer = (apiQuestion.correctAnswer || apiQuestion.correct_answer || apiQuestion.answer || {});
@@ -13,6 +13,7 @@ import { transformLabelThePicture } from '../../../components/questions/types/la
13
13
  import { transformChooseTheCorrectAnswer } from '../../../components/questions/types/choose-the-correct-answer/transform';
14
14
  import { transformChooseTheCorrectAnswerGroup } from '../../../components/questions/types/choose-the-correct-answer-group/transform';
15
15
  import { transformCrossOutWordGroup } from '../../../components/questions/types/cross-out-word-group/transform';
16
+ import { transformTrueFalseCorrectGroup } from '../../../components/questions/types/true-false-correct-group/transform';
16
17
  import { transformFillInBlank } from '../../../components/questions/types/fill-in-blank/transform';
17
18
  import { transformWriteCorrectVerbForm } from '../../../components/questions/types/write-correct-verb-form/transform';
18
19
  import { transformChooseCorrectAdjective } from '../../../components/questions/types/choose-correct-adjective/transform';
@@ -22,6 +23,7 @@ import { transformTrueFalse } from '../../../components/questions/types/true-fal
22
23
  import { transformWordOrdering } from '../../../components/questions/types/word-ordering/transform';
23
24
  import { transformWordFillParagraph } from '../../../components/questions/types/word-fill-paragraph/transform';
24
25
  import { transformMatchByWritingAnswer } from '../../../components/questions/types/match-by-writing-answer/transform';
26
+ import { transformMatchWordToPicture } from '../../../components/questions/types/match-word-to-picture/transform';
25
27
  import { transformWriteSentences } from '../../../components/questions/types/write-sentences/transform';
26
28
  import { transformListenAndChooseFromAnswerGroup } from '../../../components/questions/types/listen-and-choose-from-answer-group/transform';
27
29
  import { transformListenAndChooseObjectsInScene } from '../../../components/questions/types/listen-and-choose-objects-in-scene/transform';
@@ -62,6 +64,7 @@ const questionTransformHandlers = {
62
64
  CHOOSE_THE_CORRECT_ANSWER: transformChooseTheCorrectAnswer,
63
65
  CHOOSE_THE_CORRECT_ANSWER_GROUP: transformChooseTheCorrectAnswerGroup,
64
66
  CROSS_OUT_WORD_GROUP: transformCrossOutWordGroup,
67
+ TRUE_FALSE_CORRECT_GROUP: transformTrueFalseCorrectGroup,
65
68
  FILL_IN_BLANK: transformFillInBlank,
66
69
  WRITE_CORRECT_VERB_FORM: transformWriteCorrectVerbForm,
67
70
  CHOOSE_CORRECT_ADJECTIVE: transformChooseCorrectAdjective,
@@ -71,6 +74,7 @@ const questionTransformHandlers = {
71
74
  WORD_ORDERING: transformWordOrdering,
72
75
  WORD_FILL_PARAGRAPH: transformWordFillParagraph,
73
76
  MATCH_BY_WRITING_ANSWER: transformMatchByWritingAnswer,
77
+ MATCH_WORD_TO_PICTURE: transformMatchWordToPicture,
74
78
  WRITE_SENTENCES: transformWriteSentences,
75
79
  LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP: transformListenAndChooseFromAnswerGroup,
76
80
  LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE: transformListenAndChooseObjectsInScene,
@@ -29,7 +29,7 @@ export declare enum StudentSelectionType {
29
29
  }
30
30
  export type ResultStatus = 'PENDING' | 'PARTIAL_GRADED' | 'GRADED';
31
31
  export type StudentStatus = 'NOT_STARTED' | 'IN_PROGRESS' | 'SUBMITTED';
32
- export type QuestionType = 'FILL_IN_BLANK' | 'TRUE_FALSE' | 'MATCHING' | 'ORDERING' | 'LISTENING' | 'COLORING' | 'ESSAY' | 'LISTEN_AND_FILL_IN_THE_BLANK_WITH_IMAGE' | 'LISTEN_AND_TICK_ANSWER' | 'LISTEN_AND_FILL_NAME_NUMBER' | 'LISTENING_FILL_BLANK' | 'LISTENING_COLOR' | 'TICK_TRUE_OR_FALSE' | 'TICK_YES_OR_NO' | 'READING_TICK_CROSS' | 'ARRANGE_LETTERS_INTO_WORDS' | 'READ_PASSAGE_AND_COMPLETE_STORY' | 'CLOZE_TEST_WITH_TICK_BOX' | 'READ_AND_CHOOSE_APPROPRIATE_WORD' | 'WRITE_SHORT_PARAGRAPH' | 'LABEL_THE_PICTURE' | 'READING_LETTER_ARRANGE' | 'READING_PASSAGE_FILL' | 'FILL_BLANK' | 'FILL_MISSING_WORDS_IN_GRID' | 'LOOK_PICTURE_CHOOSE_CORRECT_ANSWER' | 'LOOK_PICTURE_FILL_BLANK_CHOOSE_ANSWER' | 'LOOK_PICTURE_FILL_WORD_HINT' | 'LABEL_THE_PICTURE' | 'READ_AND_COLOR_OBJECTS' | 'IMAGE_OBJECT_MATCHING' | 'READ_PASSAGE_AND_ANSWER_QUESTIONS' | 'CHOOSE_THE_CORRECT_ANSWER' | 'CHOOSE_THE_CORRECT_ANSWER_GROUP' | 'TRUE_FALSE_GROUP' | 'CROSS_OUT_WORD_GROUP' | 'SPONTANEOUS_QA_GROUP' | 'WORD_ORDER_AND_MATCH_GROUP' | 'WRITE_A_SHORT_LETTER' | 'WRITE_CORRECT_VERB_FORM' | 'CHOOSE_CORRECT_ADJECTIVE' | 'ANSWER_THE_QUESTION' | 'ANSWER_THE_QUESTION_GROUP' | 'WORD_ORDERING' | 'WORD_FILL_PARAGRAPH' | 'MATCH_BY_WRITING_ANSWER' | 'WRITE_SENTENCES' | 'LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP' | 'LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE' | 'LISTEN_AND_DRAG_OBJECTS_INTO_SCENE' | 'LISTEN_AND_SPEAK_ANSWER' | 'LISTEN_AND_SPEAK_IMAGE_GROUP' | 'LISTEN_AND_SPEAK_QUESTION_LIST' | 'LISTEN_AND_SPEAK_COMPARE_IMAGES' | 'LISTEN_AND_SPEAK_WITH_STORY_IMAGES' | 'LISTEN_AND_SPEAK_ODD_ONE_OUT' | 'LISTEN_AND_SPEAK_INFO_EXCHANGE' | 'READ_DISPLAYED_CONTENT' | 'SPEAKING_DESCRIBE_IMAGE' | 'SPONTANEOUS_QA' | 'SPEAKING_CONVERSATION' | 'GN_SPEAKING_INTERVIEW' | 'SPEAKING_CUE_CARD' | 'WORD_FILL_STRUCTURED_FORM' | 'CROSSWORD_PUZZLE' | 'FIND_WORDS_IN_MATRIX';
32
+ export type QuestionType = 'FILL_IN_BLANK' | 'TRUE_FALSE' | 'MATCHING' | 'ORDERING' | 'LISTENING' | 'COLORING' | 'ESSAY' | 'LISTEN_AND_FILL_IN_THE_BLANK_WITH_IMAGE' | 'LISTEN_AND_TICK_ANSWER' | 'LISTEN_AND_FILL_NAME_NUMBER' | 'LISTENING_FILL_BLANK' | 'LISTENING_COLOR' | 'TICK_TRUE_OR_FALSE' | 'TICK_YES_OR_NO' | 'READING_TICK_CROSS' | 'ARRANGE_LETTERS_INTO_WORDS' | 'READ_PASSAGE_AND_COMPLETE_STORY' | 'CLOZE_TEST_WITH_TICK_BOX' | 'READ_AND_CHOOSE_APPROPRIATE_WORD' | 'WRITE_SHORT_PARAGRAPH' | 'LABEL_THE_PICTURE' | 'READING_LETTER_ARRANGE' | 'READING_PASSAGE_FILL' | 'FILL_BLANK' | 'FILL_MISSING_WORDS_IN_GRID' | 'LOOK_PICTURE_CHOOSE_CORRECT_ANSWER' | 'LOOK_PICTURE_FILL_BLANK_CHOOSE_ANSWER' | 'LOOK_PICTURE_FILL_WORD_HINT' | 'LABEL_THE_PICTURE' | 'READ_AND_COLOR_OBJECTS' | 'IMAGE_OBJECT_MATCHING' | 'READ_PASSAGE_AND_ANSWER_QUESTIONS' | 'CHOOSE_THE_CORRECT_ANSWER' | 'CHOOSE_THE_CORRECT_ANSWER_GROUP' | 'TRUE_FALSE_GROUP' | 'TRUE_FALSE_CORRECT_GROUP' | 'CROSS_OUT_WORD_GROUP' | 'SPONTANEOUS_QA_GROUP' | 'WORD_ORDER_AND_MATCH_GROUP' | 'WRITE_A_SHORT_LETTER' | 'WRITE_CORRECT_VERB_FORM' | 'CHOOSE_CORRECT_ADJECTIVE' | 'ANSWER_THE_QUESTION' | 'ANSWER_THE_QUESTION_GROUP' | 'WORD_ORDERING' | 'WORD_FILL_PARAGRAPH' | 'MATCH_BY_WRITING_ANSWER' | 'MATCH_WORD_TO_PICTURE' | 'WRITE_SENTENCES' | 'LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP' | 'LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE' | 'LISTEN_AND_DRAG_OBJECTS_INTO_SCENE' | 'LISTEN_AND_SPEAK_ANSWER' | 'LISTEN_AND_SPEAK_IMAGE_GROUP' | 'LISTEN_AND_SPEAK_QUESTION_LIST' | 'LISTEN_AND_SPEAK_COMPARE_IMAGES' | 'LISTEN_AND_SPEAK_WITH_STORY_IMAGES' | 'LISTEN_AND_SPEAK_ODD_ONE_OUT' | 'LISTEN_AND_SPEAK_INFO_EXCHANGE' | 'READ_DISPLAYED_CONTENT' | 'SPEAKING_DESCRIBE_IMAGE' | 'SPONTANEOUS_QA' | 'SPEAKING_CONVERSATION' | 'GN_SPEAKING_INTERVIEW' | 'SPEAKING_CUE_CARD' | 'WORD_FILL_STRUCTURED_FORM' | 'CROSSWORD_PUZZLE' | 'FIND_WORDS_IN_MATRIX';
33
33
  export type Level = 'Pre-A1' | 'A1' | 'A2' | 'B1' | 'B2' | 'C1' | 'C2';
34
34
  export type Difficulty = 'EASY' | 'MEDIUM' | 'HARD';
35
35
  export type Skill = 'LISTENING' | 'READING' | 'WRITING' | 'SPEAKING';
@@ -7,6 +7,7 @@ export interface ChooseTheCorrectAnswerGroupItemContent {
7
7
  question: string;
8
8
  options: ChooseTheCorrectAnswerGroupOption[];
9
9
  optionType?: 'text' | 'image';
10
+ imageUrl?: string | string[];
10
11
  }
11
12
  export interface ChooseTheCorrectAnswerGroupItem {
12
13
  questionType?: 'CHOOSE_THE_CORRECT_ANSWER';
@@ -32,3 +32,5 @@ export * from './spontaneous-qa';
32
32
  export * from './word-order-and-match-group';
33
33
  export * from './crossword-puzzle';
34
34
  export * from './find-words-in-matrix';
35
+ export * from './match-word-to-picture';
36
+ export * from './true-false-correct-group';
@@ -50,3 +50,7 @@ export * from './word-order-and-match-group';
50
50
  export * from './crossword-puzzle';
51
51
  // FIND_WORDS_IN_MATRIX
52
52
  export * from './find-words-in-matrix';
53
+ // MATCH_WORD_TO_PICTURE
54
+ export * from './match-word-to-picture';
55
+ // TRUE_FALSE_CORRECT_GROUP
56
+ export * from './true-false-correct-group';
@@ -0,0 +1,35 @@
1
+ /**
2
+ * API Types for MATCH_WORD_TO_PICTURE question type
3
+ * Reference: .agents/templates/MATCH_WORD_TO_PICTURE.sample.json
4
+ */
5
+ import { QuestionBankSettings } from './choose-the-correct-answer';
6
+ export interface MatchWordToPictureContent {
7
+ imageUrl: string;
8
+ }
9
+ export interface MatchWordToPictureCorrectAnswer {
10
+ answer: string;
11
+ }
12
+ export interface MatchWordToPictureGroupPayload {
13
+ wordBank: string[];
14
+ instruction?: string;
15
+ }
16
+ export interface MatchWordToPictureAPIPayload {
17
+ id?: string;
18
+ questionType: 'MATCH_WORD_TO_PICTURE';
19
+ content: MatchWordToPictureContent;
20
+ correctAnswer: MatchWordToPictureCorrectAnswer;
21
+ isGroup: true;
22
+ isExample?: boolean;
23
+ questionGroup: {
24
+ groupNumber: number;
25
+ payload: MatchWordToPictureGroupPayload;
26
+ };
27
+ points: number;
28
+ explanation?: string;
29
+ questionNumber: number;
30
+ partId?: string;
31
+ partNo?: number;
32
+ addToQuestionBank?: boolean;
33
+ questionBank?: QuestionBankSettings;
34
+ }
35
+ export type MatchWordToPictureLocalStorage = Omit<MatchWordToPictureAPIPayload, 'addToQuestionBank' | 'questionBank'>;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * API Types for MATCH_WORD_TO_PICTURE question type
3
+ * Reference: .agents/templates/MATCH_WORD_TO_PICTURE.sample.json
4
+ */
5
+ export {};
@@ -0,0 +1,39 @@
1
+ export declare const TRUE_FALSE_CORRECT_ITEM_TYPE: "TRUE_FALSE_CORRECT";
2
+ export interface TrueFalseCorrectItemAnswer {
3
+ isTrue: boolean;
4
+ correction: string;
5
+ }
6
+ export interface TrueFalseCorrectGroupItemContent {
7
+ question: string;
8
+ }
9
+ export interface TrueFalseCorrectGroupItem {
10
+ questionType?: typeof TRUE_FALSE_CORRECT_ITEM_TYPE;
11
+ isExample?: boolean;
12
+ content: TrueFalseCorrectGroupItemContent;
13
+ correctAnswer?: {
14
+ answer: TrueFalseCorrectItemAnswer;
15
+ };
16
+ points?: number;
17
+ questionNumber?: number;
18
+ }
19
+ export interface TrueFalseCorrectGroupContent {
20
+ meta?: {
21
+ instruction?: string;
22
+ };
23
+ items: TrueFalseCorrectGroupItem[];
24
+ }
25
+ export interface TrueFalseCorrectGroupCorrectAnswer {
26
+ answer: TrueFalseCorrectItemAnswer[];
27
+ }
28
+ export interface TrueFalseCorrectGroupAPIPayload {
29
+ id?: string;
30
+ questionType: 'TRUE_FALSE_CORRECT_GROUP';
31
+ content: TrueFalseCorrectGroupContent;
32
+ correctAnswer: TrueFalseCorrectGroupCorrectAnswer;
33
+ points: number;
34
+ explanation?: string;
35
+ questionNumber: number;
36
+ partId?: string;
37
+ partNo?: number;
38
+ }
39
+ export type TrueFalseCorrectGroupLocalStorage = TrueFalseCorrectGroupAPIPayload;