@tinyweb_dev/oe-exam-sdk 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/dist/components/exams/take/ExamTakingPageContainer.js +6 -0
  2. package/dist/components/exams/take/components/QuestionRenderer.js +19 -3
  3. package/dist/components/exams/take/components/question-renderers/ChooseBestAnswerImages.d.ts +10 -0
  4. package/dist/components/exams/take/components/question-renderers/ChooseBestAnswerImages.js +38 -0
  5. package/dist/components/exams/take/components/question-renderers/MoversChooseBestAnswerRenderer.js +5 -11
  6. package/dist/components/exams/take/components/question-renderers/MoversFillInBlankGroupRenderer.d.ts +22 -0
  7. package/dist/components/exams/take/components/question-renderers/MoversFillInBlankGroupRenderer.js +60 -0
  8. package/dist/components/exams/take/components/question-renderers/MoversMatchWordToPictureRenderer.d.ts +16 -0
  9. package/dist/components/exams/take/components/question-renderers/MoversMatchWordToPictureRenderer.js +75 -0
  10. package/dist/components/exams/take/components/question-renderers/MoversMatchingWithLinesGroupRenderer.d.ts +13 -0
  11. package/dist/components/exams/take/components/question-renderers/MoversMatchingWithLinesGroupRenderer.js +31 -0
  12. package/dist/components/exams/take/components/question-renderers/MoversTrueFalseCorrectGroupRenderer.d.ts +18 -0
  13. package/dist/components/exams/take/components/question-renderers/MoversTrueFalseCorrectGroupRenderer.js +92 -0
  14. package/dist/components/exams/take/components/question-renderers/MoversWritingRenderer.d.ts +2 -1
  15. package/dist/components/exams/take/components/question-renderers/MoversWritingRenderer.js +2 -2
  16. package/dist/components/exams/take/components/question-renderers/index.d.ts +4 -0
  17. package/dist/components/exams/take/components/question-renderers/index.js +4 -0
  18. package/dist/components/exams/take/exam-taking.utils.d.ts +1 -0
  19. package/dist/components/exams/take/exam-taking.utils.js +9 -1
  20. package/dist/components/exams/take/types.d.ts +3 -2
  21. package/dist/components/exams/take/utils/answer-transformers.js +28 -0
  22. package/dist/components/exams/take/utils/question-transformers.d.ts +73 -1
  23. package/dist/components/exams/take/utils/question-transformers.js +188 -0
  24. package/dist/components/questions/_shared/config/question-types.config.js +24 -0
  25. package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.d.ts +9 -0
  26. package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.js +49 -1
  27. package/dist/components/questions/_shared/types/fill-in-blank-group.type.d.ts +50 -0
  28. package/dist/components/questions/_shared/types/fill-in-blank-group.type.js +2 -0
  29. package/dist/components/questions/_shared/types/index.d.ts +4 -0
  30. package/dist/components/questions/_shared/types/index.js +4 -0
  31. package/dist/components/questions/_shared/types/match-word-to-picture.type.d.ts +62 -0
  32. package/dist/components/questions/_shared/types/match-word-to-picture.type.js +1 -0
  33. package/dist/components/questions/_shared/types/matching-with-lines-group.type.d.ts +40 -0
  34. package/dist/components/questions/_shared/types/matching-with-lines-group.type.js +1 -0
  35. package/dist/components/questions/_shared/types/true-false-correct-group.type.d.ts +42 -0
  36. package/dist/components/questions/_shared/types/true-false-correct-group.type.js +2 -0
  37. package/dist/components/questions/_shared/types/write-a-short-letter.type.d.ts +4 -0
  38. package/dist/components/questions/components/QuestionSearchDropdown.js +6 -0
  39. package/dist/components/questions/components/QuestionTypeSelector.js +3 -0
  40. package/dist/components/questions/creator/question-type-registry.js +8 -0
  41. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseAnswerGroupImageUpload.d.ts +13 -0
  42. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseAnswerGroupImageUpload.js +32 -0
  43. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.d.ts +1 -1
  44. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +42 -6
  45. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupCreator.js +40 -20
  46. package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.d.ts +1 -1
  47. package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.js +5 -0
  48. package/dist/components/questions/types/choose-the-correct-answer-group/transform.js +4 -0
  49. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupClient.d.ts +2 -0
  50. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupClient.js +64 -0
  51. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupCreator.d.ts +2 -0
  52. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupCreator.js +158 -0
  53. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupMediaField.d.ts +7 -0
  54. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupMediaField.js +30 -0
  55. package/dist/components/questions/types/fill-in-blank-group/blank-utils.d.ts +20 -0
  56. package/dist/components/questions/types/fill-in-blank-group/blank-utils.js +99 -0
  57. package/dist/components/questions/types/fill-in-blank-group/index.d.ts +5 -0
  58. package/dist/components/questions/types/fill-in-blank-group/index.js +5 -0
  59. package/dist/components/questions/types/fill-in-blank-group/map-fill-in-blank-group-data.d.ts +4 -0
  60. package/dist/components/questions/types/fill-in-blank-group/map-fill-in-blank-group-data.js +102 -0
  61. package/dist/components/questions/types/fill-in-blank-group/register.d.ts +3 -0
  62. package/dist/components/questions/types/fill-in-blank-group/register.js +37 -0
  63. package/dist/components/questions/types/fill-in-blank-group/transform.d.ts +2 -0
  64. package/dist/components/questions/types/fill-in-blank-group/transform.js +68 -0
  65. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureClient.d.ts +2 -0
  66. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureClient.js +31 -0
  67. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureCreator.d.ts +2 -0
  68. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureCreator.js +267 -0
  69. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.d.ts +2 -0
  70. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.js +65 -0
  71. package/dist/components/questions/types/match-word-to-picture/WordBankEditor.d.ts +8 -0
  72. package/dist/components/questions/types/match-word-to-picture/WordBankEditor.js +15 -0
  73. package/dist/components/questions/types/match-word-to-picture/index.d.ts +6 -0
  74. package/dist/components/questions/types/match-word-to-picture/index.js +6 -0
  75. package/dist/components/questions/types/match-word-to-picture/map-match-word-to-picture-data.d.ts +2 -0
  76. package/dist/components/questions/types/match-word-to-picture/map-match-word-to-picture-data.js +53 -0
  77. package/dist/components/questions/types/match-word-to-picture/map-match-word-to-picture-group-data.d.ts +4 -0
  78. package/dist/components/questions/types/match-word-to-picture/map-match-word-to-picture-group-data.js +75 -0
  79. package/dist/components/questions/types/match-word-to-picture/register.d.ts +2 -0
  80. package/dist/components/questions/types/match-word-to-picture/register.js +55 -0
  81. package/dist/components/questions/types/match-word-to-picture/transform.d.ts +11 -0
  82. package/dist/components/questions/types/match-word-to-picture/transform.js +43 -0
  83. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesBoard.d.ts +20 -0
  84. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesBoard.js +109 -0
  85. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.d.ts +2 -0
  86. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.js +30 -0
  87. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupCreator.d.ts +2 -0
  88. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupCreator.js +180 -0
  89. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesImageField.d.ts +6 -0
  90. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesImageField.js +26 -0
  91. package/dist/components/questions/types/matching-with-lines-group/index.d.ts +4 -0
  92. package/dist/components/questions/types/matching-with-lines-group/index.js +4 -0
  93. package/dist/components/questions/types/matching-with-lines-group/map-matching-with-lines-group-data.d.ts +8 -0
  94. package/dist/components/questions/types/matching-with-lines-group/map-matching-with-lines-group-data.js +104 -0
  95. package/dist/components/questions/types/matching-with-lines-group/register.d.ts +3 -0
  96. package/dist/components/questions/types/matching-with-lines-group/register.js +37 -0
  97. package/dist/components/questions/types/matching-with-lines-group/transform.d.ts +2 -0
  98. package/dist/components/questions/types/matching-with-lines-group/transform.js +44 -0
  99. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.d.ts +2 -0
  100. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js +25 -0
  101. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupCreator.d.ts +2 -0
  102. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupCreator.js +144 -0
  103. package/dist/components/questions/types/true-false-correct-group/index.d.ts +5 -0
  104. package/dist/components/questions/types/true-false-correct-group/index.js +5 -0
  105. package/dist/components/questions/types/true-false-correct-group/map-true-false-correct-group-data.d.ts +4 -0
  106. package/dist/components/questions/types/true-false-correct-group/map-true-false-correct-group-data.js +79 -0
  107. package/dist/components/questions/types/true-false-correct-group/register.d.ts +2 -0
  108. package/dist/components/questions/types/true-false-correct-group/register.js +36 -0
  109. package/dist/components/questions/types/true-false-correct-group/transform.d.ts +2 -0
  110. package/dist/components/questions/types/true-false-correct-group/transform.js +49 -0
  111. package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterClient.js +11 -1
  112. package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterCreator.js +8 -2
  113. package/dist/components/questions/types/write-a-short-letter/register.js +4 -0
  114. package/dist/components/questions/types/write-a-short-letter/transform.js +4 -0
  115. package/dist/components/questions/viewer/ChooseTheCorrectAnswerGroupViewer.d.ts +2 -1
  116. package/dist/components/questions/viewer/ChooseTheCorrectAnswerGroupViewer.js +2 -2
  117. package/dist/components/questions/viewer/FillInBlankGroupViewer.d.ts +10 -0
  118. package/dist/components/questions/viewer/FillInBlankGroupViewer.js +14 -0
  119. package/dist/components/questions/viewer/MatchWordToPictureGroupViewer.d.ts +9 -0
  120. package/dist/components/questions/viewer/MatchWordToPictureGroupViewer.js +11 -0
  121. package/dist/components/questions/viewer/MatchWordToPictureViewer.d.ts +9 -0
  122. package/dist/components/questions/viewer/MatchWordToPictureViewer.js +8 -0
  123. package/dist/components/questions/viewer/MatchingWithLinesGroupViewer.d.ts +7 -0
  124. package/dist/components/questions/viewer/MatchingWithLinesGroupViewer.js +48 -0
  125. package/dist/components/questions/viewer/QuestionViewer.js +24 -3
  126. package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.d.ts +9 -0
  127. package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.js +14 -0
  128. package/dist/components/questions/viewer/index.d.ts +5 -0
  129. package/dist/components/questions/viewer/index.js +5 -0
  130. package/dist/components/results/renderers/ReviewChooseBestAnswerRenderer.js +4 -9
  131. package/dist/components/results/renderers/review-question-body-movers.js +74 -3
  132. package/dist/components/results/review-data.js +59 -0
  133. package/dist/components/themes/english-certification/EcWritingTask.js +2 -2
  134. package/dist/index.d.ts +1 -1
  135. package/dist/index.js +1 -1
  136. package/dist/shared/constants/question-skills.js +8 -0
  137. package/dist/shared/lib/hooks/usePresignedFileUrl.d.ts +3 -1
  138. package/dist/shared/lib/hooks/usePresignedFileUrl.js +30 -12
  139. package/dist/shared/lib/utils/question-reverse-transform.js +142 -0
  140. package/dist/shared/lib/utils/question-transform.js +8 -0
  141. package/dist/shared/types/common.types.d.ts +1 -1
  142. package/dist/shared/types/questions/choose-the-correct-answer-group.d.ts +1 -0
  143. package/dist/shared/types/questions/fill-in-blank-group.d.ts +45 -0
  144. package/dist/shared/types/questions/fill-in-blank-group.js +1 -0
  145. package/dist/shared/types/questions/index.d.ts +4 -0
  146. package/dist/shared/types/questions/index.js +8 -0
  147. package/dist/shared/types/questions/match-word-to-picture.d.ts +35 -0
  148. package/dist/shared/types/questions/match-word-to-picture.js +5 -0
  149. package/dist/shared/types/questions/matching-with-lines-group.d.ts +39 -0
  150. package/dist/shared/types/questions/matching-with-lines-group.js +1 -0
  151. package/dist/shared/types/questions/true-false-correct-group.d.ts +39 -0
  152. package/dist/shared/types/questions/true-false-correct-group.js +1 -0
  153. package/package.json +7 -2
@@ -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, FillInBlankGroupViewer, MatchingWithLinesGroupViewer, 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, FillInBlankGroupViewer, MatchingWithLinesGroupViewer, CrossOutWordGroupViewer, SpontaneousQaGroupViewer, WordOrderAndMatchGroupViewer, AnswerTheQuestionGroupViewer, MatchWordToPictureViewer, MatchWordToPictureGroupViewer, } from './components/questions/viewer';
@@ -45,6 +45,7 @@ export const QUESTION_TYPE_SKILLS = {
45
45
  'CHOOSE_THE_CORRECT_ANSWER_GROUP': SkillEnum.READING,
46
46
  'CHOOSE_CORRECT_ADJECTIVE': SkillEnum.READING,
47
47
  'FILL_IN_BLANK': SkillEnum.READING,
48
+ 'FILL_IN_BLANK_GROUP': SkillEnum.READING,
48
49
  'FILL_MISSING_WORDS_IN_GRID': SkillEnum.READING,
49
50
  'READ_PASSAGE_AND_ANSWER_QUESTIONS': SkillEnum.READING,
50
51
  'ANSWER_THE_QUESTION': SkillEnum.READING,
@@ -52,6 +53,8 @@ export const QUESTION_TYPE_SKILLS = {
52
53
  'WORD_ORDERING': SkillEnum.READING,
53
54
  'WORD_FILL_PARAGRAPH': SkillEnum.READING,
54
55
  'MATCH_BY_WRITING_ANSWER': SkillEnum.READING,
56
+ 'MATCH_WORD_TO_PICTURE': SkillEnum.READING,
57
+ 'MATCHING_WITH_LINES_GROUP': SkillEnum.READING,
55
58
  // Visual-based reading questions
56
59
  'LABEL_THE_PICTURE': SkillEnum.READING,
57
60
  'LOOK_PICTURE_CHOOSE_CORRECT_ANSWER': SkillEnum.READING,
@@ -62,6 +65,7 @@ export const QUESTION_TYPE_SKILLS = {
62
65
  // Other reading types
63
66
  'TRUE_FALSE': SkillEnum.READING,
64
67
  'TRUE_FALSE_GROUP': SkillEnum.READING,
68
+ 'TRUE_FALSE_CORRECT_GROUP': SkillEnum.READING,
65
69
  'CROSS_OUT_WORD_GROUP': SkillEnum.READING,
66
70
  'SPONTANEOUS_QA_GROUP': SkillEnum.SPEAKING,
67
71
  'WORD_ORDER_AND_MATCH_GROUP': SkillEnum.READING,
@@ -175,6 +179,7 @@ export const QUESTION_TYPE_LABELS = {
175
179
  'CHOOSE_THE_CORRECT_ANSWER_GROUP': 'Chọn đáp án đúng (nhóm)',
176
180
  'CHOOSE_CORRECT_ADJECTIVE': 'Chọn tính từ',
177
181
  'FILL_IN_BLANK': 'Điền vào chỗ trống',
182
+ 'FILL_IN_BLANK_GROUP': 'Điền chỗ trống (nhóm)',
178
183
  'FILL_MISSING_WORDS_IN_GRID': 'Điền từ vào lưới',
179
184
  'READ_PASSAGE_AND_ANSWER_QUESTIONS': 'Đọc hiểu - Trả lời',
180
185
  'LABEL_THE_PICTURE': 'Gắn nhãn hình ảnh',
@@ -185,6 +190,7 @@ export const QUESTION_TYPE_LABELS = {
185
190
  'IMAGE_OBJECT_MATCHING': 'Ghép hình ảnh - Đối tượng',
186
191
  'TRUE_FALSE': 'Đúng/Sai',
187
192
  'TRUE_FALSE_GROUP': 'Đúng/Sai (nhóm)',
193
+ 'TRUE_FALSE_CORRECT_GROUP': 'Đúng/Sai + sửa câu (nhóm)',
188
194
  'CROSS_OUT_WORD_GROUP': 'Gạch từ / sửa từ (nhóm)',
189
195
  'SPONTANEOUS_QA_GROUP': 'Hỏi đáp tự phát (nhóm)',
190
196
  'WORD_ORDER_AND_MATCH_GROUP': 'Sắp xếp từ và nối nhãn (nhóm)',
@@ -205,6 +211,8 @@ export const QUESTION_TYPE_LABELS = {
205
211
  'WORD_ORDERING': 'Sắp xếp từ',
206
212
  'WORD_FILL_PARAGRAPH': 'Điền từ vào đoạn văn',
207
213
  'MATCH_BY_WRITING_ANSWER': 'Ghép đáp án bằng cách viết',
214
+ 'MATCH_WORD_TO_PICTURE': 'Nối từ với tranh',
215
+ 'MATCHING_WITH_LINES_GROUP': 'Nối bằng đường kẻ (nhóm)',
208
216
  'WORD_FILL_STRUCTURED_FORM': 'Điền từ vào biểu mẫu có cấu trúc',
209
217
  'CROSSWORD_PUZZLE': 'Giải ô chữ (Crossword)',
210
218
  '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,121 @@ 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
+ };
311
+ const reverseFillInBlankGroup = (apiQuestion) => {
312
+ const content = asRecord(apiQuestion.content);
313
+ const answer = readAnswer(apiQuestion);
314
+ const fallbackAnswers = asArray(answer.answer);
315
+ const meta = asRecord(content.meta);
316
+ const items = asArray(content.items).map((rawItem, index) => {
317
+ const item = asRecord(rawItem);
318
+ const itemContent = asRecord(item.content);
319
+ const itemAnswer = asRecord(item.correctAnswer);
320
+ const fallback = asRecord(fallbackAnswers[index]);
321
+ const isExample = item.isExample === true;
322
+ const question = asString(itemContent.question, '{0}');
323
+ const rawAnswers = itemAnswer.answers ?? fallback.answers;
324
+ const answers = asArray(rawAnswers).map((blank) => {
325
+ const alts = Array.isArray(blank)
326
+ ? blank.map((alt) => String(alt ?? '').trim()).filter(Boolean)
327
+ : [String(blank ?? '').trim()].filter(Boolean);
328
+ return alts.length > 0 ? alts : [''];
329
+ });
330
+ return {
331
+ question,
332
+ answers: answers.length > 0 ? answers : [['']],
333
+ caseSensitive: itemAnswer.caseSensitive === true || fallback.caseSensitive === true,
334
+ imageUrl: asString(itemContent.imageUrl),
335
+ audioUrl: asString(itemContent.audioUrl),
336
+ showHints: itemContent.showHints === true,
337
+ hints: asArray(itemContent.hints).map((hint) => String(hint ?? '').trim()).filter(Boolean),
338
+ isExample,
339
+ points: isExample ? 0 : asNumber(item.points, 1),
340
+ questionNumber: asNumber(item.questionNumber, index + 1),
341
+ };
342
+ });
343
+ return {
344
+ content: '',
345
+ answer: {
346
+ instruction: asString(meta.instruction, 'Complete the sentences.'),
347
+ audioUrl: asString(meta.audioUrl),
348
+ imageUrl: asString(meta.imageUrl),
349
+ showHints: meta.showHints === true,
350
+ hints: asArray(meta.hints).map((hint) => String(hint ?? '').trim()).filter(Boolean),
351
+ items,
352
+ explanation: asString(apiQuestion.explanation),
353
+ points: items.reduce((total, item) => (item.isExample ? total : total + item.points), 0),
354
+ },
355
+ };
356
+ };
357
+ const reverseMatchingWithLinesGroup = (apiQuestion) => {
358
+ const content = asRecord(apiQuestion.content);
359
+ const answer = readAnswer(apiQuestion);
360
+ const fallbackAnswers = asArray(answer.answer);
361
+ const meta = asRecord(content.meta);
362
+ const rightItems = asArray(content.rightItems).map((raw, index) => {
363
+ const item = asRecord(raw);
364
+ return {
365
+ id: asString(item.id, `r${index + 1}`),
366
+ text: asString(item.text),
367
+ };
368
+ });
369
+ const items = asArray(content.items).map((rawItem, index) => {
370
+ const item = asRecord(rawItem);
371
+ const itemContent = asRecord(item.content);
372
+ const itemAnswer = asRecord(item.correctAnswer);
373
+ const fallback = fallbackAnswers[index];
374
+ const isExample = item.isExample === true;
375
+ return {
376
+ imageUrl: asString(itemContent.imageUrl),
377
+ correctAnswer: asString(itemAnswer.answer, typeof fallback === 'string' ? fallback : ''),
378
+ isExample,
379
+ points: isExample ? 0 : asNumber(item.points, 1),
380
+ questionNumber: asNumber(item.questionNumber, index + 1),
381
+ };
382
+ });
383
+ return {
384
+ content: '',
385
+ answer: {
386
+ title: asString(meta.title),
387
+ instruction: asString(meta.instruction, 'Look and match.'),
388
+ rightItems,
389
+ items,
390
+ explanation: asString(apiQuestion.explanation),
391
+ points: items.reduce((total, item) => (item.isExample ? total : total + item.points), 0),
392
+ },
393
+ };
394
+ };
259
395
  const reverseCrossOutWordGroup = (apiQuestion) => {
260
396
  const content = asRecord(apiQuestion.content);
261
397
  const answer = readAnswer(apiQuestion);
@@ -407,6 +543,9 @@ const handlers = {
407
543
  CHOOSE_THE_CORRECT_ANSWER_GROUP: reverseChooseTheCorrectAnswerGroup,
408
544
  ANSWER_THE_QUESTION_GROUP: reverseAnswerTheQuestionGroup,
409
545
  TRUE_FALSE_GROUP: reverseTrueFalseGroup,
546
+ TRUE_FALSE_CORRECT_GROUP: reverseTrueFalseCorrectGroup,
547
+ FILL_IN_BLANK_GROUP: reverseFillInBlankGroup,
548
+ MATCHING_WITH_LINES_GROUP: reverseMatchingWithLinesGroup,
410
549
  CROSS_OUT_WORD_GROUP: reverseCrossOutWordGroup,
411
550
  WORD_ORDER_AND_MATCH_GROUP: reverseWordOrderAndMatchGroup,
412
551
  WORD_ORDER_AND_MATCH: reverseWordOrderAndMatchGroup,
@@ -424,6 +563,7 @@ const handlers = {
424
563
  WORD_ORDERING: reverseSimpleAnswer,
425
564
  WORD_FILL_PARAGRAPH: reverseSimpleAnswer,
426
565
  MATCH_BY_WRITING_ANSWER: reverseGroupPayload,
566
+ MATCH_WORD_TO_PICTURE: reverseGroupPayload,
427
567
  WRITE_SHORT_PARAGRAPH: reverseWriteShortParagraph,
428
568
  WRITE_A_SHORT_LETTER: reverseWriteAShortLetter,
429
569
  WRITE_CORRECT_VERB_FORM: reverseSimpleAnswer,
@@ -454,6 +594,7 @@ function reverseWriteAShortLetter(apiQuestion) {
454
594
  const answer = readAnswer(apiQuestion);
455
595
  const informations = asArray(content.informations ?? answer.informations).map((item) => asString(item)).filter(Boolean);
456
596
  const guideImages = asArray(content.guideImages ?? answer.guideImages).map((item) => asString(item)).filter(Boolean);
597
+ const passage = asString(content.passage ?? answer.passage);
457
598
  const contexts = asArray(content.contexts ?? answer.contexts);
458
599
  const instructions = isRecord(content.instructions)
459
600
  ? content.instructions
@@ -471,6 +612,7 @@ function reverseWriteAShortLetter(apiQuestion) {
471
612
  instruction: asString(instructions.mainInstruction || instructions.instruction),
472
613
  informations,
473
614
  guideImages,
615
+ ...(passage ? { passage } : {}),
474
616
  contexts,
475
617
  minWords,
476
618
  maxWords,
@@ -13,6 +13,8 @@ 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';
17
+ import { transformFillInBlankGroup } from '../../../components/questions/types/fill-in-blank-group/transform';
16
18
  import { transformFillInBlank } from '../../../components/questions/types/fill-in-blank/transform';
17
19
  import { transformWriteCorrectVerbForm } from '../../../components/questions/types/write-correct-verb-form/transform';
18
20
  import { transformChooseCorrectAdjective } from '../../../components/questions/types/choose-correct-adjective/transform';
@@ -22,6 +24,8 @@ import { transformTrueFalse } from '../../../components/questions/types/true-fal
22
24
  import { transformWordOrdering } from '../../../components/questions/types/word-ordering/transform';
23
25
  import { transformWordFillParagraph } from '../../../components/questions/types/word-fill-paragraph/transform';
24
26
  import { transformMatchByWritingAnswer } from '../../../components/questions/types/match-by-writing-answer/transform';
27
+ import { transformMatchWordToPicture } from '../../../components/questions/types/match-word-to-picture/transform';
28
+ import { transformMatchingWithLinesGroup } from '../../../components/questions/types/matching-with-lines-group/transform';
25
29
  import { transformWriteSentences } from '../../../components/questions/types/write-sentences/transform';
26
30
  import { transformListenAndChooseFromAnswerGroup } from '../../../components/questions/types/listen-and-choose-from-answer-group/transform';
27
31
  import { transformListenAndChooseObjectsInScene } from '../../../components/questions/types/listen-and-choose-objects-in-scene/transform';
@@ -62,6 +66,8 @@ const questionTransformHandlers = {
62
66
  CHOOSE_THE_CORRECT_ANSWER: transformChooseTheCorrectAnswer,
63
67
  CHOOSE_THE_CORRECT_ANSWER_GROUP: transformChooseTheCorrectAnswerGroup,
64
68
  CROSS_OUT_WORD_GROUP: transformCrossOutWordGroup,
69
+ TRUE_FALSE_CORRECT_GROUP: transformTrueFalseCorrectGroup,
70
+ FILL_IN_BLANK_GROUP: transformFillInBlankGroup,
65
71
  FILL_IN_BLANK: transformFillInBlank,
66
72
  WRITE_CORRECT_VERB_FORM: transformWriteCorrectVerbForm,
67
73
  CHOOSE_CORRECT_ADJECTIVE: transformChooseCorrectAdjective,
@@ -71,6 +77,8 @@ const questionTransformHandlers = {
71
77
  WORD_ORDERING: transformWordOrdering,
72
78
  WORD_FILL_PARAGRAPH: transformWordFillParagraph,
73
79
  MATCH_BY_WRITING_ANSWER: transformMatchByWritingAnswer,
80
+ MATCH_WORD_TO_PICTURE: transformMatchWordToPicture,
81
+ MATCHING_WITH_LINES_GROUP: transformMatchingWithLinesGroup,
74
82
  WRITE_SENTENCES: transformWriteSentences,
75
83
  LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP: transformListenAndChooseFromAnswerGroup,
76
84
  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' | 'FILL_IN_BLANK_GROUP' | 'MATCHING_WITH_LINES_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';
@@ -0,0 +1,45 @@
1
+ export declare const FILL_IN_BLANK_ITEM_TYPE: "FILL_IN_BLANK";
2
+ export interface FillInBlankGroupItemAnswer {
3
+ answers: string[][];
4
+ caseSensitive?: boolean;
5
+ }
6
+ export interface FillInBlankGroupItemContent {
7
+ question: string;
8
+ imageUrl?: string;
9
+ audioUrl?: string;
10
+ showHints?: boolean;
11
+ hints?: string[];
12
+ }
13
+ export interface FillInBlankGroupItem {
14
+ questionType?: typeof FILL_IN_BLANK_ITEM_TYPE;
15
+ isExample?: boolean;
16
+ content: FillInBlankGroupItemContent;
17
+ correctAnswer?: FillInBlankGroupItemAnswer;
18
+ points?: number;
19
+ questionNumber?: number;
20
+ }
21
+ export interface FillInBlankGroupContent {
22
+ meta?: {
23
+ instruction?: string;
24
+ audioUrl?: string;
25
+ imageUrl?: string;
26
+ showHints?: boolean;
27
+ hints?: string[];
28
+ };
29
+ items: FillInBlankGroupItem[];
30
+ }
31
+ export interface FillInBlankGroupCorrectAnswer {
32
+ answer: FillInBlankGroupItemAnswer[];
33
+ }
34
+ export interface FillInBlankGroupAPIPayload {
35
+ id?: string;
36
+ questionType: 'FILL_IN_BLANK_GROUP';
37
+ content: FillInBlankGroupContent;
38
+ correctAnswer: FillInBlankGroupCorrectAnswer;
39
+ points: number;
40
+ explanation?: string;
41
+ questionNumber: number;
42
+ partId?: string;
43
+ partNo?: number;
44
+ }
45
+ export type FillInBlankGroupLocalStorage = FillInBlankGroupAPIPayload;
@@ -0,0 +1 @@
1
+ export const FILL_IN_BLANK_ITEM_TYPE = 'FILL_IN_BLANK';
@@ -32,3 +32,7 @@ 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 './matching-with-lines-group';
37
+ export * from './true-false-correct-group';
38
+ export * from './fill-in-blank-group';
@@ -50,3 +50,11 @@ 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
+ // MATCHING_WITH_LINES_GROUP
56
+ export * from './matching-with-lines-group';
57
+ // TRUE_FALSE_CORRECT_GROUP
58
+ export * from './true-false-correct-group';
59
+ // FILL_IN_BLANK_GROUP
60
+ export * from './fill-in-blank-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 interface MatchingWithLinesRightItem {
2
+ id: string;
3
+ text: string;
4
+ }
5
+ export interface MatchingWithLinesGroupItemContent {
6
+ imageUrl: string;
7
+ }
8
+ export interface MatchingWithLinesGroupItem {
9
+ isExample?: boolean;
10
+ content: MatchingWithLinesGroupItemContent;
11
+ correctAnswer?: {
12
+ answer: string;
13
+ };
14
+ points?: number;
15
+ questionNumber?: number;
16
+ }
17
+ export interface MatchingWithLinesGroupContent {
18
+ meta?: {
19
+ instruction?: string;
20
+ title?: string;
21
+ };
22
+ rightItems: MatchingWithLinesRightItem[];
23
+ items: MatchingWithLinesGroupItem[];
24
+ }
25
+ export interface MatchingWithLinesGroupCorrectAnswer {
26
+ answer: string[];
27
+ }
28
+ export interface MatchingWithLinesGroupAPIPayload {
29
+ id?: string;
30
+ questionType: 'MATCHING_WITH_LINES_GROUP';
31
+ content: MatchingWithLinesGroupContent;
32
+ correctAnswer: MatchingWithLinesGroupCorrectAnswer;
33
+ points: number;
34
+ explanation?: string;
35
+ questionNumber: number;
36
+ partId?: string;
37
+ partNo?: number;
38
+ }
39
+ export type MatchingWithLinesGroupLocalStorage = MatchingWithLinesGroupAPIPayload;
@@ -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';