@tinyweb_dev/oe-exam-sdk 0.2.4 → 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 (104) 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 +11 -1
  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/ChooseTheCorrectAnswerGroupViewer.d.ts +2 -1
  75. package/dist/components/questions/viewer/ChooseTheCorrectAnswerGroupViewer.js +2 -2
  76. package/dist/components/questions/viewer/MatchWordToPictureGroupViewer.d.ts +9 -0
  77. package/dist/components/questions/viewer/MatchWordToPictureGroupViewer.js +11 -0
  78. package/dist/components/questions/viewer/MatchWordToPictureViewer.d.ts +9 -0
  79. package/dist/components/questions/viewer/MatchWordToPictureViewer.js +8 -0
  80. package/dist/components/questions/viewer/QuestionViewer.js +18 -3
  81. package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.d.ts +9 -0
  82. package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.js +14 -0
  83. package/dist/components/questions/viewer/index.d.ts +3 -0
  84. package/dist/components/questions/viewer/index.js +3 -0
  85. package/dist/components/results/renderers/ReviewChooseBestAnswerRenderer.js +4 -9
  86. package/dist/components/results/renderers/review-question-body-movers.js +39 -3
  87. package/dist/components/results/review-data.js +22 -0
  88. package/dist/components/themes/english-certification/EcWritingTask.js +2 -2
  89. package/dist/index.d.ts +1 -1
  90. package/dist/index.js +1 -1
  91. package/dist/shared/constants/question-skills.js +4 -0
  92. package/dist/shared/lib/hooks/usePresignedFileUrl.d.ts +3 -1
  93. package/dist/shared/lib/hooks/usePresignedFileUrl.js +30 -12
  94. package/dist/shared/lib/utils/question-reverse-transform.js +56 -0
  95. package/dist/shared/lib/utils/question-transform.js +4 -0
  96. package/dist/shared/types/common.types.d.ts +1 -1
  97. package/dist/shared/types/questions/choose-the-correct-answer-group.d.ts +1 -0
  98. package/dist/shared/types/questions/index.d.ts +2 -0
  99. package/dist/shared/types/questions/index.js +4 -0
  100. package/dist/shared/types/questions/match-word-to-picture.d.ts +35 -0
  101. package/dist/shared/types/questions/match-word-to-picture.js +5 -0
  102. package/dist/shared/types/questions/true-false-correct-group.d.ts +39 -0
  103. package/dist/shared/types/questions/true-false-correct-group.js +1 -0
  104. package/package.json +7 -2
@@ -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, AnswerTheQuestionGroupViewer, } 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, AnswerTheQuestionGroupViewer, } 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,6 +477,7 @@ const handlers = {
424
477
  WORD_ORDERING: reverseSimpleAnswer,
425
478
  WORD_FILL_PARAGRAPH: reverseSimpleAnswer,
426
479
  MATCH_BY_WRITING_ANSWER: reverseGroupPayload,
480
+ MATCH_WORD_TO_PICTURE: reverseGroupPayload,
427
481
  WRITE_SHORT_PARAGRAPH: reverseWriteShortParagraph,
428
482
  WRITE_A_SHORT_LETTER: reverseWriteAShortLetter,
429
483
  WRITE_CORRECT_VERB_FORM: reverseSimpleAnswer,
@@ -454,6 +508,7 @@ function reverseWriteAShortLetter(apiQuestion) {
454
508
  const answer = readAnswer(apiQuestion);
455
509
  const informations = asArray(content.informations ?? answer.informations).map((item) => asString(item)).filter(Boolean);
456
510
  const guideImages = asArray(content.guideImages ?? answer.guideImages).map((item) => asString(item)).filter(Boolean);
511
+ const passage = asString(content.passage ?? answer.passage);
457
512
  const contexts = asArray(content.contexts ?? answer.contexts);
458
513
  const instructions = isRecord(content.instructions)
459
514
  ? content.instructions
@@ -471,6 +526,7 @@ function reverseWriteAShortLetter(apiQuestion) {
471
526
  instruction: asString(instructions.mainInstruction || instructions.instruction),
472
527
  informations,
473
528
  guideImages,
529
+ ...(passage ? { passage } : {}),
474
530
  contexts,
475
531
  minWords,
476
532
  maxWords,
@@ -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;
@@ -0,0 +1 @@
1
+ export const TRUE_FALSE_CORRECT_ITEM_TYPE = 'TRUE_FALSE_CORRECT';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tinyweb_dev/oe-exam-sdk",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Reusable OceanEdu question components.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -22,7 +22,7 @@
22
22
  "build:alias": "tsc-alias -p tsconfig.build.json",
23
23
  "build:check-aliases": "node ./scripts/check-aliases.mjs",
24
24
  "build:ts": "npm run build:compile && npm run build:assets && npm run build:alias && npm run build:check-aliases",
25
- "build:assets": "for d in cambridge-yle english-certification summer-sky; do [ -d src/components/themes/$d/assets ] && cp -R src/components/themes/$d/assets dist/components/themes/$d/ || true; done; [ -d src/components/shared/audio/assets ] && cp -R src/components/shared/audio/assets dist/components/shared/audio/ || true; [ -d src/components/results/report/assets ] && mkdir -p dist/components/results/report && cp -R src/components/results/report/assets dist/components/results/report/ || true; (cd src && find . -name '*.module.css' -exec cp --parents {} ../dist/ \\;); cp src/global.d.ts dist/global.d.ts",
25
+ "build:assets": "node ./scripts/copy-assets.mjs",
26
26
  "minify:js": "find dist -name '*.js' -type f -exec sh -c 'for f do terser \"$f\" --compress directives=false --mangle --module --comments false --output \"$f\"; done' sh {} +",
27
27
  "build": "npm run clean && npm run build:ts",
28
28
  "sync:fe": "npm run build && node ./scripts/sync-fe.mjs",
@@ -81,6 +81,11 @@
81
81
  "import": "./dist/components/questions/index.js",
82
82
  "default": "./dist/components/questions/index.js"
83
83
  },
84
+ "./components/questions/viewer": {
85
+ "types": "./dist/components/questions/viewer/index.d.ts",
86
+ "import": "./dist/components/questions/viewer/index.js",
87
+ "default": "./dist/components/questions/viewer/index.js"
88
+ },
84
89
  "./components/themes": {
85
90
  "types": "./dist/components/themes/index.d.ts",
86
91
  "import": "./dist/components/themes/index.js",